diff --git a/src/components/media/Figure.astro b/src/components/media/Figure.astro index 6d57026..df4ad5f 100644 --- a/src/components/media/Figure.astro +++ b/src/components/media/Figure.astro @@ -11,8 +11,6 @@ type Props = { size?: FigureSize effect?: ImageEffect ratio?: AspectRatio - eager?: boolean - class?: string } const LAYOUT: Record = { @@ -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, ]} > - + {(caption || credit) && (
{caption && {caption}} diff --git a/src/components/post/Header/ArticleHeader.astro b/src/components/post/Header/ArticleHeader.astro index a7c95a2..e093fd6 100644 --- a/src/components/post/Header/ArticleHeader.astro +++ b/src/components/post/Header/ArticleHeader.astro @@ -18,13 +18,15 @@ const crumbs = trailOf(article) const published = article.published --- -
+
- - <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> diff --git a/src/components/post/Header/Cover.astro b/src/components/post/Header/Cover.astro index aa9c715..24d7b8b 100644 --- a/src/components/post/Header/Cover.astro +++ b/src/components/post/Header/Cover.astro @@ -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> +)} diff --git a/src/components/post/Header/Meta.astro b/src/components/post/Header/Meta.astro index f11ddd6..20583ba 100644 --- a/src/components/post/Header/Meta.astro +++ b/src/components/post/Header/Meta.astro @@ -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> diff --git a/src/components/post/Header/StageBadge.astro b/src/components/post/Header/StageBadge.astro new file mode 100644 index 0000000..95837fa --- /dev/null +++ b/src/components/post/Header/StageBadge.astro @@ -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> diff --git a/src/components/post/Header/Title.astro b/src/components/post/Header/Title.astro index 14b6249..27e6cc0 100644 --- a/src/components/post/Header/Title.astro +++ b/src/components/post/Header/Title.astro @@ -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> diff --git a/src/keystatic/collections/general/articles.ts b/src/keystatic/collections/general/articles.ts index 83ccc62..4e01807 100644 --- a/src/keystatic/collections/general/articles.ts +++ b/src/keystatic/collections/general/articles.ts @@ -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, diff --git a/src/keystatic/fields/content/base.ts b/src/keystatic/fields/content/base.ts index 71ea4d7..f4cd809 100644 --- a/src/keystatic/fields/content/base.ts +++ b/src/keystatic/fields/content/base.ts @@ -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', }) diff --git a/src/layouts/general/ArticleLayout.astro b/src/layouts/general/ArticleLayout.astro index 24f5b25..ae2edef 100644 --- a/src/layouts/general/ArticleLayout.astro +++ b/src/layouts/general/ArticleLayout.astro @@ -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} /> diff --git a/src/lib/constants/stage.ts b/src/lib/constants/stage.ts new file mode 100644 index 0000000..d291eec --- /dev/null +++ b/src/lib/constants/stage.ts @@ -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'] diff --git a/src/lib/markdoc/index.ts b/src/lib/markdoc/index.ts index c2501a0..3ac0880 100644 --- a/src/lib/markdoc/index.ts +++ b/src/lib/markdoc/index.ts @@ -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) diff --git a/src/styles/components/callout.css b/src/styles/components/blocks/callout.css similarity index 100% rename from src/styles/components/callout.css rename to src/styles/components/blocks/callout.css diff --git a/src/styles/components/code.css b/src/styles/components/blocks/code.css similarity index 100% rename from src/styles/components/code.css rename to src/styles/components/blocks/code.css diff --git a/src/styles/components/figure.css b/src/styles/components/blocks/figure.css similarity index 100% rename from src/styles/components/figure.css rename to src/styles/components/blocks/figure.css diff --git a/src/styles/components/footnote.css b/src/styles/components/blocks/footnote.css similarity index 100% rename from src/styles/components/footnote.css rename to src/styles/components/blocks/footnote.css diff --git a/src/styles/components/blocks/index.css b/src/styles/components/blocks/index.css new file mode 100644 index 0000000..9aef59c --- /dev/null +++ b/src/styles/components/blocks/index.css @@ -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"); diff --git a/src/styles/components/lead.css b/src/styles/components/blocks/lead.css similarity index 100% rename from src/styles/components/lead.css rename to src/styles/components/blocks/lead.css diff --git a/src/styles/components/quote.css b/src/styles/components/blocks/quote.css similarity index 78% rename from src/styles/components/quote.css rename to src/styles/components/blocks/quote.css index f6b4c4b..b677d9b 100644 --- a/src/styles/components/quote.css +++ b/src/styles/components/blocks/quote.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; + } } } diff --git a/src/styles/components/reveal.css b/src/styles/components/blocks/reveal.css similarity index 100% rename from src/styles/components/reveal.css rename to src/styles/components/blocks/reveal.css diff --git a/src/styles/components/sidenote.css b/src/styles/components/blocks/sidenote.css similarity index 100% rename from src/styles/components/sidenote.css rename to src/styles/components/blocks/sidenote.css diff --git a/src/styles/components/spoiler.css b/src/styles/components/blocks/spoiler.css similarity index 100% rename from src/styles/components/spoiler.css rename to src/styles/components/blocks/spoiler.css diff --git a/src/styles/components/table-wrapper.css b/src/styles/components/blocks/table-wrapper.css similarity index 100% rename from src/styles/components/table-wrapper.css rename to src/styles/components/blocks/table-wrapper.css diff --git a/src/styles/components/terms.css b/src/styles/components/blocks/terms.css similarity index 100% rename from src/styles/components/terms.css rename to src/styles/components/blocks/terms.css diff --git a/src/styles/components/index.css b/src/styles/components/index.css index 43b4433..4316afe 100644 --- a/src/styles/components/index.css +++ b/src/styles/components/index.css @@ -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"); diff --git a/src/styles/components/post/article-header.css b/src/styles/components/post/article-header.css new file mode 100644 index 0000000..d557ad1 --- /dev/null +++ b/src/styles/components/post/article-header.css @@ -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); + } +} diff --git a/src/styles/components/post/index.css b/src/styles/components/post/index.css new file mode 100644 index 0000000..54bc72b --- /dev/null +++ b/src/styles/components/post/index.css @@ -0,0 +1,2 @@ +@import url("article-header.css"); +@import url("stage-badge.css"); diff --git a/src/styles/components/post/stage-badge.css b/src/styles/components/post/stage-badge.css new file mode 100644 index 0000000..2ca8f0e --- /dev/null +++ b/src/styles/components/post/stage-badge.css @@ -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%); + } + } +} diff --git a/src/styles/components/prose/index.css b/src/styles/components/prose/index.css new file mode 100644 index 0000000..720bcd1 --- /dev/null +++ b/src/styles/components/prose/index.css @@ -0,0 +1 @@ +@import url("prose.css"); diff --git a/src/styles/components/prose.css b/src/styles/components/prose/prose.css similarity index 100% rename from src/styles/components/prose.css rename to src/styles/components/prose/prose.css diff --git a/src/styles/mixins/typography.css b/src/styles/mixins/typography.css index 0633688..bf11b7d 100644 --- a/src/styles/mixins/typography.css +++ b/src/styles/mixins/typography.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); +}