docs(v1.2): complete project research — stack, features, architecture, pitfalls, summary

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 15:34:46 +02:00
co-authored by Claude Opus 4.6
parent 4a5ca206db
commit 4d4b01d9b7
5 changed files with 1198 additions and 941 deletions
+482 -329
View File
@@ -1,440 +1,593 @@
# Architecture Patterns
**Domain:** Pure frontend multi-step configuration wizard (static site, no backend)
**Project:** Ready2Blob
**Researched:** 2026-03-26
**Confidence:** MEDIUM — rclone.conf format and Intune deployment patterns verified from training data (stable, well-documented domains); client-side download patterns are stable browser APIs; web research unavailable for cross-validation
---
**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
### High-Level System Diagram
### 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.
```
┌─────────────────────────────────────────────────────────────────┐
│ Browser (Static App) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ Wizard UI │───▶│ Wizard State│───▶│ Config Builders │ │
│ │ (Steps/Nav) │ │ (Form Data) │ │ (rclone.conf + │ │
│ └──────────────┘ └──────────────┘ │ PS Scripts) │ │
│ └────────┬───────────┘ │
│ │ │
│ ┌────────▼───────────┐ │
│ │ Download Manager │ │
│ │ (individual files │ │
│ │ or ZIP bundle) │ │
│ └────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
(file download only)
User's disk
+---------------------+
| 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| +------------+
+----------+ +-----------+
```
No network requests leave the browser. All state is ephemeral (in-memory for the session lifetime).
### 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 |
## Component Boundaries
### Data Flow
| Component | Responsibility | Inputs | Outputs | Communicates With |
|-----------|---------------|--------|---------|-------------------|
| **Wizard UI** | Render step forms, handle navigation (next/back/jump), validate per-step | User interactions | Step completion events | Wizard State |
| **Wizard State** | Single source of truth for all collected form data; tracks current step and completion status | Step form submissions | Reactive state object | Wizard UI (reads), Config Builders (reads) |
| **Backend Schema Registry** | Defines the fields required per rclone backend type (Azure Blob, S3, OneDrive, etc.) | Backend type selection | Field definitions for each step | Wizard UI (drives dynamic form rendering) |
| **rclone.conf Builder** | Transforms wizard state into a valid rclone.conf string | Wizard State snapshot | `rclone.conf` string | Download Manager |
| **PowerShell Script Builder** | Generates PS scripts (Intune Win32 or RMM) from wizard state + optional rclone install flag | Wizard State snapshot, script type selection | `.ps1` string(s) | Download Manager |
| **Download Manager** | Packages one or more text files and triggers browser download (individual or ZIP) | File content strings + filenames | Browser file download | rclone.conf Builder, PS Script Builder |
**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
## Data Flow
**Color token flow:**
```
User fills wizard step N
Wizard UI validates step N inputs
Wizard State updated (merge step N data into central store)
User reaches Review/Download step
├──▶ rclone.conf Builder
│ reads: [remote_name, backend_type, ...backend-specific fields]
│ produces: rclone.conf string
├──▶ PowerShell Script Builder (Intune)
│ reads: [remote_name, mount_path, include_install_flag, install_source_url]
│ produces: deploy-intune.ps1 string
└──▶ PowerShell Script Builder (RMM)
reads: [remote_name, mount_path, include_install_flag]
produces: deploy-rmm.ps1 string
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
All strings → Download Manager
User selects files → individual download (Blob URL) or ZIP (JSZip)
```
## CSS Custom Properties Strategy for MD3 Color Tokens
Key invariant: builders are pure functions — same wizard state always produces the same file content. There is no side-effectful build step.
### 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.
## rclone.conf Format (INI-like)
```css
@import "tailwindcss";
**Confidence: HIGH** — rclone.conf format is stable and well-documented.
/* Enable class-based dark mode toggle */
@custom-variant dark (&:where(.dark, .dark *));
The rclone config file uses a simple INI-like format. Each remote is a named section.
/* --- 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;
```ini
[remote-name]
type = azureblob
account = mystorageaccount
key = base64encodedaccesskey==
```
/* Secondary */
--color-secondary: #5f6368;
--color-on-secondary: #ffffff;
--color-secondary-container: #e8eaed;
--color-on-secondary-container: #1f1f1f;
### Structure Rules
/* Tertiary (accent) */
--color-tertiary: #1a73e8;
--color-on-tertiary: #ffffff;
--color-tertiary-container: #d3e3fd;
--color-on-tertiary-container: #041e49;
- Section header: `[remote-name]` — any identifier the user chooses; appears in rclone commands as `remote-name:`
- Each key-value pair on its own line: `key = value` (spaces around `=` are conventional but optional)
- No quoting of values needed (rclone parses raw strings)
- Comments: lines starting with `#` or `;`
- Multiple remotes = multiple sections in the same file
/* Error */
--color-error: #dc3545;
--color-on-error: #ffffff;
--color-error-container: #f9dedc;
--color-on-error-container: #410e0b;
### Required Fields Per Backend
/* 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;
| Backend | `type` value | Minimum required fields | Common optional fields |
|---------|-------------|------------------------|------------------------|
| Azure Blob | `azureblob` | `account`, then one of: `key`, `sas_url`, or `client_id`+`client_secret`+`tenant` | `endpoint`, `chunk_size`, `upload_cutoff` |
| AWS S3 | `s3` | `provider = AWS`, `access_key_id`, `secret_access_key`, `region` | `storage_class`, `server_side_encryption` |
| S3-compatible | `s3` | `provider = Other`, `access_key_id`, `secret_access_key`, `endpoint` | varies by provider |
| OneDrive | `onedrive` | `client_id`, `client_secret`, `token` (OAuth flow) | `drive_id`, `drive_type` |
| SFTP | `sftp` | `host`, `user`, then one of `pass` or `key_file` | `port`, `use_insecure_cipher` |
| Google Drive | `drive` | `client_id`, `client_secret`, `token` (OAuth flow) | `team_drive`, `shared_with_me` |
/* Outline */
--color-outline: #dadce0;
--color-outline-variant: #e8eaed;
### Example Full Config (Azure Blob + S3)
/* 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);
```ini
[my-azure]
type = azureblob
account = contosostorage
key = dGhpcyBpcyBhIHBsYWNlaG9sZGVyIGtleQ==
/* MD3 Shape (border-radius) */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 28px;
}
[my-s3]
type = s3
provider = AWS
access_key_id = AKIAIOSFODNN7EXAMPLE
secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
region = us-east-1
```
/* --- Dark mode overrides --- */
@layer base {
.dark {
--color-primary: #a8c7fa;
--color-on-primary: #062e6f;
--color-primary-container: #0842a0;
--color-on-primary-container: #d3e3fd;
### Generation Pattern
--color-secondary: #c4c7c5;
--color-on-secondary: #303030;
--color-secondary-container: #444746;
--color-on-secondary-container: #e8eaed;
The rclone.conf builder is pure string templating:
--color-tertiary: #a8c7fa;
--color-on-tertiary: #062e6f;
--color-tertiary-container: #0842a0;
--color-on-tertiary-container: #d3e3fd;
```typescript
function buildRcloneConf(remotes: RemoteConfig[]): string {
return remotes.map(remote => {
const lines = [`[${remote.name}]`, `type = ${remote.type}`];
for (const [key, value] of Object.entries(remote.params)) {
if (value !== undefined && value !== '') {
lines.push(`${key} = ${value}`);
}
}
return lines.join('\n');
}).join('\n\n');
--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);
}
}
```
No library needed — plain string concatenation is the correct approach.
### 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`.
## Intune / RMM Deployment Patterns
### MD3 Color Roles Used (Practical Subset)
**Confidence: MEDIUM** — Intune Win32 app deployment is well-established; exact script conventions vary by org.
Full MD3 has 29+ color roles. For this wizard app, we use a practical subset:
### Intune Win32 App Approach
| 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 |
Intune Win32 apps require a `.intunewin` package, but PowerShell-only deployments (Intune PowerShell scripts feature) are simpler and sufficient for this use case.
### Current-to-Token Mapping
The generated script must:
Explicit mapping of every hardcoded color in the existing codebase:
1. (Optionally) download and install rclone — copy `rclone.exe` to a stable path (e.g., `C:\ProgramData\rclone\`)
2. Write `rclone.conf` to the user-appropriate path (`$env:APPDATA\rclone\rclone.conf` for per-user, or `C:\ProgramData\rclone\rclone.conf` for system-wide)
3. (Optionally) create a scheduled task or a startup script to mount the remote on login
4. Return exit code 0 on success; non-zero on failure (Intune reads exit codes)
| 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 |
### Intune Script Deployment Constraints
## ThemeProvider Architecture
- Scripts run as SYSTEM by default, or as logged-on user (configurable)
- Script must handle the case where rclone is already installed (idempotent)
- 64-bit PowerShell is required for `rclone.exe` (32-bit PS cannot run 64-bit binaries reliably)
- Scripts have a 30-minute execution timeout in Intune
- Output directory for config must account for execution context (SYSTEM vs user)
### Implementation
### RMM Script Approach (NinjaRMM, Datto, etc.)
```typescript
// src/store/theme-context.tsx
import { createContext, useContext, useEffect, useState, useCallback } from 'react';
Simpler than Intune Win32: paste PS script, run as SYSTEM or user. Same functional requirements as above but no `.intunewin` packaging. Script should be self-contained.
type Theme = 'light' | 'dark';
### PowerShell Script Structure
interface ThemeContextValue {
theme: Theme;
toggleTheme: () => void;
setTheme: (theme: Theme) => void;
}
```powershell
#Requires -RunAsAdministrator # or omit if running as user context
const ThemeContext = createContext<ThemeContextValue | null>(null);
$rclonePath = "C:\ProgramData\rclone"
$rcloneExe = "$rclonePath\rclone.exe"
$rcloneConf = "$rclonePath\rclone.conf"
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';
}
# --- Optional: Install rclone ---
# if ($InstallRclone) { ... download from $RcloneDownloadUrl ... }
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<Theme>(getInitialTheme);
# --- Write config ---
$confContent = @"
[remote-name]
type = azureblob
account = REPLACE_ME
key = REPLACE_ME
"@
const setTheme = useCallback((t: Theme) => {
setThemeState(t);
localStorage.setItem('r2b-theme', t);
document.documentElement.classList.toggle('dark', t === 'dark');
}, []);
New-Item -ItemType Directory -Force -Path $rclonePath | Out-Null
Set-Content -Path $rcloneConf -Value $confContent -Encoding UTF8
const toggleTheme = useCallback(() => {
setTheme(theme === 'dark' ? 'light' : 'dark');
}, [theme, setTheme]);
# --- Optional: Register mount as scheduled task ---
# ...
// Sync on mount
useEffect(() => {
document.documentElement.classList.toggle('dark', theme === 'dark');
}, []);
exit 0
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;
}
```
The PS script builder generates this structure by substituting values from wizard state into a template string.
### 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: Centralized Wizard State (Single Store)
### Pattern 1: Semantic Color Tokens Only
**What:** All form data lives in one top-level state object, passed down or accessed via context/store. Steps read from and write to slices of this store.
**What:** Never use raw Tailwind color classes (`blue-600`, `gray-50`) in components. Always use semantic MD3 token names.
**When:** Any multi-step form where later steps depend on earlier choices (e.g., backend type selection in step 1 drives which fields appear in step 2).
**When:** Every component, every color reference.
**Why:** Avoids prop-drilling, makes "go back and edit" trivial, makes config builders pure functions with a single well-typed input.
**Why:** Semantic tokens automatically adapt to dark mode via CSS variable override. Raw colors would need manual `dark:` overrides on every single usage.
**Shape:**
```typescript
interface WizardState {
currentStep: number;
remote: {
name: string;
backendType: BackendType;
params: Record<string, string>; // backend-specific key/value pairs
};
deployment: {
includeInstall: boolean;
installSource: 'github' | 'custom';
customInstallUrl?: string;
mountPath?: string;
scriptTargets: ('intune' | 'rmm')[];
};
outputOptions: {
includeConf: boolean;
includeIntune: boolean;
includeRmm: boolean;
bundleAsZip: boolean;
};
**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 2: Backend Schema Registry
### Pattern 4: MD3 Elevation via Shadow Tokens
**What:** A static data structure (not code) that defines, per backend type, which fields are required, their labels, input types, placeholder text, and validation rules.
**What:** Use the `shadow-elevation-*` tokens for card depth instead of arbitrary shadow utilities.
**When:** The wizard needs to render dynamic forms based on which rclone backend the user selected.
**When:** Cards, modals, dropdowns -- any elevated surface.
**Why:** Adding support for a new backend means adding one entry to the registry, not writing new UI components. Keeps UI code backend-agnostic.
**Example:**
```tsx
// Card at rest
<div className="bg-surface-container rounded-lg shadow-elevation-1">
**Shape:**
```typescript
interface FieldDef {
key: string; // matches rclone config key exactly
label: string;
inputType: 'text' | 'password' | 'select' | 'toggle';
required: boolean;
placeholder?: string;
helpText?: string;
options?: { value: string; label: string }[]; // for select
}
type BackendSchema = Record<BackendType, FieldDef[]>;
// Card hovered / elevated
<div className="bg-surface-container rounded-lg shadow-elevation-2 hover:shadow-elevation-3">
```
### Pattern 3: Pure Builder Functions
**What:** Config and script builders are pure functions — they receive wizard state and return a string. No side effects, no DOM access, no async.
**When:** Always — this is the core generation logic.
**Why:** Easily testable (unit test: given state X, output matches expected string). Deterministic. Separates concerns cleanly.
### Pattern 4: Blob URL Download
**What:** To trigger a file download in the browser without a server, create a `Blob` from the string content, generate an object URL, attach it to an `<a>` element, and programmatically click it.
**When:** Single file download.
```typescript
function downloadTextFile(filename: string, content: string): void {
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
```
**Confidence: HIGH** — Blob URL download is a standard, well-supported browser API (all modern browsers, no library needed for single files).
### Pattern 5: ZIP Bundle via JSZip
**What:** When the user wants all generated files in one download, use JSZip to assemble a ZIP in-memory and trigger download.
**When:** Multi-file download (rclone.conf + one or more .ps1 files).
```typescript
import JSZip from 'jszip';
async function downloadZip(files: { name: string; content: string }[]): Promise<void> {
const zip = new JSZip();
files.forEach(f => zip.file(f.name, f.content));
const blob = await zip.generateAsync({ type: 'blob' });
downloadTextFile('ready2blob-deployment.zip', URL.createObjectURL(blob));
}
```
**Confidence: HIGH** — JSZip is the established library for client-side ZIP creation. FileSaver.js is an optional companion for older browser compatibility but not required with the Blob URL pattern above.
---
## Anti-Patterns to Avoid
### Anti-Pattern 1: Per-Step Local State
### Anti-Pattern 1: Dual-Track Color System
**What:** Each wizard step manages its own form state with no shared store.
**What:** Keeping some components on raw Tailwind colors while migrating others to MD3 tokens.
**Why bad:** When the user navigates back to step 2 from step 4, their inputs are gone. The config builder cannot access step 2 data from step 4. Breaks the "review before download" pattern.
**Why bad:** Dark mode will be broken on non-migrated components. Visual inconsistency. Harder to maintain.
**Instead:** Lift all state to the wizard root. Steps only control their own UI (focus, error display), not their data.
**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: Generating Files Only at Download Time from DOM
### Anti-Pattern 2: JS-Driven Theme Prop Drilling
**What:** Reading form field values directly from the DOM to build the config string at download time.
**What:** Passing `isDark` or `theme` as a prop to every component and using ternaries to pick colors.
**Why bad:** Bypasses validation, couples the builder to the DOM structure, cannot unit-test without a browser. Fragile.
**Why bad:** Massive prop threading, re-renders on theme change, duplicated color logic.
**Instead:** Always read from wizard state, which is the validated, typed representation of user input.
**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: Hardcoding Backend Fields in Step Components
### Anti-Pattern 3: Using @material/web Components
**What:** Writing a dedicated `<AzureBlobStep />`, `<S3Step />`, `<OneDriveStep />` component for every backend.
**What:** Importing Google's Material Web Components (`<md-button>`, `<md-text-field>`).
**Why bad:** Adding a new backend requires a new component. Does not scale. Leads to duplication of validation logic.
**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:** Use the Backend Schema Registry to drive a single generic `<DynamicBackendStep />` that renders fields from schema definitions.
**Instead:** Implement MD3's *visual language* (colors, elevation, shape, typography) via CSS tokens + Tailwind utilities on standard React elements.
### Anti-Pattern 4: Storing Secrets Beyond Session
### Anti-Pattern 4: Big-Bang Step Rewrite
**What:** Persisting wizard state to `localStorage`, `sessionStorage`, or any cache.
**What:** Rewriting entire wizard step components at once with new UI.
**Why bad:** Credentials (storage keys, SAS tokens) would persist on the machine after the browser tab is closed. Security risk explicitly called out in project constraints.
**Why bad:** Breaks multiple tests simultaneously. Hard to isolate regressions. Merge conflicts if concurrent work.
**Instead:** Wizard state lives only in in-memory React/Vue/Svelte state. Closing the tab is the only "logout".
**Instead:** Layer-by-layer approach. CSS tokens first (zero breakage), then primitives (additive), then swap in composed components, then layout.
### Anti-Pattern 5: Using a Backend for File Generation
## Impact on Existing Test Suite (159 Tests)
**What:** Sending form data to a server to generate files, which returns them as downloads.
### Test Categories and Impact Assessment
**Why bad:** Server receives credentials in plaintext. Violates the project's explicit no-backend constraint. Creates data retention risk.
| 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 |
**Instead:** All generation is client-side (builders are pure TS/JS functions).
**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.
## Suggested Build Order (Component Dependencies)
**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).
Dependencies flow from foundational to dependent. Build in this order:
## Suggested Build Order (Minimizes Breakage)
```
Phase 1 — Foundation
└── Wizard State shape definition (TypeScript types + store setup)
└── Backend Schema Registry (static data, no UI)
### Phase 1: CSS Foundation (Zero Test Impact)
Phase 2 — Core Generators (no UI needed yet, fully testable)
└── rclone.conf Builder (pure function, unit-testable immediately)
└── PowerShell Script Builder — Intune variant
└── PowerShell Script Builder — RMM variant
└── Download Manager (Blob URL + JSZip wrapper)
**What:** Set up MD3 tokens and dark mode infrastructure in `index.css`. No component changes.
Phase 3 — Wizard UI Shell
└── Step navigation (stepper, next/back, step completion tracking)
└── Wizard State wired to UI (reads/writes)
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)
Phase 4 — Dynamic Step Forms
└── Backend type selector (step 1)
└── Dynamic backend fields step (step 2, driven by Schema Registry)
└── Deployment options step (step 3)
└── Review + Download step (step 4, calls builders + download manager)
**Test impact:** Zero -- existing tests pass unchanged. Add new unit tests for ThemeProvider.
Phase 5 — Polish
└── Per-step validation with user-visible errors
└── Security warning modal before download
└── Preview pane (show generated file content before download)
```
**Dependency:** None. Can start immediately.
**Rationale for this order:**
### Phase 2: Primitive Extraction (Additive Only)
- Builders and the schema registry have zero UI dependencies — build and test them first
- The wizard UI shell (navigation only) can be built against mock/empty state
- Dynamic forms are built last because they depend on both state wiring AND the schema registry being final
- The download step can only be meaningfully built once all builders exist
**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 | At MVP (5 backends) | At Growth (20+ backends) | Notes |
|---------|---------------------|--------------------------|-------|
| Backend support | Hardcode schema for top 5 | Schema registry makes adding trivial | Registry pattern is the key enabler |
| Bundle size | Single JS bundle is fine | Consider lazy-loading backend schemas | Each schema is tiny; not a real concern until 50+ backends |
| State complexity | Flat wizard state struct | No change needed | Wizard is inherently linear; state stays simple |
| Testing | Unit tests on builders | Add snapshot tests for generated files | Builders are pure functions — easiest thing to test |
| Localization | Not needed v1 | Field labels/help text in schema enables i18n | Plan label strings as separate keys in schema if i18n is future |
---
## Key Technical Decisions
| Decision | Recommended Choice | Rationale |
|----------|-------------------|-----------|
| State management | React Context + useReducer OR Zustand (lightweight store) | No server state; no need for React Query or Redux. Zustand reduces boilerplate vs Context for this use case. |
| ZIP library | JSZip v3 | De facto standard for browser ZIP. No alternatives with meaningful adoption. |
| File download | Native Blob URL API | No library needed. FileSaver.js unnecessary for modern browsers. |
| Config generation | Plain string templates (template literals) | rclone.conf is simple enough that a template engine adds no value. |
| Script generation | Template literal function per script type | Same reasoning. Mustache/Handlebars would be overkill. |
| Backend schema | Static TypeScript object (no database, no fetch) | Schemas are known at build time. Static data = zero loading time. |
---
| 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
- rclone.conf format: training data (stable since rclone v1.x; format has not changed); confidence HIGH
- Azure Blob rclone backend fields: training data; confidence MEDIUM (specific field names should be cross-checked against https://rclone.org/azureblob/ before implementing the schema registry)
- Intune PowerShell script deployment: training data; confidence MEDIUM (execution context and timeout limits are well-documented but should be verified for current Intune behavior)
- JSZip client-side ZIP: training data; confidence HIGH (library API is stable)
- Blob URL download pattern: training data (standard browser API, MDN-documented); confidence HIGH
- Web search and WebFetch unavailable during this research session — claims marked MEDIUM should be verified against official docs during implementation phases
- [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