From 6e9c0ab5035b339de50e3c7ced42edb477c6d153 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sat, 15 Aug 2026 11:17:28 +0700 Subject: [PATCH] fix(landing): run the rails past the footer, and stop stacking two rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two visible defects, both reported from a screenshot. **The vertical lines stopped dead above the footer.** `row-span-full` compiles to `grid-row: 1 / -1`, and `-1` counts back from the last line of the *explicit* grid. The layout declared `grid-cols` and nothing else, so every row was implicit, `-1` resolved to line 1, and the span collapsed to a single row: both gutters and both container rails ended where `
` ended and the footer stood beside nothing. Declaring `grid-rows-[1fr_auto]` makes `-1` mean what it reads like, and the `1fr` also pins the footer to the bottom on short pages, which `min-h-dvh` alone never did. **The gutter printed 08 and 09 on top of each other.** Five places had one block closing with a rule and the next opening with one. Two rules at the same y draw a single hairline and consume two ordinals, so the numbers collided — on the homepage at the join between the hero screenshot and the spec table, and four more times elsewhere. A boundary is one rule, and it belongs to whatever closes; the opening rules are gone from spec-strip, objection-row, download-rail, pricing's billing toggle and Download's system-requirements line. Both are held by tests now. The rule test was verified by reintroducing the spec-strip rule and watching it fail with "Rules 8/9 render at the same height" — the exact pair in the screenshot. It treats only `
` and unstyled `
` as transparent, since anything that can carry height genuinely separates two rules, and `scroll-mt-*` is excluded because it is scroll-margin and adds no layout height — which is precisely what fooled my first attempt at finding these by hand. --- .../js/components/landing/download-rail.tsx | 1 - .../js/components/landing/objection-row.tsx | 1 - resources/js/components/landing/pricing.tsx | 3 +- .../js/components/landing/spec-strip.tsx | 8 ++- resources/js/layouts/landing-layout.tsx | 12 +++- resources/js/pages/Download.tsx | 3 +- .../Feature/Landing/LandingStructureTest.php | 66 +++++++++++++++++++ 7 files changed, 86 insertions(+), 8 deletions(-) diff --git a/resources/js/components/landing/download-rail.tsx b/resources/js/components/landing/download-rail.tsx index a60ee10..6c16e6d 100644 --- a/resources/js/components/landing/download-rail.tsx +++ b/resources/js/components/landing/download-rail.tsx @@ -28,7 +28,6 @@ interface Props { export default function DownloadRail({ note }: Props) { return ( <> -

{note}

diff --git a/resources/js/components/landing/objection-row.tsx b/resources/js/components/landing/objection-row.tsx index d8c25c6..c58b272 100644 --- a/resources/js/components/landing/objection-row.tsx +++ b/resources/js/components/landing/objection-row.tsx @@ -28,7 +28,6 @@ const QUESTIONS = [ export default function ObjectionRow() { return (
-
{QUESTIONS.map((question, i) => { diff --git a/resources/js/components/landing/pricing.tsx b/resources/js/components/landing/pricing.tsx index 1fee784..e1019fd 100644 --- a/resources/js/components/landing/pricing.tsx +++ b/resources/js/components/landing/pricing.tsx @@ -345,9 +345,8 @@ export default function Pricing({ paymentProvider, teamMinSeats }: { paymentProv - {/* Billing toggle */} + {/* Billing toggle. No opening rule: the ledger above closes with one. */} -
{cycles.map((c) => ( diff --git a/resources/js/components/landing/spec-strip.tsx b/resources/js/components/landing/spec-strip.tsx index 9b48bb6..0c3276c 100644 --- a/resources/js/components/landing/spec-strip.tsx +++ b/resources/js/components/landing/spec-strip.tsx @@ -81,7 +81,13 @@ export default function SpecStrip({ latestRelease }: Props) { return (
- + {/* + * No opening rule. The block above this one closes with a + * FullLine, and two rules at the same y draw a single hairline + * while consuming two ordinals — which is how the gutter ended + * up printing 08 and 09 on top of each other. A boundary is one + * rule, and it belongs to whatever closes. + */} {header} {/* + * `grid-rows-[1fr_auto]` is load-bearing, not cosmetic. The gutters and + * the container rails span the full height with `row-span-full`, which + * compiles to `grid-row: 1 / -1` — and `-1` counts back from the last + * line of the *explicit* grid. With only `grid-cols` declared, every + * row was implicit, so `-1` resolved to line 1 and the span collapsed + * to a single row: all four vertical lines stopped dead where
+ * ended and the footer stood beside nothing. Declaring the two rows + * makes `-1` mean what it reads like. The `1fr` also pins the footer + * to the bottom on short pages, which `min-h-dvh` alone did not. + * * The middle column is 80rem, matching `Container`'s own max-width, * not `--breakpoint-2xl` (96rem). At 96rem the container floated * free of its column above ~1616px and the gutters detached from @@ -59,7 +69,7 @@ export default function LandingLayout({ header, footer, children }: Props) { * equal. */}
{/* * Left gutter. The 45-degree hatch is gone: it was the page's diff --git a/resources/js/pages/Download.tsx b/resources/js/pages/Download.tsx index b30aec5..1b1f7d9 100644 --- a/resources/js/pages/Download.tsx +++ b/resources/js/pages/Download.tsx @@ -157,9 +157,8 @@ export default function Download({ downloadUrls, githubStars }: Props) { - {/* System requirements */} + {/* System requirements. No opening rule: the block above closes with one. */} -

Requires macOS 14 Sonoma or later

diff --git a/tests/Feature/Landing/LandingStructureTest.php b/tests/Feature/Landing/LandingStructureTest.php index 4ad9afa..904ee9d 100644 --- a/tests/Feature/Landing/LandingStructureTest.php +++ b/tests/Feature/Landing/LandingStructureTest.php @@ -154,3 +154,69 @@ function landingHtml(): string expect($html)->toContain('>Included<'); expect($html)->toContain('>Not included<'); }); + +it('never draws two rules at the same height', function (): void { + $html = landingHtml(); + + /* + * Two rules with nothing between them but a section boundary render as one + * hairline and consume two ordinals, so the gutter prints both numbers on + * top of each other. That shipped, visibly, as an unreadable "08/09" — + * a boundary is one rule, and it belongs to whatever closes. + * + * Only `
` and unstyled `
` count as transparent here. Anything + * that can carry height genuinely separates the two. + */ + preg_match_all('/
]*>/', $html, $matches, PREG_OFFSET_CAPTURE); + + $ends = []; + foreach ($matches[0] as [$tag, $offset]) { + $open = $offset + strlen($tag); + $close = strpos($html, '
', $open); + if (str_contains(substr($html, $open, $close - $open), '', strpos($html, '
', $open) + 6); + } + $ends[] = [$offset, $close + 6]; + } + + $coincident = []; + for ($i = 0; $i < count($ends) - 1; $i++) { + $between = substr($html, $ends[$i][1], $ends[$i + 1][0] - $ends[$i][1]); + + if (trim(strip_tags($between)) !== '') { + continue; + } + + $boxes = false; + preg_match_all('/<(\w+)([^>]*)>/', $between, $tags, PREG_SET_ORDER); + foreach ($tags as $tag) { + if (! in_array($tag[1], ['section', 'div'], true)) { + $boxes = true; + break; + } + if (preg_match('/(?toBe([], 'Rules ' . implode(', ', $coincident) . ' render at the same height'); +}); + +it('spans the gutters and rails past the footer', function (): void { + $html = landingHtml(); + + /* + * `row-span-full` compiles to `grid-row: 1 / -1`, and `-1` counts back from + * the last line of the *explicit* grid. With only `grid-cols` declared every + * row was implicit, `-1` resolved to line 1, and all four vertical lines + * stopped dead where
ended — the footer stood beside nothing. + */ + expect($html)->toContain('grid-rows-[1fr_auto]'); + expect($html)->toContain('row-start-2'); +});