fix(hilo): la banda alargaba el documento 5.800px de vacío

Un <svg> con viewBox es un elemento reemplazado con proporción intrínseca
(100:1200). Al dimensionarlo con top-0 + bottom-20 y dejar la altura en
auto, el navegador la resolvía desde el ancho — 1152 × 12 = 13.824px — y
esa altura ganaba silenciosamente a `bottom`.

Consecuencias, todas visibles en el navegador y ninguna en los tests:
- 5.840px de blanco vacío scrolleable por debajo del footer.
- scrollYProgress repartido sobre 13.901px en vez de 8.061, así que el
  trazo nunca terminaba sobre contenido real.
- la onda estirada 1,8x de más, otra vez casi recta.

Altura explícita con h-[calc(100%-5rem)], y test que lo fija: jsdom no
hace layout, así que el guardián es la clase.

El trazo pasa a completarse a mitad de scroll (FIN 0.85 → 0.5). Con 0.85
la punta se quedaba a media pantalla durante el 42% de la página, que es
justo lo que se leía como "sin terminar".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
erikghandClaude Opus 5 committed 2026-07-31 16:11:41 -06:00
1 parent 3d3d5b2f9a
commit 174fcb855a
2 files changed
+21 -5

No files matched your search

+10 -5
View File
@@ -38,10 +38,11 @@ const TRAZO = Array.from({ length: TRAMOS }, (_, i) => {
return `C ${x} ${y + PASO * 0.25}, ${salida} ${y + PASO * 0.75}, 50 ${y + PASO}`;
}).reduce((d, tramo) => `${d} ${tramo}`, 'M50 0');
// The draw finishes at 85% of the document instead of at the very bottom:
// scrollYProgress only reaches 1 past the footer, so tying the thread to it
// left the tip trailing in mid-screen and the ending never seen.
const FIN = 0.85;
// The draw finishes halfway down the document. Tied to raw scrollYProgress the
// leading tip sat in mid-viewport for most of the page, reading as an amputated
// stub — the "unfinished" complaint. Finishing early lets the tip outrun the
// reader within the hero and leaves a complete spine for everything after.
const FIN = 0.5;
export function HiloConductor({ className = '' }: { className?: string }) {
const reduced = usePrefersReducedMotion();
@@ -87,8 +88,12 @@ export function HiloConductor({ className = '' }: { className?: string }) {
{/* Mirrors Container's own centring (`mx-auto max-w-content`) so the
thread is measured from the real column edge and stays centred in it. */}
<div className="relative mx-auto h-full w-full max-w-content">
{/* Explicit height, never a top+bottom pair: an <svg> carrying a viewBox
is a replaced element with an intrinsic 100:1200 ratio, so `height:
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 bottom-20 left-1/2 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/25"
viewBox={`0 0 100 ${ALTO}`}
preserveAspectRatio="none"
aria-hidden="true"
@@ -95,6 +95,17 @@ describe('<HiloConductor />', () => {
expect(container.querySelector('path')).toHaveAttribute('vector-effect', 'non-scaling-stroke');
});
it('sizes the band with an explicit height, never a top+bottom pair', () => {
const { container } = render(<HiloConductor />);
const cls = container.querySelector('svg')?.getAttribute('class') ?? '';
// An <svg> carrying a viewBox is a replaced element with an intrinsic
// ratio, so `height: auto` resolves from the width and silently beats
// `bottom` — which stretched this one to 13824px and hung ~6000px of blank
// scroll below the footer. jsdom has no layout, so the class is the guard.
expect(cls).toMatch(/\bh-\[/);
expect(cls).not.toMatch(/\bbottom-/);
});
it('reads as a band, not as a hairline', () => {
const { container } = render(<HiloConductor />);
const grosor = Number(container.querySelector('path')?.getAttribute('stroke-width'));