Spec
Intent
Sampling audits run on a fixed cadence — every Nth day, every Nth spec. This dial shows where in the period the next audit lands.
Acceptance Criteria
- [ ] Circular dial with tick marks for each step
- [ ] Indicator hand points at the current step
- [ ] Hand sweeps smoothly when CSS animation is allowed
- [ ] Reduced-motion: hand renders static at the current step
- [ ] Negative: dial does NOT show a real clock time
Constraints
- Pure SVG + CSS @keyframes
- Theme-aware via tokens
// sampling-cycle-dial.astro
---
import type { CompendiumMeta } from '../../../data/types';
export const meta: CompendiumMeta = {
id: 386,
slug: 'sampling-cycle-dial',
name: 'Sampling Cycle Dial',
category: 'pillars-roles',
subcategory: 'loops-cycles',
pillar: 'go',
description: 'A round dial showing how far through the current GO-pillar sampling cycle the audit pass is.',
spec: `## Intent
Sampling audits run on a fixed cadence — every Nth day, every Nth spec. This dial shows where in the period the next audit lands.
## Acceptance Criteria
- [ ] Circular dial with tick marks for each step
- [ ] Indicator hand points at the current step
- [ ] Hand sweeps smoothly when CSS animation is allowed
- [ ] Reduced-motion: hand renders static at the current step
- [ ] Negative: dial does NOT show a real clock time
## Constraints
- Pure SVG + CSS @keyframes
- Theme-aware via tokens`,
tags: ['svg', 'animated', 'reduced-motion', 'real-content', 'dark-mode-tested', 'responsive'],
status: 'stable',
health: { ics: 90, a11y: 'aa', themed: true, animated: true },
};
const steps = 8;
const current = 5;
const cx = 60, cy = 60, r = 42;
const ticks = Array.from({ length: steps }, (_, i) => {
const a = (i / steps) * 2 * Math.PI - Math.PI / 2;
return {
x1: cx + (r - 4) * Math.cos(a),
y1: cy + (r - 4) * Math.sin(a),
x2: cx + r * Math.cos(a),
y2: cy + r * Math.sin(a),
active: i === current,
};
});
const angle = (current / steps) * 360;
---
<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">// sampling · cycle</span>
<span class="font-mono text-[10px] uppercase tracking-widest">{current}/{steps}</span>
</div>
<div class="flex items-center justify-center">
<svg viewBox="0 0 120 120" class="w-full max-w-[180px] block" aria-label={`Sampling dial, step ${current} of ${steps}`} role="img">
<circle cx={cx} cy={cy} r={r} fill="var(--color-paper)" stroke="var(--color-ink)" stroke-width="3" />
{ticks.map((t) => (
<line x1={t.x1} y1={t.y1} x2={t.x2} y2={t.y2} stroke={t.active ? 'var(--color-go)' : 'var(--color-ink)'} stroke-opacity={t.active ? 1 : 0.4} stroke-width={t.active ? 3 : 2} vector-effect="non-scaling-stroke" />
))}
<g class="dial-hand" style={`transform-origin: ${cx}px ${cy}px; transform: rotate(${angle}deg);`}>
<line x1={cx} y1={cy} x2={cx} y2={cy - r + 8} stroke="var(--color-go)" stroke-width="3" stroke-linecap="round" vector-effect="non-scaling-stroke" />
</g>
<circle cx={cx} cy={cy} r="4" fill="var(--color-ink)" />
</svg>
</div>
</div>
<style>
[data-this-component] .dial-hand {
animation: dial-sweep 6s steps(8) infinite;
}
@keyframes dial-sweep {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
[data-this-component] .dial-hand { animation: none; }
}
</style>