Spec
Intent
A spec is never static — it moves through draft, review, in-progress, implemented. This ring shows where SPEC-038 is right now without prose.
Acceptance Criteria
- [ ] Four arcs labelled draft / review / in-progress / implemented
- [ ] Active state coloured by DO token; past states ink-tinted; future faded
- [ ] Centre stamp shows the spec id and current state
- [ ] Negative: does NOT require JS
Constraints
- Pure SVG
- Theme-aware via tokens
- ≤ 150 lines body
// spec-lifecycle-ring.astro
---
import type { CompendiumMeta } from '../../../data/types';
export const meta: CompendiumMeta = {
id: 382,
slug: 'spec-lifecycle-ring',
name: 'Spec Lifecycle Ring',
category: 'pillars-roles',
subcategory: 'loops-cycles',
pillar: 'do',
description: 'A 4-segment ring tracking a spec from draft to implemented — DO pillar lifecycle at a glance.',
spec: `## Intent
A spec is never static — it moves through draft, review, in-progress, implemented. This ring shows where SPEC-038 is right now without prose.
## Acceptance Criteria
- [ ] Four arcs labelled draft / review / in-progress / implemented
- [ ] Active state coloured by DO token; past states ink-tinted; future faded
- [ ] Centre stamp shows the spec id and current state
- [ ] Negative: does NOT require JS
## Constraints
- Pure SVG
- Theme-aware via tokens
- ≤ 150 lines body`,
tags: ['svg', 'real-content', 'dark-mode-tested', 'responsive'],
status: 'stable',
health: { ics: 91, a11y: 'aa', themed: true, animated: false },
};
const states = ['DRAFT', 'REVIEW', 'IN-PROGRESS', 'IMPLEMENTED'];
const activeIdx = 2;
const cx = 60, cy = 60, r = 44;
function arcPath(startDeg: number, endDeg: number) {
const s = ((startDeg - 90) * Math.PI) / 180;
const e = ((endDeg - 90) * Math.PI) / 180;
const x1 = cx + r * Math.cos(s), y1 = cy + r * Math.sin(s);
const x2 = cx + r * Math.cos(e), y2 = cy + r * Math.sin(e);
const large = endDeg - startDeg > 180 ? 1 : 0;
return `M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2}`;
}
const arcs = states.map((k, i) => {
let stroke = 'var(--color-ink)';
let op = 0.18;
if (i < activeIdx) { stroke = 'var(--color-ink)'; op = 0.55; }
if (i === activeIdx) { stroke = 'var(--color-do)'; op = 1; }
return {
k,
d: arcPath(i * 90 + 5, (i + 1) * 90 - 5),
stroke,
op,
};
});
---
<div data-this-component class="w-full bg-paper text-black border-4 border-black p-3 overflow-hidden">
<div class="flex items-baseline justify-between mb-2 gap-2">
<span class="font-mono text-[10px] uppercase tracking-widest opacity-60">// spec · lifecycle</span>
<span class="font-mono text-[10px] uppercase tracking-widest">STATE {activeIdx + 1}/4</span>
</div>
<div class="flex items-center justify-center">
<svg viewBox="0 0 120 120" class="w-full max-w-[180px] block" aria-label={`Spec lifecycle, currently ${states[activeIdx]}`} role="img">
{arcs.map((a) => (
<path d={a.d} fill="none" stroke={a.stroke} stroke-opacity={a.op} stroke-width="10" vector-effect="non-scaling-stroke" />
))}
<text x="60" y="56" text-anchor="middle" font-family="JetBrains Mono, monospace" font-size="9" letter-spacing="2" fill="var(--color-ink)" opacity="0.6">SPEC-038</text>
<text x="60" y="74" text-anchor="middle" font-family="Inter, sans-serif" font-weight="900" font-size="11" fill="var(--color-ink)">{states[activeIdx]}</text>
</svg>
</div>
</div>