feat(component): MembershipCard — dark indigo identity card

Replaces the CouncilMark slot on /pulse. Dark --ink surface, --ink-text
foreground, --ink-muted for labels. Top row: 22px cream circle with the
N monogram, COUNCIL · NNN (zero-padded to 3) on the right; defensive
fallback COUNCIL · MEMBER when member_number is null. First and last name
on separate lines in serif italic, split on the last space so compound
surnames hang together. MEMBER SINCE / month-year block sits above the
focus-tag pill row. Pills render only when focus_tags is non-empty —
no placeholder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Hvid 2026-05-11 15:56:17 +02:00
parent 368ce3ac8c
commit 302caf8896

View file

@ -0,0 +1,145 @@
---
import type { UserPublic } from '../lib/db';
import { readFocusTags } from '../lib/format';
interface Props {
member: UserPublic;
}
const { member } = Astro.props;
// Split on the last space — handles compound surnames better than first-space split.
const parts = member.name.trim().split(/\s+/);
const firstName = parts.length === 1 ? parts[0] : parts.slice(0, -1).join(' ');
const lastName = parts.length === 1 ? '' : parts[parts.length - 1];
const numberLabel = member.member_number != null
? `COUNCIL · ${String(member.member_number).padStart(3, '0')}`
: 'COUNCIL · MEMBER';
// Member-since: prefer cab_joined_date, fall back to created_at.
const sinceISO = member.cab_joined_date ?? member.created_at;
const since = new Intl.DateTimeFormat('en-GB', {
month: 'long', year: 'numeric', timeZone: 'Europe/Copenhagen',
}).format(new Date(sinceISO.replace(' ', 'T') + (sinceISO.includes('T') ? '' : 'Z')));
const tags = readFocusTags(member.focus_tags);
---
<article class="m-card" aria-label={`${member.name} — membership card`}>
<header class="m-top">
<span class="m-monogram" aria-hidden="true">N</span>
<span class="m-number">{numberLabel}</span>
</header>
<h3 class="m-name">
<span class="m-given">{firstName}</span>
{lastName && <span class="m-family">{lastName}</span>}
</h3>
<p class="m-since-block">
<span class="m-since-label">Member since</span>
<span class="m-since-value">{since}</span>
</p>
{tags.length > 0 && (
<ul class="m-tags" aria-label="Focus areas">
{tags.map(t => <li class="m-tag">{t}</li>)}
</ul>
)}
</article>
<style>
.m-card {
background: var(--ink);
color: var(--ink-text);
border-radius: var(--radius-lg);
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: var(--space-5);
min-height: 220px;
}
.m-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.m-monogram {
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--ink-text);
color: var(--ink);
display: inline-flex;
align-items: center;
justify-content: center;
font-family: var(--font-serif);
font-style: italic;
font-weight: 700;
font-size: 13px;
line-height: 1;
flex-shrink: 0;
}
.m-number {
font-family: var(--font-sans);
font-size: var(--text-label-sm);
font-weight: 500;
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--ink-muted);
}
.m-name {
margin: 0;
font-family: var(--font-serif);
font-style: italic;
font-weight: 400;
font-size: 1.5rem;
line-height: 1.15;
color: var(--ink-text);
display: flex;
flex-direction: column;
}
.m-given, .m-family { display: block; }
.m-since-block {
margin: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.m-since-label {
font-family: var(--font-sans);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
color: var(--ink-muted);
}
.m-since-value {
font-family: var(--font-sans);
font-size: var(--text-body-sm);
color: var(--ink-text);
}
.m-tags {
list-style: none;
padding: 0;
margin: auto 0 0;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.m-tag {
border: 0.5px solid rgba(232, 224, 208, 0.3);
color: var(--ink-text);
padding: 3px 8px;
border-radius: 999px;
font-family: var(--font-sans);
font-size: var(--text-label-sm);
letter-spacing: var(--tracking-wide);
white-space: nowrap;
}
</style>