Skip to content

feat(ui): accessible text-size (shortcuts + header) + locally-generated capture path (0.3.15-rc.7) - #130

Merged
unforced merged 1 commit into
mainfrom
ag-unforced-dev
May 13, 2026
Merged

feat(ui): accessible text-size (shortcuts + header) + locally-generated capture path (0.3.15-rc.7)#130
unforced merged 1 commit into
mainfrom
ag-unforced-dev

Conversation

@unforced

Copy link
Copy Markdown
Contributor

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:

  • 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 held or no modifier.
  • Header chrome — new 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.
  • Same-tab syncCustomEvent("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 in lib/text-size.ts, mirroring nextTheme'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 to memoPath() in lib/capture/recorder.ts. Same <root>/YYYY/MM-DD/HH-MM-SS shape under Notes/ so quickly-captured text and voice memos read as parallel concepts.
  • Capture pre-fillpathOverride state is seeded with quickPath() on mount. Operator sees the generated path the moment they expand More fields. They can accept, edit, or clear.
  • Escape valve preserved — clearing the input falls back to the rc.6 behavior (text → vault-picks, audio-only → memoPath()). Pinned by a new test.

Behavior changes (worth flagging for review)

  • Text-only captures now land at Notes/<date>/<time> by default (was: vault-assigned).
  • Audio captures with no override now also land under Notes/ (was: Memos/). Clearing the input restores the Memos/ rule.
  • Combined text+voice captures now land at 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 to memoPath() 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 + TextSizeShortcutsMount collocate 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.
  • Same-tab CustomEvent pattern is new for this codebase — adds explicit comment about why storage alone isn't enough. Mirror of cross-tab sync but for same-tab; not a clash with existing patterns.
  • quickPath() mirrors memoPath() 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 notes clean. Bundle ships TextSizeControl, quickPath, the new keymap handler. Browser test plan below.

Test plan

  • Header: "Aa" button visible next to Theme toggle. Click → popover with Default / Larger / Largest. Click Larger → text scales up live, popover closes, ✓ moves to Larger.
  • Cmd+= / Cmd+- cycle the size while typing in /capture. Cmd+0 resets.
  • Shift+Cmd+= does NOT cycle (avoids stomping + in markdown tables).
  • Mobile menu (narrow viewport, hamburger): "Aa" button present in the menu's chrome row. Tap → same popover.
  • Settings → Text size dropdown still works, and changing it there updates the header popover's ✓ (cross-surface sync).
  • /capture → expand More fields → Path input is pre-filled with Notes/<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 under Memos/<date> (rc.6 escape valve).
  • /new → still renders NoteNew (heavy form with attachments) — untouched.

🤖 Generated with Claude Code

…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>
@unforced
unforced merged commit 053fc38 into main May 13, 2026
@unforced
unforced deleted the ag-unforced-dev branch May 13, 2026 02:22
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>
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.

Make text-size control accessible from the editing surface (not buried in Settings)

1 participant