From 6f23c47e7a37c14bf32eb196835ee7ea0a0aa21b Mon Sep 17 00:00:00 2001 From: Jonathan Hvid Date: Mon, 11 May 2026 14:48:47 +0200 Subject: [PATCH] =?UTF-8?q?feat(component):=20CouncilMark=20=E2=80=94=20ge?= =?UTF-8?q?nerative=20member=20sigil?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SVG component, sizes sm (48) / md (100) / lg (200) on a 120 viewBox. 12 quarter positions on a circle, i=0 = current quarter at 12 o'clock, going clockwise into the past. A position is lit iff the member has a roadmap_attribution to an item with shipped_at in that quarter. Pulse votes do NOT light a quarter — the sigil represents consequential contribution, not participation. Visual layers: - faint dotted outer ring rotating 90s linear - small grey dots on empty quarters - terracotta-filled polygon (15% fill) connecting lit dots, draws in over 1.6s on mount via stroke-dasharray - terracotta dots on lit quarters with staggered 2.4s breathing - serif italic initials in the centre, walnut secondary Uses --pigment-terracotta instead of the spec's coral — no new tokens. Honours prefers-reduced-motion. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/CouncilMark.astro | 145 +++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/components/CouncilMark.astro diff --git a/src/components/CouncilMark.astro b/src/components/CouncilMark.astro new file mode 100644 index 0000000..17e1c68 --- /dev/null +++ b/src/components/CouncilMark.astro @@ -0,0 +1,145 @@ +--- +import type { UserPublic } from '../lib/db'; +import { getLitQuarters } from '../lib/db'; + +interface Props { + member: UserPublic; + size?: 'sm' | 'md' | 'lg'; +} + +const { member, size = 'md' } = Astro.props; + +const SIZES = { sm: 48, md: 100, lg: 200 } as const; +const px = SIZES[size]; + +// 12 positions on a circle, i=0 = current quarter at 12 o'clock, going clockwise +// into the past. A position is "lit" iff the member has a roadmap_attribution +// to an item with shipped_at falling in that quarter. Pulse votes do NOT light. +const lit = getLitQuarters(member.id); +const litArr = [...lit].sort((a, b) => a - b); + +const CENTER = 60; +const RADIUS = 42; +function pointAt(i: number) { + const angle = -Math.PI / 2 + (i * Math.PI * 2) / 12; + return { x: CENTER + RADIUS * Math.cos(angle), y: CENTER + RADIUS * Math.sin(angle) }; +} + +const litPoints = litArr.map(pointAt); +const polygonPoints = litPoints.length >= 3 + ? litPoints.map(p => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ') + : null; + +function initials(name: string): string { + const parts = name.trim().split(/\s+/); + if (parts.length === 1) return (parts[0][0] ?? '').toUpperCase(); + return ((parts[0][0] ?? '') + (parts[parts.length - 1][0] ?? '')).toUpperCase(); +} +--- + + + + + + {Array.from({ length: 12 }).map((_, i) => { + if (lit.has(i)) return null; + const p = pointAt(i); + return ; + })} + + {polygonPoints && ( + + )} + + {litArr.map((i, idx) => { + const p = pointAt(i); + return ( + + ); + })} + + {initials(member.name)} + + +