diff --git a/src/components/post/Footer/ArticleFooter.astro b/src/components/post/Footer/ArticleFooter.astro new file mode 100644 index 0000000..ab100f3 --- /dev/null +++ b/src/components/post/Footer/ArticleFooter.astro @@ -0,0 +1,17 @@ +--- +import type { Sequence } from '@/lib/site/navigation' + +import SequenceNav from './SequenceNav.astro' + +type Props = { + sequence: Sequence | null +} + +const { sequence } = Astro.props +--- + +{sequence && ( + +)} diff --git a/src/components/post/Footer/SequenceNav.astro b/src/components/post/Footer/SequenceNav.astro new file mode 100644 index 0000000..ea6a004 --- /dev/null +++ b/src/components/post/Footer/SequenceNav.astro @@ -0,0 +1,66 @@ +--- +import { toRoman } from '@/lib/content/numerals' +import type { Sequence } from '@/lib/site/navigation' + +type Props = { + sequence: Sequence +} + +const { parent, siblings, position, previous, next } = Astro.props.sequence +--- + + + + {previous && ( + + + Previous + + {toRoman(position)} + + + {previous.title} + + )} + + + + + + {parent.title} + + + {toRoman(position + 1)} of {toRoman(siblings.length)} + + + + {siblings.map((sibling, i) => ( + + + {toRoman(i + 1)} + + + ))} + + + + + {next && ( + + + + {toRoman(position + 2)} + + Next + + {next.title} + + )} + + diff --git a/src/content/general/articles/calcination.mdoc b/src/content/general/articles/calcination.mdoc new file mode 100644 index 0000000..6742272 --- /dev/null +++ b/src/content/general/articles/calcination.mdoc @@ -0,0 +1,21 @@ +--- +title: Calcination +subtitle: Where we burn the people +status: draft +noindex: false +stage: stub +route: + parent: stages + index: 2 + sequence: false +summary: + cover: + effect: none + ratio: original +dates: + published: 2026-09-30T12:27:00.000Z +tags: + - alchemical-materialism + - genesis + - the-crucible +--- diff --git a/src/content/general/articles/coagulation.mdoc b/src/content/general/articles/coagulation.mdoc new file mode 100644 index 0000000..d407872 --- /dev/null +++ b/src/content/general/articles/coagulation.mdoc @@ -0,0 +1,21 @@ +--- +title: Coagulation +subtitle: Where we burn the politicians +status: draft +noindex: false +stage: stub +route: + parent: stages + index: 6 + sequence: false +summary: + cover: + effect: none + ratio: original +dates: + published: 2026-09-30T12:27:00.000Z +tags: + - alchemical-materialism + - genesis + - the-crucible +--- diff --git a/src/content/general/articles/conjunction.mdoc b/src/content/general/articles/conjunction.mdoc new file mode 100644 index 0000000..1d8c014 --- /dev/null +++ b/src/content/general/articles/conjunction.mdoc @@ -0,0 +1,21 @@ +--- +title: Conjunction +subtitle: Where we burn the state +status: draft +noindex: false +stage: stub +route: + parent: stages + index: 5 + sequence: false +summary: + cover: + effect: none + ratio: original +dates: + published: 2026-09-30T12:27:00.000Z +tags: + - alchemical-materialism + - genesis + - the-crucible +--- diff --git a/src/content/general/articles/dissolution.mdoc b/src/content/general/articles/dissolution.mdoc new file mode 100644 index 0000000..f14b3b1 --- /dev/null +++ b/src/content/general/articles/dissolution.mdoc @@ -0,0 +1,21 @@ +--- +title: Dissolution +subtitle: Where we act +status: draft +noindex: false +stage: stub +route: + parent: stages + index: 7 + sequence: false +summary: + cover: + effect: none + ratio: original +dates: + published: 2026-09-30T12:27:00.000Z +tags: + - alchemical-materialism + - genesis + - the-crucible +--- diff --git a/src/content/general/articles/fermentation.mdoc b/src/content/general/articles/fermentation.mdoc new file mode 100644 index 0000000..ea87029 --- /dev/null +++ b/src/content/general/articles/fermentation.mdoc @@ -0,0 +1,21 @@ +--- +title: Fermentation +subtitle: Where we burn the priests +status: draft +noindex: false +stage: stub +route: + parent: stages + index: 3 + sequence: false +summary: + cover: + effect: none + ratio: original +dates: + published: 2026-09-30T12:27:00.000Z +tags: + - alchemical-materialism + - genesis + - the-crucible +--- diff --git a/src/content/general/articles/prima-materia.mdoc b/src/content/general/articles/prima-materia.mdoc index 8f97e39..ce3d054 100644 --- a/src/content/general/articles/prima-materia.mdoc +++ b/src/content/general/articles/prima-materia.mdoc @@ -3,8 +3,11 @@ title: Prima Materia subtitle: Where we lay the ground status: draft noindex: false +stage: stub route: parent: stages + index: 1 + sequence: false summary: cover: image: /src/assets/content/articles/prima-materia/summary/cover/image.jpg diff --git a/src/content/general/articles/stages.mdoc b/src/content/general/articles/stages.mdoc index 532e8a9..1abca15 100644 --- a/src/content/general/articles/stages.mdoc +++ b/src/content/general/articles/stages.mdoc @@ -2,10 +2,14 @@ title: Stages status: draft noindex: false +stage: stub route: parent: the-crucible + sequence: true summary: - cover: {} + cover: + effect: none + ratio: original dates: published: 2026-09-21T12:34:00.000Z tags: [] diff --git a/src/content/general/articles/sublimation.mdoc b/src/content/general/articles/sublimation.mdoc new file mode 100644 index 0000000..db7bc6c --- /dev/null +++ b/src/content/general/articles/sublimation.mdoc @@ -0,0 +1,21 @@ +--- +title: Sublimation +subtitle: Where we burn the witches +status: draft +noindex: false +stage: stub +route: + parent: stages + index: 4 + sequence: false +summary: + cover: + effect: none + ratio: original +dates: + published: 2026-09-30T12:27:00.000Z +tags: + - alchemical-materialism + - genesis + - the-crucible +--- diff --git a/src/keystatic/fields/content/route.ts b/src/keystatic/fields/content/route.ts index 241c413..43bb0a0 100644 --- a/src/keystatic/fields/content/route.ts +++ b/src/keystatic/fields/content/route.ts @@ -35,6 +35,11 @@ export const articleRoute = fields.object( label: 'Index', description: 'Sorts among siblings. Empty sorts by date, newest first', }), + sequence: fields.checkbox({ + label: 'Sequence', + description: + 'Children form a numbered series with previous/next navigation. Give each child an index', + }), }, { label: 'Route', diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index f6ae67b..af7a97a 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -36,7 +36,7 @@ const previewUrl = preview ? new URL(preview.src, Astro.site) : undefined {description && } {previewUrl && } - + diff --git a/src/layouts/general/ArticleLayout.astro b/src/layouts/general/ArticleLayout.astro index ae2edef..c78b62b 100644 --- a/src/layouts/general/ArticleLayout.astro +++ b/src/layouts/general/ArticleLayout.astro @@ -2,11 +2,13 @@ import DevBanner from '@/components/dev/DevBanner.astro' import FootnoteList from '@/components/markdoc/blocks/general/FootnoteList.astro' import Node from '@/components/markdoc/Node.astro' +import ArticleFooter from '@/components/post/Footer/ArticleFooter.astro' import ArticleHeader from '@/components/post/Header/ArticleHeader.astro' import ToC from '@/components/post/ToC.astro' import BaseLayout from '@/layouts/BaseLayout.astro' import type { Rendered } from '@/lib/markdoc/types' import { metaOf } from '@/lib/site/meta' +import { sequenceOf } from '@/lib/site/navigation' import type { Article } from '@/lib/site/types' type Props = { @@ -16,6 +18,7 @@ type Props = { const { article, rendered } = Astro.props const meta = metaOf(article) +const sequence = sequenceOf(article) --- @@ -35,6 +38,7 @@ const meta = metaOf(article) + = amount) { + result += numeral + rest -= amount + } + } + return result +} diff --git a/src/lib/site/navigation.ts b/src/lib/site/navigation.ts index 3e2faad..bd6f1b4 100644 --- a/src/lib/site/navigation.ts +++ b/src/lib/site/navigation.ts @@ -1,5 +1,30 @@ import type { Article } from './types' +export type Sequence = { + parent: Article + siblings: Article[] + position: number + previous: Article | null + next: Article | null +} + +export function sequenceOf(article: Article): Sequence | null { + const parent = article.parent + if (!parent?.route.sequence) return null + + const siblings = parent.children + const position = siblings.indexOf(article) + if (position === -1 || siblings.length < 2) return null + + return { + parent, + siblings, + position, + previous: siblings[position - 1] ?? null, + next: siblings[position + 1] ?? null, + } +} + export function trailOf(article: Article): Article[] { const trail: Article[] = [] for (let node: Article | null = article; node; node = node.parent) trail.unshift(node) diff --git a/src/styles/components/post/article-footer.css b/src/styles/components/post/article-footer.css new file mode 100644 index 0000000..cdf9097 --- /dev/null +++ b/src/styles/components/post/article-footer.css @@ -0,0 +1,9 @@ +@layer components.blocks { + .article-footer { + @mixin layout-panel; + @mixin my var(--size-12); + + display: grid; + gap: var(--size-10); + } +} diff --git a/src/styles/components/post/article-header.css b/src/styles/components/post/article-header.css index d557ad1..92d6f2d 100644 --- a/src/styles/components/post/article-header.css +++ b/src/styles/components/post/article-header.css @@ -1,12 +1,11 @@ @layer components.blocks { .article-header__panel { - @mixin mx auto; @mixin mt var(--size-10); + @mixin layout-panel; 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); diff --git a/src/styles/components/post/index.css b/src/styles/components/post/index.css index 54bc72b..6320e09 100644 --- a/src/styles/components/post/index.css +++ b/src/styles/components/post/index.css @@ -1,2 +1,4 @@ @import url("article-header.css"); @import url("stage-badge.css"); +@import url("article-footer.css"); +@import url("sequence-nav.css"); diff --git a/src/styles/components/post/sequence-nav.css b/src/styles/components/post/sequence-nav.css new file mode 100644 index 0000000..f51ddd8 --- /dev/null +++ b/src/styles/components/post/sequence-nav.css @@ -0,0 +1,188 @@ +@layer components.blocks { + .sequence-nav { + display: grid; + grid-template-areas: + "overview" + "previous" + "next"; + grid-template-columns: minmax(0, 1fr); + gap: var(--border-card); + + border: var(--border-card) solid var(--color-border-strong); + + background-color: var(--color-border-strong); + + @media (--bp-tablet-up) { + grid-template-areas: "previous overview next"; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + } + } + + .sequence-nav__step { + background-color: var(--color-surface-elevated-1); + + &:empty { + @media (--bp-mobile) { + display: none; + } + } + } + + .sequence-nav__step--previous { + grid-area: previous; + } + + .sequence-nav__step--next { + grid-area: next; + text-align: end; + } + + .sequence-nav__overview { + @mixin pa var(--size-9); + + display: flex; + grid-area: overview; + flex-direction: column; + gap: var(--size-6); + + background-color: var(--color-surface-elevated-1); + } + + .sequence-nav__link { + @mixin pa var(--size-9); + + display: flex; + flex-direction: column; + gap: var(--size-6); + + height: 100%; + + color: var(--color-text-primary); + + transition: background-color var(--motion-fast) var(--ease-out); + + &:hover { + background-color: var(--color-surface-hover); + } + + &:focus-visible { + @mixin focus-ring 2px var(--color-focus-ring) -4px; + } + } + + .sequence-nav__label { + @mixin typo_eyebrow; + @mixin ma var(--size-0); + + color: var(--color-text-secondary); + + .sequence-nav__overview & { + color: var(--color-text-primary); + } + + .sequence-nav__step--previous & { + &::before { + content: "⟪" / ""; + margin-inline-end: var(--size-2); + font-family: var(--font-body); + } + } + + .sequence-nav__step--next & { + &::after { + content: "⟫" / ""; + margin-inline-start: var(--size-2); + font-family: var(--font-body); + } + } + } + + .sequence-nav__numeral { + .sequence-nav__step--previous & { + &::before { + content: "⟫" / ""; + margin-inline: var(--size-2); + font-family: var(--font-body); + } + } + + .sequence-nav__step--next & { + &::after { + content: "⟫" / ""; + margin-inline: var(--size-2); + font-family: var(--font-body); + } + } + } + + .sequence-nav__count { + &::before { + content: "⟫" / ""; + margin-inline: var(--size-2); + font-family: var(--font-body); + } + } + + .sequence-nav__parent { + &:hover { + text-decoration: underline; + } + + &:focus-visible { + @mixin focus-ring; + } + } + + .sequence-nav__title { + @mixin typo_heading-2; + } + + .sequence-nav__squares { + @mixin ma var(--size-0); + @mixin pa var(--size-0); + + display: flex; + flex-wrap: wrap; + gap: var(--size-2); + list-style: none; + } + + .sequence-nav__square { + @mixin px var(--size-1); + + display: grid; + place-items: center; + + min-width: var(--size-10); + height: var(--size-10); + border: var(--border-hairline) solid var(--color-border-normal); + + font-family: var(--font-mono); + font-size: var(--type--3); + line-height: 1; + color: var(--color-text-secondary); + + background-color: var(--color-surface-elevated-1); + + &:visited { + border-color: var(--color-surface-inverse); + color: var(--color-text-inverse); + background-color: var(--color-surface-inverse); + } + + &:hover { + border-color: var(--color-border-strong); + } + + &[aria-current="page"] { + border-color: var(--color-primary); + font-weight: var(--font-weight-emphasis); + color: var(--color-text-on-accent); + background-color: var(--color-primary); + } + + &:focus-visible { + @mixin focus-ring; + } + } +} diff --git a/src/styles/mixins/helpers.css b/src/styles/mixins/helpers.css index 0b25cd8..f7dd7d1 100644 --- a/src/styles/mixins/helpers.css +++ b/src/styles/mixins/helpers.css @@ -9,3 +9,9 @@ @mixin px var(--space-0); } } + +@define-mixin layout-panel { + @mixin mx auto; + + width: min(100% - 2 * var(--layout-page-margin), var(--layout-wrapper-width)); +}
+ + {parent.title} + + + {toRoman(position + 1)} of {toRoman(siblings.length)} + +