Skip to content

Worktree rich text notes - #1

Merged
zacktiger merged 6 commits into
mainfrom
worktree-rich-text-notes
Aug 4, 2026
Merged

zacktiger merged 6 commits into
mainfrom
worktree-rich-text-notes

Conversation

@zacktiger

@zacktiger zacktiger commented Aug 4, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Rich text formatting: add bullet points and adjust text alignment in notes
    • Insert images directly into notes
    • Inline formatting toolbar for quick access to editing options

zacktiger and others added 6 commits August 4, 2026 13:28
A single multiline TextInput has nowhere to put a bullet, an alignment or
an image, so a note becomes a column of blocks -- one TextInput each,
edited in place. Enter splits at the caret, Backspace at the start merges
upward, and the first paragraph renders at 28px: the first line has
always been the title, this makes it look like what it already was.

Storage keeps both representations. `doc` is the new nullable column
holding the block document; `body` stays the plain-text projection of it,
so search ranking, #hashtag parsing, deriveTitle, the list preview and
the Phase 4 sync cursor go on reading exactly what they read before. A
null `doc` is a plain-text note -- every note written until now -- which
parses back as paragraphs, so there is nothing to migrate.

The detail screen moves from save-on-blur to a 600ms autosave: with a
block per line, blur fires every time the caret crosses a boundary.

Two things only the web build gets wrong, both fixed here: it renders
`<textarea rows={2}>`, which made every block two lines tall, and it ran
the default Backspace after our merge, eating the character at the seam.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The marker is structure, not text: a bullet is a block type, drawn in a
fixed gutter beside its own input. It cannot be typed over or half
deleted, and a line that wraps hangs under the first word rather than
under the dot.

The list behaves the way the key that made it suggests. Enter continues
it, Enter on an empty bullet ends it, and Backspace at the start
un-bullets the line before it starts joining it to the one above -- so
there is always a way out that is not the toolbar.

The toolbar itself appears only while a block has the caret, and hands
the caret straight back after a tap, so the keyboard never drops. It
reports what the current block is rather than listing everything
possible; alignment and images join it in the next two commits.

`toggleBullet` is a pure function in core, so what the toolbar does to a
document is tested without a simulator. Reopening a saved note now proves
the `doc` column round-trips: the bullets come back as bullets, not as
literal "- " at the front of a paragraph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Alignment is per block, which is the unit a reader thinks in -- one
centred heading over left-aligned paragraphs, rather than a mode the
whole note is in. A new line inherits the alignment of the one it came
from, so a centred run stays centred without touching the toolbar again.

It stays presentation: `documentToText` ignores it, so `body` -- and with
it search, the title and the list preview -- reads exactly the same
whichever edge the words sit against.

The icons are drawn from four bars rather than set in a font: the app
ships no icon set, and three glyphs that only differ by which end the
short bars sit at is precisely the shape being described.

Alignment goes through the style, not the same-named TextInput prop,
which react-native-web silently drops.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The picked file is copied into the app's own storage rather than
referenced where it sits: the picker hands back a URI in a cache or a
system photo container, which the app may not be able to read later.

What the note stores is a relative path, `attachments/<uuid>.jpg`, never
an absolute file:// URI -- iOS moves the app container between installs,
so an absolute URI saved today is a broken image after the next update.
The name is a fresh UUID because two photos off a camera roll are
routinely both IMG_0001.jpg. Intrinsic width and height travel with the
block, so the space is held open before the file loads and the note does
not jump as its photos appear.

Removing an image takes it out of the document and leaves the file. A
note's delete is soft everywhere else in this app, and an orphaned file
is cheaper than a photo the user cannot get back.

The web build has no expo-file-system, so it does not offer the button at
all rather than offering it and failing -- and a note written on a phone
still opens there, with its photos as placeholders.

Verified: the pure document operations by unit test, and the web
degradation in the browser. The device path -- pick, copy, render,
long-press to remove -- is not exercised here; there is no simulator in
this environment, so it needs a run on a phone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
product.md and plan.md both said "attachments, images, link unfurling --
text and URLs only through Rollout 1", which the last four commits made
false. Since product.md is the authority when the two plans disagree, it
is the file that most needs to be true.

Images are split from the thing they were bundled with. Local images are
in; link unfurling stays out, and now says why -- it is a network call
the composer should never make. Other attachments stay out: a file, a
recording or a video is not a note.

plan.md also gains `doc` in the DDL and the two lines of model rationale
worth knowing before touching it: that `body` is a projection rewritten
on every edit, and that a null `doc` is a plain-text note, which is why
this shipped without a backfill.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`editContent` and the changed `editBody` shipped without tests, and the
case that matters most had none at all: a note already on someone's
device, whose `doc` is null. It has to open, read back identically, and
gain a document on its first edit without its text moving.

Also pins the invariant the two columns rest on -- that `body` is exactly
`documentToText(doc)` -- and that `editBody` clears `doc`, so a stale
document can never outlive the text it described.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@zacktiger
zacktiger merged commit 98bebde into main Aug 4, 2026
1 of 2 checks passed
@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7c0bc979-9c0d-408f-ba84-eacaf43feb08

📥 Commits

Reviewing files that changed from the base of the PR and between 75ba1d5 and 84c1e6e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (20)
  • apps/mobile/app.json
  • apps/mobile/package.json
  • apps/mobile/src/app/index.tsx
  • apps/mobile/src/app/note/[id].tsx
  • apps/mobile/src/components/block-editor.tsx
  • apps/mobile/src/components/block-toolbar.tsx
  • apps/mobile/src/db/migrations.ts
  • apps/mobile/src/db/notes-repo.ts
  • apps/mobile/src/lib/attachments.ts
  • apps/mobile/src/store/notes-store.tsx
  • docs/plan.md
  • docs/product.md
  • packages/core/src/document.test.ts
  • packages/core/src/document.ts
  • packages/core/src/index.ts
  • packages/core/src/note.test.ts
  • packages/core/src/note.ts
  • packages/core/src/types.ts
  • packages/db/src/schema.ts
  • supabase/migrations/20260804000000_note_doc.sql

📝 Walkthrough

Walkthrough

The PR adds a block-document model with paragraphs, bullets, alignment, and images. Notes store serialized documents with plain-text projections. The mobile app adds block editing, autosave, formatting controls, local image attachments, database migrations, and rollout documentation.

Changes

Rich Text Notes

Layer / File(s) Summary
Block document model
packages/core/src/document.ts, packages/core/src/note.ts, packages/core/src/types.ts, packages/core/src/index.ts, packages/core/src/*test.ts
The core package adds block types, tolerant parsing, serialization, plain-text projection, editing helpers, and synchronized note updates. Tests cover parsing, mutations, persistence behavior, and legacy notes.
Note document persistence
packages/db/src/schema.ts, supabase/migrations/..., apps/mobile/src/db/*, apps/mobile/src/store/notes-store.tsx
The notes schema adds nullable doc storage. Repository and store methods persist blocks, serialized documents, and plain-text projections together.
Mobile block editor and attachments
apps/mobile/src/components/*, apps/mobile/src/lib/attachments.ts, apps/mobile/src/app/index.tsx, apps/mobile/src/app/note/[id].tsx, apps/mobile/app.json, apps/mobile/package.json
The mobile composer and note editor use block state, autosave, structural editing, formatting controls, and local image attachments. Expo plugins and dependencies support image rendering, picking, and file storage.
Rich text rollout specification
docs/plan.md, docs/product.md
The rollout documentation defines doc storage, the body projection, local image references, and deferred attachment capabilities.

Estimated code review effort: 4 (Complex) | ~60 minutes

Poem

A rabbit hops through blocks so bright,
Bullets bloom and photos take flight.
The notes save text and documents too,
With carrots aligned left, center, right—
Rich little pages, fresh and new!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-rich-text-notes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@zacktiger
zacktiger deleted the worktree-rich-text-notes branch August 4, 2026 11:57
@coderabbitai coderabbitai Bot mentioned this pull request Aug 4, 2026
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