Files
ScarriffleandClaude Opus 5 6b7317b4e0 docs: Design-Vertrag fuer Web, iOS und Android
Unterhalb der Farbebene war nie etwas einmal entschieden worden: Android
hat 40 verschiedene Abstandswerte, 9 Schriftgroessen, 9 Radien und vier
verschiedene Seitenraender; das Web streut aehnlich. Deshalb springt der
Textrand beim Screenwechsel und dieselbe Termin-Pille hat vier Radien.

Der Vertrag legt Abstandsleiter, Radien nach Rolle, Schriftskala,
Trefferflaechen, Komponenten-Inventar, Zustaende, Termin-Darstellung und
den gemeinsamen Widget-Zuschnitt fest -- nach dem Vorbild von
SETTINGS_SYNC.md, das denselben Zweck fuer die Einstellungen erfuellt.

Bewusst nicht vereinheitlicht: Navigation, Systemschrift, Bedienelemente
und Gesten. Gleiche Struktur und gleiche Masse, nicht gleiche Pixel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 11:56:59 +02:00

234 lines
9.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design contract (Web / iOS / Android)
> For the user-configurable **colours**, see [../THEME.md](../THEME.md). Colours come from
> the server and are already consistent across clients — this document covers everything
> below the colour level, which is not.
The three clients should read as one product. That means the same structure and the same
measurements, **not** the same pixels: each app keeps its platform's navigation, system font
and controls. A Material button on iOS would feel wrong, and a swipe-back gesture on Android
would feel broken.
This document is the reference the clients are checked against. When a value here and a value
in the code disagree, the code is wrong.
---
## Why this exists
Nothing below the colour level was ever decided once. Counted across the clients today:
| | Web | Android | iOS |
|---|---|---|---|
| Distinct spacing values | 8 | **40** | not audited |
| Distinct font sizes | 8 | **9** | system styles |
| Distinct corner radii | 8 | **9** | mixed |
| Screen gutters | — | **4** (12/16/20/28) | — |
The result is that the left text edge visibly jumps between screens, the same event pill has
four different radii, and nine list rows were each built separately with five different
paddings. That is what makes an app look hand-made.
---
## Spacing
A four-step ladder. Every margin, padding and gap is one of these — no other value is allowed
without a comment saying why.
| Token | Value | Use |
|---|---|---|
| `xs` | 4 | Inside a control; gap between an icon and its label |
| `sm` | 8 | Between related items in a list; vertical padding of a compact row |
| `md` | 12 | Vertical padding of a standard row |
| `lg` | **16** | **Screen gutter**, section spacing, dialog padding |
| `xl` | 24 | Between major sections |
| `xxl` | 32 | Above a primary action, around empty states |
**One screen gutter: `lg` (16).** Every screen, every sheet, every dialog. This is the single
change with the most visible effect — it is what stops the text edge from jumping.
| Platform | How |
|---|---|
| Web | `--space-xs … --space-xxl` in `:root`; existing `6px`/`10px` paddings collapse to `xs`/`sm` |
| Android | `ui/theme/Spacing.kt`, reached as `MaterialTheme.spacing.lg` |
| iOS | `enum Spacing { static let lg: CGFloat = 16 }` in the shared design file |
---
## Corner radii
Defined by role, not by number, so the same object cannot drift apart between views.
| Token | Value | Applies to |
|---|---|---|
| `event` | 4 | Event pills in month, week, day, agenda, all-day strip — **all of them** |
| `field` | 8 | Text fields, list rows, small cards, popovers |
| `sheet` | 20 | Modals, bottom sheets, day preview |
| `full` | pill | Buttons, chips, FAB |
Today the event pill is 3 in month view, 4 in week view, 3 in the all-day strip and 10 in the
agenda on Android; the web uses 3. All become `event`.
---
## Type scale
Six steps. **Nothing below 11.**
| Token | Size | Use | Material | iOS |
|---|---|---|---|---|
| `caption` | 11 | Timestamps, metadata, hints | `labelSmall` | `.caption2` |
| `label` | 12 | Field labels, chips, tab labels | `labelMedium` | `.caption` |
| `body` | 13 | Default reading size | `bodySmall` | `.footnote` |
| `bodyLarge` | 14 | Primary list text, input text | `bodyMedium` | `.subheadline` |
| `title` | 18 | Screen and section titles | `titleMedium` | `.headline` |
| `headline` | 22 | Page headline, month name | `headlineSmall` | `.title2` |
**Android must configure `Typography()`** — today it is the raw Material default
(`ui/theme/Theme.kt:68`), which is why every text size in the app is an ad-hoc `fontSize = N.sp`
override at the call site.
**The one documented exception:** the month grid's overflow counter (`+N`) and week-number
label may use **10**. They sit in a dense grid where 11 does not fit at the current row height.
The 8 and 9 values used today (five places) are removed outright. If the month grid is ever
made height-adaptive, this exception goes away too.
---
## Hit targets
Minimum **48 × 48** for anything tappable, on every platform. A smaller icon is fine; the
touch area around it is not.
The top app bar is **64** tall with 48 targets. Android currently uses 50 with 40 targets,
which is the first thing a user sees and reads as cramped.
Every actionable icon has an accessibility label. Decorative icons — and only those — get
none. Android has 30 `contentDescription = null` today, several of them on buttons
(month back/forward, calendar visibility toggle, remove-reminder, password reveal).
---
## Component inventory
Every client implements these, named the same way, with the same anatomy. If a screen needs
something that is not on this list, it goes on the list first.
### `ListRow`
`[leading icon or colour dot] [title, one line, ellipsised] [optional subtitle] [trailing control]`
Vertical padding `md`, horizontal `lg`, full-width tap area, `field` radius when it sits on a
surface. Android should build this on `ListItem`; today the app has **zero** `Card` and **zero**
`ListItem` — nine separate hand-rolled variants instead.
### `SettingsRow`
A `ListRow` whose trailing control is a switch, a dropdown or a colour swatch. One
implementation, not four.
### `EmptyState`
`[title] [one sentence of body] [optional action]`, centred, `xxl` above. Every list has one.
Today only the agenda does it properly (`AgendaView.kt:49-62`) — copy that.
### `LoadingState`
One spinner, centred, same size everywhere. Never a different shape per screen. The keys
`settings.loading`, `profile.loading` and `accounts.loading` already exist in the translation
tables and are never rendered.
### `Feedback`
**Success and failure appear in one place per platform** — a snackbar on Android, a toast on
web, a transient banner on iOS. Never as loose text in the page flow, and never as the last
item of a scrolling list, where the user who caused the error cannot see it.
---
## States
Every list and every screen defines all four. A missing state is a bug, not an omission.
| State | Rule |
|---|---|
| **Loading** | Shown from the first frame, never a blank screen |
| **Empty** | Title plus one sentence; an action when there is a sensible one |
| **Error** | In the feedback slot above, with what failed and what to do |
| **Partial** | Stale data stays visible. Never replace loaded content with an empty list because one source failed — see `CalendarViewModel.kt:377-416` |
---
## Event rendering
The one object all three clients draw, and the one most likely to drift.
| | Rule |
|---|---|
| Radius | `event` (4) everywhere |
| All-day | Own strip above the time grid, packed into lanes |
| Timed, overlapping | **Side by side, never stacked.** Android's week and day views currently draw every event at full column width, so one of two 10:00 meetings is completely invisible (`TimeGridView.kt:156-169`) |
| Overflow | `+N` when the lane count is exceeded, with a way to see the rest |
| Colour | Per-event colour, else calendar colour; the group view's server-resolved colour wins |
| Title | The server-decorated title (birthday age, group prefix) wins over the raw one |
---
## Widgets
Home-screen widgets are **always native**: iOS requires WidgetKit with SwiftUI views, Android
requires Glance or RemoteViews. No cross-platform framework changes this. What is shared is
the **selection and the content**, defined here once.
| Widget | Shows | iOS | Android |
|---|---|---|---|
| `today` | Today's events | small | 2×2 |
| `upNext` | The next few events | medium | 4×2 |
| `month` | Month grid with event dots | medium | 4×4 |
iOS ships 13 widgets today; these three are the ones Android mirrors. The rest stay
iOS-only — that is fine, as long as the three above look and behave the same.
**Data path, both platforms:** the app writes a snapshot into shared storage; the widget reads
it and performs **no networking of its own**. The snapshot format is defined by `CalendarrCore`
on iOS and is the reference for Android's equivalent.
---
## Wording
The translation keys in `android/…/ui/L10n.kt` and `ios/…/Models/Localization.swift` are
deliberately kept identical — Android even converts `%@` to `%s` so the same table works. That
is a contract: **a key means the same thing on every platform**, and a new key is added to all
of them.
Nothing user-visible is hard-coded. Current violations, all of which show a German string to
an English user:
- `ColorPickerDialog.kt` — `"Abbrechen"`, `"OK"`, `"Hex"`
- `NotificationScheduler.kt` / `.swift` — every notification body
- The nine watch complication names on iOS
- Date patterns built without a `Locale`, and `is24HourView = true` forced
---
## Deliberately not unified
| | Why |
|---|---|
| Navigation | Bottom sheets and a drawer on Android, navigation stack and sheets on iOS |
| System font | Roboto / SF — using one font on both would look wrong on one of them |
| Controls | Switches, pickers, date entry stay platform-native |
| Gestures | Swipe-back on iOS, system back on Android |
| Widget families | Each platform has its own sizes; only the three above must correspond |
---
## How to check
There is no automated check — both apps have **zero tests**. So it is done by eye, and these
are the questions that catch real drift:
1. Open every screen in turn. Does the **left text edge** stay put?
2. Put the two apps side by side on the same screen. Same order, same words, same rows?
3. System font size to maximum. Does anything clip? Do icons grow with their text?
4. Device language to English. Any German left? Trigger a reminder and read it.
5. Two events at the same time, week view. Are **both** visible?
6. Turn the server off and save something. Does the message appear in the feedback slot?
7. Every list: force it empty. Is there an empty state, or just nothing?