From 502d4aa0923e28f839893e6c2400c4379421cbbe Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 18 Jan 2026 18:43:13 +0100 Subject: [PATCH] website refactor --- apps/website/app/globals.css | 1 + apps/website/app/layout.tsx | 2 +- apps/website/components/AppWrapper.tsx | 25 +-- apps/website/ui/theme/Theme.ts | 57 ++++++ apps/website/ui/theme/ThemeProvider.tsx | 33 ++++ apps/website/ui/theme/theme.css | 41 +++++ apps/website/ui/theme/themes/default.ts | 51 ++++++ docs/architecture/website/UI_THEMING.md | 224 ++++++++++++++++++++++++ 8 files changed, 422 insertions(+), 12 deletions(-) create mode 100644 apps/website/ui/theme/Theme.ts create mode 100644 apps/website/ui/theme/ThemeProvider.tsx create mode 100644 apps/website/ui/theme/theme.css create mode 100644 apps/website/ui/theme/themes/default.ts create mode 100644 docs/architecture/website/UI_THEMING.md diff --git a/apps/website/app/globals.css b/apps/website/app/globals.css index 4651e67ed..288203d7a 100644 --- a/apps/website/app/globals.css +++ b/apps/website/app/globals.css @@ -1,4 +1,5 @@ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); +@import '../ui/theme/theme.css'; @tailwind base; @tailwind components; diff --git a/apps/website/app/layout.tsx b/apps/website/app/layout.tsx index d33301091..cd8361073 100644 --- a/apps/website/app/layout.tsx +++ b/apps/website/app/layout.tsx @@ -71,7 +71,7 @@ export default async function RootLayout({ const enabledFlags = featureService.getEnabledFlags(); return ( - + diff --git a/apps/website/components/AppWrapper.tsx b/apps/website/components/AppWrapper.tsx index 1763ab698..7a165b486 100644 --- a/apps/website/components/AppWrapper.tsx +++ b/apps/website/components/AppWrapper.tsx @@ -8,6 +8,7 @@ import { NotificationProvider } from '@/components/notifications/NotificationPro import { NotificationIntegration } from '@/components/errors/NotificationIntegration'; import { EnhancedErrorBoundary } from '@/components/errors/EnhancedErrorBoundary'; import { DevToolbar } from '@/components/dev/DevToolbar'; +import { ThemeProvider } from '@/ui/theme/ThemeProvider'; import React from 'react'; interface AppWrapperProps { @@ -19,17 +20,19 @@ export function AppWrapper({ children, enabledFlags }: AppWrapperProps) { return ( - - - - - - {children} - {process.env.NODE_ENV === 'development' && } - - - - + + + + + + + {children} + {process.env.NODE_ENV === 'development' && } + + + + + ); diff --git a/apps/website/ui/theme/Theme.ts b/apps/website/ui/theme/Theme.ts new file mode 100644 index 000000000..c1d95ac55 --- /dev/null +++ b/apps/website/ui/theme/Theme.ts @@ -0,0 +1,57 @@ +export interface ThemeColors { + bg: { + base: string; + surface: string; + surfaceMuted: string; + }; + border: { + default: string; + muted: string; + }; + text: { + high: string; + med: string; + low: string; + }; + intent: { + primary: string; + telemetry: string; + warning: string; + success: string; + critical: string; + }; +} + +export interface ThemeRadii { + none: string; + sm: string; + md: string; + lg: string; + xl: string; + full: string; +} + +export interface ThemeShadows { + none: string; + sm: string; + md: string; + lg: string; + xl: string; + focus: string; +} + +export interface ThemeTypography { + fontFamily: { + sans: string; + mono: string; + }; +} + +export interface Theme { + id: string; + name: string; + colors: ThemeColors; + radii: ThemeRadii; + shadows: ThemeShadows; + typography: ThemeTypography; +} diff --git a/apps/website/ui/theme/ThemeProvider.tsx b/apps/website/ui/theme/ThemeProvider.tsx new file mode 100644 index 000000000..8c62f0218 --- /dev/null +++ b/apps/website/ui/theme/ThemeProvider.tsx @@ -0,0 +1,33 @@ +'use client'; + +import React, { createContext, useContext, ReactNode } from 'react'; +import { Theme } from './Theme'; +import { defaultTheme } from './themes/default'; + +interface ThemeContextType { + theme: Theme; +} + +const ThemeContext = createContext(undefined); + +export function ThemeProvider({ children }: { children: ReactNode }) { + // For now, we only have the default theme. + // In the future, this could be driven by state, cookies, or user preferences. + const value = { + theme: defaultTheme, + }; + + return ( + + {children} + + ); +} + +export function useTheme() { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +} diff --git a/apps/website/ui/theme/theme.css b/apps/website/ui/theme/theme.css new file mode 100644 index 000000000..224f30d83 --- /dev/null +++ b/apps/website/ui/theme/theme.css @@ -0,0 +1,41 @@ +:root { + /* Base tokens mapped to default theme */ + --ui-color-bg-base: #0C0D0F; + --ui-color-bg-surface: #141619; + --ui-color-bg-surface-muted: rgba(20, 22, 25, 0.7); + + --ui-color-border-default: #23272B; + --ui-color-border-muted: rgba(35, 39, 43, 0.5); + + --ui-color-text-high: #FFFFFF; + --ui-color-text-med: #A1A1AA; + --ui-color-text-low: #71717A; + + --ui-color-intent-primary: #198CFF; + --ui-color-intent-telemetry: #4ED4E0; + --ui-color-intent-warning: #FFBE4D; + --ui-color-intent-success: #6FE37A; + --ui-color-intent-critical: #E35C5C; + + --ui-radius-none: 0; + --ui-radius-sm: 0.125rem; + --ui-radius-md: 0.375rem; + --ui-radius-lg: 0.5rem; + --ui-radius-xl: 0.75rem; + --ui-radius-full: 9999px; + + --ui-shadow-none: none; + --ui-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --ui-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --ui-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --ui-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + --ui-shadow-focus: 0 0 0 4px rgba(25, 140, 255, 0.5); + + --ui-font-sans: 'Inter', system-ui, sans-serif; + --ui-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; +} + +/* Theme override block */ +[data-theme='default'] { + /* Currently same as root, but allows for future overrides */ +} diff --git a/apps/website/ui/theme/themes/default.ts b/apps/website/ui/theme/themes/default.ts new file mode 100644 index 000000000..f0a852f7a --- /dev/null +++ b/apps/website/ui/theme/themes/default.ts @@ -0,0 +1,51 @@ +import { Theme } from '../Theme'; + +export const defaultTheme: Theme = { + id: 'default', + name: 'Precision Racing (Dark)', + colors: { + bg: { + base: '#0C0D0F', + surface: '#141619', + surfaceMuted: 'rgba(20, 22, 25, 0.7)', + }, + border: { + default: '#23272B', + muted: 'rgba(35, 39, 43, 0.5)', + }, + text: { + high: '#FFFFFF', + med: '#A1A1AA', + low: '#71717A', + }, + intent: { + primary: '#198CFF', + telemetry: '#4ED4E0', + warning: '#FFBE4D', + success: '#6FE37A', + critical: '#E35C5C', + }, + }, + radii: { + none: '0', + sm: '0.125rem', + md: '0.375rem', + lg: '0.5rem', + xl: '0.75rem', + full: '9999px', + }, + shadows: { + none: 'none', + sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', + md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', + lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', + xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', + focus: '0 0 0 4px rgba(25, 140, 255, 0.5)', + }, + typography: { + fontFamily: { + sans: "'Inter', system-ui, sans-serif", + mono: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", + }, + }, +}; diff --git a/docs/architecture/website/UI_THEMING.md b/docs/architecture/website/UI_THEMING.md new file mode 100644 index 000000000..be9fa6f47 --- /dev/null +++ b/docs/architecture/website/UI_THEMING.md @@ -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 `` 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. +