Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion resources/js/components/landing/download-rail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ interface Props {
export default function DownloadRail({ note }: Props) {
return (
<>
<FullLine />
<Container>
<div className="flex flex-col gap-3 px-4 py-4 sm:flex-row sm:items-center sm:justify-between">
<p className="font-mono text-xs text-muted-foreground">{note}</p>
Expand Down
1 change: 0 additions & 1 deletion resources/js/components/landing/objection-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ const QUESTIONS = [
export default function ObjectionRow() {
return (
<section aria-label="Common questions before downloading">
<FullLine />
<Container>
<div className="grid grid-cols-1 md:grid-cols-2">
{QUESTIONS.map((question, i) => {
Expand Down
3 changes: 1 addition & 2 deletions resources/js/components/landing/pricing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,8 @@ export default function Pricing({ paymentProvider, teamMinSeats }: { paymentProv
<FullLine />
</Container>

{/* Billing toggle */}
{/* Billing toggle. No opening rule: the ledger above closes with one. */}
<Container>
<FullLine />
<div className="flex items-center justify-center overflow-x-auto py-4">
<div className="inline-flex items-center rounded-full border border-rule bg-surface-raised p-1">
{cycles.map((c) => (
Expand Down
8 changes: 7 additions & 1 deletion resources/js/components/landing/spec-strip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,13 @@ export default function SpecStrip({ latestRelease }: Props) {

return (
<section aria-label="Key specifications" className="scroll-mt-20">
<FullLine />
{/*
* 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.
*/}
<Container>
<DataTable
className="table-fixed"
Expand Down
12 changes: 11 additions & 1 deletion resources/js/layouts/landing-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ export default function LandingLayout({ header, footer, children }: Props) {
</a>
{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 <main>
* 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
Expand All @@ -59,7 +69,7 @@ export default function LandingLayout({ header, footer, children }: Props) {
* equal.
*/}
<div
className="grid min-h-dvh grid-cols-1 justify-center [--gutter-width:2.5rem] md:-mx-4 md:grid-cols-[var(--gutter-width)_minmax(0,80rem)_var(--gutter-width)] lg:mx-0"
className="grid min-h-dvh grid-cols-1 grid-rows-[1fr_auto] justify-center [--gutter-width:2.5rem] md:-mx-4 md:grid-cols-[var(--gutter-width)_minmax(0,80rem)_var(--gutter-width)] lg:mx-0"
>
{/*
* Left gutter. The 45-degree hatch is gone: it was the page's
Expand Down
3 changes: 1 addition & 2 deletions resources/js/pages/Download.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,8 @@ export default function Download({ downloadUrls, githubStars }: Props) {
<FullLine />
</Container>

{/* System requirements */}
{/* System requirements. No opening rule: the block above closes with one. */}
<Container>
<FullLine />
<p className="px-4 py-2 text-xs text-muted-foreground-subtle">
Requires macOS 14 Sonoma or later
</p>
Expand Down
66 changes: 66 additions & 0 deletions tests/Feature/Landing/LandingStructureTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<section>` and unstyled `<div>` count as transparent here. Anything
* that can carry height genuinely separates the two.
*/
preg_match_all('/<div class="[^"]*rule-numbered[^"]*"[^>]*>/', $html, $matches, PREG_OFFSET_CAPTURE);

$ends = [];
foreach ($matches[0] as [$tag, $offset]) {
$open = $offset + strlen($tag);
$close = strpos($html, '</div>', $open);
if (str_contains(substr($html, $open, $close - $open), '<div')) {
$close = strpos($html, '</div>', strpos($html, '</div>', $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('/(?<!scroll-)\b(h-\d|min-h|p-\d|py-\d|pt-\d|pb-\d|m[tby]?-\d|grid|flex)/', $tag[2])) {
$boxes = true;
break;
}
}

if (! $boxes) {
$coincident[] = ($i + 1) . '/' . ($i + 2);
}
}

expect($coincident)->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 <main> ended — the footer stood beside nothing.
*/
expect($html)->toContain('grid-rows-[1fr_auto]');
expect($html)->toContain('row-start-2');
});
Loading