Merge branch 'fix/darkreader'
Continuous Integration / lint-and-test (push) Successful in 6s
Continuous Integration / docker-build-and-push (push) Successful in 23s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-15 11:07:43 +02:00
commit 9ae2ba3241
7 files changed
+160 -2

No files matched your search

+1 -1
View File
@@ -188,7 +188,7 @@ The whole world is scoped so it can live inside a shadow root on mangabaka.org:
## Colors
A bookshop palette: paper and umber grounds, sumi ink, one red band, one teal for completion, kraft for the furniture. Each light token has a dark (umber) counterpart under `prefers-color-scheme: dark`; `obi-ink` is shared by both schemes.
A bookshop palette: paper and umber grounds, sumi ink, one red band, one teal for completion, kraft for the furniture. Each light token has a dark (umber) counterpart under `prefers-color-scheme: dark`, repeated under `:root[data-theme="dark"]` for the page when Dark Reader is on (`src/web/dark-reader.ts`); `obi-ink` is shared by both schemes.
### Primary
- **Obi Red** (`obi-red`; dark `umber-obi-red`): the band of the main cover, the stamp buttons ("Find covers", "Rehost and file for review"), a failed stage plate, the active plate's blinking pip, the rule above notices, and the main-cover checkbox. Its depth companion **Obi Red Deep** (`obi-red-deep`) is the stamp's hover and the main band's top rule. **Obi Ink** (`obi-ink`) is the only text colour on red.
+1 -1
View File
@@ -13,7 +13,7 @@ self-hosted Bun server (primary), a userscript, and later a Bun CLI.
|---|---|
| `src/core/` | Pure pipeline: sources, Wayback history, image inspection, dedupe, seed, MangaBaka client. No DOM, Bun, Node or `GM_*` APIs. |
| `src/core/session.ts` | Review state shared by the page and the userscript: cards, main cover, rehosting, manual text, token flow (preview → confirm → save for review). |
| `src/web/` | Built. The page ("Obi Shelf" design, see `DESIGN.md`): `template.ts` markup shared with the userscript panel, `app.ts` wiring (`mountApp`), `render.ts` shelf and timeline views, `view-model.ts` pure helpers, `transport.ts` (direct to MangaBaka/litterbox, everything else via `/api/fetch`), `styles.css` scoped for a shadow root. |
| `src/web/` | Built. The page ("Obi Shelf" design, see `DESIGN.md`): `template.ts` markup shared with the userscript panel, `app.ts` wiring (`mountApp`), `render.ts` shelf and timeline views, `view-model.ts` pure helpers, `transport.ts` (direct to MangaBaka/litterbox, everything else via `/api/fetch`), `styles.css` scoped for a shadow root, `dark-reader.ts` (switches to the page's own dark theme and locks Dark Reader out when that extension is darkening the page). |
| `src/server/` | Built. `app.ts`: web-standard handler (`createApp(env)`) — static assets, `/api/config`, `/api/fetch` allowlisted proxy (redirects re-checked, 20 MB cap), `/api/upload` (store or catbox; takes a source URL, never bytes), `/covers/*`. `storage.ts` (interface, in-memory) and `storage-dir.ts` (folder, size cap, 30-day sweep). `config.ts` (env, client IP), `rate-limit.ts`, `assets-path.ts`. `bun.ts`: thin `Bun.serve` entry. Gate and Workers entry deferred. |
| `src/userscript/` | Built. `gm.ts`: `GM_xmlhttpRequest` transport (no proxy; honest User-Agent for Wayback) and GM storage. `main.ts`: a "Seed covers" launcher on MangaBaka series pages (follows client-side navigation) opening the page's app in a shadow-root panel; optional server URL in its settings. Built by `scripts/build-userscript.ts` into `dist/web/mbseed.user.js`, served at `/mbseed.user.js`. |
| `src/cli/` | Later: Bun CLI. |
+15
View File
@@ -16,6 +16,21 @@ the repo every session — measurably slower and more tokens).
**Consequences:** Rule changes happen in one file; pointer files must stay thin. Plans are working
artefacts: kept after merge they go stale and contradict the code, so the spec is the record.
## 2026-10-15 — Use our own dark theme when Dark Reader is on
**Status:** accepted
**Context:** With a light system theme the page is light, so the Dark Reader extension restyled it with
its generated colours instead of the designed umber theme (the user reported Dark Reader did not see it as
a dark site). Dark Reader marks `<html data-darkreader-scheme="dark">` while active and removes its styles
when `<meta name="darkreader-lock">` appears, even after load (read in its source, 2026-10-15).
**Decision:** `src/web/dark-reader.ts` watches for that mark; on `dark` it sets `<html data-theme="dark">`
(which turns on a copy of the dark tokens in `styles.css`) and adds the lock. One-way for the page's
lifetime; the `dimmed` (light) scheme is ignored. A test keeps the two dark token blocks identical.
The userscript panel is untouched: the rule cannot match inside its shadow root.
**Alternatives:** A static `darkreader-lock` (Dark Reader users on a light system would get the light
page); a manual theme toggle (not asked for).
**Consequences:** The dark tokens exist twice in `styles.css`. Turning Dark Reader off keeps the page
dark until reload. Relies on Dark Reader's attribute name, which is not a documented API.
## 2026-10-15 — Docker image built by Gitea CI
**Status:** accepted
**Context:** The user wants a Dockerfile that Gitea builds automatically, plus a docker-compose example,
+46
View File
@@ -0,0 +1,46 @@
// Dark Reader marks <html data-darkreader-scheme="dark"> while it restyles a page. The page has its own
// dark theme, so when that mark appears we switch to it and add <meta name="darkreader-lock">, which makes
// Dark Reader remove its styles. One-way for the page's lifetime: removing its theme also removes the mark.
export interface ThemeDocument {
documentElement: { getAttribute(name: string): string | null; setAttribute(name: string, value: string): void };
addLockMeta(): void;
/** Calls onChange when <html>'s attributes change; returns a disconnect function. */
observeRoot(onChange: () => void): () => void;
}
export function followDarkReader(doc: ThemeDocument): void {
const adopt = (): boolean => {
if (doc.documentElement.getAttribute("data-darkreader-scheme") !== "dark") return false;
doc.documentElement.setAttribute("data-theme", "dark");
doc.addLockMeta();
return true;
};
let adopted = false;
const disconnect = doc.observeRoot(() => {
if (!adopted && adopt()) {
adopted = true;
disconnect();
}
});
if (adopt()) {
adopted = true;
disconnect();
}
}
export function browserThemeDocument(document: Document): ThemeDocument {
return {
documentElement: document.documentElement,
addLockMeta: () => {
const meta = document.createElement("meta");
meta.name = "darkreader-lock";
document.head.append(meta);
},
observeRoot: (onChange) => {
const observer = new MutationObserver(onChange);
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-darkreader-scheme"] });
return () => observer.disconnect();
},
};
}
+2
View File
@@ -1,10 +1,12 @@
import { Session } from "../core/session";
import { withPolicy } from "../core/transport";
import { mountApp } from "./app";
import { browserThemeDocument, followDarkReader } from "./dark-reader";
import { APP_HTML } from "./template";
import { localStore, pageTransport } from "./transport";
import type { ServerConfig } from "./view-model";
followDarkReader(browserThemeDocument(document));
const app = document.getElementById("app")!;
app.innerHTML = APP_HTML;
const config = await fetch("/api/config")
+23
View File
@@ -48,6 +48,29 @@
}
}
/* The same dark tokens when the page forces dark (dark-reader.ts sets <html data-theme="dark">).
Keep identical to the block above; test/dark-reader.test.ts checks it. Never matches inside the
userscript's shadow root. */
:root[data-theme="dark"] .mbseed-page, :root[data-theme="dark"] .mbseed-frame, :root[data-theme="dark"] .mbseed-panel {
--mb-page: #2a2520;
--mb-panel: #332d27;
--mb-slip: #3b342d;
--mb-ink: #ece4d6;
--mb-ink-2: #bdb2a2;
--mb-obi: #c42f28;
--mb-obi-deep: #8e1f1a;
--mb-obi-text: #f2867b;
--mb-teal: #6db3a9;
--mb-kraft: #6e5a3f;
--mb-rail: #4a3c2a;
--mb-line: rgba(236, 228, 214, 0.16);
--mb-frame: rgba(236, 228, 214, 0.35);
--mb-track: rgba(236, 228, 214, 0.14);
--mb-shadow: rgba(0, 0, 0, 0.6);
--mb-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23ece4d6' stroke-width='1.6'/%3E%3C/svg%3E");
color-scheme: dark;
}
/* ---------- the page around the app ---------- */
.mbseed-page { margin: 0; background: var(--mb-page); color: var(--mb-ink); font: 14px/1.5 var(--mb-sans); }
+72
View File
@@ -0,0 +1,72 @@
import { expect, test } from "bun:test";
import { followDarkReader, type ThemeDocument } from "../src/web/dark-reader";
type Fake = ThemeDocument & { attrs: Map<string, string>; metas: string[]; fire(): void; disconnected: boolean };
function fakeDocument(scheme?: string): Fake {
const attrs = new Map<string, string>();
if (scheme) attrs.set("data-darkreader-scheme", scheme);
const metas: string[] = [];
let callback = () => {};
const doc: Fake = {
attrs,
metas,
disconnected: false,
fire: () => callback(),
documentElement: {
getAttribute: (name) => attrs.get(name) ?? null,
setAttribute: (name, value) => void attrs.set(name, value),
},
addLockMeta: () => void metas.push("darkreader-lock"),
observeRoot: (onChange) => {
callback = onChange;
return () => {
doc.disconnected = true;
};
},
};
return doc;
}
test("Dark Reader already dark at start: our dark theme, and Dark Reader is told to stand down", () => {
const doc = fakeDocument("dark");
followDarkReader(doc);
expect(doc.attrs.get("data-theme")).toBe("dark");
expect(doc.metas).toEqual(["darkreader-lock"]);
expect(doc.disconnected).toBe(true);
});
test("Dark Reader turning on after load is followed once", () => {
const doc = fakeDocument();
followDarkReader(doc);
expect(doc.attrs.has("data-theme")).toBe(false);
doc.attrs.set("data-darkreader-scheme", "dark");
doc.fire();
expect(doc.attrs.get("data-theme")).toBe("dark");
expect(doc.disconnected).toBe(true);
doc.fire();
expect(doc.metas).toEqual(["darkreader-lock"]);
});
test("no Dark Reader, or its dimmed (light) scheme: the page keeps following the system", () => {
for (const doc of [fakeDocument(), fakeDocument("dimmed")]) {
followDarkReader(doc);
doc.fire();
expect(doc.attrs.has("data-theme")).toBe(false);
expect(doc.metas).toEqual([]);
expect(doc.disconnected).toBe(false);
}
});
test("the forced dark theme uses exactly the system dark tokens", async () => {
const css = await Bun.file("src/web/styles.css").text();
const block = (start: string) => {
const at = css.indexOf(start);
expect(at).toBeGreaterThan(-1);
const open = css.indexOf("{", css.indexOf(".mbseed-page", at));
return css.slice(open + 1, css.indexOf("}", open)).replace(/\s+/g, " ").trim();
};
const system = block("@media (prefers-color-scheme: dark)");
expect(system).toContain("--mb-page: #2a2520;");
expect(block(':root[data-theme="dark"]')).toBe(system);
});