Skip to main content
Compendium / Pillars & Roles / Loops & Cycles / Spec Lifecycle Ring
#382

Spec Lifecycle Ring

A 4-segment ring tracking a spec from draft to implemented — DO pillar lifecycle at a glance.

STABLE DO
#svg #real-content #dark-mode-tested #responsive
Live
// spec · lifecycle STATE 3/4
SPEC-038 IN-PROGRESS
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>