website refactor

This commit is contained in:
2026-01-18 18:43:13 +01:00
parent c35682cae5
commit 502d4aa092
8 changed files with 422 additions and 12 deletions

View File

@@ -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;

View File

@@ -71,7 +71,7 @@ export default async function RootLayout({
const enabledFlags = featureService.getEnabledFlags();
return (
<html lang="en" className="scroll-smooth overflow-x-hidden">
<html lang="en" className="scroll-smooth overflow-x-hidden" data-theme="default">
<head>
<meta name="mobile-web-app-capable" content="yes" />
</head>

View File

@@ -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 (
<ContainerProvider>
<QueryClientProvider>
<AuthProvider>
<FeatureFlagProvider flags={enabledFlags}>
<NotificationProvider>
<NotificationIntegration />
<EnhancedErrorBoundary enableDevOverlay={process.env.NODE_ENV === 'development'}>
{children}
{process.env.NODE_ENV === 'development' && <DevToolbar />}
</EnhancedErrorBoundary>
</NotificationProvider>
</FeatureFlagProvider>
</AuthProvider>
<ThemeProvider>
<AuthProvider>
<FeatureFlagProvider flags={enabledFlags}>
<NotificationProvider>
<NotificationIntegration />
<EnhancedErrorBoundary enableDevOverlay={process.env.NODE_ENV === 'development'}>
{children}
{process.env.NODE_ENV === 'development' && <DevToolbar />}
</EnhancedErrorBoundary>
</NotificationProvider>
</FeatureFlagProvider>
</AuthProvider>
</ThemeProvider>
</QueryClientProvider>
</ContainerProvider>
);

View File

@@ -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;
}

View File

@@ -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<ThemeContextType | undefined>(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 (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

View File

@@ -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 */
}

View File

@@ -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",
},
},
};

View 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.
---
## Tailwinds 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.