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:
+482
-329
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user