feat: add PostHeader component with badge, title, meta, and responsive styles
Built the PostHeader.astro component per the design spec. It accepts props {title, date, type, readingTime}, formats dates with toLocaleDateString, renders a centered header with content type badge, h1 title, and meta line with date/reading time. Has responsive typography stepping down at 719px and 479px breakpoints. All 39 tests pass and npm run build succeeds.
Generated with Amplifier
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
date: Date;
|
||||||
|
type?: string;
|
||||||
|
readingTime?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, date, type = 'post', readingTime } = Astro.props;
|
||||||
|
|
||||||
|
const formattedDate = date.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
const isoDate = date.toISOString().split('T')[0];
|
||||||
|
---
|
||||||
|
|
||||||
|
<header class="post-header">
|
||||||
|
<span class="badge">{type}</span>
|
||||||
|
<h1 class="title">{title}</h1>
|
||||||
|
<p class="meta">
|
||||||
|
<time datetime={isoDate}>{formattedDate}</time>{readingTime && ` · ${readingTime} min read`}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.post-header {
|
||||||
|
max-width: var(--width-prose);
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding-top: var(--space-8);
|
||||||
|
padding-bottom: var(--space-8);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-family: var(--font-structure);
|
||||||
|
font-size: var(--type-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent-primary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: var(--font-structure);
|
||||||
|
font-size: var(--type-3xl);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: var(--leading-tight);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
font-family: var(--font-structure);
|
||||||
|
font-size: var(--type-sm);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
line-height: var(--leading-normal);
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 719px) {
|
||||||
|
.post-header {
|
||||||
|
padding-top: var(--space-6);
|
||||||
|
padding-bottom: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: var(--type-2xl);
|
||||||
|
line-height: var(--leading-snug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.post-header {
|
||||||
|
padding-top: var(--space-5);
|
||||||
|
padding-bottom: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: var(--type-xl);
|
||||||
|
line-height: var(--leading-snug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/**
|
||||||
|
* Build-output test for PostHeader.astro
|
||||||
|
* Checks source content for expected structure, props, styles, and responsive breakpoints.
|
||||||
|
*/
|
||||||
|
import { readFileSync, existsSync } from 'fs';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
const srcPath = resolve('src/components/PostHeader.astro');
|
||||||
|
|
||||||
|
let failures = 0;
|
||||||
|
let passes = 0;
|
||||||
|
|
||||||
|
function assert(condition, label) {
|
||||||
|
if (condition) {
|
||||||
|
console.log(` ✓ ${label}`);
|
||||||
|
passes++;
|
||||||
|
} else {
|
||||||
|
console.error(` ✗ FAIL: ${label}`);
|
||||||
|
failures++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Source file existence ──
|
||||||
|
console.log('\n--- Source file checks ---');
|
||||||
|
|
||||||
|
assert(
|
||||||
|
existsSync(srcPath),
|
||||||
|
'src/components/PostHeader.astro exists'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existsSync(srcPath)) {
|
||||||
|
const src = readFileSync(srcPath, 'utf-8');
|
||||||
|
|
||||||
|
// ── Props interface ──
|
||||||
|
console.log('\n--- Props interface checks ---');
|
||||||
|
|
||||||
|
assert(
|
||||||
|
/title:\s*string/.test(src),
|
||||||
|
'Props has title: string'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
/date:\s*Date/.test(src),
|
||||||
|
'Props has date: Date'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
/type\?:\s*string/.test(src),
|
||||||
|
'Props has type?: string'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
/readingTime\?:\s*number/.test(src),
|
||||||
|
'Props has readingTime?: number'
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Default values ──
|
||||||
|
console.log('\n--- Default value checks ---');
|
||||||
|
|
||||||
|
assert(
|
||||||
|
src.includes("'post'") || src.includes('"post"'),
|
||||||
|
'type defaults to "post"'
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Date formatting ──
|
||||||
|
console.log('\n--- Date formatting checks ---');
|
||||||
|
|
||||||
|
assert(
|
||||||
|
src.includes("toLocaleDateString('en-US'") || src.includes('toLocaleDateString("en-US"'),
|
||||||
|
'Uses toLocaleDateString with en-US locale'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
src.includes("'numeric'") || src.includes('"numeric"'),
|
||||||
|
'Date format includes numeric option'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
src.includes("'long'") || src.includes('"long"'),
|
||||||
|
'Date format includes long month'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
src.includes("toISOString()"),
|
||||||
|
'Computes ISO date string'
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── HTML structure ──
|
||||||
|
console.log('\n--- HTML structure checks ---');
|
||||||
|
|
||||||
|
assert(
|
||||||
|
src.includes('post-header'),
|
||||||
|
'Has .post-header class'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
/<header[\s>]/.test(src),
|
||||||
|
'Uses <header> element'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
src.includes('badge'),
|
||||||
|
'Has .badge class'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
/<span[^>]*class.*badge/.test(src) || /<span[^>]*badge/.test(src),
|
||||||
|
'Badge uses <span> element'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
/<h1/.test(src),
|
||||||
|
'Has <h1> element for title'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
src.includes('title') && src.includes('class'),
|
||||||
|
'Has .title class'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
/<p[^>]*class.*meta/.test(src) || src.includes("class=\"meta\"") || src.includes("class='meta'"),
|
||||||
|
'Meta uses <p> element with .meta class'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
/<time[^>]*datetime/.test(src),
|
||||||
|
'Has <time> element with datetime attribute'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
src.includes('min read'),
|
||||||
|
'Has reading time display text'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
src.includes('readingTime'),
|
||||||
|
'Conditionally shows readingTime'
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Styles ──
|
||||||
|
console.log('\n--- Style checks ---');
|
||||||
|
|
||||||
|
assert(src.includes('--width-prose'), 'Uses prose max-width');
|
||||||
|
assert(src.includes('--font-structure'), 'Uses structure font');
|
||||||
|
assert(src.includes('--type-xs'), 'Badge uses type-xs');
|
||||||
|
assert(src.includes('--type-3xl'), 'Title uses type-3xl');
|
||||||
|
assert(src.includes('--type-sm'), 'Meta uses type-sm');
|
||||||
|
assert(src.includes('--accent-primary'), 'Badge uses accent-primary');
|
||||||
|
assert(src.includes('--text-primary'), 'Title uses text-primary');
|
||||||
|
assert(src.includes('--text-tertiary'), 'Meta uses text-tertiary');
|
||||||
|
assert(src.includes('--leading-tight'), 'Title uses leading-tight');
|
||||||
|
assert(src.includes('uppercase'), 'Badge is uppercase');
|
||||||
|
assert(src.includes('0.05em'), 'Badge has 0.05em tracking');
|
||||||
|
assert(src.includes('-0.02em'), 'Title has -0.02em tracking');
|
||||||
|
|
||||||
|
// ── Responsive breakpoints ──
|
||||||
|
console.log('\n--- Responsive breakpoint checks ---');
|
||||||
|
|
||||||
|
assert(src.includes('719px'), 'Has 719px breakpoint');
|
||||||
|
assert(src.includes('479px'), 'Has 479px breakpoint');
|
||||||
|
assert(src.includes('--type-2xl'), 'Uses type-2xl at tablet breakpoint');
|
||||||
|
assert(src.includes('--type-xl'), 'Uses type-xl at mobile breakpoint');
|
||||||
|
assert(src.includes('--leading-snug'), 'Uses leading-snug at smaller breakpoints');
|
||||||
|
assert(src.includes('--space-6'), 'Uses space-6 at tablet breakpoint');
|
||||||
|
assert(src.includes('--space-5'), 'Uses space-5 at mobile breakpoint');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Summary ──
|
||||||
|
console.log(`\n--- Results: ${passes} passed, ${failures} failed ---`);
|
||||||
|
|
||||||
|
if (failures > 0) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user