feat(phase 4b): dark/light theme toggle + file-upload permanently out-of-scope

## Slice A — explicit dark/light toggle

projax now ships with two palettes and a 1y cookie to remember the choice.
Dark is the new default; ☀ button in the header nav flips to light and
writes projax_theme=light. Server reads the cookie via themeFromRequest(r)
and injects Theme + ThemeColor into every template via the centralised
render(w, r, …) path, so first paint never flashes the wrong theme. Inline
JS in layout.tmpl handles the toggle without a server roundtrip.

Every panel colour now lives in a CSS variable under
:root[data-theme=dark|light]; the only hardcoded hex values left are
inside those two :root blocks. A future palette tweak is one edit, not
30 selectors. Graph node colours, kind-badges, highlights and warn/ok/bad
all have parallel dark/light values picked for contrast.

Standalone SVG download bakes the light palette inline because the
downloaded asset has no parent :root providing vars — m's existing
snapshots stay print-friendly regardless of his current cookie.

Login page keeps its embedded dark CSS — it's the gateway, intentionally
always dark.

Tests: TestThemeDefaultIsDark, TestThemeCookieRoundTrips,
TestThemeCookieUnknownFallsBackToDark, TestThemeTogglePagesShareSameTheme,
TestThemeToggleScriptPresent, TestThemeColorMetaHelper. Full suite green.

## Slice B — file-upload permanently out of scope (m, 2026-05-17)

docs/design.md moves "File uploads / in-projax storage" from the §3c
parked list to a permanent "Out of scope (decided 2026-05-17)" clause
with the rationale: PER is the cross-reference index, not the file
system. docs/standards/per.md gains the same explicit clause so future
shifts working from the PER standard see the constraint where they
look. Memory note filed so future workers don't re-propose multipart
uploads, attachments tables, or documents buckets.

## docs/design.md §13 Theming

Documents the toggle approach, cookie semantics, palette table, the
standalone-SVG carve-out, the login-page exception, and the 4b
out-of-scope (prefers-color-scheme detection, per-page overrides,
transitions on swap).
This commit is contained in:
mAi
2026-05-17 18:14:08 +02:00
parent 69b5bfd7a0
commit 5dcacff520
17 changed files with 461 additions and 77 deletions

View File

@@ -357,10 +357,13 @@ The PER standard (`docs/standards/per.md`) needs a `(item, event_date)` pair as
Out of scope (parked):
- File uploads / in-projax storage. v1 references only.
- Recurring dated artifacts (RRULE-style). Flatten for now.
- Cross-PER linking syntax / forward-jump anchors. Phase 3d+ if m needs it.
Out of scope (permanent — decided 2026-05-17):
- **File uploads / in-projax file storage.** projax `item_links` are *references*, not files. The PER (`{path}.{YYMMDD}`) names where a document lives elsewhere — mFin/, the filesystem, a URL — and the source-of-truth stays there. Do not propose adding multipart uploads, a documents bucket, or an attachments table; m killed that on 2026-05-17. The "documents" surface is intentionally a list of `(ref_type, ref_id, event_date, note)` rows and nothing more.
## 8. Open questions (post-PRD)
- **Path-trigger correctness** under cycle attempts: enforce acyclicity via check in trigger.
@@ -530,6 +533,46 @@ Items without a date never appear here — the tree/graph/dashboard cover the re
- Per-row inline edit of VEVENT (use the source calendar app — the 3l read-only stance still holds for v1).
- Gitea issue created/closed activity (deferred until m asks; dashboard already covers it).
## 13. Theming (Phase 4b)
projax ships with an explicit dark / light toggle and a 1-year cookie that remembers the choice. Default is dark — m asked for dark-by-default; the toggle exists so light mode is one click away when the ambient lighting calls for it.
**Toggle approach**:
- `<html data-theme="dark">` (default) or `<html data-theme="light">`. CSS palettes live under `:root, :root[data-theme="dark"] { … }` and `:root[data-theme="light"] { … }` in `web/static/style.css`. Every panel colour is a `var(--foo)` — the only hardcoded hex values left in the codebase are inside those two `:root` blocks. A future palette tweak edits the variables, not 30 selectors.
- The header nav grows a small `☀` / `☾` button (sun glyph in dark mode = "switch to light", moon glyph in light mode = "switch to dark"). Inline JS in `layout.tmpl` flips `data-theme` + the apple `<meta name="theme-color">` and writes the cookie via `document.cookie =` — no server roundtrip on toggle. The cookie is `projax_theme=dark|light`, `Max-Age=31536000`, `Path=/`, `SameSite=Lax`, NOT HttpOnly (the toggle JS reads it).
- Layout reads the cookie server-side via `themeFromRequest(r)` and emits `data-theme` + `<meta name="theme-color">` at first paint — no flash-of-wrong-theme before the script runs. The render helper injects `Theme` + `ThemeColor` into every template's data map before execution, so individual handlers don't need to know about the theme.
**Palette structure**:
| Variable | Dark | Light | Used for |
|---|---|---|---|
| `--fg` | `#e6e6e0` | `#1a1a1a` | primary text |
| `--muted` | `#8a8880` | `#6a6a6a` | secondary text, slugs |
| `--bg` | `#0e0e0e` | `#fafafa` | page background |
| `--bg-alt` | `#1a1a1a` | `#f0efe8` | nav bar, code blocks, tag pills |
| `--surface` | `#161616` | `#ffffff` | card backgrounds, form inputs |
| `--surface-hover` | `#1f1f1f` | `#f7f7f7` | row hover |
| `--border` | `#2c2c2c` | `#d8d4c8` | hairline borders |
| `--accent` | `#6fa7e8` | `#2f5d9e` | links, primary buttons |
| `--accent-fg` | `#0a0a0a` | `#ffffff` | text on accent backgrounds |
| `--warn` / `--ok` / `--bad` | brighter pastels | original earthy | status colours |
| `--highlight*` | warm dark | cream | PER highlight, banner warn, overdue row |
| `--kind-*-bg` / `--kind-*-fg` | dark muted | original pastels | timeline kind-badges |
| `--graph-*` | brighter | original | graph node strokes & legend |
**Theme-color meta** flips with the page theme: `#161616` (dark — matches `--surface`, the nav bar) or `#f0efe8` (light — matches `--bg-alt`). Apple Safari uses this to tint the iOS status bar in the installed PWA.
**Standalone SVG download** (`/graph?download=svg`) bakes the LIGHT palette inline because the downloaded asset has no parent `:root` providing variables, and m's existing snapshots (presentations, mBrian) expect the original print-friendly look. The `Standalone` flag on `graphPayload` flips a `{{if .Standalone}}` block in `graph_svg.tmpl` to inject the inline `:root` declarations only on the download path.
**Login page** keeps its embedded dark CSS — it's the gateway and is intentionally always dark (mirrors the lockscreen feel). The theme toggle is hidden from users until they're signed in; switching the login template's palette would only matter for a never-signed-in user.
**Out of scope for 4b**:
- `prefers-color-scheme` auto-detect (could add: read it on first visit if no cookie). v1 is manual.
- Per-page theme overrides — one global theme is enough.
- CSS transitions on the swap. The flip is instant; that's intentional.
## 9. Phase-1 deliverable checklist
- [ ] `projax.items` + `projax.item_links` migrations in `db/migrations/`

View File

@@ -111,6 +111,10 @@ Examples:
- **Time-of-day suffixes** — date granularity is enough. If m needs to distinguish "morning meeting" vs "evening call" on the same project on the same day, that's an internal metadata concern, not a PER concern.
- **Cross-PER references / linking syntax** — Phase 2+.
## Out of scope (permanent — decided 2026-05-17)
- **In-projax file storage / uploads.** A PER *names* a document; it does not contain one. `ref_id` is a pointer (URL, file path, Drive link, mFin location, …) to where the file lives elsewhere. projax never grows a multipart-upload endpoint, an attachments table, or a documents bucket. m killed that direction explicitly on 2026-05-17; do not re-propose it. The source of truth for the bits stays wherever m already keeps it — projax is the cross-reference index, not the file system.
## Versioning
PER is a forward-compatible additive format. A v0.2 may add suffixes or qualifiers; v0.1 PERs MUST continue to resolve under any future spec. No backwards-incompatible changes without bumping the major version.