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:
1 parent
45514624f4
commit
456f9816f4
76 files changed
+7127
-1280
No files matched your search
@@ -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
|
||||
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
|
||||
Reference in new issue
Block a user