AutomationWorkflow — "Domino Effect Scroll" Section Implementation Plan
AutomationWorkflow — “Domino Effect Scroll” Section Implementation Plan
Section titled “AutomationWorkflow — “Domino Effect Scroll” Section Implementation Plan”For Claude: REQUIRED SUB-SKILL: Use
superpowers:executing-plansto implement this plan task-by-task.
Goal: Build AutomationWorkflow.tsx, a scroll-driven landing page section that animates a glowing “fiber optic” vertical spine, sequentially revealing four clinical workflow stages as the user scrolls, culminating in a trust badge — all in ClinicFlow’s Luxury Minimal / Dark Glassmorphism aesthetic.
Architecture: A single React component (AutomationWorkflow.tsx) acts as the section shell. It owns a useScroll/useTransform setup (Framer Motion) that drives a shared scrollProgress value. That value is passed via props (not Context) to four WorkflowStageCard atomic sub-components and one DominoSpine. UI mockups inside each card are isolated presentational components. The entire section uses a tall scroll container with a sticky inner layout — the standard scroll-jacking-free parallax pattern.
Tech Stack: React 18 + TypeScript, Framer Motion ^12, Tailwind CSS (project tokens), Vitest + @testing-library/react for unit tests, Playwright for E2E smoke test. No new libraries required.
Design Direction Summary (frontend-design DFII)
Section titled “Design Direction Summary (frontend-design DFII)”| Dimension | Score | Rationale |
|---|---|---|
| Aesthetic Impact | 5 | Glowing spine + sequential reveal is visually unforgettable |
| Context Fit | 5 | Dark glass on clinical SaaS reads as premium/trustworthy |
| Implementation Feasibility | 4 | Framer Motion useTransform is well-understood |
| Performance Safety | 4 | will-change: transform + prefers-reduced-motion fallback |
| Consistency Risk | 2 | All tokens from existing glass-panel, --accent, --foreground |
| DFII Score | 16 (capped at 15+) | → Execute fully |
Aesthetic name: Luxury Minimal / Dark Glassmorphism Differentiation anchor: The single glowing spine that physically connects the cards — if screenshotted without a logo, the vertical light-beam is the unmistakable ClinicFlow signature. Tone: Luxury + Utilitarian (dual blend).
Copywriting Psychology Analysis
Section titled “Copywriting Psychology Analysis”Target human: Dentist-owner, 35–55, frustrated by administrative overhead. Awareness stage: Solution Aware — they know automation software exists, they need to believe ClinicFlow’s specific workflow is superior.
Dominant mechanism: Sequential relief narrative — each stage lands like “you don’t have to do this anymore.” The progression from clinical (trusted territory) → to WhatsApp (astonishing automation) builds from identity confirmation → time savings → awe.
Proof placement: Stage 2 (“El presupuesto se crea solo”) is the highest skepticism point. The TreatmentPlanMockup must show realistic procedure names and a specific price total — not a wireframe — to suppress the “does this actually work?” resistance reflex (per copywriting-psychologist skill: add proof at the resistance point).
Copy voice: Second person, active, present tense (“Trabaje”, “Nosotros hacemos”). Specificity over vague benefit language (“los 3 mejores espacios” not “multiple slots”).
File Map
Section titled “File Map”frontend/src/├── pages/landing/│ ├── index.tsx MODIFY (add AutomationWorkflow import + JSX)│ └── AutomationWorkflow.tsx CREATE├── components/landing/ CREATE DIR│ ├── DominoSpine.tsx CREATE│ ├── WorkflowStageCard.tsx CREATE│ └── mockups/ CREATE DIR│ ├── OdontogramMockup.tsx CREATE│ ├── TreatmentPlanMockup.tsx CREATE│ ├── AgendaMockup.tsx CREATE│ └── WhatsAppMockup.tsx CREATE└── __tests__/landing/ CREATE DIR ├── DominoSpine.test.tsx CREATE ├── WorkflowStageCard.test.tsx CREATE ├── UIMockups.test.tsx CREATE └── AutomationWorkflow.test.tsx CREATE
frontend/e2e/└── automation-workflow.spec.ts CREATE
components/landing/does not yet exist. Sub-components belong there (matches project component/page separation). Section shell lives inpages/landing/matching all other sections.
Pre-flight: Verify Environment
Section titled “Pre-flight: Verify Environment”cd frontendnpm run test -- --reporter=verbose 2>&1 | tail -5# Expected: All existing tests pass. Zero failures.If tests are red, do NOT proceed. Investigate and fix first.
Task 1 — Create DominoSpine (the glowing vertical line)
Section titled “Task 1 — Create DominoSpine (the glowing vertical line)”Files:
- Create:
frontend/src/components/landing/DominoSpine.tsx - Create:
frontend/src/__tests__/landing/DominoSpine.test.tsx
What it does
Section titled “What it does”A fixed-height vertical track (100% of parent height). A gradient-filled inner bar whose scaleY (from top, originY: 0) is driven by a MotionValue<number> prop. At progress = 0 the bar is invisible; at progress = 1 the bar fills the spine. A glowing “tip” rides the leading edge creating the fiber optic illusion. Four checkpoint marker dots sit at 25%/50%/75%/100% of the spine.
Props interface
Section titled “Props interface”interface DominoSpineProps { progress: MotionValue<number>; // 0→1 Framer Motion value totalStages: 4; // fixed, positions checkpoint markers}Step 1 — Write the failing test
Section titled “Step 1 — Write the failing test”import { render, screen } from '@testing-library/react';import { motionValue } from 'framer-motion';import { DominoSpine } from '../../components/landing/DominoSpine';
describe('DominoSpine', () => { it('renders the spine track element', () => { const progress = motionValue(0); render(<DominoSpine progress={progress} totalStages={4} />); expect(screen.getByTestId('domino-spine-track')).toBeInTheDocument(); });
it('renders the animated fill bar', () => { const progress = motionValue(0); render(<DominoSpine progress={progress} totalStages={4} />); expect(screen.getByTestId('domino-spine-fill')).toBeInTheDocument(); });
it('renders four stage checkpoint markers', () => { const progress = motionValue(0); render(<DominoSpine progress={progress} totalStages={4} />); const markers = screen.getAllByTestId(/stage-marker-/); expect(markers).toHaveLength(4); });
it('renders the glowing tip element', () => { const progress = motionValue(0); render(<DominoSpine progress={progress} totalStages={4} />); expect(screen.getByTestId('domino-spine-tip')).toBeInTheDocument(); });});Step 2 — Run test, confirm RED
Section titled “Step 2 — Run test, confirm RED”cd frontend && npx vitest run src/__tests__/landing/DominoSpine.test.tsx --reporter=verbose# Expected: 4 FAIL — "Cannot find module"Step 3 — Implement DominoSpine.tsx
Section titled “Step 3 — Implement DominoSpine.tsx”import { motion, MotionValue, useTransform } from 'framer-motion';
interface DominoSpineProps { progress: MotionValue<number>; totalStages: 4;}
export function DominoSpine({ progress, totalStages }: DominoSpineProps) { const scaleY = useTransform(progress, [0, 1], [0, 1]); const tipY = useTransform(progress, [0, 1], ['0%', '100%']);
const markerPositions = Array.from({ length: totalStages }, (_, i) => `${((i + 1) / totalStages) * 100}%` );
return ( <div data-testid="domino-spine-track" className="relative w-[2px] h-full mx-auto" style={{ background: 'rgba(255,255,255,0.06)' }} > <motion.div data-testid="domino-spine-fill" className="absolute inset-x-0 top-0" style={{ scaleY, originY: 0, background: `linear-gradient(to bottom, var(--accent-lighter), var(--accent), var(--accent-dark))`, height: '100%', }} />
<motion.div data-testid="domino-spine-tip" className="absolute left-1/2 -translate-x-1/2 w-3 h-3 rounded-full pointer-events-none" style={{ top: tipY, translateY: '-50%', background: 'var(--accent-lighter)', boxShadow: '0 0 12px 4px var(--accent-glow), 0 0 24px 8px var(--accent-soft)', }} />
{markerPositions.map((top, i) => ( <div key={i} data-testid={`stage-marker-${i}`} className="absolute left-1/2 -translate-x-1/2 w-[10px] h-[10px] rounded-full border-2" style={{ top, translateY: '-50%', background: 'var(--background)', borderColor: 'var(--accent-border)', }} /> ))} </div> );}Step 4 — Run test, confirm GREEN
Section titled “Step 4 — Run test, confirm GREEN”cd frontend && npx vitest run src/__tests__/landing/DominoSpine.test.tsx --reporter=verbose# Expected: 4 PASSStep 5 — Commit
Section titled “Step 5 — Commit”git add frontend/src/components/landing/DominoSpine.tsx \ frontend/src/__tests__/landing/DominoSpine.test.tsxgit commit -m "feat(landing): add DominoSpine scroll-driven animated component"Task 2 — Create WorkflowStageCard
Section titled “Task 2 — Create WorkflowStageCard”Files:
- Create:
frontend/src/components/landing/WorkflowStageCard.tsx - Create:
frontend/src/__tests__/landing/WorkflowStageCard.test.tsx
What it does
Section titled “What it does”A glass-panel card that receives a triggerProgress threshold (0–1). When scrollProgress crosses it, the card animates in via opacity: 0→1 + y: 40px→0px over a 0.15 scroll-progress window. Contains a “Paso N” badge, headline, subtext, and a children slot (mockup component).
Step 1 — Write the failing test
Section titled “Step 1 — Write the failing test”import { render, screen } from '@testing-library/react';import { motionValue } from 'framer-motion';import { WorkflowStageCard } from '../../components/landing/WorkflowStageCard';
const defaultProps = { stageIndex: 1 as const, headline: 'Usted diagnostica. Nosotros hacemos el resto.', subtext: 'Trabaje sobre un odontograma 3D.', triggerProgress: 0.2, progress: motionValue(0), side: 'right' as const,};
describe('WorkflowStageCard', () => { it('renders the stage index badge', () => { render(<WorkflowStageCard {...defaultProps}><div /></WorkflowStageCard>); expect(screen.getByTestId('stage-badge-1')).toBeInTheDocument(); });
it('renders the headline text', () => { render(<WorkflowStageCard {...defaultProps}><div /></WorkflowStageCard>); expect(screen.getByText('Usted diagnostica. Nosotros hacemos el resto.')).toBeInTheDocument(); });
it('renders the subtext', () => { render(<WorkflowStageCard {...defaultProps}><div /></WorkflowStageCard>); expect(screen.getByText('Trabaje sobre un odontograma 3D.')).toBeInTheDocument(); });
it('renders the children slot', () => { render( <WorkflowStageCard {...defaultProps}> <div data-testid="mock-slot">mock</div> </WorkflowStageCard> ); expect(screen.getByTestId('mock-slot')).toBeInTheDocument(); });
it('renders the card wrapper with correct testid', () => { render(<WorkflowStageCard {...defaultProps}><div /></WorkflowStageCard>); expect(screen.getByTestId('workflow-card-1')).toBeInTheDocument(); });});Step 2 — Run test, confirm RED
Section titled “Step 2 — Run test, confirm RED”cd frontend && npx vitest run src/__tests__/landing/WorkflowStageCard.test.tsx --reporter=verbose# Expected: 5 FAIL — "Cannot find module"Step 3 — Implement WorkflowStageCard.tsx
Section titled “Step 3 — Implement WorkflowStageCard.tsx”import { motion, MotionValue, useTransform } from 'framer-motion';
interface WorkflowStageCardProps { stageIndex: 1 | 2 | 3 | 4; headline: string; subtext: string; triggerProgress: number; progress: MotionValue<number>; children: React.ReactNode; side: 'left' | 'right';}
export function WorkflowStageCard({ stageIndex, headline, subtext, triggerProgress, progress, children, side,}: WorkflowStageCardProps) { const opacity = useTransform( progress, [triggerProgress, triggerProgress + 0.15], [0, 1] ); const y = useTransform( progress, [triggerProgress, triggerProgress + 0.15], [40, 0] );
return ( <motion.div data-testid={`workflow-card-${stageIndex}`} style={{ opacity, y }} className={`flex flex-col gap-4 ${ side === 'right' ? 'items-start text-left' : 'items-end text-right' }`} > <div data-testid={`stage-badge-${stageIndex}`} className="flex items-center gap-2 px-3 py-1 rounded-pill text-xs font-semibold tracking-widest uppercase" style={{ background: 'var(--accent-soft)', color: 'var(--accent)', border: '1px solid var(--accent-border)', }} > <span className="w-1.5 h-1.5 rounded-full" style={{ background: 'var(--accent)' }} /> Paso {stageIndex} </div>
<div className="glass-panel glass-panel-morph rounded-lg w-full p-0 overflow-hidden" style={{ borderLeft: '2px solid var(--accent-border)' }} > <div className="p-4 border-b" style={{ borderColor: 'var(--border)' }}> {children} </div> <div className="p-5 flex flex-col gap-2"> <h3 className="font-semibold text-base leading-snug" style={{ color: 'var(--foreground)' }}> {headline} </h3> <p className="text-sm leading-relaxed" style={{ color: 'var(--muted-foreground)' }}> {subtext} </p> </div> </div> </motion.div> );}Step 4 — Run test, confirm GREEN
Section titled “Step 4 — Run test, confirm GREEN”cd frontend && npx vitest run src/__tests__/landing/WorkflowStageCard.test.tsx --reporter=verbose# Expected: 5 PASSStep 5 — Commit
Section titled “Step 5 — Commit”git add frontend/src/components/landing/WorkflowStageCard.tsx \ frontend/src/__tests__/landing/WorkflowStageCard.test.tsxgit commit -m "feat(landing): add WorkflowStageCard scroll-driven card component"Task 3 — Create the four UI Mockup components
Section titled “Task 3 — Create the four UI Mockup components”Files:
- Create:
frontend/src/components/landing/mockups/OdontogramMockup.tsx - Create:
frontend/src/components/landing/mockups/TreatmentPlanMockup.tsx - Create:
frontend/src/components/landing/mockups/AgendaMockup.tsx - Create:
frontend/src/components/landing/mockups/WhatsAppMockup.tsx - Create:
frontend/src/__tests__/landing/UIMockups.test.tsx
Design constraints (all mockups)
Section titled “Design constraints (all mockups)”- Max height:
160px— compact inside card header - Color palette: strictly
var(--*)tokens +rgba()— NO hardcoded hex - No external images — pure SVG + CSS + JSX
- No internal animations — mockups are static; animation lives in the card
Step 1 — Write the failing test
Section titled “Step 1 — Write the failing test”import { render } from '@testing-library/react';import { OdontogramMockup } from '../../components/landing/mockups/OdontogramMockup';import { TreatmentPlanMockup } from '../../components/landing/mockups/TreatmentPlanMockup';import { AgendaMockup } from '../../components/landing/mockups/AgendaMockup';import { WhatsAppMockup } from '../../components/landing/mockups/WhatsAppMockup';
describe('UI Mockup components', () => { it('renders OdontogramMockup without throwing', () => { expect(() => render(<OdontogramMockup />)).not.toThrow(); }); it('renders TreatmentPlanMockup without throwing', () => { expect(() => render(<TreatmentPlanMockup />)).not.toThrow(); }); it('renders AgendaMockup without throwing', () => { expect(() => render(<AgendaMockup />)).not.toThrow(); }); it('renders WhatsAppMockup without throwing', () => { expect(() => render(<WhatsAppMockup />)).not.toThrow(); });});Step 2 — Run test, confirm RED
Section titled “Step 2 — Run test, confirm RED”cd frontend && npx vitest run src/__tests__/landing/UIMockups.test.tsx --reporter=verbose# Expected: 4 FAIL — "Cannot find module"Step 3 — Implement the four mockups
Section titled “Step 3 — Implement the four mockups”OdontogramMockup.tsx
Section titled “OdontogramMockup.tsx”8 upper-arch teeth (FDI 11–24) as styled rectangles. 3 teeth have caries markers using var(--dental-condition-caries). A legend row below. Surgical match with the real Odontogram’s token vocabulary.
export function OdontogramMockup() { const teeth = [ { id: 11, caries: true }, { id: 12, caries: false }, { id: 13, caries: false }, { id: 14, caries: true }, { id: 21, caries: false }, { id: 22, caries: false }, { id: 23, caries: true }, { id: 24, caries: false }, ];
return ( <div className="flex flex-col gap-2 items-center py-2 px-4 max-h-[160px]"> <div className="text-[10px] tracking-widest uppercase" style={{ color: 'var(--muted-foreground)' }}> Odontograma — Cuadrante Superior </div> <div className="flex gap-1.5 justify-center"> {teeth.map((tooth) => ( <div key={tooth.id} className="flex flex-col items-center gap-0.5"> <div className="w-6 h-9 rounded-sm border relative overflow-hidden" style={{ background: tooth.caries ? 'color-mix(in srgb, var(--dental-condition-caries) 20%, var(--card))' : 'var(--card)', borderColor: tooth.caries ? 'var(--dental-condition-caries)' : 'var(--border)', }} > {tooth.caries && ( <div className="absolute bottom-1 left-1/2 -translate-x-1/2 w-2 h-2 rounded-full" style={{ background: 'var(--dental-condition-caries)', opacity: 0.8 }} /> )} </div> <span className="text-[9px]" style={{ color: 'var(--muted-foreground)' }}>{tooth.id}</span> </div> ))} </div> <div className="flex gap-3 mt-1"> <span className="flex items-center gap-1 text-[9px]" style={{ color: 'var(--dental-condition-caries)' }}> <span className="w-2 h-2 rounded-full inline-block" style={{ background: 'var(--dental-condition-caries)' }} /> Caries </span> <span className="flex items-center gap-1 text-[9px]" style={{ color: 'var(--dental-procedure-planned)' }}> <span className="w-2 h-2 rounded-full inline-block" style={{ background: 'var(--dental-procedure-planned)' }} /> Planificado </span> </div> </div> );}TreatmentPlanMockup.tsx
Section titled “TreatmentPlanMockup.tsx”3-row table with real procedure names, specific tooth IDs, and prices. Total row highlighted. This is the proof-at-resistance-point: specificity builds credibility per copywriting-psychologist.
const PROCEDURES = [ { name: 'Obturación Resina', teeth: '11, 14', price: '$450' }, { name: 'Extracción Simple', teeth: '23', price: '$280' }, { name: 'Profilaxis', teeth: 'General', price: '$120' },];
export function TreatmentPlanMockup() { return ( <div className="px-4 py-2 max-h-[160px] overflow-hidden"> <div className="text-[10px] tracking-widest uppercase mb-2" style={{ color: 'var(--muted-foreground)' }}> Plan de Tratamiento — Generado automáticamente </div> <table className="w-full text-[11px]"> <thead> <tr style={{ color: 'var(--muted-foreground)' }}> <th className="text-left font-medium pb-1">Procedimiento</th> <th className="text-center font-medium pb-1">Diente</th> <th className="text-right font-medium pb-1">Tarifa</th> </tr> </thead> <tbody> {PROCEDURES.map((p, i) => ( <tr key={i} className="border-t" style={{ borderColor: 'var(--border)' }}> <td className="py-1 pr-2" style={{ color: 'var(--foreground)' }}>{p.name}</td> <td className="py-1 text-center" style={{ color: 'var(--muted-foreground)' }}>{p.teeth}</td> <td className="py-1 text-right font-semibold" style={{ color: 'var(--accent)' }}>{p.price}</td> </tr> ))} </tbody> <tfoot> <tr className="border-t" style={{ borderColor: 'var(--accent-border)', background: 'var(--accent-soft)' }}> <td colSpan={2} className="pt-1 text-xs font-semibold" style={{ color: 'var(--foreground)' }}>Total</td> <td className="pt-1 text-right text-xs font-bold" style={{ color: 'var(--accent)' }}>$850</td> </tr> </tfoot> </table> </div> );}AgendaMockup.tsx
Section titled “AgendaMockup.tsx”Three time-slot pills with border-l-2 var(--success) accent. Conveys “3 best slots found automatically.”
const SLOTS = [ { day: 'Lun 7 Jul', time: '09:30', doctor: 'Dr. García' }, { day: 'Mié 9 Jul', time: '14:00', doctor: 'Dr. García' }, { day: 'Vie 11 Jul', time: '11:00', doctor: 'Dr. García' },];
export function AgendaMockup() { return ( <div className="px-4 py-2 max-h-[160px] flex flex-col gap-2"> <div className="text-[10px] tracking-widest uppercase" style={{ color: 'var(--muted-foreground)' }}> 3 espacios óptimos encontrados </div> {SLOTS.map((slot, i) => ( <div key={i} className="flex items-center gap-3 px-3 py-1.5 rounded-md" style={{ background: 'var(--card)', border: '1px solid var(--border)', borderLeft: '2px solid var(--success)', }} > <div className="flex flex-col leading-tight"> <span className="text-[11px] font-semibold" style={{ color: 'var(--foreground)' }}>{slot.time}</span> <span className="text-[10px]" style={{ color: 'var(--muted-foreground)' }}>{slot.day}</span> </div> <span className="ml-auto text-[10px]" style={{ color: 'var(--muted-foreground)' }}>{slot.doctor}</span> <span className="text-[9px] px-2 py-0.5 rounded-pill font-medium" style={{ background: 'color-mix(in srgb, var(--success) 15%, transparent)', color: 'var(--success)', }} > Disponible </span> </div> ))} </div> );}WhatsAppMockup.tsx
Section titled “WhatsAppMockup.tsx”Three chat bubbles: assistant offer → patient reply → confirmation. The complete loop closed. Payoff visual for Stage 4.
const MESSAGES = [ { from: 'assistant', text: 'Hola! Tenemos 3 opciones:\n1. Lun 7 Jul — 09:30\n2. Mié 9 Jul — 14:00\n3. Vie 11 Jul — 11:00\n¿Cuál le queda mejor?', time: '15:42', }, { from: 'patient', text: 'Opción 2 👍', time: '15:43' }, { from: 'assistant', text: '✅ Cita confirmada para el Mié 9 Jul a las 14:00. Le enviaremos un recordatorio 24h antes.', time: '15:43', },];
export function WhatsAppMockup() { return ( <div className="px-3 py-2 max-h-[160px] overflow-hidden flex flex-col gap-1.5" style={{ background: 'rgba(0,0,0,0.04)', borderRadius: '8px' }} > {MESSAGES.map((msg, i) => ( <div key={i} className={`flex ${msg.from === 'patient' ? 'justify-end' : 'justify-start'}`}> <div className="max-w-[85%] px-2.5 py-1.5 rounded-lg text-[10px] leading-snug whitespace-pre-line" style={{ background: msg.from === 'patient' ? 'color-mix(in srgb, var(--success) 25%, white)' : 'white', color: 'var(--foreground)', boxShadow: '0 1px 2px rgba(0,0,0,0.1)', }} > {msg.text} <span className="block text-right mt-0.5" style={{ fontSize: '8px', color: 'var(--muted-foreground)' }}> {msg.time}{msg.from === 'assistant' && ' ✓✓'} </span> </div> </div> ))} </div> );}Step 4 — Run test, confirm GREEN
Section titled “Step 4 — Run test, confirm GREEN”cd frontend && npx vitest run src/__tests__/landing/UIMockups.test.tsx --reporter=verbose# Expected: 4 PASSStep 5 — Commit
Section titled “Step 5 — Commit”git add frontend/src/components/landing/mockups/ \ frontend/src/__tests__/landing/UIMockups.test.tsxgit commit -m "feat(landing): add four static UI mockup components for workflow cards"Task 4 — Create AutomationWorkflow.tsx (section shell)
Section titled “Task 4 — Create AutomationWorkflow.tsx (section shell)”Files:
- Create:
frontend/src/pages/landing/AutomationWorkflow.tsx - Create:
frontend/src/__tests__/landing/AutomationWorkflow.test.tsx
Scroll mechanic
Section titled “Scroll mechanic”- Outer
<section>:relative,min-h-[400vh]— gives scroll room. - Inner
<div>:sticky top-0 h-screen overflow-hidden— sticky-inner parallax, no scroll-jacking. useScroll({ target: sectionRef, offset: ['start end', 'end start'] })→scrollYProgress0→1.scrollYProgresspassed asprogressprop toDominoSpineand allWorkflowStageCards.- Trigger thresholds: Stage 1 = 0.15, Stage 2 = 0.38, Stage 3 = 0.58, Stage 4 = 0.75, Badge = 0.88.
Stage data (exact copy — do NOT alter)
Section titled “Stage data (exact copy — do NOT alter)”const STAGES = [ { stageIndex: 1 as const, headline: 'Usted diagnostica. Nosotros hacemos el resto.', subtext: 'Trabaje sobre un odontograma 3D de alta precisión. Registre sus hallazgos en segundos, con un nivel clínico superior.', triggerProgress: 0.15, side: 'right' as const, mockup: 'odontogram' as const, }, { stageIndex: 2 as const, headline: 'El presupuesto se crea solo.', subtext: 'Sin cálculos manuales. ClinicFlow cruza su diagnóstico clínico con su tarifario para generar el plan de tratamiento exacto al instante.', triggerProgress: 0.38, side: 'left' as const, mockup: 'treatment' as const, }, { stageIndex: 3 as const, headline: 'La IA escanea su agenda.', subtext: 'El sistema calcula el tiempo clínico exacto que requiere el tratamiento y encuentra automáticamente los 3 mejores espacios disponibles.', triggerProgress: 0.58, side: 'right' as const, mockup: 'agenda' as const, }, { stageIndex: 4 as const, headline: 'WhatsApp cierra la cita por usted.', subtext: 'Antes de que el paciente se levante de la silla, su asistente virtual ya le envió las opciones. El paciente responde, la cita se agenda. Recepción no mueve un dedo.', triggerProgress: 0.75, side: 'left' as const, mockup: 'whatsapp' as const, },];Step 1 — Write the failing test
Section titled “Step 1 — Write the failing test”import { render, screen } from '@testing-library/react';import AutomationWorkflow from '../../pages/landing/AutomationWorkflow';
vi.mock('framer-motion', async (importOriginal) => { const actual = await importOriginal<typeof import('framer-motion')>(); return { ...actual, useScroll: () => ({ scrollYProgress: actual.motionValue(0) }), };});
describe('AutomationWorkflow section', () => { it('renders the section headline', () => { render(<AutomationWorkflow />); expect(screen.getByText(/Del diagnóstico a la cita confirmada/)).toBeInTheDocument(); });
it('renders all four stage cards', () => { render(<AutomationWorkflow />); for (let i = 1; i <= 4; i++) { expect(screen.getByTestId(`workflow-card-${i}`)).toBeInTheDocument(); } });
it('renders the DominoSpine track', () => { render(<AutomationWorkflow />); expect(screen.getByTestId('domino-spine-track')).toBeInTheDocument(); });
it('renders the trust badge with correct text', () => { render(<AutomationWorkflow />); expect(screen.getByTestId('trust-badge')).toBeInTheDocument(); expect(screen.getByText('Cero papel. 100% Legal.')).toBeInTheDocument(); });
it('renders the section with accessible landmark', () => { render(<AutomationWorkflow />); expect( screen.getByRole('region', { name: /automatización del flujo/i }) ).toBeInTheDocument(); });});Step 2 — Run test, confirm RED
Section titled “Step 2 — Run test, confirm RED”cd frontend && npx vitest run src/__tests__/landing/AutomationWorkflow.test.tsx --reporter=verbose# Expected: 5 FAIL — "Cannot find module"Step 3 — Implement AutomationWorkflow.tsx
Section titled “Step 3 — Implement AutomationWorkflow.tsx”import { useRef } from 'react';import { useScroll, useTransform, motion } from 'framer-motion';import { DominoSpine } from '../../components/landing/DominoSpine';import { WorkflowStageCard } from '../../components/landing/WorkflowStageCard';import { OdontogramMockup } from '../../components/landing/mockups/OdontogramMockup';import { TreatmentPlanMockup } from '../../components/landing/mockups/TreatmentPlanMockup';import { AgendaMockup } from '../../components/landing/mockups/AgendaMockup';import { WhatsAppMockup } from '../../components/landing/mockups/WhatsAppMockup';
// Exact copy — do NOT alterconst STAGES = [/* paste from Stage Data above */];
const MOCKUP_MAP = { odontogram: OdontogramMockup, treatment: TreatmentPlanMockup, agenda: AgendaMockup, whatsapp: WhatsAppMockup,} as const;
export default function AutomationWorkflow() { const sectionRef = useRef<HTMLElement>(null); const { scrollYProgress } = useScroll({ target: sectionRef, offset: ['start end', 'end start'], });
const badgeOpacity = useTransform(scrollYProgress, [0.88, 0.96], [0, 1]); const badgeY = useTransform(scrollYProgress, [0.88, 0.96], [30, 0]);
return ( <section ref={sectionRef} aria-label="Automatización del flujo clínico" className="relative" style={{ minHeight: '400vh' }} > {/* Sticky inner viewport */} <div className="sticky top-0 h-screen overflow-hidden flex flex-col justify-center px-4 md:px-12 lg:px-24">
{/* Section headline */} <h2 className="text-center text-2xl md:text-4xl font-bold leading-tight mb-12 md:mb-16" style={{ color: 'var(--foreground)', letterSpacing: '-0.03em' }} > Del diagnóstico a la cita confirmada.{' '} <span style={{ color: 'var(--accent)' }}>En piloto automático.</span> </h2>
{/* Three-column grid: left cards | spine | right cards */} {/* Mobile: single column, spine hidden */} <div className="relative grid grid-cols-1 md:grid-cols-[1fr_2px_1fr] gap-x-8 gap-y-8 items-start max-w-5xl mx-auto w-full">
{/* Left-side cards (stages 2 & 4) */} <div className="flex flex-col gap-12 md:gap-16 pt-0 md:pt-8"> {STAGES.filter((s) => s.side === 'left').map((stage) => { const MockupComponent = MOCKUP_MAP[stage.mockup]; return ( <WorkflowStageCard key={stage.stageIndex} stageIndex={stage.stageIndex} headline={stage.headline} subtext={stage.subtext} triggerProgress={stage.triggerProgress} progress={scrollYProgress} side="left" > <MockupComponent /> </WorkflowStageCard> ); })} </div>
{/* Center spine — hidden on mobile */} <div className="hidden md:block h-full self-stretch min-h-[400px]"> <DominoSpine progress={scrollYProgress} totalStages={4} /> </div>
{/* Right-side cards (stages 1 & 3) */} <div className="flex flex-col gap-12 md:gap-16 pt-0 md:pt-8"> {STAGES.filter((s) => s.side === 'right').map((stage) => { const MockupComponent = MOCKUP_MAP[stage.mockup]; return ( <WorkflowStageCard key={stage.stageIndex} stageIndex={stage.stageIndex} headline={stage.headline} subtext={stage.subtext} triggerProgress={stage.triggerProgress} progress={scrollYProgress} side="right" > <MockupComponent /> </WorkflowStageCard> ); })} </div> </div>
{/* Trust badge */} <motion.div data-testid="trust-badge" style={{ opacity: badgeOpacity, y: badgeY }} className="mt-12 md:mt-16 flex justify-center" > <div className="flex items-center gap-3 px-6 py-3 rounded-lg glass-panel"> {/* Heroicons shield-check */} <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 flex-shrink-0" style={{ color: 'var(--success)' }} > <path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z" /> </svg> <div> <p className="font-semibold text-sm" style={{ color: 'var(--foreground)' }}> Cero papel. 100% Legal. </p> <p className="text-xs" style={{ color: 'var(--muted-foreground)' }}> Exporte planes y odontogramas a PDF con soporte para firmas digitales integradas. </p> </div> </div> </motion.div> </div>
{/* prefers-reduced-motion: force all animated elements to final visible state */} <style>{` @media (prefers-reduced-motion: reduce) { [data-testid="domino-spine-fill"], [data-testid="domino-spine-tip"], [data-testid^="workflow-card-"], [data-testid="trust-badge"] { opacity: 1 !important; transform: none !important; } } `}</style> </section> );}Note: In the final file, paste the full
STAGESarray from the “Stage Data” block above to replace the/* paste from Stage Data above */comment.
Step 4 — Run test, confirm GREEN
Section titled “Step 4 — Run test, confirm GREEN”cd frontend && npx vitest run src/__tests__/landing/AutomationWorkflow.test.tsx --reporter=verbose# Expected: 5 PASSStep 5 — Commit
Section titled “Step 5 — Commit”git add frontend/src/pages/landing/AutomationWorkflow.tsx \ frontend/src/__tests__/landing/AutomationWorkflow.test.tsxgit commit -m "feat(landing): add AutomationWorkflow scroll-driven section shell"Task 5 — Wire into index.tsx
Section titled “Task 5 — Wire into index.tsx”Files:
- Modify:
frontend/src/pages/landing/index.tsx
Step 1 — Verify the section is currently absent (visual RED)
Section titled “Step 1 — Verify the section is currently absent (visual RED)”cd frontend && grep "AutomationWorkflow" src/pages/landing/index.tsx# Expected: no output — section not yet importedStep 2 — Add import and JSX (surgical — touch nothing else)
Section titled “Step 2 — Add import and JSX (surgical — touch nothing else)”Open frontend/src/pages/landing/index.tsx and make exactly two changes:
Add import (after the existing DentalSection import line):
import AutomationWorkflow from './AutomationWorkflow';Add JSX (between <DentalSection scrollTo={scrollTo} /> and <HowItWorks />):
<DentalSection scrollTo={scrollTo} /><AutomationWorkflow /><HowItWorks />Step 3 — Run build and full test suite
Section titled “Step 3 — Run build and full test suite”cd frontend && npm run build 2>&1 | tail -5# Expected: Compiled successfully, no TypeScript errors
cd frontend && npm run test 2>&1 | tail -10# Expected: All tests passStep 4 — Commit
Section titled “Step 4 — Commit”git add frontend/src/pages/landing/index.tsxgit commit -m "feat(landing): wire AutomationWorkflow into landing page"Task 6 — E2E Smoke Test
Section titled “Task 6 — E2E Smoke Test”Files:
- Create:
frontend/e2e/automation-workflow.spec.ts
Step 1 — Write the test
Section titled “Step 1 — Write the test”import { test, expect } from '@playwright/test';
test.describe('AutomationWorkflow section', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); });
test('section heading is present and reachable', async ({ page }) => { const heading = page.getByText(/Del diagnóstico a la cita confirmada/); await heading.scrollIntoViewIfNeeded(); await expect(heading).toBeVisible(); });
test('DominoSpine track exists in DOM', async ({ page }) => { await expect(page.getByTestId('domino-spine-track')).toBeAttached(); });
test('all four workflow cards are attached', async ({ page }) => { for (let i = 1; i <= 4; i++) { await expect(page.getByTestId(`workflow-card-${i}`)).toBeAttached(); } });
test('trust badge is attached', async ({ page }) => { await expect(page.getByTestId('trust-badge')).toBeAttached(); });
test('all cards visible with reduced motion preference', async ({ page }) => { await page.emulateMedia({ reducedMotion: 'reduce' }); await page.goto('/'); for (let i = 1; i <= 4; i++) { await expect(page.getByTestId(`workflow-card-${i}`)).toBeAttached(); } });});Step 2 — Run Playwright, confirm GREEN (requires Task 5 complete + dev server running)
Section titled “Step 2 — Run Playwright, confirm GREEN (requires Task 5 complete + dev server running)”cd frontend && npx playwright test e2e/automation-workflow.spec.ts --reporter=line# Expected: 5 PASSStep 3 — Commit
Section titled “Step 3 — Commit”git add frontend/e2e/automation-workflow.spec.tsgit commit -m "test(e2e): add AutomationWorkflow smoke tests"Task 7 — Visual Polish & Pre-Delivery Checklist
Section titled “Task 7 — Visual Polish & Pre-Delivery Checklist”No new files. Review pass against the ui-ux-pro-max Pre-Delivery Checklist.
| Check | How to verify | Expected result |
|---|---|---|
| No emojis as UI icons | Code inspection | ✅ in WhatsApp bubble is copy content, not an icon |
| All icons SVG from Heroicons | Code inspection | Shield-check inline SVG confirmed |
| Hover states on cards | npm run dev + mouse hover |
glass-panel-morph:hover triggers lift |
| Focus states visible | Tab key through section | Browser default focus ring |
| Light mode contrast ≥ 4.5:1 | Chrome → Accessibility → Color contrast | var(--foreground) on var(--background) passes |
| Dark mode glass borders visible | Toggle data-theme="dark" |
Confirmed by [data-theme='dark'] .glass-panel in index.css:297 |
| Responsive 375/768/1024/1440px | Chrome DevTools responsive | Grid collapses to grid-cols-1 below md breakpoint |
| No horizontal scroll on mobile | DevTools + scroll | overflow-x-hidden on landing root div in index.tsx |
prefers-reduced-motion |
Chrome → Rendering → Emulate | All cards instantly visible |
Step 1 — Run dev server and manually verify
Section titled “Step 1 — Run dev server and manually verify”cd frontend && npm run dev# Open http://localhost:5173 and scroll through the landing pageStep 2 — Commit any fixes found during review
Section titled “Step 2 — Commit any fixes found during review”git add frontend/src/pages/landing/AutomationWorkflow.tsxgit commit -m "polish(landing): pre-delivery checklist fixes"Task 8 — Full Regression Run
Section titled “Task 8 — Full Regression Run”Step 1 — Unit tests
Section titled “Step 1 — Unit tests”cd frontend && npm run test# Expected: All tests pass. Zero failures. Zero warnings.Step 2 — E2E suite
Section titled “Step 2 — E2E suite”cd frontend && npx playwright test --reporter=line# Expected: All E2E tests pass.Step 3 — Production build
Section titled “Step 3 — Production build”cd frontend && npm run build# Expected: No TypeScript errors. No new unused variable warnings.Step 4 — Tag
Section titled “Step 4 — Tag”git tag -a "automation-workflow-v1.0" -m "AutomationWorkflow landing section — complete"Appendix A — Framer Motion Pattern Reference
Section titled “Appendix A — Framer Motion Pattern Reference”// Standard sticky-inner scroll section (no scroll-jacking):// 1. Outer section: position relative, minHeight 400vh// 2. Inner div: position sticky, top 0, height 100vh, overflow hidden// 3. useScroll tracks the section ref:const sectionRef = useRef<HTMLElement>(null);const { scrollYProgress } = useScroll({ target: sectionRef, offset: ['start end', 'end start'], // 'start end' = section top enters bottom of viewport // 'end start' = section bottom leaves top of viewport});
// 4. useTransform maps progress to animation values:const scaleY = useTransform(scrollYProgress, [0.2, 0.5], [0, 1]);// At 20% through section scroll: scaleY = 0// At 50% through section scroll: scaleY = 1Appendix B — CSS Token Reference
Section titled “Appendix B — CSS Token Reference”| Token | Value | Usage |
|---|---|---|
var(--background) |
#FAFAF9 |
Section background |
var(--foreground) |
#0C0A09 |
Headline, card text |
var(--accent) |
#007AFF |
Spine fill, accent text |
var(--accent-lighter) |
#60A5FA |
Spine gradient top |
var(--accent-dark) |
#0056CC |
Spine gradient bottom |
var(--accent-glow) |
rgba(0,122,255,0.3) |
Spine tip glow shadow |
var(--accent-soft) |
rgba(0,122,255,0.1) |
Stage badge background |
var(--accent-border) |
color-mix(in srgb, var(--accent) 20%, transparent) |
Card left accent border |
var(--muted-foreground) |
#78716C |
Subtext, labels |
var(--border) |
rgba(0,0,0,0.08) |
Card internal dividers |
var(--success) |
#0D9488 |
Agenda slots, trust badge icon |
var(--card) |
#FFFFFF |
Mockup inner backgrounds |
.glass-panel |
rgba(255,255,255,0.12) + backdrop-filter: blur(18px) |
All cards |
.glass-panel-morph |
600ms cubic-bezier hover transition | Card hover lift |
Dark mode is handled automatically by
[data-theme='dark'] .glass-panelatindex.css:297. No extra work needed.
Appendix C — Risk Register
Section titled “Appendix C — Risk Register”| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
useScroll returns incorrect range on first paint |
Low | Medium | sectionRef null-guard via React useRef default |
backdrop-filter unsupported (Firefox < 103) |
Medium | Low | .glass-panel falls back to semi-opaque rgba without blur |
Spine height collapses on mobile (grid-cols-1) |
Medium | High | Task 7: spine hidden (hidden md:block), cards stack vertically |
| Cards overlap in short viewports (< 650px height) | Low | Medium | overflow-hidden on sticky inner container clips safely |
| Scroll jank on low-end devices | Medium | Medium | Framer Motion uses will-change: transform by default on animated elements; will-change: backdrop-filter already set on .glass-panel |
| Copy text altered during implementation | Low | High | Stage data in STAGES constant is marked “do NOT alter” — code review gate |
Execution Options
Section titled “Execution Options”Plan complete. Saved to docs/plans/2026-06-29-automation-workflow-section.md.
1. Subagent-Driven (this session) — I dispatch a fresh subagent per task using superpowers:subagent-driven-development, review between tasks, fast iteration. Recommended — tasks 1–3 are fully independent and can run in parallel.
2. Parallel Session (separate) — Open a new session, use superpowers:executing-plans, batch execution with checkpoints after tasks 1, 3, 5, and 8.
Which approach?