website refactor
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
57
apps/website/ui/theme/Theme.ts
Normal file
57
apps/website/ui/theme/Theme.ts
Normal 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;
|
||||
}
|
||||
33
apps/website/ui/theme/ThemeProvider.tsx
Normal file
33
apps/website/ui/theme/ThemeProvider.tsx
Normal 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;
|
||||
}
|
||||
41
apps/website/ui/theme/theme.css
Normal file
41
apps/website/ui/theme/theme.css
Normal 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 */
|
||||
}
|
||||
51
apps/website/ui/theme/themes/default.ts
Normal file
51
apps/website/ui/theme/themes/default.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
};
|
||||
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