Redesign the web app from a written design concept

Design concept and design system in docs/design/: principles (evidence
first, neutral, fair to the people shown, plain language, time as a
dimension, accessible and private), information architecture, core
patterns and a concept for every page; tokens with checked contrast,
component catalogue, content and accessibility rules.

web:
- neutral ink-indigo accent instead of flag red; party colours only as
  small markers next to the name; light/dark theme with a switcher
- stylesheets split into tokens, base, layout, components, print
- new header (search, filter count, language, theme, mobile menu) and
  footer; breadcrumbs, page headers, tabs with counts, filter bars that
  collapse on phones, pagination, notices and empty states
- claim cards with source meter, legal stage, innocence notice inside
  the card and a permalink; claim pages with citation (/angaben/{id})
- profiles with role timeline and key figures; party and organisation
  dashboards; vote list with position chips; calendar agenda on phones;
  tables that stack into cards; loading and error pages; favicon

backend:
- GET /api/v1/claims/{id} and MCP tool get_claim for permalinks
- parties list: evaluate person_status once instead of per party
  (30 s -> 1 s on the sample database, identical result)
- parliament calendar: HTML-decode titles ("84. Sitzung")

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
simonandClaude Opus 5.5 committed 2026-10-28 22:21:44 +01:00
1 parent 45514624f4
commit 456f9816f4
76 files changed
+7127 -1280

No files matched your search

+1 -1
View File
@@ -59,7 +59,7 @@ parsers, archive-first, per-publisher legal policy) – see ADR-20.
## Interfaces for other agents
1. **MCP** – `http://<host>:8001/mcp`: `search`, `find_person`, `get_person`, `person_network`,
`list_parties`, `get_party`, `find_organization`, `get_organization`, `list_claims`, `money_flows`,
`list_parties`, `get_party`, `find_organization`, `get_organization`, `list_claims`, `get_claim`, `money_flows`,
`social_posts`, `wikipedia_edits`, `calendar`, `person_votes`, `former_politicians`, `list_topics`.
2. **REST** – `http://<host>:8000/api/docs` (OpenAPI 3).
3. **SQL** – role `politrack_reader`; start with the view `claim_with_evidence`; `\d+ claim` shows the
+55 -22
View File
@@ -1,17 +1,25 @@
# Extending the web app
Next.js 15 (App Router, React Server Components) with next-intl (German default, English). Pages fetch
from the API on the server; there is almost no client-side JavaScript.
from the API on the server; there is almost no client-side JavaScript. How it should look and why:
[design/README.md](../design/README.md) (concept) and [design/design-system.md](../design/design-system.md)
(tokens, components, content rules).
```
web/
messages/de.json, en.json all UI text (keys must exist in both)
src/app/[locale]/<route>/page.tsx pages (German route names: /personen, /parteien, /geld …)
src/components/ shared server components
src/app/[locale]/layout.tsx frame: header, footer, theme, stylesheets
src/app/[locale]/loading.tsx, error.tsx, not-found.tsx
src/app/icon.svg favicon
src/components/ shared components (catalogue: design-system.md §4)
src/components/nav.ts site structure for header, menu and footer
src/styles/ tokens, base, layout, components, print
src/lib/api.ts typed API client (server-side only; reads the filter cookie)
src/lib/api-types.ts GENERATED from the backend's response models – do not edit
src/lib/format.ts dates, money, localized names
src/lib/prefs.ts the user's source/topic filter (cookie)
src/lib/format.ts dates, periods, numbers, money, party colours
src/lib/prefs.ts the visitor's source/topic filter and theme (cookies)
src/lib/roles.ts role labels
```
## Types come from the backend
@@ -25,34 +33,54 @@ cd ../web && npm run typecheck
`api.ts` re-exports the generated types (`Claim`, `MoneyFlow`, …) and adds one function per endpoint.
`api.claims()`, `api.money()`, `api.social()` … automatically apply the visitor's source filter
(`withPrefs`), so every list respects "which sources do I trust".
(`withPrefs`), so every list respects "which sources do I trust". `api.claim(id)` deliberately does not:
a permalink shows the same to everyone.
## Adding a page
1. Create `src/app/[locale]/<route>/page.tsx` – an async server component:
1. Create `src/app/[locale]/<route>/page.tsx` – an async server component built from the shared parts:
```tsx
export async function generateMetadata({params}: Props): Promise<Metadata> {
const {locale} = await params;
const t = await getTranslations({locale, namespace: 'MyPage'});
return {title: t('title')};
}
export default async function Page({params, searchParams}: Props) {
const {locale} = await params;
setRequestLocale(locale);
const sp = await searchParams;
const t = await getTranslations('MyPage');
const rows = await api.something({...});
return <>…</>;
const page = Math.max(1, Number(sp.page) || 1);
const rows = await api.something({kind: sp.kind, limit: PAGE, offset: (page - 1) * PAGE});
return (
<>
<PageHeader title={t('title')} lede={t('intro')} />
<FilterBar id="mypage" active={countActive(sp.kind)} resetHref="/meine-seite">
<Field label={t('kind')}><select name="kind" defaultValue={sp.kind ?? ''}>…</select></Field>
</FilterBar>
{rows.length === 0 ? <EmptyState>{t('none')}</EmptyState> : <ul className="grid-cards">…</ul>}
<Pagination pathname="/meine-seite" query={sp} page={page} hasNext={rows.length === PAGE} />
</>
);
}
```
Filters are plain `<form method="get">` – shareable URLs, no client state.
Filters are plain GET forms – shareable URLs, no client state.
2. Add the texts to **both** `messages/de.json` and `messages/en.json`.
3. Link it from the navigation (`src/app/[locale]/layout.tsx`) or the home page's "Entdecken" cards.
3. Add it to the site structure in `src/components/nav.ts` (primary, "Mehr" or a footer group) or link it
from the home page's "Entdecken" cards.
4. Page modules may only export Next.js page fields (`default`, `generateMetadata`, …); keep helpers and
constants module-local or in `src/components`.
5. Check it at 390 px and in both themes (design-system.md §7).
## Showing a new claim type
Claims render through `ClaimList`; type-specific details come from the registry in
Claims render through `ClaimList` → `ClaimCard`; type-specific details come from the registry in
`src/components/ClaimDetails.tsx`:
```tsx
const ambassador: Renderer = (claim, {t}) => (
<p className="claim-detail small">{t('country')}: {String(claim.data.country ?? '')}</p>
<p className="claim-detail">{t('country')}: {String(claim.data.country ?? '')}</p>
);
const DETAILS: Partial<Record<ClaimType, Renderer[]>> = {
@@ -61,21 +89,26 @@ const DETAILS: Partial<Record<ClaimType, Renderer[]>> = {
};
```
Also add the label under `Claims.type.<claim_type>` in both message files, and (if it should appear) the
type to the person page's `TABS`.
Also add an icon to `TYPE_ICONS` in `src/components/badges.tsx`, the label under
`Claims.type.<claim_type>` in both message files, and (if it should appear) the type to the person page's
`TABS`.
## Non-negotiables in the UI
* **Every fact shows its sources** (the "Quellen" disclosure under each claim, `[source]` links in
tables). Don't render a claim without its evidence.
* **Presumption of innocence:** `needsInnocenceNotice()` in `ClaimList.tsx` decides; never remove the
notice for anything but an undisputed final conviction.
* **Connections are not accusations:** pages that show networks or money of connected organisations
* **Every fact shows its sources** (the evidence disclosure under each claim, source chips in tables).
Don't render a claim without its evidence.
* **Presumption of innocence:** `needsInnocenceNotice()` in `ClaimList.tsx` decides; the notice stays inside
the claim card. Never remove it for anything but an undisputed final conviction.
* **Connections are not accusations:** sections that show networks or money of connected organisations
carry the note (`Network.note`, `Organizations.connectionNote`).
* **Neutral colours:** party colours only through `partyStyle()` as dots and thin bars, next to the name.
* Unverified social accounts are marked (Wikidata-only handles).
* No third-party scripts, fonts, embeds or trackers.
## Styling
One global stylesheet (`src/app/globals.css`) with CSS variables for light/dark mode. Reuse the existing
classes (`claims`, `claim-detail`, `chip`, `tag`, `notice`, `grid-2`, `compact`, `num`, `muted`, `small`)
before adding new ones.
Global CSS with design tokens, one section per component in `src/styles/components.css`. Use tokens
(`var(--ink-2)`, `var(--space-4)`) instead of raw values, reuse the components and layout utilities
listed in [design-system.md](../design/design-system.md), and add a new section (and a row in its
component table) when you create a component. A new token goes into all three blocks of `tokens.css`
(light, dark by preference, dark by choice).
+478
View File
@@ -0,0 +1,478 @@
# Politrack design concept
How Politrack looks, reads and behaves, and why. This is the concept: goals, principles, information
architecture, the core patterns and a concept for every page. The companion
[design-system.md](design-system.md) is the implementation reference: tokens, components, CSS
conventions and recipes for contributors.
Items marked **(later)** are designed here but not built yet; everything else is implemented in `web/`.
## 1. Goals
Politrack is a public record of what Austrian politicians do, where every fact links to its source.
The interface has four jobs, in this order:
1. **Make every fact checkable.** A reader is never more than one click from the document behind a
statement on the screen.
2. **Be fair to everyone shown.** The same layout, wording and weight for every person and party; the
presumption of innocence and the limits of the data are visible where they matter, not in a footer.
3. **Answer common questions fast, on a phone.** "Who is this?", "How did they vote?", "Who gave money to
whom?" in a few seconds, with no app and no account.
4. **Support deep research on a desk.** Dense tables, filters in the URL, time ranges, cross-links between
people, organisations and money, and stable links for citing.
## 2. Audiences and their tasks
| Audience | Context | Top tasks | What the UI must provide |
|---|---|---|---|
| Interested citizens | Phone, linked from news or social media, a few minutes | Look up a politician, a party's votes, donations | Search first, plain language, clear summaries, a readable profile on a small screen |
| Journalists and researchers | Desktop, long sessions, need to cite | Follow money between organisations, build a timeline, check a claim | Filters and date ranges in the URL, dense tables, permalinks per fact, source details, consistent numbers |
| NGOs and activists | Desktop and phone, recurring | Monitor topics and people | Topic filters, calendar, (later) alerts |
| The people profiled and their staff | Desktop | Check what is shown about them | Fair presentation, visible sources, (later) a corrections form |
| AI agents | No UI (MCP server, REST API) | Retrieve facts with evidence | Mentioned in the footer and methodology; the UI never shows anything the API doesn't |
## 3. Principles
Each principle comes with the rules that make it concrete.
**P1 Evidence first.** The atom of the interface is the *claim card*: a one-sentence summary plus its
sources. Rules: the number of sources is always visible; the source list is one click away; every
external link opens the original; quotes are shown as quotes (marked, attributed); there is no UI
element that states a fact without a claim behind it.
**P2 Neutral by design.** Rules: the interface accent is a colour no Austrian party uses (an ink indigo,
not the flag red, which reads as SPÖ red); party colours appear only as small identity markers (a dot or
a thin bar) and always next to the party's name, never as large backgrounds; every party and person gets
the same layout; lists are ordered by neutral criteria (date, size, alphabet); copy uses reporting verbs
("laut", "gemeldet", "berichtet") instead of judgement.
**P3 Show certainty honestly.** One vocabulary for how sure we are (see §6.3), shown the same way
everywhere: single vs. multiple sources, disputed facts, unverified accounts, unconfirmed status,
publication date vs. event date.
**P4 Fairness for legal matters.** The procedural stage is the headline of a legal claim; "Es gilt die
Unschuldsvermutung" sits inside the claim card, not in a footnote; legal claims are styled in a neutral
caution tone, never in alarm red; no portraits inside legal claim cards or feeds of allegations; connections
between people and organisations carry the note that a connection is not an accusation; profile overviews
highlight work (votes, speeches, offices), not allegations.
**P5 Plain language, German first.** German is the default at `/`, English at `/en`. Technical terms
(namentliche Abstimmung, Klub, Ministerrat, Diversion) are explained where they appear. Long German
compounds must not break layouts: hyphenation is on and long tokens may wrap.
**P6 Time is a first-class dimension.** Every list can be narrowed to a date range, profiles have a
timeline, a calendar shows what happened on a day, and "search in time" lists everything in a period.
Dates say what they are ("Datum = Veröffentlichung der Quelle" where we only know the publication date).
**P7 Fast, robust, accessible, private.** Server-rendered pages that work without JavaScript; WCAG 2.2
AA; system fonts; no third-party scripts, fonts, embeds or trackers; print stylesheets that keep source
URLs.
**P8 Shareable and citable.** Filters live in the URL; every claim has a permalink page with a ready-made
citation; page titles name the person or topic.
## 4. Brand and tone
**Name and mark.** *politrack* in lowercase, set in the system font at heavy weight. The mark is a pair of
square brackets around a dot: the citation bracket of a footnote with the piece of evidence inside. It is
the favicon too. Tagline: *Österreichs Politik, belegt.*
**Colour stance.** Near-monochrome "paper and ink" neutrals carry the interface. One accent, an ink indigo,
marks interaction (links, focus, active navigation, magnitude bars). A highlighter yellow marks quoted
evidence and search hits. Semantic colours (positive, negative, caution) appear only in small badges and
always with an icon and a word, never as colour alone. Party colours follow the rules in P2.
**Voice.** Sachlich, precise, friendly. We address readers with "du" (as the existing copy does) and use
gender-inclusive forms with a colon (Politiker:innen). We describe what a source says, with the source:
"Laut Rechnungshof spendete …", not "X kaufte sich Einfluss". Numbers are exact where the source is exact
and labelled as reported ("Beträge wie gemeldet").
## 5. Information architecture
### Sitemap
```
/ Start: search, key figures, latest facts, explore
/suche Search (full text and "search in time": a date range without a query)
/personen People (filters: party, level, status)
/personen/{slug} Profile with tabs: overview, timeline, votes, travel & meetings,
statements, social media, money & network, media & career, legal
/ehemalige Former politicians: what they do now
/parteien Parties
/parteien/{slug} Party: officials, votes, money, finances, affiliated organisations, channels
/organisationen Organisations (companies, associations, authorities, media, lobbies)
/organisationen/{slug} Organisation: money in and out, counterparties, connected people
/geld Money explorer: donations, contracts, ads, subsidies, party funding
/abstimmungen Votes (faction votes; roll calls)
/abstimmungen/{id} Vote: result, factions, individual votes, dissent
/kalender Calendar: sessions, committees, hearings, Council of Ministers, trips, meetings
/social Public posts from politicians' and parties' channels
/wikipedia Wikipedia edits from parliament and government networks
/angaben/{id} Claim permalink: one fact with all its sources and a citation
/einstellungen My filters: trusted sources, topics
/methodik Methodology, statuses, legal stages, sources and their terms
```
Route names are German in both languages (`/en/personen/…`): one URL scheme, links stay valid when
switching language.
### Navigation
* **Header**, primary: Personen, Parteien, Abstimmungen, Geld, Organisationen, Kalender; **Mehr**:
Social Media, Wikipedia-Bearbeitungen, Ehemalige, Methodik & Quellen. Utilities: a search field,
*Meine Filter* with the number of active filters, language, light/dark theme. Below 1024 px the
navigation collapses into a menu (a native disclosure, no JavaScript) and the search field becomes a
search button.
* **Footer**: the full sitemap in three groups (Menschen, Entscheidungen, Geld & Einfluss), the
principles in three lines, links for developers and agents.
* **Breadcrumbs** on every detail page (Start › Personen › Name).
* **Cross-links everywhere**: every person, party and organisation name is a link; money rows link both
sides; lists on profiles end with a "Alle …" link into the explorer, pre-filtered.
### URL conventions
All filters are query parameters on plain GET forms (`/geld?kind=donation&since=2025-01-01`), so every view
can be bookmarked and shared. Personal source/topic preferences are the only state kept outside the URL
(a cookie), and the header always shows when they are active.
## 6. Core patterns
### 6.1 The claim card
```
┌──────────────────────────────────────────────────────────────────────┐
│ ◇ SPENDE · 12. März 2026 · Österreichische Volkspartei ●●○ 2 │ type, date, subject, sources
│ │
│ Die Muster GmbH spendete der ÖVP 25.000 €. │ summary (the fact)
│ 25.000 € · Muster GmbH → ÖVP · 2025 │ type-specific details
│ [Wirtschaft] [Parteifinanzen] │ topics
│ ▸ 2 Belege Permalink ↗ │ evidence disclosure
└──────────────────────────────────────────────────────────────────────┘
```
* The header row is metadata in small type: type label with icon, date, subject (when the list mixes
subjects), and the **source meter** on the right.
* The summary is the largest text in the card. In English the English summary is used when available.
* Type-specific details (amounts, counterparts, platforms, file numbers) come from a registry, one renderer
per claim type.
* The evidence disclosure lists each source with its publisher, source kind (Amtlich, Medien, …), title,
date, stance (supports / mentions / contradicts) and quote. Quotes are set as block quotes with the
highlighter mark.
* A **permalink** opens the claim page (§7.17).
* Disputed claims get a caution edge and the word "umstritten". Claims awaiting review are never public;
if one is shown (admin views) it carries "Noch nicht redaktionell geprüft".
### 6.2 Evidence and corroboration
The source meter shows up to three dots: one filled dot = one source ("einfach belegt"), two or more
filled = "mehrfach belegt" with the number. A disputed claim shows a caution triangle and the word
"umstritten" instead. A tooltip gives the full wording ("bestätigt durch 3 unabhängige Quellen"). The meter
never stands alone: the number is always written next to it.
### 6.3 Certainty vocabulary
| Concept | Values (German UI) | Shown as |
|---|---|---|
| Claim status | einfach belegt · mehrfach belegt · umstritten | source meter; caution edge for umstritten |
| Person status | Aktiv · Ehemalig · Ohne Amt (Parteimitglied) · Verstorben | status badge; "Aktiv" without an official confirmation adds "nicht amtlich bestätigt" |
| Social account | bestätigt · nicht bestätigt | unconfirmed accounts get a dashed badge and a tooltip (source: Wikidata only) |
| Review | Noch nicht redaktionell geprüft | caution notice (admin views only) |
| Dates | Ereignisdatum · Veröffentlichungsdatum | footnote on calendar and trips: "Datum = Veröffentlichungsdatum der Quelle" |
| Amounts | wie gemeldet · Kategorie | exact amount, or the reported category ("über 50.000 €") |
### 6.4 Legal matters
```
┌──────────────────────────────────────────────────────────────────────┐
│ ⚖ VERFAHREN · 3. Februar 2026 · Name ●○○ 1 │
│ [Ermittlungsverfahren] § 302 StGB · WKStA │ stage first, as a badge
│ Summary of what the source reports … │
│ ⓘ Es gilt die Unschuldsvermutung. │ inside the card
│ ▸ 1 Beleg │
└──────────────────────────────────────────────────────────────────────┘
```
The notice is shown for every stage except an undisputed final conviction (including acquittals,
dropped proceedings and diversion, which confirm there is no finding of guilt). Automatically detected
legal claims stay in review until a human approves them. Profiles count legal claims only on their tab,
never in the "at a glance" figures.
### 6.5 Money
* Amounts in euro with locale formatting (`25.000 €`, `€25,000`); large figures in summaries and bar
lists are compact (`1,2 Mio. €`) with the exact value in the tooltip; tables show exact values.
* Direction is always explicit: **Von → An**. Both sides are links when the organisation is known.
* Every amount says what kind of money it is (Spende, öffentlicher Auftrag, Regierungsinserat, Förderung,
Parteienförderung, politische Werbung, Kredit). Different kinds are never summed into one figure
without a label.
* "Beträge wie gemeldet" and the list of registers sit under every money table.
* Money of organisations a person is *connected* to (side jobs, career) is labelled as the organisation's
money and carries the connection note.
### 6.6 Votes
* A vote shows date, item title, reference, stage, chamber, result and the position of every faction as
**position chips**: party dot + short name + position icon (✓ dafür, ✕ dagegen, – Enthaltung,
○ abwesend), with a tinted background per position.
* Roll calls are marked ("namentlich"); they show individual votes grouped by faction with counts, and
members who voted against their faction are marked "gegen die eigene Fraktion".
* On a person's vote list the position says whether it is the faction's vote or the person's own vote,
because most Austrian votes are recorded per faction only. The note explaining this sits above the list.
* *Say vs. vote*: when the person spoke on the item, a link to the speech appears under the vote.
### 6.7 Time
* Dates in lists: `12. März 2026` (de-AT) / `12 Mar 2026` (en-GB). Months in charts: `03/2026`.
* The **activity histogram** (claims per month) on profiles links each bar to that month.
* **Date range** fields on every list that supports them; with no query, search becomes "search in
time" and lists everything in the range.
* The **role timeline** on a profile draws offices and memberships as bars on a year axis.
### 6.8 Filters
Two kinds, never mixed up:
* **Page filters** (party, level, kind, date …) live in the URL. Active filters are visible in the form;
a reset link clears them. On phones the filter form is collapsed behind "Filter" with the number of
active filters.
* **My filters** (trusted sources, minimum trust, topics) are stored in a cookie and apply to every list.
When they are active, the header badge shows the count and lists show a notice with a link to change
them, so nobody is surprised by missing data.
### 6.9 Connections
Wherever the UI shows that a person is connected to an organisation (side job, career, meeting, party)
or shows the organisation's money, the note "Eine Verbindung ist kein Vorwurf." is part of the section.
### 6.10 Social posts
Posts are shown as text cards with platform, author, date and a link to the original; no embeds, no
third-party images or scripts. Accounts that only Wikidata attributes to a person are marked "Konto nicht
bestätigt". The collection policy (official APIs only; no Facebook/Instagram scraping; no WhatsApp,
Signal or private Telegram groups) is stated on the page.
### 6.11 Empty, loading and error states
* **Empty**: an icon, one sentence that says why ("Keine Angaben mit deinen Quellen- und Themenfiltern.")
and the most likely next step (reset filters, broaden the date range).
* **Loading**: skeleton blocks in the page's layout, no spinners.
* **Error**: what happened in one sentence, a retry button, a link home.
## 7. Page concepts
Every page uses one of four templates: **index** (header, filters, results, pagination, source note),
**profile** (identity header, key figures, tabs, main column plus aside), **detail** (header, verdict,
breakdown) and **prose** (methodology).
### 7.1 Start (`/`)
Purpose: orient, search, show that the data is alive.
```
┌ hero ───────────────────────────────────────────────────────────────┐
│ Politik in Österreich – nachvollziehbar. │
│ Lede: what Politrack is, "jede Angabe mit Quelle". │
│ [ Person, Thema oder Stichwort … ] [Suchen] │
│ Schnellzugriff: Parteispenden · Namentliche Abstimmungen · Inserate │
└──────────────────────────────────────────────────────────────────────┘
[ 1.234 Personen ] [ 56.789 Angaben ] [ 1.210 Abstimmungen ] [ 9.876 Geldflüsse ] Stand …
┌ Neueste Angaben (claim feed) ──────────┐ ┌ aside ───────────────────┐
│ claim card … │ │ Parteien (dot, name, n) │
│ claim card … │ │ Letzte Abstimmungen │
└─────────────────────────────────────────┘ └──────────────────────────┘
Entdecken: 6 cards with icon, title, one line (Geld, Organisationen, Social, Wikipedia, Ehemalige, Kalender)
So arbeitet Politrack: Belege · Unabhängige Bestätigung · Unschuldsvermutung → Methodik
```
### 7.2 Search (`/suche`)
A large search field with optional date range. Results are cards with type label, date, subject and a
snippet with the query terms highlighted; document hits (news, releases) show the publisher. Without a
query but with a date range, the page lists all claims in that period ("search in time"). Empty states
suggest fewer words or a wider range.
### 7.3 People (`/personen`)
Index template. Filters: name, party, level (EU, Bund, Land, Gemeinde, Partei), status (default: aktiv).
Results are **person cards** in a responsive grid: portrait or initials, name, current roles (two lines
max), party tags, and the status for non-active people. Pagination at the bottom.
### 7.4 Person profile (`/personen/{slug}`)
```
Start › Personen › Name
┌──────┐ [Aktiv] Profile elsewhere:
│ │ Vorname Nachname Parlament ↗ Wikidata ↗ EP ↗
│ foto │ Abgeordnete zum Nationalrat · ● ÖVP
│ │ * 3. Mai 1970 · Kanäle: [Bluesky] [Telegram*]
└──────┘
[Überblick] [Zeitleiste 312] [Abstimmungen 120] [Reisen & Treffen 4] [Aussagen 58] [Social Media]
[Geld & Netzwerk 6] [Medien & Werdegang 40] [Rechtliches 1]
Overview:
┌ main ─────────────────────────────────────┐ ┌ aside ───────────────────────┐
│ Laufbahn (role timeline on a year axis) │ │ Auf einen Blick │
│ Neueste Angaben (claim cards) │ │ 120 Abstimmungen · 58 Reden │
│ │ │ Funktionen & Mandate (list, │
│ │ │ each with its sources) │
└───────────────────────────────────────────┘ └──────────────────────────────┘
```
* Tabs are links (`?tab=votes`), so every tab is shareable; each shows its claim count.
* **Zeitleiste**: activity histogram, date range, all claims in the range.
* **Abstimmungen**: the faction-vote note, then the vote list with position, own/faction vote, dissent and
say-vs-vote.
* **Geld & Netzwerk**: connected organisations (via side job, career, meeting, party) with their money,
the connection note, the organisations' money flows, then money-related claims.
* **Rechtliches**: legal claims only, each with stage and the innocence notice.
* "At a glance" counts work: votes, speeches and statements, questions, side jobs, appearances, media
reports; not legal matters.
### 7.5 Former politicians (`/ehemalige`)
Index template ordered by latest media coverage. Each card: name, status, last role, last office end,
latest headline with source link and date, latest career entry. Party filter.
### 7.6 Parties (`/parteien`)
Cards with a thin top bar in the party colour, short name, full name, active officials and people
tracked. Ordered by the number of active officials.
### 7.7 Party (`/parteien/{slug}`)
```
──────── party colour bar ────────
Start › Parteien › ÖVP
ÖVP Österreichische Volkspartei
[ 312 Funktionsträger:innen ] [ 1.020 Ehemalige ] [ 830 Abstimmungen ] [ 4 abweichende Mandatar:innen ]
┌ Stimmverhalten (proportion bar) ──┐ ┌ Geld nach Jahr und Art (table) ─────┐
└───────────────────────────────────┘ └─────────────────────────────────────┘
Funktionsträger:innen grouped by level (EU · Bund · Land · Gemeinde), multi-column
┌ Rechenschaftsberichte ────────────┐ ┌ Nahestehende Organisationen ────────┐
┌ Ehemalige ────────────────────────┐ ┌ Abweichendes Stimmverhalten ────────┐
Letzte Abstimmungen · Kanäle der Partei · Belegte Angaben
```
### 7.8 Organisations (`/organisationen`)
Index template with a table: name, kind badge, paid, received (compact amounts, exact in tooltip). On
phones the table stacks into cards. Filters: name, kind.
### 7.9 Organisation (`/organisationen/{slug}`)
Header with kind badge and parent organisation; identifiers (Firmenbuch, ZVR, UID, Wikidata, EU
transparency register) in the aside with links where a public register page exists. Key figures (received,
paid, counterparties). Counterparties as two bar lists ("zahlte an", "erhielt von"). Connected people with
the connection note. Money flow table, then all claims. Parties redirect to their party page.
### 7.10 Money (`/geld`)
Explorer template. Filters: kind, name, date range, minimum amount. Unfiltered, the page opens with two
bar lists, largest payers and largest recipients, each row labelled with the kind of money. Then the money
table: date, kind, from, to, amount, sources. Footer: registers used and "Beträge wie gemeldet".
### 7.11 Votes (`/abstimmungen`) and vote (`/abstimmungen/{id}`)
The index is a list of vote rows: date, title, reference, roll-call marker, result badge, position chips
per faction. Filter: roll calls only. The vote page shows the verdict as a large result badge, links to the
item and the protocol, faction positions, and for roll calls a proportion bar over all individual votes and
the members grouped by faction with dissent marked. The protocol wording sits in a disclosure.
### 7.12 Calendar (`/kalender`)
Month grid (Monday first) with previous/next month as header actions. Each event shows its kind (colour
stripe + label), title (linked to the source) and participants. Days with only claim activity link to
"search in time" for that day. Today is highlighted. On phones the grid becomes an agenda: only days with
events, in order. A legend explains the kinds; the footnote states the date precision.
### 7.13 Social media (`/social`)
Feed of post cards in one or two columns. Filters: platform, party. Each card: platform badge, author
(person or party channel), date, title, text (clamped to a few lines), "Original ansehen ↗", unconfirmed
marker. The collection policy closes the page.
### 7.14 Wikipedia edits (`/wikipedia`)
The framing comes first: a notice that an edit from a network says nothing about which person made it.
Then a table: date, network (organisation), article (linked to the diff), size change, edit comment.
### 7.15 My filters (`/einstellungen`)
Two sections. **Quellen**: grouped by kind (Amtlich, NGO, Medien, Datenbanken, Soziale Medien,
Community, Abgeleitet); each source is a switch row with name, trust value as a small meter, and its terms
in the tooltip; a minimum-trust slider below. **Themen**: topic chips that toggle; none selected = all.
A sticky action bar with Speichern, Zurücksetzen and the saved status. Stored in a cookie only.
### 7.16 Methodology (`/methodik`)
Prose template with a table of contents: how claims and evidence work, the status vocabulary with live
badges, the legal stages glossary, the sources table (publisher, licence, access method, trust meter, terms
notes), what we deliberately don't collect, corrections.
### 7.17 Claim permalink (`/angaben/{id}`)
One fact on its own page: type, date, subject, the summary as the title, details, legal notice where
applicable, all sources expanded with quotes, and a citation box:
`Politrack: „{Zusammenfassung}“. Angabe #{id}, abgerufen am {Datum}. {URL}`. The page ignores personal
source filters, so a shared link shows the same content to everyone.
### 7.18 Not found and errors
Not found: an empty state with the search field and links to Personen, Parteien and Start. Errors: one
sentence, "Erneut versuchen", link home.
## 8. Responsive behaviour
| Width | Behaviour |
|---|---|
| < 640 px | one column; tables stack into labelled cards; filter forms collapse; role timeline labels above the bars; tabs scroll horizontally |
| < 768 px | the calendar becomes an agenda (only days with events) |
| 640–1023 px | two-column card grids; tables scroll horizontally inside their container; menu still collapsed |
| ≥ 1024 px | full header navigation with search field; profile pages get the aside column; two-column sections |
| ≥ 1280 px | content width caps at 76 rem; more cards per row |
## 9. Accessibility (WCAG 2.2 AA)
* Contrast ≥ 4.5:1 for text and ≥ 3:1 for UI parts in both themes (token values are chosen and checked
for it, see the design system).
* Never colour alone: positions, results, statuses and party identity always have text, and an icon or
a shape where possible.
* Keyboard: skip link, visible focus ring on everything, tabs and disclosures are native links and
`details` elements, no keyboard traps.
* Structure: one `h1` per page, headings in order, landmarks (header, nav, main, footer), `aria-current`
for the active page and tab, tables with header cells, charts with text equivalents.
* Language: `lang` on the document; English summaries are marked when shown in the German UI.
* Motion: no animation beyond short hover transitions; `prefers-reduced-motion` removes them.
* Targets at least 24 × 24 px; form fields have visible labels.
## 10. Privacy and performance
* No third-party requests from the page itself: system fonts, no analytics, no embeds, no CDNs.
Portraits and logos still come from Wikimedia and the parliament; they load lazily with
`referrerpolicy="no-referrer"`. **(later)** an image proxy so visitors' IP addresses never reach
third parties.
* Server-rendered HTML; client JavaScript only for the filter settings, the theme switch, the language
switch and active-link marking.
* Budget: under 100 kB of CSS and JavaScript per page (compressed), first content without waiting for
scripts.
## 11. Before going public
Not design work, but the design depends on them; flagged here so they are not forgotten:
* **Impressum and Offenlegung** (§ 5 ECG, § 25 MedienG) and a **privacy policy**: footer links and a
prose page each.
* A **corrections and right-of-reply** path (§ 9 MedienG): a contact on the methodology page and, later, a
form linked from every claim page.
* The X deletion sync (see the roadmap) before X data is shown publicly.
## 12. Later
* Claim permalinks are implemented; **(later)** a "Fehler melden" button on them.
* Compare two people or parties side by side (votes, money, statements).
* Topic pages (`/themen/{slug}`): everything on one topic across people and parties.
* Alerts (e-mail or RSS) for people, parties and topics.
* A review queue for editors in the web UI (today `politrack review` on the command line).
* A map of trips and appearances.
* Share images (Open Graph) generated per profile and claim.
* CSV export of every table.
+305
View File
@@ -0,0 +1,305 @@
# Politrack design system
The implementation reference for the web app: tokens, components, CSS conventions, content rules and
recipes. The concept behind it (goals, principles, page concepts) is in [README.md](README.md); how to add
pages and claim types step by step is in [contributing/frontend.md](../contributing/frontend.md).
## 1. Architecture
```
web/src/styles/
tokens.css design tokens (custom properties), light + dark
base.css element defaults: typography, links, tables, forms, buttons, text utilities
layout.css page frame (header, menu, footer) and layout utilities
components.css one section per component in src/components
print.css print: content only, link targets written out
web/src/components/ server components (a few client components, marked 'use client')
web/src/app/icon.svg favicon (the brand mark)
```
All five stylesheets are imported once, in order, by `src/app/[locale]/layout.tsx`.
* **Plain global CSS** with custom properties: no CSS framework, no CSS-in-JS, no npm dependency beyond
Next.js, React and next-intl. The UI must keep working where npm is slow or blocked.
* **Class names**: one block per component in kebab-case (`.claim`, `.claim-head`, `.vote-row`);
variants with `--` (`.badge--positive`, `.notice--legal`); states with `is-` (`.is-current`,
`.is-disputed`); `aria-current` for the active link or tab.
* **Only tokens** for colour, spacing, radius and type. The exceptions are deliberate: party colours
(from the data), platform brand colours, and the favicon.
* **Server first.** Pages and most components are async server components. Client components exist only
where the browser must act: `ThemeSwitcher`, `LocaleSwitcher`, `NavLink`, `SettingsForm`, the error page.
* **No JavaScript needed** for navigation, filters, tabs, disclosures or the mobile menu: links, GET forms,
`<details>` and one checkbox toggle.
## 2. Tokens
### Colour
| Token | Light | Dark | Use |
|---|---|---|---|
| `--paper` | `#f6f5f1` | `#131311` | page background |
| `--surface` | `#ffffff` | `#1b1a17` | cards, panels, tables, header |
| `--surface-2` | `#efede7` | `#24231f` | subtle fills: chips, table heads, compact stats |
| `--surface-3` | `#e4e1d9` | `#2e2c27` | avatar placeholder, skeleton highlight |
| `--line` | `#dcd8cf` | `#36332d` | borders and rules |
| `--line-strong` | `#b9b3a6` | `#57534a` | emphasised rules, decorative outlines |
| `--line-field` | `#878073` | `#7d786f` | form field borders, switch track (≥ 3:1) |
| `--ink` | `#1c1b18` | `#ecebe6` | text |
| `--ink-2` | `#47443d` | `#c9c5bb` | secondary text |
| `--ink-3` | `#66625a` | `#a29d92` | muted text, meta data |
| `--accent` | `#4338ca` | `#a5b4fc` | links, focus, active navigation, bars |
| `--accent-hover` | `#3730a3` | `#c7d2fe` | hover of links and buttons |
| `--accent-soft` / `--accent-ink` | `#ebeafd` / `#312e81` | `#25264d` / `#c7d2fe` | info notices, accent badges |
| `--on-accent` | `#ffffff` | `#16163a` | text on accent buttons |
| `--mark` / `--mark-ink` | `#fde68a` / `#1c1b18` | `#5a4b10` / `#fff6d5` | highlighter: quotes, search hits |
| `--positive` / `-soft` | `#146c43` / `#e2f2e8` | `#74d3a2` / `#11301f` | "dafür", "angenommen", verified |
| `--negative` / `-soft` | `#b3261e` / `#fbe9e6` | `#ff9d90` / `#3c1612` | "dagegen", "abgelehnt" |
| `--caution` / `-soft` | `#8a5300` / `#fcf0d4` | `#f2c35c` / `#36290c` | legal matters, disputed, dissent, review |
| `--focus` | `#4338ca` | `#a5b4fc` | focus ring |
**Contrast** (WCAG 2.2: text ≥ 4.5, UI parts ≥ 3; computed from the values above):
| Pair | Use | Light | Dark |
|---|---|---|---|
| `--ink` on `--paper` | body text | 15.8 | 15.6 |
| `--ink-2` on `--surface` | secondary text | 9.7 | 10.1 |
| `--ink-3` on `--paper` | muted text on the page | 5.6 | 6.9 |
| `--ink-3` on `--surface-2` | muted text on fills | 5.2 | 5.8 |
| `--accent` on `--surface` | links on cards | 7.9 | 8.7 |
| `--on-accent` on `--accent` | primary button | 7.9 | 8.7 |
| `--accent-ink` on `--accent-soft` | info notice, accent badge | 9.6 | 9.6 |
| `--mark-ink` on `--mark` | highlighted search hit | 13.8 | 7.9 |
| `--positive` on `--positive-soft` | positive badge | 5.6 | 7.9 |
| `--negative` on `--negative-soft` | negative badge | 5.6 | 8.0 |
| `--caution` on `--caution-soft` | caution badge | 5.6 | 8.6 |
| `--line-field` on `--surface` | form field border | 3.9 | 4.0 |
`--line` and `--line-strong` are decorative (card edges, rules) and are never the only way to recognise a
control. Re-run the numbers when you change a colour; the check is twenty lines of Python (relative
luminance per WCAG).
**Party colours** come from the data (`organization.color`) and are used only through
`partyStyle(color)` → `--party`, by `.party-dot`, `.party-band` (a 4 rem bar), `.party-card` (a 4 px top
edge) and party bars in the role timeline. Never as text colour, never as a large background, always next
to the party's name. Dots get a thin ring so dark party colours stay visible on the dark theme.
**Platform colours** (`.platform-bluesky` …) follow the same rule: a dot next to the platform name.
### Typography
System font stack (`--font-sans`): fast, private (no font downloads) and native on every device.
`--font-mono` for identifiers (file numbers, Firmenbuch numbers, item references).
| Token | Size | Use |
|---|---|---|
| `--text-xs` | 0.75 rem | meta data, badges, axis labels |
| `--text-sm` | 0.875 rem | secondary text, tables, filters |
| `--text-base` | 1 rem | body |
| `--text-md` | 1.125 rem | ledes, search field |
| `--text-lg` | 1.25 rem | h2 |
| `--text-xl` | 1.5 rem | key figures, claim title on its permalink page |
| `--text-2xl` | clamp(1.625 → 2.125 rem) | h1 |
| `--text-display` | clamp(2 → 3 rem) | home headline |
Line height 1.55 for text, 1.2 for headings. Headings use `text-wrap: balance`. Running text, list items,
table cells and headings hyphenate (`hyphens: auto` with the document's `lang`), which keeps long German
compounds from breaking layouts on phones.
### Spacing, shape, depth
* Spacing on a 4 px grid: `--space-1` 0.25 rem · `-2` 0.5 · `-3` 0.75 · `-4` 1 · `-5` 1.5 · `-6` 2 ·
`-7` 3 · `-8` 4 rem.
* Radius: `--radius-sm` 4 px (chips, position badges) · `--radius` 8 px (fields, buttons, notices) ·
`--radius-lg` 12 px (cards, panels, tables) · `--radius-pill`.
* Depth: borders first. `--shadow-sm` on hover of link cards; `--shadow` only for overlays (menus,
sticky action bar).
### Layout
* `--page-max` 76 rem content width, `--prose-max` 44 rem for reading, `--gutter` clamp(1–2 rem),
`--header-h` 3.5 rem.
* Breakpoints (media queries can't use custom properties, so they are written out): **640 px** (stacked
tables, collapsed filters, one-column role timeline), **768 px** (calendar → agenda), **1024 px** (full
header navigation, aside columns), **1360 px** (header tool labels).
### Theming
The light theme is defined on `:root`. The dark theme is defined twice with the same values: once in
`@media (prefers-color-scheme: dark)` for `:root:not([data-theme='light'])`, once for
`:root[data-theme='dark']`. `ThemeSwitcher` sets `data-theme` and the `pt_theme` cookie; the layout reads
the cookie, so the server renders the chosen theme without a flash. **A new token goes into all three
blocks.**
## 3. Icons
`<Icon name="…" />` (`src/components/Icon.tsx`): inline SVG on a 24 px grid, 1.75 px strokes in
`currentColor`, decorative (`aria-hidden`) unless you pass `label`. Sizes follow the text (`1em`) unless a
component sets them.
Available: `search person people flag building euro vote calendar message book history sliders external
document check checks alert scale globe sun moon monitor arrow-right arrow-left chevron-down
chevron-right link quote mic plane briefcase landmark info close menu pin news question pencil pie
activity shield code`.
To add one, append a list of path strings (and `c:cx,cy,r` for circles) to `PATHS`. Draw it yourself from
simple geometry rather than copying a set with a licence to track. Claim types map to icons in
`TYPE_ICONS` (`badges.tsx`).
`<BrandMark />` is the logo: brackets around a dot (a citation with its evidence). `src/app/icon.svg` is the
same mark as the favicon.
## 4. Components
| Component | File | Use |
|---|---|---|
| `SiteHeader`, `SiteFooter` | `SiteHeader.tsx`, `SiteFooter.tsx` | page frame; navigation from `nav.ts` |
| `NavLink` (client) | `NavLink.tsx` | navigation link with `aria-current` for the section |
| `ThemeSwitcher`, `LocaleSwitcher` (client) | | system → light → dark; same page in the other language |
| `PageHeader`, `Breadcrumbs`, `SectionHead` | `PageHeader.tsx` | title block; crumbs (home is added); h2 with an "Alle …" link |
| `Tabs` | `Tabs.tsx` | link tabs with counts; scroll shadows when they overflow |
| `FilterBar`, `Field`, `countActive` | `FilterBar.tsx` | GET filter form, collapses on phones |
| `Pagination` | `Pagination.tsx` | previous / page n / next (the API has no totals) |
| `Notice`, `EmptyState` | `Notice.tsx` | notes with tone `note`/`info`/`caution`/`legal`; empty results |
| `ExternalLink` | `ExternalLink.tsx` | link to a source: new tab, no referrer, announced to screen readers |
| `TypeLabel`, `SourceMeter` | `badges.tsx` | claim type with icon; source count / disputed |
| `PartyTag`, `StatusBadge` | `badges.tsx` | party dot + short name; person status |
| `PositionChip`, `ResultBadge` | `badges.tsx` | vote position (optionally per party); accepted/rejected |
| `PlatformBadge`, `platformName` | `badges.tsx` | social platform |
| `Avatar`, `PersonCard` | `Avatar.tsx`, `PersonCard.tsx` | portrait or initials; person in a grid |
| `StatGrid`, `BarList`, `ProportionBar` | `charts.tsx` | key figures; ranked bars; shares of a whole |
| `Timeline`, `DateRangeForm` | `Timeline.tsx` | activity per month (bars link to the month); date filter |
| `RoleTimeline` | `RoleTimeline.tsx` | offices and memberships on a year axis |
| `ClaimList`, `ClaimCard`, `EvidenceList` | `ClaimList.tsx` | the claim card and its sources |
| `ClaimDetails`, `OrgLink` | `ClaimDetails.tsx` | type-specific details (registry `DETAILS`) |
| `MoneyTable` | `MoneyTable.tsx` | money flows in four columns (date + kind, from, to, amount + source chips); `self` mutes the page's own organisation |
| `VoteList`, `VoteLegend` | `VoteList.tsx` | votes (vote items or one person's votes); `compact` for panels |
| `SocialFeed` | `SocialFeed.tsx` | post cards, no embeds |
| `SearchForm` | `SearchForm.tsx` | the big search field (optional date range) |
| `SettingsForm` (client) | `SettingsForm.tsx` | "Meine Filter" |
### The claim card
`ClaimCard` renders, in this order: head (type label, date, subject, source meter), legal stage row,
summary, `ClaimDetails`, innocence notice (`needsInnocenceNotice()`), review notice, topics, and the foot
with the evidence disclosure and the permalink. `standalone` is the permalink page variant: the summary
becomes the `h1` and the sources are expanded. Rules:
* Never render a claim without its evidence.
* Don't remove or move the innocence notice; it belongs inside the card.
* Show the subject only when the list mixes subjects (`showSubject={false}` on profiles).
* When the English UI shows a German summary, it is marked `lang="de"`.
### Filters
```tsx
<FilterBar id="money" active={countActive(sp.kind, sp.q)} resetHref="/geld" hidden={{org: sp.org}}>
<Field label={t('kindLabel')}><select name="kind" defaultValue={sp.kind ?? ''}>…</select></Field>
<Field label={t('nameLabel')} wide><input type="search" name="q" defaultValue={sp.q} /></Field>
</FilterBar>
```
`active` drives the count badge, the reset link and whether the phone toggle starts open. Hidden values
keep context (an organisation, the current tab) across submits.
### Tables
```tsx
<div className="table-wrap">
<table className="table-stack">
<thead><tr><th scope="col">{t('date')}</th>…</tr></thead>
<tbody><tr><td data-label={t('date')}>…</td>…</tr></tbody>
</table>
</div>
```
`.table-wrap` gives the frame and horizontal scrolling. `.table-stack` turns rows into labelled cards
below 640 px, using each cell's `data-label`. Numbers: `className="num"` (right-aligned, tabular).
Amounts: `<span className="amount">`. Keep tables to four or five columns: secondary values (a kind badge, source
chips) go into the cell they belong to rather than into a column of their own.
### Charts
All charts are HTML and CSS, with the numbers as text:
* `StatGrid` is a `<dl>`; values can link to the list behind them.
* `BarList` scales bars to the largest value in the list. Use it for rankings of the same unit only, and
label each row's kind (`meta`).
* `ProportionBar` has `role="img"` with a text alternative, plus a legend with counts.
* `Timeline` bars are links with labels ("März 2026: 12").
* `RoleTimeline` labels every bar with its years. Roles without a start date are listed in the roles panel
only.
## 5. Layout utilities
| Class | Does |
|---|---|
| `.page` | the main column (set by the layout) |
| `.stack`, `.cluster` | vertical stack with gap; wrapping row with gap |
| `.grid-2` | two columns ≥ 22 rem, one column below |
| `.grid-cards` | auto-fill card grid; `--card-min` (default 17 rem) |
| `.layout-aside` | main + 20 rem aside from 1024 px |
| `.section`, `.section-head` | vertical rhythm between sections; h2 row with a "more" link |
| `.prose` | 44 rem reading width |
| `.panel` | aside box with an h2 |
| `.link-list`, `.name-list`, `.meta-list` | ruled list; multi-column names; definition grid |
| `.muted`, `.small`, `.xsmall`, `.strong`, `.num`, `.mono`, `.nowrap`, `.block`, `.visually-hidden` | text utilities |
## 6. Content
### Voice and wording
Sachlich and precise; "du"; gender-inclusive with a colon (Politiker:innen, Funktionär:innen). Say what the
source says, with the source.
| Instead of | Write |
|---|---|
| "X kassierte 25.000 €" | "Laut Rechnungshof erhielt X 25.000 € (Spende)" |
| "Skandal um …", "dubios" | the fact and its source; no adjectives that judge |
| "X ist korrupt", "Anklage!" | the procedural stage ("Ermittlungsverfahren") + the innocence notice |
| "X und Y sind verbandelt" | "X ist laut Firmenbuch Geschäftsführer von Y" + the connection note |
| "X hat 33.000 Follower" (from somewhere) | only data we collected, with its source and date |
### Numbers, dates, money
* Use `formatDate`, `formatMonth`, `formatPeriod`, `formatNumber`, `formatMoney`, `formatMoneyCompact`
from `lib/format.ts`. They use de-AT and en-GB conventions (`12. März 2026`, `€ 25.000`,
`1,2 Mio. €`, `2. Halbjahr 2025`).
* Exact amounts in tables, compact amounts in summaries and bar lists (exact value in the `title`).
* Pass numbers to ICU messages (`{count, plural, …}`), never pre-formatted strings. Pass IDs as strings,
so they don't get thousands separators.
* Never add up different kinds of money into one figure without saying so.
### Interface text
* Every key goes into both `messages/de.json` and `messages/en.json`; German first.
* Labels are short nouns ("Quellen", "Filter"); buttons are verbs ("Anwenden", "Speichern").
* Links say where they go ("Alle Abstimmungen", "Original ansehen"), never "hier klicken".
* Keep a dash with the preceding word using a non-breaking space where a line break would start a line
with "–" (the home headline does this).
## 7. Accessibility checklist (for every UI change)
- [ ] Headings in order, one `h1`; landmarks intact; new sections have `aria-labelledby`.
- [ ] Everything works with the keyboard and shows the focus ring.
- [ ] Nothing relies on colour alone: badges have words, positions have icons, charts have numbers.
- [ ] Contrast of new colours checked (§2).
- [ ] External links use `ExternalLink` (new tab announced).
- [ ] Images: `alt=""` when decorative (the name is next to the portrait), real text otherwise.
- [ ] Phone width (390 px): no horizontal scrolling, tables stack, nothing cut off.
- [ ] Both themes look right; both languages fit (English is shorter, German compounds are longer).
## 8. Recipes
**An index page.** Use `PageHeader` → `FilterBar` → results (`grid-cards`, a `table-wrap`, or
`ClaimList`) → `Pagination` → a source `Notice`. The persons and organisations pages are the templates.
**A profile section.** Use a `section` with `aria-labelledby` and a `SectionHead` (with `more` pointing to
the pre-filtered explorer), then the list component. Put secondary facts into a `.panel` in the aside.
**Showing a new claim type.** Add a renderer to `DETAILS` in `ClaimDetails.tsx`, an icon to `TYPE_ICONS`
in `badges.tsx`, and the label `Claims.type.<type>` in both message files.
**A new chart.** Prefer `StatGrid`, `BarList` or `ProportionBar`. If you need something else, build it from
HTML/CSS with the numbers as text, add its section to `components.css`, and document it here.
**A new token.** Add it to all three blocks in `tokens.css`, check its contrast, and add it to §2.
+10
View File
@@ -34,6 +34,12 @@ Next.js frontend (de/en), Docker Compose for Unraid.
- [x] Pages: organisations, money explorer, social feed, Wikipedia edits, person network ("follow the money"),
say-vs-vote on roll calls
**Design (v2)** – see [design/README.md](design/README.md):
- [x] Design concept and design system (neutral palette, light/dark, tokens with checked contrast, icons)
- [x] Redesigned pages: header with search and filter count, profiles with role timeline and key figures,
party and organisation dashboards, vote list with position chips, calendar agenda on phones
- [x] Claim permalink pages with citation (`/angaben/{id}`, `GET /api/v1/claims/{id}`)
## Next
**Data**
@@ -53,6 +59,10 @@ Next.js frontend (de/en), Docker Compose for Unraid.
- [ ] Wayback Machine archiving of evidence URLs (`document.archive_url`)
**Product**
- [ ] Before going public: Impressum/Offenlegung (§ 5 ECG, § 25 MedienG), privacy policy, a corrections contact
- [ ] Image proxy for portraits and logos (no visitor IPs to Wikimedia/parliament)
- [ ] Compare two people or parties; topic pages (`/themen/{slug}`); Open Graph share images; CSV export
- [ ] Review queue for editors in the web UI (today `politrack review`)
- [ ] Community link submissions and a corrections/right-of-reply form
- [ ] Alerts (e-mail/RSS) for followed politicians
- [ ] Optional accounts via reverse-proxy SSO using `app_user`