Files
resolutionflow/docs/tailwind-v4-migration.md
chihlasm d365c38b61 chore: Tailwind CSS v3 → v4 migration (#99)
* chore: run Tailwind v4 upgrade tool (Phase 1)

- Upgraded tailwindcss v3 → v4.2.1, postcss plugin to @tailwindcss/postcss
- Deleted tailwind.config.js, migrated theme to CSS @theme block in index.css
- Replaced @tailwind directives with @import 'tailwindcss'
- Added @custom-variant dark, @utility blocks for custom utilities
- Updated class names across 128 files (shadow-sm → shadow-xs, etc.)
- Removed autoprefixer (built into v4)
- Added migration plan doc

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: switch from @tailwindcss/postcss to @tailwindcss/vite (Phase 2)

- Replaced @tailwindcss/postcss with @tailwindcss/vite plugin
- Deleted postcss.config.js (no longer needed)
- Tailwind now runs as a native Vite plugin for faster HMR

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: convert to OKLCH colors, move keyframes into @theme (Phase 3-4)

- Replaced all HSL color indirection with direct OKLCH values in @theme
- Moved all keyframes inside @theme block (v4 pattern)
- Eliminated hsl(var(--x)) double-indirection across 17 component files
- Replaced hsl() inline styles with var(--color-*) theme references
- Cleaned up redundant rdp-* utility blocks
- Fixed @custom-variant dark syntax to use :where()
- Added sidebar/glass/shadow vars as OKLCH in :root

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 22:10:44 -05:00

21 KiB
Raw Blame History

ResolutionFlow — Tailwind v4 Migration & Feature Guide

Claude Code Handoff Document · Pre-Investor Pitch Sprint


Purpose

This document gives Claude Code everything needed to:

  1. Execute the mechanical Tailwind v3 → v4 upgrade
  2. Know which new v4 features to use going forward so the ResolutionFlow UI looks and feels premium for the investor pitch demo

Branch

This migration touches nearly every file in the frontend. Always work on a dedicated branch:

git checkout -b feat/tailwind-v4-upgrade

Do not merge to main until all phases are complete and the full visual QA pass is done.


Stack Context

Frontend React 19.2 + Vite 7 + TypeScript 5.9
Current Tailwind v3.4.19
Component Library shadcn/ui (target: new-york style post-upgrade)
Canvas Editor React Flow (@xyflow/react)
Brand Color Cyan — #06b6d4 → #22d3ee (hsl 187 72% 43%)
Background hsl(228 12% 7%) — dark theme only, no light mode
Body Font IBM Plex Sans
Heading Font Bricolage Grotesque
Label/Mono Font JetBrains Mono
Brand Gradient linear-gradient(135deg, #06b6d4 0%, #22d3ee 100%)
Animation Library tailwindcss-animate → replace with tw-animate-css
Tailwind Plugins None


PART 1: Mechanical Migration

Execute phases in order. Verify the app builds and renders correctly after each phase before proceeding.


Phase 1 — Update Dependencies

Run the official Tailwind upgrade tool from the /frontend directory. It handles the majority of migration automatically:

# Run from /frontend
npx @tailwindcss/upgrade@latest

This tool will:

  • Update tailwindcss to v4
  • Install @tailwindcss/vite (replaces the PostCSS plugin)
  • Migrate tailwind.config.js → CSS @theme block
  • Update @tailwind directives to @import
  • Handle renamed utility classes in template files

After running, verify package.json reflects:

// devDependencies should now include:
"tailwindcss": "^4.x.x",
"@tailwindcss/vite": "^4.x.x"

// These are no longer needed and can be removed:
// "autoprefixer" (bundled in v4 via Lightning CSS)
// "postcss" (unless used for something other than Tailwind)

Phase 2 — Update Vite Config

Replace the PostCSS-based Tailwind setup with the first-party Vite plugin:

// vite.config.ts — BEFORE
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
})

// vite.config.ts — AFTER
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [react(), tailwindcss()],
})

Remove postcss.config.js if it exists and was only used for Tailwind. Lightning CSS is now bundled inside Tailwind v4.


Phase 3 — Migrate CSS Entry Point

Update index.css:

/* BEFORE — v3 directives */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* AFTER — v4 single import */
@import "tailwindcss";
@import "tw-animate-css";

/* ResolutionFlow is dark-only. Replace darkMode: ["class"] from tailwind.config.js
   with this custom variant declaration. This is the v4 equivalent. */
@custom-variant dark (&:where(.dark, .dark *));

/* React Flow styles must now live in CSS, not imported in App.tsx */
@layer base {
  @import "@xyflow/react/dist/style.css";
}

Important: Remove import '@xyflow/react/dist/style.css' from App.tsx after adding it here. Important: Remove darkMode: ["class"] from tailwind.config.js — it is now handled by @custom-variant dark above. Since ResolutionFlow is dark-only with no light mode toggle, verify that the .dark class is still present on the <html> element in index.html.


Phase 4 — Migrate Theme Configuration

The upgrade tool migrates tailwind.config.js automatically, but verify the ResolutionFlow design tokens are correctly expressed. The @theme block lives in index.css after the imports.

OKLCH Color Values

Claude Code flagged that the cyan range benefits from OKLCH for better gradient interpolation. Use these OKLCH equivalents for brand colors inside @theme — they render more accurately on wide-gamut displays (MacBooks, modern monitors used in pitch settings):

Hex OKLCH
#06b6d4 (brand-from) oklch(72% 0.15 195)
#22d3ee (brand-to) oklch(82% 0.13 195)
#0891b2 (brand-dark) oklch(62% 0.14 195)

Full @theme Block

@import "tailwindcss";
@import "tw-animate-css";

@custom-variant dark (&:where(.dark, .dark *));

@theme {
  /* ResolutionFlow Brand — Cyan system (OKLCH for wide-gamut accuracy) */
  --color-brand-from: oklch(72% 0.15 195);   /* #06b6d4 */
  --color-brand-to: oklch(82% 0.13 195);     /* #22d3ee */
  --color-brand-dark: oklch(62% 0.14 195);   /* #0891b2 */

  /* Dark surface palette */
  --color-dark-DEFAULT: #101114;
  --color-dark-card: #14161a;
  --color-dark-surface: #14161a;

  /* Text palette */
  --color-text-primary: #f8fafc;
  --color-text-secondary: #8891a0;
  --color-text-muted: #5a6170;

  /* Typography */
  --font-sans: 'IBM Plex Sans', system-ui, -apple-system, sans-serif;
  --font-heading: 'Bricolage Grotesque', system-ui, sans-serif;
  --font-label: 'JetBrains Mono', monospace;

  /* Border radius */
  --radius-lg: 0.75rem;
  --radius-md: calc(0.75rem - 2px);
  --radius-sm: calc(0.75rem - 4px);

  /* Keyframe animations — move ALL @keyframes from index.css into @theme */
  --animate-fade-in: fade-in 200ms ease-out;
  --animate-fade-in-up: fade-in-up 200ms ease-out;
  --animate-slide-in-left: slide-in-from-left 200ms ease-out;
  --animate-slide-in-bottom: slide-in-from-bottom 200ms ease-out;
  --animate-scale-in: scale-in 150ms ease-out;
  --animate-breathe-glow: breatheGlow 3s ease-in-out infinite alternate;
  --animate-bell-wobble: bellWobble 0.5s ease-in-out;

  @keyframes fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
  }
  @keyframes fade-in-up {
    from { opacity: 0; transform: translateY(4px); }
    to { opacity: 1; transform: translateY(0); }
  }
  @keyframes slide-in-from-left {
    from { transform: translateX(-100%); }
    to { transform: translateX(0); }
  }
  @keyframes slide-in-from-bottom {
    from { opacity: 0; transform: translateY(16px); }
    to { opacity: 1; transform: translateY(0); }
  }
  @keyframes scale-in {
    from { opacity: 0; transform: scale(0.95); }
    to { opacity: 1; transform: scale(1); }
  }
  @keyframes breatheGlow {
    from { box-shadow: 0 8px 32px rgba(0,0,0,0.3), 0 0 20px oklch(72% 0.15 195 / 0.04); }
    to   { box-shadow: 0 8px 32px rgba(0,0,0,0.3), 0 0 30px oklch(72% 0.15 195 / 0.12); }
  }
  @keyframes bellWobble {
    0%   { transform: rotate(0deg); }
    20%  { transform: rotate(8deg); }
    40%  { transform: rotate(-6deg); }
    60%  { transform: rotate(4deg); }
    80%  { transform: rotate(-2deg); }
    100% { transform: rotate(0deg); }
  }
}

Important: Remove the loose @keyframes blocks from index.css after moving them into @theme. They should not exist in both places.

shadcn/ui CSS Variable Bridge

Keep the existing :root block as-is — shadcn still uses HSL format for its variables:

/* shadcn/ui CSS variable bridge — keep as-is from existing index.css */
@layer base {
  :root {
    --background: 228 12% 7%;
    --foreground: 210 40% 98%;
    --card: 220 10% 10%;
    --card-foreground: 210 40% 98%;
    --popover: 220 10% 10%;
    --popover-foreground: 210 40% 98%;
    --primary: 187 72% 43%;
    --primary-foreground: 228 12% 7%;
    --secondary: 220 8% 14%;
    --secondary-foreground: 210 40% 98%;
    --muted: 220 8% 14%;
    --muted-foreground: 215 10% 58%;
    --accent: 220 8% 14%;
    --accent-foreground: 210 40% 98%;
    --destructive: 350 81% 55%;
    --destructive-foreground: 210 40% 98%;
    --border: 220 8% 14%;
    --input: 220 8% 14%;
    --ring: 187 72% 43%;
    --radius: 0.75rem;
    /* ... rest of existing tokens ... */
  }
}

---

## Phase 5  Replace Animation Library

```bash
# Remove old plugin
npm uninstall tailwindcss-animate

# Install replacement
npm install -D tw-animate-css

Remove any @plugin 'tailwindcss-animate' references from CSS files. The @import "tw-animate-css" added in Phase 3 replaces it.


Phase 7 — Consolidate Hardcoded Inline Colors

Claude Code identified 128 instances of style={{}} inline color values across 44 files, including atmosphere orbs in AppLayout.tsx using hardcoded rgba(6,182,212,...). Now that brand colors are proper CSS variables in @theme, replace all hardcoded cyan/brand references with variables.

Search and replace targets:

// FIND these hardcoded patterns:
rgba(6, 182, 212, ...)    // brand-from with opacity
rgba(34, 211, 238, ...)   // brand-to with opacity
#06b6d4                   // brand-from hex
#22d3ee                   // brand-to hex
#0891b2                   // brand-dark hex

// REPLACE with CSS variable equivalents:
oklch(72% 0.15 195 / 0.XX)   // brand-from with opacity (XX = your alpha)
oklch(82% 0.13 195 / 0.XX)   // brand-to with opacity
var(--color-brand-from)       // brand-from solid
var(--color-brand-to)         // brand-to solid
var(--color-brand-dark)       // brand-dark solid

Specifically in AppLayout.tsx — atmosphere orbs should become:

// BEFORE
style={{ background: 'rgba(6, 182, 212, 0.08)' }}

// AFTER — references the theme token, respects any future brand color changes
style={{ background: 'oklch(72% 0.15 195 / 0.08)' }}

// Or better yet, move to a CSS utility class entirely:
// .atmosphere-orb { background: oklch(72% 0.15 195 / 0.08); }

This is the right time to do this cleanup — the theme is already being touched, and it means future brand color changes only require updating @theme instead of hunting through 44 files.


Phase 8 — Reinstall shadcn/ui Components

shadcn/ui components have been updated for Tailwind v4 and React 19. Commit any custom component modifications first, then reinstall to get refreshed versions.

# Re-initialize with v4 defaults
npx shadcn@latest init

# When prompted:
#   Style: new-york        (more refined than 'default')
#   Base color: cyan       (matches ResolutionFlow brand)

# Reinstall actively used components:
npx shadcn@latest add button card dialog dropdown-menu
npx shadcn@latest add badge input label select textarea
npx shadcn@latest add table tabs tooltip separator
npx shadcn@latest add sheet command popover

What's new in shadcn v4 components:

  • forwardRef removed from all components (React 19 handles ref natively)
  • data-slot attributes added for easier CSS targeting
  • HSL colors converted to OKLCH for wider color gamut
  • Dark mode colors revisited for better accessibility
  • tailwindcss-animate deprecated in favor of tw-animate-css

Phase 9 — Visual QA Checklist

Work through the app after Phases 16. These are the most likely regression areas:

  • Borders — default border color changed from gray-200 to currentColor. Check all card, table, and input borders against the hsl(var(--border)) token.
  • Focus rings — default ring changed from 3px blue to 1px currentColor. Verify form fields and button focus states look intentional.
  • Placeholder text — changed from gray-400 to currentColor at 50% opacity. Check all inputs.
  • Button cursors — now cursor:default (browser standard). Add cursor-pointer explicitly where desired.
  • React Flow canvas — verify nodes, handles, edges, and controls render correctly with CSS import moved to @layer base.
  • Animations — check dialog open/close, sheet slide, dropdown appear animations after the animation library swap.
  • Keyframe animations — verify breatheGlow on stat cards, bellWobble on notification bell, and all fade-in-* utilities still work after moving keyframes into @theme.
  • Cyan brand gradient — verify bg-gradient-brand still renders correctly. Check it looks at least as good (ideally better) with OKLCH values.
  • Atmosphere orbs — verify AppLayout.tsx orbs render correctly after inline style replacement with OKLCH variables.
  • Glass morphism — verify .glass-card and .glass-card-static backdrop blur still applies on all surfaces.
  • JetBrains Mono — verify label/code elements still use mono font correctly.
  • Sonner toasts — verify custom toast styling (card bg, border, icon colors) still applies.
  • Dark variant — verify .dark class on <html> is present and the @custom-variant dark replacement behaves identically to the old darkMode: ["class"] config.

Commit Strategy

git checkout -b feat/tailwind-v4-upgrade

git commit -m "chore: upgrade Tailwind v3 → v4, update Vite plugin"
git commit -m "chore: migrate CSS entry point — @import, @custom-variant dark"
git commit -m "chore: migrate theme to @theme block — OKLCH colors, keyframes"
git commit -m "chore: swap tailwindcss-animate for tw-animate-css"
git commit -m "chore: consolidate 128 hardcoded inline colors to CSS variables"
git commit -m "chore: reinstall shadcn/ui components for v4 (new-york style, cyan)"
git commit -m "fix: visual QA pass — borders, rings, placeholders, glass, animations"
git commit -m "feat: apply v4 feature enhancements across components"

# When complete and QA passes:
git checkout main
git merge feat/tailwind-v4-upgrade


PART 2: Tailwind v4 Feature Guide

These are new capabilities available after the upgrade. Apply them proactively when building or modifying components — do not default to v3 patterns when a v4 equivalent is listed here.


Feature 1 — Auto-Resizing Textareas (field-sizing-content)

Apply to every multi-line text input in the app. Engineers typing session notes and documentation should never fight a fixed-height box.

Where to apply: Session runner documentation field · step notes input · FlowPilot prompt input · any multi-line input in the flow editor

// BEFORE — v3, fixed height or JS resize logic
<textarea
  className="w-full resize-none h-32 rounded-md border p-3"
/>

// AFTER — v4, grows automatically as the engineer types
<textarea
  className="w-full field-sizing-content min-h-[80px] max-h-[400px]
             rounded-md border border-input bg-background p-3
             text-sm focus-visible:ring-2 focus-visible:ring-ring"
/>

Feature 2 — CSS-Native Enter Animations (@starting-style)

Use for elements that appear in the session runner — step transitions, panel reveals, status banners. No JavaScript animation library needed for these patterns.

Where to apply: Step cards appearing in session runner · status/alert banners · completion state reveals · FlowPilot suggestion panels

/* index.css — define the utility */
@utility fade-in-up {
  transition: opacity 300ms ease, transform 300ms ease;
  opacity: 1;
  transform: translateY(0);

  @starting-style {
    opacity: 0;
    transform: translateY(8px);
  }
}

@utility scale-in {
  transition: opacity 150ms ease, transform 150ms ease;
  opacity: 1;
  transform: scale(1);

  @starting-style {
    opacity: 0;
    transform: scale(0.97);
  }
}
// Component usage — animates in on mount with no JS
<div className="fade-in-up">
  <StepCard step={currentStep} />
</div>

<div className="scale-in">
  <FlowPilotPanel />
</div>

Feature 3 — Container Queries (@container)

Use for components that need to adapt based on where they are rendered — not the viewport. Critical for ResolutionFlow where the same component may appear in a sidebar, modal, or full-width panel.

Where to apply: StepCard (sidebar vs full-width) · FlowPilot panel (collapsed vs expanded) · documentation preview panel · step library items

// Wrap the parent with @container
<div className="@container w-full">
  <StepCard step={step} />
</div>

// Inside StepCard — responds to its container, not the viewport
function StepCard({ step }: { step: Step }) {
  return (
    <div className="
      flex flex-col gap-2
      @sm:flex-row @sm:items-center
      @lg:gap-4
      p-4 rounded-lg border border-border bg-card
    ">
      <StepIcon type={step.type} />
      <StepContent step={step} />
      <div className="@sm:ml-auto">
        <StepActions step={step} />
      </div>
    </div>
  )
}

Feature 4 — Enhanced Gradient APIs

The cyan brand gradient is ResolutionFlow's primary visual identity. v4 unlocks radial gradients, angle control, and OKLCH interpolation for richer rendering.

Where to apply: Hero/header sections · card hover states · active step indicators · FlowPilot branding elements · progress indicators

// Linear — explicit angle control (v4)
<div className="bg-linear-135 from-[#06b6d4] to-[#22d3ee]">

// Radial — great for glow effects on active/highlighted elements (v4)
<div className="bg-radial from-[#06b6d4]/20 to-transparent">

// Radial with position — spotlight effect (v4)
<div className="bg-radial-[at_30%_50%] from-[#06b6d4]/15 via-[#22d3ee]/8 to-transparent">

// OKLCH interpolation — richer, more accurate gradient transition (v4)
<div className="bg-linear-to-r from-[#06b6d4] to-[#22d3ee] [color-interpolation-method:oklch]">

// Combine with glass card for the premium demo look
<div className="glass-card-static bg-radial-[at_top_left] from-[#06b6d4]/10 to-transparent">

Feature 5 — Dynamic Utility Values

In v3, non-standard values required bracket notation. In v4 the scale is continuous — use values directly.

// v3 — arbitrary brackets for anything off-scale
<div className="w-[18px] h-[18px] z-[60] grid-cols-[repeat(7,1fr)]">

// v4 — direct values work without brackets
<div className="w-4.5 h-4.5 z-60 grid-cols-7">

// Grid columns support any number directly
<div className="grid grid-cols-7">   // 7-column grid, no config needed
<div className="grid grid-cols-15">  // 15-column, works out of the box

Feature 6 — not-* Variant

Style elements only when they don't match a condition. Useful in the session runner step list.

// Bottom border on all steps except the last
<div className="not-last:border-b border-border pb-4 mb-4">
  <StepCard />
</div>

// Dim inactive steps
<div className="not-[.active]:opacity-60 not-[.active]:hover:opacity-80 transition-opacity">
  <StepCard />
</div>

Feature 7 — Theme Tokens as Native CSS Variables

After the v4 migration, all @theme tokens are exposed as native CSS custom properties. This means you can access ResolutionFlow design tokens in TypeScript — useful for React Flow node styling and Recharts chart colors.

// Access brand colors in JS/TS — no more hardcoded hex values
const style = getComputedStyle(document.documentElement)
const brandColor = style.getPropertyValue('--color-brand-from').trim() // '#06b6d4'

// Use directly in React Flow node styles
const nodeStyles = {
  background: 'var(--color-dark-card)',
  border: '1px solid var(--color-brand-from)',
  boxShadow: '0 0 12px rgba(6, 182, 212, 0.15)',
}

// Use in Recharts chartConfig — no more hsl() wrapper needed
const chartConfig = {
  sessions: {
    label: 'Sessions',
    color: 'var(--color-brand-from)',
  },
  resolved: {
    label: 'Resolved',
    color: 'var(--color-brand-to)',
  },
}

Feature 8 — Color Scheme Utilities

Control native browser UI element theming (scrollbars, form controls) to match the dark theme — previously required custom CSS.

// Apply to the root html element in index.html or App.tsx
<html className="scheme-dark">

// This makes native browser scrollbars, select dropdowns,
// date inputs, and form controls render in dark mode automatically.
// ResolutionFlow is dark-only so this should be applied globally.

Feature 9 — inert Utility

Disable interaction on an entire subtree without JavaScript. Useful for locking the flow editor or step list during FlowPilot AI generation.

// Disable the step list while FlowPilot is generating
<div className={isGenerating ? 'inert' : ''}>
  <StepList steps={steps} />
</div>

// The inert attribute disables all pointer events, focus, and
// accessibility interaction on the entire subtree at once.

General Rules for v4 Going Forward

  1. Use field-sizing-content on every <textarea> — no exceptions
  2. Use @starting-style animations instead of adding/removing CSS classes for enter effects
  3. Wrap repeated components in @container when they appear in variable-width contexts
  4. Use not-last: instead of custom :not(:last-child) selectors in lists
  5. Reference var(--color-brand-from) and var(--color-brand-to) in JS instead of hardcoded #06b6d4 or rgba(6,182,212,...)
  6. Use OKLCH for any new color valuesoklch(72% 0.15 195) not #06b6d4
  7. All new keyframe animations go inside @theme — not loose in index.css
  8. Do not use tailwindcss-animate — replaced by tw-animate-css
  9. Do not add @tailwind base/components/utilities directives — replaced by @import "tailwindcss"
  10. Do not add darkMode: ["class"] to any config — replaced by @custom-variant dark
  11. Prefer bg-linear-135 syntax over bg-gradient-to-r for the brand gradient — more explicit and v4-native

ResolutionFlow · Tailwind v4 Migration Doc · Pre-Investor Pitch Sprint