Skip to content

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-plans to 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).


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”).


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 in pages/landing/ matching all other sections.


Terminal window
cd frontend
npm 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

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.

interface DominoSpineProps {
progress: MotionValue<number>; // 0→1 Framer Motion value
totalStages: 4; // fixed, positions checkpoint markers
}
frontend/src/__tests__/landing/DominoSpine.test.tsx
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();
});
});
Terminal window
cd frontend && npx vitest run src/__tests__/landing/DominoSpine.test.tsx --reporter=verbose
# Expected: 4 FAIL — "Cannot find module"
frontend/src/components/landing/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>
);
}
Terminal window
cd frontend && npx vitest run src/__tests__/landing/DominoSpine.test.tsx --reporter=verbose
# Expected: 4 PASS
Terminal window
git add frontend/src/components/landing/DominoSpine.tsx \
frontend/src/__tests__/landing/DominoSpine.test.tsx
git commit -m "feat(landing): add DominoSpine scroll-driven animated component"

Files:

  • Create: frontend/src/components/landing/WorkflowStageCard.tsx
  • Create: frontend/src/__tests__/landing/WorkflowStageCard.test.tsx

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

frontend/src/__tests__/landing/WorkflowStageCard.test.tsx
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();
});
});
Terminal window
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”
frontend/src/components/landing/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>
);
}
Terminal window
cd frontend && npx vitest run src/__tests__/landing/WorkflowStageCard.test.tsx --reporter=verbose
# Expected: 5 PASS
Terminal window
git add frontend/src/components/landing/WorkflowStageCard.tsx \
frontend/src/__tests__/landing/WorkflowStageCard.test.tsx
git 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
  • 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
frontend/src/__tests__/landing/UIMockups.test.tsx
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();
});
});
Terminal window
cd frontend && npx vitest run src/__tests__/landing/UIMockups.test.tsx --reporter=verbose
# Expected: 4 FAIL — "Cannot find module"

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.

frontend/src/components/landing/mockups/OdontogramMockup.tsx
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>
);
}

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.

frontend/src/components/landing/mockups/TreatmentPlanMockup.tsx
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>
);
}

Three time-slot pills with border-l-2 var(--success) accent. Conveys “3 best slots found automatically.”

frontend/src/components/landing/mockups/AgendaMockup.tsx
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>
);
}

Three chat bubbles: assistant offer → patient reply → confirmation. The complete loop closed. Payoff visual for Stage 4.

frontend/src/components/landing/mockups/WhatsAppMockup.tsx
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>
);
}
Terminal window
cd frontend && npx vitest run src/__tests__/landing/UIMockups.test.tsx --reporter=verbose
# Expected: 4 PASS
Terminal window
git add frontend/src/components/landing/mockups/ \
frontend/src/__tests__/landing/UIMockups.test.tsx
git 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
  • 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'] })scrollYProgress 0→1.
  • scrollYProgress passed as progress prop to DominoSpine and all WorkflowStageCards.
  • Trigger thresholds: Stage 1 = 0.15, Stage 2 = 0.38, Stage 3 = 0.58, Stage 4 = 0.75, Badge = 0.88.
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,
},
];
frontend/src/__tests__/landing/AutomationWorkflow.test.tsx
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();
});
});
Terminal window
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”
frontend/src/pages/landing/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 alter
const 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 STAGES array from the “Stage Data” block above to replace the /* paste from Stage Data above */ comment.

Terminal window
cd frontend && npx vitest run src/__tests__/landing/AutomationWorkflow.test.tsx --reporter=verbose
# Expected: 5 PASS
Terminal window
git add frontend/src/pages/landing/AutomationWorkflow.tsx \
frontend/src/__tests__/landing/AutomationWorkflow.test.tsx
git commit -m "feat(landing): add AutomationWorkflow scroll-driven section shell"

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)”
Terminal window
cd frontend && grep "AutomationWorkflow" src/pages/landing/index.tsx
# Expected: no output — section not yet imported

Step 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 />
Terminal window
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 pass
Terminal window
git add frontend/src/pages/landing/index.tsx
git commit -m "feat(landing): wire AutomationWorkflow into landing page"

Files:

  • Create: frontend/e2e/automation-workflow.spec.ts
frontend/e2e/automation-workflow.spec.ts
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)”
Terminal window
cd frontend && npx playwright test e2e/automation-workflow.spec.ts --reporter=line
# Expected: 5 PASS
Terminal window
git add frontend/e2e/automation-workflow.spec.ts
git 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”
Terminal window
cd frontend && npm run dev
# Open http://localhost:5173 and scroll through the landing page

Step 2 — Commit any fixes found during review

Section titled “Step 2 — Commit any fixes found during review”
Terminal window
git add frontend/src/pages/landing/AutomationWorkflow.tsx
git commit -m "polish(landing): pre-delivery checklist fixes"

Terminal window
cd frontend && npm run test
# Expected: All tests pass. Zero failures. Zero warnings.
Terminal window
cd frontend && npx playwright test --reporter=line
# Expected: All E2E tests pass.
Terminal window
cd frontend && npm run build
# Expected: No TypeScript errors. No new unused variable warnings.
Terminal window
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 = 1

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-panel at index.css:297. No extra work needed.


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

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?