From 4d4b01d9b7b2cd10e806e1a8fa3d88b205ef269e Mon Sep 17 00:00:00 2001 From: Kawa Date: Tue, 31 Mar 2026 15:34:46 +0200 Subject: [PATCH] =?UTF-8?q?docs(v1.2):=20complete=20project=20research=20?= =?UTF-8?q?=E2=80=94=20stack,=20features,=20architecture,=20pitfalls,=20su?= =?UTF-8?q?mmary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- .planning/research/ARCHITECTURE.md | 811 +++++++++++++++++------------ .planning/research/FEATURES.md | 280 ++++++---- .planning/research/PITFALLS.md | 476 ++++++++--------- .planning/research/STACK.md | 305 +++++++---- .planning/research/SUMMARY.md | 267 ++++------ 5 files changed, 1198 insertions(+), 941 deletions(-) diff --git a/.planning/research/ARCHITECTURE.md b/.planning/research/ARCHITECTURE.md index 03860e4..67482c6 100644 --- a/.planning/research/ARCHITECTURE.md +++ b/.planning/research/ARCHITECTURE.md @@ -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 + | + +---------------------+ + | 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 | `` 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(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(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 ( + + {children} + + ); +} + +export function useTheme(): ThemeContextValue { + const ctx = useContext(ThemeContext); + if (!ctx) throw new Error('useTheme must be used inside '); + 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 ( + + + + + + ); +} +``` + +**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; // 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 + + +// AFTER: Reusable Button primitive + + + +``` + +### 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( + + + {ui} + + + ); } ``` -### 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 +
-**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; +// Card hovered / elevated +
``` -### 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 `` 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 { - 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 ``, ``, `` component for every backend. +**What:** Importing Google's Material Web Components (``, ``). -**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 `` 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 `
`), tests that use `container.querySelector` or rely on specific nesting could break. Mitigation: ensure new primitives produce the same semantic DOM (same ``, `` with static labels above looks dated. | MEDIUM | MD3 "outlined" style: 1px border that highlights on focus, label that floats into the border on focus/fill. Requires CSS-only animation or a small wrapper component. Tailwind v4 `@theme` tokens map well to MD3 color roles. | +| **MD3 button hierarchy (filled, outlined, text)** | Users expect visual hierarchy between primary actions (Next, Download) and secondary (Back, Copy). Current buttons use ad-hoc blue/gray with no consistent system. | LOW | Three tiers: filled (primary actions), outlined (secondary/back), text (tertiary). Use MD3 shape tokens (rounded-xl for buttons). Map to `--md-sys-color-primary` and `--md-sys-color-on-primary`. | +| **MD3 card components with elevation** | Backend selection cards and output blocks need consistent elevation, padding, and shape. Current cards have inconsistent border-only styling. | LOW | MD3 elevation uses tonal surface tint (not just shadows). Levels 0-5. Cards typically at level 1 (subtle tint + minimal shadow). Selected state bumps to level 2. | +| **Dark mode** | 87% of users prefer apps that support automatic theme switching. A dev/IT tool without dark mode feels incomplete. | MEDIUM | Three-state toggle: System / Light / Dark. Respect `prefers-color-scheme` as default. Persist choice in `localStorage`. Implement via `data-theme` attribute on `` + CSS custom properties. Tailwind v4 `dark:` variant works with `@media (prefers-color-scheme: dark)` or class strategy. | +| **Consistent color token system** | Hardcoded `text-gray-700`, `bg-blue-600`, `border-red-500` scattered across components creates maintenance burden and makes theming impossible. | MEDIUM | Define MD3 color roles as CSS custom properties: `--color-primary`, `--color-on-primary`, `--color-surface`, `--color-on-surface`, `--color-error`, etc. All components reference tokens, not raw Tailwind colors. Enables dark mode and accent colors with a single layer of indirection. | +| **Proper step indicator / progress bar** | Current StepIndicator uses inline styles, plain text with ">" separators, and checkbox emoji. Looks unprofessional. | MEDIUM | MD3 stepper pattern: numbered circles connected by lines. Completed steps show checkmark icon in filled circle. Active step is highlighted with primary color. Future steps are muted. Must show step labels and be responsive (collapse labels on mobile, show numbers only). | +| **Responsive layout (mobile-friendly forms)** | IT admins use phones/tablets for quick reference or field work. Current `max-w-2xl` centered layout does not adapt form fields for small screens. | MEDIUM | Backend cards: grid `grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`. Form fields: full-width on mobile, optionally 2-column on wide screens for short fields. Step indicator: horizontal on desktop, compact (numbers-only or dots) on mobile. Buttons: full-width on mobile. | +| **Form field error states (MD3 style)** | Current red border + small text is functional but not MD3-compliant. MD3 errors use the error color role, an error icon, and supporting text below the field. | LOW | Error state: border changes to `--color-error`, label text changes to error color, trailing error icon appears, supporting text shows error message in error color. Already have error messages; need to style them consistently. | +| **Step-level descriptions** | Each wizard step needs a brief explanation of what the user is doing and why. Current steps jump straight to form fields with only "Step N: Title". | LOW | 1-2 sentence description below each step heading. Examples: "Choose your cloud storage provider. This determines which credentials you will need." Static text, no logic. | +| **Accessible focus states** | EAA compliance (June 2025) requires visible focus indicators for keyboard navigation. Current `:focus` uses `ring-2` which is minimal. | LOW | MD3 focus: 3px outline using `--color-primary` with 2px offset. All interactive elements (buttons, inputs, cards, links) must have visible focus. Tailwind `focus-visible:` variant preferred over `focus:` to avoid showing focus on mouse click. | -## Differentiators +### Differentiators (Competitive Advantage) -Features that set Ready2Blob apart from "just read the rclone docs" or copy-pasting PS scripts from Reddit. +Features that elevate Ready2Blob from "functional tool" to "tool that inspires confidence." | Feature | Value Proposition | Complexity | Notes | |---------|-------------------|------------|-------| -| Intune-specific detection script generation | Intune Win32 apps require a separate detection script (exit 0 = installed); most admins copy-paste wrong ones | Medium | Detect by checking rclone.exe presence at install path AND config file presence. Both must exist | -| Intune packaging hints / IntuneWinAppUtil guidance | After generating scripts, show admin the exact IntuneWinAppUtil command to wrap the installer | Low | Static text block, not dynamic generation — but reduces a common stumbling point | -| RMM-specific script variants | NinjaRMM, Datto, and ConnectWise have slightly different execution contexts (SYSTEM vs user, working dir) | High | Start with a generic "SYSTEM context" PS script that works across RMMs; add named variants later | -| Rclone version pinning | MSP environments require reproducible deployments; "latest" is not acceptable for production | Low | Text input: "Pin to rclone version" (e.g., `v1.68.2`). Defaults to latest stable. Affects download URL in script | -| Config placement path options | Config can go to `%APPDATA%\rclone\rclone.conf` (user) or a machine-wide path. Intune SYSTEM context needs machine-wide | Medium | Dropdown: User profile path vs machine-wide path (`C:\ProgramData\rclone\`). Explain implications of each | -| Multiple remotes in one config | A single rclone.conf can contain multiple named remotes; some orgs need 2-3 backends on same endpoint | High | Allow "Add another remote" in wizard. Generates a single .conf with multiple sections | -| Live config preview | Admin sees the exact text of generated files before downloading — builds trust, catches errors | Low | Syntax-highlighted read-only textarea. Updates in real time as form fields change | -| Copy-to-clipboard for each output | Some RMM tools have a "run script" field — paste directly without downloading a file | Low | Copy button beside each output block | -| Field-level validation with rclone-specific rules | Azure Blob storage account names are 3-24 lowercase alphanumeric chars — catch this before the admin deploys a broken config | Medium | Per-field regex/rule validation. Reduces "why doesn't rclone connect?" support tickets | -| Explanatory tooltips on sensitive fields | "What is an SAS token vs an Access Key?" — admins often don't know which credential type to use | Low | Tooltip or inline help text per field. Reduces abandonment from confusion | -| Backend popularity ordering | Show Azure Blob, S3, OneDrive, SFTP, GCS at top — don't bury them alphabetically | Low | Simple UX decision with high impact on time-to-task-complete | +| **User-selectable accent color** | Personalizes the tool. IT admins often match internal tools to company brand colors. Also demonstrates the token system works. | MEDIUM | Offer 5-8 preset accent colors (blue default, teal, purple, green, orange, pink). User picks one, CSS custom properties update for `--color-primary` and its derivatives. Persist in `localStorage`. Use MD3 tonal palette generation: from one source color, derive on-primary, primary-container, on-primary-container. | +| **App intro / landing section** | First-time visitors need to understand what Ready2Blob does before diving into the wizard. Current app jumps straight to Step 1 with no context. | LOW | Hero section above the wizard: app name, one-sentence value prop ("Generate rclone configs and deployment scripts for Windows endpoints"), 3-4 feature bullets (client-side only, 7 backends, Intune + RMM scripts), and a "Get Started" button that scrolls to or reveals the wizard. Collapses/hides once wizard is started. | +| **Animated step transitions** | Smooth transitions between wizard steps make the app feel responsive and polished rather than jarring page swaps. | LOW | CSS-only fade or slide transition on step change. Use `opacity` + `transform: translateX()` with 200ms ease. Keep it subtle -- this is a productivity tool, not a marketing site. Respect `prefers-reduced-motion`. | +| **Contextual help popovers (upgraded)** | Current tooltip toggle (click to show/hide inline text) works but is basic. MD3-style popovers with arrow indicators and rich content feel more professional. | MEDIUM | Replace inline toggle with a proper popover component: positioned above/below the trigger, with arrow, dismissed on outside click or Escape. Still click-triggered (not hover -- touch devices need click). Consider using Floating UI (lightweight, ~3KB) for positioning math. | +| **Remote name field clarity** | The "Remote name" field confuses users who do not know rclone conventions. Needs prominent explanation, examples, and format hint. | LOW | Add: placeholder "e.g., corp-backup", help text explaining "This name identifies the storage target in rclone commands and scripts", and an inline example showing how it appears in the generated config `[corp-backup]`. Already partially addressed in v1.1 tooltips; needs more prominent treatment. | +| **MD3 select/dropdown styling** | Native `` associations, error message display, and tooltip buttons with `aria-label`. During restyling, these connections break: labels get separated from inputs by decorative wrapper divs, error messages lose their visual proximity, or card wrappers introduce unexpected tab stops. **Why it happens:** -The Intune "Run in 64-bit" option defaults to `No` per Microsoft docs. Developers test in a normal 64-bit PowerShell session. +Visual-focused restyling treats JSX as a canvas for layout. Developers restructure DOM for card layouts, add icon containers, or wrap form groups in Material-style "outlined" containers. The label-input-error chain relies on specific DOM relationships. The FieldRenderer currently does NOT use `aria-describedby` for error messages -- this is already noted as v1.1 tech debt. Restyling is the right time to fix this, but also the highest risk time to break what works. -**Consequences:** -- rclone binary installed to wrong Program Files variant -- PATH entries or shortcuts point to non-existent location +**How to avoid:** +1. Fix the `aria-describedby` gap as part of the restyling, not separately. Add `aria-describedby={error ? \`${field.key}-error\` : undefined}` to inputs and `id={\`${field.key}-error\`}` to error paragraphs. +2. After restyling each form component, verify: label click focuses the input, error messages are associated with inputs, tab order follows visual order. +3. Keep the `