Files
kawaandClaude Sonnet 4.6 eb103c4338 docs(phase-1): research foundation phase
Covers Vite 6 scaffold, Tailwind v4 CSS-first setup, Zod v4 + hookform/resolvers v5 compatibility, Backend Schema Registry pattern, WizardState useReducer design, and Vitest test map for phase success criteria.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 09:58:48 +01:00

25 KiB

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):

# 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

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:

// 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<BackendType, FieldDef[]> = {
  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:

// 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<string, z.ZodTypeAny> = {};
  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<T extends BackendType> =
  z.infer<typeof BACKEND_SCHEMAS[T]>;

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:

// src/store/types.ts

export type BackendType = 'azureblob' | 's3' | 's3-compatible';

export interface WizardState {
  currentStep: number;
  remote: {
    name: string;
    backendType: BackendType | null;
    params: Record<string, string>; // 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<string, string> }
  | { type: 'SET_DEPLOYMENT'; payload: Partial<WizardState['deployment']> }
  | { 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:

// 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<WizardAction>;
}

const WizardContext = createContext<WizardContextValue | null>(null);

export function WizardProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(wizardReducer, INITIAL_STATE);
  return (
    <WizardContext.Provider value={{ state, dispatch }}>
      {children}
    </WizardContext.Provider>
  );
}

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:

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):

@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 <AzureBlobForm /> component with hardcoded field names bypasses the registry pattern. One generic <DynamicBackendForm /> 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<typeof schema> 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)

// 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

// 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

// src/App.tsx
import { WizardProvider } from './store/context';

export default function App() {
  return (
    <WizardProvider>
      {/* Phase 3 wizard steps go here */}
      <div>Ready2Blob</div>
    </WizardProvider>
  );
}

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)

Secondary (MEDIUM confidence)

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)