Files
kawa 53bbd00533 docs(05-03): complete act()-warning elimination plan
- SUMMARY.md: userEvent v14 migration + vi.useFakeTimers() for ReviewStep
- STATE.md: advanced to completed 05-03, added patterns as decisions
- ROADMAP.md: phase 5 now 4/4 plans complete (Complete status)
- REQUIREMENTS.md: TECH-05 marked complete
2026-03-30 11:44:59 +02:00

17 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
03-wizard-ui 02 execute 2
03-01
src/components/ui/BackendCard.tsx
src/components/ui/PasswordField.tsx
src/components/ui/FieldRenderer.tsx
src/components/wizard/AzureAuthToggle.tsx
true
BACK-01
BACK-02
BACK-03
truths artifacts key_links
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
path provides exports
src/components/ui/BackendCard.tsx Clickable backend selection card
BackendCard
path provides exports
src/components/ui/PasswordField.tsx Password input with show/hide toggle
PasswordField
path provides exports
src/components/ui/FieldRenderer.tsx Single FieldDef renderer for registry-driven forms
FieldRenderer
path provides exports
src/components/wizard/AzureAuthToggle.tsx Azure SAS vs Access Key segmented toggle
AzureAuthToggle
from to via pattern
src/components/wizard/AzureAuthToggle.tsx src/schemas/registry.ts renders key and sas_url FieldDef entries via PasswordField PasswordField
from to via pattern
src/components/ui/FieldRenderer.tsx src/schemas/registry.ts accepts FieldDef and renders the correct input element 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/.

<execution_context> @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/phases/03-wizard-ui/03-CONTEXT.md @.planning/phases/03-wizard-ui/03-RESEARCH.md

From src/schemas/registry.ts:

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

// 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<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> {
  name: string;
  description: string;
  selected?: boolean;
  onClick: () => void;
}

export function BackendCard({ name, description, selected = false, onClick, ...rest }: BackendCardProps) {
  return (
    <button
      type="button"
      onClick={onClick}
      data-selected={selected}
      className={[
        'flex flex-col items-start gap-1 rounded-lg border-2 p-4 text-left transition-colors',
        selected
          ? 'border-blue-600 bg-blue-50'
          : 'border-gray-200 bg-white hover:border-blue-400 hover:bg-gray-50',
      ].join(' ')}
      {...rest}
    >
      <span className="font-semibold text-gray-900">{name}</span>
      <span className="text-sm text-gray-500">{description}</span>
    </button>
  );
}
```

**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 (
    <div className="flex flex-col gap-1">
      <label htmlFor={id} className="text-sm font-medium text-gray-700">
        {label}
      </label>
      <div className="relative">
        <input
          id={id}
          type={show ? 'text' : 'password'}
          placeholder={placeholder}
          className={[
            'w-full rounded-md border px-3 py-2 pr-10 text-sm focus:outline-none focus:ring-2',
            error ? 'border-red-500 focus:ring-red-300' : 'border-gray-300 focus:ring-blue-300',
          ].join(' ')}
          {...registration}
        />
        <button
          type="button"
          onClick={() => setShow(v => !v)}
          aria-label={show ? 'Hide' : 'Show'}
          className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-700"
        >
          {show ? '🙈' : '👁'}
        </button>
      </div>
      {helpText && !error && <p className="text-xs text-gray-500">{helpText}</p>}
      {error && <p className="text-xs text-red-600">{error.message}</p>}
    </div>
  );
}
```
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<any>), `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<any>;
  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 (
      <input
        type="hidden"
        value={field.options[0].value}
        {...register(field.key)}
      />
    );
  }

  if (field.inputType === 'password') {
    return (
      <PasswordField
        id={field.key}
        label={field.label}
        error={error}
        registration={register(field.key)}
        placeholder={field.placeholder}
        helpText={field.helpText}
      />
    );
  }

  if (field.inputType === 'select' && field.options) {
    return (
      <div className="flex flex-col gap-1">
        <label htmlFor={field.key} className="text-sm font-medium text-gray-700">
          {field.label}
          {field.required && <span className="ml-1 text-red-500">*</span>}
        </label>
        <select
          id={field.key}
          className={[
            'w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2',
            error ? 'border-red-500 focus:ring-red-300' : 'border-gray-300 focus:ring-blue-300',
          ].join(' ')}
          {...register(field.key)}
        >
          {field.options.map(opt => (
            <option key={opt.value} value={opt.value}>{opt.label}</option>
          ))}
        </select>
        {field.helpText && !error && <p className="text-xs text-gray-500">{field.helpText}</p>}
        {error && <p className="text-xs text-red-600">{error.message}</p>}
      </div>
    );
  }

  // text (default)
  return (
    <div className="flex flex-col gap-1">
      <label htmlFor={field.key} className="text-sm font-medium text-gray-700">
        {field.label}
        {field.required && <span className="ml-1 text-red-500">*</span>}
      </label>
      <input
        id={field.key}
        type="text"
        placeholder={field.placeholder}
        className={[
          'w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2',
          error ? 'border-red-500 focus:ring-red-300' : 'border-gray-300 focus:ring-blue-300',
        ].join(' ')}
        {...register(field.key)}
      />
      {field.helpText && !error && <p className="text-xs text-gray-500">{field.helpText}</p>}
      {error && <p className="text-xs text-red-600">{error.message}</p>}
    </div>
  );
}
```

**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<any>), `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<any>;
  errors: {
    key?: FieldError;
    sas_url?: FieldError;
  };
}

export function AzureAuthToggle({ register, errors }: AzureAuthToggleProps) {
  const [authMethod, setAuthMethod] = useState<AuthMethod>('sas');

  return (
    <div className="flex flex-col gap-3">
      {/* Segmented control */}
      <div className="flex rounded-md border border-gray-300 overflow-hidden">
        <button
          type="button"
          onClick={() => setAuthMethod('sas')}
          className={[
            'flex-1 py-1.5 text-sm font-medium transition-colors',
            authMethod === 'sas'
              ? 'bg-blue-600 text-white'
              : 'bg-white text-gray-700 hover:bg-gray-50',
          ].join(' ')}
        >
          SAS URL
        </button>
        <button
          type="button"
          onClick={() => setAuthMethod('key')}
          className={[
            'flex-1 py-1.5 text-sm font-medium transition-colors',
            authMethod === 'key'
              ? 'bg-blue-600 text-white'
              : 'bg-white text-gray-700 hover:bg-gray-50',
          ].join(' ')}
        >
          Access Key
        </button>
      </div>

      {/* Both fields always registered — only active one visible */}
      <div className={authMethod === 'sas' ? 'block' : 'hidden'}>
        <PasswordField
          id="sas_url"
          label="SAS URL"
          error={errors.sas_url}
          registration={register('sas_url')}
          placeholder="https://mystorageaccount.blob.core.windows.net/?sv=..."
          helpText="Full SAS URL including account and container"
        />
      </div>
      <div className={authMethod === 'key' ? 'block' : 'hidden'}>
        <PasswordField
          id="key"
          label="Access Key"
          error={errors.key}
          registration={register('key')}
          helpText="Base64-encoded storage account key"
        />
      </div>
    </div>
  );
}
```

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' && <PasswordField ... />}` — 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:

grep -n "^export" src/components/ui/BackendCard.tsx src/components/ui/PasswordField.tsx src/components/ui/FieldRenderer.tsx src/components/wizard/AzureAuthToggle.tsx

<success_criteria>

  • 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) </success_criteria>
After completion, create `.planning/phases/03-wizard-ui/03-02-SUMMARY.md`