diff --git a/src/components/markdoc/blocks/general/FootnoteList.astro b/src/components/markdoc/blocks/general/FootnoteList.astro index 58764f0..e4170b7 100644 --- a/src/components/markdoc/blocks/general/FootnoteList.astro +++ b/src/components/markdoc/blocks/general/FootnoteList.astro @@ -12,10 +12,12 @@ const { footnotes } = Astro.props
    {footnotes.map((footnote) => (
  1. - {' '} - - ↩ - +

    + {' '} + + ↩ + +

  2. ))}
diff --git a/src/components/markdoc/blocks/general/SidenoteList.astro b/src/components/markdoc/blocks/general/SidenoteList.astro deleted file mode 100644 index f474147..0000000 --- a/src/components/markdoc/blocks/general/SidenoteList.astro +++ /dev/null @@ -1,67 +0,0 @@ ---- -import Node from '@/components/markdoc/Node.astro' -import type { Sidenote } from '@/lib/markdoc/types' - -type Props = { sidenotes: Sidenote[] } - -const { sidenotes } = Astro.props ---- - -{sidenotes.map((sidenote) => ( - -))} - - diff --git a/src/content/general/articles/prima-materia.mdoc b/src/content/general/articles/prima-materia.mdoc index 0c9b635..a453055 100644 --- a/src/content/general/articles/prima-materia.mdoc +++ b/src/content/general/articles/prima-materia.mdoc @@ -98,30 +98,43 @@ for (const [key, value] of Object.entries(crucible)) { ## Tables -{% tableWrapper variant="default" caption="A wrapped table with a caption" %} +{% tableWrapper + variant="default" + caption="Stages of the Work" + columns=["rowheader", "key", "num", "num", "text"] %} {% table %} - Stage +- No - Heat +- Hours - Notes --- - Prima Materia +- I - 3 -- Longer cell text to see how wrapping behaves in a data cell +- 0.25 +- The undifferentiated ground; nothing is named yet. +--- +- Calcination +- II +- 7 +- 1.50 +- Burned down to what will not burn. --- - Solutio +- III - 5 -- Short +- 12 +- Dissolved back into flow; forms loosen. +--- +- Tincture +- VII +- 10 +- 120.75 +- The finished colour, carried into everything it touches. {% /table %} {% /tableWrapper %} -{% table %} -- Column A -- Column B ---- -- A bare table -- No wrapper -{% /table %} - ## Images {% figure diff --git a/src/keystatic/blocks/general/table-wrapper.ts b/src/keystatic/blocks/general/table-wrapper.ts index 1423843..b0a0a01 100644 --- a/src/keystatic/blocks/general/table-wrapper.ts +++ b/src/keystatic/blocks/general/table-wrapper.ts @@ -1,7 +1,7 @@ import { fields } from '@keystatic/core' import { wrapper } from '@keystatic/core/content-components' -import { TABLE_VARIANTS } from '@/lib/constants/blocks/table' +import { COLUMN_TYPES, TABLE_VARIANTS } from '@/lib/constants/blocks/table' export const tableWrapper = wrapper({ label: 'Table Wrapper', @@ -15,5 +15,17 @@ export const tableWrapper = wrapper({ caption: fields.text({ label: 'Caption', }), + columns: fields.array( + fields.select({ + label: 'Type', + options: COLUMN_TYPES, + defaultValue: 'default', + }), + { + label: 'Columns', + description: 'One per column, left to right.', + itemLabel: (item) => item.value, + }, + ), }, }) diff --git a/src/lib/constants/blocks/table.ts b/src/lib/constants/blocks/table.ts index 4fc37ed..1cf6168 100644 --- a/src/lib/constants/blocks/table.ts +++ b/src/lib/constants/blocks/table.ts @@ -4,13 +4,34 @@ export const TABLE_VARIANTS = [ value: 'default', }, { - label: 'Random', - value: 'random', + label: 'Matrix', + value: 'matrix', }, - { - label: 'Icon', - value: 'icon', - }, -] +] as const export type TableVariant = (typeof TABLE_VARIANTS)[number]['value'] + +export const COLUMN_TYPES = [ + { + label: 'Default', + value: 'default', + }, + { + label: 'Number', + value: 'num', + }, + { + label: 'Key', + value: 'key', + }, + { + label: 'Sentence', + value: 'text', + }, + { + label: 'Row header', + value: 'rowheader', + }, +] as const + +export type ColumnType = (typeof COLUMN_TYPES)[number]['value'] diff --git a/src/lib/markdoc/index.ts b/src/lib/markdoc/index.ts index 66441ff..fa40517 100644 --- a/src/lib/markdoc/index.ts +++ b/src/lib/markdoc/index.ts @@ -20,7 +20,7 @@ export async function render( headings: [], ids: new Set(), footnotes: [], - sidenotes: [], + sidenoteCount: 0, markers: new Set(), problems: [], } @@ -41,7 +41,6 @@ export async function render( tree, headings: state.headings, footnotes: state.footnotes, - sidenotes: state.sidenotes, diagnostics: problems.list, } } diff --git a/src/lib/markdoc/tags.ts b/src/lib/markdoc/tags.ts index 1e00e30..8dc8a61 100644 --- a/src/lib/markdoc/tags.ts +++ b/src/lib/markdoc/tags.ts @@ -95,9 +95,8 @@ export function createTags(state: NodeState, resolveLink: ResolveLink): Record child.tag === 'table').length + const tables = node.children.filter( + (child) => child.type === 'table' || child.tag === 'table', + ).length return tables === 1 ? [] : [ { id: 'table-wrapper-children', - level: 'warning' as const, - message: `A table wrapper should hold exactly one table, this one holds ${tables}.`, + level: 'error' as const, + message: `A table wrapper holds exactly one table, this one holds ${tables}.`, }, ] }, diff --git a/src/lib/markdoc/types.ts b/src/lib/markdoc/types.ts index 9a118e5..79217d7 100644 --- a/src/lib/markdoc/types.ts +++ b/src/lib/markdoc/types.ts @@ -13,13 +13,6 @@ export type Footnote = { children: RenderableTreeNode[] } -export type Sidenote = { - n: number - marker: string - type: string - children: RenderableTreeNode[] -} - export type Problem = { level: Level message: string @@ -29,7 +22,7 @@ export type NodeState = { headings: Heading[] ids: Set footnotes: Footnote[] - sidenotes: Sidenote[] + sidenoteCount: number markers: Set problems: Problem[] } @@ -42,7 +35,6 @@ export type Rendered = { tree: RenderableTreeNode headings: Heading[] footnotes: Footnote[] - sidenotes: Sidenote[] diagnostics: Diagnostic[] } diff --git a/src/styles/components/footnote.css b/src/styles/components/footnote.css new file mode 100644 index 0000000..1eff1c0 --- /dev/null +++ b/src/styles/components/footnote.css @@ -0,0 +1,109 @@ +@layer components.blocks { + .footnote-ref { + font-family: var(--font-mono); + font-size: 0.7em; + font-weight: var(--font-weight-emphasis); + line-height: 1; + + & a { + @mixin px var(--size-1); + + color: var(--color-text-primary); + text-decoration: none; + + &:hover { + color: var(--color-primary); + } + + &:focus-visible { + @mixin focus-ring; + } + } + } + + .footnotes { + --footnote-marker: 1.5rem; + + @mixin mt var(--space-10); + @mixin pt var(--space-6); + @mixin border-t var(--border-boundary), solid, var(--color-border-strong); + + counter-reset: fn; + + & .footnotes__title { + @mixin typo_label; + @mixin mt var(--size-0); + @mixin mb var(--size-5); + } + + & ol { + @mixin ma var(--size-0); + @mixin pl var(--size-0); + + display: flex; + flex-direction: column; + gap: var(--size-4); + list-style: none; + + & li { + @mixin typo_body-small; + + counter-increment: fn; + scroll-margin-block-start: calc(var(--layout-header-height) + var(--size-8)); + + display: grid; + grid-template-columns: var(--footnote-marker) minmax(0, 1fr); + column-gap: var(--slot-prose-gap); + + color: var(--color-text-secondary); + + &::before { + content: counter(fn); + font-family: var(--font-mono); + font-weight: var(--font-weight-emphasis); + color: var(--color-text-primary); + } + + &:target { + color: var(--color-text-primary); + + &::before { + color: var(--color-primary); + } + } + } + } + + & .footnotes__note { + @mixin ma var(--size-0); + } + + & a { + color: var(--color-text-link); + text-decoration: underline; + text-decoration-thickness: var(--size-1); + text-underline-offset: var(--size-2); + + &:visited { + color: var(--color-text-link-visited); + } + + &:hover { + color: var(--color-text-link-hover); + } + + &:focus-visible { + @mixin focus-ring; + } + } + + & a.footnotes__back { + color: var(--color-text-quiet); + text-decoration: none; + + &:hover { + color: var(--color-text-primary); + } + } + } +} diff --git a/src/styles/components/index.css b/src/styles/components/index.css index 21d6e13..af70898 100644 --- a/src/styles/components/index.css +++ b/src/styles/components/index.css @@ -3,3 +3,6 @@ @import url("lead.css"); @import url("callout.css"); @import url("reveal.css"); +@import url("table-wrapper.css"); +@import url("footnote.css"); +@import url("sidenote.css"); diff --git a/src/styles/components/prose.css b/src/styles/components/prose.css index 450d1cb..6631ba4 100644 --- a/src/styles/components/prose.css +++ b/src/styles/components/prose.css @@ -335,23 +335,5 @@ @mixin border-t var(--border-card), solid, var(--color-border-strong); } } - - & .num { - font-variant-numeric: tabular-nums; - text-align: right; - white-space: nowrap; - } - - & .key { - font-weight: var(--font-weight-emphasis); - } - - td.text { - font-family: var(--font-body); - font-size: 1.077em; - line-height: 1.55; - color: var(--color-text-secondary); - text-wrap: pretty; - } } } diff --git a/src/styles/components/sidenote.css b/src/styles/components/sidenote.css new file mode 100644 index 0000000..cfa9cb1 --- /dev/null +++ b/src/styles/components/sidenote.css @@ -0,0 +1,80 @@ +@layer components.blocks { + .sidenote-ref { + @mixin px var(--size-1); + + font-family: var(--font-mono); + font-size: 0.75em; + font-weight: var(--font-weight-emphasis); + color: var(--color-text-primary); + text-decoration: none; + + &:hover { + color: var(--color-primary); + } + + &:focus-visible { + @mixin focus-ring; + } + } + + .sidenote { + @mixin typo_body-small; + @mixin pt var(--size-3); + @mixin border-t var(--border-card), solid, var(--color-border-strong); + + scroll-margin-block-start: calc(var(--layout-header-height) + var(--size-8)); + color: var(--color-text-secondary); + + /* Hangs, so the first line starts against the marker. */ + & .sidenote__marker { + @mixin me var(--size-4); + + float: inline-start; + font-family: var(--font-mono); + font-weight: var(--font-weight-emphasis); + color: var(--color-text-primary); + } + + &:target { + color: var(--color-text-primary); + + & .sidenote__marker { + color: var(--color-primary); + } + } + } + + .sidenote--optional { + @mixin border-t var(--border-hairline), solid, var(--color-border-normal); + + color: var(--color-text-tertiary); + + & .sidenote__marker { + color: var(--color-text-quiet); + } + } + + .sidenote--example { + & .sidenote__marker { + color: var(--color-primary); + } + } + + @media (--bp-tablet-down) { + .sidenote { + @mixin my var(--space-6); + + display: block; + } + } + + @media (--bp-desktop) { + .sidenote { + @mixin my var(--size-0); + + position: absolute; + inset-inline-start: calc(100% + var(--layout-gutter)); + width: var(--layout-margin-width); + } + } +} diff --git a/src/styles/components/table-wrapper.css b/src/styles/components/table-wrapper.css new file mode 100644 index 0000000..1049547 --- /dev/null +++ b/src/styles/components/table-wrapper.css @@ -0,0 +1,121 @@ +@layer components.blocks { + .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-bg: var(--color-surface-inverse); + --table-header-rule: 0; + --table-header-edge: var(--color-border-normal); + + @mixin my var(--space-10); + + counter-increment: tbl; + + & figcaption { + @mixin typo_label; + @mixin mb var(--size-3); + display: flex; + gap: var(--size-5); + color: var(--color-text-secondary); + + &::before { + content: "Table " counter(tbl, upper-roman) " ⟫"; + font-weight: var(--font-weight-emphasis); + color: var(--color-text-primary); + white-space: nowrap; + } + } + + & table { + @mixin my var(--size-0); + + & thead { + & th { + border-block-end: var(--table-header-rule); + color: var(--table-header-fg); + background-color: var(--table-header-bg); + } + } + + & td { + &.text { + font-family: var(--font-body); + font-size: 1.077em; + line-height: 1.55; + color: var(--color-text-secondary); + text-wrap: pretty; + } + } + } + + & .num { + font-variant-numeric: tabular-nums; + text-align: right; + white-space: nowrap; + } + + & .key { + font-weight: var(--font-weight-emphasis); + } + } + + /* 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, + :is(.callout, .reveal) .table-wrapper { + --table-header-fg: inherit; + --table-header-bg: var(--color-surface-base); + --table-header-rule: var(--border-card) solid 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) { + .table-wrapper .table-scroll thead th:first-child { + border-inline-end-color: var(--table-header-edge); + } + } +} diff --git a/src/styles/mixins/spacing.css b/src/styles/mixins/spacing.css index e0c0f25..c199ac7 100644 --- a/src/styles/mixins/spacing.css +++ b/src/styles/mixins/spacing.css @@ -27,7 +27,7 @@ } @define-mixin ms $spacing { - margin-line-start: $spacing; + margin-inline-start: $spacing; } @define-mixin me $spacing {