Skip to main content
Compendium / Pillars & Roles / Loops & Cycles / Sampling Cycle Dial
#386

Sampling Cycle Dial

A round dial showing how far through the current GO-pillar sampling cycle the audit pass is.

STABLE GO
#svg #animated #reduced-motion #real-content #dark-mode-tested #responsive
Live
// sampling · cycle 5/8
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>