Refactored ArticleHeader
This commit is contained in:
@@ -11,8 +11,6 @@ type Props = {
|
||||
size?: FigureSize
|
||||
effect?: ImageEffect
|
||||
ratio?: AspectRatio
|
||||
eager?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
const LAYOUT: Record<FigureSize, { widths: number[]; sizes: string }> = {
|
||||
@@ -25,11 +23,9 @@ const {
|
||||
alt,
|
||||
caption,
|
||||
credit,
|
||||
eager,
|
||||
size = 'inline',
|
||||
effect = 'none',
|
||||
ratio = 'original',
|
||||
class: className,
|
||||
} = Astro.props
|
||||
---
|
||||
|
||||
@@ -39,10 +35,9 @@ const {
|
||||
`figure--${size}`,
|
||||
effect !== 'none' && `effect--${effect}`,
|
||||
ratio !== 'original' && `ratio--${ratio}`,
|
||||
className,
|
||||
]}
|
||||
>
|
||||
<ContentImage src={image} alt={alt} eager={eager} {...LAYOUT[size]} />
|
||||
<ContentImage src={image} alt={alt} {...LAYOUT[size]} />
|
||||
{(caption || credit) && (
|
||||
<figcaption class:list={['figure__caption', size !== 'inline' && 'dark']}>
|
||||
{caption && <span class="figure__text">{caption}</span>}
|
||||
|
||||
@@ -18,13 +18,15 @@ const crumbs = trailOf(article)
|
||||
const published = article.published
|
||||
---
|
||||
|
||||
<header>
|
||||
<header class="article-header">
|
||||
<Overline
|
||||
crumbs={crumbs}
|
||||
stamp={published ? toMilitaryDTG(published) : 'No publication date set'}
|
||||
datetime={published?.toISOString()}
|
||||
/>
|
||||
<Title title={article.title} subtitle={article.subtitle} />
|
||||
<Cover cover={article.summary.cover} />
|
||||
<Meta article={article} />
|
||||
<div class="article-header__panel">
|
||||
<Title title={article.title} subtitle={article.subtitle} />
|
||||
<Meta article={article} />
|
||||
<Cover cover={article.summary.cover} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
---
|
||||
import Figure from '@/components/media/Figure.astro'
|
||||
import ContentImage from '@/components/media/ContentImage.astro'
|
||||
import type { Article } from '@/lib/site/types'
|
||||
|
||||
type Props = { cover: Article['summary']['cover'] }
|
||||
type Props = {
|
||||
cover: Article['summary']['cover']
|
||||
}
|
||||
|
||||
const { image, ...rest } = Astro.props.cover
|
||||
const { image, alt, caption, credit, effect, ratio } = Astro.props.cover
|
||||
---
|
||||
|
||||
{image && <Figure image={image} {...rest} size="wide" eager />}
|
||||
{image && (
|
||||
<figure
|
||||
class:list={[
|
||||
'article-header__cover',
|
||||
effect !== 'none' && `effect--${effect}`,
|
||||
ratio !== 'original' && `ratio--${ratio}`,
|
||||
]}
|
||||
>
|
||||
<ContentImage
|
||||
src={image}
|
||||
alt={alt}
|
||||
widths={[800, 1600, 2400]}
|
||||
sizes="(min-width: 80rem) 74rem, 100vw"
|
||||
eager
|
||||
/>
|
||||
{(caption || credit) && (
|
||||
<figcaption class="article-header__caption dark">
|
||||
{caption && <span>{caption}</span>}
|
||||
{credit && <span class="article-header__credit">{credit}</span>}
|
||||
</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
import StageBadge from '@/components/post/Header/StageBadge.astro'
|
||||
import { toMilitaryDTG } from '@/lib/content/dates'
|
||||
import { getSite } from '@/lib/site'
|
||||
import type { Article } from '@/lib/site/types'
|
||||
@@ -11,40 +12,62 @@ const { article } = Astro.props
|
||||
const site = await getSite()
|
||||
const tags = article.tags.map((id) => site.tags.get(id)).filter((tag) => tag !== undefined)
|
||||
const date = article.updated ?? article.published
|
||||
const label = article.updated ? 'Updated' : 'Published'
|
||||
const machine = date?.toISOString()
|
||||
const display = date && toMilitaryDTG(date)
|
||||
const dateLabel = article.updated ? 'Updated' : 'Published'
|
||||
---
|
||||
|
||||
<dl class="meta">
|
||||
<div class="section">
|
||||
<dt class="label">Author</dt>
|
||||
<dd class="item">Dave Damage</dd>
|
||||
<dl class="article-header__meta">
|
||||
<div class="article-header__cell">
|
||||
<dt class="article-header__label">
|
||||
<span class="article-header__index" aria-hidden="true">
|
||||
02
|
||||
</span>
|
||||
State
|
||||
</dt>
|
||||
<dd>
|
||||
<StageBadge stage={article.stage} />
|
||||
</dd>
|
||||
</div>
|
||||
{tags.length > 0 && (
|
||||
<div class="section">
|
||||
<dt class="label">Tags</dt>
|
||||
<dd>
|
||||
<ul class="tags">
|
||||
<div class="article-header__cell">
|
||||
<dt class="article-header__label">
|
||||
<span class="article-header__index" aria-hidden="true">
|
||||
03
|
||||
</span>
|
||||
{dateLabel}
|
||||
</dt>
|
||||
<dd class="article-header__value">
|
||||
{date ? <time datetime={date.toISOString()}>{toMilitaryDTG(date)}</time> : 'Undated'}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="article-header__cell">
|
||||
<dt class="article-header__label">
|
||||
<span class="article-header__index" aria-hidden="true">
|
||||
04
|
||||
</span>
|
||||
Author
|
||||
</dt>
|
||||
<dd class="article-header__value">Dave Damage</dd>
|
||||
</div>
|
||||
<div class="article-header__cell">
|
||||
<dt class="article-header__label">
|
||||
<span class="article-header__index" aria-hidden="true">
|
||||
05
|
||||
</span>
|
||||
Tags
|
||||
</dt>
|
||||
<dd class="article-header__value">
|
||||
{tags.length > 0 ? (
|
||||
<ul class="article-header__tags">
|
||||
{tags.map((tag) => (
|
||||
<li class="tag">
|
||||
<a href={tag.path} rel="tag">
|
||||
<li>
|
||||
<a class="article-header__tag" href={tag.path} rel="tag">
|
||||
{tag.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{display && (
|
||||
<div class="section">
|
||||
<dt class="label">{label}</dt>
|
||||
<dd>
|
||||
<time class="item" datetime={machine}>
|
||||
{display}
|
||||
</time>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
'None'
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
13
src/components/post/Header/StageBadge.astro
Normal file
13
src/components/post/Header/StageBadge.astro
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
import { STAGE_OPTIONS, type Stage } from '@/lib/constants/stage'
|
||||
|
||||
type props = {
|
||||
stage: Stage
|
||||
}
|
||||
|
||||
const { stage } = Astro.props
|
||||
|
||||
const label = STAGE_OPTIONS.find((option) => option.value === stage)?.label ?? stage
|
||||
---
|
||||
|
||||
<span class:list={['stage-badge', `stage-badge--${stage}`]}>{label}</span>
|
||||
@@ -7,7 +7,11 @@ type Props = {
|
||||
const { title, subtitle } = Astro.props
|
||||
---
|
||||
|
||||
<div class="wrapper content">
|
||||
<h1 class="title">{title}</h1>
|
||||
{subtitle && <p class="subtitle">{subtitle}</p>}
|
||||
<div class="article-header__designation">
|
||||
<p class="article-header__label" aria-hidden="true">
|
||||
<span class="article-header__index">01</span>
|
||||
Designation
|
||||
</p>
|
||||
<h1 class="article-header__title">{title}</h1>
|
||||
{subtitle && <p class="article-header__subtitle">{subtitle}</p>}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { collection, fields } from '@keystatic/core'
|
||||
|
||||
import { base } from '@/keystatic/fields/content/base'
|
||||
import { base, stage } from '@/keystatic/fields/content/base'
|
||||
import { content } from '@/keystatic/fields/content/content'
|
||||
import { dates } from '@/keystatic/fields/content/dates'
|
||||
import { articleRoute } from '@/keystatic/fields/content/route'
|
||||
@@ -15,9 +15,10 @@ export const articles = collection({
|
||||
contentField: 'content',
|
||||
},
|
||||
entryLayout: 'content',
|
||||
columns: ['title', 'status'],
|
||||
columns: ['title', 'status', 'stage'],
|
||||
schema: {
|
||||
...base,
|
||||
stage,
|
||||
route: articleRoute,
|
||||
summary: articleSummary(ARTICLES),
|
||||
dates,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
|
||||
import { STAGE_OPTIONS } from '@/lib/constants/stage'
|
||||
import { STATUS_OPTIONS } from '@/lib/constants/status'
|
||||
import { SEGMENT, SEGMENT_MESSAGE, slugify } from '@/lib/content/slug'
|
||||
|
||||
@@ -33,6 +34,13 @@ export const status = fields.select({
|
||||
defaultValue: 'draft',
|
||||
})
|
||||
|
||||
export const stage = fields.select({
|
||||
label: 'Stage',
|
||||
description: 'How finished the text is. Separate from status, which controls visibility',
|
||||
options: STAGE_OPTIONS,
|
||||
defaultValue: 'stub',
|
||||
})
|
||||
|
||||
export const noindex = fields.checkbox({
|
||||
label: 'Hide from search engines',
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ const meta = metaOf(article)
|
||||
|
||||
<BaseLayout {...meta}>
|
||||
<article class="article">
|
||||
<!--<ArticleHeader article={article} />-->
|
||||
<ArticleHeader article={article} />
|
||||
<div class="frame">
|
||||
<div class="rail">
|
||||
<ToC headings={rendered.headings} />
|
||||
|
||||
20
src/lib/constants/stage.ts
Normal file
20
src/lib/constants/stage.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const STAGE_OPTIONS = [
|
||||
{
|
||||
label: 'Stub',
|
||||
value: 'stub',
|
||||
},
|
||||
{
|
||||
label: 'Developing',
|
||||
value: 'developing',
|
||||
},
|
||||
{
|
||||
label: 'Complete',
|
||||
value: 'complete',
|
||||
},
|
||||
{
|
||||
label: 'Ongoing',
|
||||
value: 'ongoing',
|
||||
},
|
||||
] as const
|
||||
|
||||
export type Stage = (typeof STAGE_OPTIONS)[number]['value']
|
||||
@@ -35,7 +35,7 @@ export async function render(
|
||||
|
||||
for (const { lines, error } of Markdoc.validate(node, config)) {
|
||||
const level = LEVEL[error.level as keyof typeof LEVEL]
|
||||
if (level) problems[level](collection, id, `Text line ${lines[0] + 1}: ${error.message}`)
|
||||
if (level) problems[level](collection, id, `Content line ${lines[0] + 1}: ${error.message}`)
|
||||
}
|
||||
const tree = Markdoc.transform(node, config)
|
||||
for (const { level, message } of state.problems) problems[level](collection, id, message)
|
||||
|
||||
11
src/styles/components/blocks/index.css
Normal file
11
src/styles/components/blocks/index.css
Normal file
@@ -0,0 +1,11 @@
|
||||
@import url("quote.css");
|
||||
@import url("lead.css");
|
||||
@import url("callout.css");
|
||||
@import url("reveal.css");
|
||||
@import url("table-wrapper.css");
|
||||
@import url("footnote.css");
|
||||
@import url("sidenote.css");
|
||||
@import url("figure.css");
|
||||
@import url("code.css");
|
||||
@import url("terms.css");
|
||||
@import url("spoiler.css");
|
||||
@@ -61,19 +61,19 @@
|
||||
.quote__source {
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media (--bp-desktop) {
|
||||
.quote {
|
||||
@mixin me calc(-1 * (var(--layout-gutter) + var(--layout-margin-width)));
|
||||
@media (--bp-desktop) {
|
||||
.quote {
|
||||
@mixin me calc(-1 * (var(--layout-gutter) + var(--layout-margin-width)));
|
||||
|
||||
grid-template-columns: minmax(0, 1fr) var(--layout-margin-width);
|
||||
column-gap: var(--layout-gutter);
|
||||
}
|
||||
grid-template-columns: minmax(0, 1fr) var(--layout-margin-width);
|
||||
column-gap: var(--layout-gutter);
|
||||
}
|
||||
|
||||
.quote__citation {
|
||||
flex-direction: column;
|
||||
gap: var(--size-2);
|
||||
align-self: end;
|
||||
.quote__citation {
|
||||
flex-direction: column;
|
||||
gap: var(--size-2);
|
||||
align-self: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,3 @@
|
||||
@import url("prose.css");
|
||||
@import url("quote.css");
|
||||
@import url("lead.css");
|
||||
@import url("callout.css");
|
||||
@import url("reveal.css");
|
||||
@import url("table-wrapper.css");
|
||||
@import url("footnote.css");
|
||||
@import url("sidenote.css");
|
||||
@import url("figure.css");
|
||||
@import url("code.css");
|
||||
@import url("terms.css");
|
||||
@import url("spoiler.css");
|
||||
@import url("prose/index.css");
|
||||
@import url("blocks/index.css");
|
||||
@import url("post/index.css");
|
||||
|
||||
171
src/styles/components/post/article-header.css
Normal file
171
src/styles/components/post/article-header.css
Normal file
@@ -0,0 +1,171 @@
|
||||
@layer components.blocks {
|
||||
.article-header__panel {
|
||||
@mixin mx auto;
|
||||
@mixin mt var(--size-10);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: var(--border-card);
|
||||
|
||||
width: min(100% - 2 * var(--layout-page-margin), var(--layout-wrapper-width));
|
||||
border: var(--border-card) solid var(--color-border-strong);
|
||||
|
||||
background-color: var(--color-border-strong);
|
||||
|
||||
@media (--bp-desktop) {
|
||||
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
|
||||
}
|
||||
}
|
||||
|
||||
.article-header__designation {
|
||||
@mixin pa var(--size-9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-10);
|
||||
background-color: var(--color-surface-elevated-1);
|
||||
}
|
||||
|
||||
.article-header__label {
|
||||
@mixin typo_eyebrow;
|
||||
@mixin ma var(--size-0);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.article-header__index {
|
||||
&::after {
|
||||
content: "⟫" / "";
|
||||
margin-inline: var(--size-2);
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
}
|
||||
|
||||
.article-header__title {
|
||||
@mixin mt auto;
|
||||
@mixin mb var(--size-0);
|
||||
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(var(--type-9), 5vw, var(--size-14));
|
||||
font-weight: var(--font-weight-display);
|
||||
line-height: 1;
|
||||
color: var(--color-text-primary);
|
||||
letter-spacing: var(--tracking-display);
|
||||
}
|
||||
|
||||
.article-header__subtitle {
|
||||
@mixin ma var(--size-0);
|
||||
|
||||
font-family: var(--font-header);
|
||||
font-size: var(--type-1);
|
||||
font-weight: var(--font-weight-body);
|
||||
line-height: var(--leading-1);
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking-subtitle);
|
||||
}
|
||||
|
||||
.article-header__meta {
|
||||
@mixin ma var(--size-0);
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--border-card);
|
||||
}
|
||||
|
||||
.article-header__cell {
|
||||
@mixin pa var(--size-9);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-8);
|
||||
background-color: var(--color-surface-elevated-1);
|
||||
|
||||
& dd {
|
||||
@mixin ma var(--size-0);
|
||||
}
|
||||
}
|
||||
|
||||
.article-header__value {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--type-0);
|
||||
line-height: var(--leading-0);
|
||||
color: var(--color-text-primary);
|
||||
text-transform: uppercase;
|
||||
|
||||
& time {
|
||||
font-weight: var(--font-weight-emphasis);
|
||||
}
|
||||
}
|
||||
|
||||
.article-header__tags {
|
||||
@mixin ma var(--size-0);
|
||||
@mixin pa var(--size-0);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-2);
|
||||
align-items: flex-start;
|
||||
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.article-header__tag {
|
||||
@mixin py var(--size-1);
|
||||
@mixin px var(--size-3);
|
||||
|
||||
display: block;
|
||||
|
||||
font-size: var(--type--2);
|
||||
font-weight: var(--font-weight-emphasis);
|
||||
line-height: var(--leading--2);
|
||||
color: var(--color-text-inverse);
|
||||
letter-spacing: var(--tracking-tag);
|
||||
|
||||
background-color: var(--color-surface-inverse);
|
||||
|
||||
transition: background-color var(--motion-fast) var(--ease-out);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-on-accent);
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@mixin focus-ring;
|
||||
}
|
||||
}
|
||||
|
||||
.article-header__cover {
|
||||
@mixin ma var(--size-0);
|
||||
|
||||
grid-column: 1 / -1;
|
||||
|
||||
& img {
|
||||
display: block;
|
||||
|
||||
aspect-ratio: var(--figure-ratio, auto);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.article-header__caption {
|
||||
@mixin typo_code;
|
||||
@mixin py var(--size-4);
|
||||
@mixin px var(--size-9);
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--size-2) var(--size-6);
|
||||
justify-content: center;
|
||||
|
||||
color: var(--color-text-primary);
|
||||
text-align: center;
|
||||
|
||||
background-color: var(--color-surface-base);
|
||||
}
|
||||
|
||||
.article-header__credit {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
2
src/styles/components/post/index.css
Normal file
2
src/styles/components/post/index.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@import url("article-header.css");
|
||||
@import url("stage-badge.css");
|
||||
52
src/styles/components/post/stage-badge.css
Normal file
52
src/styles/components/post/stage-badge.css
Normal file
@@ -0,0 +1,52 @@
|
||||
@layer components.blocks {
|
||||
.stage-badge {
|
||||
display: inline-flex;
|
||||
gap: var(--size-3);
|
||||
align-items: baseline;
|
||||
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--type--1);
|
||||
line-height: 1;
|
||||
color: var(--color-text-primary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking-tag);
|
||||
}
|
||||
|
||||
.stage-badge--stub,
|
||||
.stage-badge--developing {
|
||||
@mixin py var(--size-2);
|
||||
@mixin px var(--size-3);
|
||||
|
||||
color: var(--color-text-on-accent);
|
||||
background-color: var(--color-tertiary);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
width: 1cap;
|
||||
height: 1cap;
|
||||
background-color: currentcolor;
|
||||
}
|
||||
}
|
||||
|
||||
.stage-badge--stub {
|
||||
&::before {
|
||||
clip-path: polygon(
|
||||
evenodd,
|
||||
50% 0,
|
||||
100% 100%,
|
||||
0 100%,
|
||||
50% 0,
|
||||
50% 36%,
|
||||
21% 86%,
|
||||
79% 86%,
|
||||
50% 36%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.stage-badge--developing {
|
||||
&::before {
|
||||
clip-path: polygon(50% 0, 100% 100%, 0 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/styles/components/prose/index.css
Normal file
1
src/styles/components/prose/index.css
Normal file
@@ -0,0 +1 @@
|
||||
@import url("prose.css");
|
||||
@@ -99,3 +99,12 @@
|
||||
line-height: var(--leading-prose-tight);
|
||||
letter-spacing: var(--tracking--1);
|
||||
}
|
||||
|
||||
@define-mixin typo_eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--type--3);
|
||||
font-weight: var(--font-weight-body);
|
||||
line-height: var(--leading--3);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking-eyebrow);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user