feat(component): LatestDispatchBanner — slim single-row card
White card with 0.5px border, 12px radius, 22px/26px padding. Three-
column grid (auto / 1fr / auto):
- Left: 30px deterministic-pigment avatar + tracked 'LATEST DISPATCH ·
{relative}' eyebrow + 11px '{first_name} · {title || team}' line.
- Middle: 19px serif title + 12px single-line ellipsis excerpt.
- Right: 11px terracotta 'READ DISPATCH' link (1px terracotta bottom
border) + 10px muted 'ALL DISPATCHES →' below it.
The whole card is one <a> targeting the dispatch slug — hover lifts it
1px and tints the surface; entire surface is the click target.
Hidden entirely when getLatestPublishedDispatches(1) returns []. The
banner doesn't render an empty-state placeholder — the /roadmap page
just starts with the route in that case.
At <768px the grid collapses to a single column and the excerpt wraps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
66b460c35f
commit
884cca85f1
1 changed files with 121 additions and 0 deletions
121
src/components/LatestDispatchBanner.astro
Normal file
121
src/components/LatestDispatchBanner.astro
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
---
|
||||||
|
import Avatar from './Avatar.astro';
|
||||||
|
import { getLatestPublishedDispatches } from '../lib/db';
|
||||||
|
import {
|
||||||
|
dispatchSlug, dispatchExcerptParas, relativeTime,
|
||||||
|
} from '../lib/format';
|
||||||
|
|
||||||
|
const [latest] = getLatestPublishedDispatches(1);
|
||||||
|
const excerpt = latest ? dispatchExcerptParas(latest).lead : '';
|
||||||
|
---
|
||||||
|
{latest && (
|
||||||
|
<a href={`/dispatches/${dispatchSlug(latest)}`} class="banner">
|
||||||
|
<div class="b-left">
|
||||||
|
<Avatar id={latest.author_id} name={latest.author_name} size={30} />
|
||||||
|
<div class="b-byline">
|
||||||
|
<span class="b-eyebrow">Latest dispatch · {relativeTime(latest.published_at ?? latest.created_at)}</span>
|
||||||
|
<span class="b-author">
|
||||||
|
{latest.author_name.split(' ')[0]} · {latest.author_title ?? 'team'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="b-mid">
|
||||||
|
<span class="b-title">{latest.title}</span>
|
||||||
|
<span class="b-excerpt">{excerpt}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="b-right">
|
||||||
|
<span class="b-read">Read dispatch</span>
|
||||||
|
<span class="b-all">All dispatches →</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.banner {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 22px 26px;
|
||||||
|
background: var(--surface-card);
|
||||||
|
border: 0.5px solid var(--surface-card-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: none;
|
||||||
|
transition: background var(--duration-fast) var(--ease-standard),
|
||||||
|
transform var(--duration-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
.banner:hover {
|
||||||
|
border-bottom: none;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
background: color-mix(in oklab, var(--surface-card) 92%, var(--background));
|
||||||
|
}
|
||||||
|
|
||||||
|
.b-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||||
|
.b-byline { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||||
|
.b-eyebrow {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: var(--tracking-wider);
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--on-surface-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.b-author {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--on-surface);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.b-mid { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
||||||
|
.b-title {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 19px;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: var(--on-surface);
|
||||||
|
}
|
||||||
|
.b-excerpt {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.b-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.b-read {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: var(--tracking-wider);
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--pigment-terracotta);
|
||||||
|
border-bottom: 1px solid var(--pigment-terracotta);
|
||||||
|
padding-bottom: 1px;
|
||||||
|
}
|
||||||
|
.b-all {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: var(--tracking-wider);
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--on-surface-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.banner {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
}
|
||||||
|
.b-excerpt { white-space: normal; }
|
||||||
|
.b-right { align-items: flex-start; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Add table
Reference in a new issue