Merge branch 'fix/darkreader'
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
9ae2ba3241
7 files changed
+160
-2
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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); }
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user