redaction component, margin not stacking, other cleanup

This commit is contained in:
2026-08-22 12:18:39 -05:00
parent ce7ed0fd37
commit 5baf764d67
69 changed files with 440 additions and 77 deletions
+131 -35
View File
@@ -92,41 +92,7 @@
font-display: swap;
}
/* Alegreya */
@font-face {
font-family: 'Alegreya';
src: url('~/assets/fonts/Alegreya HT/Alegreya-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Alegreya';
src: url('~/assets/fonts/Alegreya HT/Alegreya-Italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Alegreya';
src: url('~/assets/fonts/Alegreya HT/Alegreya-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Alegreya';
src: url('~/assets/fonts/Alegreya HT/Alegreya-BoldItalic.woff2')
format('woff2');
font-weight: 700;
font-style: italic;
font-display: swap;
}
/* Redaction */
/* Redaction — 7 grades × Regular / Italic / Bold */
@font-face {
font-family: 'Redaction';
src: url('~/assets/fonts/Redaction/Redaction-Regular.woff2') format('woff2');
@@ -151,6 +117,58 @@
font-display: swap;
}
@font-face {
font-family: 'Redaction 10';
src: url('~/assets/fonts/Redaction/Redaction_10-Regular.woff2')
format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 10';
src: url('~/assets/fonts/Redaction/Redaction_10-Italic.woff2')
format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Redaction 10';
src: url('~/assets/fonts/Redaction/Redaction_10-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 20';
src: url('~/assets/fonts/Redaction/Redaction_20-Regular.woff2')
format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 20';
src: url('~/assets/fonts/Redaction/Redaction_20-Italic.woff2')
format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Redaction 20';
src: url('~/assets/fonts/Redaction/Redaction_20-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 35';
src: url('~/assets/fonts/Redaction/Redaction_35-Regular.woff2')
@@ -177,6 +195,84 @@
font-display: swap;
}
@font-face {
font-family: 'Redaction 50';
src: url('~/assets/fonts/Redaction/Redaction_50-Regular.woff2')
format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 50';
src: url('~/assets/fonts/Redaction/Redaction_50-Italic.woff2')
format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Redaction 50';
src: url('~/assets/fonts/Redaction/Redaction_50-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 70';
src: url('~/assets/fonts/Redaction/Redaction_70-Regular.woff2')
format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 70';
src: url('~/assets/fonts/Redaction/Redaction_70-Italic.woff2')
format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Redaction 70';
src: url('~/assets/fonts/Redaction/Redaction_70-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 100';
src: url('~/assets/fonts/Redaction/Redaction_100-Regular.woff2')
format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 100';
src: url('~/assets/fonts/Redaction/Redaction_100-Italic.woff2')
format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Redaction 100';
src: url('~/assets/fonts/Redaction/Redaction_100-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* Space Mono */
@font-face {
font-family: 'Space Mono';
+49 -22
View File
@@ -34,21 +34,6 @@ html.font-plantin {
--body-weight: 300;
}
html.font-alegreya {
--font: 'Alegreya', Georgia, serif;
--body-weight: 400;
}
html.font-redaction {
--font: 'Redaction', Georgia, serif;
--body-weight: 400;
}
html.font-redaction-35 {
--font: 'Redaction 35', Georgia, serif;
--body-weight: 400;
}
html.font-sohne {
--font: 'SohneBuch', -apple-system, BlinkMacSystemFont, sans-serif;
--body-weight: 400;
@@ -69,8 +54,38 @@ html.font-palatino {
--body-weight: 400;
}
html.font-iowan {
--font: 'Iowan Old Style', 'Palatino Linotype', Palatino, serif;
html.font-redaction {
--font: 'Redaction', Georgia, serif;
--body-weight: 400;
}
html.font-redaction-10 {
--font: 'Redaction 10', Georgia, serif;
--body-weight: 400;
}
html.font-redaction-20 {
--font: 'Redaction 20', Georgia, serif;
--body-weight: 400;
}
html.font-redaction-35 {
--font: 'Redaction 35', Georgia, serif;
--body-weight: 400;
}
html.font-redaction-50 {
--font: 'Redaction 50', Georgia, serif;
--body-weight: 400;
}
html.font-redaction-70 {
--font: 'Redaction 70', Georgia, serif;
--body-weight: 400;
}
html.font-redaction-100 {
--font: 'Redaction 100', Georgia, serif;
--body-weight: 400;
}
@@ -79,11 +94,6 @@ html.font-system {
--body-weight: 400;
}
html.font-courier {
--font: 'Courier New', Courier, ui-monospace, monospace;
--body-weight: 400;
}
.dark-mode,
html.theme-navy,
html.theme-dusk,
@@ -351,6 +361,7 @@ article .article-body > :first-child {
code {
font-family: var(--font-mono);
font-size: 0.86em;
color: var(--text);
background: var(--code-bg);
padding: 0.12em 0.35em;
border-radius: 3px;
@@ -359,6 +370,7 @@ code {
pre {
margin: 0 0 1em;
padding: 0.9rem 1rem;
color: var(--text);
background: var(--bg-secondary);
border-radius: var(--radius);
overflow-x: auto;
@@ -367,10 +379,25 @@ pre {
pre code {
padding: 0;
color: inherit;
background: none;
font-size: 0.8rem;
}
pre.shiki {
background: var(--bg-secondary);
}
html.light-mode .shiki span {
color: var(--shiki-light-mode, var(--shiki-default));
background: transparent;
}
html.dark-mode .shiki span {
color: var(--shiki-dark-mode, var(--text));
background: transparent;
}
@media (max-width: 640px) {
body {
font-size: 17px;
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21 -2
View File
@@ -1,8 +1,27 @@
<script setup lang="ts">
import { watchMarginNotes } from '~/utils/stackMarginNotes';
onMounted(() => {
watchMarginNotes();
});
</script>
<template>
<span class="margin-note"><slot /></span>
<span class="margin-note-root">
<span class="margin-note-mark" aria-hidden="true"></span>
<span class="margin-note"><slot /></span>
</span>
</template>
<style scoped>
.margin-note-root {
display: inline;
}
.margin-note-mark {
display: inline;
}
.margin-note {
display: block;
margin: 0.4rem 0 1rem;
@@ -15,7 +34,7 @@
@media (min-width: 1200px) {
.margin-note {
position: absolute;
left: calc(100% + 2rem);
left: calc(100% + var(--note-gap));
width: var(--note-width);
margin: 0;
font-size: 0.8rem;
+38
View File
@@ -0,0 +1,38 @@
<script setup lang="ts">
import {
redactionFamily,
resolveRedactionStrength,
} from '~/utils/fonts';
const props = withDefaults(
defineProps<{
strength?: number | string;
}>(),
{
strength: 35,
},
);
const grade = computed(() => resolveRedactionStrength(props.strength));
const family = computed(
() => `'${redactionFamily(grade.value)}', Georgia, serif`,
);
</script>
<template>
<span
class="redacted"
:data-strength="grade"
:style="{ fontFamily: family }"
><slot /></span>
</template>
<style scoped>
.redacted {
font-weight: 400;
}
:where(strong, b) .redacted {
font-weight: 700;
}
</style>
+2 -2
View File
@@ -53,7 +53,7 @@ const onFontChange = (event: Event) => {
</option>
</select>
</label>
<label class="theme-picker">
<!-- <label class="theme-picker">
<span class="visually-hidden">Font</span>
<select
class="theme-select"
@@ -68,7 +68,7 @@ const onFontChange = (event: Event) => {
{{ font.label }}
</option>
</select>
</label>
</label> -->
</nav>
</div>
<h1 class="hero__title">{{ title }}</h1>
+30 -8
View File
@@ -1,15 +1,18 @@
export const BODY_FONTS = [
{ id: 'plantin', label: 'Plantin' },
{ id: 'alegreya', label: 'Alegreya' },
{ id: 'redaction', label: 'Redaction' },
{ id: 'redaction-35', label: 'Redaction 35' },
{ id: 'sohne', label: 'Sohne' },
{ id: 'space', label: 'Space Mono' },
{ id: 'georgia', label: 'Georgia' },
{ id: 'palatino', label: 'Palatino' },
{ id: 'iowan', label: 'Iowan' },
{ id: 'redaction', label: 'Redaction' },
// These are to be used by components or other styles
// { id: 'redaction-10', label: 'Redaction 10' },
// { id: 'redaction-20', label: 'Redaction 20' },
// { id: 'redaction-35', label: 'Redaction 35' },
// { id: 'redaction-50', label: 'Redaction 50' },
// { id: 'redaction-70', label: 'Redaction 70' },
// { id: 'redaction-100', label: 'Redaction 100' },
{ id: 'sohne', label: 'Sohne' },
{ id: 'georgia', label: 'Georgia' },
{ id: 'system', label: 'System Sans' },
{ id: 'courier', label: 'Courier' },
{ id: 'space', label: 'Space Mono' },
] as const;
export type BodyFontId = (typeof BODY_FONTS)[number]['id'];
@@ -20,3 +23,22 @@ export const BODY_FONT_STORAGE_KEY = 'body-font';
export function isBodyFontId(value: string | null): value is BodyFontId {
return BODY_FONTS.some((font) => font.id === value);
}
export const REDACTION_STRENGTHS = [0, 10, 20, 35, 50, 70, 100] as const;
export type RedactionStrength = (typeof REDACTION_STRENGTHS)[number];
export function redactionFamily(strength: RedactionStrength): string {
return strength === 0 ? 'Redaction' : `Redaction ${strength}`;
}
export function resolveRedactionStrength(value: unknown): RedactionStrength {
const n = Number(value);
if (REDACTION_STRENGTHS.includes(n as RedactionStrength)) {
return n as RedactionStrength;
}
if (!Number.isFinite(n)) return 35;
return REDACTION_STRENGTHS.reduce((best, grade) =>
Math.abs(grade - n) < Math.abs(best - n) ? grade : best,
);
}
+57
View File
@@ -0,0 +1,57 @@
const WIDE = '(min-width: 1200px)';
const GAP = 8;
export function stackMarginNotes() {
if (!import.meta.client) return;
const notes = Array.from(
document.querySelectorAll<HTMLElement>('.margin-note'),
);
if (!window.matchMedia(WIDE).matches) {
for (const note of notes) note.style.top = '';
return;
}
for (const note of notes) note.style.top = '';
let previousBottom = Number.NEGATIVE_INFINITY;
for (const note of notes) {
const article = note.closest('article');
const mark = note
.closest('.margin-note-root')
?.querySelector<HTMLElement>('.margin-note-mark');
if (!article || !mark) continue;
const articleTop = article.getBoundingClientRect().top;
let top = mark.getBoundingClientRect().top - articleTop;
if (top < previousBottom + GAP) {
top = previousBottom + GAP;
}
note.style.top = `${Math.max(0, top)}px`;
previousBottom = top + note.offsetHeight;
}
}
let listening = false;
export function watchMarginNotes() {
if (!import.meta.client || listening) {
stackMarginNotes();
return;
}
listening = true;
const run = () => stackMarginNotes();
window.addEventListener('resize', run);
document.fonts?.ready.then(run);
new ResizeObserver(run).observe(document.documentElement);
new MutationObserver(run).observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
});
run();
}