Give every sin a pictogram for navigation
The gallery and the cross-links were all text, so nothing on either told you what the chart problem actually looked like. Shrinking the bad chart doesn't work here: in nearly every sin the tell lives in the axis, the scale or the legend, which is exactly what a chart loses at thumbnail size. Eight miniatures would have been eight indistinguishable squiggles with the evidence stripped out. So each sin gets a hand-drawn glyph of the tell instead — a cut baseline, a down-pointing axis arrow, class breaks that moved between two ramps. Banner across the top of the gallery tile, small glyph beside each "Often found together" link. Drawn against currentColor plus three theme tokens, so unlike the baked chart SVGs these follow light and dark. Missing thumbnail throws at build, same as an unknown citation key. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ULE5RRxdQE1ebwefEd2eCM
@@ -96,7 +96,34 @@ Each sin page follows one shape: **poke → proof → why → the fix → receip
|
|||||||
a renamed slug fails the build rather than shipping a dead link.
|
a renamed slug fails the build rather than shipping a dead link.
|
||||||
- `compareNote` — overrides "Same numbers, both charts," which is wrong on a
|
- `compareNote` — overrides "Same numbers, both charts," which is wrong on a
|
||||||
sin whose whole point is an omission.
|
sin whose whole point is an omission.
|
||||||
4. `npm run dev` and check. The OG card for the sin is generated automatically.
|
4. Draw the thumbnail: `src/thumbs/<slug>.svg` (see "Thumbnails" below). The
|
||||||
|
build throws if it's missing.
|
||||||
|
5. `npm run dev` and check. The OG card for the sin is generated automatically.
|
||||||
|
|
||||||
|
## Thumbnails
|
||||||
|
|
||||||
|
Every sin has a hand-drawn pictogram at `src/thumbs/<slug>.svg`, used as a
|
||||||
|
banner across the top of its gallery tile and as a small glyph in the
|
||||||
|
"Often found together" cross-links. Lookup is `src/lib/thumbs.ts`, rendered by
|
||||||
|
`SinThumb.astro`; a missing file fails the build.
|
||||||
|
|
||||||
|
- **A symbol of the sin, not a shrunken chart.** In nearly every sin the tell
|
||||||
|
lives in the axis, the scale or the legend — exactly what a chart loses when
|
||||||
|
you shrink it to 120px. A miniature of the bad chart throws away the evidence
|
||||||
|
and leaves a gallery of indistinguishable squiggles. Draw the *tell*: a cut
|
||||||
|
baseline, a down-pointing axis arrow, class breaks that moved.
|
||||||
|
- **The glyph shows the sin, not the fix.** The tile is a rogues' gallery you
|
||||||
|
scan for the one you recognise; the correction lives on the page.
|
||||||
|
- `viewBox="0 0 72 48"`, `fill="none"`, stroke-width 2 for structure. Neutral
|
||||||
|
linework is `currentColor`; the offending element takes `var(--thumb-accent)`.
|
||||||
|
`--thumb-alt` and `--thumb-third` exist for glyphs that need to show two or
|
||||||
|
three distinct series (dual axis, mismatched category colours).
|
||||||
|
- Those three vars resolve to site tokens in `global.css`, so **unlike the
|
||||||
|
charts, thumbnails follow the light/dark theme** — they don't need the fixed
|
||||||
|
`--chart-canvas`, and they must stay legible on both `--layer-02` surfaces.
|
||||||
|
- Check the result at both sizes: the banner is ~72px tall, the cross-link
|
||||||
|
glyph ~36px. A glyph that needs more than about six strokes won't survive the
|
||||||
|
smaller one.
|
||||||
|
|
||||||
## Design & rendering
|
## Design & rendering
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// "While you're here" — the other sins a chart like this usually commits.
|
// "While you're here" — the other sins a chart like this usually commits.
|
||||||
// Charts rarely break one rule at a time, and the person you sent this to may
|
// Charts rarely break one rule at a time, and the person you sent this to may
|
||||||
// need the neighbouring page more than this one. Renders nothing when empty.
|
// need the neighbouring page more than this one. Renders nothing when empty.
|
||||||
|
import SinThumb from './SinThumb.astro';
|
||||||
import { href } from '../lib/url';
|
import { href } from '../lib/url';
|
||||||
import { severityUnits } from '../lib/charts';
|
import { severityUnits } from '../lib/charts';
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ const { sins } = Astro.props;
|
|||||||
<ul class="related__list">
|
<ul class="related__list">
|
||||||
{sins.map((s) => (
|
{sins.map((s) => (
|
||||||
<li class="related__item">
|
<li class="related__item">
|
||||||
|
<SinThumb slug={s.id} variant="glyph" />
|
||||||
<a class="related__link" href={href(`sins/${s.id}/`)}>
|
<a class="related__link" href={href(`sins/${s.id}/`)}>
|
||||||
<span class="related__name">{s.data.title}</span>
|
<span class="related__name">{s.data.title}</span>
|
||||||
<span class="related__summary">{s.data.summary}</span>
|
<span class="related__summary">{s.data.summary}</span>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
// The pictogram that stands in for a sin in navigation — a banner across the
|
||||||
|
// top of a gallery tile, or a small glyph beside a cross-link.
|
||||||
|
//
|
||||||
|
// Always decorative: the sin's title sits right next to it and carries the
|
||||||
|
// meaning, so announcing the glyph too would just be a duplicate.
|
||||||
|
import { getThumb } from '../lib/thumbs';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
slug: string;
|
||||||
|
/** "banner" for the gallery tile, "glyph" for the compact cross-link row. */
|
||||||
|
variant?: 'banner' | 'glyph';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { slug, variant = 'banner' } = Astro.props;
|
||||||
|
const svg = getThumb(slug);
|
||||||
|
---
|
||||||
|
|
||||||
|
<span class={`thumb thumb--${variant}`} aria-hidden="true" set:html={svg} />
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// Sin thumbnails.
|
||||||
|
//
|
||||||
|
// Each sin gets one hand-drawn pictogram at src/thumbs/<slug>.svg — a *symbol*
|
||||||
|
// of the sin, not a shrunken chart. The distinction matters: in nearly every
|
||||||
|
// sin the tell lives in the axis, the scale or the legend, which is exactly
|
||||||
|
// what a chart loses when you shrink it to thumbnail size. A miniature of the
|
||||||
|
// bad chart would throw away the evidence and leave eight indistinguishable
|
||||||
|
// squiggles.
|
||||||
|
//
|
||||||
|
// The glyphs are drawn against `currentColor` plus three theme tokens
|
||||||
|
// (--thumb-accent / --thumb-alt / --thumb-third, set in global.css), so unlike
|
||||||
|
// the build-time-baked Vega charts they follow the light/dark theme instead of
|
||||||
|
// needing the fixed --chart-canvas surface.
|
||||||
|
|
||||||
|
const modules = import.meta.glob<string>('../thumbs/*.svg', {
|
||||||
|
eager: true,
|
||||||
|
query: '?raw',
|
||||||
|
import: 'default',
|
||||||
|
});
|
||||||
|
|
||||||
|
const bySlug = new Map<string, string>();
|
||||||
|
for (const [path, svg] of Object.entries(modules)) {
|
||||||
|
const slug = path.split('/').pop()!.replace(/\.svg$/, '');
|
||||||
|
bySlug.set(slug, svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Inline SVG markup for a sin's thumbnail; throws at build time if missing. */
|
||||||
|
export function getThumb(slug: string): string {
|
||||||
|
const svg = bySlug.get(slug);
|
||||||
|
if (!svg) {
|
||||||
|
throw new Error(
|
||||||
|
`No thumbnail for sin "${slug}". Add src/thumbs/${slug}.svg. ` +
|
||||||
|
`Available: ${[...bySlug.keys()].join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
import SinThumb from '../components/SinThumb.astro';
|
||||||
import { severityUnits, tagModifier } from '../lib/charts';
|
import { severityUnits, tagModifier } from '../lib/charts';
|
||||||
import { href } from '../lib/url';
|
import { href } from '../lib/url';
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ const sins = (await getCollection('sins', ({ data }) => !data.draft)).sort((a, b
|
|||||||
{
|
{
|
||||||
sins.map((sin) => (
|
sins.map((sin) => (
|
||||||
<a class="tile" href={href(`sins/${sin.id}/`)}>
|
<a class="tile" href={href(`sins/${sin.id}/`)}>
|
||||||
|
<SinThumb slug={sin.id} />
|
||||||
<span class={`tag tag--${tagModifier(sin.data.category)}`}>{sin.data.category}</span>
|
<span class={`tag tag--${tagModifier(sin.data.category)}`}>{sin.data.category}</span>
|
||||||
<h2 class="tile__title">{sin.data.title}</h2>
|
<h2 class="tile__title">{sin.data.title}</h2>
|
||||||
<p class="tile__summary">{sin.data.summary}</p>
|
<p class="tile__summary">{sin.data.summary}</p>
|
||||||
|
|||||||
@@ -339,6 +339,47 @@ code {
|
|||||||
border: 1px solid var(--border-subtle);
|
border: 1px solid var(--border-subtle);
|
||||||
margin-block: var(--spacing-09) var(--spacing-10);
|
margin-block: var(--spacing-09) var(--spacing-10);
|
||||||
}
|
}
|
||||||
|
/* ------------------------------------------------------------------ *\
|
||||||
|
Sin thumbnails — a pictogram of the sin, not a shrunken chart.
|
||||||
|
Drawn against currentColor plus three accents, so unlike the baked
|
||||||
|
chart SVGs these follow the theme instead of a fixed canvas.
|
||||||
|
\* ------------------------------------------------------------------ */
|
||||||
|
.thumb {
|
||||||
|
--thumb-accent: var(--support-error);
|
||||||
|
--thumb-alt: var(--interactive);
|
||||||
|
--thumb-third: var(--support-success);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: color var(--dur-fast) var(--ease-productive);
|
||||||
|
}
|
||||||
|
.thumb svg {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
/* banner: bleeds to the tile edges, above everything else in the tile */
|
||||||
|
.thumb--banner {
|
||||||
|
height: 7rem;
|
||||||
|
padding: var(--spacing-05) var(--spacing-06);
|
||||||
|
background: var(--layer-02);
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
margin: calc(-1 * var(--spacing-06)) calc(-1 * var(--spacing-06)) var(--spacing-02);
|
||||||
|
transition:
|
||||||
|
color var(--dur-fast) var(--ease-productive),
|
||||||
|
background var(--dur-fast) var(--ease-productive);
|
||||||
|
}
|
||||||
|
.tile:hover .thumb {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
/* glyph: the compact form for cross-link rows */
|
||||||
|
.thumb--glyph {
|
||||||
|
height: 2.25rem;
|
||||||
|
width: 3.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
.tile {
|
.tile {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -708,6 +749,7 @@ a.receipt__work {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--spacing-02);
|
gap: var(--spacing-02);
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
.related__name {
|
.related__name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<!-- One series, two frames: a cliff on the left, a shrug on the right. -->
|
||||||
|
<svg viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="8" y="6" width="20" height="36" stroke="currentColor" stroke-width="2" />
|
||||||
|
<path d="M9 38l6-11 6-5 6-10" stroke="var(--thumb-accent)" stroke-width="2" />
|
||||||
|
<rect x="38" y="19" width="28" height="14" stroke="currentColor" stroke-width="2" />
|
||||||
|
<path d="M39 31l9-4 9-2 8-3" stroke="var(--thumb-accent)" stroke-width="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 489 B |
@@ -0,0 +1,14 @@
|
|||||||
|
<!-- Bars in one order of colours, their labels in another. -->
|
||||||
|
<svg viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g stroke-width="10">
|
||||||
|
<path d="M18 32V12" stroke="var(--thumb-alt)" />
|
||||||
|
<path d="M36 32V18" stroke="var(--thumb-third)" />
|
||||||
|
<path d="M54 32V8" stroke="var(--thumb-accent)" />
|
||||||
|
</g>
|
||||||
|
<path d="M8 33h56" stroke="currentColor" stroke-width="2" />
|
||||||
|
<g>
|
||||||
|
<rect x="14" y="38" width="8" height="8" fill="var(--thumb-third)" />
|
||||||
|
<rect x="32" y="38" width="8" height="8" fill="var(--thumb-accent)" />
|
||||||
|
<rect x="50" y="38" width="8" height="8" fill="var(--thumb-alt)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 635 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<!-- Two scales, tuned until the two series trace one path. -->
|
||||||
|
<svg viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 8v32M6 14h4M6 24h4M6 34h4" stroke="var(--thumb-accent)" stroke-width="2" />
|
||||||
|
<path d="M62 8v32M62 12h4M62 22h4M62 32h4" stroke="var(--thumb-alt)" stroke-width="2" />
|
||||||
|
<path d="M15 31l12-7 12 3 18-13" stroke="var(--thumb-accent)" stroke-width="2" />
|
||||||
|
<path d="M15 37l12-7 12 3 18-13" stroke="var(--thumb-alt)" stroke-width="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 491 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<!-- Three quarters of a result, with the fourth simply absent. -->
|
||||||
|
<svg viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M36 24h17A17 17 0 1 1 36 7z" fill="currentColor" opacity=".14" />
|
||||||
|
<path d="M53 24A17 17 0 1 1 36 7" stroke="currentColor" stroke-width="2" />
|
||||||
|
<path d="M36 24v17M36 24H19" stroke="currentColor" stroke-width="2" />
|
||||||
|
<path d="M36 24h17M36 24V7" stroke="var(--thumb-accent)" stroke-width="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 455 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- The axis arrow points down, so a rising series draws as a fall. -->
|
||||||
|
<svg viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 8v30" stroke="var(--thumb-accent)" stroke-width="2" />
|
||||||
|
<path d="M7 32l5 7 5-7" stroke="var(--thumb-accent)" stroke-width="2" />
|
||||||
|
<path d="M22 11l12 8 12 5 14 12" stroke="currentColor" stroke-width="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 375 B |
@@ -0,0 +1,16 @@
|
|||||||
|
<!-- The same ramp twice, with the class breaks moved in between. -->
|
||||||
|
<svg viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g fill="currentColor">
|
||||||
|
<rect x="10" y="8" width="13" height="11" opacity=".15" />
|
||||||
|
<rect x="23" y="8" width="13" height="11" opacity=".4" />
|
||||||
|
<rect x="36" y="8" width="13" height="11" opacity=".65" />
|
||||||
|
<rect x="49" y="8" width="13" height="11" opacity=".9" />
|
||||||
|
<rect x="10" y="29" width="9" height="11" opacity=".15" />
|
||||||
|
<rect x="19" y="29" width="17" height="11" opacity=".4" />
|
||||||
|
<rect x="36" y="29" width="17" height="11" opacity=".65" />
|
||||||
|
<rect x="53" y="29" width="9" height="11" opacity=".9" />
|
||||||
|
</g>
|
||||||
|
<path d="M10 8h52v11H10zM10 29h52v11H10z" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M23 8v11M36 8v11M49 8v11" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M19 26v17M36 26v17M53 26v17" stroke="var(--thumb-accent)" stroke-width="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 943 B |
@@ -0,0 +1,10 @@
|
|||||||
|
<!-- A whole cut into more wedges than anyone can rank by eye. -->
|
||||||
|
<svg viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="36" cy="24" r="17" stroke="currentColor" stroke-width="2" />
|
||||||
|
<path
|
||||||
|
d="M36 24 36 7M36 24 44.5 9.3M36 24 50.7 15.5M36 24 53 24M36 24 50.7 32.5M36 24 44.5 38.7M36 24 36 41M36 24 27.5 38.7M36 24 21.3 32.5M36 24 19 24M36 24 21.3 15.5M36 24 27.5 9.3"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.25"
|
||||||
|
opacity=".85"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 485 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- Bars floating on a baseline that has been cut away. -->
|
||||||
|
<svg viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18 36V14M36 36V8M54 36V20" stroke="currentColor" stroke-width="8" />
|
||||||
|
<path d="M6 39h13l3-5 4 10 3-5h37" stroke="var(--thumb-accent)" stroke-width="2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 308 B |