docs: document the foliate-js reader

Adds a reader section covering the vendored tree, the $foliate alias and the
traps around it, and refreshes the stale stack line, oklch claim and rough
edges. Records why CSP is not enabled in TODO.md.
This commit is contained in:
2026-08-12 01:37:19 -04:00
parent 961a63480e
commit 51c31e6bf6
3 changed files with 106 additions and 23 deletions
+74 -16
View File
@@ -4,13 +4,16 @@ SvelteKit web app for the eBook library. See the repo-root `AGENTS.md` for the o
dev-environment setup.
**Stack:** SvelteKit 2 with `adapter-node` · Svelte 5 (runes) · Tailwind v4 · Zod v4 ·
`epubjs` · `mode-watcher` (dark mode) · `svelte-sonner` (toasts) · pnpm.
vendored `foliate-js` (EPUB) · vendored `pdf.js` (PDF) · `mode-watcher` (dark mode) ·
`svelte-sonner` (toasts) · pnpm.
Two experimental flags are on in `svelte.config.js` and the codebase depends on both:
`kit.experimental.remoteFunctions` and `compilerOptions.experimental.async` (`await` in components).
Tailwind v4 has **no config file** — the theme, oklch colour tokens and `@custom-variant dark` all
live in `src/app.css`.
Tailwind v4 has **no config file** — the theme, colour tokens (hex, not oklch) and
`@custom-variant dark` all live in `src/app.css`. `dark` is the _only_ custom variant defined, so
generated components that assume others — shadcn's slider ships `data-horizontal:` / `data-vertical:`
classes — silently produce no styles. Use the `data-[orientation=…]` form instead.
## Talking to the backend
@@ -65,8 +68,12 @@ Svelte context with a module-level `Symbol` key and a `setXState` / `getXState`
```ts
const LIBRARY_KEY = Symbol('LIBRARY');
export function setLibraryState(libraries: Library[]) { return setContext(LIBRARY_KEY, new LibraryState(libraries)); }
export function getLibraryState() { return getContext<ReturnType<typeof setLibraryState>>(LIBRARY_KEY); }
export function setLibraryState(libraries: Library[]) {
return setContext(LIBRARY_KEY, new LibraryState(libraries));
}
export function getLibraryState() {
return getContext<ReturnType<typeof setLibraryState>>(LIBRARY_KEY);
}
```
Follow that pattern rather than introducing stores. `library.svelte.ts` is the reference — including
@@ -79,10 +86,56 @@ its optimistic-delete-with-rollback and toast handling. `bookCollection` / `book
`@ieedan/shadcn-svelte-extras` (`jsrepo.json`). Treat as generated: add components with the CLIs
rather than hand-writing them, and prefer wrapping over editing.
- App components live in `forms/`, `layout/`, `view/` (browser, grid/list/table, filters, sort) and
`reader/` (epub reader + chapter sidebar).
`reader/` (see [The readers](#the-readers)).
- `cn()` from `$lib/utils` merges Tailwind classes; the `WithElementRef` / `WithoutChild` helpers
there are the shadcn prop-typing conventions.
## The readers
**PDF** is the pdf.js viewer vendored under `static/pdfjs/`, pointed at by an iframe. Untouched by
the EPUB work; leave it alone unless the task is about PDFs.
**EPUB** is built on `foliate-js`, copied verbatim into `src/lib/vendor/foliate-js/` by
`scripts/vendor-foliate.sh` (pinned commit; see `src/lib/vendor/foliate-js/README.chitai.md`).
Upstream has no npm release and recommends a submodule; this repo has none and already vendors
pdf.js the same way, so it is copied instead. Only the import closure reachable from `view.js` is
vendored, and **`pdf.js` in that directory is our stub, not upstream's** — the real one imports a
bare `@pdfjs/pdf.min.mjs` that Rollup resolves at build time even though the path never runs.
Layout:
| Path | What |
| ------------------------------------------ | ---------------------------------------------------------------------------- |
| `lib/vendor/foliate-js/` | The engine. Do not edit — `vendor-foliate.sh` overwrites it. |
| `lib/reader/foliate.ts` | Lazy loader for the custom elements. The only thing that imports `$foliate`. |
| `lib/reader/settings.ts` · `stylesheet.ts` | Defaults/bounds, and the CSS injected into the book. |
| `lib/reader/progress.ts` | Debounced progress writer with a `sendBeacon` flush. |
| `lib/state/reader-settings.svelte.ts` | Settings state, persisted to `localStorage`. |
| `components/reader/foliate-view.svelte` | Wraps `<foliate-view>`; owns the imperative lifecycle. |
| `components/reader/epub-reader.svelte` | The shell: chrome, TOC, errors, progress. |
Things that will bite:
- **`$foliate` is a Vite-only alias.** It is deliberately absent from `kit.alias` and tsconfig
`paths` so TypeScript cannot resolve it and falls back to the ambient declaration in
`lib/reader/foliate-js.d.ts`; `src/lib/vendor` is also in tsconfig `exclude`. Without both,
`checkJs` walks ~11k lines of untyped JS. The declaration file must **not** be named `foliate.d.ts`
— beside `foliate.ts`, TypeScript takes it for that file's emitted declaration and drops it.
- **Never import the vendored code at module scope.** `view.js` calls `customElements.define` and
subclasses `HTMLElement` on import, so it must stay behind `loadFoliate()` inside `onMount`. SSR is
otherwise on for the reader route.
- **Sections render in iframes, which swallow key events.** Keyboard handlers are bound per section
document on the `load` event, and modifier combinations are replayed onto the host window so app
shortcuts (the sidebar's ctrl+B) still work while reading.
- **Renderer settings split two ways.** Flow, gap, margins, column count and line width are
_attributes_ set with `setAttribute` (there is no JS property API, no `margin` shorthand and no
`spread` — a spread is `max-column-count: 2`). Typography is CSS passed to `renderer.setStyles`,
which takes a `[before, after]` pair: the first is prepended to the section head so the book
overrides it, the second appended so it wins. User settings belong in the second, with
`!important`, or the book's own CSS beats them.
- **Progress needs no locations pre-pass.** `relocate` carries both a CFI and an overall `fraction`,
which map straight onto `epub_cfi` and `percentage`.
## Routing
Route groups carry the layout structure:
@@ -95,21 +148,26 @@ Route groups carry the layout structure:
## Conventions
Prettier (`.prettierrc`): tabs, single quotes, no trailing commas, 100 columns, with the Svelte and
Tailwind plugins. Run `pnpm check` (svelte-check) and `pnpm lint` before considering work done.
Tailwind plugins. Run `pnpm check` (svelte-check) and `pnpm lint` before considering work done
but take a baseline first, because neither is clean (see below).
`src/lib/vendor/` is excluded from Prettier, ESLint and svelte-check. Don't reformat vendored code.
## Known rough edges
Observed in the current tree — don't mistake these for intentional patterns to copy:
- `src/lib/schema/openapi/schema.d.ts` is **stale** — it predates the `BookProgress` rework and has
no `BookProgressRead`, so `book.progress` types as `{}`. This is the source of most of the ~104
errors `pnpm check` reports on a clean tree; regenerating it should clear them. Get a baseline
before assuming an error is yours.
no `BookProgressRead`, so `book.progress` types as `{}`. It is the source of most of the errors
`pnpm check` reports; regenerating it should clear them. **Baseline as of 2026-08-11: 37 errors,
1 warning, 11 files.** Get your own baseline before assuming an error is yours.
- `pnpm lint` does not pass either — 131 pre-existing ESLint errors, 35 of them
`svelte/no-navigation-without-resolve` on plain `href`s, plus ~59 files Prettier would rewrite
(mostly vendored shadcn components). Check the files you touched, not the whole tree.
- `src/routes/api/[...path]/+server.ts` — all four handlers are annotated `RequestHandler` while the
import of that type is commented out at line 4.
- `src/app.d.ts``App.Locals["user"]` is typed from `lucide-svelte`'s `User` *icon* component
import of that type is commented out at line 4. It also buffers whole responses with
`arrayBuffer()` and forwards no `Range` header, so book downloads are not streamed.
- `src/app.d.ts``App.Locals["user"]` is typed from `lucide-svelte`'s `User` _icon_ component
rather than the `User` interface in `$lib/server/auth`.
- Uncommitted work in progress (as of 2026-08-10): library icons, spanning
`components/ui/icon-picker/`, the newly vendored `components/ui/popover/`,
`forms/library-create-form.svelte`, `layout/library-switcher.svelte` and `schema/library.ts`.
Prefer not to refactor those files mid-flight.
- No CSP, which foliate's README asks for because EPUBs can carry scripts. See `TODO.md` for why it
is not enabled yet.