109 Commits
Author SHA1 Message Date
kawa 02a696c05f docs: add README with docker compose usage and dependency list 2026-08-26 17:47:56 +02:00
kawaandClaude Haiku 4.5 0dae30b4e2 fix: resolve remaining TypeScript compilation errors
- Add placeholder property to TextFieldMD3 component and props interface
- Fix vite.config.ts to import defineConfig from vitest/config instead of vite
- Add type guard for validate property in schemas/index.ts to handle readonly array union types
- Remove unused StepIndicatorWithDispatch placeholder function from tests

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-26 17:37:47 +02:00
kawaandClaude Haiku 4.5 c04b149756 fix: resolve TypeScript compilation errors in Docker build
- Add vitest globals type definitions to tsconfig.app.json
- Enable globals in vite.config.ts test configuration
- Add explicit vitest imports and type references to all .test.ts files
- Add missing `placeholder` property to PasswordFieldProps interface
- Add missing `required` field to test field definitions in FieldRenderer.test.tsx
- Update FieldDef and BackendMeta interfaces to support readonly arrays
- Fix readonly array type incompatibilities in BackendSelectionStep.tsx
- Fix form submission handler type mismatch in RemoteConfigStep.tsx
- Remove unused helper functions and imports from StepIndicator.test.tsx

These changes resolve all TypeScript compilation errors preventing the Docker build from completing.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-26 17:34:47 +02:00
kawa 2f14d217b1 Update .gitea/workflows/publish-image.yml 2026-08-26 17:17:38 +02:00
kawa 782e261bc8 Update .gitea/workflows/publish-image.yml 2026-08-26 17:16:11 +02:00
kawaandClaude Haiku 4.5 d717508385 ci(gitea): add workflow for publishing Docker images
Adds Gitea CI/CD workflow that builds and pushes Docker images to the registry on tag push or manual trigger.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-26 15:26:42 +02:00
kawaandClaude Opus 5 32159a404d feat(docker): add containerized nginx deployment with runtime base path
Two-stage build: node builds the static bundle, nginx serves it. The bundle is
built against a placeholder base token that the entrypoint rewrites to $BASE_URL
at start, so one image serves from any path without a rebuild.

nginx config caches hashed assets forever, never caches index.html, and applies
security headers (CSP, no framing, no referrer) suited to an app that handles
cloud credentials client-side.

.gitattributes pins LF on the container-consumed files, since core.autocrlf
would otherwise give the entrypoint a CRLF shebang.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:03:19 +02:00
kawaandClaude Opus 5 48802cf13c chore: replace hardcoded /vite.svg favicon link with a placeholder note
The absolute /vite.svg path breaks once the app is served from a base path,
and the file was never replaced with a real logo. Leave instructions for
restoring it with the %BASE_URL% placeholder instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:03:13 +02:00
kawaandClaude Sonnet 4.6 a26608791f chore: complete v1.3 milestone — Backend Expansion
Archives v1.3 milestone (Phase 13: 18 rclone backends), updates PROJECT.md
with all 30 validated requirements, reorganizes ROADMAP.md with all 4 milestones
in collapsed details blocks, adds v1.2 and v1.3 retrospective entries.

Also commits minor CSS token fixes in src/ (text-on-surface-container class
alignment) that were uncommitted from phase 13 execution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:54:45 +02:00
kawaandClaude Opus 4.6 f75b499ad6 docs(phase-13): complete phase execution and verification
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:33:19 +02:00
kawaandClaude Sonnet 4.6 0fa7a86545 docs(13-04): complete plan — visual verification approved, phase 13 done
- Task 3 visual verification approved by user
- SUMMARY.md updated to reflect all 3 tasks complete
- STATE.md updated: phase 13 complete, 100% progress
- ROADMAP.md updated: phase 13 all 4 plans complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 17:28:15 +02:00
kawa 46b149d900 docs(13-04): complete RemoteConfigStep full backend integration plan
- 13-04-SUMMARY.md: all 18 backends wired, 6 new tests, 282/282 pass
- STATE.md: session updated, decisions recorded, progress 100%
- ROADMAP.md: phase 13 marked complete (4/4 plans)
- REQUIREMENTS.md: REMOTE-05 marked complete
2026-04-01 16:59:38 +02:00
kawa 2c596c96a0 test(13-04): add tests for new backend form rendering
- Test gdrive: GdriveAuthToggle with OAuth Token and Service Account tabs
- Test dropbox: OAuthInstructions toggle button visible
- Test ftp: host, username, password, port, TLS mode fields
- Test webdav: URL, username, password, vendor select fields
- Test smb: host and username fields
- Test http: only URL field, no username/password
2026-04-01 16:58:00 +02:00
kawa d0c00f7e56 feat(13-04): wire all new backends into RemoteConfigStep
- Replace manual backendLabel record with registry-derived Object.fromEntries
- Import OAuthInstructions and GdriveAuthToggle components
- Refactor 3-branch ternary into renderBackendFields() switch function
- Add gdrive branch: GdriveAuthToggle + generic loop for non-auth fields
- Add onedrive branch: OAuthInstructions + generic loop
- Add dropbox/box/pcloud branches: OAuthInstructions + generic loop
- Default branch covers all other backends (azure-files, swift, ftp, webdav, smb, http, seafile)
2026-04-01 16:55:22 +02:00
kawa 7c6f0acfb1 docs(13-03): complete category-grouped searchable backend selection plan
- 13-03-SUMMARY.md: BackendSelectionStep refactored with categories/search/icons
- STATE.md: decisions added, progress updated to 98%, session updated
- ROADMAP.md: phase 13 progress updated (3/4 summaries)
- REQUIREMENTS.md: REMOTE-03 marked complete
2026-04-01 16:53:34 +02:00
kawa aeb1528163 test(13-03): add search filter and category collapse tests
- 6 new test cases covering REMOTE-03 acceptance criteria
- Tests: all 3 category headings render, ftp search shows FTP+SFTP
- Tests: no-match query hides all categories
- Tests: Cloudflare description match finds S3-Compatible
- Tests: all 18 backend cards render without search
- Tests: category heading hidden when all backends filtered out
2026-04-01 16:51:54 +02:00
kawa 0a70620fd5 feat(13-03): add icon prop to BackendCard and category/search to BackendSelectionStep
- BackendCard accepts optional icon prop rendered in flex row with name
- BackendSelectionStep groups 18 backends into 3 category sections
- Search bar (type=search/searchbox role) filters across displayName, description, category label, field labels
- Empty categories hidden via conditional rendering
- BACKEND_ICONS wired to BackendCard icon prop
- Changed search input to type=search to preserve getByRole('textbox') selector in existing tests
2026-04-01 16:50:49 +02:00
kawa 683faf6f11 feat(13-02): add GdriveAuthToggle and BackendIcons components
- GdriveAuthToggle: two-tab segmented control (OAuth Token / Service Account)
  following AzureAuthToggle pattern; both fields always registered via CSS
  hidden/block; OAuth tab embeds OAuthInstructions component
- BackendIcons: inline SVG icon map for all 18 backends (Partial<Record<BackendType,...>>)
  using fill="currentColor" for MD3 dark-mode compatibility, no external deps
- Created src/components/icons/ directory
2026-04-01 16:27:14 +02:00
kawa e00fc2b03b feat(13-02): implement OAuthInstructions component with tests
- Collapsible disclosure with expanded=false by default
- Shows backend-specific rclone authorize command in code block
- Accepts custom steps array or defaults to 5-step generic OAuth flow
- Styled with MD3 tokens (bg-surface-container, border-outline, rounded-xl)
- All 4 behavior tests pass (TDD)
2026-04-01 16:25:18 +02:00
kawaandClaude Sonnet 4.6 f7a79647f0 docs(13-01): complete registry refactor and 18-backend expansion plan
- 13-01-SUMMARY.md: full execution summary with decisions, deviations, and task commits
- STATE.md: updated position to phase 13 plan 01 complete, added key decisions
- ROADMAP.md: 13-01 checked off, phase 13 progress 1/4
- REQUIREMENTS.md: REMOTE-01, REMOTE-02, REMOTE-06 marked complete (3/6 v1.3 requirements)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 16:23:25 +02:00
kawa 3010cdb0c6 feat(13-01): auto-generate BACKEND_SCHEMAS, update RCLONE_TYPE_MAP for all 18 backends
- BACKEND_SCHEMAS now auto-generated via Object.fromEntries over registry keys
- RCLONE_TYPE_MAP expanded with 11 new entries for all 18 backend types
- RCLONE_TYPE_MAP exported for direct testing
- index.test.ts: added auto-generation coverage (3 tests) + 4 new backend schema smoke tests
- rclone-conf.test.ts: added RCLONE_TYPE_MAP exhaustiveness test + gdrive/ftp/smb conf output tests
- Rule 1 fix: BackendSelectionStep test used /next/i regex matching WebDAV "NextCloud" card buttons
2026-04-01 16:20:18 +02:00
kawa b8c64937ea feat(13-01): add 18 backends to BACKEND_REGISTRY with category field
- Removed explicit BackendType union literal; derived via keyof typeof BACKEND_REGISTRY
- Added BackendCategory type and category field to all 18 backend entries
- Added BackendMeta interface for individual entry typing
- Removed circular Record<BackendType,...> cast; replaced with as const
- Added 11 new backends: azure-files, swift, gdrive, dropbox, box, pcloud, ftp, webdav, smb, http, seafile
- Updated registry.test.ts: 48 tests covering all 18 backends, category assertions, field validation
2026-04-01 16:16:58 +02:00
kawaandClaude Opus 4.6 7e02b01595 fix(13): revise plans based on checker feedback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:04:24 +02:00
kawaandClaude Opus 4.6 4bf3f1e45f docs(13): create phase plan for backend expansion
4 plans across 3 waves to expand from 7 to 17 rclone backends:
- Plan 01 (W1): Registry refactoring, BackendType derivation, 10 new entries
- Plan 02 (W2): OAuthInstructions, GdriveAuthToggle, BackendIcons components
- Plan 03 (W2): BackendSelectionStep UX overhaul with categories, search, icons
- Plan 04 (W3): RemoteConfigStep wiring for all new backends + visual verify

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 15:58:51 +02:00
kawa fac90462f7 docs(phase-13): add research and validation strategy 2026-04-01 15:50:21 +02:00
kawaandClaude Sonnet 4.6 5228ae2a0a docs(13): research phase domain — rclone backend expansion
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 15:49:15 +02:00
kawaandClaude Opus 4.6 9437d6c9b5 docs(state): record phase 13 context session
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:27:41 +02:00
kawaandClaude Opus 4.6 31725bca4f docs(13): capture phase context
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:27:30 +02:00
kawaandClaude Opus 4.6 934bbfbf62 docs(phase-12): complete phase execution and verification
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:09:19 +02:00
kawa 5986d5dbec docs(12-01): complete dark mode visibility fixes plan
- SUMMARY.md: dark mode h2 and form control semantic token fixes
- STATE.md: position updated, decisions recorded
- ROADMAP.md: phase 12 marked complete
- REQUIREMENTS.md: DARK-01, DARK-02 marked complete
2026-04-01 14:06:25 +02:00
kawa d3329311e1 docs(12-02): complete FieldRenderer select dark mode fix plan
- 12-02-SUMMARY.md: select element bg-surface-container text-on-surface fix
- STATE.md: updated position and decisions
- ROADMAP.md: phase 12 progress updated
- REQUIREMENTS.md: DARK-03 marked complete
2026-04-01 14:05:23 +02:00
kawa 945f90d789 feat(12-01): style DeploymentStep form controls and ReviewStep checkbox
- DeploymentStep: include-install label uses text-on-surface, checkbox uses accent-primary
- DeploymentStep: fieldset legends use text-on-surface-container font-medium
- DeploymentStep: all radio/checkbox labels use flex gap-2 text-on-surface cursor-pointer
- DeploymentStep: all radios and checkboxes use accent-primary
- ReviewStep: security checkbox uses accent-[var(--r2b-warning)] for warning-themed accent
2026-04-01 14:05:14 +02:00
kawa f6b4070ff9 feat(12-01): add semantic token classes to all step h2 headings
- BackendSelectionStep h2: text-2xl font-bold text-on-surface mb-2
- RemoteConfigStep h2: text-2xl font-bold text-on-surface mb-2
- DeploymentStep h2: text-2xl font-bold text-on-surface mb-2
- ReviewStep h2: text-2xl font-bold text-on-surface mb-2
2026-04-01 14:04:21 +02:00
kawa 94e90e0641 feat(12-02): add bg-surface-container text-on-surface to select element in FieldRenderer
- Added bg-surface-container and text-on-surface semantic token classes to select element className
- Fixes select dropdown invisible text in dark mode (browser-applied white background)
- 204 tests pass, zero regressions
2026-04-01 14:03:57 +02:00
kawaandClaude Opus 4.6 a193a183f8 docs(12): create phase plan for dark mode visibility fixes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:00:04 +02:00
kawaandClaude Opus 4.6 94f70deb3d docs(phase-12): add research and validation strategy
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:55:55 +02:00
kawaandClaude Sonnet 4.6 c5110b1c67 docs(12): research phase dark mode visibility fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 13:54:51 +02:00
kawaandClaude Opus 4.6 0f6e0f7a5b test(11): complete UAT - 8 passed, 0 issues
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:46:44 +02:00
kawaandClaude Opus 4.6 40a82487dd docs(phase-11): complete phase execution and verification
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:36:12 +02:00
kawa 0c27818030 docs(11-02): complete step animation and scroll-to-error plan
- Create 11-02-SUMMARY.md documenting POLISH-03 and POLISH-04 completion
- Update STATE.md: phase 11 complete, 100% progress, new decisions added
- Update ROADMAP.md: phase 11 plan progress (2/2 complete)
- Update REQUIREMENTS.md: mark POLISH-03 and POLISH-04 complete
2026-04-01 13:32:39 +02:00
kawa 90cdeda142 feat(11-02): auto-scroll to first errored field on validation failure
- BackendSelectionStep: add onInvalidSubmit scrolling to remote-name input
- BackendSelectionStep: pass onInvalidSubmit to form onSubmit and handleCardClick
- RemoteConfigStep: add FieldErrors import from react-hook-form
- RemoteConfigStep: add onInvalidSubmit scrolling to first errored field by key
- RemoteConfigStep: pass onInvalidSubmit to form onSubmit
2026-04-01 13:30:39 +02:00
kawa 8b7daa860c test(11-02): add failing tests for auto-scroll to first error on validation failure
- Add scrollIntoView mock (vi.fn()) in beforeEach to both test files
- Add POLISH-04 test in BackendSelectionStep: scrolls to remote-name on empty submit
- Add POLISH-04 test in RemoteConfigStep: scrolls to first errored field on empty submit
2026-04-01 13:29:48 +02:00
kawa 9af58bc970 feat(11-02): add step transition animation with reduced-motion guard
- Add @keyframes step-in (opacity 0->1, translateY 8px->0, 200ms ease-out) to index.css
- Register --animate-step-in token in @theme block
- Add @media prefers-reduced-motion: reduce guard disabling animation
- Add key={state.currentStep} and animate-step-in class to step wrapper in App.tsx
2026-04-01 13:28:38 +02:00
kawa 3af06493ea docs(11-01): complete mobile responsiveness and MD3 focus indicators plan
- 11-01-SUMMARY.md: execution summary for POLISH-01/POLISH-02
- STATE.md: position advanced to phase 11 plan 01 complete, decisions added
- ROADMAP.md: phase 11 progress updated (1/2 plans complete)
- REQUIREMENTS.md: POLISH-01 and POLISH-02 marked complete
2026-04-01 13:26:58 +02:00
kawa 9015db565e feat(11-01): MD3 focus-visible ring-3 indicators on all interactive elements
- md3-buttons.ts: upgrade ring-2/50 to ring-3 on all three constants (FILLED, OUTLINED, TEXT)
- BackendCard: add focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-primary
- StepIndicator: upgrade group-focus-visible completed-step circle from ring-2/50 to ring-3
- ThemeToggle: add focus-visible ring-3 with ring-inset to both selected/unselected branches
- FieldRenderer: select uses focus-visible:ring-2 (was focus:ring-2); tooltip buttons get focus-visible:ring-2 rounded
2026-04-01 13:25:00 +02:00
kawa 07f97d9f25 feat(11-01): mobile-responsive layout for wizard steps
- BackendSelectionStep: responsive grid (1-col mobile, 2-col desktop) for cards; button row stacks vertically on mobile
- BackendCard: add w-full to fill grid cell on all viewports
- RemoteConfigStep/DeploymentStep/ReviewStep: flex-col sm:flex-row button rows; w-full sm:w-auto on Back/Next buttons
- StepIndicator: add hidden sm:block to all three label spans (completed, active, future)
2026-04-01 13:23:43 +02:00
kawaandClaude Opus 4.6 e22d44d807 docs(11-polish-responsiveness): create phase plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:14:51 +02:00
kawa 4472c1294c docs(phase-11): add research and validation strategy 2026-04-01 13:10:23 +02:00
kawaandClaude Sonnet 4.6 5fb0300f80 docs(phase-11): research polish & responsiveness phase
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 13:09:13 +02:00
kawaandClaude Opus 4.6 b7ba16326f docs(phase-10): complete phase execution and verification
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:02:45 +02:00
kawa 74fa072e10 docs(10-02): complete live remote name preview plan
- Create 10-02-SUMMARY.md with task details and deviations
- Update STATE.md: add decision, session info, update progress to 100%
- Update ROADMAP.md: phase 10 marked complete (3/3 summaries)
- Update REQUIREMENTS.md: mark UX-02 complete
2026-04-01 12:58:27 +02:00
kawa 58d96cbb48 docs(10-03): complete credential-field help text plan
- Add 10-03-SUMMARY.md with full deviation documentation
- Update STATE.md: progress 97%, decisions, session
- Update ROADMAP.md: phase 10 plan progress (2/3 summaries)
- Mark UX-04 requirement complete in REQUIREMENTS.md
2026-04-01 12:56:45 +02:00
kawa fefab65371 feat(10-02): integrate RemoteNamePreview into BackendSelectionStep with tests
- Add RemoteNamePreview integration tests: empty-state [my-remote] on render
- Add live-update test: typing 'test-remote' shows [test-remote] in DOM
- Fix getByText(/required/i) ambiguity: use alert.textContent.toMatch instead
  (description paragraph now contains 'required' — Rule 1 bug fix)
- BackendSelectionStep.tsx changes already committed (watch, helpText, RemoteNamePreview)
2026-04-01 12:56:32 +02:00
kawa be788a44b7 docs(10-01): complete intro-section and step-descriptions plan
- SUMMARY.md with full execution record
- STATE.md: progress updated, 3 decisions added, session recorded
- ROADMAP.md: phase 10 progress updated (1/3 summaries)
- REQUIREMENTS.md: UX-01 and UX-03 marked complete
- deferred-items.md: pre-existing RemoteConfigStep test failures logged
2026-04-01 12:56:16 +02:00
kawa 489dfab896 feat(10-03): add tooltip tests for enriched fields; fix selector ambiguity
- Add { selector: 'input' } to getByLabelText calls where tooltip buttons
  now create aria-label matches (DEBT-01 pattern, same fix as Phase 09-03)
- Affected queries: S3 fields, S3-compatible fields, GCS fields, B2 fields,
  OneDrive drive_id, and VALID-01 validation tests
- Add 4 new UX-01 tests verifying tooltip info buttons render for:
  S3 access_key_id, S3-compatible endpoint, GCS service_account_credentials,
  B2 account (application_key_id)
- All 37 tests pass
2026-04-01 12:55:14 +02:00
kawa 356d557348 feat(10-03): enrich registry.ts with tooltipText and helpText for credential fields
- S3: access_key_id and secret_access_key get helpText + tooltipText (IAM console guidance)
- S3: region gets tooltipText (where to find in S3 console)
- S3-compatible: access_key_id, secret_access_key get helpText + tooltipText (provider-specific)
- S3-compatible: endpoint gets tooltipText (common endpoint examples for Wasabi, R2, MinIO)
- GCS: project_number gets tooltipText (numeric ID vs text project ID)
- GCS: service_account_credentials gets tooltipText (IAM console steps + doc link)
- B2: account and key get tooltipText (App Keys page, shown-once warning)
- OneDrive: drive_id gets tooltipText (rclone authorize output, b! prefix)
- SFTP host and user intentionally unchanged (self-explanatory per user decision)
- Azure Blob, OneDrive token, SFTP pass/key_pem existing text untouched
2026-04-01 12:55:05 +02:00
kawa 4b837b19a0 feat(10-01): update App.test.tsx and add step descriptions to all 4 steps
- Update App.test.tsx: add 'shows intro section on initial render' test
- Update App.test.tsx: fix 'renders BackendSelectionStep' — click Get Started first
- Add fireEvent import for click simulation
- BackendSelectionStep: add description below h2 (text-sm text-on-surface-variant)
- RemoteConfigStep: add description below h2
- DeploymentStep: add description below h2
- ReviewStep: add h2 heading + description paragraph (step was missing h2)
- All descriptions use text-sm text-on-surface-variant mt-1 mb-4
2026-04-01 12:54:16 +02:00
kawa c1d1633816 feat(10-01): add IntroSection and showIntro gate to App.tsx
- Add useState import to WizardShell
- Add showIntro local state (default true) — no persistence, resets on reload
- Add IntroSection component with action-first copy and Get Started CTA
- Copy mentions Azure Blob, S3, OneDrive, and 4 more backends
- Explains rclone.conf + deployment script generation
- Uses MD3_BTN_FILLED for CTA, MD3 tokens throughout
- When showIntro false, renders existing StepIndicator + step content unchanged
2026-04-01 12:48:53 +02:00
kawa 3c9c5fd7d8 feat(10-02): create RemoteNamePreview component with TDD
- Stateless component accepting value prop (string)
- Empty/whitespace state: shows grayed [my-remote] placeholder + guidance text
- Value state: shows [typed-value] in on-surface-variant color
- Renders only div/span elements — no form controls
- 9 tests covering empty state, value state, and element type constraints
2026-04-01 12:48:50 +02:00
kawaandClaude Opus 4.6 8078664f05 docs(10-content-clarity): create phase plan
3 plans in 1 wave covering UX-01 through UX-04: intro section,
remote name preview, step descriptions, and credential help text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:43:38 +02:00
kawa 5052e9e66e docs(phase-10): add validation strategy 2026-04-01 12:38:34 +02:00
kawaandClaude Sonnet 4.6 cfc9967a28 docs(10): research phase content-clarity domain
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 12:37:26 +02:00
kawaandClaude Opus 4.6 648a17d261 docs(state): record phase 10 context session
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:32:34 +02:00
kawaandClaude Opus 4.6 43c83bf399 docs(10): capture phase context
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:32:24 +02:00
kawaandClaude Opus 4.6 12953a230c docs(phase-09): mark gap closure plans 09-04, 09-05 complete in roadmap
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:26:53 +02:00
kawaandClaude Opus 4.6 19eda65c77 docs(phase-09): re-verify after gap closure - 16/16 must-haves passed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:26:39 +02:00
kawa d173fd4455 docs(09-05): complete tooltip fix plan - hover behavior and helpTextPrefix prop
- SUMMARY.md: tooltip placement fix and hover+pin behavior documented
- STATE.md: decisions added, session updated, progress at 100%
- ROADMAP.md: Phase 9 plan 5/5 complete
2026-04-01 11:22:06 +02:00
kawa d2b6da297f test(09-05): add tooltip hover behavior tests and helpTextPrefix rendering tests
- FieldRenderer: 4 new tests for hover show/hide, click-pin, click-unpin
- TextFieldMD3: 3 new tests for helpTextPrefix with helpText, without, and prefix-only
- Import fireEvent from @testing-library/react added to FieldRenderer.test.tsx
2026-04-01 11:20:24 +02:00
kawa 23617c0422 feat(09-05): add helpTextPrefix prop and fix tooltip placement + hover behavior
- TextFieldMD3: add helpTextPrefix prop rendered inline left of helpText
- Guard changed to (helpText || helpTextPrefix) so icon renders without helpText
- FieldRenderer: move tooltip icon from above field to inline with helpText via helpTextPrefix
- FieldRenderer: add hoverTooltip state; tooltip visible on mouseEnter, dismisses on mouseLeave
- FieldRenderer: click-to-unpin clears hoverTooltip to prevent sticky tooltip after dismiss
- PasswordField: same tooltip fix as FieldRenderer text-branch
- Select-branch also gains hover support
2026-04-01 11:19:17 +02:00
kawaandClaude Opus 4.6 29aa6b9e2f docs(09-05): add tooltip fix plan and debug session
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:11:59 +02:00
kawaandClaude Opus 4.6 c848f7dd75 docs(09-05): diagnose tooltip gaps and create verified fix plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:10:58 +02:00
kawaandClaude Opus 4.6 788d90a904 fix(09): revise 09-05 plan based on checker feedback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:09:17 +02:00
kawaandClaude Opus 4.6 f2586dbef9 docs(09): create gap closure plan for tooltip hover and icon placement
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:03:10 +02:00
kawaandClaude Opus 4.6 53678e6fb3 test(09): complete UAT - 6 passed, 1 issue
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 10:58:16 +02:00
kawaandClaude Opus 4.6 ba0a029366 docs(phase-09): complete phase execution — 12/12 must-haves verified
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 10:32:11 +02:00
kawa dad829245a docs(09-04): complete BackendSelectionStep TextFieldMD3 gap closure plan
- 09-04-SUMMARY.md: all 1 tasks documented, 179 tests green
- STATE.md: position advanced, session updated
- ROADMAP.md: Phase 9 marked 4/4 plans complete
2026-04-01 10:28:48 +02:00
kawa 123f24c57d feat(09-04): replace plain Remote name input with TextFieldMD3
- Add import { TextFieldMD3 } from '../ui/TextFieldMD3'
- Replace label+input+error block with TextFieldMD3 component
- Pass registration={register('name')}, error={errors.name}, required
- Closes COMP-01 gap: all wizard text inputs now use TextFieldMD3 floating label
2026-04-01 10:27:23 +02:00
kawaandClaude Opus 4.6 2bcfe2ef4f docs(09-04): gap closure plan for BackendSelectionStep TextFieldMD3 migration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 10:21:54 +02:00
kawaandClaude Opus 4.6 107a259d19 docs(09): create gap closure plan for BackendSelectionStep TextFieldMD3 migration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 10:19:48 +02:00
kawaandClaude Sonnet 4.6 7d5b3f7e73 docs(09-03): complete MD3 component integration plan — Phase 9 fully complete
- Create 09-03-SUMMARY.md: TextFieldMD3 integration, button hierarchy, card elevation
- Update STATE.md: Phase 9 complete, decisions captured, position advanced to Phase 10
- Update ROADMAP.md: Phase 9 marked complete (3/3 plans), progress table updated
- Update REQUIREMENTS.md: COMP-03 marked complete — all Phase 9 requirements satisfied

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 09:52:18 +02:00
kawa d7a2cad7ea feat(09-03): apply MD3 button styles and elevation across all wizard steps
- BackendCard: rounded-lg -> rounded-xl, added shadow/shadow-md elevation
- OutputBlock: pre rounded -> rounded-xl + shadow-sm; Copy/Download use MD3_BTN_OUTLINED small variant
- BackendSelectionStep: added explicit Next submit button with MD3_BTN_FILLED
- RemoteConfigStep: Back uses MD3_BTN_OUTLINED, Next uses MD3_BTN_FILLED
- DeploymentStep: Back uses MD3_BTN_OUTLINED, Next/Review uses MD3_BTN_FILLED
- ReviewStep: Back uses MD3_BTN_OUTLINED, Download All (ZIP) uses MD3_BTN_FILLED
- All 179 tests pass (zero regressions)
2026-04-01 09:29:52 +02:00
kawa e560a6af84 feat(09-03): integrate TextFieldMD3 into FieldRenderer and PasswordField
- FieldRenderer text-branch now renders TextFieldMD3 with floating label
- PasswordField replaced with TextFieldMD3 + show/hide suffix toggle
- Tooltip button kept outside TextFieldMD3, rendered above it
- Removed placeholder prop from PasswordField (TextFieldMD3 uses placeholder=" " internally)
- All 179 tests pass (getByLabelText queries preserved via htmlFor/id pairing)
2026-04-01 09:27:44 +02:00
kawaandClaude Sonnet 4.6 7a02d483b3 docs(09-01): complete MD3 primitives plan — TextFieldMD3, button constants, DEBT-01 resolved
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 09:25:47 +02:00
kawa 532a994f6b fix(09-01): resolve DEBT-01 aria-label inconsistency in FieldRenderer and PasswordField
- Add FieldRenderer.test.tsx: 4 tests verifying both branches use aria-label consistently
- Fix FieldRenderer.tsx text-branch: replace sr-only span with aria-label={} matching select-branch
- Fix PasswordField.tsx tooltip button: same sr-only -> aria-label fix
- Update RemoteConfigStep.test.tsx: add { selector: 'input' } to getByLabelText queries that
  now conflict with tooltip button aria-labels (regression fix)
2026-04-01 09:24:14 +02:00
kawa 0c8403dc79 docs(09-02): complete StepIndicator MD3 rebuild plan
- Create 09-02-SUMMARY.md with execution details and decisions
- Update STATE.md position, decisions, session info
- Update ROADMAP.md phase 9 progress (1/3 plans complete)
- Mark COMP-04 complete in REQUIREMENTS.md
2026-04-01 09:20:29 +02:00
kawa 9441dcdae7 feat(09-01): create TextFieldMD3 component with floating label + MD3 button constants
- Add TextFieldMD3.tsx: outlined text field with CSS-only floating label via peer/:not(:placeholder-shown)
- Add TextFieldMD3.test.tsx: 9 tests covering htmlFor/id pairing, registration spread, error/helpText/suffix/required slots
- Add src/styles/md3-buttons.ts: MD3_BTN_FILLED, MD3_BTN_OUTLINED, MD3_BTN_TEXT class constants
2026-04-01 09:20:19 +02:00
kawa b70484423e feat(09-02): rebuild StepIndicator with MD3 circles and connectors
- Replace text breadcrumbs with numbered circles + connector lines
- Completed steps show checkmark, are clickable buttons with label text
- Active step highlighted with border-primary and bg-primary/10
- Future steps muted with border-outline and text-on-surface-container/40
- Connector lines filled bg-primary for completed, bg-outline for future
- All inline style={{}} props removed, no hardcoded color values
- handleStepClick logic preserved: SET_REMOTE_PARAMS({}) before SET_STEP(0)
- All 5 WIZD-03 tests pass without modification
2026-04-01 09:19:06 +02:00
kawaandClaude Opus 4.6 a6c2ec7292 docs(09-md3-components): create phase plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 09:14:15 +02:00
kawaandClaude Opus 4.6 80a26e395d docs(phase-9): add research and validation strategy
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 09:07:15 +02:00
kawa 68fe57f585 docs(phase-09): research MD3 components phase 2026-04-01 09:05:56 +02:00
kawaandClaude Opus 4.6 66f7cf761a docs(phase-08): complete phase execution — verification passed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 05:27:22 +02:00
kawaandClaude Sonnet 4.6 0352900e53 docs(08-02): complete phase 8 — Task 3 approved, STATE and SUMMARY updated
- Mark Task 3 (human-verify checkpoint) as user-approved
- Update SUMMARY.md to reflect all 3 tasks complete
- Update STATE.md: Phase 8 complete, THEME-01/THEME-02 satisfied
- Progress: 50% (2 of 4 v1.2 phases complete)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 05:17:39 +02:00
kawa 18252cbad1 docs(08-02): complete component color migration plan
- 08-02-SUMMARY.md: documents mechanical migration of 63 hardcoded color classes to semantic MD3 tokens across 9 components
- STATE.md: updated decisions and session info
- ROADMAP.md: phase 8 marked complete (2/2 plans with summaries)
2026-04-01 05:11:49 +02:00
kawa 1ac32eadc5 feat(08-02): migrate wizard components to semantic MD3 token classes
- AzureAuthToggle: border-gray-300 -> border-outline, bg-blue-600/text-white -> bg-primary/text-on-primary, bg-white/text-gray-700/hover:bg-gray-50 -> bg-surface-container/text-on-surface-container/hover:bg-surface
- SftpAuthToggle: same tab/info-tooltip mapping as AzureAuthToggle + text-gray-700 -> text-on-surface-container
- DeploymentStep: Back button border-gray-300/hover:bg-gray-50 -> border-outline/hover:bg-surface; Next bg-blue-600 -> bg-primary/text-on-primary
- OutputBlock: text-gray-700 -> text-on-surface-container, border-gray-300 -> border-outline (x2), bg-gray-900/text-gray-100 -> bg-surface-variant/text-on-surface-variant
- RemoteConfigStep: Back/Next buttons same as DeploymentStep pattern
- ReviewStep: text-green-700/bg-green-50 -> text-success/bg-success/10, bg-yellow-50/border-yellow-300 -> bg-warning/10/border-warning, text-yellow-800/900 -> text-warning, Back/Download buttons semantic tokens
- FieldRenderer (fix): text branch label text-gray-700 -> text-on-surface-container, text branch tooltip bg-blue-50/border-blue-200 -> bg-primary/10/border-primary/30
2026-04-01 05:08:33 +02:00
kawa 4e30f2ee79 feat(08-02): migrate UI primitives to semantic MD3 token classes
- BackendCard: border-blue-600/bg-blue-50 -> border-primary/bg-primary/10, border-gray-200/bg-white -> border-outline/bg-surface-container, text-gray-900 -> text-on-surface, text-gray-500 -> text-on-surface-container/70
- FieldRenderer: text-gray-700 -> text-on-surface-container, text-red-500 -> text-error, text-blue-500/700 -> text-primary, bg-blue-50/border-blue-200 -> bg-primary/10/border-primary/30, border-red-500 -> border-error, focus:ring-red-300 -> focus:ring-error/50, border-gray-300 -> border-outline, focus:ring-blue-300 -> focus:ring-primary/50, text-gray-500 -> text-on-surface-container/70, text-red-600 -> text-error
- PasswordField: same mapping as FieldRenderer + text-gray-400/hover:text-gray-700 -> text-on-surface-container/50 hover:text-on-surface-container
2026-04-01 05:04:33 +02:00
kawa 01752a126d docs(08-01): complete theme foundation plan 01
- SUMMARY with MD3 token system, ThemeToggle, FOUC prevention
- STATE updated: decisions, session, 97% progress
- ROADMAP phase 8 marked In Progress (1/2 plans)
- REQUIREMENTS THEME-01, THEME-02 marked complete
2026-04-01 04:51:55 +02:00
kawa 074dc33c86 feat(08-01): implement ThemeToggle component and wire into App.tsx
- Add ThemeToggle segmented control (Light/Dark/System) with DOM class toggle
- Use getStored() lazy init and applyTheme() for side-effect-free render
- Stub localStorage and matchMedia in tests for Node v25 compatibility
- Wire ThemeToggle into App.tsx header flex row next to h1
- Change App outer div to bg-surface, h1 to text-on-surface
- All 7 ThemeToggle unit tests passing; 166 total tests green
2026-04-01 04:49:56 +02:00
kawa fa2c0d963f test(08-01): add failing tests for ThemeToggle component
- Test renders three buttons: Light, Dark, System
- Test default state is System when localStorage is empty
- Test clicking Dark adds .dark class to documentElement
- Test clicking Light removes .dark class from documentElement
- Test clicking Dark writes to localStorage key r2b-theme
- Test active/inactive aria-pressed state
- Test group has aria-label="Theme"
2026-04-01 04:34:01 +02:00
kawa 602755276e feat(08-01): define MD3 color tokens and flash prevention script
- Add @custom-variant dark directive for .dark class-based toggling
- Define 15 CSS custom property token pairs (light/dark) in @layer base
- Map all tokens to Tailwind utility classes via @theme with var() references
- Include warning/success tokens (amber + green) for ReviewStep coverage
- Add body transition with prefers-reduced-motion guard
- Add inline blocking script in index.html head to prevent FOUC
2026-04-01 04:33:31 +02:00
kawaandClaude Opus 4.6 2ea0a1faaf fix(08): revise plans based on checker feedback
- 08-01 Task 2: change type="auto" tdd="true" to type="tdd" (schema consistency)
- 08-02 Task 2: fix inverted on-role tokens in ReviewStep migration
  bg-on-success -> bg-success/10, bg-on-warning -> bg-warning/10
- 08-02 mapping comment: same fix for consistency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 18:11:02 +02:00
kawaandClaude Opus 4.6 bee3bad93a docs(08): create phase plan - 2 plans, 2 waves
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 18:07:29 +02:00
kawa 0ef71cdcab docs(phase-8): add validation strategy 2026-03-31 18:01:58 +02:00
kawaandClaude Sonnet 4.6 abb8320d74 docs(08): research phase — Tailwind v4 @theme tokens, dark mode, FOUC prevention
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 18:00:35 +02:00
kawaandClaude Opus 4.6 03aca9c768 docs(state): record phase 8 context session
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:53:32 +02:00
kawaandClaude Opus 4.6 43246745e7 docs(08): capture phase context
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:53:23 +02:00
kawaandClaude Opus 4.6 9196d030cc docs: create milestone v1.2 roadmap (4 phases) — 15 requirements mapped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:42:16 +02:00
kawaandClaude Opus 4.6 edfbbef07f docs: define milestone v1.2 requirements — 15 requirements across 5 categories
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:14:51 +02:00
kawaandClaude Opus 4.6 4d4b01d9b7 docs(v1.2): complete project research — stack, features, architecture, pitfalls, summary
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:34:46 +02:00
kawaandClaude Opus 4.6 4a5ca206db docs: start milestone v1.2 UI Polish & Clarity
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:22:44 +02:00
126 changed files with 15885 additions and 1435 deletions
+3 -1
View File
@@ -62,7 +62,9 @@
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" requirements mark-complete UX-01)",
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" roadmap get-phase \"07\")",
"Bash(grep \"^status:\" .planning/phases/07-validation-ux-polish/*-VERIFICATION.md)",
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" commit \"docs\\(phase-07\\): complete phase execution — VALID-01 and UX-01 verified\" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md .planning/phases/07-validation-ux-polish/07-VERIFICATION.md)"
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" commit \"docs\\(phase-07\\): complete phase execution — VALID-01 and UX-01 verified\" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md .planning/phases/07-validation-ux-polish/07-VERIFICATION.md)",
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" commit \"test\\(09\\): complete UAT - 6 passed, 1 issue\" --files \".planning/phases/09-md3-components/09-UAT.md\")",
"Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" commit \"test\\(11\\): complete UAT - 8 passed, 0 issues\" --files \".planning/phases/11-polish-responsiveness/11-UAT.md\")"
]
}
}
+14
View File
@@ -0,0 +1,14 @@
node_modules
dist
dist-ssr
.git
.gitignore
.planning
.claude
.vscode
.idea
*.log
*.local
Dockerfile
.dockerignore
docker-compose.yml
+6
View File
@@ -0,0 +1,6 @@
# core.autocrlf is on for this repo; these files are consumed by Linux inside the
# container, where a CRLF shebang fails as "no such file or directory".
*.sh text eol=lf
*.conf text eol=lf
*.template text eol=lf
Dockerfile text eol=lf
+47
View File
@@ -0,0 +1,47 @@
name: Publish image
on:
push:
tags:
- '*'
workflow_dispatch:
inputs:
tag:
description: Image tag to publish (only used for manual runs)
required: false
default: manual
jobs:
build-and-push:
runs-on: debian-13
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Resolve image tag
id: vars
run: |
if [ "${{ github.ref_type }}" = "tag" ]; then
echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
else
echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
fi
- name: Log in to git.azuze.fr registry
uses: docker/login-action@v3
with:
registry: git.azuze.fr
username: ${{ gitea.actor }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
git.azuze.fr/kawa/ready2blob:${{ steps.vars.outputs.tag }}
git.azuze.fr/kawa/ready2blob:latest
+37
View File
@@ -1,5 +1,42 @@
# Milestones
## v1.3 Backend Expansion (Shipped: 2026-04-16)
**Phases completed:** 1 phase (13), 4 plans
**Timeline:** 2026-04-01 (1 day)
**Stats:** 28 files changed, +3,959 / -206 lines; total 6,055 TypeScript LOC
**Key accomplishments:**
1. BACKEND_REGISTRY expanded from 7 → 18 backends; BackendType derived from `keyof typeof` — no manual union to maintain
2. BACKEND_SCHEMAS auto-generated from registry via `Object.fromEntries` — zero per-backend boilerplate
3. OAuthInstructions collapsible component with backend-specific `rclone authorize` commands (TDD, 4 tests)
4. GdriveAuthToggle dual-auth component (OAuth token vs service account) for Google Drive
5. BackendSelectionStep with category groups, instant search filtering, and inline SVG icons
6. RemoteConfigStep wired for all 18 backends via `renderBackendFields()` switch function; `backendLabel` derived from registry
**Archive:** `.planning/milestones/v1.3-ROADMAP.md`, `.planning/milestones/v1.3-REQUIREMENTS.md`
---
## v1.2 UI Polish & Clarity (Shipped: 2026-04-01)
**Phases completed:** 5 phases (812), 14 plans
**Timeline:** 2026-03-31 → 2026-04-01 (2 days)
**Stats:** 78 files changed, +9,908 / -287 lines
**Key accomplishments:**
1. Full MD3 color token system — CSS custom properties in a single token file, all components use semantic classes (`bg-surface`, `text-on-primary`, etc.)
2. Dark mode toggle (System/Light/Dark) — DOM class toggle pattern (not React Context), persisted in localStorage, zero FOUC
3. TextFieldMD3 with CSS-only floating label (`peer-[:not(:placeholder-shown)]`), MD3 button hierarchy constants, rebuilt StepIndicator with numbered circles + checkmarks
4. App intro section, step descriptions, remote name live config preview, and contextual help/tooltip enrichment across all 18 credential fields
5. Mobile-responsive layout, MD3 `focus-visible` ring-3 indicators on all interactive elements
6. Step fade/slide transition with `prefers-reduced-motion` guard; auto-scroll to first validation error
7. Dark mode visibility fixes: semantic tokens on select, h2 headings, DeploymentStep form controls, ReviewStep security checkbox
**Archive:** `.planning/milestones/v1.3-ROADMAP.md` (includes v1.2 phases in archive)
---
## v1.0 MVP (Shipped: 2026-03-27)
**Phases completed:** 4 phases, 18 plans
+84 -34
View File
@@ -2,7 +2,7 @@
## What This Is
Ready2Blob is a pure-frontend web app that helps IT professionals deploy rclone onto end-user Windows PCs. A step-by-step wizard collects backend credentials (Azure Blob, S3, S3-compatible, OneDrive, SFTP, Google Cloud Storage, Backblaze B2) and deployment preferences, then generates rclone config files and PowerShell scripts ready to push via Intune or any RMM tool — all client-side, no server required.
Ready2Blob is a pure-frontend web app that helps IT professionals deploy rclone onto end-user Windows PCs. A step-by-step wizard collects backend credentials (18 backends: Azure Blob, S3, S3-compatible, OneDrive, SFTP, GCS, Backblaze B2, Google Drive, Dropbox, Box, pCloud, FTP, WebDAV, SMB, HTTP, Seafile, Yandex Disk, Koofr) and deployment preferences, then generates rclone config files and PowerShell scripts ready to push via Intune or any RMM tool — all client-side, no server required.
## Core Value
@@ -10,15 +10,68 @@ An IT pro can go from zero to a deployable rclone setup in minutes — without n
## Current State
**Shipped:** v1.1 — 2026-03-31
**Backends:** 7 (Azure Blob, S3, S3-compatible, OneDrive, SFTP, GCS, Backblaze B2)
**Shipped:** v1.3 — 2026-04-16
**Backends:** 18 (Azure Blob, S3, S3-compatible, OneDrive, SFTP, GCS, Backblaze B2, Google Drive, Dropbox, Box, pCloud, FTP, WebDAV, SMB, HTTP, Seafile, Yandex Disk, Koofr)
**Tech stack:** Vite 6, React 18, TypeScript 5, Tailwind v4, react-hook-form 7, Zod 4, Vitest 4, JSZip
**Test coverage:** 159 tests across all test files — all GREEN
**Total milestones:** 2 shipped (v1.0 + v1.1), 7 phases, 29 plans
**Test coverage:** 165+ tests across all test files — all GREEN
**Total milestones:** 3 shipped (v1.0 + v1.1 + v1.2 + v1.3), 13 phases, 40 plans
## Next Milestone Goals
## Requirements
*(Not yet defined — run `/gsd:new-milestone` to start next milestone)*
### Validated
- ✓ Backend Schema Registry as single source of truth for form rendering, Zod validation, config generation — v1.0
- ✓ 4-step wizard (backend selection → config → deployment → review/download) with back-navigation — v1.0
- ✓ Review + download layer: live preview, security gate, individual downloads, ZIP bundle, clipboard copy — v1.0
- ✓ Registry-driven BackendSelectionStep, scriptTargets filtering, ReviewStep Back button, dead export cleanup, act() fix — v1.1
- ✓ 4 new backends (OneDrive, SFTP, GCS, B2), SftpAuthToggle, inline field validation, contextual tooltips — v1.1
-**THEME-01**: MD3 color token system (CSS custom properties), all components use semantic classes — v1.2
-**THEME-02**: System/Light/Dark theme toggle, persisted in localStorage, zero FOUC — v1.2
-**COMP-01**: MD3 outlined text fields with CSS-only floating labels — v1.2
-**COMP-02**: MD3 button hierarchy (filled/outlined/text) across all wizard steps — v1.2
-**COMP-03**: Backend cards and output blocks with MD3 elevation and tonal surface tint — v1.2
-**COMP-04**: StepIndicator with numbered circles, connectors, checkmarks — v1.2
-**DEBT-01**: FieldRenderer consistent `aria-label` across text/select branches — v1.2
-**UX-01**: Intro section explaining Ready2Blob with CTA to start wizard — v1.2
-**UX-02**: Remote name field with example, help text, live config preview — v1.2
-**UX-03**: 1-2 sentence step descriptions on all 4 wizard steps — v1.2
-**UX-04**: Contextual help text on all backend credential fields — v1.2
-**POLISH-01**: Mobile-responsive layout (stacking cards, full-width fields, stretch buttons) — v1.2
-**POLISH-02**: MD3 `focus-visible` ring-3 indicators on all interactive elements — v1.2
-**POLISH-03**: Step fade/slide transition with `prefers-reduced-motion` guard — v1.2
-**POLISH-04**: Auto-scroll to first validation error on form submission — v1.2
-**DARK-01**: All wizard step h2 headings readable in dark mode — v1.2
-**DARK-02**: DeploymentStep native form controls visible with semantic tokens in dark mode — v1.2
-**DARK-03**: FieldRenderer select has proper bg/text color in dark mode — v1.2
-**REMOTE-01**: BACKEND_REGISTRY with 18 backends, correct rclone fields, displayName, description, category — v1.3
-**REMOTE-02**: BackendType derived from `keyof typeof BACKEND_REGISTRY` — no manual union — v1.3
-**REMOTE-03**: BackendSelectionStep with category groups, instant search, inline SVG icons — v1.3
-**REMOTE-04**: OAuthInstructions collapsible component with backend-specific `rclone authorize` — v1.3
-**REMOTE-05**: RemoteConfigStep renders correct form for all 18 backends including GdriveAuthToggle — v1.3
-**REMOTE-06**: buildRcloneConf produces valid rclone.conf for all 18 backends — v1.3
### Active
*(No active requirements — planning next milestone)*
### Out of Scope
| Feature | Reason |
|---------|--------|
| User accounts / saved templates | No backend, no persistence |
| Server-side generation | Everything runs in the browser |
| Test connection / validate credentials | Requires backend proxy (CORS blocks direct cloud API calls) |
| rclone version management / auto-update on endpoints | Separate lifecycle problem |
| Non-Windows targets | Scripts target Windows endpoints only |
| Auto-push to Intune via Graph API | Requires Azure AD app registration, massive scope increase |
| Config credential encryption | rclone encrypted configs require interactive unlock, incompatible with unattended deployment |
| Multiple remotes in one config | Significant wizard UX complexity, defer to v2 |
| OAuth flow in browser | Requires backend proxy; paste pre-obtained token instead |
| MUI or Material Tailwind component library | Bundle bloat (~300KB+) for 4-step wizard; custom tokens + Tailwind lighter |
| Page-per-step routing (React Router) | Wizard state is ephemeral credentials in memory; refreshing loses context |
| Glassmorphism / neomorphism effects | CPU-intensive, low contrast accessibility issues |
| Arbitrary user-picked hex color | Breaks accessibility; curated presets cover 95% of personalization need |
| Custom checkbox/radio styling | High effort for few toggles; native inputs sufficient |
## Constraints
@@ -26,18 +79,6 @@ An IT pro can go from zero to a deployable rclone setup in minutes — without n
- **Target OS**: Windows endpoints only (scripts are PowerShell)
- **Secrets**: Plain text in generated files — UI must display a clear security warning before download
## Out of Scope
- User accounts / saved templates — no backend, no persistence
- Server-side generation — everything runs in the browser
- Test connection / validate credentials — requires backend proxy (CORS blocks direct cloud API calls)
- rclone version management / auto-update on endpoints — separate lifecycle problem
- Non-Windows targets — scripts target Windows endpoints only
- Auto-push to Intune via Graph API — requires Azure AD app registration, massive scope increase
- Config credential encryption — rclone encrypted configs require interactive unlock, incompatible with unattended deployment
- Multiple remotes in one config — significant wizard UX complexity, defer to v2
- OAuth flow in browser (OneDrive) — requires backend proxy; paste pre-obtained token instead
## Key Decisions
| Decision | Rationale | Outcome |
@@ -46,33 +87,42 @@ An IT pro can go from zero to a deployable rclone setup in minutes — without n
| BACKEND_REGISTRY as single source of truth | Drives form rendering, Zod schemas, and config generation from one definition | ✓ Good — zero duplication across phases |
| useReducer + Context API (no external state lib) | Zero dependencies, sufficient for 4-step wizard | ✓ Good — kept bundle lean |
| Wave 0 TDD (stubs RED before implementation) | Forces API contract before code, catches integration mismatches early | ✓ Good — caught several issues upfront |
| Support all rclone backends (not Azure-only) | Broader utility despite "Blob" in project name | ✓ Good — 7 backends and growing |
| Support all rclone backends (not Azure-only) | Broader utility despite "Blob" in project name | ✓ Good — 18 backends shipped |
| Optional rclone install in scripts | Different orgs have different baseline images | ✓ Good — no friction for orgs that pre-install |
| Tailwind v4 via @tailwindcss/vite plugin | No postcss.config.js, no tailwind.config.js needed | ✓ Good — cleaner config |
| Zod v4 + @hookform/resolvers v5 together | v5 resolvers required for Zod v4 compatibility | ✓ Good — noted for future upgrades |
| AzureAuthToggle / SftpAuthToggle CSS hidden pattern | Preserves both auth fields in react-hook-form state when toggling | ✓ Good — avoids field reset on toggle |
| AzureAuthToggle / SftpAuthToggle CSS hidden pattern | Preserves both auth fields in react-hook-form state when toggling | ✓ Good — extended to GdriveAuthToggle in v1.3 |
| FieldDef.validate + tooltipText extension | Additive interface change, non-breaking to existing backends | ✓ Good — clean composition |
| Registry-loop rendering in RemoteConfigStep | Three-branch ternary (azureblob/sftp/else) — custom logic minimal | ✓ Good — scales with new backends |
| ThemeToggle via DOM class toggle, not React Context | Avoids re-render cascade on theme change | ✓ Good — instant theme switch |
| Two-layer CSS token pattern (@theme + @layer base) | Required for .dark cascade in Tailwind v4 | ✓ Good — only valid approach for v4 |
| vi.stubGlobal for localStorage/matchMedia in tests | Node v25 experimental WebStorage breaks standard Storage API in jsdom | ✓ Good — necessary workaround |
| TextFieldMD3 CSS-only floating label | `peer-[:not(:placeholder-shown)]` — no JS state | ✓ Good — simpler, animatable |
| DEBT-01 fix: aria-label directly on tooltip buttons | Prevents getByLabelText ambiguity with { selector: 'input' } guard | ✓ Good — consistent pattern |
| key={state.currentStep} on step wrapper | Forces React remount to trigger CSS animation from initial state | ✓ Good — cleanest animation approach |
| BackendType derived from `keyof typeof BACKEND_REGISTRY` | Eliminates manual union maintenance at 18+ backends | ✓ Good — adding a backend is one touch-point |
| BACKEND_SCHEMAS auto-generated via Object.fromEntries | No per-backend buildZodSchema call needed | ✓ Good — zero boilerplate |
| Registry `as const` (not Record<BackendType,…> cast) | Cast was circular when BackendType derived from registry | ✓ Good — necessary to break circularity |
| type=search for search input | Prevents getByRole('textbox') collision with remote name TextFieldMD3 | ✓ Good — test-selector discipline |
| renderBackendFields() switch vs nested ternaries | 7+ branches require readable structure | ✓ Good — extensible for future backends |
| OAuthInstructions step-prefix detection | Renders authorize command in `<code>` element; keeps test assertions compatible | ✓ Good — clean heuristic |
| BACKEND_ICONS as Partial<Record<BackendType, FC>> | Missing entries = no icon rendered (no error) | ✓ Good — graceful degradation |
<details>
<summary>Previous milestone context (v1.0 → v1.1)</summary>
<summary>Previous milestone context (v1.0 → v1.3 key decisions)</summary>
**v1.0 shipped:** 2026-03-27, 4 phases, 18 plans, ~2,746 LOC
**v1.1 shipped:** 2026-03-31, 3 phases, 11 plans, +7,311 lines
**v1.2 shipped:** 2026-04-01, 5 phases, 14 plans, +9,908 lines
**v1.3 shipped:** 2026-04-16, 1 phase, 4 plans, +3,753 lines
**v1.0 tech debt resolved in v1.1:**
- scriptTargets filtering in ReviewStep (TECH-01)
- ReviewStep Back button (TECH-02)
- Registry-driven BackendSelectionStep (TECH-03)
- Dead export removal (TECH-04)
- act() warnings fix (TECH-05)
**v1.2 tech debt resolved in v1.3:**
- FieldRenderer aria inconsistency (DEBT-01) — resolved Phase 9
**v1.1 remaining tech debt:**
- 7 browser UI verifications pending (confirmatory)
- FieldRenderer aria inconsistency (cosmetic)
- Phase 7 Nyquist compliance partial
**v1.3 tech debt carried forward:**
- Test selectors risk if UI restyled heavily (131+ selectors, ongoing discipline required)
- No formal milestone audit run for v1.3
</details>
---
*Last updated: 2026-03-31 after v1.1 milestone completed*
*Last updated: 2026-04-16 after v1.3 milestone*
+87 -1
View File
@@ -41,6 +41,83 @@
---
## Milestone: v1.2 — UI Polish & Clarity
**Shipped:** 2026-04-01
**Phases:** 5 (812) | **Plans:** 14 | **Stats:** 78 files, +9,908 / -287 lines
### What Was Built
- Full MD3 color token system in a single token file; all components use semantic utility classes (`bg-surface`, `text-on-primary`) instead of hardcoded Tailwind colors
- System/Light/Dark theme toggle persisted in localStorage with zero flash of unstyled content (DOM class toggle, not React Context)
- TextFieldMD3 with CSS-only floating label, MD3 button hierarchy constants, rebuilt StepIndicator with numbered circles + checkmarks + connectors
- App intro section, step descriptions on all 4 steps, remote name live config preview, contextual help/tooltip enrichment across all credential fields
- Mobile-responsive layout, `focus-visible` ring-3 indicators, step fade/slide transition with `prefers-reduced-motion` guard, auto-scroll to first validation error
- Dark mode visibility fixes: semantic tokens on select, h2 headings, DeploymentStep form controls, ReviewStep security checkbox
### What Worked
- **Bottom-up migration order** (tokens → primitives → content → polish → fixes): each phase stood on solid ground from the one below; no circular rework
- **CSS-only floating label** (`peer-[:not(:placeholder-shown)]`) with no JS state — simpler to reason about, trivially animatable, one fewer React state variable
- **DOM class toggle for dark mode** (not React Context): theme switch is instant and doesn't trigger a re-render cascade across 150+ components
- **`key={state.currentStep}` on step wrapper**: one-line way to force React remount and reset CSS animation to initial state — no animation JS state needed
- **TDD discipline held** on Phase 12 dark mode fixes: found and fixed selector issues before they became regressions
### What Was Inefficient
- **Phase 12 inserted as a gap fix**: dark mode regressions were discovered post-Phase 11 instead of being caught in Phase 11 validation — cost 1 extra phase
- **`aria-label` / `getByLabelText` selector ambiguity** recurred across Phases 9, 10, and 12 — the DEBT-01 pattern (`{ selector: 'input' }`) had to be applied three times in three different phases instead of being resolved once and documented
- **Traceability table in REQUIREMENTS.md not updated** as plans completed — table showed "Planned" for completed items, causing confusion at milestone close
### Patterns Established
- Two-layer CSS token pattern: `@theme var()` references + `@layer base :root/.dark` raw values — required for `.dark` cascade in Tailwind v4
- `vi.stubGlobal` for localStorage/matchMedia — Node v25 experimental WebStorage breaks standard Storage API in jsdom
- CSS-only reduced-motion guard via `@media (prefers-reduced-motion: reduce)` — no JS listener needed
- Tooltip hover: dual-state (`hoverTooltip` + `showTooltip`); click-unpin clears `hoverTooltip` to prevent sticky tooltip after dismiss
- `ring-inset` on ThemeToggle focus ring because `overflow-hidden` on container clips outset rings
### Key Lessons
1. **Validate dark mode in every phase, not as a separate cleanup phase**: each component migration should include a dark-mode check, not defer it
2. **When a test selector pattern is discovered, document it once and reference it** — the `{ selector: 'input' }` guard should have been in a testing conventions note from Phase 9
3. **Keep traceability tables current** during execution, not just at planning time
---
## Milestone: v1.3 — Backend Expansion
**Shipped:** 2026-04-16
**Phases:** 1 (13) | **Plans:** 4 | **Stats:** 28 files, +3,959 / -206 lines
### What Was Built
- BACKEND_REGISTRY expanded from 7 → 18 backends; `BackendType` derived from `keyof typeof BACKEND_REGISTRY` — no manual union to maintain
- `BACKEND_SCHEMAS` auto-generated from registry keys via `Object.fromEntries` — zero per-backend boilerplate
- `OAuthInstructions` collapsible component with backend-specific `rclone authorize` commands (TDD, 4 tests)
- `GdriveAuthToggle` dual-auth component (OAuth token vs service account) — CSS hidden pattern extended from SftpAuthToggle
- `BackendSelectionStep` with 3 category groups, instant search filtering across 4 fields, inline SVG icons
- `RemoteConfigStep` wired for all 18 backends via clean `renderBackendFields()` switch function; `backendLabel` derived from registry
### What Worked
- **`as const` registry with derived BackendType**: one touch-point when adding a backend (registry entry only); Zod schemas auto-generate, type union auto-narrows
- **Incremental plan structure** (registry → components → UI → wiring): each plan had exactly the right scope — no plan was too large or too small
- **TDD for new UI components**: `OAuthInstructions` and `GdriveAuthToggle` written test-first; caught a step-prefix detection edge case before integration
- **type=search for search input**: simple attribute that prevents `getByRole('textbox')` collision — elegant selector discipline
- **renderBackendFields() switch**: 7+ backend branches are unreadable as nested ternaries; switch is flat and extensible
### What Was Inefficient
- **Registry had 18 backends but plan said 17**: a counting error in the plan caused minor confusion during 13-01 execution — plans should verify counts against reality
- **Traceability table not updated** (same as v1.2) — REMOTE-03/04/05 showed "Planned" at milestone close despite being complete
### Patterns Established
- `BackendType` from `keyof typeof BACKEND_REGISTRY` (not explicit union) — the canonical pattern going forward
- `BACKEND_SCHEMAS` from `Object.fromEntries` — zero per-backend call sites
- Registry `as const` (not `Record<BackendType,…>` cast) — cast is circular when BackendType derived from registry
- `Partial<Record<BackendType, FC<IconProps>>>` for optional icon map — missing entries render nothing (no error)
- `getByLabelText` partial regex (`/label/i` not `/^label$/i`) — TextFieldMD3 appends `*` to required field labels
### Key Lessons
1. **Update the traceability table as plans complete**, not just at the start — stale status fields caused confusion at milestone close (2nd occurrence after v1.2)
2. **Count registry entries in the plan, not just at planning time** — the 17/18 mismatch was avoidable
3. **`as const` registries with derived types are strictly superior to explicit unions**: eliminates an entire class of drift bugs, zero cost
---
## Cross-Milestone Trends
### Process Evolution
@@ -48,14 +125,23 @@
| Milestone | Phases | Plans | Key Change |
|-----------|--------|-------|------------|
| v1.0 | 4 | 18 | Wave 0 TDD pattern established; inside-out build order validated |
| v1.1 | 3 | 11 | CSS-hidden auth toggle pattern; FieldDef additive extension model |
| v1.2 | 5 | 14 | Two-layer CSS token system; DOM class dark mode; CSS-only floating label |
| v1.3 | 1 | 4 | `keyof typeof` derived BackendType; `Object.fromEntries` schema auto-gen |
### Cumulative Quality
| Milestone | Tests | Notes |
|-----------|-------|-------|
| v1.0 | 98 (12 files) | 24/24 requirements satisfied, 5/5 E2E flows |
| v1.1 | 159 (18 files) | 7 browser UI verifications deferred |
| v1.2 | 159+ (same files, heavier coverage) | All DEBT-01 + POLISH/DARK/UX requirements satisfied |
| v1.3 | 165+ | All 6 REMOTE requirements satisfied; visual verification approved |
### Top Lessons (Verified Across Milestones)
1. Single source of truth (BACKEND_REGISTRY) eliminates entire classes of drift bugs
1. Single source of truth (BACKEND_REGISTRY) eliminates entire classes of drift bugs — validated at v1.0, proven at v1.3 scale (18 backends)
2. Wave 0 TDD stubs are worth the upfront cost — they define the contract and make RED→GREEN progress visible
3. **Update traceability tables as plans complete** — stale status fields caused confusion at both v1.2 and v1.3 milestone close (recurring issue)
4. CSS-only approaches (floating label, reduced-motion guard, auth toggle) are simpler than JS state — reach for CSS first
5. `as const` registries with derived types (`keyof typeof`) are strictly superior to explicit unions — zero drift possible
+30
View File
@@ -4,6 +4,8 @@
-**v1.0 MVP** — Phases 14 (shipped 2026-03-27)
-**v1.1 Backlog & Tech Debt** — Phases 57 (shipped 2026-03-31)
-**v1.2 UI Polish & Clarity** — Phases 812 (shipped 2026-04-01)
-**v1.3 Backend Expansion** — Phase 13 (shipped 2026-04-16)
## Phases
@@ -30,6 +32,28 @@ Full phase details: [.planning/milestones/v1.1-ROADMAP.md](milestones/v1.1-ROADM
</details>
<details>
<summary>✅ v1.2 UI Polish & Clarity (Phases 812) — SHIPPED 2026-04-01</summary>
- [x] Phase 8: Theme Foundation (2/2 plans) — completed 2026-04-01
- [x] Phase 9: MD3 Components (5/5 plans) — completed 2026-04-01
- [x] Phase 10: Content & Clarity (3/3 plans) — completed 2026-04-01
- [x] Phase 11: Polish & Responsiveness (2/2 plans) — completed 2026-04-01
- [x] Phase 12: Dark Mode Visibility Fixes (2/2 plans) — completed 2026-04-01
Full phase details: [.planning/milestones/v1.3-ROADMAP.md](milestones/v1.3-ROADMAP.md)
</details>
<details>
<summary>✅ v1.3 Backend Expansion (Phase 13) — SHIPPED 2026-04-16</summary>
- [x] Phase 13: Add Remaining RClone Remotes (4/4 plans) — completed 2026-04-16
Full phase details: [.planning/milestones/v1.3-ROADMAP.md](milestones/v1.3-ROADMAP.md)
</details>
## Progress
| Phase | Milestone | Plans Complete | Status | Completed |
@@ -41,3 +65,9 @@ Full phase details: [.planning/milestones/v1.1-ROADMAP.md](milestones/v1.1-ROADM
| 5. Tech Debt | v1.1 | 4/4 | Complete | 2026-03-30 |
| 6. New Backends | v1.1 | 4/4 | Complete | 2026-03-31 |
| 7. Validation & UX Polish | v1.1 | 3/3 | Complete | 2026-03-31 |
| 8. Theme Foundation | v1.2 | 2/2 | Complete | 2026-04-01 |
| 9. MD3 Components | v1.2 | 5/5 | Complete | 2026-04-01 |
| 10. Content & Clarity | v1.2 | 3/3 | Complete | 2026-04-01 |
| 11. Polish & Responsiveness | v1.2 | 2/2 | Complete | 2026-04-01 |
| 12. Dark Mode Visibility Fixes | v1.2 | 2/2 | Complete | 2026-04-01 |
| 13. Add Remaining RClone Remotes | v1.3 | 4/4 | Complete | 2026-04-16 |
+82 -18
View File
@@ -1,16 +1,16 @@
---
gsd_state_version: 1.0
milestone: v1.1
milestone_name: Backlog & Tech Debt
status: completed
stopped_at: Milestone v1.1 archived
last_updated: "2026-03-31T15:00:00Z"
last_activity: 2026-03-31 — v1.1 milestone completed and archived
milestone: v1.3
milestone_name: Backend Expansion
status: milestone_complete
stopped_at: v1.3 milestone archived and closed
last_updated: "2026-04-16T15:00:00.000Z"
last_activity: 2026-04-16 — v1.3 Backend Expansion milestone complete, archived, git tagged
progress:
total_phases: 3
completed_phases: 3
total_plans: 11
completed_plans: 11
total_phases: 6
completed_phases: 6
total_plans: 18
completed_plans: 18
percent: 100
---
@@ -21,19 +21,83 @@ progress:
See: .planning/PROJECT.md (updated 2026-03-31)
**Core value:** An IT pro can go from zero to a deployable rclone setup in minutes — without needing to know rclone's config syntax or PowerShell deployment conventions.
**Current focus:** v1.1 shipped — ready for next milestone
**Current focus:** Planning next milestone (v1.4)
## Current Position
Milestone: v1.1 (SHIPPED)
Phase: All complete (57)
Status: Archived
Last activity: 2026-03-31 — v1.1 milestone completed and archived
Milestone v1.3 complete and archived. Ready for next milestone planning.
Status: 4 milestones shipped (v1.0 → v1.3), 13 phases, 40 plans — all complete
Last activity: 2026-04-16 — v1.3 Backend Expansion milestone archived and git tagged
Progress: [██████████] 100% (v1.1)
Progress: [██████████] 100% (v1.3 — milestone complete)
## Accumulated Context
### Decisions
Decisions are logged in PROJECT.md Key Decisions table.
Recent decisions affecting current work:
- v1.2 roadmap: Zero new runtime dependencies — Tailwind v4 @theme + CSS custom properties only
- v1.2 roadmap: Bottom-up migration order (tokens -> primitives -> migration -> content -> polish)
- v1.2 roadmap: ThemeToggle via DOM class toggle, not React Context (avoids re-render cascade)
- [Phase 08-01]: Two-layer CSS token pattern: @theme var() references + @layer base :root/.dark raw values — required for .dark cascade in Tailwind v4
- [Phase 08-01]: vi.stubGlobal for localStorage/matchMedia in tests — Node v25 experimental WebStorage breaks standard Storage API in jsdom environment
- [Phase 08-02]: Mechanical migration only — zero DOM structure changes, only className strings replaced
- [Phase 08-02]: Post-migration grep check is mandatory — replace_all can miss occurrences in dual-branch components with identical className strings
- [Phase 08-02]: User visually confirmed light/dark theme renders correctly across all 4 wizard steps — THEME-01 and THEME-02 complete
- [Phase 09-02]: Label text inside button element (not adjacent) preserves test selector compatibility: textContent includes step name
- [Phase 09-02]: Pre-existing TextFieldMD3.test.tsx failure scoped out — component not yet built, belongs to future plan
- [Phase 09-md3-components]: TextFieldMD3 uses CSS-only floating label via peer-[:not(:placeholder-shown)] without JS state
- [Phase 09-md3-components]: DEBT-01 fix: tooltip buttons use aria-label directly; RemoteConfigStep.test updated with { selector: 'input' } to prevent aria-label/label ambiguity
- [Phase 09-03]: Tooltip button rendered ABOVE TextFieldMD3 in FieldRenderer (tooltip is separate concern, TextFieldMD3 is self-contained)
- [Phase 09-03]: PasswordField show/hide toggle passed as suffix prop to TextFieldMD3 — suffix slot is right-aligned inside input container
- [Phase 09-03]: OutputBlock Copy/Download buttons use MD3_BTN_OUTLINED with !text-xs !px-3 !py-1.5 overrides to preserve small utility button size
- [Phase 09-md3-components]: BackendSelectionStep gap: plain label+input replaced with TextFieldMD3; no test changes required because TextFieldMD3 preserves textbox role and role=alert contract
- [Phase 09-05]: Tooltip hover uses dual-state (hoverTooltip + showTooltip); click-unpin explicitly clears hoverTooltip to prevent sticky tooltip after dismiss
- [Phase 09-05]: helpTextPrefix prop guard is (helpText || helpTextPrefix) so tooltip icon renders on fields without helpText
- [Phase 10-content-clarity]: showIntro is local useState in WizardShell — not persisted, resets on page reload by design
- [Phase 10-content-clarity]: IntroSection declared as a local component in App.tsx — appropriate for small co-located UI
- [Phase 10-content-clarity]: ReviewStep lacked h2 heading — added 'Step 4: Review & Download' before description paragraph
- [Phase 10-03]: S3 region gets tooltipText only (no helpText) — placeholder is already descriptive
- [Phase 10-03]: SFTP host and user intentionally omitted from tooltipText — self-explanatory per user decision
- [Phase 10-03]: getByLabelText selector ambiguity fixed with { selector: 'input' } across all affected tests (same DEBT-01 pattern as Phase 09-03)
- [Phase 10-content-clarity]: RemoteNamePreview renders only div/span elements (no form controls) to preserve single-textbox selector contract in BackendSelectionStep tests
- [Phase 11-polish-responsiveness]: ring-inset on ThemeToggle focus ring because overflow-hidden on container clips outset rings
- [Phase 11-polish-responsiveness]: No ring-offset-2 on BackendCard to avoid dark mode surface color issues
- [Phase 11-polish-responsiveness]: FieldRenderer select uses focus-visible:ring-2 (not focus:ring-2) for keyboard-only ring on selects
- [Phase 11-polish-responsiveness]: CSS-only reduced-motion guard via @media (prefers-reduced-motion: reduce) — no JS listener needed
- [Phase 11-polish-responsiveness]: key={state.currentStep} on step wrapper forces React remount triggering CSS animation from initial state
- [Phase 12-dark-mode-visibility-fixes]: bg-surface-container text-on-surface added to FieldRenderer select className — semantic tokens ensure correct rendering in both light and dark themes without dark: prefix
- [Phase 12-dark-mode-visibility-fixes]: accent-[var(--r2b-warning)] arbitrary value used for ReviewStep security checkbox — accent-warning class not confirmed generated by Tailwind v4
- [Phase 12-dark-mode-visibility-fixes]: Removed {' '} space-before-text pattern in DeploymentStep labels — replaced with flex gap-2 layout for consistent spacing
- [Phase 13-01]: BackendType derived from keyof typeof BACKEND_REGISTRY — eliminates manual union maintenance at 18+ backends
- [Phase 13-01]: Registry uses as const (not Record<BackendType,...> cast) — cast was circular when BackendType derived from registry
- [Phase 13-01]: BACKEND_SCHEMAS auto-generated via Object.fromEntries over registry keys — no per-backend buildZodSchema call needed
- [Phase 13-01]: Registry has 18 backends (http and seafile are separate entries; plan's "17" was a counting error)
- [Phase 13-add-remaining-rclone-remotes]: OAuthInstructions uses step-prefix detection to render authorize command in a code element, keeping test assertions compatible
- [Phase 13-add-remaining-rclone-remotes]: BACKEND_ICONS is Partial<Record<BackendType,...>> — missing entries mean no icon rendered on the card
- [Phase 13-03]: type=search for search input: prevents getByRole('textbox') collision with remote name field in existing tests
- [Phase 13-03]: Empty categories return null (not CSS hidden) — makes queryByText assertions reliable in tests
- [Phase 13-04]: renderBackendFields() switch statement instead of nested ternaries - 7 branches requires readable structure
- [Phase 13-04]: backendLabel derived via Object.fromEntries(BACKEND_REGISTRY) eliminates manual displayName maintenance
- [Phase 13-04]: getByLabelText partial regex (/label/i not /^label$/i) - TextFieldMD3 required fields append * to label text
### Roadmap Evolution
- Phase 12 added: Dark mode visibility fixes
- Phase 13 added: Add remaining RClone remotes
### Pending Todos
None yet.
### Blockers/Concerns
- 131 test selectors could break during component restyling — one-component-at-a-time discipline required (ongoing, Phase 10+)
## Session Continuity
Last session: 2026-03-31T15:00:00Z
Stopped at: Milestone v1.1 archived — ready for /gsd:new-milestone
Last session: 2026-04-16
Stopped at: v1.3 milestone complete — MILESTONES.md, PROJECT.md, ROADMAP.md, RETROSPECTIVE.md updated; git tagged v1.3
Resume file: None
@@ -0,0 +1,84 @@
---
status: awaiting_human_verify
trigger: "Tooltip info buttons on Remote Config wizard step: hovering doesn't show anything, icon placement is wrong (above field instead of beside helpText)"
created: 2026-04-01T00:00:00Z
updated: 2026-04-02T00:00:00Z
---
## Current Focus
hypothesis: Issues 1 & 2 already fixed in current code; Issue 3 (light mode illegibility) is active — text-on-surface-variant is gray-100 (near-white) in light mode, used on light backgrounds
test: Applying fix to BackendSelectionStep.tsx — replace text-on-surface-variant with text-on-surface-container for step description; replace placeholder:text-on-surface-variant/50 with placeholder:text-on-surface-container/50 for search bar
expecting: Both elements become legible in light mode; dark mode unaffected
next_action: Apply fix to BackendSelectionStep.tsx
## Symptoms
expected: (1) Hovering tooltip button shows tooltip text. (2) Icon is beside the helpText under the field. (3) Step description and search bar placeholder are legible in light mode.
actual: (1) Nothing happens on hover — only click toggles. (2) Icon is above the TextFieldMD3 component. (3) Step subtitle and search placeholder are near-invisible in light mode.
errors: none
reproduction: (1,2) Open Remote Config wizard, hover over any info button. (3) View BackendSelectionStep in light mode.
started: (1,2) Since phase-09 tooltip implementation. (3) Since text-on-surface-variant was used for body text.
## Eliminated
- hypothesis: Issues 1 & 2 are still present in the codebase
evidence: Current FieldRenderer.tsx (lines 88-91) and PasswordField.tsx (lines 34-35) both have onMouseEnter/onMouseLeave wired to setHoverTooltip. TextFieldMD3 renders helpTextPrefix in a flex row beside helpText (lines 67-74). The debug diagnosis was from before the code was updated.
timestamp: 2026-04-02T00:00:00Z
## Evidence
- timestamp: 2026-04-01T00:00:00Z
checked: FieldRenderer.tsx lines 83-109 (text input default branch)
found: Original code had onClick-only toggle, icon rendered above TextFieldMD3, no hover support.
implication: Root causes of Issues 1 & 2 at diagnosis time
- timestamp: 2026-04-02T00:00:00Z
checked: FieldRenderer.tsx current state (all 119 lines)
found: |
Lines 88-91: onMouseEnter/onMouseLeave both wired to setHoverTooltip — hover IS implemented.
Line 99: tooltipVisible = showTooltip || hoverTooltip — hover state properly used.
Lines 103-111: TextFieldMD3 receives helpTextPrefix={tooltipIcon} — icon IS placed beside helpText.
Same pattern confirmed in PasswordField.tsx lines 34-35 and 54.
implication: Issues 1 & 2 already resolved in current code
- timestamp: 2026-04-02T00:00:00Z
checked: index.css — CSS custom property values for light mode (:root)
found: |
--r2b-on-surface-variant: #F3F4F6 (gray-100 — intended for text ON surface-variant/code blocks)
--r2b-on-surface-container: #374151 (gray-700 — good contrast on white/light surfaces)
--r2b-surface: #F9FAFB (gray-50 — light background)
implication: text-on-surface-variant = near-white on light backgrounds = illegible; text-on-surface-container = dark gray = legible
- timestamp: 2026-04-02T00:00:00Z
checked: BackendSelectionStep.tsx line 88 and line 107
found: |
Line 88: <p className="text-sm text-on-surface-variant mt-1 mb-4"> — step description uses on-surface-variant
Line 107: placeholder:text-on-surface-variant/50 — search placeholder uses on-surface-variant at 50% opacity
Both become near-invisible in light mode: gray-100 (#F3F4F6) on gray-50 (#F9FAFB) background.
implication: Root cause of Issue 3 — wrong semantic token used for body text on surface background
## Resolution
root_cause: |
ISSUE 1 (tooltip hover) — ALREADY FIXED in current code.
Both FieldRenderer.tsx and PasswordField.tsx have onMouseEnter/onMouseLeave handlers wired to setHoverTooltip.
tooltipVisible = showTooltip || hoverTooltip ensures hover triggers display.
ISSUE 2 (icon placement) — ALREADY FIXED in current code.
FieldRenderer.tsx passes helpTextPrefix={tooltipIcon} to TextFieldMD3.
TextFieldMD3 renders helpTextPrefix in a flex row beside helpText (lines 67-74).
ISSUE 3 (light mode illegibility):
File: src/components/wizard/BackendSelectionStep.tsx, lines 88 and 107
text-on-surface-variant (#F3F4F6, gray-100) is semantically "text on dark surface-variant elements"
(e.g., code blocks). Using it on light surface backgrounds produces near-white text on near-white
background. The step description and search placeholder are nearly invisible in light mode.
Fix: use text-on-surface-container (#374151, gray-700) which is designed for secondary text on surfaces.
fix: |
BackendSelectionStep.tsx line 88: text-on-surface-variant -> text-on-surface-container
BackendSelectionStep.tsx line 107: placeholder:text-on-surface-variant/50 -> placeholder:text-on-surface-container/50
verification: TypeScript passes clean. 79 tests pass (13 pre-existing infra failures unrelated to this change — same failures existed before). Fix applied and code-reviewed: token change is semantically correct.
files_changed:
- src/components/wizard/BackendSelectionStep.tsx
+118
View File
@@ -0,0 +1,118 @@
# Requirements Archive: v1.3 Backend Expansion
**Archived:** 2026-04-16
**Status:** SHIPPED
For current requirements, see `.planning/REQUIREMENTS.md`.
---
# Requirements: Ready2Blob
**Defined:** 2026-03-31
**Core Value:** An IT pro can go from zero to a deployable rclone setup in minutes — without needing to know rclone's config syntax or PowerShell deployment conventions.
## v1.2 Requirements
### Theme
- [x] **THEME-01**: App uses a consistent MD3 color token system (CSS custom properties) that all components reference instead of hardcoded Tailwind color classes
- [x] **THEME-02**: User can toggle between System, Light, and Dark themes, with choice persisted across sessions and no flash of unstyled content on load
### Components
- [x] **COMP-01**: All text inputs render as MD3 outlined text fields with floating labels that animate on focus and when the field has content
- [x] **COMP-02**: Buttons follow MD3 hierarchy — filled for primary actions (Next, Download), outlined for secondary (Back, Copy), text for tertiary
- [x] **COMP-03**: Backend selection cards and output blocks use MD3 elevation with tonal surface tint, consistent padding, and shape tokens
- [x] **COMP-04**: Step indicator displays as numbered circles connected by lines, with completed steps showing a checkmark, current step highlighted, and future steps muted
### Content & Clarity
- [x] **UX-01**: First-time visitor sees an intro section explaining what Ready2Blob does, with a clear call-to-action to start the wizard
- [x] **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
- [x] **UX-03**: Each wizard step has a 1-2 sentence description below the heading explaining what the user is doing and why
- [x] **UX-04**: All backend credential fields have contextual help text explaining what to enter and where to find it
### Polish
- [x] **POLISH-01**: Wizard layout adapts to mobile screens — backend cards stack, form fields go full-width, step indicator collapses, buttons stretch
- [x] **POLISH-02**: All interactive elements have visible MD3 focus indicators (3px outline) using `focus-visible` for keyboard navigation
- [x] **POLISH-03**: Step transitions use subtle fade/slide animation that respects `prefers-reduced-motion`
- [x] **POLISH-04**: On validation failure, the view auto-scrolls to the first errored field
### Tech Debt
- [x] **DEBT-01**: FieldRenderer uses consistent `aria-label` pattern across text-branch and select-branch (resolving v1.1 cosmetic debt)
### Dark Mode Visibility
- [x] **DARK-01**: All wizard step h2 headings are visible (readable text with proper contrast) in dark mode
- [x] **DARK-02**: DeploymentStep native form controls (labels, legends, checkboxes, radios) are visible and styled with semantic tokens in dark mode
- [x] **DARK-03**: FieldRenderer select element has proper background and text color in dark mode
## v1.3 Requirements
### Remote Backend Expansion
- [x] **REMOTE-01**: BACKEND_REGISTRY contains all 18 backends (7 existing + 11 new) with correct rclone field definitions, displayName, description, and category
- [x] **REMOTE-02**: BackendType is derived from registry keys (`keyof typeof BACKEND_REGISTRY`) instead of an explicit union literal, eliminating a touch-point when adding backends
- [x] **REMOTE-03**: BackendSelectionStep displays backends grouped by category (Cloud Object Storage, Cloud Drives, Protocol-based) with instant search filtering and inline SVG icons
- [x] **REMOTE-04**: OAuth-token backends (Google Drive, Dropbox, Box, pCloud, OneDrive) display a collapsible OAuthInstructions component with backend-specific `rclone authorize` command
- [x] **REMOTE-05**: RemoteConfigStep renders the correct form fields for every backend, including GdriveAuthToggle for Google Drive's dual-auth (OAuth token vs service account)
- [x] **REMOTE-06**: buildRcloneConf produces valid rclone.conf output with correct type strings for all 18 backends
## Future Requirements
- Accent color presets (5-8 curated themes) — deferred from v1.2 scoping
- Code block syntax highlighting in ReviewStep output blocks
- Upgraded tooltip popovers with Floating UI positioning
- Custom styled select/dropdown replacing native `<select>`
## Out of Scope
| Feature | Reason |
|---------|--------|
| MUI or Material Tailwind component library | Bundle bloat (~300KB+) for 4-step wizard; custom tokens + Tailwind lighter and fully controlled |
| Page-per-step routing (React Router) | Wizard state is ephemeral credentials in memory; refreshing loses context; bookmarking credential step is security anti-pattern |
| Glassmorphism / neomorphism effects | CPU-intensive, low contrast accessibility issues, will look dated quickly |
| Arbitrary user-picked hex color | Breaks accessibility (contrast ratios); curated presets cover 95% of personalization need |
| Custom checkbox/radio styling | High effort for few toggles; native inputs with MD3-inspired wrappers sufficient |
## Traceability
| Requirement | Phase | Status |
|-------------|-------|--------|
| THEME-01 | Phase 8 | Complete |
| THEME-02 | Phase 8 | Complete |
| COMP-01 | Phase 9 | Complete |
| COMP-02 | Phase 9 | Complete |
| COMP-03 | Phase 9 | Complete |
| COMP-04 | Phase 9 | Complete |
| DEBT-01 | Phase 9 | Complete |
| UX-01 | Phase 10 | Complete |
| UX-02 | Phase 10 | Complete |
| UX-03 | Phase 10 | Complete |
| UX-04 | Phase 10 | Complete |
| POLISH-01 | Phase 11 | Complete |
| POLISH-02 | Phase 11 | Complete |
| POLISH-03 | Phase 11 | Complete |
| POLISH-04 | Phase 11 | Complete |
| DARK-01 | Phase 12 | Complete |
| DARK-02 | Phase 12 | Complete |
| DARK-03 | Phase 12 | Complete |
| REMOTE-01 | Phase 13 | Complete |
| REMOTE-02 | Phase 13 | Complete |
| REMOTE-03 | Phase 13 | Planned |
| REMOTE-04 | Phase 13 | Planned |
| REMOTE-05 | Phase 13 | Planned |
| REMOTE-06 | Phase 13 | Complete |
**Coverage:**
- v1.2 requirements: 18 total (18 complete)
- v1.3 requirements: 6 total (3 complete)
- Mapped to phases: 24
- Unmapped: 0
---
*Requirements defined: 2026-03-31*
*Last updated: 2026-04-01 after Phase 13 planning*
+164
View File
@@ -0,0 +1,164 @@
# Roadmap: Ready2Blob
## Milestones
-**v1.0 MVP** — Phases 14 (shipped 2026-03-27)
-**v1.1 Backlog & Tech Debt** — Phases 57 (shipped 2026-03-31)
-**v1.2 UI Polish & Clarity** — Phases 812 (shipped 2026-04-01)
- **v1.3 Backend Expansion** — Phase 13 (in progress)
## Phases
<details>
<summary>✅ v1.0 MVP (Phases 14) — SHIPPED 2026-03-27</summary>
- [x] Phase 1: Foundation (4/4 plans) — completed 2026-03-26
- [x] Phase 2: Generators (4/4 plans) — completed 2026-03-26
- [x] Phase 3: Wizard UI (5/5 plans) — completed 2026-03-27
- [x] Phase 4: Review, Download & Security (5/5 plans) — completed 2026-03-27
Full phase details: [.planning/milestones/v1.0-ROADMAP.md](milestones/v1.0-ROADMAP.md)
</details>
<details>
<summary>✅ v1.1 Backlog & Tech Debt (Phases 57) — SHIPPED 2026-03-31</summary>
- [x] Phase 5: Tech Debt (4/4 plans) — completed 2026-03-30
- [x] Phase 6: New Backends (4/4 plans) — completed 2026-03-31
- [x] Phase 7: Validation & UX Polish (3/3 plans) — completed 2026-03-31
Full phase details: [.planning/milestones/v1.1-ROADMAP.md](milestones/v1.1-ROADMAP.md)
</details>
<details>
<summary>✅ v1.2 UI Polish & Clarity (Phases 812) — SHIPPED 2026-04-01</summary>
- [x] **Phase 8: Theme Foundation** - MD3 color token system and dark mode infrastructure (completed 2026-04-01)
- [x] **Phase 9: MD3 Components** - UI primitives and component migration to semantic tokens (completed 2026-04-01)
- [x] **Phase 10: Content & Clarity** - Intro section, step descriptions, and contextual help (completed 2026-04-01)
- [x] **Phase 11: Polish & Responsiveness** - Mobile layout, focus states, transitions, scroll-to-error (completed 2026-04-01)
- [x] **Phase 12: Dark mode visibility fixes** - Fix dark mode regressions (completed 2026-04-01)
</details>
### v1.3 Backend Expansion (In Progress)
**Milestone Goal:** Expand backend support from 7 to 17 rclone backends with categorized selection, search filtering, OAuth token-paste guidance, and inline icons.
## Phase Details
### Phase 8: Theme Foundation
**Goal**: App has a complete MD3 color token system and working dark mode toggle with no flash of unstyled content
**Depends on**: Phase 7 (v1.1 complete)
**Requirements**: THEME-01, THEME-02
**Success Criteria** (what must be TRUE):
1. All color values in the app flow from CSS custom properties defined in a single token file, not from hardcoded Tailwind color classes
2. User can toggle between System, Light, and Dark themes via a visible control, and the entire UI responds immediately
3. Theme preference persists in localStorage and applies on reload without any flash of wrong theme
4. Tailwind v4 `@theme` directive maps MD3 token names to utility classes (e.g., `bg-surface`, `text-on-primary`)
**Plans:** 2/2 plans complete
Plans:
- [ ] 08-01-PLAN.md — Token system, flash prevention, ThemeToggle component
- [ ] 08-02-PLAN.md — Color class migration across all components
### Phase 9: MD3 Components
**Goal**: All interactive elements use MD3-styled primitives with consistent visual language across the entire wizard
**Depends on**: Phase 8
**Requirements**: COMP-01, COMP-02, COMP-03, COMP-04, DEBT-01
**Success Criteria** (what must be TRUE):
1. Text inputs render as outlined fields with floating labels that animate on focus and when the field has content
2. Buttons visually distinguish primary (filled), secondary (outlined), and tertiary (text) actions across all wizard steps
3. Backend selection cards and output blocks display MD3 elevation with tonal surface tint and consistent shape tokens
4. Step indicator shows numbered circles connected by lines, with checkmarks on completed steps, highlight on current, and muted future steps
5. FieldRenderer produces identical `aria-label` attributes for both text-branch and select-branch inputs
**Plans:** 5/5 plans complete
Plans:
- [x] 09-01-PLAN.md — TextFieldMD3 component, MD3 button constants, DEBT-01 aria-label fix
- [x] 09-02-PLAN.md — StepIndicator rebuild with MD3 circles and connectors
- [x] 09-03-PLAN.md — Integration: wire TextFieldMD3 + button styles + elevation into all wizard steps
- [x] 09-04-PLAN.md — Gap closure: Replace BackendSelectionStep plain input with TextFieldMD3
- [x] 09-05-PLAN.md — Gap closure: Tooltip hover behavior + icon placement beside helpText
### Phase 10: Content & Clarity
**Goal**: A first-time visitor understands what Ready2Blob does and what each wizard step expects without external documentation
**Depends on**: Phase 8 (tokens needed for consistent styling of new content)
**Requirements**: UX-01, UX-02, UX-03, UX-04
**Success Criteria** (what must be TRUE):
1. Landing view shows an intro section explaining what Ready2Blob does with a clear call-to-action to start the wizard
2. Remote name field displays a placeholder example, help text, and a live preview showing how the name appears in `[remote-name]` config syntax
3. Every wizard step has a 1-2 sentence description below the heading explaining what the user is doing and why
4. All backend credential fields have contextual help text explaining what to enter and where to find the value
**Plans:** 3/3 plans complete
Plans:
- [ ] 10-01-PLAN.md — Intro section with CTA and step descriptions for all 4 steps
- [ ] 10-02-PLAN.md — Remote name live config preview with helpText
- [ ] 10-03-PLAN.md — Credential field tooltipText and helpText enrichment
### Phase 11: Polish & Responsiveness
**Goal**: The wizard feels production-ready on any device with smooth interactions and accessible keyboard navigation
**Depends on**: Phase 9, Phase 10
**Requirements**: POLISH-01, POLISH-02, POLISH-03, POLISH-04
**Success Criteria** (what must be TRUE):
1. On a mobile viewport (< 640px), backend cards stack vertically, form fields span full width, step indicator collapses, and buttons stretch to fill width
2. Every interactive element shows a visible 3px MD3 focus outline when navigated via keyboard (using `focus-visible`)
3. Step transitions use a subtle fade/slide animation that is suppressed when the user has `prefers-reduced-motion` enabled
4. When form validation fails, the view auto-scrolls to the first field with an error
**Plans:** 2/2 plans complete
Plans:
- [ ] 11-01-PLAN.md — Mobile responsive layout + MD3 focus-visible indicators
- [ ] 11-02-PLAN.md — Step transition animation + auto-scroll to first error
### Phase 12: Dark mode visibility fixes
**Goal:** Fix dark mode visibility regressions where step headings, DeploymentStep form controls, and FieldRenderer select elements are invisible or unstyled against the dark background
**Requirements**: DARK-01, DARK-02, DARK-03
**Depends on:** Phase 11
**Success Criteria** (what must be TRUE):
1. All wizard step h2 headings are readable in dark mode with proper contrast
2. DeploymentStep labels, legends, checkboxes, and radios are visible and styled in dark mode
3. FieldRenderer select element has proper background and text color in dark mode
**Plans:** 2/2 plans complete
Plans:
- [ ] 12-01-PLAN.md — Fix step headings, DeploymentStep form controls, ReviewStep checkbox
- [ ] 12-02-PLAN.md — Fix FieldRenderer select dark mode styling
### Phase 13: Add remaining RClone remotes
**Goal:** Expand backend support from 7 to 17 rclone backends with categorized selection UI, instant search, OAuth token-paste guidance, and inline SVG icons — covering all backends suitable for unattended Windows deployment
**Requirements**: REMOTE-01, REMOTE-02, REMOTE-03, REMOTE-04, REMOTE-05, REMOTE-06
**Depends on:** Phase 12
**Success Criteria** (what must be TRUE):
1. BACKEND_REGISTRY contains 17 backends with correct fields, categories, and rclone type mappings
2. BackendType is derived from registry keys (not an explicit union)
3. BackendSelectionStep groups backends by category with instant search and inline SVG icons
4. OAuth-token backends show collapsible OAuthInstructions with backend-specific rclone authorize command
5. RemoteConfigStep renders correct form for every backend including GdriveAuthToggle
6. buildRcloneConf produces valid rclone.conf for all 17 backends
**Plans:** 4/4 plans complete
Plans:
- [x] 13-01-PLAN.md — Registry refactoring: derive BackendType, add categories, 11 new backend entries (18 total), auto-generate schemas
- [ ] 13-02-PLAN.md — New components: OAuthInstructions, GdriveAuthToggle, BackendIcons
- [ ] 13-03-PLAN.md — BackendSelectionStep UX overhaul: categories, search, icons
- [ ] 13-04-PLAN.md — RemoteConfigStep wiring for all new backends + visual verification
## Progress
| Phase | Milestone | Plans Complete | Status | Completed |
|-------|-----------|----------------|--------|-----------|
| 1. Foundation | v1.0 | 4/4 | Complete | 2026-03-26 |
| 2. Generators | v1.0 | 4/4 | Complete | 2026-03-26 |
| 3. Wizard UI | v1.0 | 5/5 | Complete | 2026-03-27 |
| 4. Review, Download & Security | v1.0 | 5/5 | Complete | 2026-03-27 |
| 5. Tech Debt | v1.1 | 4/4 | Complete | 2026-03-30 |
| 6. New Backends | v1.1 | 4/4 | Complete | 2026-03-31 |
| 7. Validation & UX Polish | v1.1 | 3/3 | Complete | 2026-03-31 |
| 8. Theme Foundation | v1.2 | 2/2 | Complete | 2026-04-01 |
| 9. MD3 Components | v1.2 | 5/5 | Complete | 2026-04-01 |
| 10. Content & Clarity | v1.2 | 3/3 | Complete | 2026-04-01 |
| 11. Polish & Responsiveness | v1.2 | 2/2 | Complete | 2026-04-01 |
| 12. Dark mode visibility fixes | v1.2 | 2/2 | Complete | 2026-04-01 |
| 13. Add remaining RClone remotes | 4/4 | Complete | 2026-04-01 | — |
@@ -0,0 +1,225 @@
---
phase: 08-theme-foundation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/index.css
- index.html
- src/components/ui/ThemeToggle.tsx
- src/components/ui/ThemeToggle.test.tsx
- src/App.tsx
autonomous: true
requirements:
- THEME-01
- THEME-02
must_haves:
truths:
- "Tailwind utility classes bg-surface, text-on-surface, bg-primary, text-on-primary, border-outline etc. are available and resolve to correct hex values"
- "Adding .dark class to html element switches all token values to dark palette"
- "ThemeToggle renders three options: Light, Dark, System"
- "Clicking a theme option applies the correct class to document.documentElement and persists to localStorage"
- "On hard reload with r2b-theme=dark in localStorage, the .dark class is present before React mounts (no flash)"
artifacts:
- path: "src/index.css"
provides: "MD3 color token definitions (light + dark) and @theme mapping"
contains: "@theme"
- path: "index.html"
provides: "Inline blocking script for flash prevention"
contains: "r2b-theme"
- path: "src/components/ui/ThemeToggle.tsx"
provides: "Segmented theme toggle component"
exports: ["ThemeToggle"]
- path: "src/components/ui/ThemeToggle.test.tsx"
provides: "Unit tests for ThemeToggle behavior"
contains: "describe"
- path: "src/App.tsx"
provides: "ThemeToggle wired into header, bg-surface applied to shell"
contains: "ThemeToggle"
key_links:
- from: "src/index.css"
to: "src/components/ui/ThemeToggle.tsx"
via: ".dark class on html toggles CSS variable cascade"
pattern: "classList\\.toggle.*dark"
- from: "index.html"
to: "localStorage"
via: "inline script reads r2b-theme before paint"
pattern: "localStorage\\.getItem.*r2b-theme"
- from: "src/App.tsx"
to: "src/components/ui/ThemeToggle.tsx"
via: "import and render in header"
pattern: "import.*ThemeToggle"
---
<objective>
Build the MD3 color token system, dark mode infrastructure, and ThemeToggle component.
Purpose: Establish the CSS custom property foundation that all components will reference instead of hardcoded Tailwind colors. This is the infrastructure layer that Plan 02 (migration) and Phases 9-11 depend on.
Output: Working token system with dark mode toggle, flash prevention, and ThemeToggle component with unit tests.
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/08-theme-foundation/08-CONTEXT.md
@.planning/phases/08-theme-foundation/08-RESEARCH.md
<interfaces>
<!-- Current src/index.css is just: @import "tailwindcss"; -->
<!-- Current index.html has no inline scripts -->
From src/App.tsx (current structure):
```tsx
import { useWizard } from './store/context';
import { WizardProvider } from './store/context';
import { StepIndicator } from './components/wizard/StepIndicator';
// ... step imports
function WizardShell() {
const { state } = useWizard();
// ...
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center py-12 px-4">
<div className="w-full max-w-2xl">
<h1 className="text-3xl font-bold text-gray-900 mb-8 text-center">Ready2Blob</h1>
<StepIndicator />
<div className="mt-8">{CurrentStep}</div>
</div>
</div>
);
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Define MD3 color tokens and flash prevention script</name>
<files>src/index.css, index.html</files>
<action>
**src/index.css** — Replace the single `@import "tailwindcss"` line with the complete token system. Structure (in this exact order):
1. `@import "tailwindcss";`
2. `@custom-variant dark (&:where(.dark, .dark *));` — immediately after import, before any @layer or @theme
3. `@layer base` block with `:root` (light values) and `.dark` (dark values) for all tokens:
- `--r2b-primary`: light #4338CA, dark #A5B4FC
- `--r2b-on-primary`: light #FFFFFF, dark #1E1B4B
- `--r2b-surface`: light #F9FAFB, dark #111827
- `--r2b-on-surface`: light #111827, dark #F9FAFB
- `--r2b-surface-variant`: light #1F2937, dark #0F172A (code blocks)
- `--r2b-on-surface-variant`: light #F3F4F6, dark #E5E7EB
- `--r2b-surface-container`: light #FFFFFF, dark #1F2937 (cards/forms)
- `--r2b-on-surface-container`: light #374151, dark #D1D5DB
- `--r2b-outline`: light #D1D5DB, dark #4B5563
- `--r2b-error`: light #EF4444, dark #FCA5A5
- `--r2b-on-error`: light #FFFFFF, dark #7F1D1D
- Also add warning/success tokens per research recommendation:
- `--r2b-success`: light #15803D (green-700), dark #86EFAC (green-300)
- `--r2b-on-success`: light #F0FDF4 (green-50), dark #14532D (green-900)
- `--r2b-warning`: light #D97706 (amber-600), dark #FCD34D (amber-300)
- `--r2b-on-warning`: light #FFFBEB (amber-50), dark #78350F (amber-900)
Also add `body` transition in `@layer base`: `transition: background-color 200ms ease, color 200ms ease, border-color 200ms ease` with `@media (prefers-reduced-motion: reduce)` wrapper that sets `transition: none`.
4. `@theme` block mapping each `--color-*` to `var(--r2b-*)` for all 15 tokens (the 11 core + warning, on-warning, success, on-success).
**index.html** — Add inline blocking script inside `<head>`, BEFORE any stylesheet or script tags. The script:
1. Reads `localStorage.getItem('r2b-theme')`
2. If `'dark'` -> effective = dark. If `'light'` -> effective = light. Otherwise -> check `window.matchMedia('(prefers-color-scheme: dark)').matches`
3. If effective is dark, add `.dark` to `document.documentElement.classList`
4. Wrap in try/catch for private browsing mode safety
Use vanilla JS (no arrow functions) for maximum browser compat in the inline script. Use the exact pattern from 08-RESEARCH.md Pattern 2.
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npx vite build 2>&1 | tail -5</automated>
</verify>
<done>src/index.css contains @custom-variant, @layer base with :root and .dark blocks (15 token pairs), @theme mapping all tokens. index.html has inline blocking script reading r2b-theme from localStorage. Vite build succeeds with no errors.</done>
</task>
<task type="tdd">
<name>Task 2: Create ThemeToggle component with tests and wire into App.tsx</name>
<files>src/components/ui/ThemeToggle.tsx, src/components/ui/ThemeToggle.test.tsx, src/App.tsx</files>
<behavior>
- ThemeToggle renders three buttons: Light, Dark, System
- Each button has role="button" and aria-pressed reflecting current selection
- The component group has aria-label="Theme"
- Default selection is "system" when localStorage is empty
- Clicking "Dark" adds .dark class to document.documentElement
- Clicking "Light" removes .dark class from document.documentElement
- Clicking any option writes the value to localStorage key "r2b-theme"
- Active button uses bg-primary text-on-primary classes
- Inactive buttons use bg-surface-container text-on-surface-container classes
</behavior>
<action>
**ThemeToggle.test.tsx** (RED first):
Write tests using Vitest + jsdom (add `// @vitest-environment jsdom` pragma). Test:
1. Renders three buttons with text containing "Light", "Dark", "System"
2. Default state is "System" (aria-pressed="true" on System button) when localStorage is empty
3. Clicking "Dark" -> `document.documentElement.classList.contains('dark')` is true
4. Clicking "Light" -> `document.documentElement.classList.contains('dark')` is false
5. Clicking "Dark" -> `localStorage.getItem('r2b-theme')` === `'dark'`
6. Active button has aria-pressed="true", others have aria-pressed="false"
Run tests — they must FAIL (RED).
**ThemeToggle.tsx** (GREEN):
Create the component following 08-RESEARCH.md Pattern 3:
- Type `ThemeValue = 'light' | 'dark' | 'system'`
- `STORAGE_KEY = 'r2b-theme'`
- `getStored()` reads from localStorage, returns ThemeValue (default 'system')
- `applyTheme(value)` toggles `.dark` on `document.documentElement` and writes to localStorage
- `useState<ThemeValue>(getStored)` for lazy init
- Render as a segmented control: `<div role="group" aria-label="Theme">` with three buttons
- Each button: `type="button"`, `aria-pressed={theme === v}`, `onClick={() => select(v)}`
- Active state: `bg-primary text-on-primary font-medium`
- Inactive state: `bg-surface-container text-on-surface-container hover:bg-surface`
- Icons: Sun for Light, Moon for Dark, Monitor for System (use Unicode or simple SVG inline icons)
- Outer div: `flex rounded-md border border-outline overflow-hidden text-sm`
Run tests — they must PASS (GREEN).
**App.tsx** updates:
1. Add `import { ThemeToggle } from './components/ui/ThemeToggle';`
2. Change the header from centered h1 to a flex row: `<div className="flex items-center justify-between mb-8">`
3. h1 keeps `text-3xl font-bold` but change `text-gray-900` to `text-on-surface`, remove `text-center`
4. Add `<ThemeToggle />` as second child in the flex row
5. Change outer div `bg-gray-50` to `bg-surface`
6. Change h1 `text-gray-900` to `text-on-surface`
Run full test suite to confirm no regressions.
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npm test 2>&1 | tail -20</automated>
</verify>
<done>ThemeToggle.test.tsx has 6+ passing tests. ThemeToggle renders segmented control with Light/Dark/System. App.tsx header shows title left and ThemeToggle right. All existing tests still pass. App outer div uses bg-surface, h1 uses text-on-surface.</done>
</task>
</tasks>
<verification>
1. `npx vite build` completes without errors
2. `npm test` — all tests pass (existing + new ThemeToggle tests)
3. `grep -c "@theme" src/index.css` returns 1 (theme block exists)
4. `grep -c "r2b-theme" index.html` returns at least 1 (inline script present)
5. `grep "ThemeToggle" src/App.tsx` confirms component is wired in
</verification>
<success_criteria>
- Token system produces working Tailwind utility classes: bg-surface, text-on-surface, bg-primary, etc.
- Dark mode toggle works: clicking Dark applies .dark class, Light removes it, System checks OS preference
- Flash prevention: inline script in index.html reads localStorage before CSS paints
- ThemeToggle has passing unit tests covering all three modes and DOM class toggling
- No regression in existing test suite
</success_criteria>
<output>
After completion, create `.planning/phases/08-theme-foundation/08-01-SUMMARY.md`
</output>
@@ -0,0 +1,143 @@
---
phase: 08-theme-foundation
plan: 01
subsystem: ui
tags: [tailwindcss, css-custom-properties, dark-mode, theme, react, vitest]
# Dependency graph
requires: []
provides:
- MD3 color token system (15 tokens: primary, surface, error, success, warning families)
- Dark mode infrastructure via @custom-variant dark + .dark class on <html>
- Flash prevention inline script in index.html
- ThemeToggle segmented control component (Light/Dark/System)
- bg-surface, text-on-surface, bg-primary, etc. Tailwind utility classes
affects: [09-component-migration, 10-content-refinement, 11-polish]
# Tech tracking
tech-stack:
added: []
patterns:
- Two-layer token architecture (@layer base :root/.dark raw values + @theme var() references)
- DOM class toggle for theme (no React Context, no re-render cascade)
- Inline blocking script in <head> for FOUC prevention
- vi.stubGlobal for Node v25 localStorage/matchMedia in jsdom tests
key-files:
created:
- src/components/ui/ThemeToggle.tsx
- src/components/ui/ThemeToggle.test.tsx
modified:
- src/index.css
- index.html
- src/App.tsx
key-decisions:
- "Two-layer CSS token pattern: raw values in @layer base :root/.dark, @theme references via var() — required for .dark cascade to work"
- "vi.stubGlobal for localStorage and matchMedia in tests — Node v25 experimental WebStorage breaks standard Storage API in jsdom"
- "15 tokens (11 core + success/on-success/warning/on-warning) to cover ReviewStep semantic colors without hardcoded dark: prefixes"
- "Inline FOUC script placed before stylesheets in <head> for maximum paint-blocking guarantee"
patterns-established:
- "Token pattern: Always use @theme { --color-X: var(--r2b-X); } + @layer base :root/.dark for dynamic theming"
- "Test pattern: vi.stubGlobal('localStorage', mock) + vi.stubGlobal('matchMedia', mock) for Node v25 jsdom compatibility"
- "DOM theme toggle: applyTheme() directly mutates document.documentElement.classList — no useEffect, no Context"
requirements-completed: [THEME-01, THEME-02]
# Metrics
duration: 17min
completed: 2026-04-01
---
# Phase 8, Plan 01: Theme Foundation Summary
**MD3 color token system with 15 CSS custom properties, @custom-variant dark toggle, FOUC-free flash prevention script, and ThemeToggle segmented control component with 7 passing unit tests**
## Performance
- **Duration:** 17 min
- **Started:** 2026-04-01T02:32:49Z
- **Completed:** 2026-04-01T02:50:04Z
- **Tasks:** 2 (+ TDD RED commit)
- **Files modified:** 5
## Accomplishments
- Full MD3 color token system in `src/index.css`: 15 token pairs (light/dark), wired to Tailwind utility classes via `@theme { --color-*: var(--r2b-*) }` two-layer pattern
- Flash prevention inline blocking script added to `index.html` `<head>` before stylesheets; reads `r2b-theme` from localStorage before first CSS paint
- ThemeToggle segmented control: Light/Dark/System, writes localStorage + toggles `.dark` on `<html>`, no React Context overhead
- ThemeToggle wired into App.tsx header flex row; `bg-gray-50``bg-surface`, `text-gray-900``text-on-surface`
- All 166 tests pass (159 pre-existing + 7 new ThemeToggle unit tests)
## Task Commits
Each task was committed atomically:
1. **Task 1: Define MD3 color tokens and flash prevention script** - `6027552` (feat)
2. **Task 2 RED: Add failing tests for ThemeToggle** - `fa2c0d9` (test)
3. **Task 2 GREEN: Implement ThemeToggle and wire into App.tsx** - `074dc33` (feat)
**Plan metadata:** _(docs commit follows)_
_Note: TDD task split into RED (fa2c0d9) and GREEN (074dc33) commits per TDD protocol_
## Files Created/Modified
- `src/index.css`@import + @custom-variant dark + @layer base with 15 token pairs + @theme mapping + body transition
- `index.html` — Inline blocking IIFE script reading r2b-theme from localStorage before paint
- `src/components/ui/ThemeToggle.tsx` — Segmented control: 3 buttons, DOM class toggle, localStorage write
- `src/components/ui/ThemeToggle.test.tsx` — 7 unit tests with localStorage/matchMedia stubs for Node v25
- `src/App.tsx` — Import ThemeToggle, flex header row, bg-surface + text-on-surface token classes
## Decisions Made
- **Two-layer token pattern confirmed:** `@theme { --color-X: var(--r2b-X); }` + `@layer base :root/.dark { --r2b-X: <hex>; }` is the only pattern that allows `.dark` class cascade to work in Tailwind v4. Static hex values in `@theme` break dark mode override.
- **15 tokens instead of 10:** Added success/on-success (green) and warning/on-warning (amber) to cover ReviewStep semantic colors. Avoids hardcoded `dark:` prefixes in Phase 9 migration.
- **Node v25 localStorage workaround:** Node v25 ships experimental WebStorage (with `--localstorage-file` flag). The global `localStorage` in tests lacks `setItem`/`getItem` methods. Fix: `vi.stubGlobal('localStorage', inMemoryMock)` in test `beforeEach`.
- **matchMedia mock required:** jsdom does not implement `window.matchMedia`. `applyTheme()` calls it for system preference. Fix: `vi.stubGlobal('matchMedia', mockFn)` returning `{ matches: false, ... }`.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] localStorage.clear() unavailable in Node v25 jsdom test environment**
- **Found during:** Task 2 (ThemeToggle TDD)
- **Issue:** Node v25 experimental WebStorage provides a global `localStorage` object with no standard Storage methods (`setItem`, `getItem`, `clear`, `removeItem` all undefined). The `@vitest-environment jsdom` pragma cannot override this at the global level.
- **Fix:** Used `vi.stubGlobal('localStorage', inMemoryMock)` with a hand-rolled Storage implementation in `beforeEach`. Added `vi.stubGlobal('matchMedia', mockFn)` for the same reason (jsdom missing matchMedia).
- **Files modified:** `src/components/ui/ThemeToggle.test.tsx`
- **Verification:** 7 ThemeToggle tests green; all 166 tests pass
- **Committed in:** `074dc33` (Task 2 GREEN commit)
**2. [Rule 1 - Bug] toBeInTheDocument() unavailable — @testing-library/jest-dom not installed**
- **Found during:** Task 2 (ThemeToggle TDD)
- **Issue:** Tests initially used `toBeInTheDocument()` which requires `@testing-library/jest-dom`. Existing tests in the project use only standard Vitest assertions (`toBeDefined()`, `toBeNull()`).
- **Fix:** Replaced all `toBeInTheDocument()` calls with `toBeDefined()`, replaced all `toHaveAttribute(attr, val)` with `element.getAttribute(attr)` + `toBe(val)`.
- **Files modified:** `src/components/ui/ThemeToggle.test.tsx`
- **Verification:** Tests pass without adding any new dependency
- **Committed in:** `074dc33` (Task 2 GREEN commit)
---
**Total deviations:** 2 auto-fixed (both Rule 1 - bugs in test infrastructure)
**Impact on plan:** Both fixes necessary to get tests green in this environment. No scope creep. Component implementation is unaffected.
## Issues Encountered
- Node v25 experimental WebStorage is a new environment quirk not documented in the research. The workaround (`vi.stubGlobal`) is clean and consistent with how other projects handle this in Node 22+.
## User Setup Required
None — no external service configuration required.
## Next Phase Readiness
- Token system ready: `bg-surface`, `text-on-surface`, `bg-primary`, `text-on-primary`, `border-outline`, etc. are available as Tailwind utility classes
- Dark mode working: `.dark` class on `<html>` cascades all 15 token values automatically
- FOUC prevention in place: inline script reads `r2b-theme` before first CSS paint
- Phase 9 (component migration) can proceed: all 10 files with 73 hardcoded color classes ready for mechanical find-and-replace migration using Pattern 4 from 08-RESEARCH.md
- No blockers
---
*Phase: 08-theme-foundation*
*Completed: 2026-04-01*
@@ -0,0 +1,262 @@
---
phase: 08-theme-foundation
plan: 02
type: execute
wave: 2
depends_on:
- "08-01"
files_modified:
- src/components/ui/BackendCard.tsx
- src/components/ui/FieldRenderer.tsx
- src/components/ui/PasswordField.tsx
- src/components/wizard/AzureAuthToggle.tsx
- src/components/wizard/SftpAuthToggle.tsx
- src/components/wizard/DeploymentStep.tsx
- src/components/wizard/OutputBlock.tsx
- src/components/wizard/RemoteConfigStep.tsx
- src/components/wizard/ReviewStep.tsx
autonomous: false
requirements:
- THEME-01
- THEME-02
must_haves:
truths:
- "No hardcoded Tailwind color classes (gray-*, blue-*, red-*, green-*, yellow-*) remain in any of the 10 migrated files (App.tsx already done in Plan 01)"
- "All components render correctly using semantic token classes (bg-surface, text-on-surface, bg-primary, etc.)"
- "Components look correct in both light and dark mode without any dark: prefixes"
- "All existing tests pass without modification (tests use semantic queries, not class selectors)"
artifacts:
- path: "src/components/ui/BackendCard.tsx"
provides: "Migrated backend selection card"
min_lines: 15
- path: "src/components/ui/FieldRenderer.tsx"
provides: "Migrated form field renderer"
min_lines: 50
- path: "src/components/ui/PasswordField.tsx"
provides: "Migrated password input"
min_lines: 30
- path: "src/components/wizard/AzureAuthToggle.tsx"
provides: "Migrated Azure auth method toggle"
min_lines: 20
- path: "src/components/wizard/SftpAuthToggle.tsx"
provides: "Migrated SFTP auth method toggle"
min_lines: 25
- path: "src/components/wizard/DeploymentStep.tsx"
provides: "Migrated deployment step"
min_lines: 50
- path: "src/components/wizard/OutputBlock.tsx"
provides: "Migrated code output block"
min_lines: 20
- path: "src/components/wizard/RemoteConfigStep.tsx"
provides: "Migrated remote config step"
min_lines: 50
- path: "src/components/wizard/ReviewStep.tsx"
provides: "Migrated review step with success/warning tokens"
min_lines: 60
key_links:
- from: "src/index.css"
to: "all 9 migrated component files"
via: "CSS variable cascade through Tailwind utility classes"
pattern: "bg-surface|text-on-surface|border-outline|bg-primary|text-on-primary"
---
<objective>
Migrate all 63 hardcoded Tailwind color classes across 9 component files to semantic MD3 token classes.
Purpose: Complete THEME-01 by eliminating all hardcoded color references. After this plan, the entire UI responds to theme changes via the CSS variable cascade established in Plan 01.
Output: 9 migrated component files with zero hardcoded color classes, all tests passing.
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/08-theme-foundation/08-CONTEXT.md
@.planning/phases/08-theme-foundation/08-RESEARCH.md
@.planning/phases/08-theme-foundation/08-01-SUMMARY.md
<interfaces>
<!-- Token utility classes available after Plan 01 (from @theme in src/index.css): -->
<!-- bg-surface, text-on-surface, bg-surface-variant, text-on-surface-variant -->
<!-- bg-surface-container, text-on-surface-container -->
<!-- bg-primary, text-on-primary, text-primary, border-primary -->
<!-- border-outline, bg-error, text-error, text-on-error -->
<!-- bg-success, text-success, text-on-success -->
<!-- bg-warning, text-warning, text-on-warning -->
<!-- Opacity modifiers work: text-on-surface-container/70, border-primary/30, etc. -->
</interfaces>
<!-- Migration mapping reference (from 08-RESEARCH.md Pattern 4): -->
<!--
bg-gray-50 -> bg-surface
bg-white -> bg-surface-container
bg-gray-900 -> bg-surface-variant (code blocks)
text-gray-900 -> text-on-surface
text-gray-700 -> text-on-surface-container
text-gray-500 -> text-on-surface-container/70
text-gray-400 -> text-on-surface-container/50
text-gray-100 -> text-on-surface-variant
border-gray-300 -> border-outline
border-gray-200 -> border-outline
hover:bg-gray-50 -> hover:bg-surface
hover:border-blue-400 -> hover:border-primary
bg-blue-600 -> bg-primary
bg-blue-50 -> bg-primary/10
text-white (on blue bg) -> text-on-primary
text-blue-600/500/700 -> text-primary
hover:bg-blue-700 -> hover:bg-primary
border-blue-600 -> border-primary
border-blue-200 -> border-primary/30
text-blue-700 bg-blue-50 border-blue-200 -> text-primary bg-primary/10 border-primary/30
border-red-500 -> border-error
focus:ring-red-300 -> focus:ring-error/50
text-red-600 -> text-error
text-red-500 -> text-error
focus:ring-blue-300 -> focus:ring-primary/50
text-green-700 bg-green-50 -> text-success bg-success/10
bg-yellow-50 border-yellow-300 text-yellow-800/900 -> bg-warning/10 border-warning text-warning
-->
</context>
<tasks>
<task type="auto">
<name>Task 1: Migrate UI primitives (BackendCard, FieldRenderer, PasswordField)</name>
<files>src/components/ui/BackendCard.tsx, src/components/ui/FieldRenderer.tsx, src/components/ui/PasswordField.tsx</files>
<action>
Migrate all hardcoded color classes to semantic tokens. This is mechanical find-and-replace following the mapping. Do NOT change any DOM structure, only className strings.
**BackendCard.tsx** (5 occurrences):
- `border-blue-600 bg-blue-50` (selected) -> `border-primary bg-primary/10`
- `border-gray-200 bg-white hover:border-blue-400 hover:bg-gray-50` (unselected) -> `border-outline bg-surface-container hover:border-primary hover:bg-surface`
- `text-gray-900` -> `text-on-surface`
- `text-gray-500` -> `text-on-surface-container/70`
**FieldRenderer.tsx** (17 occurrences):
- `text-gray-700` (labels) -> `text-on-surface-container`
- `text-red-500` (required asterisk) -> `text-error`
- `text-blue-500 hover:text-blue-700` (toggle link) -> `text-primary hover:text-primary`
- `text-blue-700 bg-blue-50 border border-blue-200` (tooltip/info) -> `text-primary bg-primary/10 border border-primary/30`
- `border-red-500 focus:ring-red-300` (error state) -> `border-error focus:ring-error/50`
- `border-gray-300 focus:ring-blue-300` (normal state) -> `border-outline focus:ring-primary/50`
- `text-gray-500` (helpText) -> `text-on-surface-container/70`
- `text-red-600` (error message) -> `text-error`
- Apply the same mapping to both the text-input branch and the select branch (they have identical color patterns)
**PasswordField.tsx** (8 occurrences):
- `text-gray-700` -> `text-on-surface-container`
- `text-blue-500 hover:text-blue-700` -> `text-primary hover:text-primary`
- `text-blue-700 bg-blue-50 border border-blue-200` -> `text-primary bg-primary/10 border border-primary/30`
- `border-red-500 focus:ring-red-300` -> `border-error focus:ring-error/50`
- `border-gray-300 focus:ring-blue-300` -> `border-outline focus:ring-primary/50`
- `text-gray-400 hover:text-gray-700` (eye icon) -> `text-on-surface-container/50 hover:text-on-surface-container`
- `text-gray-500` (helpText) -> `text-on-surface-container/70`
- `text-red-600` (error message) -> `text-error`
Run `npm test` after each file to catch any regressions immediately.
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npm test 2>&1 | tail -20</automated>
</verify>
<done>BackendCard.tsx has 0 hardcoded color classes. FieldRenderer.tsx has 0 hardcoded color classes. PasswordField.tsx has 0 hardcoded color classes. All tests pass.</done>
</task>
<task type="auto">
<name>Task 2: Migrate wizard components (toggles, steps, output block)</name>
<files>src/components/wizard/AzureAuthToggle.tsx, src/components/wizard/SftpAuthToggle.tsx, src/components/wizard/DeploymentStep.tsx, src/components/wizard/OutputBlock.tsx, src/components/wizard/RemoteConfigStep.tsx, src/components/wizard/ReviewStep.tsx</files>
<action>
Continue the mechanical migration for all wizard components. Do NOT change DOM structure, only className strings.
**AzureAuthToggle.tsx** (7 occurrences):
- `border-gray-300` -> `border-outline`
- `bg-blue-600 text-white` (active tab) -> `bg-primary text-on-primary`
- `bg-white text-gray-700 hover:bg-gray-50` (inactive tab) -> `bg-surface-container text-on-surface-container hover:bg-surface`
**SftpAuthToggle.tsx** (10 occurrences):
- `text-gray-700` (label) -> `text-on-surface-container`
- `text-blue-500 hover:text-blue-700` (toggle link) -> `text-primary hover:text-primary`
- `text-blue-700 bg-blue-50 border border-blue-200` (info) -> `text-primary bg-primary/10 border border-primary/30`
- `border-gray-300` -> `border-outline`
- `bg-blue-600 text-white` (active) -> `bg-primary text-on-primary`
- `bg-white text-gray-700 hover:bg-gray-50` (inactive) -> `bg-surface-container text-on-surface-container hover:bg-surface`
**DeploymentStep.tsx** (2 occurrences):
- `border border-gray-300 ... hover:bg-gray-50` (Back button) -> `border border-outline ... hover:bg-surface`
- `bg-blue-600 text-white ... hover:bg-blue-700` (Next button) -> `bg-primary text-on-primary ... hover:bg-primary`
**OutputBlock.tsx** (4 occurrences):
- `text-gray-700` (label) -> `text-on-surface-container`
- `border-gray-300` (action buttons, 2 occurrences) -> `border-outline`
- `bg-gray-900 text-gray-100` (code pre) -> `bg-surface-variant text-on-surface-variant`
**RemoteConfigStep.tsx** (2 occurrences):
- `border border-gray-300 ... hover:bg-gray-50` (Back button) -> `border border-outline ... hover:bg-surface`
- `bg-blue-600 text-white ... hover:bg-blue-700` (Next button) -> `bg-primary text-on-primary ... hover:bg-primary`
**ReviewStep.tsx** (6 occurrences):
- `text-green-700 bg-green-50` (client-side notice) -> `text-success bg-success/10`
- `bg-yellow-50 border border-yellow-300` (security warning box) -> `bg-warning/10 border border-warning`
- `text-yellow-800` (warning title) -> `text-warning`
- `text-yellow-900` (warning label) -> `text-warning`
- `border border-gray-300 ... hover:bg-gray-50` (Back button) -> `border border-outline ... hover:bg-surface`
- `bg-blue-600 text-white ... hover:bg-blue-700` (Download button) -> `bg-primary text-on-primary ... hover:bg-primary`
After all files migrated, run verification grep to confirm zero hardcoded color classes remain.
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npm test 2>&1 | tail -20 && echo "---GREP CHECK---" && grep -rn "text-gray-\|bg-gray-\|border-gray-\|bg-blue-\|text-blue-\|border-blue-\|text-white\|bg-white\|text-red-\|border-red-\|text-green-\|bg-green-\|bg-yellow-\|text-yellow-\|border-yellow-\|hover:bg-gray-\|hover:bg-blue-\|hover:border-blue-\|focus:ring-red-\|focus:ring-blue-" src/components/ src/App.tsx 2>/dev/null; echo "Exit: $?"</automated>
</verify>
<done>All 9 component files migrated. Grep for hardcoded color classes in src/components/ and src/App.tsx returns 0 matches. All existing tests pass without modification.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Visual verification of theme system across all wizard steps</name>
<files>none</files>
<action>
Present the completed theme system for user visual verification. Start the dev server if not already running.
</action>
<what-built>Complete MD3 token system with dark mode toggle and all 63 hardcoded color classes migrated to semantic tokens across 10 files.</what-built>
<how-to-verify>
1. Run `npm run dev` and open http://localhost:5173
2. Verify the app renders correctly in light mode (indigo primary accents, gray surfaces)
3. Click "Dark" in the theme toggle (top-right, next to "Ready2Blob" title) -- entire UI should switch to dark palette instantly
4. Click "Light" -- UI returns to light palette
5. Click "System" -- follows your OS preference
6. With "Dark" selected, hard-reload the page (Ctrl+Shift+R) -- should load directly in dark mode with NO flash of light theme
7. Navigate through all 4 wizard steps checking:
- Backend cards have correct border/background colors in both themes
- Form inputs have visible borders and labels in both themes
- Code output blocks (ReviewStep) are readable in both themes
- Warning/success notices in ReviewStep are visible in both themes
- All buttons (Back/Next/Download) are visible and styled in both themes
</how-to-verify>
<verify>User confirms visual correctness</verify>
<done>User approves visual appearance of both light and dark themes across all wizard steps.</done>
<resume-signal>Type "approved" or describe any visual issues</resume-signal>
</task>
</tasks>
<verification>
1. `npm test` -- all tests pass (existing + ThemeToggle tests from Plan 01)
2. `grep -rn "text-gray-\|bg-gray-\|border-gray-\|bg-blue-\|text-blue-\|border-blue-\|text-white\|bg-white\|text-red-\|border-red-\|text-green-\|bg-green-\|bg-yellow-\|text-yellow-\|border-yellow-" src/components/ src/App.tsx` returns 0 matches
3. `npx vite build` completes without errors
</verification>
<success_criteria>
- Zero hardcoded Tailwind color classes remain in src/components/ and src/App.tsx
- All 9 component files use only semantic token classes (bg-surface, text-on-surface, bg-primary, etc.)
- No `dark:` utility prefixes used anywhere -- the CSS variable cascade handles both themes
- All existing tests pass without modification
- Visual verification confirms both light and dark mode render correctly across all wizard steps
</success_criteria>
<output>
After completion, create `.planning/phases/08-theme-foundation/08-02-SUMMARY.md`
</output>
@@ -0,0 +1,134 @@
---
phase: 08-theme-foundation
plan: 02
subsystem: ui
tags: [tailwindcss, css-custom-properties, dark-mode, theme, react]
# Dependency graph
requires:
- phase: 08-01
provides: MD3 color token system, bg-surface/text-on-surface/bg-primary/etc. Tailwind utility classes
provides:
- Zero hardcoded Tailwind color classes in all 9 component files (BackendCard, FieldRenderer, PasswordField, AzureAuthToggle, SftpAuthToggle, DeploymentStep, OutputBlock, RemoteConfigStep, ReviewStep)
- Full semantic token adoption: bg-primary, text-on-primary, border-outline, bg-surface-container, text-on-surface-container, text-success, text-warning, bg-surface-variant, text-on-surface-variant
affects: [09-component-migration, 10-content-refinement, 11-polish]
# Tech tracking
tech-stack:
added: []
patterns:
- Mechanical find-and-replace migration: hardcoded Tailwind color classes -> semantic MD3 token utility classes
- Opacity modifier pattern: text-on-surface-container/70, text-on-surface-container/50, bg-primary/10, border-primary/30, focus:ring-error/50, focus:ring-primary/50
- replace_all cautious: when two branches have visually identical but structurally different code, replace_all can miss second instance — verify grep after
key-files:
created: []
modified:
- src/components/ui/BackendCard.tsx
- src/components/ui/FieldRenderer.tsx
- src/components/ui/PasswordField.tsx
- src/components/wizard/AzureAuthToggle.tsx
- src/components/wizard/SftpAuthToggle.tsx
- src/components/wizard/DeploymentStep.tsx
- src/components/wizard/OutputBlock.tsx
- src/components/wizard/RemoteConfigStep.tsx
- src/components/wizard/ReviewStep.tsx
key-decisions:
- "Mechanical migration only — zero DOM structure changes, only className strings replaced"
- "replace_all: true used where both branches of if/else had identical class strings, then post-verified with grep"
- "ReviewStep warning colors: text-yellow-800 and text-yellow-900 both map to text-warning (single token covers both title and label)"
patterns-established:
- "Migration pattern: grep check after migration is mandatory — replace_all can silently miss occurrences in structurally different branches"
- "Opacity pattern: text-on-surface-container/70 (helpText), /50 (muted icons), bg-primary/10 (info backgrounds), border-primary/30 (info borders), focus:ring-error/50 and focus:ring-primary/50 (focus rings)"
requirements-completed: [THEME-01, THEME-02]
# Metrics
duration: 6min
completed: 2026-04-01
---
# Phase 8, Plan 02: Component Color Migration Summary
**63 hardcoded Tailwind color classes migrated to semantic MD3 tokens across 9 components — entire UI now responds to theme changes via CSS variable cascade with zero dark: prefixes**
## Performance
- **Duration:** 6 min
- **Started:** 2026-04-01T05:03:01Z
- **Completed:** 2026-04-01T05:09:00Z
- **Tasks:** 3 (2 auto + 1 human-verify checkpoint — user approved)
- **Files modified:** 9
## Accomplishments
- All 63 hardcoded color class occurrences removed from 9 component files (BackendCard, FieldRenderer, PasswordField, AzureAuthToggle, SftpAuthToggle, DeploymentStep, OutputBlock, RemoteConfigStep, ReviewStep)
- Semantic tokens applied: primary family (bg-primary, text-on-primary, text-primary, border-primary, bg-primary/10, border-primary/30), surface family (bg-surface, bg-surface-container, bg-surface-variant, text-on-surface, text-on-surface-container, text-on-surface-variant), error (border-error, text-error, focus:ring-error/50), success (text-success, bg-success/10), warning (text-warning, bg-warning/10, border-warning), outline (border-outline, focus:ring-primary/50)
- All 166 tests pass without modification (existing tests use semantic queries, not class selectors)
- grep -rn for all hardcoded color patterns in src/components/ and src/App.tsx returns 0 matches
## Task Commits
Each task was committed atomically:
1. **Task 1: Migrate UI primitives (BackendCard, FieldRenderer, PasswordField)** - `4e30f2e` (feat)
2. **Task 2: Migrate wizard components (toggles, steps, output block)** - `1ac32ea` (feat)
3. **Task 3: Visual verification** - checkpoint: human-verify (user approved)
**Plan metadata:** _(docs commit follows)_
## Files Created/Modified
- `src/components/ui/BackendCard.tsx` — selected/unselected card border/bg colors, name/description text colors
- `src/components/ui/FieldRenderer.tsx` — label, required asterisk, tooltip toggle, tooltip box, input borders, focus rings, help text, error message (both select and text-input branches)
- `src/components/ui/PasswordField.tsx` — label, tooltip toggle, tooltip box, input borders, focus ring, eye icon, help text, error message
- `src/components/wizard/AzureAuthToggle.tsx` — segmented control border, active/inactive tab colors
- `src/components/wizard/SftpAuthToggle.tsx` — auth method label, info tooltip toggle + box, segmented control border, active/inactive tab colors
- `src/components/wizard/DeploymentStep.tsx` — Back and Next navigation buttons
- `src/components/wizard/OutputBlock.tsx` — label, Copy/Download button borders, code pre background/text
- `src/components/wizard/RemoteConfigStep.tsx` — Back and Next navigation buttons
- `src/components/wizard/ReviewStep.tsx` — client-side notice, security warning box (bg/border/title/label), Back button, Download All ZIP button
## Decisions Made
- **Mechanical migration only:** Zero DOM structure changes — only className string replacements as specified by the plan's migration mapping. Ensures no regressions.
- **Both yellow variants -> text-warning:** `text-yellow-800` (warning title) and `text-yellow-900` (warning label) both map to `text-warning`. Single token is sufficient and avoids creating artificial token variants.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] FieldRenderer text branch not fully migrated by replace_all**
- **Found during:** Task 2 verification grep
- **Issue:** FieldRenderer has two conditional branches (select and text) with visually identical but structurally different JSX. The `replace_all` edits during Task 1 missed the text branch's label className and tooltip paragraph because the select branch was matched first (different aria-label attribute present). Post-task grep revealed 2 remaining occurrences.
- **Fix:** Added targeted Edit for the text branch's label (`text-gray-700` -> `text-on-surface-container`) and tooltip paragraph (`text-blue-700 bg-blue-50 border border-blue-200` -> `text-primary bg-primary/10 border border-primary/30`).
- **Files modified:** `src/components/ui/FieldRenderer.tsx`
- **Verification:** grep returns 0 matches after fix; 166 tests still pass
- **Committed in:** `1ac32ea` (Task 2 commit, staged with wizard files)
---
**Total deviations:** 1 auto-fixed (Rule 1 - missed occurrence in dual-branch component)
**Impact on plan:** Caught by mandatory post-migration grep check. Fix was a 2-line edit. No scope creep.
## Issues Encountered
- FieldRenderer's dual-branch structure (select vs text-input, both with identical tooltip JSX) caused a missed replacement when using `replace_all`. The post-migration grep verification step is confirmed mandatory for any component with if/else branches that share className strings.
## User Setup Required
None — no external service configuration required.
## Next Phase Readiness
- Zero hardcoded Tailwind color classes remain in `src/components/` and `src/App.tsx`
- The entire UI is now driven by CSS custom properties — light/dark theme switching is fully functional via the `.dark` class on `<html>`
- Visual verification approved: user confirmed both light and dark themes render correctly across all 4 wizard steps including backend cards, form inputs, code output blocks, success/warning notices, and navigation buttons
- Phase 8 is complete (THEME-01 and THEME-02 requirements satisfied)
- No blockers for Phase 9+ (content refinement, polish)
---
*Phase: 08-theme-foundation*
*Completed: 2026-04-01*
@@ -0,0 +1,86 @@
# Phase 8: Theme Foundation - Context
**Gathered:** 2026-03-31
**Status:** Ready for planning
<domain>
## Phase Boundary
App has a complete MD3 color token system and working dark mode toggle with no flash of unstyled content. All existing hardcoded color classes are migrated to semantic tokens. This phase delivers the infrastructure that Phases 9-11 build upon.
</domain>
<decisions>
## Implementation Decisions
### Color palette scope
- Essential roles only (~10 tokens): primary, on-primary, surface, on-surface, surface-variant, surface-container, on-surface-variant, outline, error, on-error
- Primary hue: Indigo/Purple (#4338CA light, #A5B4FC dark)
- Neutral gray surfaces (no tinted surfaces) — primary color only on interactive elements
- Migrate all 63 existing hardcoded color classes to semantic tokens in Phase 8 (10 files), not deferred to Phase 9
### Theme toggle UX
- Position: top-right corner of the app, next to the "Ready2Blob" title
- Style: segmented control with icons + labels [ Sun Light | Moon Dark | Monitor System ]
- Default for first-time visitors: System (respects OS preference)
- DOM class toggle on `<html>`, not React Context (already decided in STATE.md)
### Flash prevention
- Inline blocking script in `<head>` of index.html — reads localStorage before CSS paints
- localStorage key: `r2b-theme` (namespaced to avoid conflicts)
- Values: `"light"` | `"dark"` | `"system"` — absent treated as system
- Dark class applied to `<html>` element (Tailwind v4 convention)
- No real-time OS theme sync — system preference checked only on page load
- No matchMedia change listener (user must reload if OS theme changes mid-session)
### Claude's Discretion
- Token naming convention (MD3 canonical vs simplified)
- Token file structure (single file vs split)
- Exact @theme directive mapping syntax
- Segmented control component implementation details
- Exact indigo/purple shade values and dark mode variants
- Transition animation on theme switch (if any)
</decisions>
<code_context>
## Existing Code Insights
### Reusable Assets
- No existing theme infrastructure — building from scratch
- `index.css` currently just `@import "tailwindcss"` — clean starting point for token definitions
### Established Patterns
- Tailwind v4 via `@tailwindcss/vite` plugin (no config files)
- `useReducer` + Context API for state management
- Component structure: `src/components/ui/` (primitives) and `src/components/wizard/` (steps)
### Integration Points
- `src/index.css` — token definitions and @theme directive go here
- `index.html` — inline script for flash prevention
- `src/App.tsx` — ThemeToggle component placement (top-right, next to h1)
- 10 .tsx files with 63 hardcoded color classes to migrate:
- App.tsx (2), BackendCard.tsx (5), FieldRenderer.tsx (17), PasswordField.tsx (8)
- AzureAuthToggle.tsx (7), SftpAuthToggle.tsx (10), DeploymentStep.tsx (2)
- OutputBlock.tsx (4), RemoteConfigStep.tsx (2), ReviewStep.tsx (6)
</code_context>
<specifics>
## Specific Ideas
No specific requirements — open to standard approaches
</specifics>
<deferred>
## Deferred Ideas
None — discussion stayed within phase scope
</deferred>
---
*Phase: 08-theme-foundation*
*Context gathered: 2026-03-31*
@@ -0,0 +1,600 @@
# Phase 8: Theme Foundation - Research
**Researched:** 2026-03-31
**Domain:** Tailwind v4 `@theme` directive, MD3 color tokens, dark mode toggle, FOUC prevention
**Confidence:** HIGH
---
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
- **Color palette scope:** Essential roles only (~10 tokens): `primary`, `on-primary`, `surface`, `on-surface`, `surface-variant`, `surface-container`, `on-surface-variant`, `outline`, `error`, `on-error`
- **Primary hue:** Indigo/Purple (#4338CA light, #A5B4FC dark)
- **Neutral gray surfaces** (no tinted surfaces) — primary color only on interactive elements
- **Migrate all 63 existing hardcoded color classes** to semantic tokens in Phase 8 (10 files), not deferred to Phase 9
- **Theme toggle position:** Top-right corner of the app, next to the "Ready2Blob" title
- **Theme toggle style:** Segmented control with icons + labels [ Sun Light | Moon Dark | Monitor System ]
- **Default for first-time visitors:** System (respects OS preference)
- **DOM class toggle on `<html>`**, not React Context (already decided in STATE.md)
- **Flash prevention:** Inline blocking script in `<head>` of index.html — reads localStorage before CSS paints
- **localStorage key:** `r2b-theme` (namespaced to avoid conflicts)
- **Values:** `"light"` | `"dark"` | `"system"` — absent treated as system
- **Dark class applied to `<html>` element** (Tailwind v4 convention)
- **No real-time OS theme sync** — system preference checked only on page load
- **No matchMedia change listener** (user must reload if OS theme changes mid-session)
- **Zero new runtime dependencies** — Tailwind v4 `@theme` + CSS custom properties only
### Claude's Discretion
- Token naming convention (MD3 canonical vs simplified)
- Token file structure (single file vs split)
- Exact `@theme` directive mapping syntax
- Segmented control component implementation details
- Exact indigo/purple shade values and dark mode variants
- Transition animation on theme switch (if any)
### Deferred Ideas (OUT OF SCOPE)
None — discussion stayed within phase scope
</user_constraints>
---
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|-----------------|
| THEME-01 | App uses a consistent MD3 color token system (CSS custom properties) that all components reference instead of hardcoded Tailwind color classes | Tailwind v4 `@theme` maps `--color-*` namespace to utility classes like `bg-surface`, `text-on-primary`; light/dark values via `:root` / `.dark` overrides |
| THEME-02 | User can toggle between System, Light, and Dark themes, with choice persisted across sessions and no flash of unstyled content on load | `@custom-variant dark` + inline blocking script in `<head>` + `localStorage` key `r2b-theme`; DOM class on `<html>` element |
</phase_requirements>
---
## Summary
Phase 8 installs a complete MD3 color token system on top of Tailwind v4's CSS-first configuration. The approach uses no new runtime dependencies: tokens are defined as CSS custom properties in `src/index.css` under `@layer base` (light and dark values), then wired to Tailwind utility classes via `@theme`. A single `@custom-variant dark` declaration switches the dark variant from OS media query to `.dark` class on `<html>`, enabling manual toggling.
Flash prevention is handled by a small inline blocking script injected into `index.html`'s `<head>`. The script reads `localStorage.getItem('r2b-theme')`, resolves the effective value (`"light"` / `"dark"` / system fallback via `matchMedia`), and adds or removes the `.dark` class before the first CSS paint. The ThemeToggle component is a pure DOM manipulator — it writes to `localStorage` and toggles `.dark` on `document.documentElement` directly, with no React Context or re-render cascade.
The migration work is explicit and mechanical: 63 occurrences across 10 files, mapping known Tailwind gray/blue/red/green/yellow classes to the new semantic token classes. The most important design decision delegated to Claude's discretion is the exact indigo shade values and how to handle the special-case dark code block (`bg-gray-900 text-gray-100` in OutputBlock) which should remain a surface-variant rather than following a primary token.
**Primary recommendation:** Use the two-layer pattern — `@layer base` for light/dark raw values + `@theme` referencing those variables via `var(--*)` — so that a single `.dark` class on `<html>` cascades all token values automatically through the existing utility class usage.
---
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| tailwindcss | ^4.2.2 (already installed) | Utility CSS framework + `@theme` directive | Already in project; v4 is CSS-first, no config file needed |
| @tailwindcss/vite | ^4.2.2 (already installed) | Vite plugin integration | Required by v4 for build-time processing |
### Supporting
No new runtime dependencies required. All functionality is CSS + vanilla JS in the inline script.
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| CSS custom properties + `@theme` | `next-themes` library | Library adds ~3KB runtime, overkill for a Vite/React app without SSR; `next-themes` designed for Next.js hydration |
| Inline blocking script | `useEffect` on mount | `useEffect` runs after React renders — creates visible flash; inline script is synchronous and blocks paint |
| DOM class toggle | React Context `ThemeProvider` | Context triggers full subtree re-render; DOM class toggle updates CSS variables in one paint with no React involvement |
**Installation:** None required — dependencies already present.
---
## Architecture Patterns
### Recommended Project Structure
```
src/
├── index.css # @import tailwindcss + @layer base tokens + @theme + @custom-variant
├── components/
│ ├── ui/
│ │ ├── ThemeToggle.tsx # New: segmented control [ Sun | Moon | Monitor ]
│ │ ├── BackendCard.tsx # Migrate: border-blue-600 → border-primary, etc.
│ │ ├── FieldRenderer.tsx # Migrate: border-gray-300, text-gray-700, etc.
│ │ └── PasswordField.tsx # Migrate: text-gray-400, hover:text-gray-700, etc.
│ └── wizard/
│ ├── AzureAuthToggle.tsx # Migrate: bg-blue-600, bg-white, text-gray-700
│ ├── SftpAuthToggle.tsx # Migrate: bg-blue-600, bg-white, text-gray-700
│ ├── DeploymentStep.tsx # Migrate: border-gray-300, hover:bg-gray-50
│ ├── OutputBlock.tsx # Migrate: bg-gray-900 text-gray-100 → surface-container tokens
│ ├── RemoteConfigStep.tsx # Migrate: bg-blue-600 text-white, border-gray-300
│ └── ReviewStep.tsx # Migrate: text-green-700 bg-green-50, bg-yellow-50 border-yellow-300
index.html # Add inline blocking script in <head>
```
### Pattern 1: Two-Layer Token Architecture
**What:** Separate raw color values (in `@layer base`) from the `@theme` mapping. Raw values live under `:root` (light) and `.dark` (dark). `@theme` references them with `var(--*)`.
**When to use:** Whenever you need a single class on `<html>` to cascade all color changes — no per-element `dark:` prefixes needed.
**Example:**
```css
/* src/index.css */
@import "tailwindcss";
/* 1. Declare dark variant based on .dark class on <html> */
@custom-variant dark (&:where(.dark, .dark *));
/* 2. Raw values — light defaults, overridden in .dark */
@layer base {
:root {
--r2b-primary: #4338CA; /* indigo-700 */
--r2b-on-primary: #FFFFFF;
--r2b-surface: #F9FAFB; /* gray-50 */
--r2b-on-surface: #111827; /* gray-900 */
--r2b-surface-variant: #1F2937; /* gray-800 — code blocks */
--r2b-on-surface-variant: #F3F4F6; /* gray-100 — text on code blocks */
--r2b-surface-container: #FFFFFF; /* card/form backgrounds */
--r2b-on-surface-container: #374151; /* gray-700 — labels, secondary text */
--r2b-outline: #D1D5DB; /* gray-300 — borders */
--r2b-error: #EF4444; /* red-500 */
--r2b-on-error: #FFFFFF;
}
.dark {
--r2b-primary: #A5B4FC; /* indigo-300 */
--r2b-on-primary: #1E1B4B; /* indigo-950 */
--r2b-surface: #111827; /* gray-900 */
--r2b-on-surface: #F9FAFB; /* gray-50 */
--r2b-surface-variant: #0F172A; /* slate-900 — code blocks */
--r2b-on-surface-variant: #E5E7EB; /* gray-200 */
--r2b-surface-container: #1F2937; /* gray-800 — card/form backgrounds */
--r2b-on-surface-container: #D1D5DB; /* gray-300 — labels, secondary text */
--r2b-outline: #4B5563; /* gray-600 — borders */
--r2b-error: #FCA5A5; /* red-300 */
--r2b-on-error: #7F1D1D; /* red-900 */
}
}
/* 3. Wire to Tailwind utility classes */
@theme {
--color-primary: var(--r2b-primary);
--color-on-primary: var(--r2b-on-primary);
--color-surface: var(--r2b-surface);
--color-on-surface: var(--r2b-on-surface);
--color-surface-variant: var(--r2b-surface-variant);
--color-on-surface-variant: var(--r2b-on-surface-variant);
--color-surface-container: var(--r2b-surface-container);
--color-on-surface-container: var(--r2b-on-surface-container);
--color-outline: var(--r2b-outline);
--color-error: var(--r2b-error);
--color-on-error: var(--r2b-on-error);
}
```
This generates: `bg-surface`, `text-on-surface`, `bg-surface-container`, `text-on-surface-container`, `bg-primary`, `text-on-primary`, `border-outline`, `bg-error`, `text-error`, `text-on-error`, etc.
### Pattern 2: Flash Prevention Inline Script
**What:** A synchronous script in `<head>` that runs before any CSS is applied. Reads `localStorage`, resolves the effective theme, and applies `.dark` to `<html>` if needed.
**When to use:** Always — any async approach (React `useEffect`, CSS media queries) allows one frame of wrong-theme content.
**Example:**
```html
<!-- index.html — inside <head>, before any stylesheet links -->
<script>
(function() {
try {
var stored = localStorage.getItem('r2b-theme');
var effective;
if (stored === 'dark') {
effective = 'dark';
} else if (stored === 'light') {
effective = 'light';
} else {
// stored === 'system' or absent — check OS preference
effective = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
if (effective === 'dark') {
document.documentElement.classList.add('dark');
}
} catch (e) {
// localStorage unavailable (private mode, etc.) — leave light as default
}
})();
</script>
```
### Pattern 3: ThemeToggle Component
**What:** A stateless React component that reads its current display state from `document.documentElement.classList` + `localStorage`, and writes to both on user interaction.
**When to use:** As the sole place in the codebase that touches theme state.
**Example:**
```tsx
// src/components/ui/ThemeToggle.tsx
type ThemeValue = 'light' | 'dark' | 'system';
const STORAGE_KEY = 'r2b-theme';
function getStored(): ThemeValue {
try {
const v = localStorage.getItem(STORAGE_KEY);
if (v === 'light' || v === 'dark' || v === 'system') return v;
} catch {}
return 'system';
}
function applyTheme(value: ThemeValue) {
const root = document.documentElement;
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = value === 'dark' || (value === 'system' && prefersDark);
root.classList.toggle('dark', isDark);
try { localStorage.setItem(STORAGE_KEY, value); } catch {}
}
export function ThemeToggle() {
const [theme, setTheme] = useState<ThemeValue>(getStored);
function select(value: ThemeValue) {
setTheme(value);
applyTheme(value);
}
return (
<div role="group" aria-label="Theme" className="flex rounded-md border border-outline overflow-hidden text-sm">
{(['light', 'dark', 'system'] as const).map((v) => (
<button
key={v}
type="button"
onClick={() => select(v)}
aria-pressed={theme === v}
className={theme === v
? 'flex-1 px-3 py-1 bg-primary text-on-primary font-medium'
: 'flex-1 px-3 py-1 bg-surface-container text-on-surface-container hover:bg-surface'}
>
{v === 'light' ? '☀ Light' : v === 'dark' ? '🌙 Dark' : '⊙ System'}
</button>
))}
</div>
);
}
```
Note: The `useState(getStored)` initializer (lazy init) runs once on mount — avoids localStorage access on every render. `applyTheme` is also called directly (no `useEffect` needed) because DOM mutation is synchronous.
### Pattern 4: Color Class Migration Mapping
**What:** A mechanical find-and-replace map from existing hardcoded Tailwind classes to new semantic tokens.
```
bg-gray-50 → bg-surface
bg-white → bg-surface-container
bg-gray-900 → bg-surface-variant (code blocks only)
text-gray-900 → text-on-surface
text-gray-700 → text-on-surface-container
text-gray-500 → text-on-surface-container (secondary text — same token, or add opacity)
text-gray-400 → text-on-surface-container (muted — consider opacity variant)
text-gray-100 → text-on-surface-variant (text on code blocks)
border-gray-300 → border-outline
border-gray-200 → border-outline
hover:bg-gray-50 → hover:bg-surface
hover:border-blue-400 → hover:border-primary
bg-blue-600 → bg-primary
bg-blue-50 → bg-surface (tinted interactive states — use bg-primary/10 instead)
text-white → text-on-primary
text-blue-600 → text-primary
text-blue-500 → text-primary
text-blue-700 → text-primary
hover:bg-blue-700 → hover:bg-primary
border-blue-600 → border-primary
border-blue-200 → border-primary/30
text-blue-700 bg-blue-50 border border-blue-200 → tooltip pattern: text-primary bg-surface border-primary/30
border-red-500 → border-error
focus:ring-red-300 → focus:ring-error/50
text-red-600 → text-error
text-red-500 → text-error
text-green-700 bg-green-50 → use surface-container + primary (or a dedicated success token)
bg-yellow-50 border-yellow-300 text-yellow-800 → warning pattern (see Pitfall 2)
```
### Anti-Patterns to Avoid
- **Using `dark:` utility prefixes after migration:** The whole point of the token system is that `bg-surface` already contains both light and dark values via the CSS variable cascade. Adding `dark:bg-surface-variant` creates duplication and confusion.
- **Defining tokens directly in `@theme` without `@layer base` indirection:** `@theme { --color-surface: #F9FAFB; }` puts a static value in the generated CSS. You cannot override it with `.dark { --color-surface: #111827; }` because `@theme` emits the value into `:root` directly, not as a reference. You must use `@theme { --color-surface: var(--r2b-surface); }` and define `--r2b-surface` in `@layer base`.
- **Toggling theme in React state/Context:** Causes a full subtree re-render on every toggle. DOM class on `<html>` is a CSS cascade — zero React overhead.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Dark/light value switching | Per-component `dark:` prefixes on every class | Single `.dark` class on `<html>` + CSS variable cascade | 63 classes become ~10 tokens; adding a new component costs zero extra work |
| Flash of unstyled content | React `useEffect` checking localStorage | Inline blocking `<script>` in `<head>` | React lifecycle runs after hydration; `useEffect` is always async-after-paint |
| System preference detection | Polling `matchMedia` | Check once in the inline script + ThemeToggle mount | One read on load is sufficient given no real-time OS sync requirement |
**Key insight:** The CSS variable indirection layer (`--r2b-*` raw values → `@theme` reference via `var()`) is the unlock. Without it, `@theme` emits static values and the `.dark` class override cannot work.
---
## Common Pitfalls
### Pitfall 1: Static values in @theme break dark mode override
**What goes wrong:** Developer writes `@theme { --color-surface: #F9FAFB; }`. Adds `.dark { --color-surface: #111827; }` to `@layer base`. Dark mode has no effect on `bg-surface` utility.
**Why it happens:** Tailwind v4's `@theme` compiles `--color-surface: #F9FAFB` into `:root { --color-surface: #F9FAFB; }` at build time — a static value. The `.dark` override happens at `:root` + `.dark` specificity, but the utility class `bg-surface` uses `var(--color-surface)`, which resolves to the `:root` value (not overridden because specificity battle). Actually: the real issue is Tailwind generates `background-color: var(--color-surface)` in the utility, and `.dark` does override `:root` correctly — **but only if the `@theme` value itself is `var(--r2b-surface)` not a static hex**. If static hex is used, the `:root` value is just the hex and `.dark { --color-surface: ... }` does work. However the `@theme` directive is **design-time only** — it cannot contain `var()` references to dynamic variables in some Tailwind versions.
**Verified behavior (Tailwind v4.2.2):** The recommended safe pattern confirmed by community and official examples is: keep raw values in `@layer base :root / .dark`, and use `@theme` with `var()` references. Tailwind v4 does support `var()` in `@theme` — this is what enables the entire dynamic theming approach.
**How to avoid:** Always use `@theme { --color-X: var(--r2b-X); }` with matching `@layer base { :root { --r2b-X: <light-value>; } .dark { --r2b-X: <dark-value>; } }`.
**Warning signs:** `bg-surface` shows the same color in both light and dark mode.
### Pitfall 2: Semantic gap for warning/success colors
**What goes wrong:** ReviewStep uses `text-green-700 bg-green-50` (client-side notice) and `bg-yellow-50 border-yellow-300 text-yellow-800/text-yellow-900` (security warning). These have no MD3 equivalents in the 10-token set.
**Why it happens:** MD3 defines `error` but not `success` or `warning` as primary roles.
**How to avoid:** Two valid approaches:
1. Keep these as hardcoded Tailwind classes with `dark:` overrides (acceptable for one-off semantic messages). This means those 6 color classes in ReviewStep are intentionally not migrated to tokens.
2. Add two extra tokens: `--color-warning` and `--color-on-warning` (and optionally `--color-success` / `--color-on-success`). Stays within "no new dependencies" constraint. Recommended if consistency across future phases matters.
**Warning signs:** After migration, the security warning area looks wrong in dark mode (yellow-50 is near-white — invisible against dark surfaces).
### Pitfall 3: ThemeToggle reads stale localStorage on HMR / fast refresh
**What goes wrong:** During development with Vite HMR, the ThemeToggle component re-mounts but `getStored()` runs again and the localStorage value may not match the current `.dark` class state (if developer toggled via DevTools).
**Why it happens:** The inline script runs once on hard reload. HMR does a partial re-mount.
**How to avoid:** This is a dev-only annoyance. The component reads from `localStorage` on mount which is correct behavior. In production (hard loads), it will always be consistent. No special handling needed — document it in a code comment.
### Pitfall 4: `@custom-variant dark` placement matters
**What goes wrong:** `@custom-variant dark` placed after `@import "tailwindcss"` in the CSS file may fail to override the built-in `dark:` variant in some build tool configurations.
**Why it happens:** Order of processing directives.
**How to avoid:** Place `@custom-variant dark (&:where(.dark, .dark *));` immediately after `@import "tailwindcss";`, before any `@layer` or `@theme` blocks.
### Pitfall 5: 131 existing test selectors
**What goes wrong:** Tests select elements by text content or role, but some tests in BackendSelectionStep, RemoteConfigStep, ReviewStep may also implicitly test rendered output via snapshot or class-based assertions that break when classes change.
**Why it happens:** Token migration changes class names on DOM elements.
**How to avoid:** Check existing tests before migrating each file. The 5 test files use `screen.getByText`, `screen.getByRole`, and `userEvent` — not class-based selectors. STATE.md confirms "131 test selectors could break during component restyling" but on inspection the tests use semantic queries. Risk is LOW if migration is class-name-only (no structural DOM changes).
---
## Code Examples
Verified patterns from official sources and community documentation:
### Complete index.css structure
```css
/* Source: https://tailwindcss.com/docs/dark-mode + https://tailwindcss.com/docs/theme */
@import "tailwindcss";
/* Override built-in dark variant to use .dark class instead of prefers-color-scheme */
@custom-variant dark (&:where(.dark, .dark *));
/* Raw color values — overridden per theme */
@layer base {
:root {
--r2b-primary: #4338CA;
--r2b-on-primary: #FFFFFF;
--r2b-surface: #F9FAFB;
--r2b-on-surface: #111827;
--r2b-surface-variant: #1F2937;
--r2b-on-surface-variant: #F3F4F6;
--r2b-surface-container: #FFFFFF;
--r2b-on-surface-container: #374151;
--r2b-outline: #D1D5DB;
--r2b-error: #EF4444;
--r2b-on-error: #FFFFFF;
}
.dark {
--r2b-primary: #A5B4FC;
--r2b-on-primary: #1E1B4B;
--r2b-surface: #111827;
--r2b-on-surface: #F9FAFB;
--r2b-surface-variant: #0F172A;
--r2b-on-surface-variant: #E5E7EB;
--r2b-surface-container: #1F2937;
--r2b-on-surface-container: #D1D5DB;
--r2b-outline: #4B5563;
--r2b-error: #FCA5A5;
--r2b-on-error: #7F1D1D;
}
}
/* Wire to Tailwind utility classes */
@theme {
--color-primary: var(--r2b-primary);
--color-on-primary: var(--r2b-on-primary);
--color-surface: var(--r2b-surface);
--color-on-surface: var(--r2b-on-surface);
--color-surface-variant: var(--r2b-surface-variant);
--color-on-surface-variant: var(--r2b-on-surface-variant);
--color-surface-container: var(--r2b-surface-container);
--color-on-surface-container: var(--r2b-on-surface-container);
--color-outline: var(--r2b-outline);
--color-error: var(--r2b-error);
--color-on-error: var(--r2b-on-error);
}
```
### Flash prevention script (index.html)
```html
<!-- Source: pattern verified against https://tailwindcss.com/docs/dark-mode -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ready2Blob</title>
<script>
(function() {
try {
var stored = localStorage.getItem('r2b-theme');
var effective;
if (stored === 'dark') {
effective = 'dark';
} else if (stored === 'light') {
effective = 'light';
} else {
effective = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
if (effective === 'dark') {
document.documentElement.classList.add('dark');
}
} catch (e) {}
})();
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
```
### App.tsx header with ThemeToggle
```tsx
// Updated WizardShell header area — ThemeToggle next to h1
<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>
<StepIndicator />
...
</div>
</div>
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `tailwind.config.js` `darkMode: 'class'` | `@custom-variant dark` in CSS | Tailwind v4.0 (2025) | No config file; CSS-first |
| `extend.colors` in JS config | `@theme { --color-*: ... }` in CSS | Tailwind v4.0 (2025) | Tokens and utilities in one place |
| Separate `dark:` prefix on every utility | Single CSS variable cascade via `.dark` class | CSS variables + v4 | Zero per-element dark variants needed |
| `matchMedia` change listener for live sync | Check on page load only | Project decision | Simpler; avoids mid-session inconsistency |
**Deprecated/outdated:**
- `tailwind.config.js` `darkMode: 'class'` key: replaced by `@custom-variant` in CSS
- `theme.extend.colors`: replaced by `@theme` block
---
## Open Questions
1. **Warning/success tokens**
- What we know: ReviewStep has green (client-side notice) and yellow (security warning) color combinations that have no direct MD3 token equivalent
- What's unclear: Should these be added as `--color-warning` / `--color-success` tokens or kept as hardcoded Tailwind classes with `dark:` overrides?
- Recommendation: Add `--color-warning` and `--color-on-warning` (amber-based) and `--color-success` and `--color-on-success` (green-based) tokens to keep the codebase clean. Total token count goes from 10 to 14 — still minimal. This keeps THEME-01 fully satisfied.
2. **Transition animation on theme switch**
- What we know: Claude's Discretion — user left this open
- What's unclear: A CSS `transition: color 150ms, background-color 150ms` on `:root` or `body` would animate all color changes on toggle
- Recommendation: Add `transition: background-color 200ms ease, color 200ms ease, border-color 200ms ease` to the `body` in `@layer base`. Disable with `@media (prefers-reduced-motion: reduce)` wrapper. Adds visual polish at zero cost.
3. **`on-surface-container` vs `on-surface` for gray-500/gray-400**
- What we know: The codebase uses both `text-gray-700` (labels) and `text-gray-500` (secondary) and `text-gray-400` (muted/icons)
- What's unclear: Whether to create separate `--color-muted` token or use opacity modifiers (`text-on-surface/60`)
- Recommendation: Use Tailwind's opacity modifier: `text-on-surface-container/70` for secondary text, `text-on-surface-container/50` for muted. No extra tokens needed. This matches how MD3 uses opacity on surface roles.
---
## Validation Architecture
`workflow.nyquist_validation` is `true` — include this section.
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Vitest ^4.1.1 |
| Config file | `vite.config.ts` (test block: `environment: 'jsdom'`) |
| Quick run command | `npm test` |
| Full suite command | `npm test` |
Note: `vitest.config.ts` sets `environment: 'node'` and `globals: true`. The component tests override with `// @vitest-environment jsdom` pragma. The `vite.config.ts` also defines `test: { environment: 'jsdom' }`. The component test files win because they use the pragma.
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| THEME-01 | Token classes (`bg-surface`, `text-on-primary`, etc.) are used in migrated components instead of hardcoded Tailwind colors | unit | `npm test` (all existing component tests pass) | ✅ existing |
| THEME-01 | All 63 hardcoded color classes are absent after migration | lint/grep | `grep -r "text-gray-\|bg-gray-\|border-gray-\|bg-blue-\|text-blue-" src/` | manual verification |
| THEME-02 | ThemeToggle renders with three options (Light, Dark, System) | unit | `npm test -- --reporter=verbose` | ❌ Wave 0 |
| THEME-02 | Clicking Dark adds `.dark` class to `document.documentElement` | unit | `npm test -- --reporter=verbose` | ❌ Wave 0 |
| THEME-02 | Theme choice persists in `localStorage` under key `r2b-theme` | unit | `npm test -- --reporter=verbose` | ❌ Wave 0 |
| THEME-02 | Flash prevention: `dark` class applied before first render | manual | Open browser in dark OS mode, hard reload, observe | manual-only |
### Sampling Rate
- **Per task commit:** `npm test`
- **Per wave merge:** `npm test`
- **Phase gate:** Full suite green before `/gsd:verify-work`
### Wave 0 Gaps
- [ ] `src/components/ui/ThemeToggle.test.tsx` — covers THEME-02 (component unit tests)
*(All existing 5 test files cover structural/routing behaviors unaffected by class-name changes — they will continue to pass as-is after migration.)*
---
## Sources
### Primary (HIGH confidence)
- [Tailwind CSS — Theme variables](https://tailwindcss.com/docs/theme) — `@theme` directive syntax, `--color-*` namespace, `var()` in `@theme`
- [Tailwind CSS — Dark mode](https://tailwindcss.com/docs/dark-mode) — `@custom-variant dark`, `.dark` class on `<html>`, system preference pattern
### Secondary (MEDIUM confidence)
- [Tailwind CSS — Multi-theme system (Medium)](https://medium.com/render-beyond/build-a-flawless-multi-theme-ui-using-new-tailwind-css-v4-react-dca2b3c95510) — Two-layer pattern (`:root` raw values + `@theme` var references) verified against official docs
- [Tailwind CSS theming best practices discussion](https://github.com/tailwindlabs/tailwindcss/discussions/18471) — `@theme inline` vs property-scoped variables
- [Invertase dark mode blog](https://invertase.io/blog/tailwind-dark-mode) — Inline blocking script pattern; v3 syntax adapted to v4
### Tertiary (LOW confidence)
- [Dark mode discussion #15083](https://github.com/tailwindlabs/tailwindcss/discussions/15083) — Community patterns for CSS variables in dark/light mode
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — already installed and in use; no new dependencies
- Architecture: HIGH — `@theme` + `@custom-variant` patterns verified against official docs
- Token values: MEDIUM — exact hex values for discretion items (indigo shades, dark variants) are Claude's choices; correctness depends on visual review not API docs
- Pitfalls: HIGH for `@theme` static-vs-dynamic (verified); MEDIUM for warning/success color gap (project-specific judgment)
- Migration map: HIGH — mechanical mapping from known Tailwind classes to known token roles
**Research date:** 2026-03-31
**Valid until:** 2026-09-30 (Tailwind v4 is recently stable; CSS custom properties are CSS3 — very stable)
@@ -0,0 +1,76 @@
---
phase: 8
slug: theme-foundation
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-03-31
---
# Phase 8 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution.
---
## Test Infrastructure
| Property | Value |
|----------|-------|
| **Framework** | Vitest ^4.1.1 |
| **Config file** | `vite.config.ts` (test block: `environment: 'jsdom'`) |
| **Quick run command** | `npm test` |
| **Full suite command** | `npm test` |
| **Estimated runtime** | ~5 seconds |
---
## Sampling Rate
- **After every task commit:** Run `npm test`
- **After every plan wave:** Run `npm test`
- **Before `/gsd:verify-work`:** Full suite must be green
- **Max feedback latency:** 10 seconds
---
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
| 08-01-01 | 01 | 0 | THEME-02 | unit | `npm test` | ❌ W0 | ⬜ pending |
| 08-02-01 | 02 | 1 | THEME-01 | unit | `npm test` | ✅ existing | ⬜ pending |
| 08-02-02 | 02 | 1 | THEME-01 | grep | `grep -r "text-gray-\|bg-gray-\|border-gray-\|bg-blue-\|text-blue-" src/` | manual | ⬜ pending |
| 08-03-01 | 03 | 1 | THEME-02 | unit | `npm test` | ❌ W0 | ⬜ pending |
| 08-03-02 | 03 | 1 | THEME-02 | manual | Browser: dark OS → hard reload → no flash | manual-only | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
---
## Wave 0 Requirements
- [ ] `src/components/ui/ThemeToggle.test.tsx` — stubs for THEME-02 (toggle render, dark class toggle, localStorage persistence)
*Existing 5 test files use semantic queries (getByText, getByRole) — unaffected by class-name migration.*
---
## Manual-Only Verifications
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| No flash of wrong theme on reload | THEME-02 | Requires browser paint timing — jsdom cannot verify visual flash | 1. Set theme to Dark, reload page 2. Set theme to Light, reload in dark OS mode 3. Clear localStorage, reload — should follow OS preference |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 10s
- [ ] `nyquist_compliant: true` set in frontmatter
**Approval:** pending
@@ -0,0 +1,95 @@
---
phase: 08-theme-foundation
verified: 2026-04-01T05:30:00Z
status: passed
score: 9/9 must-haves verified
gaps: []
human_verification:
- test: "Flash prevention — dark mode on hard reload"
expected: "With r2b-theme=dark in localStorage, hard-reload (Ctrl+Shift+R) loads directly in dark mode with no visible flash of light theme"
why_human: "Cannot verify paint timing or visual FOUC programmatically"
status: "approved — user verified during checkpoint Task 3 of Plan 02"
- test: "Dark mode visual correctness across all wizard steps"
expected: "Clicking Dark in ThemeToggle switches entire UI to dark palette — all cards, inputs, code blocks, warnings, buttons render correctly"
why_human: "Visual appearance and contrast require human review"
status: "approved — user verified during checkpoint Task 3 of Plan 02"
- test: "System theme follows OS preference"
expected: "Clicking System with OS in dark mode applies dark palette; OS in light mode shows light palette"
why_human: "Requires OS-level matchMedia interaction to test"
status: "approved — user verified during checkpoint Task 3 of Plan 02"
---
# Phase 8: Theme Foundation Verification Report
**Phase Goal:** Establish MD3-inspired color token system with dark mode toggle. Deliver CSS custom property foundation, ThemeToggle component, and migrate all hardcoded Tailwind color classes to semantic tokens.
**Verified:** 2026-04-01T05:30:00Z
**Status:** passed
**Re-verification:** Corrected from initial gaps_found — verifier agent had corrupted its working tree, causing false gap reports. Orchestrator confirmed 166/166 tests pass on clean HEAD.
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | Tailwind utility classes bg-surface, text-on-surface, bg-primary etc. resolve to correct hex values | VERIFIED | `src/index.css`: `@theme` block maps 15 `--color-*` tokens to `var(--r2b-*)` values; `:root` sets light hex values, `.dark` overrides all 15 |
| 2 | Adding .dark class to html element switches all token values to dark palette | VERIFIED | `@custom-variant dark (&:where(.dark, .dark *))` + `@layer base .dark { ... }` block with all 15 dark hex values |
| 3 | ThemeToggle renders three options: Light, Dark, System | VERIFIED | `ThemeToggle.tsx` renders `['light', 'dark', 'system']` via `.map()` in a `role="group"` div |
| 4 | Clicking a theme option applies the correct class to document.documentElement and persists to localStorage | VERIFIED | `applyTheme()`: `root.classList.toggle('dark', isDark)` + `localStorage.setItem(STORAGE_KEY, value)` |
| 5 | On hard reload with r2b-theme=dark in localStorage, the .dark class is present before React mounts (no flash) | VERIFIED (human) | Inline IIFE script in `index.html` placed before `<link>` tags. User approved visual verification. |
| 6 | No hardcoded Tailwind color classes remain in any of the 10 migrated files | VERIFIED | grep for hardcoded color patterns returns 0 matches across all 9 components + App.tsx |
| 7 | No dark: utility prefixes used anywhere | VERIFIED | Zero `dark:` className usages in any component |
| 8 | ThemeToggle unit tests pass (7 tests) | VERIFIED | `npx vitest run` — 166 tests pass including 7 ThemeToggle tests |
| 9 | No regression in existing test suite | VERIFIED | 166/166 tests pass (159 pre-existing + 7 new ThemeToggle tests) |
**Score:** 9/9 truths verified
### Required Artifacts
**Plan 01 artifacts:**
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `src/index.css` | MD3 color token definitions (light + dark) and @theme mapping | VERIFIED | `@custom-variant dark`, `@layer base` with `:root` (15 tokens) + `.dark` (15 overrides), `@theme` block with 15 `--color-*` mappings, body transition with reduced-motion media query |
| `index.html` | Inline blocking script for flash prevention | VERIFIED | IIFE script reads `r2b-theme` from localStorage before `<link>` or module scripts; try/catch for private mode |
| `src/components/ui/ThemeToggle.tsx` | Segmented theme toggle component | VERIFIED | Exports `ThemeToggle`, `applyTheme()` uses `classList.toggle`, `getStored()` lazy init, 3 buttons with `aria-pressed` |
| `src/components/ui/ThemeToggle.test.tsx` | Unit tests for ThemeToggle behavior | VERIFIED | 7 passing tests covering renders, default state, Dark/Light toggling, localStorage persistence, aria-pressed |
| `src/App.tsx` | ThemeToggle wired into header, bg-surface applied to shell | VERIFIED | `import { ThemeToggle }`, flex header row, `bg-surface`, `text-on-surface` |
**Plan 02 artifacts (min_lines check):**
| Artifact | Min Lines | Status |
|----------|-----------|--------|
| `src/components/ui/BackendCard.tsx` | 15 | VERIFIED |
| `src/components/ui/FieldRenderer.tsx` | 50 | VERIFIED |
| `src/components/ui/PasswordField.tsx` | 30 | VERIFIED |
| `src/components/wizard/AzureAuthToggle.tsx` | 20 | VERIFIED |
| `src/components/wizard/SftpAuthToggle.tsx` | 25 | VERIFIED |
| `src/components/wizard/DeploymentStep.tsx` | 50 | VERIFIED |
| `src/components/wizard/OutputBlock.tsx` | 20 | VERIFIED |
| `src/components/wizard/RemoteConfigStep.tsx` | 50 | VERIFIED |
| `src/components/wizard/ReviewStep.tsx` | 60 | VERIFIED |
### Key Link Verification
| From | To | Via | Status |
|------|----|-----|--------|
| `src/index.css` | `ThemeToggle.tsx` | `.dark` class toggled by `classList.toggle('dark', isDark)` | VERIFIED |
| `index.html` | localStorage | inline script reads `r2b-theme` before paint | VERIFIED |
| `src/App.tsx` | `ThemeToggle.tsx` | import and render in header | VERIFIED |
| `src/index.css` | all 9 migrated files | CSS variable cascade via Tailwind utility classes | VERIFIED |
### Requirements Coverage
| Requirement | Source Plan | Status | Evidence |
|-------------|-------------|--------|----------|
| THEME-01 | 08-01, 08-02 | SATISFIED | 15 CSS custom properties; all 10 files use semantic token classes; 0 hardcoded color classes |
| THEME-02 | 08-01, 08-02 | SATISFIED | ThemeToggle persists to localStorage + toggles `.dark`; FOUC prevention script in `index.html`; user-approved visual verification |
Both THEME-01 and THEME-02 are mapped to Phase 8 in REQUIREMENTS.md traceability table. No orphaned requirements.
---
_Verified: 2026-04-01T05:30:00Z_
_Verifier: Claude (gsd-verifier, corrected by orchestrator)_
@@ -0,0 +1,204 @@
---
phase: 09-md3-components
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/components/ui/TextFieldMD3.tsx
- src/components/ui/TextFieldMD3.test.tsx
- src/styles/md3-buttons.ts
- src/components/ui/FieldRenderer.tsx
- src/components/ui/FieldRenderer.test.tsx
autonomous: true
requirements: [COMP-01, COMP-02, DEBT-01]
must_haves:
truths:
- "TextFieldMD3 renders an outlined input with a floating label that floats on focus"
- "TextFieldMD3 floating label floats when the field has a value (not just on focus)"
- "TextFieldMD3 preserves htmlFor/id pairing so getByLabelText queries work"
- "MD3 button class constants exist for filled, outlined, and text variants"
- "FieldRenderer text-branch tooltip button uses aria-label instead of sr-only span"
artifacts:
- path: "src/components/ui/TextFieldMD3.tsx"
provides: "MD3 outlined text field with CSS floating label"
exports: ["TextFieldMD3"]
- path: "src/components/ui/TextFieldMD3.test.tsx"
provides: "Unit tests for floating label behavior"
min_lines: 30
- path: "src/styles/md3-buttons.ts"
provides: "MD3 button class constants"
exports: ["MD3_BTN_FILLED", "MD3_BTN_OUTLINED", "MD3_BTN_TEXT"]
- path: "src/components/ui/FieldRenderer.test.tsx"
provides: "Tests for DEBT-01 aria-label consistency"
min_lines: 20
key_links:
- from: "src/components/ui/TextFieldMD3.tsx"
to: "react-hook-form"
via: "registration prop spread onto input"
pattern: "\\.\\.\\.(registration|register)"
---
<objective>
Create MD3 UI primitives: TextFieldMD3 component with CSS-only floating label, MD3 button class constants, and fix FieldRenderer DEBT-01 aria-label inconsistency.
Purpose: Establish the reusable building blocks that Plan 03 will wire into all wizard steps. TextFieldMD3 is the most complex new component and needs its own test coverage before integration.
Output: TextFieldMD3.tsx + tests, md3-buttons.ts constants, FieldRenderer aria-label fix + tests.
</objective>
<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>
<context>
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-md3-components/09-RESEARCH.md
@src/index.css
@src/components/ui/FieldRenderer.tsx
@src/components/ui/PasswordField.tsx
<interfaces>
<!-- FieldRenderer and PasswordField both use react-hook-form registration -->
From src/components/ui/FieldRenderer.tsx:
```typescript
interface FieldRendererProps {
field: FieldDef;
register: UseFormRegister<any>;
error?: FieldError;
}
```
From src/components/ui/PasswordField.tsx:
```typescript
interface PasswordFieldProps {
id: string;
label: string;
error?: FieldError;
registration: UseFormRegisterReturn;
placeholder?: string;
helpText?: string;
tooltipText?: string;
}
```
From src/schemas/registry.ts (FieldDef shape):
```typescript
interface FieldDef {
key: string;
label: string;
required?: boolean;
inputType?: 'text' | 'password' | 'select';
options?: { value: string; label: string }[];
placeholder?: string;
helpText?: string;
tooltipText?: string;
}
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Create TextFieldMD3 component with floating label + MD3 button constants</name>
<files>src/components/ui/TextFieldMD3.tsx, src/components/ui/TextFieldMD3.test.tsx, src/styles/md3-buttons.ts</files>
<behavior>
- Test 1: TextFieldMD3 renders an input with the given id and a label with matching htmlFor
- Test 2: screen.getByLabelText(label) finds the input (htmlFor/id pairing works)
- Test 3: Input receives registration props (can be queried after typing)
- Test 4: Error message renders with role="alert" when error prop is passed
- Test 5: Help text renders when helpText prop is passed and no error
- Test 6: Required asterisk renders when required=true
- Test 7: Suffix slot renders (for PasswordField show/hide button)
- Test 8: Input has placeholder=" " for CSS floating label trick
</behavior>
<action>
Create `src/components/ui/TextFieldMD3.tsx` implementing the CSS-only floating label pattern from the research doc (Pattern 1). Key details:
- Interface: `{ id, label, error?, registration, type?, helpText?, required?, suffix? }` where registration is `UseFormRegisterReturn`
- Input gets `placeholder=" "` for `:placeholder-shown` CSS detection
- Label is absolutely positioned, uses Tailwind `peer` utilities to float on focus and when input has value
- Use `peer-[:not(:placeholder-shown)]:` arbitrary variant for has-value state (Tailwind v4 supports this)
- All colors use semantic tokens: `border-outline`, `focus:border-primary`, `text-on-surface-container/60`, `text-error`
- Error state: `border-error` + error message with `role="alert"`
- Suffix slot: absolutely positioned right side (for password show/hide toggle)
- Spread `{...registration}` onto the input element
Create `src/styles/md3-buttons.ts` exporting three constants:
- `MD3_BTN_FILLED`: `px-6 py-2.5 rounded-full bg-primary text-on-primary text-sm font-medium hover:opacity-90 active:opacity-80 transition-opacity disabled:opacity-40 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50`
- `MD3_BTN_OUTLINED`: `px-6 py-2.5 rounded-full border border-outline text-on-surface text-sm font-medium hover:bg-primary/8 active:bg-primary/12 transition-colors disabled:opacity-40 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50`
- `MD3_BTN_TEXT`: `px-4 py-2.5 rounded-full text-primary text-sm font-medium hover:bg-primary/8 active:bg-primary/12 transition-colors disabled:opacity-40 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50`
Write tests FIRST (RED), then implement (GREEN).
</action>
<verify>
<automated>npx vitest run src/components/ui/TextFieldMD3.test.tsx --reporter=dot</automated>
</verify>
<done>TextFieldMD3 component renders floating label with proper htmlFor/id pairing, getByLabelText works, error/helpText/suffix/required all render correctly. MD3 button constants exported. All tests pass.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Fix DEBT-01 aria-label inconsistency in FieldRenderer + add tests</name>
<files>src/components/ui/FieldRenderer.tsx, src/components/ui/FieldRenderer.test.tsx</files>
<behavior>
- Test 1: Text-branch tooltip button has aria-label="More info about {field.label}" (not sr-only span)
- Test 2: Select-branch tooltip button has aria-label="More info about {field.label}"
- Test 3: Both branches' tooltip buttons are findable via getByRole('button', { name: /more info about/i })
</behavior>
<action>
Create `src/components/ui/FieldRenderer.test.tsx` with tests for DEBT-01 aria-label consistency. Tests render FieldRenderer with a mock field that has tooltipText, and verify the tooltip button uses `aria-label` attribute (not sr-only span) for both text-branch and select-branch.
Then fix FieldRenderer.tsx text-branch (lines 91-99): Replace the `<span className="sr-only">...</span><span aria-hidden="true">...</span>` pattern with a direct `aria-label={...}` on the button and plain `i` icon content, matching the select-branch pattern (lines 49-54).
The fix is exactly:
```tsx
// Before (text branch, ~line 91):
<button type="button" onClick={...} className="...">
<span className="sr-only">More info about {field.label}</span>
<span aria-hidden="true">ⓘ</span>
</button>
// After:
<button
type="button"
onClick={...}
aria-label={`More info about ${field.label}`}
className="..."
>
</button>
```
Also apply the same fix to PasswordField.tsx (lines 27-31) which has the identical sr-only pattern.
Run full test suite after to confirm no regressions.
</action>
<verify>
<automated>npx vitest run src/components/ui/FieldRenderer.test.tsx --reporter=dot && npx vitest run --reporter=dot</automated>
</verify>
<done>Both text-branch and select-branch tooltip buttons use aria-label attribute consistently. PasswordField tooltip also fixed. All 166+ existing tests still pass. DEBT-01 resolved.</done>
</task>
</tasks>
<verification>
- `npx vitest run --reporter=dot` — all existing tests pass (no regressions)
- `npx vitest run src/components/ui/TextFieldMD3.test.tsx` — new component tests pass
- `npx vitest run src/components/ui/FieldRenderer.test.tsx` — DEBT-01 tests pass
- `src/styles/md3-buttons.ts` exports MD3_BTN_FILLED, MD3_BTN_OUTLINED, MD3_BTN_TEXT
</verification>
<success_criteria>
- TextFieldMD3 component exists with floating label, proper htmlFor/id, and all tests passing
- MD3 button constants exported from src/styles/md3-buttons.ts
- FieldRenderer and PasswordField tooltip buttons use consistent aria-label (DEBT-01 complete)
- Zero test regressions across the full suite
</success_criteria>
<output>
After completion, create `.planning/phases/09-md3-components/09-01-SUMMARY.md`
</output>
@@ -0,0 +1,73 @@
---
phase: 09-md3-components
plan: "01"
subsystem: ui-components
tags: [md3, floating-label, aria, accessibility, debt]
dependency_graph:
requires: []
provides: [TextFieldMD3, MD3_BTN_FILLED, MD3_BTN_OUTLINED, MD3_BTN_TEXT, FieldRenderer-aria-fix]
affects: [FieldRenderer, PasswordField, RemoteConfigStep.test]
tech_stack:
added: []
patterns: [css-peer-floating-label, aria-label-direct, md3-button-constants]
key_files:
created:
- src/components/ui/TextFieldMD3.tsx
- src/components/ui/TextFieldMD3.test.tsx
- src/styles/md3-buttons.ts
- src/components/ui/FieldRenderer.test.tsx
modified:
- src/components/ui/FieldRenderer.tsx
- src/components/ui/PasswordField.tsx
- src/components/wizard/RemoteConfigStep.test.tsx
decisions:
- "Used peer-[:not(:placeholder-shown)] arbitrary variant for has-value floating label detection (Tailwind v4 supports bracket syntax)"
- "TextFieldMD3 uses placeholder=' ' (space) to drive CSS-only floating label, not JS state"
- "Updated RemoteConfigStep.test.tsx getByLabelText to add { selector: 'input' } to prevent matching tooltip button aria-labels (Rule 1 regression fix)"
metrics:
duration_seconds: 381
completed_date: "2026-04-01"
tasks_completed: 2
tasks_total: 2
files_created: 4
files_modified: 3
---
# Phase 9 Plan 01: MD3 UI Primitives Summary
**One-liner:** CSS-only floating label TextFieldMD3 component + MD3 button class constants + DEBT-01 aria-label consistency fix across FieldRenderer, PasswordField, and tooltip buttons.
## Tasks Completed
| Task | Name | Commit | Status |
|------|------|--------|--------|
| 1 | Create TextFieldMD3 + MD3 button constants | 9441dcd | Complete |
| 2 | Fix DEBT-01 aria-label in FieldRenderer + tests | 532a994 | Complete |
## Verification
- `npx vitest run src/components/ui/TextFieldMD3.test.tsx` — 9/9 tests pass
- `npx vitest run src/components/ui/FieldRenderer.test.tsx` — 4/4 tests pass
- `npx vitest run --reporter=dot` — 179/179 tests pass (zero regressions)
- `src/styles/md3-buttons.ts` exports `MD3_BTN_FILLED`, `MD3_BTN_OUTLINED`, `MD3_BTN_TEXT`
## Decisions Made
1. **Tailwind v4 `peer-[:not(:placeholder-shown)]` arbitrary variant:** Used the bracket syntax form directly — verified it passes through the test suite correctly. No need for the `data-has-value` JS fallback.
2. **`placeholder=" "` (space) for floating label:** TextFieldMD3 always renders with `placeholder=" "` and `placeholder-transparent` CSS. This is the CSS-only detection mechanism for the `:not(:placeholder-shown)` pseudo-class — no React state needed.
3. **`{ selector: 'input' }` added to RemoteConfigStep.test.tsx:** Once tooltip buttons received `aria-label="More info about {field.label}"`, the `getByLabelText(/sas url/i)` query matched both the input (via its `<label>` association) and the tooltip button (via its `aria-label`). Fixed by scoping the query to `{ selector: 'input' }` in 6 call sites. This is correct behavior — the tests were too broad before.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] Test regression from DEBT-01 aria-label fix**
- **Found during:** Task 2, after fixing FieldRenderer.tsx
- **Issue:** `getByLabelText(/sas url/i)` in RemoteConfigStep.test.tsx started matching both the PasswordField input (via label association) AND the new tooltip button `aria-label="More info about SAS URL"`, causing "Found multiple elements" errors in 9 tests.
- **Fix:** Added `{ selector: 'input' }` option to 6 `getByLabelText` call sites in RemoteConfigStep.test.tsx that queried fields with tooltip buttons. This scopes the accessible name lookup to input elements only.
- **Files modified:** `src/components/wizard/RemoteConfigStep.test.tsx`
- **Commit:** 532a994 (included in Task 2 commit)
## Self-Check: PASSED
@@ -0,0 +1,157 @@
---
phase: 09-md3-components
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- src/components/wizard/StepIndicator.tsx
- src/components/wizard/StepIndicator.test.tsx
autonomous: true
requirements: [COMP-04]
must_haves:
truths:
- "Step indicator shows numbered circles for each wizard step"
- "Completed steps display a checkmark and are clickable buttons"
- "Current step is visually highlighted with primary color"
- "Future steps appear muted"
- "Connector lines link steps — filled for completed, muted for future"
- "Clicking step 0 dispatches SET_REMOTE_PARAMS({}) then SET_STEP(0)"
artifacts:
- path: "src/components/wizard/StepIndicator.tsx"
provides: "Rebuilt MD3 step indicator with circles and connectors"
exports: ["StepIndicator"]
min_lines: 40
key_links:
- from: "src/components/wizard/StepIndicator.tsx"
to: "src/store/context.tsx"
via: "useWizard() hook for state.currentStep and dispatch"
pattern: "useWizard"
---
<objective>
Rebuild StepIndicator from text breadcrumbs to MD3 visual step indicator with numbered circles, connector lines, checkmarks on completed steps, and proper semantic tokens.
Purpose: Replace the current text-based breadcrumb (using inline styles and hardcoded colors) with an MD3-compliant visual stepper that is dark-mode aware and accessible.
Output: Rebuilt StepIndicator.tsx + updated tests.
</objective>
<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>
<context>
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-md3-components/09-RESEARCH.md
@src/components/wizard/StepIndicator.tsx
@src/components/wizard/StepIndicator.test.tsx
<interfaces>
From src/store/context.tsx:
```typescript
function useWizard(): { state: WizardState; dispatch: Dispatch<WizardAction> }
```
From src/store/types.ts:
```typescript
type WizardAction =
| { type: 'SET_STEP'; payload: number }
| { type: 'SET_REMOTE_PARAMS'; payload: Record<string, unknown> }
| ...
```
Current StepIndicator test selectors (MUST remain compatible):
- `screen.getByText(/Backend/)` — finds step label
- `screen.getAllByRole('button')` — finds completed step buttons
- `buttons.find(b => b.textContent?.includes('Backend'))` — locates specific step button
- `screen.findByText(/Deployment/)` — finds step label async
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Rebuild StepIndicator with MD3 circles and connectors</name>
<files>src/components/wizard/StepIndicator.tsx, src/components/wizard/StepIndicator.test.tsx</files>
<action>
Rebuild `src/components/wizard/StepIndicator.tsx` from the text breadcrumb to a visual MD3 step indicator. Keep the existing `handleStepClick` logic EXACTLY as-is (SET_REMOTE_PARAMS({}) before SET_STEP when going to step 0). Remove ALL inline `style={{}}` props.
Structure (from research Pattern 4):
- Wrap in `<nav aria-label="Wizard steps"><ol className="flex items-center w-full">...</ol></nav>`
- Each step is an `<li>` containing a circle + label + optional connector
- Completed steps: `<button>` with checkmark icon, `bg-primary text-on-primary`, `aria-label="Go to step N: Label"`
- Active step: `<span>` with number, `border-2 border-primary bg-primary/10 text-primary font-bold`
- Future steps: `<span>` with number, `border-2 border-outline text-on-surface-container/40`
- All circles: `w-8 h-8 rounded-full flex items-center justify-center text-sm`
- Labels below circles: `text-xs mt-1 text-center`
- Connector lines between steps: `flex-1 h-0.5 mx-2`, `bg-primary` if completed else `bg-outline`
- Connector is `aria-hidden="true"`
CRITICAL for test compatibility: Completed step buttons MUST include the label text in their content so `b.textContent?.includes('Backend')` still works. Structure the button as:
```tsx
<button ...>
<span aria-hidden="true">&#10003;</span>
<span className="sr-only">{label}</span>
</button>
```
AND render the label as a visible `<span>` adjacent to the button (inside the same flex container). The test checks `b.textContent?.includes('Backend')` on the button element itself, so the label text must be INSIDE the button. Use:
```tsx
<button ... aria-label={`Go to step ${i + 1}: ${label}`}>
&#10003; {label}
</button>
```
This way `textContent` includes "Backend" and `aria-label` provides accessible name. The visual label below the circle can be separate.
Actually, looking at the test more carefully: the label text appears as a visible `<span>` below the circle in the new design. But the button `textContent` needs to include the label for the test. Solution: render the label text INSIDE the button element so textContent includes it, and use CSS to visually position it below the circle icon.
Simplest approach that preserves tests: make the completed step button contain BOTH the checkmark and the label text:
```tsx
<button type="button" onClick={() => handleStepClick(i)}
className="flex flex-col items-center gap-1 shrink-0 group focus-visible:outline-none">
<span className="w-8 h-8 rounded-full bg-primary text-on-primary flex items-center justify-center text-sm font-medium group-focus-visible:ring-2 group-focus-visible:ring-primary/50">
&#10003;
</span>
<span className="text-xs text-primary">{label}</span>
</button>
```
This way `button.textContent` is `"✓Backend"` which includes "Backend".
For active and future steps (non-clickable), use a `<div>` wrapper with the circle `<span>` and label `<span>` inside.
Update `StepIndicator.test.tsx` only if needed. The existing 5 tests should pass with this structure since:
- `screen.getByText(/Backend/)` will match the label span
- `screen.getAllByRole('button')` finds completed step buttons
- `buttons.find(b => b.textContent?.includes('Backend'))` works because label is inside button
- Dispatch behavior is unchanged
Run the full test suite after rebuild to catch any regressions.
</action>
<verify>
<automated>npx vitest run src/components/wizard/StepIndicator.test.tsx --reporter=dot && npx vitest run --reporter=dot</automated>
</verify>
<done>StepIndicator renders numbered circles connected by lines, completed steps show checkmarks and are clickable, current step is highlighted, future steps are muted. All inline styles removed. All 5 existing WIZD-03 tests pass. No regressions in full suite.</done>
</task>
</tasks>
<verification>
- `npx vitest run src/components/wizard/StepIndicator.test.tsx` — all 5 WIZD-03 tests pass
- `npx vitest run --reporter=dot` — full suite green
- No inline `style={{}}` props remain in StepIndicator.tsx
- No hardcoded color values (`#999`, etc.) remain
</verification>
<success_criteria>
- StepIndicator uses MD3 circles with numbered/checkmark states
- Connector lines between steps show progress visually
- All semantic tokens used (no hardcoded colors)
- All existing tests pass without modification (or with minimal test selector updates if button textContent changed)
</success_criteria>
<output>
After completion, create `.planning/phases/09-md3-components/09-02-SUMMARY.md`
</output>
@@ -0,0 +1,106 @@
---
phase: 09-md3-components
plan: "02"
subsystem: ui
tags: [react, tailwind, md3, wizard, step-indicator, accessibility]
# Dependency graph
requires:
- phase: 08-theme-foundation
provides: MD3 semantic token classes (bg-primary, text-on-primary, border-outline, etc.)
provides:
- MD3 visual step indicator with numbered circles, checkmark completed states, connector lines
affects: [wizard layout, dark-mode rendering, any plan touching StepIndicator or wizard navigation]
# Tech tracking
tech-stack:
added: []
patterns:
- "MD3 stepper pattern: circles + flex-1 connector lines inside ol/li structure"
- "Test-compatible button pattern: label text inside button so textContent includes step name"
- "flex flex-col button wrapping both icon and label keeps button.textContent matchable by tests"
key-files:
created: []
modified:
- src/components/wizard/StepIndicator.tsx
key-decisions:
- "Label text inside button element (not adjacent) so textContent includes step name for test selectors"
- "Connector div uses mb-4 to visually align with circle midline"
- "Pre-existing TextFieldMD3.test.tsx failure is out-of-scope (component not yet built — belongs to a future plan)"
patterns-established:
- "MD3 step indicator: ol > li with circle span + connector div between steps"
- "Completed step: button with flex-col containing icon span + label span"
- "Active step: non-interactive div with border-primary ring"
- "Future step: non-interactive div with border-outline muted text"
requirements-completed: [COMP-04]
# Metrics
duration: 6min
completed: 2026-04-01
---
# Phase 9 Plan 02: StepIndicator MD3 Rebuild Summary
**Text breadcrumbs replaced with MD3 visual stepper: numbered circles, checkmark completed steps, connector lines, semantic color tokens — all 5 WIZD-03 tests pass without modification**
## Performance
- **Duration:** ~6 min
- **Started:** 2026-04-01T07:18:12Z
- **Completed:** 2026-04-01T07:24:00Z
- **Tasks:** 1
- **Files modified:** 1
## Accomplishments
- StepIndicator rebuilt from text breadcrumbs to MD3 circles with `w-8 h-8 rounded-full` sizing
- Completed steps render as clickable `<button>` with checkmark circle + label; `textContent` includes step name so test selectors work unchanged
- Active step uses `border-primary bg-primary/10 text-primary font-bold` ring styling
- Future steps muted with `border-outline text-on-surface-container/40`
- Connector lines between steps: `bg-primary` for completed segments, `bg-outline` for future
- All inline `style={{}}` props removed; no hardcoded color values (`#999`, etc.)
- `handleStepClick` logic unchanged: dispatches `SET_REMOTE_PARAMS({})` before `SET_STEP(0)` when navigating back to step 0
## Task Commits
Each task was committed atomically:
1. **Task 1: Rebuild StepIndicator with MD3 circles and connectors** - `b704844` (feat)
**Plan metadata:** (docs commit — see below)
## Files Created/Modified
- `src/components/wizard/StepIndicator.tsx` - Rebuilt MD3 visual step indicator; 55 lines
## Decisions Made
- **Label inside button:** The plan's CRITICAL note specified label text must be inside the `<button>` element so `button.textContent?.includes('Backend')` works. Used `flex-col` button layout with icon span + label span both inside the button.
- **Connector vertical alignment:** Added `mb-4` to connector `div` to visually align the `h-0.5` line with the circle midpoints given the flex-col label below.
- **Pre-existing test failure scoped out:** `TextFieldMD3.test.tsx` fails with "Cannot find module './TextFieldMD3'" — this is a test file for a component not yet built (future plan). Not caused by this change. Logged to deferred-items.
## Deviations from Plan
None — plan executed exactly as written.
## Issues Encountered
- `TextFieldMD3.test.tsx` was already failing in the full test suite before this change (missing component file). Confirmed out-of-scope — not introduced by StepIndicator rebuild.
## User Setup Required
None — no external service configuration required.
## Next Phase Readiness
- StepIndicator is fully MD3-compliant and dark-mode aware via semantic tokens
- Ready for Phase 9 Plan 03 (TextFieldMD3 component build, which will also fix the pre-existing test failure)
- 131 test selector fragility concern remains (ongoing, per STATE.md blocker) — this plan's approach of keeping label text inside button elements is the established pattern to follow
---
*Phase: 09-md3-components*
*Completed: 2026-04-01*
@@ -0,0 +1,272 @@
---
phase: 09-md3-components
plan: 03
type: execute
wave: 2
depends_on: ["09-01"]
files_modified:
- src/components/ui/FieldRenderer.tsx
- src/components/ui/PasswordField.tsx
- src/components/ui/BackendCard.tsx
- src/components/wizard/OutputBlock.tsx
- src/components/wizard/BackendSelectionStep.tsx
- src/components/wizard/RemoteConfigStep.tsx
- src/components/wizard/DeploymentStep.tsx
- src/components/wizard/ReviewStep.tsx
- src/index.css
autonomous: false
requirements: [COMP-01, COMP-02, COMP-03]
must_haves:
truths:
- "All text inputs in the wizard render with floating labels that animate on focus and when the field has content"
- "Next and Download All buttons use MD3 filled style (rounded-full, bg-primary)"
- "Back, Copy, and Download buttons use MD3 outlined style (rounded-full, border)"
- "BackendCard uses rounded-xl shape and shadow elevation"
- "OutputBlock pre blocks use rounded-xl and subtle shadow"
- "All existing tests pass with no regressions"
artifacts:
- path: "src/components/ui/FieldRenderer.tsx"
provides: "Text-branch using TextFieldMD3 component"
contains: "TextFieldMD3"
- path: "src/components/ui/PasswordField.tsx"
provides: "Password field using TextFieldMD3 layout"
contains: "TextFieldMD3"
- path: "src/components/ui/BackendCard.tsx"
provides: "MD3 elevation and shape"
contains: "rounded-xl"
- path: "src/components/wizard/BackendSelectionStep.tsx"
provides: "MD3 filled button for Next"
contains: "MD3_BTN_FILLED"
key_links:
- from: "src/components/ui/FieldRenderer.tsx"
to: "src/components/ui/TextFieldMD3.tsx"
via: "import and render TextFieldMD3"
pattern: "import.*TextFieldMD3"
- from: "src/components/wizard/BackendSelectionStep.tsx"
to: "src/styles/md3-buttons.ts"
via: "import button constants"
pattern: "import.*MD3_BTN"
- from: "src/components/wizard/ReviewStep.tsx"
to: "src/styles/md3-buttons.ts"
via: "import button constants"
pattern: "import.*MD3_BTN"
---
<objective>
Wire TextFieldMD3 into FieldRenderer and PasswordField, apply MD3 button styles across all wizard steps, and add MD3 elevation to BackendCard and OutputBlock.
Purpose: This is the integration plan that connects the primitives from Plan 01 into the actual wizard UI, completing COMP-01 (floating labels everywhere), COMP-02 (button hierarchy everywhere), and COMP-03 (elevation on cards and code blocks).
Output: All wizard components updated with MD3 styling.
</objective>
<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>
<context>
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-md3-components/09-RESEARCH.md
@.planning/phases/09-md3-components/09-01-SUMMARY.md
@src/components/ui/FieldRenderer.tsx
@src/components/ui/PasswordField.tsx
@src/components/ui/BackendCard.tsx
@src/components/wizard/OutputBlock.tsx
@src/components/wizard/BackendSelectionStep.tsx
@src/components/wizard/RemoteConfigStep.tsx
@src/components/wizard/DeploymentStep.tsx
@src/components/wizard/ReviewStep.tsx
<interfaces>
<!-- TextFieldMD3 and button constants created in Plan 01 -->
From src/components/ui/TextFieldMD3.tsx (created in 09-01):
```typescript
interface TextFieldMD3Props {
id: string;
label: string;
error?: FieldError;
registration: UseFormRegisterReturn;
type?: 'text' | 'password';
helpText?: string;
required?: boolean;
suffix?: React.ReactNode;
}
export function TextFieldMD3(props: TextFieldMD3Props): JSX.Element;
```
From src/styles/md3-buttons.ts (created in 09-01):
```typescript
export const MD3_BTN_FILLED: string; // filled primary action
export const MD3_BTN_OUTLINED: string; // outlined secondary action
export const MD3_BTN_TEXT: string; // text tertiary action
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Integrate TextFieldMD3 into FieldRenderer and PasswordField</name>
<files>src/components/ui/FieldRenderer.tsx, src/components/ui/PasswordField.tsx</files>
<action>
**FieldRenderer.tsx — text branch (default, lines 82-119):**
Replace the entire text-branch return block with a `<TextFieldMD3>` render. Import TextFieldMD3 at top. The text branch currently renders label + input + helpText + error manually. Replace with:
```tsx
return (
<div className="flex flex-col gap-1">
{field.tooltipText && /* keep tooltip button + tooltip display exactly as-is */}
<TextFieldMD3
id={field.key}
label={field.label}
error={error}
registration={register(field.key)}
helpText={field.helpText}
required={field.required}
/>
</div>
);
```
CRITICAL: The tooltip button (lines 88-99) must remain OUTSIDE TextFieldMD3, rendered above it. TextFieldMD3 handles the label, input, helpText, and error display. The tooltip is a separate concern that stays in FieldRenderer.
Actually, re-examining the layout: the current text branch has tooltip button inline with label. Since TextFieldMD3 includes its own label (floating), the tooltip must be rendered separately. Place the tooltip button + tooltip content ABOVE the TextFieldMD3 component in the flex column. This preserves existing tooltip behavior.
Keep the select branch (lines 40-79) UNCHANGED — select fields do NOT get floating labels.
**PasswordField.tsx:**
Replace the manual label + input layout with TextFieldMD3, passing the show/hide toggle as `suffix`:
```tsx
import { TextFieldMD3 } from './TextFieldMD3';
export function PasswordField({ id, label, error, registration, helpText, tooltipText }: PasswordFieldProps) {
const [show, setShow] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const toggleButton = (
<button type="button" onClick={() => setShow(v => !v)}
aria-label={show ? 'Hide' : 'Show'}
className="text-on-surface-container/50 hover:text-on-surface-container text-sm">
{show ? 'Hide' : 'Show'}
</button>
);
return (
<div className="flex flex-col gap-1">
{tooltipText && (
<button type="button" onClick={() => setShowTooltip(v => !v)}
aria-label={`More info about ${label}`}
className="text-primary hover:text-primary text-xs leading-none self-start">
</button>
)}
{tooltipText && showTooltip && (
<p className="text-xs text-primary bg-primary/10 border border-primary/30 rounded px-2 py-1.5">
{tooltipText}
</p>
)}
<TextFieldMD3
id={id}
label={label}
error={error}
registration={registration}
type={show ? 'text' : 'password'}
helpText={helpText}
suffix={toggleButton}
/>
</div>
);
}
```
Remove the `placeholder` prop from PasswordFieldProps interface since TextFieldMD3 uses `placeholder=" "` internally.
Run full test suite after — ALL `getByLabelText` queries must still work because TextFieldMD3 preserves htmlFor/id pairing.
</action>
<verify>
<automated>npx vitest run --reporter=dot</automated>
</verify>
<done>FieldRenderer text-branch renders TextFieldMD3 with floating label. PasswordField uses TextFieldMD3 with suffix for show/hide toggle. All existing tests pass (getByLabelText queries work). No regressions.</done>
</task>
<task type="auto">
<name>Task 2: Apply MD3 button styles and elevation across all wizard steps</name>
<files>src/components/wizard/BackendSelectionStep.tsx, src/components/wizard/RemoteConfigStep.tsx, src/components/wizard/DeploymentStep.tsx, src/components/wizard/ReviewStep.tsx, src/components/wizard/OutputBlock.tsx, src/components/ui/BackendCard.tsx, src/index.css</files>
<action>
**Button styling (COMP-02):** Import `{ MD3_BTN_FILLED, MD3_BTN_OUTLINED }` from `../../styles/md3-buttons` in each wizard step file. Apply className replacements:
BackendSelectionStep.tsx: "Next" button gets `className={MD3_BTN_FILLED}`. No Back button here.
RemoteConfigStep.tsx: "Back" button gets `className={MD3_BTN_OUTLINED}`, "Next" (submit) button gets `className={MD3_BTN_FILLED}`.
DeploymentStep.tsx: "Back" button gets `className={MD3_BTN_OUTLINED}`, "Next" button gets `className={MD3_BTN_FILLED}`.
ReviewStep.tsx: "Back" button gets `className={MD3_BTN_OUTLINED}`, "Download All (ZIP)" button gets `className={MD3_BTN_FILLED}`.
OutputBlock.tsx: "Copy" button gets `className={MD3_BTN_OUTLINED + ' text-xs !px-3 !py-1'}` (keep smaller size). "Download" button gets same. Import from `../../styles/md3-buttons`. Note: OutputBlock buttons are small utility buttons — keep `text-xs` and reduce padding with overrides. Alternatively, create a size-reduced variant inline: `className={\`\${MD3_BTN_OUTLINED} !text-xs !px-3 !py-1.5\`}`.
CRITICAL: Do NOT change button text content — only className. Tests use `getByRole('button', { name: /next/i })` etc.
**BackendCard elevation (COMP-03):**
Update BackendCard.tsx className:
- Change `rounded-lg` to `rounded-xl` (MD3 medium shape, 12px)
- Add `shadow hover:shadow-md` to unselected state
- Selected state: add `shadow-md`
- Add `transition-all` (already present — keep)
**OutputBlock elevation (COMP-03):**
Update OutputBlock.tsx `<pre>` className:
- Change `rounded` to `rounded-xl`
- Add `shadow-sm`
**Optional: Add elevation shadow token to index.css:**
If standard Tailwind `shadow` is insufficient, add to `@theme` block:
```css
--shadow-elevation-1: 0 1px 4px 0 rgb(0 0 0 / 0.37);
```
Only add this if the default `shadow` utility doesn't provide enough visual lift. Use your judgment.
Run full test suite after all changes.
</action>
<verify>
<automated>npx vitest run --reporter=dot</automated>
</verify>
<done>All Next/Download buttons use MD3 filled (rounded-full, bg-primary). All Back/Copy buttons use MD3 outlined (rounded-full, border). BackendCard has rounded-xl and shadow. OutputBlock pre has rounded-xl and shadow-sm. All tests pass.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Visual verification of complete MD3 component set</name>
<what-built>Complete MD3 component migration: floating label text fields, MD3 button hierarchy (filled/outlined), card elevation, and rebuilt step indicator</what-built>
<how-to-verify>
1. Run `npm run dev` and open http://localhost:5173
2. **Step Indicator:** Verify numbered circles with connector lines at top. Step 1 should be highlighted, future steps muted.
3. **Backend Selection:** Click a backend card — verify rounded corners (rounded-xl) and shadow elevation. Verify "Next" button is pill-shaped (rounded-full) with primary fill color.
4. **Remote Config:** Verify text inputs have floating labels that animate up on focus and stay floated when field has value. Verify "Back" button is pill-shaped outlined, "Next" is pill-shaped filled.
5. **Password fields:** Verify floating label works with show/hide toggle button visible on right side.
6. **Deployment Step:** Verify Back/Next button styles match MD3 hierarchy.
7. **Review Step:** Verify "Back" is outlined, "Download All" is filled. Verify OutputBlock code areas have rounded corners and subtle shadow. Verify Copy/Download buttons are outlined style.
8. **Navigate back** to step 1 — verify step indicator shows checkmark on completed step, and the step is clickable.
9. **Toggle dark mode** — verify all new components look correct in dark theme (no hardcoded colors, shadows work).
</how-to-verify>
<resume-signal>Type "approved" or describe issues</resume-signal>
</task>
</tasks>
<verification>
- `npx vitest run` — full suite passes (166+ tests, zero regressions)
- All text inputs render floating labels
- Buttons follow MD3 filled/outlined/text hierarchy
- Cards and code blocks have MD3 elevation
- Dark mode renders correctly
</verification>
<success_criteria>
- COMP-01: All text inputs (FieldRenderer text-branch + PasswordField) render as MD3 outlined fields with floating labels
- COMP-02: Buttons across all wizard steps follow filled/outlined/text hierarchy
- COMP-03: BackendCard has rounded-xl + shadow, OutputBlock pre has rounded-xl + shadow-sm
- Zero test regressions
- User visually approves the complete MD3 component set
</success_criteria>
<output>
After completion, create `.planning/phases/09-md3-components/09-03-SUMMARY.md`
</output>
@@ -0,0 +1,116 @@
---
phase: 09-md3-components
plan: "03"
subsystem: ui-components
tags: [md3, floating-label, buttons, elevation, wizard, react]
# Dependency graph
requires:
- phase: 09-01
provides: [TextFieldMD3, MD3_BTN_FILLED, MD3_BTN_OUTLINED, MD3_BTN_TEXT]
provides:
- TextFieldMD3 wired into FieldRenderer (text-branch) and PasswordField
- MD3 button hierarchy applied across all wizard steps
- MD3 elevation and shape on BackendCard and OutputBlock
affects: [FieldRenderer, PasswordField, BackendSelectionStep, RemoteConfigStep, DeploymentStep, ReviewStep, OutputBlock, BackendCard]
# Tech tracking
tech-stack:
added: []
patterns: [md3-button-constants-import, textfield-md3-composition, show-hide-suffix-pattern]
key-files:
created: []
modified:
- src/components/ui/FieldRenderer.tsx
- src/components/ui/PasswordField.tsx
- src/components/ui/BackendCard.tsx
- src/components/wizard/OutputBlock.tsx
- src/components/wizard/BackendSelectionStep.tsx
- src/components/wizard/RemoteConfigStep.tsx
- src/components/wizard/DeploymentStep.tsx
- src/components/wizard/ReviewStep.tsx
key-decisions:
- "TextFieldMD3 composition: tooltip button rendered ABOVE TextFieldMD3 in FieldRenderer (tooltip is a separate concern from the field itself)"
- "PasswordField show/hide toggle passed as suffix prop to TextFieldMD3 — suffix slot is right-aligned inside the input container"
- "OutputBlock Copy/Download buttons use MD3_BTN_OUTLINED with !text-xs !px-3 !py-1.5 overrides to preserve small utility button size"
patterns-established:
- "Tooltip button pattern: tooltip UI rendered above TextFieldMD3 component in a flex-col wrapper — keeps TextFieldMD3 self-contained"
- "Button hierarchy: MD3_BTN_FILLED for primary forward actions (Next, Download All), MD3_BTN_OUTLINED for secondary/back actions (Back, Copy, Download)"
- "MD3 elevation pattern: rounded-xl + shadow on cards, rounded-xl + shadow-sm on code blocks"
requirements-completed: [COMP-01, COMP-02, COMP-03]
# Metrics
duration: ~25min
completed: "2026-04-01"
---
# Phase 9 Plan 03: MD3 Component Integration Summary
**TextFieldMD3 wired into all wizard text inputs with floating labels, MD3 filled/outlined button hierarchy applied across all 4 wizard steps, and MD3 elevation (rounded-xl + shadow) added to BackendCard and OutputBlock.**
## Performance
- **Duration:** ~25 min
- **Started:** 2026-04-01
- **Completed:** 2026-04-01
- **Tasks:** 3 (2 auto + 1 human-verify checkpoint)
- **Files modified:** 8
## Accomplishments
- All text inputs in the wizard now render MD3 floating labels via TextFieldMD3 — labels animate on focus and stay floated when a value is present
- All primary action buttons (Next, Download All) use MD3 filled pill style; all secondary actions (Back, Copy, Download) use MD3 outlined pill style
- BackendCard uses rounded-xl shape with shadow elevation; OutputBlock pre blocks use rounded-xl with shadow-sm
## Task Commits
Each task was committed atomically:
1. **Task 1: Integrate TextFieldMD3 into FieldRenderer and PasswordField** - `e560a6a` (feat)
2. **Task 2: Apply MD3 button styles and elevation across all wizard steps** - `d7a2cad` (feat)
3. **Task 3: Visual verification of complete MD3 component set** - checkpoint:human-verify — approved
## Files Created/Modified
- `src/components/ui/FieldRenderer.tsx` - Text-branch replaced with TextFieldMD3; tooltip rendered above as separate element
- `src/components/ui/PasswordField.tsx` - Rebuilt using TextFieldMD3 with show/hide toggle passed as suffix prop
- `src/components/ui/BackendCard.tsx` - rounded-xl shape + shadow hover:shadow-md elevation added
- `src/components/wizard/OutputBlock.tsx` - Pre block rounded-xl + shadow-sm; Copy/Download buttons use MD3_BTN_OUTLINED
- `src/components/wizard/BackendSelectionStep.tsx` - Next button uses MD3_BTN_FILLED
- `src/components/wizard/RemoteConfigStep.tsx` - Back uses MD3_BTN_OUTLINED, Next uses MD3_BTN_FILLED
- `src/components/wizard/DeploymentStep.tsx` - Back uses MD3_BTN_OUTLINED, Next uses MD3_BTN_FILLED
- `src/components/wizard/ReviewStep.tsx` - Back uses MD3_BTN_OUTLINED, Download All uses MD3_BTN_FILLED
## Decisions Made
1. **Tooltip position in FieldRenderer:** The tooltip button is rendered above the TextFieldMD3 component inside a flex-col wrapper. TextFieldMD3 is self-contained (manages its own label, input, helpText, error) so the tooltip is a separate concern that lives outside it.
2. **PasswordField suffix pattern:** The show/hide toggle is passed as the `suffix` prop to TextFieldMD3, which renders it inside the input container on the right. This keeps TextFieldMD3's layout contract intact while adding the toggle.
3. **OutputBlock button size overrides:** OutputBlock Copy/Download are small utility buttons. MD3_BTN_OUTLINED base classes are overridden with `!text-xs !px-3 !py-1.5` to preserve compact sizing without creating a new button variant.
## Deviations from Plan
None - plan executed exactly as written.
## Issues Encountered
None — all 179+ tests passed after both task commits with zero regressions. User visually verified the complete MD3 component set and approved.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- COMP-01 (floating labels), COMP-02 (button hierarchy), COMP-03 (elevation) all satisfied
- Phase 9 MD3 component migration complete: primitives (09-01), StepIndicator (09-02), integration (09-03)
- Ready for Phase 10 (Content & Copy — UX text, help content, empty states)
---
*Phase: 09-md3-components*
*Completed: 2026-04-01*
@@ -0,0 +1,149 @@
---
phase: 09-md3-components
plan: 04
type: execute
wave: 1
depends_on: []
files_modified:
- src/components/wizard/BackendSelectionStep.tsx
autonomous: true
gap_closure: true
requirements:
- COMP-01
- COMP-02
- COMP-03
- COMP-04
- DEBT-01
must_haves:
truths:
- "BackendSelectionStep 'Remote name' input renders as a TextFieldMD3 with floating label"
- "Existing BackendSelectionStep tests pass without modification"
- "Full test suite remains green (179+ tests)"
artifacts:
- path: "src/components/wizard/BackendSelectionStep.tsx"
provides: "TextFieldMD3 integration for Remote name field"
contains: "TextFieldMD3"
key_links:
- from: "src/components/wizard/BackendSelectionStep.tsx"
to: "src/components/ui/TextFieldMD3.tsx"
via: "import { TextFieldMD3 }"
pattern: "import.*TextFieldMD3.*from"
- from: "src/components/wizard/BackendSelectionStep.tsx"
to: "react-hook-form"
via: "register('name') passed as registration prop"
pattern: "registration=.*register"
---
<objective>
Close verification gap: BackendSelectionStep "Remote name" input is the only text input in the wizard that does not use TextFieldMD3 with a floating label. This violates COMP-01's "All text inputs" requirement.
Purpose: Achieve 12/12 must-have truths for Phase 9 verification (currently 11/12 partial).
Output: Updated BackendSelectionStep.tsx with TextFieldMD3 replacing the plain label+input.
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-md3-components/09-03-SUMMARY.md
<interfaces>
<!-- TextFieldMD3 component interface — the exact contract to use -->
From src/components/ui/TextFieldMD3.tsx:
```typescript
interface TextFieldMD3Props {
id: string;
label: string;
error?: FieldError;
registration: UseFormRegisterReturn;
type?: 'text' | 'password';
helpText?: string;
required?: boolean;
suffix?: React.ReactNode;
}
export function TextFieldMD3({ id, label, error, registration, type, helpText, required, suffix }: TextFieldMD3Props): JSX.Element;
```
From src/styles/md3-buttons.ts:
```typescript
export const MD3_BTN_FILLED: string;
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Replace plain Remote name input with TextFieldMD3</name>
<files>src/components/wizard/BackendSelectionStep.tsx</files>
<action>
In BackendSelectionStep.tsx, replace the plain label+input block (lines 57-63) with a TextFieldMD3 component:
1. Add import: `import { TextFieldMD3 } from '../ui/TextFieldMD3';`
2. Replace this block:
```tsx
<div>
<label htmlFor="remote-name">Remote name</label>
<input id="remote-name" type="text" {...register('name')} />
{errors.name && (
<p role="alert">{errors.name.message}</p>
)}
</div>
```
With:
```tsx
<TextFieldMD3
id="remote-name"
label="Remote name"
registration={register('name')}
error={errors.name}
required
/>
```
Key constraints:
- Keep `id="remote-name"` unchanged (matches existing test selectors via getByRole('textbox'))
- Pass `register('name')` as `registration` prop (same pattern as FieldRenderer, line 103 of FieldRenderer.tsx)
- Pass `errors.name` as `error` prop — TextFieldMD3 already renders `role="alert"` on error messages
- Remove the manual `{errors.name && ...}` block since TextFieldMD3 handles error display internally
- Add `required` prop since the name field is required (schema has `.min(1)`)
The existing tests use `screen.getByRole('textbox')` to find the input and `screen.getByRole('alert')` for error messages. TextFieldMD3 preserves both: the `<input>` retains its textbox role, and the error `<p>` has `role="alert"`.
</action>
<verify>
<automated>npx vitest run src/components/wizard/BackendSelectionStep.test.tsx --reporter=dot && npx vitest run --reporter=dot</automated>
</verify>
<done>BackendSelectionStep renders "Remote name" as a TextFieldMD3 with floating label. All 5 BackendSelectionStep tests pass. Full test suite (179+ tests) remains green. COMP-01 "All text inputs" requirement is fully satisfied.</done>
</task>
</tasks>
<verification>
1. `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` — all 7 tests pass (WIZD-01 + WIZD-04)
2. `npx vitest run --reporter=dot` — full suite green, zero regressions
3. Grep check: no remaining plain `<label>` + `<input>` patterns for text fields in wizard steps
`grep -n "<label.*htmlFor" src/components/wizard/BackendSelectionStep.tsx` — should return no results (label is now inside TextFieldMD3)
4. Import check: `grep "TextFieldMD3" src/components/wizard/BackendSelectionStep.tsx` — confirms import present
</verification>
<success_criteria>
- BackendSelectionStep "Remote name" field renders as TextFieldMD3 with floating label
- All BackendSelectionStep tests pass without any test modifications
- Full test suite passes with zero regressions
- Phase 9 VERIFICATION.md Truth #12 can be re-verified as VERIFIED (12/12)
</success_criteria>
<output>
After completion, create `.planning/phases/09-md3-components/09-04-SUMMARY.md`
</output>
@@ -0,0 +1,94 @@
---
phase: 09-md3-components
plan: "04"
subsystem: ui
tags: [react, tailwind, md3, textfield, floating-label, react-hook-form]
# Dependency graph
requires:
- phase: 09-md3-components/09-03
provides: TextFieldMD3 component with floating label, FieldRenderer integration
provides:
- BackendSelectionStep Remote name field migrated to TextFieldMD3
- COMP-01 "All text inputs" requirement fully satisfied (12/12 wizard must-have truths)
affects: [Phase 10, verification]
# Tech tracking
tech-stack:
added: []
patterns: [TextFieldMD3 registration prop pattern extended to BackendSelectionStep]
key-files:
created: []
modified:
- src/components/wizard/BackendSelectionStep.tsx
key-decisions:
- "BackendSelectionStep gap: plain label+input replaced with TextFieldMD3; no test changes required — getByRole('textbox') and role='alert' preserved by TextFieldMD3 internals"
patterns-established:
- "All wizard text inputs now use TextFieldMD3 with registration prop pattern"
requirements-completed: [COMP-01, COMP-02, COMP-03, COMP-04, DEBT-01]
# Metrics
duration: 5min
completed: 2026-04-01
---
# Phase 9 Plan 04: BackendSelectionStep TextFieldMD3 Gap Closure Summary
**TextFieldMD3 floating-label replaces BackendSelectionStep's plain Remote name input, closing the last COMP-01 gap and achieving 12/12 wizard must-have truths**
## Performance
- **Duration:** 5 min
- **Started:** 2026-04-01T10:26:00Z
- **Completed:** 2026-04-01T10:31:00Z
- **Tasks:** 1
- **Files modified:** 1
## Accomplishments
- Replaced `<label htmlFor="remote-name"> + <input> + <p role="alert">` block with `<TextFieldMD3>` in BackendSelectionStep
- Added `import { TextFieldMD3 } from '../ui/TextFieldMD3'` to the component
- All 10 BackendSelectionStep tests pass without modification (textbox role and alert role preserved by TextFieldMD3 internals)
- Full suite of 179 tests remains green, zero regressions
- COMP-01 "All text inputs use floating label" requirement fully satisfied across all wizard steps
## Task Commits
Each task was committed atomically:
1. **Task 1: Replace plain Remote name input with TextFieldMD3** - `123f24c` (feat)
**Plan metadata:** _(pending final docs commit)_
## Files Created/Modified
- `src/components/wizard/BackendSelectionStep.tsx` - TextFieldMD3 integration replacing plain label+input block
## Decisions Made
- No test changes needed: TextFieldMD3 renders `<input>` (preserving `getByRole('textbox')`) and `<p role="alert">` on error (preserving `getByRole('alert')`) — exact same DOM contract as the replaced plain elements
## Deviations from Plan
None - plan executed exactly as written.
## Issues Encountered
None.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- Phase 9 verification truth #12 can now be confirmed as VERIFIED (12/12 truths satisfied)
- COMP-01, COMP-02, COMP-03, COMP-04, DEBT-01 requirements fully closed
- Ready for Phase 10
---
*Phase: 09-md3-components*
*Completed: 2026-04-01*
## Self-Check: PASSED
- FOUND: src/components/wizard/BackendSelectionStep.tsx
- FOUND: .planning/phases/09-md3-components/09-04-SUMMARY.md
- FOUND: commit 123f24c
@@ -0,0 +1,307 @@
---
phase: 09-md3-components
plan: 05
type: execute
wave: 2
depends_on: ["09-04"]
files_modified:
- src/components/ui/TextFieldMD3.tsx
- src/components/ui/TextFieldMD3.test.tsx
- src/components/ui/FieldRenderer.tsx
- src/components/ui/FieldRenderer.test.tsx
- src/components/ui/PasswordField.tsx
autonomous: true
gap_closure: true
requirements: [COMP-04]
must_haves:
truths:
- "Hovering the tooltip info button reveals tooltip text without clicking"
- "Clicking the tooltip info button pins it open; clicking again dismisses it"
- "Tooltip icon appears beside the helpText below the field, not above the field"
- "Tooltip icon renders even when field has tooltipText but no helpText"
artifacts:
- path: "src/components/ui/TextFieldMD3.tsx"
provides: "helpTextPrefix prop for rendering inline content left of helpText"
contains: "helpTextPrefix"
- path: "src/components/ui/FieldRenderer.tsx"
provides: "Tooltip icon passed as helpTextPrefix, hover+click behavior"
contains: "onMouseEnter"
- path: "src/components/ui/PasswordField.tsx"
provides: "Same tooltip fix as FieldRenderer text-branch"
contains: "onMouseEnter"
key_links:
- from: "src/components/ui/FieldRenderer.tsx"
to: "src/components/ui/TextFieldMD3.tsx"
via: "helpTextPrefix prop"
pattern: "helpTextPrefix.*ⓘ"
- from: "src/components/ui/PasswordField.tsx"
to: "src/components/ui/TextFieldMD3.tsx"
via: "helpTextPrefix prop"
pattern: "helpTextPrefix.*ⓘ"
---
<objective>
Fix two UAT-reported tooltip issues: (1) tooltip doesn't show on hover, only on click; (2) tooltip icon is positioned above the field instead of beside the helpText below it.
Purpose: Close the last remaining UAT gap in Phase 9 (test 7: Tooltip Info Buttons Accessibility).
Output: Tooltip icon renders inline with helpText below the field, and tooltip text appears on hover (pinnable via click).
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-md3-components/09-03-SUMMARY.md
<interfaces>
<!-- TextFieldMD3 current props -->
From src/components/ui/TextFieldMD3.tsx:
```typescript
interface TextFieldMD3Props {
id: string;
label: string;
error?: FieldError;
registration: UseFormRegisterReturn;
type?: 'text' | 'password';
helpText?: string;
required?: boolean;
suffix?: React.ReactNode;
}
```
<!-- FieldRenderer current tooltip pattern (lines 84-108) — tooltip button ABOVE TextFieldMD3 -->
From src/components/ui/FieldRenderer.tsx:
```typescript
// text (default) — uses TextFieldMD3 for floating label
return (
<div className="flex flex-col gap-1">
{field.tooltipText && (
<button type="button" onClick={() => setShowTooltip(v => !v)}
aria-label={`More info about ${field.label}`}
className="text-primary hover:text-primary text-xs leading-none self-start">
</button>
)}
{field.tooltipText && showTooltip && (
<p className="text-xs text-primary bg-primary/10 border border-primary/30 rounded px-2 py-1.5">
{field.tooltipText}
</p>
)}
<TextFieldMD3 id={field.key} label={field.label} error={error}
registration={register(field.key)} helpText={field.helpText} required={field.required} />
</div>
);
```
<!-- PasswordField has identical tooltip pattern (lines 30-56) -->
<!-- TextFieldMD3 helpText rendering (lines 65-67) -->
```typescript
{helpText && !error && (
<p className="text-xs text-on-surface-container/70">{helpText}</p>
)}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add helpTextPrefix prop to TextFieldMD3 and fix tooltip placement + hover in FieldRenderer and PasswordField</name>
<files>src/components/ui/TextFieldMD3.tsx, src/components/ui/FieldRenderer.tsx, src/components/ui/PasswordField.tsx</files>
<action>
**TextFieldMD3.tsx — Add helpTextPrefix prop:**
1. Add `helpTextPrefix?: React.ReactNode` to `TextFieldMD3Props` interface.
2. Destructure `helpTextPrefix` in the component function.
3. Change the helpText rendering block (lines 65-67) from:
```tsx
{helpText && !error && (
<p className="text-xs text-on-surface-container/70">{helpText}</p>
)}
```
To:
```tsx
{(helpText || helpTextPrefix) && !error && (
<div className="flex items-start gap-1">
{helpTextPrefix}
{helpText && (
<p className="text-xs text-on-surface-container/70">{helpText}</p>
)}
</div>
)}
```
IMPORTANT: The outer guard MUST be `(helpText || helpTextPrefix) && !error` — not just `helpText && !error`. This ensures the tooltip icon (passed as helpTextPrefix) still renders when a field has `tooltipText` but no `helpText`. The inner `helpText &&` guard prevents an empty `<p>` tag when helpText is absent.
**FieldRenderer.tsx — Fix text-branch tooltip (lines 83-109):**
1. Remove the tooltip `<button>` from ABOVE `TextFieldMD3` (lines 85-93). Remove the tooltip content `<p>` from above as well (lines 95-98).
2. Add `onMouseEnter` / `onMouseLeave` state: add `const [hoverTooltip, setHoverTooltip] = useState(false)` alongside existing `showTooltip` state.
3. Create a tooltip icon element that will be passed as `helpTextPrefix`:
```tsx
const tooltipIcon = field.tooltipText ? (
<button
type="button"
onClick={() => setShowTooltip(v => !v)}
onMouseEnter={() => setHoverTooltip(true)}
onMouseLeave={() => setHoverTooltip(false)}
aria-label={`More info about ${field.label}`}
className="text-primary hover:text-primary/80 text-xs leading-none mt-px shrink-0"
>
</button>
) : undefined;
```
4. Compute tooltip visibility: `const tooltipVisible = showTooltip || hoverTooltip`.
5. Render the tooltip content BELOW TextFieldMD3 (after the component, inside the flex-col wrapper), conditioned on `tooltipVisible`:
```tsx
{field.tooltipText && tooltipVisible && (
<p className="text-xs text-primary bg-primary/10 border border-primary/30 rounded px-2 py-1.5">
{field.tooltipText}
</p>
)}
```
6. Pass `helpTextPrefix={tooltipIcon}` to TextFieldMD3.
7. The final text-branch return should look like:
```tsx
return (
<div className="flex flex-col gap-1">
<TextFieldMD3
id={field.key}
label={field.label}
error={error}
registration={register(field.key)}
helpText={field.helpText}
required={field.required}
helpTextPrefix={tooltipIcon}
/>
{field.tooltipText && tooltipVisible && (
<p className="text-xs text-primary bg-primary/10 border border-primary/30 rounded px-2 py-1.5">
{field.tooltipText}
</p>
)}
</div>
);
```
**FieldRenderer.tsx — Fix select-branch tooltip (lines 40-78):**
The select branch also has a tooltip button. Apply the same hover fix to the select branch:
1. Add `onMouseEnter={() => setHoverTooltip(true)}` and `onMouseLeave={() => setHoverTooltip(false)}` to the existing select-branch tooltip button (line 51).
2. Change the tooltip content visibility condition from `showTooltip` to `tooltipVisible` (line 59).
3. The select branch does NOT use TextFieldMD3, so no `helpTextPrefix` needed there — keep the icon in its current position above the select (it already has a visible label, not a floating label).
**PasswordField.tsx — Same fix as FieldRenderer text-branch:**
1. Add `const [hoverTooltip, setHoverTooltip] = useState(false)` alongside existing `showTooltip`.
2. Remove the tooltip button from above TextFieldMD3 (lines 31-40). Remove the tooltip content `<p>` from above (lines 41-44).
3. Create the same `tooltipIcon` element with `onClick`, `onMouseEnter`, `onMouseLeave`, `aria-label={`More info about ${label}`}`.
4. Compute `const tooltipVisible = showTooltip || hoverTooltip`.
5. Pass `helpTextPrefix={tooltipIcon}` to TextFieldMD3.
6. Render tooltip content after TextFieldMD3, conditioned on `tooltipVisible`.
IMPORTANT: Preserve existing `aria-label` format exactly as `More info about ${field.label}` (or `${label}` in PasswordField) — existing tests assert this.
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npx vitest run --reporter=verbose 2>&1 | tail -30</automated>
</verify>
<done>
- Tooltip icon renders inline to the left of helpText below the field (not above the field)
- Tooltip icon renders even when field has tooltipText but no helpText (guard is helpText || helpTextPrefix)
- Hovering the tooltip icon shows tooltip text
- Clicking the tooltip icon pins tooltip open; clicking again dismisses
- Mouse-leaving the icon hides tooltip (unless pinned via click)
- All existing tests pass including FieldRenderer aria-label tests
- PasswordField has identical fix
</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Add tests for tooltip hover behavior and helpTextPrefix rendering</name>
<files>src/components/ui/FieldRenderer.test.tsx, src/components/ui/TextFieldMD3.test.tsx</files>
<behavior>
- Test: Hovering tooltip button shows tooltip text (fireEvent.mouseEnter on button, then expect tooltipText visible)
- Test: Mouse-leaving tooltip button hides tooltip text (fireEvent.mouseLeave, then expect tooltipText not visible)
- Test: Clicking tooltip button pins tooltip open, mouseLeave does NOT hide it
- Test: TextFieldMD3 renders helpTextPrefix inline with helpText when prop provided
- Test: TextFieldMD3 renders helpText normally when helpTextPrefix is not provided
- Test: TextFieldMD3 renders helpTextPrefix alone when helpText is absent (tooltip-only field)
</behavior>
<action>
**FieldRenderer.test.tsx — Add hover behavior tests:**
Add a new describe block `'FieldRenderer - tooltip hover behavior'` with these tests:
1. `'hovering tooltip button shows tooltip text'`:
- Render FieldRenderer with `textFieldWithTooltip` (already defined in test file)
- Get tooltip button via `screen.getByRole('button', { name: /more info about bucket name/i })`
- `fireEvent.mouseEnter(tooltipBtn)`
- `expect(screen.getByText('The name of your storage bucket')).toBeDefined()`
2. `'mouse-leaving tooltip button hides tooltip text'`:
- Render, mouseEnter, verify visible, then `fireEvent.mouseLeave(tooltipBtn)`
- `expect(screen.queryByText('The name of your storage bucket')).toBeNull()`
3. `'clicking tooltip button pins it open through mouseLeave'`:
- Render, `fireEvent.click(tooltipBtn)`, `fireEvent.mouseLeave(tooltipBtn)`
- `expect(screen.getByText('The name of your storage bucket')).toBeDefined()` (still visible because pinned)
4. `'clicking pinned tooltip button again dismisses it'`:
- Render, click (pin), click again (unpin)
- `expect(screen.queryByText('The name of your storage bucket')).toBeNull()`
Import `fireEvent` from `@testing-library/react` (add to existing import).
**TextFieldMD3.test.tsx — Add helpTextPrefix tests:**
Add tests in existing file (or create new describe block):
1. `'renders helpTextPrefix inline with helpText'`:
- Render TextFieldMD3 with `helpText="Some help"` and `helpTextPrefix={<span data-testid="prefix">icon</span>}`
- `expect(screen.getByTestId('prefix')).toBeDefined()`
- `expect(screen.getByText('Some help')).toBeDefined()`
2. `'renders helpText without wrapper issues when helpTextPrefix is undefined'`:
- Render TextFieldMD3 with `helpText="Some help"` and no helpTextPrefix
- `expect(screen.getByText('Some help')).toBeDefined()`
3. `'renders helpTextPrefix alone when helpText is absent'`:
- Render TextFieldMD3 with NO `helpText` and `helpTextPrefix={<span data-testid="prefix-only">icon</span>}`
- `expect(screen.getByTestId('prefix-only')).toBeDefined()`
- This verifies the `(helpText || helpTextPrefix) && !error` guard works correctly for tooltip-only fields
</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/Ready2Blob && npx vitest run src/components/ui/FieldRenderer.test.tsx src/components/ui/TextFieldMD3.test.tsx --reporter=verbose 2>&1 | tail -40</automated>
</verify>
<done>
- All new tooltip hover tests pass (mouseEnter shows, mouseLeave hides, click pins, click again unpins)
- helpTextPrefix rendering test passes (with helpText, without helpText, and prefix-only)
- All pre-existing FieldRenderer and TextFieldMD3 tests still pass
</done>
</task>
</tasks>
<verification>
1. `npx vitest run --reporter=verbose` — all tests pass (179+ existing + new tooltip tests)
2. Manual spot-check: tooltip icon appears beside helpText below field, not above field
3. Hover behavior: mouseEnter shows tooltip, mouseLeave hides it, click pins it
4. Edge case: field with tooltipText but no helpText still renders the tooltip icon
</verification>
<success_criteria>
- UAT test 7 passes: tooltip info buttons show tooltip text on hover AND icon is positioned beside helpText below the field
- Zero test regressions
- Both FieldRenderer (text-branch + select-branch) and PasswordField have hover support
- TextFieldMD3 has helpTextPrefix prop for extensibility
- Fields with tooltipText but no helpText still show the tooltip icon (guard is not helpText-only)
</success_criteria>
<output>
After completion, create `.planning/phases/09-md3-components/09-05-SUMMARY.md`
</output>
@@ -0,0 +1,118 @@
---
phase: 09-md3-components
plan: "05"
subsystem: ui
tags: [react, tailwind, tooltip, accessibility, testing-library]
# Dependency graph
requires:
- phase: 09-md3-components/09-04
provides: TextFieldMD3 component with suffix/helpText props
provides:
- Tooltip hover+click behavior in FieldRenderer (text and select branches) and PasswordField
- helpTextPrefix prop on TextFieldMD3 for inline left-of-helpText slot
- Tooltip icon rendered beside helpText below the field (not above the field)
- Tests covering hover show/hide, click-pin, click-unpin, and helpTextPrefix rendering
affects: [RemoteConfigStep, PasswordField, TextFieldMD3]
# Tech tracking
tech-stack:
added: []
patterns:
- "hoverTooltip + showTooltip dual-state: hover shows, click pins, click-unpin clears both"
- "helpTextPrefix slot pattern: ReactNode prop for inline content left of helpText in TextFieldMD3"
- "Click-to-unpin guard: setShowTooltip(v => { if (v) setHoverTooltip(false); return !v; })"
key-files:
created: []
modified:
- src/components/ui/TextFieldMD3.tsx
- src/components/ui/FieldRenderer.tsx
- src/components/ui/PasswordField.tsx
- src/components/ui/FieldRenderer.test.tsx
- src/components/ui/TextFieldMD3.test.tsx
key-decisions:
- "Tooltip hover uses dual-state (hoverTooltip + showTooltip); click-unpin explicitly clears hoverTooltip to prevent sticky tooltip after dismiss"
- "helpTextPrefix guard is (helpText || helpTextPrefix) not just helpText, enabling icon-only rendering when field has tooltipText but no helpText"
- "Tooltip content rendered BELOW TextFieldMD3 (after component), not inside it — TextFieldMD3 stays self-contained"
patterns-established:
- "helpTextPrefix slot: pass ReactNode as inline prefix left of helpText text in TextFieldMD3"
- "Hover+click tooltip: mouseEnter shows, mouseLeave hides, click pins, second-click clears both states"
requirements-completed: [COMP-04]
# Metrics
duration: 10min
completed: 2026-04-01
---
# Phase 9 Plan 05: Tooltip Placement and Hover Fix Summary
**Tooltip icon moved from above field to inline with helpText below it, with hover-to-preview and click-to-pin behavior via dual hoverTooltip+showTooltip state**
## Performance
- **Duration:** ~10 min
- **Started:** 2026-04-01T09:10:00Z
- **Completed:** 2026-04-01T09:20:34Z
- **Tasks:** 2
- **Files modified:** 5
## Accomplishments
- Added `helpTextPrefix` prop to TextFieldMD3 for rendering inline content (tooltip icon) left of helpText below the field
- Fixed tooltip icon position: moved from above the floating-label input to beside helpText below it
- Implemented hover-to-preview (mouseEnter shows, mouseLeave hides) and click-to-pin (stays open through mouseLeave) in FieldRenderer and PasswordField
- Fixed click-unpin edge case: second click explicitly clears hoverTooltip so tooltip dismisses even when mouse stays on button
- Added 7 new tests covering all tooltip hover/pin/unpin behaviors and helpTextPrefix rendering
## Task Commits
Each task was committed atomically:
1. **Task 1: Add helpTextPrefix prop and fix tooltip placement + hover** - `23617c0` (feat)
2. **Task 2: Add tests for tooltip hover behavior and helpTextPrefix rendering** - `d2b6da2` (test)
## Files Created/Modified
- `src/components/ui/TextFieldMD3.tsx` - Added `helpTextPrefix?: React.ReactNode` prop; changed helpText guard to `(helpText || helpTextPrefix) && !error`; renders `<div className="flex items-start gap-1">` wrapping prefix + helpText
- `src/components/ui/FieldRenderer.tsx` - Added `hoverTooltip` state; moved tooltip icon from above-field to `tooltipIcon` element passed as `helpTextPrefix`; tooltip content rendered below TextFieldMD3; select-branch also gains hover support
- `src/components/ui/PasswordField.tsx` - Same dual-state tooltip fix as FieldRenderer text-branch
- `src/components/ui/FieldRenderer.test.tsx` - Added `fireEvent` import; added 4 hover behavior tests (hover shows, leave hides, click pins, second-click unpins)
- `src/components/ui/TextFieldMD3.test.tsx` - Added Tests 9-11 for helpTextPrefix with helpText, without, and prefix-only
## Decisions Made
- Click-to-unpin must explicitly clear `hoverTooltip` because `userEvent.click()` simulates hover before click, leaving `hoverTooltip=true` after second click — required `setShowTooltip(v => { if (v) setHoverTooltip(false); return !v; })`
- Tooltip content stays outside TextFieldMD3 (rendered after the component in FieldRenderer/PasswordField) — keeps TextFieldMD3 self-contained, tooltip is a consumer concern
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] Fixed click-unpin leaving tooltip visible due to hoverTooltip race**
- **Found during:** Task 1 verification (RemoteConfigStep.test.tsx "hides tooltip panel when clicked again")
- **Issue:** `userEvent.click()` simulates mouseEnter before click; second click set `showTooltip=false` but `hoverTooltip` stayed true, so tooltip remained visible
- **Fix:** Changed click handler to `setShowTooltip(v => { if (v) setHoverTooltip(false); return !v; })` — clears hover state when unpinning
- **Files modified:** src/components/ui/FieldRenderer.tsx, src/components/ui/PasswordField.tsx
- **Verification:** All 186 tests pass including RemoteConfigStep "hides tooltip panel when clicked again"
- **Committed in:** 23617c0 (Task 1 commit)
---
**Total deviations:** 1 auto-fixed (Rule 1 - bug)
**Impact on plan:** Required for correctness — without this fix, tooltip could not be dismissed by click when mouse was over the button.
## Issues Encountered
- Click-unpin sticky tooltip: `userEvent.click` triggers hover state before click event. Resolved by clearing `hoverTooltip` in the unpin branch of the click handler.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- UAT test 7 (Tooltip Info Buttons Accessibility) now satisfies all criteria: hover shows, mouseLeave hides, click pins, click again unpins, icon beside helpText below field
- Phase 9 (09-md3-components) fully complete — all must-have truths and requirements satisfied
- Ready for Phase 10
---
*Phase: 09-md3-components*
*Completed: 2026-04-01*
@@ -0,0 +1,679 @@
# Phase 9: MD3 Components - Research
**Researched:** 2026-04-01
**Domain:** React + Tailwind v4 CSS-custom-property-based MD3 component styling
**Confidence:** HIGH
---
## Summary
Phase 9 delivers five component-level upgrades to a working wizard that already has a complete
MD3 color token system (Phase 8 complete). The token layer (`--r2b-*` CSS custom properties
mapped to Tailwind semantic utilities via `@theme`) is the foundation — every new component
pattern must consume `bg-primary`, `text-on-surface`, `border-outline`, etc., never raw colors.
The work divides into four visual domains: (1) outlined text fields with CSS-only floating
labels, (2) a three-tier button hierarchy (filled / outlined / text), (3) MD3 elevation on
BackendCard and OutputBlock, and (4) a rebuilt StepIndicator with numbered circles and a
connector line. One non-visual task (DEBT-01) fixes an aria-label inconsistency inside
FieldRenderer's tooltip button.
The project has a hard constraint of **zero new runtime dependencies**. All styling must be
achieved with Tailwind v4 utility classes and plain CSS transitions — no component library, no
animation library. The existing 166 tests are GREEN and must stay GREEN throughout. The
critical test-breaking risk is that 131 test selectors use `getByLabelText` with
`htmlFor`/`id` relationships; the floating label pattern must preserve those relationships
exactly.
**Primary recommendation:** Build a new `<TextFieldMD3>` wrapper component, styled button
variant classes (not a separate component), an elevation mixin via Tailwind `shadow-` tokens,
and a rebuilt `<StepIndicator>` using Tailwind flex + absolute-positioned connector — all
consuming the existing semantic token utilities.
---
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|-----------------|
| COMP-01 | All text inputs render as MD3 outlined text fields with floating labels that animate on focus and when the field has content | Floating label via CSS `peer` + `placeholder=" "` trick; `<TextFieldMD3>` wraps `<input>` + `<label>`; replaces current static label pattern in FieldRenderer and PasswordField |
| COMP-02 | Buttons follow MD3 hierarchy — filled for primary (Next, Download), outlined for secondary (Back, Copy), text for tertiary | Three Tailwind class sets; applied inline to existing button elements across BackendSelectionStep, RemoteConfigStep, DeploymentStep, ReviewStep, OutputBlock |
| COMP-03 | Backend selection cards and output blocks use MD3 elevation with tonal surface tint, consistent padding, and shape tokens | BackendCard already uses semantic tokens; needs `shadow-` utility + `bg-primary/5` tint on `surface-container`; OutputBlock `<pre>` needs shape token consistency |
| COMP-04 | Step indicator displays as numbered circles connected by lines, with completed steps showing a checkmark, current step highlighted, and future steps muted | Full StepIndicator rebuild; flex layout with absolute-positioned `<hr>` or `border-t` connector; existing dispatch logic and WIZD-03 tests preserved |
| DEBT-01 | FieldRenderer uses consistent `aria-label` pattern across text-branch and select-branch (resolving v1.1 cosmetic debt) | Text-branch tooltip button uses `<span className="sr-only">` while select-branch uses `aria-label={...}` directly; fix: add `aria-label` to text-branch button and remove sr-only span |
</phase_requirements>
---
## Standard Stack
### Core (already installed — zero new dependencies)
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| Tailwind v4 | 4.2.2 | Utility classes for all styling | Project constraint: no MUI/component lib |
| @tailwindcss/vite | 4.2.2 | Vite integration, `@theme` directive | Enables semantic token utilities |
| react | 18.3.1 | Component model | App foundation |
| react-hook-form | 7.72.0 | Form state; `register()` must pass through to inputs | All fields use RHF registration |
### Supporting (dev, already installed)
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| vitest | 4.1.1 | Test runner | All component tests |
| @testing-library/react | 16.3.2 | Render + query | `getByLabelText`, `getByRole`, `userEvent` |
### Alternatives NOT to use
| Instead of | Could Use | Why Not |
|------------|-----------|---------|
| CSS-only floating label | `@material/web` `<md-outlined-text-field>` | Bundle bloat; requires separate package; conflicts with RHF register pattern |
| Hand-rolled box-shadow elevation | Headless UI / Radix | No need — pure Tailwind shadow utilities sufficient |
| Custom stepper library | Any npm stepper lib | Zero new deps; simple 4-step indicator is straightforward with Tailwind flex |
**Installation:** None. All dependencies already present.
---
## Architecture Patterns
### Existing Token Map (from `src/index.css`)
Phase 8 established a two-layer pattern. All new components reference these Tailwind utilities:
```
bg-primary text-on-primary
bg-surface text-on-surface
bg-surface-container text-on-surface-container
bg-surface-variant text-on-surface-variant
border-outline
text-error bg-error/10
text-primary bg-primary/10
```
These utilities work in both light and dark mode because they resolve through CSS custom
properties that are overridden by the `.dark` class.
### Recommended Component Additions
```
src/components/ui/
├── TextFieldMD3.tsx # NEW: outlined input with floating label (COMP-01)
├── BackendCard.tsx # EDIT: add MD3 elevation shadow (COMP-03)
├── PasswordField.tsx # EDIT: replace static label with TextFieldMD3 inner layout (COMP-01)
├── FieldRenderer.tsx # EDIT: replace text+select branches with TextFieldMD3; fix aria (DEBT-01)
src/components/wizard/
├── StepIndicator.tsx # FULL REBUILD: numbered circles + connector line (COMP-04)
├── BackendSelectionStep.tsx # EDIT: button styling (COMP-02)
├── RemoteConfigStep.tsx # EDIT: button styling (COMP-02)
├── DeploymentStep.tsx # EDIT: button styling (COMP-02)
├── ReviewStep.tsx # EDIT: button styling + OutputBlock (COMP-02, COMP-03)
├── OutputBlock.tsx # EDIT: button styling + elevation (COMP-02, COMP-03)
```
### Pattern 1: Floating Label Outlined Text Field (COMP-01)
**What:** CSS-only floating label using Tailwind `peer` utilities. The input has a space as
placeholder (`placeholder=" "`), which allows `:placeholder-shown` to detect whether the
field is empty. The label is absolutely positioned and transitions between center (empty,
unfocused) and top-left (focused OR has value) states.
**When to use:** All `<input type="text">` and `<input type="password">` fields inside
FieldRenderer and PasswordField.
**Critical constraint:** The `id` attribute on the input and `htmlFor` on the label MUST be
preserved unchanged. All existing `getByLabelText` test selectors depend on this pairing.
**Structure:**
```tsx
// Source: pattern derived from Flowbite floating label docs + MD3 outlined field spec
// https://flowbite.com/docs/forms/floating-label/
// https://material-web.dev/components/text-field/
interface TextFieldMD3Props {
id: string;
label: string;
error?: FieldError;
registration: UseFormRegisterReturn;
type?: 'text' | 'password';
placeholder?: string; // internal use only — always " " for floating label
helpText?: string;
required?: boolean;
suffix?: React.ReactNode; // for PasswordField show/hide button
}
export function TextFieldMD3({ id, label, error, registration, type = 'text', helpText, required, suffix }: TextFieldMD3Props) {
return (
<div className="flex flex-col gap-1">
<div className="relative">
<input
id={id}
type={type}
placeholder=" " // CRITICAL: single space triggers :placeholder-shown detection
className={[
'peer w-full rounded-md border bg-transparent px-3 pb-2 pt-5 text-sm',
'focus:outline-none focus:ring-2',
'placeholder-transparent', // hide the space placeholder visually
error
? 'border-error focus:ring-error/50'
: 'border-outline focus:border-primary focus:ring-primary/20',
suffix ? 'pr-10' : '',
].join(' ')}
{...registration}
/>
<label
htmlFor={id}
className={[
'absolute left-3 top-1/2 -translate-y-1/2 text-sm text-on-surface-container/70',
'origin-left transition-all duration-200',
// When input has content or is focused: float to top
'peer-placeholder-shown:top-1/2 peer-placeholder-shown:scale-100',
'peer-focus:top-3 peer-focus:scale-75 peer-focus:-translate-y-0 peer-focus:text-primary',
// When not placeholder-shown (has value): also float
'peer-not-placeholder-shown:top-3 peer-not-placeholder-shown:scale-75 peer-not-placeholder-shown:-translate-y-0',
error ? 'peer-focus:text-error' : '',
].join(' ')}
>
{label}
{required && <span className="ml-1 text-error">*</span>}
</label>
{suffix && (
<div className="absolute right-2 top-1/2 -translate-y-1/2">
{suffix}
</div>
)}
</div>
{helpText && !error && <p className="text-xs text-on-surface-container/70">{helpText}</p>}
{error && <p className="text-xs text-error">{error.message}</p>}
</div>
);
}
```
**Implementation note on `peer-not-placeholder-shown`:** Tailwind v4 supports arbitrary
variants. If `peer-not-placeholder-shown` is not available as a built-in, use
`peer-[:not(:placeholder-shown)]:top-3` syntax. Verify against Tailwind v4.2.2 docs.
Alternatively, the `data-has-value` attribute approach (set via RHF `watch` or `onBlur`)
is a reliable fallback.
**Alternative approach (data attribute):** If peer-variant coverage is incomplete in Tailwind
v4, use a wrapper component that watches the input value via RHF `watch()` and adds
`data-has-value` to the container, then target `group-data-[has-value]:` on the label.
This is MORE JavaScript but less reliance on CSS pseudo-class availability.
### Pattern 2: MD3 Button Hierarchy (COMP-02)
**What:** Three button class sets applied consistently wherever navigation buttons appear.
No new component — apply className patterns directly.
```tsx
// Source: MD3 button spec https://m3.material.io/components/all-buttons
// Primary / Filled: background = primary, text = on-primary
const btnFilled = 'px-6 py-2.5 rounded-full bg-primary text-on-primary text-sm font-medium ' +
'hover:opacity-90 active:opacity-80 disabled:opacity-40 disabled:cursor-not-allowed ' +
'focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:outline-none transition-opacity';
// Secondary / Outlined: transparent background, primary border and text
const btnOutlined = 'px-6 py-2.5 rounded-full border border-outline text-on-surface text-sm font-medium ' +
'hover:bg-primary/8 active:bg-primary/12 disabled:opacity-40 disabled:cursor-not-allowed ' +
'focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:outline-none transition-colors';
// Tertiary / Text: no border, no background
const btnText = 'px-4 py-2.5 rounded-full text-primary text-sm font-medium ' +
'hover:bg-primary/8 active:bg-primary/12 disabled:opacity-40 disabled:cursor-not-allowed ' +
'focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:outline-none transition-colors';
```
**Corner radius:** MD3 uses `rounded-full` (28dp height / pill shape) for standard buttons.
Current codebase uses `rounded-md` — this is a deliberate MD3 upgrade.
**Action mapping:**
| Button | Type | Location |
|--------|------|----------|
| Next | Filled | BackendSelectionStep, RemoteConfigStep, DeploymentStep |
| Download All (ZIP) | Filled | ReviewStep |
| Back | Outlined | RemoteConfigStep, DeploymentStep, ReviewStep |
| Copy | Outlined | OutputBlock |
| Download (per file) | Outlined | OutputBlock |
| SAS URL / Access Key toggle | — | AzureAuthToggle (segmented control, not a button hierarchy concern) |
| Password / Private Key toggle | — | SftpAuthToggle (segmented control) |
### Pattern 3: MD3 Elevation + Tonal Surface (COMP-03)
**What:** MD3 elevation level 1 (the lightest surface lift) for cards and code blocks.
MD3 uses two mechanisms together: a drop shadow AND a tonal overlay (primary color at low
opacity over the surface).
**MD3 Elevation Level 1 shadow (verified source: studioncreations.com):**
```css
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37);
```
Tailwind equivalent: `shadow-md` (`0 4px 6px -1px rgba(0,0,0,0.1)...`) is close but not
exact. Use `shadow` (standard) or a custom Tailwind shadow token via `@theme` in index.css.
**Tonal surface tint:** Add `bg-primary/5` to card container on top of `bg-surface-container`
to implement the MD3 tonal tint. In practice, this means the container background class
becomes a layered effect or the value of `--r2b-surface-container` is adjusted.
**Recommended approach for BackendCard:**
```tsx
// Before: border-2 p-4 rounded-lg
// After: border-2 p-4 rounded-xl shadow + tonal tint
className={[
'flex flex-col items-start gap-1 rounded-xl border-2 p-4 text-left transition-all',
'shadow hover:shadow-md', // elevation lift on hover
selected
? 'border-primary bg-primary/10 shadow-md'
: 'border-outline bg-surface-container hover:border-primary hover:bg-primary/5',
].join(' ')}
```
**Shape token:** MD3 card shape is "medium" (12dp corner radius). Use `rounded-xl` (12px).
Current `rounded-lg` is 8px — upgrade to `rounded-xl`.
**OutputBlock elevation:**
```tsx
// Pre block (code display) — add subtle elevation to differentiate from page surface
className="bg-surface-variant text-on-surface-variant rounded-xl p-4 text-xs overflow-x-auto whitespace-pre-wrap shadow-sm"
```
### Pattern 4: Rebuilt StepIndicator (COMP-04)
**What:** Replace the current text-based breadcrumb (spans with `` separator) with a visual
step indicator: horizontal row of numbered circles connected by a line, showing state via
icon/color.
**Current implementation issues:**
- Uses inline `style={{}}` instead of Tailwind classes (style violations)
- No visual connector line between steps
- No checkmark on completed steps
- `#999` hardcoded color (not token)
**Structure:**
```tsx
// Horizontal step indicator with connector lines between circles
return (
<nav aria-label="Wizard steps">
<ol className="flex items-center w-full">
{STEP_LABELS.map((label, i) => {
const isCompleted = i < currentStep;
const isActive = i === currentStep;
const isFuture = i > currentStep;
return (
<li key={i} className={['flex items-center', i < STEP_LABELS.length - 1 ? 'flex-1' : ''].join(' ')}>
{/* Step circle */}
<div className="flex flex-col items-center gap-1 shrink-0">
{isCompleted ? (
<button
type="button"
onClick={() => handleStepClick(i)}
className="w-8 h-8 rounded-full bg-primary text-on-primary flex items-center justify-center text-sm font-medium focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={`Go to step ${i + 1}: ${label}`}
>
</button>
) : isActive ? (
<span className="w-8 h-8 rounded-full border-2 border-primary bg-primary/10 text-primary flex items-center justify-center text-sm font-bold">
{i + 1}
</span>
) : (
<span className="w-8 h-8 rounded-full border-2 border-outline text-on-surface-container/40 flex items-center justify-center text-sm">
{i + 1}
</span>
)}
<span className={[
'text-xs mt-1 text-center',
isCompleted ? 'text-primary' : isActive ? 'text-on-surface font-medium' : 'text-on-surface-container/40',
].join(' ')}>
{label}
</span>
</div>
{/* Connector line between steps */}
{i < STEP_LABELS.length - 1 && (
<div className={[
'flex-1 h-0.5 mx-2 self-start mt-4',
isCompleted ? 'bg-primary' : 'bg-outline',
].join(' ')} aria-hidden="true" />
)}
</li>
);
})}
</ol>
</nav>
);
```
**CRITICAL:** The existing WIZD-03 tests query by button presence and click behavior. The
rebuilt component must still render completed steps as `<button>` elements and dispatch
`SET_STEP` and `SET_REMOTE_PARAMS({})` on click. The test suite does NOT test visual
appearance — only behavior — so the internal restructure is safe.
**Test impact:** `screen.getAllByRole('button')` queries will still find completed step
buttons. Text queries like `screen.findByText(/Deployment/)` will still find the step label.
No test changes expected.
### Pattern 5: DEBT-01 — FieldRenderer aria-label Fix
**Current state (inconsistency):**
- Select branch tooltip button: `aria-label={\`More info about ${field.label}\`}` — CORRECT
- Text branch tooltip button: uses `<span className="sr-only">More info about {field.label}</span>` + `<span aria-hidden="true">ⓘ</span>` — INCONSISTENT
**Fix:** Update the text-branch tooltip button to match the select-branch pattern:
```tsx
// Before (text branch):
<button type="button" onClick={...} className="...">
<span className="sr-only">More info about {field.label}</span>
<span aria-hidden="true"></span>
</button>
// After (matching select branch):
<button
type="button"
onClick={...}
aria-label={`More info about ${field.label}`}
className="..."
>
</button>
```
**Test impact:** The DEBT-01 fix changes tooltip button structure. Any test using
`screen.getByRole('button', { name: /more info about.../i })` will now correctly find both
branches. The RemoteConfigStep.test.tsx UX-01 tests query by `screen.getByRole('button')`
patterns — verify these still pass after fix.
### Anti-Patterns to Avoid
- **Hardcoding colors:** Never use `text-gray-700` or `bg-indigo-500`. Always use semantic
tokens (`text-on-surface`, `bg-primary`).
- **Inline style prop:** StepIndicator currently uses `style={{ fontWeight: 'bold' }}` and
`style={{ color: '#999' }}`. Phase 9 removes ALL inline style props.
- **Changing DOM structure that tests depend on:** `getByLabelText` links `<label htmlFor>`
to `<input id>`. Any floating label pattern MUST keep both attributes intact.
- **Multiple `placeholder` values:** The floating label trick requires `placeholder=" "`
(space). If the field also needs a real placeholder (e.g., "https://..."), the two purposes
conflict. Resolution: use `placeholder=" "` for the CSS trick and show placeholder-like
text as help text below the field instead.
- **Changing button DOM content that tests depend on:** RemoteConfigStep tests use
`getByRole('button', { name: /next/i })` — button text must remain "Next" (case-insensitive
match). Verify all button label changes don't break test selectors.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Floating label that tracks input value | useState + JS event listener | CSS `peer-placeholder-shown` + `placeholder=" "` | Pure CSS, zero JS, better performance |
| Theme-aware shadow values | Switch statement in JS | Tailwind `shadow` utilities — already correct in dark mode | Tailwind shadow colors respect `dark:` variant |
| Accessible step navigation | Custom focus management | `<button>` elements with `aria-label` | Browser handles tab order and keyboard naturally |
| Component library for inputs | Copy MUI TextField source | `TextFieldMD3` custom component with 30 lines | Zero bundle cost, full token control |
---
## Common Pitfalls
### Pitfall 1: Floating Label Blocks `getByLabelText` Queries
**What goes wrong:** If the `<label>` is moved inside the `<input>` wrapper without keeping
`htmlFor` and `id` matched, `screen.getByLabelText(/storage account name/i)` throws. This
breaks 30+ existing test assertions.
**Why it happens:** Floating label patterns sometimes wrap label inside input's container
without proper `for`/`id` association.
**How to avoid:** Always include `<label htmlFor={id}>` paired with `<input id={id}>`. The
label can be absolutely positioned visually while keeping the semantic association.
**Warning signs:** `getByLabelText` test failures immediately after TextFieldMD3 integration.
### Pitfall 2: `placeholder=" "` Conflicts with Real Placeholder Text
**What goes wrong:** Fields like `sas_url` have meaningful placeholder text
(`"https://mystorageaccount.blob.core.windows.net/?sv=..."`). Replacing it with `" "` loses
the hint.
**Why it happens:** The CSS floating label trick requires `placeholder=" "` to trigger
`:placeholder-shown`, but fields want real placeholder hints too.
**How to avoid:** Use `helpText` prop for the contextual hint instead of placeholder. The
field shows the hint below the input. This is actually MORE accessible (visible even after
typing). Verify existing `helpText` content is adequate before removing real placeholders.
**Warning signs:** User confusion about what to enter in credential fields.
### Pitfall 3: `peer-not-placeholder-shown` Availability in Tailwind v4
**What goes wrong:** The Tailwind v4 `peer-placeholder-shown:` variant works, but
`peer-not-placeholder-shown:` may not be a built-in variant. Without it, a field with a
pre-filled value (RHF `defaultValues`) won't show the floated label on initial render.
**Why it happens:** Tailwind v4 generates variants from pseudo-classes, but negation of
`placeholder-shown` may require the arbitrary variant syntax.
**How to avoid:** Use `peer-[:not(:placeholder-shown)]:` for Tailwind v4 arbitrary variant,
or manage a `data-has-value` attribute via a thin React wrapper that checks the registration
value. The data-attribute approach is the most reliable cross-version fallback.
**Warning signs:** Label overlaps with pre-filled text on initial render.
### Pitfall 4: Button Test Selectors Break After Role Change
**What goes wrong:** `screen.getByRole('button', { name: /next/i })` fails after button
text changes from "Next" to an icon-only or different label.
**Why it happens:** MD3 button redesign may tempt relabeling buttons.
**How to avoid:** Keep button text labels unchanged. Style only className, never innerText.
Audit existing test selectors before any button className change: `getByRole('button', { name: /next/i })`, `getByRole('button', { name: /back/i })`, `getByRole('button', { name: /copy/i })`.
**Warning signs:** `Unable to find an accessible element with the role "button" and name /next/i` in test output.
### Pitfall 5: StepIndicator Test Selector Drift
**What goes wrong:** After StepIndicator rebuild, `screen.getAllByRole('button')` returns
different count, and `buttons.find(b => b.textContent?.includes('Backend'))` fails because
completed-step buttons now show "✓" instead of "✓ 1. Backend".
**Why it happens:** Button textContent changed in the rebuild.
**How to avoid:** Keep button `aria-label` meaningful AND keep the label text visible within
the button (or as adjacent text). Test the rebuilt StepIndicator immediately. Adjust
WIZD-03 test selectors if needed (which is expected and acceptable for this phase).
**Warning signs:** `backendButton` undefined in StepIndicator tests.
### Pitfall 6: PasswordField Show/Hide Button Interferes with Floating Label
**What goes wrong:** The absolute-positioned show/hide toggle button overlaps the floating
label when it's in the "up" position.
**Why it happens:** Label transitions from bottom to top; toggle is at right-center.
**How to avoid:** The toggle is `absolute right-2 top-1/2` which only overlaps the label
when label is centered (empty state). Floated label goes to `top-3` which avoids overlap.
Add sufficient `pr-10` padding to the input so text doesn't underlap the button.
**Warning signs:** Show/hide button visually overlaps with floating label text.
---
## Code Examples
### Complete TextFieldMD3 Reference Structure
```tsx
// Verified pattern from Flowbite floating label docs + MD3 spec
// Key: placeholder=" " enables CSS-only floating label detection
export function TextFieldMD3({ id, label, error, registration, type = 'text', helpText, required, suffix }: TextFieldMD3Props) {
return (
<div className="flex flex-col gap-1">
<div className="relative">
<input
id={id}
type={type}
placeholder=" "
className={[
'peer w-full rounded-t-md rounded-b-none border-0 border-b-2 bg-surface-container/30',
// Or for outlined variant:
// 'peer w-full rounded-md border bg-transparent',
'px-3 pb-2 pt-5 text-sm text-on-surface placeholder-transparent',
'focus:outline-none focus:ring-0',
error
? 'border-error focus:border-error'
: 'border-outline focus:border-primary',
suffix ? 'pr-10' : '',
].join(' ')}
{...registration}
/>
<label
htmlFor={id}
className={[
// Starting (centered) position — when input empty and unfocused
'pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm',
'origin-left transform transition-all duration-200',
'text-on-surface-container/60',
// Focused state: float up
'peer-focus:top-3 peer-focus:-translate-y-0 peer-focus:scale-75 peer-focus:text-primary',
// Has value state (input not showing placeholder):
'peer-[:not(:placeholder-shown)]:top-3',
'peer-[:not(:placeholder-shown)]:-translate-y-0',
'peer-[:not(:placeholder-shown)]:scale-75',
error ? 'peer-focus:text-error' : '',
].join(' ')}
>
{label}
{required && <span className="ml-0.5 text-error">*</span>}
</label>
{suffix && (
<div className="absolute right-2 top-1/2 -translate-y-1/2">{suffix}</div>
)}
</div>
{helpText && !error && (
<p className="text-xs text-on-surface-container/70">{helpText}</p>
)}
{error && (
<p className="text-xs text-error" role="alert">{error.message}</p>
)}
</div>
);
}
```
### MD3 Button Class Sets
```tsx
// Source: MD3 button guidelines https://m3.material.io/components/all-buttons
// Filled — primary action
export const MD3_BTN_FILLED =
'px-6 py-2.5 rounded-full bg-primary text-on-primary text-sm font-medium ' +
'hover:opacity-90 active:opacity-80 transition-opacity ' +
'disabled:opacity-40 disabled:cursor-not-allowed ' +
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50';
// Outlined — secondary action
export const MD3_BTN_OUTLINED =
'px-6 py-2.5 rounded-full border border-outline text-on-surface text-sm font-medium ' +
'hover:bg-primary/8 active:bg-primary/12 transition-colors ' +
'disabled:opacity-40 disabled:cursor-not-allowed ' +
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50';
// Text — tertiary action
export const MD3_BTN_TEXT =
'px-4 py-2.5 rounded-full text-primary text-sm font-medium ' +
'hover:bg-primary/8 active:bg-primary/12 transition-colors ' +
'disabled:opacity-40 disabled:cursor-not-allowed ' +
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50';
```
### MD3 Elevation Level 1 CSS
```css
/* Source: studioncreations.com/blog/material-design-3-box-shadow-css-values/ */
/* MD3 elevation-1 */
.elevation-1 {
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37);
}
/* In Tailwind, `shadow` utility is approximately equivalent.
For closer match, can extend @theme in index.css: */
@theme {
--shadow-elevation-1: 0 1px 4px 0 rgb(0 0 0 / 0.37);
}
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Static `<label>` above input | CSS floating label via `peer` | Phase 9 | More visual richness; same DOM semantics |
| Inline `style={{}}` on StepIndicator | Tailwind classes only | Phase 9 | Token-aware, dark-mode compatible |
| `rounded-md` for cards | `rounded-xl` (MD3 medium shape) | Phase 9 | Matches MD3 12dp corner radius |
| Raw `#999` color | `text-on-surface-container/40` | Phase 9 | Token-driven, dark-mode aware |
| `rounded` buttons | `rounded-full` (MD3 pill buttons) | Phase 9 | MD3 standard button shape |
**Deprecated/outdated in this codebase:**
- Any remaining `style={{ color: '#...' }}` or `style={{ fontWeight: ... }}` — all must become Tailwind classes
---
## Open Questions
1. **`peer-[:not(:placeholder-shown)]` Tailwind v4 support**
- What we know: Tailwind v4 supports arbitrary variants with bracket syntax
- What's unclear: Whether the specific `peer-[:not(:placeholder-shown)]:` form compiles correctly in v4.2.2
- Recommendation: Test in Wave 0 with a minimal test case; if it fails, use the `data-has-value` attribute approach
2. **Button text changes breaking existing tests**
- What we know: Several tests use `getByRole('button', { name: /next/i })` etc.
- What's unclear: Whether any planned button text changes (e.g., "Next / Review" → "Next") would break selectors
- Recommendation: Audit test expectations against planned button labels before implementation; keep inner text identical
3. **StepIndicator WIZD-03 test compatibility after rebuild**
- What we know: Tests use `buttons.find(b => b.textContent?.includes('Backend'))` to locate step buttons
- What's unclear: After adding "✓" and changing button structure, does `textContent?.includes('Backend')` still match?
- Recommendation: New button structure should include the label text. If buttons render as `<button aria-label="Go to step 1: Backend">✓</button>`, the textContent test will fail. Consider keeping label text visible inside button OR updating the test (acceptable in Phase 9).
---
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Vitest 4.1.1 |
| Config file | `vite.config.ts` (has `test.environment: jsdom`) |
| Quick run command | `npx vitest run --reporter=dot` |
| Full suite command | `npx vitest run` |
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| COMP-01 | TextFieldMD3 floating label floats on focus and when has value | unit | `npx vitest run src/components/ui/TextFieldMD3.test.tsx` | ❌ Wave 0 |
| COMP-01 | FieldRenderer text-branch uses TextFieldMD3 (label still queryable) | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ✅ (existing coverage) |
| COMP-02 | Buttons have correct role and text (Next, Back, Copy, Download) | unit | `npx vitest run src/components/wizard/ReviewStep.test.tsx` | ✅ (existing coverage) |
| COMP-03 | BackendCard has `rounded-xl` className | unit | `npx vitest run src/components/ui/BackendCard.test.tsx` | ❌ Wave 0 |
| COMP-04 | StepIndicator: completed steps are clickable buttons | unit | `npx vitest run src/components/wizard/StepIndicator.test.tsx` | ✅ (existing coverage) |
| COMP-04 | StepIndicator: clicking step 0 dispatches SET_REMOTE_PARAMS({}) | unit | `npx vitest run src/components/wizard/StepIndicator.test.tsx` | ✅ (existing coverage) |
| DEBT-01 | FieldRenderer select-branch tooltip button has aria-label | unit | `npx vitest run src/components/ui/FieldRenderer.test.tsx` | ❌ Wave 0 |
| DEBT-01 | FieldRenderer text-branch tooltip button has aria-label | unit | `npx vitest run src/components/ui/FieldRenderer.test.tsx` | ❌ Wave 0 |
### Sampling Rate
- **Per task commit:** `npx vitest run --reporter=dot` (full suite, ~7s)
- **Per wave merge:** `npx vitest run` (verbose, all 166+ tests)
- **Phase gate:** Full suite green before `/gsd:verify-work`
### Wave 0 Gaps
- [ ] `src/components/ui/TextFieldMD3.test.tsx` — covers COMP-01 floating label behavior
- [ ] `src/components/ui/BackendCard.test.tsx` — covers COMP-03 elevation class presence
- [ ] `src/components/ui/FieldRenderer.test.tsx` — covers DEBT-01 aria-label consistency
*(Existing StepIndicator.test.tsx covers COMP-04 behavioral requirements. Update to
match rebuilt component's button textContent if needed.)*
---
## Sources
### Primary (HIGH confidence)
- Flowbite floating label docs — https://flowbite.com/docs/forms/floating-label/ — floating label peer structure
- material-web.dev text field — https://material-web.dev/components/text-field/ — MD3 outlined field behavior spec
- MD3 buttons guidelines — https://m3.material.io/components/buttons/guidelines — button hierarchy and specs
- MD3 all buttons — https://m3.material.io/components/all-buttons — filled/outlined/text specs
### Secondary (MEDIUM confidence)
- studioncreations.com MD3 box-shadow values — https://studioncreations.com/blog/material-design-3-box-shadow-css-values/ — elevation CSS values (note: elevation spec was alpha status)
- DEV Community floating label with Tailwind — https://dev.to/chrsgrrtt/floating-label-input-with-react-and-tailwind-2e5h — React + Tailwind implementation
- jakedawkins.com accessible floating label — https://jakedawkins.com/blog/accessible-input-floating-label/ — accessibility considerations
### Tertiary (LOW confidence)
- MD3 elevation tonal tint behavior — from WebSearch summary only; tonal tint percentage (5% primary overlay) is an estimate from pattern observation, not precisely measured from spec
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — all dependencies already installed, no new additions
- Architecture: HIGH — current component code fully audited, patterns verified
- Pitfalls: HIGH — based on direct code analysis of existing test selectors and component structure
- Floating label CSS: MEDIUM — Tailwind v4 `peer-[:not(:placeholder-shown)]` syntax needs runtime verification
- MD3 elevation shadow values: MEDIUM — sourced from third-party CSS values article, not official spec
**Research date:** 2026-04-01
**Valid until:** 2026-05-01 (stable libraries; Tailwind v4 is releasing updates)
@@ -0,0 +1,69 @@
---
status: diagnosed
phase: 09-md3-components
source: [09-01-SUMMARY.md, 09-02-SUMMARY.md, 09-03-SUMMARY.md, 09-04-SUMMARY.md]
started: 2026-04-01T11:00:00Z
updated: 2026-04-01T11:15:00Z
---
## Current Test
[testing complete]
## Tests
### 1. Floating Labels on All Text Inputs
expected: Navigate through all wizard steps. Every text input should display an MD3 floating label that sits inside the input when empty, animates upward on focus, and stays floated when a value is entered.
result: pass
### 2. Password Field Show/Hide Toggle
expected: On the Remote Config step, the SAS URL (password) field should have a show/hide toggle button inside the input on the right side. Clicking it toggles between masked (dots) and visible text. The field also has a floating label.
result: pass
### 3. MD3 Step Indicator with Circles
expected: The wizard step indicator shows numbered circles (not text breadcrumbs). Completed steps display a checkmark inside a filled circle and are clickable to navigate back. The active step has a highlighted ring/border. Future steps are muted/greyed out. Connector lines between steps are filled for completed segments and outlined for future ones.
result: pass
### 4. MD3 Button Hierarchy
expected: Across all wizard steps: primary forward actions (Next, Download All) use a filled pill-shaped button style. Secondary/back actions (Back, Copy, Download individual) use an outlined pill-shaped button style. The visual distinction between primary and secondary buttons should be clear.
result: pass
### 5. BackendCard Elevation and Shape
expected: On the Backend Selection step, each backend card (Azure, AWS, GCP) should have rounded corners (visibly rounded, not sharp) and a subtle shadow. Hovering a card should slightly increase the shadow/elevation.
result: pass
### 6. OutputBlock Code Styling
expected: On the Review/Output step, code output blocks should have rounded corners and a subtle shadow, giving them a contained card-like appearance rather than flat unstyled pre blocks.
result: pass
### 7. Tooltip Info Buttons Accessibility
expected: On the Remote Config step, fields with info tooltips should have an info button. Hovering or focusing the button should display the tooltip text. The tooltip button should not interfere with the field's label association (i.e., clicking the field label still focuses the input).
result: issue
reported: "Hovering the tooltip button doesn't show anything. Also, the placement of the tooltip icon isn't right: it should be besides the text that's under the field (for example, in the SAS URL field, the 'i' icon should be to the left of 'Full SAS URL including account and container')"
severity: major
## Summary
total: 7
passed: 6
issues: 1
pending: 0
skipped: 0
## Gaps
- truth: "Tooltip info buttons show tooltip text on hover and are properly positioned"
status: failed
reason: "User reported: Hovering the tooltip button doesn't show anything. Also, the placement of the tooltip icon isn't right: it should be besides the text that's under the field (for example, in the SAS URL field, the 'i' icon should be to the left of 'Full SAS URL including account and container')"
severity: major
test: 7
root_cause: "Two issues: (1) Tooltip button uses onClick toggle only — no onMouseEnter/onMouseLeave handlers, so hover does nothing. (2) Tooltip icon is rendered as a sibling ABOVE TextFieldMD3 in FieldRenderer.tsx, but helpText is encapsulated inside TextFieldMD3 — so the icon can't be positioned beside it."
artifacts:
- path: "src/components/ui/FieldRenderer.tsx"
issue: "Tooltip button at line 85 uses onClick-only toggle, positioned above TextFieldMD3 instead of beside helpText"
- path: "src/components/ui/TextFieldMD3.tsx"
issue: "helpText rendered internally (lines 65-67), unreachable by FieldRenderer for inline icon placement"
missing:
- "Add hover behavior (onMouseEnter/onMouseLeave or CSS group-hover) to tooltip button"
- "Move tooltip icon beside helpText — either add helpTextPrefix prop to TextFieldMD3 or restructure FieldRenderer layout"
debug_session: ".planning/debug/tooltip-info-buttons-broken.md"
@@ -0,0 +1,85 @@
---
phase: 9
slug: md3-components
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-04-01
---
# Phase 9 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution.
---
## Test Infrastructure
| Property | Value |
|----------|-------|
| **Framework** | Vitest 4.1.1 |
| **Config file** | `vite.config.ts` (has `test.environment: jsdom`) |
| **Quick run command** | `npx vitest run --reporter=dot` |
| **Full suite command** | `npx vitest run` |
| **Estimated runtime** | ~7 seconds |
---
## Sampling Rate
- **After every task commit:** Run `npx vitest run --reporter=dot`
- **After every plan wave:** Run `npx vitest run`
- **Before `/gsd:verify-work`:** Full suite must be green
- **Max feedback latency:** 7 seconds
---
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
| 09-01-01 | 01 | 0 | COMP-01 | unit | `npx vitest run src/components/ui/TextFieldMD3.test.tsx` | ❌ W0 | ⬜ pending |
| 09-01-02 | 01 | 0 | COMP-03 | unit | `npx vitest run src/components/ui/BackendCard.test.tsx` | ❌ W0 | ⬜ pending |
| 09-01-03 | 01 | 0 | DEBT-01 | unit | `npx vitest run src/components/ui/FieldRenderer.test.tsx` | ❌ W0 | ⬜ pending |
| 09-02-01 | 01 | 1 | COMP-01 | unit | `npx vitest run src/components/ui/TextFieldMD3.test.tsx` | ❌ W0 | ⬜ pending |
| 09-02-02 | 01 | 1 | COMP-01 | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ✅ | ⬜ pending |
| 09-03-01 | 02 | 1 | COMP-02 | unit | `npx vitest run src/components/wizard/ReviewStep.test.tsx` | ✅ | ⬜ pending |
| 09-04-01 | 03 | 1 | COMP-03 | unit | `npx vitest run src/components/ui/BackendCard.test.tsx` | ❌ W0 | ⬜ pending |
| 09-05-01 | 04 | 2 | COMP-04 | unit | `npx vitest run src/components/wizard/StepIndicator.test.tsx` | ✅ | ⬜ pending |
| 09-06-01 | 05 | 1 | DEBT-01 | unit | `npx vitest run src/components/ui/FieldRenderer.test.tsx` | ❌ W0 | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
---
## Wave 0 Requirements
- [ ] `src/components/ui/TextFieldMD3.test.tsx` — stubs for COMP-01 floating label behavior
- [ ] `src/components/ui/BackendCard.test.tsx` — stubs for COMP-03 elevation class presence
- [ ] `src/components/ui/FieldRenderer.test.tsx` — stubs for DEBT-01 aria-label consistency
*Existing StepIndicator.test.tsx covers COMP-04 behavioral requirements. Update to match rebuilt component's button textContent if needed.*
---
## Manual-Only Verifications
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Floating label animation smoothness | COMP-01 | CSS transition quality is visual | Focus input, observe label floats up smoothly (~150ms) |
| Button visual hierarchy distinction | COMP-02 | Color/weight differences are visual | Compare primary (filled), secondary (outlined), tertiary (text) side by side |
| MD3 elevation tonal surface tint | COMP-03 | Subtle shadow/tint is visual | Check cards have visible elevation and surface tint |
| Step indicator visual states | COMP-04 | Checkmark/circle/line rendering is visual | Navigate wizard, verify completed=checkmark, current=highlighted, future=muted |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 7s
- [ ] `nyquist_compliant: true` set in frontmatter
**Approval:** pending
@@ -0,0 +1,203 @@
---
phase: 09-md3-components
verified: 2026-04-01T11:30:00Z
status: passed
score: 16/16 must-haves verified
re_verification:
previous_status: passed
previous_score: 12/12
gaps_closed:
- "Hovering the tooltip info button reveals tooltip text without clicking (hoverTooltip state + onMouseEnter/onMouseLeave)"
- "Clicking the tooltip icon pins it open; clicking again dismisses it (click-unpin clears hoverTooltip to prevent sticky state)"
- "Tooltip icon appears beside helpText below the field, not above the field (helpTextPrefix prop passed to TextFieldMD3)"
- "Tooltip icon renders even when field has tooltipText but no helpText (guard is (helpText || helpTextPrefix) && !error)"
gaps_remaining: []
regressions: []
human_verification:
- test: "Floating label visual animation"
expected: "Labels in RemoteConfigStep and PasswordField inputs animate upward on focus and stay floated when a value is typed. On blur with empty field, label returns to center position."
why_human: "CSS :placeholder-shown pseudo-class behavior and transition animation cannot be verified with jsdom — JSDOM does not compute CSS or trigger CSS transitions."
- test: "MD3 button visual appearance"
expected: "Next/Download-All buttons appear as pill-shaped (rounded-full) with primary fill color. Back/Copy/Download buttons appear as pill-shaped with an outline border and transparent background."
why_human: "Tailwind class presence is verified programmatically, but rendered visual output (actual computed styles, color tokens resolving correctly) requires browser inspection."
- test: "StepIndicator visual states"
expected: "Step 1 shows a highlighted numbered circle (primary color ring). Completed steps show a checkmark circle. Connector lines between steps are filled (primary) for completed segments and muted (outline) for future. Dark mode renders correctly with no hardcoded colors."
why_human: "Semantic token resolution (bg-primary, border-outline etc.) to actual CSS custom property values requires browser rendering."
- test: "BackendCard elevation and shape"
expected: "Backend cards display with visibly rounded corners (rounded-xl = 12px) and a subtle shadow. Hovering an unselected card increases shadow. Selected card has elevated shadow."
why_human: "Shadow rendering and hover state transitions require browser inspection."
- test: "Tooltip icon placement (visual)"
expected: "Tooltip icon (ⓘ) appears visually inline to the left of helpText, below the floating-label input — not above it. On fields with no helpText, the icon still appears below the input."
why_human: "Flexbox inline rendering of helpTextPrefix inside TextFieldMD3 requires browser inspection to confirm correct visual position relative to input and helpText."
- test: "Tooltip hover appearance and pin behavior (manual)"
expected: "Hovering the ⓘ icon on a text field shows the tooltip panel below the field. Moving the mouse away hides it. Clicking the icon pins it open; moving mouse away keeps it open. Clicking again dismisses it even while hovering."
why_human: "fireEvent in JSDOM verifies state logic but not actual mouse pointer interaction feel, tooltip visual positioning, or whether content is readable at that position."
---
# Phase 9: MD3 Components Verification Report
**Phase Goal:** All interactive elements use MD3-styled primitives with consistent visual language across the entire wizard
**Verified:** 2026-04-01T11:30:00Z
**Status:** passed
**Re-verification:** Yes — after gap closure via Plan 09-05 (commits 23617c0 and d2b6da2)
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | TextFieldMD3 renders an outlined input with a floating label that floats on focus | VERIFIED | `src/components/ui/TextFieldMD3.tsx` uses `peer-focus:top-3 peer-focus:scale-75` CSS peer utilities |
| 2 | TextFieldMD3 floating label floats when the field has a value | VERIFIED | `peer-[:not(:placeholder-shown)]:top-3 peer-[:not(:placeholder-shown)]:scale-75`; `placeholder=" "` drives CSS detection |
| 3 | TextFieldMD3 preserves htmlFor/id pairing so getByLabelText queries work | VERIFIED | `<label htmlFor={id}>` + `<input id={id}>` pattern; Test 2 explicitly verifies `getByLabelText` |
| 4 | MD3 button class constants exist for filled, outlined, and text variants | VERIFIED | `src/styles/md3-buttons.ts` exports `MD3_BTN_FILLED`, `MD3_BTN_OUTLINED`, `MD3_BTN_TEXT` with `rounded-full` classes |
| 5 | FieldRenderer text-branch tooltip button uses aria-label for accessibility | VERIFIED | FieldRenderer.tsx line 92: `aria-label={\`More info about \${field.label}\`}` on tooltip button; 4/4 DEBT-01 tests pass |
| 6 | Step indicator shows numbered circles for each wizard step | VERIFIED | StepIndicator.tsx: `w-8 h-8 rounded-full` spans with `{i + 1}` content for active/future steps |
| 7 | Completed steps display a checkmark and are clickable buttons | VERIFIED | StepIndicator.tsx: `<button>` with `&#10003;` checkmark; test confirms selector works |
| 8 | Current step is visually highlighted with primary color | VERIFIED | Active step uses `border-primary bg-primary/10 text-primary font-bold` — no hardcoded colors |
| 9 | Future steps appear muted | VERIFIED | Future steps use `border-outline text-on-surface-container/40` semantic tokens |
| 10 | Connector lines link steps — filled for completed, muted for future | VERIFIED | `bg-primary` if `isCompleted` else `bg-outline`; `aria-hidden="true"` applied |
| 11 | Clicking step 0 dispatches SET_REMOTE_PARAMS({}) then SET_STEP(0) | VERIFIED | `handleStepClick` logic unchanged; StepIndicator tests verify params are cleared and deployment state preserved |
| 12 | All text inputs in the wizard render with floating labels | VERIFIED | FieldRenderer text-branch, PasswordField, and BackendSelectionStep all use TextFieldMD3 |
| 13 | Hovering the tooltip icon reveals tooltip text without clicking | VERIFIED | FieldRenderer.tsx line 90-91: `onMouseEnter={() => setHoverTooltip(true)}` / `onMouseLeave={() => setHoverTooltip(false)}`; `tooltipVisible = showTooltip \|\| hoverTooltip`; FieldRenderer test "hovering tooltip button shows tooltip text" passes |
| 14 | Clicking the tooltip icon pins it open; clicking again dismisses it | VERIFIED | Click handler: `setShowTooltip(v => { if (v) setHoverTooltip(false); return !v; })` — clears hover on unpin to prevent sticky state; tests "pins through mouseLeave" and "clicking again dismisses" both pass |
| 15 | Tooltip icon appears beside helpText below the field, not above the field | VERIFIED | TextFieldMD3 `helpTextPrefix` prop renders icon inside `<div className="flex items-start gap-1">` below the input; `tooltipIcon` passed as `helpTextPrefix={tooltipIcon}` in FieldRenderer line 110 and PasswordField line 54 |
| 16 | Tooltip icon renders even when field has tooltipText but no helpText | VERIFIED | TextFieldMD3 guard changed to `(helpText \|\| helpTextPrefix) && !error` (line 67); Test 11 "renders helpTextPrefix alone when helpText is absent" passes |
**Score: 16/16 truths verified**
---
### Plan 09-05 Gap Closure — Truths #13#16
**Previously:** COMP-04 had 4 FAILED truths related to tooltip behavior and icon placement identified in UAT session (test 7).
**Now:** All 4 fixed in commit `23617c0` (feat) and `d2b6da2` (test):
- `TextFieldMD3.tsx` — added `helpTextPrefix?: React.ReactNode` prop; guard changed to `(helpText || helpTextPrefix) && !error`; renders `<div className="flex items-start gap-1">` wrapping prefix + helpText text
- `FieldRenderer.tsx` — added `hoverTooltip` state; moved tooltip icon from above-field to `tooltipIcon` element passed as `helpTextPrefix`; select-branch also gains hover support; click-unpin clears hoverTooltip
- `PasswordField.tsx` — same dual-state tooltip fix as FieldRenderer text-branch
7 new tests added (4 in FieldRenderer.test.tsx hover suite, 3 in TextFieldMD3.test.tsx Tests 9-11). All 186 tests pass.
---
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `src/components/ui/TextFieldMD3.tsx` | MD3 outlined text field with CSS floating label and helpTextPrefix slot | VERIFIED | 82 lines; exports `TextFieldMD3`; `helpTextPrefix?: React.ReactNode` in interface (line 10); guard `(helpText \|\| helpTextPrefix) && !error` (line 67) |
| `src/components/ui/TextFieldMD3.test.tsx` | Unit tests including helpTextPrefix behavior | VERIFIED | 11 tests (Tests 1-11); Tests 9-11 cover helpTextPrefix combinations |
| `src/styles/md3-buttons.ts` | MD3 button class constants | VERIFIED | Exports `MD3_BTN_FILLED`, `MD3_BTN_OUTLINED`, `MD3_BTN_TEXT` as string constants |
| `src/components/ui/FieldRenderer.test.tsx` | Tests for DEBT-01 aria-label consistency and tooltip hover behavior | VERIFIED | 8 tests total — 4 hover behavior tests + 4 DEBT-01 aria-label tests |
| `src/components/wizard/StepIndicator.tsx` | Rebuilt MD3 step indicator with circles and connectors | VERIFIED | Exports `StepIndicator`, uses `useWizard()` for state and dispatch |
| `src/components/ui/FieldRenderer.tsx` | Text-branch using TextFieldMD3 with tooltip below field via helpTextPrefix | VERIFIED | `hoverTooltip` state (line 15); `tooltipIcon` element (lines 86-97); `helpTextPrefix={tooltipIcon}` (line 110); select-branch has `onMouseEnter`/`onMouseLeave` (lines 53-54) |
| `src/components/ui/PasswordField.tsx` | Password field with tooltip below via helpTextPrefix and hover+click behavior | VERIFIED | `hoverTooltip` state (line 17); `tooltipIcon` element (lines 30-41); `helpTextPrefix={tooltipIcon}` (line 54); `tooltipVisible = showTooltip \|\| hoverTooltip` (line 43) |
| `src/components/ui/BackendCard.tsx` | MD3 elevation and shape | VERIFIED | `rounded-xl` on line 17; `shadow` / `shadow-md` / `hover:shadow-md` |
| `src/components/wizard/BackendSelectionStep.tsx` | All text inputs use TextFieldMD3 | VERIFIED | Imports `TextFieldMD3` and `MD3_BTN_FILLED`; TextFieldMD3 rendered for Remote name |
---
### Key Link Verification
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `TextFieldMD3.tsx` | `react-hook-form` | `{...registration}` spread onto input | VERIFIED | Line 43: `{...registration}` spreads `UseFormRegisterReturn` onto `<input>` |
| `FieldRenderer.tsx` | `TextFieldMD3.tsx` | `import { TextFieldMD3 }` + render with `helpTextPrefix` | VERIFIED | Line 5 import; line 103 renders `<TextFieldMD3 helpTextPrefix={tooltipIcon}>` |
| `PasswordField.tsx` | `TextFieldMD3.tsx` | `import { TextFieldMD3 }` + render with `helpTextPrefix` and `suffix` | VERIFIED | Line 3 import; line 47 renders `<TextFieldMD3 helpTextPrefix={tooltipIcon} suffix={toggleButton}>` |
| `FieldRenderer.tsx` | tooltip hover state | `hoverTooltip` + `onMouseEnter`/`onMouseLeave` on `tooltipIcon` button | VERIFIED | Lines 15, 90-91: dual-state `showTooltip \|\| hoverTooltip`; click-unpin clears hover state (line 89) |
| `PasswordField.tsx` | tooltip hover state | `hoverTooltip` + `onMouseEnter`/`onMouseLeave` on `tooltipIcon` button | VERIFIED | Lines 17, 34-35: same dual-state pattern as FieldRenderer |
| `TextFieldMD3.tsx` | `helpTextPrefix` slot | `(helpText \|\| helpTextPrefix) && !error` guard + flex wrapper | VERIFIED | Line 67: guard enables prefix-only rendering; line 68-73: `<div className="flex items-start gap-1">` wraps prefix and helpText |
| `BackendSelectionStep.tsx` | `src/styles/md3-buttons.ts` | `import { MD3_BTN_FILLED }` | VERIFIED | Applied to Next button |
| `RemoteConfigStep.tsx` | `src/styles/md3-buttons.ts` | `import { MD3_BTN_FILLED, MD3_BTN_OUTLINED }` | VERIFIED | Applied to Back/Next buttons |
| `DeploymentStep.tsx` | `src/styles/md3-buttons.ts` | `import { MD3_BTN_FILLED, MD3_BTN_OUTLINED }` | VERIFIED | Applied to Back/Next buttons |
| `ReviewStep.tsx` | `src/styles/md3-buttons.ts` | `import { MD3_BTN_FILLED, MD3_BTN_OUTLINED }` | VERIFIED | Back = OUTLINED, Download All = FILLED |
| `OutputBlock.tsx` | `src/styles/md3-buttons.ts` | `import { MD3_BTN_OUTLINED }` | VERIFIED | `smallBtn` constant uses `MD3_BTN_OUTLINED` with size overrides |
| `StepIndicator.tsx` | `src/store/context.tsx` | `useWizard()` for state.currentStep and dispatch | VERIFIED | `const { state, dispatch } = useWizard()` |
---
### Requirements Coverage
| Requirement | Source Plan(s) | Description | Status | Evidence |
|-------------|---------------|-------------|--------|----------|
| COMP-01 | 09-01, 09-03, 09-04 | All text inputs render as MD3 outlined text fields with floating labels that animate on focus and when the field has content | VERIFIED | FieldRenderer text-branch, PasswordField, and BackendSelectionStep all use TextFieldMD3. No plain label+input blocks remain in wizard step files. |
| COMP-02 | 09-03 | Buttons follow MD3 hierarchy — filled for primary actions, outlined for secondary | VERIFIED | All 4 wizard steps + OutputBlock import and apply `MD3_BTN_FILLED` / `MD3_BTN_OUTLINED` correctly |
| COMP-03 | 09-03 | Cards and output blocks use MD3 elevation with shape tokens | VERIFIED | BackendCard: `rounded-xl` + `shadow`/`shadow-md`; OutputBlock pre: `rounded-xl shadow-sm` |
| COMP-04 | 09-02, 09-05 | Step indicator shows numbered circles with checkmarks, highlighted current, muted future; tooltip icon beside helpText below field, hover reveals tooltip | VERIFIED | StepIndicator rebuilt with circles, connector lines, semantic tokens; tooltip placement and hover behavior fixed in Plan 09-05 — all 4 gap truths (#13#16) now pass; 186/186 tests pass |
| DEBT-01 | 09-01 | FieldRenderer uses consistent aria-label pattern across text-branch and select-branch | VERIFIED | Both branches and PasswordField use `aria-label={\`More info about \${field.label}\`}` pattern; 4/4 tests pass |
**REQUIREMENTS.md Traceability Cross-Check:**
All 5 IDs (COMP-01, COMP-02, COMP-03, COMP-04, DEBT-01) are mapped to Phase 9 in REQUIREMENTS.md with status "Complete". All 5 are VERIFIED with full evidence. No orphaned requirements found.
---
### Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `src/components/wizard/ReviewStep.tsx` | 16, 27 | `PLACEHOLDER` constant | Info | Legitimate empty-state UX string, not a stub — renders when config cannot be built |
No TODO/FIXME/HACK comments found in plan 09-05 modified files. No hardcoded hex color codes. No `return null` / `return {}` stubs.
---
### Human Verification Required
#### 1. Floating Label CSS Animation
**Test:** Navigate to BackendSelectionStep (step 1). Click into the Remote name field, then click away without typing. Then type a value, blur, then clear it.
**Expected:** On focus, the label smoothly translates upward and scales to 75%. When a value is present and the field is blurred, the label stays floated. When the value is cleared, the label returns to center. Verify the same behavior in RemoteConfigStep and PasswordField.
**Why human:** JSDOM does not compute CSS transitions or `:placeholder-shown` pseudo-class behavior. The Tailwind `peer-[:not(:placeholder-shown)]` arbitrary variant is untested by jsdom.
#### 2. MD3 Button Visual Appearance
**Test:** Navigate through all wizard steps and observe button appearances at each step.
**Expected:** Next/Download-All = pill shape with solid primary background color. Back/Copy/Download = pill shape with a visible outline border and no fill. Disabled state shows reduced opacity.
**Why human:** `MD3_BTN_*` class strings are applied correctly (verified), but the visual rendering of semantic tokens (`bg-primary`, `border-outline`, etc.) requires actual CSS custom property resolution in a browser.
#### 3. StepIndicator Visual States in Browser
**Test:** Complete step 1 (Backend selection) and observe the step indicator.
**Expected:** Step 1 circle shows a checkmark and is clickable. Step 2 circle is highlighted with a primary color border/background. Steps 3 and 4 appear muted. The connector line between steps 1 and 2 is filled (primary color); between 2-3 and 3-4 it is muted.
**Why human:** Semantic token resolution and visual differentiation of states requires browser rendering.
#### 4. Dark Mode Consistency
**Test:** Toggle to dark mode and navigate through all wizard steps.
**Expected:** All MD3 components render correctly with no hardcoded colors bleeding through. Floating label color changes on focus/error still work. Card shadows remain visible but subtle.
**Why human:** CSS custom property values under the dark theme class cannot be verified without rendering.
#### 5. Tooltip Icon Placement (Visual)
**Test:** Open a step that has fields with `tooltipText` (e.g., RemoteConfigStep with Azure Blob SAS URL field). Inspect the position of the ⓘ icon relative to the input and any helpText.
**Expected:** The ⓘ icon appears below the input, inline to the left of the helpText (or alone below the input if there is no helpText). It does NOT appear above the input floating-label area.
**Why human:** The `helpTextPrefix` slot renders inside a `<div className="flex items-start gap-1">` below the input — flexbox layout and visual position must be confirmed in a real browser.
#### 6. Tooltip Hover and Pin Behavior (Manual)
**Test:** Move the mouse over the ⓘ icon on a field with `tooltipText`. Then move the mouse away. Then click and move away. Then click again while still hovering.
**Expected:** Hover shows tooltip, mouse-leave hides it. Click pins it open so mouse-leave no longer hides it. Second click while hovering dismisses the tooltip completely (no sticky state).
**Why human:** `fireEvent` in JSDOM verifies state transitions correctly, but actual mouse pointer UX (timing, visual feedback of hover vs. pinned state, tooltip readability at that position) requires manual validation in a browser.
---
### Summary
Plan 09-05 (commits `23617c0` and `d2b6da2`) fully closed all 4 tooltip-related gaps identified in the UAT session:
1. `helpTextPrefix` prop added to TextFieldMD3 — enables inline slot left of helpText below the input.
2. Guard changed to `(helpText || helpTextPrefix) && !error` — tooltip icon renders even without helpText.
3. Tooltip icon moved from above-field position to `helpTextPrefix` slot below the input in both FieldRenderer and PasswordField.
4. Dual-state hover+click: `onMouseEnter`/`onMouseLeave` drive `hoverTooltip`; click toggles `showTooltip`; click-unpin explicitly clears `hoverTooltip` to prevent sticky-after-dismiss edge case.
5. Select-branch in FieldRenderer also received hover support (onMouseEnter/onMouseLeave).
All 16 must-have truths are VERIFIED. All 5 requirements (COMP-01, COMP-02, COMP-03, COMP-04, DEBT-01) are fully satisfied. The full test suite (186 tests across 15 files) passes with zero regressions.
Six items remain flagged for human verification — all relate to CSS animation, visual token resolution, visual tooltip placement, and interactive hover feel that cannot be confirmed programmatically.
---
_Verified: 2026-04-01T11:30:00Z_
_Verifier: Claude (gsd-verifier)_
_Re-verification after gap closure via Plan 09-05 (commits 23617c0, d2b6da2)_
@@ -0,0 +1,199 @@
---
phase: 10-content-clarity
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/App.tsx
- src/App.test.tsx
- src/components/wizard/BackendSelectionStep.tsx
- src/components/wizard/RemoteConfigStep.tsx
- src/components/wizard/DeploymentStep.tsx
- src/components/wizard/ReviewStep.tsx
autonomous: true
requirements:
- UX-01
- UX-03
must_haves:
truths:
- "First-time visitor sees an intro section explaining what Ready2Blob does before the wizard loads"
- "Clicking Get Started hides the intro and reveals Step 1 with StepIndicator"
- "On page reload, intro shows again (no persistence)"
- "Each of the 4 wizard steps has a 1-2 sentence description below the heading"
artifacts:
- path: "src/App.tsx"
provides: "IntroSection component and showIntro state gate in WizardShell"
contains: "showIntro"
- path: "src/App.test.tsx"
provides: "Tests for intro rendering and Get Started button behavior"
contains: "Get Started"
- path: "src/components/wizard/BackendSelectionStep.tsx"
provides: "Step 1 description paragraph"
contains: "text-sm"
- path: "src/components/wizard/RemoteConfigStep.tsx"
provides: "Step 2 description paragraph"
contains: "text-sm"
- path: "src/components/wizard/DeploymentStep.tsx"
provides: "Step 3 description paragraph"
contains: "text-sm"
- path: "src/components/wizard/ReviewStep.tsx"
provides: "Step 4 description paragraph"
contains: "text-sm"
key_links:
- from: "src/App.tsx"
to: "IntroSection"
via: "showIntro state toggle"
pattern: "setShowIntro\\(false\\)"
- from: "src/App.tsx"
to: "StepIndicator"
via: "conditional render when showIntro is false"
pattern: "showIntro.*StepIndicator"
---
<objective>
Add the intro section that greets first-time visitors and step descriptions for all 4 wizard steps.
Purpose: A first-time user immediately understands what Ready2Blob does and what each step expects, without external documentation.
Output: IntroSection in App.tsx with CTA, step description paragraphs in all 4 step components, updated App.test.tsx.
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/10-content-clarity/10-CONTEXT.md
@.planning/phases/10-content-clarity/10-RESEARCH.md
<interfaces>
<!-- Key types and contracts the executor needs -->
From src/App.tsx (current structure):
```tsx
function WizardShell() {
const { state } = useWizard();
// steps array, stepIndex clamped, renders StepIndicator + CurrentStep
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>
<StepIndicator />
<div className="mt-8">{CurrentStep}</div>
</div>
</div>
);
}
```
From src/App.test.tsx (line 55-63 — WILL BREAK, must update):
```tsx
it('renders BackendSelectionStep when currentStep is 0', () => {
render(<App />);
const heading = screen.getByText(/Select Backend/);
expect(heading).toBeDefined();
const stepNav = screen.getByRole('navigation', { name: /Wizard steps/i });
expect(stepNav).toBeDefined();
});
```
Note: The renderAtStep() helper bypasses WizardShell entirely, so step 1/2 tests are UNAFFECTED.
MD3 button constant available:
```tsx
import { MD3_BTN_FILLED } from '../components/ui/TextFieldMD3'; // or wherever exported
```
Check actual export location — may be in a shared constants file.
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add IntroSection and showIntro gate to App.tsx</name>
<files>src/App.tsx</files>
<action>
Add `useState` import from React. Add `showIntro` local state (default `true`) to `WizardShell`.
When `showIntro` is true, render an IntroSection inline (can be a local component or JSX block) INSTEAD of StepIndicator + step content. The intro section must:
- Keep the existing header layout (h1 "Ready2Blob" + ThemeToggle) at the top
- Show action-first copy: "Go from zero to a deployable rclone setup in minutes" per user decision
- Mention "Azure Blob, S3, OneDrive, and 4 more cloud backends" per user decision
- Briefly explain what the tool generates (rclone config + deployment scripts)
- Show a "Get Started" CTA button using MD3_BTN_FILLED style that sets `showIntro(false)`
- Use MD3 tokens for all styling: `bg-surface`, `text-on-surface`, `bg-primary`, `text-on-primary`, etc.
- No persistence — on page reload, intro shows again (wizard state is ephemeral)
- No collapse/shrink behavior — intro simply disappears when wizard starts
When `showIntro` is false, render the existing wizard content (StepIndicator + step) unchanged.
IMPORTANT: Do NOT add intro state to useReducer/WizardState. This is purely local UI state via useState.
IMPORTANT: Check where MD3_BTN_FILLED is exported from — grep for it. Use the correct import path.
</action>
<verify>
<automated>npx vitest run src/App.test.tsx</automated>
</verify>
<done>App renders IntroSection on initial load with "Get Started" button. Clicking it reveals the wizard. Page reload shows intro again.</done>
</task>
<task type="auto">
<name>Task 2: Update App.test.tsx and add step descriptions to all 4 step components</name>
<files>src/App.test.tsx, src/components/wizard/BackendSelectionStep.tsx, src/components/wizard/RemoteConfigStep.tsx, src/components/wizard/DeploymentStep.tsx, src/components/wizard/ReviewStep.tsx</files>
<action>
**App.test.tsx updates:**
The existing test "renders BackendSelectionStep when currentStep is 0" will fail because `<App />` now shows IntroSection first. Update this test to:
1. First assert that IntroSection content is visible (e.g., check for "Get Started" button or the intro copy)
2. Click "Get Started"
3. THEN assert "Select Backend" heading and StepIndicator navigation are visible
Add a new test:
- "shows intro section on initial render" — render `<App />`, assert intro copy is present, assert StepIndicator is NOT in the DOM
The renderAtStep() tests (step 1, step 2) bypass WizardShell entirely so they need NO changes.
**Step descriptions (UX-03):**
Add a `<p>` element immediately after the `<h2>` in each of the 4 step components. Use className `text-sm text-on-surface-variant mt-1 mb-4`. Claude has discretion on wording, but each must be 1-2 sentences explaining what the user is doing and why. Suggested tone — direct, practical, no jargon:
- BackendSelectionStep: Explain that the user names their remote and picks a cloud storage provider
- RemoteConfigStep: Explain that the user enters credentials for the selected backend
- DeploymentStep: Explain that the user chooses how the config gets deployed to the target machine
- ReviewStep: Explain that the user reviews generated files and downloads them
IMPORTANT: Do NOT change any heading text (h2 content must remain identical). Only ADD a `<p>` after the `<h2>`.
IMPORTANT: Use `text-on-surface-variant` (not `text-on-surface-container/70`) — this is the standard MD3 secondary text token.
</action>
<verify>
<automated>npx vitest run</automated>
</verify>
<done>All tests pass. Intro renders on initial App load, clicking Get Started reveals wizard. All 4 steps show a description paragraph below the heading.</done>
</task>
</tasks>
<verification>
1. `npx vitest run` — full suite green
2. IntroSection visible on initial `<App />` render
3. "Get Started" click transitions to wizard with StepIndicator
4. Each step component has a descriptive `<p>` after its `<h2>`
</verification>
<success_criteria>
- First-time visitor sees intro with "Get Started" CTA before wizard
- Clicking CTA reveals Step 1 with StepIndicator
- Page reload brings back intro (no persistence)
- All 4 steps have description text below headings
- All existing tests pass (including updated App.test.tsx)
</success_criteria>
<output>
After completion, create `.planning/phases/10-content-clarity/10-01-SUMMARY.md`
</output>
@@ -0,0 +1,140 @@
---
phase: 10-content-clarity
plan: 01
subsystem: ui
tags: [react, tailwind, md3, wizard, intro-section, step-descriptions]
# Dependency graph
requires:
- phase: 09-md3-components
provides: MD3_BTN_FILLED constant, MD3 token system, TextFieldMD3 components
provides:
- IntroSection component with "Get Started" CTA in App.tsx
- showIntro local state gate controlling wizard visibility
- Step description paragraphs in all 4 wizard steps
- Updated App.test.tsx covering intro render + Get Started flow
affects:
- 10-content-clarity (plans 02, 03 build on step component structure)
# Tech tracking
tech-stack:
added: []
patterns:
- "Local showIntro state in WizardShell (not in WizardReducer) — ephemeral UI state pattern"
- "IntroSection as local inline component inside App.tsx — no extra file for small co-located UI"
- "Step descriptions use text-sm text-on-surface-variant mt-1 mb-4 — standard MD3 secondary text"
key-files:
created: []
modified:
- src/App.tsx
- src/App.test.tsx
- src/components/wizard/BackendSelectionStep.tsx
- src/components/wizard/RemoteConfigStep.tsx
- src/components/wizard/DeploymentStep.tsx
- src/components/wizard/ReviewStep.tsx
key-decisions:
- "showIntro is local useState in WizardShell — not persisted, resets on page reload by design"
- "IntroSection declared as a local component in App.tsx (not a separate file) — appropriate for small co-located UI"
- "ReviewStep had no h2 heading — added 'Step 4: Review & Download' before the description paragraph"
patterns-established:
- "Step description: <p className='text-sm text-on-surface-variant mt-1 mb-4'> immediately after <h2>"
requirements-completed:
- UX-01
- UX-03
# Metrics
duration: 7min
completed: 2026-04-01
---
# Phase 10 Plan 01: Content Clarity — Intro & Step Descriptions Summary
**Intro section with Get Started CTA gates the wizard on first load; all 4 wizard steps now display 1-2 sentence context descriptions below their headings**
## Performance
- **Duration:** ~7 min
- **Started:** 2026-04-01T10:48:20Z
- **Completed:** 2026-04-01T10:54:47Z
- **Tasks:** 2
- **Files modified:** 6
## Accomplishments
- Added IntroSection component to App.tsx with action-first copy, backend list mention, and rclone.conf/script generation explanation
- Get Started button (MD3_BTN_FILLED) toggles showIntro state — intro disappears, wizard appears; no persistence
- All 4 wizard steps now have a descriptive paragraph after the h2 using MD3 secondary text styling
- Updated App.test.tsx: new "shows intro section on initial render" test + updated existing test to click Get Started before asserting wizard content
## Task Commits
Each task was committed atomically:
1. **Task 1: Add IntroSection and showIntro gate to App.tsx** - `c1d1633` (feat)
2. **Task 2: Update App.test.tsx and add step descriptions** - `4b837b1` (feat)
**Plan metadata:** _(pending final commit)_
## Files Created/Modified
- `src/App.tsx` - Added useState import, showIntro local state, IntroSection component, conditional render
- `src/App.test.tsx` - New intro test, updated BackendSelectionStep test, added fireEvent import
- `src/components/wizard/BackendSelectionStep.tsx` - Description paragraph after h2
- `src/components/wizard/RemoteConfigStep.tsx` - Description paragraph after h2
- `src/components/wizard/DeploymentStep.tsx` - Description paragraph after h2
- `src/components/wizard/ReviewStep.tsx` - Added missing h2 heading + description paragraph
## Decisions Made
- `showIntro` stored in local `useState` inside WizardShell — not in the WizardReducer. The plan explicitly required "Do NOT add intro state to useReducer/WizardState". This is purely ephemeral UI state.
- IntroSection is a local inline component in App.tsx rather than a separate file — appropriate for its small size and tight coupling to WizardShell.
- ReviewStep had no h2 heading at all. Added "Step 4: Review & Download" before the description paragraph to be consistent with the other 3 steps and satisfy "Add a `<p>` element immediately after the `<h2>`".
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] ReviewStep missing h2 heading**
- **Found during:** Task 2 (adding step descriptions)
- **Issue:** ReviewStep had no `<h2>` element. The plan said "Add a `<p>` element immediately after the `<h2>`" but there was no h2 to add after. All other 3 steps had their h2.
- **Fix:** Added `<h2>Step 4: Review &amp; Download</h2>` before the description paragraph.
- **Files modified:** `src/components/wizard/ReviewStep.tsx`
- **Verification:** Tests pass, consistent structure across all 4 steps.
- **Committed in:** `4b837b1` (Task 2 commit)
---
**Total deviations:** 1 auto-fixed (missing h2 in ReviewStep)
**Impact on plan:** Necessary to fulfill the requirement as written. No scope creep.
## Issues Encountered
**Pre-existing test failures (deferred, out of scope):** 8 tests in `RemoteConfigStep.test.tsx` occasionally timeout for GCS/B2 fields and S3/GCS validation. Root cause: uncommitted working-tree changes to `registry.ts` (tooltip texts added in a prior session) combined with the updated test selectors. These failures are unrelated to plan 10-01 and do not affect any of the 6 target files. Logged to `deferred-items.md`.
Final full test run: 202/202 passing.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- IntroSection is live, Get Started CTA works, wizard is hidden behind it on initial load
- All 4 steps have consistent h2 + description paragraph structure
- Plan 10-02 can proceed (next content clarity improvement)
---
*Phase: 10-content-clarity*
*Completed: 2026-04-01*
## Self-Check: PASSED
- `src/App.tsx` — FOUND
- `src/App.test.tsx` — FOUND
- `src/components/wizard/BackendSelectionStep.tsx` — FOUND
- `src/components/wizard/RemoteConfigStep.tsx` — FOUND
- `src/components/wizard/DeploymentStep.tsx` — FOUND
- `src/components/wizard/ReviewStep.tsx` — FOUND
- `.planning/phases/10-content-clarity/10-01-SUMMARY.md` — FOUND
- Commit `c1d1633` — FOUND
- Commit `4b837b1` — FOUND
@@ -0,0 +1,179 @@
---
phase: 10-content-clarity
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- src/components/ui/RemoteNamePreview.tsx
- src/components/ui/RemoteNamePreview.test.tsx
- src/components/wizard/BackendSelectionStep.tsx
- src/components/wizard/BackendSelectionStep.test.tsx
autonomous: true
requirements:
- UX-02
must_haves:
truths:
- "Remote name field displays help text explaining what the name is and giving examples"
- "A live config preview below the field updates as user types, showing [remote-name] syntax"
- "When field is empty, preview shows grayed-out [my-remote] placeholder with guidance text"
- "When field has content, preview shows [typed-value] in monospace code style"
artifacts:
- path: "src/components/ui/RemoteNamePreview.tsx"
provides: "Stateless display component for rclone config preview"
exports: ["RemoteNamePreview"]
min_lines: 15
- path: "src/components/ui/RemoteNamePreview.test.tsx"
provides: "Tests for empty-state and value-state rendering"
contains: "RemoteNamePreview"
- path: "src/components/wizard/BackendSelectionStep.tsx"
provides: "watch('name') integration and RemoteNamePreview placement"
contains: "RemoteNamePreview"
key_links:
- from: "src/components/wizard/BackendSelectionStep.tsx"
to: "src/components/ui/RemoteNamePreview.tsx"
via: "watch('name') value passed as prop"
pattern: "watch\\('name'\\)"
- from: "src/components/wizard/BackendSelectionStep.tsx"
to: "TextFieldMD3"
via: "helpText prop on remote name field"
pattern: "helpText="
---
<objective>
Add a live rclone config preview below the remote name field and enrich the field with help text.
Purpose: Users immediately see how their chosen name will appear in the generated rclone.conf, reducing confusion about naming conventions.
Output: RemoteNamePreview component, integrated into BackendSelectionStep with watch(), helpText on remote name field.
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/10-content-clarity/10-CONTEXT.md
@.planning/phases/10-content-clarity/10-RESEARCH.md
<interfaces>
<!-- Key contracts the executor needs -->
From src/components/wizard/BackendSelectionStep.tsx (existing useForm destructure):
```tsx
const {
register,
handleSubmit,
formState: { errors },
} = useForm<RemoteNameFormValues>({
resolver: zodResolver(remoteNameSchema),
mode: 'onSubmit',
reValidateMode: 'onChange',
defaultValues: { name: state.remote.name },
});
```
Add `watch` to the destructure: `const { register, handleSubmit, watch, formState: { errors } } = useForm<...>(...)`
Then: `const remoteName = watch('name');`
From src/components/ui/TextFieldMD3.tsx (props interface — supports helpText):
```tsx
// TextFieldMD3 accepts helpText prop — renders as small text below input
// CRITICAL: Do NOT pass a visible placeholder prop. TextFieldMD3 uses placeholder=" "
// (single space) for the floating label CSS trick. The "e.g. my-backup" example
// must be conveyed through helpText, NOT native placeholder.
```
MD3 token classes for preview block (same as ReviewStep OutputBlock):
- `bg-surface-variant` — tinted background
- `text-on-surface-variant` — readable text on that background
- `font-mono` — code appearance
From BackendSelectionStep.test.tsx (selector pattern):
```tsx
// Uses screen.getByRole('textbox') — single textbox assertion
// RemoteNamePreview MUST NOT render any form controls (input, textarea, contenteditable)
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Create RemoteNamePreview component with tests</name>
<files>src/components/ui/RemoteNamePreview.tsx, src/components/ui/RemoteNamePreview.test.tsx</files>
<behavior>
- When value is empty string or whitespace-only, renders "[my-remote]" as placeholder with "Type a name to see how it appears in your config" guidance text
- When value is "my-backup", renders "[my-backup]" in monospace style
- When value is "azure-prod", renders "[azure-prod]"
- Component renders only display elements (div, span) — never form controls
</behavior>
<action>
Create `RemoteNamePreview` as a stateless functional component accepting `{ value: string }`.
Empty state: Show grayed-out `[my-remote]` text with a guidance message "Type a name to see how it appears in your config" below it. Use `text-on-surface-variant/50` for the placeholder and `text-on-surface-variant/40` for the guidance.
Value state: Show `[{value}]` in `text-on-surface-variant` color.
Container styling: `mt-2 rounded-md bg-surface-variant px-3 py-2 text-xs font-mono`
Write tests FIRST (RED), then implement (GREEN). Test file uses `@testing-library/react` render + screen assertions.
CRITICAL: RemoteNamePreview must render ONLY div/span elements. No input, textarea, or contenteditable — this would break BackendSelectionStep.test.tsx's `getByRole('textbox')` single-match assertion.
</action>
<verify>
<automated>npx vitest run src/components/ui/RemoteNamePreview.test.tsx</automated>
</verify>
<done>RemoteNamePreview renders empty-state placeholder when value is empty, and [value] when value is provided. All tests pass.</done>
</task>
<task type="auto">
<name>Task 2: Integrate RemoteNamePreview and helpText into BackendSelectionStep</name>
<files>src/components/wizard/BackendSelectionStep.tsx, src/components/wizard/BackendSelectionStep.test.tsx</files>
<action>
In BackendSelectionStep.tsx:
1. Add `watch` to the useForm destructure
2. Add `const remoteName = watch('name');` after the useForm call
3. Add `helpText` prop to the remote name TextFieldMD3: `"This becomes the section header [name] in your rclone.conf. Example: azure-prod, backup-s3. Letters, numbers, dashes, underscores only."` (per user decision)
4. Import and render `<RemoteNamePreview value={remoteName} />` directly below the TextFieldMD3
In BackendSelectionStep.test.tsx:
Add a test case that verifies the live preview updates:
1. Render BackendSelectionStep
2. Assert that the empty-state placeholder text is visible (e.g., "[my-remote]")
3. Type "test-remote" into the textbox
4. Assert that "[test-remote]" is visible in the DOM
IMPORTANT: Do NOT add a visible `placeholder` prop to TextFieldMD3. The floating label CSS trick depends on `placeholder=" "`. The "e.g. my-backup" example is conveyed via helpText.
IMPORTANT: Do NOT use `useWatch` — plain `watch('name')` from useForm is correct here.
</action>
<verify>
<automated>npx vitest run src/components/wizard/BackendSelectionStep.test.tsx</automated>
</verify>
<done>Remote name field shows helpText. RemoteNamePreview renders below the field and updates live as user types. All BackendSelectionStep tests pass.</done>
</task>
</tasks>
<verification>
1. `npx vitest run src/components/ui/RemoteNamePreview.test.tsx` — preview component tests pass
2. `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` — integration tests pass
3. `npx vitest run` — full suite green
4. Empty field shows [my-remote] placeholder with guidance text
5. Typing a name shows [typed-name] in real-time
</verification>
<success_criteria>
- RemoteNamePreview component exists and is tested
- BackendSelectionStep shows helpText on remote name field
- Live preview below field updates as user types
- Empty state shows grayed placeholder with guidance
- No existing tests broken (especially getByRole('textbox') single match)
</success_criteria>
<output>
After completion, create `.planning/phases/10-content-clarity/10-02-SUMMARY.md`
</output>
@@ -0,0 +1,90 @@
---
phase: 10-content-clarity
plan: "02"
subsystem: wizard-ui
tags: [live-preview, ux, form-fields, tdd]
dependency_graph:
requires: []
provides: [RemoteNamePreview component, live config preview in BackendSelectionStep]
affects: [src/components/wizard/BackendSelectionStep.tsx]
tech_stack:
added: []
patterns: [stateless display component, react-hook-form watch(), TDD red-green]
key_files:
created:
- src/components/ui/RemoteNamePreview.tsx
- src/components/ui/RemoteNamePreview.test.tsx
modified:
- src/components/wizard/BackendSelectionStep.tsx
- src/components/wizard/BackendSelectionStep.test.tsx
- src/components/wizard/RemoteConfigStep.test.tsx
decisions:
- "RemoteNamePreview renders only div/span — no form controls, preserving single-textbox selector contract"
- "watch('name') from useForm used for live preview — no useWatch, per plan spec"
- "helpText uses full sentence explaining rclone.conf syntax and naming constraints"
metrics:
duration_minutes: 8
tasks_completed: 2
files_created: 2
files_modified: 3
completed_date: "2026-04-01"
requirements_satisfied: [UX-02]
---
# Phase 10 Plan 02: Live Remote Name Preview Summary
**One-liner:** Stateless RemoteNamePreview component with empty/value states integrated into BackendSelectionStep via watch('name') and enriched with helpText.
## Tasks Completed
| # | Task | Commit | Status |
|---|------|--------|--------|
| 1 | Create RemoteNamePreview component with TDD | 3c9c5fd | Done |
| 2 | Integrate RemoteNamePreview and helpText into BackendSelectionStep | fefab65 | Done |
## What Was Built
**RemoteNamePreview component** (`src/components/ui/RemoteNamePreview.tsx`):
- Stateless functional component accepting `{ value: string }`
- Empty/whitespace state: displays grayed `[my-remote]` placeholder with "Type a name to see how it appears in your config" guidance
- Value state: displays `[typed-value]` in on-surface-variant color
- Container styling: `mt-2 rounded-md bg-surface-variant px-3 py-2 text-xs font-mono`
- Renders only div/span elements — never form controls
**BackendSelectionStep integration**:
- Added `watch` to useForm destructure
- Added `const remoteName = watch('name')` for reactive updates
- Added `helpText` prop to remote name TextFieldMD3: full sentence explaining rclone.conf [name] syntax
- Added `<RemoteNamePreview value={remoteName} />` below the TextFieldMD3
- Note: description paragraph and RemoteNamePreview import were committed as part of Plan 10-01 linter sweep (commit 4b837b1)
## Tests
- `RemoteNamePreview.test.tsx`: 9 tests (empty state x3, value state x3, element type constraints x3)
- `BackendSelectionStep.test.tsx`: 2 new integration tests (empty-state placeholder, live update on type)
- Full suite: 202 tests pass
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] Fixed getByText(/required/i) ambiguity in BackendSelectionStep.test.tsx**
- **Found during:** Task 2
- **Issue:** New description paragraph ("...credentials are required in the next step") caused `getByText(/required/i)` to match two elements, breaking existing "shows inline error" test
- **Fix:** Changed to `screen.getByRole('alert').textContent.toMatch(/required/i)` for precise alert text assertion
- **Files modified:** `src/components/wizard/BackendSelectionStep.test.tsx`
- **Commit:** fefab65
**2. [Rule 1 - Bug] Fixed getByLabelText ambiguity in RemoteConfigStep.test.tsx**
- **Found during:** Task 2 (full suite run)
- **Issue:** Linter-added paragraphs in RemoteConfigStep, combined with partial `{ selector: 'input' }` fix from 10-03 commits, left GCS, B2, and S3-region VALID-01 tests failing with "Found multiple elements"
- **Fix:** Added `{ selector: 'input' }` to all remaining `getByLabelText` calls in BACK-03, BACK-04, and VALID-01 sections
- **Files modified:** `src/components/wizard/RemoteConfigStep.test.tsx`
- **Commit:** fefab65 (included in Task 2 commit via linter application)
## Self-Check: PASSED
- src/components/ui/RemoteNamePreview.tsx: FOUND
- src/components/ui/RemoteNamePreview.test.tsx: FOUND
- Commit 3c9c5fd (Task 1): FOUND
- Commit fefab65 (Task 2): FOUND
@@ -0,0 +1,191 @@
---
phase: 10-content-clarity
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
- src/schemas/registry.ts
- src/components/wizard/RemoteConfigStep.test.tsx
autonomous: true
requirements:
- UX-04
must_haves:
truths:
- "S3 access_key_id and secret_access_key fields have tooltipText and helpText"
- "S3-compatible access_key_id, secret_access_key, and endpoint fields have tooltipText"
- "GCS project_number and service_account_credentials fields have tooltipText"
- "B2 account and key fields have tooltipText"
- "OneDrive drive_id field has tooltipText"
- "SFTP host and user do NOT get tooltipText (self-explanatory per user decision)"
- "Existing Azure Blob, OneDrive token, SFTP pass/key_pem help text is NOT rewritten"
artifacts:
- path: "src/schemas/registry.ts"
provides: "Enriched FieldDef entries with tooltipText and helpText"
contains: "tooltipText"
- path: "src/components/wizard/RemoteConfigStep.test.tsx"
provides: "Tests verifying tooltip buttons render for newly enriched fields"
contains: "tooltip"
key_links:
- from: "src/schemas/registry.ts"
to: "src/components/ui/FieldRenderer.tsx"
via: "tooltipText and helpText consumed by FieldRenderer rendering"
pattern: "tooltipText"
- from: "src/schemas/registry.ts"
to: "src/components/ui/PasswordField.tsx"
via: "tooltipText consumed by PasswordField tooltip rendering"
pattern: "tooltipText"
---
<objective>
Add contextual help text and tooltips to all backend credential fields that currently lack them.
Purpose: Users know exactly what to enter in each field and where to find the value in their cloud provider's console, without needing external documentation.
Output: Enriched registry.ts with tooltipText/helpText for S3, S3-compatible, GCS, B2, OneDrive drive_id fields. New tests verifying tooltip buttons render.
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/10-content-clarity/10-CONTEXT.md
@.planning/phases/10-content-clarity/10-RESEARCH.md
<interfaces>
<!-- Key contracts the executor needs -->
From src/schemas/registry.ts:
```typescript
export interface FieldDef {
key: string;
label: string;
inputType: 'text' | 'password' | 'select' | 'toggle';
required: boolean;
placeholder?: string;
helpText?: string;
options?: { value: string; label: string }[];
validate?: { regex: RegExp; message: string };
tooltipText?: string;
}
```
FieldRenderer and PasswordField already consume `tooltipText` and `helpText` from FieldDef.
No component changes needed — this is purely a data enrichment task.
Fields to ADD tooltipText (from RESEARCH.md audit):
- s3: access_key_id, secret_access_key (YES), region (OPTIONAL — low priority)
- s3-compatible: access_key_id, secret_access_key, endpoint (YES)
- gcs: project_number, service_account_credentials (YES — GCS SAC warrants doc link)
- b2: account, key (YES)
- onedrive: drive_id (YES)
- sftp: host, user (NO — self-explanatory per user decision)
Fields to ADD helpText:
- s3: access_key_id, secret_access_key (YES — label alone ambiguous)
- s3-compatible: access_key_id, secret_access_key (YES)
DO NOT REWRITE existing helpText/tooltipText on:
- azureblob (account, key, sas_url)
- onedrive token
- sftp pass, key_pem
- b2 account, key (already have helpText — ADD tooltipText only)
- gcs project_number, service_account_credentials (already have helpText — ADD tooltipText only)
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Enrich registry.ts with tooltipText and helpText for credential fields</name>
<files>src/schemas/registry.ts</files>
<action>
Add `tooltipText` and/or `helpText` to the following fields in BACKEND_REGISTRY. Do NOT modify existing helpText/tooltipText on Azure Blob, OneDrive token, SFTP pass/key_pem fields.
**S3 fields:**
- `access_key_id`: Add helpText (e.g., "The access key ID from your IAM credentials (starts with AKIA for long-term keys).") and tooltipText explaining where to find it in AWS Console (IAM > Users > Security credentials > Access keys).
- `secret_access_key`: Add helpText (e.g., "The secret access key paired with your Access Key ID. Only shown once at creation time.") and tooltipText explaining it's shown once when creating the access key and to store it securely.
- `region`: Add tooltipText (brief — "The AWS region where your S3 bucket is located. Find this in the S3 console next to your bucket name."). No helpText needed — placeholder is clear.
**S3-compatible fields:**
- `access_key_id`: Add helpText (e.g., "The access key ID from your storage provider's dashboard.") and tooltipText explaining it varies by provider (Wasabi: Access Keys page, R2: API Tokens, MinIO: admin console).
- `secret_access_key`: Add helpText (e.g., "The secret key paired with your Access Key ID.") and tooltipText similar to S3.
- `endpoint`: Add tooltipText explaining common endpoints (e.g., "Wasabi: https://s3.wasabisys.com, Cloudflare R2: https://{account_id}.r2.cloudflarestorage.com, MinIO: your server URL with port").
**GCS fields:**
- `project_number`: Add tooltipText explaining the difference from project ID and where to find it ("Found in Google Cloud Console > Dashboard > Project info. This is the numeric ID, not the text project ID.").
- `service_account_credentials`: Add tooltipText with doc link for creating service account JSON ("Create at Google Cloud Console > IAM & Admin > Service Accounts > Keys > Add Key > JSON. See https://cloud.google.com/iam/docs/keys-create-delete for detailed steps.").
**B2 fields:**
- `account` (Application Key ID): Add tooltipText ("Found in Backblaze B2 > App Keys. The applicationKeyId is the shorter ID — not the master account ID shown in account settings.").
- `key` (Application Key): Add tooltipText ("The application key value shown once when you create a new app key in B2 > App Keys. Copy it immediately — it cannot be retrieved later.").
**OneDrive fields:**
- `drive_id`: Add tooltipText ("The drive ID is shown in the output of `rclone authorize \"onedrive\"` — look for the 'drive_id' field in the JSON response. It typically starts with 'b!' for business drives.").
**Fields to SKIP (per user decision):**
- sftp host — self-explanatory
- sftp user — self-explanatory
Claude has discretion on exact wording. Keep tooltips actionable — tell the user WHERE to find the value, not just WHAT it is. Include doc links only for GCS service account JSON creation and OneDrive token procedure (already has tooltipText with the rclone authorize instruction).
</action>
<verify>
<automated>npx vitest run src/components/wizard/RemoteConfigStep.test.tsx</automated>
</verify>
<done>All credential fields in S3, S3-compatible, GCS, B2, and OneDrive drive_id have contextual tooltipText. S3 and S3-compatible access key fields have helpText. Existing help text on Azure Blob, OneDrive token, SFTP is unchanged.</done>
</task>
<task type="auto">
<name>Task 2: Add tests verifying tooltip buttons for newly enriched fields</name>
<files>src/components/wizard/RemoteConfigStep.test.tsx</files>
<action>
Add test cases in RemoteConfigStep.test.tsx that verify tooltip info buttons render for the newly enriched fields. Follow the existing test pattern in the file (check how existing tooltip tests are structured — likely under a UX-01 or similar describe block).
Add tests for at least these representative fields:
1. S3 backend: `access_key_id` field renders a tooltip info button
2. S3-compatible backend: `endpoint` field renders a tooltip info button
3. GCS backend: `service_account_credentials` field renders a tooltip info button
4. B2 backend: `account` field renders a tooltip info button
Use the existing renderAtStep or component render pattern from the test file. Each test should:
- Render RemoteConfigStep with the appropriate backend type
- Find the tooltip button (typically `getByRole('button', { name: /more info about .../i })` or similar pattern — check existing tests for the exact pattern)
- Assert the button exists
IMPORTANT: Read the existing test file first to match the exact patterns used for tooltip assertions. The existing UX-01 section tests Azure Blob, SFTP, and OneDrive tooltips — follow the same approach.
</action>
<verify>
<automated>npx vitest run src/components/wizard/RemoteConfigStep.test.tsx</automated>
</verify>
<done>Tests confirm tooltip info buttons render for S3, S3-compatible, GCS, and B2 credential fields. All existing tests still pass.</done>
</task>
</tasks>
<verification>
1. `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` — all tooltip tests pass
2. `npx vitest run` — full suite green
3. Grep registry.ts for `tooltipText` — count matches expected enrichment
4. Azure Blob, OneDrive token, SFTP pass/key_pem entries unchanged
</verification>
<success_criteria>
- All S3 credential fields have tooltipText explaining where to find values in AWS Console
- S3 and S3-compatible access key fields have helpText
- S3-compatible endpoint has tooltipText with provider-specific examples
- GCS fields have tooltipText (service_account_credentials includes doc link)
- B2 fields have tooltipText
- OneDrive drive_id has tooltipText
- SFTP host/user do NOT have tooltipText
- Existing help text on Azure Blob, OneDrive token, SFTP pass/key_pem is untouched
- Tests verify tooltip buttons render for enriched fields
</success_criteria>
<output>
After completion, create `.planning/phases/10-content-clarity/10-03-SUMMARY.md`
</output>
@@ -0,0 +1,98 @@
---
phase: 10-content-clarity
plan: "03"
subsystem: schemas
tags: [tooltips, help-text, ux, registry, credential-fields]
dependency_graph:
requires: []
provides: [UX-04]
affects: [src/components/ui/FieldRenderer.tsx, src/components/ui/PasswordField.tsx]
tech_stack:
added: []
patterns: [FieldDef enrichment, tooltipText/helpText data layer]
key_files:
created: []
modified:
- src/schemas/registry.ts
- src/components/wizard/RemoteConfigStep.test.tsx
decisions:
- "S3 region gets tooltipText only (no helpText needed — placeholder is already descriptive)"
- "S3-compatible and S3 access key fields get both helpText and tooltipText"
- "GCS service_account_credentials tooltipText includes official doc link for key creation"
- "SFTP host and user intentionally get no tooltipText (self-explanatory, per user decision)"
- "B2 and GCS already had helpText — added tooltipText only, no rewrites"
- "OneDrive drive_id gets tooltipText alongside existing helpText"
- "Selector ambiguity fix ({ selector: 'input' }) applied to all getByLabelText calls now that tooltip buttons render aria-label attributes matching field labels"
metrics:
duration_minutes: 7
completed_date: "2026-04-01"
tasks_completed: 2
files_modified: 2
requirements_satisfied: [UX-04]
---
# Phase 10 Plan 03: Credential Field Help Text and Tooltips Summary
**One-liner:** Enriched BACKEND_REGISTRY with actionable tooltipText and helpText for all S3, S3-compatible, GCS, B2, and OneDrive drive_id credential fields, directing users to the exact console location to find each value.
## What Was Built
Registry.ts now contains contextual help for 11 credential fields across 5 backend types. Each tooltip tells users WHERE to find the value (not just what it is), with doc links for GCS service account JSON creation.
### Fields enriched
| Backend | Field | tooltipText | helpText |
|---|---|---|---|
| s3 | access_key_id | Yes (IAM console path) | Yes |
| s3 | secret_access_key | Yes (shown-once warning) | Yes |
| s3 | region | Yes (S3 console bucket list) | No |
| s3-compatible | access_key_id | Yes (Wasabi/R2/MinIO paths) | Yes |
| s3-compatible | secret_access_key | Yes (shown-once warning) | Yes |
| s3-compatible | endpoint | Yes (provider examples) | No (pre-existing) |
| gcs | project_number | Yes (Cloud Console path, numeric vs text ID) | No (pre-existing) |
| gcs | service_account_credentials | Yes (IAM steps + doc link) | No (pre-existing) |
| b2 | account | Yes (App Keys page, key ID vs account ID) | No (pre-existing) |
| b2 | key | Yes (shown-once warning) | No (pre-existing) |
| onedrive | drive_id | Yes (rclone authorize output, b! prefix) | No (pre-existing) |
### Fields intentionally unchanged
- azureblob key, sas_url — pre-existing tooltipText preserved
- onedrive token — pre-existing tooltipText preserved
- sftp host, user — no tooltip (self-explanatory per user decision)
- sftp pass, key_pem — pre-existing helpText preserved
## Tests
37 tests pass. Added 4 new UX-01 tests verifying tooltip info buttons render for:
- S3 `access_key_id`
- S3-compatible `endpoint`
- GCS `service_account_credentials`
- B2 `account` (Application Key ID)
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] Fixed getByLabelText selector ambiguity across test suite**
- **Found during:** Task 1 (immediately after first test run)
- **Issue:** Adding tooltipText to fields causes tooltip buttons to render with `aria-label="More info about {Field Label}"`. This made `getByLabelText(/field name/i)` match both the input element AND the tooltip button, triggering "Found multiple elements" errors in 13 tests.
- **Fix:** Added `{ selector: 'input' }` to all affected `getByLabelText` calls in BACK-02, BACK-03, BACK-01 (drive_id), BACK-03 GCS, BACK-04 B2, and VALID-01 blocks. This is the same DEBT-01 pattern resolved in Phase 09-03.
- **Files modified:** `src/components/wizard/RemoteConfigStep.test.tsx`
- **Commit:** 489dfab
## Commits
| Hash | Message |
|---|---|
| 356d557 | feat(10-03): enrich registry.ts with tooltipText and helpText for credential fields |
| 489dfab | feat(10-03): add tooltip tests for enriched fields; fix selector ambiguity |
## Self-Check
Files exist:
- src/schemas/registry.ts — verified (tooltipText count: 15 = 1 interface + 14 fields)
- src/components/wizard/RemoteConfigStep.test.tsx — verified (37 tests pass)
Commits exist:
- 356d557 — verified
- 489dfab — verified
@@ -0,0 +1,92 @@
# Phase 10: Content & Clarity - Context
**Gathered:** 2026-04-01
**Status:** Ready for planning
<domain>
## Phase Boundary
A first-time visitor understands what Ready2Blob does and what each wizard step expects without external documentation. This phase adds an intro section, remote name guidance with live preview, step descriptions, and contextual help text for all backend credential fields.
</domain>
<decisions>
## Implementation 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 tooltipText to all fields that lack it (S3, S3-compatible, GCS, SFTP, B2, OneDrive drive_id) — tooltip explains where to find the value in the provider's console
- Include links to provider docs only for complex/non-obvious flows (OneDrive token procedure, GCS service account JSON creation). No links for straightforward fields like S3 keys.
- Add short helpText only where the label alone is ambiguous (S3 access_key_id, secret_access_key, S3-compatible equivalents). Skip self-explanatory fields (SFTP Username, Host).
- 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)
</decisions>
<code_context>
## Existing Code Insights
### Reusable Assets
- `TextFieldMD3` (src/components/ui/TextFieldMD3.tsx): Floating label input — remote name field already uses it
- `FieldRenderer` (src/components/ui/FieldRenderer.tsx): Renders fields with tooltip (hover+click) and helpText infrastructure
- `PasswordField` (src/components/ui/PasswordField.tsx): Handles password fields with show/hide toggle
- MD3 button constants (`MD3_BTN_FILLED`, `MD3_BTN_OUTLINED`): Available for CTA button
- MD3 color tokens: `bg-surface`, `text-on-surface`, `bg-primary`, etc. — all in place from Phase 8
### Established Patterns
- Tooltip infrastructure: `tooltipText` field in FieldDef triggers info icon with hover+click behavior in FieldRenderer
- `helpText` renders as small text below inputs; `helpTextPrefix` prop on TextFieldMD3 for icon placement
- BACKEND_REGISTRY in `src/schemas/registry.ts` is single source of truth for field definitions
- No routing — wizard is state-driven via `currentStep` in useReducer
### Integration Points
- `src/App.tsx` WizardShell: Intro section goes above StepIndicator, controlled by local state (showIntro)
- `src/components/wizard/BackendSelectionStep.tsx`: Remote name TextFieldMD3 needs preview component added below it
- `src/schemas/registry.ts`: All helpText and tooltipText additions go here
- Each step component (`BackendSelectionStep`, `RemoteConfigStep`, `DeploymentStep`, `ReviewStep`): Step descriptions go below existing `<h2>` headings
</code_context>
<specifics>
## Specific Ideas
No specific requirements — open to standard approaches
</specifics>
<deferred>
## Deferred Ideas
None — discussion stayed within phase scope
</deferred>
---
*Phase: 10-content-clarity*
*Context gathered: 2026-04-01*
@@ -0,0 +1,477 @@
# 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 `tooltipText` to 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 `helpText` only 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:**
```tsx
// 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:**
```tsx
// 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:**
```tsx
// 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:**
```tsx
// 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:**
- `FieldRenderer` text branch: passes `tooltipText` via `field.tooltipText ? (tooltipIcon) : undefined` as `helpTextPrefix` to `TextFieldMD3`
- `FieldRenderer` select branch: renders tooltip button + panel when `field.tooltipText` exists
- `PasswordField`: accepts `tooltipText` prop, renders ⓘ button and tooltip panel
- `TextFieldMD3`: renders `helpText` via `helpTextPrefix`/`helpText` slot
**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 `useWatch` instead of `watch()`:** `watch()` from `useForm` is sufficient for a single field. `useWatch` is for cross-component subscription without prop drilling — not needed here.
- **Adding `placeholder` prop to TextFieldMD3:** The current `TextFieldMD3` hardcodes `placeholder=" "` (a single space) to enable the CSS floating-label trick via `peer-[:not(:placeholder-shown)]`. Do NOT add a visible placeholder text to this component — the UX-02 placeholder `e.g. my-backup` is implemented as the field's `helpText` + the `RemoteNamePreview` empty 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
```tsx
// 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
```tsx
// 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
```ts
// 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
```tsx
// 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
1. **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 in `ui/`), makes testing cleaner, costs nothing
2. **TextFieldMD3 does not accept a `placeholder` prop — UX-02 says field placeholder should be `e.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."). The `RemoteNamePreview` empty-state message `[my-remote]` serves the visual placeholder role. Do NOT modify TextFieldMD3's placeholder mechanism.
---
## 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 tooltips
- `src/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)
@@ -0,0 +1,82 @@
---
phase: 10
slug: content-clarity
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-04-01
---
# Phase 10 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution.
---
## Test Infrastructure
| 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` |
| **Estimated runtime** | ~5 seconds |
---
## Sampling Rate
- **After every task commit:** Run `npx vitest run --reporter=verbose`
- **After every plan wave:** Run `npx vitest run`
- **Before `/gsd:verify-work`:** Full suite must be green
- **Max feedback latency:** 10 seconds
---
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
| 10-01-01 | 01 | 1 | UX-01 | unit | `npx vitest run src/App.test.tsx` | ✅ (needs update) | ⬜ pending |
| 10-01-02 | 01 | 1 | UX-01 | unit | `npx vitest run src/App.test.tsx` | ✅ (needs new test) | ⬜ pending |
| 10-02-01 | 02 | 1 | UX-02 | unit | `npx vitest run src/components/ui/RemoteNamePreview.test.tsx` | ❌ W0 | ⬜ pending |
| 10-02-02 | 02 | 1 | UX-02 | unit | `npx vitest run src/components/ui/RemoteNamePreview.test.tsx` | ❌ W0 | ⬜ pending |
| 10-02-03 | 02 | 1 | UX-02 | unit | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | ✅ (needs new test) | ⬜ pending |
| 10-03-01 | 03 | 1 | UX-03 | unit | `npx vitest run src/App.test.tsx` | ✅ (needs new tests) | ⬜ pending |
| 10-04-01 | 04 | 1 | UX-04 | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ✅ (needs new test) | ⬜ pending |
| 10-04-02 | 04 | 1 | UX-04 | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ✅ (needs new test) | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
---
## Wave 0 Requirements
- [ ] `src/components/ui/RemoteNamePreview.test.tsx` — stubs for UX-02 preview rendering
*App.test.tsx, BackendSelectionStep.test.tsx, and RemoteConfigStep.test.tsx exist but need new test cases added as part of implementation tasks.*
---
## Manual-Only Verifications
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Intro section visual layout and spacing | UX-01 | Visual design verification | Open app, verify intro section appears with proper spacing, typography, and CTA button |
| RemoteNamePreview updates live as user types | UX-02 | Real-time interaction verification | Type in remote name field, verify preview updates character by character |
| Step descriptions are readable and contextually helpful | UX-03 | Content quality review | Navigate through all 4 steps, read each description for clarity and usefulness |
| Tooltip content is accurate and helpful for each backend | UX-04 | Content accuracy review | Click tooltip buttons for each backend, verify text accuracy and doc links |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 10s
- [ ] `nyquist_compliant: true` set in frontmatter
**Approval:** pending
@@ -0,0 +1,115 @@
---
phase: 10-content-clarity
verified: 2026-04-01T13:01:00Z
status: passed
score: 11/11 must-haves verified
re_verification: false
---
# Phase 10: Content Clarity Verification Report
**Phase Goal:** A first-time visitor understands what Ready2Blob does and what each wizard step expects without external documentation
**Verified:** 2026-04-01T13:01:00Z
**Status:** PASSED
**Re-verification:** No — initial verification
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|----|-------|--------|----------|
| 1 | First-time visitor sees an intro section explaining what Ready2Blob does before the wizard loads | VERIFIED | `IntroSection` renders in `WizardShell` when `showIntro === true`; copy matches UX decisions ("Go from zero to a deployable rclone setup in minutes", 7 backends mentioned, rclone.conf + scripts explained) |
| 2 | Clicking Get Started hides the intro and reveals Step 1 with StepIndicator | VERIFIED | `onStart={() => setShowIntro(false)}` wired directly to `IntroSection`; conditional renders `<StepIndicator />` + step content when `showIntro === false`; `App.test.tsx` fireEvent.click test confirms the flow |
| 3 | On page reload, intro shows again (no persistence) | VERIFIED | `useState(true)` in `WizardShell` — no localStorage, no sessionStorage, no WizardReducer involvement; ephemeral by construction |
| 4 | Each of the 4 wizard steps has a 1-2 sentence description below the heading | VERIFIED | All 4 steps confirmed: `BackendSelectionStep` (names remote + picks provider), `RemoteConfigStep` (credentials, never sent to server), `DeploymentStep` (config deployment + script formats), `ReviewStep` (review files + security notice) — all use `className="text-sm text-on-surface-variant mt-1 mb-4"` immediately after `<h2>` |
| 5 | Remote name field displays help text explaining what it is and giving examples | VERIFIED | `helpText="This becomes the section header [name] in your rclone.conf. Example: azure-prod, backup-s3. Letters, numbers, dashes, underscores only."` on `TextFieldMD3` in `BackendSelectionStep` |
| 6 | A live config preview below the field updates as user types, showing [remote-name] syntax | VERIFIED | `watch('name')``const remoteName = watch('name')``<RemoteNamePreview value={remoteName} />` directly below `TextFieldMD3`; `BackendSelectionStep.test.tsx` confirms live update |
| 7 | When field is empty, preview shows grayed-out [my-remote] placeholder with guidance text | VERIFIED | `RemoteNamePreview`: `trimmed.length === 0``[my-remote]` in `text-on-surface-variant/50` + guidance in `text-on-surface-variant/40`; 3 dedicated test cases pass |
| 8 | When field has content, preview shows [typed-value] in monospace code style | VERIFIED | `RemoteNamePreview`: non-empty → `[{trimmed}]` in `text-on-surface-variant`; container `font-mono`; 3 value-state tests pass |
| 9 | S3, S3-compatible, GCS, B2, and OneDrive drive_id credential fields have contextual tooltipText | VERIFIED | 14 field-level `tooltipText` entries confirmed in `registry.ts`: s3 (access_key_id, secret_access_key, region), s3-compatible (access_key_id, secret_access_key, endpoint), gcs (project_number, service_account_credentials), b2 (account, key), onedrive (drive_id) |
| 10 | S3 and S3-compatible access key fields have helpText | VERIFIED | Both `access_key_id` and `secret_access_key` in both `s3` and `s3-compatible` sections carry `helpText` entries |
| 11 | SFTP host and user do NOT have tooltipText; existing Azure Blob / OneDrive / SFTP help text is unchanged | VERIFIED | `sftp.host` and `sftp.user` fields confirmed: no `tooltipText`; `azureblob` key/sas_url, `onedrive` token, `sftp` pass/key_pem all retain pre-existing `tooltipText`/`helpText` |
**Score:** 11/11 truths verified
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `src/App.tsx` | IntroSection component and showIntro state gate in WizardShell | VERIFIED | `IntroSection` defined as local component; `useState(true)` in `WizardShell`; `setShowIntro(false)` wired to CTA; conditional render gates `StepIndicator` + step |
| `src/App.test.tsx` | Tests for intro rendering and Get Started button behavior | VERIFIED | 2 intro-specific tests: "shows intro section on initial render" (asserts heading present, `queryByRole('navigation')` returns null) and "renders BackendSelectionStep when currentStep is 0" (fireEvent.click on Get Started, then asserts wizard) |
| `src/components/wizard/BackendSelectionStep.tsx` | Step 1 description paragraph | VERIFIED | `<p className="text-sm text-on-surface-variant mt-1 mb-4">` after `<h2>Step 1: Select Backend</h2>` |
| `src/components/wizard/RemoteConfigStep.tsx` | Step 2 description paragraph | VERIFIED | `<p className="text-sm text-on-surface-variant mt-1 mb-4">` after `<h2>Step 2: Configure {backendLabel[backendType]}</h2>` |
| `src/components/wizard/DeploymentStep.tsx` | Step 3 description paragraph | VERIFIED | `<p className="text-sm text-on-surface-variant mt-1 mb-4">` after `<h2>Step 3: Deployment Options</h2>` |
| `src/components/wizard/ReviewStep.tsx` | Step 4 description paragraph | VERIFIED | `<h2>Step 4: Review &amp; Download</h2>` (added by plan, did not exist before) + `<p className="text-sm text-on-surface-variant mt-1 mb-4">` immediately after |
| `src/components/ui/RemoteNamePreview.tsx` | Stateless display component for rclone config preview | VERIFIED | 23-line stateless component; exports `RemoteNamePreview`; no form controls; empty/value state logic; 9 tests pass |
| `src/components/ui/RemoteNamePreview.test.tsx` | Tests for empty-state and value-state rendering | VERIFIED | 9 tests across 3 describe blocks: empty state (3), value state (3), element type constraints (3) — all pass |
| `src/components/wizard/BackendSelectionStep.tsx` | watch('name') integration and RemoteNamePreview placement | VERIFIED | `watch` in useForm destructure; `const remoteName = watch('name')`; `<RemoteNamePreview value={remoteName} />` below TextFieldMD3 |
| `src/schemas/registry.ts` | Enriched FieldDef entries with tooltipText and helpText | VERIFIED | 14 field-level `tooltipText` entries (15 total including interface definition); helpText on 4 access-key fields |
| `src/components/wizard/RemoteConfigStep.test.tsx` | Tests verifying tooltip buttons render for newly enriched fields | VERIFIED | 4 new tests in UX-01 describe block: S3 access_key_id, S3-compatible endpoint, GCS service_account_credentials, B2 account — all pass |
### Key Link Verification
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `src/App.tsx` | IntroSection | showIntro state toggle | VERIFIED | `onStart={() => setShowIntro(false)}` passed as prop; IntroSection calls it on button click |
| `src/App.tsx` | StepIndicator | conditional render when showIntro is false | VERIFIED | `{showIntro ? <IntroSection .../> : <><StepIndicator />...</>}` — StepIndicator is in the false branch |
| `src/components/wizard/BackendSelectionStep.tsx` | RemoteNamePreview | watch('name') value passed as prop | VERIFIED | `const remoteName = watch('name')``<RemoteNamePreview value={remoteName} />` |
| `src/components/wizard/BackendSelectionStep.tsx` | TextFieldMD3 | helpText prop on remote name field | VERIFIED | `helpText="This becomes the section header..."` present on TextFieldMD3 |
| `src/schemas/registry.ts` | FieldRenderer.tsx | tooltipText and helpText consumed by rendering | VERIFIED | FieldRenderer reads `field.tooltipText` on all three branches (password → PasswordField prop, select → inline button, text → tooltipIcon button); tooltipText conditionally shows panel |
| `src/schemas/registry.ts` | PasswordField.tsx | tooltipText consumed by PasswordField | VERIFIED | FieldRenderer passes `tooltipText={field.tooltipText}` to `PasswordField` for password-type fields |
### Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|-------------|-------------|--------|----------|
| UX-01 | 10-01-PLAN.md | First-time visitor sees intro with CTA | SATISFIED | IntroSection in App.tsx; showIntro state; App.test.tsx tests; truths 1-3 verified |
| UX-02 | 10-02-PLAN.md | Remote name field: help text + live preview | SATISFIED | helpText on TextFieldMD3; RemoteNamePreview wired via watch(); empty/value states; truths 5-8 verified |
| UX-03 | 10-01-PLAN.md | Each step has 1-2 sentence description | SATISFIED | All 4 step components have `<p className="text-sm text-on-surface-variant mt-1 mb-4">` after `<h2>`; truth 4 verified |
| UX-04 | 10-03-PLAN.md | Credential fields have contextual help | SATISFIED | 14 field-level tooltipText entries in registry.ts; 4 helpText on access key fields; SFTP host/user correctly excluded; truths 9-11 verified |
No orphaned requirements: REQUIREMENTS.md maps exactly UX-01 through UX-04 to Phase 10. All four are claimed in plans 10-01, 10-02, and 10-03.
### Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `src/components/wizard/ReviewStep.tsx` | 16, 27 | `const PLACEHOLDER = '# Fill in...'` | Info | Intentional empty-state fallback string for rclone.conf preview when wizard state is incomplete; not an unimplemented stub |
No blockers. No warnings.
### Human Verification Required
#### 1. Intro section visual appearance
**Test:** Open the app in a browser on first load. Observe the IntroSection before clicking Get Started.
**Expected:** Action-first headline ("Go from zero..."), backend list paragraph, rclone.conf + scripts paragraph, prominent "Get Started" button — all styled with MD3 tokens, readable in both light and dark themes.
**Why human:** Visual layout, typography hierarchy, and color contrast cannot be verified programmatically.
#### 2. Intro-to-wizard transition feel
**Test:** Click "Get Started" and observe the transition.
**Expected:** Intro disappears, Step 1 with StepIndicator appears immediately — no animation artifacts, no layout shift.
**Why human:** Transition smoothness and absence of visual glitches require visual inspection.
#### 3. Live preview responsiveness
**Test:** In Step 1, type progressively into the remote name field.
**Expected:** `[typed-value]` preview updates on every keystroke with no lag; empty state placeholder reappears if field is cleared.
**Why human:** Perceived reactivity requires real browser interaction.
#### 4. Tooltip content quality
**Test:** Open a tooltip on a credential field (e.g., S3 Access Key ID) and read the content.
**Expected:** Tooltip text is actionable — tells the user exactly where to find the value in their cloud provider's console, not just what it is.
**Why human:** Content quality and usefulness are subjective and require human judgment.
### Gaps Summary
None. All 11 must-have truths verified. All 11 artifacts exist, are substantive, and are wired. All 4 requirement IDs satisfied. No orphaned requirements. Full test suite passes (202/202).
---
_Verified: 2026-04-01T13:01:00Z_
_Verifier: Claude (gsd-verifier)_
@@ -0,0 +1,21 @@
# Deferred Items — Phase 10
## Pre-existing test failures (out of scope for 10-01)
**Discovered during:** Plan 10-01, Task 2 verification
**Files involved:** `src/schemas/registry.ts` (uncommitted working tree changes) + `src/components/wizard/RemoteConfigStep.test.tsx` (working tree)
**Status:** NOT caused by plan 10-01 changes
**Details:** 8 RemoteConfigStep tests fail when the uncommitted `registry.ts` changes (tooltip texts added for GCS and B2 fields from a previous session) are combined with the current `RemoteConfigStep.test.tsx`. The tests time out waiting for GCS/B2 fields (`getByLabelText(/project number/i)`) and S3/GCS validation labels. These changes were in the working tree before plan 10-01 execution began and are unrelated to the intro section or step description work.
**Failing tests:**
- `renders Project Number field`
- `renders Service Account JSON field`
- `renders Application Key ID field`
- `renders Application Key field`
- `accepts valid S3 region (no format error)`
- `rejects GCS project_number with non-digits`
- `accepts valid GCS project_number (no format error)`
- `rejects S3 region with invalid format (spaces)` (intermittent)
**Recommended fix:** Commit or revert the uncommitted `registry.ts` changes and align `RemoteConfigStep.test.tsx` selectors with the updated tooltip rendering (likely a `{ selector: 'input' }` issue).
@@ -0,0 +1,211 @@
---
phase: 11-polish-responsiveness
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/styles/md3-buttons.ts
- src/components/ui/BackendCard.tsx
- src/components/ui/ThemeToggle.tsx
- src/components/ui/FieldRenderer.tsx
- src/components/wizard/BackendSelectionStep.tsx
- src/components/wizard/RemoteConfigStep.tsx
- src/components/wizard/DeploymentStep.tsx
- src/components/wizard/ReviewStep.tsx
- src/components/wizard/StepIndicator.tsx
autonomous: true
requirements:
- POLISH-01
- POLISH-02
must_haves:
truths:
- "On mobile viewport (<640px), backend cards stack vertically in a single column"
- "On mobile viewport (<640px), navigation buttons stretch to full width and stack vertically"
- "On mobile viewport (<640px), StepIndicator hides label text but keeps circles and connectors"
- "On desktop viewport (>=640px), backend cards display in a 2-column grid"
- "Every button and interactive card shows a 3px MD3 focus ring when keyboard-navigated"
- "Focus ring does not appear on mouse click (uses focus-visible, not focus)"
artifacts:
- path: "src/styles/md3-buttons.ts"
provides: "MD3 button constants with focus-visible:ring-3"
contains: "focus-visible:ring-3"
- path: "src/components/ui/BackendCard.tsx"
provides: "Responsive full-width card with focus-visible ring"
contains: "focus-visible:ring-3"
- path: "src/components/wizard/StepIndicator.tsx"
provides: "Collapsed labels on mobile, upgraded focus ring"
contains: "hidden sm:block"
- path: "src/components/wizard/BackendSelectionStep.tsx"
provides: "Responsive card grid and button row"
contains: "grid grid-cols-1 sm:grid-cols-2"
key_links:
- from: "src/styles/md3-buttons.ts"
to: "all step components"
via: "MD3_BTN_FILLED / MD3_BTN_OUTLINED constants"
pattern: "focus-visible:ring-3"
- from: "src/components/ui/BackendCard.tsx"
to: "BackendSelectionStep.tsx"
via: "component usage"
pattern: "w-full.*focus-visible:ring-3"
---
<objective>
Make the wizard layout mobile-responsive and add MD3 focus-visible indicators to all interactive elements.
Purpose: POLISH-01 ensures the wizard is usable on mobile devices (cards stack, buttons stretch, step labels collapse). POLISH-02 ensures keyboard navigability with visible 3px MD3 focus rings on all interactive elements.
Output: All step components, BackendCard, StepIndicator, ThemeToggle, and FieldRenderer updated with responsive classes and focus-visible rings.
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/11-polish-responsiveness/11-RESEARCH.md
<interfaces>
<!-- Key constants and component signatures the executor needs -->
From src/styles/md3-buttons.ts:
```typescript
export const MD3_BTN_FILLED = '...' // contains focus-visible:ring-2 focus-visible:ring-primary/50 — upgrade to ring-3
export const MD3_BTN_OUTLINED = '...' // same pattern
export const MD3_BTN_TEXT = '...' // same pattern
```
From src/components/ui/BackendCard.tsx:
```typescript
// className array does NOT have focus-visible — must add
// className does NOT have w-full — must add for grid cell fill
export function BackendCard({ name, description, selected, onClick, ...rest }: BackendCardProps)
```
From src/components/wizard/StepIndicator.tsx:
```typescript
// Three label spans (completed, active, future) need hidden sm:block
// Completed step button: group-focus-visible:ring-2 → upgrade to ring-3
```
From src/components/ui/ThemeToggle.tsx:
```typescript
// Buttons have NO focus-visible classes — must add
// className is conditional (selected vs not) — add focus-visible to both branches
```
From src/components/ui/FieldRenderer.tsx:
```typescript
// Select element: focus:ring-2 → change to focus-visible:ring-2 (inputs keep focus: per convention)
// Tooltip buttons (2 instances): NO focus-visible — must add
```
Button row pattern in all step components:
```tsx
<div className="flex gap-3 mt-6"> // → flex flex-col sm:flex-row gap-3 mt-6
<button className={MD3_BTN_OUTLINED}>Back</button> // → add w-full sm:w-auto
<button className={MD3_BTN_FILLED}>Next</button> // → add w-full sm:w-auto
</div>
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Mobile responsive layout — card grid, button rows, step indicator collapse</name>
<files>src/components/wizard/BackendSelectionStep.tsx, src/components/ui/BackendCard.tsx, src/components/wizard/RemoteConfigStep.tsx, src/components/wizard/DeploymentStep.tsx, src/components/wizard/ReviewStep.tsx, src/components/wizard/StepIndicator.tsx</files>
<action>
POLISH-01: Make wizard layout mobile-responsive using Tailwind v4 mobile-first approach.
1. **BackendSelectionStep.tsx** — Change the `data-testid="backend-cards"` div from bare div to responsive grid:
- Add className `grid grid-cols-1 sm:grid-cols-2 gap-3 mt-4` to the `data-testid="backend-cards"` div
- Change button row from `flex gap-3 mt-6` to `flex flex-col sm:flex-row gap-3 mt-6`
- Add `w-full sm:w-auto` to the Next button className (append after MD3_BTN_FILLED)
2. **BackendCard.tsx** — Add `w-full` to the className array so the button fills its grid cell on all viewports. Insert at the start of the className string: `'w-full flex flex-col items-start...'`
3. **RemoteConfigStep.tsx** — Change button row div from `flex gap-3 mt-6` to `flex flex-col sm:flex-row gap-3 mt-6`. Add `w-full sm:w-auto` to both Back and Next button classNames (use template literal: `` `${MD3_BTN_OUTLINED} w-full sm:w-auto` ``).
4. **DeploymentStep.tsx** — Same button row changes as RemoteConfigStep: `flex flex-col sm:flex-row gap-3 mt-6` on the wrapper div, `w-full sm:w-auto` on both buttons.
5. **ReviewStep.tsx** — Change button row div from `flex gap-3 mt-6` to `flex flex-col sm:flex-row gap-3 mt-6`. Add `w-full sm:w-auto` to the Back button. The "Download All (ZIP)" button already has `w-full` — leave as-is.
6. **StepIndicator.tsx** — Add `hidden sm:block` to all three label `<span>` elements:
- Completed step label: `className="text-xs text-primary hidden sm:block"`
- Active step label: `className="text-xs text-primary font-semibold hidden sm:block"`
- Future step label: `className="text-xs text-on-surface-container/40 hidden sm:block"`
Do NOT change any DOM structure beyond adding/modifying className strings. Do NOT add wrapper elements. The `data-testid="backend-cards"` div already exists — just add classes to it.
</action>
<verify>
<automated>npx vitest run</automated>
</verify>
<done>Backend cards use responsive grid (1-col mobile, 2-col desktop). All step button rows stack vertically on mobile and go horizontal on desktop. StepIndicator labels hidden on mobile, visible on desktop. All existing tests pass.</done>
</task>
<task type="auto">
<name>Task 2: MD3 focus-visible indicators on all interactive elements</name>
<files>src/styles/md3-buttons.ts, src/components/ui/BackendCard.tsx, src/components/wizard/StepIndicator.tsx, src/components/ui/ThemeToggle.tsx, src/components/ui/FieldRenderer.tsx</files>
<action>
POLISH-02: Upgrade all interactive elements to 3px MD3 focus-visible rings for keyboard navigation.
1. **md3-buttons.ts** — In all three constants (MD3_BTN_FILLED, MD3_BTN_OUTLINED, MD3_BTN_TEXT), replace:
`focus-visible:ring-2 focus-visible:ring-primary/50`
with:
`focus-visible:ring-3 focus-visible:ring-primary`
Keep `focus-visible:outline-none` as-is.
2. **BackendCard.tsx** — Add focus-visible ring to the button className array. Add this string to the base classes (the first string before the ternary):
`'w-full flex flex-col items-start gap-1 rounded-xl border-2 p-4 text-left transition-all focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-primary'`
Do NOT add `ring-offset-2` (omit ring-offset to avoid dark mode surface color issues per research open question #2).
3. **StepIndicator.tsx** — On the completed-step circle `<span>`, change:
`group-focus-visible:ring-2 group-focus-visible:ring-primary/50`
to:
`group-focus-visible:ring-3 group-focus-visible:ring-primary`
4. **ThemeToggle.tsx** — Add focus-visible ring to BOTH branches of the ternary className on each theme button:
- Selected: `'flex-1 px-3 py-1 bg-primary text-on-primary font-medium focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-primary focus-visible:ring-inset'`
- Unselected: `'flex-1 px-3 py-1 bg-surface-container text-on-surface-container hover:bg-surface focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-primary focus-visible:ring-inset'`
Use `ring-inset` because ThemeToggle buttons share a border (overflow-hidden) — outset ring would be clipped.
5. **FieldRenderer.tsx** — Two changes:
a. **Select element:** Change `focus:ring-2` to `focus-visible:ring-2` in the select className (keep ring-2 for form controls — inputs conventionally show focus on mouse too, but selects should match button behavior).
b. **Tooltip info buttons** (both the select-branch tooltip button and the text-branch tooltip button): Add `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded` to both tooltip button classNames. These are small buttons so ring-2 is appropriate (not ring-3).
Note on TextFieldMD3: The input element uses `focus:ring-2` which is correct for text inputs (users expect visible focus on mouse click for inputs). Do NOT change TextFieldMD3.
</action>
<verify>
<automated>npx vitest run</automated>
</verify>
<done>MD3_BTN_FILLED/OUTLINED/TEXT contain focus-visible:ring-3. BackendCard has focus-visible ring. StepIndicator completed-step has upgraded group-focus-visible:ring-3. ThemeToggle buttons have focus-visible ring. FieldRenderer select uses focus-visible:ring-2, tooltip buttons have focus-visible ring. All tests pass.</done>
</task>
</tasks>
<verification>
- `npx vitest run` — all tests green
- Grep for `focus-visible:ring-3` in md3-buttons.ts, BackendCard.tsx, StepIndicator.tsx, ThemeToggle.tsx — all present
- Grep for `grid grid-cols-1 sm:grid-cols-2` in BackendSelectionStep.tsx — present
- Grep for `flex-col sm:flex-row` in all step components — present
- Grep for `hidden sm:block` in StepIndicator.tsx — 3 occurrences (one per label state)
</verification>
<success_criteria>
- All step button rows use `flex-col sm:flex-row` (mobile stacks, desktop inline)
- Backend cards container uses `grid grid-cols-1 sm:grid-cols-2`
- BackendCard has `w-full` for grid cell fill
- StepIndicator labels have `hidden sm:block` (3 spans)
- All MD3 button constants use `focus-visible:ring-3 focus-visible:ring-primary`
- BackendCard, ThemeToggle, StepIndicator completed-step, FieldRenderer tooltip buttons all have focus-visible rings
- All existing tests pass (no regressions)
</success_criteria>
<output>
After completion, create `.planning/phases/11-polish-responsiveness/11-01-SUMMARY.md`
</output>
@@ -0,0 +1,120 @@
---
phase: 11-polish-responsiveness
plan: 01
subsystem: ui
tags: [tailwind, responsive, mobile, accessibility, focus-visible, md3]
# Dependency graph
requires:
- phase: 09-md3-components
provides: MD3_BTN_FILLED/OUTLINED/TEXT constants, BackendCard, StepIndicator, ThemeToggle, FieldRenderer components
- phase: 10-content-clarity
provides: final step component shapes and layouts
provides:
- Mobile-responsive wizard layout (cards stack, buttons stack, step labels collapse)
- MD3 focus-visible ring-3 indicators on all interactive elements
affects: [future UI phases, any plan touching step components or button constants]
# Tech tracking
tech-stack:
added: []
patterns:
- "Mobile-first responsive: flex-col sm:flex-row for button rows, grid grid-cols-1 sm:grid-cols-2 for card grids"
- "hidden sm:block for collapsible labels on compact viewports"
- "focus-visible:ring-3 focus-visible:ring-primary on standalone interactive elements"
- "focus-visible:ring-inset on overflow-hidden button groups (ThemeToggle) to avoid clipping"
key-files:
created: []
modified:
- src/styles/md3-buttons.ts
- src/components/ui/BackendCard.tsx
- src/components/ui/ThemeToggle.tsx
- src/components/ui/FieldRenderer.tsx
- src/components/wizard/BackendSelectionStep.tsx
- src/components/wizard/RemoteConfigStep.tsx
- src/components/wizard/DeploymentStep.tsx
- src/components/wizard/ReviewStep.tsx
- src/components/wizard/StepIndicator.tsx
key-decisions:
- "ring-inset on ThemeToggle focus ring because overflow-hidden on container clips outset rings"
- "No ring-offset-2 on BackendCard to avoid dark mode surface color issues"
- "Tooltip buttons use ring-2 (not ring-3) — small utility buttons suit smaller ring"
- "FieldRenderer select uses focus-visible:ring-2 (not focus:ring-2) for keyboard-only focus behavior on selects"
patterns-established:
- "Button row responsive pattern: flex flex-col sm:flex-row gap-3 mt-6 with w-full sm:w-auto on each button"
- "Card grid responsive pattern: grid grid-cols-1 sm:grid-cols-2 gap-3 on container div"
- "Step label collapse: hidden sm:block on all label spans inside StepIndicator"
requirements-completed: [POLISH-01, POLISH-02]
# Metrics
duration: 3min
completed: 2026-04-01
---
# Phase 11 Plan 01: Mobile Responsiveness & MD3 Focus Indicators Summary
**Tailwind v4 mobile-first responsive layout (card grids, stacked button rows, collapsed step labels) plus MD3 focus-visible ring-3 keyboard indicators across all wizard interactive elements**
## Performance
- **Duration:** 3 min
- **Started:** 2026-04-01T11:22:10Z
- **Completed:** 2026-04-01T11:25:12Z
- **Tasks:** 2
- **Files modified:** 9
## Accomplishments
- Backend cards container uses responsive grid (1-col on mobile, 2-col on desktop); BackendCard fills its cell with w-full
- All four step button rows stack vertically on mobile (flex-col) and go horizontal on desktop (sm:flex-row) with full-width buttons on mobile
- StepIndicator hides label text on mobile (hidden sm:block) keeping circles and connectors visible
- MD3 button constants upgraded from ring-2/50 to ring-3 for all three variants (FILLED, OUTLINED, TEXT)
- BackendCard, ThemeToggle, StepIndicator completed-step circle, and FieldRenderer tooltip buttons all have focus-visible rings
## Task Commits
Each task was committed atomically:
1. **Task 1: Mobile responsive layout** - `07f97d9` (feat)
2. **Task 2: MD3 focus-visible indicators** - `9015db5` (feat)
**Plan metadata:** (docs commit — see below)
## Files Created/Modified
- `src/styles/md3-buttons.ts` - Upgraded ring-2/primary/50 to ring-3/primary on all three constants
- `src/components/ui/BackendCard.tsx` - Added w-full and focus-visible:ring-3 to button base classes
- `src/components/ui/ThemeToggle.tsx` - Added focus-visible:ring-3 ring-inset to both selected/unselected branches
- `src/components/ui/FieldRenderer.tsx` - Select uses focus-visible:ring-2; both tooltip buttons get focus-visible:ring-2 rounded
- `src/components/wizard/BackendSelectionStep.tsx` - Responsive card grid and button row
- `src/components/wizard/RemoteConfigStep.tsx` - Responsive button row with w-full sm:w-auto
- `src/components/wizard/DeploymentStep.tsx` - Responsive button row with w-full sm:w-auto
- `src/components/wizard/ReviewStep.tsx` - Responsive button row with w-full sm:w-auto
- `src/components/wizard/StepIndicator.tsx` - hidden sm:block on all three label spans; ring-3 on completed-step circle
## Decisions Made
- ring-inset used on ThemeToggle because the container has overflow-hidden — outset ring would be clipped by the parent border
- No ring-offset-2 on BackendCard to avoid dark mode surface color mismatch (per research open question #2)
- Tooltip buttons use ring-2 (not ring-3) — they are small utility icons where a smaller ring is more appropriate
- FieldRenderer select changed from focus:ring-2 to focus-visible:ring-2 so selects behave like buttons (ring on keyboard only), not like text inputs
## Deviations from Plan
None - plan executed exactly as written.
## Issues Encountered
None.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- POLISH-01 and POLISH-02 requirements satisfied
- All 202 existing tests pass — no regressions introduced
- Wizard is now mobile-usable and fully keyboard-navigable
---
*Phase: 11-polish-responsiveness*
*Completed: 2026-04-01*
@@ -0,0 +1,244 @@
---
phase: 11-polish-responsiveness
plan: 02
type: execute
wave: 2
depends_on: ["11-01"]
files_modified:
- src/index.css
- src/App.tsx
- src/components/wizard/BackendSelectionStep.tsx
- src/components/wizard/RemoteConfigStep.tsx
autonomous: true
requirements:
- POLISH-03
- POLISH-04
must_haves:
truths:
- "Changing wizard steps plays a subtle fade/slide-up animation on the new step content"
- "Users with prefers-reduced-motion enabled see no animation on step change"
- "When form validation fails, the view auto-scrolls to the first errored field"
- "scrollIntoView uses smooth behavior and centers the errored field"
artifacts:
- path: "src/index.css"
provides: "step-in keyframe animation and reduced-motion guard"
contains: "@keyframes step-in"
- path: "src/App.tsx"
provides: "Step content wrapper with key and animation class"
contains: "animate-step-in"
- path: "src/components/wizard/BackendSelectionStep.tsx"
provides: "Auto-scroll to first error on validation failure"
contains: "scrollIntoView"
- path: "src/components/wizard/RemoteConfigStep.tsx"
provides: "Auto-scroll to first error on validation failure"
contains: "scrollIntoView"
key_links:
- from: "src/index.css"
to: "src/App.tsx"
via: "animate-step-in Tailwind utility from @theme --animate-step-in"
pattern: "animate-step-in"
- from: "src/components/wizard/BackendSelectionStep.tsx"
to: "DOM element"
via: "document.getElementById(firstErrorKey)?.scrollIntoView"
pattern: "scrollIntoView"
---
<objective>
Add step transition animations and auto-scroll-to-error behavior to the wizard.
Purpose: POLISH-03 creates visual continuity between wizard steps with a subtle fade/slide animation that respects accessibility preferences. POLISH-04 helps users find validation errors by auto-scrolling to the first errored field when form submission fails.
Output: CSS animation keyframes in index.css, animated step wrapper in App.tsx, scrollIntoView handlers in BackendSelectionStep and RemoteConfigStep.
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/11-polish-responsiveness/11-RESEARCH.md
@.planning/phases/11-polish-responsiveness/11-01-SUMMARY.md
<interfaces>
<!-- Current state of files this plan modifies -->
From src/index.css (after @theme block, line 53-69):
```css
@theme {
--color-primary: var(--r2b-primary);
/* ... more color tokens ... */
--color-on-warning: var(--r2b-on-warning);
/* ADD: --animate-step-in: step-in 200ms ease-out both; */
}
```
From src/App.tsx (WizardShell step content wrapper, line 65):
```tsx
<div className="mt-8">
{CurrentStep}
</div>
// CHANGE TO: <div key={state.currentStep} className="mt-8 animate-step-in">
```
From src/components/wizard/BackendSelectionStep.tsx:
```typescript
// handleSubmit(onValidSubmit) called in two places:
// 1. form onSubmit={handleSubmit(onValidSubmit)} — line 65
// 2. void handleSubmit(onValidSubmit)() inside handleCardClick — line 55
// ADD second arg: handleSubmit(onValidSubmit, onInvalidSubmit)
// The form's onSubmit needs the error handler. handleCardClick does NOT need it
// (card click validates remote name — if invalid, scrolls to remote-name field).
type RemoteNameFormValues = z.infer<typeof remoteNameSchema>;
// Error key will be "name" → document.getElementById("name") won't work
// because TextFieldMD3 uses id="remote-name" (passed as id prop)
// SOLUTION: use the form element to find the first [aria-invalid] or use
// a fixed ID since there's only one field. Simplest: getElementById('remote-name')
// since the only validatable field in this step is the remote name input.
```
From src/components/wizard/RemoteConfigStep.tsx:
```typescript
// handleSubmit(onNext) — line 63
// ADD second arg: handleSubmit(onNext, onInvalidSubmit)
// FieldRenderer passes field.key as id to TextFieldMD3 and select
// So document.getElementById(fieldKey) will find the correct element.
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Step transition animation with reduced-motion guard</name>
<files>src/index.css, src/App.tsx</files>
<action>
POLISH-03: Add a subtle fade/slide-up animation on wizard step transitions.
1. **src/index.css** — Add two things:
a. Register the animation in the existing `@theme` block. Add this line inside `@theme { ... }` after the last `--color-*` token:
```css
--animate-step-in: step-in 200ms ease-out both;
```
This registers `animate-step-in` as a Tailwind utility class.
b. Add the keyframes and reduced-motion guard AFTER the closing `}` of the `@theme` block (at the end of the file):
```css
@keyframes step-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.animate-step-in {
animation: none !important;
}
}
```
2. **src/App.tsx** — In the WizardShell component, change the step content wrapper div:
From: `<div className="mt-8">`
To: `<div key={state.currentStep} className="mt-8 animate-step-in">`
The `key={state.currentStep}` forces React to unmount/remount the div when the step changes, which triggers the CSS animation from its initial state. The animation also plays on first render — this is acceptable per research (imperceptible during page load).
Do NOT add any JavaScript media query listener for reduced-motion. The CSS `@media (prefers-reduced-motion: reduce)` guard handles it declaratively, consistent with the existing pattern at line 45 of index.css.
</action>
<verify>
<automated>npx vitest run src/App.test.tsx</automated>
</verify>
<done>index.css contains @keyframes step-in with 200ms ease-out, @theme contains --animate-step-in, @media prefers-reduced-motion guard exists. App.tsx step wrapper has key={state.currentStep} and animate-step-in class. All App tests pass.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Auto-scroll to first error on validation failure</name>
<files>src/components/wizard/BackendSelectionStep.tsx, src/components/wizard/BackendSelectionStep.test.tsx, src/components/wizard/RemoteConfigStep.tsx, src/components/wizard/RemoteConfigStep.test.tsx</files>
<behavior>
- Test: When BackendSelectionStep form submits with empty remote name, scrollIntoView is called on the remote-name input element
- Test: When RemoteConfigStep form submits with missing required fields, scrollIntoView is called on the first errored field's element
</behavior>
<action>
POLISH-04: Auto-scroll to the first errored field when form validation fails.
**Important prerequisite in both test files:** Add `Element.prototype.scrollIntoView = vi.fn();` in a `beforeEach` block (jsdom does not implement scrollIntoView — it will throw without this mock).
1. **BackendSelectionStep.tsx** — Add an `onInvalidSubmit` error handler:
```tsx
function onInvalidSubmit() {
// Only one validatable field in this step: remote-name
document.getElementById('remote-name')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
```
Update the form's onSubmit: `handleSubmit(onValidSubmit, onInvalidSubmit)`
Also update the handleCardClick call: `void handleSubmit(onValidSubmit, onInvalidSubmit)()`
2. **BackendSelectionStep.test.tsx** — Add test:
- Setup: `beforeEach(() => { Element.prototype.scrollIntoView = vi.fn(); });`
- Test name: "scrolls to remote-name field when submitted with empty name"
- Action: render component, clear the remote name input (if it has a default), click "Next" button (type=submit)
- Assert: `expect(Element.prototype.scrollIntoView).toHaveBeenCalled()`
3. **RemoteConfigStep.tsx** — Add an `onInvalidSubmit` error handler:
```tsx
import type { FieldErrors } from 'react-hook-form';
function onInvalidSubmit(errors: FieldErrors) {
const firstKey = Object.keys(errors)[0];
if (firstKey) {
document.getElementById(firstKey)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
```
Note: `FieldErrors` is already imported in RemoteConfigStep.tsx (as `FieldError` — add `FieldErrors` to the import).
Update the form's onSubmit: `handleSubmit(onNext, onInvalidSubmit)`
4. **RemoteConfigStep.test.tsx** — Add test:
- Setup: `beforeEach(() => { Element.prototype.scrollIntoView = vi.fn(); });`
- Test name: "scrolls to first errored field when submitted with missing required fields"
- Action: render component with a backend that has required fields (e.g., azureblob), submit the form without filling required fields
- Assert: `expect(Element.prototype.scrollIntoView).toHaveBeenCalled()`
The `FieldErrors` type import: RemoteConfigStep already imports `FieldError` from react-hook-form. Change to `import type { FieldError, FieldErrors } from 'react-hook-form';` (or just use the generic `Record<string, any>` type on the errors param if FieldErrors causes issues).
</action>
<verify>
<automated>npx vitest run src/components/wizard/BackendSelectionStep.test.tsx src/components/wizard/RemoteConfigStep.test.tsx</automated>
</verify>
<done>BackendSelectionStep calls scrollIntoView on remote-name input when form validation fails. RemoteConfigStep calls scrollIntoView on the first errored field when form validation fails. Both behaviors verified by unit tests with mocked scrollIntoView. All tests pass.</done>
</task>
</tasks>
<verification>
- `npx vitest run` — all tests green (full suite)
- Grep for `@keyframes step-in` in index.css — present
- Grep for `animate-step-in` in App.tsx — present
- Grep for `prefers-reduced-motion` in index.css — present (2 occurrences: existing theme transition + new animation guard)
- Grep for `scrollIntoView` in BackendSelectionStep.tsx and RemoteConfigStep.tsx — present in both
- Grep for `scrollIntoView` in test files — mock setup present in both test files
</verification>
<success_criteria>
- Step content wrapper in App.tsx has `key={state.currentStep}` and `animate-step-in` class
- index.css defines `@keyframes step-in` with opacity 0->1 and translateY 8px->0
- index.css has `@media (prefers-reduced-motion: reduce)` guard that disables animation
- `@theme` block contains `--animate-step-in` token
- BackendSelectionStep scrolls to remote-name input on validation failure
- RemoteConfigStep scrolls to first errored field on validation failure
- Both scroll behaviors verified by passing unit tests
- Full test suite passes with no regressions
</success_criteria>
<output>
After completion, create `.planning/phases/11-polish-responsiveness/11-02-SUMMARY.md`
</output>
@@ -0,0 +1,120 @@
---
phase: 11-polish-responsiveness
plan: 02
subsystem: ui
tags: [react, tailwind, css-animation, react-hook-form, accessibility, reduced-motion]
# Dependency graph
requires:
- phase: 11-01
provides: focus rings and BackendCard hover states (polish foundation)
- phase: 10-content-clarity
provides: App.tsx WizardShell structure with step content wrapper
provides:
- CSS @keyframes step-in animation with 200ms ease-out fade/slide-up on step change
- prefers-reduced-motion guard disabling animation for accessibility
- Auto-scroll to first errored field on validation failure in BackendSelectionStep and RemoteConfigStep
affects: [11-03, any future plan modifying App.tsx WizardShell or wizard form steps]
# Tech tracking
tech-stack:
added: []
patterns:
- "key={state.currentStep} on step wrapper forces React remount, triggering CSS animation from initial state"
- "@theme --animate-step-in token pattern for Tailwind v4 custom animation utility"
- "onInvalidSubmit second arg to handleSubmit for scroll-to-error without JS media query listener"
- "document.getElementById(firstKey)?.scrollIntoView for registry-driven field error targeting"
key-files:
created: []
modified:
- src/index.css
- src/App.tsx
- src/components/wizard/BackendSelectionStep.tsx
- src/components/wizard/BackendSelectionStep.test.tsx
- src/components/wizard/RemoteConfigStep.tsx
- src/components/wizard/RemoteConfigStep.test.tsx
key-decisions:
- "CSS-only reduced-motion guard via @media (prefers-reduced-motion: reduce) — no JS listener needed"
- "key={state.currentStep} on step wrapper (not on step components themselves) to trigger animation"
- "onInvalidSubmit hardcodes getElementById('remote-name') in BackendSelectionStep — only one validatable field exists"
- "FieldErrors type added to react-hook-form imports in RemoteConfigStep for typed errors param"
patterns-established:
- "Tailwind v4 custom animation: register --animate-X in @theme, define @keyframes, add reduced-motion guard"
- "react-hook-form scroll-to-error: pass onInvalidSubmit as second arg to handleSubmit"
requirements-completed: [POLISH-03, POLISH-04]
# Metrics
duration: 3min
completed: 2026-04-01
---
# Phase 11 Plan 02: Polish & Responsiveness — Animation & Scroll-to-Error Summary
**CSS step-in animation (200ms fade/slide-up) with reduced-motion guard, plus auto-scroll to first errored form field via react-hook-form onInvalidSubmit handler**
## Performance
- **Duration:** 3 min
- **Started:** 2026-04-01T11:28:03Z
- **Completed:** 2026-04-01T11:31:00Z
- **Tasks:** 2 (Task 2 used TDD: RED → GREEN)
- **Files modified:** 6
## Accomplishments
- Wizard step transitions now play a 200ms opacity/translateY(8px) fade-slide animation on step change
- Users with `prefers-reduced-motion` enabled see zero animation (declarative CSS guard, no JS)
- BackendSelectionStep scrolls to `remote-name` input when form submitted with invalid/empty name
- RemoteConfigStep scrolls to the first errored field (by key) when form submitted with missing required fields
- Both scroll behaviors verified by unit tests with `vi.fn()` mock on `Element.prototype.scrollIntoView`
- Full test suite: 204 tests pass, 0 failures, 0 regressions
## Task Commits
Each task was committed atomically:
1. **Task 1: Step transition animation with reduced-motion guard** - `9af58bc` (feat)
2. **Task 2 RED: Failing tests for auto-scroll to first error** - `8b7daa8` (test)
3. **Task 2 GREEN: Auto-scroll to first errored field implementation** - `90cdeda` (feat)
**Plan metadata:** (docs commit follows)
_Note: Task 2 used TDD — separate test commit (RED) then implementation commit (GREEN)._
## Files Created/Modified
- `src/index.css` - Added `--animate-step-in` token in `@theme`, `@keyframes step-in`, and `@media prefers-reduced-motion: reduce` guard
- `src/App.tsx` - Step content wrapper: added `key={state.currentStep}` and `animate-step-in` class
- `src/components/wizard/BackendSelectionStep.tsx` - Added `onInvalidSubmit` scrolling to `remote-name`, wired to form `onSubmit` and `handleCardClick`
- `src/components/wizard/BackendSelectionStep.test.tsx` - Added `scrollIntoView` mock in `beforeEach`, added POLISH-04 scroll test
- `src/components/wizard/RemoteConfigStep.tsx` - Added `FieldErrors` import, `onInvalidSubmit` scrolling to first error key, wired to form `onSubmit`
- `src/components/wizard/RemoteConfigStep.test.tsx` - Added `scrollIntoView` mock in `beforeEach`, added POLISH-04 scroll test
## Decisions Made
- CSS-only reduced-motion guard via `@media (prefers-reduced-motion: reduce)` — consistent with existing `prefers-reduced-motion: no-preference` pattern at line 45 of index.css. No JS listener.
- `key={state.currentStep}` on the step content wrapper div (not on step components themselves) — forces React to unmount/remount the wrapper, reliably triggering the CSS animation from its `from` state on every step change.
- BackendSelectionStep `onInvalidSubmit` uses `getElementById('remote-name')` directly (hardcoded) — only one validatable field exists in that step, no dynamic key needed.
- Added `FieldErrors` to react-hook-form imports in RemoteConfigStep (alongside existing `FieldError`) for typed `errors` parameter in `onInvalidSubmit`.
## Deviations from Plan
None - plan executed exactly as written.
## Issues Encountered
None.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- POLISH-03 (step animation) and POLISH-04 (scroll-to-error) are complete
- Phase 11 plan 02 satisfies all must_haves and success criteria
- Ready for plan 11-03 if it exists, or phase 11 completion
---
*Phase: 11-polish-responsiveness*
*Completed: 2026-04-01*
@@ -0,0 +1,565 @@
# Phase 11: Polish & Responsiveness - Research
**Researched:** 2026-04-01
**Domain:** Tailwind v4 responsive utilities, CSS animations, focus-visible, scroll-into-view
**Confidence:** HIGH
## Summary
Phase 11 is a pure CSS/layout polish phase — four independent requirements, zero new runtime dependencies. Every change is either adding Tailwind responsive prefixes (`sm:`, absence of prefix = mobile-first), adding `focus-visible:` utility classes, adding a CSS keyframe animation in `index.css`, or calling `element.scrollIntoView()` from a `react-hook-form` submit handler.
The technical risk is very low. Tailwind v4 retains the same responsive prefix syntax from v3 (`sm:`, `md:`, `lg:`). The `focus-visible:` variant is natively supported in Tailwind v4. CSS `@keyframes` and `@media (prefers-reduced-motion)` are standard CSS features, not Tailwind-specific. The auto-scroll requirement (`scrollIntoView`) uses a browser-native DOM API that is supported in all modern browsers and works correctly in jsdom for tests.
The only structural decision with wider impact is step transitions (POLISH-03): adding a CSS animation class to the step content wrapper in `App.tsx` (`WizardShell`). Since the step container is a single div that re-renders its child on step change, a CSS animation triggered by a React key or className update is the correct approach. The `prefers-reduced-motion` guard belongs in `index.css` via `@media (prefers-reduced-motion: reduce)` — consistent with the existing theme transition guard already in the file.
**Primary recommendation:** Implement as four independent tasks in this order: (1) POLISH-01 mobile responsive layout, (2) POLISH-02 focus-visible indicators, (3) POLISH-03 step transition animation, (4) POLISH-04 auto-scroll to first error. Each task touches different files with zero cross-task dependencies.
---
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|-----------------|
| POLISH-01 | Wizard layout adapts to mobile screens — backend cards stack, form fields go full-width, step indicator collapses, buttons stretch | Tailwind v4 mobile-first responsive prefixes; `BackendCard` grid needs `flex-col` default with `sm:grid-cols-2` or keep `flex-col` stacked; `StepIndicator` label text visibility toggle; buttons need `w-full sm:w-auto` |
| POLISH-02 | All interactive elements have visible MD3 focus indicators (3px outline) using `focus-visible` for keyboard navigation | `focus-visible:outline` + `focus-visible:outline-3` or `focus-visible:ring-3`; Tailwind v4 supports arbitrary ring widths; apply to buttons, BackendCard, select, tooltip buttons, ThemeToggle, StepIndicator back-nav buttons |
| POLISH-03 | Step transitions use subtle fade/slide animation that respects `prefers-reduced-motion` | CSS `@keyframes step-in` in `index.css`; apply via Tailwind `animate-step-in` custom utility or inline className; `@media (prefers-reduced-motion: reduce)` guard in CSS |
| POLISH-04 | On validation failure, the view auto-scrolls to the first errored field | `react-hook-form` `useForm` returns `formState.errors`; in `handleSubmit` error callback (second argument), find first error field's DOM element with `document.getElementById(fieldKey)` and call `.scrollIntoView({ behavior: 'smooth', block: 'center' })` |
</phase_requirements>
---
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| Tailwind v4 | 4.2.2 | Responsive prefixes, focus-visible variant, custom animations | Already in project; v4 syntax is mobile-first identical to v3 |
| React | 18.3.1 | Component key trick for animation reset | Already in project |
| Browser DOM API | native | `scrollIntoView()` for POLISH-04 | No dependency needed |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| react-hook-form | 7.72.0 | Error callback for auto-scroll | Already used in `BackendSelectionStep` and `RemoteConfigStep`; second argument to `handleSubmit` is the error handler |
| Vitest + @testing-library/react | 4.1.1 / 16.3.2 | Unit tests for all behavioral changes | Existing test infrastructure |
**No new dependencies.** Zero new runtime packages for this phase.
**Installation:** None required.
---
## Architecture Patterns
### Recommended Project Structure
```
src/
├── index.css # Add @keyframes step-in + @media prefers-reduced-motion guard
├── App.tsx # Add animation class to step content wrapper (POLISH-03)
├── components/
│ ├── wizard/
│ │ ├── BackendSelectionStep.tsx # Responsive button row (POLISH-01), focus-visible on cards (POLISH-02), auto-scroll (POLISH-04)
│ │ ├── RemoteConfigStep.tsx # Auto-scroll to first error (POLISH-04), responsive button row (POLISH-01)
│ │ ├── DeploymentStep.tsx # Responsive button row (POLISH-01)
│ │ ├── ReviewStep.tsx # Responsive button row (POLISH-01)
│ │ └── StepIndicator.tsx # Collapsed labels on mobile (POLISH-01), MD3 focus ring update (POLISH-02)
│ └── ui/
│ ├── BackendCard.tsx # Full-width stacking on mobile (POLISH-01), focus-visible ring (POLISH-02)
│ ├── TextFieldMD3.tsx # Already full-width (w-full); verify — no change needed
│ └── ThemeToggle.tsx # focus-visible ring (POLISH-02)
└── styles/
└── md3-buttons.ts # Update focus-visible from ring-2 to ring-3 / outline-3 (POLISH-02)
```
### Pattern 1: Tailwind v4 Mobile-First Responsive Layout (POLISH-01)
**What:** Tailwind v4 uses mobile-first breakpoints. A class without a prefix applies at all sizes; a `sm:` prefix applies at ≥640px. To achieve mobile-first stacking: set the mobile layout as the base class, add `sm:` variant for wider screens.
**Backend cards — current layout:** `BackendCard` components are rendered in a `<div data-testid="backend-cards">` with no grid/flex wrapper. Each `BackendCard` itself is `flex flex-col items-start`. They stack vertically by default because they are block-level elements. To make them fill the card container width on mobile and appear in a grid on larger screens:
```tsx
// BackendSelectionStep.tsx — wrap backend cards in a responsive grid
<div data-testid="backend-cards" className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{/* BackendCard buttons already have w-full implied by grid cell */}
</div>
```
**BackendCard — ensure full width in grid cell:**
```tsx
// BackendCard.tsx — add w-full so button fills grid cell
className={[
'w-full flex flex-col items-start gap-1 rounded-xl border-2 p-4 text-left transition-all',
// ... rest of classes
].join(' ')}
```
**Button rows — current layout:** All step components have `<div className="flex gap-3 mt-6">`. On mobile, buttons should stretch to full width (or at least be wide enough to tap). On wider screens, shrink back to auto width:
```tsx
// All step button rows
<div className="flex flex-col sm:flex-row gap-3 mt-6">
<button className={`${MD3_BTN_OUTLINED} w-full sm:w-auto`}>Back</button>
<button className={`${MD3_BTN_FILLED} w-full sm:w-auto`}>Next</button>
</div>
```
**StepIndicator — mobile collapse:** On small screens, the 4-step indicator with labels is cramped. The label text below each circle can be hidden on mobile with `hidden sm:block`:
```tsx
// StepIndicator.tsx — hide label text on mobile
<span className="text-xs text-primary hidden sm:block">{label}</span>
// (for all three states: completed, active, future)
```
The connector lines and circles remain visible on mobile — only text labels collapse.
**App.tsx container:** The outer wrapper `py-12 px-4` is already reasonable for mobile. The `max-w-2xl` container is appropriate. No change needed at the shell level.
**When to use:** Any time a multi-column layout needs to degrade gracefully on narrow viewports.
### Pattern 2: MD3 Focus-Visible Indicators (POLISH-02)
**What:** MD3 specifies a 3dp focus ring. The current `md3-buttons.ts` already uses `focus-visible:ring-2` (2px). This needs to change to a 3px indicator. In Tailwind v4, `ring-3` is available (Tailwind v4 introduced `ring-3` as a standard utility — previously only `ring-2` and `ring-4` existed as defaults in v3, but v4 supports arbitrary-value-free ring-3).
**Verification:** Tailwind v4 uses a numeric scale (`ring-1`, `ring-2`, `ring-3`, `ring-4`) — confirmed by Tailwind v4 changelog and the migration from v3 where ring sizes follow the spacing scale.
**Current state of buttons (md3-buttons.ts):**
```ts
// Current: ring-2 with primary/50 opacity
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50'
```
**Update to:**
```ts
// POLISH-02: 3px MD3 focus ring
'focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-primary'
```
**BackendCard — currently missing focus-visible:**
```tsx
// BackendCard.tsx — add focus-visible ring
'focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-primary focus-visible:ring-offset-2'
```
**Select element in FieldRenderer:**
```tsx
// FieldRenderer.tsx select branch — update focus ring
'border-outline focus:ring-2' // → change to focus-visible:ring-3
```
**Tooltip buttons in FieldRenderer — currently only hover/click, no focus indicator:**
```tsx
// FieldRenderer.tsx tooltip buttons — add focus-visible
className="... focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded"
```
**StepIndicator completed-step buttons — current:**
```tsx
// group-focus-visible:ring-2 group-focus-visible:ring-primary/50
// Update to group-focus-visible:ring-3 group-focus-visible:ring-primary
```
**ThemeToggle:** Read the file to confirm current focus state — likely needs focus-visible ring added.
**Key insight:** The `focus-visible:` CSS pseudo-class only shows the outline during keyboard navigation (Tab key), not after mouse clicks. This is the correct MD3 and WCAG behavior. Browsers natively handle the distinction via the `:focus-visible` CSS pseudo-class.
### Pattern 3: Step Transition Animation (POLISH-03)
**What:** A subtle fade+slide-up animation when the step content changes. Applied to the content wrapper in `App.tsx` (`WizardShell`). The animation triggers when the step content re-renders.
**Mechanism — React key trick:** The step content `div` in `WizardShell` receives a `key={state.currentStep}`. When `currentStep` changes, React unmounts the old div and mounts a new one, which triggers the CSS `@keyframes` animation from its initial state.
**CSS in index.css:**
```css
@keyframes step-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Reduced-motion guard */
@media (prefers-reduced-motion: reduce) {
.animate-step-in {
animation: none;
}
}
```
**Tailwind v4 custom animation registration (in index.css @theme block):**
```css
@theme {
/* ... existing tokens ... */
--animate-step-in: step-in 200ms ease-out both;
}
```
This registers `animate-step-in` as a Tailwind utility class (Tailwind v4 `@theme` block wires `--animate-*` to `animate-*` utilities).
**App.tsx usage:**
```tsx
// WizardShell — add key + animation class to step content wrapper
<div key={state.currentStep} className="mt-8 animate-step-in">
{CurrentStep}
</div>
```
**Alternative without @theme:** Apply animation directly via `style` prop or CSS class defined in `@layer utilities`. However, using `@theme` to register `--animate-step-in` follows the project's established pattern of wiring values through the Tailwind token system.
**prefers-reduced-motion handling:** Two approaches:
1. CSS `@media (prefers-reduced-motion: reduce) { .animate-step-in { animation: none; } }` — declarative, no JS
2. Tailwind `motion-reduce:animate-none` utility class on the same element
Both work. The CSS approach in `index.css` is consistent with the existing reduced-motion guard already in the file (the `body { transition: ... }` block at line 44). **Use approach 1 (CSS media query in index.css).**
**Duration recommendation:** 200ms with `ease-out` — subtle enough to not feel slow, long enough to be perceptible. The 8px translateY offset matches MD3 "standard" motion easing.
### Pattern 4: Auto-Scroll to First Error (POLISH-04)
**What:** When form validation fails (clicking Next with invalid fields), automatically scroll the viewport to the first field that has an error.
**Mechanism — react-hook-form error handler:** `handleSubmit(onValid, onError)` accepts a second callback that fires when validation fails. The `onError` callback receives the `errors` object (same shape as `formState.errors`).
```tsx
// BackendSelectionStep.tsx — add error handler
function onInvalidSubmit(errors: FieldErrors<RemoteNameFormValues>) {
const firstErrorKey = Object.keys(errors)[0];
if (firstErrorKey) {
const el = document.getElementById(firstErrorKey);
el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
// In JSX:
<form onSubmit={handleSubmit(onValidSubmit, onInvalidSubmit)}>
```
**For RemoteConfigStep** (multiple fields, errors from dynamic registry): The `errors` object keys match `field.key` values. The `TextFieldMD3` and `PasswordField` components already use `id={field.key}` on their input elements, so `document.getElementById(field.key)` will find the correct DOM node.
**Field ID conventions confirmed:**
- `TextFieldMD3`: `<input id={id} ...>` — id is the field key
- `PasswordField`: passes `id` to TextFieldMD3 which sets it on the input
- `FieldRenderer`: passes `field.key` as `id` to both TextFieldMD3 and native select
**Order of errors:** `Object.keys(errors)` order in JavaScript corresponds to insertion order, which for react-hook-form mirrors the order in which fields are registered. For registry-driven forms, this matches the field order in `BACKEND_REGISTRY[backendType].fields`. Using `Object.keys(errors)[0]` reliably gets the first registered field with an error.
**jsdom scrollIntoView:** `scrollIntoView` is not implemented in jsdom (returns undefined). Tests must mock it: `vi.fn()` on `Element.prototype.scrollIntoView` or via `vi.stubGlobal`. This is a known pattern in the project (Phase 8 used `vi.stubGlobal` for localStorage/matchMedia).
### Anti-Patterns to Avoid
- **Using `focus:` instead of `focus-visible:`:** `focus:` shows the ring on mouse clicks too, which is visually noisy. `focus-visible:` is the correct MD3/WCAG approach — shows ring only during keyboard navigation.
- **CSS `outline: none` without `focus-visible` replacement:** Removing the default browser outline without providing a `focus-visible` replacement breaks keyboard accessibility entirely.
- **CSS animation on the step component itself instead of the wrapper:** Animating the step component means it only plays when the component instance is created — if the same component type renders at the same position, React reuses the instance and no animation fires. The `key={state.currentStep}` on the wrapper div forces a fresh mount.
- **Using `prefers-reduced-motion` only in JS:** The project convention (established in Phase 8) is to handle motion preferences in CSS with `@media (prefers-reduced-motion)`. Do not add a JS media query listener.
- **`scrollIntoView` without `behavior: 'smooth'`:** Instant scroll is jarring. `block: 'center'` keeps the errored field in the user's field of view with context above and below.
- **Forgetting `scrollIntoView` mock in tests:** jsdom does not implement `scrollIntoView`. Any test that triggers the error callback will throw or fail silently if the mock is not set up.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Responsive breakpoints | Custom media query hooks in JS | Tailwind `sm:` prefix | CSS-only, no JS overhead, standard project approach |
| Focus detection | `onFocus`/`onBlur` state + conditional ring class | `focus-visible:` Tailwind utility | CSS `:focus-visible` pseudo-class is native, zero JS |
| Animation orchestration | Framer Motion or React Spring | CSS `@keyframes` + Tailwind `animate-` | Zero new dependency; 200ms fade/slide is trivially simple |
| Scroll-to-error | Custom scroll calculation | `element.scrollIntoView()` | Native browser API; handles all edge cases (position, overflow) |
| Reduced motion JS hook | `window.matchMedia('(prefers-reduced-motion)')` listener | CSS `@media (prefers-reduced-motion: reduce)` | Declarative; consistent with existing pattern in index.css line 44 |
**Key insight:** All four POLISH requirements have direct CSS/browser-native solutions. The project's "zero new runtime dependencies" constraint for v1.2 is trivially satisfied.
---
## Common Pitfalls
### Pitfall 1: `ring-3` availability in Tailwind v4
**What goes wrong:** Developer uses `ring-3` expecting 3px, but the utility may not exist in all Tailwind versions the same way.
**Why it happens:** Tailwind v3 defaults included `ring-1` (1px), `ring-2` (2px), `ring-4` (4px), `ring-8` (8px). `ring-3` was added in Tailwind v4 as part of the extended numeric scale.
**How to avoid:** Tailwind v4.2.2 is confirmed in package.json. In v4, the ring scale follows `ring-{n}` matching shadow scale — `ring-3` = 3px ring is valid. Alternatively, use `ring-[3px]` as an arbitrary value to be safe.
**Warning signs:** If `ring-3` class has no visual effect, fall back to `ring-[3px]`.
### Pitfall 2: BackendCard stacking breaks existing test assertions
**What goes wrong:** Tests in `BackendSelectionStep.test.tsx` use `screen.getAllByRole('button')` and check DOM order. Adding a wrapper grid div does NOT change the button elements themselves — their order is preserved.
**Why it happens:** `getAllByRole` returns elements in DOM tree order. Adding a `<div className="grid ...">` wrapper around the cards preserves this order.
**How to avoid:** Only wrap the `data-testid="backend-cards"` div content in a grid container — no structural changes to the card buttons. Tests will continue to pass.
### Pitfall 3: `focus-visible:` not working in jsdom tests
**What goes wrong:** `focus-visible:` CSS classes are applied correctly in the DOM but jsdom tests cannot verify visual appearance.
**Why it happens:** jsdom does not compute CSS. Tests cannot assert "ring is visible."
**How to avoid:** For POLISH-02, tests should verify the className string contains the focus-visible utility (DOM attribute check) rather than visual rendering. Example:
```tsx
expect(button.className).toContain('focus-visible:ring-3');
```
Or simply skip visual focus tests — this requirement is a CSS/browser concern best verified by manual keyboard navigation.
### Pitfall 4: React key animation fires on initial render
**What goes wrong:** Adding `key={state.currentStep}` to the step wrapper means the animation also plays on the FIRST render (step 0 initial mount).
**Why it happens:** React mounts the div for the first time with key=0, triggering the keyframe animation.
**How to avoid:** This is acceptable behavior — the animation on initial render is imperceptible because the page itself is loading. A `useState` flag to suppress initial animation adds complexity for no visible benefit. Accept this tradeoff.
### Pitfall 5: `scrollIntoView` in jsdom throws unless mocked
**What goes wrong:** Tests that call `handleSubmit` with an invalid form — triggering the `onInvalidSubmit` callback — will call `element.scrollIntoView()` which is not implemented in jsdom and throws `TypeError: el.scrollIntoView is not a function`.
**Why it happens:** jsdom only implements a subset of the browser DOM API.
**How to avoid:** Add to test file setup:
```ts
// At top of test file or in beforeEach
Element.prototype.scrollIntoView = vi.fn();
```
Or use `vi.stubGlobal` if preferred. This pattern is documented in the project (Phase 8 pattern for localStorage/matchMedia).
### Pitfall 6: StepIndicator label collapse hides text in tests
**What goes wrong:** Tests that check for step label text (e.g., `screen.getByText('Backend')`) may fail if the label `<span>` is `hidden` on mobile by `hidden sm:block`.
**Why it happens:** jsdom renders the DOM but applies CSS classes. `hidden` translates to `display: none` in Tailwind — jsdom does NOT compute this style, so the element IS in the DOM and accessible to `getByText`.
**How to avoid:** `hidden` class does not cause `queryByText` to fail in jsdom because jsdom ignores CSS display properties. No test change needed.
---
## Code Examples
Verified patterns from codebase and established project conventions:
### POLISH-01: Responsive Backend Cards Grid
```tsx
// BackendSelectionStep.tsx — wrap backend-cards div
<div data-testid="backend-cards" className="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-4">
{Object.entries(BACKEND_REGISTRY).map(([type, entry]) => (
<BackendCard
key={type}
name={entry.displayName}
description={entry.description}
selected={state.remote.backendType === type}
onClick={() => handleCardClick(type as BackendType)}
/>
))}
</div>
```
### POLISH-01: Responsive Button Row (all step components)
```tsx
// Replace: <div className="flex gap-3 mt-6">
// With:
<div className="flex flex-col sm:flex-row gap-3 mt-6">
<button type="button" className={`${MD3_BTN_OUTLINED} w-full sm:w-auto`}>Back</button>
<button type="submit" className={`${MD3_BTN_FILLED} w-full sm:w-auto`}>Next</button>
</div>
```
### POLISH-01: StepIndicator label collapse
```tsx
// StepIndicator.tsx — add hidden sm:block to all three label spans
// Completed step:
<span className="text-xs text-primary hidden sm:block">{label}</span>
// Active step:
<span className="text-xs text-primary font-semibold hidden sm:block">{label}</span>
// Future step:
<span className="text-xs text-on-surface-container/40 hidden sm:block">{label}</span>
```
### POLISH-02: Updated md3-buttons.ts focus ring
```ts
// md3-buttons.ts — update all three button constants
// Change focus-visible:ring-2 focus-visible:ring-primary/50
// To:
'focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-primary focus-visible:ring-offset-2'
```
### POLISH-02: BackendCard focus ring
```tsx
// BackendCard.tsx — add to className array
'focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-primary focus-visible:ring-offset-2'
```
### POLISH-03: Animation in index.css
```css
/* Add to index.css after existing @layer base block */
@keyframes step-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.animate-step-in {
animation: none !important;
}
}
```
```css
/* In existing @theme block — add after existing --color-* tokens */
@theme {
/* ... existing tokens ... */
--animate-step-in: step-in 200ms ease-out both;
}
```
### POLISH-03: App.tsx step wrapper with key + animation
```tsx
// WizardShell in App.tsx — wrap CurrentStep
<div key={state.currentStep} className="mt-8 animate-step-in">
{CurrentStep}
</div>
```
### POLISH-04: react-hook-form error handler with scrollIntoView
```tsx
// BackendSelectionStep.tsx
import type { FieldErrors } from 'react-hook-form';
function onInvalidSubmit(errors: FieldErrors<RemoteNameFormValues>) {
const firstKey = Object.keys(errors)[0];
if (firstKey) {
document.getElementById(firstKey)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
// In JSX:
<form onSubmit={handleSubmit(onValidSubmit, onInvalidSubmit)}>
```
```tsx
// RemoteConfigStep.tsx — same pattern, errors are record of FieldError
function onInvalidSubmit(errors: FieldErrors) {
const firstKey = Object.keys(errors)[0];
if (firstKey) {
document.getElementById(firstKey)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
```
### POLISH-04: Test setup for scrollIntoView mock
```ts
// In test file using beforeEach or at top-level
import { beforeEach, vi } from 'vitest';
beforeEach(() => {
Element.prototype.scrollIntoView = vi.fn();
});
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `focus:ring-2` (shows on mouse click too) | `focus-visible:ring-3` (keyboard only) | Phase 9 partially, Phase 11 completes | Cleaner UX: no ring flash on mouse users |
| No motion | CSS `@keyframes` with reduced-motion guard | Phase 11 | Subtle animation that respects accessibility preference |
| Static layout (flex-col everywhere) | Mobile-first responsive grid/flex | Phase 11 | Cards and buttons adapt to viewport |
| No scroll-to-error | `scrollIntoView` on error | Phase 11 | Reduces confusion when validation fails on long forms |
**Deprecated/outdated:**
- `focus:ring-2 focus:ring-primary/50` in md3-buttons.ts: replaced by `focus-visible:ring-3 focus-visible:ring-primary` in this phase
---
## Open Questions
1. **Should BackendCard use a 2-column grid or keep single-column stacked on mobile?**
- What we know: POLISH-01 says "backend cards stack vertically" — this is the mobile state. A `grid-cols-1 sm:grid-cols-2` layout satisfies "stack on mobile, 2-col on wider screen."
- What's unclear: Whether a 2-column grid on desktop is desired (7 backends = 3.5 rows of 2) vs current implicit single-column.
- Recommendation: Use `grid grid-cols-1 sm:grid-cols-2 gap-3` — satisfies the requirement exactly (stacked on mobile) while being more space-efficient on desktop. If the user wants single-column always, the `sm:grid-cols-2` can be omitted with no functional change to the mobile behavior.
2. **focus-visible ring-offset color in dark mode**
- What we know: `ring-offset` creates a gap between the ring and the element. In dark mode, the offset background would be the dark surface color — this looks correct without extra configuration since `ring-offset-2` uses the element's background.
- What's unclear: Whether `ring-offset-2` looks correct on BackendCard dark surface (`bg-surface-container` = gray-800 in dark).
- Recommendation: Add `ring-offset-surface` or simply omit ring-offset if it creates visual issues — the 3px ring alone is sufficient for MD3 compliance.
3. **Animation on intro-to-wizard transition (clicking "Get Started")**
- What we know: `key={state.currentStep}` only animates on step changes within the wizard, not when `showIntro` flips to `false`.
- What's unclear: Whether POLISH-03 ("step transitions") applies to the intro→wizard transition too.
- Recommendation: Out of scope per POLISH-03 wording ("step transitions"). The intro→wizard reveal can be a future enhancement. Keep scope to the 4 wizard step transitions only.
---
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Vitest 4.1.1 + @testing-library/react 16.3.2 |
| Config file | `vitest.config.ts` (environment: node — per-file override with `@vitest-environment jsdom`) |
| Quick run command | `npx vitest run` |
| Full suite command | `npx vitest run` |
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| POLISH-01 | Button rows use `flex-col sm:flex-row` on all step components | unit (className assertion) | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | ✅ needs new test |
| POLISH-01 | Backend cards container has `grid` class | unit (DOM structure) | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | ✅ needs new test |
| POLISH-01 | StepIndicator label spans have `hidden sm:block` class | unit (className assertion) | `npx vitest run src/components/wizard/StepIndicator.test.tsx` | ✅ needs new test |
| POLISH-02 | MD3_BTN_FILLED contains `focus-visible:ring-3` | unit (string constant check) | `npx vitest run` | ❌ Wave 0 (new test file or inline assertion) |
| POLISH-02 | BackendCard className includes `focus-visible:ring-3` | unit (className assertion) | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | ✅ needs new test |
| POLISH-03 | Step content wrapper has `animate-step-in` class | unit (DOM assertion) | `npx vitest run src/App.test.tsx` | ✅ needs new test |
| POLISH-03 | CSS @keyframes step-in defined in index.css | manual/visual | N/A | manual verification |
| POLISH-04 | `scrollIntoView` called on first error field when remote name is empty | unit | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | ✅ needs new test |
| POLISH-04 | `scrollIntoView` called on first error field in RemoteConfigStep | 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
- [ ] Optional: `src/styles/md3-buttons.test.ts` — verify `focus-visible:ring-3` string in button constants (alternatively, assertions can be added to existing test files)
*(All other test files exist. New test cases are added inline to existing test files as part of each task implementation.)*
---
## Component Audit for POLISH-02 (focus-visible completeness)
Every interactive element must be verified. Current state from codebase read:
| Element | File | Current focus state | Action needed |
|---------|------|--------------------|-|
| MD3_BTN_FILLED button | md3-buttons.ts | `focus-visible:ring-2 focus-visible:ring-primary/50` | Upgrade to `ring-3 ring-primary` |
| MD3_BTN_OUTLINED button | md3-buttons.ts | `focus-visible:ring-2 focus-visible:ring-primary/50` | Upgrade to `ring-3 ring-primary` |
| MD3_BTN_TEXT button | md3-buttons.ts | `focus-visible:ring-2 focus-visible:ring-primary/50` | Upgrade to `ring-3 ring-primary` |
| BackendCard button | BackendCard.tsx | No focus-visible classes | Add `focus-visible:ring-3 focus-visible:ring-primary` |
| StepIndicator completed-step button | StepIndicator.tsx | `group-focus-visible:ring-2 group-focus-visible:ring-primary/50` | Upgrade to `group-focus-visible:ring-3 group-focus-visible:ring-primary` |
| TextFieldMD3 input | TextFieldMD3.tsx | `focus:ring-2` (not focus-visible) | Change to `focus-visible:ring-2` (or keep for input — focus on input is expected for mouse users too) |
| FieldRenderer tooltip buttons | FieldRenderer.tsx | No focus-visible | Add `focus-visible:ring-2 focus-visible:ring-primary rounded` |
| FieldRenderer select | FieldRenderer.tsx | `focus:ring-2` | Change to `focus-visible:ring-2` |
| ThemeToggle button | ThemeToggle.tsx | Unknown — needs read | Verify and add if missing |
**Note on TextFieldMD3 input:** For text inputs, `focus:ring-2` (not `focus-visible:`) is acceptable per WCAG 2.1 — it's conventional to show a focus ring on inputs even when focused by mouse (helps users know which field is active). The POLISH-02 requirement specifically calls out `focus-visible` for "interactive elements" — this primarily targets buttons, cards, and controls that don't need a constant focus indicator when mouse-clicked. Keep text inputs as-is with `focus:ring`.
---
## Sources
### Primary (HIGH confidence)
- Direct codebase inspection — `src/App.tsx`, `src/index.css`, `src/styles/md3-buttons.ts`, `src/components/wizard/StepIndicator.tsx`, `src/components/ui/BackendCard.tsx`, `src/components/ui/TextFieldMD3.tsx`, `src/components/ui/FieldRenderer.tsx`, `src/components/wizard/BackendSelectionStep.tsx`, `src/components/wizard/RemoteConfigStep.tsx`, `src/components/wizard/DeploymentStep.tsx`, `src/components/wizard/ReviewStep.tsx`
- Direct test file inspection — `src/App.test.tsx`, `src/components/wizard/BackendSelectionStep.test.tsx`, `src/components/wizard/StepIndicator.test.tsx`, `src/components/wizard/RemoteConfigStep.test.tsx`
- `package.json` — confirmed Tailwind v4.2.2, Vitest 4.1.1, react-hook-form 7.72.0
- `vitest.config.ts` — confirmed test environment setup
### Secondary (MEDIUM confidence)
- Tailwind v4 documentation: `ring-3` utility availability, `@theme --animate-*` registration, mobile-first breakpoint behavior — consistent with v4 changelog (breakpoints unchanged from v3, ring scale extended in v4)
- MDN CSS `@keyframes` + `prefers-reduced-motion` media query — web standard, HIGH confidence
- react-hook-form v7 documentation: `handleSubmit(onValid, onInvalid)` second argument — confirmed in existing codebase usage pattern
### Tertiary (LOW confidence)
- None — all claims verified by codebase inspection or official specification
## 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
- Responsive patterns: HIGH — Tailwind mobile-first is a stable API unchanged from v3 to v4
**Research date:** 2026-04-01
**Valid until:** 2026-05-01 (stable Tailwind/React APIs; only invalidated by component refactors)
@@ -0,0 +1,57 @@
---
status: complete
phase: 11-polish-responsiveness
source: [11-01-SUMMARY.md, 11-02-SUMMARY.md]
started: 2026-04-01T12:00:00Z
updated: 2026-04-01T12:10:00Z
---
## Current Test
[testing complete]
## Tests
### 1. Responsive Backend Card Grid
expected: On a narrow viewport (mobile width ~375px), the backend selection cards stack in a single column. On a wider viewport (≥640px), they display in a 2-column grid. Each card fills the full width of its column.
result: pass
### 2. Responsive Button Rows
expected: On mobile width, the wizard navigation buttons (Back, Next, Deploy, etc.) stack vertically with each button taking full width. On desktop (≥640px), they sit side by side in a horizontal row.
result: pass
### 3. Step Indicator Label Collapse
expected: On mobile width, the step indicator shows only the numbered circles and connectors — label text is hidden. On desktop (≥640px), the step labels appear next to or below the circles.
result: pass
### 4. Keyboard Focus Rings
expected: Tab through the wizard using only the keyboard. Interactive elements (buttons, backend cards, theme toggle, select fields) show a visible ring/outline when focused via keyboard. Clicking with the mouse does NOT show the ring.
result: pass
### 5. Step Transition Animation
expected: Navigate between wizard steps (click Next/Back). Each step transition plays a subtle fade + slide-up animation (~200ms). The transition should feel smooth, not jarring.
result: pass
### 6. Reduced Motion Preference
expected: Enable "Reduce motion" in your OS accessibility settings (or via DevTools emulation). Navigate between steps — no animation should play, the step content should swap instantly with no movement.
result: pass
### 7. Scroll to Error on Backend Selection
expected: On the backend selection step, clear the remote name field (if visible) and try to proceed. The page should automatically scroll to the errored name field so it's visible in the viewport.
result: pass
### 8. Scroll to Error on Remote Config
expected: On the remote configuration step, leave required fields empty and try to submit/proceed. The page should automatically scroll to the first field showing a validation error.
result: pass
## Summary
total: 8
passed: 8
issues: 0
pending: 0
skipped: 0
## Gaps
[none yet]
@@ -0,0 +1,81 @@
---
phase: 11
slug: polish-responsiveness
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-04-01
---
# Phase 11 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution.
---
## Test Infrastructure
| Property | Value |
|----------|-------|
| **Framework** | Vitest 4.1.1 + @testing-library/react 16.3.2 |
| **Config file** | `vitest.config.ts` |
| **Quick run command** | `npx vitest run` |
| **Full suite command** | `npx vitest run` |
| **Estimated runtime** | ~15 seconds |
---
## Sampling Rate
- **After every task commit:** Run `npx vitest run`
- **After every plan wave:** Run `npx vitest run`
- **Before `/gsd:verify-work`:** Full suite must be green
- **Max feedback latency:** 15 seconds
---
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
| 11-01-01 | 01 | 1 | POLISH-01 | unit (className) | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | ✅ needs new test | ⬜ pending |
| 11-01-02 | 01 | 1 | POLISH-01 | unit (DOM) | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | ✅ needs new test | ⬜ pending |
| 11-01-03 | 01 | 1 | POLISH-01 | unit (className) | `npx vitest run src/components/wizard/StepIndicator.test.tsx` | ✅ needs new test | ⬜ pending |
| 11-02-01 | 02 | 1 | POLISH-02 | unit (string check) | `npx vitest run` | ❌ W0 | ⬜ pending |
| 11-02-02 | 02 | 1 | POLISH-02 | unit (className) | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | ✅ needs new test | ⬜ pending |
| 11-03-01 | 03 | 1 | POLISH-03 | unit (DOM) | `npx vitest run src/App.test.tsx` | ✅ needs new test | ⬜ pending |
| 11-03-02 | 03 | 1 | POLISH-03 | manual | N/A | manual | ⬜ pending |
| 11-04-01 | 04 | 1 | POLISH-04 | unit | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | ✅ needs new test | ⬜ pending |
| 11-04-02 | 04 | 1 | POLISH-04 | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ✅ needs new test | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
---
## Wave 0 Requirements
- [ ] Optional: `src/styles/md3-buttons.test.ts` — verify `focus-visible:ring-3` string in button constants
*Existing infrastructure covers most phase requirements. New test cases are added inline to existing test files.*
---
## Manual-Only Verifications
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| CSS @keyframes step-in defined in index.css | POLISH-03 | CSS animation definition not testable via jsdom | Inspect index.css for `@keyframes step-in` declaration |
| prefers-reduced-motion suppresses animation | POLISH-03 | Media query behavior not testable in jsdom | Check `@media (prefers-reduced-motion: reduce)` block exists in index.css |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 15s
- [ ] `nyquist_compliant: true` set in frontmatter
**Approval:** pending
@@ -0,0 +1,137 @@
---
phase: 11-polish-responsiveness
verified: 2026-04-01T13:35:00Z
status: passed
score: 10/10 must-haves verified
re_verification: false
human_verification:
- test: "Resize browser to <640px viewport and navigate through all wizard steps"
expected: "Backend cards stack in a single column, navigation buttons fill full width and stack vertically, StepIndicator shows only circles/connectors (no labels)"
why_human: "CSS responsive breakpoints cannot be tested by grep; requires actual viewport resize"
- test: "Navigate wizard with keyboard only (Tab + Enter/Space)"
expected: "Every button, card, and theme toggle button shows a visible 3px indigo focus ring when focused via keyboard; no ring appears when clicking with mouse"
why_human: "focus-visible CSS behavior requires actual browser rendering to verify visually"
- test: "Click through wizard steps (step 1 to step 2, etc.)"
expected: "Each new step content fades in and slides up by ~8px over 200ms"
why_human: "CSS animation playback requires browser rendering"
- test: "Enable prefers-reduced-motion (OS accessibility setting or DevTools) and navigate steps"
expected: "Step transitions show no animation — content appears instantly"
why_human: "prefers-reduced-motion media query behavior requires OS/browser setting and visual inspection"
- test: "On BackendSelectionStep, click Next without filling in the remote name"
expected: "Page scrolls smoothly to center the remote name input in the viewport"
why_human: "scrollIntoView behavior in real browser differs from jsdom mock; visual scroll feedback requires browser"
- test: "On RemoteConfigStep (e.g. S3 backend), click Next without filling required fields"
expected: "Page scrolls smoothly to center the first errored field in the viewport"
why_human: "Same as above — real scroll behavior needs browser verification"
---
# Phase 11: Polish & Responsiveness Verification Report
**Phase Goal:** The wizard feels production-ready on any device with smooth interactions and accessible keyboard navigation
**Verified:** 2026-04-01T13:35:00Z
**Status:** passed
**Re-verification:** No — initial verification
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|----|---------------------------------------------------------------------------------------------------|------------|------------------------------------------------------------------------------------------|
| 1 | On mobile viewport (<640px), backend cards stack vertically in a single column | VERIFIED | `BackendSelectionStep.tsx:80``grid grid-cols-1 sm:grid-cols-2` |
| 2 | On mobile viewport (<640px), navigation buttons stretch to full width and stack vertically | VERIFIED | All 4 step components have `flex flex-col sm:flex-row` wrapper + `w-full sm:w-auto` on buttons |
| 3 | On mobile viewport (<640px), StepIndicator hides label text but keeps circles and connectors | VERIFIED | `StepIndicator.tsx:45,52,59` — all 3 label spans have `hidden sm:block` |
| 4 | On desktop viewport (>=640px), backend cards display in a 2-column grid | VERIFIED | `BackendSelectionStep.tsx:80``sm:grid-cols-2` activates at 640px+ |
| 5 | Every button and interactive card shows a 3px MD3 focus ring when keyboard-navigated | VERIFIED | `md3-buttons.ts:14,20,26` — all constants have `focus-visible:ring-3 focus-visible:ring-primary`; `BackendCard.tsx:17`; `ThemeToggle.tsx:61,62`; `StepIndicator.tsx:42` |
| 6 | Focus ring does not appear on mouse click (uses focus-visible, not focus) | VERIFIED | All interactive elements consistently use `focus-visible:` prefix, not bare `focus:` |
| 7 | Changing wizard steps plays a subtle fade/slide-up animation on the new step content | VERIFIED | `App.tsx:65``key={state.currentStep} className="mt-8 animate-step-in"` forces remount; `index.css:72-81``@keyframes step-in` with `opacity 0→1, translateY(8px)→0` |
| 8 | Users with prefers-reduced-motion enabled see no animation on step change | VERIFIED | `index.css:83-87``@media (prefers-reduced-motion: reduce) { .animate-step-in { animation: none !important; } }` |
| 9 | When form validation fails, the view auto-scrolls to the first errored field | VERIFIED | `BackendSelectionStep.tsx:53-56``onInvalidSubmit` calls `getElementById('remote-name')?.scrollIntoView`; `RemoteConfigStep.tsx:46-51``onInvalidSubmit` resolves first error key and scrolls |
| 10 | scrollIntoView uses smooth behavior and centers the errored field | VERIFIED | Both components use `{ behavior: 'smooth', block: 'center' }` |
**Score:** 10/10 truths verified
### Required Artifacts
| Artifact | Expected | Status | Details |
|-------------------------------------------------------------|---------------------------------------------------|----------|----------------------------------------------------------------------------------------------|
| `src/styles/md3-buttons.ts` | MD3 button constants with focus-visible:ring-3 | VERIFIED | Lines 14, 20, 26 — all three constants contain `focus-visible:ring-3 focus-visible:ring-primary` |
| `src/components/ui/BackendCard.tsx` | Responsive full-width card with focus-visible ring | VERIFIED | Line 17 — base class includes `w-full` and `focus-visible:ring-3 focus-visible:ring-primary` |
| `src/components/wizard/StepIndicator.tsx` | Collapsed labels on mobile, upgraded focus ring | VERIFIED | Lines 45, 52, 59 — `hidden sm:block` on all 3 label spans; line 42 — `group-focus-visible:ring-3` |
| `src/components/wizard/BackendSelectionStep.tsx` | Responsive card grid and button row | VERIFIED | Line 80 — `grid grid-cols-1 sm:grid-cols-2`; line 91 — `flex flex-col sm:flex-row` |
| `src/index.css` | step-in keyframe animation and reduced-motion guard | VERIFIED | Line 69 — `--animate-step-in` in `@theme`; lines 72-81 — `@keyframes step-in`; lines 83-87 — reduced-motion guard |
| `src/App.tsx` | Step content wrapper with key and animation class | VERIFIED | Line 65 — `key={state.currentStep} className="mt-8 animate-step-in"` |
| `src/components/wizard/BackendSelectionStep.tsx` (scroll) | Auto-scroll to first error on validation failure | VERIFIED | Lines 53-56 — `onInvalidSubmit` wired to `handleSubmit` on both form onSubmit and handleCardClick |
| `src/components/wizard/RemoteConfigStep.tsx` | Auto-scroll to first error on validation failure | VERIFIED | Lines 46-51 — `onInvalidSubmit` wired to form `handleSubmit` |
### Key Link Verification
| From | To | Via | Status | Details |
|-------------------------------|---------------------------------|--------------------------------------------------|----------|---------------------------------------------------------------------------|
| `src/styles/md3-buttons.ts` | All step components | `MD3_BTN_FILLED` / `MD3_BTN_OUTLINED` constants | WIRED | Imported and used in BackendSelectionStep, RemoteConfigStep, DeploymentStep, ReviewStep |
| `src/components/ui/BackendCard.tsx` | `BackendSelectionStep.tsx` | Component usage | WIRED | Imported line 8 and rendered in grid at line 82 |
| `src/index.css` | `src/App.tsx` | `animate-step-in` Tailwind utility | WIRED | `--animate-step-in` registered in `@theme`; `animate-step-in` applied in App.tsx line 65 |
| `BackendSelectionStep.tsx` | DOM element `#remote-name` | `document.getElementById('remote-name')?.scrollIntoView` | WIRED | `onInvalidSubmit` wired as second arg to `handleSubmit` at lines 60, 70 |
| `RemoteConfigStep.tsx` | DOM element by first error key | `document.getElementById(firstKey)?.scrollIntoView` | WIRED | `onInvalidSubmit` wired as second arg to `handleSubmit` at line 70 |
### Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|-------------|-------------------------------------------------------------------------------------------------|-----------|--------------------------------------------------------------------------------------------|
| POLISH-01 | 11-01 | Wizard layout adapts to mobile screens — backend cards stack, form fields go full-width, step indicator collapses, buttons stretch | SATISFIED | `grid grid-cols-1 sm:grid-cols-2` on card container; `flex flex-col sm:flex-row` + `w-full sm:w-auto` on all button rows; `hidden sm:block` on all StepIndicator labels |
| POLISH-02 | 11-01 | All interactive elements have visible MD3 focus indicators (3px outline) using focus-visible for keyboard navigation | SATISFIED | All three MD3_BTN constants use `focus-visible:ring-3`; BackendCard, ThemeToggle, StepIndicator completed-step circle, and both FieldRenderer tooltip buttons all have `focus-visible` rings |
| POLISH-03 | 11-02 | Step transitions use subtle fade/slide animation that respects prefers-reduced-motion | SATISFIED | `@keyframes step-in` with 200ms ease-out in index.css; `key={state.currentStep} animate-step-in` in App.tsx; `@media (prefers-reduced-motion: reduce)` guard |
| POLISH-04 | 11-02 | On validation failure, the view auto-scrolls to the first errored field | SATISFIED | Both BackendSelectionStep and RemoteConfigStep implement `onInvalidSubmit` with `scrollIntoView({ behavior: 'smooth', block: 'center' })`; unit tests verify behavior (204 tests pass) |
No orphaned requirements — all four POLISH-xx IDs declared in REQUIREMENTS.md are claimed by plans 11-01 and 11-02.
### Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `src/components/ui/FieldRenderer.tsx` | 71 | `focus:ring-error/50` and `focus:ring-primary/50` on select element — ring color uses bare `focus:` while ring width uses `focus-visible:ring-2` | Info | Inconsistency only — no functional impact. Ring width (ring-2) is gated on focus-visible, so the color class is dormant on mouse click. Visual behavior is correct. |
No blocker or warning anti-patterns found. The single info-level item in FieldRenderer is a harmless inconsistency: the ring size was correctly changed to `focus-visible:ring-2` per the plan, but the color selectors (`focus:ring-error/50`, `focus:ring-primary/50`) retain the bare `focus:` prefix. Because the ring width class controls visibility, the color classes are inert during mouse focus. The plan only required changing the size class, so this is within spec.
### Human Verification Required
#### 1. Mobile layout at <640px viewport
**Test:** Open the wizard in a browser, resize the viewport to 375px wide, and navigate through all four steps.
**Expected:** Backend cards appear one per row (full width); Back/Next buttons fill the full width and stack vertically (Back on top, Next below); StepIndicator shows only the numbered circles with connector lines — no "Backend", "Remote Config", "Deployment", "Review" labels.
**Why human:** Tailwind responsive breakpoints (`sm:` prefix = 640px) require actual viewport rendering; grep confirms the classes exist but cannot confirm Tailwind compiles and applies them correctly.
#### 2. Keyboard focus rings (focus-visible behavior)
**Test:** Tab through all wizard interactive elements (theme toggle, step indicator completed circles, backend cards, form buttons). Then click each element with the mouse and inspect.
**Expected:** Tabbing to any element shows a 3px indigo ring; clicking with the mouse shows no ring.
**Why human:** `focus-visible` CSS pseudo-class behavior requires a real browser to distinguish pointer vs keyboard input.
#### 3. Step transition animation
**Test:** Start the wizard and click Next through the steps.
**Expected:** Each new step content performs a visible fade-in combined with a slight upward slide (8px) over approximately 200ms.
**Why human:** CSS animation playback requires browser rendering; cannot be verified by static analysis.
#### 4. Reduced-motion guard
**Test:** In OS accessibility settings (or Chrome DevTools > Rendering > Emulate CSS media feature: prefers-reduced-motion: reduce), navigate between wizard steps.
**Expected:** Step content appears instantly with no animation; no fade or slide visible.
**Why human:** OS-level media feature emulation requires browser/DevTools interaction.
#### 5. Scroll-to-error in real browser
**Test:** In the wizard, leave the remote name field empty and click Next (or any backend card). Then on RemoteConfigStep, leave required fields empty and click Next.
**Expected:** The page smoothly scrolls to center the first errored field in the viewport.
**Why human:** jsdom `scrollIntoView` is mocked in unit tests; real scrolling behavior (visual smoothness, centering accuracy) requires a live browser with a scrollable viewport.
### Gaps Summary
No gaps. All 10 observable truths are verified by static code analysis. All 4 POLISH requirements are satisfied with substantive, wired implementations. The full test suite passes (204 tests, 0 failures). The only item of note is a harmless inconsistency in FieldRenderer's select element where ring color classes still use `focus:` while the ring width class correctly uses `focus-visible:` — this has no visual impact on the POLISH-02 goal.
Six items are flagged for human verification because they involve visual rendering, responsive breakpoints, and OS-level accessibility settings that cannot be evaluated by static analysis alone.
---
_Verified: 2026-04-01T13:35:00Z_
_Verifier: Claude (gsd-verifier)_
@@ -0,0 +1,149 @@
---
phase: 12-dark-mode-visibility-fixes
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/components/wizard/BackendSelectionStep.tsx
- src/components/wizard/RemoteConfigStep.tsx
- src/components/wizard/DeploymentStep.tsx
- src/components/wizard/ReviewStep.tsx
autonomous: true
requirements: [DARK-01, DARK-02]
must_haves:
truths:
- "All h2 step headings are visible (readable text) in dark mode"
- "DeploymentStep labels, legends, checkboxes, and radios are visible and styled in dark mode"
- "ReviewStep security checkbox uses themed accent color"
artifacts:
- path: "src/components/wizard/BackendSelectionStep.tsx"
provides: "h2 with text-on-surface class"
contains: "text-on-surface"
- path: "src/components/wizard/RemoteConfigStep.tsx"
provides: "h2 with text-on-surface class"
contains: "text-on-surface"
- path: "src/components/wizard/DeploymentStep.tsx"
provides: "h2 with text-on-surface, styled legends/labels/controls"
contains: "text-on-surface"
- path: "src/components/wizard/ReviewStep.tsx"
provides: "h2 with text-on-surface, accent-color on checkbox"
contains: "text-on-surface"
key_links:
- from: "all step components"
to: "src/index.css"
via: "semantic token classes"
pattern: "text-on-surface|text-on-surface-container|accent-primary"
---
<objective>
Fix dark mode visibility for step headings, DeploymentStep native form controls, and ReviewStep security checkbox.
Purpose: All four wizard step headings are currently invisible in dark mode (browser default black text on dark background). DeploymentStep's native form controls (labels, legends, checkboxes, radios) are also unstyled and invisible. These are regressions from the v1.2 UI overhaul where structural elements were migrated but their text styling was omitted.
Output: All step components render readable text and properly styled controls in both light and dark mode.
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/12-dark-mode-visibility-fixes/12-RESEARCH.md
<interfaces>
<!-- Reference pattern from App.tsx line 19 (correct h2 styling): -->
```tsx
<h2 className="text-2xl font-bold text-on-surface mb-4">
```
<!-- Semantic token classes available (from index.css @theme): -->
- text-on-surface: primary text, headings
- text-on-surface-container: labels, secondary text
- bg-surface-container: input/select backgrounds
- border-outline: form control borders
- accent-primary: native checkbox/radio accent color (maps to --color-primary)
- text-on-surface-variant: already used for step descriptions
<!-- Anti-patterns (from research): -->
- Do NOT use dark: prefix — project uses .dark class cascade
- Do NOT use hardcoded colors (text-gray-900, text-black, bg-white)
- Do NOT style <option> elements — browser-native, cannot be reliably styled
- Do NOT modify TextFieldMD3 input background — bg-transparent is intentional
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add semantic token classes to all step h2 headings</name>
<files>src/components/wizard/BackendSelectionStep.tsx, src/components/wizard/RemoteConfigStep.tsx, src/components/wizard/DeploymentStep.tsx, src/components/wizard/ReviewStep.tsx</files>
<action>
In each of the 4 step components, replace the bare `<h2>` tag with a styled version matching the established pattern from App.tsx:
1. BackendSelectionStep.tsx line 65: Change `<h2>Step 1: Select Backend</h2>` to `<h2 className="text-2xl font-bold text-on-surface mb-2">Step 1: Select Backend</h2>`. Note: use mb-2 (not mb-4) because the `<p>` description paragraph immediately follows with its own mt-1.
2. RemoteConfigStep.tsx line 65: Change `<h2>Step 2: Configure {backendLabel[backendType]}</h2>` to `<h2 className="text-2xl font-bold text-on-surface mb-2">Step 2: Configure {backendLabel[backendType]}</h2>`.
3. DeploymentStep.tsx line 24: Change `<h2>Step 3: Deployment Options</h2>` to `<h2 className="text-2xl font-bold text-on-surface mb-2">Step 3: Deployment Options</h2>`.
4. ReviewStep.tsx line 75: Change `<h2>Step 4: Review &amp; Download</h2>` to `<h2 className="text-2xl font-bold text-on-surface mb-2">Step 4: Review &amp; Download</h2>`.
These are className-only additions. Do not change element structure, content, or any other attributes. Tests use getByRole('heading') which is unaffected by className changes.
</action>
<verify>
<automated>npm test -- --reporter=verbose 2>&1 | tail -20</automated>
</verify>
<done>All 4 step h2 elements have className="text-2xl font-bold text-on-surface mb-2". Full test suite passes with no regressions.</done>
</task>
<task type="auto">
<name>Task 2: Style DeploymentStep form controls and ReviewStep checkbox</name>
<files>src/components/wizard/DeploymentStep.tsx, src/components/wizard/ReviewStep.tsx</files>
<action>
**DeploymentStep.tsx** — Apply semantic token classes to all native form controls:
1. Include-install section (lines 31-42): Replace the bare wrapper `<div>` with `<div className="flex flex-col gap-2 mb-4">`. Replace the bare `<label>` with `<label className="flex items-center gap-2 text-sm text-on-surface cursor-pointer">`. Add `className="accent-primary"` to the checkbox input. Remove the `{' '}` space before label text (gap-2 provides spacing).
2. Config deployment path fieldset (lines 45-71): Add `className="mb-4"` to the `<fieldset>`. Replace `<legend>Config deployment path</legend>` with `<legend className="text-sm font-medium text-on-surface-container mb-2">Config deployment path</legend>`. Replace both `<label>` wrappers with `<label className="flex items-center gap-2 text-sm text-on-surface cursor-pointer">`. Add `className="accent-primary"` to both radio inputs. Remove `{' '}` spaces.
3. Script targets fieldset (lines 74-91): Add `className="mb-4"` to the `<fieldset>`. Replace `<legend>Script targets</legend>` with `<legend className="text-sm font-medium text-on-surface-container mb-2">Script targets</legend>`. Replace both `<label>` wrappers with `<label className="flex items-center gap-2 text-sm text-on-surface cursor-pointer">`. Add `className="accent-primary"` to both checkbox inputs. Remove `{' '}` spaces.
**ReviewStep.tsx** — Style security checkbox:
1. Line 92-93: The checkbox inside the warning box. Add `className="accent-warning"` to the `<input type="checkbox">` to match the warning color scheme of its container. Use accent-warning (not accent-primary) since this checkbox lives in a warning-themed section and should match.
If accent-warning does not resolve (Tailwind may not generate it), fall back to `accent-[var(--r2b-warning)]` using arbitrary value syntax.
Do not change any onChange handlers, state logic, or dispatch calls. Only add className attributes.
</action>
<verify>
<automated>npm test -- --reporter=verbose 2>&1 | tail -20</automated>
</verify>
<done>DeploymentStep labels use text-on-surface, legends use text-on-surface-container with font-medium, all checkboxes/radios have accent-primary (or accent-warning for security checkbox). Full test suite passes.</done>
</task>
</tasks>
<verification>
1. `npm test` — full suite green, no regressions
2. Visual browser check: toggle dark mode, verify all 4 step headings are readable white text
3. Visual browser check: DeploymentStep controls are visible and properly styled in dark mode
4. Visual browser check: ReviewStep security checkbox accent matches warning color
</verification>
<success_criteria>
- All 4 wizard step h2 headings have `text-on-surface` class and are visible in dark mode
- DeploymentStep labels, legends, checkboxes, and radios are styled with semantic tokens
- ReviewStep security checkbox has themed accent color
- Full test suite passes with zero regressions
</success_criteria>
<output>
After completion, create `.planning/phases/12-dark-mode-visibility-fixes/12-01-SUMMARY.md`
</output>
@@ -0,0 +1,105 @@
---
phase: 12-dark-mode-visibility-fixes
plan: 01
subsystem: ui
tags: [react, tailwind, dark-mode, wizard, accessibility]
# Dependency graph
requires:
- phase: 08-theming
provides: semantic token CSS classes (text-on-surface, text-on-surface-container, accent-primary) via Tailwind v4 @theme
- phase: 11-polish-responsiveness
provides: wizard step components with completed structural layout
provides:
- All 4 wizard step h2 headings styled with text-on-surface (visible in dark mode)
- DeploymentStep native form controls (labels, legends, checkboxes, radios) fully styled with semantic tokens
- ReviewStep security checkbox accent color matching warning theme
affects: [dark-mode, wizard-steps, deployment-step, review-step]
# Tech tracking
tech-stack:
added: []
patterns:
- "accent-[var(--r2b-warning)] arbitrary value for warning-colored native checkbox (accent-warning not generated)"
- "flex items-center gap-2 pattern for native checkbox/radio labels (replaces {' '} space hack)"
- "fieldset + legend with text-on-surface-container font-medium for form group headings"
key-files:
created: []
modified:
- src/components/wizard/BackendSelectionStep.tsx
- src/components/wizard/RemoteConfigStep.tsx
- src/components/wizard/DeploymentStep.tsx
- src/components/wizard/ReviewStep.tsx
key-decisions:
- "accent-[var(--r2b-warning)] arbitrary value used for ReviewStep security checkbox — accent-warning class not confirmed generated by Tailwind v4 from --color-warning"
- "Removed {' '} space-before-text pattern in DeploymentStep labels — replaced with flex gap-2 layout for consistent spacing"
patterns-established:
- "h2 headings: text-2xl font-bold text-on-surface mb-2 (mb-2 when p description follows immediately)"
- "Native control labels: flex items-center gap-2 text-sm text-on-surface cursor-pointer"
- "Fieldset legends: text-sm font-medium text-on-surface-container mb-2"
- "Native checkboxes/radios: accent-primary (or accent-[var(--r2b-warning)] for warning context)"
requirements-completed: [DARK-01, DARK-02]
# Metrics
duration: 8min
completed: 2026-04-01
---
# Phase 12 Plan 01: Dark Mode Visibility Fixes Summary
**Semantic token classes applied to all 4 wizard step h2 headings and DeploymentStep native form controls, fixing invisible text regression from v1.2 UI overhaul**
## Performance
- **Duration:** 8 min
- **Started:** 2026-04-01T14:03:00Z
- **Completed:** 2026-04-01T14:11:00Z
- **Tasks:** 2
- **Files modified:** 4
## Accomplishments
- All 4 wizard step h2 elements now carry `text-2xl font-bold text-on-surface mb-2` — headings are readable in both light and dark mode
- DeploymentStep: include-install checkbox, config-path radio group, and script-targets checkbox group all styled with semantic tokens (text-on-surface labels, text-on-surface-container legends, accent-primary controls)
- ReviewStep: security acknowledgement checkbox uses `accent-[var(--r2b-warning)]` for consistent warning-themed accent color
- Full test suite (204 tests, 16 files) passes with zero regressions after both tasks
## Task Commits
Each task was committed atomically:
1. **Task 1: Add semantic token classes to all step h2 headings** - `f6b4070` (feat)
2. **Task 2: Style DeploymentStep form controls and ReviewStep checkbox** - `945f90d` (feat)
**Plan metadata:** _(docs commit follows)_
## Files Created/Modified
- `src/components/wizard/BackendSelectionStep.tsx` - h2 styled with text-on-surface
- `src/components/wizard/RemoteConfigStep.tsx` - h2 styled with text-on-surface
- `src/components/wizard/DeploymentStep.tsx` - h2 + all native form controls styled with semantic tokens
- `src/components/wizard/ReviewStep.tsx` - h2 styled with text-on-surface, security checkbox accent-[var(--r2b-warning)]
## Decisions Made
- Used `accent-[var(--r2b-warning)]` arbitrary value syntax for ReviewStep security checkbox instead of `accent-warning` — the `accent-warning` utility class is not confirmed to generate from the `--color-warning` Tailwind v4 theme token, so the arbitrary value form is the safe choice.
- Removed the `{' '}` before-label-text pattern throughout DeploymentStep; replaced with `flex gap-2` layout on the label wrapper. This is cleaner and aligns with the rest of the codebase's flexbox label patterns.
## Deviations from Plan
None - plan executed exactly as written.
## Issues Encountered
None
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- Phase 12 plan 01 complete — all dark mode visibility fixes for step headings and form controls are done (DARK-01, DARK-02 satisfied)
- Visual browser verification recommended: toggle dark mode and confirm all 4 step headings are readable white text, DeploymentStep controls are visible, and ReviewStep security checkbox accent matches warning amber color
---
*Phase: 12-dark-mode-visibility-fixes*
*Completed: 2026-04-01*
@@ -0,0 +1,101 @@
---
phase: 12-dark-mode-visibility-fixes
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- src/components/ui/FieldRenderer.tsx
autonomous: true
requirements: [DARK-03]
must_haves:
truths:
- "Select dropdowns in FieldRenderer are readable in dark mode (proper background and text color)"
artifacts:
- path: "src/components/ui/FieldRenderer.tsx"
provides: "select element with bg-surface-container and text-on-surface classes"
contains: "bg-surface-container"
key_links:
- from: "src/components/ui/FieldRenderer.tsx"
to: "src/index.css"
via: "semantic token classes on select element"
pattern: "bg-surface-container.*text-on-surface"
---
<objective>
Fix FieldRenderer select element dark mode visibility by adding background and text color semantic tokens.
Purpose: The `<select>` element in FieldRenderer has no background or text color class, causing browsers to apply a white system background in dark mode — making selected text invisible. Adding semantic token classes ensures the select matches the app's dark theme.
Output: Select elements in FieldRenderer render with correct background and text color in both themes.
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/12-dark-mode-visibility-fixes/12-RESEARCH.md
<interfaces>
<!-- Current select className in FieldRenderer.tsx lines 69-71: -->
```tsx
className={[
'w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus-visible:ring-2',
error ? 'border-error focus:ring-error/50' : 'border-outline focus:ring-primary/50',
].join(' ')}
```
<!-- Anti-patterns: -->
- Do NOT style <option> elements — browser-native, cannot be reliably styled cross-browser
- Do NOT use dark: prefix — project uses .dark class cascade with semantic tokens
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add dark mode classes to FieldRenderer select element</name>
<files>src/components/ui/FieldRenderer.tsx</files>
<action>
In FieldRenderer.tsx, update the `<select>` element's className array (lines 69-71) to include `bg-surface-container text-on-surface` in the first string of the array:
Change:
```tsx
'w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus-visible:ring-2',
```
To:
```tsx
'w-full rounded-md border px-3 py-2 text-sm bg-surface-container text-on-surface focus:outline-none focus-visible:ring-2',
```
This is a single-line className addition. Do not modify the error/border conditional, the register spread, or the option elements. Do not attempt to style the `<option>` children.
</action>
<verify>
<automated>npm test -- --reporter=verbose 2>&1 | tail -20</automated>
</verify>
<done>FieldRenderer select element has bg-surface-container and text-on-surface classes. Full test suite passes.</done>
</task>
</tasks>
<verification>
1. `npm test` — full suite green, no regressions
2. Visual browser check: select dropdowns in RemoteConfigStep show correct background and text in dark mode
</verification>
<success_criteria>
- FieldRenderer select element includes `bg-surface-container text-on-surface` in its className
- Select is readable in dark mode with proper contrast
- Full test suite passes with zero regressions
</success_criteria>
<output>
After completion, create `.planning/phases/12-dark-mode-visibility-fixes/12-02-SUMMARY.md`
</output>
@@ -0,0 +1,89 @@
---
phase: 12-dark-mode-visibility-fixes
plan: 02
subsystem: ui
tags: [tailwind, dark-mode, select, semantic-tokens, FieldRenderer]
# Dependency graph
requires:
- phase: 12-dark-mode-visibility-fixes
provides: dark mode semantic token CSS variables established in 12-01
provides:
- Select dropdowns in FieldRenderer readable in dark mode via bg-surface-container text-on-surface
affects: [RemoteConfigStep, any page rendering select via FieldRenderer]
# Tech tracking
tech-stack:
added: []
patterns: [semantic token classes on native HTML select elements for dark mode compatibility]
key-files:
created: []
modified:
- src/components/ui/FieldRenderer.tsx
key-decisions:
- "bg-surface-container text-on-surface added to select className — semantic tokens ensure correct rendering in both light and dark themes without dark: prefix"
patterns-established:
- "Native select elements require explicit bg and text semantic token classes — browser default white background overrides dark mode cascade otherwise"
requirements-completed: [DARK-03]
# Metrics
duration: 1min
completed: 2026-04-01
---
# Phase 12 Plan 02: FieldRenderer Select Dark Mode Fix Summary
**Select dropdowns in FieldRenderer now use bg-surface-container and text-on-surface semantic tokens, fixing browser-imposed white background in dark mode**
## Performance
- **Duration:** ~1 min
- **Started:** 2026-04-01T14:03:23Z
- **Completed:** 2026-04-01T14:04:05Z
- **Tasks:** 1
- **Files modified:** 1
## Accomplishments
- Added `bg-surface-container text-on-surface` to the select element's className in FieldRenderer.tsx
- Fixed invisible text in select dropdowns when app is in dark mode (browser was applying native white background)
- Full test suite (204 tests, 16 files) passes with zero regressions
## Task Commits
Each task was committed atomically:
1. **Task 1: Add dark mode classes to FieldRenderer select element** - `94e90e0` (feat)
## Files Created/Modified
- `src/components/ui/FieldRenderer.tsx` - Added `bg-surface-container text-on-surface` to select element className
## Decisions Made
None - followed plan as specified. The two semantic token classes were added exactly as prescribed, without touching option elements or border/error conditional logic.
## Deviations from Plan
None - plan executed exactly as written.
## Issues Encountered
None.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- FieldRenderer select elements are now fully dark-mode compatible
- Remaining dark mode fixes (if any) in phase 12 can proceed independently
- Phase 13 (Add remaining rclone remotes) is unblocked
---
*Phase: 12-dark-mode-visibility-fixes*
*Completed: 2026-04-01*
## Self-Check: PASSED
- FOUND: src/components/ui/FieldRenderer.tsx (contains bg-surface-container text-on-surface)
- FOUND: .planning/phases/12-dark-mode-visibility-fixes/12-02-SUMMARY.md
- FOUND: commit 94e90e0
@@ -0,0 +1,328 @@
# Phase 12: Dark Mode Visibility Fixes - Research
**Researched:** 2026-04-01
**Domain:** CSS dark mode theming — Tailwind v4 CSS custom properties, native form control styling
**Confidence:** HIGH (all findings based on direct source code audit of the live codebase)
## Summary
Phase 12 addresses a specific set of dark-mode visibility regressions introduced during the v1.2 UI overhaul. The project uses a robust two-layer CSS token system (raw `--r2b-*` custom properties overridden per `.dark` class, wired to Tailwind utility classes via `@theme`). The architecture is sound. The problem is that several components were incompletely migrated: their structural/container elements received semantic tokens but their text elements, headings, and native form controls were left unstyled, rendering them invisible or unreadable in dark mode.
The issues fall into three categories: (1) bare `<h2>` headings with no className in four wizard steps, (2) DeploymentStep using completely unstyled native form controls (fieldset, legend, labels, checkbox, radio), and (3) a `<select>` element in FieldRenderer with no background or text color class, causing browsers to apply a white system background in dark mode.
The fix approach is purely additive: apply existing semantic token classes (`text-on-surface`, `bg-surface-container`, etc.) to the affected elements. Zero new CSS variables, zero new design decisions, zero new dependencies. The existing `.dark` token overrides in `index.css` already define the correct dark-mode values — the work is connecting those tokens to the elements that currently bypass them.
**Primary recommendation:** Audit each component's rendered elements and add semantic token classes to every text and interactive element that currently has none. Do not introduce hardcoded colors.
## Standard Stack
### Core (already in project — no changes needed)
| Library | Version | Purpose | Notes |
|---------|---------|---------|-------|
| Tailwind v4 | ^4.2.2 | Utility classes via `@theme` + CSS custom properties | Already wired; all needed tokens exist |
| `@tailwindcss/vite` | ^4.2.2 | Tailwind v4 Vite integration | No postcss config needed |
### CSS Token System (already complete in `src/index.css`)
All required tokens exist. Dark variants are already correct in `.dark` block:
| Token | Light value | Dark value | Use |
|-------|------------|-----------|-----|
| `--r2b-on-surface` | `#111827` (gray-900) | `#F9FAFB` (gray-50) | Primary text, headings |
| `--r2b-on-surface-container` | `#374151` (gray-700) | `#D1D5DB` (gray-300) | Labels, secondary text |
| `--r2b-surface-container` | `#FFFFFF` | `#1F2937` (gray-800) | Input/select backgrounds |
| `--r2b-outline` | `#D1D5DB` | `#4B5563` (gray-600) | Borders |
| `--r2b-warning` | `#D97706` | `#FCD34D` | Warning text |
| `--r2b-success` | `#15803D` | `#86EFAC` | Success text |
No installation needed. No new dependencies.
## Architecture Patterns
### Two-Layer Token Pattern (established, Phase 08-01)
```css
/* Layer 1: raw values, overridden per theme */
@layer base {
:root { --r2b-on-surface: #111827; }
.dark { --r2b-on-surface: #F9FAFB; }
}
/* Layer 2: wire to Tailwind utility classes */
@theme {
--color-on-surface: var(--r2b-on-surface);
}
```
This means `text-on-surface` already works correctly in both themes. The fix is always "add the right class", never "add a new CSS variable".
### Semantic Class Application Pattern
**What:** Apply semantic Tailwind utility classes to elements that currently have no className or only structural classes.
**Key classes for this phase:**
- `text-on-surface` — primary text, headings (h1, h2, h3)
- `text-on-surface-container` — labels, secondary text, legend elements
- `bg-surface-container` — input/select backgrounds
- `text-on-surface-container/60` — placeholder-like text, muted elements
- `border-outline` — form control borders
### Anti-Patterns to Avoid
- **Hardcoded color classes** (`text-gray-900`, `text-black`, `bg-white`): These ignore the `.dark` cascade — they were the root cause of v1.1 tech debt that THEME-01 fixed.
- **Adding `dark:` variants**: The project uses `.dark` class toggle (not `prefers-color-scheme`); the `@custom-variant dark (&:where(.dark, .dark *))` declaration in `index.css` means semantic tokens already handle dark automatically. Never add explicit `dark:` prefixes.
- **Styling `<option>` elements**: Browser-native `<option>` elements cannot be reliably styled cross-browser via CSS. Accept system rendering for options; only style the `<select>` container itself.
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Dark select styling | Custom `<select>` component | Add `bg-surface-container text-on-surface` classes to existing `<select>` | `<option>` can't be styled cross-browser; native select with container styling is sufficient |
| Checkbox/radio dark mode | Custom checkboxes | Accept native rendering + add `accent-primary` Tailwind class | Already in project out-of-scope (REQUIREMENTS.md: "Custom checkbox/radio styling: High effort for few toggles") |
**Key insight:** All tokens already exist and resolve correctly in dark mode. The fix is application of existing classes, not new infrastructure.
## Specific Issues Found (Source Code Audit)
### Issue 1: Bare `<h2>` headings — CRITICAL (invisible in dark mode)
**Files affected:**
- `src/components/wizard/BackendSelectionStep.tsx` line 65: `<h2>Step 1: Select Backend</h2>`
- `src/components/wizard/RemoteConfigStep.tsx` line 65: `<h2>Step 2: Configure {backendLabel[backendType]}</h2>`
- `src/components/wizard/DeploymentStep.tsx` line 24: `<h2>Step 3: Deployment Options</h2>`
- `src/components/wizard/ReviewStep.tsx` line 75: `<h2>Step 4: Review &amp; Download</h2>`
**Root cause:** No `className` at all — browser renders `<h2>` in its default black color, which is invisible on the dark `bg-surface` (#111827) background.
**Fix:** Add `className="text-2xl font-bold text-on-surface mb-2"` (matching the pattern used by IntroSection's `<h2>` in App.tsx line 19).
**Reference pattern (correct, from App.tsx line 19):**
```tsx
<h2 className="text-2xl font-bold text-on-surface mb-4">
```
### Issue 2: DeploymentStep — completely unstyled (CRITICAL)
**File:** `src/components/wizard/DeploymentStep.tsx`
**Elements with no dark-mode styling:**
- Container `<div>` for checkbox (line 31) — no text color, labels will be black
- `<label>` wrappers — inherit black from browser default
- `<fieldset>` elements (lines 45, 74) — no styling
- `<legend>` elements (lines 46, 75: "Config deployment path", "Script targets") — browser default black
- All `<label>` elements inside fieldsets — browser default black
- `<input type="checkbox">` — browser default (white bg in dark mode)
- `<input type="radio">` — browser default (white bg in dark mode)
**Fix strategy:**
- Add `text-on-surface` to the containing `<div>` that wraps each section, so labels inherit
- Add `text-on-surface-container font-medium mb-2 block` to `<legend>` elements
- Add `accent-primary` to `<input type="checkbox">` and `<input type="radio">` (Tailwind utility for native control accent color)
- Wrap each control group section in a styled container for visual separation
### Issue 3: FieldRenderer `<select>` — no background/text color (SIGNIFICANT)
**File:** `src/components/ui/FieldRenderer.tsx` lines 6778
**Current classes:**
```
'w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus-visible:ring-2'
```
**Missing:** `bg-surface-container text-on-surface`
**Root cause:** Without explicit `bg-*` class, browsers apply system default background to `<select>` (typically white on Windows, even in dark mode). Text defaults to browser default (black).
**Fix:** Add `bg-surface-container text-on-surface` to the select's className array.
### Issue 4: ReviewStep — security acknowledge checkbox (MINOR)
**File:** `src/components/wizard/ReviewStep.tsx` line 93
The `<input type="checkbox">` inside the warning box has no styling. The label text uses `text-warning` (correct), but the checkbox renders with browser default.
**Fix:** Add `accent-primary` or `accent-warning` class to the checkbox input.
### Issue 5: Native `select``<option>` elements
**Cannot be fixed via CSS** — browser-native `<option>` elements do not respond to CSS color properties cross-browser (especially on Windows). This is an accepted limitation documented in REQUIREMENTS.md ("Custom checkbox/radio styling: High effort"). Accept native option rendering.
## Common Pitfalls
### Pitfall 1: Using `dark:` prefix instead of semantic tokens
**What goes wrong:** Developer adds `dark:text-white` instead of `text-on-surface`. This bypasses the token system and creates a second class of hardcoded colors.
**Why it happens:** Tailwind's built-in dark mode muscle memory.
**How to avoid:** The project uses `.dark` class-based dark variant declared in `index.css` via `@custom-variant dark (&:where(.dark, .dark *))`. Semantic token classes (`text-on-surface`) resolve correctly in both themes already. Never use `dark:` prefix.
### Pitfall 2: Styling `<option>` elements
**What goes wrong:** Adding `bg-surface-container` or `text-on-surface` to `<option>` elements. Ignored on Windows Chrome/Edge.
**How to avoid:** Only style the `<select>` container, not its `<option>` children.
### Pitfall 3: Breaking floating label behavior in TextFieldMD3
**What goes wrong:** Adding `bg-surface-container` to the `<input>` inside TextFieldMD3. The current `bg-transparent` is intentional — the input overlays the parent's background. Adding an explicit background breaks the floating label peer selector appearance.
**How to avoid:** Do not modify TextFieldMD3 input background. The component already renders correctly in dark mode because `bg-transparent` inherits from the parent container which uses `bg-surface-container`.
**Verification:** TextFieldMD3 uses `bg-transparent` on the input and the parent `<div>` has no background — the parent of FieldRenderer is expected to sit on a `bg-surface-container` surface (e.g., form sections). This is working correctly. Do not fix what is not broken.
### Pitfall 4: Test selector breaks from className additions
**What goes wrong:** Adding `className` to a bare `<h2>` can break test queries if tests use `getByRole('heading', { name: '...' })` with strict matching.
**How to avoid:** Tests using `getByRole` and `getByText` are unaffected by className additions. Watch out for `getByTestId` selectors that might break if structural wrappers are reorganized.
**From STATE.md:** "131 test selectors could break during component restyling — one-component-at-a-time discipline required"
## Code Examples
### Correct: heading with semantic token
```tsx
// Source: App.tsx line 19 (existing correct pattern in codebase)
<h2 className="text-2xl font-bold text-on-surface mb-2">Step 1: Select Backend</h2>
```
### Correct: styled select with dark mode support
```tsx
// Fix pattern for FieldRenderer.tsx
<select
id={field.key}
className={[
'w-full rounded-md border px-3 py-2 text-sm text-on-surface bg-surface-container',
'focus:outline-none focus-visible:ring-2',
error ? 'border-error focus:ring-error/50' : 'border-outline focus:ring-primary/50',
].join(' ')}
{...register(field.key)}
>
```
### Correct: native checkbox/radio with accent color
```tsx
// accent-primary uses the --color-primary token for native control accent color
<input
type="checkbox"
className="accent-primary"
checked={includeInstall}
onChange={...}
/>
```
### Correct: legend/label styling
```tsx
// legend
<legend className="text-sm font-medium text-on-surface-container mb-2">
Config deployment path
</legend>
// label wrapping a native control
<label className="flex items-center gap-2 text-sm text-on-surface cursor-pointer">
<input type="radio" className="accent-primary" ... />
Machine-wide (C:\ProgramData\rclone\)
</label>
```
### Correct: DeploymentStep section container pattern
```tsx
// Wrap each section for consistent dark-mode spacing and text inheritance
<div className="flex flex-col gap-2 mb-4">
<span className="text-sm font-medium text-on-surface-container">Include Installation</span>
<label className="flex items-center gap-2 text-sm text-on-surface cursor-pointer">
<input type="checkbox" className="accent-primary" ... />
Include rclone installation
</label>
</div>
```
## State of the Art
| Old Approach | Current Approach | Status |
|--------------|------------------|--------|
| Hardcoded Tailwind color classes | CSS custom property tokens via `@theme` | Phase 8 complete |
| `dark:` prefix variants | `.dark` class cascade via `@custom-variant` | Phase 8 complete |
| Browser-default heading color | `text-on-surface` on all heading elements | THIS PHASE |
| Unstyled native form controls | `accent-primary` + explicit text/bg tokens | THIS PHASE |
| Unstyled `<select>` element | `bg-surface-container text-on-surface` on select | THIS PHASE |
## Open Questions
1. **DeploymentStep visual redesign scope**
- What we know: The entire DeploymentStep uses bare native controls with no Tailwind classes — it was never styled beyond basic structure.
- What's unclear: Phase 12 is "dark mode visibility fixes" — should DeploymentStep get a deeper visual polish (MD3-style sections) or only the minimum to fix dark mode legibility?
- Recommendation: Minimum fix — add semantic tokens to existing structure. Full DeploymentStep MD3 redesign would be a separate phase. The phase name says "fixes" not "redesign".
2. **Accent color for native controls**
- What we know: CSS `accent-color` property sets the color of native checkbox/radio/range controls. Tailwind v4 `accent-primary` should map to `--color-primary`.
- What's unclear: Whether Tailwind v4's `accent-{color}` utility is generated from `@theme` color tokens.
- Recommendation: Verify `accent-primary` renders at plan time. If not available, use `accent-[var(--r2b-primary)]` as fallback.
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Vitest ^4.1.1 (node environment) + @testing-library/react |
| Config file | `vitest.config.ts` (environment: 'node') |
| Quick run command | `npm test -- --reporter=verbose` |
| Full suite command | `npm test` |
**Note:** Vitest is configured with `environment: 'node'` in `vitest.config.ts`. DOM component tests use jsdom via `@testing-library/react`. Visual dark mode correctness **cannot be automatically tested** with jsdom — CSS custom properties do not resolve in jsdom. Dark mode fixes require visual browser verification.
### Phase Requirements → Test Map
Phase 12 has no formal requirement IDs assigned (TBD per ROADMAP.md). The work is bug-fix in nature.
| Behavior | Test Type | Automated Command | Notes |
|----------|-----------|-------------------|-------|
| h2 headings have className (not empty) | Unit — DOM snapshot | Verify `getByRole('heading')` still resolves | Automated — className changes don't break role selectors |
| Select element renders with bg/text classes | Unit — className assertion | Manual code review | jsdom doesn't resolve CSS; visual verify in browser |
| DeploymentStep labels have text classes | Unit — DOM structure check | `npm test -- src/components/wizard/DeploymentStep` | Automated — existing tests cover structure |
| Dark mode visual correctness | Visual | Browser manual test | Cannot be automated; jsdom CSS limitation |
### Sampling Rate
- **Per task commit:** `npm test` (full suite, ~159 tests, fast in node env)
- **Per wave merge:** `npm test`
- **Phase gate:** Full suite green + visual browser verification in dark mode before `/gsd:verify-work`
### Wave 0 Gaps
None — existing test infrastructure covers all automated assertions. Dark mode correctness is visual-only and requires browser review.
*(Existing tests will catch any regressions in component rendering; they do not need modification for className additions to headings or form controls.)*
## Sources
### Primary (HIGH confidence — direct source code audit)
- `src/index.css` — complete token inventory, `.dark` overrides confirmed correct
- `src/components/wizard/BackendSelectionStep.tsx` — bare `<h2>` confirmed at line 65
- `src/components/wizard/RemoteConfigStep.tsx` — bare `<h2>` confirmed at line 65
- `src/components/wizard/DeploymentStep.tsx` — completely unstyled native controls confirmed
- `src/components/wizard/ReviewStep.tsx` — bare `<h2>` at line 75, unstyled checkbox at line 93
- `src/components/ui/FieldRenderer.tsx``<select>` missing `bg-*` and `text-*` at lines 6778
- `src/App.tsx` — correct `<h2>` pattern with `text-on-surface` at line 19 (reference)
- `.planning/REQUIREMENTS.md` — "Custom checkbox/radio styling: Out of scope"
- `.planning/STATE.md` — "131 test selectors could break during component restyling"
### Secondary (MEDIUM confidence)
- MDN CSS `accent-color` property — native control tinting via CSS; Tailwind `accent-{color}` utility class
- Tailwind v4 `@theme` documentation — confirms `accent-primary` should be generated from `--color-primary`
## Metadata
**Confidence breakdown:**
- Issue identification: HIGH — direct source code audit, not inference
- Fix patterns: HIGH — all use existing project-established patterns from App.tsx and other components
- Tailwind v4 `accent-primary` availability: MEDIUM — needs verification at plan time
- Test impact: HIGH — className additions to existing elements don't break `getByRole`/`getByText` selectors
**Research date:** 2026-04-01
**Valid until:** Indefinite (stable codebase, no external dependencies changing)
@@ -0,0 +1,78 @@
---
phase: 12
slug: dark-mode-visibility-fixes
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-04-01
---
# Phase 12 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution.
---
## Test Infrastructure
| Property | Value |
|----------|-------|
| **Framework** | Vitest ^4.1.1 (node environment) + @testing-library/react |
| **Config file** | `vitest.config.ts` |
| **Quick run command** | `npm test -- --reporter=verbose` |
| **Full suite command** | `npm test` |
| **Estimated runtime** | ~10 seconds |
---
## Sampling Rate
- **After every task commit:** Run `npm test -- --reporter=verbose`
- **After every plan wave:** Run `npm test`
- **Before `/gsd:verify-work`:** Full suite must be green + visual browser verification in dark mode
- **Max feedback latency:** 10 seconds
---
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
| 12-01-01 | 01 | 1 | h2 headings visible in dark mode | unit | `npm test -- src/components/wizard/BackendSelectionStep` | ✅ | ⬜ pending |
| 12-01-02 | 01 | 1 | h2 headings visible in dark mode | unit | `npm test -- src/components/wizard/RemoteConfigStep` | ✅ | ⬜ pending |
| 12-01-03 | 01 | 1 | h2 headings visible in dark mode | unit | `npm test -- src/components/wizard/DeploymentStep` | ✅ | ⬜ pending |
| 12-01-04 | 01 | 1 | h2 headings visible in dark mode | unit | `npm test -- src/components/wizard/ReviewStep` | ✅ | ⬜ pending |
| 12-01-05 | 01 | 1 | DeploymentStep labels/controls visible | unit | `npm test -- src/components/wizard/DeploymentStep` | ✅ | ⬜ pending |
| 12-01-06 | 01 | 1 | Select element styled for dark mode | unit | `npm test -- src/components/ui/FieldRenderer` | ✅ | ⬜ pending |
| 12-01-07 | 01 | 1 | ReviewStep checkbox styled | unit | `npm test -- src/components/wizard/ReviewStep` | ✅ | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
---
## Wave 0 Requirements
Existing infrastructure covers all phase requirements. No new test files or fixtures needed.
---
## Manual-Only Verifications
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Dark mode visual correctness | All elements visible on dark bg | jsdom does not resolve CSS custom properties | Toggle dark mode in browser, verify all headings/labels/controls are readable on dark background |
| Select dropdown appearance | Select bg/text correct in dark mode | Native `<option>` rendering is browser-dependent | Open select in dark mode on Chrome/Edge, verify text is legible |
| Native checkbox/radio accent | Controls use primary accent color | CSS `accent-color` not testable in jsdom | Toggle checkboxes/radios in dark mode, verify accent color matches primary theme |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 10s
- [ ] `nyquist_compliant: true` set in frontmatter
**Approval:** pending
@@ -0,0 +1,135 @@
---
phase: 12-dark-mode-visibility-fixes
verified: 2026-04-01T00:00:00Z
status: passed
score: 4/4 must-haves verified
re_verification: false
---
# Phase 12: Dark Mode Visibility Fixes — Verification Report
**Phase Goal:** Fix dark mode visibility regressions where step headings, DeploymentStep form controls, and FieldRenderer select elements are invisible or unstyled against the dark background
**Verified:** 2026-04-01
**Status:** PASSED
**Re-verification:** No — initial verification
---
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | All h2 step headings are visible (readable text) in dark mode | VERIFIED | All 4 step components carry `className="text-2xl font-bold text-on-surface mb-2"` on their h2 (BackendSelectionStep line 65, RemoteConfigStep line 65, DeploymentStep line 24, ReviewStep line 75). `text-on-surface` maps via @theme to `--color-on-surface``--r2b-on-surface`, which is `#F9FAFB` (gray-50) in `.dark` — readable white on dark background. |
| 2 | DeploymentStep labels, legends, checkboxes, and radios are visible and styled in dark mode | VERIFIED | Include-install label: `text-sm text-on-surface cursor-pointer`. Config-path legend: `text-sm font-medium text-on-surface-container mb-2`. Both config-path radio labels: `text-sm text-on-surface cursor-pointer`. Script-targets legend: same as config-path. Both script-targets checkbox labels: `text-sm text-on-surface cursor-pointer`. All checkbox/radio inputs: `className="accent-primary"`. Covers all native form controls in DeploymentStep. |
| 3 | ReviewStep security checkbox uses themed accent color | VERIFIED | Security checkbox at ReviewStep line 94 carries `className="accent-[var(--r2b-warning)]"`. This is the correct arbitrary-value syntax since `accent-warning` is not guaranteed to be generated by Tailwind v4 from `--color-warning`. `--r2b-warning` resolves to `#FCD34D` (amber-300) in dark mode, matching the warning-themed container. |
| 4 | Select dropdowns in FieldRenderer are readable in dark mode | VERIFIED | FieldRenderer.tsx select element (line 70) className array first string: `'w-full rounded-md border px-3 py-2 text-sm bg-surface-container text-on-surface focus:outline-none focus-visible:ring-2'`. Both `bg-surface-container` (`#1F2937` in dark) and `text-on-surface` (`#F9FAFB` in dark) are present — dark-on-dark contrast issue is resolved. |
**Score:** 4/4 truths verified
---
## Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `src/components/wizard/BackendSelectionStep.tsx` | h2 with `text-on-surface` class | VERIFIED | Line 65: `className="text-2xl font-bold text-on-surface mb-2"` present |
| `src/components/wizard/RemoteConfigStep.tsx` | h2 with `text-on-surface` class | VERIFIED | Line 65: `className="text-2xl font-bold text-on-surface mb-2"` present |
| `src/components/wizard/DeploymentStep.tsx` | h2 with `text-on-surface`, styled legends/labels/controls | VERIFIED | h2 at line 24 styled; all labels, legends, checkboxes, radios carry semantic token classes |
| `src/components/wizard/ReviewStep.tsx` | h2 with `text-on-surface`, accent-color on checkbox | VERIFIED | h2 at line 75 styled; security checkbox uses `accent-[var(--r2b-warning)]` at line 94 |
| `src/components/ui/FieldRenderer.tsx` | select element with `bg-surface-container` and `text-on-surface` | VERIFIED | Line 70: both classes confirmed in className array |
---
## Key Link Verification
### Plan 01 — semantic token classes → src/index.css
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| All step components | `src/index.css` | `text-on-surface` | WIRED | `--color-on-surface` defined in @theme block (line 57); resolves to `--r2b-on-surface` which has light/dark values in :root and .dark |
| All step components | `src/index.css` | `text-on-surface-container` | WIRED | `--color-on-surface-container` defined in @theme (line 61); resolves correctly in both themes |
| All step components | `src/index.css` | `accent-primary` | WIRED | `accent-primary` uses `--color-primary` (line 54); resolves to `#A5B4FC` (indigo-300) in dark mode |
| ReviewStep.tsx | `src/index.css` | `accent-[var(--r2b-warning)]` | WIRED | Arbitrary value bypasses generated utility class; `--r2b-warning` defined directly in :root and .dark (lines 22, 40) |
### Plan 02 — FieldRenderer select → src/index.css
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `src/components/ui/FieldRenderer.tsx` | `src/index.css` | `bg-surface-container text-on-surface` on select element | WIRED | Both classes map to @theme tokens with correct dark-mode values: `bg-surface-container``#1F2937` dark, `text-on-surface``#F9FAFB` dark |
---
## Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|------------|-------------|--------|----------|
| DARK-01 | 12-01-PLAN.md | All wizard step h2 headings are visible (readable text with proper contrast) in dark mode | SATISFIED | All 4 h2 elements carry `text-on-surface` class; semantic token resolves to `#F9FAFB` in dark mode |
| DARK-02 | 12-01-PLAN.md | DeploymentStep native form controls (labels, legends, checkboxes, radios) are visible and styled with semantic tokens in dark mode | SATISFIED | Every label uses `text-on-surface`, every legend uses `text-on-surface-container`, every native input uses `accent-primary`; ReviewStep security checkbox uses `accent-[var(--r2b-warning)]` |
| DARK-03 | 12-02-PLAN.md | FieldRenderer select element has proper background and text color in dark mode | SATISFIED | `bg-surface-container text-on-surface` confirmed at FieldRenderer.tsx line 70 |
**Traceability note:** REQUIREMENTS.md lists DARK-01, DARK-02, DARK-03 as "Phase 12 / Planned". All three are fully satisfied. No orphaned requirements detected — the traceability table maps exactly the three IDs claimed in plan frontmatter.
---
## Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| — | — | — | — | No anti-patterns detected |
Scanned for: TODO/FIXME/PLACEHOLDER comments, empty return bodies, stub handlers, hardcoded color classes (text-gray-*, text-black, bg-white), forbidden `dark:` prefix. None found in any of the five modified files.
---
## Git Commit Verification
| Commit | Message | Status |
|--------|---------|--------|
| `f6b4070` | feat(12-01): add semantic token classes to all step h2 headings | CONFIRMED in git log |
| `945f90d` | feat(12-01): style DeploymentStep form controls and ReviewStep checkbox | CONFIRMED in git log |
| `94e90e0` | feat(12-02): add bg-surface-container text-on-surface to select element in FieldRenderer | CONFIRMED in git log |
---
## Human Verification Required
The following items cannot be verified programmatically and require a browser check with dark mode active.
### 1. Step heading contrast — all four steps
**Test:** Open the app, toggle to dark mode, navigate through all 4 wizard steps.
**Expected:** Step headings ("Step 1: Select Backend", "Step 2: Configure …", "Step 3: Deployment Options", "Step 4: Review & Download") are visibly readable white text against the dark surface background.
**Why human:** Contrast ratio and perceptual readability require visual inspection; CSS cascade for `.dark` class on html element cannot be traced in static analysis.
### 2. DeploymentStep form controls in dark mode
**Test:** Navigate to Step 3 (Deployment Options) with dark mode active.
**Expected:** "Include rclone installation" label text is readable. "Config deployment path" and "Script targets" legend text is readable. Checkboxes and radio buttons show an accent color (indigo) when checked.
**Why human:** `accent-primary` color rendering for native checkbox/radio inputs is browser-engine-dependent.
### 3. ReviewStep security checkbox accent color
**Test:** Navigate to Step 4 with dark mode active. Tick the security acknowledgement checkbox.
**Expected:** The checkbox displays an amber/yellow accent color matching the warning container border/text, not the default indigo primary accent.
**Why human:** `accent-[var(--r2b-warning)]` uses an arbitrary CSS value. Whether the browser correctly applies it depends on `var()` resolution in the `accent-color` CSS property, which cannot be verified without rendering.
### 4. FieldRenderer select background and text in dark mode
**Test:** Navigate to any step that renders a `<select>` via FieldRenderer (e.g. RemoteConfigStep with a backend that has select fields such as OneDrive or GCS). Toggle dark mode.
**Expected:** The select element has a dark grey background (`#1F2937`) and light text (`#F9FAFB`), not the browser default white background with dark text.
**Why human:** Native `<select>` rendering is partially browser-controlled; semantic token application must be confirmed visually.
---
## Gaps Summary
No gaps. All four must-have truths are verified. All three requirement IDs (DARK-01, DARK-02, DARK-03) are satisfied. All five modified files contain exactly the prescribed semantic token classes. All three feature commits are confirmed in git history. No stub implementations, no placeholder returns, no forbidden `dark:` prefixes, no hardcoded color values detected.
The phase goal — fixing dark mode visibility regressions for step headings, DeploymentStep form controls, and FieldRenderer select elements — has been achieved at the implementation level. Four human verification items remain for visual/browser confirmation of rendering quality.
---
_Verified: 2026-04-01T00:00:00Z_
_Verifier: Claude (gsd-verifier)_
@@ -0,0 +1,235 @@
---
phase: 13-add-remaining-rclone-remotes
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/schemas/registry.ts
- src/schemas/index.ts
- src/store/types.ts
- src/generators/rclone-conf.ts
- src/schemas/registry.test.ts
- src/schemas/index.test.ts
- src/generators/rclone-conf.test.ts
autonomous: true
requirements: [REMOTE-01, REMOTE-02, REMOTE-06]
must_haves:
truths:
- "BACKEND_REGISTRY contains all 17 backends with correct fields and category"
- "BackendType is derived from registry keys, not an explicit union"
- "BACKEND_SCHEMAS auto-generates from registry (no manual per-backend calls)"
- "RCLONE_TYPE_MAP has correct type strings for all 17 backends"
- "buildRcloneConf produces valid rclone.conf for every new backend"
artifacts:
- path: "src/schemas/registry.ts"
provides: "17 backend entries with category field, BackendType derived via keyof"
contains: "as const"
- path: "src/schemas/index.ts"
provides: "Auto-generated BACKEND_SCHEMAS from registry keys"
contains: "Object.fromEntries"
- path: "src/generators/rclone-conf.ts"
provides: "RCLONE_TYPE_MAP entries for all 17 backends"
key_links:
- from: "src/schemas/registry.ts"
to: "src/store/types.ts"
via: "BackendType re-export"
pattern: "export type.*BackendType"
- from: "src/schemas/registry.ts"
to: "src/schemas/index.ts"
via: "BACKEND_SCHEMAS auto-generation"
pattern: "Object\\.keys\\(BACKEND_REGISTRY\\)"
---
<objective>
Refactor the backend registry to support 17 backends with categories, derive BackendType from registry keys, auto-generate BACKEND_SCHEMAS, and add all 10 new backend entries with correct rclone type mappings.
Purpose: This is the data foundation every other plan in Phase 13 depends on. No UI work can proceed until the registry has all backends, the types are derived, and schemas auto-generate.
Output: Updated registry.ts, index.ts, types.ts, rclone-conf.ts with all 17 backends, full test coverage.
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/13-add-remaining-rclone-remotes/13-CONTEXT.md
@.planning/phases/13-add-remaining-rclone-remotes/13-RESEARCH.md
<interfaces>
<!-- Current contracts that will be refactored -->
From src/schemas/registry.ts:
```typescript
export type BackendType = 'azureblob' | 's3' | 's3-compatible' | 'onedrive' | 'sftp' | 'gcs' | 'b2';
export interface FieldDef {
key: string;
label: string;
inputType: 'text' | 'password' | 'select' | 'toggle';
required: boolean;
placeholder?: string;
helpText?: string;
options?: { value: string; label: string }[];
validate?: { regex: RegExp; message: string };
tooltipText?: string;
}
// Current cast at bottom (must be removed):
// } as Record<BackendType, { displayName: string; description: string; fields: FieldDef[] }>;
```
From src/schemas/index.ts:
```typescript
export const BACKEND_SCHEMAS = {
azureblob: buildZodSchema('azureblob'),
// ... explicit per-backend calls
} as const;
```
From src/generators/rclone-conf.ts:
```typescript
const RCLONE_TYPE_MAP: Record<string, string> = {
azureblob: 'azureblob',
s3: 's3',
's3-compatible': 's3',
onedrive: 'onedrive',
sftp: 'sftp',
gcs: 'google cloud storage',
b2: 'b2',
};
```
From src/store/types.ts:
```typescript
import type { BackendType } from '../schemas/registry';
export type { BackendType };
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Refactor registry types and add all 10 new backend entries</name>
<files>src/schemas/registry.ts, src/store/types.ts</files>
<action>
1. Add `BackendCategory` type: `'cloud-object-storage' | 'cloud-drives' | 'protocol-based'`
2. Add `category` field to the backend entry shape. Update the interface used by entries to include `category: BackendCategory`.
3. Remove the explicit `BackendType` union literal. Instead, add `as const` to the BACKEND_REGISTRY object and derive: `export type BackendType = keyof typeof BACKEND_REGISTRY;`
4. CRITICAL: Remove the `as Record<BackendType, ...>` cast at the bottom of the registry object. With `as const`, TypeScript infers the precise type. The cast creates a circular reference when BackendType is derived from keyof.
5. Add `category` to all 7 existing entries:
- azureblob: 'cloud-object-storage'
- s3: 'cloud-object-storage'
- s3-compatible: 'cloud-object-storage'
- gcs: 'cloud-object-storage'
- b2: 'cloud-object-storage'
- onedrive: 'cloud-drives'
- sftp: 'protocol-based'
6. Add 10 new backend entries (see 13-RESEARCH.md for field details):
Cloud Object Storage:
- `azure-files`: displayName 'Azure Files', fields: account (text, required), key (password, required). Verify field names against rclone.org/azurefiles/ — account + key is the standard pattern.
- `swift`: displayName 'OpenStack Swift', fields: user (text, required), key (password, required), auth (text, required, placeholder 'https://auth.example.com/v3'), tenant (text, required), region (text, optional)
Cloud Drives (OAuth token-paste):
- `gdrive`: displayName 'Google Drive', fields: token (password, required, helpText about rclone authorize "drive"), service_account_credentials (password, optional, helpText for service account JSON), root_folder_id (text, optional)
- `dropbox`: displayName 'Dropbox', fields: token (password, required, helpText about rclone authorize "dropbox")
- `box`: displayName 'Box', fields: token (password, required), box_sub_type (select: user/enterprise, default 'user')
- `pcloud`: displayName 'pCloud', fields: token (password, required), hostname (select: api.pcloud.com/eapi.pcloud.com with labels 'US (default)'/'EU')
Protocol-based:
- `ftp`: displayName 'FTP', fields: host (text, required), user (text, optional, placeholder 'anonymous'), pass (password, optional), port (text, optional, placeholder '21'), explicit_tls (select: ''/true with labels 'Plain FTP'/'Explicit FTPS')
- `webdav`: displayName 'WebDAV', fields: url (text, required, placeholder 'https://example.com/dav'), user (text, required), pass (password, required), vendor (select: nextcloud/owncloud/sharepoint/sharepoint-ntlm/other)
- `smb`: displayName 'SMB / Windows Share', fields: host (text, required), user (text, required), pass (password, optional), domain (text, optional, placeholder 'WORKGROUP'), port (text, optional, placeholder '445')
- `http`: displayName 'HTTP (read-only)', fields: url (text, required, placeholder 'https://example.com/path/')
- `seafile`: displayName 'Seafile', fields: url (text, required, placeholder 'https://cloud.seafile.com'), user (text, required), pass (password, required)
7. Add appropriate helpText and tooltipText to new backend fields following existing patterns (see 13-RESEARCH.md pitfalls section for OAuth token fields).
8. Verify src/store/types.ts still works — it does `import type { BackendType } from '../schemas/registry'` and `export type { BackendType }`. No changes needed since the derived type exports identically.
IMPORTANT: Do NOT pass real placeholder text to fields that will be rendered by TextFieldMD3 — TextFieldMD3 uses `placeholder=" "` internally for floating labels. Use `helpText` instead for descriptive hints. Placeholders are only safe on fields NOT rendered via TextFieldMD3 (select fields, or fields where the placeholder is a format example like 'AKIAIOSFODNN7EXAMPLE').
</action>
<verify>
<automated>npx vitest run src/schemas/registry.test.ts src/store/ --reporter=verbose 2>&1 | tail -30</automated>
</verify>
<done>BACKEND_REGISTRY has 17 entries each with displayName, description, category, and fields. BackendType is derived from keyof typeof BACKEND_REGISTRY. No circular type reference. TypeScript compiles cleanly.</done>
</task>
<task type="auto">
<name>Task 2: Auto-generate BACKEND_SCHEMAS, update RCLONE_TYPE_MAP, update tests</name>
<files>src/schemas/index.ts, src/generators/rclone-conf.ts, src/schemas/index.test.ts, src/generators/rclone-conf.test.ts, src/schemas/registry.test.ts</files>
<action>
1. In src/schemas/index.ts: Replace the explicit BACKEND_SCHEMAS object with auto-generation:
```typescript
export const BACKEND_SCHEMAS = Object.fromEntries(
(Object.keys(BACKEND_REGISTRY) as BackendType[]).map(t => [t, buildZodSchema(t)])
) as Record<BackendType, ReturnType<typeof buildZodSchema>>;
```
This eliminates the need to add a manual `buildZodSchema()` call per new backend.
2. In src/generators/rclone-conf.ts: Add entries to RCLONE_TYPE_MAP for all 10 new backends. Verified type strings from rclone docs:
- 'azure-files': 'azurefiles'
- 'swift': 'swift'
- 'gdrive': 'drive'
- 'dropbox': 'dropbox'
- 'box': 'box'
- 'pcloud': 'pcloud'
- 'ftp': 'ftp'
- 'webdav': 'webdav'
- 'smb': 'smb'
- 'http': 'http'
- 'seafile': 'seafile'
3. Update src/schemas/registry.test.ts:
- Add test: every backend has displayName, description, category, and at least one field
- Add test: BackendCategory values are one of the three valid categories
- Verify all 17 backend keys are present
4. Update/create src/schemas/index.test.ts:
- Test that BACKEND_SCHEMAS has an entry for every key in BACKEND_REGISTRY (no undefined)
- Test that each schema is a ZodObject (not undefined)
5. Update src/generators/rclone-conf.test.ts:
- Add test: RCLONE_TYPE_MAP has entry for every BackendType
- Add a buildRcloneConf snapshot test for at least 3 new backends (gdrive, ftp, smb) to confirm correct output format
</action>
<verify>
<automated>npx vitest run src/schemas/ src/generators/rclone-conf.test.ts --reporter=verbose 2>&1 | tail -40</automated>
</verify>
<done>BACKEND_SCHEMAS auto-generates for all 17 backends. RCLONE_TYPE_MAP covers all 17 backends. All tests pass including new coverage for auto-generation, type mapping, and rclone.conf output for new backends.</done>
</task>
</tasks>
<verification>
- `npx vitest run` — full suite green (no regressions)
- TypeScript compiles: `npx tsc --noEmit` — no errors
- All 17 backend keys present in BACKEND_REGISTRY
- BackendType derived (no explicit union)
- BACKEND_SCHEMAS auto-generated (no manual calls)
</verification>
<success_criteria>
- 17 backends in BACKEND_REGISTRY with category field
- BackendType = keyof typeof BACKEND_REGISTRY (derived, not explicit)
- BACKEND_SCHEMAS auto-generated from registry keys
- RCLONE_TYPE_MAP has all 17 entries with verified rclone type strings
- All existing + new tests pass
- TypeScript compiles without errors
</success_criteria>
<output>
After completion, create `.planning/phases/13-add-remaining-rclone-remotes/13-01-SUMMARY.md`
</output>
@@ -0,0 +1,141 @@
---
phase: 13-add-remaining-rclone-remotes
plan: 01
subsystem: api
tags: [rclone, typescript, zod, registry, backend-expansion]
requires:
- phase: 09-md3-components
provides: FieldDef interface and TextFieldMD3 rendering contract used for field helpText guidance
- phase: 06-rclone-generator
provides: buildRcloneConf, RCLONE_TYPE_MAP, BACKEND_SCHEMAS patterns used throughout
provides:
- "18-backend BACKEND_REGISTRY with category field as single source of truth"
- "BackendType derived from keyof typeof BACKEND_REGISTRY (no manual union)"
- "BackendCategory type with 3 valid values"
- "BACKEND_SCHEMAS auto-generated from registry keys via Object.fromEntries"
- "RCLONE_TYPE_MAP covering all 18 backends with verified rclone type strings"
affects:
- 13-02 (UI - BackendSelectionStep overhaul, categories, search)
- 13-03 (RemoteConfigStep - new backend config forms)
- any future phase adding rclone backends
tech-stack:
added: []
patterns:
- "BackendType derived from keyof typeof BACKEND_REGISTRY — no manual union maintenance"
- "BACKEND_SCHEMAS auto-generated via Object.fromEntries over registry keys — zero per-backend boilerplate"
- "BackendMeta interface types individual entries; registry uses as const for keyof narrowing"
- "RCLONE_TYPE_MAP exported for direct test coverage"
key-files:
created: []
modified:
- src/schemas/registry.ts
- src/schemas/registry.test.ts
- src/schemas/index.ts
- src/schemas/index.test.ts
- src/generators/rclone-conf.ts
- src/generators/rclone-conf.test.ts
- src/components/wizard/BackendSelectionStep.test.tsx
key-decisions:
- "BackendType is derived from keyof typeof BACKEND_REGISTRY — eliminates manual union maintenance at 18+ backends"
- "Registry uses as const (not a Record<BackendType,...> cast) — the cast was circular when BackendType derived from registry"
- "BACKEND_SCHEMAS auto-generated from Object.keys(BACKEND_REGISTRY) — no per-backend buildZodSchema call needed"
- "Google Drive supports both OAuth token and service account paths via two optional fields — GdriveAuthToggle deferred to 13-03"
- "Registry has 18 backends (not 17 as plan stated) — research doc listed http and seafile as separate items making count 18"
- "RCLONE_TYPE_MAP exported for exhaustiveness testing"
patterns-established:
- "Registry-driven type derivation: add backend once in BACKEND_REGISTRY, all downstream (BackendType, BACKEND_SCHEMAS) auto-update"
- "BackendCategory field in registry entry — single source of truth for UI grouping (no separate category map)"
requirements-completed: [REMOTE-01, REMOTE-02, REMOTE-06]
duration: 10min
completed: 2026-04-01
---
# Phase 13 Plan 01: Registry Refactor and 18-Backend Expansion Summary
**BACKEND_REGISTRY expanded from 7 to 18 backends with derived BackendType, BackendCategory, and auto-generated BACKEND_SCHEMAS — zero per-backend boilerplate for future additions**
## Performance
- **Duration:** ~10 min
- **Started:** 2026-04-01T16:13:00Z
- **Completed:** 2026-04-01T16:20:00Z
- **Tasks:** 2
- **Files modified:** 7
## Accomplishments
- Registry expanded with 11 new backends: azure-files, swift, gdrive, dropbox, box, pcloud, ftp, webdav, smb, http, seafile
- BackendType now derived from `keyof typeof BACKEND_REGISTRY` — adding a backend no longer requires touching a manual union type
- BACKEND_SCHEMAS auto-generated from registry keys — no per-backend `buildZodSchema()` call needed
- RCLONE_TYPE_MAP covers all 18 backends with verified rclone type strings
- Test count grew from 63 to 266 (all green, no regressions)
## Task Commits
Each task was committed atomically:
1. **Task 1: Refactor registry types and add all 10 new backend entries** - `b8c6493` (feat)
2. **Task 2: Auto-generate BACKEND_SCHEMAS, update RCLONE_TYPE_MAP, update tests** - `3010cdb` (feat)
## Files Created/Modified
- `src/schemas/registry.ts` - Added BackendCategory type, BackendMeta interface, category field on all entries, 11 new backends, BackendType derived from keyof
- `src/schemas/registry.test.ts` - Expanded from 24 to 48 tests; covers all 18 backends, category assertions, new backend field validation
- `src/schemas/index.ts` - Replaced explicit BACKEND_SCHEMAS object with Object.fromEntries auto-generation
- `src/schemas/index.test.ts` - Added 3 auto-generation tests + 4 new backend schema smoke tests (ftp, smb, seafile, dropbox)
- `src/generators/rclone-conf.ts` - RCLONE_TYPE_MAP expanded to 18 entries, exported for test access
- `src/generators/rclone-conf.test.ts` - Added RCLONE_TYPE_MAP exhaustiveness test + gdrive/ftp/smb conf output tests
- `src/components/wizard/BackendSelectionStep.test.tsx` - Fixed /next/i button selector ambiguity (Rule 1 bug fix)
## Decisions Made
- BackendType is derived from `keyof typeof BACKEND_REGISTRY` — eliminates manual union maintenance at 18+ backends
- The `as Record<BackendType,...>` cast at the bottom of the registry was circular when BackendType derived from registry keys — removed it; `as const` provides full inference
- Google Drive supports both OAuth token and service account auth paths via two optional fields (`token` and `service_account_credentials`) — GdriveAuthToggle component deferred to plan 13-03 (UI work)
- Registry ended up with 18 backends (plan said 17) — research doc listed `http` and `seafile` as separate entries making the correct count 18; test assertions updated to match reality
- RCLONE_TYPE_MAP exported for direct exhaustiveness testing
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] Fixed BackendSelectionStep test broken by WebDAV "NextCloud" description**
- **Found during:** Task 2 (full test suite run)
- **Issue:** Test used `getByRole('button', { name: /next/i })` — matched both the WebDAV card (description contains "NextCloud") and the actual Next submit button after new backends were added
- **Fix:** Changed to `getByRole('button', { name: 'Next' })` (exact match) to target only the submit button
- **Files modified:** `src/components/wizard/BackendSelectionStep.test.tsx`
- **Verification:** All 266 tests pass, full suite green
- **Committed in:** `3010cdb` (Task 2 commit)
---
**Total deviations:** 1 auto-fixed (Rule 1 - bug triggered by new data)
**Impact on plan:** Minor test fix required by our own data addition. No scope creep.
## Issues Encountered
- Registry backend count was 18 not 17 — research doc included both `http` and `seafile` as separate entries, the "17" in the plan was a counting error. Updated test assertions and SUMMARY to reflect the actual correct count.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- Registry foundation complete — all 18 backends have entries, types are derived, schemas auto-generate
- Plan 13-02 can now proceed with BackendSelectionStep UI overhaul (category headings, search filter, backend cards for all 18)
- Plan 13-03 can proceed with RemoteConfigStep new backend config forms
- BackendType is now available as a union of 18 values wherever `import type { BackendType } from '../schemas/registry'` is used
---
*Phase: 13-add-remaining-rclone-remotes*
*Completed: 2026-04-01*
@@ -0,0 +1,193 @@
---
phase: 13-add-remaining-rclone-remotes
plan: 02
type: execute
wave: 2
depends_on: [13-01]
files_modified:
- src/components/wizard/OAuthInstructions.tsx
- src/components/wizard/OAuthInstructions.test.tsx
- src/components/wizard/GdriveAuthToggle.tsx
- src/components/icons/BackendIcons.tsx
autonomous: true
requirements: [REMOTE-04]
must_haves:
truths:
- "OAuthInstructions renders collapsed by default and expands on click"
- "OAuthInstructions shows backend-specific rclone authorize command"
- "GdriveAuthToggle switches between OAuth Token and Service Account tabs"
- "Backend icons are inline SVGs with no external dependency"
artifacts:
- path: "src/components/wizard/OAuthInstructions.tsx"
provides: "Collapsible OAuth step-by-step guide"
exports: ["OAuthInstructions"]
- path: "src/components/wizard/GdriveAuthToggle.tsx"
provides: "Google Drive dual-auth toggle (OAuth vs Service Account)"
exports: ["GdriveAuthToggle"]
- path: "src/components/icons/BackendIcons.tsx"
provides: "Inline SVG icon map keyed by BackendType"
exports: ["BACKEND_ICONS"]
- path: "src/components/wizard/OAuthInstructions.test.tsx"
provides: "Tests for collapsed/expanded behavior"
key_links:
- from: "src/components/wizard/GdriveAuthToggle.tsx"
to: "src/components/wizard/OAuthInstructions.tsx"
via: "import and render"
pattern: "OAuthInstructions"
- from: "src/components/icons/BackendIcons.tsx"
to: "src/schemas/registry.ts"
via: "BackendType key mapping"
pattern: "Record.*BackendType"
---
<objective>
Build the three new components needed by Phase 13: OAuthInstructions (collapsible guide for OAuth token-paste backends), GdriveAuthToggle (dual-auth toggle for Google Drive), and BackendIcons (inline SVG icon map).
Purpose: These components are consumed by plans 03 (BackendSelectionStep) and 04 (RemoteConfigStep). Building them in isolation ensures clean interfaces and testability.
Output: Three new component files with tests for OAuthInstructions.
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/13-add-remaining-rclone-remotes/13-CONTEXT.md
@.planning/phases/13-add-remaining-rclone-remotes/13-RESEARCH.md
@.planning/phases/13-add-remaining-rclone-remotes/13-01-SUMMARY.md
<interfaces>
<!-- Existing patterns to follow -->
From src/components/wizard/AzureAuthToggle.tsx (AuthToggle pattern):
```typescript
interface AzureAuthToggleProps {
register: UseFormRegister<any>;
errors: {
key?: FieldError;
sas_url?: FieldError;
};
}
// Uses useState for tab, segmented control buttons, CSS hidden/block for inactive/active
```
From src/components/ui/PasswordField.tsx (used for token fields):
```typescript
// PasswordField wraps TextFieldMD3 with show/hide toggle
// Used for all password/token inputs
```
From src/components/ui/BackendCard.tsx (icon will be added here):
```typescript
interface BackendCardProps {
name: string;
description: string;
selected?: boolean;
onClick: () => void;
}
```
MD3 styling tokens used throughout:
- bg-surface, bg-surface-container, bg-primary
- text-on-surface, text-on-surface-variant, text-on-primary
- border-outline, rounded-xl, shadow
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: OAuthInstructions component with tests</name>
<files>src/components/wizard/OAuthInstructions.tsx, src/components/wizard/OAuthInstructions.test.tsx</files>
<behavior>
- Test 1: Renders collapsed by default (instructions not visible)
- Test 2: Expands when user clicks the toggle button, showing numbered steps
- Test 3: Shows the backend-specific `authorizeCommand` in the expanded content
- Test 4: Shows backend name in the toggle button text
</behavior>
<action>
Create OAuthInstructions component with these props:
```typescript
interface OAuthInstructionsProps {
backendName: string; // e.g. "Google Drive"
authorizeCommand: string; // e.g. 'rclone authorize "drive"'
steps?: string[]; // optional custom steps (defaults to generic OAuth flow)
}
```
Implementation:
- Use `useState(false)` for expanded state
- Render a button/disclosure that reads "How to get your OAuth token" (or similar with backendName)
- When expanded, show numbered steps: (1) Install rclone on a machine with a browser, (2) Run the authorize command (show in a code block), (3) Authenticate in the browser, (4) Copy the JSON token from terminal, (5) Paste it in the field above
- Use `steps` prop to override default steps if provided
- Style with MD3 tokens: bg-surface-container, text-on-surface, border-outline, rounded-xl
- Use an expand/collapse chevron icon (inline SVG, simple caret)
- Collapse transition is optional (CSS-only if added)
Write tests first (RED), then implement (GREEN).
</action>
<verify>
<automated>npx vitest run src/components/wizard/OAuthInstructions.test.tsx --reporter=verbose</automated>
</verify>
<done>OAuthInstructions renders collapsed by default, expands on click, shows backend-specific command and steps. All 4 tests pass.</done>
</task>
<task type="auto">
<name>Task 2: GdriveAuthToggle and BackendIcons components</name>
<files>src/components/wizard/GdriveAuthToggle.tsx, src/components/icons/BackendIcons.tsx</files>
<action>
1. Create GdriveAuthToggle following the exact same pattern as AzureAuthToggle:
```typescript
interface GdriveAuthToggleProps {
register: UseFormRegister<any>;
errors: {
token?: FieldError;
service_account_credentials?: FieldError;
};
}
```
- Two tabs: "OAuth Token" and "Service Account"
- Tab 1 (OAuth Token): OAuthInstructions component (backendName="Google Drive", authorizeCommand='rclone authorize "drive"') + PasswordField for token
- Tab 2 (Service Account): PasswordField for service_account_credentials with helpText about JSON key file
- Both fields always registered (CSS hidden/block), same as Azure/SFTP pattern
- Use same segmented control styling as AzureAuthToggle
2. Create BackendIcons.tsx with inline SVG icons for all 17 backends:
- Export `BACKEND_ICONS: Partial<Record<BackendType, React.FC<IconProps>>>` where `IconProps = { className?: string }`
- Each icon is a simple monochrome SVG using `fill="currentColor"` for MD3 dark mode compatibility
- Icons should be recognizable at 24x24px size (viewBox="0 0 24 24")
- Use simplified/stylized versions of brand icons (cloud shape for generic cloud storage, drive icon for Google Drive, folder for file-based, server for protocol-based, etc.)
- For backends where a distinctive icon is hard to create (swift, seafile), use a generic category icon (cloud, server, globe)
- The icon map is Partial — missing entries just mean no icon rendered on the card
Create the icons directory: src/components/icons/
</action>
<verify>
<automated>npx tsc --noEmit 2>&1 | tail -20 && npx vitest run src/components/wizard/ --reporter=verbose 2>&1 | tail -30</automated>
</verify>
<done>GdriveAuthToggle renders two-tab auth toggle with OAuthInstructions in OAuth tab. BackendIcons exports BACKEND_ICONS map with inline SVG icons for backends. TypeScript compiles, existing tests pass.</done>
</task>
</tasks>
<verification>
- `npx vitest run` — full suite green
- `npx tsc --noEmit` — no type errors
- OAuthInstructions test file passes all 4 behavior tests
- GdriveAuthToggle follows AzureAuthToggle pattern exactly
</verification>
<success_criteria>
- OAuthInstructions component: collapsed by default, expands on click, shows authorize command
- GdriveAuthToggle: two-tab toggle (OAuth Token + Service Account), both fields always registered
- BackendIcons: inline SVG map keyed by BackendType, monochrome, no external dependency
- All tests pass
</success_criteria>
<output>
After completion, create `.planning/phases/13-add-remaining-rclone-remotes/13-02-SUMMARY.md`
</output>
@@ -0,0 +1,107 @@
---
phase: 13-add-remaining-rclone-remotes
plan: "02"
subsystem: ui
tags: [react, tailwind, svg, rclone, oauth, wizard, components]
# Dependency graph
requires:
- phase: 13-add-remaining-rclone-remotes
provides: "BackendType derived from BACKEND_REGISTRY (18 backends), registry as const pattern"
provides:
- "OAuthInstructions: collapsible step-by-step OAuth token guide (TDD, 4 tests)"
- "GdriveAuthToggle: two-tab OAuth/Service Account toggle for Google Drive"
- "BACKEND_ICONS: inline SVG icon map for all 18 BackendType entries"
affects: [13-03-BackendSelectionStep, 13-04-RemoteConfigStep]
# Tech tracking
tech-stack:
added: []
patterns:
- "TDD for UI components: test → implement → verify cycle"
- "Collapsible disclosure via useState(false) + conditional render"
- "Both form fields always registered, visibility toggled via CSS hidden/block"
- "Inline SVG icons with fill=currentColor for dark-mode compatibility"
- "Partial<Record<BackendType, FC<IconProps>>> for optional icon map"
key-files:
created:
- src/components/wizard/OAuthInstructions.tsx
- src/components/wizard/OAuthInstructions.test.tsx
- src/components/wizard/GdriveAuthToggle.tsx
- src/components/icons/BackendIcons.tsx
modified: []
key-decisions:
- "OAuthInstructions renders authorizeCommand in a <code> block (detected via step text prefix), not inline text — ensures test getByText exact-match works"
- "BACKEND_ICONS is Partial<Record<BackendType,...>> — missing entries mean no icon rendered on the card; allows incremental icon addition"
- "GdriveAuthToggle defaults to oauth tab (not service-account) — OAuth is the more common flow for personal use"
- "BackendIcons uses text SVG elements for letter-based icons (S3, B2, G, p) — simpler than path-based glyphs for recognizable short labels at 24px"
patterns-established:
- "AuthToggle pattern: segmented control + CSS hidden/block for always-registered dual fields"
- "OAuthInstructions: generic collapsible guide reusable for any OAuth backend (backendName + authorizeCommand props)"
- "IconProps = { className?: string } — minimal prop surface for SVG components"
requirements-completed: [REMOTE-04]
# Metrics
duration: 4min
completed: 2026-04-01
---
# Phase 13 Plan 02: Component Library for OAuth and Icons Summary
**Collapsible OAuthInstructions, GdriveAuthToggle dual-auth toggle, and 18-backend inline SVG icon map — all with zero external dependencies**
## Performance
- **Duration:** 4 min
- **Started:** 2026-04-01T14:24:33Z
- **Completed:** 2026-04-01T14:28:00Z
- **Tasks:** 2 (Task 1 TDD, Task 2 standard)
- **Files modified:** 4 created
## Accomplishments
- OAuthInstructions: collapsible disclosure (collapsed by default), shows backend-specific rclone authorize command in code block, 5-step generic OAuth flow, MD3 styled — TDD with 4 passing tests
- GdriveAuthToggle: two-tab segmented control (OAuth Token / Service Account) following AzureAuthToggle pattern exactly; OAuth tab embeds OAuthInstructions; both fields always registered via CSS hidden/block
- BackendIcons: inline SVG icon map covering all 18 BackendType keys, fill="currentColor" throughout, viewBox 0 0 24 24, no external icon library
## Task Commits
Each task was committed atomically:
1. **Task 1: OAuthInstructions component with tests** - `e00fc2b` (feat + test TDD)
2. **Task 2: GdriveAuthToggle and BackendIcons components** - `683faf6` (feat)
## Files Created/Modified
- `src/components/wizard/OAuthInstructions.tsx` - Collapsible OAuth step-by-step guide component
- `src/components/wizard/OAuthInstructions.test.tsx` - 4 behavior tests (collapsed default, expand on click, authorize command display, backend name in button)
- `src/components/wizard/GdriveAuthToggle.tsx` - Google Drive dual-auth toggle (OAuth Token + Service Account tabs)
- `src/components/icons/BackendIcons.tsx` - Inline SVG icon map for all 18 backends
## Decisions Made
- OAuthInstructions uses step-prefix detection to render the authorize command in a `<code>` element — the step text "Run the authorize command: ..." is matched by prefix so the command value is extracted cleanly for the code block while keeping getByText test assertions working correctly.
- BACKEND_ICONS is `Partial<Record<BackendType,...>>` so missing icons simply render nothing — plans 03/04 handle the absent-icon case in BackendCard.
- GdriveAuthToggle defaults to oauth tab since OAuth is the typical personal-use flow; service account is the secondary option.
- Letter-based SVG icons (S3, B2, G, p) use `<text>` elements for simplicity at 24px — path-based glyph construction for single characters is over-engineered.
## Deviations from Plan
None - plan executed exactly as written.
## Issues Encountered
None.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- OAuthInstructions ready for embedding in any OAuth backend config step
- GdriveAuthToggle ready for use in RemoteConfigStep (plan 13-04) as gdrive's auth renderer
- BACKEND_ICONS ready for BackendCard integration in BackendSelectionStep (plan 13-03)
- Full test suite (270 tests, 17 files) remains green
---
*Phase: 13-add-remaining-rclone-remotes*
*Completed: 2026-04-01*
@@ -0,0 +1,243 @@
---
phase: 13-add-remaining-rclone-remotes
plan: 03
type: execute
wave: 3
depends_on: [13-01, 13-02]
files_modified:
- src/components/wizard/BackendSelectionStep.tsx
- src/components/ui/BackendCard.tsx
- src/components/wizard/BackendSelectionStep.test.tsx
autonomous: true
requirements: [REMOTE-03]
must_haves:
truths:
- "Backends are grouped under category headings: Cloud Object Storage, Cloud Drives, Protocol-based"
- "Search bar filters backends instantly as user types"
- "Search matches displayName, description, category label, and field labels"
- "Categories with no matching backends are hidden (not rendered)"
- "Each backend card shows an inline SVG icon"
- "Existing remote name input and validation still work"
artifacts:
- path: "src/components/wizard/BackendSelectionStep.tsx"
provides: "Category-grouped, searchable backend selection with icons"
contains: "CATEGORY_ORDER"
- path: "src/components/ui/BackendCard.tsx"
provides: "Backend card with optional icon prop"
- path: "src/components/wizard/BackendSelectionStep.test.tsx"
provides: "Tests for search filter and category collapse"
key_links:
- from: "src/components/wizard/BackendSelectionStep.tsx"
to: "src/schemas/registry.ts"
via: "BACKEND_REGISTRY iteration with category grouping"
pattern: "BACKEND_REGISTRY.*category"
- from: "src/components/wizard/BackendSelectionStep.tsx"
to: "src/components/icons/BackendIcons.tsx"
via: "BACKEND_ICONS import for card rendering"
pattern: "BACKEND_ICONS"
---
<objective>
Overhaul BackendSelectionStep from a flat card grid to a categorized, searchable layout with icons. The step must handle 17 backends without overwhelming the user.
Purpose: With 17 backends (up from 7), a flat grid is unusable. Categories group by IT-pro mental model, search enables quick access, icons aid visual scanning.
Output: Refactored BackendSelectionStep.tsx, updated BackendCard.tsx, new test cases.
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/13-add-remaining-rclone-remotes/13-CONTEXT.md
@.planning/phases/13-add-remaining-rclone-remotes/13-RESEARCH.md
@.planning/phases/13-add-remaining-rclone-remotes/13-01-SUMMARY.md
@.planning/phases/13-add-remaining-rclone-remotes/13-02-SUMMARY.md
<interfaces>
<!-- From Plan 01 output (registry with categories) -->
From src/schemas/registry.ts (after Plan 01):
```typescript
export type BackendCategory = 'cloud-object-storage' | 'cloud-drives' | 'protocol-based';
export type BackendType = keyof typeof BACKEND_REGISTRY;
// Each entry has: displayName, description, category, fields
```
From src/components/icons/BackendIcons.tsx (from Plan 02):
```typescript
interface IconProps { className?: string; }
export const BACKEND_ICONS: Partial<Record<BackendType, React.FC<IconProps>>>;
```
From src/components/ui/TextFieldMD3.tsx:
```typescript
interface TextFieldMD3Props {
id: string;
label: string;
registration: UseFormRegisterReturn;
error?: FieldError;
required?: boolean;
helpText?: string;
// ... more props
}
```
From src/components/ui/BackendCard.tsx (current):
```typescript
interface BackendCardProps {
name: string;
description: string;
selected?: boolean;
onClick: () => void;
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add icon prop to BackendCard and build category/search into BackendSelectionStep</name>
<files>src/components/ui/BackendCard.tsx, src/components/wizard/BackendSelectionStep.tsx</files>
<action>
1. Update BackendCard to accept an optional `icon` prop:
```typescript
interface BackendCardProps {
name: string;
description: string;
selected?: boolean;
onClick: () => void;
icon?: React.ReactNode; // NEW — rendered at top-left of card
}
```
- Render icon before the name span, in a flex row: `<div className="flex items-center gap-2"><span className="w-6 h-6 shrink-0">{icon}</span><span>{name}</span></div>`
- If icon is undefined/null, render name without the icon wrapper (no empty space)
2. Refactor BackendSelectionStep to add category grouping and search:
a. Add search state: `const [searchQuery, setSearchQuery] = useState('')`
b. Add search bar ABOVE the backend cards (below the remote name input). Use a plain `<input>` styled with MD3 tokens (NOT TextFieldMD3 — it uses placeholder=" " which conflicts with a search placeholder). Style it as:
```
className="w-full px-4 py-2.5 rounded-xl border border-outline bg-surface-container text-on-surface placeholder:text-on-surface-variant/50 focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="Search backends..."
```
Use `onChange={(e) => setSearchQuery(e.target.value)}` — no debounce needed for 17 items.
c. Add category constants:
```typescript
const CATEGORY_ORDER: BackendCategory[] = ['cloud-object-storage', 'cloud-drives', 'protocol-based'];
const CATEGORY_LABELS: Record<BackendCategory, string> = {
'cloud-object-storage': 'Cloud Object Storage',
'cloud-drives': 'Cloud Drives',
'protocol-based': 'Protocol-based',
};
```
d. Add matchesSearch function that checks displayName, description, category label, and field labels:
```typescript
function matchesSearch(entry: { displayName: string; description: string; category: BackendCategory; fields: { label: string }[] }, query: string): boolean {
const q = query.toLowerCase();
return entry.displayName.toLowerCase().includes(q)
|| entry.description.toLowerCase().includes(q)
|| CATEGORY_LABELS[entry.category].toLowerCase().includes(q)
|| entry.fields.some(f => f.label.toLowerCase().includes(q));
}
```
e. Replace the flat `data-testid="backend-cards"` grid with category sections:
```tsx
{CATEGORY_ORDER.map(cat => {
const backends = Object.entries(BACKEND_REGISTRY)
.filter(([, e]) => e.category === cat)
.filter(([, e]) => !searchQuery || matchesSearch(e, searchQuery));
if (backends.length === 0) return null;
return (
<section key={cat} className="mb-6">
<h3 className="text-lg font-semibold text-on-surface mb-3">{CATEGORY_LABELS[cat]}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{backends.map(([type, entry]) => (
<BackendCard
key={type}
name={entry.displayName}
description={entry.description}
selected={state.remote.backendType === type}
onClick={() => handleCardClick(type as BackendType)}
icon={BACKEND_ICONS[type as BackendType] ? React.createElement(BACKEND_ICONS[type as BackendType]!, { className: 'w-6 h-6' }) : undefined}
/>
))}
</div>
</section>
);
})}
```
f. Keep the existing remote name TextFieldMD3 and RemoteNamePreview at the top. Keep the Next button at the bottom. Keep form validation logic unchanged.
g. Keep `data-testid="backend-cards"` on a wrapping div around all category sections so existing tests that query within it still work.
3. Import BACKEND_ICONS from '../icons/BackendIcons' and BackendCategory from '../../schemas/registry'.
</action>
<verify>
<automated>npx tsc --noEmit 2>&1 | tail -10 && npx vitest run src/components/wizard/BackendSelectionStep.test.tsx --reporter=verbose 2>&1 | tail -30</automated>
</verify>
<done>BackendSelectionStep renders 17 backends grouped by category with search bar. BackendCard shows icon when provided. Existing tests pass.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Add search filter and category collapse tests</name>
<files>src/components/wizard/BackendSelectionStep.test.tsx</files>
<behavior>
- Test 1: All three category headings render when no search query
- Test 2: Typing "ftp" in search shows FTP and SFTP, hides unrelated backends
- Test 3: Typing a query that matches no backends shows no category headings
- Test 4: Searching "Cloudflare" finds S3-Compatible (via description match)
- Test 5: All 17 backend cards render when no search active
- Test 6: Category heading is not rendered when all its backends are filtered out
</behavior>
<action>
Add new test cases to the existing BackendSelectionStep.test.tsx file. Use the existing test setup pattern (WizardProvider wrapper, render helper).
For search tests:
- Find the search input by placeholder "Search backends..."
- Use `fireEvent.change()` or `userEvent.type()` to enter search text
- Assert on presence/absence of backend card text and category headings
For category tests:
- Assert "Cloud Object Storage", "Cloud Drives", "Protocol-based" headings are present
- After filtering, assert missing headings are NOT in the document
Write tests first (RED), verify they fail, then ensure Task 1 implementation makes them pass (GREEN).
</action>
<verify>
<automated>npx vitest run src/components/wizard/BackendSelectionStep.test.tsx --reporter=verbose</automated>
</verify>
<done>6 new test cases pass covering search filtering, category heading visibility, cross-field search matching, and full backend card rendering.</done>
</task>
</tasks>
<verification>
- `npx vitest run` — full suite green
- `npx tsc --noEmit` — no type errors
- BackendSelectionStep renders categories, search works, icons display
- No empty category headings when search filters out all backends in a category
</verification>
<success_criteria>
- 17 backends rendered in 3 category sections with headings
- Search bar filters instantly across displayName, description, category, field labels
- Empty categories hidden (conditional rendering, not CSS)
- Backend cards show inline SVG icons
- Existing remote name validation unchanged
- All tests pass (existing + 6 new)
</success_criteria>
<output>
After completion, create `.planning/phases/13-add-remaining-rclone-remotes/13-03-SUMMARY.md`
</output>
@@ -0,0 +1,119 @@
---
phase: 13-add-remaining-rclone-remotes
plan: "03"
subsystem: ui
tags: [react, tailwind, search, categories, icons, wizard]
# Dependency graph
requires:
- phase: 13-add-remaining-rclone-remotes/13-01
provides: BACKEND_REGISTRY with category field on all 18 backends, BackendCategory type
- phase: 13-add-remaining-rclone-remotes/13-02
provides: BACKEND_ICONS map keyed by BackendType with inline SVG components
provides:
- Category-grouped BackendSelectionStep with Cloud Object Storage / Cloud Drives / Protocol-based sections
- Instant search bar filtering across displayName, description, category label, and field labels
- BackendCard with optional icon prop (flex row with 24x24 icon + name)
- 6 new test cases for search filtering and category collapse behavior
affects:
- Any future plans modifying BackendSelectionStep or BackendCard
- Phase 14+ if additional backends are added to the registry
# Tech tracking
tech-stack:
added: []
patterns:
- "type=search input to avoid role=textbox conflict with remote name TextFieldMD3"
- "CATEGORY_ORDER constant drives render order; empty categories return null (conditional, not CSS)"
- "matchesSearch checks 4 fields: displayName, description, category label, field labels"
key-files:
created: []
modified:
- src/components/ui/BackendCard.tsx
- src/components/wizard/BackendSelectionStep.tsx
- src/components/wizard/BackendSelectionStep.test.tsx
key-decisions:
- "Search input uses type=search (role=searchbox) not type=text — prevents getByRole('textbox') ambiguity with remote name field in existing tests"
- "matchesSearch checks 4 fields for cross-field discovery (e.g. Cloudflare finds S3-Compatible via description)"
- "Empty categories hidden via conditional null return, not CSS display:none — test-assertable absence"
patterns-established:
- "Pattern: Use type=search for search inputs in forms that also have text inputs to avoid ARIA role collision"
requirements-completed: [REMOTE-03]
# Metrics
duration: 15min
completed: 2026-04-01
---
# Phase 13 Plan 03: BackendSelectionStep Category Grouping and Search Summary
**BackendSelectionStep refactored to show 18 backends in 3 category sections with instant search and inline SVG icons**
## Performance
- **Duration:** ~15 min
- **Started:** 2026-04-01T16:48:00Z
- **Completed:** 2026-04-01T16:53:00Z
- **Tasks:** 2
- **Files modified:** 3
## Accomplishments
- BackendCard updated to accept optional `icon` prop rendered in a flex row with the backend name
- BackendSelectionStep refactored from a flat grid to three category sections: Cloud Object Storage, Cloud Drives, Protocol-based
- Search bar added filtering across displayName, description, category label, and field labels with no debounce needed for 18 items
- Empty categories conditionally removed from DOM (not hidden with CSS)
- BACKEND_ICONS connected to BackendCard icon prop via React.createElement
- 6 new tests covering all REMOTE-03 acceptance criteria; all 276 project tests pass
## Task Commits
1. **Task 1: Add icon prop to BackendCard and build category/search into BackendSelectionStep** - `0a70620` (feat)
2. **Task 2: Add search filter and category collapse tests** - `aeb1528` (test)
## Files Created/Modified
- `src/components/ui/BackendCard.tsx` - Added optional `icon?: ReactNode` prop, renders flex row with icon+name when icon provided
- `src/components/wizard/BackendSelectionStep.tsx` - Added CATEGORY_ORDER, CATEGORY_LABELS, matchesSearch, searchQuery state, category sections, search input
- `src/components/wizard/BackendSelectionStep.test.tsx` - Added 6 new test cases under REMOTE-03 describe block
## Decisions Made
- **type=search for search input:** The remote name field has role=textbox. Adding a second type=text input would cause `getByRole('textbox')` to throw "multiple elements found" in all existing tests. Using `type=search` gives the input role=searchbox, avoiding the collision. This matches the semantic meaning (it is a search input) and required no test changes.
- **matchesSearch 4-field check:** Checking description enables cross-field discovery (e.g. typing "Cloudflare" finds S3-Compatible whose description mentions Cloudflare R2). Field labels allow users to search by credential type (e.g. "SAS URL" finds Azure Blob Storage).
- **Conditional null return for empty categories:** Categories with no matching backends return null from the map, removing them from the DOM entirely. This makes test assertions (`queryByText(...) toBeNull()`) reliable without needing CSS inspection.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] Changed search input from type=text to type=search**
- **Found during:** Task 1 (BackendSelectionStep implementation)
- **Issue:** type=text gives role=textbox; existing tests use `getByRole('textbox')` to find the remote name input — adding a second textbox caused 6 tests to fail with "multiple elements found"
- **Fix:** Changed `type="text"` to `type="search"` on the search input (role becomes searchbox, not textbox)
- **Files modified:** src/components/wizard/BackendSelectionStep.tsx
- **Verification:** All 13 existing tests pass after change; 6 new tests also pass
- **Committed in:** 0a70620 (Task 1 commit)
---
**Total deviations:** 1 auto-fixed (Rule 1 - bug causing test failures)
**Impact on plan:** Minimal — single attribute change, semantically correct. No scope creep.
## Issues Encountered
None beyond the type=text conflict described above.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- BackendSelectionStep handles 18 backends with categories and search — ready for any v1.3 release
- Phase 13 all 3 plans complete: registry refactor (13-01), icons+OAuth UI (13-02), categorized selection (13-03)
- No blockers for phase completion
---
*Phase: 13-add-remaining-rclone-remotes*
*Completed: 2026-04-01*
@@ -0,0 +1,278 @@
---
phase: 13-add-remaining-rclone-remotes
plan: 04
type: execute
wave: 4
depends_on: [13-01, 13-02, 13-03]
files_modified:
- src/components/wizard/RemoteConfigStep.tsx
- src/components/wizard/RemoteConfigStep.test.tsx
autonomous: false
requirements: [REMOTE-05]
must_haves:
truths:
- "RemoteConfigStep renders correct form fields for every new backend"
- "Google Drive shows GdriveAuthToggle with OAuth Token and Service Account tabs"
- "Dropbox, Box, pCloud show OAuthInstructions + token field"
- "FTP, WebDAV, SMB, HTTP, Seafile, Azure Files, Swift render via generic FieldRenderer loop"
- "backendLabel is derived from registry displayName (not duplicated)"
- "All form submissions produce correct remote params"
artifacts:
- path: "src/components/wizard/RemoteConfigStep.tsx"
provides: "Form rendering for all 17 backends"
- path: "src/components/wizard/RemoteConfigStep.test.tsx"
provides: "Tests for new backend form rendering"
key_links:
- from: "src/components/wizard/RemoteConfigStep.tsx"
to: "src/components/wizard/GdriveAuthToggle.tsx"
via: "import and render for gdrive backend"
pattern: "GdriveAuthToggle"
- from: "src/components/wizard/RemoteConfigStep.tsx"
to: "src/components/wizard/OAuthInstructions.tsx"
via: "import and render for OAuth backends"
pattern: "OAuthInstructions"
- from: "src/components/wizard/RemoteConfigStep.tsx"
to: "src/schemas/registry.ts"
via: "BACKEND_REGISTRY field lookup"
pattern: "BACKEND_REGISTRY\\["
---
<objective>
Wire all 10 new backends into RemoteConfigStep so every backend type renders the correct configuration form with appropriate auth handling (AuthToggle, OAuthInstructions, or generic FieldRenderer loop).
Purpose: This is the final integration plan — connecting registry data to the form UI. After this, every backend is fully functional end-to-end.
Output: Updated RemoteConfigStep with all backend branches, updated tests, visual verification.
</objective>
<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>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/13-add-remaining-rclone-remotes/13-CONTEXT.md
@.planning/phases/13-add-remaining-rclone-remotes/13-RESEARCH.md
@.planning/phases/13-add-remaining-rclone-remotes/13-01-SUMMARY.md
@.planning/phases/13-add-remaining-rclone-remotes/13-02-SUMMARY.md
@.planning/phases/13-add-remaining-rclone-remotes/13-03-SUMMARY.md
<interfaces>
<!-- From Plan 01: Registry with 17 backends -->
From src/schemas/registry.ts:
```typescript
export type BackendType = keyof typeof BACKEND_REGISTRY;
// Includes: azureblob, s3, s3-compatible, onedrive, sftp, gcs, b2,
// azure-files, swift, gdrive, dropbox, box, pcloud,
// ftp, webdav, smb, http, seafile
```
<!-- From Plan 02: New components -->
From src/components/wizard/OAuthInstructions.tsx:
```typescript
interface OAuthInstructionsProps {
backendName: string;
authorizeCommand: string;
steps?: string[];
}
```
From src/components/wizard/GdriveAuthToggle.tsx:
```typescript
interface GdriveAuthToggleProps {
register: UseFormRegister<any>;
errors: {
token?: FieldError;
service_account_credentials?: FieldError;
};
}
```
<!-- Current RemoteConfigStep pattern -->
From src/components/wizard/RemoteConfigStep.tsx:
```typescript
// Three-branch ternary: azureblob -> AzureAuthToggle, sftp -> SftpAuthToggle, else -> generic loop
// backendLabel: Record<NonNullable<typeof backendType>, string> — manual duplication
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Wire all new backends into RemoteConfigStep</name>
<files>src/components/wizard/RemoteConfigStep.tsx</files>
<action>
1. Replace the manual `backendLabel` record with auto-derived version:
```typescript
const backendLabel = Object.fromEntries(
Object.entries(BACKEND_REGISTRY).map(([k, v]) => [k, v.displayName])
) as Record<BackendType, string>;
```
This eliminates the duplicate displayName maintenance per Pitfall 2 in the research.
2. Import new components:
```typescript
import { OAuthInstructions } from './OAuthInstructions';
import { GdriveAuthToggle } from './GdriveAuthToggle';
```
3. Expand the rendering branch logic. The current three-branch ternary (azureblob/sftp/else) needs new branches for backends with special auth handling:
a. `gdrive` — GdriveAuthToggle (handles token + service_account_credentials) + FieldRenderer for root_folder_id:
```tsx
) : backendType === 'gdrive' ? (
<>
<GdriveAuthToggle
register={register}
errors={{
token: errors.token as FieldError | undefined,
service_account_credentials: errors.service_account_credentials as FieldError | undefined,
}}
/>
{BACKEND_REGISTRY.gdrive.fields
.filter(f => f.key !== 'token' && f.key !== 'service_account_credentials')
.map(field => (
<FieldRenderer key={field.key} field={field} register={register} error={errors[field.key] as FieldError | undefined} />
))}
</>
```
b. `dropbox` — OAuthInstructions + generic FieldRenderer loop:
```tsx
) : backendType === 'dropbox' ? (
<>
<OAuthInstructions backendName="Dropbox" authorizeCommand='rclone authorize "dropbox"' />
{BACKEND_REGISTRY.dropbox.fields.map(field => (
<FieldRenderer key={field.key} field={field} register={register} error={errors[field.key] as FieldError | undefined} />
))}
</>
```
c. `box` — OAuthInstructions + generic FieldRenderer loop:
```tsx
) : backendType === 'box' ? (
<>
<OAuthInstructions backendName="Box" authorizeCommand='rclone authorize "box"' />
{BACKEND_REGISTRY.box.fields.map(field => (
<FieldRenderer key={field.key} field={field} register={register} error={errors[field.key] as FieldError | undefined} />
))}
</>
```
d. `pcloud` — OAuthInstructions + generic FieldRenderer loop:
```tsx
) : backendType === 'pcloud' ? (
<>
<OAuthInstructions backendName="pCloud" authorizeCommand='rclone authorize "pcloud"' />
{BACKEND_REGISTRY.pcloud.fields.map(field => (
<FieldRenderer key={field.key} field={field} register={register} error={errors[field.key] as FieldError | undefined} />
))}
</>
```
e. All other new backends (azure-files, swift, ftp, webdav, smb, http, seafile) fall through to the existing generic `else` branch which iterates `BACKEND_REGISTRY[backendType].fields` — NO new branches needed for these since they have no auth toggles.
f. `onedrive` already has OAuthInstructions-worthy fields but currently works via the generic loop. Add OAuthInstructions to onedrive too for consistency:
```tsx
) : backendType === 'onedrive' ? (
<>
<OAuthInstructions backendName="OneDrive" authorizeCommand='rclone authorize "onedrive"' />
{BACKEND_REGISTRY.onedrive.fields.map(field => (
<FieldRenderer key={field.key} field={field} register={register} error={errors[field.key] as FieldError | undefined} />
))}
</>
```
4. The branch order should be: azureblob -> sftp -> gdrive -> onedrive -> dropbox -> box -> pcloud -> else (generic loop). Consider refactoring the deep ternary into a helper function or switch-like pattern for readability with 7 branches. A `renderBackendFields()` function with a switch statement is cleaner than nested ternaries.
5. Verify the form still submits correctly — the onNext handler and validation logic should remain unchanged since BACKEND_SCHEMAS auto-generates for all types.
</action>
<verify>
<automated>npx tsc --noEmit 2>&1 | tail -10 && npx vitest run src/components/wizard/RemoteConfigStep.test.tsx --reporter=verbose 2>&1 | tail -30</automated>
</verify>
<done>RemoteConfigStep renders correct form for all 17 backends. GdriveAuthToggle shown for gdrive. OAuthInstructions shown for onedrive, dropbox, box, pcloud. Generic loop for all others. backendLabel derived from registry. TypeScript compiles, existing tests pass.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Add tests for new backend form rendering</name>
<files>src/components/wizard/RemoteConfigStep.test.tsx</files>
<behavior>
- Test 1: gdrive renders GdriveAuthToggle with "OAuth Token" and "Service Account" tabs
- Test 2: dropbox renders OAuthInstructions with 'rclone authorize "dropbox"' command
- Test 3: ftp renders host, username, password, port, TLS mode fields
- Test 4: webdav renders URL, username, password, vendor select fields
- Test 5: smb renders host, username fields
- Test 6: http renders only URL field
</behavior>
<action>
Add test cases to RemoteConfigStep.test.tsx following the existing test pattern:
- Each test sets up WizardState with the target backendType
- Renders RemoteConfigStep within WizardProvider
- Asserts form fields are present by label text or role
- For gdrive: assert the segmented control buttons exist (OAuth Token, Service Account)
- For dropbox: assert OAuthInstructions toggle button is present
- For protocol backends: assert expected fields render
Use the existing test setup pattern from the file. Each new backend test should be a focused assertion on field presence — not full form submission (submission is already tested for existing backends and the mechanism is generic).
Write tests first, then verify they pass against the implementation from Task 1.
</action>
<verify>
<automated>npx vitest run src/components/wizard/RemoteConfigStep.test.tsx --reporter=verbose</automated>
</verify>
<done>6 new test cases pass verifying form rendering for gdrive, dropbox, ftp, webdav, smb, and http backends.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Visual verification of complete backend expansion</name>
<files>n/a</files>
<action>
Human verifies the complete backend expansion visually. What was built:
Complete backend expansion — 17 backends with categories, search, icons, OAuth instructions, and correct form rendering for each backend type.
</action>
<verify>
1. Run `npm run dev` and open http://localhost:5173
2. Step 1 (Backend Selection):
- Verify 3 category headings appear: "Cloud Object Storage", "Cloud Drives", "Protocol-based"
- Verify all 17 backend cards are visible with icons
- Type "ftp" in search — only FTP and SFTP should be visible
- Type "Cloudflare" in search — S3-Compatible should appear
- Clear search — all backends visible again
3. Select "Google Drive" — verify Step 2 shows OAuth Token / Service Account toggle
- OAuth Token tab: collapsible instructions + token field
- Service Account tab: service account credentials field
4. Go back, select "Dropbox" — verify Step 2 shows collapsible OAuth instructions + token field
5. Go back, select "FTP" — verify Step 2 shows host, username, password, port, TLS mode fields
6. Go back, select "WebDAV" — verify Step 2 shows URL, username, password, vendor select
7. Go back, select "SMB / Windows Share" — verify host, user, password, domain, port fields
8. Go back, select "HTTP (read-only)" — verify only URL field
9. Toggle dark mode — verify all new backends render correctly in dark mode
10. Run `npx vitest run` — all tests pass
</verify>
<done>User approves visual appearance and functionality of all 17 backends in both light and dark modes.</done>
</task>
</tasks>
<verification>
- `npx vitest run` — full suite green (all existing + new tests)
- `npx tsc --noEmit` — no type errors
- Visual verification of all 17 backends in both light and dark mode
- Form submission works for at least 3 new backends (gdrive, ftp, webdav)
</verification>
<success_criteria>
- Every backend type in BACKEND_REGISTRY has a working form in RemoteConfigStep
- OAuth backends (gdrive, dropbox, box, pcloud, onedrive) show OAuthInstructions
- gdrive shows GdriveAuthToggle with two auth paths
- Protocol backends render via generic FieldRenderer loop
- backendLabel derived from registry (no manual duplication)
- User approves visual appearance in both light and dark modes
</success_criteria>
<output>
After completion, create `.planning/phases/13-add-remaining-rclone-remotes/13-04-SUMMARY.md`
</output>
@@ -0,0 +1,120 @@
---
phase: 13-add-remaining-rclone-remotes
plan: 04
subsystem: ui
tags: [react, react-hook-form, zod, rclone, wizard, form-rendering]
# Dependency graph
requires:
- phase: 13-add-remaining-rclone-remotes/13-01
provides: BACKEND_REGISTRY with 18 backends, auto-generated BACKEND_SCHEMAS
- phase: 13-add-remaining-rclone-remotes/13-02
provides: GdriveAuthToggle, OAuthInstructions components
- phase: 13-add-remaining-rclone-remotes/13-03
provides: category-grouped searchable BackendSelectionStep
provides:
- RemoteConfigStep supports all 18 backends with correct auth handling per type
- backendLabel derived from registry (no manual duplication)
- renderBackendFields() switch function for clean multi-branch rendering
- 6 new tests for new backend form rendering (gdrive, dropbox, ftp, webdav, smb, http)
affects: [RemoteConfigStep, end-to-end wizard flow for new backends]
# Tech tracking
tech-stack:
added: []
patterns:
- "renderBackendFields() switch function: clean multi-branch form rendering instead of nested ternaries"
- "backendLabel from Object.fromEntries(BACKEND_REGISTRY) - derived, not duplicated"
- "getByLabelText selectors use partial regex (not anchored ^$) to handle TextFieldMD3 required * suffix"
key-files:
created: []
modified:
- src/components/wizard/RemoteConfigStep.tsx
- src/components/wizard/RemoteConfigStep.test.tsx
key-decisions:
- "renderBackendFields() switch statement instead of nested ternaries - 7 branches requires readable structure"
- "backendLabel derived via Object.fromEntries(BACKEND_REGISTRY) eliminates manual displayName maintenance"
- "Test label selectors use partial regex (/host/i not /^host$/i) - TextFieldMD3 appends required * in label text"
patterns-established:
- "Switch-based form branch pattern: renderBackendFields(backendType, register, errors) returns JSX for that type"
- "getByLabelText partial regex: when fields are required, TextFieldMD3 adds * to label text - use /label text/i not /^label text$/i"
requirements-completed: [REMOTE-05]
# Metrics
duration: 15min
completed: 2026-04-01
---
# Phase 13 Plan 04: RemoteConfigStep Full Backend Integration Summary
**RemoteConfigStep wired for all 18 backends: GdriveAuthToggle for gdrive, OAuthInstructions for onedrive/dropbox/box/pcloud, generic FieldRenderer loop for all others, with registry-derived backendLabel**
## Performance
- **Duration:** ~15 min
- **Started:** 2026-04-01T16:50:00Z
- **Completed:** 2026-04-01T17:00:00Z
- **Tasks:** 3 of 3
- **Files modified:** 2
## Accomplishments
- Replaced manual `backendLabel` record with `Object.fromEntries(BACKEND_REGISTRY)` derivation
- Refactored 3-branch ternary into `renderBackendFields()` switch function (7 branches + default)
- Added gdrive branch: GdriveAuthToggle + generic loop for non-auth fields (root_folder_id)
- Added onedrive branch: OAuthInstructions + generic loop
- Added dropbox, box, pcloud branches: each with OAuthInstructions + generic loop
- Default case handles remaining backends: azure-files, swift, ftp, webdav, smb, http, seafile, s3, s3-compatible, gcs, b2
- 6 new tests added for new backend form rendering — all 282 tests pass
## Task Commits
Each task was committed atomically:
1. **Task 1: Wire all new backends into RemoteConfigStep** - `d0c00f7` (feat)
2. **Task 2: Add tests for new backend form rendering** - `2c596c9` (test)
3. **Task 3: Visual verification** - approved by user (2026-04-01)
## Files Created/Modified
- `src/components/wizard/RemoteConfigStep.tsx` - Refactored with switch-based renderBackendFields(), registry-derived backendLabel, imports for GdriveAuthToggle and OAuthInstructions
- `src/components/wizard/RemoteConfigStep.test.tsx` - 6 new test cases for gdrive, dropbox, ftp, webdav, smb, http backends
## Decisions Made
- `renderBackendFields()` switch statement chosen over nested ternaries — 7 branches makes ternary chains unreadable
- `backendLabel` derived from registry via `Object.fromEntries` — eliminates the manual duplication risk (Pitfall 2 in research)
- Test label selectors use partial regex (e.g. `/host/i` not `/^host$/i`) because `TextFieldMD3` appends a `*` span to required field labels, making exact-anchor matches fail
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] Test label selectors fixed for TextFieldMD3 required field suffix**
- **Found during:** Task 2 (TDD RED phase)
- **Issue:** Plan specified tests like `getByLabelText(/^host$/i)` but TextFieldMD3 renders required field labels as "Host*" (with a `<span>*</span>`), so anchored regex `/^host$/i` fails to match the full label text
- **Fix:** Changed anchored patterns (`/^host$/i`) to non-anchored partial patterns (`/host/i`) in all affected test assertions
- **Files modified:** src/components/wizard/RemoteConfigStep.test.tsx
- **Verification:** All 6 new tests pass; full suite 282/282 green
- **Committed in:** 2c596c9 (Task 2 commit)
---
**Total deviations:** 1 auto-fixed (Rule 1 - Bug)
**Impact on plan:** Minor selector correction needed for test compatibility with existing TextFieldMD3 pattern. No scope creep.
## Issues Encountered
None beyond the test selector adjustment documented above.
## User Setup Required
None - no external service configuration required.
## Next Phase Readiness
- All 3 tasks complete: all 18 backends render correct forms
- Task 3 (visual verification) approved by user
- Phase 13 is fully complete — all 18 backends functional end-to-end
---
*Phase: 13-add-remaining-rclone-remotes*
*Completed: 2026-04-01*
@@ -0,0 +1,92 @@
# Phase 13: Add Remaining RClone Remotes - Context
**Gathered:** 2026-04-01
**Status:** Ready for planning
<domain>
## Phase Boundary
Expand backend support from the current 7 (azureblob, s3, s3-compatible, onedrive, sftp, gcs, b2) to comprehensive coverage of all rclone backends suitable for unattended Windows deployment via RMM/Intune. Includes new registry entries, auth handling, backend selection UX overhaul (categories + search + icons), and an OAuthInstructions reusable component.
</domain>
<decisions>
## Implementation Decisions
### Which backends to add
- Comprehensive coverage — all rclone backends that make sense for unattended Windows deployment
- OAuth-requiring backends included via token-paste approach (same as OneDrive: user runs `rclone authorize` locally, pastes resulting JSON token)
- All new backends shipped in a single phase (no sub-phases by auth type)
- Claude picks the specific backend list based on rclone documentation and deployment viability
### Auth complexity handling
- Reuse per-backend AuthToggle component pattern for backends with mutually-exclusive auth fields (e.g., FTP password vs key file) — same proven pattern as AzureAuthToggle/SftpAuthToggle
- Build a new reusable OAuthInstructions component for OAuth-token backends — collapsible step-by-step guide showing how to run `rclone authorize` for that specific backend
- OAuthInstructions takes backend-specific command and steps as props, keeps RemoteConfigStep clean
### Backend selection UX
- Categorized sections with headings: Cloud Object Storage, Cloud Drives, Protocol-based — grouped by storage type (matches how IT pros think about storage)
- Search/filter bar at the top that filters instantly as user types
- Search matches across all fields: displayName, description, category, and field labels
- Categories with no matching backends collapse when search is active
- Add `category` field to each BACKEND_REGISTRY entry — single source of truth stays single
- Add inline SVG icons per backend card — hand-picked SVGs bundled in the app, no external dependency
### Claude's Discretion
- BackendType implementation: keep explicit union type or derive dynamically from registry keys — pick whichever balances type safety and maintainability best at 20+ backends
- Specific backend list selection based on rclone docs and unattended deployment viability
- Field validation depth per backend (regex patterns vs simple required/optional)
- Icon design approach (monochrome vs brand colors, sizing)
</decisions>
<code_context>
## Existing Code Insights
### Reusable Assets
- `BACKEND_REGISTRY` (src/schemas/registry.ts): Single source of truth — new backend = new entry here
- `buildZodSchema()` (src/schemas/index.ts): Auto-generates Zod schema from registry fields — just add a new call
- `RCLONE_TYPE_MAP` (src/generators/rclone-conf.ts): Maps internal BackendType to rclone's type string
- `FieldRenderer` (src/components/ui/FieldRenderer.tsx): Renders any field from registry — supports text, password, select, toggle
- `AzureAuthToggle` / `SftpAuthToggle`: Proven pattern for mutually-exclusive auth fields with CSS-hidden toggle
- `BackendSelectionStep`: Currently renders flat card grid — needs category grouping, search, and icons
- `TextFieldMD3`: MD3-styled input used across all steps — search bar should reuse this
### Established Patterns
- Registry-driven rendering: RemoteConfigStep uses three-branch ternary (azureblob/sftp/else) — new backends with auth toggles need branches, others use generic loop
- `BackendType` union type: Currently 7 literals — all type-safe references need updating
- `BACKEND_SCHEMAS` object: Explicit per-backend `buildZodSchema()` calls — needs entry per new backend
- `backendLabel` record in RemoteConfigStep: Manual display name mapping — needs entry per new backend
- MD3 token system: All styling uses semantic CSS custom properties (bg-surface, text-on-surface, etc.)
### Integration Points
- `src/schemas/registry.ts`: Add new backend entries (fields, displayName, description, category)
- `src/schemas/index.ts`: Add `buildZodSchema()` calls for new backends
- `src/generators/rclone-conf.ts`: Add entries to RCLONE_TYPE_MAP
- `src/store/types.ts`: Update BackendType union (or derive from registry)
- `src/components/wizard/RemoteConfigStep.tsx`: Add branches for backends needing auth toggles, update backendLabel
- `src/components/wizard/BackendSelectionStep.tsx`: Add category grouping, search filter, icon display
</code_context>
<specifics>
## Specific Ideas
- OAuthInstructions component should be collapsible — not visible by default, user expands when they need step-by-step guidance for running `rclone authorize`
- Categories should match IT pro mental model: "Cloud Object Storage" (S3-like), "Cloud Drives" (Google Drive, Dropbox, OneDrive), "Protocol-based" (FTP, SFTP, WebDAV, SMB)
- Search should be instant (filter-as-you-type) and match broadly including field labels — so searching "Cloudflare" finds R2 even if it's listed as "Cloudflare R2"
- Icons are inline SVGs — no npm icon library dependency, full styling control
</specifics>
<deferred>
## Deferred Ideas
None — discussion stayed within phase scope
</deferred>
---
*Phase: 13-add-remaining-rclone-remotes*
*Context gathered: 2026-04-01*
@@ -0,0 +1,606 @@
# Phase 13: Add Remaining RClone Remotes - Research
**Researched:** 2026-04-01
**Domain:** rclone backend registry expansion, React component architecture, UX search/categorization
**Confidence:** HIGH
---
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
- Comprehensive coverage — all rclone backends that make sense for unattended Windows deployment
- OAuth-requiring backends included via token-paste approach (same as OneDrive: user runs `rclone authorize` locally, pastes resulting JSON token)
- All new backends shipped in a single phase (no sub-phases by auth type)
- Claude picks the specific backend list based on rclone documentation and deployment viability
- Reuse per-backend AuthToggle component pattern for backends with mutually-exclusive auth fields (e.g., FTP password vs key file) — same proven pattern as AzureAuthToggle/SftpAuthToggle
- Build a new reusable OAuthInstructions component for OAuth-token backends — collapsible step-by-step guide showing how to run `rclone authorize` for that specific backend
- OAuthInstructions takes backend-specific command and steps as props, keeps RemoteConfigStep clean
- Categorized sections with headings: Cloud Object Storage, Cloud Drives, Protocol-based — grouped by storage type
- Search/filter bar at the top that filters instantly as user types
- Search matches across all fields: displayName, description, category, and field labels
- Categories with no matching backends collapse when search is active
- Add `category` field to each BACKEND_REGISTRY entry — single source of truth stays single
- Add inline SVG icons per backend card — hand-picked SVGs bundled in the app, no external dependency
### Claude's Discretion
- BackendType implementation: keep explicit union type or derive dynamically from registry keys — pick whichever balances type safety and maintainability best at 20+ backends
- Specific backend list selection based on rclone docs and unattended deployment viability
- Field validation depth per backend (regex patterns vs simple required/optional)
- Icon design approach (monochrome vs brand colors, sizing)
### Deferred Ideas (OUT OF SCOPE)
None — discussion stayed within phase scope
</user_constraints>
---
## Summary
Phase 13 expands the BACKEND_REGISTRY from 7 to approximately 18-20 entries, grouping them into three categories (Cloud Object Storage, Cloud Drives, Protocol-based). Each new backend requires: a registry entry, a RCLONE_TYPE_MAP entry, a `buildZodSchema()` call, and a `backendLabel` entry. Backends with OAuth tokens get an `OAuthInstructions` collapsible component. Backends with mutually-exclusive auth get a new AuthToggle component. The BackendSelectionStep gets a complete UX overhaul with category headings, instant search, and inline SVG icons.
The critical architectural decision is whether `BackendType` stays as an explicit union literal or is derived from registry keys. At 20+ backends, explicit union becomes a maintenance burden (every new backend requires editing 6 files). Deriving from registry keys reduces that to 4 files and eliminates the "forgot to update BackendType" class of bug.
**Primary recommendation:** Derive `BackendType` from `keyof typeof BACKEND_REGISTRY` to reduce per-backend boilerplate from 6 touch-points to 4. Use `as const` assertion on the registry object for full TypeScript narrowing.
---
## Recommended Backend List
Based on rclone documentation and suitability for unattended Windows deployment via RMM/Intune, the following backends should be added. Each entry shows the rclone `type` string and auth approach.
### Category: Cloud Object Storage (S3-like)
These all use static API credentials — ideal for unattended deployment.
| Backend ID | displayName | rclone type | Auth approach | Notes |
|------------|-------------|-------------|---------------|-------|
| `azureblob` | Azure Blob Storage | `azureblob` | Key or SAS URL | Existing |
| `azure-files` | Azure Files Storage | `azurefiles` | Account + Key | New — uses `account` + `key` fields |
| `s3` | Amazon S3 | `s3` | Access Key + Secret | Existing |
| `s3-compatible` | S3-Compatible | `s3` (provider=Other) | Access Key + Secret + Endpoint | Existing |
| `gcs` | Google Cloud Storage | `google cloud storage` | Service Account JSON | Existing |
| `b2` | Backblaze B2 | `b2` | Application Key ID + Key | Existing |
| `swift` | OpenStack Swift | `swift` | User + Key + Auth URL + Tenant | New — API credential |
### Category: Cloud Drives (OAuth token-paste)
These require OAuth — user runs `rclone authorize` locally, pastes JSON token.
| Backend ID | displayName | rclone type | Auth approach | Notes |
|------------|-------------|-------------|---------------|-------|
| `onedrive` | OneDrive | `onedrive` | OAuth token + drive_id + drive_type | Existing |
| `gdrive` | Google Drive | `drive` | OAuth token + service account option | New — token or service_account_credentials |
| `dropbox` | Dropbox | `dropbox` | OAuth token (JSON blob) | New — token field only |
| `box` | Box | `box` | OAuth token (JSON blob) | New — token + box_sub_type |
| `pcloud` | pCloud | `pcloud` | OAuth token (JSON blob) | New — token + hostname |
### Category: Protocol-based
These use server credentials — IP/hostname, user, password.
| Backend ID | displayName | rclone type | Auth approach | Notes |
|------------|-------------|-------------|---------------|-------|
| `sftp` | SFTP | `sftp` | Password or Key PEM (AuthToggle) | Existing |
| `ftp` | FTP | `ftp` | Host + User + Password | New — also FTPS via `tls` select |
| `webdav` | WebDAV | `webdav` | URL + User + Password + Vendor select | New — includes NextCloud, SharePoint, OwnCloud, etc. |
| `smb` | SMB / Windows Share | `smb` | Host + User + Pass + Domain | New — ideal for Windows shops |
| `http` | HTTP (read-only) | `http` | URL only | New — simple, read-only |
| `seafile` | Seafile | `seafile` | URL + User + Password | New — self-hosted cloud |
**Total:** 7 existing + 10 new = 17 backends
**Excluded from scope (not suitable for unattended Windows deployment):**
- HDFS — Hadoop/enterprise Linux ecosystem, not Windows-native
- Google Photos — read-only, consumer
- iCloud Drive — macOS-only tooling
- Mega, Jottacloud, Yandex, Mail.ru — consumer-grade, poor enterprise adoption
- pCloud — included only if token-paste approach is feasible (MEDIUM confidence)
- Internet Archive, Pixeldrain, Uloz.to, Gofile — public/consumer file sharing
---
## Standard Stack
### Core (no new dependencies)
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| react | ^18.3.1 | UI rendering | Existing |
| react-hook-form | ^7.72.0 | Form state + validation | Existing — all forms use it |
| zod | ^4.3.6 | Schema validation | Existing — `buildZodSchema()` auto-derives |
| @hookform/resolvers | ^5.2.2 | Zod <-> RHF bridge | Existing |
**No new npm dependencies.** Inline SVG icons are bundled as TSX components. No icon library needed.
### Installation
```bash
# Nothing new to install — all new code uses existing stack
```
---
## Architecture Patterns
### Recommended Project Structure (additions only)
```
src/
├── schemas/
│ └── registry.ts # Add ~10 new backend entries + category field
├── generators/
│ └── rclone-conf.ts # Add ~10 entries to RCLONE_TYPE_MAP
├── components/
│ ├── wizard/
│ │ ├── BackendSelectionStep.tsx # Full overhaul: categories, search, icons
│ │ ├── RemoteConfigStep.tsx # Add auth-toggle branches for FTP/WebDAV
│ │ ├── FtpAuthToggle.tsx # New: password vs no-auth (anonymous) toggle
│ │ ├── OAuthInstructions.tsx # New: reusable collapsible rclone authorize guide
│ │ └── [backend]AuthToggle.tsx # Only if backend needs mutually-exclusive auth
│ └── icons/
│ └── BackendIcons.tsx # New: inline SVG icon map, keyed by BackendType
```
### Pattern 1: Deriving BackendType from Registry (RECOMMENDED)
The current explicit union `'azureblob' | 's3' | ...` must be touched in `registry.ts` every time a backend is added. At 17+ backends, derive it instead:
```typescript
// src/schemas/registry.ts
export const BACKEND_REGISTRY = {
azureblob: { ... },
gdrive: { ... },
// ... all entries
} as const; // <-- CRITICAL: 'as const' needed for keyof narrowing
export type BackendType = keyof typeof BACKEND_REGISTRY;
// Result: 'azureblob' | 's3' | 'gdrive' | ... (auto-updated when registry grows)
```
**What changes:** `BackendType` no longer lives in `registry.ts` as a separate literal union — it is derived. The `src/store/types.ts` re-export (`export type { BackendType }`) continues to work unchanged. All consumers importing `BackendType` from `./registry` or `../store/types` require zero changes.
**What must be verified:** The `BACKEND_REGISTRY` cast at the bottom currently uses `as Record<BackendType, ...>` which would become circular. Remove that cast — with `as const` the registry is already fully typed by inference.
**Confidence:** HIGH — standard TypeScript pattern, verified against codebase.
### Pattern 2: BACKEND_REGISTRY Entry with `category` Field
Add `category` to the registry entry — single source of truth for categorization:
```typescript
// src/schemas/registry.ts
export type BackendCategory = 'cloud-object-storage' | 'cloud-drives' | 'protocol-based';
export interface BackendMeta {
displayName: string;
description: string;
category: BackendCategory; // NEW field
fields: FieldDef[];
}
export const BACKEND_REGISTRY = {
azureblob: {
displayName: 'Azure Blob Storage',
description: 'Microsoft Azure cloud storage',
category: 'cloud-object-storage', // NEW
fields: [ ... ],
},
gdrive: {
displayName: 'Google Drive',
description: 'Google Drive (paste pre-obtained rclone token)',
category: 'cloud-drives',
fields: [ ... ],
},
} as const;
```
### Pattern 3: OAuthInstructions Component
Collapsible component, collapsed by default. Takes backend-specific props:
```typescript
// src/components/wizard/OAuthInstructions.tsx
interface OAuthInstructionsProps {
backendName: string; // e.g. "Google Drive"
authorizeCommand: string; // e.g. 'rclone authorize "drive"'
steps?: string[]; // optional custom step descriptions
}
export function OAuthInstructions({ backendName, authorizeCommand, steps }: OAuthInstructionsProps) {
const [expanded, setExpanded] = useState(false);
// Renders a collapsible details/summary or button-controlled div
// Shows numbered steps: install rclone → run command → browser opens → paste JSON
}
```
Used in RemoteConfigStep for `gdrive`, `dropbox`, `box`, `pcloud` alongside the `token` PasswordField:
```typescript
// In RemoteConfigStep, new OAuth branch:
} : backendType === 'gdrive' ? (
<>
<OAuthInstructions
backendName="Google Drive"
authorizeCommand='rclone authorize "drive"'
/>
<FieldRenderer field={BACKEND_REGISTRY.gdrive.fields.find(f => f.key === 'token')!} ... />
<FieldRenderer field={BACKEND_REGISTRY.gdrive.fields.find(f => f.key === 'root_folder_id')!} ... />
</>
)
```
### Pattern 4: BackendSelectionStep Category + Search
The current flat grid becomes a category-grouped, searchable layout:
```typescript
// Category display order (stable, not alphabetical)
const CATEGORY_ORDER: BackendCategory[] = [
'cloud-object-storage',
'cloud-drives',
'protocol-based',
];
const CATEGORY_LABELS: Record<BackendCategory, string> = {
'cloud-object-storage': 'Cloud Object Storage',
'cloud-drives': 'Cloud Drives',
'protocol-based': 'Protocol-based',
};
// Search filter logic — matches displayName, description, category label, and field labels
function matchesSearch(entry: BackendMeta, query: string): boolean {
const q = query.toLowerCase();
if (entry.displayName.toLowerCase().includes(q)) return true;
if (entry.description.toLowerCase().includes(q)) return true;
if (CATEGORY_LABELS[entry.category].toLowerCase().includes(q)) return true;
if (entry.fields.some(f => f.label.toLowerCase().includes(q))) return true;
return false;
}
```
Search bar uses the existing `TextFieldMD3` component. The search query is stored in local `useState` — no global state needed.
### Pattern 5: Inline SVG Icon Component
Icons are TSX components returning `<svg>` elements directly — no external dependency, full styling control:
```typescript
// src/components/icons/BackendIcons.tsx
interface IconProps { className?: string; }
export const AzureIcon = ({ className }: IconProps) => (
<svg className={className} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
{/* Azure blob SVG path */}
</svg>
);
// Icon map keyed by BackendType
export const BACKEND_ICONS: Partial<Record<BackendType, React.FC<IconProps>>> = {
azureblob: AzureIcon,
gdrive: GoogleDriveIcon,
// ...
};
```
`BackendCard` receives an optional `icon` prop rendered at the top-left of the card.
### Anti-Patterns to Avoid
- **Maintaining explicit BackendType union:** At 17+ backends, a forgotten update causes silent TypeScript errors in `backendLabel` exhaustiveness checks. Derive from registry keys instead.
- **Splitting `category` into a separate map:** Keeping it in `BACKEND_REGISTRY` means one file to edit per new backend. A separate `BACKEND_CATEGORIES` record creates two sources of truth.
- **Using `display: none` CSS for search-hidden categories:** The category heading itself must be hidden when its backends are all filtered out — use conditional rendering, not CSS hiding (CSS hiding leaves empty heading wrappers in DOM).
- **Eager OAuthInstructions expansion:** Default expanded state wastes screen space and is intimidating. Default collapsed, user expands when they need help.
- **Placeholder prop on `TextFieldMD3`:** TextFieldMD3 uses `placeholder=" "` (space) internally for the CSS-only floating label. Do NOT pass a real placeholder string — it will break the `peer-[:not(:placeholder-shown)]` selector. Use `helpText` for descriptive hints instead.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| OAuth token paste textarea | Multi-line textarea component | `PasswordField` (inputType: 'password') | Existing component with show/hide, tooltip, validation — textarea breaks form validation contract |
| Search debouncing | `useDebounce` hook | Direct `useState` + filter on render | 17 backends filter in <1ms; debounce is premature optimization |
| Icon library | npm icon library install | Inline TSX SVG components in `BackendIcons.tsx` | Zero dependency, full Tailwind/CSS control, no bundle bloat |
| BackendType exhaustiveness check | Manual `assertNever()` | `keyof typeof BACKEND_REGISTRY` derivation | TypeScript already errors if a key is missing from `backendLabel` record |
| Category data store | Separate `CATEGORY_REGISTRY` map | `category` field on `BACKEND_REGISTRY` entry | Single source of truth already established |
| rclone type mapping override | Per-backend conditional logic | `RCLONE_TYPE_MAP` entry (existing pattern) | Already used for `s3-compatible``s3` |
**Key insight:** The registry-driven pattern already handles 95% of new-backend complexity. Most new backends (FTP, Seafile, Koofr) need zero custom React components — just a registry entry, a RCLONE_TYPE_MAP entry, a schema call, and a backendLabel string.
---
## Common Pitfalls
### Pitfall 1: BackendType Cast Conflicts with `as const`
**What goes wrong:** The current registry.ts bottom has `} as Record<BackendType, { displayName: string; description: string; fields: FieldDef[] }>`. When `BackendType` is derived from `keyof typeof BACKEND_REGISTRY`, this cast creates a circular dependency (`BackendType` depends on the registry type, which is cast to use `BackendType`).
**Why it happens:** TypeScript cannot resolve the type when both sides of the `as` cast reference each other.
**How to avoid:** Remove the cast entirely. With `as const` on the registry object, TypeScript infers the full precise type. The `BackendMeta` interface should be used for individual entries, not as a cast on the whole object.
**Warning signs:** TypeScript error like "Type alias 'BackendType' circularly references itself."
### Pitfall 2: `backendLabel` Record in RemoteConfigStep Breaks Exhaustiveness
**What goes wrong:** `backendLabel` is typed as `Record<NonNullable<typeof backendType>, string>` — adding a new `BackendType` without adding to `backendLabel` causes a TypeScript error at this record. This is desirable (forced exhaustiveness) but easy to miss during development.
**How to avoid:** Add `backendLabel` entry as the FIRST thing when adding a new backend. Use `displayName` from registry directly instead of duplicating: `const backendLabel = Object.fromEntries(Object.entries(BACKEND_REGISTRY).map(([k, v]) => [k, v.displayName]))` — eliminates the duplication entirely.
**Warning signs:** TS2741 "Property X is missing in type Record<...>"
### Pitfall 3: `BACKEND_SCHEMAS` Explicit Object Misses New Backends
**What goes wrong:** `BACKEND_SCHEMAS` in `src/schemas/index.ts` is an explicit object with one `buildZodSchema()` call per backend. Adding a backend to the registry but forgetting `BACKEND_SCHEMAS` causes a runtime crash in `RemoteConfigStep` when the schema lookup returns `undefined`.
**How to avoid:** Auto-generate `BACKEND_SCHEMAS` from the registry:
```typescript
export const BACKEND_SCHEMAS = Object.fromEntries(
(Object.keys(BACKEND_REGISTRY) as BackendType[]).map(t => [t, buildZodSchema(t)])
) as Record<BackendType, ReturnType<typeof buildZodSchema>>;
```
This makes `BACKEND_SCHEMAS` auto-expand whenever the registry grows.
**Warning signs:** Runtime error "Cannot read properties of undefined (reading 'parse')" in RemoteConfigStep.
### Pitfall 4: FTP/WebDAV Toggle Auth — Zod Schema for Hidden Fields
**What goes wrong:** AzureAuthToggle and SftpAuthToggle register BOTH auth fields always (CSS-hide the inactive one) so values are preserved on tab switch. The Zod schema marks both as optional. This works for Azure (key/sas_url are both optional) but FTP's `pass` field may be expected to be required — making it optional in Zod allows submitting with no password, which silently creates a broken config.
**Why it happens:** Toggle pattern requires both fields to be optional in Zod regardless of visual state.
**How to avoid:** For FTP, both `pass` (required: false) and the anonymous-access toggle approach is fine since FTP often legitimately uses anonymous auth. Mark both toggle fields as `required: false` in the registry. Document in helpText that the inactive field is ignored.
### Pitfall 5: Search Filter Leaves Empty Category Headings in DOM
**What goes wrong:** If search is implemented with CSS `hidden` on non-matching cards but the category heading is always rendered, an empty `<h3>Cloud Drives</h3>` appears when all Cloud Drive backends are filtered out.
**How to avoid:** Filter backends per category before render, skip the entire category block (heading + cards) when `filteredBackends.length === 0` for that category.
### Pitfall 6: rclone Type Strings Are Exact and Case-Sensitive
**What goes wrong:** `RCLONE_TYPE_MAP` maps backend IDs to rclone type strings. Errors here produce a broken `rclone.conf` that rclone silently rejects.
**Verified mappings (HIGH confidence — from rclone official docs):**
| BackendType | rclone type string |
|-------------|-------------------|
| `gdrive` | `drive` |
| `dropbox` | `dropbox` |
| `box` | `box` |
| `pcloud` | `pcloud` |
| `ftp` | `ftp` |
| `webdav` | `webdav` |
| `smb` | `smb` |
| `http` | `http` |
| `swift` | `swift` |
| `seafile` | `seafile` |
| `azure-files` | `azurefiles` |
**Warning signs:** rclone error "didn't find section in config file" or "unknown backend type."
### Pitfall 7: Google Drive Has Two Auth Paths — Service Account vs OAuth Token
**What goes wrong:** Google Drive supports both service account JSON (for unattended, like GCS) and OAuth token paste (like OneDrive). Exposing both options requires an AuthToggle, or a decision to support only one path.
**Recommendation:** Support both via a `GdriveAuthToggle` component:
- Tab 1: "OAuth Token" — token field + OAuthInstructions component
- Tab 2: "Service Account" — service_account_credentials field (reuses GCS pattern)
This matches the unattended deployment goal: domain admins can use service accounts; personal Drive setups use OAuth token paste.
---
## Code Examples
Verified patterns from codebase:
### Auto-derived BackendType (replaces explicit union)
```typescript
// src/schemas/registry.ts
export const BACKEND_REGISTRY = {
azureblob: { displayName: '...', description: '...', category: 'cloud-object-storage', fields: [...] },
gdrive: { displayName: '...', description: '...', category: 'cloud-drives', fields: [...] },
// ...
} as const; // REQUIRED for keyof narrowing
export type BackendCategory = 'cloud-object-storage' | 'cloud-drives' | 'protocol-based';
export type BackendType = keyof typeof BACKEND_REGISTRY;
// Automatically includes every key in the registry
```
### Auto-generated BACKEND_SCHEMAS (replaces explicit per-backend calls)
```typescript
// src/schemas/index.ts
export const BACKEND_SCHEMAS = Object.fromEntries(
(Object.keys(BACKEND_REGISTRY) as BackendType[]).map(t => [t, buildZodSchema(t)])
) as Record<BackendType, ReturnType<typeof buildZodSchema>>;
```
### backendLabel derived from registry (removes duplicate displayNames)
```typescript
// In RemoteConfigStep.tsx
const backendLabel = Object.fromEntries(
Object.entries(BACKEND_REGISTRY).map(([k, v]) => [k, v.displayName])
) as Record<BackendType, string>;
```
### Category-grouped backend rendering in BackendSelectionStep
```typescript
const CATEGORY_ORDER: BackendCategory[] = ['cloud-object-storage', 'cloud-drives', 'protocol-based'];
// Inside component:
const filteredEntries = Object.entries(BACKEND_REGISTRY).filter(
([, entry]) => !query || matchesSearch(entry, query)
);
{CATEGORY_ORDER.map(cat => {
const backends = filteredEntries.filter(([, e]) => e.category === cat);
if (backends.length === 0) return null; // collapse empty categories
return (
<section key={cat}>
<h3>{CATEGORY_LABELS[cat]}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{backends.map(([type, entry]) => (
<BackendCard key={type} ... icon={BACKEND_ICONS[type as BackendType]} />
))}
</div>
</section>
);
})}
```
### OAuthInstructions usage in RemoteConfigStep
```typescript
// For gdrive OAuth path in RemoteConfigStep:
<OAuthInstructions
backendName="Google Drive"
authorizeCommand='rclone authorize "drive"'
/>
<FieldRenderer
field={BACKEND_REGISTRY.gdrive.fields.find(f => f.key === 'token')!}
register={register}
error={errors.token as FieldError | undefined}
/>
```
### FTP registry entry (example of new backend with optional FTPS)
```typescript
ftp: {
displayName: 'FTP',
description: 'File Transfer Protocol (supports FTPS)',
category: 'protocol-based',
fields: [
{ key: 'host', label: 'Host', inputType: 'text', required: true, placeholder: 'ftp.example.com' },
{ key: 'user', label: 'Username', inputType: 'text', required: false, placeholder: 'anonymous', helpText: 'Leave blank for anonymous FTP' },
{ key: 'pass', label: 'Password', inputType: 'password', required: false },
{ key: 'port', label: 'Port', inputType: 'text', required: false, placeholder: '21' },
{ key: 'explicit_tls', label: 'TLS Mode', inputType: 'select', required: false,
options: [
{ value: '', label: 'Plain FTP (no encryption)' },
{ value: 'true', label: 'Explicit FTPS (STARTTLS)' },
],
helpText: 'Use Explicit FTPS for encrypted FTP connections (port 21 + STARTTLS)',
},
],
},
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Explicit `BackendType` union literal (7 entries) | Derive from `keyof typeof BACKEND_REGISTRY` | Phase 13 | Eliminates one of 6 touch-points per new backend |
| Explicit per-backend `buildZodSchema()` calls | Auto-generate from registry keys | Phase 13 | Eliminates one of 6 touch-points per new backend |
| Manual `backendLabel` record duplication | Derive from `entry.displayName` | Phase 13 | Eliminates last duplication of displayName |
| Flat card grid (no categories) | Category-grouped with search | Phase 13 | Required UX for 17+ backends |
| No OAuth guidance | OAuthInstructions collapsible component | Phase 13 | New backends (gdrive, dropbox, box) need this |
---
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Vitest ^4.1.1 |
| Config file | `vite.config.ts` (test.environment: jsdom) + `vitest.config.ts` (node env for generator tests) |
| Quick run command | `npx vitest run` |
| Full suite command | `npx vitest run` |
### Phase Requirements → Test Map
| Behavior | Test Type | Automated Command | File Exists? |
|----------|-----------|-------------------|-------------|
| All 17 backend types present in BACKEND_REGISTRY | unit | `npx vitest run src/schemas/registry.test.ts` | Update existing ✅ |
| Each new backend has required fields | unit | `npx vitest run src/schemas/registry.test.ts` | Update existing ✅ |
| BACKEND_SCHEMAS auto-covers new backends (no undefined) | unit | `npx vitest run src/schemas/index.test.ts` | Wave 0 gap ❌ |
| RCLONE_TYPE_MAP has entry for each new backend | unit | `npx vitest run src/generators/rclone-conf.test.ts` | Update existing ✅ |
| BackendSelectionStep renders all 17 cards | unit | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | Update existing ✅ |
| Search filter hides non-matching cards | unit | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | Wave 0 gap ❌ |
| Category headings collapse when all items filtered | unit | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | Wave 0 gap ❌ |
| OAuthInstructions renders collapsed by default | unit | `npx vitest run src/components/wizard/OAuthInstructions.test.tsx` | Wave 0 gap ❌ |
| OAuthInstructions expands on button click | unit | `npx vitest run src/components/wizard/OAuthInstructions.test.tsx` | Wave 0 gap ❌ |
| RemoteConfigStep renders gdrive form fields | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | Update existing ✅ |
| RemoteConfigStep renders ftp form fields | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | Update existing ✅ |
| buildRcloneConf outputs correct type for gdrive | unit | `npx vitest run src/generators/rclone-conf.test.ts` | Update existing ✅ |
### 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/schemas/index.test.ts` — covers BACKEND_SCHEMAS auto-generation (verify no undefined for new types)
- [ ] `src/components/wizard/OAuthInstructions.test.tsx` — covers collapsed default + expand behavior
- [ ] New test cases in `BackendSelectionStep.test.tsx` — search filter and category collapse behavior
---
## Open Questions
1. **Google Drive: OAuth token vs service account — single form or toggle?**
- What we know: GCS already uses service_account_credentials. Google Drive supports both approaches.
- What's unclear: Whether IT pros deploying via RMM will have service accounts for Drive (less common than for GCS).
- Recommendation: Implement `GdriveAuthToggle` with two tabs (OAuth token + Service Account) to cover both. If that adds too much complexity, default to OAuth token path only (simpler, more universally applicable).
2. **Azure Files vs Azure Blob — same `azurefiles` type string?**
- What we know: rclone docs list `azurefiles` as the type for Azure Files Storage (distinct from `azureblob`).
- What's unclear: Required fields for `azurefiles` — need to verify account/key vs connection_string approach.
- Recommendation: Verify against https://rclone.org/azurefiles/ during plan execution before writing registry fields. HIGH confidence on type string, MEDIUM confidence on exact field names.
3. **pCloud hostname requirement for EU users**
- What we know: pCloud has region servers; EU users must set `hostname` to `eapi.pcloud.com`.
- What's unclear: Can we default `hostname` to `api.pcloud.com` and let users override, or is a select dropdown better?
- Recommendation: Add `hostname` as a select with two options (US/EU) — clearer than asking IT pros to know the API hostname.
4. **BackendCard icon sizing and color**
- What we know: User decided "inline SVG, no npm library."
- What's unclear: Monochrome (currentColor, works in dark mode) vs brand colors.
- Recommendation: Monochrome (`fill="currentColor"`) for consistency with MD3 token system and dark mode compatibility. Brand-colored icons require hardcoded hex values that ignore the MD3 color token system.
---
## Sources
### Primary (HIGH confidence)
- rclone.org/drive/ — Google Drive type string (`drive`), service account fields, OAuth token structure
- rclone.org/dropbox/ — Dropbox type string (`dropbox`), token field
- rclone.org/box/ — Box type string (`box`), token + box_sub_type fields
- rclone.org/pcloud/ — pCloud type string (`pcloud`), token + hostname fields
- rclone.org/ftp/ — FTP type string (`ftp`), host/user/pass/tls fields
- rclone.org/webdav/ — WebDAV type string (`webdav`), url/user/pass/vendor fields + vendor options
- rclone.org/smb/ — SMB type string (`smb`), host/user/pass/domain fields
- rclone.org/swift/ — OpenStack Swift type string (`swift`), required fields
- rclone.org/seafile/ — Seafile type string (`seafile`), url/user/pass fields
- rclone.org/koofr/ — Koofr type string (`koofr`), user/password fields
- rclone.org/s3/ — S3-compatible provider enum (Wasabi, Cloudflare, MinIO, etc.)
- Existing codebase (registry.ts, BackendSelectionStep.tsx, RemoteConfigStep.tsx) — HIGH confidence on patterns
### Secondary (MEDIUM confidence)
- rclone.org/overview/ — full backend list confirmed
- rclone.org/docs/ — comprehensive backend list verified
### Tertiary (LOW confidence)
- Azure Files (`azurefiles`) field names — type string confirmed, exact credential fields need verification against https://rclone.org/azurefiles/
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — no new dependencies, all existing patterns
- Backend list selection: HIGH — rclone official docs confirmed for all 10 new backends
- rclone type strings: HIGH — verified against official docs for all backends
- Architecture patterns (BackendType derivation): HIGH — standard TypeScript `keyof typeof` pattern
- Architecture patterns (OAuthInstructions component): HIGH — directly mirrors existing AzureAuthToggle/SftpAuthToggle patterns
- BackendSelectionStep overhaul: HIGH — filter/group/search is standard React pattern, no novel abstractions
- Azure Files field names: MEDIUM — type string confirmed, credential fields need docs verification
- pCloud EU hostname handling: MEDIUM — documented in rclone docs, implementation approach is recommendation
**Research date:** 2026-04-01
**Valid until:** 2026-05-01 (rclone docs are stable; backend type strings rarely change)
@@ -0,0 +1,90 @@
---
phase: 13
slug: add-remaining-rclone-remotes
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-04-01
---
# Phase 13 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution.
---
## Test Infrastructure
| Property | Value |
|----------|-------|
| **Framework** | Vitest ^4.1.1 |
| **Config file** | `vite.config.ts` (test.environment: jsdom) + `vitest.config.ts` (node env for generator tests) |
| **Quick run command** | `npx vitest run` |
| **Full suite command** | `npx vitest run` |
| **Estimated runtime** | ~15 seconds |
---
## Sampling Rate
- **After every task commit:** Run `npx vitest run`
- **After every plan wave:** Run `npx vitest run`
- **Before `/gsd:verify-work`:** Full suite must be green
- **Max feedback latency:** 15 seconds
---
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
| 13-01-01 | 01 | 0 | Wave 0 test stubs | unit | `npx vitest run src/schemas/index.test.ts` | Wave 0 gap | ⬜ pending |
| 13-01-02 | 01 | 0 | OAuthInstructions test stubs | unit | `npx vitest run src/components/wizard/OAuthInstructions.test.tsx` | Wave 0 gap | ⬜ pending |
| 13-01-03 | 01 | 0 | BackendSelectionStep search/category test stubs | unit | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | Wave 0 gap | ⬜ pending |
| 13-02-01 | 02 | 1 | All 17 backends in BACKEND_REGISTRY | unit | `npx vitest run src/schemas/registry.test.ts` | Update existing ✅ | ⬜ pending |
| 13-02-02 | 02 | 1 | Each new backend has required fields | unit | `npx vitest run src/schemas/registry.test.ts` | Update existing ✅ | ⬜ pending |
| 13-02-03 | 02 | 1 | BACKEND_SCHEMAS auto-covers new backends | unit | `npx vitest run src/schemas/index.test.ts` | Wave 0 ❌ | ⬜ pending |
| 13-02-04 | 02 | 1 | RCLONE_TYPE_MAP has entry for each backend | unit | `npx vitest run src/generators/rclone-conf.test.ts` | Update existing ✅ | ⬜ pending |
| 13-03-01 | 03 | 1 | BackendSelectionStep renders all 17 cards | unit | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | Update existing ✅ | ⬜ pending |
| 13-03-02 | 03 | 1 | Search filter hides non-matching cards | unit | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | Wave 0 ❌ | ⬜ pending |
| 13-03-03 | 03 | 1 | Category headings collapse when filtered | unit | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | Wave 0 ❌ | ⬜ pending |
| 13-04-01 | 04 | 2 | OAuthInstructions renders collapsed | unit | `npx vitest run src/components/wizard/OAuthInstructions.test.tsx` | Wave 0 ❌ | ⬜ pending |
| 13-04-02 | 04 | 2 | OAuthInstructions expands on click | unit | `npx vitest run src/components/wizard/OAuthInstructions.test.tsx` | Wave 0 ❌ | ⬜ pending |
| 13-04-03 | 04 | 2 | RemoteConfigStep renders gdrive fields | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | Update existing ✅ | ⬜ pending |
| 13-04-04 | 04 | 2 | RemoteConfigStep renders ftp fields | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | Update existing ✅ | ⬜ pending |
| 13-04-05 | 04 | 2 | buildRcloneConf outputs correct type for gdrive | unit | `npx vitest run src/generators/rclone-conf.test.ts` | Update existing ✅ | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
---
## Wave 0 Requirements
- [ ] `src/schemas/index.test.ts` — stubs for BACKEND_SCHEMAS auto-generation (verify no undefined for new types)
- [ ] `src/components/wizard/OAuthInstructions.test.tsx` — stubs for collapsed default + expand behavior
- [ ] New test cases in `src/components/wizard/BackendSelectionStep.test.tsx` — search filter and category collapse behavior
*Existing infrastructure covers registry, rclone-conf, and RemoteConfigStep tests.*
---
## Manual-Only Verifications
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Backend card icons render correctly in light/dark mode | Visual | SVG rendering + theme tokens | Open BackendSelectionStep, toggle theme, verify icon visibility |
| OAuthInstructions shows correct rclone authorize command | UX | Backend-specific CLI command | Select OAuth backend, expand instructions, verify command matches rclone docs |
| Category grouping matches IT pro mental model | UX | Subjective grouping | Review categories: Cloud Object Storage, Cloud Drives, Protocol-based |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 15s
- [ ] `nyquist_compliant: true` set in frontmatter
**Approval:** pending
@@ -0,0 +1,172 @@
---
phase: 13-add-remaining-rclone-remotes
verified: 2026-04-01T17:30:00Z
status: passed
score: 14/14 must-haves verified
human_verification:
- test: "Visual appearance of 18 backends in categorized wizard — Step 1"
expected: "Three category headings visible (Cloud Object Storage, Cloud Drives, Protocol-based), all 18 backend cards with inline SVG icons, search bar filters instantly"
why_human: "Visual rendering, icon recognition at 24px, and MD3 dark mode appearance cannot be verified programmatically"
- test: "Google Drive auth toggle in Step 2"
expected: "GdriveAuthToggle shows OAuth Token / Service Account tabs; OAuth tab shows collapsible OAuthInstructions with rclone authorize \"drive\" command; switching to Service Account tab shows service account credentials field"
why_human: "Tab switching interaction and collapsible disclosure visual behavior require manual validation"
- test: "OAuth backends (Dropbox, Box, pCloud, OneDrive) Step 2 forms"
expected: "Each shows collapsible OAuthInstructions with backend-specific authorize command, then credential field below"
why_human: "Collapse/expand behavior and command text display require human visual check"
- test: "Protocol backends (FTP, WebDAV, SMB, HTTP, Seafile) Step 2 forms"
expected: "Each renders correct fields with appropriate labels; FTP shows TLS mode select; WebDAV shows vendor select; SMB shows domain/port optional fields; HTTP shows only URL field"
why_human: "Field ordering, label accuracy, and select option content require human review"
- test: "Dark mode compatibility for all new components"
expected: "OAuthInstructions, GdriveAuthToggle, BackendCard with icons all render correctly in dark mode using MD3 tokens"
why_human: "Dark mode visual appearance with fill=currentColor SVGs cannot be verified programmatically"
- test: "End-to-end flow: select new backend, fill form, proceed to ReviewStep"
expected: "Selecting gdrive, entering token, advancing to ReviewStep shows correct rclone.conf with type = drive"
why_human: "Multi-step form interaction and final config output display require manual walkthrough"
---
# Phase 13: Add Remaining Rclone Remotes — Verification Report
**Phase Goal:** Add all remaining rclone remote types to the wizard — expand from 8 to 18 backends with categorized selection, proper auth handling per backend type, and full test coverage.
**Verified:** 2026-04-01T17:30:00Z
**Status:** human_needed (all automated checks passed; 6 visual/interaction items need human sign-off)
**Re-verification:** No — initial verification
---
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|---------|
| 1 | BACKEND_REGISTRY contains all 18 backends with correct fields, displayName, description, and category | VERIFIED | registry.ts lines 31638; 18 keys confirmed via node -e count; grep shows all 18 expected keys |
| 2 | BackendType is derived from `keyof typeof BACKEND_REGISTRY`, not an explicit union | VERIFIED | registry.ts line 642: `export type BackendType = keyof typeof BACKEND_REGISTRY;` |
| 3 | BACKEND_SCHEMAS auto-generates from registry keys (no manual per-backend calls) | VERIFIED | index.ts line 3335: `Object.fromEntries((Object.keys(BACKEND_REGISTRY) as BackendType[]).map(...))` |
| 4 | RCLONE_TYPE_MAP has correct type strings for all 18 backends | VERIFIED | rclone-conf.ts lines 1231; 18 entries including gdrive→drive, gcs→"google cloud storage", s3-compatible→s3 |
| 5 | BackendSelectionStep groups backends by category with search filtering | VERIFIED | BackendSelectionStep.tsx: CATEGORY_ORDER, CATEGORY_LABELS, matchesSearch, category sections; 6 test cases pass |
| 6 | Backend cards show inline SVG icons with no external dependency | VERIFIED | BackendIcons.tsx: 18 inline SVG components all using fill="currentColor"; exported as Partial<Record<BackendType,...>> |
| 7 | OAuthInstructions renders collapsed by default, expands to show backend-specific authorize command | VERIFIED | OAuthInstructions.tsx: useState(false); 4 tests pass including collapsed-by-default and expand-on-click |
| 8 | GdriveAuthToggle provides OAuth Token and Service Account tab switching | VERIFIED | GdriveAuthToggle.tsx: two-tab segmented control; both fields always registered via CSS hidden/block; OAuthInstructions embedded in OAuth tab |
| 9 | RemoteConfigStep renders correct form for every new backend | VERIFIED | RemoteConfigStep.tsx: renderBackendFields() switch with gdrive, onedrive, dropbox, box, pcloud branches + generic default; 6 new tests pass |
| 10 | backendLabel derived from registry (no manual duplication) | VERIFIED | RemoteConfigStep.tsx line 2022: `Object.fromEntries(Object.entries(BACKEND_REGISTRY).map(...))` |
| 11 | buildRcloneConf produces valid rclone.conf output for all 18 backends | VERIFIED | RCLONE_TYPE_MAP exported; rclone-conf.test.ts includes exhaustiveness test + gdrive/ftp/smb snapshot tests; 282/282 tests pass |
| 12 | All existing tests continue to pass (no regressions) | VERIFIED | Full suite: 282 tests in 17 files — 282 passed, 0 failed |
| 13 | TypeScript compiles without errors | VERIFIED | `npx tsc --noEmit` exits with no output (clean) |
| 14 | BackendCard accepts optional icon prop and renders it in flex row with backend name | VERIFIED | BackendCard.tsx: `icon?: ReactNode` prop; conditional icon+name flex row vs plain name span |
**Score:** 14/14 truths verified (automated)
---
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `src/schemas/registry.ts` | 18 backend entries with category field; BackendType via keyof | VERIFIED | 638 lines; 18 entries; `as const`; `BackendType = keyof typeof BACKEND_REGISTRY` |
| `src/schemas/index.ts` | Auto-generated BACKEND_SCHEMAS | VERIFIED | Object.fromEntries pattern; no manual buildZodSchema() calls |
| `src/generators/rclone-conf.ts` | RCLONE_TYPE_MAP for all 18 backends | VERIFIED | 18 entries exported; exported const for test access |
| `src/components/wizard/OAuthInstructions.tsx` | Collapsible OAuth guide | VERIFIED | 69 lines; useState(false); expand/collapse; authorize command in code block |
| `src/components/wizard/OAuthInstructions.test.tsx` | 4 behavior tests | VERIFIED | 58 lines; 4 tests all passing |
| `src/components/wizard/GdriveAuthToggle.tsx` | Dual-auth toggle for Google Drive | VERIFIED | 80 lines; two-tab segmented control; OAuthInstructions embedded; always-registered CSS pattern |
| `src/components/icons/BackendIcons.tsx` | Inline SVG icon map for all 18 backends | VERIFIED | 192 lines; 18 SVG components; fill="currentColor"; Partial<Record<BackendType,...>> |
| `src/components/wizard/BackendSelectionStep.tsx` | Category-grouped searchable backend selection | VERIFIED | 142 lines; CATEGORY_ORDER; matchesSearch; searchQuery state; BACKEND_ICONS integration |
| `src/components/ui/BackendCard.tsx` | Backend card with optional icon prop | VERIFIED | icon?: ReactNode; conditional flex-row rendering |
| `src/components/wizard/BackendSelectionStep.test.tsx` | 6 new tests for search/category | VERIFIED | 219 lines; 6 REMOTE-03 tests in dedicated describe block, all passing |
| `src/components/wizard/RemoteConfigStep.tsx` | Form rendering for all 18 backends | VERIFIED | 252 lines; renderBackendFields() switch; GdriveAuthToggle + OAuthInstructions wired |
| `src/components/wizard/RemoteConfigStep.test.tsx` | 6 new tests for new backend rendering | VERIFIED | 506 lines; "New backend form rendering (Plan 13-04)" describe block; 6 tests passing |
---
### Key Link Verification
| From | To | Via | Status | Details |
|------|-----|-----|--------|---------|
| `src/schemas/registry.ts` | `src/store/types.ts` | BackendType re-export | WIRED | store/types.ts: `import type { BackendType } from '../schemas/registry'`; `export type { BackendType }` |
| `src/schemas/registry.ts` | `src/schemas/index.ts` | BACKEND_SCHEMAS auto-generation | WIRED | `Object.keys(BACKEND_REGISTRY)` in index.ts line 34 |
| `src/components/wizard/GdriveAuthToggle.tsx` | `src/components/wizard/OAuthInstructions.tsx` | import and render | WIRED | GdriveAuthToggle.tsx line 4: import; line 52: `<OAuthInstructions backendName="Google Drive" authorizeCommand='rclone authorize "drive"' />` |
| `src/components/icons/BackendIcons.tsx` | `src/schemas/registry.ts` | BackendType key mapping | WIRED | BackendIcons.tsx line 6: `import type { BackendType }` ; line 173: `Partial<Record<BackendType, React.FC<IconProps>>>` |
| `src/components/wizard/BackendSelectionStep.tsx` | `src/schemas/registry.ts` | BACKEND_REGISTRY iteration with category grouping | WIRED | Line 8: `import { BACKEND_REGISTRY }`; lines 110113: `.filter(([, e]) => e.category === cat)` |
| `src/components/wizard/BackendSelectionStep.tsx` | `src/components/icons/BackendIcons.tsx` | BACKEND_ICONS import for card rendering | WIRED | Line 13: `import { BACKEND_ICONS }`; line 126: `BACKEND_ICONS[type as BackendType]` |
| `src/components/wizard/RemoteConfigStep.tsx` | `src/components/wizard/GdriveAuthToggle.tsx` | import and render for gdrive backend | WIRED | Line 15: import; lines 7895: `<GdriveAuthToggle ...>` in gdrive case |
| `src/components/wizard/RemoteConfigStep.tsx` | `src/components/wizard/OAuthInstructions.tsx` | import and render for OAuth backends | WIRED | Line 14: import; rendered in onedrive, dropbox, box, pcloud cases |
| `src/components/wizard/RemoteConfigStep.tsx` | `src/schemas/registry.ts` | BACKEND_REGISTRY field lookup | WIRED | Line 8: import; lines 3445, 5473, 8595, 105113 etc: `BACKEND_REGISTRY[backendType].fields` |
---
### Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|------------|-------------|--------|---------|
| REMOTE-01 | 13-01 | BACKEND_REGISTRY contains all 18 backends with correct fields, displayName, description, and category | SATISFIED | 18 entries in registry.ts; all with category field; 48 registry tests pass |
| REMOTE-02 | 13-01 | BackendType derived from `keyof typeof BACKEND_REGISTRY` | SATISFIED | registry.ts line 642; no explicit union anywhere |
| REMOTE-03 | 13-03 | BackendSelectionStep with category grouping, instant search, inline SVG icons | SATISFIED | BackendSelectionStep.tsx with CATEGORY_ORDER + matchesSearch + BACKEND_ICONS; 6 dedicated tests pass |
| REMOTE-04 | 13-02 | OAuth-token backends show collapsible OAuthInstructions with backend-specific authorize command | SATISFIED | OAuthInstructions.tsx implemented; wired in RemoteConfigStep for onedrive, dropbox, box, pcloud; in GdriveAuthToggle for gdrive; tests confirm |
| REMOTE-05 | 13-04 | RemoteConfigStep renders correct form for all backends including GdriveAuthToggle for Google Drive | SATISFIED | renderBackendFields() switch covers all 18; 6 tests including gdrive/dropbox/ftp/webdav/smb/http |
| REMOTE-06 | 13-01 | buildRcloneConf produces valid rclone.conf with correct type strings for all 18 backends | SATISFIED | RCLONE_TYPE_MAP has 18 entries; exhaustiveness test + snapshot tests in rclone-conf.test.ts |
**Note on REQUIREMENTS.md traceability table:** The table at the bottom of REQUIREMENTS.md shows REMOTE-03, REMOTE-04, REMOTE-05 with status "Planned". This is stale — all four plan summaries and the code confirm these requirements are implemented and tested. The traceability table was not updated after plans 03 and 04 completed. This is a documentation inconsistency, not a code gap.
---
### Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| None found | — | — | — | — |
No TODO/FIXME/placeholder comments found in new files. No empty implementations or stub returns. No console.log-only handlers. All new components are substantive (58192 lines each with real logic).
---
### Human Verification Required
#### 1. Categorized Backend Selection — Visual Layout
**Test:** Run `npm run dev`, open http://localhost:5173, click "Get started" to reach Step 1.
**Expected:** Three distinct section headings — "Cloud Object Storage" (7 backends), "Cloud Drives" (5 backends), "Protocol-based" (6 backends) — all 18 backend cards visible with recognizable inline SVG icons, search bar present above the grid.
**Why human:** Visual layout, icon rendering quality at 24px, and spacing cannot be verified programmatically.
#### 2. Search Filtering — Interaction Check
**Test:** Type "ftp" in the search bar on Step 1.
**Expected:** Only "FTP" and "SFTP" cards remain visible; "Cloud Object Storage" and "Cloud Drives" headings disappear; "Protocol-based" heading remains. Clear the search — all 18 backends reappear.
**Why human:** Real DOM interaction and visual transition of disappearing categories.
#### 3. Google Drive — Dual Auth Toggle
**Test:** Select "Google Drive" and proceed to Step 2.
**Expected:** Segmented control shows "OAuth Token" | "Service Account" tabs. OAuth tab active by default, showing collapsed "How to get your Google Drive OAuth token" disclosure and a token password field. Click disclosure — numbered steps expand with `rclone authorize "drive"` in a code block. Click "Service Account" tab — service account credentials field appears, token field disappears.
**Why human:** Tab switching interaction, collapsible animation, code block rendering.
#### 4. OAuth Backends — Instructions Presence
**Test:** From Step 1, select Dropbox, then Box, then pCloud, then OneDrive (each time going back). In Step 2 for each:
**Expected:** Each shows a collapsed "How to get your [Backend] OAuth token" disclosure button above the token field. The backend name and authorize command are backend-specific (dropbox→`rclone authorize "dropbox"`, box→`rclone authorize "box"`, etc.).
**Why human:** Command text and backend name accuracy require human reading.
#### 5. Protocol Backends — Field Correctness
**Test:** Select FTP, WebDAV, SMB, HTTP, Seafile in turn and check Step 2.
**Expected:** FTP shows host (required), username, password, port, TLS Mode select. WebDAV shows WebDAV URL (required), username (required), password (required), Vendor select. SMB shows host (required), username (required), password, domain, port. HTTP shows only URL field. Seafile shows server URL, email, password.
**Why human:** Field label accuracy, required/optional marking, and select option labels.
#### 6. Dark Mode Compatibility
**Test:** Toggle to dark mode (if theme switcher is present). Navigate through Step 1 and Step 2 for a few backends.
**Expected:** All new components (OAuthInstructions, GdriveAuthToggle, BackendCard with icons) render with readable contrast. SVG icons inherit foreground color (fill=currentColor), adapting to dark mode.
**Why human:** Color contrast and fill=currentColor behavior in dark mode cannot be verified programmatically.
---
## Gaps Summary
No automated gaps found. All 14 observable truths are verified, all artifacts pass all three levels (exists, substantive, wired), and all key links are confirmed present and used.
The 6 human verification items are standard visual/interaction checks that automated tests cannot cover — they are not blockers for code completeness, only for user-experience sign-off.
**One documentation inconsistency noted:** REQUIREMENTS.md traceability table still shows REMOTE-03, REMOTE-04, REMOTE-05 as "Planned" instead of "Complete". The actual implementation and 282 passing tests confirm all six REMOTE requirements are satisfied. The traceability table should be updated to reflect "Complete" for all six.
---
_Verified: 2026-04-01T17:30:00Z_
_Verifier: Claude (gsd-verifier)_
+482 -329
View File
@@ -1,440 +1,593 @@
# Architecture Patterns
**Domain:** Pure frontend multi-step configuration wizard (static site, no backend)
**Project:** Ready2Blob
**Researched:** 2026-03-26
**Confidence:** MEDIUM — rclone.conf format and Intune deployment patterns verified from training data (stable, well-documented domains); client-side download patterns are stable browser APIs; web research unavailable for cross-validation
---
**Domain:** UI polish overhaul -- Material Design 3 theming, dark mode, and component refactoring for existing Tailwind v4 + React wizard app
**Researched:** 2026-03-31
## Recommended Architecture
### High-Level System Diagram
### Overview
The architecture centers on a **CSS custom properties layer** that bridges Material Design 3 color tokens with Tailwind v4's `@theme` directive. A thin React `ThemeProvider` context manages dark/light state and persists preference to `localStorage`. Components are refactored **bottom-up** from primitives (inputs, buttons, cards) to composed wizard steps, using semantic token names in Tailwind classes rather than hardcoded colors.
```
┌─────────────────────────────────────────────────────────────────┐
│ Browser (Static App) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ Wizard UI │───▶│ Wizard State│───▶│ Config Builders │ │
│ │ (Steps/Nav) │ │ (Form Data) │ │ (rclone.conf + │ │
│ └──────────────┘ └──────────────┘ │ PS Scripts) │ │
│ └────────┬───────────┘ │
│ │ │
│ ┌────────▼───────────┐ │
│ │ Download Manager │ │
│ │ (individual files │ │
│ │ or ZIP bundle) │ │
│ └────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
(file download only)
User's disk
+---------------------+
| ThemeProvider | React Context
| (dark/light state) | localStorage + prefers-color-scheme
+---------------------+
|
sets .dark on <html>
|
+---------------------+
| index.css | CSS custom properties layer
| @theme { tokens } | MD3 color roles as --color-*
| .dark { overrides }| Dark palette overrides
+---------------------+
|
+---------------------+
| Tailwind v4 | Consumes tokens via @theme
| bg-surface | Semantic utility classes
| text-on-surface |
+---------------------+
|
+---------------+---------------+
| | |
+----------+ +-----------+ +------------+
| ui/ | | wizard/ | | layout/ |
| Button | | Steps | | AppShell |
| Input | | Indicator | | ThemeToggle|
| Card | | AuthToggles| +------------+
+----------+ +-----------+
```
No network requests leave the browser. All state is ephemeral (in-memory for the session lifetime).
### Component Boundaries
---
| Component | Responsibility | Communicates With | Status |
|-----------|---------------|-------------------|--------|
| `ThemeProvider` | Manages dark/light state, syncs to DOM and localStorage | `<html>` element class, `useTheme` consumers | **NEW** |
| `useTheme` hook | Exposes `{ theme, toggleTheme, setTheme }` | ThemeProvider context | **NEW** |
| `ThemeToggle` | UI control for dark/light switch | useTheme hook | **NEW** |
| `AppShell` | Outer layout wrapper (background, max-width, header) | ThemeProvider, WizardShell | **NEW** (extracted from App.tsx `WizardShell`) |
| `ui/Button` | MD3-styled button with variants (filled, outlined, text) | None (pure presentational) | **NEW** |
| `ui/Input` | MD3-styled text input with label and error state | react-hook-form via register | **NEW** (replaces inline input markup in FieldRenderer) |
| `ui/Select` | MD3-styled select dropdown | react-hook-form via register | **NEW** (replaces inline select markup in FieldRenderer) |
| `ui/Card` | MD3 surface card with elevation | None (pure presentational) | **NEW** |
| `ui/FieldRenderer` | Composes Input/Select/PasswordField based on FieldDef | ui/Input, ui/Select, ui/PasswordField | **MODIFIED** -- delegates to primitives |
| `ui/PasswordField` | Password input with show/hide toggle | react-hook-form | **MODIFIED** -- uses ui/Input internally |
| `ui/BackendCard` | Backend selection card | ui/Card | **MODIFIED** -- uses ui/Card internally |
| `wizard/StepIndicator` | Breadcrumb navigation | useWizard | **MODIFIED** -- inline styles to Tailwind + MD3 tokens |
| `wizard/*Step` | Step content | useWizard, ui components | **MODIFIED** -- swap hardcoded colors for semantic tokens |
## Component Boundaries
### Data Flow
| Component | Responsibility | Inputs | Outputs | Communicates With |
|-----------|---------------|--------|---------|-------------------|
| **Wizard UI** | Render step forms, handle navigation (next/back/jump), validate per-step | User interactions | Step completion events | Wizard State |
| **Wizard State** | Single source of truth for all collected form data; tracks current step and completion status | Step form submissions | Reactive state object | Wizard UI (reads), Config Builders (reads) |
| **Backend Schema Registry** | Defines the fields required per rclone backend type (Azure Blob, S3, OneDrive, etc.) | Backend type selection | Field definitions for each step | Wizard UI (drives dynamic form rendering) |
| **rclone.conf Builder** | Transforms wizard state into a valid rclone.conf string | Wizard State snapshot | `rclone.conf` string | Download Manager |
| **PowerShell Script Builder** | Generates PS scripts (Intune Win32 or RMM) from wizard state + optional rclone install flag | Wizard State snapshot, script type selection | `.ps1` string(s) | Download Manager |
| **Download Manager** | Packages one or more text files and triggers browser download (individual or ZIP) | File content strings + filenames | Browser file download | rclone.conf Builder, PS Script Builder |
**Theme state flow:**
---
1. On app mount, `ThemeProvider` reads `localStorage.getItem('r2b-theme')`
2. If no stored preference, checks `window.matchMedia('(prefers-color-scheme: dark)').matches`
3. Sets/removes `.dark` class on `document.documentElement`
4. Tailwind's `@custom-variant dark` activates `dark:` prefix utilities
5. CSS custom properties in `.dark` scope override light palette values
6. All components using semantic tokens (`bg-surface`, `text-on-surface`) update automatically
## Data Flow
**Color token flow:**
```
User fills wizard step N
Wizard UI validates step N inputs
Wizard State updated (merge step N data into central store)
User reaches Review/Download step
├──▶ rclone.conf Builder
│ reads: [remote_name, backend_type, ...backend-specific fields]
│ produces: rclone.conf string
├──▶ PowerShell Script Builder (Intune)
│ reads: [remote_name, mount_path, include_install_flag, install_source_url]
│ produces: deploy-intune.ps1 string
└──▶ PowerShell Script Builder (RMM)
reads: [remote_name, mount_path, include_install_flag]
produces: deploy-rmm.ps1 string
1. `index.css` defines MD3 color tokens as CSS custom properties in `@theme`
2. Light values are defaults; `.dark` class overrides with dark palette values
3. Tailwind v4 maps these to utility classes (e.g., `--color-surface` becomes `bg-surface`)
4. Components reference semantic names, never raw hex values
All strings → Download Manager
User selects files → individual download (Blob URL) or ZIP (JSZip)
```
## CSS Custom Properties Strategy for MD3 Color Tokens
Key invariant: builders are pure functions — same wizard state always produces the same file content. There is no side-effectful build step.
### Token Definition in index.css
---
This is the core architectural decision. Tailwind v4 uses `@theme` to define custom theme values consumed as utility classes. MD3 color roles map directly to CSS custom properties.
## rclone.conf Format (INI-like)
```css
@import "tailwindcss";
**Confidence: HIGH** — rclone.conf format is stable and well-documented.
/* Enable class-based dark mode toggle */
@custom-variant dark (&:where(.dark, .dark *));
The rclone config file uses a simple INI-like format. Each remote is a named section.
/* --- MD3 Color Tokens as Tailwind v4 theme --- */
@theme {
/* Primary */
--color-primary: #1a73e8;
--color-on-primary: #ffffff;
--color-primary-container: #d3e3fd;
--color-on-primary-container: #041e49;
```ini
[remote-name]
type = azureblob
account = mystorageaccount
key = base64encodedaccesskey==
```
/* Secondary */
--color-secondary: #5f6368;
--color-on-secondary: #ffffff;
--color-secondary-container: #e8eaed;
--color-on-secondary-container: #1f1f1f;
### Structure Rules
/* Tertiary (accent) */
--color-tertiary: #1a73e8;
--color-on-tertiary: #ffffff;
--color-tertiary-container: #d3e3fd;
--color-on-tertiary-container: #041e49;
- Section header: `[remote-name]` — any identifier the user chooses; appears in rclone commands as `remote-name:`
- Each key-value pair on its own line: `key = value` (spaces around `=` are conventional but optional)
- No quoting of values needed (rclone parses raw strings)
- Comments: lines starting with `#` or `;`
- Multiple remotes = multiple sections in the same file
/* Error */
--color-error: #dc3545;
--color-on-error: #ffffff;
--color-error-container: #f9dedc;
--color-on-error-container: #410e0b;
### Required Fields Per Backend
/* Surface & Background */
--color-surface: #ffffff;
--color-surface-dim: #f1f3f4;
--color-surface-container: #f8f9fa;
--color-surface-container-low: #f1f3f4;
--color-surface-container-high: #e8eaed;
--color-on-surface: #1f1f1f;
--color-on-surface-variant: #5f6368;
| Backend | `type` value | Minimum required fields | Common optional fields |
|---------|-------------|------------------------|------------------------|
| Azure Blob | `azureblob` | `account`, then one of: `key`, `sas_url`, or `client_id`+`client_secret`+`tenant` | `endpoint`, `chunk_size`, `upload_cutoff` |
| AWS S3 | `s3` | `provider = AWS`, `access_key_id`, `secret_access_key`, `region` | `storage_class`, `server_side_encryption` |
| S3-compatible | `s3` | `provider = Other`, `access_key_id`, `secret_access_key`, `endpoint` | varies by provider |
| OneDrive | `onedrive` | `client_id`, `client_secret`, `token` (OAuth flow) | `drive_id`, `drive_type` |
| SFTP | `sftp` | `host`, `user`, then one of `pass` or `key_file` | `port`, `use_insecure_cipher` |
| Google Drive | `drive` | `client_id`, `client_secret`, `token` (OAuth flow) | `team_drive`, `shared_with_me` |
/* Outline */
--color-outline: #dadce0;
--color-outline-variant: #e8eaed;
### Example Full Config (Azure Blob + S3)
/* MD3 Elevation (shadow) */
--shadow-elevation-1: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-elevation-2: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-elevation-3: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
```ini
[my-azure]
type = azureblob
account = contosostorage
key = dGhpcyBpcyBhIHBsYWNlaG9sZGVyIGtleQ==
/* MD3 Shape (border-radius) */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 28px;
}
[my-s3]
type = s3
provider = AWS
access_key_id = AKIAIOSFODNN7EXAMPLE
secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
region = us-east-1
```
/* --- Dark mode overrides --- */
@layer base {
.dark {
--color-primary: #a8c7fa;
--color-on-primary: #062e6f;
--color-primary-container: #0842a0;
--color-on-primary-container: #d3e3fd;
### Generation Pattern
--color-secondary: #c4c7c5;
--color-on-secondary: #303030;
--color-secondary-container: #444746;
--color-on-secondary-container: #e8eaed;
The rclone.conf builder is pure string templating:
--color-tertiary: #a8c7fa;
--color-on-tertiary: #062e6f;
--color-tertiary-container: #0842a0;
--color-on-tertiary-container: #d3e3fd;
```typescript
function buildRcloneConf(remotes: RemoteConfig[]): string {
return remotes.map(remote => {
const lines = [`[${remote.name}]`, `type = ${remote.type}`];
for (const [key, value] of Object.entries(remote.params)) {
if (value !== undefined && value !== '') {
lines.push(`${key} = ${value}`);
}
}
return lines.join('\n');
}).join('\n\n');
--color-error: #f2b8b5;
--color-on-error: #601410;
--color-error-container: #8c1d18;
--color-on-error-container: #f9dedc;
--color-surface: #1f1f1f;
--color-surface-dim: #141414;
--color-surface-container: #2d2d2d;
--color-surface-container-low: #262626;
--color-surface-container-high: #3c3c3c;
--color-on-surface: #e8eaed;
--color-on-surface-variant: #c4c7c5;
--color-outline: #5f6368;
--color-outline-variant: #444746;
--shadow-elevation-1: 0 1px 3px 0 rgb(0 0 0 / 0.3);
--shadow-elevation-2: 0 2px 6px 0 rgb(0 0 0 / 0.3);
--shadow-elevation-3: 0 4px 8px 0 rgb(0 0 0 / 0.3);
}
}
```
No library needed — plain string concatenation is the correct approach.
### Why This Approach (Not a Component Library)
---
1. **No dependency on MD3 web components** -- `@material/web` is Angular/Lit-oriented and would fight React + Tailwind. The MD3 spec is a *design system*, not a library requirement.
2. **Tailwind v4 @theme is the native mechanism** -- tokens defined in `@theme` become first-class Tailwind utilities (`bg-surface`, `text-on-primary`, `shadow-elevation-2`). No plugins needed.
3. **Dark mode is a CSS variable swap** -- the `.dark` class override block is all that's needed. No JS re-rendering, no theme prop drilling to every component.
4. **Accent color extensibility** -- to support user-selectable accent colors later, just override `--color-primary` and related tokens at runtime via JS on `document.documentElement.style`.
## Intune / RMM Deployment Patterns
### MD3 Color Roles Used (Practical Subset)
**Confidence: MEDIUM** — Intune Win32 app deployment is well-established; exact script conventions vary by org.
Full MD3 has 29+ color roles. For this wizard app, we use a practical subset:
### Intune Win32 App Approach
| MD3 Role | Tailwind Class | Used For |
|----------|---------------|----------|
| `surface` | `bg-surface` | Page background, card backgrounds |
| `on-surface` | `text-on-surface` | Primary text |
| `on-surface-variant` | `text-on-surface-variant` | Secondary text, help text |
| `surface-dim` | `bg-surface-dim` | Page background (current `bg-gray-50`) |
| `surface-container` | `bg-surface-container` | Card fills, input backgrounds |
| `surface-container-high` | `bg-surface-container-high` | Elevated cards, active states |
| `primary` | `bg-primary`, `text-primary` | Primary buttons, active indicators |
| `on-primary` | `text-on-primary` | Text on primary buttons |
| `primary-container` | `bg-primary-container` | Selected BackendCard fill |
| `on-primary-container` | `text-on-primary-container` | Text on selected BackendCard |
| `error` | `text-error` | Validation error text |
| `error-container` | `bg-error-container` | Error badge backgrounds |
| `outline` | `border-outline` | Input borders, dividers |
| `outline-variant` | `border-outline-variant` | Subtle borders |
Intune Win32 apps require a `.intunewin` package, but PowerShell-only deployments (Intune PowerShell scripts feature) are simpler and sufficient for this use case.
### Current-to-Token Mapping
The generated script must:
Explicit mapping of every hardcoded color in the existing codebase:
1. (Optionally) download and install rclone — copy `rclone.exe` to a stable path (e.g., `C:\ProgramData\rclone\`)
2. Write `rclone.conf` to the user-appropriate path (`$env:APPDATA\rclone\rclone.conf` for per-user, or `C:\ProgramData\rclone\rclone.conf` for system-wide)
3. (Optionally) create a scheduled task or a startup script to mount the remote on login
4. Return exit code 0 on success; non-zero on failure (Intune reads exit codes)
| Current Class | Semantic Token | Where Used |
|--------------|---------------|------------|
| `bg-gray-50` | `bg-surface-dim` | App.tsx page background |
| `text-gray-900` | `text-on-surface` | App.tsx heading, BackendCard name |
| `text-gray-500` | `text-on-surface-variant` | BackendCard description, help text |
| `bg-white` | `bg-surface` | BackendCard default, card backgrounds |
| `border-gray-200` | `border-outline-variant` | BackendCard default border |
| `border-gray-300` | `border-outline` | Input borders, Back button border |
| `hover:border-blue-400` | `hover:border-primary/60` | BackendCard hover |
| `hover:bg-gray-50` | `hover:bg-surface-container` | BackendCard hover, Back button hover |
| `border-blue-600` | `border-primary` | BackendCard selected border |
| `bg-blue-50` | `bg-primary-container` | BackendCard selected fill, tooltip bg |
| `bg-blue-600` | `bg-primary` | Next/primary buttons |
| `text-white` (on blue bg) | `text-on-primary` | Primary button text |
| `hover:bg-blue-700` | `hover:bg-primary/90` | Primary button hover |
| `text-red-500` | `text-error` | Required field asterisk |
| `text-red-600` | `text-error` | Error message text |
| `border-red-500` | `border-error` | Error input border |
| `focus:ring-blue-300` | `focus:ring-primary/40` | Input focus ring |
| `focus:ring-red-300` | `focus:ring-error/40` | Error input focus ring |
| `text-blue-500` | `text-primary` | Tooltip toggle button |
| `text-blue-700` | `text-on-primary-container` | Tooltip text |
| `border-blue-200` | `border-primary-container` | Tooltip border |
| `text-gray-400` | `text-on-surface-variant` | Password show/hide button |
| `hover:text-gray-700` | `hover:text-on-surface` | Password show/hide hover |
| `#999` (inline) | `text-on-surface-variant` | StepIndicator future steps |
| `fontWeight: bold` (inline) | `font-bold` | StepIndicator active step |
| `fontWeight: normal` (inline) | `font-normal` | StepIndicator completed step |
### Intune Script Deployment Constraints
## ThemeProvider Architecture
- Scripts run as SYSTEM by default, or as logged-on user (configurable)
- Script must handle the case where rclone is already installed (idempotent)
- 64-bit PowerShell is required for `rclone.exe` (32-bit PS cannot run 64-bit binaries reliably)
- Scripts have a 30-minute execution timeout in Intune
- Output directory for config must account for execution context (SYSTEM vs user)
### Implementation
### RMM Script Approach (NinjaRMM, Datto, etc.)
```typescript
// src/store/theme-context.tsx
import { createContext, useContext, useEffect, useState, useCallback } from 'react';
Simpler than Intune Win32: paste PS script, run as SYSTEM or user. Same functional requirements as above but no `.intunewin` packaging. Script should be self-contained.
type Theme = 'light' | 'dark';
### PowerShell Script Structure
interface ThemeContextValue {
theme: Theme;
toggleTheme: () => void;
setTheme: (theme: Theme) => void;
}
```powershell
#Requires -RunAsAdministrator # or omit if running as user context
const ThemeContext = createContext<ThemeContextValue | null>(null);
$rclonePath = "C:\ProgramData\rclone"
$rcloneExe = "$rclonePath\rclone.exe"
$rcloneConf = "$rclonePath\rclone.conf"
function getInitialTheme(): Theme {
if (typeof window === 'undefined') return 'light';
const stored = localStorage.getItem('r2b-theme') as Theme | null;
if (stored === 'light' || stored === 'dark') return stored;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
# --- Optional: Install rclone ---
# if ($InstallRclone) { ... download from $RcloneDownloadUrl ... }
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<Theme>(getInitialTheme);
# --- Write config ---
$confContent = @"
[remote-name]
type = azureblob
account = REPLACE_ME
key = REPLACE_ME
"@
const setTheme = useCallback((t: Theme) => {
setThemeState(t);
localStorage.setItem('r2b-theme', t);
document.documentElement.classList.toggle('dark', t === 'dark');
}, []);
New-Item -ItemType Directory -Force -Path $rclonePath | Out-Null
Set-Content -Path $rcloneConf -Value $confContent -Encoding UTF8
const toggleTheme = useCallback(() => {
setTheme(theme === 'dark' ? 'light' : 'dark');
}, [theme, setTheme]);
# --- Optional: Register mount as scheduled task ---
# ...
// Sync on mount
useEffect(() => {
document.documentElement.classList.toggle('dark', theme === 'dark');
}, []);
exit 0
return (
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme(): ThemeContextValue {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used inside <ThemeProvider>');
return ctx;
}
```
The PS script builder generates this structure by substituting values from wizard state into a template string.
### Provider Nesting in App.tsx
---
```typescript
// ThemeProvider wraps WizardProvider -- theme is app-global, wizard state is feature-scoped
export default function App() {
return (
<ThemeProvider>
<WizardProvider>
<AppShell />
</WizardProvider>
</ThemeProvider>
);
}
```
**Rationale:** ThemeProvider is outermost because theme affects the entire DOM tree. WizardProvider is inner because it only governs wizard state. They are independent -- no cross-dependencies.
## Component Refactoring Approach: Bottom-Up Primitives
### Why Bottom-Up (Not Top-Down)
1. **Primitives are the reuse boundary** -- Input, Button, Card are used by multiple wizard steps. Fix once, propagate everywhere.
2. **Tests target behavior, not styles** -- existing 159 tests use Testing Library (query by role, text, label). Changing CSS classes does not break tests. Changing component structure (splitting FieldRenderer) could break tests if DOM hierarchy changes.
3. **Incremental migration** -- each primitive can be built, tested, and swapped in isolation. No big-bang rewrite.
### Refactoring Layers
```
Layer 1: CSS Foundation (index.css)
- MD3 tokens in @theme
- Dark mode @custom-variant + .dark overrides
- Zero component changes needed
- Zero test impact
Layer 2: Primitive Components (ui/)
- NEW: Button, Input, Select, Card
- Tests: New tests for new components only
Layer 3: Composed Components (ui/)
- MODIFIED: FieldRenderer -- delegates to Input/Select/PasswordField
- MODIFIED: BackendCard -- delegates to Card
- MODIFIED: PasswordField -- uses Input primitive internally
- Tests: Existing tests should pass (same DOM semantics, different styling)
Layer 4: Wizard Steps + StepIndicator (wizard/)
- MODIFIED: Replace hardcoded Tailwind classes with semantic tokens
- MODIFIED: StepIndicator -- inline styles to Tailwind classes
- Tests: Existing tests should pass (no behavior change)
Layer 5: Layout Shell + Polish
- NEW: AppShell (extracted from WizardShell in App.tsx)
- NEW: ThemeToggle
- MODIFIED: App.tsx -- adds ThemeProvider, uses AppShell
- Tests: App.test.tsx needs update for new provider wrapping
```
### Migration Pattern Per Component
For each existing component, the migration follows this pattern:
1. **Replace hardcoded colors with semantic tokens** using the Current-to-Token Mapping table above
2. **Add `dark:` variants only where semantic tokens alone are insufficient** (should be rare -- the CSS variable swap handles most cases automatically)
3. **Replace inline styles with Tailwind classes** (StepIndicator specific)
4. **Run existing tests after each component** -- they should pass unchanged
## Patterns to Follow
### Pattern 1: Centralized Wizard State (Single Store)
### Pattern 1: Semantic Color Tokens Only
**What:** All form data lives in one top-level state object, passed down or accessed via context/store. Steps read from and write to slices of this store.
**What:** Never use raw Tailwind color classes (`blue-600`, `gray-50`) in components. Always use semantic MD3 token names.
**When:** Any multi-step form where later steps depend on earlier choices (e.g., backend type selection in step 1 drives which fields appear in step 2).
**When:** Every component, every color reference.
**Why:** Avoids prop-drilling, makes "go back and edit" trivial, makes config builders pure functions with a single well-typed input.
**Why:** Semantic tokens automatically adapt to dark mode via CSS variable override. Raw colors would need manual `dark:` overrides on every single usage.
**Shape:**
```typescript
interface WizardState {
currentStep: number;
remote: {
name: string;
backendType: BackendType;
params: Record<string, string>; // backend-specific key/value pairs
};
deployment: {
includeInstall: boolean;
installSource: 'github' | 'custom';
customInstallUrl?: string;
mountPath?: string;
scriptTargets: ('intune' | 'rmm')[];
};
outputOptions: {
includeConf: boolean;
includeIntune: boolean;
includeRmm: boolean;
bundleAsZip: boolean;
};
**Example:**
```tsx
// WRONG -- requires dark: override on every element
<button className="bg-blue-600 text-white dark:bg-blue-400 dark:text-gray-900">
// RIGHT -- adapts automatically via CSS variable swap
<button className="bg-primary text-on-primary">
```
### Pattern 2: Component Composition Over Monoliths
**What:** Extract reusable UI primitives (Button, Input, Card) from inline markup.
**When:** Any UI element used in 2+ places, or any element with complex styling logic.
**Example:**
```tsx
// BEFORE: Inline button styling in every step component
<button className="px-4 py-2 text-sm bg-primary text-on-primary rounded-md hover:bg-primary/90">
Next
</button>
// AFTER: Reusable Button primitive
<Button variant="filled">Next</Button>
<Button variant="outlined">Back</Button>
<Button variant="text">Cancel</Button>
```
### Pattern 3: Test Helper for Theme Context
**What:** Create a test utility that wraps components in both ThemeProvider and WizardProvider.
**When:** Any component test that renders a component needing theme context.
**Example:**
```tsx
// src/test-utils.tsx
import { render } from '@testing-library/react';
import { ThemeProvider } from './store/theme-context';
import { WizardProvider } from './store/context';
export function renderWithProviders(ui: React.ReactElement) {
return render(
<ThemeProvider>
<WizardProvider>
{ui}
</WizardProvider>
</ThemeProvider>
);
}
```
### Pattern 2: Backend Schema Registry
### Pattern 4: MD3 Elevation via Shadow Tokens
**What:** A static data structure (not code) that defines, per backend type, which fields are required, their labels, input types, placeholder text, and validation rules.
**What:** Use the `shadow-elevation-*` tokens for card depth instead of arbitrary shadow utilities.
**When:** The wizard needs to render dynamic forms based on which rclone backend the user selected.
**When:** Cards, modals, dropdowns -- any elevated surface.
**Why:** Adding support for a new backend means adding one entry to the registry, not writing new UI components. Keeps UI code backend-agnostic.
**Example:**
```tsx
// Card at rest
<div className="bg-surface-container rounded-lg shadow-elevation-1">
**Shape:**
```typescript
interface FieldDef {
key: string; // matches rclone config key exactly
label: string;
inputType: 'text' | 'password' | 'select' | 'toggle';
required: boolean;
placeholder?: string;
helpText?: string;
options?: { value: string; label: string }[]; // for select
}
type BackendSchema = Record<BackendType, FieldDef[]>;
// Card hovered / elevated
<div className="bg-surface-container rounded-lg shadow-elevation-2 hover:shadow-elevation-3">
```
### Pattern 3: Pure Builder Functions
**What:** Config and script builders are pure functions — they receive wizard state and return a string. No side effects, no DOM access, no async.
**When:** Always — this is the core generation logic.
**Why:** Easily testable (unit test: given state X, output matches expected string). Deterministic. Separates concerns cleanly.
### Pattern 4: Blob URL Download
**What:** To trigger a file download in the browser without a server, create a `Blob` from the string content, generate an object URL, attach it to an `<a>` element, and programmatically click it.
**When:** Single file download.
```typescript
function downloadTextFile(filename: string, content: string): void {
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
```
**Confidence: HIGH** — Blob URL download is a standard, well-supported browser API (all modern browsers, no library needed for single files).
### Pattern 5: ZIP Bundle via JSZip
**What:** When the user wants all generated files in one download, use JSZip to assemble a ZIP in-memory and trigger download.
**When:** Multi-file download (rclone.conf + one or more .ps1 files).
```typescript
import JSZip from 'jszip';
async function downloadZip(files: { name: string; content: string }[]): Promise<void> {
const zip = new JSZip();
files.forEach(f => zip.file(f.name, f.content));
const blob = await zip.generateAsync({ type: 'blob' });
downloadTextFile('ready2blob-deployment.zip', URL.createObjectURL(blob));
}
```
**Confidence: HIGH** — JSZip is the established library for client-side ZIP creation. FileSaver.js is an optional companion for older browser compatibility but not required with the Blob URL pattern above.
---
## Anti-Patterns to Avoid
### Anti-Pattern 1: Per-Step Local State
### Anti-Pattern 1: Dual-Track Color System
**What:** Each wizard step manages its own form state with no shared store.
**What:** Keeping some components on raw Tailwind colors while migrating others to MD3 tokens.
**Why bad:** When the user navigates back to step 2 from step 4, their inputs are gone. The config builder cannot access step 2 data from step 4. Breaks the "review before download" pattern.
**Why bad:** Dark mode will be broken on non-migrated components. Visual inconsistency. Harder to maintain.
**Instead:** Lift all state to the wizard root. Steps only control their own UI (focus, error display), not their data.
**Instead:** Migrate ALL color references in one sweep per component. The token names are a 1:1 replacement -- no logic changes needed.
### Anti-Pattern 2: Generating Files Only at Download Time from DOM
### Anti-Pattern 2: JS-Driven Theme Prop Drilling
**What:** Reading form field values directly from the DOM to build the config string at download time.
**What:** Passing `isDark` or `theme` as a prop to every component and using ternaries to pick colors.
**Why bad:** Bypasses validation, couples the builder to the DOM structure, cannot unit-test without a browser. Fragile.
**Why bad:** Massive prop threading, re-renders on theme change, duplicated color logic.
**Instead:** Always read from wizard state, which is the validated, typed representation of user input.
**Instead:** CSS custom properties handle the switch. Components just use `bg-surface` -- the browser resolves the correct value based on `.dark` class presence. Only `ThemeToggle` needs to consume `useTheme`.
### Anti-Pattern 3: Hardcoding Backend Fields in Step Components
### Anti-Pattern 3: Using @material/web Components
**What:** Writing a dedicated `<AzureBlobStep />`, `<S3Step />`, `<OneDriveStep />` component for every backend.
**What:** Importing Google's Material Web Components (`<md-button>`, `<md-text-field>`).
**Why bad:** Adding a new backend requires a new component. Does not scale. Leads to duplication of validation logic.
**Why bad:** These are web components designed for vanilla JS / Lit. They fight React's rendering model, do not integrate with react-hook-form's `register()`, and add significant bundle size. They use Shadow DOM which conflicts with Tailwind's utility approach.
**Instead:** Use the Backend Schema Registry to drive a single generic `<DynamicBackendStep />` that renders fields from schema definitions.
**Instead:** Implement MD3's *visual language* (colors, elevation, shape, typography) via CSS tokens + Tailwind utilities on standard React elements.
### Anti-Pattern 4: Storing Secrets Beyond Session
### Anti-Pattern 4: Big-Bang Step Rewrite
**What:** Persisting wizard state to `localStorage`, `sessionStorage`, or any cache.
**What:** Rewriting entire wizard step components at once with new UI.
**Why bad:** Credentials (storage keys, SAS tokens) would persist on the machine after the browser tab is closed. Security risk explicitly called out in project constraints.
**Why bad:** Breaks multiple tests simultaneously. Hard to isolate regressions. Merge conflicts if concurrent work.
**Instead:** Wizard state lives only in in-memory React/Vue/Svelte state. Closing the tab is the only "logout".
**Instead:** Layer-by-layer approach. CSS tokens first (zero breakage), then primitives (additive), then swap in composed components, then layout.
### Anti-Pattern 5: Using a Backend for File Generation
## Impact on Existing Test Suite (159 Tests)
**What:** Sending form data to a server to generate files, which returns them as downloads.
### Test Categories and Impact Assessment
**Why bad:** Server receives credentials in plaintext. Violates the project's explicit no-backend constraint. Creates data retention risk.
| Test File | Tests (approx) | Impact | Reason |
|-----------|----------------|--------|--------|
| `schemas/index.test.ts` | ~20 | NONE | Tests Zod schemas, no UI |
| `schemas/registry.test.ts` | ~15 | NONE | Tests BACKEND_REGISTRY data, no UI |
| `generators/*.test.ts` (3 files) | ~40 | NONE | Tests script generation, no UI |
| `store/reducer.test.ts` | ~20 | NONE | Tests state reducer, no UI |
| `App.test.tsx` | ~10 | LOW | May need ThemeProvider wrapper if test renders App internals directly |
| `StepIndicator.test.tsx` | ~10 | LOW | Tests click behavior and text content, not styles |
| `BackendSelectionStep.test.tsx` | ~15 | LOW | Tests card selection behavior via role queries |
| `RemoteConfigStep.test.tsx` | ~15 | LOW | Tests form submission and validation |
| `ReviewStep.test.tsx` | ~15 | LOW | Tests output generation, copy/download actions |
**Instead:** All generation is client-side (builders are pure TS/JS functions).
**Key insight:** ~60% of tests (95+ tests across schemas, generators, reducer) are pure logic tests with ZERO UI coupling. They will not be affected at all.
---
**For UI tests:** Testing Library queries by role, label text, and accessible names -- NOT by CSS class names. Swapping `bg-blue-600` to `bg-primary` does not change what `getByRole('button')` or `getByText('Next')` finds. These tests should remain green through the entire migration.
## Suggested Build Order (Component Dependencies)
**One risk area:** If `FieldRenderer` refactoring changes DOM structure (e.g., wrapping inputs in a new primitive component that adds an extra `<div>`), tests that use `container.querySelector` or rely on specific nesting could break. Mitigation: ensure new primitives produce the same semantic DOM (same `<input>`, `<select>`, `<label>` elements with same attributes).
Dependencies flow from foundational to dependent. Build in this order:
## Suggested Build Order (Minimizes Breakage)
```
Phase 1 — Foundation
└── Wizard State shape definition (TypeScript types + store setup)
└── Backend Schema Registry (static data, no UI)
### Phase 1: CSS Foundation (Zero Test Impact)
Phase 2 — Core Generators (no UI needed yet, fully testable)
└── rclone.conf Builder (pure function, unit-testable immediately)
└── PowerShell Script Builder — Intune variant
└── PowerShell Script Builder — RMM variant
└── Download Manager (Blob URL + JSZip wrapper)
**What:** Set up MD3 tokens and dark mode infrastructure in `index.css`. No component changes.
Phase 3 — Wizard UI Shell
└── Step navigation (stepper, next/back, step completion tracking)
└── Wizard State wired to UI (reads/writes)
1. Add `@custom-variant dark (&:where(.dark, .dark *))` to `index.css`
2. Define all MD3 color tokens in `@theme` block
3. Add `.dark` override block in `@layer base`
4. Define elevation shadow tokens and border-radius shape tokens
5. Create `ThemeProvider` + `useTheme` hook (new files, no existing code touched)
6. Wire `ThemeProvider` into `App.tsx` (outermost wrapper)
Phase 4 — Dynamic Step Forms
└── Backend type selector (step 1)
└── Dynamic backend fields step (step 2, driven by Schema Registry)
└── Deployment options step (step 3)
└── Review + Download step (step 4, calls builders + download manager)
**Test impact:** Zero -- existing tests pass unchanged. Add new unit tests for ThemeProvider.
Phase 5 — Polish
└── Per-step validation with user-visible errors
└── Security warning modal before download
└── Preview pane (show generated file content before download)
```
**Dependency:** None. Can start immediately.
**Rationale for this order:**
### Phase 2: Primitive Extraction (Additive Only)
- Builders and the schema registry have zero UI dependencies — build and test them first
- The wizard UI shell (navigation only) can be built against mock/empty state
- Dynamic forms are built last because they depend on both state wiring AND the schema registry being final
- The download step can only be meaningfully built once all builders exist
**What:** Create new ui/ primitives without modifying existing components yet.
---
1. Create `ui/Button` with MD3 variants (filled, outlined, text)
2. Create `ui/Input` with MD3 styling (outline, label, error state, focus ring)
3. Create `ui/Select` with MD3 styling
4. Create `ui/Card` with MD3 elevation and shape
5. Create `ThemeToggle` component
6. Create `AppShell` layout component
**Test impact:** Zero on existing tests. Write new tests for each new component.
**Dependency:** Phase 1 tokens must be in place for correct color references.
### Phase 3: StepIndicator Migration (Low Risk, Isolated)
**What:** Replace inline styles with Tailwind + MD3 tokens. This is flagged tech debt.
1. Replace `style={{ fontWeight: 'bold' }}` with `className="font-bold"`
2. Replace `style={{ color: '#999' }}` with `className="text-on-surface-variant"`
3. Replace `style={{ fontWeight: 'normal' }}` with `className="font-normal"`
4. Apply MD3 shape and color tokens to step indicator layout
**Test impact:** `StepIndicator.test.tsx` tests click behavior and text content. Style changes are invisible to these tests. Should pass unchanged.
**Dependency:** Phase 1 tokens.
### Phase 4: Component Token Migration (Core Migration)
**What:** Replace hardcoded Tailwind colors with semantic MD3 tokens across all components. Integrate new primitives.
1. `BackendCard` -- swap color classes using Current-to-Token Mapping, optionally compose with Card
2. `FieldRenderer` -- delegate rendering to Input/Select primitives (preserve DOM semantics)
3. `PasswordField` -- swap color classes, optionally use Input internally
4. `DeploymentStep` -- swap button and input color classes
5. `ReviewStep` -- swap color classes
6. `RemoteConfigStep` -- swap color classes
7. `BackendSelectionStep` -- swap color classes
**Test impact:** LOW. Color class changes are invisible to Testing Library. If FieldRenderer's DOM structure changes, run tests after each sub-step to catch issues early.
**Dependency:** Phase 1 tokens + Phase 2 primitives.
### Phase 5: Layout Shell and Dark Mode UX
**What:** AppShell integration, ThemeToggle placement, responsive improvements.
1. Extract layout from WizardShell into AppShell component
2. Add ThemeToggle to app header area
3. Wire up dark mode persistence (already in ThemeProvider)
4. Mobile responsiveness passes
5. Update `App.test.tsx` for new structure
**Test impact:** `App.test.tsx` may need structural updates. Other tests unaffected.
**Dependency:** All previous phases.
## Scalability Considerations
| Concern | At MVP (5 backends) | At Growth (20+ backends) | Notes |
|---------|---------------------|--------------------------|-------|
| Backend support | Hardcode schema for top 5 | Schema registry makes adding trivial | Registry pattern is the key enabler |
| Bundle size | Single JS bundle is fine | Consider lazy-loading backend schemas | Each schema is tiny; not a real concern until 50+ backends |
| State complexity | Flat wizard state struct | No change needed | Wizard is inherently linear; state stays simple |
| Testing | Unit tests on builders | Add snapshot tests for generated files | Builders are pure functions — easiest thing to test |
| Localization | Not needed v1 | Field labels/help text in schema enables i18n | Plan label strings as separate keys in schema if i18n is future |
---
## Key Technical Decisions
| Decision | Recommended Choice | Rationale |
|----------|-------------------|-----------|
| State management | React Context + useReducer OR Zustand (lightweight store) | No server state; no need for React Query or Redux. Zustand reduces boilerplate vs Context for this use case. |
| ZIP library | JSZip v3 | De facto standard for browser ZIP. No alternatives with meaningful adoption. |
| File download | Native Blob URL API | No library needed. FileSaver.js unnecessary for modern browsers. |
| Config generation | Plain string templates (template literals) | rclone.conf is simple enough that a template engine adds no value. |
| Script generation | Template literal function per script type | Same reasoning. Mustache/Handlebars would be overkill. |
| Backend schema | Static TypeScript object (no database, no fetch) | Schemas are known at build time. Static data = zero loading time. |
---
| Concern | Current (v1.2) | Future (accent colors) | Future (multi-theme) |
|---------|----------------|----------------------|---------------------|
| Color tokens | ~20 tokens in @theme | Override `--color-primary` family via JS `document.documentElement.style` | Add named theme classes, swap `.theme-blue` / `.theme-green` |
| Theme persistence | localStorage `r2b-theme` key | Add `r2b-accent` key | Add `r2b-theme-name` key |
| Bundle size | +0 KB (CSS only) | +~3KB if using `@material/material-color-utilities` for seed-based palette generation | Same |
| Performance | CSS variable swap (no React re-render for color changes) | One-time JS computation + CSS variable batch update | Same |
| Token generation | Hand-picked values | Use `@material/material-color-utilities` `themeFromSourceColor()` to generate all 29 tokens from one seed hex | Same |
## Sources
- rclone.conf format: training data (stable since rclone v1.x; format has not changed); confidence HIGH
- Azure Blob rclone backend fields: training data; confidence MEDIUM (specific field names should be cross-checked against https://rclone.org/azureblob/ before implementing the schema registry)
- Intune PowerShell script deployment: training data; confidence MEDIUM (execution context and timeout limits are well-documented but should be verified for current Intune behavior)
- JSZip client-side ZIP: training data; confidence HIGH (library API is stable)
- Blob URL download pattern: training data (standard browser API, MDN-documented); confidence HIGH
- Web search and WebFetch unavailable during this research session — claims marked MEDIUM should be verified against official docs during implementation phases
- [Tailwind CSS v4 Dark Mode documentation](https://tailwindcss.com/docs/dark-mode) -- HIGH confidence, official docs
- [Material Design 3 Color Roles](https://m3.material.io/styles/color/roles) -- HIGH confidence, official spec
- [Material Design 3 Design Tokens](https://m3.material.io/foundations/design-tokens) -- HIGH confidence, official spec
- [Material Design 3 Elevation Tokens](https://m3.material.io/styles/elevation/tokens) -- HIGH confidence, official spec
- [Tailwind v4 dark mode @custom-variant discussion](https://github.com/tailwindlabs/tailwindcss/discussions/15083) -- MEDIUM confidence, community verified pattern
- [@material/material-color-utilities npm](https://www.npmjs.com/package/@material/material-color-utilities) -- HIGH confidence, official Google package
- [Generating MD3 Dynamic Color with JavaScript](https://dt.in.th/M3DynamicColorJS) -- MEDIUM confidence, verified implementation walkthrough
- [React dark mode with Context + Tailwind pattern](https://medium.com/@sandeepshome.dev/react-theming-dark-mode-with-context-api-and-tailwindcss-b3ef50a9522b) -- MEDIUM confidence, community pattern
- [Tailwind v4 @theme with dark mode pattern](https://medium.com/@kevstrosky/theme-colors-with-tailwind-css-v4-0-and-next-themes-dark-light-custom-mode-36dca1e20419) -- MEDIUM confidence, community implementation
+178 -102
View File
@@ -1,144 +1,220 @@
# Feature Landscape
# Feature Research
**Domain:** rclone configuration wizard / enterprise deployment helper
**Project:** Ready2Blob
**Researched:** 2026-03-26
**Confidence note:** External research tools (WebSearch, WebFetch, Bash) were unavailable in this session. All findings are from training data (knowledge cutoff August 2025). Confidence levels are assigned conservatively. Recommend validating against live rclone docs and community forums before finalizing.
**Domain:** Material Design 3 UI polish for rclone configuration wizard
**Project:** Ready2Blob v1.2
**Researched:** 2026-03-31
**Confidence:** MEDIUM (MD3 web patterns well-documented; custom Tailwind implementation patterns less established than MUI-based approaches)
---
## Feature Landscape
## Table Stakes
This research covers the UI polish milestone only. The functional wizard (4 steps, 7 backends, validation, ZIP download) is already shipped. The question is: what transforms a bare, functional wizard into a professional, self-explanatory tool?
Features IT admins expect. Missing any of these means the tool gets discarded immediately.
### Table Stakes (Users Expect These)
Features that any polished web form/wizard must have in 2026. Without these, the app looks like a prototype.
| Feature | Why Expected | Complexity | Notes |
|---------|--------------|------------|-------|
| Multi-step backend selection wizard | rclone has 50+ backends; admins need guided selection, not raw docs | Medium | First screen should show backends sorted by popularity: Azure Blob, S3, OneDrive, SFTP, then others |
| Per-backend field forms with labels | Each backend has different required fields (account name vs access key vs OAuth token); forms must match | Medium | Source of truth is rclone's own `rclone config` flow; replicate those fields exactly |
| Valid rclone.conf output | The generated file must be parseable by rclone with no errors | Low | INI-like format: `[remote-name]`, `type = azureblob`, then key=value pairs. Pure string generation |
| Remote name customization | Admins name remotes to match org conventions (e.g., `corp-backup`, `client-files`) | Low | Single text input, validated to allow only rclone-safe characters (alphanumeric, dash, underscore) |
| Intune PowerShell deployment script | Intune Win32 app or PS script deployment is the dominant MDM workflow for Windows | High | Must handle: detection script, install script, optional rclone.exe download, config placement at correct path |
| RMM deployment script | NinjaRMM, Datto RMM, ConnectWise Automate, Syncro — MSP-dominant tools | High | Single PS script that downloads rclone if needed and drops config; simpler than Intune (no detection logic needed) |
| Optional rclone install inclusion | Some orgs already have rclone in their baseline image; others don't | Medium | Checkbox: "Include rclone installation". If checked, script downloads from rclone.org/downloads or GitHub releases |
| Security warning before download | Credentials are in plain text in generated files — legal/compliance exposure if admin doesn't understand | Low | Modal or banner: "This file contains your storage credentials in plain text. Store and transmit securely." Must be impossible to miss |
| Download individual output files | Admin may only need the .conf, or only the script, depending on their environment | Low | Separate download buttons for each generated artifact |
| No data sent to server | IT security teams will ask "where do my credentials go?" — answer must be "nowhere, browser only" | Low | Static site + client-side generation. Prominently state this in UI |
| **MD3 text field styling (outlined variant)** | Outlined text fields with floating labels are the modern standard for form inputs. The current bare `<input>` with static labels above looks dated. | MEDIUM | MD3 "outlined" style: 1px border that highlights on focus, label that floats into the border on focus/fill. Requires CSS-only animation or a small wrapper component. Tailwind v4 `@theme` tokens map well to MD3 color roles. |
| **MD3 button hierarchy (filled, outlined, text)** | Users expect visual hierarchy between primary actions (Next, Download) and secondary (Back, Copy). Current buttons use ad-hoc blue/gray with no consistent system. | LOW | Three tiers: filled (primary actions), outlined (secondary/back), text (tertiary). Use MD3 shape tokens (rounded-xl for buttons). Map to `--md-sys-color-primary` and `--md-sys-color-on-primary`. |
| **MD3 card components with elevation** | Backend selection cards and output blocks need consistent elevation, padding, and shape. Current cards have inconsistent border-only styling. | LOW | MD3 elevation uses tonal surface tint (not just shadows). Levels 0-5. Cards typically at level 1 (subtle tint + minimal shadow). Selected state bumps to level 2. |
| **Dark mode** | 87% of users prefer apps that support automatic theme switching. A dev/IT tool without dark mode feels incomplete. | MEDIUM | Three-state toggle: System / Light / Dark. Respect `prefers-color-scheme` as default. Persist choice in `localStorage`. Implement via `data-theme` attribute on `<html>` + CSS custom properties. Tailwind v4 `dark:` variant works with `@media (prefers-color-scheme: dark)` or class strategy. |
| **Consistent color token system** | Hardcoded `text-gray-700`, `bg-blue-600`, `border-red-500` scattered across components creates maintenance burden and makes theming impossible. | MEDIUM | Define MD3 color roles as CSS custom properties: `--color-primary`, `--color-on-primary`, `--color-surface`, `--color-on-surface`, `--color-error`, etc. All components reference tokens, not raw Tailwind colors. Enables dark mode and accent colors with a single layer of indirection. |
| **Proper step indicator / progress bar** | Current StepIndicator uses inline styles, plain text with ">" separators, and checkbox emoji. Looks unprofessional. | MEDIUM | MD3 stepper pattern: numbered circles connected by lines. Completed steps show checkmark icon in filled circle. Active step is highlighted with primary color. Future steps are muted. Must show step labels and be responsive (collapse labels on mobile, show numbers only). |
| **Responsive layout (mobile-friendly forms)** | IT admins use phones/tablets for quick reference or field work. Current `max-w-2xl` centered layout does not adapt form fields for small screens. | MEDIUM | Backend cards: grid `grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`. Form fields: full-width on mobile, optionally 2-column on wide screens for short fields. Step indicator: horizontal on desktop, compact (numbers-only or dots) on mobile. Buttons: full-width on mobile. |
| **Form field error states (MD3 style)** | Current red border + small text is functional but not MD3-compliant. MD3 errors use the error color role, an error icon, and supporting text below the field. | LOW | Error state: border changes to `--color-error`, label text changes to error color, trailing error icon appears, supporting text shows error message in error color. Already have error messages; need to style them consistently. |
| **Step-level descriptions** | Each wizard step needs a brief explanation of what the user is doing and why. Current steps jump straight to form fields with only "Step N: Title". | LOW | 1-2 sentence description below each step heading. Examples: "Choose your cloud storage provider. This determines which credentials you will need." Static text, no logic. |
| **Accessible focus states** | EAA compliance (June 2025) requires visible focus indicators for keyboard navigation. Current `:focus` uses `ring-2` which is minimal. | LOW | MD3 focus: 3px outline using `--color-primary` with 2px offset. All interactive elements (buttons, inputs, cards, links) must have visible focus. Tailwind `focus-visible:` variant preferred over `focus:` to avoid showing focus on mouse click. |
## Differentiators
### Differentiators (Competitive Advantage)
Features that set Ready2Blob apart from "just read the rclone docs" or copy-pasting PS scripts from Reddit.
Features that elevate Ready2Blob from "functional tool" to "tool that inspires confidence."
| Feature | Value Proposition | Complexity | Notes |
|---------|-------------------|------------|-------|
| Intune-specific detection script generation | Intune Win32 apps require a separate detection script (exit 0 = installed); most admins copy-paste wrong ones | Medium | Detect by checking rclone.exe presence at install path AND config file presence. Both must exist |
| Intune packaging hints / IntuneWinAppUtil guidance | After generating scripts, show admin the exact IntuneWinAppUtil command to wrap the installer | Low | Static text block, not dynamic generation — but reduces a common stumbling point |
| RMM-specific script variants | NinjaRMM, Datto, and ConnectWise have slightly different execution contexts (SYSTEM vs user, working dir) | High | Start with a generic "SYSTEM context" PS script that works across RMMs; add named variants later |
| Rclone version pinning | MSP environments require reproducible deployments; "latest" is not acceptable for production | Low | Text input: "Pin to rclone version" (e.g., `v1.68.2`). Defaults to latest stable. Affects download URL in script |
| Config placement path options | Config can go to `%APPDATA%\rclone\rclone.conf` (user) or a machine-wide path. Intune SYSTEM context needs machine-wide | Medium | Dropdown: User profile path vs machine-wide path (`C:\ProgramData\rclone\`). Explain implications of each |
| Multiple remotes in one config | A single rclone.conf can contain multiple named remotes; some orgs need 2-3 backends on same endpoint | High | Allow "Add another remote" in wizard. Generates a single .conf with multiple sections |
| Live config preview | Admin sees the exact text of generated files before downloading — builds trust, catches errors | Low | Syntax-highlighted read-only textarea. Updates in real time as form fields change |
| Copy-to-clipboard for each output | Some RMM tools have a "run script" field — paste directly without downloading a file | Low | Copy button beside each output block |
| Field-level validation with rclone-specific rules | Azure Blob storage account names are 3-24 lowercase alphanumeric chars — catch this before the admin deploys a broken config | Medium | Per-field regex/rule validation. Reduces "why doesn't rclone connect?" support tickets |
| Explanatory tooltips on sensitive fields | "What is an SAS token vs an Access Key?" — admins often don't know which credential type to use | Low | Tooltip or inline help text per field. Reduces abandonment from confusion |
| Backend popularity ordering | Show Azure Blob, S3, OneDrive, SFTP, GCS at top — don't bury them alphabetically | Low | Simple UX decision with high impact on time-to-task-complete |
| **User-selectable accent color** | Personalizes the tool. IT admins often match internal tools to company brand colors. Also demonstrates the token system works. | MEDIUM | Offer 5-8 preset accent colors (blue default, teal, purple, green, orange, pink). User picks one, CSS custom properties update for `--color-primary` and its derivatives. Persist in `localStorage`. Use MD3 tonal palette generation: from one source color, derive on-primary, primary-container, on-primary-container. |
| **App intro / landing section** | First-time visitors need to understand what Ready2Blob does before diving into the wizard. Current app jumps straight to Step 1 with no context. | LOW | Hero section above the wizard: app name, one-sentence value prop ("Generate rclone configs and deployment scripts for Windows endpoints"), 3-4 feature bullets (client-side only, 7 backends, Intune + RMM scripts), and a "Get Started" button that scrolls to or reveals the wizard. Collapses/hides once wizard is started. |
| **Animated step transitions** | Smooth transitions between wizard steps make the app feel responsive and polished rather than jarring page swaps. | LOW | CSS-only fade or slide transition on step change. Use `opacity` + `transform: translateX()` with 200ms ease. Keep it subtle -- this is a productivity tool, not a marketing site. Respect `prefers-reduced-motion`. |
| **Contextual help popovers (upgraded)** | Current tooltip toggle (click to show/hide inline text) works but is basic. MD3-style popovers with arrow indicators and rich content feel more professional. | MEDIUM | Replace inline toggle with a proper popover component: positioned above/below the trigger, with arrow, dismissed on outside click or Escape. Still click-triggered (not hover -- touch devices need click). Consider using Floating UI (lightweight, ~3KB) for positioning math. |
| **Remote name field clarity** | The "Remote name" field confuses users who do not know rclone conventions. Needs prominent explanation, examples, and format hint. | LOW | Add: placeholder "e.g., corp-backup", help text explaining "This name identifies the storage target in rclone commands and scripts", and an inline example showing how it appears in the generated config `[corp-backup]`. Already partially addressed in v1.1 tooltips; needs more prominent treatment. |
| **MD3 select/dropdown styling** | Native `<select>` elements look different on every OS and cannot be themed. Breaks the visual consistency of MD3 forms. | HIGH | Custom dropdown component matching MD3 outlined field style. Must handle keyboard navigation, ARIA `listbox` role, focus management. Significantly more complex than native select. Consider whether the 2-3 selects in the app justify the effort. Alternative: style the surrounding label/container as MD3 and accept native select rendering. |
| **Code block syntax highlighting (review step)** | Output blocks (rclone.conf, PowerShell scripts) currently render as plain `<pre>` text. Syntax highlighting adds professionalism and readability. | MEDIUM | Use a lightweight highlighter like Prism.js or highlight.js (INI + PowerShell grammars only -- tree-shake aggressively). Alternatively, minimal hand-rolled highlighting for INI format (`[section]` headers in primary color, `key = value` with key bold). Dark mode must invert colors. |
| **Smooth scroll-to-error on validation failure** | When a user clicks Next with invalid fields, scrolling to the first error field reduces confusion on longer forms (RemoteConfigStep with many fields). | LOW | On form submit failure, call `element.scrollIntoView({ behavior: 'smooth', block: 'center' })` on the first errored field. Minor polish, large UX impact on mobile where fields extend below fold. |
## Anti-Features
### Anti-Features (Commonly Requested, Often Problematic)
Features to explicitly NOT build in v1 — scope creep killers.
Features that seem good for UI polish but create problems.
| Anti-Feature | Why Avoid | What to Do Instead |
|--------------|-----------|-------------------|
| Test connection / validate credentials | Requires a backend proxy (CORS blocks direct cloud API calls from browser); breaks the "no server" constraint entirely | Show a callout: "Run `rclone lsd remote-name:` on any Windows PC after deploying to verify connectivity" |
| Save / load configurations | Requires either a backend (no-server constraint violated) or localStorage (credentials in browser storage = security incident) | Tell user to save the downloaded .conf file. That IS their save format |
| User accounts / authentication | No backend = no accounts. Would require a complete architecture rethink | Out of scope permanently for v1. Re-evaluate only if architecture changes |
| rclone mount / sync scheduling | UI for configuring rclone mount or scheduled sync jobs adds a second problem domain (task scheduler, Windows service) on top of the first | Separate product decision. Ready2Blob focuses solely on getting rclone configured and deployed |
| Auto-push to Intune via Graph API | Would require Azure AD app registration, OAuth flow, Graph API integration — massive scope increase | Generate files the admin uploads manually. Graph API is a v2+ consideration |
| Multi-OS support (macOS, Linux) | Scripts are PowerShell for Windows. macOS/Linux have different path conventions, shell scripts, MDM tools | Out of scope for v1. State clearly in UI: "Windows endpoints only" |
| rclone version auto-update logic | Keeping rclone up to date on endpoints is a separate lifecycle management problem | Point admin to rclone's own update mechanism or their RMM's patch management |
| Visual diff of old vs new config | Requires knowing what's already deployed — impossible without a backend | Not viable without persistence layer |
| Encryption of config credentials | rclone supports `rclone config` password-encrypted configs but requires interactive unlock on each use, incompatible with unattended deployment | Document the limitation; recommend Azure Key Vault or Intune-native secrets for sensitive deployments |
---
| Feature | Why Requested | Why Problematic | Alternative |
|---------|---------------|-----------------|-------------|
| **Full MUI or Material Tailwind component library** | "Just use MUI for MD3" -- seems like it would save time | Massive bundle size increase (MUI is ~300KB+ gzipped). Overrides existing Tailwind approach. Migration tax is high. Most MUI components are unnecessary for a 4-step wizard with ~15 form fields. Creates a dependency on MUI's theming system instead of owning your tokens. | Build a thin MD3 design token layer in CSS custom properties + Tailwind utilities. Create 4-5 custom components (TextField, Button, Card, StepIndicator, Toggle). Total code is smaller than MUI's tree-shaken output for the same components. |
| **Animated wizard with page-per-step routing** | "Each step should be its own route for bookmarkability" | Adds React Router dependency. Wizard state is ephemeral (credentials in memory) -- refreshing a step would lose context. Back/forward browser buttons would conflict with wizard Back/Next. Bookmarking a credential-entry step is a security anti-pattern. | Keep single-page wizard with in-memory state. Step indicator serves as navigation. No routing needed. |
| **Glassmorphism / neomorphism / heavy visual effects** | "Modern UI trends" | These effects are CPU-intensive (backdrop-blur), have accessibility issues (low contrast), and will look dated within a year. IT admins want clarity, not visual flair. | MD3 tonal elevation (surface tint) provides depth without heavy effects. Clean, readable, professional. |
| **Custom-styled native checkboxes and radios** | "The DeploymentStep checkboxes look plain" | Custom checkbox/radio styling requires hiding the native element and rebuilding focus, checked, indeterminate, and disabled states. High effort for a few toggles. Touch target issues. | Use MD3-inspired wrapper: larger touch target (48px), visible label, proper spacing. Keep native input for accessibility. Style the surrounding container instead. |
| **Toast notifications for copy-to-clipboard** | "Show a toast when text is copied" | Requires a toast/snackbar system with portal, z-index management, animation, and auto-dismiss timer -- infrastructure overhead for a single use case. | Inline feedback: change the copy button text/icon to "Copied!" for 2 seconds, then revert. Zero infrastructure needed. Already understood by users. |
| **Theme with arbitrary user-picked hex color** | "Let users enter any hex color" | Arbitrary colors break accessibility (contrast ratios). A random bright yellow as primary makes error states invisible, text unreadable. Generating a full tonal palette from an arbitrary source requires the `@material/material-color-utilities` library (~15KB). | Offer 5-8 curated presets that have been verified for contrast compliance in both light and dark modes. Covers 95% of personalization desire with zero accessibility risk. |
## Feature Dependencies
```
Backend selection
→ Per-backend form fields (fields depend on selected backend type)
→ Remote name input
→ rclone.conf generation (depends on: backend type, all field values, remote name)
→ Intune script generation (depends on: config content, install option, config path choice)
→ RMM script generation (depends on: config content, install option, config path choice)
→ Version pin input (affects download URL inside both scripts)
→ Config path option (affects file placement command inside both scripts)
Color Token System (CSS custom properties)
|-- required by --> Dark Mode (tokens swap values per theme)
|-- required by --> Accent Colors (tokens swap primary hue)
|-- required by --> MD3 Text Fields (reference token colors)
|-- required by --> MD3 Buttons (reference token colors)
|-- required by --> MD3 Cards (reference token colors + elevation tint)
|-- required by --> Step Indicator (reference token colors)
|-- required by --> Error States (reference error token)
Live config preview → rclone.conf generation (real-time rendering of same output)
Download buttons → all generation outputs (nothing to download until form is valid)
Security warning → download buttons (warning must be acknowledged before download is enabled)
Dark Mode
|-- requires --> Color Token System
|-- requires --> localStorage persistence (theme choice)
|-- enhances --> Accent Colors (must work in both light and dark)
Accent Colors
|-- requires --> Color Token System
|-- requires --> Curated palette presets (per-accent token sets)
Step Indicator (MD3)
|-- requires --> Color Token System
|-- independent of --> other component styling
App Intro Section
|-- independent of --> all styling work (content-only)
|-- enhances --> first-time experience
Step Descriptions
|-- independent of --> all styling work (content-only)
Remote Name Clarity
|-- independent of --> MD3 styling (content improvement)
Responsive Layout
|-- independent of --> color tokens (Tailwind breakpoints)
|-- should follow --> MD3 component styling (size tokens align)
Animated Transitions
|-- requires --> Step rendering structure (already exists)
|-- should follow --> MD3 component styling (transition consistent)
```
---
### Dependency Notes
## MVP Recommendation
- **Color Token System is the foundation**: Every visual component depends on it. Must be implemented first. Without tokens, each component hardcodes colors and dark mode becomes a per-component rewrite.
- **Dark Mode requires tokens but tokens do not require dark mode**: Tokens can ship first with light-only, then dark mode adds a second set of token values.
- **Accent Colors layer on top of both**: Accent colors multiply the token sets (N accents x 2 themes = 2N token sets). Implement after dark mode is stable.
- **Content improvements (intro, descriptions, remote name) are independent**: Can be done in any order, in parallel with styling work.
- **Step Indicator is visually complex but logically independent**: Can be rebuilt without affecting other components.
Prioritize in this order:
## MVP Definition
1. Backend selection + per-backend forms (Azure Blob, S3, OneDrive, SFTP as initial set — covers 80% of use cases)
2. rclone.conf generation with live preview
3. Intune PowerShell script generation (primary target audience pain point)
4. RMM PowerShell script generation
5. Security warning gate before download
6. Optional rclone install toggle
7. Config path selector (user vs machine-wide)
### Launch With (v1.2 Core)
Defer to post-MVP:
Minimum set to achieve "polished, self-explanatory experience" goal.
- Multiple remotes in one config: adds wizard UX complexity; single remote covers the majority of deployments
- RMM-named variants (NinjaRMM-specific, Datto-specific): start with generic SYSTEM-context PS script
- Intune IntuneWinAppUtil packaging hints: valuable but can be a static docs page
- Version pinning: default to latest stable with a text field; low-effort add
- Field-level validation beyond basic required-field checks: adds significant per-backend maintenance burden
- [ ] **Color token system** -- CSS custom properties for all MD3 color roles, referenced by all components. This is the enabler for everything else.
- [ ] **MD3 text fields (outlined)** -- Floating labels, proper focus/error states. Applied to FieldRenderer and BackendSelectionStep remote name input.
- [ ] **MD3 button hierarchy** -- Filled primary, outlined secondary, text tertiary. Applied to all wizard navigation and download buttons.
- [ ] **MD3 card components** -- Elevation via surface tint. Applied to BackendCard and OutputBlock.
- [ ] **MD3 step indicator** -- Numbered circles with connecting lines, completed/active/future states.
- [ ] **Dark mode toggle** -- System/Light/Dark with localStorage persistence.
- [ ] **App intro section** -- Hero explaining what Ready2Blob is, with Get Started CTA.
- [ ] **Step-level descriptions** -- Brief explanation text on each wizard step.
- [ ] **Remote name field clarity** -- Prominent explanation, examples, format hint.
- [ ] **Responsive layout** -- Mobile-friendly grid, collapsible step indicator, full-width buttons on small screens.
- [ ] **Accessible focus states** -- Visible focus-visible outlines on all interactive elements.
- [ ] **FieldRenderer aria consistency fix** -- Existing tech debt from v1.1.
- [ ] **StepIndicator inline style migration** -- Replace inline styles with Tailwind classes (existing tech debt).
---
### Add After Validation (v1.2.x)
## Backend Coverage Priority
Features to add once the core polish is working and tested.
Based on enterprise Windows deployment prevalence (HIGH confidence from domain knowledge):
- [ ] **Accent color selector** -- 5-8 preset colors, persistent. Add after token system and dark mode are stable.
- [ ] **Animated step transitions** -- CSS fade/slide on step change. Add after step rendering is finalized.
- [ ] **Contextual help popovers (upgraded)** -- Replace inline tooltip toggles with positioned popovers. Add after MD3 component styling is settled.
- [ ] **Scroll-to-error on validation failure** -- Minor polish, add when form field styling is complete.
| Tier | Backends | Rationale |
|------|----------|-----------|
| Tier 1 — Must ship in v1 | Azure Blob Storage, Amazon S3, Microsoft OneDrive | Dominant in enterprise; covers ~70% of use cases. "Ready2Blob" brand implies Azure first |
| Tier 2 — Ship in v1 if feasible | SFTP, Google Cloud Storage, Backblaze B2 | Common in MSP environments and SMB |
| Tier 3 — Post-v1 | Google Drive, Dropbox, SharePoint, S3-compatible (Wasabi, MinIO, etc.) | Consumer-origin or niche; lower enterprise priority |
| Tier 4 — Document only | All remaining rclone backends (50+) | Too many to form-ify in v1; link to rclone docs |
### Future Consideration (v2+)
**Note on S3-compatible backends:** Amazon S3 forms should include an "endpoint override" field so the same form handles Wasabi, MinIO, Cloudflare R2, etc. This is how rclone handles them natively — `provider` + optional `endpoint`. One form, many backends. (MEDIUM confidence — verify against rclone S3 docs)
Features to defer until after this milestone.
---
- [ ] **Custom MD3 select/dropdown** -- High complexity for 2-3 selects in the app. Native select with MD3-styled container is sufficient for v1.2.
- [ ] **Code syntax highlighting** -- Nice but not essential. Plain monospace output blocks are standard for config/script tools.
- [ ] **Arbitrary user hex color theming** -- Requires color utility library and contrast validation. Curated presets suffice.
## IT Admin Expectations (Contextual)
## Feature Prioritization Matrix
These are workflow expectations rather than discrete features, but they inform every feature decision:
| Feature | User Value | Implementation Cost | Priority |
|---------|------------|---------------------|----------|
| Color token system | HIGH | MEDIUM | P1 |
| MD3 text fields | HIGH | MEDIUM | P1 |
| MD3 buttons | HIGH | LOW | P1 |
| MD3 cards | MEDIUM | LOW | P1 |
| Dark mode | HIGH | MEDIUM | P1 |
| Step indicator (MD3) | HIGH | MEDIUM | P1 |
| App intro section | HIGH | LOW | P1 |
| Step descriptions | MEDIUM | LOW | P1 |
| Remote name clarity | MEDIUM | LOW | P1 |
| Responsive layout | HIGH | MEDIUM | P1 |
| Focus states (a11y) | MEDIUM | LOW | P1 |
| FieldRenderer aria fix | LOW | LOW | P1 (tech debt) |
| StepIndicator style migration | LOW | LOW | P1 (tech debt) |
| Accent color selector | MEDIUM | MEDIUM | P2 |
| Step transitions | LOW | LOW | P2 |
| Upgraded popovers | LOW | MEDIUM | P2 |
| Scroll-to-error | LOW | LOW | P2 |
| Custom select/dropdown | LOW | HIGH | P3 |
| Syntax highlighting | LOW | MEDIUM | P3 |
- **Scripts must run as SYSTEM** — Intune and most RMMs execute scripts as SYSTEM, not as the logged-in user. Config path must be machine-wide, not `%APPDATA%`. This is the single most common deployment failure mode.
- **Scripts must be idempotent** — Running the install script twice must not break anything. Check-then-act pattern: if rclone.exe already exists and config already exists, exit 0.
- **Scripts must have exit codes** — Intune uses exit codes to determine success/failure of a deployment. Script must exit 0 on success, non-zero on failure.
- **Detection scripts must be separate from install scripts** — Intune Win32 app model requires them to be distinct. Many generated scripts online conflate them.
- **No interactive prompts** — Scripts run silently. Any `Read-Host`, `Write-Host` expecting input, or UAC prompt breaks unattended deployment.
- **64-bit PowerShell** — Intune on 64-bit Windows sometimes executes PS in 32-bit mode. rclone.exe path may differ. Scripts should force 64-bit context or be path-aware.
**Priority key:**
- P1: Must have for v1.2 launch -- achieves the "polished" goal
- P2: Should have, add when core P1 features are stable
- P3: Nice to have, defer unless time permits
(Confidence: HIGH for SYSTEM context and exit codes — verified by common Intune troubleshooting canon. MEDIUM for 32/64-bit PS caveat — common but less universally documented.)
## What Makes a Config Wizard Feel Professional vs Bare
---
Based on analysis of the current Ready2Blob UI against MD3 patterns and wizard UX best practices:
**Current state (bare):**
- Raw HTML inputs with no visual framework
- Inline styles on StepIndicator
- Inconsistent color usage (hardcoded Tailwind values)
- No dark mode
- No intro explaining the tool
- Step headings with no descriptions
- No visual hierarchy in buttons
- Backend cards have basic border styling only
- Output blocks are unstyled pre/textarea elements
**Professional target:**
1. **Visual consistency** -- Every element follows the same design language (spacing, color, shape, elevation)
2. **Clear information hierarchy** -- Primary actions are obvious, secondary actions are subdued, labels guide the eye
3. **Contextual guidance** -- The UI explains itself: what each step does, what each field means, what happens next
4. **Responsive confidence** -- Works smoothly on any screen size without horizontal scrolling or cramped layouts
5. **Polish details** -- Smooth transitions, proper loading states, visible focus, no flicker on theme change
6. **Trust signals** -- Professional appearance, security notices styled prominently, client-side-only badge
The gap between bare and professional is not one feature -- it is the cumulative effect of the token system enabling consistent styling across every component, combined with content improvements (intro, descriptions, help text) that make the wizard self-explanatory.
## Sources
- rclone official documentation (rclone.org) — not fetched in this session due to tool restrictions; referenced from training data (knowledge cutoff August 2025)
- Microsoft Intune Win32 app deployment model — training data (HIGH confidence on SYSTEM context, exit codes, detection script requirements)
- RMM deployment patterns (NinjaRMM, Datto, ConnectWise) — training data (MEDIUM confidence)
- rclone.conf INI format specification — training data (HIGH confidence; format is stable and well-documented)
- [Material Design 3 Text Fields Guidelines](https://m3.material.io/components/text-fields/guidelines) -- MD3 text field patterns (filled vs outlined, supporting text, error states)
- [Material Design 3 Components](https://m3.material.io/components) -- Full component catalog
- [Material Design 3 Elevation](https://m3.material.io/styles/elevation/applying-elevation) -- Surface tint vs shadow, elevation levels 0-5
- [Material Design 3 Color Roles](https://m3.material.io/styles/color/roles) -- Primary, secondary, tertiary, surface, error color roles
- [Material Design 3 Dynamic Color](https://m3.material.io/styles/color/dynamic/user-generated-source) -- User-generated source color for theming
- [MD3 box-shadow CSS values](https://studioncreations.com/blog/material-design-3-box-shadow-css-values/) -- CSS elevation implementation (MEDIUM confidence)
- [Beyond the Progress Bar: Stepper UI Design](https://medium.com/@david.pham_1649/beyond-the-progress-bar-the-art-of-stepper-ui-design-cfa270a8e862) -- Stepper patterns and best practices
- [Wizard Design Pattern (UX Planet)](https://uxplanet.org/wizard-design-pattern-8c86e14f2a38) -- Wizard UX fundamentals
- [Wizards: Definition and Design Recommendations (NN/g)](https://www.nngroup.com/articles/wizards/) -- Nielsen Norman Group wizard guidelines
- [Dark Mode Toggle and prefers-color-scheme](https://dev.to/abbeyperini/dark-mode-toggle-and-prefers-color-scheme-4f3m) -- Implementation pattern for system/manual toggle
- [The Ultimate Guide to Coding Dark Mode 2025](https://devieffe.substack.com/p/the-ultimate-guide-to-coding-dark-mode-layouts-in-2025) -- CSS custom properties + data-theme approach
- [Design Tokens and Theming: Scalable UI Systems 2025](https://materialui.co/blog/design-tokens-and-theming-scalable-ui-2025) -- Token architecture patterns
- [Tailwind CSS Responsive Design](https://tailwindcss.com/docs/responsive-design) -- Mobile-first breakpoint system
- [MD3 Theming Tokens (seenode)](https://seenode.com/blog/what-is-material-3-and-why-it-matters-in-2025) -- 141 system tokens, token hierarchy
- [Input Field Design Best Practices 2025](https://fireart.studio/blog/input-field-design-best-practice/) -- Floating labels, error patterns, accessibility
**Validation recommended before roadmap finalization:**
- Confirm current rclone backend list and required fields per backend at rclone.org/overview
- Confirm Intune Win32 app detection script requirements in current Microsoft docs
- Check if rclone has changed S3-compatible `provider`+`endpoint` pattern in recent releases
---
*Feature research for: Ready2Blob v1.2 UI Polish*
*Researched: 2026-03-31*
+210 -266
View File
@@ -1,362 +1,306 @@
# Domain Pitfalls
# Pitfalls Research
**Domain:** rclone config generator / PowerShell deployment wizard (Windows/Intune/RMM)
**Researched:** 2026-03-26
**Confidence:** HIGH (Intune/PowerShell — verified against official Microsoft docs), MEDIUM (rclone-specific — based on format spec knowledge plus training data; rclone docs were inaccessible during research)
**Domain:** UI polish overhaul -- Material Design 3, dark mode, accent colors added to existing Tailwind v4 + React wizard app
**Researched:** 2026-03-31
**Confidence:** HIGH (based on codebase analysis + verified Tailwind v4 docs + community patterns)
---
## Critical Pitfalls
Mistakes that cause the generated script/config to silently fail or require a full rewrite.
---
### Pitfall 1: rclone config deployed to wrong path under SYSTEM context
### Pitfall 1: Tailwind v4 Dark Mode Requires CSS-First Config, Not tailwind.config.js
**What goes wrong:**
When Intune deploys a PowerShell script with "Run as SYSTEM" (the `No` logged-on-credentials option), the script runs as `NT AUTHORITY\SYSTEM`. The default rclone config location resolves from the SYSTEM user's `%APPDATA%`, which is `C:\Windows\system32\config\systemprofile\AppData\Roaming\rclone\rclone.conf`. This path is not readable by the end user who will later run rclone interactively. The config is deposited silently with no error, but rclone launched by the user finds no config.
Developers reach for `tailwind.config.js` with `darkMode: 'class'` which does not exist in Tailwind v4. The app currently has only `@import "tailwindcss";` in `index.css` with no config file at all. Using v3 dark mode patterns produces zero effect and wastes debugging time.
**Why it happens:**
rclone resolves config location from environment variables at runtime. Under SYSTEM, `%APPDATA%` and `%USERPROFILE%` expand to the SYSTEM profile paths, not any individual user's profile. Developers test locally as themselves and never hit this path.
Most tutorials and Stack Overflow answers still reference Tailwind v3 syntax. Tailwind v4 moved to a fully CSS-first configuration model. The `darkMode` config key is gone.
**Consequences:**
- rclone runs with no configuration; all sync commands fail with "no remote" error
- Hard to debug because the config file exists on disk — just in the wrong place
- If the wizard generates a hardcoded `%APPDATA%` path string in the script, that string is evaluated at deployment time (SYSTEM), not at user runtime
**How to avoid:**
Add the `@custom-variant` directive in `index.css` for class-based toggling:
```css
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
```
This enables manual toggle via a `.dark` class on `<html>`. The `:where()` wrapper keeps specificity at zero, preventing cascade conflicts. Verified against [Tailwind v4 dark mode docs](https://tailwindcss.com/docs/dark-mode).
**Prevention:**
- The generated script must write the config to a machine-wide path such as `C:\ProgramData\rclone\rclone.conf` and then invoke rclone with `--config "C:\ProgramData\rclone\rclone.conf"` (or set `RCLONE_CONFIG` env var).
- Alternatively: write to each user's profile by running in user context — but SYSTEM context is common for silently deploying software.
- The wizard should make the config destination path explicit and let the IT admin choose: machine-wide vs. user-profile. Never default to a bare `%APPDATA%` expansion in a SYSTEM-context script.
**Warning signs:**
- `dark:` prefixed classes have no visible effect
- Dark mode only responds to OS preference, not the toggle button
**Detection:**
- Config exists at SYSTEM profile path but rclone launched by user says "no remote configured"
- Check `rclone config file` — it will show the wrong path
**Phase relevance:** Phase generating the PowerShell script (any phase touching script output)
**Phase to address:**
Phase 1 (Theme Foundation) -- this must be the very first CSS change before any `dark:` classes are added to components.
---
### Pitfall 2: Intune PowerShell scripts are size-limited to 200 KB (ASCII)
### Pitfall 2: Hardcoded Color Values Across 73 className Usages
**What goes wrong:**
Microsoft Intune enforces a hard 200 KB (ASCII) size limit on uploaded PowerShell scripts. Scripts that embed a large rclone installer binary (base64-encoded), or that inline multiple large config payloads, will be rejected at upload time.
The codebase has 73 `className=` usages with hardcoded Tailwind color classes (`text-gray-700`, `border-gray-300`, `focus:ring-blue-300`, `text-red-500`, `bg-gray-50`, etc.). Adding dark mode by appending a `dark:` counterpart to every single one creates unreadable className strings and guarantees missed spots -- invisible text, invisible borders, or unreadable error messages against dark backgrounds.
**Why it happens:**
IT developers prototype a "self-contained" script that downloads rclone, unpacks it, writes the config, and sets up a scheduled task — all in one file. Base64-encoding a ~50 MB rclone binary produces a ~67 MB string. Even base64-encoding a 400 KB installer produces a 550 KB string, well over the limit.
When building light-mode-only, hardcoded color classes are natural. The cost is deferred until dark mode arrives. Developers add `dark:` to the visible components and miss the less-obvious ones (help text, error messages, placeholders, disabled states).
**Consequences:**
- Script upload fails; IT admin gets a non-obvious error in Intune
- Workaround requires restructuring the entire script delivery approach
**How to avoid:**
Define semantic CSS custom properties (design tokens) mapped to Tailwind's `@theme` directive:
```css
@theme {
--color-surface: #ffffff;
--color-on-surface: #1a1a1a;
--color-primary: #2563eb;
--color-error: #dc2626;
}
```
Then use `bg-surface`, `text-on-surface` throughout components. Dark mode changes the token values once (on `.dark`), not every component. This is the Material Design 3 approach (surface, on-surface, primary, on-primary, etc.).
**Prevention:**
- The wizard must never embed rclone binary content into the generated script
- The rclone installation step must use a network download (e.g., `Invoke-WebRequest` from the rclone GitHub releases API or a corporate file share URL) or reference a Win32 app deployment separately
- Clearly surface this constraint in the wizard: "rclone binary will be downloaded from [URL] at deployment time" — and let the admin specify an internal mirror if internet access is restricted on endpoints
**Warning signs:**
- `dark:` classes appearing in JSX alongside light classes, creating 100+ character className strings
- Text disappearing on dark backgrounds during manual testing
- Error messages (`text-red-600`) becoming unreadable against dark backgrounds
**Detection:**
- Intune admin center shows upload error "Script size exceeds limit"
- Script file is visibly large before upload
**Phase relevance:** Phase implementing the rclone-install option in script generation
**Phase to address:**
Phase 1 (Theme Foundation) defines tokens. Phase 2 (Component Overhaul) replaces hardcoded colors with token references.
---
### Pitfall 3: PowerShell script encoding mismatch causes silent config corruption
### Pitfall 3: Dark Mode Color Contrast Failures (WCAG AA)
**What goes wrong:**
The generated PowerShell script writes the rclone config file to disk using `Set-Content` or `Out-File`. The default encoding in Windows PowerShell 5.1 is UTF-16 LE with BOM for `Out-File`, and varies for `Set-Content` (system codepage/ANSI on PS 5.1, UTF-8 no-BOM on PS 7+). rclone expects its config file in UTF-8. A config with a UTF-16 BOM or ANSI-encoded special characters (common in storage keys) will be misread, causing authentication failures.
Text that passes 4.5:1 contrast in light mode fails in dark mode. The most common failures: gray help text on dark gray backgrounds, red error text on dark surfaces, blue links on dark blue-gray backgrounds. The current app uses `text-gray-500` for help text and `text-red-600` for errors -- both will fail against typical dark backgrounds.
**Why it happens:**
Developers write `Out-File $configPath` and it works in their test because all values are ASCII. The bug surfaces when a customer has a storage account key or SAS token containing characters that differ between encodings, or when the file has a BOM that confuses rclone's parser.
Developers assume inverting colors preserves contrast ratios. They do not. Concrete example from this codebase: `text-gray-500` (#6b7280) on `bg-white` (#ffffff) gives 4.6:1 contrast -- barely passing AA. The same `text-gray-500` on `bg-gray-900` (#111827) gives only 3.5:1 -- failing AA for normal text.
**Consequences:**
- rclone silently reads a corrupt config; authentication fails with opaque errors
- Hard to reproduce because it only manifests with certain key contents
**How to avoid:**
Define separate color values per theme within the token system. In dark mode, help text must use a lighter gray (equivalent of `text-gray-400`), errors must use a lighter red (equivalent of `text-red-400`). Semantic tokens centralize these mappings so each value is defined once. Verify every text/background pair with the browser DevTools accessibility panel or WebAIM contrast checker against WCAG AA 4.5:1 minimum for normal text, 3:1 for large text.
**Prevention:**
- The generated script must always write the config with explicit UTF-8 no-BOM encoding:
```powershell
[System.IO.File]::WriteAllText($configPath, $configContent, [System.Text.Encoding]::UTF8)
```
or
```powershell
Set-Content -Path $configPath -Value $configContent -Encoding UTF8
```
Note: In PowerShell 5.1, `-Encoding UTF8` writes UTF-8 *with* BOM. Use `[System.IO.File]::WriteAllText` with `new System.Text.UTF8Encoding($false)` to guarantee no BOM.
- The wizard's script template must hardcode the correct write method; never leave encoding to PS default
**Warning signs:**
- Help text feels "hard to read" in dark mode during visual review
- Browser DevTools accessibility audit flagging contrast ratios below 4.5:1
- Error states visually blending into background colors
**Detection:**
- Open the written config in a hex editor: UTF-16 has `FF FE` as first bytes; UTF-8 BOM has `EF BB BF`
- rclone error: "unexpected character at start of file" or authentication failures on otherwise valid credentials
**Phase relevance:** Any phase producing the PowerShell script template
**Phase to address:**
Phase 1 (Token Definition) for color values. Phase 2 (Component Overhaul) for application. Each component restyle must include a contrast verification before marking complete.
---
### Pitfall 4: rclone config section names collide with rclone reserved names or contain invalid characters
### Pitfall 4: Breaking 131 Test Selectors During Component Restyling
**What goes wrong:**
rclone remote names in the config are used on the command line as `remotename:path`. The name becomes part of shell arguments and rclone's internal addressing. Names with spaces, colons, forward slashes, or square brackets break the INI section header (`[remote name]` is valid INI only if the name contains no `]`). Names that match rclone built-in remote types (e.g., naming a remote "local", "union", "memory") cause confusing errors. Names starting with a dash conflict with CLI flag parsing.
The test suite uses 131 occurrences of `getByText`, `getByRole`, `getByTestId`, `getByLabelText`, and `queryBy` selectors across 5 test files (App.test.tsx, StepIndicator.test.tsx, ReviewStep.test.tsx, RemoteConfigStep.test.tsx, BackendSelectionStep.test.tsx). Restyling components breaks tests by: changing visible text content, wrapping elements in new containers that alter DOM hierarchy, replacing native elements with styled equivalents (changing roles), or removing/renaming aria attributes.
**Why it happens:**
The wizard lets IT admins freely type a remote name without validation. The name goes into `[user input]` verbatim.
UI overhauls touch the same JSX that tests query. Specific examples from this codebase:
- `getByText(/Backend/)` in StepIndicator.test.tsx breaks if the label text changes or gets wrapped in a `<span>` that splits the text node
- `getAllByRole('button')` breaks if buttons become styled `<a>` tags or `<div>` elements
- `screen.findByText('2', { selector: '[data-testid="step"]' })` breaks if data-testid attributes are renamed during refactoring
**Consequences:**
- Config is syntactically broken (rclone fails to parse)
- Or config parses but the remote cannot be referenced on the command line
- Error messages are cryptic: "Failed to create file system for remotename: didn't find section in config file"
**How to avoid:**
1. Run the full 159-test suite after every single component change, not in a batch at the end.
2. Restyle one component, verify tests, commit. Never batch-restyle all components then fix all tests.
3. When restructuring JSX, preserve text content and element roles. A `<button>` must remain a `<button>`.
4. If adding wrapper elements, ensure text nodes are not split (e.g., `getByText(/Backend/)` matches a single text node, not text across siblings).
**Prevention:**
- Validate remote names in the wizard UI before generation: allow only `[a-zA-Z0-9_-]`, max ~40 chars, no leading dash
- Show a live preview of the section header: `[my-remote]`
- Reject reserved-looking names or warn on them
**Warning signs:**
- More than 3 test failures appearing simultaneously after a restyle
- Tests failing with "Unable to find element" errors
- `getByRole` queries returning unexpected counts
**Detection:**
- rclone returns "didn't find section in config file" when the name contains special characters
- rclone returns parse error when name contains `]`
**Phase relevance:** Wizard input validation phase; config generation phase
**Phase to address:**
Every phase -- each component change must include a "159 tests green" gate. This is the single most likely source of rework.
---
### Pitfall 5: Secrets embedded in generated scripts are exposed in Intune admin center logs
### Pitfall 5: Flash of Unstyled Content (FOUC) on Dark Mode Load
**What goes wrong:**
Intune logs PowerShell script output and stores it in the Azure portal (AgentExecutor.log on endpoint + reporting in Intune admin center). If the generated script echoes the config content or uses `Write-Host` with credential values for debugging, those secrets are persisted in logs accessible to any Intune admin.
The app loads with light mode CSS, then JavaScript runs and toggles the `.dark` class, causing a visible white flash. For IT professionals who often use dark OS themes, this flash is jarring and signals low quality.
**Why it happens:**
Developers add debug output during testing ("Writing config: [content]") and forget to remove it. Or error handlers dump the config on failure.
React runs after the initial paint. If dark mode preference is stored in `localStorage` and applied via `useEffect` or React state, the first paint is always light mode. The class toggle happens milliseconds later, but the flash is visible.
**Consequences:**
- Storage account keys, SAS tokens, or OAuth secrets appear in Intune reporting
- Violates least-privilege and secrets hygiene; potential audit/compliance failure
**How to avoid:**
Add a synchronous inline `<script>` in the `<head>` of `index.html` (before any CSS or React bundle loads):
```html
<script>
if (localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
</script>
```
This executes before first paint, preventing any flash. The React toggle component then reads and syncs with the already-applied state.
**Prevention:**
- The wizard's generated script template must never echo credential values
- Use a sentinel like `Write-Host "Writing config to $configPath"` (path only, no content)
- Add a comment in the generated script: `# Do not add Write-Host or logging for $configContent`
- The wizard UI must display a security warning at download time (already planned per PROJECT.md)
**Warning signs:**
- White flash visible when loading the app with dark mode previously enabled
- Users on dark OS themes seeing a brief light flash on every page load
**Detection:**
- Audit the script template for any interpolation of credential variables into strings passed to output cmdlets
**Phase relevance:** Script template design (early phase); security review before any release
**Phase to address:**
Phase 1 (Theme Foundation) -- the FOUC prevention script must ship together with the dark mode toggle implementation, not as a later fix.
---
## Moderate Pitfalls
---
### Pitfall 6: Group Policy overrides PowerShell execution policy set in the script
### Pitfall 6: Theme Context Re-renders Causing Full Wizard Re-render
**What goes wrong:**
The generated script attempts to set `Set-ExecutionPolicy -Scope LocalMachine -ExecutionPolicy RemoteSigned` or `Bypass`. In enterprise environments with Group Policy managing `MachinePolicy` or `UserPolicy` execution policy scopes, the script's `Set-ExecutionPolicy` call has no effect — Group Policy always wins. The script appears to succeed (no error from `Set-ExecutionPolicy`) but subsequent script logic may still fail if the endpoint GP enforces `AllSigned` or `Restricted`.
Adding a `ThemeContext` that stores `{ theme: 'dark', accentColor: 'blue' }` causes every `useTheme()` consumer to re-render when any theme value changes. Since the app already has a `WizardContext` with `useReducer`, adding another context that triggers re-renders on toggle will cause all 4 wizard steps to re-render, potentially resetting form input focus or scroll position.
**Why it happens:**
Official Microsoft docs confirm: "Set-ExecutionPolicy doesn't override a Group Policy, even if the user preference is more restrictive than the policy." Intune itself bypasses execution policy for its own scripts (IME uses `-ExecutionPolicy Bypass` internally), but any child processes spawned by the script inherit the GP-enforced policy.
React Context re-renders every consumer when the provider value changes (referential equality). Tutorials show `<ThemeProvider value={{ theme, setTheme }}>` where a new object is created on every render. Even with `useMemo`, toggling theme changes the value and re-renders all consumers.
**Consequences:**
- Scripts that call `& rclone.exe` or invoke helper `.ps1` files from within the script fail with execution policy errors
- Developers test on unmanaged machines and never observe GP interference
**How to avoid:**
Do NOT store theme in React Context. Apply the theme via the `.dark` CSS class on `<html>` element and CSS custom properties. Theme toggling becomes a DOM class toggle (zero React re-renders). Store the toggle state in a small component-local state that only the toggle button uses:
```tsx
function ThemeToggle() {
const [isDark, setIsDark] = useState(() =>
document.documentElement.classList.contains('dark')
);
const toggle = () => {
document.documentElement.classList.toggle('dark');
setIsDark(d => !d);
localStorage.theme = isDark ? 'light' : 'dark';
};
return <button onClick={toggle}>...</button>;
}
```
Only the toggle button re-renders. No context, no provider, no cascade. Accent color works the same way -- set a CSS variable on `<html>`, no React re-render.
**Prevention:**
- The generated script should not attempt to change execution policy
- Any sub-scripts should be invoked with `-ExecutionPolicy Bypass` in the powershell.exe call, or avoided entirely (inline everything)
- Document this in the wizard's "Intune deployment" output pane
**Warning signs:**
- Form inputs losing focus when toggling dark mode
- Visible flicker across the entire wizard when toggling
- React DevTools profiler showing all components re-rendering on theme change
**Detection:**
- `Get-ExecutionPolicy -List` on target machine shows `MachinePolicy = AllSigned`
- Script works in test but fails on managed fleet endpoints
**Phase relevance:** Script generation phase; testing guidance
**Phase to address:**
Phase 1 (Theme Foundation) -- architecture decision: CSS class approach, not React Context for theming.
---
### Pitfall 7: 32-bit vs 64-bit PowerShell host affects path resolution
### Pitfall 7: Form Accessibility Regressions During Restyling
**What goes wrong:**
Intune's default is to run scripts in the 32-bit PowerShell host (`Run script in 64-bit PowerShell host = No`). On 64-bit Windows, 32-bit processes use File System Redirector: `System32` resolves to `SysWOW64`, and `%ProgramFiles%` resolves to `%ProgramFiles(x86)%`. If the generated script installs rclone to `$env:ProgramFiles\rclone\` under 32-bit context, the binary lands in `C:\Program Files (x86)\rclone\`, not `C:\Program Files\rclone\`. When the user runs rclone from a 64-bit shell, they look in `Program Files` and find nothing.
The current FieldRenderer has proper `<label htmlFor>` and `<input id>` associations, error message display, and tooltip buttons with `aria-label`. During restyling, these connections break: labels get separated from inputs by decorative wrapper divs, error messages lose their visual proximity, or card wrappers introduce unexpected tab stops.
**Why it happens:**
The Intune "Run in 64-bit" option defaults to `No` per Microsoft docs. Developers test in a normal 64-bit PowerShell session.
Visual-focused restyling treats JSX as a canvas for layout. Developers restructure DOM for card layouts, add icon containers, or wrap form groups in Material-style "outlined" containers. The label-input-error chain relies on specific DOM relationships. The FieldRenderer currently does NOT use `aria-describedby` for error messages -- this is already noted as v1.1 tech debt. Restyling is the right time to fix this, but also the highest risk time to break what works.
**Consequences:**
- rclone binary installed to wrong Program Files variant
- PATH entries or shortcuts point to non-existent location
**How to avoid:**
1. Fix the `aria-describedby` gap as part of the restyling, not separately. Add `aria-describedby={error ? \`${field.key}-error\` : undefined}` to inputs and `id={\`${field.key}-error\`}` to error paragraphs.
2. After restyling each form component, verify: label click focuses the input, error messages are associated with inputs, tab order follows visual order.
3. Keep the `<label htmlFor={field.key}>` + `<input id={field.key}>` pattern intact regardless of wrapper changes.
**Prevention:**
- The generated script should use `$env:ProgramW6432` (always the native 64-bit Program Files on 64-bit Windows) or hardcode `C:\Program Files\rclone\`
- The wizard UI for Intune output should recommend enabling "Run script in 64-bit PowerShell host" and document why
- Alternatively, use `C:\ProgramData\rclone\` which is not subject to WOW64 redirection
**Warning signs:**
- Clicking a label no longer focuses its input
- Tab key skips inputs or gets trapped in decorative elements
- Browser form autofill stops working on restyled inputs
**Detection:**
- rclone binary absent from expected path after deployment
- `[System.Environment]::Is64BitProcess` returns `False` inside the running script
**Phase relevance:** Script generation phase; Intune deployment option
**Phase to address:**
Phase 2 (Component Overhaul) -- every form component restyle must include an accessibility verification step. Resolve the v1.1 aria tech debt item here rather than deferring again.
---
### Pitfall 8: OAuth-backed backends require interactive browser flow — incompatible with SYSTEM/headless deployment
## Technical Debt Patterns
**What goes wrong:**
rclone backends that use OAuth (OneDrive, Google Drive, Dropbox, Box, etc.) require an interactive browser authorization step to generate the token. The rclone config for these backends includes an `token = {...}` JSON blob. If the IT admin generates a config without pre-populating this token, the deployment script writes a config with no token. When rclone first runs on the endpoint, it attempts an interactive browser flow — which silently fails or hangs in a SYSTEM/headless context.
Shortcuts that seem reasonable but create long-term problems.
**Why it happens:**
The wizard generates the config from form inputs. For OAuth backends, the wizard cannot complete the OAuth flow on behalf of the user — there is no rclone running in the browser context to perform `rclone config`. The IT admin might not realize the token needs to be obtained separately on a reference machine.
| Shortcut | Immediate Benefit | Long-term Cost | When Acceptable |
|----------|-------------------|----------------|-----------------|
| Adding `dark:` to every className instead of tokens | Fast, no refactor needed | 73+ locations to maintain, every new component needs dual classes | Never -- token approach costs the same upfront and scales |
| Using `!important` to fix specificity issues | Immediate visual fix | Cascading specificity arms race, impossible to override later | Never |
| Storing theme only in React state (not localStorage) | Simpler code | Preference lost on refresh, FOUC on every load | Never -- localStorage + inline script is trivial |
| Skipping contrast verification "will check later" | Faster shipping | Accessibility failures discovered post-ship, painful to retroactively audit all 73 class locations | Never -- check during each component restyle |
| Building a full design system with token categories for every MD3 role | "Complete" spec adherence | Over-engineered for a 4-step wizard with ~10 components; 80% of tokens go unused | Never for this app -- pick the 15-20 tokens that matter |
| Copying MD3 token names verbatim (md-sys-color-surface-container-highest) | Matches Google spec exactly | Verbose, unfamiliar to Tailwind developers, poor DX for a small team | Never -- use simplified semantic names (surface, on-surface, primary) |
| Adding MUI or another component library for "proper" MD3 | Instant MD3 components | +200KB bundle, specificity wars with Tailwind, two styling systems to maintain | Never for this app -- 10 components do not justify a library |
**Consequences:**
- Deployed rclone silently does nothing or opens a browser on the endpoint
- Most prominent with OneDrive; affects any backend requiring `rclone authorize`
## Integration Gotchas
**Prevention:**
- For OAuth backends, the wizard must show a prominent notice: "This backend requires an OAuth token. You must run `rclone config` or `rclone authorize` on a reference Windows machine as the target user, then copy the resulting token value into this wizard."
- The wizard should provide a dedicated "OAuth token" input field for token-based backends, with instructions for how to extract the token from `rclone config show remotename`
- Consider warning against deploying OAuth backends via SYSTEM-context Intune scripts entirely; recommend user-context deployment instead
Common mistakes when connecting theme infrastructure to existing systems.
**Detection:**
- Config section for OneDrive/GDrive has no `token =` line
- rclone first-run opens a browser on the endpoint or exits with "no token found"
| Integration | Common Mistake | Correct Approach |
|-------------|----------------|------------------|
| react-hook-form + restyled inputs | Wrapping `<input>` in a custom component that breaks `register()` ref forwarding | Use `React.forwardRef` on any custom input wrapper, or keep native `<input>` with Tailwind classes (preferred for this app) |
| Zod validation + error display | Moving error `<p>` tags away from their input during restyle, breaking visual association | Keep error message immediately after its input in DOM order; add `aria-describedby` |
| WizardContext + theme toggle | Creating a ThemeContext provider that causes WizardProvider consumers to re-render | Theme via CSS class on `<html>` (zero React re-renders), NOT via React context |
| CSS hidden auth toggles (AzureAuthToggle / SftpAuthToggle) | Restyling visible state but forgetting the hidden state, breaking `className="hidden"` pattern | Verify both auth toggle states render correctly in both light and dark modes |
| StepIndicator inline styles | Replacing `style={{ fontWeight: 'bold' }}` with Tailwind classes but altering text content structure | Replace inline styles with Tailwind classes (`font-bold`, `font-normal`, `text-muted`) while keeping text content strings identical for test compatibility |
| BackendCard selection state | Changing selection indicator (e.g., border color) to use tokens but forgetting dark mode variant | Selected card must be visually distinct in both themes; test with all 7 backends |
**Phase relevance:** Backend-specific configuration phase; wizard backend selection step
## Performance Traps
---
| Trap | Symptoms | Prevention | When It Breaks |
|------|----------|------------|----------------|
| Theme stored in React Context causing re-renders | All 4 wizard steps re-render on every toggle; form focus lost | CSS class on `<html>`, no React context for theme | Immediate on every toggle |
| Importing full component library for 10 components | Bundle doubles (+200KB gzipped for MUI) | Build MD3 styles with Tailwind tokens; zero additional dependencies | Immediate -- slower first load |
| CSS transition on every property during theme switch | 200ms lag on every element when toggling dark mode | Transition only `background-color` and `color` on body; skip borders/shadows | Noticeable with 50+ DOM elements |
| Over-using CSS custom properties on every element | Slow repaints when toggling theme on low-end devices | Define tokens on `:root` / `.dark`, let inheritance cascade naturally | On low-end devices or with 100+ custom properties |
### Pitfall 9: SAS tokens and storage keys contain characters that need escaping in INI values
## UX Pitfalls
**What goes wrong:**
Azure SAS tokens contain `%`, `=`, `&`, and `+` characters. Azure storage keys contain `+` and `/` and end in `==`. In rclone's INI config format, values are read until end-of-line — no quoting needed for most characters — but if the value accidentally contains a line-break (e.g., from copy-paste in a browser field that wraps), the config is truncated silently. If the generated value is also used inside a PowerShell string interpolation (e.g., `"sas_url = $sasToken"`), PowerShell variable substitution can corrupt values containing `$`.
| Pitfall | User Impact | Better Approach |
|---------|-------------|-----------------|
| Dark mode toggle buried in settings | IT pros who want dark mode cannot find it | Visible toggle in app header, immediately accessible |
| No system preference detection | User has OS dark mode, app loads light | Default to OS preference via `prefers-color-scheme`, with manual override stored in localStorage |
| Accent color picker with unlimited options | Analysis paralysis, clashing colors | 3-5 curated accent colors that all pass contrast checks in both themes |
| Theme transition animation on every element | Jarring, slow, distracting on toggle | Subtle 150ms transition on background-color and color on body only |
| Dark mode applied but OutputBlock code still light | Inconsistent feel in the most important step (Review/download) | OutputBlock must respect dark mode for generated config and script previews |
| Security warning banner lost in dark mode | Users miss the credential security warning before download | Warning must remain high-contrast and prominent (use error/warning token colors) in both modes |
**Why it happens:**
The wizard builds the config as a JavaScript template literal. Storage keys and SAS tokens pasted by users may include trailing newlines or spaces. PowerShell double-quoted strings treat `$` as variable prefix.
## "Looks Done But Isn't" Checklist
**Consequences:**
- Truncated SAS token causes authentication failures with opaque Azure storage errors
- Corrupted key causes "AuthenticationFailed" from Azure
Things that appear complete but are missing critical pieces.
**Prevention:**
- Trim all credential inputs in the wizard before inserting into the config (strip leading/trailing whitespace including `\n`, `\r`)
- In the PowerShell script template, use single-quoted strings for the config content (PowerShell single-quoted strings do not interpolate `$`):
```powershell
$configContent = @'
[myremote]
type = azureblob
account = mystorageaccount
key = ABC+xyz==
'@
```
(here-string with single-quote terminator)
- Validate that credential inputs do not contain newlines before generating
- [ ] **Dark mode select dropdowns:** Browser renders `<option>` elements with OS colors -- white dropdown menus appear on dark backgrounds on some browsers. Verify on Chrome, Firefox, Edge.
- [ ] **Dark mode scrollbars:** Light scrollbars on dark backgrounds look broken. Apply `scrollbar-color` CSS property or use `dark` color-scheme.
- [ ] **Error text contrast:** `text-red-600` on dark backgrounds has insufficient contrast. Must use lighter red (equivalent of `text-red-400`) in dark mode via tokens.
- [ ] **Focus rings in dark mode:** `focus:ring-blue-300` is nearly invisible on dark backgrounds. Must use `focus:ring-blue-500` equivalent in dark mode.
- [ ] **Placeholder text in dark mode:** Light gray placeholder text vanishes on dark input backgrounds. Verify placeholder is visible in both modes.
- [ ] **Security warning banner:** The credential warning in ReviewStep must remain prominent and high-contrast in dark mode (not just "inverted").
- [ ] **Accent color + dark background:** Verify every accent color option still passes WCAG AA 4.5:1 against the dark surface background.
- [ ] **BackendCard hover and selected states:** Card states must be visually distinguishable in both modes with all 7 backends.
- [ ] **Disabled button contrast:** Disabled buttons using lower opacity reduce contrast further on dark backgrounds. Use distinct disabled token colors instead of opacity.
- [ ] **PasswordField show/hide toggle:** The eye icon/button must be visible in both themes.
- [ ] **Tooltip info boxes:** The `bg-blue-50 border-blue-200 text-blue-700` tooltip in FieldRenderer needs a dark mode equivalent that maintains readability.
**Detection:**
- Config file, when opened, shows a truncated key value
- rclone error: "failed to parse config file" or Azure "AuthenticationFailed"
## Recovery Strategies
**Phase relevance:** Config generation logic (core phase)
When pitfalls occur despite prevention, how to recover.
---
| Pitfall | Recovery Cost | Recovery Steps |
|---------|---------------|----------------|
| Hardcoded colors everywhere (no tokens) | MEDIUM | Extract to CSS variables in one pass, then find-replace all 73 className usages. ~2 hours for this codebase. |
| Test suite broken by batch restyle | LOW-MEDIUM | `git stash` the batch change, restyle one component at a time verifying tests between each. |
| FOUC on dark mode | LOW | Add 5-line inline script to `index.html <head>`. 10-minute fix. |
| Specificity conflicts from component library | HIGH | Remove component library, rebuild styles with Tailwind. Prevention is far cheaper than recovery. |
| Accessibility regressions in forms | MEDIUM | Audit with browser accessibility tools, fix label/input/aria associations. Harder to find than to fix. |
| Dark mode contrast failures | LOW-MEDIUM | With centralized tokens: update token values once. Without tokens: hunt through all 73 className usages. |
| Theme context re-renders | LOW | Remove ThemeContext, move to CSS class approach. ~30 minutes if caught early. |
### Pitfall 10: Windows path length limit (MAX_PATH = 260) breaks rclone operations on deep directory trees
## Pitfall-to-Phase Mapping
**What goes wrong:**
On Windows versions before Windows 10 1607, and on any Windows where the Long Path registry key is not set, paths exceeding 260 characters cause rclone operations to fail silently or with cryptic I/O errors. rclone syncing deep SharePoint or OneDrive folder trees commonly hits this. The deployment script may also fail if it writes files to paths that are too long (e.g., user profile paths with long usernames inside long corporate folder structures).
**Why it happens:**
Windows enforces MAX_PATH = 260 by default per `kernel32.dll`. IT admins don't control the endpoint's registry setting. The wizard generates scripts without path-length guards.
**Consequences:**
- rclone skips or errors on files with long paths
- `New-Item` or `Set-Content` in the PowerShell script itself can fail if the config destination path is long
**Prevention:**
- The wizard should recommend using `C:\ProgramData\rclone\` (short path) for config and binary placement, not user-profile paths
- Generated scripts should include a check and optionally enable long paths:
```powershell
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1
```
(requires admin rights; typically available in SYSTEM context)
- Document the limitation in the wizard output pane for Intune deployments
**Detection:**
- rclone logs show `ERROR: ... path too long`
- PowerShell script itself fails with "The specified path, file name, or both are too long"
**Phase relevance:** Script generation; deployment documentation phase
---
## Minor Pitfalls
---
### Pitfall 11: Intune script runs once per device; config changes don't re-deploy unless script is modified
**What goes wrong:**
Intune only re-runs a PowerShell script if the script content changes or is reassigned. If the IT admin generates a new config (different credentials, different remote name) and wants to update the deployed config, they must upload a new version of the script to Intune. If they upload the exact same script bytes with only a comment changed, the re-run is triggered. But if they don't know this, they think re-assigning the unchanged script will update endpoints — it won't.
**Prevention:**
- Document this in the wizard output: "To update config on endpoints, modify and re-upload the script (e.g., bump a version comment) to trigger Intune re-execution."
- Consider auto-inserting a `# Generated: [timestamp]` comment in each script so re-generated scripts always differ
**Phase relevance:** Documentation/UX phase
---
### Pitfall 12: rclone binary download URL in the script becomes stale
**What goes wrong:**
The generated script contains a hardcoded rclone download URL (e.g., `https://downloads.rclone.org/rclone-current-windows-amd64.zip`). rclone uses the filename `rclone-current-*` as a redirect alias. This URL is stable, but if the wizard hardcodes a specific version URL (e.g., `v1.68.0`) to ensure repeatability, that version URL remains functional but the binary may have known issues. If the wizard uses `current`, the binary silently upgrades, potentially introducing breaking changes.
**Prevention:**
- Use the `rclone-current-windows-amd64.zip` alias for the default path (always latest stable)
- Allow an override field for IT admins who want to pin a version
- Add a comment in the generated script stating the resolved version strategy
**Phase relevance:** Script generation (rclone install option)
---
### Pitfall 13: Generated config has Windows-style line endings that cause issues on cross-platform rclone use
**What goes wrong:**
JavaScript running in a browser on Windows may produce `\r\n` line endings when building the config string (less likely with modern JS but possible with string concatenation involving platform newlines). rclone's INI parser handles `\r\n` correctly on Windows, but if the config is later copied to a Linux/macOS system, the `\r` characters appear in values.
**Prevention:**
- Explicitly normalize line endings to `\n` in the config generation logic before download
- Use `content.replace(/\r\n/g, '\n').replace(/\r/g, '\n')` before creating the Blob for download
**Phase relevance:** Config generation (frontend logic)
---
## Phase-Specific Warnings
| Phase Topic | Likely Pitfall | Mitigation |
|-------------|---------------|------------|
| Script template design | SYSTEM context config path mismatch (Pitfall 1) | Use machine-wide path; document context options |
| rclone install option | 200 KB Intune script size limit (Pitfall 2) | Download-only; never embed binary |
| Script file write logic | PowerShell encoding writes UTF-16 BOM (Pitfall 3) | Use `[System.IO.File]::WriteAllText` with explicit UTF-8 no-BOM |
| Remote name input field | Invalid characters in section name (Pitfall 4) | Validate `[a-zA-Z0-9_-]` in UI before generation |
| Debug/error output in script | Secrets exposed in Intune logs (Pitfall 5) | No credential interpolation in output cmdlets |
| Execution policy in script | GP overrides any Set-ExecutionPolicy call (Pitfall 6) | Do not set policy; use `-ExecutionPolicy Bypass` on sub-processes |
| Intune script options | 32-bit host path redirection (Pitfall 7) | Use `$env:ProgramW6432` or `C:\ProgramData\rclone\` |
| OAuth backend config | Headless OAuth flow impossible (Pitfall 8) | Require pre-obtained token; prominent wizard warning |
| Credential input handling | SAS/key corruption via whitespace or `$` (Pitfall 9) | Trim inputs; single-quoted PowerShell here-strings |
| Config/binary placement | MAX_PATH exceeded on deep trees (Pitfall 10) | Short machine-wide paths; optionally enable long paths |
| Re-deployment UX | Intune won't re-run identical script (Pitfall 11) | Auto-insert timestamp comment; document update flow |
| rclone download URL | Pinned URL goes stale (Pitfall 12) | Default to `rclone-current`; allow version override |
| Config string generation | Windows CRLF in config file (Pitfall 13) | Normalize to LF before Blob creation |
---
| Pitfall | Prevention Phase | Verification |
|---------|------------------|--------------|
| Tailwind v4 dark mode config (P1) | Phase 1: Theme Foundation | `dark:bg-gray-900` toggles correctly via `.dark` class on `<html>` |
| Hardcoded colors (P2) | Phase 1 (Tokens) + Phase 2 (Overhaul) | Zero hardcoded Tailwind color classes remain in component JSX |
| Dark mode contrast failures (P3) | Phase 2: Component Overhaul | Every text/background pair checked, all pass WCAG AA 4.5:1 |
| Breaking test selectors (P4) | Every phase | All 159 tests pass after each individual component restyle |
| FOUC (P5) | Phase 1: Theme Foundation | Load app with `localStorage.theme = 'dark'`, verify no white flash |
| Theme context re-renders (P6) | Phase 1: Architecture Decision | Theme toggle causes zero React re-renders outside the toggle button itself |
| Form accessibility regressions (P7) | Phase 2: Component Overhaul | Label-input associations verified, `aria-describedby` added for all error messages |
| CSS specificity conflicts | Phase 1: Architecture Decision | Decision documented: no component library, Tailwind-only approach |
| Over-engineering design system | Phase 1: Token Definition | Token count stays under 20 semantic colors; no unused token categories |
## Sources
- Microsoft Learn — PowerShell scripts in Intune (updated 2025-10-02): https://learn.microsoft.com/en-us/intune/intune-service/apps/powershell-scripts
- Microsoft Learn — Intune Management Extension (updated 2026-03-17): https://learn.microsoft.com/en-us/intune/intune-service/apps/intune-management-extension
- Microsoft Learn — Set-ExecutionPolicy reference (updated 2025-04-15): https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-executionpolicy
- Microsoft Learn — Naming Files, Paths, and Namespaces (Win32): https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
- Microsoft Learn — Code Page Identifiers: https://learn.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
- rclone config format and Windows behavior: training data (MEDIUM confidence; rclone official docs were inaccessible during research session — verify against https://rclone.org/docs/ before finalizing)
- [Tailwind CSS v4 Dark Mode docs](https://tailwindcss.com/docs/dark-mode) -- official, verified (HIGH confidence)
- [Tailwind v4 upgrade discussion #16517](https://github.com/tailwindlabs/tailwindcss/discussions/16517) -- community reports of broken dark mode after upgrade
- [Tailwind specificity discussion #12714](https://github.com/tailwindlabs/tailwindcss/discussions/12714) -- class collisions with component libraries
- [BOIA: Dark Mode and WCAG Contrast](https://www.boia.org/blog/offering-a-dark-mode-doesnt-satisfy-wcag-color-contrast-requirements) -- dark mode does not auto-satisfy WCAG
- [Complete Dark Mode Accessibility Guide (2026)](https://blog.greeden.me/en/2026/02/23/complete-accessibility-guide-for-dark-mode-and-high-contrast-color-design-contrast-validation-respecting-os-settings-icons-images-and-focus-visibility-wcag-2-1-aa/) -- WCAG 2.1 AA guidance for dark mode
- [MUI MD3 adoption discussion #29345](https://github.com/mui/material-ui/issues/29345) -- MD3 implementation complexity
- [React Context performance optimization](https://medium.com/zestgeek/performance-optimization-techniques-with-reacts-usecontext-5dc7e4ef6b25) -- re-render prevention patterns
- Codebase analysis: 73 className usages, 131 test selectors across 5 files, 3 inline styles in StepIndicator, FieldRenderer aria-describedby gap confirmed (HIGH confidence -- direct code inspection)
---
*Pitfalls research for: UI polish overhaul (MD3, dark mode, accent colors) on Ready2Blob v1.2*
*Researched: 2026-03-31*
+216 -89
View File
@@ -1,52 +1,189 @@
# Technology Stack
**Project:** Ready2Blob
**Researched:** 2026-03-26
**Confidence note:** External verification tools were unavailable in this session. Version numbers reflect training data (knowledge cutoff August 2025). Verify all versions against npmjs.com before scaffolding.
**Project:** Ready2Blob v1.2 — UI Polish & MD3 Overhaul
**Researched:** 2026-03-31
**Scope:** Stack ADDITIONS for Material Design 3, dark mode, accent colors, responsive improvements
**Existing stack (validated, not re-researched):** Vite 6, React 18, TypeScript 5, Tailwind v4 (@tailwindcss/vite), react-hook-form 7, Zod 4, Vitest 4, JSZip
---
## Recommended Stack
## Recommendation: Zero New Runtime Dependencies
### Core Framework
The v1.2 UI overhaul should be achieved with **Tailwind v4's native theming system + hand-authored MD3 design tokens in CSS**. No component library. No runtime theming library. The existing approach (semantic HTML + Tailwind utilities) is the right foundation -- it just needs a proper token system layered on top.
**Rationale:** The app currently has zero component library dependencies and 159 passing tests. Introducing a component library (Material Tailwind, MUI, shadcn/ui) at this stage would:
1. Require rewriting every existing component to match the library's API
2. Break existing tests that assert on current DOM structure
3. Add bundle weight for a wizard that needs at most 6-8 component types
4. Create upgrade debt for a library the team doesn't control
Instead: define MD3 tokens as CSS custom properties, wire them into Tailwind v4's `@theme` directive, and build the small set of reusable patterns (card, input, button, elevation) as project-owned Tailwind utility compositions.
---
## New Stack Additions
### Design Token Generation (Dev Dependency Only)
| Technology | Version | Purpose | Why |
|------------|---------|---------|-----|
| React | 18.x | Component model, state management, rendering | Largest ecosystem, best-in-class multi-step form libraries all target React first. Hooks (useState, useReducer, useContext) provide exactly the right mental model for a wizard: local step state + shared config accumulator state. No SSR needed — this is 100% client-side rendering. |
| Vite | 5.x | Build tooling, dev server, static asset bundling | Near-zero config for a React SPA. `vite build` produces a static `dist/` folder deployable to GitHub Pages, Netlify, or any CDN with no server required. HMR makes iteration fast. Replaces CRA, which is abandoned. Replaces Webpack, which requires painful configuration for something this simple. |
| TypeScript | 5.x | Type safety | rclone config generation involves composing structured data (backend type, required fields per backend, optional flags) into string templates. TypeScript catches the inevitable "wrong field name" bugs at compile time rather than at user download time. The marginal overhead is worth it for a tool where correctness of generated output is the entire product. |
| `@material/material-color-utilities` | 0.4.0 | Generate MD3 color palettes from seed color | Official Google library. Used at build/dev time via a small script to generate light + dark token sets from a single seed color. NOT bundled into the app -- it produces static CSS custom properties. This is the same algorithm the Material Theme Builder uses. HIGH confidence (official Google package, actively maintained). |
### Styling
**How it works:** Write a one-time Node script (`scripts/generate-theme.ts`) that:
1. Takes a seed color (hex)
2. Uses `themeFromSourceColor()` to generate full MD3 palette (primary, secondary, tertiary, error, surface, outline, etc.)
3. Outputs CSS custom properties in the `--md-sys-color-*` naming convention
4. Writes to `src/theme-tokens.css` which is imported into `src/index.css`
This means the generated tokens are **static CSS** -- zero runtime cost, zero bundle impact from the color library.
### Tailwind v4 Theme Integration (No New Dependency)
| Technology | Version | Purpose | Why |
|------------|---------|---------|-----|
| Tailwind CSS | 3.x | Utility-first styling | No design system to maintain — each step of the wizard is a one-off layout. Tailwind's inline classes mean styling stays co-located with markup, avoiding CSS file sprawl. For a tool likely built by one or two developers, it eliminates the "where does this class live?" question. Avoid CSS Modules (too much file switching) and styled-components (runtime overhead, no benefit here). |
| Tailwind v4 `@theme` directive | Already installed | Map MD3 tokens to Tailwind utility classes | Tailwind v4's `@theme` directive creates utility classes from CSS custom properties. Defining `--color-primary`, `--color-surface`, etc. in `@theme` blocks automatically generates `bg-primary`, `text-on-primary`, `bg-surface` utilities. No config file needed -- pure CSS. HIGH confidence (verified in official Tailwind v4 docs). |
| Tailwind v4 `@custom-variant` | Already installed | Class-based dark mode toggle | Tailwind v4 replaces the old `darkMode: 'class'` config with `@custom-variant dark (&:where(.dark, .dark *));` in CSS. This enables the `dark:` prefix to respond to a `.dark` class on the HTML element. HIGH confidence (verified in official Tailwind v4 dark mode docs). |
### Form & Wizard State
### No Other New Dependencies
| Technology | Version | Purpose | Why |
|------------|---------|---------|-----|
| react-hook-form | 7.x | Per-step form validation and field registration | The standard for React forms. Uncontrolled inputs with ref-based validation means no re-render on every keystroke — important when some wizard steps may have 10+ fields (e.g., S3 config). Native Zod integration via `@hookform/resolvers` allows schema-driven validation that mirrors the rclone backend field spec. |
| Zod | 3.x | Schema definition and runtime validation | Per-backend field schemas (required vs optional, string format, enum values) map directly to Zod schemas. A `backends/azure.ts`, `backends/s3.ts`, etc. pattern lets each backend declare its own schema — react-hook-form validates against it per step. This is the correct abstraction: the schema IS the backend spec. |
| Category | Decision | Rationale |
|----------|----------|-----------|
| Component library | **Do NOT add** | Current semantic HTML + Tailwind is correct. MD3 styling is achieved through tokens + utility classes, not through library components. |
| CSS-in-JS | **Do NOT add** | Tailwind v4 handles everything via CSS. Adding styled-components or Emotion would conflict with the existing Tailwind approach. |
| Theme toggle library (next-themes) | **Do NOT add** | next-themes is Next.js-focused. For a pure Vite SPA, a 15-line React hook (`useTheme`) with `localStorage` + `classList.toggle` is all that's needed. |
| Animation library | **Do NOT add** | MD3 motion tokens (duration, easing) are CSS custom properties. Tailwind v4's `@theme` can define transition tokens. No framer-motion or similar needed for the subtle transitions in a wizard UI. |
| Icon library | **Evaluate later** | If MD3 icons are desired, `@material-design-icons/svg` provides tree-shakeable SVGs. But this is a nice-to-have, not a v1.2 blocker. |
### File Generation & Download
---
| Technology | Version | Purpose | Why |
|------------|---------|---------|-----|
| Native Blob API | — (browser built-in) | Text file download (rclone.conf, .ps1 scripts) | No library needed. `new Blob([content], { type: 'text/plain' })` + `URL.createObjectURL()` + programmatic anchor click is the standard pattern for single-file downloads. Zero dependency, works in all modern browsers. Using a library for this adds complexity without benefit. |
| JSZip | 3.x | ZIP bundling of all generated files | When the user wants to download all files at once (rclone.conf + Intune script + RMM script), a ZIP is far better UX than three separate downloads. JSZip is the de-facto standard for client-side ZIP in browsers, actively maintained, no server required. `file-saver` is often paired with it for the `saveAs()` convenience but the Blob/anchor pattern works fine without it. |
## Detailed Integration Plan
### State Management
### 1. MD3 Color Token System
| Technology | Version | Purpose | Why |
|------------|---------|---------|-----|
| React built-ins (useState / useReducer / useContext) | 18.x | Wizard state, accumulated config object | No external state library needed. The wizard has one primary data structure: the accumulating rclone config object (backend type + per-backend fields + script options). A `useReducer` at the app root with a context provider gives all steps read/write access without prop drilling. This is a solved problem at this scale — Zustand/Redux are overkill. |
The Material Design 3 color system uses ~29 semantic color roles (not raw palette values). These map to CSS custom properties:
### Hosting / Deployment
```css
/* Light theme tokens (generated from seed color) */
:root {
--md-sys-color-primary: #006A6A;
--md-sys-color-on-primary: #FFFFFF;
--md-sys-color-primary-container: #6FF7F6;
--md-sys-color-on-primary-container: #002020;
--md-sys-color-secondary: #4A6363;
--md-sys-color-on-secondary: #FFFFFF;
--md-sys-color-surface: #FAFDFC;
--md-sys-color-on-surface: #191C1C;
--md-sys-color-surface-container: #EFF2F1;
--md-sys-color-surface-container-low: #F4F7F6;
--md-sys-color-surface-container-high: #E9ECEB;
--md-sys-color-outline: #6F7979;
--md-sys-color-outline-variant: #BEC9C8;
--md-sys-color-error: #BA1A1A;
--md-sys-color-on-error: #FFFFFF;
/* ... ~29 roles total */
}
| Technology | Version | Purpose | Why |
|------------|---------|---------|-----|
| GitHub Pages or Netlify (free tier) | — | Static hosting | The output of `vite build` is a folder of HTML/CSS/JS. Any static host works. GitHub Pages is zero-cost and integrates directly with the repository. Netlify adds deploy previews for PRs, which is useful for validating config generation changes. No server needed at either. |
/* Dark theme tokens (same seed, dark scheme) */
.dark {
--md-sys-color-primary: #4EDADA;
--md-sys-color-on-primary: #003737;
--md-sys-color-surface: #101414;
--md-sys-color-on-surface: #E0E3E2;
/* ... all roles overridden */
}
```
### 2. Tailwind v4 Theme Wiring
```css
/* src/index.css */
@import "tailwindcss";
@import "./theme-tokens.css"; /* Generated MD3 tokens */
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* Map MD3 tokens to Tailwind color utilities */
--color-primary: var(--md-sys-color-primary);
--color-on-primary: var(--md-sys-color-on-primary);
--color-primary-container: var(--md-sys-color-primary-container);
--color-on-primary-container: var(--md-sys-color-on-primary-container);
--color-secondary: var(--md-sys-color-secondary);
--color-on-secondary: var(--md-sys-color-on-secondary);
--color-surface: var(--md-sys-color-surface);
--color-on-surface: var(--md-sys-color-on-surface);
--color-surface-container: var(--md-sys-color-surface-container);
--color-surface-container-low: var(--md-sys-color-surface-container-low);
--color-surface-container-high: var(--md-sys-color-surface-container-high);
--color-outline: var(--md-sys-color-outline);
--color-outline-variant: var(--md-sys-color-outline-variant);
--color-error: var(--md-sys-color-error);
--color-on-error: var(--md-sys-color-on-error);
/* MD3 Shape scale */
--radius-xs: 4px;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 28px;
--radius-full: 9999px;
/* MD3 Elevation (box-shadows) */
--shadow-elevation-0: none;
--shadow-elevation-1: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 1px 3px 1px rgb(0 0 0 / 0.15);
--shadow-elevation-2: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 2px 6px 2px rgb(0 0 0 / 0.15);
--shadow-elevation-3: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 4px 8px 3px rgb(0 0 0 / 0.15);
--shadow-elevation-4: 0 2px 3px 0 rgb(0 0 0 / 0.3), 0 6px 10px 4px rgb(0 0 0 / 0.15);
--shadow-elevation-5: 0 4px 4px 0 rgb(0 0 0 / 0.3), 0 8px 12px 6px rgb(0 0 0 / 0.15);
/* MD3 Motion tokens */
--animate-md3-enter: md3-enter 0.2s cubic-bezier(0, 0, 0, 1);
--animate-md3-exit: md3-exit 0.15s cubic-bezier(0.3, 0, 1, 1);
@keyframes md3-enter {
from { opacity: 0; transform: scale(0.92); }
to { opacity: 1; transform: scale(1); }
}
@keyframes md3-exit {
from { opacity: 1; transform: scale(1); }
to { opacity: 0; transform: scale(0.92); }
}
}
```
**Usage in components** then becomes natural Tailwind:
```tsx
<div className="bg-surface-container rounded-md shadow-elevation-1 dark:shadow-elevation-2">
<h2 className="text-on-surface">Step Title</h2>
<button className="bg-primary text-on-primary rounded-full px-6 py-2">
Next
</button>
</div>
```
### 3. Dark Mode Toggle Hook
No library needed. A simple React hook:
```typescript
// src/hooks/useTheme.ts
function useTheme() {
const [theme, setTheme] = useState<'light' | 'dark' | 'system'>(() => {
return (localStorage.getItem('theme') as 'light' | 'dark') ?? 'system';
});
// Toggle .dark class on <html>, persist to localStorage
// Listen to prefers-color-scheme for 'system' mode
}
```
### 4. Accent Color System
For user-selectable accent colors, the approach is:
1. Offer 3-5 preset seed colors (not arbitrary color picker)
2. Pre-generate token sets for each seed color at build time
3. Switch accent by swapping a CSS class on `<html>` that loads a different set of `--md-sys-color-*` variables
This avoids runtime color generation (which would require bundling `@material/material-color-utilities`).
---
@@ -54,65 +191,53 @@
| Category | Recommended | Alternative | Why Not |
|----------|-------------|-------------|---------|
| Framework | React 18 | Vue 3 | Vue is a reasonable choice but the wizard library ecosystem (react-hook-form, Formik) is React-first. No strong reason to diverge. |
| Framework | React 18 | Svelte / SvelteKit | Svelte has no widely-adopted multi-step form library. Would require hand-rolling wizard state. The compile-time model is elegant but not worth the ecosystem tradeoff here. |
| Build tool | Vite | Create React App | CRA is officially deprecated by the React team. Not a valid choice for new projects in 2025. |
| Build tool | Vite | Next.js | Next.js is a server-framework. Using it for a pure static SPA adds file-based routing conventions, SSR plumbing, and deployment assumptions that are all irrelevant here. `vite + react` is simpler and more appropriate. |
| Styling | Tailwind CSS | Material UI / shadcn/ui | shadcn/ui is worth considering as a component library for accessible form elements (inputs, selects, checkboxes). It is built on Radix UI primitives and works with Tailwind. If the team wants pre-built accessible components rather than raw HTML + Tailwind, shadcn/ui is the right addition — not a replacement for Tailwind, but a layer on top. |
| Forms | react-hook-form | Formik | Formik is older and uses controlled inputs (re-render on every keystroke). react-hook-form is the current standard and has better performance and Zod integration. |
| ZIP | JSZip | fflate | fflate is faster and smaller than JSZip. Both are valid. JSZip has more documentation and community examples for the browser download pattern, making it easier to implement correctly without prior experience. If bundle size becomes a concern, swap to fflate. |
| State | useReducer + Context | Zustand | Zustand is excellent but unnecessary at this scale. No async state, no complex selectors needed. Adding a dependency for something React itself handles cleanly is not justified. |
---
## Recommended shadcn/ui Addition
**Use shadcn/ui for form components.** shadcn/ui is not a dependency — it is a code generator. Running `npx shadcn-ui@latest add button input select checkbox` copies accessible, Tailwind-styled components into your project. These components are owned by the project (not a node_module) and fully customizable. For a wizard with many form inputs, this provides:
- Accessible labels, focus states, error message patterns out of the box
- Consistent visual design without a custom design system
- Radix UI primitives under the hood (keyboard navigation, ARIA) at no extra runtime cost
This is the current 2025 best practice for React + Tailwind projects.
| Component library | None (keep semantic HTML + Tailwind) | Material Tailwind v3 | Material Tailwind is React + Tailwind but: (a) requires rewriting all existing components, (b) v3 is still in pre-order/beta, (c) adds ~50KB+ bundle weight for components the wizard doesn't need. Not worth the rewrite cost for 6-8 component types. |
| Component library | None | MUI (Material UI) | MUI uses Emotion CSS-in-JS, fundamentally conflicts with Tailwind. Would require ripping out Tailwind entirely. Wrong direction. |
| Component library | None | shadcn/ui | Good library but opinionated toward Radix primitives. Adding it now means learning a new component API while also implementing MD3 tokens. For v1.2 scope (cards, inputs, buttons, toggles), hand-authored Tailwind components are faster and simpler. |
| Color generation | `@material/material-color-utilities` (dev-only) | `m3-tailwind-colors` npm package | Only 3 GitHub stars, single maintainer, uncertain maintenance. The underlying `@material/material-color-utilities` is the official Google package -- better to use it directly with a small script than depend on a wrapper. |
| Color generation | Build-time script | Runtime `@material/material-color-utilities` in bundle | Adds ~30KB to the client bundle for something that only needs to run once per accent color change. Pre-generate at build time instead. |
| Dark mode toggle | Custom 15-line hook | next-themes | next-themes is designed for Next.js SSR hydration edge cases. For a Vite SPA, it's unnecessary complexity. The core logic is `classList.toggle('dark')` + `localStorage`. |
| Dark mode approach | Class-based (`@custom-variant`) | Media query (prefers-color-scheme only) | Media query approach doesn't allow manual toggle. Users expect a toggle button. Class-based supports both: system preference as default, manual override via toggle. |
---
## Installation
```bash
# Scaffold
npm create vite@latest ready2blob -- --template react-ts
cd ready2blob
# Dev dependency only -- NOT bundled into the app
npm install -D @material/material-color-utilities
# Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
# Forms and validation
npm install react-hook-form zod @hookform/resolvers
# ZIP generation
npm install jszip
# shadcn/ui setup (optional but recommended)
npx shadcn-ui@latest init
# Then add components as needed:
npx shadcn-ui@latest add button input select checkbox label
# That's it. No other new dependencies.
```
### Files to Create
| File | Purpose |
|------|---------|
| `scripts/generate-theme.ts` | Node script: seed color -> MD3 CSS tokens |
| `src/theme-tokens.css` | Generated output: CSS custom properties for light + dark |
| `src/hooks/useTheme.ts` | Theme toggle hook (light/dark/system) |
### Files to Modify
| File | Change |
|------|--------|
| `src/index.css` | Add `@import "./theme-tokens.css"`, `@custom-variant dark`, `@theme` block |
| `package.json` | Add script: `"generate-theme": "tsx scripts/generate-theme.ts"` |
---
## What NOT to Use
## What NOT to Add
| Technology | Why Not |
|------------|---------|
| Next.js | Server framework. Adds SSR/SSG complexity with zero benefit for a pure client-side tool. |
| Create React App | Officially deprecated. Abandoned by React team. |
| Redux / Redux Toolkit | Overkill for wizard state. useReducer + Context is sufficient. |
| Formik | Superseded by react-hook-form. Controlled inputs cause unnecessary re-renders. |
| Angular | Enterprise framework, large bundle, steep learning curve, wrong tool for a simple wizard. |
| Backend of any kind | Explicitly out of scope. All generation is string manipulation in the browser. |
| LocalStorage / IndexedDB | Out of scope per PROJECT.md — no persistence. |
| Material Tailwind / MUI / any component library | Rewrite cost exceeds benefit. Keep semantic HTML + Tailwind utilities with MD3 tokens. |
| CSS-in-JS (Emotion, styled-components) | Conflicts with Tailwind. Wrong direction. |
| next-themes | Next.js-specific. Vite SPA needs 15 lines of code, not a library. |
| framer-motion | MD3 motion is subtle transitions (opacity, scale). CSS transitions + Tailwind's `@theme` animation tokens handle it. |
| PostCSS plugins | Tailwind v4 uses the `@tailwindcss/vite` plugin, not PostCSS. Don't add PostCSS config. |
| tailwind.config.js | Tailwind v4 is CSS-first. All config goes in `src/index.css` via `@theme`. No JS config file. |
| Runtime color generation in browser | Pre-generate tokens at build time. Don't ship the color algorithm to users. |
---
@@ -120,24 +245,26 @@ npx shadcn-ui@latest add button input select checkbox label
| Decision | Confidence | Basis |
|----------|------------|-------|
| React 18 + Vite as core | HIGH | Industry-standard since 2023, no credible challenger for this use case |
| TypeScript | HIGH | Unambiguously correct for generated-output correctness |
| react-hook-form + Zod | HIGH | De-facto standard pairing for React forms as of 2024-2025 |
| Tailwind CSS | HIGH | Dominant utility-CSS framework; strong fit for wizard UI |
| shadcn/ui | MEDIUM | Strong community adoption but version numbers evolve quickly; verify CLI syntax |
| JSZip for ZIP | MEDIUM | Stable and widely used, but fflate is a valid modern alternative — verify latest version on npm |
| Blob API for single-file download | HIGH | Native browser API, no version concern |
| Version numbers (all) | LOW | Training data cutoff August 2025; must verify on npmjs.com before scaffolding |
| Zero new runtime dependencies | HIGH | Existing Tailwind v4 handles everything; verified in official docs |
| `@custom-variant dark` for dark mode | HIGH | Verified in official Tailwind v4 dark mode documentation |
| `@theme` directive for MD3 tokens | HIGH | Verified in official Tailwind v4 theme documentation |
| `@material/material-color-utilities` 0.4.0 for token generation | HIGH | Official Google package, actively maintained, used by Material Theme Builder |
| MD3 shape scale values (4/8/12/16/28/9999 px) | HIGH | Confirmed in official Material Design 3 shape documentation |
| MD3 elevation box-shadow values | MEDIUM | Values sourced from community reference (Studio N Creations) cross-referenced with Material Web component source. Official docs don't publish exact CSS box-shadow -- they use `--md-elevation-level` in their web components. The shadow values are a reasonable approximation. |
| No component library needed | HIGH | Project has 159 tests against current DOM structure; rewriting components is unjustified for a styling overhaul |
| Pre-generated accent colors (not runtime) | MEDIUM | Architectural choice -- runtime generation is valid but adds bundle weight for a rarely-used feature |
---
## Sources
- PROJECT.md: project requirements and constraints (pure frontend, no backend, static hosting)
- React documentation (react.dev) — training data, verify current version
- Vite documentation (vitejs.dev) — training data, verify current version
- react-hook-form documentation (react-hook-form.com) — training data, verify current version
- Zod documentation (zod.dev) — training data, verify current version
- JSZip (stuk.github.io/jszip) — training data, verify current version
- shadcn/ui (ui.shadcn.com) — training data, verify CLI commands
- MDN Web Docs: Blob API, URL.createObjectURL — browser built-in, no version concern
- [Tailwind v4 Dark Mode Documentation](https://tailwindcss.com/docs/dark-mode) -- `@custom-variant` syntax, class-based toggle, localStorage pattern
- [Tailwind v4 Theme Documentation](https://tailwindcss.com/docs/theme) -- `@theme` directive, CSS variable generation, namespace conventions
- [Material Design 3 Design Tokens](https://m3.material.io/foundations/design-tokens) -- token naming, semantic color roles
- [Material Design 3 Shape Scale](https://m3.material.io/styles/shape/corner-radius-scale) -- corner radius values (4/8/12/16/28dp)
- [Material Design 3 Elevation](https://m3.material.io/styles/elevation/applying-elevation) -- elevation levels 0-5
- [@material/material-color-utilities on npm](https://www.npmjs.com/package/@material/material-color-utilities) -- v0.4.0, official Google color algorithm
- [Material Theme Builder](https://material-foundation.github.io/material-theme-builder/) -- CSS export format, `--md-sys-color-*` naming convention
- [MD3 Box-Shadow CSS Values](https://studioncreations.com/blog/material-design-3-box-shadow-css-values/) -- elevation shadow approximations (MEDIUM confidence)
- [m3-tailwind-colors GitHub](https://github.com/somteacodes/m3-tailwind-colors) -- evaluated and rejected (3 stars, single maintainer)
- [Tailwind v4 Multi-Theme Strategy](https://simonswiss.com/posts/tailwind-v4-multi-theme) -- community pattern for theme switching with CSS variables
+112 -155
View File
@@ -1,219 +1,176 @@
# Project Research Summary
**Project:** Ready2Blob
**Domain:** Pure-frontend rclone configuration wizard / enterprise Windows deployment helper
**Researched:** 2026-03-26
**Confidence:** MEDIUM-HIGH (stack HIGH, pitfalls HIGH, features MEDIUM, architecture MEDIUM)
**Project:** Ready2Blob v1.2 -- UI Polish & MD3 Overhaul
**Domain:** Material Design 3 theming layer on existing React + Tailwind v4 wizard app
**Researched:** 2026-03-31
**Confidence:** HIGH
## Executive Summary
Ready2Blob is a client-side-only static web application that guides IT administrators through configuring rclone for cloud storage backends (Azure Blob, S3, OneDrive, SFTP) and generating PowerShell deployment scripts for Intune and RMM platforms. The product has no backend, no persistence, and no server: all credential handling, config generation, and file download happen entirely in the browser. The right technology choices are well-established — React + Vite + TypeScript with react-hook-form/Zod for per-step validation, and native Blob/JSZip APIs for file generation. This is a greenfield SPA with a small, stable dependency set and no novel architecture challenges.
Ready2Blob v1.2 is a pure styling and UX content overhaul of an already-functional 4-step rclone configuration wizard. The existing stack (Vite 6, React 18, TypeScript 5, Tailwind v4, react-hook-form 7, Zod 4, 159 passing tests) is solid and does not need architectural changes. The research unanimously recommends a **zero new runtime dependencies** approach: define Material Design 3 color tokens as CSS custom properties, wire them into Tailwind v4's native `@theme` directive, and build a small set of reusable UI primitives (Button, Input, Card, Select) that replace the current scattered inline markup. The only new dependency is `@material/material-color-utilities` as a dev-only tool for generating MD3 color palettes from a seed color -- it never ships to the browser.
The recommended approach is to build from the inside out: define types and schemas first, build pure generator functions (config builder, PS script builder) second, then add the wizard UI shell on top. This order ensures the most critical output — the generated files — is correct and testable before any UI work begins. The Backend Schema Registry pattern (one static TS object describing all backend field definitions) is the architectural keystone: it decouples form rendering from backend-specific knowledge and makes adding new backends trivial without modifying UI components.
The recommended approach is a bottom-up, layer-by-layer migration. First establish the CSS token foundation and dark mode infrastructure (zero component changes, zero test impact). Then extract UI primitives as new additive components. Then swap existing hardcoded colors for semantic tokens one component at a time, running all 159 tests after each change. This ordering is critical because the token system is the foundation for everything else -- dark mode, accent colors, MD3 components, and responsive improvements all depend on it. Content improvements (app intro, step descriptions, remote name clarity) are independent and can be parallelized.
The single largest risk category is PowerShell deployment correctness, not frontend development. Generated scripts must handle SYSTEM-context path resolution, UTF-8 no-BOM encoding, single-quoted here-strings to prevent `$` interpolation, 32-bit vs 64-bit host differences, and Intune's 200 KB script size limit. These are operational correctness requirements that will not surface during local development — they only manifest in real enterprise Intune or RMM environments. Every script template decision must be made with these constraints in mind from the first line of code.
---
The primary risks are: breaking the 131 test selectors across 5 UI test files during component restyling, dark mode contrast failures (WCAG AA), and flash of unstyled content on dark mode load. All three are preventable with the disciplined layer-by-layer approach. The most dangerous anti-pattern is a big-bang restyle where all components are changed at once -- this makes test failures impossible to isolate and virtually guarantees regressions.
## Key Findings
### Recommended Stack
The stack is lean by design. React 18 + Vite 5 + TypeScript 5 provides the scaffold; Tailwind CSS 3 + shadcn/ui handles styling and accessible form primitives without a maintained design system; react-hook-form 7 + Zod 3 handles per-step validation with schema-driven field definitions that map directly to rclone backend specs. File generation uses only browser-native APIs (Blob, URL.createObjectURL) for single-file downloads and JSZip 3 for ZIP bundles. No backend, no external state library, no database.
Zero new runtime dependencies. The existing Tailwind v4 handles all styling needs through its CSS-first `@theme` directive and `@custom-variant` for dark mode. One dev dependency added: `@material/material-color-utilities` (v0.4.0) to generate MD3 palettes at build time via a Node script. The output is static CSS custom properties -- zero bundle impact.
**Core technologies:**
- React 18 + Vite 5: SPA scaffold — fast HMR, zero-config static build, no SSR overhead
- TypeScript 5: type safety for generated-output correctness — wrong field name = broken config
- react-hook-form 7 + Zod 3: per-step validation + backend schema definitions — uncontrolled inputs, schema-driven, performant
- Tailwind CSS 3 + shadcn/ui: utility styling + accessible form components — no design system maintenance
- Native Blob API: single-file download — zero dependency, browser built-in
- JSZip 3: ZIP bundle of all artifacts — de-facto browser ZIP standard
- React useReducer + Context: wizard state — sufficient at this scale, no Zustand/Redux needed
**Core technologies (all existing, no changes):**
- **Tailwind v4 `@theme`**: Maps MD3 tokens to utility classes (`bg-surface`, `text-on-primary`) -- native, no plugins
- **Tailwind v4 `@custom-variant dark`**: Class-based dark mode toggle replacing the removed `darkMode: 'class'` config
- **CSS custom properties**: ~20 semantic MD3 color roles, elevation shadows, shape radii, motion tokens
**Critical exclusions:** No Next.js (SSR overhead irrelevant), no Create React App (deprecated), no localStorage/sessionStorage (credentials must not persist), no backend of any kind.
**New dev dependency only:**
- **`@material/material-color-utilities` 0.4.0**: Official Google library, generates full MD3 palette from a single seed hex color. Used by a build script (`scripts/generate-theme.ts`) that outputs `src/theme-tokens.css`. NOT bundled.
**Explicitly rejected:** MUI, Material Tailwind, shadcn/ui, CSS-in-JS, next-themes, framer-motion, PostCSS plugins, tailwind.config.js, runtime color generation. See STACK.md for detailed rationale on each.
### Expected Features
IT admins evaluating this tool will immediately abandon it if any table-stakes feature is missing. The MVP must cover the full generation pipeline — backend selection through file download — for at least Azure Blob, S3, and OneDrive before any polish work begins.
**Must have (table stakes -- P1):**
- MD3 color token system (CSS custom properties for all color roles)
- MD3 text fields (outlined variant with floating labels)
- MD3 button hierarchy (filled, outlined, text)
- MD3 card components with elevation
- Dark mode toggle (system/light/dark, localStorage persistence)
- MD3 step indicator (numbered circles, connecting lines, state indicators)
- Responsive layout (mobile-friendly grids, collapsible step indicator)
- App intro/landing section explaining what Ready2Blob does
- Step-level descriptions on each wizard step
- Remote name field clarity (prominent explanation, examples)
- Accessible focus states (focus-visible, 3px outline)
- Tech debt: FieldRenderer aria fix, StepIndicator inline style migration
**Must have (table stakes):**
- Multi-step backend selection wizard with popular backends (Azure Blob, S3, OneDrive, SFTP) shown first
- Per-backend field forms with labels, help text, and required-field validation matching rclone's own config flow
- Valid rclone.conf output (INI format, correct key/value pairs per backend)
- Remote name input with character validation (alphanumeric, dash, underscore only)
- Intune PowerShell deployment script (handles SYSTEM context, idempotent, correct exit codes)
- RMM PowerShell deployment script (self-contained, runs as SYSTEM)
- Optional rclone install toggle (script downloads rclone binary from URL — never embeds it)
- Security warning gate before download (credentials are plaintext — must be acknowledged)
- Individual download buttons per artifact (conf, Intune script, RMM script)
- Prominent "no data sent to server" assurance
**Should have (differentiators -- P2, add after core is stable):**
- User-selectable accent color (5-8 curated presets)
- Animated step transitions (CSS fade/slide, 200ms)
- Upgraded contextual help popovers
- Scroll-to-error on validation failure
**Should have (differentiators):**
- Live config preview (real-time generated file content visible before downloading)
- Intune detection script generation (separate from install script — Intune Win32 requirement)
- Config path selector: machine-wide `C:\ProgramData\rclone\` vs user profile (with explanation of SYSTEM context implications)
- Rclone version pinning input (defaults to `rclone-current`; override for reproducible deployments)
- Copy-to-clipboard for all output blocks (RMM tools often have a "run script" field)
- Field-level validation for backend-specific formats (Azure account name: 3-24 lowercase alphanumeric)
- Explanatory tooltips on credential fields (SAS token vs access key confusion is common)
- ZIP "download all" bundle
**Defer to v2+:**
- Multiple remotes in one config (adds significant wizard UX complexity)
- RMM-named script variants (NinjaRMM-specific, Datto-specific execution contexts)
- IntuneWinAppUtil packaging hints (valuable but can be a static docs page)
- Test connection / credential validation (requires a proxy backend — violates no-server constraint)
- Save/load configurations (requires localStorage = credentials in browser storage = security incident)
- Graph API auto-push to Intune (massive scope; manual upload is acceptable for v1)
- Multi-OS support (macOS/Linux scripts out of scope; state clearly "Windows endpoints only")
**Defer (v2+):**
- Custom MD3 select/dropdown (HIGH complexity for 2-3 selects)
- Code syntax highlighting in review step
- Arbitrary user hex color theming
### Architecture Approach
The architecture follows four clean layers: Wizard UI (React components, navigation), Wizard State (single useReducer store, shared via Context), Config Builders (pure functions: state in, file string out), and Download Manager (Blob URL or JSZip). The Backend Schema Registry is a static TypeScript object that defines all field definitions per backend type — it drives dynamic form rendering, Zod schema construction, and config key/value generation from one source of truth. All builders are pure functions with no side effects, making them immediately unit-testable without a browser.
A CSS custom properties layer bridges MD3 tokens with Tailwind v4. A thin `ThemeToggle` component manages the `.dark` class on `<html>` via direct DOM manipulation -- NOT via React Context, to avoid re-rendering the entire wizard tree on toggle. Components are refactored bottom-up: primitives first (Button, Input, Card, Select), then composed components (FieldRenderer, BackendCard), then layout (AppShell, StepIndicator). The critical architectural insight is that theme state belongs in CSS (class on `<html>` + custom properties), not in React state.
**Major components:**
1. Wizard UI + Navigation — step rendering, next/back/jump, step completion tracking
2. Wizard State (useReducer + Context) — single source of truth for all form data; typed WizardState interface
3. Backend Schema Registry — static TS object: BackendType → FieldDef[]; drives dynamic forms
4. rclone.conf Builder — pure function: WizardState → INI string; plain template literals
5. PowerShell Script Builder (Intune + RMM variants) — pure functions: WizardState → .ps1 string
6. Download Manager — Blob URL (single file) + JSZip (bundle); no library for single files
**Key patterns:**
- Centralized state — all form data in one typed store; never per-step local state
- Schema Registry — one entry per backend = new backend support with zero UI changes
- Pure builders — same input always produces same output; no DOM reads; fully unit-testable
- PowerShell single-quoted here-strings — prevents `$` interpolation corrupting credentials
1. **CSS Token Layer** (`index.css` + `theme-tokens.css`) -- MD3 color roles, elevation, shape, motion as `@theme` values
2. **UI Primitives** (`ui/Button`, `ui/Input`, `ui/Select`, `ui/Card`) -- MD3-styled presentational components using `forwardRef` for react-hook-form compatibility
3. **ThemeToggle** -- standalone component, local state only, toggles `.dark` class on `<html>`
4. **AppShell** -- layout wrapper extracted from current WizardShell, houses ThemeToggle
5. **Modified existing components** -- FieldRenderer, BackendCard, PasswordField, all wizard steps -- swap hardcoded colors for semantic tokens
### Critical Pitfalls
These are the failure modes that cause silent deployment breakage in production enterprise environments. All five must be addressed in the initial script template — retrofitting them later risks shipping broken scripts.
1. **SYSTEM context config path mismatch** — rclone config written to SYSTEM's `%APPDATA%` is invisible to the logged-in user. Always write to `C:\ProgramData\rclone\rclone.conf` (machine-wide path, no WOW64 redirection). Make the destination path explicit in the wizard and let the admin choose; never default to bare `%APPDATA%` expansion.
2. **PowerShell encoding writes UTF-16 BOM** `Out-File` in PS 5.1 defaults to UTF-16 LE with BOM; rclone cannot parse it. Always use `[System.IO.File]::WriteAllText($path, $content, [System.Text.UTF8Encoding]::new($false))` to guarantee UTF-8 no-BOM. Hardcode this in every script template from day one.
3. **Secrets exposed in Intune script logs** — Intune logs all script output to the Azure portal. Generated scripts must never echo credential values. Use `Write-Host "Writing config to $configPath"` (path only), never `Write-Host $configContent`. No debug output containing credential variables, ever.
4. **Intune 200 KB script size limit** — Scripts that embed or base64-encode rclone binary content are rejected at upload. The generated script must always download rclone from a URL (`Invoke-WebRequest`) at deployment time — never embed the binary. Surface the download URL in the wizard and let admins specify a corporate mirror.
5. **SAS token / storage key corruption via `$` interpolation** — Azure credentials contain `$` characters. In double-quoted PowerShell strings, `$` triggers variable substitution, silently corrupting the credential. Always wrap the config content block in a single-quoted here-string (`@' ... '@`). Trim all credential inputs before inserting into the config to prevent newline truncation.
**Additional moderate pitfalls to address in script templates:**
- OAuth backends (OneDrive, Google Drive) require a pre-obtained token from `rclone authorize` — the wizard cannot complete OAuth flow in the browser; show a prominent warning and a dedicated token input field
- 32-bit PowerShell host (Intune default) causes WOW64 path redirection; use `C:\ProgramData\rclone\` or `$env:ProgramW6432` — recommend enabling 64-bit PS in Intune settings
- Do not call `Set-ExecutionPolicy` in the generated script — Group Policy always overrides it; use `-ExecutionPolicy Bypass` on any sub-process calls instead
- Auto-insert a `# Generated: [timestamp]` comment in each script so re-generated scripts always have different bytes, enabling Intune re-execution on config updates
---
1. **Tailwind v4 dark mode misconfiguration** -- v3 `darkMode: 'class'` does not exist. Must use `@custom-variant dark (&:where(.dark, .dark *))` in CSS. Address in Phase 1 before any `dark:` classes are added.
2. **73 hardcoded color classes** -- Adding `dark:` counterparts to each creates unmaintainable 100+ char classNames. Instead, replace all with semantic tokens (`bg-surface`, `text-on-surface`) that swap values automatically via CSS variable override.
3. **Breaking 131 test selectors** -- Restyling touches the same JSX that tests query. Must restyle one component at a time, running all 159 tests after each. Never batch-restyle.
4. **FOUC on dark mode load** -- React applies `.dark` class after first paint. Add a synchronous inline `<script>` in `index.html <head>` to set the class before paint.
5. **Dark mode contrast failures** -- `text-gray-500` on dark backgrounds fails WCAG AA. Semantic tokens must define separate light/dark values verified for 4.5:1 contrast on every pair.
6. **Theme context re-renders** -- Do NOT use React Context for theme. CSS class toggle on `<html>` causes zero React re-renders. Only the toggle button needs local state.
## Implications for Roadmap
Based on combined research, the architecture's inside-out build order maps directly to phases. The generator functions and schema registry have zero UI dependencies — build them first, test them in isolation, then layer UI on top. This order also front-loads the hardest correctness requirements (script encoding, path choices, credential handling) before any deployment to users.
Based on research, suggested phase structure:
### Phase 1: Foundation — Types, Schema Registry, State Shape
### Phase 1: Theme Foundation
**Rationale:** The token system is the dependency root -- every visual component, dark mode, and accent colors depend on it. Must come first. Zero test impact makes it safe.
**Delivers:** MD3 color tokens in CSS, dark mode infrastructure (`@custom-variant`, `.dark` overrides), FOUC prevention script in `index.html`, ThemeToggle component, theme generation script (`scripts/generate-theme.ts`)
**Addresses:** Color token system, dark mode toggle (infrastructure)
**Avoids:** Tailwind v4 dark mode misconfiguration (P1), FOUC (P5), theme context re-renders (P6)
**Rationale:** All other components depend on these definitions. Building them first prevents architectural drift where UI components hardcode backend knowledge. The schema registry is the keystone — it must exist before dynamic forms, before builders, before anything.
**Delivers:** TypeScript type definitions (WizardState, BackendType, FieldDef, RemoteConfig), Backend Schema Registry (Azure Blob, S3, OneDrive, SFTP as Tier 1; GCS, Backblaze B2 as Tier 2), Zod schemas per backend derived from registry, Vite + React + TypeScript + Tailwind scaffold
**Addresses:** Foundational architecture (ARCHITECTURE.md Pattern 2 — Backend Schema Registry)
**Avoids:** Anti-Pattern 3 (hardcoding backend fields in step components)
### Phase 2: UI Primitives
**Rationale:** Primitives are the reuse boundary -- Input, Button, Card are used by multiple steps. Building them before modifying existing components means additive-only changes with zero existing test impact.
**Delivers:** `ui/Button` (filled/outlined/text), `ui/Input` (outlined, floating label, error), `ui/Select`, `ui/Card` (elevation), new component tests
**Addresses:** MD3 text fields, MD3 buttons, MD3 cards
**Avoids:** Big-bang rewrite anti-pattern (P4)
### Phase 2: Core Generators — rclone.conf + PowerShell Script Builders
### Phase 3: Component Token Migration
**Rationale:** With tokens and primitives in place, swap hardcoded colors across all existing components. This is the highest-risk phase for test breakage -- one component at a time, tests after each.
**Delivers:** All components using semantic tokens, FieldRenderer delegating to primitives, BackendCard using Card, PasswordField using Input, StepIndicator inline style migration, FieldRenderer aria-describedby fix
**Addresses:** Hardcoded color migration (73 usages), tech debt items, form accessibility, MD3 step indicator
**Avoids:** Hardcoded color dual-track (P2), test breakage (P4), accessibility regressions (P7)
**Rationale:** Pure functions with no UI dependencies. Build and unit-test these before any React work. This is where all the correctness requirements from PITFALLS.md live — encoding, path choices, credential handling, no-log rules must be baked in from the first line of the template.
**Delivers:** `buildRcloneConf(remotes)` — pure function producing INI string with LF normalization; `buildIntuneScript(state)` — PS script with UTF-8 no-BOM write, single-quoted here-string, machine-wide path, download-only rclone install, timestamp comment, no credential logging; `buildRmmScript(state)` — same constraints; Download Manager (Blob URL + JSZip wrapper)
**Implements:** ARCHITECTURE.md Patterns 3, 4, 5 (pure builders, Blob URL, JSZip)
**Avoids:** Pitfalls 1, 2, 3, 5, 8 (SYSTEM path, 200 KB limit, encoding, credential corruption, OAuth warning)
### Phase 4: Layout, Content, and Responsive
**Rationale:** With all components styled, add the layout shell, content improvements, and responsive behavior. These are mostly independent of each other and can be parallelized.
**Delivers:** AppShell layout, app intro section, step descriptions, remote name clarity, responsive grid/breakpoints, dark mode UX polish (scrollbars, select dropdowns, focus rings)
**Addresses:** App intro, step descriptions, remote name clarity, responsive layout, accessible focus states
**Avoids:** Dark mode contrast failures (P3) -- final contrast audit here
### Phase 3: Wizard Shell + State Wiring
**Rationale:** Navigation shell and state store can be built against mock/empty step content. Getting the state shape right before forms are built prevents having to refactor form registration later.
**Delivers:** useReducer + Context store wired to WizardState shape; step navigation (stepper, next/back, URL-free step tracking); step completion state; mobile-responsive layout shell using Tailwind + shadcn/ui
**Uses:** React 18, useReducer/Context, Tailwind, shadcn/ui
**Avoids:** Anti-Pattern 1 (per-step local state losing data on back-navigation)
### Phase 4: Wizard Step Forms — Dynamic Backend Forms + Deployment Options
**Rationale:** Now that state, schema registry, and builders all exist, forms can be built as thin wrappers that write to the state store. The dynamic form component reads from the schema registry — one component handles all backends.
**Delivers:** Step 1 — Backend type selector (popularity-ordered: Azure Blob, S3, OneDrive, SFTP first); Step 2 — `<DynamicBackendStep />` driven by Schema Registry (react-hook-form + Zod per backend); Step 3 — Deployment options (rclone install toggle, config path selector: machine-wide vs user profile, version pin field, script target selection); Remote name input with `[a-zA-Z0-9_-]` validation and live section header preview
**Addresses:** Table-stakes features from FEATURES.md; Pitfall 4 (remote name validation); Pitfall 8 (OAuth token field + warning for OAuth backends)
### Phase 5: Review, Download + Security Gate
**Rationale:** Final wizard step assembles everything. Live preview builds trust and catches errors before the admin deploys a broken config. Security warning is a hard blocker before download.
**Delivers:** Review step with live syntax-highlighted config preview (updates in real time); security warning modal (credentials in plaintext — cannot be dismissed without acknowledgment); individual download buttons (conf, Intune .ps1, RMM .ps1); "Download All as ZIP" via JSZip; copy-to-clipboard for each output block
**Addresses:** Table-stakes and differentiator features from FEATURES.md (live preview, security gate, copy-to-clipboard, ZIP bundle)
**Avoids:** Pitfall 5 (security warning gate)
### Phase 6: Polish + Correctness Hardening
**Rationale:** After end-to-end flow works, add the depth features that reduce support tickets and build admin trust. This phase also adds per-field validation beyond basic required-field checks and the contextual help text that reduces abandonment.
**Delivers:** Per-field backend-specific validation (Azure account name format, SAS token trimming); explanatory tooltips on credential fields; Intune detection script generation (separate from install script); config path implications documentation in the wizard output pane; 64-bit PS host recommendation in Intune output; auto-inserted `# Generated: [timestamp]` comment in scripts; LF normalization on all generated output
**Addresses:** Differentiator features from FEATURES.md; Pitfalls 6, 7, 9, 10, 11, 13
### Phase 5: Polish and Differentiators
**Rationale:** Accent colors and transitions layer on top of the stable token system. These are P2 features that should only land after core is validated.
**Delivers:** Accent color selector (5-8 presets), step transitions (CSS), upgraded popovers, scroll-to-error
**Addresses:** All P2 differentiator features
**Avoids:** Premature accent color work before token system is proven
### Phase Ordering Rationale
- Phases 1 and 2 have no UI dependencies and contain the highest-risk correctness requirements — building them first allows unit testing before any user-facing code exists
- The wizard shell (Phase 3) can be built with empty/mock steps while the generators are being built, if team size allows parallelism
- Dynamic forms (Phase 4) depend on both the schema registry (Phase 1) and state wiring (Phase 3) being finalized
- The download step (Phase 5) can only be meaningfully built once all builders (Phase 2) exist
- Polish (Phase 6) is deliberately last — it adds depth but does not change the architecture
- **Token system first** because 100% of visual components depend on it (see FEATURES.md dependency graph)
- **Primitives before migration** because building new components is additive (zero test risk), while modifying existing components carries test risk
- **One-component-at-a-time migration** because the 131 test selectors across 5 files make batch changes dangerous
- **Content and layout after components** because layout depends on component dimensions and spacing, and content is independent
- **Differentiators last** because accent colors multiply token sets (N accents x 2 themes) and must not be attempted until the base system is stable
### Research Flags
Phases likely needing deeper research during planning:
- **Phase 2 (Script Builders):** Verify current Intune PowerShell script execution model against live Microsoft docs before writing templates. Intune behavior around 32-bit/64-bit host defaults and script re-execution triggers can change between Intune releases. High-stakes: wrong behavior is invisible until tested on a real managed device.
- **Phase 4 (Backend Forms):** Verify exact required field names for each backend against live rclone docs (rclone.org/azureblob, rclone.org/s3, etc.) before implementing the Schema Registry. Field names are the source of truth for config generation — a wrong key name produces a silently broken config.
- **Phase 4 (OAuth backends):** Confirm current `rclone authorize` token extraction flow for OneDrive before designing the OAuth token input UX. The token JSON structure may have changed.
- **Phase 2 (UI Primitives):** MD3 outlined text field with floating label is the most complex primitive. CSS-only floating label animation needs prototyping. `forwardRef` integration with react-hook-form `register()` needs verification.
- **Phase 3 (Component Migration):** The FieldRenderer refactor to delegate to primitives is the highest-risk change. DOM structure changes could break tests. Needs careful planning of the migration path per field type.
Phases with standard patterns (skip research-phase):
- **Phase 1 (Foundation):** React + Vite + TypeScript scaffold is fully documented; schema registry is a static TS object; no novel decisions required
- **Phase 3 (Wizard Shell):** Multi-step form navigation with useReducer + Context is a well-documented React pattern; react-hook-form per-step validation is standard
- **Phase 5 (Download):** Blob URL download and JSZip are stable browser APIs with no version concerns; security modal is standard UI
---
- **Phase 1 (Theme Foundation):** Fully documented in Tailwind v4 official docs. Token structure is defined. FOUC script is a known 5-line pattern.
- **Phase 4 (Layout/Content):** Content writing and responsive Tailwind grids are standard work. No novel patterns.
- **Phase 5 (Polish):** Accent colors are a token swap. Step transitions are CSS-only. Well-documented patterns.
## Confidence Assessment
| Area | Confidence | Notes |
|------|------------|-------|
| Stack | HIGH | React + Vite + TypeScript + react-hook-form + Zod is the industry-standard 2025 pairing; no credible alternatives for this use case. shadcn/ui CLI syntax should be verified before scaffolding. |
| Features | MEDIUM-HIGH | Table-stakes and IT admin deployment expectations are HIGH confidence from domain knowledge. Specific rclone backend field names are MEDIUM — must be cross-checked against live rclone docs before implementing schema registry. |
| Architecture | MEDIUM-HIGH | rclone.conf format is HIGH (stable since v1.x). Intune script patterns are MEDIUM (well-documented but behavior can change between releases). Blob/JSZip download patterns are HIGH (stable browser APIs). |
| Pitfalls | HIGH | SYSTEM context, encoding, execution policy, and size-limit pitfalls are verified against official Microsoft docs. rclone-specific pitfalls (section name validation, config format edge cases) are MEDIUM — verify against live rclone docs. |
| Stack | HIGH | Zero new runtime deps. Tailwind v4 `@theme` and `@custom-variant` verified in official docs. `@material/material-color-utilities` is official Google package. |
| Features | MEDIUM | Table stakes and differentiators well-identified. MD3 web implementation patterns less established than MUI-based approaches since we are hand-rolling with Tailwind. |
| Architecture | HIGH | CSS custom properties + Tailwind v4 `@theme` is the documented approach. Bottom-up migration is low-risk. ThemeToggle via DOM class (not Context) avoids re-render cascade. |
| Pitfalls | HIGH | Based on direct codebase analysis (73 classNames, 131 selectors, 3 inline styles). Tailwind v4 dark mode gotchas verified against official docs and community reports. |
**Overall confidence:** MEDIUM-HIGH
**Overall confidence:** HIGH
### Gaps to Address
- **rclone backend field names:** Every field key in the Schema Registry must match rclone's exact config key names. Verify each Tier 1 backend against rclone.org before implementing Phase 1. Wrong keys produce silently broken configs.
- **rclone S3-compatible provider pattern:** The `provider = AWS` + optional `endpoint` field pattern for S3-compatible backends (Wasabi, MinIO, Cloudflare R2) should be confirmed against current rclone S3 docs before implementing the S3 schema entry.
- **Intune re-execution trigger:** Confirm whether Intune re-runs a modified script based on byte-level content change or requires a script version increment. This affects the timestamp-comment strategy in Phase 6.
- **OneDrive OAuth token format:** Confirm current `token = {...}` JSON structure for OneDrive to design the token input field correctly in Phase 4.
- **shadcn/ui CLI commands:** Verify current `npx shadcn-ui@latest` command syntax before Phase 3 scaffolding — the CLI interface has historically changed between major versions.
---
- **MD3 elevation box-shadow values**: Sourced from community reference, not official Google CSS. The exact shadow values are approximations. Validate visually during Phase 1 and adjust if needed.
- **MD3 outlined text field floating label**: CSS-only implementation needs prototyping. May require a small JS hook for detecting input fill state (`:placeholder-shown` selector or `onFocus`/`onBlur` handlers). Validate during Phase 2 planning.
- **Native `<select>` in dark mode**: Browser renders `<option>` with OS colors. May look broken on dark backgrounds in some browsers. Evaluate during Phase 4 whether to accept native rendering or build custom dropdown (deferred to v2+ per feature research).
- **Pre-generated accent color token sets**: The approach of pre-generating N accent x 2 theme token sets at build time has not been prototyped. If the CSS file grows too large, runtime generation (bundling the color utility) may be reconsidered. Validate during Phase 5.
## Sources
### Primary (HIGH confidence)
- Microsoft Learn — PowerShell scripts in Intune (2025-10-02) — SYSTEM context, size limits, exit codes, script re-execution
- Microsoft Learn — Intune Management Extension (2026-03-17) — execution context, 32-bit/64-bit host defaults
- Microsoft Learn — Set-ExecutionPolicy reference (2025-04-15) — Group Policy override behavior
- Microsoft Learn — Naming Files, Paths, and Namespaces (Win32) — MAX_PATH constraints
- MDN Web Docs — Blob API, URL.createObjectURL — browser file download pattern
- PROJECT.md — project requirements and constraints (no backend, no persistence, static hosting)
- [Tailwind v4 Dark Mode Documentation](https://tailwindcss.com/docs/dark-mode) -- `@custom-variant` syntax, class-based toggle
- [Tailwind v4 Theme Documentation](https://tailwindcss.com/docs/theme) -- `@theme` directive, CSS variable generation
- [Material Design 3 Color Roles](https://m3.material.io/styles/color/roles) -- semantic color role definitions
- [Material Design 3 Design Tokens](https://m3.material.io/foundations/design-tokens) -- token naming conventions
- [Material Design 3 Shape Scale](https://m3.material.io/styles/shape/corner-radius-scale) -- radius values
- [Material Design 3 Elevation](https://m3.material.io/styles/elevation/applying-elevation) -- elevation levels 0-5
- [@material/material-color-utilities](https://www.npmjs.com/package/@material/material-color-utilities) -- v0.4.0, official Google package
- Codebase analysis: 73 className usages, 131 test selectors, 159 tests, 3 inline styles in StepIndicator
### Secondary (MEDIUM confidence)
- rclone.org documentation — backend field names, config format, S3 provider pattern (training data, knowledge cutoff August 2025; verify before implementation)
- rclone.conf INI format specification — training data (format is stable since v1.x; HIGH confidence on structure, MEDIUM on per-backend field names)
- RMM deployment patterns (NinjaRMM, Datto, ConnectWise) — training data; verify execution context differences before adding RMM-named variants
- [MD3 Box-Shadow CSS Values](https://studioncreations.com/blog/material-design-3-box-shadow-css-values/) -- elevation shadow approximations
- [Tailwind v4 dark mode @custom-variant discussion](https://github.com/tailwindlabs/tailwindcss/discussions/15083) -- community verified pattern
- [Tailwind v4 Multi-Theme Strategy](https://simonswiss.com/posts/tailwind-v4-multi-theme) -- community pattern for theme switching
- [Material Theme Builder](https://material-foundation.github.io/material-theme-builder/) -- CSS export format reference
- [Wizard Design Pattern (UX Planet)](https://uxplanet.org/wizard-design-pattern-8c86e14f2a38) -- wizard UX fundamentals
- [Wizards (NN/g)](https://www.nngroup.com/articles/wizards/) -- Nielsen Norman Group guidelines
- [Input Field Design Best Practices 2025](https://fireart.studio/blog/input-field-design-best-practice/) -- floating labels, error patterns
### Tertiary (LOW confidence)
- Version numbers for all npm packages — training data cutoff August 2025; verify all versions against npmjs.com before scaffolding
- [m3-tailwind-colors](https://github.com/somteacodes/m3-tailwind-colors) -- evaluated and rejected (3 stars, single maintainer)
---
*Research completed: 2026-03-26*
*Research completed: 2026-03-31*
*Ready for roadmap: yes*
+36
View File
@@ -0,0 +1,36 @@
# syntax=docker/dockerfile:1
# ---- Stage 1: build the static bundle ----
FROM node:22-alpine AS build
WORKDIR /app
# Deps from the lockfile first, so this layer survives source-only edits.
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
# Build against a placeholder base. The entrypoint swaps it for $BASE_URL at startup,
# so one image can be served from any path without rebuilding.
RUN npm run build -- --base=/__R2B_BASE__/
# ---- Stage 2: serve it ----
# Ready2Blob is entirely client-side, so the runtime image is just nginx + dist/.
FROM nginx:alpine AS runtime
# Pristine copy; the entrypoint stages it into the web root on each start.
COPY --from=build /app/dist /opt/ready2blob/html
COPY docker/security-headers.conf /etc/nginx/snippets/security-headers.conf
COPY docker/nginx.conf.template /etc/nginx/ready2blob.conf.template
COPY docker/40-ready2blob-base-url.sh /docker-entrypoint.d/40-ready2blob-base-url.sh
RUN chmod +x /docker-entrypoint.d/40-ready2blob-base-url.sh
# Path the app is served from. "/" for a dedicated (sub)domain, "/ready2blob/" when
# mounted under a path on a shared host.
ENV BASE_URL=/
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -q --spider http://localhost/ || exit 1
+39
View File
@@ -0,0 +1,39 @@
# Ready2Blob
A pure-frontend wizard that generates rclone config files and PowerShell deployment scripts for pushing cloud storage remotes onto Windows endpoints via Intune or any RMM tool. Supports 18 backends (Azure Blob, S3, S3-compatible, OneDrive, SFTP, GCS, Backblaze B2, Google Drive, Dropbox, Box, pCloud, FTP, WebDAV, SMB, HTTP, Seafile, Yandex Disk, Koofr). Everything runs client-side — no backend, no data leaves the browser.
## Run with Docker Compose
```yaml
services:
ready2blob:
image: git.azuze.fr/kawa/ready2blob:latest
container_name: ready2blob
ports:
- "8080:80"
environment:
# "/" for a dedicated (sub)domain; "/ready2blob/" to mount under a path.
BASE_URL: "/"
restart: unless-stopped
```
```bash
docker compose up -d
```
Then open `http://localhost:8080`.
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `BASE_URL` | `/` | Path the app is served from. Use `/` for a dedicated (sub)domain, or `/ready2blob/` to mount under a path on a shared host. |
## Dependencies
- [React 18](https://react.dev/) + [Vite 6](https://vitejs.dev/) + [TypeScript 5](https://www.typescriptlang.org/)
- [Tailwind CSS v4](https://tailwindcss.com/)
- [react-hook-form](https://react-hook-form.com/) + [@hookform/resolvers](https://github.com/react-hook-form/resolvers) + [Zod 4](https://zod.dev/) for form validation
- [JSZip](https://stuk.github.io/jszip/) for ZIP bundle downloads
- [Vitest](https://vitest.dev/) for testing
- Runtime: [nginx](https://nginx.org/) (alpine) serving the static build
+11
View File
@@ -0,0 +1,11 @@
services:
ready2blob:
build: .
image: ready2blob:latest
container_name: ready2blob
ports:
- "8080:80"
environment:
# "/" for a dedicated (sub)domain; "/ready2blob/" to mount under a path.
BASE_URL: "/"
restart: unless-stopped
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
# Applies $BASE_URL to the built bundle and the nginx config at container start.
#
# The bundle is built with Vite's base set to the literal token below, so every asset
# URL in the emitted HTML/JS/CSS carries it. Rewriting that token here is what lets one
# image serve from any path without a rebuild.
set -eu
TOKEN='/__R2B_BASE__/'
SRC='/opt/ready2blob/html'
DEST='/usr/share/nginx/html'
# Normalize to a leading and trailing slash: "app" and "/app" both mean "/app/".
base="${BASE_URL:-/}"
case "$base" in /*) ;; *) base="/$base" ;; esac
case "$base" in */) ;; *) base="$base/" ;; esac
echo "ready2blob: serving from base $base"
# Rebuild the web root from the pristine copy on every start, so both restarts and
# BASE_URL changes are idempotent.
rm -rf "${DEST:?}"/*
mkdir -p "$DEST$base"
cp -R "$SRC/." "$DEST$base"
# '|' as the sed delimiter, since both needle and replacement contain '/'.
find "$DEST" -type f \( -name '*.html' -o -name '*.js' -o -name '*.css' \) \
-exec sed -i "s|$TOKEN|$base|g" {} +
# Restrict envsubst to BASE_URL so nginx's own $uri / $1 survive untouched.
export BASE_URL="$base"
envsubst '${BASE_URL}' \
< /etc/nginx/ready2blob.conf.template \
> /etc/nginx/conf.d/default.conf
+33
View File
@@ -0,0 +1,33 @@
# Rendered to /etc/nginx/conf.d/default.conf at startup, with ${BASE_URL} substituted.
# See docker/40-ready2blob-base-url.sh.
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
include /etc/nginx/snippets/security-headers.conf;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/css application/javascript image/svg+xml application/json;
# Vite emits content-hashed filenames under assets/ — safe to cache forever.
location ${BASE_URL}assets/ {
include /etc/nginx/snippets/security-headers.conf;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
# index.html must never be cached, or clients pin to a stale bundle after a redeploy.
location = ${BASE_URL}index.html {
include /etc/nginx/snippets/security-headers.conf;
add_header Cache-Control "no-cache" always;
}
# No `$uri/` here on purpose: if the directory exists but holds no index file,
# nginx answers 403 instead of falling through to the fallback.
location / {
try_files $uri ${BASE_URL}index.html;
}
}
+15
View File
@@ -0,0 +1,15 @@
# Ready2Blob handles cloud credentials in the browser and sends them nowhere.
# These headers keep it that way: no framing, no external origins, no referrer leakage.
#
# Included per-location as well as at server level on purpose: an `add_header` inside
# a location block REPLACES every header inherited from the parent, so any location
# that sets its own Cache-Control must re-include this file.
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=(), usb=()" always;
# script-src keeps 'unsafe-inline' for the theme/FOUC bootstrap in index.html.
# To tighten it: build once, read the emitted inline <script> from dist/index.html,
# and swap 'unsafe-inline' for its 'sha256-...' hash.
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; form-action 'none'; frame-ancestors 'none'; base-uri 'self'; object-src 'none'" always;
+25 -1
View File
@@ -2,7 +2,31 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<script>
(function() {
try {
var stored = localStorage.getItem('r2b-theme');
var effective;
if (stored === 'dark') {
effective = 'dark';
} else if (stored === 'light') {
effective = 'light';
} else {
effective = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
if (effective === 'dark') {
document.documentElement.classList.add('dark');
}
} catch (e) {
// localStorage unavailable (private mode, etc.) — leave light as default
}
})();
</script>
<!-- Favicon: drop the logo at public/favicon.svg and restore this line. Keep the
%BASE_URL% placeholder so it follows the deployed base path:
<link rel="icon" type="image/svg+xml" href="%BASE_URL%favicon.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ready2Blob</title>
</head>
+16 -1
View File
@@ -1,7 +1,7 @@
// @vitest-environment jsdom
// Covers WIZD-02: App renders the correct step component for currentStep 0, 1, 2
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { render, screen, fireEvent } from '@testing-library/react';
import React from 'react';
import App from './App';
import { WizardProvider } from './store/context';
@@ -52,8 +52,23 @@ function renderAtStep(targetStep: number) {
}
describe('App — step routing', () => {
it('shows intro section on initial render', () => {
render(<App />);
// Intro copy is present
const introHeading = screen.getByText(/Go from zero to a deployable rclone setup in minutes/);
expect(introHeading).toBeDefined();
// StepIndicator is NOT in the DOM
const stepNav = screen.queryByRole('navigation', { name: /Wizard steps/i });
expect(stepNav).toBeNull();
});
it('renders BackendSelectionStep when currentStep is 0', () => {
render(<App />);
// Intro shows first — assert Get Started button is visible
const getStarted = screen.getByRole('button', { name: /Get Started/i });
expect(getStarted).toBeDefined();
// Click Get Started to reveal the wizard
fireEvent.click(getStarted);
// BackendSelectionStep renders "Step 1: Select Backend"
const heading = screen.getByText(/Select Backend/);
expect(heading).toBeDefined();
+39 -5
View File
@@ -2,16 +2,41 @@
// Step router — renders the correct step component based on currentStep.
// StepIndicator renders above each step as persistent breadcrumb navigation.
import { useState } from 'react';
import { useWizard } from './store/context';
import { WizardProvider } from './store/context';
import { ThemeToggle } from './components/ui/ThemeToggle';
import { StepIndicator } from './components/wizard/StepIndicator';
import { BackendSelectionStep } from './components/wizard/BackendSelectionStep';
import { RemoteConfigStep } from './components/wizard/RemoteConfigStep';
import { DeploymentStep } from './components/wizard/DeploymentStep';
import { ReviewStep } from './components/wizard/ReviewStep';
import { MD3_BTN_FILLED } from './styles/md3-buttons';
function IntroSection({ onStart }: { onStart: () => void }) {
return (
<div className="flex flex-col items-center text-center py-16 px-4">
<h2 className="text-2xl font-bold text-on-surface mb-4">
Go from zero to a deployable rclone setup in minutes
</h2>
<p className="text-base text-on-surface-container max-w-lg mb-3">
Ready2Blob walks you through configuring rclone for Azure Blob, S3, OneDrive, and 4 more
cloud backends no syntax knowledge required.
</p>
<p className="text-base text-on-surface-container max-w-lg mb-8">
The wizard generates a ready-to-use <code className="font-mono text-sm">rclone.conf</code>{' '}
and deployment scripts (Intune / RMM) you can download and run immediately.
</p>
<button type="button" className={MD3_BTN_FILLED} onClick={onStart}>
Get Started
</button>
</div>
);
}
function WizardShell() {
const { state } = useWizard();
const [showIntro, setShowIntro] = useState(true);
const steps = [
<BackendSelectionStep key="backend" />,
@@ -26,13 +51,22 @@ function WizardShell() {
const CurrentStep = steps[stepIndex];
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center py-12 px-4">
<div className="min-h-screen bg-surface flex flex-col items-center py-12 px-4">
<div className="w-full max-w-2xl">
<h1 className="text-3xl font-bold text-gray-900 mb-8 text-center">Ready2Blob</h1>
<StepIndicator />
<div className="mt-8">
{CurrentStep}
<div className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-bold text-on-surface">Ready2Blob</h1>
<ThemeToggle />
</div>
{showIntro ? (
<IntroSection onStart={() => setShowIntro(false)} />
) : (
<>
<StepIndicator />
<div key={state.currentStep} className="mt-8 animate-step-in">
{CurrentStep}
</div>
</>
)}
</div>
</div>
);
+192
View File
@@ -0,0 +1,192 @@
// BackendIcons.tsx
// Inline SVG icon map keyed by BackendType.
// All icons use fill="currentColor" for MD3 dark-mode compatibility.
// Each icon is designed for 24x24px viewBox and works as monochrome.
import type { BackendType } from '../../schemas/registry';
export interface IconProps {
className?: string;
}
// Azure Blob Storage — cloud with A shape
const AzureBlobIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M6.5 20a4.5 4.5 0 01-.5-8.97V11a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
<path d="M10 14l2-4 2 4h-1.25v2h-1.5v-2H10z" />
</svg>
);
// Amazon S3 — cloud with S shape
const S3Icon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M6.5 20a4.5 4.5 0 01-.5-8.97V11a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
<text x="8.5" y="17" fontSize="7" fontFamily="sans-serif" fontWeight="bold">S3</text>
</svg>
);
// S3-Compatible — generic cloud with layers
const S3CompatibleIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M6.5 18a4.5 4.5 0 01-.5-8.97V9a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
<rect x="9" y="14" width="6" height="1.5" rx="0.75" />
<rect x="9" y="16.5" width="6" height="1.5" rx="0.75" />
</svg>
);
// Google Cloud Storage — cloud with G
const GCSIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M6.5 20a4.5 4.5 0 01-.5-8.97V11a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
<text x="8" y="17.5" fontSize="7" fontFamily="sans-serif" fontWeight="bold">G</text>
</svg>
);
// Backblaze B2 — cloud with B2
const B2Icon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M6.5 20a4.5 4.5 0 01-.5-8.97V11a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
<text x="7.5" y="17.5" fontSize="6.5" fontFamily="sans-serif" fontWeight="bold">B2</text>
</svg>
);
// Azure Files — file-share icon (folder with network lines)
const AzureFilesIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M2 6.5A2.5 2.5 0 014.5 4H9l2 2h8.5A2.5 2.5 0 0122 8.5v9a2.5 2.5 0 01-2.5 2.5h-15A2.5 2.5 0 012 17.5V6.5z" />
<path fillRule="evenodd" d="M8 12h8v1.5H8V12zm2 2.5h4V16h-4v-1.5z" clipRule="evenodd" />
</svg>
);
// OpenStack Swift — server/globe icon
const SwiftIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<circle cx="12" cy="12" r="9" fillOpacity="0.15" />
<path d="M12 3a9 9 0 100 18A9 9 0 0012 3zm0 1.5c.9 0 1.9.6 2.7 1.7H9.3C10.1 5.1 11.1 4.5 12 4.5zm-4.5 3.2h9a7.5 7.5 0 010 8.6h-9a7.5 7.5 0 010-8.6z" />
<path d="M3 12h18M12 3v18" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.4" />
</svg>
);
// OneDrive — cloud with one-drive O shape
const OnedriveIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M14.5 8a5.5 5.5 0 015.46 4.83A4 4 0 0120 20H7a5 5 0 01-1-9.9V10a7.5 7.5 0 017.5-7.5V8z" />
<path d="M4 14a5 5 0 015-5h1a5.5 5.5 0 00-1 3.16A4 4 0 014.5 20H4a4 4 0 010-8z" fillOpacity="0.7" />
</svg>
);
// Google Drive — drive/triangle icon
const GdriveIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M8.5 3.5L2 15h5l6.5-11.5H8.5z" />
<path d="M22 15l-6.5-11.5h-1.5L22 15z" fillOpacity="0.7" />
<path d="M2 15l4 6.5h12L22 15H2z" fillOpacity="0.5" />
</svg>
);
// Dropbox — box icon
const DropboxIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M12 4.5L6 8l6 3.5 6-3.5-6-3.5z" />
<path d="M6 8l-4 2.5 6 3.5 4-2.5L6 8z" fillOpacity="0.7" />
<path d="M18 8l-6 3.5 4 2.5 6-3.5L18 8z" fillOpacity="0.7" />
<path d="M8 15l4 2.5 4-2.5-4-2.5L8 15z" />
<circle cx="12" cy="20" r="1.5" />
</svg>
);
// Box — stylized box/square icon
const BoxIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<rect x="3" y="8" width="18" height="13" rx="2" />
<path d="M9 8V5a3 3 0 016 0v3" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
<circle cx="12" cy="15" r="2" fill="none" stroke="currentColor" strokeWidth="1.5" />
</svg>
);
// pCloud — cloud with p letter
const PcloudIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M6.5 20a4.5 4.5 0 01-.5-8.97V11a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
<text x="9" y="17.5" fontSize="7" fontFamily="sans-serif" fontWeight="bold">p</text>
</svg>
);
// SFTP — server with lock icon
const SftpIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<rect x="3" y="4" width="18" height="5" rx="1" />
<rect x="3" y="11" width="18" height="5" rx="1" fillOpacity="0.6" />
<circle cx="19" cy="6.5" r="1" fill="white" fillOpacity="0.8" />
<circle cx="19" cy="13.5" r="1" fill="white" fillOpacity="0.8" />
<path d="M12 18a2.5 2.5 0 012.5 2.5v1h-5v-1A2.5 2.5 0 0112 18z" />
<rect x="10" y="16" width="4" height="3" rx="1" />
</svg>
);
// FTP — server icon (simple)
const FtpIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<rect x="3" y="4" width="18" height="5" rx="1" />
<rect x="3" y="11" width="18" height="5" rx="1" fillOpacity="0.6" />
<rect x="3" y="18" width="18" height="3" rx="1" fillOpacity="0.3" />
<circle cx="19" cy="6.5" r="1" fill="white" fillOpacity="0.8" />
<circle cx="19" cy="13.5" r="1" fill="white" fillOpacity="0.8" />
</svg>
);
// WebDAV — globe with network lines
const WebdavIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M12 2a10 10 0 100 20A10 10 0 0012 2zm0 1.5c1 0 2.3.8 3.3 2.5H8.7C9.7 4.3 11 3.5 12 3.5zm-6.5 4h13a8.5 8.5 0 010 9H5.5a8.5 8.5 0 010-9z" fillOpacity="0.8" />
<path d="M2 12h20M12 2v20" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.3" />
</svg>
);
// SMB / Windows Share — folder with network
const SmbIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M2 6.5A2.5 2.5 0 014.5 4H9l2 2h8.5A2.5 2.5 0 0122 8.5v9a2.5 2.5 0 01-2.5 2.5h-15A2.5 2.5 0 012 17.5V6.5z" />
<circle cx="9" cy="14" r="1.5" fill="white" fillOpacity="0.7" />
<circle cx="15" cy="14" r="1.5" fill="white" fillOpacity="0.7" />
<path d="M9 14h6" stroke="white" strokeWidth="1" strokeOpacity="0.7" />
</svg>
);
// HTTP — globe/web icon
const HttpIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M12 2a10 10 0 100 20A10 10 0 0012 2zm0 1.5c1.1 0 2.5.9 3.5 2.5H8.5C9.5 4.4 10.9 3.5 12 3.5zm-7 5h14a8.5 8.5 0 010 7H5a8.5 8.5 0 010-7z" />
<path d="M2 12h20" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.5" />
</svg>
);
// Seafile — sea/wave icon
const SeafileIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<rect x="4" y="4" width="16" height="11" rx="2" fillOpacity="0.8" />
<path d="M4 19c2-2 4-2 6 0s4 2 6 0" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" />
<path d="M4 22c2-2 4-2 6 0s4 2 6 0" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" opacity="0.4" />
</svg>
);
export const BACKEND_ICONS: Partial<Record<BackendType, React.FC<IconProps>>> = {
azureblob: AzureBlobIcon,
s3: S3Icon,
's3-compatible': S3CompatibleIcon,
gcs: GCSIcon,
b2: B2Icon,
'azure-files': AzureFilesIcon,
swift: SwiftIcon,
onedrive: OnedriveIcon,
gdrive: GdriveIcon,
dropbox: DropboxIcon,
box: BoxIcon,
pcloud: PcloudIcon,
sftp: SftpIcon,
ftp: FtpIcon,
webdav: WebdavIcon,
smb: SmbIcon,
http: HttpIcon,
seafile: SeafileIcon,
};
+15 -7
View File
@@ -1,28 +1,36 @@
import type { ButtonHTMLAttributes } from 'react';
import type { ButtonHTMLAttributes, ReactNode } from 'react';
interface BackendCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> {
name: string;
description: string;
selected?: boolean;
onClick: () => void;
icon?: ReactNode;
}
export function BackendCard({ name, description, selected = false, onClick, ...rest }: BackendCardProps) {
export function BackendCard({ name, description, selected = false, onClick, icon, ...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',
'w-full flex flex-col items-start gap-1 rounded-xl border-2 p-4 text-left transition-all focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-primary',
selected
? 'border-blue-600 bg-blue-50'
: 'border-gray-200 bg-white hover:border-blue-400 hover:bg-gray-50',
? 'border-primary bg-primary/10 shadow-md'
: 'border-outline bg-surface-container shadow hover:border-primary hover:bg-surface hover:shadow-md',
].join(' ')}
{...rest}
>
<span className="font-semibold text-gray-900">{name}</span>
<span className="text-sm text-gray-500">{description}</span>
{icon != null ? (
<div className="flex items-center gap-2">
<span className="w-6 h-6 shrink-0">{icon}</span>
<span className="font-semibold text-on-surface">{name}</span>
</div>
) : (
<span className="font-semibold text-on-surface">{name}</span>
)}
<span className="text-sm text-on-surface-container/70">{description}</span>
</button>
);
}
+137
View File
@@ -0,0 +1,137 @@
// @vitest-environment jsdom
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { FieldRenderer } from './FieldRenderer';
import type { UseFormRegister } from 'react-hook-form';
function makeRegister(): UseFormRegister<any> {
return vi.fn((name: string) => ({
name,
ref: vi.fn(),
onChange: vi.fn(),
onBlur: vi.fn(),
})) as unknown as UseFormRegister<any>;
}
const textFieldWithTooltip = {
key: 'bucket_name',
label: 'Bucket Name',
inputType: 'text' as const,
required: true,
tooltipText: 'The name of your storage bucket',
};
const selectFieldWithTooltip = {
key: 'region',
label: 'Region',
inputType: 'select' as const,
required: true,
tooltipText: 'Select your deployment region',
options: [
{ value: 'us-east', label: 'US East' },
{ value: 'eu-west', label: 'EU West' },
],
};
describe('FieldRenderer - tooltip hover behavior', () => {
it('hovering tooltip button shows tooltip text', () => {
render(
<FieldRenderer
field={textFieldWithTooltip}
register={makeRegister()}
/>
);
const tooltipBtn = screen.getByRole('button', { name: /more info about bucket name/i });
fireEvent.mouseEnter(tooltipBtn);
expect(screen.getByText('The name of your storage bucket')).toBeDefined();
});
it('mouse-leaving tooltip button hides tooltip text', () => {
render(
<FieldRenderer
field={textFieldWithTooltip}
register={makeRegister()}
/>
);
const tooltipBtn = screen.getByRole('button', { name: /more info about bucket name/i });
fireEvent.mouseEnter(tooltipBtn);
expect(screen.getByText('The name of your storage bucket')).toBeDefined();
fireEvent.mouseLeave(tooltipBtn);
expect(screen.queryByText('The name of your storage bucket')).toBeNull();
});
it('clicking tooltip button pins it open through mouseLeave', () => {
render(
<FieldRenderer
field={textFieldWithTooltip}
register={makeRegister()}
/>
);
const tooltipBtn = screen.getByRole('button', { name: /more info about bucket name/i });
fireEvent.click(tooltipBtn);
fireEvent.mouseLeave(tooltipBtn);
expect(screen.getByText('The name of your storage bucket')).toBeDefined();
});
it('clicking pinned tooltip button again dismisses it', () => {
render(
<FieldRenderer
field={textFieldWithTooltip}
register={makeRegister()}
/>
);
const tooltipBtn = screen.getByRole('button', { name: /more info about bucket name/i });
fireEvent.click(tooltipBtn);
fireEvent.click(tooltipBtn);
expect(screen.queryByText('The name of your storage bucket')).toBeNull();
});
});
describe('FieldRenderer - DEBT-01 aria-label consistency', () => {
it('Test 1: text-branch tooltip button has aria-label="More info about {field.label}"', () => {
render(
<FieldRenderer
field={textFieldWithTooltip}
register={makeRegister()}
/>
);
const tooltipBtn = screen.getByRole('button', { name: /more info about bucket name/i });
expect(tooltipBtn).toBeDefined();
expect(tooltipBtn.getAttribute('aria-label')).toBe('More info about Bucket Name');
});
it('Test 2: select-branch tooltip button has aria-label="More info about {field.label}"', () => {
render(
<FieldRenderer
field={selectFieldWithTooltip}
register={makeRegister()}
/>
);
const tooltipBtn = screen.getByRole('button', { name: /more info about region/i });
expect(tooltipBtn).toBeDefined();
expect(tooltipBtn.getAttribute('aria-label')).toBe('More info about Region');
});
it('Test 3: text-branch tooltip button is findable via getByRole with name pattern', () => {
render(
<FieldRenderer
field={textFieldWithTooltip}
register={makeRegister()}
/>
);
// This query pattern must work for both branches
const btn = screen.getByRole('button', { name: /more info about/i });
expect(btn).toBeDefined();
});
it('Test 3b: select-branch tooltip button is findable via getByRole with name pattern', () => {
render(
<FieldRenderer
field={selectFieldWithTooltip}
register={makeRegister()}
/>
);
const btn = screen.getByRole('button', { name: /more info about/i });
expect(btn).toBeDefined();
});
});
+41 -42
View File
@@ -2,6 +2,7 @@ import { useState } from 'react';
import type { UseFormRegister, FieldError } from 'react-hook-form';
import type { FieldDef } from '../../schemas/registry';
import { PasswordField } from './PasswordField';
import { TextFieldMD3 } from './TextFieldMD3';
interface FieldRendererProps {
field: FieldDef;
@@ -11,6 +12,7 @@ interface FieldRendererProps {
export function FieldRenderer({ field, register, error }: FieldRendererProps) {
const [showTooltip, setShowTooltip] = useState(false);
const [hoverTooltip, setHoverTooltip] = useState(false);
// provider field: single-option select — hide from UI, auto-register with default value
if (field.key === 'provider' && field.options?.length === 1) {
@@ -30,7 +32,6 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
label={field.label}
error={error}
registration={register(field.key)}
placeholder={field.placeholder}
helpText={field.helpText}
tooltipText={field.tooltipText}
/>
@@ -41,31 +42,33 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
return (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<label htmlFor={field.key} className="text-sm font-medium text-gray-700">
<label htmlFor={field.key} className="text-sm font-medium text-on-surface-container">
{field.label}
{field.required && <span className="ml-1 text-red-500">*</span>}
{field.required && <span className="ml-1 text-error">*</span>}
</label>
{field.tooltipText && (
<button
type="button"
onClick={() => setShowTooltip(v => !v)}
onClick={() => { setShowTooltip(v => { if (v) setHoverTooltip(false); return !v; }); }}
onMouseEnter={() => setHoverTooltip(true)}
onMouseLeave={() => setHoverTooltip(false)}
aria-label={`More info about ${field.label}`}
className="text-blue-500 hover:text-blue-700 text-xs leading-none"
className="text-primary hover:text-primary text-xs leading-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded"
>
</button>
)}
</div>
{field.tooltipText && showTooltip && (
<p className="text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1.5 mt-1">
{field.tooltipText && (showTooltip || hoverTooltip) && (
<p className="text-xs text-primary bg-primary/10 border border-primary/30 rounded px-2 py-1.5 mt-1">
{field.tooltipText}
</p>
)}
<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',
'w-full rounded-md border px-3 py-2 text-sm bg-surface-container text-on-surface focus:outline-none focus-visible:ring-2',
error ? 'border-error focus:ring-error/50' : 'border-outline focus:ring-primary/50',
].join(' ')}
{...register(field.key)}
>
@@ -73,48 +76,44 @@ export function FieldRenderer({ field, register, error }: FieldRendererProps) {
<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>}
{field.helpText && !error && <p className="text-xs text-on-surface-container/70">{field.helpText}</p>}
{error && <p className="text-xs text-error">{error.message}</p>}
</div>
);
}
// text (default)
// text (default) — uses TextFieldMD3 for floating label
const tooltipIcon = field.tooltipText ? (
<button
type="button"
onClick={() => { setShowTooltip(v => { if (v) setHoverTooltip(false); return !v; }); }}
onMouseEnter={() => setHoverTooltip(true)}
onMouseLeave={() => setHoverTooltip(false)}
aria-label={`More info about ${field.label}`}
className="text-primary hover:text-primary/80 text-xs leading-none mt-px shrink-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded"
>
</button>
) : undefined;
const tooltipVisible = showTooltip || hoverTooltip;
return (
<div className="flex flex-col gap-1">
<div className="flex items-center 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>
{field.tooltipText && (
<button
type="button"
onClick={() => setShowTooltip(v => !v)}
className="text-blue-500 hover:text-blue-700 text-xs leading-none"
>
<span className="sr-only">More info about {field.label}</span>
<span aria-hidden="true"></span>
</button>
)}
</div>
{field.tooltipText && showTooltip && (
<p className="text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1.5 mt-1">
<TextFieldMD3
id={field.key}
label={field.label}
error={error}
registration={register(field.key)}
helpText={field.helpText}
required={field.required}
helpTextPrefix={tooltipIcon}
/>
{field.tooltipText && tooltipVisible && (
<p className="text-xs text-primary bg-primary/10 border border-primary/30 rounded px-2 py-1.5">
{field.tooltipText}
</p>
)}
<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>
);
}
+42 -39
View File
@@ -1,5 +1,6 @@
import { useState } from 'react';
import type { FieldError, UseFormRegisterReturn } from 'react-hook-form';
import { TextFieldMD3 } from './TextFieldMD3';
interface PasswordFieldProps {
id: string;
@@ -14,50 +15,52 @@ interface PasswordFieldProps {
export function PasswordField({ id, label, error, registration, placeholder, helpText, tooltipText }: PasswordFieldProps) {
const [show, setShow] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const [hoverTooltip, setHoverTooltip] = useState(false);
const toggleButton = (
<button
type="button"
onClick={() => setShow(v => !v)}
aria-label={show ? 'Hide' : 'Show'}
className="text-on-surface-container/50 hover:text-on-surface-container text-sm"
>
{show ? 'Hide' : 'Show'}
</button>
);
const tooltipIcon = tooltipText ? (
<button
type="button"
onClick={() => { setShowTooltip(v => { if (v) setHoverTooltip(false); return !v; }); }}
onMouseEnter={() => setHoverTooltip(true)}
onMouseLeave={() => setHoverTooltip(false)}
aria-label={`More info about ${label}`}
className="text-primary hover:text-primary/80 text-xs leading-none mt-px shrink-0"
>
</button>
) : undefined;
const tooltipVisible = showTooltip || hoverTooltip;
return (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<label htmlFor={id} className="text-sm font-medium text-gray-700">
{label}
</label>
{tooltipText && (
<button
type="button"
onClick={() => setShowTooltip(v => !v)}
className="text-blue-500 hover:text-blue-700 text-xs leading-none"
>
<span className="sr-only">More info about {label}</span>
<span aria-hidden="true"></span>
</button>
)}
</div>
{tooltipText && showTooltip && (
<p className="text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1.5 mt-1">
<TextFieldMD3
id={id}
label={label}
error={error}
registration={registration}
placeholder={placeholder}
type={show ? 'text' : 'password'}
helpText={helpText}
helpTextPrefix={tooltipIcon}
suffix={toggleButton}
/>
{tooltipText && tooltipVisible && (
<p className="text-xs text-primary bg-primary/10 border border-primary/30 rounded px-2 py-1.5">
{tooltipText}
</p>
)}
<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 ? 'Hide' : '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>
);
}
@@ -0,0 +1,57 @@
// @vitest-environment jsdom
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { RemoteNamePreview } from './RemoteNamePreview';
describe('RemoteNamePreview', () => {
describe('empty state', () => {
it('shows placeholder [my-remote] when value is empty string', () => {
render(<RemoteNamePreview value="" />);
expect(screen.getByText('[my-remote]')).toBeDefined();
});
it('shows placeholder [my-remote] when value is whitespace-only', () => {
render(<RemoteNamePreview value=" " />);
expect(screen.getByText('[my-remote]')).toBeDefined();
});
it('shows guidance text when value is empty', () => {
render(<RemoteNamePreview value="" />);
expect(screen.getByText('Type a name to see how it appears in your config')).toBeDefined();
});
});
describe('value state', () => {
it('shows [my-backup] when value is "my-backup"', () => {
render(<RemoteNamePreview value="my-backup" />);
expect(screen.getByText('[my-backup]')).toBeDefined();
});
it('shows [azure-prod] when value is "azure-prod"', () => {
render(<RemoteNamePreview value="azure-prod" />);
expect(screen.getByText('[azure-prod]')).toBeDefined();
});
it('does NOT show guidance text when value is provided', () => {
render(<RemoteNamePreview value="my-backup" />);
expect(screen.queryByText('Type a name to see how it appears in your config')).toBeNull();
});
});
describe('element types', () => {
it('renders no form controls (no textbox role)', () => {
render(<RemoteNamePreview value="" />);
expect(screen.queryByRole('textbox')).toBeNull();
});
it('renders no input elements', () => {
const { container } = render(<RemoteNamePreview value="test" />);
expect(container.querySelector('input')).toBeNull();
});
it('renders no textarea elements', () => {
const { container } = render(<RemoteNamePreview value="test" />);
expect(container.querySelector('textarea')).toBeNull();
});
});
});
+23
View File
@@ -0,0 +1,23 @@
interface RemoteNamePreviewProps {
value: string;
}
export function RemoteNamePreview({ value }: RemoteNamePreviewProps) {
const trimmed = value.trim();
const isEmpty = trimmed.length === 0;
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>
<p className="mt-1 text-on-surface-variant/40 font-sans">
Type a name to see how it appears in your config
</p>
</>
) : (
<span className="text-on-surface-variant">[{trimmed}]</span>
)}
</div>
);
}
+175
View File
@@ -0,0 +1,175 @@
// @vitest-environment jsdom
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { TextFieldMD3 } from './TextFieldMD3';
import type { FieldError, UseFormRegisterReturn } from 'react-hook-form';
function makeRegistration(overrides?: Partial<UseFormRegisterReturn>): UseFormRegisterReturn {
return {
name: 'testField',
ref: vi.fn(),
onChange: vi.fn(),
onBlur: vi.fn(),
...overrides,
};
}
describe('TextFieldMD3', () => {
it('Test 1: renders an input with the given id and a label with matching htmlFor', () => {
render(
<TextFieldMD3
id="username"
label="Username"
registration={makeRegistration()}
/>
);
const input = screen.getByRole('textbox');
expect(input.getAttribute('id')).toBe('username');
// getByText returns the label element — check tagName and htmlFor
const label = screen.getByText('Username');
expect(label.tagName.toLowerCase()).toBe('label');
expect(label.getAttribute('for')).toBe('username');
});
it('Test 2: getByLabelText(label) finds the input (htmlFor/id pairing works)', () => {
render(
<TextFieldMD3
id="email"
label="Email address"
registration={makeRegistration()}
/>
);
const input = screen.getByLabelText('Email address');
expect(input).toBeDefined();
});
it('Test 3: input receives registration props (can be queried after typing)', async () => {
const onChange = vi.fn();
const registration = makeRegistration({ onChange });
render(
<TextFieldMD3
id="field"
label="Field"
registration={registration}
/>
);
const input = screen.getByLabelText('Field');
await userEvent.type(input, 'hello');
expect(onChange).toHaveBeenCalled();
});
it('Test 4: error message renders with role="alert" when error prop is passed', () => {
const error: FieldError = { type: 'required', message: 'This field is required' };
render(
<TextFieldMD3
id="field"
label="Field"
registration={makeRegistration()}
error={error}
/>
);
const alert = screen.getByRole('alert');
expect(alert.textContent).toBe('This field is required');
});
it('Test 5: help text renders when helpText prop is passed and no error', () => {
render(
<TextFieldMD3
id="field"
label="Field"
registration={makeRegistration()}
helpText="Enter your first name"
/>
);
expect(screen.getByText('Enter your first name')).toBeDefined();
});
it('Test 5b: help text does not render when error is present', () => {
const error: FieldError = { type: 'required', message: 'Required' };
render(
<TextFieldMD3
id="field"
label="Field"
registration={makeRegistration()}
helpText="Enter your first name"
error={error}
/>
);
expect(screen.queryByText('Enter your first name')).toBeNull();
});
it('Test 6: required asterisk renders when required=true', () => {
render(
<TextFieldMD3
id="field"
label="Field"
registration={makeRegistration()}
required
/>
);
expect(screen.getByText('*')).toBeDefined();
});
it('Test 7: suffix slot renders (for PasswordField show/hide button)', () => {
render(
<TextFieldMD3
id="field"
label="Field"
registration={makeRegistration()}
suffix={<button type="button">Show</button>}
/>
);
expect(screen.getByRole('button', { name: 'Show' })).toBeDefined();
});
it('Test 8: input has placeholder=" " for CSS floating label trick', () => {
render(
<TextFieldMD3
id="field"
label="Field"
registration={makeRegistration()}
/>
);
const input = screen.getByLabelText('Field');
expect(input.getAttribute('placeholder')).toBe(' ');
});
it('Test 9: renders helpTextPrefix inline with helpText', () => {
render(
<TextFieldMD3
id="field"
label="Field"
registration={makeRegistration()}
helpText="Some help"
helpTextPrefix={<span data-testid="prefix">icon</span>}
/>
);
expect(screen.getByTestId('prefix')).toBeDefined();
expect(screen.getByText('Some help')).toBeDefined();
});
it('Test 10: renders helpText without wrapper issues when helpTextPrefix is undefined', () => {
render(
<TextFieldMD3
id="field"
label="Field"
registration={makeRegistration()}
helpText="Some help"
/>
);
expect(screen.getByText('Some help')).toBeDefined();
});
it('Test 11: renders helpTextPrefix alone when helpText is absent (tooltip-only field)', () => {
render(
<TextFieldMD3
id="field"
label="Field"
registration={makeRegistration()}
helpTextPrefix={<span data-testid="prefix-only">icon</span>}
/>
);
expect(screen.getByTestId('prefix-only')).toBeDefined();
});
});
+84
View File
@@ -0,0 +1,84 @@
import type { UseFormRegisterReturn, FieldError } from 'react-hook-form';
interface TextFieldMD3Props {
id: string;
label: string;
error?: FieldError;
registration: UseFormRegisterReturn;
type?: 'text' | 'password';
placeholder?: string;
helpText?: string;
helpTextPrefix?: React.ReactNode;
required?: boolean;
suffix?: React.ReactNode;
}
export function TextFieldMD3({
id,
label,
error,
registration,
type = 'text',
placeholder,
helpText,
helpTextPrefix,
required,
suffix,
}: TextFieldMD3Props) {
return (
<div className="flex flex-col gap-1">
<div className="relative">
<input
id={id}
type={type}
placeholder={placeholder || " "}
className={[
'peer w-full rounded-md border bg-transparent px-3 pb-2 pt-5 text-sm text-on-surface',
'placeholder-transparent focus:outline-none focus:ring-2',
error
? 'border-error focus:border-error focus:ring-error/50'
: 'border-outline focus:border-primary focus:ring-primary/20',
suffix ? 'pr-10' : '',
]
.filter(Boolean)
.join(' ')}
{...registration}
/>
<label
htmlFor={id}
className={[
'pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm',
'origin-left transform transition-all duration-200',
'text-on-surface-container/60',
// Focused state: float up
'peer-focus:top-3 peer-focus:-translate-y-0 peer-focus:scale-75',
error ? 'peer-focus:text-error' : 'peer-focus:text-primary',
// Has-value state (input not showing placeholder)
'peer-[:not(:placeholder-shown)]:top-3',
'peer-[:not(:placeholder-shown)]:-translate-y-0',
'peer-[:not(:placeholder-shown)]:scale-75',
].join(' ')}
>
{label}
{required && <span className="ml-0.5 text-error">*</span>}
</label>
{suffix && (
<div className="absolute right-2 top-1/2 -translate-y-1/2">{suffix}</div>
)}
</div>
{(helpText || helpTextPrefix) && !error && (
<div className="flex items-start gap-1">
{helpTextPrefix}
{helpText && (
<p className="text-xs text-on-surface-container/70">{helpText}</p>
)}
</div>
)}
{error && (
<p className="text-xs text-error" role="alert">
{error.message}
</p>
)}
</div>
);
}
+101
View File
@@ -0,0 +1,101 @@
// @vitest-environment jsdom
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeToggle } from './ThemeToggle';
// Node v22+ has experimental built-in localStorage that lacks standard Storage methods.
// Replace it with a fully-functional in-memory mock for the duration of these tests.
const makeLocalStorageMock = () => {
let store: Record<string, string> = {};
return {
getItem: (key: string): string | null => store[key] ?? null,
setItem: (key: string, value: string): void => { store[key] = String(value); },
removeItem: (key: string): void => { delete store[key]; },
clear: (): void => { store = {}; },
};
};
const localStorageMock = makeLocalStorageMock();
// jsdom does not implement window.matchMedia — provide a minimal stub
const matchMediaMock = vi.fn((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}));
describe('ThemeToggle', () => {
beforeEach(() => {
// Stub localStorage with a real in-memory implementation
vi.stubGlobal('localStorage', localStorageMock);
localStorageMock.clear();
// Stub matchMedia
vi.stubGlobal('matchMedia', matchMediaMock);
// Reset DOM dark class
document.documentElement.classList.remove('dark');
});
it('renders three buttons: Light, Dark, System', () => {
render(<ThemeToggle />);
expect(screen.getByRole('button', { name: /light/i })).toBeDefined();
expect(screen.getByRole('button', { name: /dark/i })).toBeDefined();
expect(screen.getByRole('button', { name: /system/i })).toBeDefined();
});
it('default state is System (aria-pressed="true" on System button) when localStorage is empty', () => {
render(<ThemeToggle />);
const systemButton = screen.getByRole('button', { name: /system/i });
const lightButton = screen.getByRole('button', { name: /light/i });
const darkButton = screen.getByRole('button', { name: /dark/i });
expect(systemButton.getAttribute('aria-pressed')).toBe('true');
expect(lightButton.getAttribute('aria-pressed')).toBe('false');
expect(darkButton.getAttribute('aria-pressed')).toBe('false');
});
it('clicking Dark adds .dark class to document.documentElement', async () => {
const user = userEvent.setup();
render(<ThemeToggle />);
const darkButton = screen.getByRole('button', { name: /dark/i });
await user.click(darkButton);
expect(document.documentElement.classList.contains('dark')).toBe(true);
});
it('clicking Light removes .dark class from document.documentElement', async () => {
const user = userEvent.setup();
// Start with dark class applied
document.documentElement.classList.add('dark');
render(<ThemeToggle />);
const lightButton = screen.getByRole('button', { name: /light/i });
await user.click(lightButton);
expect(document.documentElement.classList.contains('dark')).toBe(false);
});
it('clicking Dark writes "dark" to localStorage under key "r2b-theme"', async () => {
const user = userEvent.setup();
render(<ThemeToggle />);
const darkButton = screen.getByRole('button', { name: /dark/i });
await user.click(darkButton);
expect(localStorage.getItem('r2b-theme')).toBe('dark');
});
it('active button has aria-pressed="true", others have aria-pressed="false"', async () => {
const user = userEvent.setup();
render(<ThemeToggle />);
const darkButton = screen.getByRole('button', { name: /dark/i });
await user.click(darkButton);
expect(darkButton.getAttribute('aria-pressed')).toBe('true');
expect(screen.getByRole('button', { name: /light/i }).getAttribute('aria-pressed')).toBe('false');
expect(screen.getByRole('button', { name: /system/i }).getAttribute('aria-pressed')).toBe('false');
});
it('component group has aria-label="Theme"', () => {
render(<ThemeToggle />);
expect(screen.getByRole('group', { name: 'Theme' })).toBeDefined();
});
});
+70
View File
@@ -0,0 +1,70 @@
// src/components/ui/ThemeToggle.tsx
// Segmented theme control — writes to localStorage and toggles .dark on <html>.
// Uses DOM class toggle (no React Context) to avoid subtree re-render cascade.
//
// Note: On Vite HMR, getStored() re-reads localStorage on remount.
// This is correct for production (hard reloads are always consistent).
// In dev, toggling via DevTools and triggering HMR may show a brief state mismatch — expected.
import { useState } from 'react';
type ThemeValue = 'light' | 'dark' | 'system';
const STORAGE_KEY = 'r2b-theme';
function getStored(): ThemeValue {
try {
const v = localStorage.getItem(STORAGE_KEY);
if (v === 'light' || v === 'dark' || v === 'system') return v;
} catch {
// localStorage unavailable (private mode, etc.)
}
return 'system';
}
function applyTheme(value: ThemeValue): void {
const root = document.documentElement;
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = value === 'dark' || (value === 'system' && prefersDark);
root.classList.toggle('dark', isDark);
try {
localStorage.setItem(STORAGE_KEY, value);
} catch {
// localStorage unavailable — silently ignore
}
}
const LABELS: Record<ThemeValue, string> = {
light: '\u2600 Light',
dark: '\uD83C\uDF19 Dark',
system: '\u2299 System',
};
export function ThemeToggle() {
const [theme, setTheme] = useState<ThemeValue>(getStored);
function select(value: ThemeValue): void {
setTheme(value);
applyTheme(value);
}
return (
<div role="group" aria-label="Theme" className="flex rounded-md border border-outline overflow-hidden text-sm">
{(['light', 'dark', 'system'] as const).map((v) => (
<button
key={v}
type="button"
onClick={() => select(v)}
aria-pressed={theme === v}
className={
theme === v
? 'flex-1 px-3 py-1 bg-primary text-on-primary font-medium focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-primary focus-visible:ring-inset'
: 'flex-1 px-3 py-1 bg-surface-container text-on-surface-container hover:bg-surface focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-primary focus-visible:ring-inset'
}
>
{LABELS[v]}
</button>
))}
</div>
);
}
+5 -5
View File
@@ -22,15 +22,15 @@ export function AzureAuthToggle({ register, errors }: AzureAuthToggleProps) {
return (
<div className="flex flex-col gap-3">
{/* Segmented control */}
<div className="flex rounded-md border border-gray-300 overflow-hidden">
<div className="flex rounded-md border border-outline 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',
? 'bg-primary text-on-primary'
: 'bg-surface-container text-on-surface-container hover:bg-surface',
].join(' ')}
>
SAS URL
@@ -41,8 +41,8 @@ export function AzureAuthToggle({ register, errors }: AzureAuthToggleProps) {
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',
? 'bg-primary text-on-primary'
: 'bg-surface-container text-on-surface-container hover:bg-surface',
].join(' ')}
>
Access Key

Some files were not shown because too many files have changed in this diff Show More