Spec
Intent
Make the CO pillar's test-driven loop visible at a glance: which third of the cycle is @author currently in.
Acceptance Criteria
- [ ] Three equal arcs — red (fail), green (pass), refactor
- [ ] Active segment uses its band token; idle segments are ink-tinted
- [ ] Centre label names the active step
- [ ] Negative: does NOT require JS
Constraints
- Pure SVG
- Theme-aware via tokens
- ≤ 150 lines body
// red-green-refactor-ring.astro
---
import type { CompendiumMeta } from '../../../data/types';
export const meta: CompendiumMeta = {
id: 381,
slug: 'red-green-refactor-ring',
name: 'RED-GREEN-REFACTOR Ring',
category: 'pillars-roles',
subcategory: 'loops-cycles',
pillar: 'co',
description: 'A 3-segment ring tracking the CO pillar TDD loop — RED · GREEN · REFACTOR — with the active step highlighted.',
spec: `## Intent
Make the CO pillar's test-driven loop visible at a glance: which third of the cycle is @author currently in.
## Acceptance Criteria
- [ ] Three equal arcs — red (fail), green (pass), refactor
- [ ] Active segment uses its band token; idle segments are ink-tinted
- [ ] Centre label names the active step
- [ ] 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: 92, a11y: 'aa', themed: true, animated: false },
};
const steps = [
{ key: 'RED', tone: 'danger' },
{ key: 'GREEN', tone: 'success' },
{ key: 'REFACTOR', tone: 'attention' },
];
const activeIdx = 1;
const cx = 60, cy = 60, r = 44;
// Arc helper: each arc is 120deg, with a 6deg gap on each side.
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 = steps.map((s, i) => ({
...s,
d: arcPath(i * 120 + 6, (i + 1) * 120 - 6),
active: i === activeIdx,
}));
---
<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">// co · tdd</span>
<span class="font-mono text-[10px] uppercase tracking-widest">RING</span>
</div>
<div class="flex items-center justify-center">
<svg viewBox="0 0 120 120" class="w-full max-w-[180px] block" aria-label={`TDD ring, active step ${steps[activeIdx].key}`} role="img">
{arcs.map((a) => (
<path
d={a.d}
fill="none"
stroke={a.active ? `var(--color-${a.tone})` : 'var(--color-ink)'}
stroke-opacity={a.active ? 1 : 0.18}
stroke-width="10"
stroke-linecap="butt"
vector-effect="non-scaling-stroke"
/>
))}
<text x="60" y="58" text-anchor="middle" font-family="JetBrains Mono, monospace" font-size="8" letter-spacing="2" fill="var(--color-ink)" opacity="0.55">STEP {activeIdx + 1}/3</text>
<text x="60" y="74" text-anchor="middle" font-family="Inter, sans-serif" font-weight="900" font-size="15" fill="var(--color-ink)">{steps[activeIdx].key}</text>
</svg>
</div>
<div class="mt-2 flex items-baseline justify-between font-mono text-[9px] uppercase tracking-widest">
{steps.map((s, i) => (
<span class={i === activeIdx ? 'font-black text-black' : 'opacity-50'}>{s.key}</span>
))}
</div>
</div>