Added ArticleFooter and SequenceNav

This commit is contained in:
2026-09-30 12:50:56 +02:00
parent 90b5390795
commit 8ef3899782
20 changed files with 485 additions and 4 deletions

View File

@@ -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 && (
<footer class="article-footer">
<SequenceNav sequence={sequence} />
</footer>
)}

View File

@@ -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
---
<nav class="sequence-nav" aria-label={`${parent.title} sequence`}>
<div class="sequence-nav__step sequence-nav__step--previous">
{previous && (
<a class="sequence-nav__link" href={previous.path} rel="prev">
<span class="sequence-nav__label">
Previous
<span class="sequence-nav__numeral" aria-hidden="true">
{toRoman(position)}
</span>
</span>
<span class="sequence-nav__title">{previous.title}</span>
</a>
)}
</div>
<div class="sequence-nav__overview">
<p class="sequence-nav__label">
<a class="sequence-nav__parent" href={parent.path}>
{parent.title}
</a>
<span class="sequence-nav__count">
{toRoman(position + 1)} of {toRoman(siblings.length)}
</span>
</p>
<ol class="sequence-nav__squares">
{siblings.map((sibling, i) => (
<li>
<a
class="sequence-nav__square"
href={sibling.path}
aria-label={sibling.title}
title={sibling.title}
aria-current={i === position ? 'page' : undefined}
>
{toRoman(i + 1)}
</a>
</li>
))}
</ol>
</div>
<div class="sequence-nav__step sequence-nav__step--next">
{next && (
<a class="sequence-nav__link" href={next.path} rel="next">
<span class="sequence-nav__label">
<span class="sequence-nav__numeral" aria-hidden="true">
{toRoman(position + 2)}
</span>
Next
</span>
<span class="sequence-nav__title">{next.title}</span>
</a>
)}
</div>
</nav>

View File

@@ -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
---

View File

@@ -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
---

View File

@@ -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
---

View File

@@ -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
---

View File

@@ -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
---

View File

@@ -3,8 +3,11 @@ title: Prima Materia
subtitle: Where we lay the ground subtitle: Where we lay the ground
status: draft status: draft
noindex: false noindex: false
stage: stub
route: route:
parent: stages parent: stages
index: 1
sequence: false
summary: summary:
cover: cover:
image: /src/assets/content/articles/prima-materia/summary/cover/image.jpg image: /src/assets/content/articles/prima-materia/summary/cover/image.jpg

View File

@@ -2,10 +2,14 @@
title: Stages title: Stages
status: draft status: draft
noindex: false noindex: false
stage: stub
route: route:
parent: the-crucible parent: the-crucible
sequence: true
summary: summary:
cover: {} cover:
effect: none
ratio: original
dates: dates:
published: 2026-09-21T12:34:00.000Z published: 2026-09-21T12:34:00.000Z
tags: [] tags: []

View File

@@ -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
---

View File

@@ -35,6 +35,11 @@ export const articleRoute = fields.object(
label: 'Index', label: 'Index',
description: 'Sorts among siblings. Empty sorts by date, newest first', 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', label: 'Route',

View File

@@ -36,7 +36,7 @@ const previewUrl = preview ? new URL(preview.src, Astro.site) : undefined
{description && <meta property="og:description" content={description} />} {description && <meta property="og:description" content={description} />}
{previewUrl && <meta property="og:image" content={previewUrl} />} {previewUrl && <meta property="og:image" content={previewUrl} />}
<meta name="twitter:card" content={previewUrl ? 'summary_large_image' : 'summary'} /> <meta name="twitter:card" content={previewUrl ? 'summary_large_image' : 'summary'} />
<slot name="head" />
<Font cssVariable="--font-geist" preload /> <Font cssVariable="--font-geist" preload />
<Font cssVariable="--font-geist-mono" preload /> <Font cssVariable="--font-geist-mono" preload />
<Font cssVariable="--font-blaka" preload /> <Font cssVariable="--font-blaka" preload />

View File

@@ -2,11 +2,13 @@
import DevBanner from '@/components/dev/DevBanner.astro' import DevBanner from '@/components/dev/DevBanner.astro'
import FootnoteList from '@/components/markdoc/blocks/general/FootnoteList.astro' import FootnoteList from '@/components/markdoc/blocks/general/FootnoteList.astro'
import Node from '@/components/markdoc/Node.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 ArticleHeader from '@/components/post/Header/ArticleHeader.astro'
import ToC from '@/components/post/ToC.astro' import ToC from '@/components/post/ToC.astro'
import BaseLayout from '@/layouts/BaseLayout.astro' import BaseLayout from '@/layouts/BaseLayout.astro'
import type { Rendered } from '@/lib/markdoc/types' import type { Rendered } from '@/lib/markdoc/types'
import { metaOf } from '@/lib/site/meta' import { metaOf } from '@/lib/site/meta'
import { sequenceOf } from '@/lib/site/navigation'
import type { Article } from '@/lib/site/types' import type { Article } from '@/lib/site/types'
type Props = { type Props = {
@@ -16,6 +18,7 @@ type Props = {
const { article, rendered } = Astro.props const { article, rendered } = Astro.props
const meta = metaOf(article) const meta = metaOf(article)
const sequence = sequenceOf(article)
--- ---
<BaseLayout {...meta}> <BaseLayout {...meta}>
@@ -35,6 +38,7 @@ const meta = metaOf(article)
<FootnoteList footnotes={rendered.footnotes} /> <FootnoteList footnotes={rendered.footnotes} />
</div> </div>
</div> </div>
<ArticleFooter sequence={sequence} />
</article> </article>
<DevBanner <DevBanner
collection={article.collection} collection={article.collection}

View File

@@ -0,0 +1,27 @@
const NUMERALS = [
[1000, 'M'],
[900, 'CM'],
[500, 'D'],
[400, 'CD'],
[100, 'C'],
[90, 'XC'],
[50, 'L'],
[40, 'XL'],
[10, 'X'],
[9, 'IX'],
[5, 'V'],
[4, 'IV'],
[1, 'I'],
] as const
export function toRoman(value: number): string {
let rest = value
let result = ''
for (const [amount, numeral] of NUMERALS) {
while (rest >= amount) {
result += numeral
rest -= amount
}
}
return result
}

View File

@@ -1,5 +1,30 @@
import type { Article } from './types' 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[] { export function trailOf(article: Article): Article[] {
const trail: Article[] = [] const trail: Article[] = []
for (let node: Article | null = article; node; node = node.parent) trail.unshift(node) for (let node: Article | null = article; node; node = node.parent) trail.unshift(node)

View File

@@ -0,0 +1,9 @@
@layer components.blocks {
.article-footer {
@mixin layout-panel;
@mixin my var(--size-12);
display: grid;
gap: var(--size-10);
}
}

View File

@@ -1,12 +1,11 @@
@layer components.blocks { @layer components.blocks {
.article-header__panel { .article-header__panel {
@mixin mx auto;
@mixin mt var(--size-10); @mixin mt var(--size-10);
@mixin layout-panel;
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
gap: var(--border-card); 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); border: var(--border-card) solid var(--color-border-strong);
background-color: var(--color-border-strong); background-color: var(--color-border-strong);

View File

@@ -1,2 +1,4 @@
@import url("article-header.css"); @import url("article-header.css");
@import url("stage-badge.css"); @import url("stage-badge.css");
@import url("article-footer.css");
@import url("sequence-nav.css");

View File

@@ -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;
}
}
}

View File

@@ -9,3 +9,9 @@
@mixin px var(--space-0); @mixin px var(--space-0);
} }
} }
@define-mixin layout-panel {
@mixin mx auto;
width: min(100% - 2 * var(--layout-page-margin), var(--layout-wrapper-width));
}