Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
594 lines
27 KiB
Markdown
594 lines
27 KiB
Markdown
# Architecture Patterns
|
|
|
|
**Domain:** UI polish overhaul -- Material Design 3 theming, dark mode, and component refactoring for existing Tailwind v4 + React wizard app
|
|
**Researched:** 2026-03-31
|
|
|
|
## Recommended Architecture
|
|
|
|
### Overview
|
|
|
|
The architecture centers on a **CSS custom properties layer** that bridges Material Design 3 color tokens with Tailwind v4's `@theme` directive. A thin React `ThemeProvider` context manages dark/light state and persists preference to `localStorage`. Components are refactored **bottom-up** from primitives (inputs, buttons, cards) to composed wizard steps, using semantic token names in Tailwind classes rather than hardcoded colors.
|
|
|
|
```
|
|
+---------------------+
|
|
| ThemeProvider | React Context
|
|
| (dark/light state) | localStorage + prefers-color-scheme
|
|
+---------------------+
|
|
|
|
|
sets .dark on <html>
|
|
|
|
|
+---------------------+
|
|
| index.css | CSS custom properties layer
|
|
| @theme { tokens } | MD3 color roles as --color-*
|
|
| .dark { overrides }| Dark palette overrides
|
|
+---------------------+
|
|
|
|
|
+---------------------+
|
|
| Tailwind v4 | Consumes tokens via @theme
|
|
| bg-surface | Semantic utility classes
|
|
| text-on-surface |
|
|
+---------------------+
|
|
|
|
|
+---------------+---------------+
|
|
| | |
|
|
+----------+ +-----------+ +------------+
|
|
| ui/ | | wizard/ | | layout/ |
|
|
| Button | | Steps | | AppShell |
|
|
| Input | | Indicator | | ThemeToggle|
|
|
| Card | | AuthToggles| +------------+
|
|
+----------+ +-----------+
|
|
```
|
|
|
|
### Component Boundaries
|
|
|
|
| Component | Responsibility | Communicates With | Status |
|
|
|-----------|---------------|-------------------|--------|
|
|
| `ThemeProvider` | Manages dark/light state, syncs to DOM and localStorage | `<html>` element class, `useTheme` consumers | **NEW** |
|
|
| `useTheme` hook | Exposes `{ theme, toggleTheme, setTheme }` | ThemeProvider context | **NEW** |
|
|
| `ThemeToggle` | UI control for dark/light switch | useTheme hook | **NEW** |
|
|
| `AppShell` | Outer layout wrapper (background, max-width, header) | ThemeProvider, WizardShell | **NEW** (extracted from App.tsx `WizardShell`) |
|
|
| `ui/Button` | MD3-styled button with variants (filled, outlined, text) | None (pure presentational) | **NEW** |
|
|
| `ui/Input` | MD3-styled text input with label and error state | react-hook-form via register | **NEW** (replaces inline input markup in FieldRenderer) |
|
|
| `ui/Select` | MD3-styled select dropdown | react-hook-form via register | **NEW** (replaces inline select markup in FieldRenderer) |
|
|
| `ui/Card` | MD3 surface card with elevation | None (pure presentational) | **NEW** |
|
|
| `ui/FieldRenderer` | Composes Input/Select/PasswordField based on FieldDef | ui/Input, ui/Select, ui/PasswordField | **MODIFIED** -- delegates to primitives |
|
|
| `ui/PasswordField` | Password input with show/hide toggle | react-hook-form | **MODIFIED** -- uses ui/Input internally |
|
|
| `ui/BackendCard` | Backend selection card | ui/Card | **MODIFIED** -- uses ui/Card internally |
|
|
| `wizard/StepIndicator` | Breadcrumb navigation | useWizard | **MODIFIED** -- inline styles to Tailwind + MD3 tokens |
|
|
| `wizard/*Step` | Step content | useWizard, ui components | **MODIFIED** -- swap hardcoded colors for semantic tokens |
|
|
|
|
### Data Flow
|
|
|
|
**Theme state flow:**
|
|
|
|
1. On app mount, `ThemeProvider` reads `localStorage.getItem('r2b-theme')`
|
|
2. If no stored preference, checks `window.matchMedia('(prefers-color-scheme: dark)').matches`
|
|
3. Sets/removes `.dark` class on `document.documentElement`
|
|
4. Tailwind's `@custom-variant dark` activates `dark:` prefix utilities
|
|
5. CSS custom properties in `.dark` scope override light palette values
|
|
6. All components using semantic tokens (`bg-surface`, `text-on-surface`) update automatically
|
|
|
|
**Color token flow:**
|
|
|
|
1. `index.css` defines MD3 color tokens as CSS custom properties in `@theme`
|
|
2. Light values are defaults; `.dark` class overrides with dark palette values
|
|
3. Tailwind v4 maps these to utility classes (e.g., `--color-surface` becomes `bg-surface`)
|
|
4. Components reference semantic names, never raw hex values
|
|
|
|
## CSS Custom Properties Strategy for MD3 Color Tokens
|
|
|
|
### Token Definition in index.css
|
|
|
|
This is the core architectural decision. Tailwind v4 uses `@theme` to define custom theme values consumed as utility classes. MD3 color roles map directly to CSS custom properties.
|
|
|
|
```css
|
|
@import "tailwindcss";
|
|
|
|
/* Enable class-based dark mode toggle */
|
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
|
|
/* --- MD3 Color Tokens as Tailwind v4 theme --- */
|
|
@theme {
|
|
/* Primary */
|
|
--color-primary: #1a73e8;
|
|
--color-on-primary: #ffffff;
|
|
--color-primary-container: #d3e3fd;
|
|
--color-on-primary-container: #041e49;
|
|
|
|
/* Secondary */
|
|
--color-secondary: #5f6368;
|
|
--color-on-secondary: #ffffff;
|
|
--color-secondary-container: #e8eaed;
|
|
--color-on-secondary-container: #1f1f1f;
|
|
|
|
/* Tertiary (accent) */
|
|
--color-tertiary: #1a73e8;
|
|
--color-on-tertiary: #ffffff;
|
|
--color-tertiary-container: #d3e3fd;
|
|
--color-on-tertiary-container: #041e49;
|
|
|
|
/* Error */
|
|
--color-error: #dc3545;
|
|
--color-on-error: #ffffff;
|
|
--color-error-container: #f9dedc;
|
|
--color-on-error-container: #410e0b;
|
|
|
|
/* Surface & Background */
|
|
--color-surface: #ffffff;
|
|
--color-surface-dim: #f1f3f4;
|
|
--color-surface-container: #f8f9fa;
|
|
--color-surface-container-low: #f1f3f4;
|
|
--color-surface-container-high: #e8eaed;
|
|
--color-on-surface: #1f1f1f;
|
|
--color-on-surface-variant: #5f6368;
|
|
|
|
/* Outline */
|
|
--color-outline: #dadce0;
|
|
--color-outline-variant: #e8eaed;
|
|
|
|
/* MD3 Elevation (shadow) */
|
|
--shadow-elevation-1: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
--shadow-elevation-2: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
--shadow-elevation-3: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
|
|
/* MD3 Shape (border-radius) */
|
|
--radius-sm: 8px;
|
|
--radius-md: 12px;
|
|
--radius-lg: 16px;
|
|
--radius-xl: 28px;
|
|
}
|
|
|
|
/* --- Dark mode overrides --- */
|
|
@layer base {
|
|
.dark {
|
|
--color-primary: #a8c7fa;
|
|
--color-on-primary: #062e6f;
|
|
--color-primary-container: #0842a0;
|
|
--color-on-primary-container: #d3e3fd;
|
|
|
|
--color-secondary: #c4c7c5;
|
|
--color-on-secondary: #303030;
|
|
--color-secondary-container: #444746;
|
|
--color-on-secondary-container: #e8eaed;
|
|
|
|
--color-tertiary: #a8c7fa;
|
|
--color-on-tertiary: #062e6f;
|
|
--color-tertiary-container: #0842a0;
|
|
--color-on-tertiary-container: #d3e3fd;
|
|
|
|
--color-error: #f2b8b5;
|
|
--color-on-error: #601410;
|
|
--color-error-container: #8c1d18;
|
|
--color-on-error-container: #f9dedc;
|
|
|
|
--color-surface: #1f1f1f;
|
|
--color-surface-dim: #141414;
|
|
--color-surface-container: #2d2d2d;
|
|
--color-surface-container-low: #262626;
|
|
--color-surface-container-high: #3c3c3c;
|
|
--color-on-surface: #e8eaed;
|
|
--color-on-surface-variant: #c4c7c5;
|
|
|
|
--color-outline: #5f6368;
|
|
--color-outline-variant: #444746;
|
|
|
|
--shadow-elevation-1: 0 1px 3px 0 rgb(0 0 0 / 0.3);
|
|
--shadow-elevation-2: 0 2px 6px 0 rgb(0 0 0 / 0.3);
|
|
--shadow-elevation-3: 0 4px 8px 0 rgb(0 0 0 / 0.3);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Why This Approach (Not a Component Library)
|
|
|
|
1. **No dependency on MD3 web components** -- `@material/web` is Angular/Lit-oriented and would fight React + Tailwind. The MD3 spec is a *design system*, not a library requirement.
|
|
2. **Tailwind v4 @theme is the native mechanism** -- tokens defined in `@theme` become first-class Tailwind utilities (`bg-surface`, `text-on-primary`, `shadow-elevation-2`). No plugins needed.
|
|
3. **Dark mode is a CSS variable swap** -- the `.dark` class override block is all that's needed. No JS re-rendering, no theme prop drilling to every component.
|
|
4. **Accent color extensibility** -- to support user-selectable accent colors later, just override `--color-primary` and related tokens at runtime via JS on `document.documentElement.style`.
|
|
|
|
### MD3 Color Roles Used (Practical Subset)
|
|
|
|
Full MD3 has 29+ color roles. For this wizard app, we use a practical subset:
|
|
|
|
| MD3 Role | Tailwind Class | Used For |
|
|
|----------|---------------|----------|
|
|
| `surface` | `bg-surface` | Page background, card backgrounds |
|
|
| `on-surface` | `text-on-surface` | Primary text |
|
|
| `on-surface-variant` | `text-on-surface-variant` | Secondary text, help text |
|
|
| `surface-dim` | `bg-surface-dim` | Page background (current `bg-gray-50`) |
|
|
| `surface-container` | `bg-surface-container` | Card fills, input backgrounds |
|
|
| `surface-container-high` | `bg-surface-container-high` | Elevated cards, active states |
|
|
| `primary` | `bg-primary`, `text-primary` | Primary buttons, active indicators |
|
|
| `on-primary` | `text-on-primary` | Text on primary buttons |
|
|
| `primary-container` | `bg-primary-container` | Selected BackendCard fill |
|
|
| `on-primary-container` | `text-on-primary-container` | Text on selected BackendCard |
|
|
| `error` | `text-error` | Validation error text |
|
|
| `error-container` | `bg-error-container` | Error badge backgrounds |
|
|
| `outline` | `border-outline` | Input borders, dividers |
|
|
| `outline-variant` | `border-outline-variant` | Subtle borders |
|
|
|
|
### Current-to-Token Mapping
|
|
|
|
Explicit mapping of every hardcoded color in the existing codebase:
|
|
|
|
| Current Class | Semantic Token | Where Used |
|
|
|--------------|---------------|------------|
|
|
| `bg-gray-50` | `bg-surface-dim` | App.tsx page background |
|
|
| `text-gray-900` | `text-on-surface` | App.tsx heading, BackendCard name |
|
|
| `text-gray-500` | `text-on-surface-variant` | BackendCard description, help text |
|
|
| `bg-white` | `bg-surface` | BackendCard default, card backgrounds |
|
|
| `border-gray-200` | `border-outline-variant` | BackendCard default border |
|
|
| `border-gray-300` | `border-outline` | Input borders, Back button border |
|
|
| `hover:border-blue-400` | `hover:border-primary/60` | BackendCard hover |
|
|
| `hover:bg-gray-50` | `hover:bg-surface-container` | BackendCard hover, Back button hover |
|
|
| `border-blue-600` | `border-primary` | BackendCard selected border |
|
|
| `bg-blue-50` | `bg-primary-container` | BackendCard selected fill, tooltip bg |
|
|
| `bg-blue-600` | `bg-primary` | Next/primary buttons |
|
|
| `text-white` (on blue bg) | `text-on-primary` | Primary button text |
|
|
| `hover:bg-blue-700` | `hover:bg-primary/90` | Primary button hover |
|
|
| `text-red-500` | `text-error` | Required field asterisk |
|
|
| `text-red-600` | `text-error` | Error message text |
|
|
| `border-red-500` | `border-error` | Error input border |
|
|
| `focus:ring-blue-300` | `focus:ring-primary/40` | Input focus ring |
|
|
| `focus:ring-red-300` | `focus:ring-error/40` | Error input focus ring |
|
|
| `text-blue-500` | `text-primary` | Tooltip toggle button |
|
|
| `text-blue-700` | `text-on-primary-container` | Tooltip text |
|
|
| `border-blue-200` | `border-primary-container` | Tooltip border |
|
|
| `text-gray-400` | `text-on-surface-variant` | Password show/hide button |
|
|
| `hover:text-gray-700` | `hover:text-on-surface` | Password show/hide hover |
|
|
| `#999` (inline) | `text-on-surface-variant` | StepIndicator future steps |
|
|
| `fontWeight: bold` (inline) | `font-bold` | StepIndicator active step |
|
|
| `fontWeight: normal` (inline) | `font-normal` | StepIndicator completed step |
|
|
|
|
## ThemeProvider Architecture
|
|
|
|
### Implementation
|
|
|
|
```typescript
|
|
// src/store/theme-context.tsx
|
|
import { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
|
|
|
type Theme = 'light' | 'dark';
|
|
|
|
interface ThemeContextValue {
|
|
theme: Theme;
|
|
toggleTheme: () => void;
|
|
setTheme: (theme: Theme) => void;
|
|
}
|
|
|
|
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
|
|
function getInitialTheme(): Theme {
|
|
if (typeof window === 'undefined') return 'light';
|
|
const stored = localStorage.getItem('r2b-theme') as Theme | null;
|
|
if (stored === 'light' || stored === 'dark') return stored;
|
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
}
|
|
|
|
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
const [theme, setThemeState] = useState<Theme>(getInitialTheme);
|
|
|
|
const setTheme = useCallback((t: Theme) => {
|
|
setThemeState(t);
|
|
localStorage.setItem('r2b-theme', t);
|
|
document.documentElement.classList.toggle('dark', t === 'dark');
|
|
}, []);
|
|
|
|
const toggleTheme = useCallback(() => {
|
|
setTheme(theme === 'dark' ? 'light' : 'dark');
|
|
}, [theme, setTheme]);
|
|
|
|
// Sync on mount
|
|
useEffect(() => {
|
|
document.documentElement.classList.toggle('dark', theme === 'dark');
|
|
}, []);
|
|
|
|
return (
|
|
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
|
|
{children}
|
|
</ThemeContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useTheme(): ThemeContextValue {
|
|
const ctx = useContext(ThemeContext);
|
|
if (!ctx) throw new Error('useTheme must be used inside <ThemeProvider>');
|
|
return ctx;
|
|
}
|
|
```
|
|
|
|
### Provider Nesting in App.tsx
|
|
|
|
```typescript
|
|
// ThemeProvider wraps WizardProvider -- theme is app-global, wizard state is feature-scoped
|
|
export default function App() {
|
|
return (
|
|
<ThemeProvider>
|
|
<WizardProvider>
|
|
<AppShell />
|
|
</WizardProvider>
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
```
|
|
|
|
**Rationale:** ThemeProvider is outermost because theme affects the entire DOM tree. WizardProvider is inner because it only governs wizard state. They are independent -- no cross-dependencies.
|
|
|
|
## Component Refactoring Approach: Bottom-Up Primitives
|
|
|
|
### Why Bottom-Up (Not Top-Down)
|
|
|
|
1. **Primitives are the reuse boundary** -- Input, Button, Card are used by multiple wizard steps. Fix once, propagate everywhere.
|
|
2. **Tests target behavior, not styles** -- existing 159 tests use Testing Library (query by role, text, label). Changing CSS classes does not break tests. Changing component structure (splitting FieldRenderer) could break tests if DOM hierarchy changes.
|
|
3. **Incremental migration** -- each primitive can be built, tested, and swapped in isolation. No big-bang rewrite.
|
|
|
|
### Refactoring Layers
|
|
|
|
```
|
|
Layer 1: CSS Foundation (index.css)
|
|
- MD3 tokens in @theme
|
|
- Dark mode @custom-variant + .dark overrides
|
|
- Zero component changes needed
|
|
- Zero test impact
|
|
|
|
Layer 2: Primitive Components (ui/)
|
|
- NEW: Button, Input, Select, Card
|
|
- Tests: New tests for new components only
|
|
|
|
Layer 3: Composed Components (ui/)
|
|
- MODIFIED: FieldRenderer -- delegates to Input/Select/PasswordField
|
|
- MODIFIED: BackendCard -- delegates to Card
|
|
- MODIFIED: PasswordField -- uses Input primitive internally
|
|
- Tests: Existing tests should pass (same DOM semantics, different styling)
|
|
|
|
Layer 4: Wizard Steps + StepIndicator (wizard/)
|
|
- MODIFIED: Replace hardcoded Tailwind classes with semantic tokens
|
|
- MODIFIED: StepIndicator -- inline styles to Tailwind classes
|
|
- Tests: Existing tests should pass (no behavior change)
|
|
|
|
Layer 5: Layout Shell + Polish
|
|
- NEW: AppShell (extracted from WizardShell in App.tsx)
|
|
- NEW: ThemeToggle
|
|
- MODIFIED: App.tsx -- adds ThemeProvider, uses AppShell
|
|
- Tests: App.test.tsx needs update for new provider wrapping
|
|
```
|
|
|
|
### Migration Pattern Per Component
|
|
|
|
For each existing component, the migration follows this pattern:
|
|
|
|
1. **Replace hardcoded colors with semantic tokens** using the Current-to-Token Mapping table above
|
|
2. **Add `dark:` variants only where semantic tokens alone are insufficient** (should be rare -- the CSS variable swap handles most cases automatically)
|
|
3. **Replace inline styles with Tailwind classes** (StepIndicator specific)
|
|
4. **Run existing tests after each component** -- they should pass unchanged
|
|
|
|
## Patterns to Follow
|
|
|
|
### Pattern 1: Semantic Color Tokens Only
|
|
|
|
**What:** Never use raw Tailwind color classes (`blue-600`, `gray-50`) in components. Always use semantic MD3 token names.
|
|
|
|
**When:** Every component, every color reference.
|
|
|
|
**Why:** Semantic tokens automatically adapt to dark mode via CSS variable override. Raw colors would need manual `dark:` overrides on every single usage.
|
|
|
|
**Example:**
|
|
```tsx
|
|
// WRONG -- requires dark: override on every element
|
|
<button className="bg-blue-600 text-white dark:bg-blue-400 dark:text-gray-900">
|
|
|
|
// RIGHT -- adapts automatically via CSS variable swap
|
|
<button className="bg-primary text-on-primary">
|
|
```
|
|
|
|
### Pattern 2: Component Composition Over Monoliths
|
|
|
|
**What:** Extract reusable UI primitives (Button, Input, Card) from inline markup.
|
|
|
|
**When:** Any UI element used in 2+ places, or any element with complex styling logic.
|
|
|
|
**Example:**
|
|
```tsx
|
|
// BEFORE: Inline button styling in every step component
|
|
<button className="px-4 py-2 text-sm bg-primary text-on-primary rounded-md hover:bg-primary/90">
|
|
Next
|
|
</button>
|
|
|
|
// AFTER: Reusable Button primitive
|
|
<Button variant="filled">Next</Button>
|
|
<Button variant="outlined">Back</Button>
|
|
<Button variant="text">Cancel</Button>
|
|
```
|
|
|
|
### Pattern 3: Test Helper for Theme Context
|
|
|
|
**What:** Create a test utility that wraps components in both ThemeProvider and WizardProvider.
|
|
|
|
**When:** Any component test that renders a component needing theme context.
|
|
|
|
**Example:**
|
|
```tsx
|
|
// src/test-utils.tsx
|
|
import { render } from '@testing-library/react';
|
|
import { ThemeProvider } from './store/theme-context';
|
|
import { WizardProvider } from './store/context';
|
|
|
|
export function renderWithProviders(ui: React.ReactElement) {
|
|
return render(
|
|
<ThemeProvider>
|
|
<WizardProvider>
|
|
{ui}
|
|
</WizardProvider>
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Pattern 4: MD3 Elevation via Shadow Tokens
|
|
|
|
**What:** Use the `shadow-elevation-*` tokens for card depth instead of arbitrary shadow utilities.
|
|
|
|
**When:** Cards, modals, dropdowns -- any elevated surface.
|
|
|
|
**Example:**
|
|
```tsx
|
|
// Card at rest
|
|
<div className="bg-surface-container rounded-lg shadow-elevation-1">
|
|
|
|
// Card hovered / elevated
|
|
<div className="bg-surface-container rounded-lg shadow-elevation-2 hover:shadow-elevation-3">
|
|
```
|
|
|
|
## Anti-Patterns to Avoid
|
|
|
|
### Anti-Pattern 1: Dual-Track Color System
|
|
|
|
**What:** Keeping some components on raw Tailwind colors while migrating others to MD3 tokens.
|
|
|
|
**Why bad:** Dark mode will be broken on non-migrated components. Visual inconsistency. Harder to maintain.
|
|
|
|
**Instead:** Migrate ALL color references in one sweep per component. The token names are a 1:1 replacement -- no logic changes needed.
|
|
|
|
### Anti-Pattern 2: JS-Driven Theme Prop Drilling
|
|
|
|
**What:** Passing `isDark` or `theme` as a prop to every component and using ternaries to pick colors.
|
|
|
|
**Why bad:** Massive prop threading, re-renders on theme change, duplicated color logic.
|
|
|
|
**Instead:** CSS custom properties handle the switch. Components just use `bg-surface` -- the browser resolves the correct value based on `.dark` class presence. Only `ThemeToggle` needs to consume `useTheme`.
|
|
|
|
### Anti-Pattern 3: Using @material/web Components
|
|
|
|
**What:** Importing Google's Material Web Components (`<md-button>`, `<md-text-field>`).
|
|
|
|
**Why bad:** These are web components designed for vanilla JS / Lit. They fight React's rendering model, do not integrate with react-hook-form's `register()`, and add significant bundle size. They use Shadow DOM which conflicts with Tailwind's utility approach.
|
|
|
|
**Instead:** Implement MD3's *visual language* (colors, elevation, shape, typography) via CSS tokens + Tailwind utilities on standard React elements.
|
|
|
|
### Anti-Pattern 4: Big-Bang Step Rewrite
|
|
|
|
**What:** Rewriting entire wizard step components at once with new UI.
|
|
|
|
**Why bad:** Breaks multiple tests simultaneously. Hard to isolate regressions. Merge conflicts if concurrent work.
|
|
|
|
**Instead:** Layer-by-layer approach. CSS tokens first (zero breakage), then primitives (additive), then swap in composed components, then layout.
|
|
|
|
## Impact on Existing Test Suite (159 Tests)
|
|
|
|
### Test Categories and Impact Assessment
|
|
|
|
| Test File | Tests (approx) | Impact | Reason |
|
|
|-----------|----------------|--------|--------|
|
|
| `schemas/index.test.ts` | ~20 | NONE | Tests Zod schemas, no UI |
|
|
| `schemas/registry.test.ts` | ~15 | NONE | Tests BACKEND_REGISTRY data, no UI |
|
|
| `generators/*.test.ts` (3 files) | ~40 | NONE | Tests script generation, no UI |
|
|
| `store/reducer.test.ts` | ~20 | NONE | Tests state reducer, no UI |
|
|
| `App.test.tsx` | ~10 | LOW | May need ThemeProvider wrapper if test renders App internals directly |
|
|
| `StepIndicator.test.tsx` | ~10 | LOW | Tests click behavior and text content, not styles |
|
|
| `BackendSelectionStep.test.tsx` | ~15 | LOW | Tests card selection behavior via role queries |
|
|
| `RemoteConfigStep.test.tsx` | ~15 | LOW | Tests form submission and validation |
|
|
| `ReviewStep.test.tsx` | ~15 | LOW | Tests output generation, copy/download actions |
|
|
|
|
**Key insight:** ~60% of tests (95+ tests across schemas, generators, reducer) are pure logic tests with ZERO UI coupling. They will not be affected at all.
|
|
|
|
**For UI tests:** Testing Library queries by role, label text, and accessible names -- NOT by CSS class names. Swapping `bg-blue-600` to `bg-primary` does not change what `getByRole('button')` or `getByText('Next')` finds. These tests should remain green through the entire migration.
|
|
|
|
**One risk area:** If `FieldRenderer` refactoring changes DOM structure (e.g., wrapping inputs in a new primitive component that adds an extra `<div>`), tests that use `container.querySelector` or rely on specific nesting could break. Mitigation: ensure new primitives produce the same semantic DOM (same `<input>`, `<select>`, `<label>` elements with same attributes).
|
|
|
|
## Suggested Build Order (Minimizes Breakage)
|
|
|
|
### Phase 1: CSS Foundation (Zero Test Impact)
|
|
|
|
**What:** Set up MD3 tokens and dark mode infrastructure in `index.css`. No component changes.
|
|
|
|
1. Add `@custom-variant dark (&:where(.dark, .dark *))` to `index.css`
|
|
2. Define all MD3 color tokens in `@theme` block
|
|
3. Add `.dark` override block in `@layer base`
|
|
4. Define elevation shadow tokens and border-radius shape tokens
|
|
5. Create `ThemeProvider` + `useTheme` hook (new files, no existing code touched)
|
|
6. Wire `ThemeProvider` into `App.tsx` (outermost wrapper)
|
|
|
|
**Test impact:** Zero -- existing tests pass unchanged. Add new unit tests for ThemeProvider.
|
|
|
|
**Dependency:** None. Can start immediately.
|
|
|
|
### Phase 2: Primitive Extraction (Additive Only)
|
|
|
|
**What:** Create new ui/ primitives without modifying existing components yet.
|
|
|
|
1. Create `ui/Button` with MD3 variants (filled, outlined, text)
|
|
2. Create `ui/Input` with MD3 styling (outline, label, error state, focus ring)
|
|
3. Create `ui/Select` with MD3 styling
|
|
4. Create `ui/Card` with MD3 elevation and shape
|
|
5. Create `ThemeToggle` component
|
|
6. Create `AppShell` layout component
|
|
|
|
**Test impact:** Zero on existing tests. Write new tests for each new component.
|
|
|
|
**Dependency:** Phase 1 tokens must be in place for correct color references.
|
|
|
|
### Phase 3: StepIndicator Migration (Low Risk, Isolated)
|
|
|
|
**What:** Replace inline styles with Tailwind + MD3 tokens. This is flagged tech debt.
|
|
|
|
1. Replace `style={{ fontWeight: 'bold' }}` with `className="font-bold"`
|
|
2. Replace `style={{ color: '#999' }}` with `className="text-on-surface-variant"`
|
|
3. Replace `style={{ fontWeight: 'normal' }}` with `className="font-normal"`
|
|
4. Apply MD3 shape and color tokens to step indicator layout
|
|
|
|
**Test impact:** `StepIndicator.test.tsx` tests click behavior and text content. Style changes are invisible to these tests. Should pass unchanged.
|
|
|
|
**Dependency:** Phase 1 tokens.
|
|
|
|
### Phase 4: Component Token Migration (Core Migration)
|
|
|
|
**What:** Replace hardcoded Tailwind colors with semantic MD3 tokens across all components. Integrate new primitives.
|
|
|
|
1. `BackendCard` -- swap color classes using Current-to-Token Mapping, optionally compose with Card
|
|
2. `FieldRenderer` -- delegate rendering to Input/Select primitives (preserve DOM semantics)
|
|
3. `PasswordField` -- swap color classes, optionally use Input internally
|
|
4. `DeploymentStep` -- swap button and input color classes
|
|
5. `ReviewStep` -- swap color classes
|
|
6. `RemoteConfigStep` -- swap color classes
|
|
7. `BackendSelectionStep` -- swap color classes
|
|
|
|
**Test impact:** LOW. Color class changes are invisible to Testing Library. If FieldRenderer's DOM structure changes, run tests after each sub-step to catch issues early.
|
|
|
|
**Dependency:** Phase 1 tokens + Phase 2 primitives.
|
|
|
|
### Phase 5: Layout Shell and Dark Mode UX
|
|
|
|
**What:** AppShell integration, ThemeToggle placement, responsive improvements.
|
|
|
|
1. Extract layout from WizardShell into AppShell component
|
|
2. Add ThemeToggle to app header area
|
|
3. Wire up dark mode persistence (already in ThemeProvider)
|
|
4. Mobile responsiveness passes
|
|
5. Update `App.test.tsx` for new structure
|
|
|
|
**Test impact:** `App.test.tsx` may need structural updates. Other tests unaffected.
|
|
|
|
**Dependency:** All previous phases.
|
|
|
|
## Scalability Considerations
|
|
|
|
| Concern | Current (v1.2) | Future (accent colors) | Future (multi-theme) |
|
|
|---------|----------------|----------------------|---------------------|
|
|
| Color tokens | ~20 tokens in @theme | Override `--color-primary` family via JS `document.documentElement.style` | Add named theme classes, swap `.theme-blue` / `.theme-green` |
|
|
| Theme persistence | localStorage `r2b-theme` key | Add `r2b-accent` key | Add `r2b-theme-name` key |
|
|
| Bundle size | +0 KB (CSS only) | +~3KB if using `@material/material-color-utilities` for seed-based palette generation | Same |
|
|
| Performance | CSS variable swap (no React re-render for color changes) | One-time JS computation + CSS variable batch update | Same |
|
|
| Token generation | Hand-picked values | Use `@material/material-color-utilities` `themeFromSourceColor()` to generate all 29 tokens from one seed hex | Same |
|
|
|
|
## Sources
|
|
|
|
- [Tailwind CSS v4 Dark Mode documentation](https://tailwindcss.com/docs/dark-mode) -- HIGH confidence, official docs
|
|
- [Material Design 3 Color Roles](https://m3.material.io/styles/color/roles) -- HIGH confidence, official spec
|
|
- [Material Design 3 Design Tokens](https://m3.material.io/foundations/design-tokens) -- HIGH confidence, official spec
|
|
- [Material Design 3 Elevation Tokens](https://m3.material.io/styles/elevation/tokens) -- HIGH confidence, official spec
|
|
- [Tailwind v4 dark mode @custom-variant discussion](https://github.com/tailwindlabs/tailwindcss/discussions/15083) -- MEDIUM confidence, community verified pattern
|
|
- [@material/material-color-utilities npm](https://www.npmjs.com/package/@material/material-color-utilities) -- HIGH confidence, official Google package
|
|
- [Generating MD3 Dynamic Color with JavaScript](https://dt.in.th/M3DynamicColorJS) -- MEDIUM confidence, verified implementation walkthrough
|
|
- [React dark mode with Context + Tailwind pattern](https://medium.com/@sandeepshome.dev/react-theming-dark-mode-with-context-api-and-tailwindcss-b3ef50a9522b) -- MEDIUM confidence, community pattern
|
|
- [Tailwind v4 @theme with dark mode pattern](https://medium.com/@kevstrosky/theme-colors-with-tailwind-css-v4-0-and-next-themes-dark-light-custom-mode-36dca1e20419) -- MEDIUM confidence, community implementation
|