Styling for Quote and Lead Content Blocks
This commit is contained in:
@@ -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
|
||||
|
||||
3
src/components/markdoc/blocks/general/Lead.astro
Normal file
3
src/components/markdoc/blocks/general/Lead.astro
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="lead">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
7
src/keystatic/blocks/general/lead.ts
Normal file
7
src/keystatic/blocks/general/lead.ts
Normal 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: {},
|
||||
})
|
||||
@@ -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
25
src/lib/markdoc/tree.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
@layer components {
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
@import url("prose.css");
|
||||
@import url("blocks.css");
|
||||
@import url("quote.css");
|
||||
@import url("lead.css");
|
||||
|
||||
29
src/styles/components/lead.css
Normal file
29
src/styles/components/lead.css
Normal 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);
|
||||
}
|
||||
}
|
||||
79
src/styles/components/quote.css
Normal file
79
src/styles/components/quote.css
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user