--- phase: 03-wizard-ui plan: "02" type: execute wave: 2 depends_on: - "03-01" files_modified: - src/components/ui/BackendCard.tsx - src/components/ui/PasswordField.tsx - src/components/ui/FieldRenderer.tsx - src/components/wizard/AzureAuthToggle.tsx autonomous: true requirements: - BACK-01 - BACK-02 - BACK-03 must_haves: truths: - "BackendCard renders a clickable card with backend name and calls onClick when clicked" - "PasswordField renders a password input with a show/hide eye toggle button" - "FieldRenderer renders the correct input type for text, password, select, and toggle FieldDef entries" - "AzureAuthToggle renders a segmented SAS/Key toggle, shows the active field, and defaults to SAS" artifacts: - path: "src/components/ui/BackendCard.tsx" provides: "Clickable backend selection card" exports: ["BackendCard"] - path: "src/components/ui/PasswordField.tsx" provides: "Password input with show/hide toggle" exports: ["PasswordField"] - path: "src/components/ui/FieldRenderer.tsx" provides: "Single FieldDef renderer for registry-driven forms" exports: ["FieldRenderer"] - path: "src/components/wizard/AzureAuthToggle.tsx" provides: "Azure SAS vs Access Key segmented toggle" exports: ["AzureAuthToggle"] key_links: - from: "src/components/wizard/AzureAuthToggle.tsx" to: "src/schemas/registry.ts" via: "renders key and sas_url FieldDef entries via PasswordField" pattern: "PasswordField" - from: "src/components/ui/FieldRenderer.tsx" to: "src/schemas/registry.ts" via: "accepts FieldDef and renders the correct input element" pattern: "FieldDef" --- Create the four reusable UI atom components that the wizard step components depend on: BackendCard, PasswordField, FieldRenderer, and AzureAuthToggle. Purpose: Establish shared building blocks so Plans 03 and 04 can implement their step components without duplicating input logic. Output: Four new component files in src/components/ui/ and src/components/wizard/. @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/phases/03-wizard-ui/03-CONTEXT.md @.planning/phases/03-wizard-ui/03-RESEARCH.md From src/schemas/registry.ts: ```typescript export type BackendType = 'azureblob' | 's3' | 's3-compatible'; export interface FieldDef { key: string; // MUST match rclone config key exactly (snake_case) label: string; inputType: 'text' | 'password' | 'select' | 'toggle'; required: boolean; placeholder?: string; helpText?: string; options?: { value: string; label: string }[]; // for inputType: 'select' } export const BACKEND_REGISTRY: Record = { azureblob: [ { key: 'account', label: 'Storage Account Name', inputType: 'text', required: true }, { key: 'key', label: 'Access Key', inputType: 'password', required: false }, { key: 'sas_url', label: 'SAS URL', inputType: 'password', required: false }, ], s3: [ { key: 'provider', label: 'Provider', inputType: 'select', required: true }, { 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 }, ], 's3-compatible': [ { key: 'provider', label: 'Provider', inputType: 'select', required: true }, { 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 }, { key: 'region', label: 'Region', inputType: 'text', required: false }, ], }; ``` From react-hook-form (installed as react-hook-form ^7.72.0): ```typescript // UseFormRegister and FieldError types used in FieldRenderer and PasswordField import type { UseFormRegister, FieldError } from 'react-hook-form'; ``` Task 1: Create BackendCard and PasswordField UI atoms src/components/ui/BackendCard.tsx, src/components/ui/PasswordField.tsx Create `src/components/ui/` directory and the two simplest atom components. **src/components/ui/BackendCard.tsx** A clickable card for the backend selection grid. Props: `name` (display string), `description` (short subtitle), `onClick` (void callback), `selected` (boolean for visual highlight). ```tsx import type { ButtonHTMLAttributes } from 'react'; interface BackendCardProps extends Omit, 'onClick'> { name: string; description: string; selected?: boolean; onClick: () => void; } export function BackendCard({ name, description, selected = false, onClick, ...rest }: BackendCardProps) { return ( ); } ``` **src/components/ui/PasswordField.tsx** Password input with a show/hide eye toggle. Per locked decision: all password-type fields (access key, SAS URL, S3 secret) have a show/hide toggle. Per project instructions: each field has its own independent visibility state. Props: `id` (string), `label` (string), `error` (FieldError | undefined), `registration` (return value of react-hook-form `register()`), `placeholder` (optional). ```tsx import { useState } from 'react'; import type { FieldError, UseFormRegisterReturn } from 'react-hook-form'; interface PasswordFieldProps { id: string; label: string; error?: FieldError; registration: UseFormRegisterReturn; placeholder?: string; helpText?: string; } export function PasswordField({ id, label, error, registration, placeholder, helpText }: PasswordFieldProps) { const [show, setShow] = useState(false); return (
{helpText && !error &&

{helpText}

} {error &&

{error.message}

}
); } ```
npx vitest run --reporter=verbose 2>&1 | tail -20 BackendCard.tsx and PasswordField.tsx exist with correct exports. Vitest still reports RED (test stubs still fail — components exist but tests have expect.fail).
Task 2: Create FieldRenderer and AzureAuthToggle src/components/ui/FieldRenderer.tsx, src/components/wizard/AzureAuthToggle.tsx **src/components/ui/FieldRenderer.tsx** Renders a single `FieldDef` from the BACKEND_REGISTRY as the appropriate input element. This is the registry-driven form engine — no hardcoded per-backend JSX is allowed anywhere else. The `provider` select field (s3 and s3-compatible backends) has only one option — hide it from the user and auto-register it with its default value. This follows the recommendation from 03-RESEARCH.md open question #2. Props: `field` (FieldDef), `register` (UseFormRegister), `error` (FieldError | undefined), `defaultValue` (string | undefined — used to auto-register hidden fields). ```tsx import type { UseFormRegister, FieldError } from 'react-hook-form'; import type { FieldDef } from '../../schemas/registry'; import { PasswordField } from './PasswordField'; interface FieldRendererProps { field: FieldDef; register: UseFormRegister; error?: FieldError; } export function FieldRenderer({ field, register, error }: FieldRendererProps) { // provider field: single-option select — hide from UI, auto-register with default value if (field.key === 'provider' && field.options?.length === 1) { return ( ); } if (field.inputType === 'password') { return ( ); } if (field.inputType === 'select' && field.options) { return (
{field.helpText && !error &&

{field.helpText}

} {error &&

{error.message}

}
); } // text (default) return (
{field.helpText && !error &&

{field.helpText}

} {error &&

{error.message}

}
); } ``` **src/components/wizard/AzureAuthToggle.tsx** Azure-specific segmented control that toggles between "SAS URL" and "Access Key". Per locked decision: - Default method: SAS URL - Switching PRESERVES both field values (both are always registered in react-hook-form) - Only the active field is displayed - Both values flow into `remote.params` on form submit (generator strips inactive one) Props: `register` (UseFormRegister), `errors` (object with key and sas_url FieldError entries). ```tsx import { useState } from 'react'; import type { UseFormRegister, FieldError } from 'react-hook-form'; import { PasswordField } from '../ui/PasswordField'; type AuthMethod = 'sas' | 'key'; interface AzureAuthToggleProps { register: UseFormRegister; errors: { key?: FieldError; sas_url?: FieldError; }; } export function AzureAuthToggle({ register, errors }: AzureAuthToggleProps) { const [authMethod, setAuthMethod] = useState('sas'); return (
{/* Segmented control */}
{/* Both fields always registered — only active one visible */}
); } ``` CRITICAL: Using CSS `hidden` class (not conditional rendering) ensures both fields are registered with react-hook-form and their values are preserved when toggling. Do not use `{authMethod === 'sas' && }` — that unmounts the hidden field and react-hook-form loses its value.
npx vitest run --reporter=verbose 2>&1 | tail -20 FieldRenderer.tsx and AzureAuthToggle.tsx exist with correct exports. All four atoms exist. Vitest still RED (stubs still have expect.fail — implementation tasks come in Plans 03 and 04).
Run after both tasks complete: ```bash npx vitest run --reporter=verbose 2>&1 ``` Vitest runs cleanly (no crashes). Tests remain RED from stubs. All four atom files exist with named exports. Check exports: ```bash grep -n "^export" src/components/ui/BackendCard.tsx src/components/ui/PasswordField.tsx src/components/ui/FieldRenderer.tsx src/components/wizard/AzureAuthToggle.tsx ``` - BackendCard, PasswordField, FieldRenderer, AzureAuthToggle all export their named components - FieldRenderer hides the `provider` field for single-option selects and auto-registers the default value - AzureAuthToggle registers BOTH `key` and `sas_url` with react-hook-form regardless of active method (uses CSS show/hide, not conditional rendering) - PasswordField has per-instance show/hide state (not a global toggle) - No new Tailwind config file created (Tailwind v4 CSS-first — all styles use utility classes directly) After completion, create `.planning/phases/03-wizard-ui/03-02-SUMMARY.md`