docs/designs/notebook-linked-writing-workspace.md
Generated by /office-hours on 2026-08-28 Branch: fix/5917-notebook-editor Repo: readest/readest Status: APPROVED Mode: Builder Issue: https://github.com/readest/readest/issues/5917
The right-side Notebook currently lists only excerpt records, while highlights and attached notes live in the left Annotations panel. A reader who has annotations but no excerpts sees “No Notes,” which looks like missing data even though the annotations exist.
The fix is not to duplicate the annotation list. Notebook should become one freeform document per book, while Annotations remains the source-bound collection and navigation surface.
Annotations preserve what the book says. Notebook is where the reader develops what they think. The two spaces can remain clear and still work together: an annotation can be inserted into the document as a Markdown quote with its source link, and the Notebook header can open the full Annotations panel in one action.
This matches current reading-tool patterns: source-bound notes are organized in a side panel, writing can stay beside the book, and captured passages can flow into synthesis without losing context. References: McGraw Hill Reader, BOOX split-screen reading, Shadow Reader source backlinks, and Readwise writing integrations.
eink-bordered where a boundary must survive e-ink.The approved interaction structure is embedded here so it remains available to every implementer:
Desktop: [ Annotations source list ] [ Book page ] [ Notebook editor ]
Mobile: [ Notebook editor ] -- Annotations action --> [ Annotations panel ]
annotation records and preserves existing excerpt records behind an All / With notes / Clippings filter. Bookmarks remain in their existing tab.Use one deterministic layout predicate for this action: forced-mobile layout, viewport width below 640 px, or viewport height below 640 px swaps panels; every other layout keeps both panel states unchanged. In both layouts, focus the Annotations view heading after the tab opens. Do not condition focus on whether search is visible.
Annotation filters behave as follows. Counts use all active, non-deleted records before the current search query; search matches both quoted text and attached note text. Rows keep the existing book/TOC order.
| Filter | Membership | Default |
|---|---|---|
| All | annotation and excerpt records | Yes |
| With notes | Any included record whose note.trim() is non-empty | No |
| Clippings | excerpt records, including excerpts with attached notes | No |
Extend BookNoteType with notebook. Store one record per book:
{
id: 'notebook',
type: 'notebook',
cfi: creationTimeBookCfi,
note: markdown,
createdAt,
updatedAt
}
note contains the Markdown document. text, style, color, and page are not used.useNotesSync and file-sync mergeNotes provide per-record last-write-wins behavior using updatedAt and deletedAt. The existing Notes sync preference gates Notebook sync.updatedAt and participates in the approved last-write-wins policy. Conflict UI and collaborative text merging are out of scope.book_notes.type and book_notes.note are text columns, but that is only accepted after contract tests cover every boundary: local BookConfig JSON, cloud request transform, API upsert, cloud response transform, file-sync encode/parse/merge, and full BookConfig/backup restoration of an unknown note type. Annotation import remains a known-type exchange and must ignore notebook.| State | Stored record | Editor behavior |
|---|---|---|
| Untouched | No Notebook record | Show the blank editor; do not create a record merely by opening it. |
| First edit | Create the singleton record on the first content change | Save the typed Markdown and capture the compatibility CFI. |
| Cleared | Keep the live singleton with note: '' and a newer updatedAt | Reopening shows a blank editor; the empty value syncs over older non-empty content. |
| Tombstoned remotely | Treat as no live record when the editor is clean | Show blank; do not recreate until the user edits. |
| Edited after tombstone | Reuse the stable ID, retain createdAt, clear deletedAt, and set a newer updatedAt | The first new edit recreates the document. |
There is no Delete Notebook action in this issue. Clearing text is an ordinary saved edit, not a tombstone.
The creation-time CFI is required so a previous Readest release treats the unknown notebook row as an anchored, opaque note instead of dropping it at the central CFI guard.
Resolve the transport anchor in this order: retain the existing Notebook CFI; otherwise use the current non-empty config.location after the active view accepts it as a navigation target. This applies to reflowable and fixed-layout books. If neither source is available or valid, keep the recovery draft but do not create or sync a Notebook record. Show Waiting for a valid book position and retry when location changes. A close may proceed only when recovery storage succeeded; saving must never emit a CFI-less or invalid record.
| Previous-client path | Required behavior | Proof |
|---|---|---|
| Cloud pull and push | Preserve the unknown type string and non-empty CFI; known-type UI and exporters ignore it | Transform plus old-filter compatibility test |
| File-sync pull, merge, and rewrite | Preserve the row by stable ID and valid CFI when rewriting the full note set | Legacy-projection file-sync test |
| Local config save/reload | Preserve the row through generic JSON serialization and the CFI guard | BookConfig round-trip test |
| Clear annotations and third-party sync | Ignore notebook because those flows select known types explicitly | Exclusion tests for each boundary |
Compatibility is a hard feature-release gate, not a degraded mode. If any previous-release cloud transform, UI filter, clear-annotations flow, file-sync rewrite, or local-config projection drops or mutates the row, do not ship the Notebook editor until a compatible versioned encoding exists.
TextEncoder; do not truncate.compositionend; if the composed result is too large, restore the last accepted value and announce the limit after composition so the input method is not interrupted.NotebookEditor component rather than growing Notebook.tsx further.TextEditor for a plain Markdown textarea. Make it fill the Notebook body, enable text selection and spellcheck, and use dir="auto" for document content.updatedAt and content hash, and a monotonic local pending revision; it is cleared only after the same revision is durably written through saveConfig.saveConfig; if that also fails, cancel the transition and offer Retry, Copy draft, or the explicitly destructive Discard draft action.bookKey, selects the Annotations tab, clears unrelated sidebar navigation state, and focuses the Annotations view heading without changing reading position.(content, snippet, selectionStart, selectionEnd). Replace the selected range; a collapsed selection inserts at the caret. Trim only the snippet’s outer blank lines, preserve the user’s Markdown and line endings elsewhere, and add exactly one blank line at each non-empty boundary. Derive inserted separators from the document’s existing style (\r\n when present, otherwise \n) and cover CRLF explicitly. An empty document becomes the snippet with no leading or trailing blank line. Repeated inserts are allowed because they are explicit user actions."alpha" plus "> quote" at caret 5 becomes "alpha\n\n> quote". Replacing "selected" in "before selected after" becomes "before \n\n> quote\n\n after". The helper returns the inserted range so focus and selection can be restored.excerpt record, then inserts that clipping into the Notebook document using the same Markdown insertion path.dir="auto"; application chrome keeps its established direction behavior.eink-bordered; e-ink removes animations and keeps a crisp 1 px boundary.The existing Readest web, desktop, and mobile release pipelines cover this change. No new package, service, migration rollout, or user opt-in is required.
NotebookEditor, autosave/flush behavior, save status, and the new header action.pnpm lint, formatting checks, and manual desktop/mobile/RTL/e-ink verification.Build the smallest end-to-end vertical slice first: create, edit, autosave, reopen, and sync one Notebook document, then open Annotations from its header. Only after that passes should insertion and legacy clipping discovery be layered on.