const SECTION_ORDER = [
'breaking',
'added',
'changed',
'fixed',
'removed',
'deprecated',
'security',
];
const SECTION_LABEL = {
breaking: 'BREAKING',
added: 'ADDED',
changed: 'CHANGED',
fixed: 'FIXED',
removed: 'REMOVED',
deprecated: 'DEPRECATED',
security: 'SECURITY',
};
const SECTION_GLYPH = {
added: '+',
changed: '~',
fixed: '!',
removed: '-',
deprecated: '×',
security: '◆',
};
function sortSections(sections) {
return [...sections].sort(
(a, b) => SECTION_ORDER.indexOf(a.name) - SECTION_ORDER.indexOf(b.name)
);
}
function countsLine(sections) {
return sortSections(sections)
.map((s) => `${s.bullets.length} ${SECTION_LABEL[s.name].toLowerCase()}`)
.join(' · ');
}
function ReleaseCard({ entry }) {
const sorted = sortSections(entry.sections || []);
const breaking = sorted.find((s) => s.name === 'breaking');
const rest = sorted.filter((s) => s.name !== 'breaking');
return (
<div className="release">
<div className="when">
<span className="ver-pill">v{entry.ver}</span>
<span>{entry.date}</span>
<span style={{ opacity: 0.6 }}>git tag · v{entry.ver}</span>
</div>
<div className="body">
{breaking && (
<div
style={{
marginBottom: 'var(--s-3)',
padding: 'var(--s-2) var(--s-3)',
borderLeft: '3px solid var(--c-magenta, #d4339a)',
background: 'color-mix(in oklab, var(--c-magenta, #d4339a) 8%, transparent)',
fontFamily: 'var(--f-mono)',
fontSize: 12,
}}
>
<div
style={{
letterSpacing: '0.14em',
textTransform: 'uppercase',
marginBottom: 'var(--s-2)',
opacity: 0.85,
}}
>
⚠ BREAKING
</div>
<ul style={{ margin: 0, paddingLeft: '1.2em' }}>
{breaking.bullets.map((b, i) => (
<li key={i}>{b}</li>
))}
</ul>
</div>
)}
{rest.map((s) => (
<div key={s.name} style={{ marginBottom: 'var(--s-2)' }}>
<div
style={{
fontFamily: 'var(--f-mono)',
fontSize: 11,
letterSpacing: '0.14em',
color: 'var(--fg-dim)',
marginBottom: 4,
}}
>
{SECTION_LABEL[s.name]}
</div>
<ul>
{s.bullets.map((b, i) => (
<li key={i}>
<span style={{ opacity: 0.5, marginRight: 6 }}>
{SECTION_GLYPH[s.name] || '·'}
</span>
{b}
</li>
))}
</ul>
</div>
))}
</div>
<div className="stats">
<div>
<b>{countsLine(entry.sections || [])}</b>
</div>
</div>
</div>
);
}
function Releases() {
const releases = window.WHETSTONE_CHANGELOG || [];
const visible = releases.slice(0, 4);
return (
<section className="section ws-wrap" id="releases">
<div className="ws-sec-head">
<div className="ws-sec-tag">07 · RELEASES</div>
<h2>Recent shipments.</h2>
</div>
<div className="releases">
{visible.map((entry) => (
<ReleaseCard key={entry.ver} entry={entry} />
))}
</div>
<div
style={{
marginTop: 'var(--s-5)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
gap: 'var(--s-3)',
}}
>
<div
style={{
fontFamily: 'var(--f-mono)',
fontSize: 12,
color: 'var(--fg-dim)',
letterSpacing: '0.14em',
textTransform: 'uppercase',
}}
>
// SHOWING {visible.length} OF {releases.length} · source CHANGELOG.md
</div>
<a
className="ws-btn ws-btn--sm ws-btn--ghost"
href="https://github.com/z19r/whetstone/blob/main/CHANGELOG.md"
target="_blank"
rel="noreferrer"
>
FULL CHANGELOG ↗
</a>
</div>
</section>
);
}
Object.assign(window, { Releases });