Added definition list block and code block
This commit is contained in:
@@ -21,7 +21,8 @@
|
|||||||
"astro": "^7.0.4",
|
"astro": "^7.0.4",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
"sharp": "^0.35.4"
|
"sharp": "^0.35.4",
|
||||||
|
"shiki": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/check": "^0.9.10",
|
"@astrojs/check": "^0.9.10",
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -41,6 +41,9 @@ importers:
|
|||||||
sharp:
|
sharp:
|
||||||
specifier: ^0.35.4
|
specifier: ^0.35.4
|
||||||
version: 0.35.4(@types/node@22.20.4)
|
version: 0.35.4(@types/node@22.20.4)
|
||||||
|
shiki:
|
||||||
|
specifier: ^4.4.3
|
||||||
|
version: 4.4.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@astrojs/check':
|
'@astrojs/check':
|
||||||
specifier: ^0.9.10
|
specifier: ^0.9.10
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import ContentImage from '@/components/media/ContentImage.astro'
|
|||||||
|
|
||||||
import Figure from '../media/Figure.astro'
|
import Figure from '../media/Figure.astro'
|
||||||
import Callout from './blocks/general/Callout.astro'
|
import Callout from './blocks/general/Callout.astro'
|
||||||
|
import Code from './blocks/general/Code.astro'
|
||||||
|
import Definition from './blocks/general/Definitions/Definition.astro'
|
||||||
|
import DefinitionList from './blocks/general/Definitions/DefinitionList.astro'
|
||||||
import Lead from './blocks/general/Lead.astro'
|
import Lead from './blocks/general/Lead.astro'
|
||||||
import Quote from './blocks/general/Quote.astro'
|
import Quote from './blocks/general/Quote.astro'
|
||||||
import Reveal from './blocks/general/Reveal.astro'
|
import Reveal from './blocks/general/Reveal.astro'
|
||||||
@@ -17,9 +20,12 @@ type Props = { node: RenderableTreeNode | RenderableTreeNode[] }
|
|||||||
const COMPONENTS: Record<string, AstroComponentFactory> = {
|
const COMPONENTS: Record<string, AstroComponentFactory> = {
|
||||||
Callout,
|
Callout,
|
||||||
Reveal,
|
Reveal,
|
||||||
|
Code,
|
||||||
Figure,
|
Figure,
|
||||||
TableWrapper,
|
TableWrapper,
|
||||||
Quote,
|
Quote,
|
||||||
|
Definition,
|
||||||
|
DefinitionList,
|
||||||
Lead,
|
Lead,
|
||||||
Image: ContentImage,
|
Image: ContentImage,
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/components/markdoc/blocks/general/Code.astro
Normal file
14
src/components/markdoc/blocks/general/Code.astro
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
type Props = {
|
||||||
|
code: string
|
||||||
|
lang?: string
|
||||||
|
html?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { code, lang, html } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="code" data-language={lang}>
|
||||||
|
{lang && <span class="code__lang">{lang}</span>}
|
||||||
|
{html ? <Fragment set:html={html} /> : <pre tabindex="0"><code>{code}</code></pre>}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
import TermEntry, { type SeeLink } from '@/components/terms/TermEntry.astro'
|
||||||
|
import type { TermKind } from '@/lib/constants/terms'
|
||||||
|
import { slugify } from '@/lib/content/slug'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
term: string
|
||||||
|
kind?: TermKind
|
||||||
|
see?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { term, kind = 'entry', see = '' } = Astro.props
|
||||||
|
|
||||||
|
const links: SeeLink[] = see
|
||||||
|
.split(',')
|
||||||
|
.map((name) => name.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((name) => ({ title: name, href: `#${slugify(name)}` }))
|
||||||
|
---
|
||||||
|
|
||||||
|
<TermEntry id={slugify(term)} title={term} kind={kind} see={links} ink>
|
||||||
|
<slot />
|
||||||
|
</TermEntry>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<dl class="terms terms--cross">
|
||||||
|
<slot />
|
||||||
|
</dl>
|
||||||
@@ -7,4 +7,4 @@ type Props = { cover: Article['summary']['cover'] }
|
|||||||
const { image, ...rest } = Astro.props.cover
|
const { image, ...rest } = Astro.props.cover
|
||||||
---
|
---
|
||||||
|
|
||||||
{image && <Figure image={image} {...rest} size="full" eager />}
|
{image && <Figure image={image} {...rest} size="wide" eager />}
|
||||||
|
|||||||
42
src/components/terms/TermEntry.astro
Normal file
42
src/components/terms/TermEntry.astro
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
import { TERM_KINDS, type TermKind } from '@/lib/constants/terms'
|
||||||
|
|
||||||
|
export type SeeLink = {
|
||||||
|
title: string
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
kind: TermKind
|
||||||
|
see?: SeeLink[]
|
||||||
|
ink?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, title, kind, see = [], ink } = Astro.props
|
||||||
|
|
||||||
|
const label = TERM_KINDS.find((option) => option.value === kind)?.label ?? kind
|
||||||
|
---
|
||||||
|
|
||||||
|
<dt class:list={['term', ink && 'dark']} id={id}>
|
||||||
|
<span class="term__name">{title}</span>
|
||||||
|
<span class="term__kind">{label}</span>
|
||||||
|
</dt>
|
||||||
|
|
||||||
|
<dd class="term__body">
|
||||||
|
<p class="term__definition">
|
||||||
|
<slot />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{see.length > 0 && (
|
||||||
|
<p class="term__see">
|
||||||
|
<span class="term__see-label">See</span>
|
||||||
|
{see.map((related) => (
|
||||||
|
<a class="term__link" href={related.href}>
|
||||||
|
{related.title}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</dd>
|
||||||
@@ -144,8 +144,67 @@ for (const [key, value] of Object.entries(crucible)) {
|
|||||||
credit="Dave Damage"
|
credit="Dave Damage"
|
||||||
effect="xeroxed"
|
effect="xeroxed"
|
||||||
ratio="16-9"
|
ratio="16-9"
|
||||||
size="wide" /%}
|
size="inline" /%}
|
||||||
|
|
||||||
Aliquam erat volutpat. Fusce sed velit at arcu tincidunt vulputate{% sidenote type="example" marker="‡" note="A second sidenote, typed as an example." /%}. Nam vitae lacus a nisl ultrices dictum.
|
Aliquam erat volutpat. Fusce sed velit at arcu tincidunt vulputate{% sidenote type="example" marker="‡" note="A second sidenote, typed as an example." /%}. Nam vitae lacus a nisl ultrices dictum.
|
||||||
|
|
||||||
Finally, a link to another article: {% xref reference="the-crucible" %}The Crucible{% /xref %}.
|
Finally, a link to another article: {% xref reference="the-crucible" %}The Crucible{% /xref %}.
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const crucible = { stage: 'prima-materia', heat: 3 }
|
||||||
|
|
||||||
|
function temper(work: typeof crucible): 'calcined' | 'raw' {
|
||||||
|
return work.heat > 5 ? 'calcined' : 'raw'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A fence with no language, which should render unhighlighted and with no label:
|
||||||
|
|
||||||
|
```
|
||||||
|
Prima materia — the undifferentiated ground.
|
||||||
|
Nothing is named yet.
|
||||||
|
```
|
||||||
|
|
||||||
|
A long line, which should scroll inside the block rather than widen the article:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.prose :is(h2, h3, h4) + .table-wrapper thead th { border-block-end: var(--border-card) solid var(--color-border-strong); color: inherit; background-color: transparent; }
|
||||||
|
```
|
||||||
|
|
||||||
|
An unknown language, which should render plain and raise a warning in the dev banner:
|
||||||
|
|
||||||
|
```brainfuck
|
||||||
|
+[----->+++<]>+.
|
||||||
|
```
|
||||||
|
|
||||||
|
{% callout type="example" title="Code inside a block" %}
|
||||||
|
The frame should sit inside the callout body, not fight it:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export const isTag = (node) => Markdoc.Tag.isTag(node)
|
||||||
|
```
|
||||||
|
{% /callout %}
|
||||||
|
|
||||||
|
## Spoiler
|
||||||
|
|
||||||
|
Duis aute irure dolor in reprehenderit, and then {% spoiler %}the prima materia was the vessel all along{% /spoiler %}, which changes the reading of everything above.
|
||||||
|
|
||||||
|
A spoiler long enough to wrap across a line break: {% spoiler %}Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua{% /spoiler %}. And text after it.
|
||||||
|
|
||||||
|
## Definitions
|
||||||
|
|
||||||
|
{% definitions %}
|
||||||
|
{% definition term="Prima Materia" kind="prime" see="Calcination, Solutio" %}
|
||||||
|
The undifferentiated ground. Nothing is named yet, which is precisely what makes it workable.
|
||||||
|
{% /definition %}
|
||||||
|
|
||||||
|
{% definition term="Calcination" kind="stage" see="Prima Materia" %}
|
||||||
|
Burned down to what will not burn.
|
||||||
|
{% /definition %}
|
||||||
|
|
||||||
|
{% definition term="Solutio" kind="stage" %}
|
||||||
|
Dissolved back into flow, so that forms loosen.
|
||||||
|
{% /definition %}
|
||||||
|
{% /definitions %}
|
||||||
|
|||||||
27
src/keystatic/blocks/general/definition-list/definition.ts
Normal file
27
src/keystatic/blocks/general/definition-list/definition.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { fields } from '@keystatic/core'
|
||||||
|
import { wrapper } from '@keystatic/core/content-components'
|
||||||
|
|
||||||
|
import { TERM_KINDS } from '@/lib/constants/terms'
|
||||||
|
|
||||||
|
export const definition = wrapper({
|
||||||
|
label: 'Definition',
|
||||||
|
description: 'One term and its meaning',
|
||||||
|
forSpecificLocations: true,
|
||||||
|
schema: {
|
||||||
|
term: fields.text({
|
||||||
|
label: 'Term',
|
||||||
|
validation: {
|
||||||
|
isRequired: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
kind: fields.select({
|
||||||
|
label: 'Kind',
|
||||||
|
options: TERM_KINDS,
|
||||||
|
defaultValue: 'entry',
|
||||||
|
}),
|
||||||
|
see: fields.text({
|
||||||
|
label: 'See also',
|
||||||
|
description: 'Other terms in this list, comma separated',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
13
src/keystatic/blocks/general/definition-list/definitions.ts
Normal file
13
src/keystatic/blocks/general/definition-list/definitions.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { repeating } from '@keystatic/core/content-components'
|
||||||
|
|
||||||
|
export const definitions = repeating({
|
||||||
|
label: 'Definitions',
|
||||||
|
description: 'A list of terms defined for this article only',
|
||||||
|
children: ['definition'],
|
||||||
|
validation: {
|
||||||
|
children: {
|
||||||
|
min: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
schema: {},
|
||||||
|
})
|
||||||
@@ -1,21 +1,27 @@
|
|||||||
import { callout } from './callout'
|
import { callout } from './callout'
|
||||||
|
import { definition } from './definition-list/definition'
|
||||||
|
import { definitions } from './definition-list/definitions'
|
||||||
import { figure } from './figure'
|
import { figure } from './figure'
|
||||||
import { footnote } from './footnote'
|
import { footnote } from './footnote'
|
||||||
import { lead } from './lead'
|
import { lead } from './lead'
|
||||||
import { quote } from './quote'
|
import { quote } from './quote'
|
||||||
import { reveal } from './reveal'
|
import { reveal } from './reveal'
|
||||||
import { sidenote } from './sidenote'
|
import { sidenote } from './sidenote'
|
||||||
|
import { spoiler } from './spoiler'
|
||||||
import { tableWrapper } from './table-wrapper'
|
import { tableWrapper } from './table-wrapper'
|
||||||
import { xref } from './xref'
|
import { xref } from './xref'
|
||||||
|
|
||||||
export const generalBlocks = (collection: string) => ({
|
export const generalBlocks = (collection: string) => ({
|
||||||
figure: figure(collection),
|
figure: figure(collection),
|
||||||
callout,
|
callout,
|
||||||
|
definition,
|
||||||
|
definitions,
|
||||||
lead,
|
lead,
|
||||||
footnote,
|
footnote,
|
||||||
sidenote,
|
sidenote,
|
||||||
tableWrapper,
|
tableWrapper,
|
||||||
quote,
|
quote,
|
||||||
reveal,
|
reveal,
|
||||||
|
spoiler,
|
||||||
xref: xref(),
|
xref: xref(),
|
||||||
})
|
})
|
||||||
|
|||||||
15
src/keystatic/blocks/general/spoiler.ts
Normal file
15
src/keystatic/blocks/general/spoiler.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { mark } from '@keystatic/core/content-components'
|
||||||
|
|
||||||
|
import { eyeOffIcon } from '@keystar/ui/icon/icons/eyeOffIcon'
|
||||||
|
|
||||||
|
export const spoiler = mark({
|
||||||
|
label: 'Spoiler',
|
||||||
|
icon: eyeOffIcon,
|
||||||
|
tag: 'span',
|
||||||
|
schema: {},
|
||||||
|
style: {
|
||||||
|
backgroundColor: '#2A2A2A',
|
||||||
|
color: '#F5F5F5',
|
||||||
|
padding: '0 2px',
|
||||||
|
},
|
||||||
|
})
|
||||||
11
src/lib/constants/terms.ts
Normal file
11
src/lib/constants/terms.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export const TERM_KINDS = [
|
||||||
|
{ label: 'Entry', value: 'entry' },
|
||||||
|
{ label: 'Prime', value: 'prime' },
|
||||||
|
{ label: 'Essence', value: 'essence' },
|
||||||
|
{ label: 'Element', value: 'element' },
|
||||||
|
{ label: 'Stage', value: 'stage' },
|
||||||
|
{ label: 'Tier', value: 'tier' },
|
||||||
|
{ label: 'Premise', value: 'premise' },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type TermKind = (typeof TERM_KINDS)[number]['value']
|
||||||
38
src/lib/markdoc/highlight.ts
Normal file
38
src/lib/markdoc/highlight.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { createHighlighter, type Highlighter } from 'shiki'
|
||||||
|
|
||||||
|
const THEMES = { light: 'github-light', dark: 'github-dark' } as const
|
||||||
|
|
||||||
|
const LANGS = [
|
||||||
|
'astro',
|
||||||
|
'bash',
|
||||||
|
'css',
|
||||||
|
'diff',
|
||||||
|
'html',
|
||||||
|
'ini',
|
||||||
|
'js',
|
||||||
|
'json',
|
||||||
|
'jsonc',
|
||||||
|
'jsx',
|
||||||
|
'md',
|
||||||
|
'python',
|
||||||
|
'sql',
|
||||||
|
'ts',
|
||||||
|
'tsx',
|
||||||
|
'xml',
|
||||||
|
'yaml',
|
||||||
|
]
|
||||||
|
|
||||||
|
export type Highlight = (code: string, lang: string) => string | null
|
||||||
|
|
||||||
|
let ready: Promise<Highlighter> | null = null
|
||||||
|
|
||||||
|
export async function createHighlight(): Promise<Highlight> {
|
||||||
|
ready ??= createHighlighter({ themes: Object.values(THEMES), langs: LANGS })
|
||||||
|
const highlighter = await ready
|
||||||
|
const loaded = new Set(highlighter.getLoadedLanguages())
|
||||||
|
|
||||||
|
return (code, lang) =>
|
||||||
|
loaded.has(lang)
|
||||||
|
? highlighter.codeToHtml(code, { lang, themes: THEMES, defaultColor: false })
|
||||||
|
: null
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import Markdoc from '@markdoc/markdoc'
|
import Markdoc from '@markdoc/markdoc'
|
||||||
|
|
||||||
import { createDiagnostics } from '../diagnostics'
|
import { createDiagnostics } from '../diagnostics'
|
||||||
|
import { createHighlight } from './highlight'
|
||||||
import { createNodes } from './nodes'
|
import { createNodes } from './nodes'
|
||||||
import { createTags } from './tags'
|
import { createTags } from './tags'
|
||||||
import type { NodeState, Renderable, Rendered, ResolveLink } from './types'
|
import type { NodeState, Renderable, Rendered, ResolveLink } from './types'
|
||||||
@@ -21,11 +22,13 @@ export async function render(
|
|||||||
ids: new Set(),
|
ids: new Set(),
|
||||||
footnotes: [],
|
footnotes: [],
|
||||||
sidenoteCount: 0,
|
sidenoteCount: 0,
|
||||||
|
spoilerCount: 0,
|
||||||
markers: new Set(),
|
markers: new Set(),
|
||||||
problems: [],
|
problems: [],
|
||||||
}
|
}
|
||||||
|
const highlight = await createHighlight()
|
||||||
const config = {
|
const config = {
|
||||||
nodes: createNodes(state),
|
nodes: createNodes(state, highlight),
|
||||||
tags: createTags(state, resolveLink),
|
tags: createTags(state, resolveLink),
|
||||||
}
|
}
|
||||||
const problems = createDiagnostics()
|
const problems = createDiagnostics()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Markdoc, { type Node, type Schema } from '@markdoc/markdoc'
|
import Markdoc, { type Node, type Schema } from '@markdoc/markdoc'
|
||||||
|
|
||||||
import { slugify } from '../content/slug'
|
import { slugify } from '../content/slug'
|
||||||
|
import type { Highlight } from './highlight'
|
||||||
import type { NodeState } from './types'
|
import type { NodeState } from './types'
|
||||||
|
|
||||||
const { Tag } = Markdoc
|
const { Tag } = Markdoc
|
||||||
@@ -18,8 +19,27 @@ function uniqueId(text: string, ids: Set<string>): string {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createNodes(state: NodeState): Record<string, Schema> {
|
export function createNodes(state: NodeState, highlight: Highlight): Record<string, Schema> {
|
||||||
return {
|
return {
|
||||||
|
fence: {
|
||||||
|
...Markdoc.nodes.fence,
|
||||||
|
transform(node) {
|
||||||
|
/* transformAttributes renames `language` to `data-language`, so the
|
||||||
|
raw attribute is the honest source. The content carries a trailing
|
||||||
|
newline that would render as a blank last line. */
|
||||||
|
const lang = String(node.attributes.language ?? '').trim()
|
||||||
|
const code = String(node.attributes.content ?? '').replace(/\n$/, '')
|
||||||
|
const html = lang ? highlight(code, lang) : null
|
||||||
|
|
||||||
|
if (lang && !html)
|
||||||
|
state.problems.push({
|
||||||
|
level: 'warning',
|
||||||
|
message: `No highlighting for language "${lang}" — add it to LANGS in highlight.ts.`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Tag('Code', { code, lang: lang || undefined, html: html ?? undefined }, [])
|
||||||
|
},
|
||||||
|
},
|
||||||
document: {
|
document: {
|
||||||
...Markdoc.nodes.document,
|
...Markdoc.nodes.document,
|
||||||
transform: (node, config) => node.transformChildren(config),
|
transform: (node, config) => node.transformChildren(config),
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ const generated =
|
|||||||
components: generalBlocks(ARTICLES),
|
components: generalBlocks(ARTICLES),
|
||||||
render: {
|
render: {
|
||||||
tags: {
|
tags: {
|
||||||
|
definitions: 'DefinitionList',
|
||||||
|
definition: 'Definition',
|
||||||
callout: 'Callout',
|
callout: 'Callout',
|
||||||
figure: 'Figure',
|
figure: 'Figure',
|
||||||
tableWrapper: 'TableWrapper',
|
tableWrapper: 'TableWrapper',
|
||||||
@@ -84,6 +86,16 @@ export function createTags(state: NodeState, resolveLink: ResolveLink): Record<s
|
|||||||
])
|
])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
spoiler: {
|
||||||
|
...generated.spoiler,
|
||||||
|
transform(node, config) {
|
||||||
|
const id = `spoiler-${(state.spoilerCount += 1)}`
|
||||||
|
return new Tag('span', { class: 'spoiler' }, [
|
||||||
|
new Tag('input', { id, class: 'spoiler__toggle', type: 'checkbox' }, []),
|
||||||
|
new Tag('label', { class: 'spoiler__text', for: id }, node.transformChildren(config)),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
},
|
||||||
sidenote: {
|
sidenote: {
|
||||||
...generated.sidenote,
|
...generated.sidenote,
|
||||||
transform(node, config) {
|
transform(node, config) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export type NodeState = {
|
|||||||
ids: Set<string>
|
ids: Set<string>
|
||||||
footnotes: Footnote[]
|
footnotes: Footnote[]
|
||||||
sidenoteCount: number
|
sidenoteCount: number
|
||||||
|
spoilerCount: number
|
||||||
markers: Set<string>
|
markers: Set<string>
|
||||||
problems: Problem[]
|
problems: Problem[]
|
||||||
}
|
}
|
||||||
|
|||||||
49
src/styles/components/code.css
Normal file
49
src/styles/components/code.css
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
@layer components.blocks {
|
||||||
|
.code {
|
||||||
|
@mixin my var(--space-9);
|
||||||
|
border: var(--border-card) solid var(--color-border-strong);
|
||||||
|
background-color: var(--color-surface-elevated-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code__lang {
|
||||||
|
@mixin typo_label;
|
||||||
|
@mixin py var(--size-3);
|
||||||
|
@mixin px var(--size-7);
|
||||||
|
@mixin border-b var(--border-card), solid, var(--color-border-strong);
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--type--3);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
|
||||||
|
background-color: var(--color-surface-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code pre {
|
||||||
|
@mixin ma var(--size-0);
|
||||||
|
@mixin py var(--size-7);
|
||||||
|
@mixin px var(--size-8);
|
||||||
|
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 0;
|
||||||
|
tab-size: 2;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code pre code {
|
||||||
|
@mixin pa var(--size-0);
|
||||||
|
|
||||||
|
font: inherit;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code .shiki,
|
||||||
|
.code .shiki span {
|
||||||
|
color: var(--shiki-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .code .shiki,
|
||||||
|
.dark .code .shiki span {
|
||||||
|
color: var(--shiki-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,3 +7,6 @@
|
|||||||
@import url("footnote.css");
|
@import url("footnote.css");
|
||||||
@import url("sidenote.css");
|
@import url("sidenote.css");
|
||||||
@import url("figure.css");
|
@import url("figure.css");
|
||||||
|
@import url("code.css");
|
||||||
|
@import url("terms.css");
|
||||||
|
@import url("spoiler.css");
|
||||||
|
|||||||
35
src/styles/components/spoiler.css
Normal file
35
src/styles/components/spoiler.css
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@layer components.blocks {
|
||||||
|
.spoiler__toggle {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
clip-path: inset(50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler__text {
|
||||||
|
@mixin px var(--space-1);
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
color: transparent;
|
||||||
|
background-color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler__toggle:checked + .spoiler__text {
|
||||||
|
cursor: text;
|
||||||
|
user-select: text;
|
||||||
|
color: inherit;
|
||||||
|
background-color: var(--color-surface-elevated-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler__toggle:focus-visible + .spoiler__text {
|
||||||
|
@mixin focus-ring;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
@layer components.blocks {
|
@layer components.blocks {
|
||||||
.table-wrapper {
|
.table-wrapper {
|
||||||
/* The header treatment. Ink by default: a standalone wrapper is a
|
|
||||||
citable object. Position overrides these — nothing is authored. */
|
|
||||||
--table-header-fg: var(--color-text-inverse);
|
--table-header-fg: var(--color-text-inverse);
|
||||||
--table-header-bg: var(--color-surface-inverse);
|
--table-header-bg: var(--color-surface-inverse);
|
||||||
--table-header-rule: 0;
|
--table-header-rule: 0;
|
||||||
@@ -59,11 +57,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Underline header: a table introduced by a heading, or one inside a
|
|
||||||
block, is part of the argument rather than a reference. A block with
|
|
||||||
its own surface redeclares --table-header-bg so the sticky first
|
|
||||||
column on mobile paints the right colour. */
|
|
||||||
|
|
||||||
.prose :is(h2, h3, h4) + .table-wrapper,
|
.prose :is(h2, h3, h4) + .table-wrapper,
|
||||||
:is(.callout, .reveal) .table-wrapper {
|
:is(.callout, .reveal) .table-wrapper {
|
||||||
--table-header-fg: inherit;
|
--table-header-fg: inherit;
|
||||||
@@ -72,47 +65,6 @@
|
|||||||
--table-header-edge: var(--color-border-strong);
|
--table-header-edge: var(--color-border-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === MATRIX ===
|
|
||||||
A correspondence table: the cells carry hits, not values, so it has to
|
|
||||||
read as a grid — rules on both axes, everything centred, and a miss
|
|
||||||
stated rather than left looking like an oversight. */
|
|
||||||
|
|
||||||
.table-wrapper--matrix {
|
|
||||||
& :is(th, td) {
|
|
||||||
@mixin px var(--space-4);
|
|
||||||
@mixin border-r var(--border-hairline), solid, var(--color-border-subtle);
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
& :is(th, td):last-child {
|
|
||||||
border-inline-end: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The first column is the subject; it keeps its reading direction. */
|
|
||||||
& :is(thead, tbody) :is(th, td):first-child {
|
|
||||||
@mixin border-r var(--border-card), solid, var(--color-border-strong);
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Axis labels are short. Let them wrap instead of widening a column. */
|
|
||||||
& thead th {
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* A hit: the cell itself is the mark. */
|
|
||||||
& tbody td.is-set {
|
|
||||||
background-color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* A miss, said out loud. */
|
|
||||||
& tbody td:empty::before {
|
|
||||||
content: "·";
|
|
||||||
color: var(--color-text-quiet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (--bp-mobile) {
|
@media (--bp-mobile) {
|
||||||
.table-wrapper .table-scroll thead th:first-child {
|
.table-wrapper .table-scroll thead th:first-child {
|
||||||
border-inline-end-color: var(--table-header-edge);
|
border-inline-end-color: var(--table-header-edge);
|
||||||
|
|||||||
103
src/styles/components/terms.css
Normal file
103
src/styles/components/terms.css
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
@layer components.blocks {
|
||||||
|
.terms {
|
||||||
|
@mixin ma var(--size-0);
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-8);
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term__name {
|
||||||
|
@mixin typo_heading-3;
|
||||||
|
hyphens: auto;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term__kind {
|
||||||
|
@mixin typo_label;
|
||||||
|
|
||||||
|
font-size: var(--type--2);
|
||||||
|
color: var(--color-text-quarternary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.term__body {
|
||||||
|
@mixin ma var(--size-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.term__definition {
|
||||||
|
@mixin typo_body;
|
||||||
|
@mixin ma var(--size-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.term__see {
|
||||||
|
@mixin typo_label;
|
||||||
|
@mixin mt var(--size-8);
|
||||||
|
@mixin mb var(--size-0);
|
||||||
|
@mixin pt var(--size-5);
|
||||||
|
@mixin border-t var(--border-hairline), solid, var(--color-border-subtle);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--size-5);
|
||||||
|
|
||||||
|
font-size: var(--type--2);
|
||||||
|
color: var(--color-text-quarternary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.term__see-label::after {
|
||||||
|
content: " ⟫";
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms .term__link {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: var(--size-2);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms--cross {
|
||||||
|
@mixin my var(--space-9);
|
||||||
|
|
||||||
|
grid-template-columns: minmax(0, 11rem) minmax(0, 1fr);
|
||||||
|
gap: var(--border-card);
|
||||||
|
border: var(--border-card) solid var(--color-border-strong);
|
||||||
|
background-color: var(--color-border-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms--cross .term {
|
||||||
|
@mixin py var(--size-8);
|
||||||
|
@mixin px var(--size-9);
|
||||||
|
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
background-color: var(--color-surface-base);
|
||||||
|
|
||||||
|
&:not(:first-of-type) {
|
||||||
|
box-shadow: 0 calc(-1 * var(--border-card)) 0 var(--color-border-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms--cross .term:target {
|
||||||
|
color: var(--color-text-on-accent);
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms--cross .term:target .term__kind {
|
||||||
|
color: var(--color-text-on-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms--cross .term__body {
|
||||||
|
@mixin py var(--size-8);
|
||||||
|
@mixin px var(--size-9);
|
||||||
|
|
||||||
|
background-color: var(--color-surface-elevated-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user