---
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 atomssrc/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 -20BackendCard.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 AzureAuthTogglesrc/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)