feat(disciplinas): la tarjeta y su hebra pasan a ser la misma cosa
El dibujo de convergencia llevaba desde el principio al lado de las tres tarjetas sin relación con ellas: tres objetos sueltos en una página. Al señalar una disciplina, su hebra se enciende en dorado y engorda, y las otras dos se apagan al 35%. Es mejora solo con puntero, y a propósito: no esconde nada. Las tres tarjetas y las tres hebras están completas sin tocar el ratón, así que quien no lo tenga no pierde nada. Verificado en navegador, no solo en jsdom: al señalar «Digital» la hebra central queda en rgb(196,148,46) —el dorado de marca— a 3,5px, y las otras en rgba(170,170,170,0.35) a 2px. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7af36e2a93
commit
9a8534f10b
4 files changed
+79
-6
No files matched your search
@@ -6,6 +6,11 @@ import { useEnVista } from '@/lib/motion';
|
|||||||
// Three strands — comunicación, digital, experiencias — meeting and leaving as
|
// Three strands — comunicación, digital, experiencias — meeting and leaving as
|
||||||
// a single gold line. The section's own lead, drawn: "No son la promesa, son
|
// a single gold line. The section's own lead, drawn: "No son la promesa, son
|
||||||
// los medios." The three are grey; only what they produce is gold.
|
// los medios." The three are grey; only what they produce is gold.
|
||||||
|
//
|
||||||
|
// `activa` lights the strand belonging to the discipline the reader is pointing
|
||||||
|
// at, and dims the other two. Until then the drawing sat beside the three cards
|
||||||
|
// with no relationship to them; now the card and its strand are visibly the
|
||||||
|
// same thing, and the reader can see which of the three produces what.
|
||||||
const HEBRAS = [
|
const HEBRAS = [
|
||||||
'M6 18 C 60 20, 90 52, 120 60',
|
'M6 18 C 60 20, 90 52, 120 60',
|
||||||
'M6 60 C 60 60, 90 60, 120 60',
|
'M6 60 C 60 60, 90 60, 120 60',
|
||||||
@@ -13,15 +18,28 @@ const HEBRAS = [
|
|||||||
];
|
];
|
||||||
const ESTRATEGIA = 'M120 60 L 214 60';
|
const ESTRATEGIA = 'M120 60 L 214 60';
|
||||||
|
|
||||||
export function IlustracionConvergencia({ className = '' }: { className?: string }) {
|
export function IlustracionConvergencia({
|
||||||
|
className = '',
|
||||||
|
activa = null,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
activa?: number | null;
|
||||||
|
}) {
|
||||||
const { ref, enVista, reduced } = useEnVista();
|
const { ref, enVista, reduced } = useEnVista();
|
||||||
|
|
||||||
|
// `stroke` follows `currentColor`, so moving the text colour moves the line —
|
||||||
|
// and transition-colors carries it across instead of snapping.
|
||||||
|
const tono = (i: number) =>
|
||||||
|
activa === null ? 'text-gris-medio' : activa === i ? 'text-dorado' : 'text-gris-medio/35';
|
||||||
|
const grosor = (i: number) => (activa === i ? 3.5 : 2);
|
||||||
|
const transicion = 'transition-all duration-300 ease-out motion-reduce:transition-none';
|
||||||
|
|
||||||
// Plain <path> under reduced motion — motion.path would hide the drawing.
|
// Plain <path> under reduced motion — motion.path would hide the drawing.
|
||||||
if (reduced) {
|
if (reduced) {
|
||||||
return (
|
return (
|
||||||
<svg ref={ref} viewBox="0 0 220 120" className={className} aria-hidden="true" focusable="false">
|
<svg ref={ref} viewBox="0 0 220 120" className={className} aria-hidden="true" focusable="false">
|
||||||
{HEBRAS.map((d) => (
|
{HEBRAS.map((d, i) => (
|
||||||
<path key={d} d={d} fill="none" stroke="currentColor" className="text-gris-medio" strokeWidth={2} strokeLinecap="round" />
|
<path key={d} d={d} data-hebra={i} fill="none" stroke="currentColor" className={`${tono(i)} ${transicion}`} strokeWidth={grosor(i)} strokeLinecap="round" />
|
||||||
))}
|
))}
|
||||||
<path d={ESTRATEGIA} fill="none" stroke="currentColor" className="text-dorado" strokeWidth={3} strokeLinecap="round" />
|
<path d={ESTRATEGIA} fill="none" stroke="currentColor" className="text-dorado" strokeWidth={3} strokeLinecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -37,7 +55,7 @@ export function IlustracionConvergencia({ className = '' }: { className?: string
|
|||||||
return (
|
return (
|
||||||
<svg ref={ref} viewBox="0 0 220 120" className={className} aria-hidden="true" focusable="false">
|
<svg ref={ref} viewBox="0 0 220 120" className={className} aria-hidden="true" focusable="false">
|
||||||
{HEBRAS.map((d, i) => (
|
{HEBRAS.map((d, i) => (
|
||||||
<motion.path key={d} d={d} fill="none" stroke="currentColor" className="text-gris-medio" strokeWidth={2} strokeLinecap="round" {...dibuja(i * 0.12)} />
|
<motion.path key={d} d={d} data-hebra={i} fill="none" stroke="currentColor" className={`${tono(i)} ${transicion}`} strokeWidth={grosor(i)} strokeLinecap="round" {...dibuja(i * 0.12)} />
|
||||||
))}
|
))}
|
||||||
<motion.path d={ESTRATEGIA} fill="none" stroke="currentColor" className="text-dorado" strokeWidth={3} strokeLinecap="round" {...dibuja(0.7)} />
|
<motion.path d={ESTRATEGIA} fill="none" stroke="currentColor" className="text-dorado" strokeWidth={3} strokeLinecap="round" {...dibuja(0.7)} />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -21,4 +21,29 @@ describe('<IlustracionConvergencia />', () => {
|
|||||||
expect(anchos.length).toBeGreaterThan(0);
|
expect(anchos.length).toBeGreaterThan(0);
|
||||||
anchos.forEach((a) => expect(['1.5', '2', '3']).toContain(a));
|
anchos.forEach((a) => expect(['1.5', '2', '3']).toContain(a));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('lights one strand and dims the rest when a discipline is pointed at', () => {
|
||||||
|
const { container } = render(<IlustracionConvergencia activa={1} />);
|
||||||
|
const hebras = Array.from(container.querySelectorAll('[data-hebra]'));
|
||||||
|
expect(hebras).toHaveLength(3);
|
||||||
|
// The point of the highlight is the pairing: the reader has to be able to
|
||||||
|
// tell WHICH strand belongs to the card under the cursor. One gold, two
|
||||||
|
// faded — if all three stayed grey the drawing would still be decoration.
|
||||||
|
const doradas = hebras.filter((h) => (h.getAttribute('class') ?? '').includes('text-dorado'));
|
||||||
|
expect(doradas).toHaveLength(1);
|
||||||
|
expect(doradas[0].getAttribute('data-hebra')).toBe('1');
|
||||||
|
hebras
|
||||||
|
.filter((h) => h.getAttribute('data-hebra') !== '1')
|
||||||
|
.forEach((h) => expect(h.getAttribute('class') ?? '').toMatch(/text-gris-medio\/35/));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the three strands equal when nothing is pointed at', () => {
|
||||||
|
const { container } = render(<IlustracionConvergencia />);
|
||||||
|
const hebras = Array.from(container.querySelectorAll('[data-hebra]'));
|
||||||
|
hebras.forEach((h) => {
|
||||||
|
const cls = h.getAttribute('class') ?? '';
|
||||||
|
expect(cls).toContain('text-gris-medio');
|
||||||
|
expect(cls).not.toContain('text-dorado');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Container } from '@/components/primitives/Container';
|
import { Container } from '@/components/primitives/Container';
|
||||||
import { TarjetaSpotlight } from '@/components/primitives/TarjetaSpotlight';
|
import { TarjetaSpotlight } from '@/components/primitives/TarjetaSpotlight';
|
||||||
@@ -11,6 +12,11 @@ const ICONOS: IconoNombre[] = ['comunicacion', 'digital', 'experiencias'];
|
|||||||
|
|
||||||
export function Disciplinas() {
|
export function Disciplinas() {
|
||||||
const reduced = usePrefersReducedMotion();
|
const reduced = usePrefersReducedMotion();
|
||||||
|
// Which discipline the reader is pointing at, so its strand lights up in the
|
||||||
|
// drawing above. A pointer-only enhancement on purpose: it reveals nothing —
|
||||||
|
// all three cards and all three strands are fully there without it — so
|
||||||
|
// nobody loses anything by not having a mouse.
|
||||||
|
const [senalada, setSenalada] = useState<number | null>(null);
|
||||||
return (
|
return (
|
||||||
<section className="bg-white">
|
<section className="bg-white">
|
||||||
<Container className="py-16 md:py-24">
|
<Container className="py-16 md:py-24">
|
||||||
@@ -21,12 +27,14 @@ export function Disciplinas() {
|
|||||||
<motion.div variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
<motion.div variants={fadeInUp} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
||||||
className="mt-4 md:flex md:items-center md:justify-between md:gap-10">
|
className="mt-4 md:flex md:items-center md:justify-between md:gap-10">
|
||||||
<p className="text-gris-medio text-lg">{disciplinas.lead}</p>
|
<p className="text-gris-medio text-lg">{disciplinas.lead}</p>
|
||||||
<IlustracionConvergencia className="mt-6 md:mt-0 w-56 md:w-64 h-auto shrink-0" />
|
<IlustracionConvergencia className="mt-6 md:mt-0 w-56 md:w-64 h-auto shrink-0" activa={senalada} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.ul variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
<motion.ul variants={staggerContainer} initial="hidden" whileInView="visible" viewport={viewportOnce} {...motionProps(reduced)}
|
||||||
className="mt-12 grid gap-6 md:grid-cols-3 list-none p-0">
|
className="mt-12 grid gap-6 md:grid-cols-3 list-none p-0">
|
||||||
{disciplinas.items.map((item, i) => (
|
{disciplinas.items.map((item, i) => (
|
||||||
<motion.li key={item.name} variants={fadeInUp}
|
<motion.li key={item.name} variants={fadeInUp}
|
||||||
|
onPointerEnter={() => setSenalada(i)}
|
||||||
|
onPointerLeave={() => setSenalada(null)}
|
||||||
className="transition-transform duration-300 ease-out hover:-translate-y-1.5 motion-reduce:transform-none motion-reduce:transition-none">
|
className="transition-transform duration-300 ease-out hover:-translate-y-1.5 motion-reduce:transform-none motion-reduce:transition-none">
|
||||||
<TarjetaSpotlight className="h-full rounded-lg bg-azul-profundo text-white p-7">
|
<TarjetaSpotlight className="h-full rounded-lg bg-azul-profundo text-white p-7">
|
||||||
<div className="grid place-items-center w-14 h-14 rounded-full bg-white/10">
|
<div className="grid place-items-center w-14 h-14 rounded-full bg-white/10">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { Disciplinas } from '../Disciplinas';
|
import { Disciplinas } from '../Disciplinas';
|
||||||
|
|
||||||
@@ -16,4 +16,26 @@ describe('<Disciplinas />', () => {
|
|||||||
const items = list?.querySelectorAll('li');
|
const items = list?.querySelectorAll('li');
|
||||||
expect(items?.length).toBe(3);
|
expect(items?.length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('lights the strand of the discipline being pointed at', () => {
|
||||||
|
const { container } = render(<Disciplinas />);
|
||||||
|
const tarjetas = container.querySelectorAll('ul > li');
|
||||||
|
expect(tarjetas).toHaveLength(3);
|
||||||
|
|
||||||
|
// Before anything is pointed at, the drawing is neutral.
|
||||||
|
const hebrasDoradas = () =>
|
||||||
|
Array.from(container.querySelectorAll('[data-hebra]')).filter((h) =>
|
||||||
|
(h.getAttribute('class') ?? '').includes('text-dorado'),
|
||||||
|
);
|
||||||
|
expect(hebrasDoradas()).toHaveLength(0);
|
||||||
|
|
||||||
|
// Pointing at "Digital", the second card, has to light the second strand
|
||||||
|
// and nothing else. This wiring is the whole change: without it the three
|
||||||
|
// cards and the drawing beside them are unrelated objects on a page.
|
||||||
|
fireEvent.pointerEnter(tarjetas[1]);
|
||||||
|
expect(hebrasDoradas().map((h) => h.getAttribute('data-hebra'))).toEqual(['1']);
|
||||||
|
|
||||||
|
fireEvent.pointerLeave(tarjetas[1]);
|
||||||
|
expect(hebrasDoradas()).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user