feat: style release changelog with color-coded sections

Gives the GitHub-release-body markdown real typography on the
/flash page:

- `## [version]` becomes a bordered mono version heading
- `### Added / Changed / Fixed / Removed / Deprecated / Security`
  render as uppercase section headers, color-coded per
  Keep-a-Changelog semantics (green / yellow / blue / red)
- Nested bullet lists switch marker style (disc → circle → square)
- Inline `code`, fenced `pre`, hr, strong, em, blockquote all styled
- Proper vertical rhythm (line-height 1.65, section spacing)

To colorize the section headers, `VersionSelector`.renderBody now
post-processes marked's output to tag known h3 texts with a
`data-section` attribute that CSS targets.

Rules live in `src/styles/global.css` under `@layer base`, not in
the Svelte component's `<style>` block: the Astro + Svelte 5 build
pipeline was silently dropping the component's scoped CSS in the
production bundle (works in dev via HMR, but `dist/_astro/*.css`
shipped with zero `.changelog` rules), leaving the netlify site
completely unstyled. Moving to global CSS sidesteps that toolchain
issue; the `.changelog` parent selector is specific enough that no
scope leakage is possible.
This commit is contained in:
swaits committed 2026-04-11 23:17:59 -06:00
1 parent 81130108b7
commit cfa6cf2be2
2 files changed
+162 -41

No files matched your search

+16 -41
View File
@@ -12,7 +12,17 @@
let { releases, selected, onselect, loading, error }: Props = $props();
let changelogHtml = $derived(selected?.body ? marked.parse(selected.body) : "");
// Tag Keep-a-Changelog section headings so CSS can color-code them
// (Added → green, Changed → yellow, Fixed → blue, etc.).
function renderBody(body: string): string {
const html = marked.parse(body) as string;
return html.replace(
/<h3>(Added|Changed|Fixed|Removed|Deprecated|Security)<\/h3>/g,
(_, text) => `<h3 data-section="${text.toLowerCase()}">${text}</h3>`,
);
}
let changelogHtml = $derived(selected?.body ? renderBody(selected.body) : "");
function handleChange(e: Event) {
const tag = (e.target as HTMLSelectElement).value;
@@ -61,49 +71,14 @@
</div>
{#if selected && changelogHtml}
<div class="mt-4 bg-bg-card border border-border rounded p-4">
<h4 class="font-mono text-xs text-text-dim uppercase tracking-wider mb-3">Changelog</h4>
<div class="prose-dark text-sm text-text-muted leading-relaxed">
<div class="mt-4 bg-bg-card border border-border rounded p-5">
<h4 class="font-mono text-xs text-text-dim uppercase tracking-wider mb-4">Changelog</h4>
<!-- Styles live in src/styles/global.css — Svelte component <style>
scoping wasn't being emitted into the prod CSS bundle. -->
<div class="changelog text-sm text-text-muted">
{@html changelogHtml}
</div>
</div>
{/if}
{/if}
</div>
<style>
.prose-dark :global(h1),
.prose-dark :global(h2),
.prose-dark :global(h3) {
color: var(--color-text);
font-family: var(--font-mono);
font-size: 0.875rem;
font-weight: 600;
margin-top: 1rem;
margin-bottom: 0.5rem;
}
.prose-dark :global(ul) {
list-style-type: disc;
padding-left: 1.25rem;
}
.prose-dark :global(li) {
margin-bottom: 0.25rem;
}
.prose-dark :global(code) {
font-family: var(--font-mono);
font-size: 0.8em;
color: var(--color-accent);
background: var(--color-bg-elevated);
padding: 0.1em 0.3em;
border-radius: 2px;
}
.prose-dark :global(a) {
color: var(--color-accent);
}
.prose-dark :global(a:hover) {
color: var(--color-accent-bright);
}
.prose-dark :global(p) {
margin-bottom: 0.5rem;
}
</style>
+146
View File
@@ -70,4 +70,150 @@
padding: 1rem;
font-size: 0.875rem;
}
/* Changelog typography (markdown rendered from GitHub release bodies).
Kept here rather than in VersionSelector.svelte because the Astro +
Svelte 5 build pipeline wasn't emitting the component's <style>
block into any CSS file, leaving the prod changelog unstyled. */
.changelog {
line-height: 1.65;
}
/* Top-level heading (e.g. `## [0.4.0] - 2026-04-11`) */
.changelog h1,
.changelog h2 {
font-family: var(--font-mono);
font-size: 1rem;
font-weight: 600;
color: var(--color-text);
margin-top: 1.5rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-border);
}
.changelog h1:first-child,
.changelog h2:first-child {
margin-top: 0;
}
/* Section heading (e.g. `### Added`) */
.changelog h3 {
font-family: var(--font-mono);
font-size: 0.8125rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text);
margin-top: 1.25rem;
margin-bottom: 0.5rem;
}
.changelog h3:first-child {
margin-top: 0;
}
/* Color-code Keep-a-Changelog sections */
.changelog h3[data-section="added"] { color: var(--color-accent); }
.changelog h3[data-section="changed"] { color: var(--color-warn); }
.changelog h3[data-section="fixed"] { color: var(--color-info); }
.changelog h3[data-section="removed"],
.changelog h3[data-section="deprecated"] { color: var(--color-error); }
.changelog h3[data-section="security"] { color: var(--color-warn); }
.changelog h4,
.changelog h5,
.changelog h6 {
font-family: var(--font-mono);
font-size: 0.8125rem;
font-weight: 600;
color: var(--color-text);
margin-top: 0.75rem;
margin-bottom: 0.25rem;
}
.changelog p {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
/* Lists */
.changelog ul,
.changelog ol {
margin-top: 0.25rem;
margin-bottom: 0.5rem;
padding-left: 1.4rem;
}
.changelog ul { list-style-type: disc; }
.changelog ol { list-style-type: decimal; }
.changelog li { margin-bottom: 0.35rem; }
.changelog li::marker { color: var(--color-text-dim); }
.changelog li > p { margin: 0; }
/* Nested lists: different marker, tighter spacing */
.changelog ul ul {
list-style-type: circle;
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
.changelog ul ul ul { list-style-type: square; }
/* Inline code */
.changelog code {
font-family: var(--font-mono);
font-size: 0.85em;
color: var(--color-accent);
background: var(--color-bg-elevated);
padding: 0.1em 0.35em;
border-radius: 3px;
}
/* Fenced code blocks */
.changelog pre {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: 4px;
padding: 0.75rem 1rem;
margin: 0.75rem 0;
font-size: 0.8125rem;
overflow-x: auto;
}
.changelog pre code {
background: none;
padding: 0;
color: var(--color-text);
font-size: inherit;
}
/* Links */
.changelog a {
color: var(--color-accent);
text-decoration: underline;
text-decoration-color: color-mix(in srgb, var(--color-accent) 40%, transparent);
text-underline-offset: 2px;
}
.changelog a:hover {
color: var(--color-accent-bright);
text-decoration-color: var(--color-accent-bright);
}
/* Horizontal rule (version separator) */
.changelog hr {
border: 0;
border-top: 1px solid var(--color-border);
margin: 1.5rem 0;
}
.changelog strong {
color: var(--color-text);
font-weight: 600;
}
.changelog em {
color: var(--color-text);
font-style: italic;
}
.changelog blockquote {
border-left: 3px solid var(--color-border-bright);
margin: 0.5rem 0;
padding: 0.25rem 0 0.25rem 1rem;
color: var(--color-text-dim);
}
}