feat(diseño): los diez arreglos de la revisión visual
Revisada la página con Chromium a 1440x900 y 390x844 en nueve alturas de scroll, y verificado cada cambio contra capturas nuevas. - El ≡ deja de leerse como icono de menú. lib/simbolo.ts se anunciaba como single source of truth pero no coincidía con Wordmark.tsx, que traza la marca con las proporciones medidas del Manual: barra de largo 5.919× su grosor, hueco de 2×, extremos de píldora. Ahora se derivan de ahí. De paso, las líneas se meten medio grosor por cada extremo: el remate redondo alargaba el trazo un grosor entero sobre la barra que debía reproducir. - Banda: 12px en escritorio y 7px en móvil vía CSS (14px era el 3,6% de una pantalla de 390px y partía los párrafos), opacidad al 18%, y muelle de 90/30 a 210/40 — sobreamortiguado tardaba ~2s en asentar y la punta se arrastraba a media pantalla. El punto suelto que aparecía centrado arriba al cargar era el remate redondo pintándose en el origen con longitud cero: ahora la opacidad sigue al avance. - Ritmo vertical un 25-30% más apretado en las diez secciones. La página pasa de 8.061 a 7.457px sin tocar contenido. - Números 01-06: de marca de agua de 48px al 6% (que chocaba con títulos y reglas) a índice dorado sentado sobre la regla. - Hero: los dos CTA al mismo ancho en móvil, el patrón de puntos fuera del logotipo, y la secuencia visible en todos los tamaños — oculta bajo lg dejaba el hero del móvil sin ninguna pieza gráfica. La convergencia de la variante "hero" se cierra más para que se lea como líneas buscando su eje y no como un garabato tachado. - Estratega: fuera la retícula de puntos del retrato, que parecía el recuadro de imagen rota del navegador; la etiqueta del 30+ cabía en menos de lo que mide "construyendo" y salía en tres líneas. - El nudo de Dolores, más pequeño y con algo más de contraste. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
174fcb855a
commit
fa86605b39
15 files changed
+135
-52
No files matched your search
@@ -3,7 +3,7 @@
|
||||
import { useState } from 'react';
|
||||
import { motion, useMotionValueEvent, useScroll, useSpring, useTransform } from 'framer-motion';
|
||||
import { usePrefersReducedMotion } from '@/lib/motion';
|
||||
import { BARRAS_SIMBOLO, VIEWBOX_SIMBOLO } from '@/lib/simbolo';
|
||||
import { BARRAS_SIMBOLO, RADIO_BARRA, VIEWBOX_SIMBOLO } from '@/lib/simbolo';
|
||||
|
||||
// The gold spine of the page: one continuous line down the vertical centre,
|
||||
// drawing itself as the reader scrolls and resolving into the ≡ at the close.
|
||||
@@ -23,8 +23,12 @@ const ALTO = 1200;
|
||||
const PASO = ALTO / TRAMOS;
|
||||
|
||||
// A band, not a thread. Held in real pixels by vector-effect below, so it never
|
||||
// again collapses to a hairline when the viewBox scale changes.
|
||||
const GROSOR = 14;
|
||||
// again collapses to a hairline when the viewBox scale changes. The attribute
|
||||
// is the desktop weight and the SSR fallback; GROSORES overrides it in CSS,
|
||||
// because 14px is 3.6% of a 390px screen and cut straight through the body
|
||||
// copy there. CSS stroke-width wins over the presentation attribute.
|
||||
const GROSOR = 12;
|
||||
const GROSORES = '[stroke-width:7px] md:[stroke-width:12px]';
|
||||
|
||||
// Four long swells rather than one page-long curve or ten short ones. Stretched
|
||||
// over the whole document with preserveAspectRatio="none", a single swell reads
|
||||
@@ -48,7 +52,13 @@ export function HiloConductor({ className = '' }: { className?: string }) {
|
||||
const reduced = usePrefersReducedMotion();
|
||||
const { scrollYProgress } = useScroll();
|
||||
const recorrido = useTransform(scrollYProgress, [0, FIN], [0, 1], { clamp: true });
|
||||
const avance = useSpring(recorrido, { stiffness: 90, damping: 30, restDelta: 0.001 });
|
||||
// Stiff enough to keep up with the reader. At 90/30 the spring is heavily
|
||||
// overdamped and takes some two seconds to settle, so the leading edge
|
||||
// visibly trailed behind the scroll and read as an amputated stub.
|
||||
const avance = useSpring(recorrido, { stiffness: 210, damping: 40, restDelta: 0.001 });
|
||||
// At rest the round cap still paints its own dot at the path's origin, which
|
||||
// landed as a stray blob dead centre at the top of the page on first load.
|
||||
const aparicion = useTransform(avance, [0, 0.015], [0, 1]);
|
||||
|
||||
const [dibujado, setDibujado] = useState(false);
|
||||
useMotionValueEvent(avance, 'change', (v) => {
|
||||
@@ -65,6 +75,7 @@ export function HiloConductor({ className = '' }: { className?: string }) {
|
||||
const trazo = reduced ? (
|
||||
<path
|
||||
d={TRAZO}
|
||||
className={GROSORES}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={GROSOR}
|
||||
@@ -74,12 +85,13 @@ export function HiloConductor({ className = '' }: { className?: string }) {
|
||||
) : (
|
||||
<motion.path
|
||||
d={TRAZO}
|
||||
className={GROSORES}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={GROSOR}
|
||||
strokeLinecap="round"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
style={{ pathLength: avance }}
|
||||
style={{ pathLength: avance, opacity: aparicion }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -93,7 +105,7 @@ export function HiloConductor({ className = '' }: { className?: string }) {
|
||||
auto` resolves from the width (1152 → 13824px) and silently wins
|
||||
over `bottom`, adding ~6000px of blank scroll below the footer. */}
|
||||
<svg
|
||||
className="absolute top-0 left-1/2 h-[calc(100%-5rem)] w-full -translate-x-1/2 text-dorado/25"
|
||||
className="absolute top-0 left-1/2 h-[calc(100%-5rem)] w-full -translate-x-1/2 text-dorado/[0.18]"
|
||||
viewBox={`0 0 100 ${ALTO}`}
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden="true"
|
||||
@@ -105,19 +117,19 @@ export function HiloConductor({ className = '' }: { className?: string }) {
|
||||
the wordmark's ≡, never a lookalike. Uniformly scaled in its own
|
||||
svg, because the band's stretch would flatten the bars. */}
|
||||
<svg
|
||||
className="absolute bottom-4 left-1/2 w-24 -translate-x-1/2 text-dorado"
|
||||
className="absolute bottom-4 left-1/2 w-20 -translate-x-1/2 text-dorado"
|
||||
viewBox={VIEWBOX_SIMBOLO}
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
{BARRAS_SIMBOLO.map((barra, i) =>
|
||||
reduced ? (
|
||||
<rect key={i} {...barra} rx={1.5} fill="currentColor" />
|
||||
<rect key={i} {...barra} rx={RADIO_BARRA} fill="currentColor" />
|
||||
) : (
|
||||
<motion.rect
|
||||
key={i}
|
||||
{...barra}
|
||||
rx={1.5}
|
||||
rx={RADIO_BARRA}
|
||||
fill="currentColor"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: resuelto ? 1 : 0 }}
|
||||
|
||||
@@ -63,12 +63,15 @@ export function SecuenciaClaridad({
|
||||
if (!completa) {
|
||||
const eje = linea.y1;
|
||||
const d = DISPERSAS[i];
|
||||
// Pulled a good way further in than it used to be: at a third of the way
|
||||
// the tangle still read as a scratched-out scribble instead of as lines
|
||||
// finding their axis. It must arrive close to aligned and stop short.
|
||||
return {
|
||||
x1: d.x1 + (26 - d.x1) * 0.35,
|
||||
y1: d.y1 + (eje - d.y1) * 0.55,
|
||||
x2: d.x2 + (38 - d.x2) * 0.35,
|
||||
y2: d.y2 + (eje - d.y2) * 0.55,
|
||||
opacity: sobrevive ? 0.95 : 0.3,
|
||||
x1: d.x1 + (26 - d.x1) * 0.5,
|
||||
y1: d.y1 + (eje - d.y1) * 0.7,
|
||||
x2: d.x2 + (38 - d.x2) * 0.5,
|
||||
y2: d.y2 + (eje - d.y2) * 0.7,
|
||||
opacity: sobrevive ? 0.95 : 0.28,
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -108,11 +108,14 @@ describe('<HiloConductor />', () => {
|
||||
|
||||
it('reads as a band, not as a hairline', () => {
|
||||
const { container } = render(<HiloConductor />);
|
||||
const grosor = Number(container.querySelector('path')?.getAttribute('stroke-width'));
|
||||
const path = container.querySelector('path');
|
||||
// Twice regressed into a thread nobody could see. The band is the page's
|
||||
// spine and has to carry weight; the text stays legible because it sits a
|
||||
// stacking level above, not because the band is faint.
|
||||
expect(grosor).toBeGreaterThanOrEqual(10);
|
||||
// stacking level above, not because the band is faint. The attribute is the
|
||||
// desktop weight, the classes narrow it on phones where 12px is 3% of the
|
||||
// screen — CSS stroke-width beats the presentation attribute.
|
||||
expect(Number(path?.getAttribute('stroke-width'))).toBeGreaterThanOrEqual(10);
|
||||
expect(path?.getAttribute('class') ?? '').toMatch(/md:\[stroke-width:/);
|
||||
});
|
||||
|
||||
it('sweeps wide enough across the column to read as a route', () => {
|
||||
|
||||
@@ -13,7 +13,30 @@ describe('geometría del ≡', () => {
|
||||
expect(izquierdas.size).toBe(1);
|
||||
});
|
||||
|
||||
it('derives stroke lines through each bar centre', () => {
|
||||
expect(LINEAS_SIMBOLO.map((l) => l.y1)).toEqual([17, 32, 47]);
|
||||
it('holds the Manual proportions that make the mark read as an E', () => {
|
||||
const [b1, b2, b3] = BARRAS_SIMBOLO;
|
||||
// Measured off the Manual and traced in Wordmark.tsx. Stubbier bars or
|
||||
// tighter gaps than these and the ≡ reads as a hamburger menu icon.
|
||||
expect(b1.width / b1.height).toBeCloseTo(5.919, 3);
|
||||
expect((b2.y - (b1.y + b1.height)) / b1.height).toBeCloseTo(2, 6);
|
||||
expect(b3.y - b2.y).toBeCloseTo(b2.y - b1.y, 6);
|
||||
});
|
||||
|
||||
it('centres the mark in its own viewBox', () => {
|
||||
const [b1, , b3] = BARRAS_SIMBOLO;
|
||||
expect(b1.y).toBeCloseTo(64 - (b3.y + b3.height), 6);
|
||||
expect(b1.x).toBeCloseTo(64 - (b1.x + b1.width), 6);
|
||||
});
|
||||
|
||||
it('reproduces each bar exactly when drawn as a round-capped stroke', () => {
|
||||
BARRAS_SIMBOLO.forEach((barra, i) => {
|
||||
const linea = LINEAS_SIMBOLO[i];
|
||||
// A round cap reaches half the stroke width past its endpoint, so an
|
||||
// un-inset line would draw a pill a full thickness longer than the bar.
|
||||
expect(linea.x1 - linea.grosor / 2).toBeCloseTo(barra.x, 6);
|
||||
expect(linea.x2 + linea.grosor / 2).toBeCloseTo(barra.x + barra.width, 6);
|
||||
expect(linea.y1).toBeCloseTo(barra.y + barra.height / 2, 6);
|
||||
expect(linea.y2).toBeCloseTo(linea.y1, 6);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user