Skip to content

Theme Creator — Free-Form Supporter Color Customization

Status: Shipped — July 2026 (PR #1796) Author: Penny (proposer agent) Date: 2026-07-25 Roadmap ref: none — single bundled phase, no milestone (Josh confirmed) PRD file: docs/product/completed/supporter-theme-creator-prd.md


1. Problem

The Supporter tier's flagship perk ("custom app themes," docs/product/pricing-philosophy.md) shipped as 9 fixed, hand-authored color palettes (ionic_frontend/src/config/themes.ts, ionic_frontend/src/theme/supporter-themes.css). Supporters chose from a curated list; they couldn't express their own color preference. docs/features/supporter.md recorded this as a known, unscoped gap ("Theme creator — Let Supporters customize primary/accent colors freely," Priority: Low).

Objectuve's competitive analysis rated Objectuve "Moderate" on Customization & Flexibility versus Habitica's "Advanced" (docs/product/competitive/feature-matrix.md §11), attributable in part to fixed-palette-only theming versus deeper cosmetic personalization elsewhere in the category.

2. Goal

Let Supporters pick their own primary and accent colors, while guaranteeing every resulting combination stays WCAG AA-compliant and visually coherent with the rest of the design system.

3. Non-goals

  • Not a redesign of the base design token system (design_system/) — extends the existing Supporter-only override layer (html[data-theme="x"]), doesn't replace it.
  • Not a functional/free-tier feature — stays Supporter-gated per docs/product/pricing-philosophy.md.
  • Not full per-component theming (fonts, spacing, iconography) — color only.
  • Not a marketplace or sharing mechanism.

4. What shipped

  1. Picker UX — constrained sliders (Primary hue, Accent hue, Depth/saturation), not raw hex — ThemeCreator.vue. Lightness is never user-controlled; it's always seeded and left to the AA clamp.
  2. Contrast-safe generationderiveCustomTheme (ionic_frontend/src/lib/customTheme.ts) derives the full ~15-variable token set from a picked primary/accent hue, clamping lightness until every WCAG AA condition holds (4.5:1 text, 3:1 large-text/UI) in both light and dark. A pick that would fail AA is auto-adjusted with visible live feedback ("Nudged for readability."), never silently accepted or hard-rejected.
  3. Persistence — localStorage-only in this phase (payload shape — hues, not a slug — designed so a future backend field can carry it verbatim for cross-device sync, coordinated with the OBJ-1757 preset-sync work). Cross-device sync of the actual custom payload remains a follow-up, tracked separately.
  4. Live preview — reuses ThemePreviewTile.vue, always rendering the clamped result.
  5. Reset — one-tap return to the Supporter's last-selected preset, persisted so it survives a reload.

Full architecture, file map, and test coverage: docs/features/supporter.md — Theme Creator.

5. Decisions locked during planning (Orion)

  • Constrained hue-wheel-style sliders, not freeform hex.
  • A 10th "Custom" tile appended to the existing preset grid (kept all 9 presets); grid is 11 tiles total including the free "Legacy Blue" default.
  • No interaction with the unrelated Critical Path mini-game theme system (stimThemes.ts) — confirmed fully separate.
  • Theme-selection analytics deferred (none exist today for presets or custom).

6. Success metrics (not yet instrumented)

  • % of Supporters who create a custom theme within 30 days of shipping
  • Supporter tier conversion rate delta (one of several concurrent variables, won't isolate cleanly)
  • Zero WCAG AA contrast regressions shipped (CI-gated via tests/unit/tokenDrift.spec.ts + tests/unit/lib/customTheme.test.ts)

No adoption analytics are wired up yet — this PRD's own scoping deferred instrumentation to a follow-up.

7. Shipping notes

PR #1790 (the original implementation) merged to master and was emergency-reverted 8 minutes later (PR #1795) after it accidentally bundled in the entire unmerged gsd/v4.14-activity-nexus-connected-apps branch, breaking the staging deploy pipeline. The Theme Creator work itself was unaffected — it was re-cut cleanly off master and shipped via PR #1796. See CHANGELOG.md's ci: PR-scope guard against cross-branch contamination entry for the incident writeup and the new CI guard that now catches this class of bundling.


Last updated: 2026-07-25

Loading…