Back to Readest

Design: Notebook as a Linked Writing Workspace

docs/designs/notebook-linked-writing-workspace.md

0.12.621.8 KB
Original Source

Design: Notebook as a Linked Writing Workspace

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

Problem Statement

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.

What Makes This Cool

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.

Constraints

  • Preserve every existing annotation, attached note, bookmark, and excerpt. Do not run a destructive migration.
  • Reuse Readest’s existing note-sync category, API/table, timestamps, tombstones, and file-sync note merge. Do not add a sync service or database migration.
  • Keep the existing Notebook AI tab working without adding Notebook content to AI context in this issue.
  • Work on desktop, web, phone, tablet, RTL books, fixed-layout books, and e-ink mode.
  • Keep new UI quiet and Adwaita-aligned. Use existing surface tokens, logical spacing, visible focus, safe-area handling, and eink-bordered where a boundary must survive e-ink.
  • Use the project’s key-as-content i18n conventions for every new visible string.

Premises

  1. Each book has exactly one autosaved Markdown Notebook document.
  2. Opening Notebook always shows an editor, including when the document is blank.
  3. The left Annotations panel is authoritative for highlights, attached notes, excerpts, search, navigation, and annotation export.
  4. Notebook and Annotations are separate in purpose but connected by an Annotations header action and an Insert into Notebook action.
  5. Notebook reuses notes sync as a reserved singleton record. Whole-document conflicts use the existing last-write-wins note behavior.

Approaches Considered

  • Editor plus legacy Clippings: rejected because it would leave two unrelated content models inside Notebook.
  • Linked writing workspace: selected because it gives each panel one clear job and connects them without duplicating lists.
  • Embedded Sources rail: rejected because it would duplicate the left panel and become cramped at existing Notebook widths.

The approved interaction structure is embedded here so it remains available to every implementer:

text
Desktop: [ Annotations source list ] [ Book page ] [ Notebook editor ]
Mobile:  [ Notebook editor ] -- Annotations action --> [ Annotations panel ]

Information architecture

  • Left Annotations panel: source material. The existing Annotations view includes annotation records and preserves existing excerpt records behind an All / With notes / Clippings filter. Bookmarks remain in their existing tab.
  • Center reader: unchanged reading surface.
  • Right Notebook panel: authored material. The Notes tab becomes a full-height Markdown editor backed by the singleton Notebook record. The AI tab remains unchanged.
  • On desktop, opening Annotations from Notebook keeps a pinned Notebook visible so source and writing can coexist. If Notebook is unpinned, opening the left panel does not dismiss it unless existing width/layout rules require that.
  • On mobile, Annotations swaps from Notebook to the left Annotations panel. Back returns to reading; the existing Notebook control reopens the editor.

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.

FilterMembershipDefault
Allannotation and excerpt recordsYes
With notesAny included record whose note.trim() is non-emptyNo
Clippingsexcerpt records, including excerpts with attached notesNo

Notebook record and sync

Extend BookNoteType with notebook. Store one record per book:

ts
{
  id: 'notebook',
  type: 'notebook',
  cfi: creationTimeBookCfi,
  note: markdown,
  createdAt,
  updatedAt
}
  • The stable ID is safe because cloud note identity is scoped by user and book hash; file sync is already scoped by book.
  • note contains the Markdown document. text, style, color, and page are not used.
  • Capture a valid CFI from the open book when the record is first created. It is a compatibility anchor for existing validation and mixed-version file-sync clients, not a user-visible Notebook location.
  • The reserved type must never appear in annotation lists, reader overlays, annotation navigation, Readwise, Hardcover, BookOrbit, annotation import/export, or clear-annotations behavior unless a flow explicitly targets Notebook.
  • Existing useNotesSync and file-sync mergeNotes provide per-record last-write-wins behavior using updatedAt and deletedAt. The existing Notes sync preference gates Notebook sync.
  • Keep the editor draft separate from the last durable Notebook record. A remote update replaces the editor only when it is clean. If a remote update arrives while dirty or while a save is in flight, keep the local draft; its next successful write receives a new updatedAt and participates in the approved last-write-wins policy. Conflict UI and collaborative text merging are out of scope.
  • Continue using the existing client clock and tie behavior. Add tests for remote arrival during the debounce, remote arrival during a save, simultaneous device edits, equal timestamps, and forward/backward clock skew so the chosen data-loss tradeoff is explicit.
  • No database migration is expected because cloud 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.

Notebook record lifecycle

StateStored recordEditor behavior
UntouchedNo Notebook recordShow the blank editor; do not create a record merely by opening it.
First editCreate the singleton record on the first content changeSave the typed Markdown and capture the compatibility CFI.
ClearedKeep the live singleton with note: '' and a newer updatedAtReopening shows a blank editor; the empty value syncs over older non-empty content.
Tombstoned remotelyTreat as no live record when the editor is cleanShow blank; do not recreate until the user edits.
Edited after tombstoneReuse the stable ID, retain createdAt, clear deletedAt, and set a newer updatedAtThe 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.

Previous-release compatibility

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 pathRequired behaviorProof
Cloud pull and pushPreserve the unknown type string and non-empty CFI; known-type UI and exporters ignore itTransform plus old-filter compatibility test
File-sync pull, merge, and rewritePreserve the row by stable ID and valid CFI when rewriting the full note setLegacy-projection file-sync test
Local config save/reloadPreserve the row through generic JSON serialization and the CFI guardBookConfig round-trip test
Clear annotations and third-party syncIgnore notebook because those flows select known types explicitlyExclusion 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.

Supported size and performance

  • Support Notebook Markdown up to 256 KiB in UTF-8 per book, roughly enough for tens of thousands of words. Measure bytes with TextEncoder; do not truncate.
  • Route typing, paste, IME completion, Insert, Copy to Notebook, recovery, and remote application through one byte-limit validator. Reject any mutation that would cross the limit, retain the last accepted content, and show Notebook is too large to save. Copy or remove some text to continue.
  • Do not validate intermediate IME composition events. Validate on 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.
  • If an existing durable or recovery document already exceeds the limit, load it intact and allow only edits that reduce its byte size until it returns below the limit. Never truncate existing content.
  • Contract tests send a near-limit document through local BookConfig serialization, cloud request/response transforms, API upsert fixtures, file-sync encode/parse/merge, backup restore, and recovery storage.
  • Benchmark on an Android API 26 emulator using the minimum-supported System WebView profile. Across 50 input operations, require p95 main-thread handling below 16 ms for a 10 KiB document and below 50 ms at 256 KiB, with no operation above 100 ms. Cover textarea change handling, recovery write, BookConfig serialization, and provider payload construction. If synchronous recovery cannot meet this budget, replace the recovery backend before shipping rather than weakening durability.

Editor behavior

  • Extract a focused NotebookEditor component rather than growing Notebook.tsx further.
  • Reuse TextEditor for a plain Markdown textarea. Make it fill the Notebook body, enable text selection and spellcheck, and use dir="auto" for document content.
  • The blank state is the editor with placeholder text: Start writing about this book… Never render “No Notes.”
  • On every accepted change, update the in-memory draft and synchronously update a recovery entry in local storage. Namespace the key by authenticated user ID, or by installation-scoped anonymous profile ID, plus book hash. The entry contains content, the base durable record’s updatedAt and content hash, and a monotonic local pending revision; it is cleared only after the same revision is durably written through saveConfig.
  • Do not use wall-clock freshness to restore recovery. If the durable record still matches the entry’s base revision/hash, restore the unacknowledged draft and announce Recovered unsaved changes once. If both durable content and the recovery draft diverged from the base, keep the durable document in the editor and offer Recover draft or Use latest saved; never auto-overwrite either copy.
  • If recovery storage throws because of quota, disabled storage, or WebView failure, keep editing in memory but show persistent Notebook recovery unavailable status. Explicit close or book switch must then wait for a successful durable saveConfig; if that also fails, cancel the transition and offer Retry, Copy draft, or the explicitly destructive Discard draft action.
  • Persist the Notebook record after a 750 ms idle debounce. Explicit close, book switch, and Cmd/Ctrl+S await the same flush promise. Blur starts the flush but cannot delay focus movement. If a durable flush fails after recovery succeeded, close/switch may proceed and the draft is restored next time with a warning. Unmount and app/browser lifecycle hooks request a flush only as secondary protection; correctness comes from the recovery entry because asynchronous teardown is not reliable across web, Tauri, and mobile WebViews.
  • On logout, remove recovery entries only for the departing account after its close/flush policy completes. On permanent book removal, remove that profile-and-book entry. Anonymous recovery data is isolated by the installation profile ID.
  • Concurrent tabs/windows follow the same approved whole-record last-write-wins behavior as concurrent devices. Exclusive locks, read-only takeover UI, and same-device text merging are out of scope; the recovery divergence choice protects an unacknowledged local draft when it is reopened.
  • If a durable write fails, retain the recovery entry, show Couldn’t save, and retry on the next edit or Cmd/Ctrl+S.
  • Saving…, Saved, and Couldn’t save refer only to local BookConfig durability. Cloud/file sync status remains owned by the existing sync UI.
  • Search is removed from Notebook because it only searched excerpts. Annotation search stays in the left panel. Notebook search is unavailable in v1; platform find behavior is not part of the contract.
  • Markdown preview, formatting toolbar, multiple Notebook documents, history, and collaborative merging are out of scope.

Notebook header

  • Keep the existing pin and close behavior.
  • Replace the excerpt-search action with a flat Annotations action using the existing annotation icon and at least a 40 px hit target; use 44 px on mobile.
  • Activating it opens the left sidebar for the same bookKey, selects the Annotations tab, clears unrelated sidebar navigation state, and focuses the Annotations view heading without changing reading position.
  • Use logical start/end classes and keep the title centered when the trailing action is present.

Attached annotation notes

  • Annotate from the selection toolbar remains a source-bound note action. It creates/reuses the highlight exactly as today, opens the left Annotations panel, focuses that annotation, and starts inline note editing there.
  • Move new-note draft/cancel ownership from the Notebook surface to the Annotations flow. Preserve current placeholder cleanup for cancel, Escape, navigation, panel close, cross-page selections, and book switching.
  • Editing an attached note from an annotation row stays inline in the left panel. It no longer opens the Notebook document.

Insert into Notebook

  • Add a flat Insert action to annotation and clipping rows. The action uses the existing Markdown-copy helper and the user’s annotation-link preference to produce a block quote, attached note when present, and source link.
  • Use one pure insertion helper with (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.
  • Example: "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.
  • If the Notebook editor is mounted, use its current textarea selection. Otherwise append at document end, open Notebook, and select the inserted range.
  • On layouts outside the mobile predicate, keep Annotations open after insertion. On forced-mobile or sub-640 px width/height layouts, switch to Notebook after insertion.
  • The v1 editor is raw Markdown, so the deep link is preserved for preview/export and direct text use; rendered inline link interaction is deferred with Markdown preview.
  • Run the insertion result through the shared size validator before changing the document. If it fails, leave both the document and source annotation unchanged and show the size-limit message.

Copy to Notebook and legacy excerpts

  • When Copy to Notebook is enabled, copying selected book text continues to create or reuse the existing anchored excerpt record, then inserts that clipping into the Notebook document using the same Markdown insertion path.
  • Validate the prospective document before creating a new excerpt. If insertion would exceed the limit, the clipboard copy still succeeds, but no new excerpt or Notebook insertion is saved; show Copied, but the Notebook is too large to add this clipping.
  • Existing excerpts remain untouched and become visible under the Clippings filter in Annotations. Do not auto-append them to the document, because that would mutate user-authored content without consent.
  • Annotation export continues to export annotations/excerpts as it does today. Notebook export is not added in this issue; users can select/copy the Markdown document.

Responsive, accessibility, and e-ink behavior

  • Preserve the current resizable desktop panel and mobile swipe-to-dismiss sheet, including top and bottom safe-area calculations.
  • The textarea has a visible focus treatment, programmatic label, selectable text, and keyboard-safe tab order. Save status uses a polite live region without announcing every keystroke.
  • All new edge utilities use logical properties for RTL. The Markdown editor uses dir="auto"; application chrome keeps its established direction behavior.
  • Do not use shadow or color as the only hierarchy. The editor boundary uses existing semantic surface tokens and eink-bordered; e-ink removes animations and keeps a crisp 1 px boundary.

Success Criteria

  • A book with annotations but no excerpts opens a blank, editable Notebook instead of “No Notes.”
  • Typing, clearing, closing, reopening, and switching books preserve the correct per-book document.
  • The singleton record round-trips through cloud note sync and provider file sync, gated by the Notes sync preference, with no database migration.
  • Mixed-version/file-sync coverage proves the Notebook record is not discarded by the existing CFI validation path.
  • The Notebook record never appears as an annotation, overlay, clipping, bookmark, or third-party-export item.
  • The Notebook Annotations action opens the correct left tab for the same book on desktop and mobile.
  • Selection Annotate creates/edits the attached note in the left panel and retains all current cancel-cleanup behavior.
  • Insert and Copy to Notebook add correctly formatted Markdown at the caret or document end without deleting the source record.
  • Existing excerpts are visible as Clippings and remain byte-for-byte unchanged until the user edits or deletes them.
  • AI tab, pinning, resizing, swipe dismissal, RTL, keyboard focus, safe areas, and e-ink presentation regressions are covered proportionally.

Distribution Plan

The existing Readest web, desktop, and mobile release pipelines cover this change. No new package, service, migration rollout, or user opt-in is required.

Next Steps

  1. Add failing tests for the singleton record, note-sync/file-sync round trips, type exclusion, blank editor, panel navigation, insertion formatting, and legacy excerpts.
  2. Implement the Notebook record helpers and sync exclusions without changing database schema.
  3. Build NotebookEditor, autosave/flush behavior, save status, and the new header action.
  4. Move attached-note creation/editing to the left panel and add Clippings plus Insert actions.
  5. Run focused tests, the full unit suite, pnpm lint, formatting checks, and manual desktop/mobile/RTL/e-ink verification.

The Assignment

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.

What I noticed about how you think

  • You did not settle for making the empty list less confusing; you said “make it an editor,” which changed Notebook from a passive collection into a useful writing surface.
  • You kept the existing information architecture in view when you asked for “a link to the Annotation panel in the left sidebar” instead of requesting a second annotation list.
  • You challenged the only new-infrastructure premise with “can we reuse the notes sync?” That constraint removed a database migration and kept the implementation aligned with how users already control Notes sync.