A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

Add design spec for publication theme presets (sky phases)

+154
+154
docs/superpowers/specs/2026-06-09-publication-themes-design.md
··· 1 + # Publication theme presets ("sky phases") 2 + 3 + **Date:** 2026-06-09 4 + **Status:** Approved (brainstorm) — ready for plan 5 + **Area:** publication system (SP10), reader/renderer (SP4/SP7), brand (SP6) 6 + 7 + ## Summary 8 + 9 + Let a publisher pick a colour theme for their publication from a small set of 10 + curated **sky-phase** presets. The chosen palette is stored on the publication 11 + record using the standard.site `basicTheme` field, surfaces on publication 12 + creation and in publication settings, and is applied when the publication and 13 + its articles are rendered publicly. Default is **no theme** (today's SkyPress 14 + look, unchanged). 15 + 16 + ## Motivation 17 + 18 + standard.site's `site.standard.publication` lexicon defines an optional 19 + `basicTheme` (a `site.standard.theme.basic` object) so a publication can carry a 20 + visual identity that any standard.site reader can honour. SkyPress does not use 21 + it yet. Adding it lets publishers personalise their space, and — because we 22 + store the *standard* field rather than a SkyPress-private one — other readers 23 + (Leaflet, etc.) honour the same colours. 24 + 25 + The Twenty Twenty-Five theme's colour style variations are named after times of 26 + day (evening, noon, dusk, afternoon, twilight, morning, sunrise, midnight). That 27 + is a direct fit for SkyPress's "open sky" brand and the existing `data-phase` 28 + attribute on `<html>`, so the presets ship as **sky phases**. 29 + 30 + ## Data model 31 + 32 + ### `site.standard.theme.basic` (embedded object) 33 + 34 + Four required RGB colours (each `{ r, g, b }`, integers 0–255; alpha not used): 35 + 36 + | field | meaning | 37 + |---|---| 38 + | `background` | content background | 39 + | `foreground` | content text | 40 + | `accent` | links + button backgrounds | 41 + | `accentForeground` | button text | 42 + 43 + Represented in TypeScript as: 44 + 45 + ```ts 46 + interface Rgb { r: number; g: number; b: number; } 47 + interface BasicTheme { 48 + $type: 'site.standard.theme.basic'; 49 + background: Rgb; 50 + foreground: Rgb; 51 + accent: Rgb; 52 + accentForeground: Rgb; 53 + } 54 + ``` 55 + 56 + ### Changes to existing types (additions only — lexicon discipline) 57 + 58 + - `PublicationRecord` (`src/lib/publish/records.ts`): add optional `basicTheme?: BasicTheme`. 59 + - `buildPublicationRecord`: accept + conditionally include `basicTheme`. 60 + - `PublicationInput` / `Publication` (`src/lib/publish/publications.ts`): add optional `basicTheme`. 61 + - `ReaderPublication` (`src/lib/reader/publications.ts`): surface `basicTheme`. 62 + 63 + No field is ever removed or made required. `basicTheme` absent ⇒ default look. 64 + 65 + ## Presets — `src/lib/publish/themes.ts` (pure, tested) 66 + 67 + A new dependency-free module exporting the 8 sky-phase presets derived from the 68 + Twenty Twenty-Five colour palettes: 69 + 70 + ```ts 71 + interface ThemePreset { slug: string; label: string; colors: BasicTheme; } 72 + export const THEME_PRESETS: readonly ThemePreset[]; 73 + ``` 74 + 75 + Mapping from each TT5 palette: `background ← base`, `foreground ← contrast`, and 76 + an `accent` / `accentForeground` pair chosen so button/link text clears 77 + **WCAG AA (contrast ≥ 4.5:1)**. The accent is the palette's most vivid usable 78 + accent; `accentForeground` is whichever of the palette's light/dark colours 79 + passes against it. 80 + 81 + Phases (TT5 source): Evening, Noon, Dusk, Afternoon, Twilight, Morning, 82 + Sunrise, Midnight. 83 + 84 + Helpers: 85 + - `findPresetByColors(theme): ThemePreset | null` — reverse-match stored colours 86 + to a preset so the settings UI can highlight the current selection (returns 87 + `null` for none/custom). 88 + - `themeToCssVars(theme): Record<string, string>` — map a `BasicTheme` to the 89 + SkyPress design tokens it overrides. Pure, unit-tested. 90 + 91 + ## UI — preset picker in `PublicationForm` 92 + 93 + A swatch-grid radio group added to `PublicationForm.tsx`, so it appears 94 + identically on **creation** and in the **Settings tab** (which already reuses 95 + `PublicationForm`). Each option is a small preview card showing the preset's 96 + background/text/accent, plus a "No theme" option (default, selected when the 97 + publication has no `basicTheme`). 98 + 99 + **Hand-rolled React + plain CSS — not `@wordpress/components`.** AGENTS.md 100 + constraint #3 forbids `@wordpress/*` outside the editor/Studio island; the 101 + dashboard is an explicit no-`@wordpress` zone (importing it risks the duplicate 102 + `@wordpress/data` registry crash from Decision 0003). The picker matches the 103 + existing `PublicationForm` plain-React/CSS style. 104 + 105 + Accessibility: a labelled radio group (`role="radiogroup"`), each preset an 106 + `<input type="radio">` with a visible label, keyboard-navigable, with a visible 107 + focus/selected state. 108 + 109 + ## Public rendering 110 + 111 + In the publication-home (`[author]/[slug]/index.astro`) and article 112 + (`[author]/[slug]/[rkey].astro`) pages, when the resolved publication has a 113 + `basicTheme`, inject a small `<style>` into the existing `<Fragment slot="head">` 114 + that overrides the design tokens on `:root` via `themeToCssVars` (e.g. 115 + `--paper`←background, `--ink`←foreground, `--sun`/`--btn-primary`←accent, 116 + button text←accentForeground). 117 + 118 + A publisher's palette is a **fixed identity**, so it intentionally overrides 119 + both the light and dark `prefers-color-scheme` defaults (standard.site's 120 + "consistent across platforms" intent). When there is no `basicTheme`, nothing is 121 + injected and the existing light/dark behaviour is untouched. 122 + 123 + The injected values come from a trusted, app-controlled preset table keyed by 124 + the stored colours — but since the record is PDS-sourced, the renderer validates 125 + each channel is an integer 0–255 and emits only `rgb(...)` strings (never raw 126 + record text) to avoid CSS injection. 127 + 128 + ## Tests (TDD, failing-test-first) 129 + 130 + - `themes.test.ts`: all 8 presets present; each accent/accentForeground pair 131 + clears WCAG AA; `findPresetByColors` round-trips; `themeToCssVars` output. 132 + - `records.test.ts`: `buildPublicationRecord` includes `basicTheme` when given, 133 + omits when not; RGB shape preserved. 134 + - `publications.test.ts`: create + update round-trip `basicTheme`. 135 + - `reader/publications.test.ts`: `basicTheme` surfaces (and malformed/absent → 136 + undefined). 137 + - Renderer mapping covered by `themeToCssVars` test + channel validation test. 138 + - Picker: radio-group semantics / selection state. 139 + 140 + ## Docs & decisions 141 + 142 + - New decision doc `docs/decisions/0012-publication-theme-presets.md`: why the 143 + standard `basicTheme` field, why hand-rolled UI (not `@wordpress`), and the 144 + light/dark override semantics. 145 + - Update `lexicons/README.md` — add `basicTheme` to the 146 + `site.standard.publication` fields table. 147 + 148 + ## Out of scope (YAGNI) 149 + 150 + - Custom colour pickers / arbitrary palettes (presets only). 151 + - Theming the authenticated dashboard/editor chrome (public reader only). 152 + - Fonts, layout/spacing, or per-article overrides. 153 + - `labels` / `preferences` publication fields. 154 + ```