← back to Patty
patty: add /pulse — Petition Pulse public visualization (CNCP item 19)
ca02103d8c0192c9433549b2d0f2cf679ecc3cfc · 2026-05-30 12:37:25 -0700 · Steve Abrams
ECG/heartbeat SVG sweep mirroring Grant's Deadline River aesthetic in
Patty's violet palette. Each petition is a pulse spike: x = days since
launch (right = newest), y = signature velocity (signatures/day, taller
= faster growth), color = category (climate / ai_safety / governance /
civil_rights / economic_justice / healthcare), glow intensity = progress
toward signature goal.
Public route — no auth (middleware only matches /api/*). 24 representative
sample petitions baked in so the page demos without backend setup; can be
swapped for a server-side SELECT against the petitions table when wired.
Mirrors Grant's RiverCanvas component split (page → server, Canvas → client
island, Table → static companion). 10-min ISR matching Grant's revalidate.
All three files (page.tsx + PulseCanvas.tsx + PulseTable.tsx) esbuild-parse
clean. Fixed a Fragment-without-key issue (used <Fragment key={cat}> in the
Legend grid instead of <>).
Files touched
A app/pulse/PulseCanvas.tsxA app/pulse/PulseTable.tsxA app/pulse/page.tsx
Diff
commit ca02103d8c0192c9433549b2d0f2cf679ecc3cfc
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat May 30 12:37:25 2026 -0700
patty: add /pulse — Petition Pulse public visualization (CNCP item 19)
ECG/heartbeat SVG sweep mirroring Grant's Deadline River aesthetic in
Patty's violet palette. Each petition is a pulse spike: x = days since
launch (right = newest), y = signature velocity (signatures/day, taller
= faster growth), color = category (climate / ai_safety / governance /
civil_rights / economic_justice / healthcare), glow intensity = progress
toward signature goal.
Public route — no auth (middleware only matches /api/*). 24 representative
sample petitions baked in so the page demos without backend setup; can be
swapped for a server-side SELECT against the petitions table when wired.
Mirrors Grant's RiverCanvas component split (page → server, Canvas → client
island, Table → static companion). 10-min ISR matching Grant's revalidate.
All three files (page.tsx + PulseCanvas.tsx + PulseTable.tsx) esbuild-parse
clean. Fixed a Fragment-without-key issue (used <Fragment key={cat}> in the
Legend grid instead of <>).
---
app/pulse/PulseCanvas.tsx | 311 ++++++++++++++++++++++++++++++++++++++++++++++
app/pulse/PulseTable.tsx | 113 +++++++++++++++++
app/pulse/page.tsx | 114 +++++++++++++++++
3 files changed, 538 insertions(+)
diff --git a/app/pulse/PulseCanvas.tsx b/app/pulse/PulseCanvas.tsx
new file mode 100644
index 0000000..a3134b5
--- /dev/null
+++ b/app/pulse/PulseCanvas.tsx
@@ -0,0 +1,311 @@
+'use client';
+
+/**
+ * PulseCanvas — SVG ECG/heartbeat visualization of petition signature velocity.
+ *
+ * Each petition is a pulse spike on a horizontal sweep:
+ * x = days since launch (right = newest, max ~30d)
+ * y = signature velocity (top = high signatures/day)
+ * color = category (climate / ai_safety / governance / etc.)
+ * height = velocity bar, with a glowing spike at peak
+ *
+ * Mirrors Grant's RiverCanvas structure (pure SVG, hover detail, legend in a
+ * corner, slow tick drives the animation) — different visual metaphor and
+ * different palette so the two visualizations feel like sister tools, not
+ * forks of one template.
+ */
+
+import { Fragment, useState, useMemo, useEffect } from 'react';
+
+export type Petition = {
+ id: string;
+ slug: string;
+ title: string;
+ category: 'climate' | 'ai_safety' | 'governance' | 'civil_rights' | 'economic_justice' | 'healthcare';
+ signatures: number;
+ goal: number;
+ velocity: number; // signatures/day
+ daysSinceStart: number; // 0 = launched today, 30 = a month ago
+};
+
+// ---------------------------------------------------------------------------
+// Category palette — distinct hues that read well on near-black.
+// ---------------------------------------------------------------------------
+const CATEGORY_COLOR: Record<Petition['category'], string> = {
+ climate: '#10b981', // emerald
+ ai_safety: '#a78bfa', // violet-400 (the house color)
+ governance: '#fbbf24', // amber
+ civil_rights: '#fb7185', // rose
+ economic_justice: '#38bdf8', // sky
+ healthcare: '#ec4899', // pink
+};
+
+const CATEGORY_LABEL: Record<Petition['category'], string> = {
+ climate: 'Climate',
+ ai_safety: 'AI Safety',
+ governance: 'Governance',
+ civil_rights: 'Civil Rights',
+ economic_justice: 'Economic Justice',
+ healthcare: 'Healthcare',
+};
+
+// ---------------------------------------------------------------------------
+// Viewport
+// ---------------------------------------------------------------------------
+const W = 1400;
+const H = 520;
+const MARGIN = { top: 40, right: 60, bottom: 50, left: 80 };
+const WINDOW_DAYS = 30; // x-axis horizon
+const MAX_VELOCITY = 1000; // y-axis ceiling (signatures/day)
+
+// ---------------------------------------------------------------------------
+// Build a stable ECG-like baseline path so the canvas has a heartbeat
+// background even with no data. The path is regenerated on every tick to
+// give the sweep its "running" feel. Tiny noise — pure decoration.
+// ---------------------------------------------------------------------------
+function ecgBaseline(tick: number): string {
+ const seg = 14; // px between baseline samples
+ const yMid = H - MARGIN.bottom - 8;
+ let d = `M${MARGIN.left} ${yMid}`;
+ for (let x = MARGIN.left; x < W - MARGIN.right; x += seg) {
+ // Mostly flat, with a faint sinusoidal jitter
+ const phase = (x + tick * 2) * 0.018;
+ const y = yMid + Math.sin(phase) * 0.6 + Math.sin(phase * 3.3) * 0.3;
+ d += ` L${x + seg} ${y}`;
+ }
+ return d;
+}
+
+export default function PulseCanvas({ petitions }: { petitions: Petition[] }) {
+ const [hover, setHover] = useState<Petition | null>(null);
+ const [tick, setTick] = useState(0);
+
+ // Slow tick drives the sweep + spike pulsing
+ useEffect(() => {
+ const id = setInterval(() => setTick((t) => t + 1), 90);
+ return () => clearInterval(id);
+ }, []);
+
+ // Lay out each petition as a pulse spike at its x,y coordinate.
+ const spikes = useMemo(() => {
+ return petitions.map((p) => {
+ // X: 0d = right edge (= today), WINDOW_DAYS = left edge.
+ // Newer petitions on the right matches Grant's "approaching now" feel.
+ const xFrac = 1 - Math.min(1, p.daysSinceStart / WINDOW_DAYS);
+ const x = MARGIN.left + xFrac * (W - MARGIN.left - MARGIN.right);
+
+ // Y: high velocity → near top. Clamp into the drawing area.
+ const yFrac = Math.min(1, p.velocity / MAX_VELOCITY);
+ const yPeak = H - MARGIN.bottom - 8 - yFrac * (H - MARGIN.top - MARGIN.bottom - 8);
+
+ // Progress (signatures/goal) controls the spike's "alive" intensity
+ const progress = Math.min(1, p.signatures / Math.max(1, p.goal));
+ const color = CATEGORY_COLOR[p.category];
+ return { p, x, yPeak, progress, color };
+ });
+ }, [petitions]);
+
+ return (
+ <div style={{ position: 'relative', width: '100%', overflow: 'hidden',
+ border: '1px solid #1a1326', borderRadius: 10, background: '#08060e' }}>
+ <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: 'block' }}>
+ {/* Glow defs — one radial per category so highest-velocity spikes pop */}
+ <defs>
+ <linearGradient id="pulse-bg" x1="0" y1="0" x2="0" y2="1">
+ <stop offset="0" stopColor="#0d0a18" />
+ <stop offset="1" stopColor="#06050a" />
+ </linearGradient>
+ {Object.entries(CATEGORY_COLOR).map(([cat, color]) => (
+ <radialGradient key={cat} id={`glow-${cat}`}>
+ <stop offset="0" stopColor={color} stopOpacity="0.55" />
+ <stop offset="1" stopColor={color} stopOpacity="0" />
+ </radialGradient>
+ ))}
+ </defs>
+ <rect x="0" y="0" width={W} height={H} fill="url(#pulse-bg)" />
+
+ {/* Velocity reference lines */}
+ {[200, 400, 600, 800].map((v) => {
+ const y = H - MARGIN.bottom - 8 - (v / MAX_VELOCITY) * (H - MARGIN.top - MARGIN.bottom - 8);
+ return (
+ <g key={v}>
+ <line x1={MARGIN.left} y1={y} x2={W - MARGIN.right} y2={y}
+ stroke="#1a1326" strokeDasharray="2,6" strokeWidth="0.5" />
+ <text x={MARGIN.left - 10} y={y + 3} textAnchor="end"
+ fill="#5a4c70" fontSize="10" fontFamily="ui-monospace, monospace">
+ {v}/d
+ </text>
+ </g>
+ );
+ })}
+
+ {/* Time axis ticks */}
+ {[30, 21, 14, 7, 0].map((d) => {
+ const xFrac = 1 - d / WINDOW_DAYS;
+ const x = MARGIN.left + xFrac * (W - MARGIN.left - MARGIN.right);
+ return (
+ <g key={d}>
+ <line x1={x} y1={MARGIN.top} x2={x} y2={H - MARGIN.bottom}
+ stroke="#1a1326" strokeDasharray="2,6" strokeWidth="0.5" />
+ <text x={x} y={H - MARGIN.bottom + 22} textAnchor="middle"
+ fill="#5a4c70" fontSize="10" fontFamily="ui-monospace, monospace">
+ {d === 0 ? 'today' : `${d}d ago`}
+ </text>
+ </g>
+ );
+ })}
+
+ {/* ECG baseline — animated */}
+ <path d={ecgBaseline(tick)} stroke="#3b2a52" strokeWidth="0.8"
+ fill="none" opacity="0.7" />
+
+ {/* Active baseline highlight (rightmost ~20% = "now") */}
+ <rect x={MARGIN.left + 0.8 * (W - MARGIN.left - MARGIN.right)}
+ y={MARGIN.top} width={0.2 * (W - MARGIN.left - MARGIN.right)}
+ height={H - MARGIN.top - MARGIN.bottom}
+ fill="#a78bfa" opacity="0.04" />
+
+ {/* Pulse spikes */}
+ {spikes.map(({ p, x, yPeak, progress, color }) => {
+ // Each spike is a P-Q-R-S-T mini-shape (an ECG beat). The R-peak is
+ // tallest; surrounding shoulders give it the heartbeat-look. Spike
+ // amplitude scales with velocity already (yPeak).
+ const baselineY = H - MARGIN.bottom - 8;
+ const peakH = baselineY - yPeak;
+ // Mini-beat: pre-shoulder dip, then R-peak, then post-shoulder dip
+ const preX = x - 18;
+ const postX = x + 18;
+ const dipY = baselineY + 4;
+ const beatPath =
+ `M${preX} ${baselineY} L${x - 9} ${baselineY - peakH * 0.12} ` +
+ `L${x - 4} ${dipY} L${x} ${yPeak} L${x + 4} ${dipY} ` +
+ `L${x + 9} ${baselineY - peakH * 0.18} L${postX} ${baselineY}`;
+
+ // Pulse the glow softly on tick
+ const pulse = 0.7 + Math.sin((tick + p.id.length * 11) / 8) * 0.25;
+ const isHover = hover?.id === p.id;
+ const glowR = 18 + peakH * 0.18 + (isHover ? 14 : 0);
+
+ return (
+ <g
+ key={p.id}
+ onMouseEnter={() => setHover(p)}
+ onMouseLeave={() => setHover(null)}
+ onClick={() => window.open(`/petitions/${p.slug}`, '_blank')}
+ style={{ cursor: 'pointer' }}
+ >
+ {/* Glow at the peak — intensity = progress toward goal */}
+ <circle cx={x} cy={yPeak} r={glowR}
+ fill={`url(#glow-${p.category})`}
+ opacity={pulse * (0.5 + progress * 0.5)} />
+
+ {/* The beat shape */}
+ <path d={beatPath} stroke={color} strokeWidth={isHover ? 2.2 : 1.4}
+ fill="none" strokeLinejoin="round" opacity={0.9} />
+
+ {/* Velocity reading above the peak — only show if there's room */}
+ {peakH > 30 && (
+ <text x={x} y={yPeak - 10} textAnchor="middle"
+ fill={color} fontSize="10" fontFamily="ui-monospace, monospace"
+ opacity={isHover ? 1 : 0.7}>
+ {p.velocity}
+ </text>
+ )}
+
+ {/* Hover ring */}
+ {isHover && (
+ <circle cx={x} cy={yPeak} r={glowR - 4} fill="none"
+ stroke={color} strokeWidth="1" strokeOpacity="0.7"
+ strokeDasharray="3,3" />
+ )}
+ </g>
+ );
+ })}
+
+ {/* "NOW" tick — pinned to the right edge */}
+ <line x1={W - MARGIN.right} y1={MARGIN.top}
+ x2={W - MARGIN.right} y2={H - MARGIN.bottom}
+ stroke="#a78bfa" strokeWidth="0.8" opacity="0.5" />
+ <text x={W - MARGIN.right + 4} y={MARGIN.top + 12}
+ fill="#a78bfa" fontSize="10" fontFamily="ui-monospace, monospace"
+ opacity="0.7">
+ NOW
+ </text>
+ </svg>
+
+ {/* Hover detail card — top-left, mirrors Grant's pattern */}
+ {hover && (
+ <div style={{
+ position: 'absolute', top: 16, left: 16, maxWidth: 460,
+ background: '#0d0a18',
+ border: `1px solid ${CATEGORY_COLOR[hover.category]}`,
+ borderRadius: 8, padding: '14px 16px', pointerEvents: 'none',
+ boxShadow: '0 4px 24px rgba(0,0,0,0.5)',
+ }}>
+ <div style={{ fontSize: 13, fontWeight: 600, color: '#e4d8f1', marginBottom: 6, lineHeight: 1.35 }}>
+ {hover.title}
+ </div>
+ <div style={{ fontSize: 11, color: '#7a6c8a', display: 'flex', gap: 14, flexWrap: 'wrap' }}>
+ <span style={{ color: CATEGORY_COLOR[hover.category] }}>
+ {CATEGORY_LABEL[hover.category]}
+ </span>
+ <span>· launched {hover.daysSinceStart}d ago</span>
+ <span>· velocity <strong style={{ color: '#e4d8f1' }}>{hover.velocity}/day</strong></span>
+ </div>
+ <div style={{ marginTop: 10, fontSize: 11, color: '#7a6c8a' }}>
+ <strong style={{ color: '#e4d8f1' }}>
+ {hover.signatures.toLocaleString()}
+ </strong>
+ {' '}of {hover.goal.toLocaleString()} signatures
+ {' · '}
+ <strong style={{ color: CATEGORY_COLOR[hover.category] }}>
+ {Math.round(100 * hover.signatures / Math.max(1, hover.goal))}%
+ </strong>
+ </div>
+ {/* progress bar */}
+ <div style={{ marginTop: 8, height: 4, background: '#1a1326', borderRadius: 2, overflow: 'hidden' }}>
+ <div style={{
+ width: `${Math.min(100, 100 * hover.signatures / Math.max(1, hover.goal))}%`,
+ height: '100%', background: CATEGORY_COLOR[hover.category], opacity: 0.85,
+ }} />
+ </div>
+ <div style={{ fontSize: 10, color: CATEGORY_COLOR[hover.category], marginTop: 10 }}>
+ click spike to open petition
+ </div>
+ </div>
+ )}
+
+ {/* Legend — bottom-right, mirrors Grant's pattern */}
+ {!hover && (
+ <div style={{
+ position: 'absolute', bottom: 12, right: 12, padding: '12px 14px',
+ background: 'rgba(13,10,24,0.92)', border: '1px solid #1a1326',
+ borderRadius: 6, fontSize: 10, color: '#7a6c8a', maxWidth: 220,
+ lineHeight: 1.55,
+ }}>
+ <div style={{
+ color: '#e4d8f1', marginBottom: 6, fontWeight: 600,
+ letterSpacing: '0.08em', textTransform: 'uppercase', fontSize: 9,
+ }}>
+ Legend
+ </div>
+ <div style={{ display: 'grid', gridTemplateColumns: '14px 1fr', gap: '5px 8px', alignItems: 'center' }}>
+ {(Object.keys(CATEGORY_COLOR) as Array<Petition['category']>).map((cat) => (
+ <Fragment key={cat}>
+ <span style={{
+ width: 12, height: 5, background: CATEGORY_COLOR[cat], borderRadius: 3,
+ }} />
+ <span>{CATEGORY_LABEL[cat]}</span>
+ </Fragment>
+ ))}
+ </div>
+ <div style={{ marginTop: 8, paddingTop: 8, borderTop: '1px solid #1a1326' }}>
+ height → signatures/day<br />
+ glow → progress toward goal<br />
+ rightmost → most recent
+ </div>
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/app/pulse/PulseTable.tsx b/app/pulse/PulseTable.tsx
new file mode 100644
index 0000000..481393c
--- /dev/null
+++ b/app/pulse/PulseTable.tsx
@@ -0,0 +1,113 @@
+/**
+ * PulseTable — server-component sortable list of the highest-velocity
+ * petitions, paired with PulseCanvas. Static table (no interactive sort) so
+ * the page stays light and the canvas component is the only client island.
+ *
+ * Mirrors Grant's RiverTable role — the visualization is the hero, the table
+ * is the "give me the actual numbers" companion.
+ */
+
+import type { Petition } from './PulseCanvas';
+
+const CATEGORY_COLOR: Record<Petition['category'], string> = {
+ climate: '#10b981',
+ ai_safety: '#a78bfa',
+ governance: '#fbbf24',
+ civil_rights: '#fb7185',
+ economic_justice: '#38bdf8',
+ healthcare: '#ec4899',
+};
+
+const CATEGORY_LABEL: Record<Petition['category'], string> = {
+ climate: 'Climate',
+ ai_safety: 'AI Safety',
+ governance: 'Governance',
+ civil_rights: 'Civil Rights',
+ economic_justice: 'Economic Justice',
+ healthcare: 'Healthcare',
+};
+
+export default function PulseTable({ petitions }: { petitions: Petition[] }) {
+ // Sort by velocity descending, top 10 shown.
+ const top = [...petitions].sort((a, b) => b.velocity - a.velocity).slice(0, 10);
+
+ return (
+ <div style={{
+ border: '1px solid #1a1326', borderRadius: 8, overflow: 'hidden',
+ background: '#0d0a18',
+ }}>
+ <table style={{
+ width: '100%', borderCollapse: 'collapse', fontSize: 12, color: '#c8b8d8',
+ }}>
+ <thead>
+ <tr style={{ background: '#100b1f', color: '#7a6c8a',
+ fontSize: 10, letterSpacing: '0.08em', textTransform: 'uppercase' }}>
+ <th style={{ textAlign: 'left', padding: '10px 16px', fontWeight: 500 }}>Petition</th>
+ <th style={{ textAlign: 'left', padding: '10px 16px', fontWeight: 500 }}>Category</th>
+ <th style={{ textAlign: 'right', padding: '10px 16px', fontWeight: 500 }}>Velocity</th>
+ <th style={{ textAlign: 'right', padding: '10px 16px', fontWeight: 500 }}>Signatures</th>
+ <th style={{ textAlign: 'right', padding: '10px 16px', fontWeight: 500 }}>Progress</th>
+ <th style={{ textAlign: 'right', padding: '10px 16px', fontWeight: 500 }}>Age</th>
+ </tr>
+ </thead>
+ <tbody>
+ {top.map((p, i) => {
+ const pct = Math.round(100 * p.signatures / Math.max(1, p.goal));
+ const color = CATEGORY_COLOR[p.category];
+ return (
+ <tr key={p.id} style={{
+ borderTop: i === 0 ? 'none' : '1px solid #1a1326',
+ }}>
+ <td style={{ padding: '12px 16px', lineHeight: 1.4 }}>
+ <a href={`/petitions/${p.slug}`}
+ style={{ color: '#e4d8f1', textDecoration: 'none' }}>
+ {p.title}
+ </a>
+ </td>
+ <td style={{ padding: '12px 16px' }}>
+ <span style={{
+ fontSize: 10, padding: '2px 8px', borderRadius: 99,
+ background: `${color}1a`, color, letterSpacing: '0.03em',
+ }}>
+ {CATEGORY_LABEL[p.category]}
+ </span>
+ </td>
+ <td style={{ padding: '12px 16px', textAlign: 'right',
+ fontVariantNumeric: 'tabular-nums', color, fontWeight: 500 }}>
+ {p.velocity.toLocaleString()}/d
+ </td>
+ <td style={{ padding: '12px 16px', textAlign: 'right',
+ fontVariantNumeric: 'tabular-nums' }}>
+ {p.signatures.toLocaleString()}
+ <span style={{ color: '#5a4c70', fontSize: 11 }}> / {p.goal.toLocaleString()}</span>
+ </td>
+ <td style={{ padding: '12px 16px', textAlign: 'right',
+ fontVariantNumeric: 'tabular-nums' }}>
+ <div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
+ <div style={{
+ width: 60, height: 4, background: '#1a1326',
+ borderRadius: 2, overflow: 'hidden',
+ }}>
+ <div style={{
+ width: `${Math.min(100, pct)}%`, height: '100%',
+ background: color, opacity: 0.85,
+ }} />
+ </div>
+ <span style={{ color: pct >= 75 ? color : '#c8b8d8',
+ minWidth: 32, textAlign: 'right' }}>
+ {pct}%
+ </span>
+ </div>
+ </td>
+ <td style={{ padding: '12px 16px', textAlign: 'right',
+ color: '#7a6c8a', fontVariantNumeric: 'tabular-nums' }}>
+ {p.daysSinceStart}d
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
diff --git a/app/pulse/page.tsx b/app/pulse/page.tsx
new file mode 100644
index 0000000..16215fa
--- /dev/null
+++ b/app/pulse/page.tsx
@@ -0,0 +1,114 @@
+/**
+ * /pulse — "Petition Pulse" public visualization of advocacy petitions as
+ * heartbeat spikes on an ECG line, mirroring Grant's "Deadline River"
+ * aesthetic in Patty's violet palette.
+ *
+ * 2026-05-30 (CNCP _shared item 19): different domain than Grant's federal-
+ * grants river — Patty visualizes petition signature velocity over the last
+ * 30 days. Each petition is a pulse spike: x = days-since-creation
+ * (rightmost = newest), y = signature velocity (signatures/day, taller =
+ * faster growth), color = category.
+ *
+ * No auth required (middleware only matches /api/*). No DB query — the page
+ * embeds a representative sample so the demo works without backend setup.
+ * When wired to the real /api/petitions list it can replace the sample with
+ * live rows server-side; the canvas component is data-shape agnostic.
+ */
+
+import PulseCanvas, { type Petition } from './PulseCanvas';
+import PulseTable from './PulseTable';
+
+// 10-min ISR matching Grant's river — same fetch-amplification reasoning.
+export const revalidate = 600;
+
+// ---------------------------------------------------------------------------
+// Sample petitions — representative across the 6 categories so the demo
+// looks like a fleet, not a single cause. Numbers (signatures, goal,
+// velocity) are illustrative; replace with a live SELECT against
+// poppy/patty.petitions when wiring to production data.
+// ---------------------------------------------------------------------------
+const SAMPLES: Petition[] = [
+ // Climate
+ { id: 'p001', slug: 'fossil-fuel-phaseout', title: 'Federal Fossil Fuel Phaseout by 2035', category: 'climate', signatures: 18432, goal: 25000, velocity: 412, daysSinceStart: 18 },
+ { id: 'p002', slug: 'protect-old-growth', title: 'Permanent Protection for Old-Growth Forests', category: 'climate', signatures: 7811, goal: 10000, velocity: 198, daysSinceStart: 21 },
+ { id: 'p003', slug: 'methane-emergency', title: 'Declare Methane Leaks a National Emergency', category: 'climate', signatures: 3201, goal: 15000, velocity: 89, daysSinceStart: 11 },
+
+ // AI Safety
+ { id: 'p010', slug: 'pause-frontier-training', title: 'Mandatory Pause on Frontier Model Training Above 1e26 FLOPs', category: 'ai_safety', signatures: 24108, goal: 30000, velocity: 856, daysSinceStart: 14 },
+ { id: 'p011', slug: 'open-eval-mandate', title: 'Open Third-Party Evals for Deployed Frontier Models', category: 'ai_safety', signatures: 9412, goal: 12000, velocity: 312, daysSinceStart: 9 },
+ { id: 'p012', slug: 'agi-disclosure', title: 'Public Disclosure Rule for AGI-Class Capabilities', category: 'ai_safety', signatures: 5872, goal: 10000, velocity: 201, daysSinceStart: 24 },
+
+ // Governance
+ { id: 'p020', slug: 'algorithmic-transparency', title: 'Algorithmic Transparency in Federal Benefits Decisions', category: 'governance', signatures: 4108, goal: 8000, velocity: 121, daysSinceStart: 16 },
+ { id: 'p021', slug: 'ranked-choice', title: 'Ranked-Choice Voting for Federal Primaries', category: 'governance', signatures: 11203, goal: 15000, velocity: 287, daysSinceStart: 22 },
+ { id: 'p022', slug: 'lobbying-disclosure', title: 'Real-Time Lobbying Expenditure Disclosure', category: 'governance', signatures: 6722, goal: 9000, velocity: 178, daysSinceStart: 27 },
+ { id: 'p023', slug: 'ftc-rulemaking', title: 'Public Comment Periods for FTC Rulemaking', category: 'governance', signatures: 2118, goal: 6000, velocity: 63, daysSinceStart: 7 },
+
+ // Civil Rights
+ { id: 'p030', slug: 'voting-access', title: 'Restore Section 5 Voting Rights Act Preclearance', category: 'civil_rights', signatures: 16204, goal: 20000, velocity: 489, daysSinceStart: 19 },
+ { id: 'p031', slug: 'gender-affirming-care', title: 'Federal Protection for Gender-Affirming Care', category: 'civil_rights', signatures: 22041, goal: 25000, velocity: 612, daysSinceStart: 13 },
+ { id: 'p032', slug: 'criminal-justice', title: 'End Cash Bail in Federal Pre-Trial Detention', category: 'civil_rights', signatures: 4812, goal: 10000, velocity: 142, daysSinceStart: 20 },
+
+ // Economic Justice
+ { id: 'p040', slug: 'living-wage', title: '$22 Federal Minimum Wage Indexed to CPI', category: 'economic_justice', signatures: 31204, goal: 40000, velocity: 902, daysSinceStart: 17 },
+ { id: 'p041', slug: 'student-debt', title: 'Universal Student Loan Discharge for Under-$75k Households', category: 'economic_justice', signatures: 18722, goal: 25000, velocity: 521, daysSinceStart: 12 },
+ { id: 'p042', slug: 'corporate-tax-floor', title: 'Restore Pre-2017 Corporate Tax Floor', category: 'economic_justice', signatures: 5412, goal: 12000, velocity: 167, daysSinceStart: 23 },
+ { id: 'p043', slug: 'childcare-subsidy', title: 'Universal Childcare Subsidy for Working Families', category: 'economic_justice', signatures: 8911, goal: 15000, velocity: 234, daysSinceStart: 8 },
+
+ // Healthcare
+ { id: 'p050', slug: 'medicare-negotiation', title: 'Expand Medicare Drug Price Negotiation to All Drugs', category: 'healthcare', signatures: 14502, goal: 18000, velocity: 401, daysSinceStart: 15 },
+ { id: 'p051', slug: 'mental-health-parity', title: 'Enforce Mental Health Parity in Federal Plans', category: 'healthcare', signatures: 6118, goal: 9000, velocity: 189, daysSinceStart: 25 },
+ { id: 'p052', slug: 'rural-hospital', title: 'Emergency Rural Hospital Stabilization Fund', category: 'healthcare', signatures: 3922, goal: 8000, velocity: 124, daysSinceStart: 10 },
+ { id: 'p053', slug: 'maternal-health', title: 'National Maternal Mortality Review System', category: 'healthcare', signatures: 7411, goal: 10000, velocity: 212, daysSinceStart: 4 },
+
+ // A few "older" entries to make the left side feel populated
+ { id: 'p060', slug: 'broadband-universal', title: 'Universal Broadband as Public Utility', category: 'governance', signatures: 9821, goal: 12000, velocity: 84, daysSinceStart: 29 },
+ { id: 'p061', slug: 'pfas-ban', title: 'Federal Ban on PFAS in Consumer Products', category: 'climate', signatures: 5202, goal: 10000, velocity: 71, daysSinceStart: 28 },
+ { id: 'p062', slug: 'data-broker-registry', title: 'National Data Broker Public Registry', category: 'governance', signatures: 3411, goal: 7000, velocity: 52, daysSinceStart: 30 },
+];
+
+export default function PulsePage() {
+ return (
+ <div style={{
+ minHeight: '100vh',
+ background: '#06050a',
+ color: '#e4d8f1',
+ fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Display", system-ui, sans-serif',
+ padding: '32px 24px 80px',
+ }}>
+ <div style={{ maxWidth: 1500, margin: '0 auto' }}>
+ <header style={{
+ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
+ marginBottom: 8, gap: 24,
+ }}>
+ <h1 style={{ margin: 0, fontSize: 22, letterSpacing: '-0.01em', fontWeight: 600 }}>
+ <span style={{ color: '#a78bfa' }}>●</span> Petition Pulse
+ <span style={{ fontWeight: 400, color: '#7a6c8a', fontSize: 14, marginLeft: 8 }}>
+ · live signature velocity across {SAMPLES.length} active petitions
+ </span>
+ </h1>
+ <div style={{ color: '#7a6c8a', fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>
+ sample · last 30d window
+ </div>
+ </header>
+
+ <p style={{ color: '#7a6c8a', fontSize: 12, lineHeight: 1.6, maxWidth: 720, marginTop: 0, marginBottom: 24 }}>
+ Each spike is a petition. X-axis is days since launch (right = newest).
+ Spike height is current signature velocity (signatures/day). Color is
+ cause category. Taller, more recent spikes are the petitions worth
+ riding the momentum on.
+ </p>
+
+ <PulseCanvas petitions={SAMPLES} />
+
+ <div style={{ marginTop: 36 }}>
+ <h2 style={{ fontSize: 13, color: '#a78bfa', textTransform: 'uppercase',
+ letterSpacing: '0.08em', marginBottom: 12 }}>
+ Top-velocity petitions
+ </h2>
+ <PulseTable petitions={SAMPLES} />
+ </div>
+ </div>
+ </div>
+ );
+}
← 6065515 harden(auth): add scrypt + rate-limit + refuse-to-boot to /a
·
back to Patty
·
Add 3 guest login tiers via shared capability layer 2d7ba5c →