Blog #1

Merged
Dave merged 22 commits from blog into main 2026-09-30 16:48:38 +02:00
11 changed files with 206 additions and 7 deletions
Showing only changes of commit 3b639f8a5b - Show all commits

View File

@@ -7,6 +7,7 @@ import ContentImage from '@/components/media/ContentImage.astro'
import Figure from '../media/Figure.astro'
import Callout from './blocks/general/Callout.astro'
import Lead from './blocks/general/Lead.astro'
import Quote from './blocks/general/Quote.astro'
import TableWrapper from './blocks/general/TableWrapper.astro'
@@ -17,6 +18,7 @@ const COMPONENTS: Record<string, AstroComponentFactory> = {
Figure,
TableWrapper,
Quote,
Lead,
Image: ContentImage,
}
const { node } = Astro.props

View File

@@ -0,0 +1,3 @@
<div class="lead">
<slot />
</div>

View File

@@ -19,9 +19,9 @@ tags:
- alchemical-materialism
- genesis
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. This first paragraph is the lead, so it should be larger and carry the drop cap.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. A sentence with *emphasis*, **strong text**, `inline code`, ~~struck text~~ and an [external link](https://example.com).
{% lead %}
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. This first paragraph is the lead, so it should be larger and carry the drop cap. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. A sentence with *emphasis*, **strong text**, `inline code`, ~~struck text~~ and an [external link](https://example.com).
{% /lead %}
## Heading two, long enough to wrap onto a second line

View File

@@ -1,6 +1,7 @@
import { callout } from './callout'
import { figure } from './figure'
import { footnote } from './footnote'
import { lead } from './lead'
import { quote } from './quote'
import { sidenote } from './sidenote'
import { tableWrapper } from './table-wrapper'
@@ -9,6 +10,7 @@ import { xref } from './xref'
export const generalBlocks = (collection: string) => ({
figure: figure(collection),
callout,
lead,
footnote,
sidenote,
tableWrapper,

View File

@@ -0,0 +1,7 @@
import { wrapper } from '@keystatic/core/content-components'
export const lead = wrapper({
label: 'Lead',
description: 'The opening paragraph, set larger and with the drop cap',
schema: {},
})

View File

@@ -1,15 +1,60 @@
import { fields } from '@keystatic/core'
import Markdoc, { type Node, type Schema } from '@markdoc/markdoc'
import Markdoc, { type Node, type RenderableTreeNode, type Schema } from '@markdoc/markdoc'
import { generalBlocks } from '@/keystatic/blocks/general'
import { ARTICLES } from '../constants/collections'
import { inlineOf } from './notes'
import { eachTag, firstTag, isTag } from './tree'
import type { NodeState, ResolveLink } from './types'
const { Tag } = Markdoc
function capitalise(children: RenderableTreeNode[]): void {
const paragraph = firstTag(children, 'p')
if (!paragraph) return
const [first, ...rest] = paragraph.children
if (typeof first !== 'string' || !first) return
paragraph.children = [
new Tag('span', { class: 'lead__cap' }, [first.slice(0, 1)]),
first.slice(1),
...rest,
]
}
function applyColumns(
children: RenderableTreeNode[],
columns: string[],
state: NodeState,
): void {
let widest = 0
eachTag(children, 'tr', (row) => {
widest = Math.max(widest, row.children.length)
row.children.forEach((cell, index) => {
const kind = columns[index]
if (!isTag(cell) || !kind || kind === 'default') return
if (kind === 'rowheader') {
if (cell.name !== 'td') return
cell.name = 'th'
cell.attributes.scope = 'row'
return
}
cell.attributes.class = [cell.attributes.class, kind].filter(Boolean).join(' ')
})
})
if (widest && widest !== columns.length)
state.problems.push({
level: 'warning',
message: `A table wrapper lists ${columns.length} column types for a table ${widest} columns wide.`,
})
}
const generated =
fields.markdoc.createMarkdocConfig({
components: generalBlocks(ARTICLES),
@@ -103,5 +148,13 @@ export function createTags(state: NodeState, resolveLink: ResolveLink): Record<s
]
},
},
lead: {
...generated.lead,
transform(node, config) {
const children = node.transformChildren(config)
capitalise(children)
return new Tag('Lead', node.transformAttributes(config), children)
},
},
}
}

25
src/lib/markdoc/tree.ts Normal file
View File

@@ -0,0 +1,25 @@
import Markdoc, { type RenderableTreeNode, type Tag } from '@markdoc/markdoc'
export const isTag = (node: RenderableTreeNode): node is Tag => Markdoc.Tag.isTag(node)
export function firstTag(nodes: RenderableTreeNode[], name: string): Tag | null {
for (const node of nodes) {
if (!isTag(node)) continue
if (node.name === name) return node
const found = firstTag(node.children, name)
if (found) return found
}
return null
}
export function eachTag(
nodes: RenderableTreeNode[],
name: string,
visit: (tag: Tag) => void,
): void {
for (const node of nodes) {
if (!isTag(node)) continue
if (node.name === name) visit(node)
else eachTag(node.children, name, visit)
}
}

View File

@@ -1,2 +0,0 @@
@layer components {
}

View File

@@ -1,2 +1,3 @@
@import url("prose.css");
@import url("blocks.css");
@import url("quote.css");
@import url("lead.css");

View File

@@ -0,0 +1,29 @@
@layer components {
.lead {
--prose-line: 1lh;
@mixin my var(--space-6);
display: flow-root;
font-size: var(--type-1);
line-height: var(--leading-prose-tight);
}
.lead__cap {
@mixin me var(--size-8);
float: inline-start;
display: grid;
place-items: center;
aspect-ratio: 1;
width: calc(var(--prose-line) * 3);
font-family: var(--font-display);
font-size: calc(var(--prose-line) * 1.9);
line-height: 1;
color: var(--color-text-inverse);
background-color: var(--color-surface-inverse);
}
}

View File

@@ -0,0 +1,79 @@
@layer components {
.quote {
@mixin my var(--space-11);
@mixin py var(--space-9);
display: grid;
gap: var(--size-9);
border-block: var(--border-boundary) solid var(--color-border-strong);
}
.prose {
& .quote {
& blockquote {
@mixin typo_heading-3;
@mixin ma var(--size-0);
@mixin ps var(--size-0);
font-size: var(--type-4);
font-style: normal;
line-height: 1.1;
color: var(--color-text-primary);
text-wrap: balance;
}
}
& .quote__source {
& a {
color: inherit;
text-decoration-color: var(--color-primary);
text-decoration-thickness: var(--size-px);
text-underline-offset: var(--size-2);
&:hover,
&:focus-visible {
color: var(--color-primary);
}
}
}
}
.quote__citation {
@mixin typo_label;
display: flex;
flex-wrap: wrap;
gap: var(--size-4);
font-size: var(--type--2);
color: var(--color-text-quarternary);
letter-spacing: var(--tracking-plate);
}
.quote__who {
color: var(--color-text-primary);
&::before {
content: "⟫ " / "";
}
}
.quote__source {
font-style: normal;
}
}
@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);
}
.quote__citation {
flex-direction: column;
gap: var(--size-2);
align-self: end;
}
}