208 lines
7.1 KiB
Markdown
208 lines
7.1 KiB
Markdown
# Website UI Theming Concept
|
||
|
||
This document defines the *conceptual* theming architecture for the Website UI layer. It is written to make the UI maintainable, themeable, and **decoupled from Tailwind** (Tailwind may exist as an implementation detail, never as the source of truth).
|
||
|
||
For the existing visual direction and palette intent, see [`docs/THEME.md`](docs/THEME.md).
|
||
|
||
---
|
||
|
||
## Problem
|
||
|
||
The current UI style surface mixes:
|
||
|
||
- semantic intent (primary, surface, outline)
|
||
- concrete implementation (Tailwind class strings)
|
||
- and multiple naming systems (Tailwind tokens, ad-hoc names, CSS variables)
|
||
|
||
This makes it hard to:
|
||
|
||
- change colors consistently,
|
||
- introduce additional themes,
|
||
- reason about component contracts,
|
||
- and enforce architectural boundaries in [`apps/website/ui`](apps/website/ui).
|
||
|
||
---
|
||
|
||
## Goals
|
||
|
||
1. **Single source of truth** for theme tokens (colors, typography, radii, shadows).
|
||
2. **Semantic tokens** (intent-based), not implementation tokens (utility-class-based).
|
||
3. **Runtime theme selection** via CSS variables (SSR-correct by default).
|
||
4. UI components remain **pure** and follow [`docs/architecture/website/COMPONENT_ARCHITECTURE.md`](docs/architecture/website/COMPONENT_ARCHITECTURE.md).
|
||
5. Tailwind is optional and must remain a **consumer/adapter**, not the authoring surface.
|
||
|
||
Non-goal for this iteration: shipping a user-facing theme switcher. We implement a single default theme but design the architecture to add more later.
|
||
|
||
---
|
||
|
||
## Core idea: Theme contract + CSS variables boundary
|
||
|
||
### 1) Theme contract (TypeScript)
|
||
|
||
We define a typed theme contract, e.g. [`Theme`](apps/website/ui/theme/Theme.ts:1), that expresses the semantic palette and style tokens.
|
||
|
||
Principle:
|
||
|
||
- UI components accept **semantic props** (variant, intent, emphasis).
|
||
- UI components **do not accept** Tailwind token strings like `bg-panel-gray`.
|
||
|
||
Example shape (conceptual):
|
||
|
||
- [`Theme`](apps/website/ui/theme/Theme.ts:1)
|
||
- `color`
|
||
- `bg.base`
|
||
- `bg.surface`
|
||
- `bg.surfaceMuted`
|
||
- `border.default`
|
||
- `text.high`
|
||
- `text.med`
|
||
- `text.low`
|
||
- `intent.primary`
|
||
- `intent.warning`
|
||
- `intent.success`
|
||
- `intent.critical`
|
||
- `radius`
|
||
- `sm` `md` `lg` `xl`
|
||
- `shadow`
|
||
- `sm` `md` `lg` `focus`
|
||
- `font`
|
||
- `body` `mono` `heading`
|
||
|
||
The *exact* token taxonomy can evolve, but the rule is stable: tokens represent meaning, not implementation.
|
||
|
||
### 2) CSS variables as the runtime boundary
|
||
|
||
The theme is applied by setting CSS custom properties. UI components reference variables, not Tailwind classes.
|
||
|
||
The canonical variables are defined in a dedicated stylesheet, e.g. [`docs/architecture/website/UI_THEMING.md`](docs/architecture/website/UI_THEMING.md) describes this as: `--ui-color-bg-base`, `--ui-color-text-high`, etc.
|
||
|
||
Implementation typically lives in [`apps/website/ui/theme/theme.css`](apps/website/ui/theme/theme.css) and is imported by [`apps/website/app/globals.css`](apps/website/app/globals.css:1).
|
||
|
||
The existing variables in [`apps/website/app/globals.css`](apps/website/app/globals.css:1) are already a strong start. The key change is to treat them as the *UI contract* and stop treating Tailwind names as the contract.
|
||
|
||
---
|
||
|
||
## Where theme lives (layering)
|
||
|
||
This respects the UI purity rules in [`docs/architecture/website/COMPONENT_ARCHITECTURE.md`](docs/architecture/website/COMPONENT_ARCHITECTURE.md).
|
||
|
||
### Authoritative token source
|
||
|
||
- `CSS variables` are the runtime truth.
|
||
- `TypeScript` theme contract is the compile-time truth.
|
||
|
||
The theme is a public contract in the UI layer:
|
||
|
||
- [`apps/website/ui/theme/Theme.ts`](apps/website/ui/theme/Theme.ts:1)
|
||
- [`apps/website/ui/theme/themes/default.ts`](apps/website/ui/theme/themes/default.ts)
|
||
- [`apps/website/ui/theme/theme.css`](apps/website/ui/theme/theme.css)
|
||
|
||
### Applying the theme (SSR correctness)
|
||
|
||
SSR correctness means: the initial HTML response already knows which theme is active.
|
||
|
||
For a single default theme, the simplest SSR-correct approach is:
|
||
|
||
- set `data-theme=default` on the `<html>` element in [`RootLayout`](apps/website/app/layout.tsx:45).
|
||
|
||
Later, if theme becomes user-selectable, the server can choose the theme per request using cookies/headers in [`RootLayout`](apps/website/app/layout.tsx:45), still using the same CSS-variable mechanism.
|
||
|
||
---
|
||
|
||
## Tailwind’s role (strictly non-authoritative)
|
||
|
||
Tailwind may remain in the project for page/layout scaffolding or legacy areas, but it must be treated as:
|
||
|
||
- a *consumer* of theme variables (e.g. Tailwind config maps colors to `var(--ui-...)`), or
|
||
- an *adapter* for non-UI-layer code.
|
||
|
||
In other words:
|
||
|
||
- UI components inside [`apps/website/ui`](apps/website/ui) never rely on Tailwind class name semantics.
|
||
- If Tailwind is used, it only references CSS variables (so switching the theme changes the look without changing class names).
|
||
|
||
This prevents “utility token drift” where a Tailwind rename becomes a breaking theme change.
|
||
|
||
---
|
||
|
||
## Component authoring rules
|
||
|
||
### Components are theme-driven, not class-driven
|
||
|
||
Inside [`apps/website/ui`](apps/website/ui):
|
||
|
||
1. Use CSS Modules for component styling.
|
||
2. Use semantic variants and intent props.
|
||
3. Use CSS variables for actual values.
|
||
|
||
Example conceptually:
|
||
|
||
- [`Button`](apps/website/ui/Button.tsx)
|
||
- `variant: primary | secondary | ghost`
|
||
- `intent: default | danger`
|
||
- CSS module uses variables like `background: var(--ui-color-intent-primary)`.
|
||
|
||
---
|
||
|
||
## Token naming: semantic + stable
|
||
|
||
Recommended CSS variable naming:
|
||
|
||
- `--ui-color-bg-base`
|
||
- `--ui-color-bg-surface`
|
||
- `--ui-color-border-default`
|
||
- `--ui-color-text-high`
|
||
- `--ui-color-intent-primary`
|
||
- `--ui-radius-md`
|
||
- `--ui-shadow-md`
|
||
- `--ui-font-body`
|
||
|
||
Rules:
|
||
|
||
1. Prefix with `--ui-` to avoid collisions.
|
||
2. Use semantic names (surface, text, intent), not “brand” names.
|
||
3. Avoid numeric-only palettes in component code (no `gray-600` in UI internals).
|
||
|
||
---
|
||
|
||
## Adding themes (extensibility model)
|
||
|
||
To add a new theme:
|
||
|
||
1. Keep the token contract stable (same variables).
|
||
2. Provide a new theme override block in [`apps/website/ui/theme/theme.css`](apps/website/ui/theme/theme.css), e.g.:
|
||
- `:root[data-theme=default] { ... }`
|
||
- `:root[data-theme=brand-x] { ... }`
|
||
3. Optionally provide a matching TypeScript definition in [`apps/website/ui/theme/themes`](apps/website/ui/theme/themes) for tooling and validation.
|
||
|
||
The important property: **components do not change** when a new theme is added.
|
||
|
||
---
|
||
|
||
## Architecture view
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A[Semantic theme contract] --> B[CSS variables]
|
||
B --> C[UI components]
|
||
B --> D[Tailwind adapter]
|
||
C --> E[App components]
|
||
```
|
||
|
||
Interpretation:
|
||
|
||
- Theme contract defines the tokens we promise.
|
||
- CSS variables implement those tokens at runtime.
|
||
- UI components consume tokens directly.
|
||
- Tailwind, if present, only consumes variables.
|
||
|
||
---
|
||
|
||
## Consequences (why this improves maintainability)
|
||
|
||
1. **Token changes are localized** to the theme definition (CSS vars) rather than spread across UI components.
|
||
2. UI components become **portable**: they do not depend on any particular styling framework.
|
||
3. It becomes possible to add enforcement (lint rules, conventions) around “no Tailwind tokens inside UI”.
|
||
4. Visual identity evolves without turning into a codebase-wide search-and-replace.
|
||
|