website refactor
This commit is contained in:
224
docs/architecture/website/UI_THEMING.md
Normal file
224
docs/architecture/website/UI_THEMING.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# 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)`.
|
||||
|
||||
### Primitives
|
||||
|
||||
The primitives in [`apps/website/ui/primitives`](apps/website/ui/primitives) exist to build consistent semantics. They should not encode a “theme” directly; they expose minimal layout/styling mechanics.
|
||||
|
||||
Examples in code today:
|
||||
|
||||
- [`Box`](apps/website/ui/primitives/Box.tsx:1)
|
||||
- [`Stack`](apps/website/ui/primitives/Stack.tsx:1)
|
||||
- [`Surface`](apps/website/ui/primitives/Surface.tsx:1)
|
||||
|
||||
Conceptually, primitives should either:
|
||||
|
||||
- map typed props to standard CSS properties (style object), or
|
||||
- map typed props to CSS variables that are already in the theme contract.
|
||||
|
||||
What primitives should **not** do: generate Tailwind class strings as their main behavior.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
Reference in New Issue
Block a user