Files
Ready2Blob/.planning/phases/09-md3-components/09-RESEARCH.md
T

680 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)