feat: redesign homepage as multi-column visual landing page
- Full-width hero with CSS grid: site title/tagline (left) + hand-coded SVG workbench illustration (right), stacks on mobile - Featured excerpt section with terracotta left border and inline diagram from 'Your Build Cache Is Lying to You' - Rage series as 2-column card grid at container width (1100px): each card shows number, title, description, reading time or 'Coming soon' badge - All posts as 2-column grid at reading width (720px) - Three-tier width system: hero (100%), series (--width-container), all-posts (--width-prose) - Responsive: collapses to single column below 768px - Updated tests to verify multi-column layout, SVG illustration, card grid structure, and responsive breakpoints (41 tests, all pass)
This commit is contained in:
+349
-142
@@ -16,6 +16,15 @@ const ragePosts: Record<string, string> = {
|
|||||||
'TypeScript 7 Makes the Build Harness More Important': 'typescript-7-makes-the-build-harness-more-important',
|
'TypeScript 7 Makes the Build Harness More Important': 'typescript-7-makes-the-build-harness-more-important',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rageDescriptions = [
|
||||||
|
'The silent stale cache problem. Every JS monorepo build cache shares this flaw.',
|
||||||
|
'Weak fingerprints find candidates. Strong fingerprints verify them. The pathset is the bridge.',
|
||||||
|
'Three operating systems. Three interception mechanisms. macOS interpose, Linux eBPF, Windows Detours.',
|
||||||
|
'Counter-intuitive: tsgo\u2019s 10x speedup makes distributed coordination more important, not less.',
|
||||||
|
'Per-package content-addressed storage. The unsexy problem that kills CI build times.',
|
||||||
|
'Personal narrative. From lage maintainer to Rust rewrite. The architecture\u2019s original sin.',
|
||||||
|
];
|
||||||
|
|
||||||
const rageSeries = [
|
const rageSeries = [
|
||||||
'Your Build Cache Is Lying to You',
|
'Your Build Cache Is Lying to You',
|
||||||
'Two-Phase Fingerprinting: BuildXL\u2019s Deep Magic',
|
'Two-Phase Fingerprinting: BuildXL\u2019s Deep Magic',
|
||||||
@@ -23,7 +32,7 @@ const rageSeries = [
|
|||||||
'TypeScript 7 Makes the Build Harness More Important',
|
'TypeScript 7 Makes the Build Harness More Important',
|
||||||
'node_modules in a Content-Addressed Store',
|
'node_modules in a Content-Addressed Store',
|
||||||
'What I Learned Maintaining Lage and Why I Rewrote It',
|
'What I Learned Maintaining Lage and Why I Rewrote It',
|
||||||
].map(title => {
|
].map((title, i) => {
|
||||||
const slug = ragePosts[title];
|
const slug = ragePosts[title];
|
||||||
const post = slug ? posts.find(p => p.id === slug) : null;
|
const post = slug ? posts.find(p => p.id === slug) : null;
|
||||||
return {
|
return {
|
||||||
@@ -31,6 +40,8 @@ const rageSeries = [
|
|||||||
slug: slug || null,
|
slug: slug || null,
|
||||||
published: !!post,
|
published: !!post,
|
||||||
readingTime: post?.body ? getReadingTime(post.body) : null,
|
readingTime: post?.body ? getReadingTime(post.body) : null,
|
||||||
|
number: i + 1,
|
||||||
|
description: rageDescriptions[i],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
---
|
---
|
||||||
@@ -38,93 +49,185 @@ const rageSeries = [
|
|||||||
<BaseLayout title='Crash Test Dev'>
|
<BaseLayout title='Crash Test Dev'>
|
||||||
<NavBar slot="nav" />
|
<NavBar slot="nav" />
|
||||||
|
|
||||||
|
<!-- Hero: full-width, two-column -->
|
||||||
<section class="hero" aria-label="Introduction">
|
<section class="hero" aria-label="Introduction">
|
||||||
<h1 class="site-title">Crash Test Dev</h1>
|
<div class="hero-text">
|
||||||
<p class="tagline">Engineering judgment, tested against reality.</p>
|
<h1 class="site-title">Crash Test Dev</h1>
|
||||||
<p class="byline">By Ken Chau</p>
|
<p class="tagline">Engineering judgment, tested against reality.</p>
|
||||||
|
<p class="byline">By Ken Chau</p>
|
||||||
|
</div>
|
||||||
|
<div class="hero-illustration" aria-hidden="true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-label="Engineering workbench illustration">
|
||||||
|
<!-- Background -->
|
||||||
|
<rect width="400" height="400" fill="#F5F0E8" rx="6"/>
|
||||||
|
|
||||||
|
<!-- Pegboard grid (subtle dots) -->
|
||||||
|
<g fill="#DDD7CD">
|
||||||
|
<circle cx="60" cy="60" r="2"/><circle cx="100" cy="60" r="2"/><circle cx="140" cy="60" r="2"/><circle cx="180" cy="60" r="2"/><circle cx="220" cy="60" r="2"/><circle cx="260" cy="60" r="2"/><circle cx="300" cy="60" r="2"/><circle cx="340" cy="60" r="2"/>
|
||||||
|
<circle cx="60" cy="100" r="2"/><circle cx="100" cy="100" r="2"/><circle cx="140" cy="100" r="2"/><circle cx="180" cy="100" r="2"/><circle cx="220" cy="100" r="2"/><circle cx="260" cy="100" r="2"/><circle cx="300" cy="100" r="2"/><circle cx="340" cy="100" r="2"/>
|
||||||
|
<circle cx="60" cy="140" r="2"/><circle cx="100" cy="140" r="2"/><circle cx="140" cy="140" r="2"/><circle cx="180" cy="140" r="2"/><circle cx="220" cy="140" r="2"/><circle cx="260" cy="140" r="2"/><circle cx="300" cy="140" r="2"/><circle cx="340" cy="140" r="2"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Wrench (hanging tool) -->
|
||||||
|
<line x1="100" y1="60" x2="100" y2="90" stroke="#2B2421" stroke-width="2"/>
|
||||||
|
<rect x="88" y="90" width="24" height="8" rx="2" fill="none" stroke="#2B2421" stroke-width="1.5"/>
|
||||||
|
<rect x="92" y="98" width="4" height="32" rx="1" fill="#2B2421"/>
|
||||||
|
<circle cx="94" cy="134" r="8" fill="none" stroke="#2B2421" stroke-width="1.5"/>
|
||||||
|
|
||||||
|
<!-- Caliper (hanging tool) -->
|
||||||
|
<line x1="220" y1="60" x2="220" y2="85" stroke="#2B2421" stroke-width="2"/>
|
||||||
|
<rect x="205" y="85" width="30" height="6" rx="1" fill="#7D6C2F"/>
|
||||||
|
<line x1="210" y1="91" x2="210" y2="140" stroke="#2B2421" stroke-width="1.5"/>
|
||||||
|
<line x1="230" y1="91" x2="230" y2="130" stroke="#2B2421" stroke-width="1.5"/>
|
||||||
|
<line x1="210" y1="140" x2="218" y2="148" stroke="#2B2421" stroke-width="1.5"/>
|
||||||
|
<line x1="230" y1="130" x2="222" y2="138" stroke="#2B2421" stroke-width="1.5"/>
|
||||||
|
|
||||||
|
<!-- Gear 1 (large) -->
|
||||||
|
<circle cx="160" cy="260" r="44" fill="none" stroke="#A0522D" stroke-width="2"/>
|
||||||
|
<circle cx="160" cy="260" r="32" fill="none" stroke="#A0522D" stroke-width="1.5"/>
|
||||||
|
<circle cx="160" cy="260" r="8" fill="#A0522D"/>
|
||||||
|
<!-- Gear teeth -->
|
||||||
|
<rect x="155" y="212" width="10" height="10" rx="1" fill="#A0522D"/>
|
||||||
|
<rect x="155" y="298" width="10" height="10" rx="1" fill="#A0522D"/>
|
||||||
|
<rect x="200" y="255" width="10" height="10" rx="1" fill="#A0522D"/>
|
||||||
|
<rect x="112" y="255" width="10" height="10" rx="1" fill="#A0522D"/>
|
||||||
|
<rect x="188" y="225" width="10" height="10" rx="1" fill="#A0522D" transform="rotate(45 193 230)"/>
|
||||||
|
<rect x="122" y="285" width="10" height="10" rx="1" fill="#A0522D" transform="rotate(45 127 290)"/>
|
||||||
|
<rect x="122" y="225" width="10" height="10" rx="1" fill="#A0522D" transform="rotate(-45 127 230)"/>
|
||||||
|
<rect x="188" y="285" width="10" height="10" rx="1" fill="#A0522D" transform="rotate(-45 193 290)"/>
|
||||||
|
|
||||||
|
<!-- Gear 2 (smaller, interlocking) -->
|
||||||
|
<circle cx="280" cy="240" r="28" fill="none" stroke="#7D6C2F" stroke-width="2"/>
|
||||||
|
<circle cx="280" cy="240" r="18" fill="none" stroke="#7D6C2F" stroke-width="1.5"/>
|
||||||
|
<circle cx="280" cy="240" r="5" fill="#7D6C2F"/>
|
||||||
|
<!-- Gear teeth -->
|
||||||
|
<rect x="275" y="208" width="10" height="8" rx="1" fill="#7D6C2F"/>
|
||||||
|
<rect x="275" y="264" width="10" height="8" rx="1" fill="#7D6C2F"/>
|
||||||
|
<rect x="304" y="235" width="8" height="10" rx="1" fill="#7D6C2F"/>
|
||||||
|
<rect x="248" y="235" width="8" height="10" rx="1" fill="#7D6C2F"/>
|
||||||
|
|
||||||
|
<!-- Workbench surface -->
|
||||||
|
<rect x="30" y="340" width="340" height="12" rx="2" fill="#2B2421"/>
|
||||||
|
<rect x="24" y="352" width="16" height="28" rx="1" fill="#2B2421"/>
|
||||||
|
<rect x="360" y="352" width="16" height="28" rx="1" fill="#2B2421"/>
|
||||||
|
|
||||||
|
<!-- Blueprint/ruler on bench -->
|
||||||
|
<rect x="50" y="324" width="80" height="14" rx="1" fill="none" stroke="#7D6C2F" stroke-width="1"/>
|
||||||
|
<line x1="60" y1="324" x2="60" y2="338" stroke="#7D6C2F" stroke-width="0.5"/>
|
||||||
|
<line x1="70" y1="328" x2="70" y2="338" stroke="#7D6C2F" stroke-width="0.5"/>
|
||||||
|
<line x1="80" y1="324" x2="80" y2="338" stroke="#7D6C2F" stroke-width="0.5"/>
|
||||||
|
<line x1="90" y1="328" x2="90" y2="338" stroke="#7D6C2F" stroke-width="0.5"/>
|
||||||
|
<line x1="100" y1="324" x2="100" y2="338" stroke="#7D6C2F" stroke-width="0.5"/>
|
||||||
|
<line x1="110" y1="328" x2="110" y2="338" stroke="#7D6C2F" stroke-width="0.5"/>
|
||||||
|
<line x1="120" y1="324" x2="120" y2="338" stroke="#7D6C2F" stroke-width="0.5"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr class="divider" />
|
<!-- Featured excerpt: full-width with visual -->
|
||||||
|
|
||||||
<section class="featured" aria-label="Featured excerpt">
|
<section class="featured" aria-label="Featured excerpt">
|
||||||
<p class="featured-label">From the latest</p>
|
<div class="featured-inner">
|
||||||
<blockquote class="excerpt">
|
<div class="featured-content">
|
||||||
AI raises the floor dramatically. It does not flatten the ceiling. The profession that survives is the one hardest to demo without, not just hardest to do without. The professional sees second-order failures coming. The amateur with AI doesn't know there is a second order.
|
<p class="featured-label">From the series</p>
|
||||||
</blockquote>
|
<blockquote class="excerpt">
|
||||||
<a class="featured-link" href={`${base}posts/ai-commoditizes-features-but-judgment-compounds/`}>
|
Your CI passed. Your tests passed. The build cache said nothing changed. It was wrong. You shipped a broken build to production because a cache returned stale output from three weeks ago. The bug took two days to find. The fix was one line. The root cause was not your code. It was your build system's trust model.
|
||||||
Read "AI Commoditizes Features, But Judgment Compounds"
|
</blockquote>
|
||||||
</a>
|
<a class="featured-link" href={`${base}posts/your-build-cache-is-lying-to-you/`}>
|
||||||
|
Read "Your Build Cache Is Lying to You" →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="featured-diagram" aria-hidden="true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 200" role="img" aria-label="The Silent Lie: how an undeclared input causes a stale cache hit">
|
||||||
|
<rect width="420" height="200" fill="#F5F0E8" rx="4"/>
|
||||||
|
<text x="210" y="24" text-anchor="middle" font-family="Georgia, serif" font-size="12" font-weight="bold" fill="#2B2421">The Silent Lie</text>
|
||||||
|
<rect x="14" y="40" width="100" height="44" rx="3" fill="none" stroke="#2B2421" stroke-width="1.2"/>
|
||||||
|
<text x="64" y="58" text-anchor="middle" font-family="monospace" font-size="8" fill="#2B2421">Developer changes</text>
|
||||||
|
<text x="64" y="72" text-anchor="middle" font-family="monospace" font-size="8" font-weight="bold" fill="#A0522D">feature-flags.json</text>
|
||||||
|
<line x1="114" y1="62" x2="136" y2="62" stroke="#2B2421" stroke-width="1.2"/>
|
||||||
|
<polygon points="134,58 142,62 134,66" fill="#2B2421"/>
|
||||||
|
<rect x="144" y="40" width="100" height="44" rx="3" fill="none" stroke="#2B2421" stroke-width="1.2"/>
|
||||||
|
<text x="194" y="56" text-anchor="middle" font-family="monospace" font-size="7.5" fill="#2B2421">Cache check:</text>
|
||||||
|
<text x="194" y="68" text-anchor="middle" font-family="monospace" font-size="7.5" fill="#2B2421">hash(src/**/*.ts)</text>
|
||||||
|
<text x="194" y="78" text-anchor="middle" font-family="monospace" font-size="7.5" fill="#2B2421">Nothing changed</text>
|
||||||
|
<line x1="244" y1="62" x2="266" y2="62" stroke="#2B2421" stroke-width="1.2"/>
|
||||||
|
<polygon points="264,58 272,62 264,66" fill="#2B2421"/>
|
||||||
|
<rect x="274" y="40" width="60" height="44" rx="3" fill="#A0522D"/>
|
||||||
|
<text x="304" y="60" text-anchor="middle" font-family="Georgia, serif" font-size="11" font-weight="bold" fill="#F5F0E8">MATCH</text>
|
||||||
|
<text x="304" y="74" text-anchor="middle" font-family="monospace" font-size="7" fill="#F5F0E8">(stale!)</text>
|
||||||
|
<line x1="334" y1="62" x2="356" y2="62" stroke="#2B2421" stroke-width="1.2"/>
|
||||||
|
<polygon points="354,58 362,62 354,66" fill="#2B2421"/>
|
||||||
|
<rect x="364" y="44" width="44" height="36" rx="3" fill="#A0522D"/>
|
||||||
|
<text x="386" y="62" text-anchor="middle" font-family="Georgia, serif" font-size="8" font-weight="bold" fill="#F5F0E8">Bug ships</text>
|
||||||
|
<text x="386" y="72" text-anchor="middle" font-family="Georgia, serif" font-size="8" font-weight="bold" fill="#F5F0E8">to prod</text>
|
||||||
|
<rect x="120" y="110" width="200" height="36" rx="3" fill="none" stroke="#A0522D" stroke-width="1" stroke-dasharray="4,2"/>
|
||||||
|
<text x="220" y="128" text-anchor="middle" font-family="Georgia, serif" font-size="9" fill="#A0522D">feature-flags.json was never declared.</text>
|
||||||
|
<text x="220" y="140" text-anchor="middle" font-family="Georgia, serif" font-size="9" fill="#A0522D">The cache didn't know it existed.</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr class="divider" />
|
<!-- Rage Series: 2-column grid at container width -->
|
||||||
|
|
||||||
<section class="series" aria-label="Rage series">
|
<section class="series" aria-label="Rage series">
|
||||||
<h2 class="section-heading">Rage: Rebuilding the Build System</h2>
|
<h2 class="section-heading">Rage: Rebuilding the Build System</h2>
|
||||||
<p class="series-description">A six-part investigation into rage, a Rust monorepo build system that fuses BuildXL's provable correctness with lage's ergonomics. From syscall sandboxing to content-addressed storage.</p>
|
<p class="series-description">A six-part investigation into rage, a Rust monorepo build system that fuses BuildXL's provable correctness with lage's ergonomics. From syscall sandboxing to content-addressed storage.</p>
|
||||||
<ol class="series-list">
|
<div class="series-grid">
|
||||||
{rageSeries.map((item) => (
|
{rageSeries.map((item) => (
|
||||||
<li class={`series-item ${item.published ? 'published' : ''}`}>
|
<div class={`series-card ${item.published ? 'published' : ''}`}>
|
||||||
{item.published ? (
|
<span class="card-number">{String(item.number).padStart(2, '0')}</span>
|
||||||
<a href={`${base}posts/${item.slug}/`} class="series-item-link">
|
<div class="card-body">
|
||||||
<span class="series-item-title">{item.title}</span>
|
<span class="card-title">{item.title}</span>
|
||||||
<span class="series-item-meta">{item.readingTime} min</span>
|
<span class="card-description">{item.description}</span>
|
||||||
</a>
|
{item.published ? (
|
||||||
) : (
|
<a href={`${base}posts/${item.slug}/`} class="card-link">
|
||||||
<>
|
Read · {item.readingTime} min
|
||||||
<span class="series-item-title">{item.title}</span>
|
</a>
|
||||||
<span class="series-item-status">Coming soon</span>
|
) : (
|
||||||
</>
|
<span class="card-status">Coming soon</span>
|
||||||
)}
|
)}
|
||||||
</li>
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr class="divider" />
|
<!-- All Posts: compact 2-column grid at reading width -->
|
||||||
|
|
||||||
<section class="all-posts" aria-label="All posts">
|
<section class="all-posts" aria-label="All posts">
|
||||||
<h2 class="section-heading">All Posts</h2>
|
<h2 class="section-heading">All Posts</h2>
|
||||||
{sortedPosts.map((post) => (
|
<div class="posts-grid">
|
||||||
<article class="post-entry">
|
{sortedPosts.map((post) => (
|
||||||
<a href={`${base}posts/${post.id}/`} class="post-entry-link">
|
<article class="post-entry">
|
||||||
<span class="post-entry-badge">{post.data.type}</span>
|
<a href={`${base}posts/${post.id}/`} class="post-entry-link">
|
||||||
<span class="post-entry-title">{post.data.title}</span>
|
<span class="post-entry-badge">{post.data.type}</span>
|
||||||
{post.body && (
|
<span class="post-entry-title">{post.data.title}</span>
|
||||||
<span class="post-entry-meta">{getReadingTime(post.body)} min</span>
|
{post.body && (
|
||||||
)}
|
<span class="post-entry-meta">{getReadingTime(post.body)} min</span>
|
||||||
</a>
|
)}
|
||||||
</article>
|
</a>
|
||||||
))}
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Layout column */
|
/* ── Hero: full-width two-column ────────────────────────────── */
|
||||||
.hero,
|
|
||||||
.featured,
|
|
||||||
.series,
|
|
||||||
.all-posts {
|
|
||||||
max-width: var(--width-prose);
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dividers */
|
|
||||||
.divider {
|
|
||||||
max-width: var(--width-prose);
|
|
||||||
margin: var(--space-6) auto;
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid var(--border-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hero */
|
|
||||||
.hero {
|
.hero {
|
||||||
margin-top: var(--space-10);
|
max-width: 100%;
|
||||||
margin-bottom: var(--space-6);
|
display: grid;
|
||||||
text-align: center;
|
grid-template-columns: 1fr 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-6);
|
||||||
|
padding: var(--space-10) var(--space-8);
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero .site-title {
|
.hero-text {
|
||||||
|
max-width: 540px;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-title {
|
||||||
font-family: var(--font-structure);
|
font-family: var(--font-structure);
|
||||||
font-size: var(--type-3xl);
|
font-size: var(--type-3xl);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -134,15 +237,16 @@ const rageSeries = [
|
|||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero .tagline {
|
.tagline {
|
||||||
font-family: var(--font-prose);
|
font-family: var(--font-prose);
|
||||||
font-size: var(--type-lg);
|
font-size: var(--type-lg);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
line-height: var(--leading-prose);
|
line-height: var(--leading-prose);
|
||||||
margin-bottom: var(--space-1);
|
font-style: italic;
|
||||||
|
margin-bottom: var(--space-1-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero .byline {
|
.byline {
|
||||||
font-family: var(--font-structure);
|
font-family: var(--font-structure);
|
||||||
font-size: var(--type-sm);
|
font-size: var(--type-sm);
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
@@ -150,11 +254,37 @@ const rageSeries = [
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Featured excerpt */
|
.hero-illustration {
|
||||||
|
max-width: 400px;
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-illustration svg {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: var(--rounded-lg);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Featured excerpt: full-width with terracotta border ────── */
|
||||||
.featured {
|
.featured {
|
||||||
|
max-width: var(--width-container);
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding: var(--space-8) 0;
|
||||||
|
border-left: 4px solid var(--accent-primary);
|
||||||
|
margin-top: var(--space-6);
|
||||||
margin-bottom: var(--space-6);
|
margin-bottom: var(--space-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.featured-inner {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: var(--space-6);
|
||||||
|
align-items: center;
|
||||||
|
padding-left: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
.featured-label {
|
.featured-label {
|
||||||
font-family: var(--font-structure);
|
font-family: var(--font-structure);
|
||||||
font-size: var(--type-xs);
|
font-size: var(--type-xs);
|
||||||
@@ -170,8 +300,6 @@ const rageSeries = [
|
|||||||
font-size: var(--type-lg);
|
font-size: var(--type-lg);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
line-height: var(--leading-prose);
|
line-height: var(--leading-prose);
|
||||||
border-left: 3px solid var(--accent-primary);
|
|
||||||
padding-left: var(--space-3);
|
|
||||||
margin-bottom: var(--space-3);
|
margin-bottom: var(--space-3);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
@@ -182,9 +310,20 @@ const rageSeries = [
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Series */
|
.featured-diagram svg {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: var(--rounded-md);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Series: container-width 2-column grid ─────────────────── */
|
||||||
.series {
|
.series {
|
||||||
margin-bottom: var(--space-6);
|
max-width: var(--width-container);
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-bottom: var(--space-8);
|
||||||
|
padding: var(--space-6) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-heading {
|
.section-heading {
|
||||||
@@ -202,100 +341,106 @@ const rageSeries = [
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
line-height: var(--leading-prose);
|
line-height: var(--leading-prose);
|
||||||
margin-bottom: var(--space-4);
|
margin-bottom: var(--space-4);
|
||||||
|
max-width: var(--width-prose);
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-list {
|
.series-grid {
|
||||||
list-style: none;
|
display: grid;
|
||||||
counter-reset: series;
|
grid-template-columns: 1fr 1fr;
|
||||||
display: flex;
|
gap: var(--space-3);
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-1-5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-item {
|
.series-card {
|
||||||
counter-increment: series;
|
background: var(--surface-card);
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: baseline;
|
|
||||||
padding: var(--space-1) var(--space-1-5);
|
|
||||||
border-radius: var(--rounded-md);
|
|
||||||
background-color: var(--surface-card);
|
|
||||||
border: 1px solid var(--border-subtle);
|
border: 1px solid var(--border-subtle);
|
||||||
}
|
border-radius: var(--rounded-lg);
|
||||||
|
padding: var(--space-3);
|
||||||
.series-item::before {
|
box-shadow: var(--shadow-sm);
|
||||||
content: counter(series) ".";
|
|
||||||
font-family: var(--font-evidence);
|
|
||||||
font-size: var(--type-sm);
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
margin-right: var(--space-1-5);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.series-item-title {
|
|
||||||
font-family: var(--font-structure);
|
|
||||||
font-size: var(--type-sm);
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-primary);
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.series-item-status {
|
|
||||||
font-family: var(--font-evidence);
|
|
||||||
font-size: var(--type-xs);
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-left: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.series-item-link {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
gap: var(--space-2);
|
||||||
align-items: baseline;
|
align-items: flex-start;
|
||||||
width: 100%;
|
transition: box-shadow var(--duration-normal) var(--ease-default);
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-item-link:hover {
|
.series-card:hover {
|
||||||
text-decoration: none;
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-item-link:hover .series-item-title {
|
.series-card.published {
|
||||||
color: var(--accent-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.series-item-meta {
|
|
||||||
font-family: var(--font-evidence);
|
|
||||||
font-size: var(--type-xs);
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-left: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.series-item.published {
|
|
||||||
border-color: var(--border-default);
|
border-color: var(--border-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
.series-item.published .series-item-title {
|
.card-number {
|
||||||
|
font-family: var(--font-evidence);
|
||||||
|
font-size: var(--type-xl);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-primary);
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-0-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-family: var(--font-structure);
|
||||||
|
font-size: var(--type-base);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: var(--leading-snug);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-description {
|
||||||
|
font-family: var(--font-prose);
|
||||||
|
font-size: var(--type-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: var(--leading-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-link {
|
||||||
|
font-family: var(--font-structure);
|
||||||
|
font-size: var(--type-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: var(--space-0-5);
|
||||||
transition: color var(--duration-fast) var(--ease-default);
|
transition: color var(--duration-fast) var(--ease-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* All posts */
|
.card-status {
|
||||||
|
font-family: var(--font-evidence);
|
||||||
|
font-size: var(--type-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin-top: var(--space-0-5);
|
||||||
|
background: var(--surface-inset);
|
||||||
|
padding: var(--space-0-5) var(--space-1);
|
||||||
|
border-radius: var(--rounded-full);
|
||||||
|
display: inline-block;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── All posts: reading-width 2-column grid ────────────────── */
|
||||||
.all-posts {
|
.all-posts {
|
||||||
|
max-width: var(--width-prose);
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
margin-bottom: var(--space-12);
|
margin-bottom: var(--space-12);
|
||||||
|
padding: var(--space-6) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.all-posts .section-heading {
|
.all-posts .section-heading {
|
||||||
margin-bottom: var(--space-3);
|
margin-bottom: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-entry {
|
.posts-grid {
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0 var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-entry:last-child {
|
.post-entry {
|
||||||
border-bottom: none;
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-entry-link {
|
.post-entry-link {
|
||||||
@@ -341,4 +486,66 @@ const rageSeries = [
|
|||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Responsive: tablet ────────────────────────────────────── */
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
.hero {
|
||||||
|
padding: var(--space-8) var(--space-4);
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-text {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-illustration {
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-inner {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Responsive: mobile ────────────────────────────────────── */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-6) var(--space-3);
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-text {
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-illustration {
|
||||||
|
justify-self: center;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured {
|
||||||
|
border-left: none;
|
||||||
|
border-top: 4px solid var(--accent-primary);
|
||||||
|
padding: var(--space-4) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-inner {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+112
-16
@@ -63,6 +63,24 @@ describe('Index page', () => {
|
|||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /class="byline"/, 'should have a byline element');
|
assert.match(content, /class="byline"/, 'should have a byline element');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should contain an inline SVG hero illustration', () => {
|
||||||
|
const content = readPage();
|
||||||
|
// The hero section should contain an SVG element
|
||||||
|
assert.match(content, /class="hero-illustration"/, 'should have a hero illustration');
|
||||||
|
assert.match(content, /<svg[^>]*viewBox/, 'should have an inline SVG with viewBox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use CSS grid for hero layout', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /\.hero\s*\{[^}]*display:\s*grid/s, 'hero should use CSS grid');
|
||||||
|
assert.match(content, /\.hero\s*\{[^}]*grid-template-columns/s, 'hero should define grid-template-columns');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be full viewport width', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /\.hero\s*\{[^}]*max-width:\s*100%/s, 'hero should be full viewport width');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Featured excerpt section', () => {
|
describe('Featured excerpt section', () => {
|
||||||
@@ -78,12 +96,22 @@ describe('Index page', () => {
|
|||||||
|
|
||||||
it('should have a link to the full post', () => {
|
it('should have a link to the full post', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
// The featured section should contain a link to read the full post
|
|
||||||
assert.match(content, /class="featured-link"/, 'should have a featured-link to the full post');
|
assert.match(content, /class="featured-link"/, 'should have a featured-link to the full post');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should have a terracotta left border on the featured section', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /\.featured[\s\S]*?border-left.*accent-primary/s, 'should have terracotta left border');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include an inline SVG diagram from a post', () => {
|
||||||
|
const content = readPage();
|
||||||
|
// The featured section should contain a small SVG from one of the posts
|
||||||
|
assert.match(content, /class="featured-diagram"/, 'should have a featured-diagram element');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Series section', () => {
|
describe('Series section - multi-column grid', () => {
|
||||||
it('should have a series section', () => {
|
it('should have a series section', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /class="series"/, 'should have a section with class series');
|
assert.match(content, /class="series"/, 'should have a section with class series');
|
||||||
@@ -94,33 +122,77 @@ describe('Index page', () => {
|
|||||||
assert.match(content, /Rage/, 'should mention Rage in the series section');
|
assert.match(content, /Rage/, 'should mention Rage in the series section');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should list planned posts', () => {
|
it('should use a CSS grid for series cards', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /class="series-item"/, 'should have series items');
|
assert.match(content, /\.series-grid\s*\{[^}]*display:\s*grid/s, 'should use CSS grid for series cards');
|
||||||
|
assert.match(content, /\.series-grid\s*\{[^}]*grid-template-columns:\s*1fr\s+1fr/s, 'should have 2-column grid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have series card elements', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /series-card/, 'should have series-card elements');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show card numbers in mono font with accent color', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /class="card-number"/, 'should have card-number elements');
|
||||||
|
assert.match(content, /\.card-number\s*\{[^}]*font-family:\s*var\(--font-evidence\)/s, 'card number should use mono font');
|
||||||
|
assert.match(content, /\.card-number\s*\{[^}]*color:\s*var\(--accent-primary\)/s, 'card number should use accent color');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show card descriptions', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /class="card-description"/, 'should have card-description elements');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should indicate coming soon items', () => {
|
it('should indicate coming soon items', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /coming soon/i, 'should indicate coming soon posts');
|
assert.match(content, /coming soon/i, 'should indicate coming soon posts');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use --width-container for series section width', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /\.series\s*\{[^}]*max-width:\s*var\(--width-container\)/s, 'series should use container width');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should collapse to single column on mobile', () => {
|
||||||
|
const content = readPage();
|
||||||
|
// Check for media query that changes grid to 1 column
|
||||||
|
assert.match(content, /@media.*768px[\s\S]*?grid-template-columns:\s*1fr\s*[;}]/s, 'should collapse to 1 column on mobile');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have card surface background', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /\.series-card\s*\{[^}]*background[^:]*:\s*var\(--surface-card\)/s, 'cards should have card surface bg');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('All posts section', () => {
|
describe('All posts section - 2-column grid', () => {
|
||||||
it('should have a posts listing section below the pitch', () => {
|
it('should have a posts listing section', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /class="all-posts"/, 'should have a section with class all-posts');
|
assert.match(content, /class="all-posts"/, 'should have a section with class all-posts');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should map posts to compact entries', () => {
|
it('should map posts to entries', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /sortedPosts\.map/, 'should map sortedPosts');
|
assert.match(content, /sortedPosts\.map/, 'should map sortedPosts');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show post titles as links', () => {
|
it('should show post titles as links', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
// Each post entry should link to its page
|
|
||||||
assert.match(content, /posts\/\$\{post\.id\}/, 'should link to post pages');
|
assert.match(content, /posts\/\$\{post\.id\}/, 'should link to post pages');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use --width-prose for all-posts width', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /\.all-posts\s*\{[^}]*max-width:\s*var\(--width-prose\)/s, 'all-posts should use prose width');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use CSS grid for posts on desktop', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /\.posts-grid\s*\{[^}]*display:\s*grid/s, 'should use CSS grid for posts');
|
||||||
|
assert.match(content, /\.posts-grid\s*\{[^}]*grid-template-columns:\s*1fr\s+1fr/s, 'should have 2-column posts grid');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Layout', () => {
|
describe('Layout', () => {
|
||||||
@@ -136,25 +208,49 @@ describe('Index page', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Style', () => {
|
describe('Style', () => {
|
||||||
it('should use --width-prose for content width', () => {
|
|
||||||
const content = readPage();
|
|
||||||
assert.match(content, /max-width\s*:\s*var\(--width-prose\)/, 'should constrain content to prose width');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use --font-structure for hero heading', () => {
|
it('should use --font-structure for hero heading', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /\.hero[\s\S]*?font-family\s*:\s*var\(--font-structure\)/, 'should use structure font in hero');
|
assert.match(content, /\.hero[\s\S]*?font-family:\s*var\(--font-structure\)/, 'should use structure font in hero');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use --font-prose for excerpt', () => {
|
it('should use --font-prose for excerpt', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
assert.match(content, /\.excerpt[\s\S]*?font-family\s*:\s*var\(--font-prose\)/, 'should use prose font for excerpt');
|
assert.match(content, /\.excerpt[\s\S]*?font-family:\s*var\(--font-prose\)/, 'should use prose font for excerpt');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not have the old .post-list as the main layout', () => {
|
it('should not have the old .post-list as the main layout', () => {
|
||||||
const content = readPage();
|
const content = readPage();
|
||||||
// The old flat list structure should be gone
|
|
||||||
assert.doesNotMatch(content, /<ArticleCard/, 'should not use ArticleCard component');
|
assert.doesNotMatch(content, /<ArticleCard/, 'should not use ArticleCard component');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use different widths for different sections', () => {
|
||||||
|
const content = readPage();
|
||||||
|
// Hero: full width (100%)
|
||||||
|
assert.match(content, /\.hero[\s\S]*?max-width:\s*100%/s, 'hero should be full width');
|
||||||
|
// Series: container width
|
||||||
|
assert.match(content, /\.series[\s\S]*?max-width:\s*var\(--width-container\)/s, 'series should use container width');
|
||||||
|
// All posts: prose width
|
||||||
|
assert.match(content, /\.all-posts[\s\S]*?max-width:\s*var\(--width-prose\)/s, 'all-posts should use prose width');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Rage series data', () => {
|
||||||
|
it('should have descriptions for each series item', () => {
|
||||||
|
const content = readPage();
|
||||||
|
// Check for at least a couple of the descriptions
|
||||||
|
assert.match(content, /silent stale cache/i, 'should have description for post 1');
|
||||||
|
assert.match(content, /Weak fingerprints find candidates/i, 'should have description for post 2');
|
||||||
|
assert.match(content, /content-addressed storage/i, 'should have description for post 5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include all 6 rage series items', () => {
|
||||||
|
const content = readPage();
|
||||||
|
assert.match(content, /Your Build Cache Is Lying to You/, 'should have post 1');
|
||||||
|
assert.match(content, /Two-Phase Fingerprinting/, 'should have post 2');
|
||||||
|
assert.match(content, /Hooking Syscalls/, 'should have post 3');
|
||||||
|
assert.match(content, /TypeScript 7/, 'should have post 4');
|
||||||
|
assert.match(content, /node_modules/, 'should have post 5');
|
||||||
|
assert.match(content, /Maintaining Lage/, 'should have post 6');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user