diff --git a/src/components/markdoc/Node.astro b/src/components/markdoc/Node.astro index b25d4f4..a80a566 100644 --- a/src/components/markdoc/Node.astro +++ b/src/components/markdoc/Node.astro @@ -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 = { Figure, TableWrapper, Quote, + Lead, Image: ContentImage, } const { node } = Astro.props diff --git a/src/components/markdoc/blocks/general/Lead.astro b/src/components/markdoc/blocks/general/Lead.astro new file mode 100644 index 0000000..a158932 --- /dev/null +++ b/src/components/markdoc/blocks/general/Lead.astro @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/content/general/articles/prima-materia.mdoc b/src/content/general/articles/prima-materia.mdoc index 6aa2bb6..7ad1438 100644 --- a/src/content/general/articles/prima-materia.mdoc +++ b/src/content/general/articles/prima-materia.mdoc @@ -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 diff --git a/src/keystatic/blocks/general/index.ts b/src/keystatic/blocks/general/index.ts index 02e8e26..21b85a9 100644 --- a/src/keystatic/blocks/general/index.ts +++ b/src/keystatic/blocks/general/index.ts @@ -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, diff --git a/src/keystatic/blocks/general/lead.ts b/src/keystatic/blocks/general/lead.ts new file mode 100644 index 0000000..4760733 --- /dev/null +++ b/src/keystatic/blocks/general/lead.ts @@ -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: {}, +}) diff --git a/src/lib/markdoc/tags.ts b/src/lib/markdoc/tags.ts index 3d5ecc1..79cb697 100644 --- a/src/lib/markdoc/tags.ts +++ b/src/lib/markdoc/tags.ts @@ -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 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) + } +} diff --git a/src/styles/components/blocks.css b/src/styles/components/blocks.css deleted file mode 100644 index 4a38b34..0000000 --- a/src/styles/components/blocks.css +++ /dev/null @@ -1,2 +0,0 @@ -@layer components { -} diff --git a/src/styles/components/index.css b/src/styles/components/index.css index 9734dbb..5c4ec0f 100644 --- a/src/styles/components/index.css +++ b/src/styles/components/index.css @@ -1,2 +1,3 @@ @import url("prose.css"); -@import url("blocks.css"); +@import url("quote.css"); +@import url("lead.css"); diff --git a/src/styles/components/lead.css b/src/styles/components/lead.css new file mode 100644 index 0000000..0cc56c7 --- /dev/null +++ b/src/styles/components/lead.css @@ -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); + } +} diff --git a/src/styles/components/quote.css b/src/styles/components/quote.css new file mode 100644 index 0000000..099474c --- /dev/null +++ b/src/styles/components/quote.css @@ -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; + } +}