From c6023cf857c9ce942cac0ec2e6e5733e871b736a Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 30 Sep 2026 14:04:20 +0200 Subject: [PATCH] Added RevisionCard --- keystatic.config.ts | 9 ++ .../post/ArticleCard/ArticleCardItem.astro | 2 +- .../post/Footer/ArticleFooter.astro | 11 ++- src/components/post/Footer/RevisionCard.astro | 38 ++++++++ .../post/Header/ArticleHeader.astro | 2 +- src/components/post/Header/Meta.astro | 6 +- src/content/general/articles/calcination.mdoc | 3 +- src/content/general/articles/coagulation.mdoc | 3 +- src/content/general/articles/conjunction.mdoc | 3 +- src/content/general/articles/dissolution.mdoc | 3 +- .../general/articles/fermentation.mdoc | 3 +- .../general/articles/prima-materia.mdoc | 3 +- src/content/general/articles/stages.mdoc | 3 +- src/content/general/articles/sublimation.mdoc | 3 +- .../general/articles/the-crucible.mdoc | 3 +- src/content/site/releases/v0-12.yaml | 6 ++ src/content/site/releases/v0-13.yaml | 12 +++ src/content/site/releases/v0-14.yaml | 19 ++++ src/keystatic/collections/general/articles.ts | 5 +- src/keystatic/collections/site/index.ts | 7 ++ src/keystatic/collections/site/releases.ts | 66 +++++++++++++ src/keystatic/fields/content/base.ts | 7 ++ src/keystatic/fields/content/dates.ts | 18 ---- src/layouts/general/ArticleLayout.astro | 10 +- src/lib/constants/changelog.ts | 20 ++++ src/lib/constants/collections.ts | 3 + src/lib/content/load.ts | 9 +- src/lib/content/types.ts | 2 + src/lib/site/index.ts | 3 + src/lib/site/releases.ts | 59 ++++++++++++ src/lib/site/rules.ts | 2 +- src/lib/site/tree.ts | 4 +- src/lib/site/types.ts | 20 +++- src/lib/site/validate.ts | 5 +- src/styles/components/post/article-footer.css | 12 +++ src/styles/components/post/article-header.css | 9 +- src/styles/components/post/index.css | 1 + src/styles/components/post/revision-card.css | 92 +++++++++++++++++++ src/styles/mixins/helpers.css | 8 ++ src/styles/mixins/typography.css | 9 ++ 40 files changed, 431 insertions(+), 72 deletions(-) create mode 100644 src/components/post/Footer/RevisionCard.astro create mode 100644 src/content/site/releases/v0-12.yaml create mode 100644 src/content/site/releases/v0-13.yaml create mode 100644 src/content/site/releases/v0-14.yaml create mode 100644 src/keystatic/collections/site/index.ts create mode 100644 src/keystatic/collections/site/releases.ts delete mode 100644 src/keystatic/fields/content/dates.ts create mode 100644 src/lib/constants/changelog.ts create mode 100644 src/lib/site/releases.ts create mode 100644 src/styles/components/post/revision-card.css diff --git a/keystatic.config.ts b/keystatic.config.ts index 6e916f5..34c61d6 100644 --- a/keystatic.config.ts +++ b/keystatic.config.ts @@ -1,12 +1,21 @@ import { config } from '@keystatic/core' import { contentCollections } from '@/keystatic/collections/general' +import { siteCollections } from '@/keystatic/collections/site' +import { ARTICLES, PAGES, RELEASES, TAGS } from '@/lib/constants/collections' export default config({ storage: { kind: 'local', }, + ui: { + navigation: { + Content: [ARTICLES, PAGES, TAGS], + Site: [RELEASES], + }, + }, collections: { ...contentCollections, + ...siteCollections, }, }) diff --git a/src/components/post/ArticleCard/ArticleCardItem.astro b/src/components/post/ArticleCard/ArticleCardItem.astro index d7219e5..b583838 100644 --- a/src/components/post/ArticleCard/ArticleCardItem.astro +++ b/src/components/post/ArticleCard/ArticleCardItem.astro @@ -10,7 +10,7 @@ type Props = { const { article } = Astro.props const { cover, excerpt } = article.summary -const date = article.published +const date = article.publishedAt ---
diff --git a/src/components/post/Footer/ArticleFooter.astro b/src/components/post/Footer/ArticleFooter.astro index ab100f3..09623d3 100644 --- a/src/components/post/Footer/ArticleFooter.astro +++ b/src/components/post/Footer/ArticleFooter.astro @@ -1,17 +1,22 @@ --- import type { Sequence } from '@/lib/site/navigation' +import type { Article } from '@/lib/site/types' +import RevisionCard from './RevisionCard.astro' import SequenceNav from './SequenceNav.astro' type Props = { + article: Article sequence: Sequence | null } -const { sequence } = Astro.props +const { article, sequence } = Astro.props +const { title, changes } = article --- -{sequence && ( +{(changes.length > 0 || sequence) && (
- + {changes.length > 0 && } + {sequence && }
)} diff --git a/src/components/post/Footer/RevisionCard.astro b/src/components/post/Footer/RevisionCard.astro new file mode 100644 index 0000000..7e7cb9f --- /dev/null +++ b/src/components/post/Footer/RevisionCard.astro @@ -0,0 +1,38 @@ +--- +import { CHANGE_TYPES } from '@/lib/constants/changelog' +import { toMilitaryDTG } from '@/lib/content/dates' +import type { Change } from '@/lib/site/types' + +type Props = { + title: string + changes: Change[] +} + +const LIMIT = 5 + +const { title, changes } = Astro.props + +const labelOf = (kind: Change['kind']) => + CHANGE_TYPES.find((option) => option.value === kind)?.label ?? kind +--- + +
+
+

+ Revisions +

+

{title}

+
+
    + {changes.slice(0, LIMIT).map(({ kind, text, release }) => ( +
  1. + v{release.version} + + {labelOf(kind)} +

    {text}

    +
  2. + ))} +
+
diff --git a/src/components/post/Header/ArticleHeader.astro b/src/components/post/Header/ArticleHeader.astro index e093fd6..b25baa6 100644 --- a/src/components/post/Header/ArticleHeader.astro +++ b/src/components/post/Header/ArticleHeader.astro @@ -15,7 +15,7 @@ type Props = { const { article } = Astro.props const crumbs = trailOf(article) -const published = article.published +const published = article.publishedAt ---
diff --git a/src/components/post/Header/Meta.astro b/src/components/post/Header/Meta.astro index 20583ba..eb9d8d8 100644 --- a/src/components/post/Header/Meta.astro +++ b/src/components/post/Header/Meta.astro @@ -11,8 +11,10 @@ type Props = { 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 dateLabel = article.updated ? 'Updated' : 'Published' +const latest = article.changes[0]?.release.date +const updated = latest && article.publishedAt && latest > article.publishedAt ? latest : null +const date = updated ?? article.publishedAt +const dateLabel = updated ? 'Updated' : 'Published' ---
diff --git a/src/content/general/articles/calcination.mdoc b/src/content/general/articles/calcination.mdoc index 6742272..9d91ced 100644 --- a/src/content/general/articles/calcination.mdoc +++ b/src/content/general/articles/calcination.mdoc @@ -12,8 +12,7 @@ summary: cover: effect: none ratio: original -dates: - published: 2026-09-30T12:27:00.000Z +published: 2026-09-30T12:27:00.000Z tags: - alchemical-materialism - genesis diff --git a/src/content/general/articles/coagulation.mdoc b/src/content/general/articles/coagulation.mdoc index d407872..ccf4937 100644 --- a/src/content/general/articles/coagulation.mdoc +++ b/src/content/general/articles/coagulation.mdoc @@ -12,8 +12,7 @@ summary: cover: effect: none ratio: original -dates: - published: 2026-09-30T12:27:00.000Z +published: 2026-09-30T12:27:00.000Z tags: - alchemical-materialism - genesis diff --git a/src/content/general/articles/conjunction.mdoc b/src/content/general/articles/conjunction.mdoc index 1d8c014..79c2dec 100644 --- a/src/content/general/articles/conjunction.mdoc +++ b/src/content/general/articles/conjunction.mdoc @@ -12,8 +12,7 @@ summary: cover: effect: none ratio: original -dates: - published: 2026-09-30T12:27:00.000Z +published: 2026-09-30T12:27:00.000Z tags: - alchemical-materialism - genesis diff --git a/src/content/general/articles/dissolution.mdoc b/src/content/general/articles/dissolution.mdoc index f14b3b1..d81310a 100644 --- a/src/content/general/articles/dissolution.mdoc +++ b/src/content/general/articles/dissolution.mdoc @@ -12,8 +12,7 @@ summary: cover: effect: none ratio: original -dates: - published: 2026-09-30T12:27:00.000Z +published: 2026-09-30T12:27:00.000Z tags: - alchemical-materialism - genesis diff --git a/src/content/general/articles/fermentation.mdoc b/src/content/general/articles/fermentation.mdoc index ea87029..023b660 100644 --- a/src/content/general/articles/fermentation.mdoc +++ b/src/content/general/articles/fermentation.mdoc @@ -12,8 +12,7 @@ summary: cover: effect: none ratio: original -dates: - published: 2026-09-30T12:27:00.000Z +published: 2026-09-30T12:27:00.000Z tags: - alchemical-materialism - genesis diff --git a/src/content/general/articles/prima-materia.mdoc b/src/content/general/articles/prima-materia.mdoc index ce3d054..9990edb 100644 --- a/src/content/general/articles/prima-materia.mdoc +++ b/src/content/general/articles/prima-materia.mdoc @@ -15,8 +15,7 @@ summary: credit: John Martin – The Great Day of His Wrath effect: xeroxed ratio: 21-9 -dates: - published: 2026-09-21T12:35:00.000Z +published: 2026-09-21T12:35:00.000Z tags: - the-crucible - alchemical-materialism diff --git a/src/content/general/articles/stages.mdoc b/src/content/general/articles/stages.mdoc index 1abca15..2e0a118 100644 --- a/src/content/general/articles/stages.mdoc +++ b/src/content/general/articles/stages.mdoc @@ -10,7 +10,6 @@ summary: cover: effect: none ratio: original -dates: - published: 2026-09-21T12:34:00.000Z +published: 2026-09-21T12:34:00.000Z tags: [] --- diff --git a/src/content/general/articles/sublimation.mdoc b/src/content/general/articles/sublimation.mdoc index db7bc6c..686d91a 100644 --- a/src/content/general/articles/sublimation.mdoc +++ b/src/content/general/articles/sublimation.mdoc @@ -12,8 +12,7 @@ summary: cover: effect: none ratio: original -dates: - published: 2026-09-30T12:27:00.000Z +published: 2026-09-30T12:27:00.000Z tags: - alchemical-materialism - genesis diff --git a/src/content/general/articles/the-crucible.mdoc b/src/content/general/articles/the-crucible.mdoc index 930f79c..213d6e5 100644 --- a/src/content/general/articles/the-crucible.mdoc +++ b/src/content/general/articles/the-crucible.mdoc @@ -8,8 +8,7 @@ summary: Where we explore the crucible – a materialistic alchemist Worldbuilding Framework cover: {} -dates: - published: 2026-09-21T12:33:00.000Z +published: 2026-09-21T12:33:00.000Z tags: - the-crucible --- diff --git a/src/content/site/releases/v0-12.yaml b/src/content/site/releases/v0-12.yaml new file mode 100644 index 0000000..5d67b2c --- /dev/null +++ b/src/content/site/releases/v0-12.yaml @@ -0,0 +1,6 @@ +version: '0.12' +date: 2026-09-30T13:00:00.000Z +changes: + - kind: added + article: prima-materia + text: Section on the ground state; first table of correspondences diff --git a/src/content/site/releases/v0-13.yaml b/src/content/site/releases/v0-13.yaml new file mode 100644 index 0000000..45fbe48 --- /dev/null +++ b/src/content/site/releases/v0-13.yaml @@ -0,0 +1,12 @@ +version: '0.13' +date: 2026-09-30T13:00:00.000Z +changes: + - kind: added + article: calcination + text: Test + - kind: added + article: fermentation + text: Big test with a longer sentence + - kind: changed + article: coagulation + text: Lorem ipsum, blablabla ipbsum whorum diff --git a/src/content/site/releases/v0-14.yaml b/src/content/site/releases/v0-14.yaml new file mode 100644 index 0000000..c3c5176 --- /dev/null +++ b/src/content/site/releases/v0-14.yaml @@ -0,0 +1,19 @@ +version: '0.14' +date: 2026-09-30T13:00:00.000Z +changes: + - kind: added + article: coagulation + text: >- + s the raw release exactly as Keystatic reads it, inferred from the schema + like the other three + - kind: changed + article: prima-materia + text: >- + releases are loaded, resolved and attached to articles. Six files; nothing + renders yet. + - kind: fixed + article: fermentation + text: We don't burn the priests anymore, we burn the gods + - kind: removed + article: the-crucible + text: AND EVERYTHING GOES BOOOOOOOM diff --git a/src/keystatic/collections/general/articles.ts b/src/keystatic/collections/general/articles.ts index 4e01807..5670ec6 100644 --- a/src/keystatic/collections/general/articles.ts +++ b/src/keystatic/collections/general/articles.ts @@ -1,8 +1,7 @@ import { collection, fields } from '@keystatic/core' -import { base, stage } from '@/keystatic/fields/content/base' +import { base, published, 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' import { articleSummary } from '@/keystatic/fields/content/summary' import { ARTICLES, TAGS } from '@/lib/constants/collections' @@ -21,7 +20,7 @@ export const articles = collection({ stage, route: articleRoute, summary: articleSummary(ARTICLES), - dates, + published, tags: fields.multiRelationship({ label: 'Tags', collection: TAGS, diff --git a/src/keystatic/collections/site/index.ts b/src/keystatic/collections/site/index.ts new file mode 100644 index 0000000..49a9119 --- /dev/null +++ b/src/keystatic/collections/site/index.ts @@ -0,0 +1,7 @@ +import { RELEASES } from '@/lib/constants/collections' + +import { releases } from './releases' + +export const siteCollections = { + [RELEASES]: releases, +} diff --git a/src/keystatic/collections/site/releases.ts b/src/keystatic/collections/site/releases.ts new file mode 100644 index 0000000..5a86f54 --- /dev/null +++ b/src/keystatic/collections/site/releases.ts @@ -0,0 +1,66 @@ +import { collection, fields } from '@keystatic/core' + +import { CHANGE_TYPES } from '@/lib/constants/changelog' +import { ARTICLES, RELEASES } from '@/lib/constants/collections' + +const VERSION = /^\d+\.\d+(\.\d+)?$/ + +export const releases = collection({ + label: 'Releases', + slugField: 'version', + path: `src/content/site/${RELEASES}/*`, + columns: ['version', 'date'], + schema: { + version: fields.slug({ + name: { + label: 'Version', + description: 'Without the v: 0.12, 1.0, 1.0.3', + validation: { + isRequired: true, + pattern: { + regex: VERSION, + message: 'Numbers separated by dots, like 0.12 or 1.0.3', + }, + }, + }, + slug: { + label: 'File ID', + generate: (version) => `v${version.replaceAll('.', '-')}`, + }, + }), + date: fields.datetime({ + label: 'Date', + defaultValue: { + kind: 'now', + }, + validation: { + isRequired: true, + }, + }), + changes: fields.array( + fields.object({ + kind: fields.select({ + label: 'Kind', + options: CHANGE_TYPES, + defaultValue: 'added', + }), + article: fields.relationship({ + label: 'Article', + description: 'Empty for site-wide changes', + collection: ARTICLES, + }), + text: fields.text({ + label: 'Text', + validation: { + isRequired: true, + }, + }), + }), + { + label: 'Changes', + itemLabel: (props) => + `${props.fields.kind.value} · ${props.fields.article.value ?? 'site'} · ${props.fields.text.value}`, + }, + ), + }, +}) diff --git a/src/keystatic/fields/content/base.ts b/src/keystatic/fields/content/base.ts index f4cd809..995c3d1 100644 --- a/src/keystatic/fields/content/base.ts +++ b/src/keystatic/fields/content/base.ts @@ -45,6 +45,13 @@ export const noindex = fields.checkbox({ label: 'Hide from search engines', }) +export const published = fields.datetime({ + label: 'Published', + defaultValue: { + kind: 'now', + }, +}) + export const base = { title, subtitle, diff --git a/src/keystatic/fields/content/dates.ts b/src/keystatic/fields/content/dates.ts deleted file mode 100644 index 9f08760..0000000 --- a/src/keystatic/fields/content/dates.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { fields } from '@keystatic/core' - -export const dates = fields.object( - { - published: fields.datetime({ - label: 'Published', - defaultValue: { - kind: 'now', - }, - }), - updated: fields.datetime({ - label: 'Updated', - }), - }, - { - label: 'Dates', - }, -) diff --git a/src/layouts/general/ArticleLayout.astro b/src/layouts/general/ArticleLayout.astro index c78b62b..6fa3823 100644 --- a/src/layouts/general/ArticleLayout.astro +++ b/src/layouts/general/ArticleLayout.astro @@ -38,7 +38,7 @@ const sequence = sequenceOf(article) - +
{ list(): Promise @@ -36,10 +36,11 @@ async function readAll( export async function loadContent(): Promise { const { collections } = createContentReader() const problems = createDiagnostics() - const [articles, pages, tags] = await Promise.all([ + const [articles, pages, tags, releases] = await Promise.all([ readAll(collections.articles, ARTICLES, problems), readAll(collections.pages, PAGES, problems), readAll(collections.tags, TAGS, problems), + readAll(collections.releases, RELEASES, problems), ]) - return { articles, pages, tags, diagnostics: problems.list } + return { articles, pages, tags, releases, diagnostics: problems.list } } diff --git a/src/lib/content/types.ts b/src/lib/content/types.ts index 6c409df..b1d8e0c 100644 --- a/src/lib/content/types.ts +++ b/src/lib/content/types.ts @@ -9,10 +9,12 @@ type Collections = typeof config.collections export type ArticleEntry = Entry export type PageEntry = Entry export type TagEntry = Entry +export type ReleaseEntry = Entry export type Content = { articles: Map pages: Map tags: Map + releases: Map diagnostics: Diagnostic[] } diff --git a/src/lib/site/index.ts b/src/lib/site/index.ts index 193642c..6b87fe5 100644 --- a/src/lib/site/index.ts +++ b/src/lib/site/index.ts @@ -3,6 +3,7 @@ import { loadContent } from '../content/load' import type { Content } from '../content/types' import { createDiagnostics } from '../diagnostics' import type { Diagnostics } from '../diagnostics/types' +import { collectReleases } from './releases' import { isBuilt, newestFirst, @@ -58,6 +59,7 @@ export function buildSite(content: Content, visibility: Visibility): Site { const articles = placeArticles(content, visibility, problems) const pages = placePages(content, visibility, problems) const tags = collectTags(content, articles) + const releases = collectReleases(content, articles, problems) validate(articles.values(), content, problems) const byPath = indexUrls([...articles.values(), ...pages.values()], problems) const roots = [...articles.values()] @@ -68,6 +70,7 @@ export function buildSite(content: Content, visibility: Visibility): Site { articles, pages, tags, + releases, byPath, known: new Set(content.articles.keys()), roots, diff --git a/src/lib/site/releases.ts b/src/lib/site/releases.ts new file mode 100644 index 0000000..cf048fb --- /dev/null +++ b/src/lib/site/releases.ts @@ -0,0 +1,59 @@ +import { RELEASES } from '../constants/collections' +import { parseDate } from '../content/dates' +import type { Content } from '../content/types' +import type { Diagnostics } from '../diagnostics/types' +import type { Article, Release } from './types' + +const partsOf = (version: string) => version.split('.').map(Number) + +function compareVersions(a: string, b: string): number { + const [left, right] = [partsOf(a), partsOf(b)] + for (let i = 0; i < Math.max(left.length, right.length); i++) { + const difference = (left[i] ?? 0) - (right[i] ?? 0) + if (difference) return difference + } + return 0 +} + +const releaseOrder = (a: Release, b: Release) => compareVersions(b.version, a.version) + +export function collectReleases( + content: Content, + articles: Map, + problems: Diagnostics, +): Release[] { + const releases: Release[] = [] + + for (const [id, entry] of content.releases) { + const date = parseDate(entry.date) + if (!date) { + problems.error(RELEASES, id, 'Needs a date') + continue + } + + const release: Release = { id, version: entry.version, date, changes: [] } + for (const { kind, article: target, text } of entry.changes) { + const article = target ? articles.get(target) : null + if (target && !article) { + if (!content.articles.has(target)) + problems.error(RELEASES, id, `Article "${target}" does not exist`) + continue + } + release.changes.push({ kind, article: article ?? null, text, release }) + } + + if (release.changes.length) releases.push(release) + } + + releases.sort(releaseOrder) + releases.forEach((release, i) => { + const older = releases[i + 1] + if (older && release.date < older.date) + problems.warning(RELEASES, release.id, `Dated before v${older.version}`) + }) + + for (const release of releases) + for (const change of release.changes) change.article?.changes.push(change) + + return releases +} diff --git a/src/lib/site/rules.ts b/src/lib/site/rules.ts index b9b2597..4f33a53 100644 --- a/src/lib/site/rules.ts +++ b/src/lib/site/rules.ts @@ -27,7 +27,7 @@ export function segmentProblem(segment: string, atRoot: boolean): string | null const time = (date: Date | null) => date?.getTime() ?? 0 const UNINDEXED = Number.MAX_SAFE_INTEGER -export const newestFirst = (a: Article, b: Article) => time(b.published) - time(a.published) +export const newestFirst = (a: Article, b: Article) => time(b.publishedAt) - time(a.publishedAt) export const siblingOrder = (a: Article, b: Article) => (a.route.index ?? UNINDEXED) - (b.route.index ?? UNINDEXED) || diff --git a/src/lib/site/tree.ts b/src/lib/site/tree.ts index ded37ec..2d13a41 100644 --- a/src/lib/site/tree.ts +++ b/src/lib/site/tree.ts @@ -54,10 +54,10 @@ export function placeArticles( id, path: `${parent?.path ?? ''}/${segment}`, listed: isListed(entry.status, visibility), - published: parseDate(entry.dates.published), - updated: parseDate(entry.dates.updated), + publishedAt: parseDate(entry.published), parent, children: [], + changes: [], } placed.set(id, article) return article diff --git a/src/lib/site/types.ts b/src/lib/site/types.ts index 5a852ad..0e781f2 100644 --- a/src/lib/site/types.ts +++ b/src/lib/site/types.ts @@ -1,3 +1,4 @@ +import type { ChangeType } from '../constants/changelog' import { ARTICLES, PAGES, TAGS } from '../constants/collections' import type { ArticleEntry, PageEntry, TagEntry } from '../content/types' import type { Diagnostic } from '../diagnostics/types' @@ -7,10 +8,10 @@ export type Article = ArticleEntry & { id: string path: string listed: boolean - published: Date | null - updated: Date | null + publishedAt: Date | null parent: Article | null children: Article[] + changes: Change[] } export type Page = PageEntry & { @@ -26,12 +27,27 @@ export type Tag = TagEntry & { articles: Article[] } +export type Release = { + id: string + version: string + date: Date + changes: Change[] +} + +export type Change = { + kind: ChangeType + article: Article | null + text: string + release: Release +} + export type Routed = Article | Page export type Site = { articles: Map pages: Map tags: Map + releases: Release[] byPath: Map known: Set roots: Article[] diff --git a/src/lib/site/validate.ts b/src/lib/site/validate.ts index 6e4e181..3c5c9f8 100644 --- a/src/lib/site/validate.ts +++ b/src/lib/site/validate.ts @@ -4,17 +4,16 @@ import type { Diagnostics } from '../diagnostics/types' import type { Article } from './types' export function validate(articles: Iterable
, content: Content, problems: Diagnostics) { - for (const { id, status, summary, published, updated, tags } of articles) { + for (const { id, status, summary, publishedAt, tags } of articles) { const { excerpt, cover } = summary const error = (message: string) => problems.error(ARTICLES, id, message) const warn = (message: string) => problems.warning(ARTICLES, id, message) if (status !== 'draft' && !excerpt) error('Needs an excerpt') - if (status !== 'draft' && !published) error('Needs a published date') + if (status !== 'draft' && !publishedAt) error('Needs a published date') if (cover.image && !cover.alt) error('Cover image needs alt text') if (!cover.image && (cover.alt || cover.caption || cover.credit)) warn('The cover has text but not image!') - if (published && updated && updated < published) warn('Updated before it was published') for (const tag of tags) if (!content.tags.has(tag)) error(`Tag "${tag}" does not exist`) } } diff --git a/src/styles/components/post/article-footer.css b/src/styles/components/post/article-footer.css index cdf9097..2dbc2b2 100644 --- a/src/styles/components/post/article-footer.css +++ b/src/styles/components/post/article-footer.css @@ -5,5 +5,17 @@ display: grid; gap: var(--size-10); + + @media (--bp-desktop) { + @mixin layout-columns; + + & > .revision-card { + grid-column: body-start / body-end; + } + + & > .sequence-nav { + grid-column: rail-start / margin-end; + } + } } } diff --git a/src/styles/components/post/article-header.css b/src/styles/components/post/article-header.css index 92d6f2d..eda91fb 100644 --- a/src/styles/components/post/article-header.css +++ b/src/styles/components/post/article-header.css @@ -109,17 +109,10 @@ .article-header__tag { @mixin py var(--size-1); @mixin px var(--size-3); - + @mixin typo_tag; 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 { diff --git a/src/styles/components/post/index.css b/src/styles/components/post/index.css index 6320e09..c827ac9 100644 --- a/src/styles/components/post/index.css +++ b/src/styles/components/post/index.css @@ -2,3 +2,4 @@ @import url("stage-badge.css"); @import url("article-footer.css"); @import url("sequence-nav.css"); +@import url("revision-card.css"); diff --git a/src/styles/components/post/revision-card.css b/src/styles/components/post/revision-card.css new file mode 100644 index 0000000..65a1b1e --- /dev/null +++ b/src/styles/components/post/revision-card.css @@ -0,0 +1,92 @@ +@layer components.blocks { + .revision-card { + @mixin border-t var(--border-boundary), solid, var(--color-border-strong); + @mixin pt var(--size-8); + } + + .revision-card__head { + @mixin mb var(--size-6); + + display: flex; + flex-wrap: wrap; + gap: var(--size-2) var(--size-6); + align-items: baseline; + justify-content: space-between; + } + + .revision-card__heading { + @mixin typo_heading-2; + @mixin ma var(--size-0); + } + + .revision-card__subject { + @mixin typo_eyebrow; + @mixin ma var(--size-0); + + color: var(--color-text-secondary); + } + + .revision-card__list { + @mixin ma var(--size-0); + @mixin pa var(--size-0); + + list-style: none; + + @media (--bp-tablet-up) { + display: grid; + grid-template-columns: auto auto auto minmax(0, 1fr); + column-gap: var(--size-10); + } + } + + .revision-card__row { + @mixin border-b var(--border-hairline), solid, var(--color-border-subtle); + @mixin py var(--size-6); + + display: grid; + grid-template-columns: auto auto minmax(0, 1fr); + gap: var(--size-2) var(--size-8); + align-items: baseline; + + font-family: var(--font-mono); + font-size: var(--type-0); + line-height: var(--leading-0); + + @media (--bp-tablet-up) { + grid-column: 1 / -1; + grid-template-columns: subgrid; + } + } + + .revision-card__version { + font-weight: var(--font-weight-emphasis); + color: var(--color-text-primary); + } + + .revision-card__date { + color: var(--color-text-quiet); + } + + .revision-card__kind { + @mixin typo_tag; + @mixin py var(--size-1); + @mixin px var(--size-3); + + justify-self: start; + color: var(--color-text-inverse); + background-color: var(--color-surface-inverse); + } + + .revision-card__text { + @mixin ma var(--size-0); + + grid-column: 1 / -1; + font-family: var(--font-body); + line-height: var(--leading-prose-tight); + color: var(--color-text-primary); + + @media (--bp-tablet-up) { + grid-column: auto; + } + } +} diff --git a/src/styles/mixins/helpers.css b/src/styles/mixins/helpers.css index f7dd7d1..5b7e84e 100644 --- a/src/styles/mixins/helpers.css +++ b/src/styles/mixins/helpers.css @@ -15,3 +15,11 @@ width: min(100% - 2 * var(--layout-page-margin), var(--layout-wrapper-width)); } + +@define-mixin layout-columns { + grid-template-columns: + [rail-start] minmax(var(--layout-rail-width), 1fr) + [rail-end body-start] var(--layout-content-width) + [body-end margin-start] var(--layout-margin-width) [margin-end]; + column-gap: var(--layout-gutter); +} diff --git a/src/styles/mixins/typography.css b/src/styles/mixins/typography.css index bf11b7d..51123bb 100644 --- a/src/styles/mixins/typography.css +++ b/src/styles/mixins/typography.css @@ -108,3 +108,12 @@ text-transform: uppercase; letter-spacing: var(--tracking-eyebrow); } + +@define-mixin typo_tag { + font-family: var(--font-mono); + font-size: var(--type--2); + font-weight: var(--font-weight-emphasis); + line-height: var(--leading--2); + text-transform: uppercase; + letter-spacing: var(--tracking-tag); +}