diff --git a/components/primitives/Container.tsx b/components/primitives/Container.tsx index 98d938c..6393319 100644 --- a/components/primitives/Container.tsx +++ b/components/primitives/Container.tsx @@ -1,5 +1,5 @@ import type { ElementType, ReactNode } from 'react'; export function Container({ as: Tag = 'div', className = '', children }: { as?: ElementType; className?: string; children: ReactNode }) { - return {children}; + return {children}; } diff --git a/components/primitives/HiloConductor.tsx b/components/primitives/HiloConductor.tsx index 911f158..8d60c21 100644 --- a/components/primitives/HiloConductor.tsx +++ b/components/primitives/HiloConductor.tsx @@ -3,45 +3,28 @@ import { motion, useScroll, useSpring } from 'framer-motion'; import { usePrefersReducedMotion } from '@/lib/motion'; -// One continuous gold line running the height of the page, drawing itself as -// the reader scrolls. It is the spine of the layout: it stitches the sections -// together so the landing reads as a route rather than a stack of blocks. +// The gold spine of the page: one continuous line down the vertical centre, +// drawing itself as the reader scrolls. It is what turns ten sections into a +// single route — *claridad · dirección · crecimiento* made visible, per the +// Manual de Identidad Visual. // -// Positioned `absolute`, never `fixed` — the thread belongs to the document -// and scrolls with it. A fixed layer would read as an ornament stuck to the -// window. +// Centred, never in a side gutter: the brand's through-line belongs in the +// middle, on every breakpoint. It sits ABOVE the section backgrounds (z-10) +// but BELOW the text, which `Container` lifts to z-20 — so the thread reads +// as a faint watermark spine behind the reading, never crossing it. // -// Uses the page-level useScroll() with no target ref on purpose: no element -// measuring, no "container has a static position" warnings, and one scroll -// listener for the whole page. -// -// It lives in the empty margin band to the left of the centred -// `max-w-content` (1152px) column, never over it — some sections are -// `position: relative` for unrelated reasons and some are not, so an -// absolutely-positioned overlay with no explicit stacking level paints -// inconsistently: above the static sections, behind the relative ones. `z-30` -// gives it its own stacking context that beats every section root regardless -// of that accident, while staying under the sticky header's `z-50`. -// -// Hidden below `2xl` (1536px): that's the first breakpoint that guarantees a -// safe margin outside the 1152px column — (1536 - 1152) / 2 = 192px per side, -// comfortable room for the thread's own offset plus breathing space to the -// viewport edge. Narrower viewports have no safe band, so the thread simply -// doesn't render there rather than creeping over the text. +// Page-level useScroll() with no target ref on purpose: no element measuring, +// no "container has a static position" warnings, one listener for the page. const TRAZO = - 'M50 0 C 20 140, 80 260, 50 400 C 20 540, 80 660, 50 800 C 20 940, 80 1060, 50 1200'; + 'M50 0 C 42 200, 58 400, 50 600 C 42 800, 58 1000, 50 1200'; export function HiloConductor({ className = '' }: { className?: string }) { const reduced = usePrefersReducedMotion(); const { scrollYProgress } = useScroll(); const avance = useSpring(scrollYProgress, { stiffness: 90, damping: 30, restDelta: 0.001 }); - // Percentage-based band geometry against the containing block, never - // `100vw` — `100vw` includes the scrollbar and would overshoot the real - // document width by ~15px on desktop. `inset-0` and the inner `mx-auto - // max-w-content` box both resolve against `
`'s own width instead. - const wrapper = `pointer-events-none absolute inset-0 z-30 hidden 2xl:block ${className}`; + const wrapper = `pointer-events-none absolute inset-0 z-10 ${className}`; // Plain for reduced motion: motion.path applies dash scaffolding even // without initial/animate and would leave the thread invisible. @@ -61,10 +44,10 @@ export function HiloConductor({ className = '' }: { className?: string }) { return (
{/* Mirrors Container's own centring (`mx-auto max-w-content`) so the - band below is measured from the real column edge. */} + thread is measured from the real column edge and stays centred in it. */}
', () => { + it('lifts its content above the conductor thread', () => { + const { container } = render(contenido); + const cls = container.firstElementChild?.className ?? ''; + // The thread sits at z-10; the section backgrounds are z-auto. Lifting the + // content column to z-20 is what puts the text *above* a centred thread, + // so the spine reads behind the reading instead of across it. + expect(cls).toContain('relative'); + expect(cls).toMatch(/\bz-20\b/); + }); +}); diff --git a/components/primitives/__tests__/HiloConductor.test.tsx b/components/primitives/__tests__/HiloConductor.test.tsx index 08eaea5..0b7cacc 100644 --- a/components/primitives/__tests__/HiloConductor.test.tsx +++ b/components/primitives/__tests__/HiloConductor.test.tsx @@ -33,15 +33,15 @@ describe('', () => { expect(container.firstElementChild?.className).toContain('pointer-events-none'); }); - it('sits in the margin band beside the column, never over it', () => { + it('runs down the centre of the column, not in a side gutter', () => { const { container } = render(); - const svg = container.querySelector('svg'); - // `right-full` (`right: 100%`) anchors the thread's right edge to the - // left edge of the `max-w-content` column it shares a parent with — - // structurally outside the column, not merely visually offset within it. - // Read via getAttribute: on an SVGElement, `.className` is an - // SVGAnimatedString, not a plain string. - expect(svg?.getAttribute('class')).toContain('right-full'); + const cls = container.querySelector('svg')?.getAttribute('class') ?? ''; + // `left-1/2 -translate-x-1/2` anchors the thread to the column's centre; + // the old `right-full` banished it to the left gutter, which is why the + // thread read as off to the side instead of as the page's spine. + expect(cls).toContain('left-1/2'); + expect(cls).toContain('-translate-x-1/2'); + expect(cls).not.toContain('right-full'); }); it('declares a stacking level that beats section roots but stays under the header', () => { @@ -55,13 +55,14 @@ describe('', () => { expect(wrapperClass).not.toContain('z-50'); }); - it('is hidden below the breakpoint that guarantees a safe margin', () => { + it('renders on every breakpoint, not only on very wide screens', () => { const { container } = render(); const wrapperClass = container.firstElementChild?.className ?? ''; - // No safe band exists under the content column's own breakpoint; hiding - // it there is what keeps it from ever creeping over the text. - expect(wrapperClass).toContain('hidden'); - expect(wrapperClass).toContain('2xl:block'); + // The thread is the brand's through-line; it belongs in the middle on a + // phone too. Legibility is handled by layering it behind the text, not by + // hiding it below a breakpoint. + expect(wrapperClass).not.toContain('hidden'); + expect(wrapperClass).not.toContain('2xl:block'); }); it('renders a genuine plain path under reduced motion, not a neutralised motion.path', () => {