diff --git a/.gitattributes b/.gitattributes index 2bce16d..1743d8c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,5 @@ # Mark pdfjs as vendored code -frontend/static/pdfjs/** linguist-vendored \ No newline at end of file +frontend/static/pdfjs/** linguist-vendored + +# Mark foliate-js as vendored code +frontend/src/lib/vendor/** linguist-vendored \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 43764cc..46473a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,13 +9,15 @@ a KOSync-compatible endpoint. ## Layout -| Path | What | -| --- | --- | -| `backend/` | Litestar REST API + PostgreSQL. See `backend/AGENTS.md`. | -| `frontend/` | SvelteKit SSR web app. See `frontend/AGENTS.md`. | -| `docker-compose.yml` | Production stack: `db` (postgres:17), `backend`, `frontend`. | -| `docs/screenshots/` | Images used by `README.md`. | -| `shell.nix` | Root dev shell; composes the two sub-shells. | +| Path | What | +| -------------------------- | ---------------------------------------------------------------------------------------- | +| `backend/` | Litestar REST API + PostgreSQL. See `backend/AGENTS.md`. | +| `frontend/` | SvelteKit SSR web app. See `frontend/AGENTS.md`. | +| `frontend/src/lib/vendor/` | Vendored `foliate-js` (the EPUB engine), copied by `frontend/scripts/vendor-foliate.sh`. | +| `frontend/static/pdfjs/` | Vendored pdf.js viewer, used by the PDF reader in an iframe. | +| `docker-compose.yml` | Production stack: `db` (postgres:17), `backend`, `frontend`. | +| `docs/screenshots/` | Images used by `README.md`. | +| `shell.nix` | Root dev shell; composes the two sub-shells. | ## Development environment diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..5cf2712 --- /dev/null +++ b/TODO.md @@ -0,0 +1,335 @@ +# TODO + +Known issues and deferred work. Agent-facing notes belong in the `AGENTS.md` files; +this is for things that are broken or missing and not yet scheduled. + +## Backend + +### Identifier extraction drops most ISBNs + +`backend/src/chitai/services/metadata_extractor.py` — `EpubExtractor._extract_identifiers` + +The EPUB path validates `DC:identifier` values verbatim: + +```python +for id in epub.get_metadata("DC", "identifier"): + if is_valid_isbn(id[0]): + ... +``` + +`is_valid_isbn` branches on `len(isbn)` being exactly 10 or 13, so anything carrying +formatting fails. Two consequences: + +- **Hyphenated ISBNs are silently dropped.** `978-0-486-28211-4` is 17 characters, so it + never reaches the checksum. Most EPUBs write ISBNs hyphenated, so the majority are lost. + The PDF path already does this correctly — `_extract_isbns` calls + `match.replace("-", "")` before validating. +- **`urn:isbn:` prefixes are dropped** for the same reason. This is a common EPUB form. + +Fix: normalise before validating — strip a leading `urn:isbn:`, then remove everything +that isn't `0-9` or `X`. Reuse the PDF path's approach rather than duplicating it. + +Note this only runs at upload, so fixing it changes nothing for books already imported. +A backfill would need to re-read the files on disk. + +### Non-ISBN identifiers are discarded + +Same function. Anything that isn't a valid ISBN is thrown away, including values EPUBs +routinely carry: `urn:uuid:…`, `calibre:…`, Google Books volume IDs and ASINs. + +The `Identifier` model is already generic (`name` + `value`, unique per book), so storing +them needs no schema change — only the extractor decides what survives. The frontend +already renders ISBN, ASIN and DOI as links and shows unknown types as plain values, so +anything stored will display sensibly. + +Worth adding at the same time: + +- A DOI regex (`10.\d{4,9}/\S+`) alongside the ISBN scan in `PdfExtractor._extract_isbns` + — academic PDFs carry one and it is the most useful identifier they have. +- Deriving ISBN-10 from ISBN-13 when only the latter is present. It is a pure checksum + conversion and doubles the chance of an external lookup matching. + +### Any authenticated user can delete any library or book + +`backend/src/chitai/database/models/library.py`, `backend/src/chitai/database/models/user.py` + +There is no authorization tier. `Library` has no owner column, and `User` carries only +`email` and `password` — no role, no `is_active`. So every authenticated account can create +and delete libraries, and delete books along with their files on disk. Per-user scoping +exists only for reading progress and bookshelves, which `provide_book_service` restricts +correctly. + +For a single-household deployment that may well be acceptable. The point is that it is +emergent rather than chosen. The cheapest meaningful step is an `is_admin` flag gating +library deletion and `delete_books` — a model change plus a migration. + +### Basic auth answers a malformed header with a 500 + +`backend/src/chitai/middleware/basic_auth.py` — line 22 + +```python +username, password = b64decode(auth_header.split("Basic ")[1]).decode().split(":") +``` + +Nothing guards the parse. A `Bearer` token raises `IndexError`, non-base64 raises +`binascii.Error`, and a credential with no colon raises `ValueError` — as does a password +that *contains* one, since there is no `maxsplit=1`. Every case surfaces as a 500 on an +unauthenticated endpoint. All of them should be 401. + +### An unknown KOSync API key returns 404 + +`backend/src/chitai/middleware/kosync_auth.py` — line 32 + +`KosyncDeviceService.get_by_api_key` uses `get_one`, which raises `NotFoundError`, but the +middleware catches only `PermissionDeniedException`. The global handler in +`exceptions/handlers.py` then renders it as a 404, so a device presenting a bad key is told +the route does not exist rather than that it is unauthorized. The same file still carries a +leftover `print(exc)`. + +Worth doing at the same time: `KosyncDeviceService._generate_api_key` uses +`secrets.token_hex(8)`. 64 bits is thin for a long-lived bearer credential where 32 bytes +is the convention. + +### The multi-book download cannot be driven from a test + +`backend/src/chitai/services/book.py` — `BookService.get_files` + +`/books/download` is the only handler returning a Litestar `Stream`, and it cannot be +exercised through `AsyncTestClient`. The request itself succeeds, then fixture teardown +hangs: the test transport never sends the `http.disconnect` that the streaming response +waits on, so the app's lifespan shutdown never completes. Coverage therefore sits at the +service level, on `get_files` directly. + +Unresolved whether the endpoint also stalls behind a real ASGI server, where that +disconnect does arrive. Worth one manual check against `litestar run` before relying on it. + +### The production image runs the development server + +`backend/Dockerfile` — the final `CMD` + +``` +CMD ["litestar", "--app-dir", "chitai", "run", "--host", "0.0.0.0", "--port", "8000"] +``` + +`litestar run` is the CLI development runner. Production should invoke uvicorn or granian +directly, with a worker count. + +### Nothing gates formatting, linting or types + +`ruff format --check src/` reports 27 of 61 files unformatted, and `ruff check src/` finds +114 errors — 100 of them unused imports, the rest bare `except`, unused variables and +`== True` comparisons. `ruff check --fix` clears 51 automatically. + +There is no `[tool.ruff]` section in `pyproject.toml`, so only ruff's default `E4/E7/E9/F` +rules run, and no type checker is configured at all despite `# type: ignore` comments in +the tree. Individually these are trivial; collectively they say nothing runs on commit. + +## Frontend + +### Scripted EPUBs run against the app origin + +**This is a regression from the foliate-js migration, not a pre-existing gap.** + +The old epub.js reader never passed `allowScriptedContent`. epub.js defaults it to +`false`, which sets `iframe.sandbox = "allow-same-origin"` — no `allow-scripts` — so +script inside a book never ran. The vendored foliate-js sets, unconditionally: + +```js +// paginator.js — and the same in fixed-layout.js +// `allow-scripts` is needed for events because of WebKit bug +this.#iframe.setAttribute("sandbox", "allow-same-origin allow-scripts"); +``` + +`allow-same-origin` together with `allow-scripts` is the combination that makes the +sandbox attribute do nothing. Sections are served as same-origin `blob:` URLs, so script +in a book can reach `/api/*` with the session cookie attached. foliate's README says as +much and tells you to use a CSP instead; we have not added one. + +This is not theoretical. Audiobookshelf shipped the same combination and got +**CVE-2024-35236** — scripted EPUB plus an unrestricted upload gave remote code +execution; fixed in 2.10.0 by making scripted content a per-library opt-in, off by +default. Kavita (CVE-2024-39307) and Jellyfin (fixed 10.9.8) are variations on it. +Write-up: . + +**An app-wide CSP is the wrong shape.** `kit.csp` with `script-src: ['self']` also blocks +`mode-watcher`'s inline `setInitialMode`, which sets the dark class before first paint — +SvelteKit only nonces the bootstrap script it injects itself, so every page load would +flash the light theme. Pinning a hash of a third-party inline script breaks silently on +upgrade. + +**Grimmory solves it properly**, and it runs foliate-js too. Rather than handing foliate +the whole file, it serves each EPUB entry from its own endpoint and puts the strict +policy on that response: + +```java +// EpubReaderController.java +response.setHeader("Content-Security-Policy", "script-src 'none'"); +``` + +The app shell keeps its own, more permissive policy. That works because each section is +then a real same-origin document with its own header, rather than a `blob:` — and a +`blob:` inherits the CSP of the document that created it, which is exactly why a header +on `/api/books/download/…` would achieve nothing today. + +Two ways forward: + +1. **Cheap.** Patch the vendored `sandbox` attribute to drop `allow-scripts`, restoring + what epub.js gave us. Cost is the WebKit bug the upstream comment cites: events inside + the iframe get swallowed, which would likely break touch/swipe paging and possibly the + in-iframe keyboard handling in `foliate-view.svelte`. Needs testing before trusting. +2. **Right.** Follow grimmory: serve individual EPUB entries from the backend with + `script-src 'none'` on each response, and drive foliate through its loader hooks + instead of a whole-file blob. This is **net-new capability on both sides**, not a + rewiring of something that exists — see below. + +Option 2 also fixes the memory cost below, which is why it is worth more than it looks. + +#### What option 2 actually involves + +Today the browser fetches the whole `.epub` from `download/{book_id}/{file_id}`, which +returns a Litestar `File` and knows nothing about the archive's contents. foliate then +opens the zip **in the browser** (`makeZipLoader` in `view.js`) and turns every chapter, +image and stylesheet into a `blob:` URL via `Loader.createURL` in `epub.js`. A `blob:` +carries no headers of its own — it inherits the CSP of the document that created it — +which is why there is nowhere to attach a policy except the app shell. + +foliate's parser never touches the zip directly. `EPUB` is constructed with a loader: + +```js +// view.js — makeZipLoader is one implementation; makeDirectoryLoader below is another +return { entries, loadText, loadBlob, getSize }; +``` + +`name` is the **zip entry path**, because `makeZipLoader` keys its map on +`entry.filename` — so `OEBPS/Text/chapter01.xhtml`, `OEBPS/Images/cover.jpg`. The parser +resolves hrefs from the OPF manifest into those names and asks the loader for them, +without caring where the bytes come from. A third implementation that fetches over HTTP +is the same shape. + +**Backend.** An endpoint taking a path inside the archive, e.g. +`GET books/{book_id}/files/{file_id}/entry/{path:path}`, returning a `Stream` over +`zipfile.ZipFile.open(name)` so an entry never lands in memory whole, with the content +type from the manifest and `Content-Security-Policy: script-src 'none'` on the response. + +Two things to get right: + +- **`path` is caller-supplied.** Resolve it against the archive's `namelist()` and reject + anything absent, rather than trusting the string — `../` traversal is the hazard. +- **`getSize` is synchronous** in foliate's loader contract, and it feeds `SectionProgress`, + which produces the reading percentage. So the endpoint needs a companion that returns + entry names and sizes up front — one extra call at open — because sizes cannot be + discovered per request. + +Opening the zip per request costs a central-directory read each time. Probably fine for +chapter-sized reads, worth measuring rather than assuming. + +Note the backend already reads inside EPUBs — `metadata_extractor.py` uses ebooklib at +ingest for title, authors, identifiers and the cover. What is missing is serving an +arbitrary entry by path, not the ability to open the archive. + +**Frontend.** `foliate-view.svelte` stops calling `view.open(file)` and builds an `EPUB` +around a loader backed by that endpoint. The whole-file fetch in `epub-reader.svelte` +goes away with it. + +### The proxy buffers whole files and drops range headers + +Two separate problems that both live in `frontend/src/routes/api/[...path]/+server.ts`. + +**Buffering.** Litestar already streams: `ASGIFileResponse` reads in 1 MB chunks +(`response/file.py`), so the backend never holds a file whole. The proxy then undoes it +with `await response.arrayBuffer()`, which does not resolve until the last byte arrives — +so the whole file sits in the node process, per concurrent reader, and the browser gets +nothing until it completes. Passing `response.body` straight through restores the stream +and is a small change. + +**Range.** The proxy forwards only `Content-Type`, `Content-Disposition` and +`Content-Length`. It never sends the client's `Range` upstream, and would drop +`Accept-Ranges` and `Content-Range` coming back — a 206 without `Content-Range` is +broken. So range support cannot work until the proxy is fixed, whatever the backend does. + +**Litestar has no range support of its own.** In 2.21.1 the only mention of 206 in the +whole package is the `HTTP_206_PARTIAL_CONTENT` constant; there is no `Accept-Ranges` or +`Content-Range` handling anywhere. This has to be written. + +#### What pdf.js actually needs + +It decides from the **initial 200 response**, not from anything on a 206. +`validateRangeRequestCapabilities` in `frontend/static/pdfjs/build/pdf.mjs`: + +```js +if (responseHeaders.get("Accept-Ranges") !== "bytes") { + return returnValues; // allowRangeRequests stays false +} +``` + +It also needs a parseable `Content-Length`, `Content-Encoding: identity`, and a length +greater than twice `rangeChunkSize`. Miss any of those and it downloads the whole file +however good the range support is. + +So the single highest-value header is **`Accept-Ranges: bytes` on the ordinary 200** — +that is what makes pdf.js switch to fetching progressively at all. + +#### Approach + +Put it on the existing `get_file` handler in `controllers/book.py`, which already resolves +`book_id`/`file_id` through the service with library scoping and auth: + +- No `Range` → `Stream` the file with `Accept-Ranges: bytes` and `Content-Length`. +- `Range` present → parse, seek, `Stream` with 206 and `Content-Range`. +- Proxy: forward `Range` up; pass `response.body` through; forward `Accept-Ranges`, + `Content-Range` and the status back. + +There are `RangeRequestMiddleware` snippets circulating for Litestar that wrap +`create_static_files_router`. They are the wrong shape here — book files are served by an +authenticated handler resolving database ids, not by a directory mapping, and using one +would mean exposing disk paths as URLs and re-solving ownership checks that already +exist. The common version also only sets `Accept-Ranges` on the 206, so it would not +switch pdf.js over, and it derives its path with `str.lstrip(prefix)`, which strips a +character set rather than a prefix — `/static/castle.pdf` becomes `le.pdf`. Worth reading +its `parse_range_header` for the parsing rules and writing the rest fresh. + +### Remove the epub.js locations-cache purge + +`frontend/src/lib/reader/legacy-cache.ts` — `purgeLegacyLocationCache` + +The epub.js reader cached generated locations in `localStorage` under +`${bookId}-locations`, a few hundred KB of JSON per long book. foliate computes +progress from section byte sizes at open time, so nothing writes those keys any +more, but existing browsers still hold them — and a reader near the 5–10 MB +origin quota would make the new reader-settings write throw `QuotaExceededError`. + +The reader clears them once per browser, behind a `chitai:locations-purged` flag. +Delete the module, its call in `epub-reader.svelte` and the flag once deployments +have had a release or two to run it — after roughly 2026-12. + +### Cover dimensions are unknown until load + +`book-cover.svelte` renders covers at a fixed height with natural width so nothing is +cropped or distorted. Because the intrinsic size is not known ahead of time, the text +beside the cover settles once when the image loads. `min-width` bounds the movement but +does not remove it. + +Removing it properly means storing cover dimensions at ingest and emitting them as +`width`/`height` attributes so the browser reserves exact space. That is a model change +plus a migration. + +### No series navigation + +`Book` has `series` and `series_position`, and the detail page shows both, but there is no +way to reach the other volumes. The books list endpoint filters by author, publisher, tag, +shelf and progress — a `series` filter would need adding in +`backend/src/chitai/services/filters/book.py` and wiring through +`services/dependencies.py`, following the existing `AuthorFilter` pattern. + +### Book pages are sparse for most books + +EPUB metadata is thin: most imports arrive with a title, an author and nothing else. Two +independent directions, neither started: + +- **Use what exists.** "More by this author" (the list endpoint already accepts + `authors=`), and exposing `Book.created_at` and `BookProgress.updated_at` — both are in + the database, neither is on `BookRead` / `BookProgressRead`. +- **Fetch from outside.** Open Library or Google Books lookup by ISBN for descriptions and + covers. This is what actually fixes the sparseness, but it needs outbound requests, rate + limiting, a manual-vs-automatic decision, and a rule for not clobbering hand-edited + metadata. diff --git a/frontend/.prettierignore b/frontend/.prettierignore index be19ecc..27b587b 100644 --- a/frontend/.prettierignore +++ b/frontend/.prettierignore @@ -11,3 +11,6 @@ coverage # Miscellaneous /static/ + +# Vendored third-party source, copied verbatim by scripts/vendor-foliate.sh +/src/lib/vendor/ diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 9a907e9..caeb146 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -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>(LIBRARY_KEY); } +export function setLibraryState(libraries: Library[]) { + return setContext(LIBRARY_KEY, new LibraryState(libraries)); +} +export function getLibraryState() { + return getContext>(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 ``; 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. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 2c49fa6..30448e4 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -12,6 +12,8 @@ const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); export default defineConfig( includeIgnoreFile(gitignorePath), + // Vendored third-party source. Tracked, so .gitignore does not cover it. + { ignores: ['src/lib/vendor/**'] }, js.configs.recommended, ...ts.configs.recommended, ...svelte.configs.recommended, @@ -27,6 +29,12 @@ export default defineConfig( 'no-undef': 'off' } }, + { + // Generated shadcn components take href as a prop and cannot resolve it — + // that is the caller's job. Editing them here would be lost on regeneration. + files: ['src/lib/components/ui/**'], + rules: { 'svelte/no-navigation-without-resolve': 'off' } + }, { files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], languageOptions: { diff --git a/frontend/package.json b/frontend/package.json index ad28162..3c257f0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,8 +33,8 @@ "jsrepo": "^2.5.2", "openapi-typescript": "^7.13.0", "prettier": "^3.8.1", - "prettier-plugin-svelte": "^3.5.1", - "prettier-plugin-tailwindcss": "^0.6.14", + "prettier-plugin-svelte": "^3.5.2", + "prettier-plugin-tailwindcss": "^0.8.1", "svelte": "^5.53.7", "svelte-check": "^4.4.5", "tailwind-merge": "^3.5.0", @@ -47,7 +47,7 @@ "vite": "^7.3.1" }, "dependencies": { - "epubjs": "^0.3.93", + "construct-style-sheets-polyfill": "^3.1.0", "mode-watcher": "^1.1.0", "svelte-sonner": "^1.0.8", "zod": "^4.3.6" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 153ef8e..401a373 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -8,9 +8,9 @@ importers: .: dependencies: - epubjs: - specifier: ^0.3.93 - version: 0.3.93 + construct-style-sheets-polyfill: + specifier: ^3.1.0 + version: 3.1.0 mode-watcher: specifier: ^1.1.0 version: 1.1.0(svelte@5.53.7) @@ -23,7 +23,7 @@ importers: devDependencies: '@eslint/compat': specifier: ^1.4.1 - version: 1.4.1(eslint@9.39.4(jiti@2.6.1)) + version: 1.4.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2)) '@eslint/js': specifier: ^9.39.4 version: 9.39.4 @@ -59,19 +59,19 @@ importers: version: 2.1.1 eslint: specifier: ^9.39.4 - version: 9.39.4(jiti@2.6.1) + version: 9.39.4(jiti@2.6.1)(supports-color@10.2.2) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.4(jiti@2.6.1)) + version: 10.1.8(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2)) eslint-plugin-svelte: specifier: ^3.15.0 - version: 3.15.0(eslint@9.39.4(jiti@2.6.1))(svelte@5.53.7) + version: 3.15.0(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(svelte@5.53.7) globals: specifier: ^16.5.0 version: 16.5.0 jsrepo: specifier: ^2.5.2 - version: 2.5.2(typescript@5.9.3)(zod@4.3.6) + version: 2.5.2(supports-color@10.2.2)(typescript@5.9.3)(zod@4.3.6) openapi-typescript: specifier: ^7.13.0 version: 7.13.0(typescript@5.9.3) @@ -79,11 +79,11 @@ importers: specifier: ^3.8.1 version: 3.8.1 prettier-plugin-svelte: - specifier: ^3.5.1 - version: 3.5.1(prettier@3.8.1)(svelte@5.53.7) + specifier: ^3.5.2 + version: 3.5.2(prettier@3.8.1)(svelte@5.53.7) prettier-plugin-tailwindcss: - specifier: ^0.6.14 - version: 0.6.14(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.53.7))(prettier@3.8.1) + specifier: ^0.8.1 + version: 0.8.1(prettier-plugin-svelte@3.5.2(prettier@3.8.1)(svelte@5.53.7))(prettier@3.8.1) svelte: specifier: ^5.53.7 version: 5.53.7 @@ -110,7 +110,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.56.1 - version: 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + version: 8.56.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) vite: specifier: ^7.3.1 version: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1) @@ -488,36 +488,42 @@ packages: engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-arm64-musl@0.72.3': resolution: {integrity: sha512-4DswiIK5dI7hFqcMKWtZ7IZnWkRuskh6poI1ad4gkY2p678NOGtl6uOGCCRlDmLOOhp3R27u4VCTzQ6zra977w==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-parser/binding-linux-riscv64-gnu@0.72.3': resolution: {integrity: sha512-R9GEiA4WFPGU/3RxAhEd6SaMdpqongGTvGEyTvYCS/MAQyXKxX/LFvc2xwjdvESpjIemmc/12aTTq6if28vHkQ==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-s390x-gnu@0.72.3': resolution: {integrity: sha512-/sEYJQMVqikZO8gK9VDPT4zXo9du3gvvu8jp6erMmW5ev+14PErWRypJjktp0qoTj+uq4MzXro0tg7U+t5hP1w==} engines: {node: '>=14.0.0'} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-x64-gnu@0.72.3': resolution: {integrity: sha512-hlyljEZ0sMPKJQCd5pxnRh2sAf/w+Ot2iJecgV9Hl3brrYrYCK2kofC0DFaJM3NRmG/8ZB3PlxnSRSKZTocwCw==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-x64-musl@0.72.3': resolution: {integrity: sha512-T17S8ORqAIq+YDFMvLfbNdAiYHYDM1+sLMNhesR5eWBtyTHX510/NbgEvcNemO9N6BNR7m4A9o+q468UG+dmbg==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@oxc-parser/binding-wasm32-wasi@0.72.3': resolution: {integrity: sha512-x0Ojn/jyRUk6MllvVB/puSvI2tczZBIYweKVYHNv1nBatjPRiqo+6/uXiKrZwSfGLkGARrKkTuHSa5RdZBMOdA==} @@ -622,66 +628,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -798,24 +817,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.1': resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.1': resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.1': resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.1': resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} @@ -873,10 +896,6 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/localforage@0.0.34': - resolution: {integrity: sha512-tJxahnjm9dEI1X+hQSC5f2BSd/coZaqbIl1m3TCl0q9SVuC52XcXfV0XmoCU1+PmjyucuVITwoTnN8OlTbEXXA==} - deprecated: This is a stub types definition for localforage (https://github.com/localForage/localForage). localforage provides its own type definitions, so you don't need @types/localforage installed! - '@types/node@22.19.15': resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} @@ -982,11 +1001,6 @@ packages: '@vue/shared@3.5.29': resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} - '@xmldom/xmldom@0.7.13': - resolution: {integrity: sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==} - engines: {node: '>=10.0.0'} - deprecated: this version is no longer supported, please update to at least 0.8.* - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1143,16 +1157,13 @@ packages: resolution: {integrity: sha512-L6BuueHTRuJHQvQVc6YXYZRtN5vJUtOdCTLn0tRYYV5azfbAFcPghB5zEE40mVrV6w7slMTqUfkDomutIK14fw==} engines: {node: '>=20'} + construct-style-sheets-polyfill@3.1.0: + resolution: {integrity: sha512-HBLKP0chz8BAY6rBdzda11c3wAZeCZ+kIG4weVC2NM3AXzxx09nhe8t0SQNdloAvg5GLuHwq/0SPOOSPvtCcKw==} + cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} - core-js@3.48.0: - resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==} - - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1168,10 +1179,6 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - d@1.0.2: - resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} - engines: {node: '>=0.12'} - debounce-fn@6.0.0: resolution: {integrity: sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==} engines: {node: '>=18'} @@ -1233,20 +1240,6 @@ packages: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - epubjs@0.3.93: - resolution: {integrity: sha512-c06pNSdBxcXv3dZSbXAVLE1/pmleRhOT6mXNZo6INKmvuKpYB65MwU/lO7830czCtjIiK9i+KR+3S+p0wtljrw==} - - es5-ext@0.10.64: - resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} - engines: {node: '>=0.10'} - - es6-iterator@2.0.3: - resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} - - es6-symbol@3.1.4: - resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} - engines: {node: '>=0.12'} - esbuild@0.27.3: resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} @@ -1305,10 +1298,6 @@ packages: esm-env@1.2.2: resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} - esniff@2.0.1: - resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} - engines: {node: '>=0.10'} - espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1338,12 +1327,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - event-emitter@0.3.5: - resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} - - ext@1.7.0: - resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1449,9 +1432,6 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} - immediate@3.0.6: - resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1464,9 +1444,6 @@ packages: resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} engines: {node: '>=18'} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -1503,9 +1480,6 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1546,9 +1520,6 @@ packages: resolution: {integrity: sha512-LoCmV2n7rVry/gD4aMd9No7N3rB6xxxbbJedtdku8Ic7+JYbJRly6GWw+tO28/iuDxzAI0fpcgEoO0JyW+AUPg==} hasBin: true - jszip@3.10.1: - resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1563,12 +1534,6 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lie@3.1.1: - resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} - - lie@3.3.0: - resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} - lightningcss-android-arm64@1.31.1: resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} engines: {node: '>= 12.0.0'} @@ -1604,24 +1569,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.31.1: resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} @@ -1643,9 +1612,6 @@ packages: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} - localforage@1.10.0: - resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} - locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} @@ -1656,9 +1622,6 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash@4.17.23: - resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} - lru-cache@11.2.6: resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} @@ -1674,9 +1637,6 @@ packages: resolution: {integrity: sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw==} engines: {node: ^20.17.0 || >=22.9.0} - marks-pane@1.0.9: - resolution: {integrity: sha512-Ahs4oeG90tbdPWwAJkAAoHg2lRR8lAs9mZXETNPO9hYg3AkjUJBKi1NQ4aaIQZVGrig7c/3NUV1jANl8rFTeMg==} - mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -1752,9 +1712,6 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - next-tick@1.1.0: - resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} - node-machine-id@1.1.12: resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} @@ -1805,9 +1762,6 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} - pako@1.0.11: - resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1834,9 +1788,6 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} - path-webpack@0.0.3: - resolution: {integrity: sha512-AmeDxedoo5svf7aB3FYqSAKqMxys014lVKBzy1o/5vv9CtU7U4wgGWL1dA2o6MOzcD53ScN4Jmiq6VbtLz1vIQ==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1887,15 +1838,15 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-plugin-svelte@3.5.1: - resolution: {integrity: sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==} + prettier-plugin-svelte@3.5.2: + resolution: {integrity: sha512-ItFouLvzSFE3ulNl4DKoWM3BGcbDCNVpIyy/Y3F2gC3aNiGLxtFUdffVqO5Z5hhYG+DFT5KULWaxmeFFpdbvaQ==} peerDependencies: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - prettier-plugin-tailwindcss@0.6.14: - resolution: {integrity: sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==} - engines: {node: '>=14.21.3'} + prettier-plugin-tailwindcss@0.8.1: + resolution: {integrity: sha512-iaFMYqDsE4ffdDkn5qup0j5f2aCEBFZrdrZnvu9QKTlWx/iGPeQ4HHu7b7fCPMxeo9nwQBiOAh2nSypdFYWJkw==} + engines: {node: '>=20.19'} peerDependencies: '@ianvs/prettier-plugin-sort-imports': '*' '@prettier/plugin-hermes': '*' @@ -1907,14 +1858,12 @@ packages: prettier: ^3.0 prettier-plugin-astro: '*' prettier-plugin-css-order: '*' - prettier-plugin-import-sort: '*' prettier-plugin-jsdoc: '*' prettier-plugin-marko: '*' prettier-plugin-multiline-arrays: '*' prettier-plugin-organize-attributes: '*' prettier-plugin-organize-imports: '*' prettier-plugin-sort-imports: '*' - prettier-plugin-style-order: '*' prettier-plugin-svelte: '*' peerDependenciesMeta: '@ianvs/prettier-plugin-sort-imports': @@ -1935,8 +1884,6 @@ packages: optional: true prettier-plugin-css-order: optional: true - prettier-plugin-import-sort: - optional: true prettier-plugin-jsdoc: optional: true prettier-plugin-marko: @@ -1949,8 +1896,6 @@ packages: optional: true prettier-plugin-sort-imports: optional: true - prettier-plugin-style-order: - optional: true prettier-plugin-svelte: optional: true @@ -1968,9 +1913,6 @@ packages: resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} engines: {node: ^20.17.0 || >=22.9.0} - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1979,9 +1921,6 @@ packages: resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} - readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -2039,9 +1978,6 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2053,9 +1989,6 @@ packages: set-cookie-parser@3.0.1: resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} - setimmediate@1.0.5: - resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2102,9 +2035,6 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2244,9 +2174,6 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} - type@2.7.3: - resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} - typescript-eslint@8.56.1: resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2539,20 +2466,20 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))': dependencies: - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1)(supports-color@10.2.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@1.4.1(eslint@9.39.4(jiti@2.6.1))': + '@eslint/compat@1.4.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))': dependencies: '@eslint/core': 0.17.0 optionalDependencies: - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1)(supports-color@10.2.2) - '@eslint/config-array@0.21.2': + '@eslint/config-array@0.21.2(supports-color@10.2.2)': dependencies: '@eslint/object-schema': 2.1.7 debug: 4.4.3(supports-color@10.2.2) @@ -2568,7 +2495,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.5': + '@eslint/eslintrc@3.3.5(supports-color@10.2.2)': dependencies: ajv: 6.14.0 debug: 4.4.3(supports-color@10.2.2) @@ -2662,13 +2589,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@npmcli/agent@4.0.0': + '@npmcli/agent@4.0.0(supports-color@10.2.2)': dependencies: agent-base: 7.1.4 - http-proxy-agent: 7.0.2 + http-proxy-agent: 7.0.2(supports-color@10.2.2) https-proxy-agent: 7.0.6(supports-color@10.2.2) lru-cache: 11.2.6 - socks-proxy-agent: 8.0.5 + socks-proxy-agent: 8.0.5(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -3003,10 +2930,6 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/localforage@0.0.34': - dependencies: - localforage: 1.10.0 - '@types/node@22.19.15': dependencies: undici-types: 6.21.0 @@ -3017,15 +2940,15 @@ snapshots: '@types/trusted-types@2.0.7': {} - '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.1 - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1)(supports-color@10.2.2) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -3033,19 +2956,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.56.1 '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(supports-color@10.2.2)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.1 debug: 4.4.3(supports-color@10.2.2) - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1)(supports-color@10.2.2) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.56.1(supports-color@10.2.2)(typescript@5.9.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) '@typescript-eslint/types': 8.56.1 @@ -3063,13 +2986,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.56.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) debug: 4.4.3(supports-color@10.2.2) - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1)(supports-color@10.2.2) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -3077,9 +3000,9 @@ snapshots: '@typescript-eslint/types@8.56.1': {} - '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.56.1(supports-color@10.2.2)(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) + '@typescript-eslint/project-service': 8.56.1(supports-color@10.2.2)(typescript@5.9.3) '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) '@typescript-eslint/types': 8.56.1 '@typescript-eslint/visitor-keys': 8.56.1 @@ -3092,13 +3015,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2)) '@typescript-eslint/scope-manager': 8.56.1 '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - eslint: 9.39.4(jiti@2.6.1) + '@typescript-eslint/typescript-estree': 8.56.1(supports-color@10.2.2)(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1)(supports-color@10.2.2) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3166,8 +3089,6 @@ snapshots: '@vue/shared@3.5.29': {} - '@xmldom/xmldom@0.7.13': {} - acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -3325,12 +3246,10 @@ snapshots: semver: 7.7.4 uint8array-extras: 1.5.0 + construct-style-sheets-polyfill@3.1.0: {} + cookie@0.6.0: {} - core-js@3.48.0: {} - - core-util-is@1.0.3: {} - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3345,11 +3264,6 @@ snapshots: csstype@3.2.3: {} - d@1.0.2: - dependencies: - es5-ext: 0.10.64 - type: 2.7.3 - debounce-fn@6.0.0: dependencies: mimic-function: 5.0.1 @@ -3391,36 +3305,6 @@ snapshots: env-paths@3.0.0: {} - epubjs@0.3.93: - dependencies: - '@types/localforage': 0.0.34 - '@xmldom/xmldom': 0.7.13 - core-js: 3.48.0 - event-emitter: 0.3.5 - jszip: 3.10.1 - localforage: 1.10.0 - lodash: 4.17.23 - marks-pane: 1.0.9 - path-webpack: 0.0.3 - - es5-ext@0.10.64: - dependencies: - es6-iterator: 2.0.3 - es6-symbol: 3.1.4 - esniff: 2.0.1 - next-tick: 1.1.0 - - es6-iterator@2.0.3: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - es6-symbol: 3.1.4 - - es6-symbol@3.1.4: - dependencies: - d: 1.0.2 - ext: 1.7.0 - esbuild@0.27.3: optionalDependencies: '@esbuild/aix-ppc64': 0.27.3 @@ -3454,15 +3338,15 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.6.1)): + eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2)): dependencies: - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1)(supports-color@10.2.2) - eslint-plugin-svelte@3.15.0(eslint@9.39.4(jiti@2.6.1))(svelte@5.53.7): + eslint-plugin-svelte@3.15.0(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(svelte@5.53.7): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2)) '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 9.39.4(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1)(supports-color@10.2.2) esutils: 2.0.3 globals: 16.5.0 known-css-properties: 0.37.0 @@ -3487,14 +3371,14 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.4(jiti@2.6.1): + eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.2 + '@eslint/config-array': 0.21.2(supports-color@10.2.2) '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.5 + '@eslint/eslintrc': 3.3.5(supports-color@10.2.2) '@eslint/js': 9.39.4 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 @@ -3530,13 +3414,6 @@ snapshots: esm-env@1.2.2: {} - esniff@2.0.1: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - event-emitter: 0.3.5 - type: 2.7.3 - espree@10.4.0: dependencies: acorn: 8.16.0 @@ -3565,15 +3442,6 @@ snapshots: esutils@2.0.3: {} - event-emitter@0.3.5: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - - ext@1.7.0: - dependencies: - type: 2.7.3 - fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -3641,7 +3509,7 @@ snapshots: http-cache-semantics@4.2.0: {} - http-proxy-agent@7.0.2: + http-proxy-agent@7.0.2(supports-color@10.2.2): dependencies: agent-base: 7.1.4 debug: 4.4.3(supports-color@10.2.2) @@ -3664,8 +3532,6 @@ snapshots: ignore@7.0.5: {} - immediate@3.0.6: {} - import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -3675,8 +3541,6 @@ snapshots: index-to-position@1.2.0: {} - inherits@2.0.4: {} - inline-style-parser@0.2.7: {} ip-address@10.1.0: {} @@ -3705,8 +3569,6 @@ snapshots: is-unicode-supported@2.1.0: {} - isarray@1.0.0: {} - isexe@2.0.0: {} jiti@2.6.1: {} @@ -3731,7 +3593,7 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - jsrepo@2.5.2(typescript@5.9.3)(zod@4.3.6): + jsrepo@2.5.2(supports-color@10.2.2)(typescript@5.9.3)(zod@4.3.6): dependencies: '@anthropic-ai/sdk': 0.62.0 '@biomejs/js-api': 3.0.0(@biomejs/wasm-nodejs@2.4.6) @@ -3750,7 +3612,7 @@ snapshots: get-tsconfig: 4.13.6 ignore: 7.0.5 is-unicode-supported: 2.1.0 - make-fetch-happen: 15.0.4 + make-fetch-happen: 15.0.4(supports-color@10.2.2) node-machine-id: 1.1.12 ollama: 0.5.18 openai: 5.23.2(zod@4.3.6) @@ -3776,13 +3638,6 @@ snapshots: - ws - zod - jszip@3.10.1: - dependencies: - lie: 3.3.0 - pako: 1.0.11 - readable-stream: 2.3.8 - setimmediate: 1.0.5 - keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -3796,14 +3651,6 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lie@3.1.1: - dependencies: - immediate: 3.0.6 - - lie@3.3.0: - dependencies: - immediate: 3.0.6 - lightningcss-android-arm64@1.31.1: optional: true @@ -3855,10 +3702,6 @@ snapshots: lilconfig@2.1.0: {} - localforage@1.10.0: - dependencies: - lie: 3.1.1 - locate-character@3.0.0: {} locate-path@6.0.0: @@ -3867,8 +3710,6 @@ snapshots: lodash.merge@4.6.2: {} - lodash@4.17.23: {} - lru-cache@11.2.6: {} lz-string@1.5.0: {} @@ -3877,10 +3718,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - make-fetch-happen@15.0.4: + make-fetch-happen@15.0.4(supports-color@10.2.2): dependencies: '@gar/promise-retry': 1.0.2 - '@npmcli/agent': 4.0.0 + '@npmcli/agent': 4.0.0(supports-color@10.2.2) cacache: 20.0.3 http-cache-semantics: 4.2.0 minipass: 7.1.3 @@ -3893,8 +3734,6 @@ snapshots: transitivePeerDependencies: - supports-color - marks-pane@1.0.9: {} - mimic-function@5.0.1: {} minimatch@10.2.4: @@ -3961,8 +3800,6 @@ snapshots: negotiator@1.0.0: {} - next-tick@1.1.0: {} - node-machine-id@1.1.12: {} obug@2.1.1: {} @@ -4025,8 +3862,6 @@ snapshots: package-manager-detector@1.6.0: {} - pako@1.0.11: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -4052,8 +3887,6 @@ snapshots: lru-cache: 11.2.6 minipass: 7.1.3 - path-webpack@0.0.3: {} - pathe@2.0.3: {} picocolors@1.1.1: {} @@ -4090,16 +3923,16 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.53.7): + prettier-plugin-svelte@3.5.2(prettier@3.8.1)(svelte@5.53.7): dependencies: prettier: 3.8.1 svelte: 5.53.7 - prettier-plugin-tailwindcss@0.6.14(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.53.7))(prettier@3.8.1): + prettier-plugin-tailwindcss@0.8.1(prettier-plugin-svelte@3.5.2(prettier@3.8.1)(svelte@5.53.7))(prettier@3.8.1): dependencies: prettier: 3.8.1 optionalDependencies: - prettier-plugin-svelte: 3.5.1(prettier@3.8.1)(svelte@5.53.7) + prettier-plugin-svelte: 3.5.2(prettier@3.8.1)(svelte@5.53.7) prettier@3.8.1: {} @@ -4111,22 +3944,10 @@ snapshots: proc-log@6.1.0: {} - process-nextick-args@2.0.1: {} - punycode@2.3.1: {} react@19.2.0: {} - readable-stream@2.3.8: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - readdirp@4.1.2: {} require-from-string@2.0.2: {} @@ -4202,8 +4023,6 @@ snapshots: dependencies: mri: 1.2.0 - safe-buffer@5.1.2: {} - safer-buffer@2.1.2: optional: true @@ -4211,8 +4030,6 @@ snapshots: set-cookie-parser@3.0.1: {} - setimmediate@1.0.5: {} - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4229,7 +4046,7 @@ snapshots: smart-buffer@4.2.0: {} - socks-proxy-agent@8.0.5: + socks-proxy-agent@8.0.5(supports-color@10.2.2): dependencies: agent-base: 7.1.4 debug: 4.4.3(supports-color@10.2.2) @@ -4262,10 +4079,6 @@ snapshots: get-east-asian-width: 1.5.0 strip-ansi: 7.2.0 - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -4420,15 +4233,13 @@ snapshots: type-fest@4.41.0: {} - type@2.7.3: {} - - typescript-eslint@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.56.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.4(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1)(supports-color@10.2.2) typescript: 5.9.3 transitivePeerDependencies: - supports-color diff --git a/frontend/scripts/vendor-foliate.sh b/frontend/scripts/vendor-foliate.sh new file mode 100755 index 0000000..9ca3281 --- /dev/null +++ b/frontend/scripts/vendor-foliate.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# +# Vendors foliate-js into src/lib/vendor/foliate-js/. +# +# foliate-js has no npm release and no build step; upstream recommends a git +# submodule. We copy instead, because this repo has no submodules (pdf.js is +# vendored the same way under static/pdfjs) and because a submodule would drag +# in 231 files / 13 MB, of which 191 files / 12 MB is a bundled pdf.js build we +# deliberately do not use — Chitai serves PDFs through static/pdfjs/web/viewer.html. +# +# Only the files reachable from view.js are copied: 15 upstream files, ~656 KB. +# pdf.js is NOT copied; a stub is written in its place (see below). +# +# To update: bump FOLIATE_SHA, re-run, review the diff, then smoke-test the +# reader — paginator.js is ~3800 lines of gesture and animation code and this +# fork is pushed to frequently. +# +# Usage: ./scripts/vendor-foliate.sh +set -euo pipefail + +FOLIATE_REPO="https://github.com/readest/foliate-js.git" +FOLIATE_SHA="63a2eb1fc1e4813c4e849ccdb3d4be2c54a35869" + +DEST="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/src/lib/vendor/foliate-js" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +# The reachable closure from view.js. Everything else upstream ships is either +# unreachable (dict.js, opds.js, footnotes.js, quote-image.js, uri-template.js), +# a demo (reader.js), or build tooling (rollup.config.js, eslint.config.js). +FILES=( + view.js + epub.js + epubcfi.js + paginator.js + fixed-layout.js + overlayer.js + progress.js + search.js + text-walker.js + tts.js + mobi.js + comic-book.js + fb2.js + vendor/zip.js + vendor/fflate.js +) + +echo "Cloning $FOLIATE_REPO @ ${FOLIATE_SHA:0:7} ..." +git clone --quiet --filter=blob:none --no-checkout "$FOLIATE_REPO" "$TMP/foliate" +git -C "$TMP/foliate" checkout --quiet "$FOLIATE_SHA" + +rm -rf "$DEST" +mkdir -p "$DEST/vendor" + +for f in "${FILES[@]}"; do + if [ ! -f "$TMP/foliate/$f" ]; then + echo "ERROR: $f is missing upstream at ${FOLIATE_SHA:0:7}." >&2 + echo "The file list in this script is stale; re-check the import graph." >&2 + exit 1 + fi + cp "$TMP/foliate/$f" "$DEST/$f" +done + +cp "$TMP/foliate/LICENSE" "$DEST/LICENSE" + +# view.js does `await import('./pdf.js')` inside makeBook. That is a static-string +# dynamic import, so Rollup resolves it at build time whether or not the code path +# ever runs — and upstream's pdf.js opens with `import '@pdfjs/pdf.min.mjs'`, a bare +# specifier that does not resolve here. Shipping this stub at that path keeps the +# build working without a Vite alias, and without vendoring 12 MB of pdf.js. +cat > "$DEST/pdf.js" <<'STUB' +// NOT upstream foliate-js. See README.chitai.md. +// +// Chitai renders PDFs with the pdf.js viewer vendored at static/pdfjs/, so +// foliate's PDF backend is not vendored. view.js still references this module +// from makeBook via a static-string dynamic import, which Rollup resolves at +// build time regardless of whether it executes — so the file has to exist. +// +// Throwing at module scope surfaces a legible message in the reader's error +// card if a PDF is ever routed to the EPUB reader by mistake, rather than a +// TypeError from `globalThis.pdfjsLib` being undefined. +throw new Error('foliate-js PDF rendering is not enabled in Chitai'); +STUB + +cat > "$DEST/README.chitai.md" < (Readest's fork of johnfactotum/foliate-js) | +| Pinned commit | \`$FOLIATE_SHA\` | +| Licence | MIT — see \`LICENSE\` | + +Readest's fork is used rather than upstream for its paginator work: touch/swipe +turn handling, fixed-layout spread centring, and a malformed-XHTML fallback in +\`loadDocument\`. + +## What is here + +Only the import closure reachable from \`view.js\`. Not vendored, because nothing +reaches them: \`dict.js\`, \`opds.js\`, \`footnotes.js\`, \`quote-image.js\`, +\`uri-template.js\`, \`reader.js\` (upstream's demo), and the build configs. + +## pdf.js is ours, not upstream's + +\`pdf.js\` in this directory is a **stub that throws**. Upstream's version imports +\`@pdfjs/pdf.min.mjs\` — a bare specifier backed by a 12 MB vendored pdf.js build — +and \`view.js\` reaches it through \`await import('./pdf.js')\`, which Rollup resolves +at build time even though Chitai never takes that path. Chitai serves PDFs from +\`static/pdfjs/web/viewer.html\` instead. + +To enable foliate's PDF backend, add \`pdf.js\` and \`vendor/pdfjs/\` to the file list +in the vendor script and drop the stub. + +## Updating + +Bump \`FOLIATE_SHA\` in \`frontend/scripts/vendor-foliate.sh\`, re-run it, review the +diff, and smoke-test the reader — \`paginator.js\` is ~3800 lines of gesture and +animation code and this fork is pushed to frequently. +EOF + +echo +echo "Vendored ${#FILES[@]} files + LICENSE + pdf.js stub + README.chitai.md to:" +echo " $DEST" +du -sh "$DEST" | sed 's/^/ /' diff --git a/frontend/src/app.css b/frontend/src/app.css index 3a6fb71..1ea6b25 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -8,74 +8,100 @@ :root { --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.129 0.042 264.695); - --card: oklch(1 0 0); - --card-foreground: oklch(0.129 0.042 264.695); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.129 0.042 264.695); - --primary: oklch(0.208 0.042 265.755); - --primary-foreground: oklch(0.984 0.003 247.858); - --secondary: oklch(0.968 0.007 247.896); - --secondary-foreground: oklch(0.208 0.042 265.755); - --muted: oklch(0.968 0.007 247.896); - --muted-foreground: oklch(0.554 0.046 257.417); - --accent: oklch(0.968 0.007 247.896); - --accent-foreground: oklch(0.208 0.042 265.755); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.929 0.013 255.508); - --input: oklch(0.929 0.013 255.508); - --ring: oklch(0.704 0.04 256.788); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.984 0.003 247.858); - --sidebar-foreground: oklch(0.129 0.042 264.695); - --sidebar-primary: oklch(0.208 0.042 265.755); - --sidebar-primary-foreground: oklch(0.984 0.003 247.858); - --sidebar-accent: oklch(0.968 0.007 247.896); - --sidebar-accent-foreground: oklch(0.208 0.042 265.755); - --sidebar-border: oklch(0.929 0.013 255.508); - --sidebar-ring: oklch(0.704 0.04 256.788); + + /* Typography — system stacks, so nothing depends on a CDN or a webfont build. */ + --app-font-sans: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --app-font-serif: Georgia, 'Iowan Old Style', 'Times New Roman', serif; + --app-font-mono: ui-monospace, 'SF Mono', 'Cascadia Mono', Menlo, Consolas, monospace; + + /* Reading Room — light */ + --background: #e9ebee; + --foreground: #1b1f24; + --card: #fbfbfc; + --card-foreground: #1b1f24; + --popover: #fbfbfc; + --popover-foreground: #1b1f24; + --primary: #1f5f5b; + --primary-foreground: #f2f7f6; + --secondary: #dfe3e8; + --secondary-foreground: #1b1f24; + --muted: #e2e5e9; + --muted-foreground: #7d858f; + --accent: #d7e5e3; + --accent-foreground: #164743; + --destructive: #a6402f; + --border: #d2d6dc; + --input: #d2d6dc; + --ring: #1f5f5b; + + /* Semantic — deliberately not the accent, so state never reads as branding. */ + --success: #2f7d4f; + --success-foreground: #f2f7f6; + --flag: #b08a1e; + --star: #c79a25; + + --chart-1: #1f5f5b; + --chart-2: #2f7d4f; + --chart-3: #b08a1e; + --chart-4: #4c6b8a; + --chart-5: #a6402f; + + --sidebar: #e2e5e9; + --sidebar-foreground: #1b1f24; + --sidebar-primary: #1f5f5b; + --sidebar-primary-foreground: #f2f7f6; + --sidebar-accent: #d7e5e3; + --sidebar-accent-foreground: #164743; + --sidebar-border: #d2d6dc; + --sidebar-ring: #1f5f5b; } .dark { - --background: oklch(0.129 0.042 264.695); - --foreground: oklch(0.984 0.003 247.858); - --card: oklch(0.208 0.042 265.755); - --card-foreground: oklch(0.984 0.003 247.858); - --popover: oklch(0.208 0.042 265.755); - --popover-foreground: oklch(0.984 0.003 247.858); - --primary: oklch(0.929 0.013 255.508); - --primary-foreground: oklch(0.208 0.042 265.755); - --secondary: oklch(0.279 0.041 260.031); - --secondary-foreground: oklch(0.984 0.003 247.858); - --muted: oklch(0.279 0.041 260.031); - --muted-foreground: oklch(0.704 0.04 256.788); - --accent: oklch(0.279 0.041 260.031); - --accent-foreground: oklch(0.984 0.003 247.858); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.551 0.027 264.364); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.208 0.042 265.755); - --sidebar-foreground: oklch(0.984 0.003 247.858); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.984 0.003 247.858); - --sidebar-accent: oklch(0.279 0.041 260.031); - --sidebar-accent-foreground: oklch(0.984 0.003 247.858); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.551 0.027 264.364); + /* Reading Room — dark */ + --background: #15191b; + --foreground: #e6eae9; + --card: #1d2226; + --card-foreground: #e6eae9; + --popover: #1d2226; + --popover-foreground: #e6eae9; + --primary: #6fbab0; + --primary-foreground: #0e1a19; + --secondary: #232a2d; + --secondary-foreground: #e6eae9; + --muted: #232a2d; + --muted-foreground: #78868a; + --accent: #1b3330; + --accent-foreground: #9fd8d0; + --destructive: #e0796b; + --border: #2a3034; + --input: #2a3034; + --ring: #6fbab0; + + --success: #4fa97a; + --success-foreground: #0e1a19; + --flag: #d4a63a; + --star: #e5b84b; + + --chart-1: #6fbab0; + --chart-2: #4fa97a; + --chart-3: #d4a63a; + --chart-4: #7f9dc0; + --chart-5: #e0796b; + + --sidebar: #111517; + --sidebar-foreground: #e6eae9; + --sidebar-primary: #6fbab0; + --sidebar-primary-foreground: #0e1a19; + --sidebar-accent: #1b3330; + --sidebar-accent-foreground: #9fd8d0; + --sidebar-border: #2a3034; + --sidebar-ring: #6fbab0; } @theme inline { + --font-sans: var(--app-font-sans); + --font-serif: var(--app-font-serif); + --font-mono: var(--app-font-mono); --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); @@ -95,6 +121,10 @@ --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + --color-flag: var(--flag); + --color-star: var(--star); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts index 6cafeb6..8b24ae2 100644 --- a/frontend/src/app.d.ts +++ b/frontend/src/app.d.ts @@ -2,7 +2,8 @@ // for information about these interfaces import type { ApiClient } from '$lib/server/api'; -import type { User } from 'lucide-svelte'; +import type { User } from '$lib/server/auth'; +import type { ThemeConfig } from '$lib/theme/presets'; declare global { namespace App { @@ -11,6 +12,7 @@ declare global { authToken: string | null; api: ApiClient; user: User; + theme: ThemeConfig; } // interface PageData {} // interface PageState {} diff --git a/frontend/src/app.html b/frontend/src/app.html index 06a6722..9a35db9 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -4,6 +4,7 @@ %sveltekit.head% +
%sveltekit.body%
diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index f7ac519..af0d5c5 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -1,5 +1,6 @@ import { ApiClient } from '$lib/server/api'; import { validateToken } from '$lib/server/auth'; +import { THEME_COOKIE, parseThemeCookie, themeToCss } from '$lib/theme/presets'; import { redirect, type Handle } from '@sveltejs/kit'; import { sequence } from '@sveltejs/kit/hooks'; @@ -34,4 +35,28 @@ const protectedRoutesHandle: Handle = async ({ event, resolve }) => { return resolve(event); }; -export const handle = sequence(authHandle, protectedRoutesHandle); +/** + * Inline the stored theme into the document head. + * + * The palette has to be in the very first byte of HTML the browser paints, + * otherwise every page load flashes the default theme before hydration swaps + * it. The `` placeholder in app.html is the injection point. + */ +const themeHandle: Handle = async ({ event, resolve }) => { + const config = parseThemeCookie(event.cookies.get(THEME_COOKIE)); + event.locals.theme = config; + + return resolve(event, { + // The placeholder comment is kept, not replaced. Svelte 5 uses HTML + // comments as hydration markers, so SvelteKit warns when a chunk comes + // back with fewer comments than it went in with — removing this one is + // enough to trip that check. + transformPageChunk: ({ html }) => + html.replace( + '', + `` + ) + }); +}; + +export const handle = sequence(themeHandle, authHandle, protectedRoutesHandle); diff --git a/frontend/src/lib/components/forms/book-drop-zone.svelte b/frontend/src/lib/components/forms/book-drop-zone.svelte new file mode 100644 index 0000000..befa705 --- /dev/null +++ b/frontend/src/lib/components/forms/book-drop-zone.svelte @@ -0,0 +1,223 @@ + + +
{ + e.preventDefault(); + if (active) dragging = true; + }} + ondragleave={() => (dragging = false)} + ondrop={handleDrop} + class={cn( + 'flex flex-col items-center gap-3 rounded-lg border-2 border-dashed border-border bg-accent/20 p-6 text-center transition-colors', + dragging && 'border-primary bg-accent/50', + !active && 'opacity-50', + className + )} +> +
+ +
+ +
+ + {busy ? 'Reading folder…' : 'Drop books here'} + + A folder keeps its structure +
+ + or + +
+ + +
+ + + + +
diff --git a/frontend/src/lib/components/forms/books-upload.svelte b/frontend/src/lib/components/forms/books-upload.svelte index a27375e..fb75948 100644 --- a/frontend/src/lib/components/forms/books-upload.svelte +++ b/frontend/src/lib/components/forms/books-upload.svelte @@ -1,165 +1,231 @@ - - {#if uploadBooks.pending} -
- Uploading {uploadBooks.fields.files.value().length} files... - -
- {:else} - - Upload Books - + + + + Add books + + Files or a folder. A folder becomes one book per directory. + + - // Check if there are any validation issues - const issues = uploadBooks.fields.allIssues(); - if (issues && issues.length > 0) { - return; - } - - // Update library book count - const count = uploadBooks.result.total - libraryState.libraries.find(lib => uploadBooks.fields.library_id.value() == lib.id.toString())!.total += count - - // Reset the files field - uploadBooks.fields.files.set([]); - toast.success('Books successfully uploaded!'); - - if (navigateOnUpload) { - navigateToBooks(uploadBooks.result); - } - } catch (error) { - console.error('Failed to upload book: ', error); - toast.error('Failed to upload books'); - } - })} - bind:this={formEl} - enctype="multipart/form-data" - class="flex w-full flex-col gap-2 p-4" - > - - Select Library - - {#each libraryState.libraries as library} - - {library.name} - +
+
+ Library + + {#each libraryState.libraries as library (library.id)} + {library.name} {/each} +
- - -
- {#each files as file, idx} -
-
- {file.name} - {displaySize(file.size)} -
- + + + {#if files.length > 0} +
+ {files.length} ready to upload + + {displaySize(totalSize)} + +
+ {/if} + +
+ {#each files as file, idx (file.name)} + {@const location = splitPath(file.name)} +
+ +
+ {location.name} + + {#if location.dir} + {location.dir} + {/if} + {displaySize(file.size)} +
- {/each} -
+ +
+ {/each} +
-
-
- - Auto upload on file drop - -
-
- - Navigate to book on upload + {#if rejected.length > 0} +
+
+ + {rejected.length} + {rejected.length === 1 ? 'file was' : 'files were'} skipped + +
+ {#if showRejected} +
    + {#each rejected as entry (entry.name)} +
  • + + {splitPath(entry.name).name} + + {entry.reason} +
  • + {/each} +
+ {/if}
- - {/if} + {/if} + +
+
+ + + Start as soon as books are added + + +
+
+ + + Open the book when a single one is added + +
+
+
diff --git a/frontend/src/lib/components/forms/edit-book/edit-book.svelte b/frontend/src/lib/components/forms/edit-book/edit-book.svelte index 1b0a5e0..edbc865 100644 --- a/frontend/src/lib/components/forms/edit-book/edit-book.svelte +++ b/frontend/src/lib/components/forms/edit-book/edit-book.svelte @@ -1,39 +1,68 @@ + {#if book} - - - - Metadata - Cover - Files - + {#key book.id} + + + {book.title} + + {book.authors.map((author) => author.name).join(', ') || 'Unknown author'} + + - - - - + +
+ - - - - +
+ +
+
- - - - -
-
+ + +

Cover and file changes apply immediately

+
+ + +
+
+ + {/key} {/if}
diff --git a/frontend/src/lib/components/forms/edit-book/edit-cover.svelte b/frontend/src/lib/components/forms/edit-book/edit-cover.svelte index db2578c..ab834f9 100644 --- a/frontend/src/lib/components/forms/edit-book/edit-cover.svelte +++ b/frontend/src/lib/components/forms/edit-book/edit-cover.svelte @@ -1,33 +1,23 @@ -
{ - try { - await submit(); - form.reset(); - open = false; - toast.success('Updated book cover!'); - } catch (error) { - console.error('Failed to update book cover: ', error); - toast.error('Failed to update cover.'); - } - })} - enctype="multipart/form-data" - class="grid grid-cols-[1fr_2fr] gap-4 p-6" -> - +
+

Cover

-
- -
+ + + { + try { + await submit(); + form.reset(); + // Deliberately does not close the dialog. The cover is one panel of a + // larger form now, and closing here would throw away metadata edits + // the reader has not saved yet. + toast.success('Cover updated'); + } catch (error) { + console.error('Failed to update book cover: ', error); + toast.error('Failed to update the cover'); + } + })} + enctype="multipart/form-data" + class="flex flex-col gap-2" + > + + -
- -
- {#if updateBookCover.fields.file.value()} -
-
- {updateBookCover.fields.file.value().name} - {displaySize(updateBookCover.fields.file.value().size)} -
- -
- {/if} -
- -
- - Auto upload on file drop - -
-
- + +
diff --git a/frontend/src/lib/components/forms/edit-book/edit-files.svelte b/frontend/src/lib/components/forms/edit-book/edit-files.svelte index e932bd8..31dbf95 100644 --- a/frontend/src/lib/components/forms/edit-book/edit-files.svelte +++ b/frontend/src/lib/components/forms/edit-book/edit-files.svelte @@ -1,98 +1,196 @@ -
{ - try { - await submit(); +
+

Files

- // Check if there are any validation issues - const issues = uploadBookFiles.fields.allIssues(); - if (issues && issues.length > 0) { - return; - } + {#if files.length === 0} +

+ No files yet. Add one below so this book can be read or downloaded. +

+ {/if} - // Reset the files field - uploadBookFiles.fields.files.set([]); - toast.success('Files successfully added!'); - } catch (error) { - console.error('Failed to upload files: ', error); - toast.error('Failed to upload files'); - } - })} - bind:this={formEl} - enctype="multipart/form-data" - class="flex w-full flex-col gap-2 p-4" -> - - - - -
- {#each files as file, idx} -
-
- {file.name} - {displaySize(file.size)} -
- -
- {/each} -
+ {getFileType(file.filename)} + -
- - Auto upload on file drop - -
- + + {file.filename} + + {formatFileSize(file.size)} + + + + + + + + {/each} + + {#each pending as file (file.name)} +
  • + + + {file.name} + Uploading… + +
  • + {/each} + + +
    { + try { + await submit(); + + const issues = uploadBookFiles.fields.allIssues(); + if (issues && issues.length > 0) return; + + // The endpoint answers with the updated book, so the new files come + // back with their ids rather than having to be guessed at. + files = uploadBookFiles.result?.files ?? files; + uploadBookFiles.fields.files.set([]); + toast.success('Files added'); + } catch (error) { + console.error('Failed to add files: ', error); + toast.error('Failed to add files'); + } + })} + enctype="multipart/form-data" + class="flex flex-col gap-2" + > + + + + + +
    + + + + + Remove {fileToDelete?.filename}? + + This cannot be undone. The other files on this book are not affected. + + + + +
    + + + Also delete the file from the filesystem + +
    + + + Cancel + + Remove + + +
    +
    diff --git a/frontend/src/lib/components/forms/edit-book/edit-metadata.svelte b/frontend/src/lib/components/forms/edit-book/edit-metadata.svelte index 7b935be..45e149f 100644 --- a/frontend/src/lib/components/forms/edit-book/edit-metadata.svelte +++ b/frontend/src/lib/components/forms/edit-book/edit-metadata.svelte @@ -1,26 +1,32 @@ - -
    { - try { - await submit(); - - // Check if there are any validation issues - const issues = updateBookMetadata.fields.allIssues(); - if (issues && issues.length > 0) { - return; - } - - open = false; - book = book; - toast.success('Updated book metadata!'); - } catch (error) { - console.error('Error occurred updating book metadata: ', error); - toast.error('Failed to update book metadata.'); - } - })} +{#snippet groupHeading(label: string)} +

    - - - - - + {label} +

    +{/snippet} - - - Title - - {#each updateBookMetadata.fields.title.issues() ?? [] as issue} - {issue.message} - {/each} - + { + try { + await submit(); - - - Subtitle - - {#each updateBookMetadata.fields.subtitle.issues() ?? [] as issue} - {issue.message} - {/each} - + // Check if there are any validation issues + const issues = updateBookMetadata.fields.allIssues(); + if (issues && issues.length > 0) { + return; + } -
    - - - Series - - {#each updateBookMetadata.fields.series.issues() ?? [] as issue} - {issue.message} - {/each} - + open = false; + toast.success('Updated book metadata!'); + } catch (error) { + console.error('Error occurred updating book metadata: ', error); + toast.error('Failed to update book metadata.'); + } + })} + class="grid grid-cols-1 items-start gap-x-4 gap-y-3 sm:grid-cols-2" +> + - - - Series position - - {#each updateBookMetadata.fields.series_position.issues() ?? [] as issue} - {issue.message} - {/each} - -
    + {@render groupHeading('Identity')} - - - Authors - - {#each authors as author} - - {/each} - {#each updateBookMetadata.fields.authors.issues() ?? [] as issue} - {issue.message} - {/each} - + + Title + + {#each updateBookMetadata.fields.title.issues() ?? [] as issue} + {issue.message} + {/each} + - - - Tags - - {#each tags as tag} - - {/each} - {#each updateBookMetadata.fields.tags.issues() ?? [] as issue} - {issue.message} - {/each} - + + Subtitle + + {#each updateBookMetadata.fields.subtitle.issues() ?? [] as issue} + {issue.message} + {/each} + - - - Description -