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

34 KiB
Raw Blame History

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.

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:

// 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.

// 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):

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:

// 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:

// 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:

// 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:

// 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

// 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

// 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

/* 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)

Secondary (MEDIUM confidence)

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)