Spec
Intent
A four-way segmented control that lets the user pick a pillar. Each segment is a stampable ballot tile; the active selection is filled and badged.
Acceptance Criteria
- [ ] Four interactive segments labelled DO/CO/DE/GO
- [ ] Active state is visually unambiguous (filled + role caption)
- [ ] Clicking a segment updates the active state
- [ ] Keyboard focus is clearly visible
- [ ] Does not pollute the global page when multiple instances exist
Constraints
- JS scoped per component instance
- Theme-aware via tokens
- No hex literals in brand-colour slots
- ≤ 150 lines body
// pillar-switch.astro
---
import type { CompendiumMeta } from '../../../data/types';
export const meta: CompendiumMeta = {
id: 23,
slug: 'pillar-switch',
name: 'Pillar Switch',
category: 'pillars-roles',
subcategory: 'comparison',
description: 'Segmented control to select one of the four pillars — DO / CO / DE / GO — styled as a ballot selector.',
spec: `## Intent
A four-way segmented control that lets the user pick a pillar. Each segment is a stampable ballot tile; the active selection is filled and badged.
## Acceptance Criteria
- [ ] Four interactive segments labelled DO/CO/DE/GO
- [ ] Active state is visually unambiguous (filled + role caption)
- [ ] Clicking a segment updates the active state
- [ ] Keyboard focus is clearly visible
- [ ] Does not pollute the global page when multiple instances exist
## Constraints
- JS scoped per component instance
- Theme-aware via tokens
- No hex literals in brand-colour slots
- ≤ 150 lines body`,
tags: ['form-control', 'navigation', 'real-content', 'dark-mode-tested'],
status: 'stable',
health: { ics: 100, a11y: 'aa', themed: true, animated: false },
};
const pillars = [
{ k: 'do', short: 'DO', role: 'Author', activeCls: 'data-[active=true]:bg-do' },
{ k: 'co', short: 'CO', role: 'Architect', activeCls: 'data-[active=true]:bg-co' },
{ k: 'de', short: 'DE', role: 'Gatekeeper', activeCls: 'data-[active=true]:bg-de' },
{ k: 'go', short: 'GO', role: 'Governor', activeCls: 'data-[active=true]:bg-go' },
];
---
<div data-this-component class="w-full bg-paper text-black border-4 border-black shadow-nb-sm p-3 overflow-hidden">
<div class="flex items-center justify-between mb-2 gap-2">
<div class="font-mono text-[9px] uppercase tracking-[0.25em] opacity-60 truncate">Ballot · Select One</div>
<div class="font-mono text-[9px] uppercase tracking-[0.25em] opacity-60 shrink-0">Form S-23</div>
</div>
<div class="grid grid-cols-4 border-2 border-black" role="radiogroup" aria-label="Select pillar">
{pillars.map((p, i) => (
<button
type="button"
role="radio"
aria-checked={i === 1 ? 'true' : 'false'}
data-key={p.k}
data-active={i === 1 ? 'true' : 'false'}
class:list={[
'group relative px-1 py-3 text-center transition-none focus:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-4px] focus-visible:outline-black cursor-pointer',
i < 3 ? 'border-e-2 border-black' : '',
p.activeCls,
]}
>
<div class="font-black text-base leading-none">{p.short}</div>
<div class="font-mono text-[8px] uppercase tracking-widest mt-0.5 opacity-70">{p.role}</div>
<div class="mt-1 mx-auto w-3 h-3 border-2 border-black bg-paper flex items-center justify-center text-[9px] font-black group-data-[active=true]:bg-ink group-data-[active=true]:text-paper">✓</div>
</button>
))}
</div>
<div class="mt-2 flex items-center justify-between font-mono text-[9px] uppercase tracking-widest gap-2">
<span class="opacity-60 shrink-0">Selected:</span>
<span data-current class="font-black truncate">CO · Architect</span>
</div>
</div>
<script>
const LABELS: Record<string, string> = {
do: 'DO · Author',
co: 'CO · Architect',
de: 'DE · Gatekeeper',
go: 'GO · Governor',
};
// Scope to every instance on the page, not just the first.
document.querySelectorAll('[data-this-component]').forEach((root) => {
const buttons = root.querySelectorAll<HTMLButtonElement>('button[data-key]');
if (!buttons.length) return;
const current = root.querySelector('[data-current]');
buttons.forEach((btn) => {
btn.addEventListener('click', () => {
buttons.forEach((b) => {
b.setAttribute('data-active', 'false');
b.setAttribute('aria-checked', 'false');
});
btn.setAttribute('data-active', 'true');
btn.setAttribute('aria-checked', 'true');
const k = btn.getAttribute('data-key') ?? 'co';
if (current) current.textContent = LABELS[k];
});
});
});
</script>