Spec
Intent
A bakelite-instrument dial that reads generation velocity. The needle sweeps a 240° arc from 0 to redline. Ticks every 25%. The reading is "specs/day" — the rate at which intent enters the system.
Acceptance Criteria
[ ] 240° arc with tick marks and labels
[ ] Needle position maps proportionally to value
[ ] Numeric readout + unit below the dial
Constraints
Pure SVG, no runtime
Theme-aware via tokens
// velocity-meter.astro copy
---
import type { CompendiumMeta } from '../../../data/types';
export const meta: CompendiumMeta = {
id: 32,
slug: 'velocity-meter',
name: 'Velocity Meter',
category: 'maturity-process',
subcategory: 'instruments',
pillar: 'de',
description: 'Analog-style dial showing AI generation velocity — specs per day, PRs per day, files per hour.',
spec: `## Intent
A bakelite-instrument dial that reads generation velocity. The needle sweeps a 240° arc from 0 to redline. Ticks every 25%. The reading is "specs/day" — the rate at which intent enters the system.
## Acceptance Criteria
- [ ] 240° arc with tick marks and labels
- [ ] Needle position maps proportionally to value
- [ ] Numeric readout + unit below the dial
## Constraints
- Pure SVG, no runtime
- Theme-aware via tokens`,
tags: ['svg', 'gauge', 'animated', 'reduced-motion', 'real-content', 'dark-mode-tested'],
status: 'stable',
health: { ics: 90, a11y: 'aa', themed: true, animated: true },
};
const value = 14; // specs/day
const max = 20;
const pct = Math.min(value / max, 1);
const angle = -120 + pct * 240;
const ticks = [0, 25, 50, 75, 100].map((p) => ({ a: -120 + (p / 100) * 240, n: Math.round((p / 100) * max) }));
---
<div data-this-component class="w-full bg-paper text-ink border-4 border-ink overflow-hidden">
<header class="flex items-baseline justify-between px-3 py-2 border-b-2 border-ink">
<span class="font-mono text-[10px] uppercase tracking-widest opacity-60">// velocity</span>
<span class="font-mono text-[10px] uppercase tracking-widest">gen rate</span>
</header>
<div class="p-3 flex flex-col items-center">
<svg viewBox="0 0 200 140" class="w-full max-w-[240px] block" role="img" aria-label={`Velocity ${value} specs per day`}>
<path d="M 30 110 A 70 70 0 1 1 170 110" fill="none" stroke="var(--color-ink)" stroke-width="3" />
<path d="M 30 110 A 70 70 0 0 1 100 40" fill="none" stroke="var(--color-success)" stroke-width="6" opacity="0.85" />
<path d="M 100 40 A 70 70 0 0 1 140 52" fill="none" stroke="var(--color-do)" stroke-width="6" opacity="0.85" />
<path d="M 140 52 A 70 70 0 0 1 170 110" fill="none" stroke="var(--color-danger)" stroke-width="6" opacity="0.85" />
{ticks.map((t) => {
const rad = ((t.a - 90) * Math.PI) / 180;
const x1 = 100 + Math.cos(rad) * 60, y1 = 100 + Math.sin(rad) * 60;
const x2 = 100 + Math.cos(rad) * 72, y2 = 100 + Math.sin(rad) * 72;
const lx = 100 + Math.cos(rad) * 50, ly = 100 + Math.sin(rad) * 50;
return (
<>
<line x1={x1} y1={y1} x2={x2} y2={y2} stroke="var(--color-ink)" stroke-width="2" />
<text x={lx} y={ly + 3} text-anchor="middle" font-family="JetBrains Mono, monospace" font-size="7" fill="var(--color-ink)" opacity="0.7">{t.n}</text>
</>
);
})}
<g transform={`rotate(${angle} 100 100)`} class="needle">
<polygon points="100,38 96,104 104,104" fill="var(--color-ink)" />
<circle cx="100" cy="100" r="6" fill="var(--color-de)" stroke="var(--color-ink)" stroke-width="2" />
</g>
<text x="100" y="130" text-anchor="middle" font-family="Inter, sans-serif" font-weight="900" font-size="20" fill="var(--color-ink)">{value}</text>
</svg>
<span class="font-mono text-[10px] uppercase tracking-widest opacity-60 -mt-1">specs / day</span>
</div>
</div>
<style>
[data-this-component] .needle { transition: transform 800ms cubic-bezier(0.2,0,0.13,1.4); }
@media (prefers-reduced-motion: reduce) {
[data-this-component] .needle { transition: none; }
}
</style>