26 KiB
Phase 10: Content & Clarity - Research
Researched: 2026-04-01 Domain: React UI — content additions, inline preview component, registry data enrichment Confidence: HIGH
Summary
Phase 10 is a pure content and UI composition phase — no new dependencies, no new architectural patterns. Every change is either adding JSX to existing components, adding a small new component (RemoteNamePreview), or enriching data objects in BACKEND_REGISTRY. The technical risk is low; the effort is mostly writing copy and wiring a controlled preview.
The one genuinely new code unit is the live remote-name config preview below the TextFieldMD3 in BackendSelectionStep. It needs a watched value from react-hook-form (watch('name')) and renders a styled <code> block. The intro section (UX-01) requires local showIntro state in WizardShell (App.tsx) and a conditional render — straightforward React state work.
The credential help-text task (UX-04) is purely a data change in registry.ts — no component work required because FieldRenderer and PasswordField already consume helpText and tooltipText from FieldDef.
Primary recommendation: Implement in four isolated tasks: (1) intro section in App.tsx, (2) remote-name preview in BackendSelectionStep, (3) step descriptions in each step component, (4) registry enrichment for credential fields. Each task is independently testable and zero-dependency on the others.
<user_constraints>
User Constraints (from CONTEXT.md)
Locked Decisions
Intro section (UX-01)
- Inline above wizard in the same page — no routing, no separate landing page
- Action-first tone: lead with the outcome ("Go from zero to a deployable rclone setup in minutes")
- Mention top 3 backends + count: "Azure Blob, S3, OneDrive, and 4 more cloud backends"
- CTA button "Get Started" — clicking hides the intro and reveals Step 1 with StepIndicator
- On page reload, intro shows again (no persistence — wizard state is ephemeral)
- No collapse/shrink behavior — intro simply disappears when wizard starts
Remote name experience (UX-02)
- Inline config preview below the TextFieldMD3 field, styled with MD3 surface tokens
- Live preview updates as user types, showing
[remote-name]config syntax in a small code box - When field is empty: show grayed-out placeholder example
[my-remote]with note "Type a name to see how it appears in your config" - Field placeholder:
e.g. my-backup - Help text: detailed with example — "This becomes the section header [name] in your rclone.conf. Example: azure-prod, backup-s3. Letters, numbers, dashes, underscores only."
- Preview depth (header only vs header + type line): Claude's discretion
Step descriptions (UX-03)
- Claude's discretion on tone and wording
- Each of the 4 steps gets a 1-2 sentence description below the heading explaining what the user is doing and why
Credential help text (UX-04)
- Add
tooltipTextto all fields that lack it (S3, S3-compatible, GCS, SFTP, B2, OneDrive drive_id) - Include links to provider docs only for complex/non-obvious flows (OneDrive token procedure, GCS service account JSON creation)
- Add short
helpTextonly where the label alone is ambiguous (S3 access_key_id, secret_access_key, S3-compatible equivalents) - Existing helpText and tooltipText (Azure Blob fields, OneDrive token, SFTP password/key_pem) are already good — don't rewrite
Claude's Discretion
- Step description wording and tone for all 4 steps
- Remote name preview depth (header only vs header + type line)
- Exact tooltip wording for each backend field
- Which fields qualify as "non-obvious" enough to warrant doc links
- Intro section visual styling (spacing, typography, icon/illustration presence)
Deferred Ideas (OUT OF SCOPE)
None — discussion stayed within phase scope </user_constraints>
<phase_requirements>
Phase Requirements
| ID | Description | Research Support |
|---|---|---|
| UX-01 | First-time visitor sees an intro section explaining what Ready2Blob does, with a clear call-to-action to start the wizard | App.tsx WizardShell gains local showIntro state; intro conditionally renders above StepIndicator; "Get Started" dispatches nothing — just sets showIntro = false |
| UX-02 | Remote name field includes a placeholder example, help text explaining what it is, and a visual preview showing how it appears in the generated [remote-name] config |
BackendSelectionStep adds watch('name') + new RemoteNamePreview component below TextFieldMD3 |
| UX-03 | Each wizard step has a 1-2 sentence description below the heading explaining what the user is doing and why | Each of the 4 step components gets a <p> element inserted after <h2>; no architectural change |
| UX-04 | All backend credential fields have contextual help text explaining what to enter and where to find it | BACKEND_REGISTRY in registry.ts enriched with tooltipText and helpText for S3, S3-compatible, GCS, SFTP, B2, OneDrive drive_id fields |
| </phase_requirements> |
Standard Stack
Core
| Library | Version | Purpose | Why Standard |
|---|---|---|---|
| React | 18.3.1 | UI rendering, local state | Already in project |
| react-hook-form | 7.72.0 | Form values — watch() for live preview |
Already in use in BackendSelectionStep |
| Tailwind v4 | 4.2.2 | Styling with MD3 tokens | Established pattern in project |
Supporting
| Library | Version | Purpose | When to Use |
|---|---|---|---|
| Vitest + @testing-library/react | 4.1.1 / 16.3.2 | Unit tests | All new components need tests |
| @testing-library/user-event | 14.6.1 | User interaction simulation | Typing in preview field |
No new dependencies needed. Zero new runtime packages for this phase.
Installation: None required.
Architecture Patterns
Recommended Project Structure
src/
├── App.tsx # Add showIntro state + IntroSection component
├── components/
│ ├── wizard/
│ │ ├── BackendSelectionStep.tsx # Add watch('name') + RemoteNamePreview
│ │ ├── RemoteConfigStep.tsx # Add step description <p>
│ │ ├── DeploymentStep.tsx # Add step description <p>
│ │ └── ReviewStep.tsx # Add step description <p>
│ └── ui/
│ └── RemoteNamePreview.tsx # NEW: live config preview component
└── schemas/
└── registry.ts # Add tooltipText + helpText to credential fields
Pattern 1: Intro Section — Local State Gate in WizardShell
What: showIntro boolean in WizardShell (not in useReducer — this is UI-only ephemeral state). When true, render <IntroSection> and hide StepIndicator + step content. When false, render normal wizard.
When to use: Any piece of UI that gates the main content with a one-time splash that resets on reload.
Example:
// src/App.tsx — WizardShell modification
function WizardShell() {
const { state } = useWizard();
const [showIntro, setShowIntro] = useState(true);
if (showIntro) {
return (
<div className="min-h-screen bg-surface flex flex-col items-center py-12 px-4">
<div className="w-full max-w-2xl">
<div className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-bold text-on-surface">Ready2Blob</h1>
<ThemeToggle />
</div>
<IntroSection onStart={() => setShowIntro(false)} />
</div>
</div>
);
}
// ... existing wizard render
}
IMPORTANT: IntroSection can be defined in App.tsx (inline) or extracted to a separate file. The existing test App.test.tsx tests screen.getByText(/Select Backend/) — this test will still pass because when showIntro=true, BackendSelectionStep is not rendered. Tests that call renderAtStep() bypass WizardShell entirely so they are unaffected.
Pattern 2: Live Preview with react-hook-form watch()
What: watch('name') subscribes to live field value without triggering re-validation. Pass the watched value down to a display-only component.
When to use: Whenever a field value needs to be reflected in a real-time preview outside the field itself.
Example:
// BackendSelectionStep.tsx — add watch to existing useForm destructure
const {
register,
handleSubmit,
watch, // ADD THIS
formState: { errors },
} = useForm<RemoteNameFormValues>({ ... });
const remoteName = watch('name'); // re-renders on every keystroke
// In JSX, below the TextFieldMD3:
<RemoteNamePreview value={remoteName} />
Pattern 3: RemoteNamePreview Component
What: A stateless display component that takes a value: string prop and renders the rclone config syntax.
Preview depth decision (Claude's discretion): Show header + type line. This gives users a more realistic preview of the actual config file and makes the format immediately understandable. Example:
[my-backup]
type = azureblob
This is more informative than header-only and the type line is always present in every generated config. However — since the backend is not yet selected at Step 1, showing a static type = ... placeholder is misleading. Recommend: header-only preview. The type is selected in Step 2; Step 1 is purely about naming.
Example:
// src/components/ui/RemoteNamePreview.tsx
interface RemoteNamePreviewProps {
value: string;
}
export function RemoteNamePreview({ value }: RemoteNamePreviewProps) {
const isEmpty = !value || value.trim() === '';
return (
<div className="mt-2 rounded-md bg-surface-variant px-3 py-2 text-xs font-mono">
{isEmpty ? (
<span className="text-on-surface-variant/50">
[my-remote]
<span className="block text-on-surface-variant/40 font-sans mt-1 text-xs">
Type a name to see how it appears in your config
</span>
</span>
) : (
<span className="text-on-surface-variant">[{value}]</span>
)}
</div>
);
}
Pattern 4: Step Descriptions — Simple JSX Addition
What: A <p> element placed immediately after the <h2> in each step component.
When to use: All 4 step components. No component extraction needed — just inline JSX.
Example:
// BackendSelectionStep.tsx
<h2>Step 1: Select Backend</h2>
<p className="text-sm text-on-surface-container/70 mt-1 mb-4">
Give your remote connection a name and choose where your files will be stored.
The name appears as a section header in your rclone.conf file.
</p>
Pattern 5: Registry Enrichment (UX-04)
What: Add tooltipText and helpText string properties to FieldDef objects in BACKEND_REGISTRY. No component changes — FieldRenderer and PasswordField already consume these.
When to use: Any time field-level guidance needs to be added or updated.
Existing infrastructure confirmed:
FieldRenderertext branch: passestooltipTextviafield.tooltipText ? (tooltipIcon) : undefinedashelpTextPrefixtoTextFieldMD3FieldRendererselect branch: renders tooltip button + panel whenfield.tooltipTextexistsPasswordField: acceptstooltipTextprop, renders ⓘ button and tooltip panelTextFieldMD3: rendershelpTextviahelpTextPrefix/helpTextslot
No new infrastructure needed for UX-04.
Anti-Patterns to Avoid
- Adding intro state to useReducer/WizardState: Intro visibility is a one-time UI gate that resets on reload. It should be local React state (
useState) in WizardShell, not global wizard state. - Extracting step descriptions to a data structure: 4 static strings don't benefit from a lookup table. Inline JSX is clearer and easier to maintain.
- Using
useWatchinstead ofwatch():watch()fromuseFormis sufficient for a single field.useWatchis for cross-component subscription without prop drilling — not needed here. - Adding
placeholderprop to TextFieldMD3: The currentTextFieldMD3hardcodesplaceholder=" "(a single space) to enable the CSS floating-label trick viapeer-[:not(:placeholder-shown)]. Do NOT add a visible placeholder text to this component — the UX-02 placeholdere.g. my-backupis implemented as the field'shelpText+ theRemoteNamePreviewempty state, not as a native HTML placeholder.
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| Live field value subscription | Custom event listeners or manual state sync | watch('name') from react-hook-form |
Already integrated; single source of truth; handles defaultValues correctly |
| Tooltip display | New tooltip infrastructure | Existing tooltipText in FieldDef + FieldRenderer/PasswordField |
All tooltip UI (hover, click, dual-state) already implemented in Phase 9 |
| Code block styling | Custom code component | Tailwind font-mono + bg-surface-variant + text-on-surface-variant |
MD3 token system already has these tokens for code blocks (used in ReviewStep OutputBlock) |
Key insight: This phase is content, not infrastructure. Every piece of infrastructure needed already exists from Phases 8 and 9. The only new component is RemoteNamePreview (~30 lines).
Common Pitfalls
Pitfall 1: App.test.tsx — "Select Backend" heading test
What goes wrong: App.test.tsx line 58 tests screen.getByText(/Select Backend/). If showIntro=true hides BackendSelectionStep on initial render, this test will fail because the heading is no longer in the DOM.
Why it happens: The test renders <App /> which now shows IntroSection by default.
How to avoid: Update App.test.tsx to either (a) test that IntroSection renders at step 0 instead, or (b) click "Get Started" before asserting step content. The test's intent (WIZD-02: "renders the correct step component") is better served by testing both intro state and post-intro state.
Warning signs: Unable to find an element with the text: /Select Backend/ in App.test.tsx run.
Pitfall 2: TextFieldMD3 placeholder is always " " (single space)
What goes wrong: A developer adds a placeholder prop to TextFieldMD3 expecting visible placeholder text, breaking the floating label CSS trick.
Why it happens: The CSS peer-[:not(:placeholder-shown)] selector relies on placeholder=" " being set. Any non-space placeholder value would make the label float immediately even when the field is empty, defeating the UX.
How to avoid: The UX-02 "placeholder example" (e.g. my-backup) must be implemented as helpText content, not as a native placeholder attribute. The RemoteNamePreview empty state message handles the visual placeholder role.
Pitfall 3: BackendSelectionStep has only one getByRole('textbox') — adding watch must not add extra inputs
What goes wrong: Tests in BackendSelectionStep.test.tsx find the remote name input via screen.getByRole('textbox') (single textbox assertion). If RemoteNamePreview accidentally renders an input or contenteditable, this assertion breaks.
Why it happens: getByRole throws if multiple matches exist.
How to avoid: RemoteNamePreview must render only display elements (div, span, code) — never form controls.
Pitfall 4: watch() subscribes on every render — performance is fine for 1 field
What goes wrong: Concern that calling watch('name') causes excessive re-renders.
Why it happens: Misunderstanding of react-hook-form internals.
How to avoid: watch('name') is optimized — it only triggers re-render of the subscriber when name changes. For a single text field this is the correct and intended API. No useCallback or memoization needed.
Pitfall 5: registry.ts tooltip text for S3 fields — existing test checks getByRole('button', { name: /more info about access key id/i })
What goes wrong: If tooltipText is NOT added to s3.access_key_id, but the test expects a tooltip button there, the test will fail. Conversely, if added where not expected, other tests could find unexpected buttons.
Why it happens: RemoteConfigStep.test.tsx has UX-01 stubs (lines 339-397) that were written as RED tests anticipating future tooltip additions. These tests currently test SAS URL, SFTP auth, and OneDrive token tooltips (already existing). Adding S3 tooltips would not break these tests unless a test specifically asserts the absence of a button.
How to avoid: Check RemoteConfigStep.test.tsx UX-01 section before finalizing which fields get tooltips. All current UX-01 tests target fields that already have tooltipText. New UX-04 fields are additive.
Code Examples
Verified patterns from existing codebase:
Remote Name Watch + Preview Integration
// BackendSelectionStep.tsx — verified pattern using existing useForm
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<RemoteNameFormValues>({
resolver: zodResolver(remoteNameSchema),
mode: 'onSubmit',
reValidateMode: 'onChange',
defaultValues: { name: state.remote.name },
});
const remoteName = watch('name');
// In JSX below TextFieldMD3:
<TextFieldMD3
id="remote-name"
label="Remote name"
registration={register('name')}
error={errors.name}
required
helpText="This becomes the section header [name] in your rclone.conf. Example: azure-prod, backup-s3. Letters, numbers, dashes, underscores only."
/>
<RemoteNamePreview value={remoteName} />
MD3 Token Usage for Preview Block
// Confirmed token names from index.css @theme block:
// bg-surface-variant = --r2b-surface-variant (gray-800 light / slate-900 dark)
// text-on-surface-variant = --r2b-on-surface-variant (gray-100 light / gray-200 dark)
// These are identical to what ReviewStep's OutputBlock uses for code display
<div className="mt-2 rounded-md bg-surface-variant px-3 py-2 text-xs font-mono text-on-surface-variant">
[{value}]
</div>
Registry Field Enrichment Pattern
// registry.ts — add tooltipText to s3 access_key_id (currently missing)
{
key: 'access_key_id',
label: 'Access Key ID',
inputType: 'text',
required: true,
placeholder: 'AKIAIOSFODNN7EXAMPLE',
helpText: 'The access key ID from your IAM credentials (starts with AKIA for long-term keys).',
tooltipText: 'Found in the AWS Console under IAM → Users → Security credentials → Access keys. Create a new access key if you don\'t have one. Use an IAM user with least-privilege S3 access — avoid root account keys.',
},
Step Description Pattern
// DeploymentStep.tsx — insert after <h2>
<h2>Step 3: Deployment Options</h2>
<p className="text-sm text-on-surface-container/70 mt-1 mb-4">
Choose how the rclone config file will be placed on the target machine and
whether to include an rclone installation script.
</p>
Validation Architecture
Test Framework
| Property | Value |
|---|---|
| Framework | Vitest 4.1.1 + @testing-library/react 16.3.2 |
| Config file | vite.config.ts (vitest inline config) |
| Quick run command | npx vitest run --reporter=verbose |
| Full suite command | npx vitest run |
Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|---|---|---|---|---|
| UX-01 | IntroSection renders on initial App load | unit | npx vitest run src/App.test.tsx |
✅ (needs update) |
| UX-01 | "Get Started" click hides intro and shows Step 1 | unit | npx vitest run src/App.test.tsx |
✅ (needs new test) |
| UX-02 | RemoteNamePreview renders [value] when field has content |
unit | npx vitest run src/components/ui/RemoteNamePreview.test.tsx |
❌ Wave 0 |
| UX-02 | RemoteNamePreview shows empty-state message when value is empty | unit | npx vitest run src/components/ui/RemoteNamePreview.test.tsx |
❌ Wave 0 |
| UX-02 | BackendSelectionStep live preview updates as user types | unit | npx vitest run src/components/wizard/BackendSelectionStep.test.tsx |
✅ (needs new test) |
| UX-03 | Each step component renders a description paragraph below h2 | unit | npx vitest run src/App.test.tsx |
✅ (needs new tests) |
| UX-04 | S3 access_key_id field renders tooltip ⓘ button | unit | npx vitest run src/components/wizard/RemoteConfigStep.test.tsx |
✅ (needs new test) |
| UX-04 | GCS service_account_credentials field renders tooltip ⓘ button | unit | npx vitest run src/components/wizard/RemoteConfigStep.test.tsx |
✅ (needs new test) |
Sampling Rate
- Per task commit:
npx vitest run - Per wave merge:
npx vitest run - Phase gate: Full suite green before
/gsd:verify-work
Wave 0 Gaps
src/components/ui/RemoteNamePreview.test.tsx— covers UX-02 preview rendering
(App.test.tsx, BackendSelectionStep.test.tsx, and RemoteConfigStep.test.tsx exist but need new test cases added as part of the implementation tasks.)
Fields Requiring UX-04 Enrichment
Complete audit of registry.ts against UX-04 decisions:
Fields to ADD tooltipText (currently missing)
| Backend | Field key | Label | Tooltip needed? | Why |
|---|---|---|---|---|
| s3 | access_key_id | Access Key ID | YES | Non-obvious where to find in AWS console |
| s3 | secret_access_key | Secret Access Key | YES | Security-sensitive; users confuse with access key ID |
| s3 | region | Region | OPTIONAL | us-east-1 placeholder is self-explanatory; low priority |
| s3-compatible | access_key_id | Access Key ID | YES | Provider-specific location varies |
| s3-compatible | secret_access_key | Secret Access Key | YES | Same as S3 |
| s3-compatible | endpoint | Endpoint URL | YES | Location varies by provider (Wasabi vs R2 vs MinIO) |
| gcs | project_number | Project Number | YES | Users confuse project number with project ID |
| gcs | service_account_credentials | Service Account JSON | YES | Non-obvious creation flow — warrants doc link |
| sftp | host | Host | NO | Self-explanatory |
| sftp | user | Username | NO | Self-explanatory |
| b2 | account | Application Key ID | YES | Users confuse with account ID |
| b2 | key | Application Key | YES | Location in B2 dashboard not obvious |
| onedrive | drive_id | Drive ID | YES | Not obvious — only found in rclone authorize output |
Fields to ADD helpText (currently missing)
| Backend | Field key | Label | helpText needed? | Why |
|---|---|---|---|---|
| s3 | access_key_id | Access Key ID | YES | Label alone ambiguous (vs secret key) |
| s3 | secret_access_key | Secret Access Key | YES | Label alone ambiguous |
| s3-compatible | access_key_id | Access Key ID | YES | Same as S3 |
| s3-compatible | secret_access_key | Secret Access Key | YES | Same as S3 |
Fields already well-documented (DO NOT REWRITE)
- azureblob: account, key, sas_url — all have helpText + tooltipText
- onedrive: token — has helpText + tooltipText
- sftp: pass, key_pem — have helpText
- b2: account, key — have basic helpText (may need tooltip additions per above)
State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|---|---|---|---|
| Hardcoded colors | MD3 CSS token system | Phase 8 | All new UI uses bg-surface, text-on-surface-container etc. |
| Static field labels only | tooltipText + helpText in FieldDef | Phase 9 (FieldRenderer rebuilt) | No new infrastructure needed for UX-04 |
| No input styling | TextFieldMD3 floating label | Phase 9 | Remote name field already uses TextFieldMD3 |
Open Questions
-
Should RemoteNamePreview be a separate file or defined inline in BackendSelectionStep?
- What we know: It's ~30 lines, used only in BackendSelectionStep, testable either way
- What's unclear: Whether it will be reused elsewhere (ReviewStep could theoretically show it)
- Recommendation: Extract to
src/components/ui/RemoteNamePreview.tsx— matches project pattern (all reusable UI inui/), makes testing cleaner, costs nothing
-
TextFieldMD3 does not accept a
placeholderprop — UX-02 says field placeholder should bee.g. my-backup- What we know: The
placeholder=" "hardcoding is load-bearing for the floating label. The spec's "placeholder" intent is to show the user an example value. - What's unclear: Whether to extend TextFieldMD3 to support an optional visible placeholder while preserving the CSS trick, or to convey the example through helpText.
- Recommendation: Convey the example through
helpText("This becomes the section header [name] in your rclone.conf. Example: azure-prod, backup-s3."). TheRemoteNamePreviewempty-state message[my-remote]serves the visual placeholder role. Do NOT modify TextFieldMD3's placeholder mechanism.
- What we know: The
Sources
Primary (HIGH confidence)
- Direct codebase inspection —
src/App.tsx,src/components/ui/TextFieldMD3.tsx,src/components/ui/FieldRenderer.tsx,src/components/ui/PasswordField.tsx,src/components/wizard/BackendSelectionStep.tsx,src/schemas/registry.ts,src/index.css src/components/wizard/RemoteConfigStep.test.tsx— confirmed existing UX-01 tooltip tests and which fields already have tooltipssrc/components/wizard/BackendSelectionStep.test.tsx— confirmed test selector patterns to avoid breaking
Secondary (MEDIUM confidence)
- react-hook-form documentation:
watch()API behavior for controlled preview use case — consistent with observed usage in existing codebase
Tertiary (LOW confidence)
- None
Metadata
Confidence breakdown:
- Standard stack: HIGH — verified from package.json and codebase
- Architecture: HIGH — all integration points verified by reading actual source files
- Pitfalls: HIGH — derived from reading actual test files and component implementations
Research date: 2026-04-01 Valid until: 2026-05-01 (stable React/RHF APIs; only invalidated by component refactors)