A book whose files were all already stored settles as skipped rather than done, naming the book that holds them and offering to add it anyway.
176 lines
10 KiB
Markdown
176 lines
10 KiB
Markdown
# Chitai frontend
|
|
|
|
SvelteKit web app for the eBook library. See the repo-root `AGENTS.md` for the overall picture and
|
|
dev-environment setup.
|
|
|
|
**Stack:** SvelteKit 2 with `adapter-node` · Svelte 5 (runes) · Tailwind v4 · Zod v4 ·
|
|
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, 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
|
|
|
|
There are two mechanisms; pick deliberately.
|
|
|
|
**1. Remote functions — the default.** `src/lib/api/*.remote.ts` export `query` / `command` / `form`
|
|
functions from `$app/server`. Each takes a Zod schema from `$lib/schema` as its validator and runs
|
|
on the server, reaching the API through `locals.api` (the `ApiClient` in `src/lib/server/api.ts`):
|
|
|
|
```ts
|
|
export const getBook = query(stringCoerce, async (id): Promise<Book> => {
|
|
const { locals } = getRequestEvent();
|
|
const response = await locals.api.get(`/books/${id}`);
|
|
if (!response.ok) error(response.status === 404 ? 404 : 500, '…');
|
|
return await response.json();
|
|
});
|
|
```
|
|
|
|
Conventions: build query strings with `createQueryParams` from `$lib/utils`; on a failed response
|
|
throw SvelteKit's `error(status, message)`; multipart uploads go through `postMultipart` /
|
|
`putMultipart`. Re-export new modules from `src/lib/api/index.ts`.
|
|
|
|
**2. The catch-all proxy** at `src/routes/api/[...path]/+server.ts` forwards GET/POST/PATCH/DELETE
|
|
to the backend with the auth header attached. Use it only where the **browser itself** must fetch
|
|
the backend — e.g. streaming a book file into the EPUB/PDF reader. It is not the general-purpose
|
|
path.
|
|
|
|
## Auth
|
|
|
|
`src/hooks.server.ts` is a `sequence` of two handles: the first reads the `authToken` cookie,
|
|
constructs an `ApiClient`, validates it with `GET /access/me` and fills `locals.user` /
|
|
`locals.authToken` / `locals.api` (clearing the cookie if invalid); the second redirects any route
|
|
outside `/login` to the login page when there is no user.
|
|
|
|
The cookie is set in `src/lib/api/auth.remote.ts` (`login`) — httpOnly, secure, sameSite strict, one
|
|
week — and deleted by `logout`. The backend JWT never reaches client-side JS.
|
|
|
|
## Schemas
|
|
|
|
- `src/lib/schema/*.ts` — hand-written Zod schemas, used as remote-function input validators and as
|
|
the source of the exported TS types. Mirror `backend/src/chitai/schemas/` when the API changes.
|
|
- `src/lib/schema/common.ts` — shared building blocks: `stringCoerce` / `arrayCoerce` coercion
|
|
helpers, `PaginatedResponse<T>`, and the pagination / search / order query schemas that most list
|
|
endpoints compose from.
|
|
- `src/lib/schema/openapi/schema.d.ts` — **generated** from the backend's OpenAPI document with
|
|
`openapi-typescript`. Never hand-edit; regenerate after backend API changes.
|
|
|
|
## State
|
|
|
|
Client state lives in classes in `src/lib/state/*.svelte.ts` using `$state` / `$derived`, shared via
|
|
Svelte context with a module-level `Symbol` key and a `setXState` / `getXState` pair:
|
|
|
|
```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);
|
|
}
|
|
```
|
|
|
|
Follow that pattern rather than introducing stores. `library.svelte.ts` is the reference — including
|
|
its optimistic-delete-with-rollback and toast handling. `bookCollection` / `bookSelection` /
|
|
`bookOperations` split list data, selection and mutations across three cooperating classes.
|
|
|
|
## Components
|
|
|
|
- `src/lib/components/ui/` — vendored shadcn-svelte (`components.json`) plus jsrepo blocks from
|
|
`@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/` (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:
|
|
|
|
- `(root)` — the authenticated shell (sidebar, header); `+layout.server.ts` loads the libraries.
|
|
- `(root)/(library)` — library-scoped pages: `library/[libraryId]/view`, `book/[bookId]`, edit, and
|
|
the readers at `book/[bookId]/read/{epub,pdf}/[fileId]`.
|
|
- `+layout@.svelte` breakouts reset to the root layout for the login page and the full-screen reader.
|
|
|
|
## 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 —
|
|
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:
|
|
|
|
- `pnpm check` is not clean. **Baseline as of 2026-08-13: 30 errors, 1 warning, 8 files**, most of
|
|
them in `src/routes/api/[...path]/+server.ts` (see below). Get your own baseline before assuming
|
|
an error is yours. `src/lib/schema/openapi/schema.d.ts` was regenerated on that date and is
|
|
current; regenerate it again after any backend API change, with
|
|
`pnpm exec openapi-typescript http://localhost:8000/schema/openapi.json -o src/lib/schema/openapi/schema.d.ts`
|
|
against a backend running **your** branch — a stale server silently writes a stale file.
|
|
- `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. 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`.
|
|
- No CSP, which foliate's README asks for because EPUBs can carry scripts. See `TODO.md` for why it
|
|
is not enabled yet.
|