feat(recorrido): ParDialogo — la situación y la respuesta como un intercambio
Las dos voces se distinguen por tres cosas a la vez —etiqueta de quien habla, columna y regla lateral—, nunca solo por color. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
170482a32b
commit
a9d1f44df2
2 files changed
+98
No files matched your search
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { Wordmark } from '@/components/primitives/Wordmark';
|
||||
import { fadeInUp } from '@/lib/motion';
|
||||
|
||||
// One exchange of the page's conversation: the reader's situation, then what
|
||||
// creeer does about it.
|
||||
//
|
||||
// The sides never swap from one pair to the next. Each voice keeps its own
|
||||
// margin, the way a conversation reads on a page; what alternates going down is
|
||||
// situación → respuesta, not left → right. Flipping sides per pair would cost
|
||||
// the reader the one cue that tells them who is speaking.
|
||||
//
|
||||
// Three cues carry that distinction, never colour alone: the speaker label, the
|
||||
// column, and the rule hanging off each block. The answer also drops half a
|
||||
// step below the situation on wide screens, so the eye reads it as a reply
|
||||
// rather than as a second column of the same thought.
|
||||
//
|
||||
// No motion.li here on purpose: Framer propagates variants through React
|
||||
// context, so the two motion.div children still inherit the stagger from the
|
||||
// <ul> the section wraps them in, even across this plain <li>. Making the item
|
||||
// itself a motion.li would add a third beat nobody asked for.
|
||||
|
||||
export function ParDialogo({
|
||||
n,
|
||||
name,
|
||||
situacion,
|
||||
respuesta,
|
||||
}: {
|
||||
n: number;
|
||||
name: string;
|
||||
situacion: string;
|
||||
respuesta: string;
|
||||
}) {
|
||||
return (
|
||||
<li className="grid gap-5 md:grid-cols-2 md:gap-12">
|
||||
<motion.div variants={fadeInUp} className="border-l-2 border-azul-profundo/25 pl-5">
|
||||
<p className="text-xs font-black uppercase tracking-[0.2em] text-dorado">
|
||||
{String(n).padStart(2, '0')} · {name}
|
||||
</p>
|
||||
<p className="mt-3 font-sans font-bold text-xl md:text-2xl text-azul-profundo leading-snug">
|
||||
«{situacion}»
|
||||
</p>
|
||||
</motion.div>
|
||||
<motion.div variants={fadeInUp} className="border-l-2 border-dorado pl-5 md:mt-10">
|
||||
<Wordmark size="sm" />
|
||||
<p className="mt-3 text-gris-medio leading-relaxed">{respuesta}</p>
|
||||
</motion.div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ParDialogo } from '../ParDialogo';
|
||||
|
||||
const par = {
|
||||
n: 1,
|
||||
name: 'Crecer',
|
||||
situacion: 'El siguiente salto no tiene mapa.',
|
||||
respuesta: 'Me sumerjo hasta entender tu empresa desde dentro.',
|
||||
};
|
||||
|
||||
describe('<ParDialogo />', () => {
|
||||
it('renders both voices of the exchange', () => {
|
||||
render(<ul><ParDialogo {...par} /></ul>);
|
||||
expect(screen.getByText(/El siguiente salto no tiene mapa/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Me sumerjo hasta entender tu empresa desde dentro/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('attributes the answer to creeer, not to the reader', () => {
|
||||
render(<ul><ParDialogo {...par} /></ul>);
|
||||
// Without a speaker label the answer reads as more description of the
|
||||
// problem. The wordmark is that label — and <Wordmark /> is the only thing
|
||||
// allowed to draw cre≡er, never hand-typed in the JSX.
|
||||
expect(screen.getByLabelText('creeer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('labels the situation with its inflection point', () => {
|
||||
render(<ul><ParDialogo {...par} /></ul>);
|
||||
expect(screen.getByText(/01 · Crecer/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('tells the two voices apart by more than colour', () => {
|
||||
const { container } = render(<ul><ParDialogo {...par} /></ul>);
|
||||
// A reader who cannot separate gold from navy still has the speaker label,
|
||||
// the column, and the rule hanging off each block. Colour alone would fail
|
||||
// the spec's accessibility criterion.
|
||||
const bloques = container.querySelectorAll('li > div');
|
||||
expect(bloques).toHaveLength(2);
|
||||
bloques.forEach((b) => expect(b.className).toMatch(/border-l-2/));
|
||||
});
|
||||
|
||||
it('is a list item, so the section can mount six of them as a list', () => {
|
||||
render(<ul><ParDialogo {...par} /></ul>);
|
||||
expect(screen.getAllByRole('listitem')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user