feat(pulse): council marquee auto-scrolls all 7 members across the page
Council section gets a proper carousel — a horizontal marquee that moves continuously across the page, listing every cab member in turn rather than a fixed-size grid. Implementation: - Members rendered twice in a single flex track; CSS keyframe translates from translateX(0) to translateX(-50%) over 40s+ (duration scales with member count via the --marquee-duration inline custom prop, capped at 6 sec per member or 28 sec minimum). At -50% the first copy is fully offscreen and the second copy occupies the visible window seamlessly; the loop resets without a visible jump. - aria-hidden on the duplicated copies so screen readers don't double- announce. - mask-image fades both edges so members slide in and out softly rather than clipping at the container edge. - Paused on hover so a reader can stop and parse a tile. - prefers-reduced-motion: animation off and the strip becomes a quietly scrollable horizontal list — keyboard / trackpad users can pan manually instead of relying on the animation. Seed adds 3 more cab members for a total of 7 (Mads Lindberg, Camilla Storm, Frederik Lund) with backdated cab_joined_date so member_numbers allocate 5/6/7. Each gets title + pull_quote + focus_tags consistent with the existing four. Tenure spread is now 3 → 24 weeks across the seven members so /members renders meaningfully varied 'member since' dates. The previous 4-tile grid + 5th-tile-as-link case is gone; the marquee loops the full set so no truncation is needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5ddaad3da3
commit
39996ab93e
2 changed files with 80 additions and 46 deletions
|
|
@ -61,6 +61,9 @@ const newCabs = [
|
|||
{ name: 'Anna Kjær', email: 'anna@virk3.dk', org: 'Virksomhed 3' },
|
||||
{ name: 'Søren Vedel', email: 'soren@virk4.dk', org: 'Virksomhed 4' },
|
||||
{ name: 'Henriette Rask',email: 'henriette@virk5.dk',org: 'Virksomhed 5' },
|
||||
{ name: 'Mads Lindberg', email: 'mads@virk6.dk', org: 'Virksomhed 6' },
|
||||
{ name: 'Camilla Storm', email: 'camilla@virk7.dk', org: 'Virksomhed 7' },
|
||||
{ name: 'Frederik Lund', email: 'frederik@virk8.dk', org: 'Virksomhed 8' },
|
||||
];
|
||||
|
||||
const insertUser = db.prepare(`
|
||||
|
|
@ -75,7 +78,15 @@ for (const c of newCabs) {
|
|||
// We backdate cab_joined_date first, then let allocateMemberNumber pick it up.
|
||||
// Lars: 0 weeks ago (most senior), then 2 / 4 / 6 weeks for the others.
|
||||
const cabRows = db.prepare("SELECT id, email, name FROM users WHERE role = 'cab' AND active = 1 ORDER BY id").all();
|
||||
const tenureWeeks = { 'lars@virk2.dk': 24, 'anna@virk3.dk': 6, 'soren@virk4.dk': 4, 'henriette@virk5.dk': 2 };
|
||||
const tenureWeeks = {
|
||||
'lars@virk2.dk': 24,
|
||||
'anna@virk3.dk': 14,
|
||||
'soren@virk4.dk': 12,
|
||||
'henriette@virk5.dk': 10,
|
||||
'mads@virk6.dk': 8,
|
||||
'camilla@virk7.dk': 6,
|
||||
'frederik@virk8.dk': 3,
|
||||
};
|
||||
|
||||
const setCabMeta = db.prepare(`
|
||||
UPDATE users
|
||||
|
|
@ -108,6 +119,21 @@ const cabMeta = {
|
|||
pull_quote: 'I\'ve never trusted a system I couldn\'t cross-examine.',
|
||||
focus_tags: ['Legal', 'Policy', 'EU AI Act'],
|
||||
},
|
||||
'mads@virk6.dk': {
|
||||
title: 'Chief Strategy Officer',
|
||||
pull_quote: 'Healthcare runs on consent — and consent runs on trust.',
|
||||
focus_tags: ['Healthcare', 'Consent', 'Governance'],
|
||||
},
|
||||
'camilla@virk7.dk': {
|
||||
title: 'Head of Cyber Resilience',
|
||||
pull_quote: 'Cyber resilience is not a feature — it is the substrate.',
|
||||
focus_tags: ['Defence', 'Resilience'],
|
||||
},
|
||||
'frederik@virk8.dk': {
|
||||
title: 'Director of Public Innovation',
|
||||
pull_quote: 'Public innovation succeeds when it is measurably better, not just newer.',
|
||||
focus_tags: ['Public sector', 'Measurement'],
|
||||
},
|
||||
};
|
||||
|
||||
for (const u of cabRows) {
|
||||
|
|
@ -343,7 +369,7 @@ insertActivity.run(cabs[0].id,'voted', 'pulse', decisionPulseId, no
|
|||
insertActivity.run(cabs[1].id,'voted', 'pulse', decisionPulseId, nowIso(-30 * 60));
|
||||
insertActivity.run(cabs[0].id,'rsvped', 'event', db.prepare("SELECT id FROM events WHERE slug = ?").get(dinnerSlug).id, nowIso(-8 * 3600));
|
||||
|
||||
console.log(' pulse #' + decisionPulseId + ' open, 2 of 4 voted');
|
||||
console.log(` pulse #${decisionPulseId} open, 2 of ${cabs.length} voted`);
|
||||
console.log(' roadmap: 7 items (2 shipping / 1 in_beta / 2 exploring / 2 considering)');
|
||||
console.log(' contributions: 3 (most recent has 3 reactions)');
|
||||
console.log(' dispatches: 4 published (2/5/9/12 days ago)');
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ const members = getAllCabMembers();
|
|||
</div>
|
||||
)}
|
||||
|
||||
<!-- ── Council — matches roadmap section framing ─────────────── -->
|
||||
<!-- ── Council — auto-scrolling marquee, all members ───────── -->
|
||||
{members.length > 0 && (
|
||||
<section class="cascade council-section" aria-label="The council">
|
||||
<header class="council-header">
|
||||
|
|
@ -237,9 +237,10 @@ const members = getAllCabMembers();
|
|||
<a href="/members" class="council-all">See who our council is made up of →</a>
|
||||
</header>
|
||||
|
||||
<ul class="council-grid">
|
||||
{members.slice(0, 4).map(m => (
|
||||
<li class="council-tile">
|
||||
<div class="council-marquee" aria-roledescription="carousel">
|
||||
<ul class="council-marquee-track" style={`--marquee-duration: ${Math.max(28, members.length * 6)}s;`}>
|
||||
{[...members, ...members].map((m, i) => (
|
||||
<li class="council-tile" aria-hidden={i >= members.length ? 'true' : undefined}>
|
||||
<Avatar id={m.id} name={m.name} size={38} />
|
||||
<div class="council-tile-text">
|
||||
<span class="council-tile-name">{m.name}</span>
|
||||
|
|
@ -248,14 +249,8 @@ const members = getAllCabMembers();
|
|||
</div>
|
||||
</li>
|
||||
))}
|
||||
{members.length > 4 && (
|
||||
<li class="council-tile council-tile--more">
|
||||
<a href="/members" class="council-tile-link">
|
||||
See all {members.length} council members →
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
|
@ -648,19 +643,48 @@ const members = getAllCabMembers();
|
|||
}
|
||||
.council-all:hover { opacity: 0.8; border-bottom: none; }
|
||||
|
||||
.council-grid {
|
||||
/* Marquee — list is rendered twice so translateX(-50%) wraps seamlessly.
|
||||
The duration is set via --marquee-duration on the inline style so the
|
||||
loop speed scales with member count. Paused on hover and for users
|
||||
with reduced-motion preferences. */
|
||||
.council-marquee {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%);
|
||||
mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%);
|
||||
}
|
||||
.council-marquee-track {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 24px;
|
||||
display: flex;
|
||||
gap: 48px;
|
||||
width: max-content;
|
||||
animation: council-scroll var(--marquee-duration, 40s) linear infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
.council-marquee:hover .council-marquee-track {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
@keyframes council-scroll {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.council-marquee-track { animation: none; }
|
||||
.council-marquee {
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.council-marquee::-webkit-scrollbar { display: none; }
|
||||
}
|
||||
|
||||
.council-tile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
min-width: 220px;
|
||||
}
|
||||
.council-tile-text {
|
||||
display: flex;
|
||||
|
|
@ -674,12 +698,14 @@ const members = getAllCabMembers();
|
|||
font-size: 15px;
|
||||
line-height: 1.15;
|
||||
color: var(--on-surface);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.council-tile-title {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 11px;
|
||||
color: var(--on-surface-variant);
|
||||
line-height: 1.35;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.council-tile-org {
|
||||
font-family: var(--font-sans);
|
||||
|
|
@ -687,30 +713,12 @@ const members = getAllCabMembers();
|
|||
letter-spacing: var(--tracking-wide);
|
||||
text-transform: uppercase;
|
||||
color: var(--on-surface-variant);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.council-tile--more {
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.council-tile-link {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 11px;
|
||||
letter-spacing: var(--tracking-wider);
|
||||
text-transform: uppercase;
|
||||
color: var(--pigment-terracotta);
|
||||
text-decoration: none;
|
||||
border-bottom: none;
|
||||
text-align: right;
|
||||
}
|
||||
.council-tile-link:hover { opacity: 0.8; border-bottom: none; }
|
||||
|
||||
/* ── Responsive ───────────────────────────────────────────────── */
|
||||
@media (max-width: 880px) {
|
||||
.editorial-row { grid-template-columns: 1fr; gap: var(--space-8); }
|
||||
.also-coming-up { flex-direction: column; align-items: flex-start; gap: var(--space-3); }
|
||||
.council-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
.council-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue