# Phase 1: Foundation - Research **Researched:** 2026-03-26 **Domain:** React + TypeScript SPA scaffold, Backend Schema Registry, Zod validation, useReducer wizard state **Confidence:** HIGH --- ## Summary Phase 1 builds the architectural skeleton that every downstream phase depends on. There are four concrete deliverables: (1) a Vite + React + TypeScript dev server that runs cleanly, (2) a Backend Schema Registry defining the field structure for Azure Blob, S3, and S3-compatible remotes, (3) Zod schemas derived from that registry for runtime validation, and (4) a WizardState useReducer store wired via Context so any component can read or dispatch to it. This phase has no novel research challenges. The technology choices are industry-standard and well-documented. The main research value here is locking in current version numbers and noting the two significant ecosystem changes since the project's training-data baseline: Tailwind CSS moved to v4 (CSS-first, no tailwind.config.js, new Vite plugin), and the shadcn CLI was renamed from `shadcn-ui` to `shadcn`. Getting these wrong at scaffold time would require painful migration later. The Backend Schema Registry is the most architecturally critical deliverable. It is the single source of truth that drives dynamic form rendering (Phase 3), Zod schema construction (this phase), and rclone.conf key/value generation (Phase 2). Every backend field key in the registry must exactly match rclone's config key names — wrong keys produce silently broken configs. For Phase 1, the registry only needs to cover Azure Blob, S3, and S3-compatible backends (as required by the success criteria); exact rclone field name verification against live rclone.org docs is flagged as a Phase 2 concern but should be reviewed here to avoid a schema rewrite. **Primary recommendation:** Scaffold with `npm create vite@latest` (create-vite v9, produces Vite 6), install Tailwind v4 via the `@tailwindcss/vite` plugin (NOT the v3 PostCSS path), use `npx shadcn@latest init` (NOT the old `shadcn-ui` package), and pin Zod at v4.x with `@hookform/resolvers` v5.x for stable Zod v4 compatibility. --- ## Standard Stack ### Core | Library | Version | Purpose | Why Standard | |---------|---------|---------|--------------| | React | 18.x | Component model, hooks, rendering | Largest ecosystem; useReducer + useContext is exactly the right mental model for wizard state | | Vite | 6.x | Dev server, HMR, static build | create-vite v9 produces Vite 6; replaces deprecated CRA; `vite build` outputs a static `dist/` folder | | TypeScript | 5.x | Type safety | Wrong field name in registry = silently broken rclone config; TS catches this at compile time | | Zod | 4.x | Schema definition and runtime validation | v4 released May 2025; 14x faster parsing, 57% smaller core; production-stable as of v4.1.x+ | | react-hook-form | 7.x | Per-step form validation | Uncontrolled inputs (no re-render per keystroke); native Zod v4 integration via @hookform/resolvers v5 | | @hookform/resolvers | 5.x | Bridges react-hook-form with Zod v4 | v5.2.2+ supports Zod v3.25+ and v4.0+; automatic runtime schema detection | ### Styling and UI | Library | Version | Purpose | When to Use | |---------|---------|---------|-------------| | Tailwind CSS | 4.x | Utility-first CSS | CSS-first config (no tailwind.config.js); use `@tailwindcss/vite` plugin for Vite integration | | shadcn/ui | CLI v4 (March 2026) | Accessible form component library | Code-generator, not a dependency; `npx shadcn@latest init` copies Radix UI-backed components into project | ### Supporting | Library | Version | Purpose | When to Use | |---------|---------|---------|-------------| | Vitest | 2.x | Unit testing | Same Vite config, zero extra setup; used to test pure functions (Zod schema validation correctness) | | @testing-library/react | 16.x | React component testing | For testing Context-wired components in Phase 3+ | ### Alternatives Considered | Instead of | Could Use | Tradeoff | |------------|-----------|----------| | Zod v4 | Zod v3 | v3 is still widely used but v4 is production-stable and the resolver is confirmed compatible; no reason to start a new project on v3 | | @tailwindcss/vite plugin | @tailwindcss/postcss | Both work; Vite plugin is the simpler path for a Vite project (no postcss.config.js needed) | | shadcn/ui | Raw Tailwind + HTML | shadcn provides accessible Radix primitives; saves building focus/keyboard nav from scratch | | useReducer + Context | Zustand | Zustand reduces boilerplate; at this scale useReducer is sufficient and adds zero dependencies | **Installation (scaffold + Phase 1 deps):** ```bash # Scaffold npm create vite@latest ready2blob -- --template react-ts cd ready2blob # Tailwind v4 via Vite plugin (NOT the v3 PostCSS path) npm install -D tailwindcss @tailwindcss/vite # Zod v4 + react-hook-form v7 + resolvers v5 npm install zod react-hook-form @hookform/resolvers # Testing (dev only) npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/dom # shadcn/ui (run interactively, copies components into src/components/ui/) npx shadcn@latest init ``` --- ## Architecture Patterns ### Recommended Project Structure ``` src/ ├── schemas/ # Backend Schema Registry + derived Zod schemas │ ├── registry.ts # BackendSchema: BackendType → FieldDef[] │ ├── azure.ts # Zod schema for Azure Blob fields │ ├── s3.ts # Zod schema for S3 / S3-compatible fields │ └── index.ts # Re-exports ├── store/ # WizardState useReducer + Context │ ├── types.ts # WizardState interface, Action union type │ ├── reducer.ts # Pure reducer function │ └── context.tsx # WizardContext + WizardProvider + useWizard hook ├── components/ │ └── ui/ # shadcn/ui generated components (Button, Input, etc.) ├── App.tsx └── main.tsx ``` ### Pattern 1: Backend Schema Registry **What:** A static TypeScript object that maps each backend type to an ordered list of field definitions. Each FieldDef contains the exact rclone config key, display label, input type, required flag, placeholder, and optional help text. **When to use:** Drives dynamic form rendering in Phase 3 and Zod schema construction in this phase. Adding a new backend = one new entry in the registry, zero UI changes. **Example:** ```typescript // src/schemas/registry.ts export type BackendType = 'azureblob' | 's3' | 's3-compatible'; export interface FieldDef { key: string; // MUST match rclone config key exactly label: string; inputType: 'text' | 'password' | 'select' | 'toggle'; required: boolean; placeholder?: string; helpText?: string; options?: { value: string; label: string }[]; // for select inputs } export const BACKEND_REGISTRY: Record = { azureblob: [ { key: 'account', label: 'Storage Account Name', inputType: 'text', required: true, placeholder: 'mystorageaccount' }, { key: 'key', label: 'Access Key', inputType: 'password', required: false, helpText: 'Base64-encoded storage account key' }, { key: 'sas_url', label: 'SAS URL', inputType: 'password', required: false, helpText: 'Full SAS URL including account and container' }, ], s3: [ { key: 'provider', label: 'Provider', inputType: 'select', required: true, options: [{ value: 'AWS', label: 'Amazon S3' }] }, { key: 'access_key_id', label: 'Access Key ID', inputType: 'text', required: true }, { key: 'secret_access_key', label: 'Secret Access Key',inputType: 'password', required: true }, { key: 'region', label: 'Region', inputType: 'text', required: true, placeholder: 'us-east-1' }, ], 's3-compatible': [ { key: 'provider', label: 'Provider', inputType: 'select', required: true, options: [{ value: 'Other', label: 'S3-Compatible' }] }, { key: 'access_key_id', label: 'Access Key ID', inputType: 'text', required: true }, { key: 'secret_access_key', label: 'Secret Access Key',inputType: 'password', required: true }, { key: 'endpoint', label: 'Endpoint URL', inputType: 'text', required: true, placeholder: 'https://s3.wasabisys.com' }, { key: 'region', label: 'Region', inputType: 'text', required: false }, ], }; ``` ### Pattern 2: Zod Schemas Derived from the Registry **What:** Zod schemas are built programmatically from FieldDef arrays, not written by hand. This ensures schema and registry never drift apart. **Example:** ```typescript // src/schemas/index.ts import { z } from 'zod'; import { BACKEND_REGISTRY, BackendType } from './registry'; function buildZodSchema(backendType: BackendType) { const fields = BACKEND_REGISTRY[backendType]; const shape: Record = {}; for (const field of fields) { shape[field.key] = field.required ? z.string().min(1, `${field.label} is required`) : z.string().optional(); } return z.object(shape); } export const BACKEND_SCHEMAS = { azureblob: buildZodSchema('azureblob'), s3: buildZodSchema('s3'), 's3-compatible': buildZodSchema('s3-compatible'), } as const; export type BackendFormValues = z.infer; ``` ### Pattern 3: WizardState with useReducer + Context **What:** A single top-level state object holds all wizard data. useReducer handles state transitions. Context exposes `state` and `dispatch` to all components without prop drilling. **When to use:** Always — per-step local state causes data loss on back-navigation. **State shape:** ```typescript // src/store/types.ts export type BackendType = 'azureblob' | 's3' | 's3-compatible'; export interface WizardState { currentStep: number; remote: { name: string; backendType: BackendType | null; params: Record; // backend-specific key/value pairs }; deployment: { includeInstall: boolean; configPath: 'machine-wide' | 'user-profile'; // machine-wide = C:\ProgramData\rclone\ scriptTargets: ('intune' | 'rmm')[]; }; } export type WizardAction = | { type: 'SET_STEP'; payload: number } | { type: 'SET_BACKEND_TYPE'; payload: BackendType } | { type: 'SET_REMOTE_NAME'; payload: string } | { type: 'SET_REMOTE_PARAMS'; payload: Record } | { type: 'SET_DEPLOYMENT'; payload: Partial } | { type: 'RESET' }; export const INITIAL_STATE: WizardState = { currentStep: 0, remote: { name: '', backendType: null, params: {} }, deployment: { includeInstall: false, configPath: 'machine-wide', scriptTargets: ['intune', 'rmm'] }, }; ``` **Context setup:** ```typescript // src/store/context.tsx import React, { createContext, useContext, useReducer } from 'react'; import { WizardState, WizardAction, INITIAL_STATE } from './types'; import { wizardReducer } from './reducer'; interface WizardContextValue { state: WizardState; dispatch: React.Dispatch; } const WizardContext = createContext(null); export function WizardProvider({ children }: { children: React.ReactNode }) { const [state, dispatch] = useReducer(wizardReducer, INITIAL_STATE); return ( {children} ); } export function useWizard(): WizardContextValue { const ctx = useContext(WizardContext); if (!ctx) throw new Error('useWizard must be used inside WizardProvider'); return ctx; } ``` ### Pattern 4: Tailwind v4 Vite Setup **What:** Tailwind v4 uses a Vite plugin instead of PostCSS + tailwind.config.js. CSS config lives in the CSS file itself. **vite.config.ts:** ```typescript import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ plugins: [react(), tailwindcss()], }); ``` **src/index.css (or src/app.css):** ```css @import "tailwindcss"; ``` That is the entire Tailwind setup. No `tailwind.config.js`, no `postcss.config.js`, no content globs. ### Anti-Patterns to Avoid - **Per-step local state:** Each wizard step managing its own useState loses data on back-navigation. All form data must go into the centralized WizardState store. - **Hardcoding backend fields in components:** Writing a dedicated `` component with hardcoded field names bypasses the registry pattern. One generic `` driven by the registry is the correct shape — even though it is built in Phase 3. - **Writing Zod schemas by hand separate from the registry:** Schema and registry will drift. Derive Zod schemas programmatically from FieldDef arrays. - **Using the old shadcn-ui CLI:** `npx shadcn-ui@latest init` is the old package name. Use `npx shadcn@latest init`. The old command will fail silently or install the wrong version. - **Using Tailwind v3 setup instructions:** v3 required `tailwindcss init -p`, PostCSS config, and content globs. v4 is completely different. Do not follow v3 guides. --- ## Don't Hand-Roll | Problem | Don't Build | Use Instead | Why | |---------|-------------|-------------|-----| | Form validation per step | Custom validation logic in component | Zod + @hookform/resolvers | Schema-driven validation; handles async, nested objects, cross-field rules, error messages | | Accessible form inputs (focus, ARIA, keyboard nav) | Custom input components | shadcn/ui (code-generated) | Radix UI primitives cover all accessibility requirements; hand-rolled inputs consistently fail keyboard and screen-reader tests | | State management boilerplate | Custom pub/sub or event bus | useReducer + Context | React's built-in, well-understood, zero-dependency pattern for this exact use case | | Schema-to-TypeScript type inference | Manual type definitions for form values | `z.infer` | Zod generates TypeScript types from schemas; single source of truth | --- ## Common Pitfalls ### Pitfall 1: Tailwind v4 vs v3 setup confusion **What goes wrong:** Developer follows a Tailwind v3 guide (first Google result for "Tailwind CSS Vite setup") — creates `tailwind.config.js`, runs `npx tailwindcss init -p`, installs `postcss` and `autoprefixer`. None of this is needed or correct for v4. **Why it happens:** Tailwind v4 was released January 2025. Most tutorials and Stack Overflow answers indexed before that date describe v3. Even 2025-dated articles may describe the v3 flow. **How to avoid:** Install `@tailwindcss/vite` (not `tailwindcss postcss autoprefixer`). Add the Vite plugin. Add `@import "tailwindcss"` to the CSS entry point. Done. **Warning signs:** If your setup involves `postcss.config.js` or `tailwind.config.js` or `content: [...]` globs, you are following a v3 guide. ### Pitfall 2: Old shadcn CLI package name **What goes wrong:** `npx shadcn-ui@latest init` fails or installs an outdated package. The CLI was renamed from `shadcn-ui` to `shadcn`. **How to avoid:** Use `npx shadcn@latest init`. As of March 2026 the CLI is at v4 and supports Vite scaffolding natively. ### Pitfall 3: Zod v4 beta incompatibility with @hookform/resolvers **What goes wrong:** Zod v4 beta versions (pre-v4.1.x) throw `ZodError` directly instead of capturing in `formState.errors`. With @hookform/resolvers v5, errors do not appear in the form. **How to avoid:** Pin Zod at v4.1.x or later (latest stable as of research: 4.3.6). The issue is in betas only; stable v4 works correctly with @hookform/resolvers v5.2.2+. ### Pitfall 4: Registry field keys not matching rclone config keys **What goes wrong:** The registry uses `storageAccountName` (camelCase) instead of `account` (rclone's actual key). Phase 2's `buildRcloneConf()` uses registry keys to build the INI file. Wrong keys produce a config that parses but silently fails to authenticate. **Why it happens:** Developer guesses field names without checking rclone docs. **How to avoid:** For the three backends in Phase 1's success criteria: - Azure Blob: type = `azureblob`, credential field = `account` (account name) + either `key` (access key) or `sas_url` (SAS URL) - S3: type = `s3`, fields = `provider` (set to `AWS`), `access_key_id`, `secret_access_key`, `region` - S3-compatible: type = `s3`, fields = `provider` (set to `Other`), `access_key_id`, `secret_access_key`, `endpoint` (required), `region` (optional) These keys are from training data (MEDIUM confidence). Verify against https://rclone.org/azureblob/ and https://rclone.org/s3/ before implementing Phase 2 generators. ### Pitfall 5: Persisting WizardState to localStorage **What goes wrong:** Developer adds `localStorage.setItem` to the reducer or a `useEffect` to "preserve state on refresh." This is a hard security requirement violation — credentials (storage keys, SAS tokens) must never be written to browser storage. **Why it matters:** SECU-03 explicitly prohibits this. Any persistence means credentials survive after the browser tab closes. **How to avoid:** WizardState lives exclusively in-memory. No `localStorage`, no `sessionStorage`, no `IndexedDB`. Closing the tab is the intended "clear all" operation. --- ## Code Examples ### Vitest config for pure function testing (no DOM) ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'node', // pure functions don't need DOM globals: true, }, }); ``` ### Testing Zod schema correctness ```typescript // src/schemas/registry.test.ts import { describe, it, expect } from 'vitest'; import { BACKEND_SCHEMAS } from './index'; describe('Azure Blob schema', () => { it('accepts valid account + key', () => { const result = BACKEND_SCHEMAS.azureblob.safeParse({ account: 'mystorageaccount', key: 'dGVzdGtleQ==', }); expect(result.success).toBe(true); }); it('rejects empty account', () => { const result = BACKEND_SCHEMAS.azureblob.safeParse({ account: '' }); expect(result.success).toBe(false); }); }); ``` ### WizardProvider wired in App ```typescript // src/App.tsx import { WizardProvider } from './store/context'; export default function App() { return ( {/* Phase 3 wizard steps go here */}
Ready2Blob
); } ``` --- ## State of the Art | Old Approach | Current Approach | When Changed | Impact | |--------------|------------------|--------------|--------| | `npm install -D tailwindcss postcss autoprefixer` + `tailwind.config.js` | `npm install -D tailwindcss @tailwindcss/vite` + `@import "tailwindcss"` in CSS | January 2025 (v4 stable) | Following v3 guides produces a broken or unnecessary setup | | `npx shadcn-ui@latest init` | `npx shadcn@latest init` | 2024 (renamed); CLI v4 March 2026 | Old package name no longer maintained | | Zod v3 (`import { z } from 'zod'`) | Zod v4 same import, but `@hookform/resolvers` v5 required | May 2025 (v4 stable) | @hookform/resolvers v3/v4 do not support Zod v4 | | `Create React App` | `npm create vite@latest` (create-vite v9, Vite 6) | CRA deprecated 2023 | CRA is abandoned; all new React projects use Vite | **Deprecated/outdated:** - `Create React App`: officially deprecated by React team; not a valid choice for new projects - `tailwind.config.js` (for new projects): not needed in Tailwind v4; config moves to CSS file - `npx shadcn-ui@latest`: old package name; replaced by `npx shadcn@latest` - `@hookform/resolvers` v3/v4: use v5.x for Zod v4 compatibility --- ## Open Questions 1. **Exact rclone field keys for Azure Blob (SAS URL variant)** - What we know: Azure Blob uses `type = azureblob`, `account` for account name, `key` for access key, `sas_url` for SAS tokens (from training data + ARCHITECTURE.md) - What's unclear: Whether `sas_url` is the correct key name or if it changed in recent rclone releases - Recommendation: Treat training-data field names as working hypothesis; verify against https://rclone.org/azureblob/ before Phase 2 generator implementation. For Phase 1 registry, use the training-data keys and flag them for verification. 2. **shadcn/ui v4 component API changes** - What we know: CLI v4 was released March 2026; the CLI now scaffolds full project templates - What's unclear: Whether specific component APIs (Input, Select, Button) changed between CLI v3 and v4 - Recommendation: Run `npx shadcn@latest add button input select` after init and inspect generated files. Components are owned by the project after generation, so API drift only matters at init time. 3. **Node.js version requirement for Vite 6** - What we know: Vite 6 requires Node.js 20.19+ or 22.12+ - What's unclear: Whether the dev machine meets this requirement - Recommendation: Run `node --version` before scaffolding. If below 20.19, update Node before proceeding. --- ## Validation Architecture ### Test Framework | Property | Value | |----------|-------| | Framework | Vitest 2.x | | Config file | `vitest.config.ts` — Wave 0 task | | Quick run command | `npx vitest run` | | Full suite command | `npx vitest run --coverage` | ### Phase Requirements → Test Map Phase 1 has no numbered requirements (it is infrastructure). The four success criteria map to tests: | Success Criterion | Behavior | Test Type | Automated Command | File Exists? | |-------------------|----------|-----------|-------------------|-------------| | SC-1: Dev server starts | Vite config is valid | smoke (manual) | `npm run dev` — no errors in console | N/A | | SC-2: Registry defines Azure Blob, S3, S3-compatible | Registry exports all three BackendTypes with FieldDef arrays | unit | `npx vitest run src/schemas/registry.test.ts` | Wave 0 | | SC-3: Zod schemas validate correct/incorrect input | safeParse returns success:true/false correctly | unit | `npx vitest run src/schemas/index.test.ts` | Wave 0 | | SC-4: WizardState reducer initializes and accepts dispatch | Reducer returns correct state for each Action type | unit | `npx vitest run src/store/reducer.test.ts` | Wave 0 | ### Sampling Rate - **Per task commit:** `npx vitest run` - **Per wave merge:** `npx vitest run` - **Phase gate:** All unit tests green before marking Phase 1 complete ### Wave 0 Gaps - [ ] `src/schemas/registry.test.ts` — verifies BackendType keys + FieldDef array structure - [ ] `src/schemas/index.test.ts` — verifies Zod safeParse success/failure for all three backends - [ ] `src/store/reducer.test.ts` — verifies all WizardAction types produce correct state transitions - [ ] `vitest.config.ts` — test runner configuration (node environment, globals: true) --- ## Sources ### Primary (HIGH confidence) - [create-vite npm](https://www.npmjs.com/package/create-vite) — v9.0.3 current, Vite 6.x scaffold - [Tailwind CSS v4.0 announcement](https://tailwindcss.com/blog/tailwindcss-v4) — CSS-first config, @tailwindcss/vite plugin - [Install Tailwind CSS with Vite (official)](https://tailwindcss.com/docs) — verified Vite plugin setup - [shadcn/ui CLI docs](https://ui.shadcn.com/docs/cli) — `npx shadcn@latest init` current command - [shadcn/ui March 2026 changelog](https://ui.shadcn.com/docs/changelog/2026-03-cli-v4) — CLI v4 release - [zod npm](https://www.npmjs.com/package/zod) — v4.3.6 current, v4 stable since v4.1.x - [@hookform/resolvers npm](https://www.npmjs.com/package/@hookform/resolvers) — v5.2.2 supports Zod v3.25+ and v4 - [Vitest 2026 overview](https://dev.to/ottoaria/vitest-in-2026-the-testing-framework-that-makes-you-actually-want-to-write-tests-kap) — Vitest as default testing framework for Vite projects - .planning/research/STACK.md — project-level stack decisions - .planning/research/ARCHITECTURE.md — Backend Schema Registry pattern, WizardState shape, rclone config field names ### Secondary (MEDIUM confidence) - [Zod v4 InfoQ release article](https://www.infoq.com/news/2025/08/zod-v4-available/) — v4 performance improvements, mini package - [react-hook-form resolvers Zod v4 compatibility issue thread](https://github.com/react-hook-form/react-hook-form/issues/12829) — confirmed v5 resolvers required for Zod v4 - [DEV Community: React Hook Form + Zod 2026](https://dev.to/marufrahmanlive/react-hook-form-with-zod-complete-guide-for-2026-1em1) — confirmed Zod v4.1.x+ stable with resolvers v5 ### Tertiary (LOW confidence) - rclone.org field names for Azure Blob, S3, S3-compatible — from .planning/research/ARCHITECTURE.md (training data, knowledge cutoff August 2025; verify against live rclone docs before Phase 2) --- ## Metadata **Confidence breakdown:** - Standard stack versions: HIGH — verified from npm and official changelogs - Tailwind v4 setup: HIGH — official Tailwind docs confirm Vite plugin path - shadcn CLI rename: HIGH — official shadcn changelog confirms `shadcn` package name - Zod v4 + resolvers v5 compatibility: HIGH — confirmed production-stable in multiple sources - Registry field keys (rclone): MEDIUM — training data; must be verified against rclone.org before Phase 2 **Research date:** 2026-03-26 **Valid until:** 2026-06-26 (stable ecosystem; Tailwind v4 and shadcn CLI v4 are recent but now stable)