Solarized-themed desktop PDF reader for math textbooks. Tauri 2 + React 19 + TypeScript.
src/pages/OverviewPage.tsx — library grid (starred section + per-directory sections)
src/pages/ReaderPage.tsx — PDF viewer + notes split-pane + tabs + snip mode
src/pages/LoopPage.tsx — snip review carousel (sorted/shuffled modes)
src/components/PdfThumbnail.tsx — lazy thumbnail via pdfium:// protocol
src/components/PdfViewer.tsx — virtual-scroll PDF renderer (buffer=5 pages, imperative zoom)
src/components/NotesPanel.tsx — CodeMirror 6 with vim, markdown, KaTeX math
src/components/TabBar.tsx — horizontal tab strip with context menu (close, close others)
src/components/HighlightsPanel.tsx — highlights list pane (grouped by page)
src/components/BookmarksPanel.tsx — bookmarks list pane (highlights with color="bookmark")
src/components/CommandPalette.tsx — Ctrl+P command palette (panel toggles, theme, zen mode, snip, loop)
src/components/ReaderToolbar.tsx — reader toolbar (back, page counter, zoom, search, palette button)
src/components/SnipOverlay.tsx — drag-to-select crosshair overlay for snip region capture
src/components/SnipBanner.tsx — inline label input banner shown after snip region selection
src/components/LoopCarousel.tsx — flashcard carousel with reveal, prev/next, XP tracking
src/lib/thumbnail-queue.ts — concurrency limiter (MAX_CONCURRENT=3)
src/lib/palette.ts — module-level toggle callback for command palette
src/lib/readerState.ts — module-level store bridging ReaderPage state to Layout/palette
src/hooks/useTheme.ts — theme store with setTheme() export for direct setting
src/hooks/useSnips.ts — snip CRUD + XP tracking (IPC → .axiomatic/ JSON)
src/components/SlugMigrationDialog.tsx — orphan slug detection + migration dialog
src-tauri/src/commands.rs — general Tauri IPC commands (db, files, tags, project state)
src-tauri/src/highlight_commands.rs — highlight CRUD IPC commands
src-tauri/src/snip_commands.rs — snip CRUD IPC commands
src-tauri/src/pdf_commands.rs — PDF-specific IPC commands (open, outline, links, text, search, clip)
src-tauri/src/pdf_engine.rs — PDFium render thread (mpsc recv loop, LRU cache)
src-tauri/src/pdf_protocol.rs — pdfium:// custom protocol handler
src-tauri/src/db.rs — SQLite schema + versioned migration framework (directories, notes, note_images, tags, highlights)
React (Vite + SWC) <──IPC──> Tauri/Rust <───> SQLite
<──pdfium://> PDFium <───> File system (PDF dirs)
PDF rendering uses PDFium (C library) via pdfium-render crate. Pages served as JPEG via pdfium:// custom protocol. Render thread handles page rendering, text extraction, outlines, links, search, and clipping. Document open runs on spawn_blocking (off the render thread) for instant response.
Routes: / OverviewPage, /read/:slug ReaderPage, /loop/:slug LoopPage. Layout wraps all with custom Titlebar.
| Data | Where | Pattern |
|---|---|---|
| Progress (page/total) | .axiomatic/progress.json |
IPC get_all_progress/save_progress, 300ms debounced writes |
| Starred books | .axiomatic/starred.json |
IPC get_starred/toggle_starred, optimistic toggle |
| Snips | .axiomatic/snips.json |
IPC list_snips/create_snip/delete_snip, UUID IDs |
| Snip XP | .axiomatic/xp.json |
IPC get_xp/increment_xp, per-slug counter |
| Theme | localStorage | custom store with OS detection (dbus + matchMedia) |
| Notes | SQLite | in-memory Map cache, 150ms debounced writes |
| Highlights | SQLite | useHighlights hook, bookmarks stored as color="bookmark" |
| Tabs | localStorage | useTabs hook with reopen stack + useTabNavigation for route-aware nav |
createLocalStorageStore (lib/createStore.ts) is a generic factory for remaining localStorage state (theme, tabs): load() returns parsed snapshot, emitChange() re-reads from localStorage and notifies subscribers.
Three-tier storage: SQLite (structured relational: notes, highlights, tags), .axiomatic/ ProjectStateDir (portable per-book JSON: progress, starred, snips, XP), localStorage (ephemeral UI: theme, tabs). Each library directory has its own .axiomatic/ dir — copying the library folder preserves all per-book state.
Thumbnail pipeline — see docs/pdf-pipeline.md. Thumbnails are <img> tags pointing at pdfium:// URLs. thumbnail-queue.ts limits concurrent loads to 3; no IndexedDB cache needed.
Zoom — two-tier imperative system. Immediate: CSS transform: scale() via useImperativeHandle (no React re-render). Committed: 300ms debounced startTransition re-render for layout recalculation. See docs/pdf-pipeline.md for details.
Document open — open_document runs on spawn_blocking, not the render thread. Generation counter (AtomicU64) preempts stale renders. Module-level caches in useDocument and useTextbooks avoid re-fetching on navigation.
OverviewPage re-renders — BookTile is memo'd with a custom comparator that checks progress.currentPage and progress.totalPages by value (not reference), because the localStorage store creates fresh objects on every read. handleTotalPages uses a ref to avoid depending on the progress object.
useBatchedRender — progressively mounts BookTiles in batches of 20 via requestAnimationFrame, preventing initial load from blocking the main thread.
Command palette (Ctrl+P or toolbar button) — floating overlay with fuzzy filter. Available on both overview and reader pages. Commands:
- Always: theme switching (OS / light / dark)
- Reader only: toggle outline, notes, bookmarks, highlights, zen mode, snip mode
- Reader only (when snips exist): loop sorted, loop shuffled
Panel toggle commands dispatch CustomEvent on window (e.g. axiomatic:toggle-outline), listened to by ReaderPage. The palette button uses a module-level callback (src/lib/palette.ts) to avoid circular imports between router.tsx and page/component modules.
Zen mode — hides toolbar, tabs, outline, bookmarks, and highlights panels. Notes remain openable (Ctrl+L or command palette). ESC exits zen mode.
Toolbar layout (reader): Left (back, page counter, zoom) | Center (title) | Right (search, palette button).
Keyboard safety — useVimReader and ReaderPage keyboard handlers skip all non-modifier keys when document.activeElement is an <input> or <textarea>, preventing vim navigation from interfering with the command palette, search bar, or any future text fields.
Snip mode — activated via command palette. Renders a SnipOverlay (crosshair cursor) on each visible PDF page. Drag to select a normalized rect (0–1 coordinates). On mouseup, SnipBanner appears for labeling. Saved snips persist to .axiomatic/snips.json via snip_commands.rs (UUID IDs, ISO-8601 timestamps).
Reader state bridge — readerState.ts is a module-level store (not React state) that exposes snipMode and hasSnips to the Layout/command palette via useSyncExternalStore. ReaderPage writes; router.tsx reads. This avoids prop drilling across the route boundary.
Loop page (/loop/:slug) — carousel of snips for a given book. Modes: sorted (creation order) or shuffled (Fisher-Yates, computed once on mount). Navigation: j/k or arrow keys, Space to reveal, ESC to exit. XP counter persisted per-slug in .axiomatic/xp.json via IPC. LoopCarousel crops the snip region from a full-page pdfium:// render using canvas drawImage.
Route-aware tabs — OpenTab now carries a route field. useTabNavigation wraps useTabs with navigate() calls that respect the stored route, enabling non-reader tabs (e.g. loop tabs with slug loop:{slug}).
- Solarized palette: hard-coded hex values (
#fdf6e3light bg,#002b36dark bg, etc.) - Tailwind 4 with
dark:variants; dark mode toggled via<html class="dark"> - Vim keybindings everywhere: h/j/k/l in overview grid, j/k scroll in reader, full vim in notes editor
- No component library — all UI is hand-written
- PDFs rendered via
pdfium://custom protocol (native PDFium, JPEG output) - Test-before-modify rule: Before changing any logic, verify it has test coverage. If not, extract into a testable unit and write tests FIRST. This applies to all logic — component wiring, command lists, state assembly — not just leaf components. Tests must assert desired behavior, not mirror current implementation.
- Pre-release gate: Always run
./scripts/prebuild.sh <version>before pushing a release tag. Never tag manually. The script runs the full lint → typecheck → test pipeline and only tags if everything passes.
npm run dev # tauri dev (vite + rust)
npm run build # tauri build
npm run vite:dev # vite only (no tauri)
npx tsc -b # type-check (same as build uses)
npm run lint # eslint
npm run test # vitest unit tests
cargo test --lib # rust unit tests (from src-tauri/)./scripts/prebuild.sh <version> # bump, lint, fix, test, commit, tag
git push origin master --tags # push commit + tag → triggers CI release- PDFium shared library (
libpdfium.so/.dylib/.dll) must be insrc-tauri/resources/for dev or bundled as a Tauri resource for production. Download from https://github.com/bblanchon/pdfium-binaries. PdfiumisBox::leak'd to&'static— shared viaPdfiumHandlewrapper (unsafe Send+Sync) andusizepointer casts for closures. Seelib.rssetup.- Bookmarks are highlights with
color = "bookmark"—useHighlightssplits them viacolorHighlights/bookmarkHighlights. - Slug migration — When a PDF is renamed,
detect_orphaned_slugsfinds data referencing unknown slugs and suggests mappings via bigram similarity.migrate_slugatomically updates all storage tiers (SQLite + JSON + localStorage tabs). TheSlugMigrationDialogsurfaces after library scan on OverviewPage. - Versioned migrations —
db.rsuses amigrationstable with sequential version numbers. Add new migrations to theMIGRATIONSarray;init_db()runs pending ones on startup.
Cache iteration: 35
You are the DDD orchestrator. Dispatch phase agents, manage transitions, own global state. Commands: /ddd-begin, /ddd-explore, /ddd-spec, /ddd-plan, /ddd-exec, /ddd-gate, /ddd-approve, /ddd-clarify, /ddd-rewind, /ddd-status, /ddd-cache, /ddd-refactor, /ddd-onboard, /ddd-amend, /ddd-migrate.
Phase: exec | Iteration: 35
None
concept/fragment-001.md— PDF rendering pipelineconcept/fragment-002.md— Document managementconcept/fragment-003.md— Annotation systemconcept/fragment-004.md— Study featuresconcept/fragment-005.md— Navigation & UIconcept/fragment-006.md— Persistenceconcept/fragment-007.md— Platform concernsconcept/fragment-008.md— First-class snippet support (seeded from cl-005)concept/fragment-009.md— AI-enabled highlights (seeded from cl-006)concept/fragment-010.md— Retroactive TDD adoption (seeded from cl-007)concept/fragment-011.md— Pomodoro/timer feature (seeded from cl-008)concept/fragment-012.md— Cycle 3: Pomodoro UX, Stats Page, Toolbar Buttons, Sidebar Navigationconcept/explore-summary.md— Explore phase synthesis (cycle 1 + 2)concept/atoms.yaml— 61 atoms (49 cycle 1 + 9 cycle 2 + 2 cycle 3 + 1 ProjectStateDir)
| Sub-spec | Title | Approval |
|---|---|---|
| spec-rendering | PDF Rendering Pipeline | approved (iter 27) |
| spec-text-extraction | Text Extraction & Spatial Model | approved (iter 27) |
| spec-documents | Document Management & Library | approved (iter 28) |
| spec-annotations | Annotation System | approved (iter 27) |
| spec-study | Study Features (Snips, Tags, Filtering & Loop Review) | approved (iter 28) |
| spec-navigation | Navigation & Interaction | approved (iter 27) |
| spec-persistence | Persistence & Storage | approved (iter 27) |
| spec-platform | Platform & Runtime | approved (iter 27) |
| spec-testing | Full-Stack Test Infrastructure | approved (iter 25) |
| spec-pomodoro | Pomodoro Study Timer & Statistics | approved (iter 28) |
| Task | Title | Status |
|---|---|---|
| task-001 | Versioned migration framework | done |
| task-002 | ProjectStateDir creation & walkdir filtering | done |
| task-003 | Move BookProgress to .axiomatic/ | done |
| task-004 | Move StarredSet to .axiomatic/ | done |
| task-005 | Move Snips to .axiomatic/ | done |
| task-006 | Move SnipXP to .axiomatic/ | done |
| task-007 | Slug migration | done |
| task-008 | Snip tags backend and model update | done |
| task-009 | Snip table view page with search and navigation | done |
| task-010 | Snip filtering and cross-book loop overlay | done |
| task-011 | Pomodoro timer with presets and break notifications | done |
| task-012 | Pomodoro session logging and XP backend | done |
| task-013 | Study statistics on OverviewPage | done |
| task-014 | Rust unit tests for all mutating IPC commands | done (44 tests) |
| task-015 | Vitest infrastructure and frontend tests | done (52 tests) |
| task-016 | Playwright E2E infrastructure and happy-path tests | done (22 tests) |
| task-017 | Fix Pomodoro Timer UX | done |
| task-018 | Create /stats route and StatsPage | done |
| task-019 | Surface palette commands as toolbar buttons | done |
| task-020 | Persistent sidebar navigation | done |
| ID | Topic | Status |
|---|---|---|
| cl-006 | AI-enabled highlights | open (deferred) |
| cl-009 | Vim navigation consistency (overview, snip overlay, loop overlay) | open (deferred) |
| cl-010 | Zoom consistency (overview thumbnails, snip overlay, loop carousel) | done (iter 35) |
| cl-011 | Toggleable select mode (text selection vs vim nav) | done (iter 35) |
| cl-012 | Zoomable snips (pinch/scroll zoom on snip regions) | done (iter 35) |
| cl-013 | Pomodoro/learning tools missing from SnipsPage loop overlay (overlay bypasses LoopPage infrastructure) | done (iter 35) |
All 10 sub-specs approved (119 ACs, 61 atoms). Plans: cycle 2 approved (iter 30, 9 tasks), cycle 3 approved (iter 34, 4 tasks). Exec phase active.
- [iter 32] Exec batch: all 9 cycle 2 tasks completed (008–016). 44 Rust tests, 52 Vitest tests, 22 Playwright E2E tests all passing. Cycle 2 complete.
- [iter 33] Rewind: exec → explore (cycle 3). fragment-012, 2 new atoms (Sidebar, StatsPage), spec amendments (ac-150..157)
- [iter 34] Fast-forward: explore → exec (cycle 3). 4 tasks (task-017..020), TDD required
- [iter 35] Exec batch: all 4 cycle 3 tasks completed (017–020). Red-green TDD. 75 Vitest tests, 44 Rust tests all passing. Cycle 3 complete.