Files
chitai/frontend/AGENTS.md
T

5.9 KiB

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 · epubjs · 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.

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):

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.tsgenerated 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:

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/ (epub reader + chapter sidebar).
  • cn() from $lib/utils merges Tailwind classes; the WithElementRef / WithoutChild helpers there are the shadcn prop-typing conventions.

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.

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.
  • 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.tsApp.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.