Styling and astro refactor of sidenotes
This commit is contained in:
@@ -12,10 +12,12 @@ const { footnotes } = Astro.props
|
||||
<ol>
|
||||
{footnotes.map((footnote) => (
|
||||
<li id={`fn-${footnote.n}`}>
|
||||
<p class="footnotes__note">
|
||||
<Node node={footnote.children} />{' '}
|
||||
<a href={`#fnref-${footnote.n}`} class="footnotes__back" aria-label="Back to text">
|
||||
↩
|
||||
</a>
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
@@ -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) => (
|
||||
<aside
|
||||
class:list={['sidenote', `sidenote--${sidenote.type}`]}
|
||||
id={`sn-${sidenote.n}`}
|
||||
style={`position-anchor: --sn-${sidenote.n}`}
|
||||
role="note"
|
||||
>
|
||||
<a class="marker" href={`#snref-${sidenote.n}`} aria-label="Back to the text">
|
||||
{sidenote.marker}
|
||||
</a>
|
||||
<p class="note">
|
||||
{sidenote.type !== 'default' && <span class="kind">{sidenote.type} ⟫ </span>}
|
||||
<Node node={sidenote.children} />
|
||||
</p>
|
||||
</aside>
|
||||
))}
|
||||
|
||||
<style>
|
||||
.sidenote {
|
||||
@mixin typo_body-small;
|
||||
@mixin my var(--space-8);
|
||||
|
||||
scroll-margin-top: calc(var(--layout-header-height) + var(--size-8));
|
||||
}
|
||||
|
||||
/* The marker hangs: it lines the note up with the text it belongs to. */
|
||||
.marker {
|
||||
@mixin me var(--size-2);
|
||||
|
||||
float: inline-start;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.note {
|
||||
@mixin ma var(--size-0);
|
||||
}
|
||||
|
||||
/* Stacked under the article: a list, separated by rules. */
|
||||
@media (--bp-tablet-down) {
|
||||
.sidenote {
|
||||
@mixin pt var(--space-4);
|
||||
@mixin border-t var(--size-1), solid, var(--color-border-subtle);
|
||||
}
|
||||
}
|
||||
|
||||
@media (--bp-desktop) {
|
||||
@supports (anchor-name: --x) {
|
||||
.sidenote {
|
||||
@mixin my var(--size-0);
|
||||
|
||||
position: absolute;
|
||||
top: anchor(top);
|
||||
left: anchor(--margin-column left);
|
||||
width: anchor-size(--margin-column width);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,9 +95,8 @@ export function createTags(state: NodeState, resolveLink: ResolveLink): Record<s
|
||||
message: `Two sidenotes use the marker "${marker}".`,
|
||||
})
|
||||
state.markers.add(marker)
|
||||
const n = state.sidenotes.length + 1
|
||||
const n = (state.sidenoteCount += 1)
|
||||
const children = inlineOf(text(node.attributes.note))
|
||||
state.sidenotes.push({ n, marker, type: noteType, children })
|
||||
return [
|
||||
new Tag('a', { class: 'sidenote-ref', id: `snref-${n}`, href: `#sn-${n}` }, [marker]),
|
||||
new Tag(
|
||||
@@ -136,15 +135,33 @@ export function createTags(state: NodeState, resolveLink: ResolveLink): Record<s
|
||||
},
|
||||
tableWrapper: {
|
||||
...generated.tableWrapper,
|
||||
/* Keystatic makes an array attribute required; a table without column
|
||||
types is the normal case. */
|
||||
attributes: {
|
||||
...generated.tableWrapper?.attributes,
|
||||
columns: { ...generated.tableWrapper?.attributes?.columns, required: false },
|
||||
},
|
||||
transform(node, config) {
|
||||
const { columns, ...attributes } = node.transformAttributes(config) as {
|
||||
columns?: string[]
|
||||
}
|
||||
const children = node.transformChildren(config)
|
||||
if (columns?.length) applyColumns(children, columns, state)
|
||||
return new Tag('TableWrapper', attributes, children)
|
||||
},
|
||||
/* The children of a Keystatic wrapper are free-form block content, so
|
||||
"exactly one table" can only be a build-time check. */
|
||||
validate(node) {
|
||||
const tables = node.children.filter((child) => 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}.`,
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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<string>
|
||||
footnotes: Footnote[]
|
||||
sidenotes: Sidenote[]
|
||||
sidenoteCount: number
|
||||
markers: Set<string>
|
||||
problems: Problem[]
|
||||
}
|
||||
@@ -42,7 +35,6 @@ export type Rendered = {
|
||||
tree: RenderableTreeNode
|
||||
headings: Heading[]
|
||||
footnotes: Footnote[]
|
||||
sidenotes: Sidenote[]
|
||||
diagnostics: Diagnostic[]
|
||||
}
|
||||
|
||||
|
||||
109
src/styles/components/footnote.css
Normal file
109
src/styles/components/footnote.css
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
80
src/styles/components/sidenote.css
Normal file
80
src/styles/components/sidenote.css
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/styles/components/table-wrapper.css
Normal file
121
src/styles/components/table-wrapper.css
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
}
|
||||
|
||||
@define-mixin ms $spacing {
|
||||
margin-line-start: $spacing;
|
||||
margin-inline-start: $spacing;
|
||||
}
|
||||
|
||||
@define-mixin me $spacing {
|
||||
|
||||
Reference in New Issue
Block a user