WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto

Add theming system design plan

Plan for a phpBB-inspired admin-customizable theme system with a
neobrutal default. Covers CSS custom property architecture, design
token schema, lexicon for theme storage on Forum DID, admin editor
workflow, and built-in preset themes. Scoped as post-MVP work.

https://claude.ai/code/session_01Y3xoFe9ty2gduA4KHVKeYx

Claude ec5c9012 30d024c1

+368
+368
docs/theming-plan.md
··· 1 + # Theming System — Design Plan 2 + 3 + **Status:** Future (post-MVP) 4 + **Package:** `@atbb/web` 5 + **Goal:** Admin-customizable forum themes with a neobrutal default, inspired by phpBB's theme flexibility. 6 + 7 + --- 8 + 9 + ## Design Principles 10 + 11 + 1. **Admin control without code.** Forum admins pick a theme or tweak design tokens from an admin panel — no CSS authoring needed. 12 + 2. **Theme-as-data.** Themes are serializable JSON stored as AT Proto records on the Forum DID's PDS, making them portable and versionable. 13 + 3. **Progressive enhancement.** The base HTML is semantic and readable with no styles at all (it already is). Themes layer on top via CSS custom properties. If CSS fails to load, the forum is still usable. 14 + 4. **No build step per theme.** Themes are applied at runtime through CSS custom properties and a server-rendered `<style>` block. No per-theme CSS compilation. 15 + 5. **Neobrutal default.** The built-in theme uses a neobrutal aesthetic: bold borders, solid shadows, high contrast, punchy accent colors, chunky type. 16 + 17 + --- 18 + 19 + ## Architecture 20 + 21 + ### How Themes Work 22 + 23 + ``` 24 + Forum DID PDS Web Server (Hono) 25 + ┌──────────────────┐ ┌─────────────────────────┐ 26 + │ space.atbb.forum │ │ │ 27 + │ .theme record │───fetch───▶│ BaseLayout renders: │ 28 + │ │ │ <style>:root { ... }</style>│ 29 + │ { tokens, ... } │ │ + theme class on <body>│ 30 + └──────────────────┘ │ + /static/theme.css │ 31 + └─────────────────────────┘ 32 + ``` 33 + 34 + 1. **Theme record** lives on the Forum DID's PDS as a `space.atbb.forum.theme` record. 35 + 2. **On request**, the web server reads the active theme config (cached) and injects CSS custom properties into a `<style>` block in `<head>`. 36 + 3. **A single base stylesheet** (`theme.css`) references only custom properties — never hardcoded colors or sizes. Swapping property values completely changes the look. 37 + 4. **Optional per-theme CSS overrides** can extend the base for structural changes (e.g., sidebar layout vs. top-nav), stored as a `cssOverrides` string in the theme record. 38 + 39 + ### Theme Layers 40 + 41 + ``` 42 + Layer 0: Reset / Normalize (minimal, ships with @atbb/web) 43 + Layer 1: Base component styles (theme.css — uses only custom properties) 44 + Layer 2: Design tokens (<style>:root { --color-bg: ... }) 45 + Layer 3: Per-theme CSS overrides (optional structural tweaks) 46 + ``` 47 + 48 + --- 49 + 50 + ## Design Token Schema 51 + 52 + Themes are defined as a flat set of design tokens. The token names map 1:1 to CSS custom properties. 53 + 54 + ### Color Tokens 55 + 56 + | Token | Description | Neobrutal Default | 57 + |-------|-------------|-------------------| 58 + | `color-bg` | Page background | `#f5f0e8` (warm off-white) | 59 + | `color-surface` | Card/panel background | `#ffffff` | 60 + | `color-text` | Primary text | `#1a1a1a` | 61 + | `color-text-muted` | Secondary/meta text | `#555555` | 62 + | `color-primary` | Primary accent (links, buttons) | `#ff5c00` (bold orange) | 63 + | `color-primary-hover` | Primary accent hover state | `#e04f00` | 64 + | `color-secondary` | Secondary accent | `#3a86ff` (vivid blue) | 65 + | `color-border` | Border color | `#1a1a1a` (black) | 66 + | `color-shadow` | Box-shadow color | `#1a1a1a` | 67 + | `color-success` | Success/positive | `#2ec44a` | 68 + | `color-warning` | Warning | `#ffbe0b` | 69 + | `color-danger` | Danger/destructive | `#ff006e` | 70 + | `color-code-bg` | Code block background | `#1a1a1a` | 71 + | `color-code-text` | Code block text | `#f5f0e8` | 72 + 73 + ### Typography Tokens 74 + 75 + | Token | Description | Neobrutal Default | 76 + |-------|-------------|-------------------| 77 + | `font-body` | Body font stack | `'Space Grotesk', system-ui, sans-serif` | 78 + | `font-heading` | Heading font stack | `'Space Grotesk', system-ui, sans-serif` | 79 + | `font-mono` | Monospace font stack | `'JetBrains Mono', ui-monospace, monospace` | 80 + | `font-size-base` | Base font size | `16px` | 81 + | `font-size-sm` | Small text | `14px` | 82 + | `font-size-lg` | Large text | `20px` | 83 + | `font-size-xl` | XL text (headings) | `28px` | 84 + | `font-size-2xl` | 2XL text (page titles) | `36px` | 85 + | `font-weight-normal` | Normal weight | `400` | 86 + | `font-weight-bold` | Bold weight | `700` | 87 + | `line-height-body` | Body line height | `1.6` | 88 + | `line-height-heading` | Heading line height | `1.2` | 89 + 90 + ### Spacing & Layout Tokens 91 + 92 + | Token | Description | Neobrutal Default | 93 + |-------|-------------|-------------------| 94 + | `space-xs` | Extra-small spacing | `4px` | 95 + | `space-sm` | Small spacing | `8px` | 96 + | `space-md` | Medium spacing | `16px` | 97 + | `space-lg` | Large spacing | `24px` | 98 + | `space-xl` | Extra-large spacing | `40px` | 99 + | `radius` | Border radius | `0px` (sharp corners — neobrutal) | 100 + | `border-width` | Default border width | `3px` (chunky — neobrutal) | 101 + | `shadow-offset` | Box-shadow offset | `4px` (solid offset shadows) | 102 + | `content-width` | Max content width | `960px` | 103 + 104 + ### Component-Level Tokens 105 + 106 + | Token | Description | Neobrutal Default | 107 + |-------|-------------|-------------------| 108 + | `button-radius` | Button border radius | `0px` | 109 + | `button-shadow` | Button box-shadow | `4px 4px 0 var(--color-shadow)` | 110 + | `card-radius` | Card border radius | `0px` | 111 + | `card-shadow` | Card box-shadow | `6px 6px 0 var(--color-shadow)` | 112 + | `input-radius` | Input border radius | `0px` | 113 + | `input-border` | Input border | `3px solid var(--color-border)` | 114 + | `nav-height` | Navigation bar height | `64px` | 115 + 116 + --- 117 + 118 + ## Neobrutal Default Theme — Design Direction 119 + 120 + The neobrutal aesthetic is characterized by: 121 + 122 + - **Thick black borders** on cards, buttons, and inputs (3px+) 123 + - **Solid offset box-shadows** instead of soft/blurred shadows (e.g., `4px 4px 0 #1a1a1a`) 124 + - **Sharp corners** (border-radius: 0) or very slight rounding 125 + - **High contrast** color palette — dark text on light backgrounds, bold accent colors 126 + - **Punchy accent colors** — saturated oranges, blues, pinks (not pastels) 127 + - **Chunky, confident typography** — geometric sans-serifs, generous sizing 128 + - **Flat color fills** — no gradients, no translucency 129 + - **Deliberate "roughness"** — the UI looks intentionally bold and unpolished, like a zine or poster 130 + 131 + ### Visual Reference Points 132 + 133 + - [Gumroad's redesign](https://gumroad.com) — the canonical neobrutal web product 134 + - [Figma neobrutal UI kits](https://www.figma.com/community/tag/neobrutalism) — community references 135 + - The general energy: "Web Brutalism meets a friendly color palette" 136 + 137 + ### Layout Sketch 138 + 139 + ``` 140 + ┌─────────────────────────────────────────────────────┐ 141 + │ ██ atBB Forum Name [Login] │ <- thick bottom border 142 + ├─────────────────────────────────────────────────────┤ 143 + │ │ 144 + │ ┌─────────────────────────────────────────────┐ │ 145 + │ │ 📁 Category Name 12 topics│ │ <- solid shadow card 146 + │ │ Description of the category... │ │ 147 + │ └──┬──┬───────────────────────────────────────┘ │ 148 + │ └──┘ (offset shadow) │ 149 + │ │ 150 + │ ┌─────────────────────────────────────────────┐ │ 151 + │ │ 📁 Another Category 8 topics│ │ 152 + │ │ Another description here... │ │ 153 + │ └──┬──┬───────────────────────────────────────┘ │ 154 + │ └──┘ │ 155 + │ │ 156 + │ ┌──────────────────┐ │ 157 + │ │ [+ New Topic] │ <- bold button w/ shadow │ 158 + │ └──┬──┬────────────┘ │ 159 + │ └──┘ │ 160 + │ │ 161 + ├─────────────────────────────────────────────────────┤ 162 + │ Powered by atBB on the ATmosphere │ 163 + └─────────────────────────────────────────────────────┘ 164 + ``` 165 + 166 + --- 167 + 168 + ## Lexicon: `space.atbb.forum.theme` 169 + 170 + A new record type on the Forum DID for storing theme configuration. 171 + 172 + ```yaml 173 + lexiconId: space.atbb.forum.theme 174 + key: tid # Multiple themes per forum, one marked active 175 + fields: 176 + name: string (required) # "Neobrutal Default", "Dark Mode", etc. 177 + active: boolean # Is this the currently applied theme? 178 + tokens: map<string, string> # Design token key-value pairs 179 + cssOverrides: string (optional)# Raw CSS for structural overrides 180 + fontUrls: array<string> (opt) # Google Fonts or self-hosted font URLs 181 + createdAt: datetime 182 + updatedAt: datetime 183 + ``` 184 + 185 + **Record ownership:** Forum DID (same as `forum.forum`, `forum.category`). 186 + 187 + **Why `tid` key?** Forums can have multiple saved themes (like phpBB's theme gallery). Only one has `active: true` at a time. The AppView enforces the single-active constraint. 188 + 189 + --- 190 + 191 + ## Admin Theme Editor 192 + 193 + The admin panel gets a theme management section: 194 + 195 + ### Theme List View 196 + - Shows all saved themes with preview thumbnails 197 + - Toggle active theme 198 + - Create / duplicate / delete themes 199 + 200 + ### Theme Editor View 201 + - **Live preview panel** — shows a sample forum page with current token values 202 + - **Token editor** — grouped by category (colors, typography, spacing, components) 203 + - Color tokens: color picker inputs 204 + - Typography tokens: font selector + size sliders 205 + - Spacing tokens: numeric inputs with preview 206 + - Component tokens: composite editors (shadow builder, border builder) 207 + - **CSS overrides** — code editor (CodeMirror or similar) for advanced users 208 + - **Font management** — add Google Fonts URLs or upload self-hosted fonts 209 + - **Import/Export** — download theme as JSON, upload to share between forums 210 + - **Preset gallery** — start from built-in presets (Neobrutal, Clean, Dark, Classic BB) 211 + 212 + ### Implementation Notes 213 + - The editor itself is an HTMX-driven form. Token changes POST to the server, which returns an updated `<style>` block for the preview panel via an `hx-swap`. 214 + - No client-side JS framework needed — HTMX + server rendering is sufficient for the live preview workflow. 215 + - Theme JSON import/export is just the `tokens` + `cssOverrides` + `fontUrls` fields serialized. 216 + 217 + --- 218 + 219 + ## Built-in Preset Themes 220 + 221 + Ship with a small set of presets that admins can use as starting points: 222 + 223 + | Preset | Description | 224 + |--------|-------------| 225 + | **Neobrutal** (default) | Bold borders, solid shadows, warm palette, sharp corners | 226 + | **Clean** | Minimal, airy, subtle shadows, rounded corners, neutral palette | 227 + | **Dark** | Dark backgrounds, muted borders, cool accent colors | 228 + | **Classic BB** | Nostalgic phpBB/vBulletin feel — blue headers, gray panels, small type | 229 + 230 + Each preset is a complete set of token values. Admins pick one, then customize from there. 231 + 232 + --- 233 + 234 + ## CSS Architecture 235 + 236 + ### File Structure (future) 237 + 238 + ``` 239 + packages/web/src/ 240 + styles/ 241 + reset.css # Minimal normalize/reset 242 + theme.css # All component styles using var(--token) references 243 + presets/ 244 + neobrutal.json # Token values for neobrutal preset 245 + clean.json # Token values for clean preset 246 + dark.json # Token values for dark preset 247 + classic.json # Token values for classic BB preset 248 + ``` 249 + 250 + ### Base Stylesheet Approach 251 + 252 + `theme.css` is written once and never changes per-theme. It references custom properties exclusively: 253 + 254 + ```css 255 + /* Example — not final */ 256 + body { 257 + font-family: var(--font-body); 258 + font-size: var(--font-size-base); 259 + line-height: var(--line-height-body); 260 + color: var(--color-text); 261 + background: var(--color-bg); 262 + } 263 + 264 + .card { 265 + background: var(--color-surface); 266 + border: var(--border-width) solid var(--color-border); 267 + border-radius: var(--card-radius); 268 + box-shadow: var(--card-shadow); 269 + padding: var(--space-md); 270 + } 271 + 272 + .btn-primary { 273 + background: var(--color-primary); 274 + color: var(--color-surface); 275 + border: var(--border-width) solid var(--color-border); 276 + border-radius: var(--button-radius); 277 + box-shadow: var(--button-shadow); 278 + font-weight: var(--font-weight-bold); 279 + padding: var(--space-sm) var(--space-md); 280 + } 281 + 282 + .btn-primary:hover { 283 + background: var(--color-primary-hover); 284 + transform: translate(2px, 2px); 285 + box-shadow: 2px 2px 0 var(--color-shadow); 286 + } 287 + ``` 288 + 289 + ### Server-Side Token Injection 290 + 291 + `BaseLayout` renders the active theme's tokens as a `<style>` block: 292 + 293 + ```tsx 294 + // Pseudocode — future implementation 295 + const BaseLayout: FC<PropsWithChildren<LayoutProps>> = (props) => ( 296 + <html lang="en"> 297 + <head> 298 + <title>{props.title ?? "atBB Forum"}</title> 299 + <style>{`:root { ${tokensToCss(props.theme.tokens)} }`}</style> 300 + <link rel="stylesheet" href="/static/reset.css" /> 301 + <link rel="stylesheet" href="/static/theme.css" /> 302 + {props.theme.fontUrls?.map(url => ( 303 + <link rel="stylesheet" href={url} /> 304 + ))} 305 + {props.theme.cssOverrides && ( 306 + <style>{props.theme.cssOverrides}</style> 307 + )} 308 + <script src="https://unpkg.com/htmx.org@2.0.4" /> 309 + </head> 310 + <body> 311 + {props.children} 312 + </body> 313 + </html> 314 + ); 315 + ``` 316 + 317 + --- 318 + 319 + ## Multi-Theme / User Preference (Stretch) 320 + 321 + Beyond admin-set themes, a stretch goal is letting users pick from the forum's saved themes: 322 + 323 + - Store preference in a cookie or as part of the user's `membership` record 324 + - Server reads preference and injects that theme's tokens instead of the forum default 325 + - Useful for light/dark mode toggle at minimum 326 + 327 + This is secondary to the admin theme system and can come later. 328 + 329 + --- 330 + 331 + ## Implementation Phases 332 + 333 + This work is post-MVP. Suggested ordering: 334 + 335 + ### Theme Phase 1: Foundation 336 + - Add `reset.css` and `theme.css` with custom property references 337 + - Hardcode neobrutal tokens in `BaseLayout` as the default 338 + - Style all existing views (homepage, category, topic, compose, admin) 339 + - No admin editor yet — just ship a good-looking default 340 + 341 + ### Theme Phase 2: Token System 342 + - Define `space.atbb.forum.theme` lexicon 343 + - Build `tokensToCss()` utility 344 + - Load active theme from Forum DID's PDS (with caching) 345 + - Ship built-in preset JSON files 346 + - Inject tokens dynamically in `BaseLayout` 347 + 348 + ### Theme Phase 3: Admin Editor 349 + - Theme list management UI 350 + - Token editor with grouped controls 351 + - Live preview via HTMX 352 + - Import/export functionality 353 + 354 + ### Theme Phase 4: Polish 355 + - User theme preference (light/dark toggle) 356 + - CSS override editor for advanced admins 357 + - Theme sharing between forums 358 + - Preset gallery expansion 359 + 360 + --- 361 + 362 + ## Open Questions 363 + 364 + 1. **Font loading strategy.** Google Fonts is easy but has privacy implications for self-hosters. Should we bundle a few default fonts, allow self-hosted uploads, or both? 365 + 2. **CSS override sandboxing.** Raw CSS in `cssOverrides` could break layouts or introduce XSS (via `url()`, `expression()`, etc.). Need a sanitization strategy — maybe a CSS parser that strips dangerous properties. 366 + 3. **Theme record size limits.** AT Proto records have size limits. If `cssOverrides` gets large, might need to store it as a blob reference instead of inline. 367 + 4. **Static asset serving.** Currently no static file serving in the web package. Need to add a `/static/` route (Hono has `serveStatic` middleware) before any CSS files can be loaded. 368 + 5. **Build-time vs. runtime tokens.** The plan above is fully runtime (no CSS build per theme). This is simpler but means we can't use tools like Tailwind for the base styles. Is that acceptable?