From 5bfe5086429e7527c4e17ee1424d07be54446867 Mon Sep 17 00:00:00 2001 From: ChrisBeWithYou Date: Mon, 29 Jun 2026 16:50:36 -0500 Subject: [PATCH] docs: add plugin-author theming migration guide + scope the reconciliation rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/plugin-theming.md — the how-to for plugin authors: the golden rule (reference roles + recipe slots, never hardcode a device), what the host provides (--fbv-* role tokens incl. on-accent/focus-ring + window.feedBack.theme), and two paths — Path A (no own skin → derive surfaces from host + pick devices from capabilities) and Path B (own deliberate skins → own surfaces, adopt only the per-skin device pattern), plus accessibility + the pre-merge matrix check. Workstream item 4 of #644. - Refines the contract's reconciliation rule: "derive surfaces from host" applies to plugins WITHOUT their own identity; plugins WITH deliberate skins (note_detect) own their surfaces (deriving would erase metal's steel) and adopt only the role/recipe pattern. Records the 2026-06-29 decision to keep note_detect's skins self-owned + their current button text — so item 2 is the per-skin pattern + the verification gate, not surface-derivation. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF --- docs/host-theme-contract.md | 19 ++++++-- docs/plugin-theming.md | 95 +++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 docs/plugin-theming.md diff --git a/docs/host-theme-contract.md b/docs/host-theme-contract.md index 2b66652..3a9d4be 100644 --- a/docs/host-theme-contract.md +++ b/docs/host-theme-contract.md @@ -94,11 +94,20 @@ On the existing `window.feedBack` bus: - `theme:changed` event → `{ id, tokens, capabilities }` (emitted at theme-core's existing apply chokepoint; analogous to `note_detect`'s `notedetect:skin`) -**Reconciliation rule (ends the two-disconnected-systems problem):** a plugin skin -**derives surface/text/border from host tokens** (`--nd-bg: var(--fb-card)`, etc.) and -**owns only its accent + its devices**, selecting the device via the recipe. A host theme then -pulls plugin chrome along (one truth for surfaces), while the plugin layers identity on top and -never imposes a device the active theme neutralizes. +**Reconciliation rule (ends the two-disconnected-systems problem) — depends on whether the +plugin has its own identity:** + +- **A plugin *without* its own skin** should **derive surface/text/border from host tokens** + (`background: var(--fbv-card)`, etc.) and **own only its accent + its devices**, selecting the + device via the recipe. A host theme then pulls its chrome along — one truth for surfaces — and + it never imposes a device the active theme neutralizes. This is the common case. +- **A plugin *with* deliberate skins** (e.g. `note_detect`'s neon / esports / metal, which are + full design languages) **owns its surfaces** — deriving them from the host theme would *erase* + the skin's identity (metal's brushed steel becomes a flat host colour). Such a plugin adopts the + **role + recipe *pattern*** (per-skin device tokens, `on-accent`, `focus-ring`, "off" is legal) + and verifies across **its own** skin matrix, but does not blindly inherit host surfaces. + *(Decided 2026-06-29: keep note_detect's skins self-owned + their current button text — so the + fix is the per-skin device pattern + the verification gate, not surface-derivation.)* ## 5. Consumption pattern (the rule for feature authors) diff --git a/docs/plugin-theming.md b/docs/plugin-theming.md new file mode 100644 index 0000000..1ff9a5d --- /dev/null +++ b/docs/plugin-theming.md @@ -0,0 +1,95 @@ +# Theming a plugin so it works in every theme + +This is the how-to for plugin authors. It exists because of one recurring bug: +a plugin's UI is built and eyeballed against the **default** look, ships, and +then a user switches themes and the feature's *colours adapt but its visual +devices vanish* — a glow ring, a colour gradient — because the active theme +doesn't speak that visual language. + +**The golden rule:** themes are different **design languages**, not palettes. +Reference **roles** and **recipe slots**; never hardcode a *device* (a literal +`box-shadow` glow, a literal `linear-gradient`, a hex colour). Then your feature +renders correctly in a theme nobody has invented yet — and **verify it across +the whole theme set before you merge.** + +(Design + rationale: [host-theme-contract.md](host-theme-contract.md), got-feedback/feedBack#644.) + +## What the host gives you + +Always-present CSS role tokens on `:root` (resolve themed *or* un-themed): +`--fbv-bg / sidebar / card / cardMuted / primary / primaryHi / accent / text / +textDim / border / good / mid / low / gold`, plus **`--fbv-on-accent`** (a +foreground legible *on* an accent fill) and **`--fbv-focus-ring`**. Use as +`color: rgb(var(--fbv-text))`, `background: rgb(var(--fbv-card))`, etc. + +A JS read surface on the event bus: + +```js +const t = window.feedBack?.theme; +t?.get(); // { id, isThemed, tokens } +t?.capabilities(); // { glow, gradients, motion } — what devices this theme permits +t?.prefersReducedMotion(); // boolean (one central matchMedia) +window.feedBack.on('theme:changed', (e) => { /* e.detail = {id, tokens, capabilities} */ }); +``` + +Feature-detect everything (`window.feedBack?.theme?.get`) and pass a fallback to +every `var(--fbv-x, )` so you degrade cleanly on an older host. + +## Pick your path + +### Path A — you do NOT have your own skins (most plugins) + +Look like the host. **Derive surfaces from host tokens** and **choose devices +from capabilities** instead of hardcoding them: + +```js +const caps = window.feedBack?.theme?.capabilities?.() ?? { glow: true, motion: true }; +heroEl.classList.toggle('use-glow', caps.glow && !window.feedBack.theme.prefersReducedMotion()); +``` +```css +.hero { background: rgb(var(--fbv-primary)); color: rgb(var(--fbv-on-accent, #fff)); } +.hero:focus-visible { outline: 2px solid rgb(var(--fbv-focus-ring)); outline-offset: 2px; } +.hero.use-glow { box-shadow: 0 0 16px -2px rgb(var(--fbv-primary)); } /* only where the theme allows it */ +``` +Re-read on `theme:changed` if you cache anything (e.g. a canvas palette). + +### Path B — you HAVE your own deliberate skins (like the scoring UI) + +Your skins are an identity (neon vs steel vs clean) — **own your surfaces**; +don't inherit host surfaces or you'll erase that identity. Adopt only the +**pattern**: make every visual *device* a **per-skin token whose "off" value is +legal**, so each skin authors its own version and a glow-less skin simply +doesn't glow. Example (the "make the hero special" device): + +```css +/* base / neon */ :root { --hero-ring: .5; --hero-border: transparent; --acc-fill: linear-gradient(135deg, var(--accent), var(--accent2)); } +/* glow-less skin */ [data-skin="clean"] { --hero-ring: 0; --hero-border: var(--accent); --acc-fill: var(--accent); } +.hero { background: var(--acc-fill); border-color: var(--hero-border); } +.hero::after { opacity: var(--hero-ring); /* the glow ring; 0 = off */ } +``` +Neon emphasises with the ring, the clean skin with a solid border — **neither is +empty of emphasis; each speaks its own language.** Same idea for an accent-filled +number (`--acc-fill` = a gradient in one skin, a solid in another so it doesn't +wash out to white). Add `on-accent` + `focus-ring` per skin too. + +## Accessibility (part of the contract, not an afterthought) + +- **Reduced motion:** gate decorative animation on `prefers-reduced-motion` + (CSS `@media`, or `theme.prefersReducedMotion()`); functional transitions are fine. +- **Focus:** always a visible `:focus-visible` outline using `focus-ring` — don't + bind focus only to `accent` (it can ≈ the surface in some themes). +- **On-accent contrast:** text on an accent fill uses `on-accent`; watch light + accents (a mid-amber needs dark text, not white). + +## Before you merge — verify across the matrix + +Render your UI in **every** theme/skin and look at them together. If you ship +your own skins, do this with a render-matrix in CI/local (reference: the scoring +UI's `npm run render-skins` + `theme-matrix-checklist.md`). The checklist that +catches this bug class: + +- [ ] colours via tokens, no hardcoded hexes +- [ ] rendered in **all** themes/skins (not just the default) +- [ ] every new *device* is per-skin / capability-gated and **legible when its token is `none`** +- [ ] reduced-motion + visible focus in every theme +- [ ] text on accent stays legible everywhere