UX sprint U2: design-spec.md, screen catalogue, strings.csv (owner approved)
Kalas (muted) design spec with final tokens, components and every screen S-1 to S-22 in all states; 80-frame screen catalogue with 155 PNGs in docs/ux/screens/; strings.csv with D-017 module prefixes (558 rows, en and sv); colour tokens renamed to the landed contract names slate and amber; fixed category ids; proposed-decisions.md with the final §9.7 hex values and UX-9. Supersedes tokens.md, components.md, screens.md and the U1 Kalas PNGs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U2TeboHB8AtnzTrt3bWFzY
This commit is contained in:
+16
-44
@@ -1,50 +1,22 @@
|
||||
# Kvitt UX (sprint U1)
|
||||
# Kvitt UX
|
||||
|
||||
The UX deliverables for Kvitt v1 (SPEC §13), produced 2026-10-17. Phase 1 (U1) is here: three visual directions plus the direction-independent design documents. After the owner picks a direction, phase 2 (U2) consolidates `tokens.md`, `components.md` and `screens.md` into `design-spec.md` for the chosen direction (SPEC §13.4) and deletes what it supersedes.
|
||||
The UX deliverables for Kvitt v1 (SPEC §13). Phase 1 (U1, 2026-10-17) proposed three visual directions; the owner picked **3 "Kalas", muted** on 2026-10-18. Phase 2 (U2, SPEC §13.4) is the binding design for M6 (`:core:design`) and M7a–M7f (the feature screens).
|
||||
|
||||
## How to read
|
||||
## Binding files (U2)
|
||||
|
||||
1. **Look first:** open `directions/1-skargard/index.html`, `directions/2-kvitto/index.html` and `directions/3-kalas/index.html` in a browser (or the PNGs in each `png/` folder). Every screen S-1 to S-22 is there in light and dark, in Swedish, with the same household data, so the same file name compares one screen across directions.
|
||||
2. **Compare and decide:** `directions/README.md` has the comparison table and the recommendation.
|
||||
3. **Then the structure,** which holds whichever direction wins:
|
||||
| File | What it is | Used by |
|
||||
|---|---|---|
|
||||
| `design-spec.md` | The design spec: tokens (colour roles light and dark, 14 category colours with containers and contrast, chart and avatar colours, type scale, spacing, shapes, elevation, motion, icons, the 14 default categories, Compose mapping), the component inventory with anatomy, sizes, states and semantics, every screen S-1 to S-22 and the notifications with layout, the binding list of states, interactions, TalkBack and 200 %, the add-expense flow in detail, open points | M6, M7a–M7f, screenshot tests |
|
||||
| `strings.csv` | Every user-visible string, `key,en,sv,notes`, keys prefixed per D-017 (feature modules) or unprefixed (`:core:design`); placeholders `%1$s`, plurals as `<key>:one` / `<key>:other` | M6, M7 (`values/strings.xml`, `values-sv/strings.xml`) |
|
||||
| `screens/` | The screen catalogue: `screens/index.html` and one PNG per frame and theme, `screens/<frame-id>-light.png` / `-dark.png` (200 % and English frames light only), Swedish. Every state that `design-spec.md` §4 lists has a frame | M7 (look), the owner |
|
||||
| `ia.md` | Bottom navigation, screen inventory with modules and state sources, session routing, navigation map, data ownership, admin vs member | M7, M8 |
|
||||
| `flows.md` | F-1 … F-11 step by step | M7, M8 |
|
||||
| `category-suggestion.md` | The category suggestion rule CS-1 … CS-7 and the evidence from the household's Splitwise export | M5/M7c, the spec change UX-3 |
|
||||
| `category-keywords.csv` | The built-in keyword table of CS-6 (draft of `contracts/category-keywords.csv`) | `:domain`, UX-3 |
|
||||
| `proposed-decisions.md` | The owner's answers and decisions UX-1 … UX-8, with the spec text a docs agent records on `main` (numbers assigned then) | orchestrator, docs agent |
|
||||
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| `ia.md` | Bottom navigation (three destinations, argued), screen inventory S-1…S-22 with modules and state sources, session routing, navigation map, which screen owns which data, admin vs member |
|
||||
| `screens.md` | Every screen: purpose, content in priority order, empty/loading/offline/error/long-content states, actions; the add-expense flow in detail |
|
||||
| `flows.md` | F-1 … F-11: first run as admin, join as partner, add expense online and offline, edit the other's expense, global and per-account settle-up, recurring, archive, in-app update, remove a lost phone |
|
||||
| `components.md` | The `:core:design` component list: anatomy, variants, states, semantics |
|
||||
| `tokens.md` | Token names and their values in all three directions (colour roles light/dark, semantic and category colours with contrast, charts, type, spacing, shapes, elevation, motion, icons) |
|
||||
| `strings.csv` | Every UI string, `key,en,sv,notes` (SPEC §13.4 format). Placeholders are Android format arguments (`%1$s`, `%d`); plurals are two rows `<key>:one` and `<key>:other`, which become one `<plurals name="<key>">`. Notification copy (SPEC §13.3, Swedish written here) has the prefix `notif_`. |
|
||||
| `directions/` | The three directions, their PNGs and `export-png.sh` |
|
||||
| `proposed-decisions.md` | The owner's answers (direction pick: Kalas, muted), findings, and the decisions and spec text a docs agent should record on `main` |
|
||||
Precedence: SPEC rules › `design-spec.md` › PNGs; words from `strings.csv`. `design-spec.md` §1 lists where the design depends on a decision that is not in SPEC.md yet (UX-n).
|
||||
|
||||
## The three directions
|
||||
## U1 record (not maintained)
|
||||
|
||||
**1 Skärgård** — the Stockholm archipelago on a calm day. Material 3 Expressive in sea blue, granite and sand, with large rounded shapes and generous whitespace. Home is a hero: "Du är skyldig Anna" above a large, narrow Roboto Flex figure and a single "Gör upp" button; accounts are a calm grouped list. The editor puts the amount in display size over an in-app keypad with Expressive key shapes. The most Android-native of the three, and the only one that can follow Material You wallpaper colours.
|
||||
|
||||
**2 Kvitto** — a till receipt and a household ledger. Warm paper and ink, one ink-blue accent, every amount in IBM Plex Mono, dashed rules between groups. Home opens with a compact receipt block and then a dense ledger table; account detail reads like a bank statement; an expense is a printed receipt with an "audit trail" below; "Ni är kvitt" is a rubber stamp. The densest and calmest direction; its risk is feeling austere.
|
||||
|
||||
**3 Kalas** — a crayfish party. Cream and white cards, a coral primary, very round shapes, and a solid category-colour tile on every expense so you recognise it before reading it. The balance is a seesaw (whoever owes sits lower, and the words always say it); settling up raises bunting. The editor shows the six most used categories as big tiles under the amount. The most personality; its risk is wearing thin over years of daily use.
|
||||
|
||||
## Where this follows the spec instead of the U1 brief
|
||||
|
||||
The owner's brief for this sprint and SPEC.md differ in places; the spec wins (CLAUDE.md › Source of truth):
|
||||
|
||||
| Brief | Spec (followed) |
|
||||
|---|---|
|
||||
| Screen ids `S-01 …` | `S-1 … S-22` as in SPEC §13.3 |
|
||||
| Bottom nav: recommend up to 4 | SPEC §13.2's three destinations kept, with the argument in `ia.md` §1 |
|
||||
| `docs/ux/proposals/`, 360 dp frames, S26+ at 384 dp | `docs/ux/directions/<n>-<slug>/index.html` at 411 dp (SPEC §13.1/§13.4); both phones are ~411 dp wide, so there is no separate S26+ layout |
|
||||
| Swedish in one direction, English in the others | Swedish everywhere (§13.4), plus one English Home frame per direction to show English fits |
|
||||
| `copy.md` table | `strings.csv` in the §13.4 format |
|
||||
| Category suggested from the description | AC-8.6: last category used in that account on this phone, else Övrigt (a description-based suggestion would be a new rule; open question) |
|
||||
| `tokens.md` "for the chosen scheme family" | Values for all three directions until the owner picks (§13.4 makes the pick precede the final spec) |
|
||||
|
||||
Additions the spec did not state, flagged for U2:
|
||||
|
||||
* **S-1 has a "Ditt namn" field:** AC-2.4 sends a display name with the accept call, but §13.3's S-1 lists no name field. A partner therefore types the name on S-1 and confirms it on S-2 (AC-2.10).
|
||||
* **Category colour hex values** changed for contrast (`tokens.md` §4); adopting them needs a §9.7 spec change after the pick.
|
||||
* **Avatar colours** for initials (AC-4.5) are defined per direction (`avatar-1`, `avatar-2`).
|
||||
* **Expense save shows no "Ångra"** (an undo would be an audited create + delete that also notifies the partner).
|
||||
* **Archived account detail hides the FAB** (an archived account is not offered for new expenses, AC-6.4).
|
||||
`directions/` holds the three U1 directions as they were shown to the owner: `1-skargard/`, `2-kvitto/`, `3-kalas/` (each an `index.html` review page with every frame), `README.md` (comparison and recommendation) and `export-png.sh`. Token values in `design-spec.md` were copied from `3-kalas/index.html`; later changes are made in `design-spec.md` and `screens/` only. U1's direction-independent `screens.md`, `components.md` and `tokens.md` were folded into `design-spec.md` and removed.
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
# Kvitt: components for `:core:design`
|
||||
|
||||
Status: U1 proposal (2026-10-17), direction-independent anatomy, states and API shape. The look (colours, shapes, type) comes from the chosen direction's tokens (`tokens.md`). In U2 this list moves into `design-spec.md` with final sizes. Names are Compose function names; every component takes a `Modifier` and gets screenshot tests in light and dark, Swedish and English, at 100 % and 200 % font scale (M6).
|
||||
|
||||
Rules for all components:
|
||||
|
||||
* Colours and type only from `KvittTheme` (Material 3 `ColorScheme` + `KvittColors` extension + `KvittTypography`); no literal values.
|
||||
* Strings only from resources (`strings.csv` keys); components that need words take them as resolved `String` parameters or take the model value and resolve inside `:core:design` (shared strings live there, §14.7).
|
||||
* Touch targets ≥ 48 dp (`minimumInteractiveComponentSize`), TalkBack `contentDescription` / `semantics` as stated per component.
|
||||
* Material 3 Expressive APIs (`@ExperimentalMaterial3ExpressiveApi`) are used only behind a `:core:design` wrapper so the stable fallback named below can be swapped in one place. The implementing agent confirms each API's status from the official documentation at implementation time (CLAUDE.md › Fast-moving APIs).
|
||||
|
||||
## 1. Money and text
|
||||
|
||||
### `MoneyText`
|
||||
Formats and shows an amount in öre via `MoneyFormatter` (§9.8). Never shows a sign.
|
||||
* Params: `ore: Long`, `style: AmountStyle` (`Hero`, `Large`, `Medium`, `Small`), `tone: AmountTone` (`Neutral`, `Owed`, `Owing`, `Muted`), `maxLines` (default 1; never ellipsizes: overflow wraps to 2 lines or auto-steps down one type size for `Hero`/`Large`).
|
||||
* Tabular figures always (`fontFeatureSettings = "tnum"`).
|
||||
* Semantics: spoken form from `MoneyFormatter.spoken(ore)`: Swedish "3 250,00 kronor", English "3,250.00 Swedish kronor" (TalkBack reads "kr" and "SEK" poorly).
|
||||
|
||||
### `AmountInput`
|
||||
Large amount entry for S-7, S-9, S-19. Parses per §9.6 (string → öre, never floating point).
|
||||
* Params: `value: String`, `onValueChange`, `error: String?`, `size` (`Display`, `Field`).
|
||||
* States: empty (hint "0,00"), typing, error (supporting text, error colour), disabled.
|
||||
* Accepts `,` and `.`; displays the locale's separator; inserts no-break-space thousands grouping while typing.
|
||||
|
||||
### `AmountKeypad` (only if the chosen direction uses an in-app keypad)
|
||||
3 × 4 grid: 1–9, decimal separator (locale), 0, backspace (TalkBack "Radera"); keys ≥ 64 dp tall; optional "Klar". Fallback: system keyboard `KeyboardType.Decimal`.
|
||||
|
||||
### `EffectLine`
|
||||
The one-line effect from the viewer's side (§9.2) for an `ExpenseEffect`.
|
||||
* Variants: `Lent(ore)` "Du lånade ut X" (owed tone) · `Borrowed(ore)` "Du lånade X" (owing tone) · `NotInvolved` "Inte inblandad" · `PaidForYourself` "Betalade för dig själv" (neutral).
|
||||
|
||||
### `BalanceWording`
|
||||
Wording + amount for a `BalanceView` (§9.3).
|
||||
* Variants: `Global` ("Anna är skyldig dig", "Du är skyldig Anna", "Ni är kvitt") and `Account` (… "Kvitt").
|
||||
* Semantics: merged into one sentence.
|
||||
|
||||
## 2. People
|
||||
|
||||
### `PersonAvatar`
|
||||
* Params: `user: User`, `size` (24, 32, 40, 56, 96 dp), `shape` (direction token; fallback `CircleShape`).
|
||||
* Picture via Coil keyed by `user.id` + `pictureVersion` (AC-4.6); else initials (first letter of the first two words, uppercase, AC-4.5) on `avatar-1`/`avatar-2` chosen by a stable hash of the user id (the two colours are tokens).
|
||||
* States: loading (initials shown), picture, initials, uploading (progress ring overlay).
|
||||
* Semantics: the display name (or decorative when the name is next to it).
|
||||
|
||||
### `PersonChip`
|
||||
Avatar 24 dp + name ("Du" for the viewer). Variants: `Display`, `Selectable` (S-7 payer toggle, S-7 full-split bearer). Selected state per M3 FilterChip.
|
||||
|
||||
### `PayerToggle`
|
||||
Two `PersonChip`s in a connected group; one tap switches. Label "Betalt av". Semantics: "Betalt av: du. Dubbeltryck för att byta till Anna."
|
||||
|
||||
## 3. Categories and accounts
|
||||
|
||||
### `CategoryIcon`
|
||||
Material Symbols icon (token from §9.7) on a container in `cat-<color>-container` with the icon in `cat-<color>`. Sizes 24/32/40/56 dp. Shape per direction (fallback circle or rounded square). Decorative when a name is shown next to it; else the category name.
|
||||
|
||||
### `CategoryChip`
|
||||
`CategoryIcon` (24) + name. Variants: `Display`, `Selectable`. Archived variant adds "(arkiverad)".
|
||||
|
||||
### `CategoryGrid`
|
||||
Picker grid in S-7 and S-19: tiles of icon + name, 4 columns at 411 dp (3 at 200 %), selected with outline/check. Only active categories.
|
||||
|
||||
### `CategoryIconPicker`, `ColorSwatchPicker`
|
||||
S-15 editor: 30 icons (6 columns), 12 colour swatches (6 columns), each 48 dp, selected with a check mark (not colour alone). Swatches have names for TalkBack ("Grön", "Orange", …).
|
||||
|
||||
### `AccountRow`
|
||||
Home and pickers: name, `BalanceWording.Account`, amount, secondary "11 utgifter i oktober". Variants: `Home`, `Picker` (no count), `Archived` (badge). 200 %: amount moves under the name.
|
||||
|
||||
## 4. Ledger
|
||||
|
||||
### `LedgerMonthHeader`
|
||||
"Oktober · 7 457,90 kr" (household spend); sticky in `LazyColumn`. Year added when not the current year.
|
||||
|
||||
### `ExpenseRow`
|
||||
`CategoryIcon`, description (1 line), date, `MoneyText` amount, `EffectLine`, optional account name (S-12), `SyncPendingMarker`. Heights per direction (56–88 dp). States: normal, pending, pressed. Semantics: "ICA Maxi Lindhagen, 1 247,60 kronor, 16 oktober, du lånade 623,80 kronor, väntar på synk".
|
||||
|
||||
### `SettlementRow`
|
||||
`payments` icon, "Anna betalade dig 800,00 kr", "· 2 konton", date, pending marker.
|
||||
|
||||
### `SyncPendingMarker`
|
||||
Small icon (`cloud_upload` or direction equivalent) in `pending`; label "Väntar på synk".
|
||||
|
||||
## 5. Balance and sync
|
||||
|
||||
### `BalanceHero`
|
||||
Home's global balance (S-4) and the account header (S-5, smaller variant).
|
||||
* Anatomy: both `PersonAvatar`s, `BalanceWording.Global`, `MoneyText(Hero)`, optional action button "Gör upp" / "Gör upp det här kontot".
|
||||
* States: `Owed` (partner owes you: owed colours, button), `Owing` (you owe: owing colours, button), `Settled` ("Ni är kvitt"/"Kvitt", no amount, no button), `Empty` (no accounts: welcome text), `Loading` (skeleton).
|
||||
* 200 %: wording and amount each take a full line; avatars may shrink to 40 dp.
|
||||
* Semantics: one merged sentence + the button.
|
||||
|
||||
### `SyncStatusLine`
|
||||
From `SyncSummary` + connectivity.
|
||||
* Variants: `Synced` ("Synkad för 3 min sedan"), `Syncing` ("Synkar…"), `Offline` ("Offline"), with suffix "· 2 ändringar väntar på synk" (plural); `Warning` (rejections or clock skew) with icon and warning colour.
|
||||
* Tap → S-16 (whole line is the 48 dp target). Relative time updates every minute.
|
||||
|
||||
### `UpdateBanner`
|
||||
S-17 states: `Available` (text + "Uppdatera" + "Senare"), `Downloading` (determinate `LinearProgressIndicator` + percent), `Failed` (text + "Försök igen"). Fallback for Expressive wavy progress: standard linear.
|
||||
|
||||
### `NeedsConnectionHint`
|
||||
Inline supporting text / banner "Kräver anslutning" with `cloud_off` icon; used on every online-only control (§9.9). Variant `Banner` for whole forms (S-6, S-14, S-15, S-19).
|
||||
|
||||
## 6. Split and settle
|
||||
|
||||
### `SplitModeSelector`
|
||||
Single-select connected buttons (Expressive `ButtonGroup`/connected toggle buttons; fallback `SingleChoiceSegmentedButtonRow`): "Kontots standard 60/40", "Egen fördelning", "Exakta belopp", "Allt på en person". Wraps to 2 rows (Swedish), 4 rows at 200 % (a vertical radio list fallback when the row cannot fit).
|
||||
|
||||
### `RatioEditor`
|
||||
Slider (0–100, step 1, haptic at 50) + two percent `TextField`s summing to 100 (two decimals) + preset chips (50/50, 60/40, 40/60). Used in S-6 and S-7 `ratio`. Semantics: slider "Jacobs andel, 60 procent".
|
||||
|
||||
### `ExactSplitEditor`
|
||||
Two money fields: the typed one editable, the other read-only "beloppet minus …"; error "Andelen är större än beloppet".
|
||||
|
||||
### `FullSplitPicker`
|
||||
Two `PersonChip`s: "Allt på Jacob" / "Allt på Anna".
|
||||
|
||||
### `SplitPreview`
|
||||
"Du 206,00 kr · Anna 206,00 kr" + the effect the row will show. Data from `ExpenseRepository.preview`; hidden when null. Optional bar showing the two shares as proportions (decorative).
|
||||
|
||||
### `SplitPicker` (composite)
|
||||
The S-7 split section: `SplitModeSelector` + the mode's editor + `SplitPreview`; collapsed state is a single chip "Delning: Kontots standard 50/50" which expands it in one tap.
|
||||
|
||||
### `DirectionSwitcher`
|
||||
S-9 "Du betalar Anna" with avatars and an arrow; swap button `swap_horiz` (TalkBack "Byt riktning").
|
||||
|
||||
### `AllocationEditor`
|
||||
S-9 list of `AllocationRow`s + "Lägg till konto" + `RemainingIndicator`.
|
||||
* `AllocationRow`: account name, debt line ("Du är skyldig 3 084,50 kr"), `AmountInput(Field)`, remove (`close`, "Ta bort Huset från fördelningen"), warning line (`warning` icon + "Det här reglerar mer än som är skyldigt i Japan 2027") when `overSettles`. Variant `Fixed` (per-account settle: no remove).
|
||||
* `RemainingIndicator`: "Kvar att fördela 0,00 kr" (neutral/positive), "250,00 kr kvar" (warning), "250,00 kr för mycket" (error), always words + colour.
|
||||
|
||||
## 7. Timeline
|
||||
|
||||
### `TimelineItem`
|
||||
Anatomy: actor avatar (24 dp; system icon `autorenew` for recurring, `upload_file` for import), sentence, "· 16 okt. 19:40"; vertical connector between items.
|
||||
* Variants: `Create`, `Update` (one line per `FieldChange`, sentence built from field + old → new, values formatted by `MoneyFormatter`/`DateFormatter`, ids already names), `Delete`, `PendingLocal` ("Din ändring väntar på synk", pending colour, dashed connector), `System` (recurring/import).
|
||||
* Field names (sv/en) in `strings.csv` (`timeline_field_*`).
|
||||
|
||||
### `Timeline`
|
||||
Column of `TimelineItem`, oldest first, newest last (AC-12.4), with the section title "Historik".
|
||||
|
||||
## 8. Charts (`:core:design`, Vico or Canvas fallback per §6.2)
|
||||
|
||||
### `ChartCard`
|
||||
Container: title, optional subtitle (range), the chart, legend, and an invisible-but-spoken summary sentence (`semantics { contentDescription = summary }`). States: loaded, empty ("Inga utgifter i den här perioden"), loading (shimmer; only when the computation takes > 300 ms).
|
||||
|
||||
### `StackedBarChart`
|
||||
Buckets on x (month "maj" or week "v. 41"), kronor on y (whole kronor, 3–5 gridlines, compact "20 tkr" labels in sv, "20k" in en), segments coloured by group (category colour or `chart-cat-n` for accounts, stable order = legend order), 2 dp gap between segments; selected bar highlighted, others dimmed; tap → callback(bucketKey). Each bar is a focusable semantics node ("September, 41 484 kronor"). Horizontal scroll when more than ~8 buckets; newest visible.
|
||||
|
||||
### `ChartLegend`
|
||||
Wrapping row of swatch + label; only groups with data; static (filtering by legend is not in the spec).
|
||||
|
||||
### `BreakdownBarList`
|
||||
Rows: icon/swatch, name, proportional bar, amount, percent (one decimal, AC-14.10). Sorted descending. Tap → callback(groupId). 200 %: bar on its own line.
|
||||
|
||||
### `FilterBar`
|
||||
S-11 filters: range chip with menu (`DropdownMenu` of `RangePreset`s), three binary connected toggles. Wraps to two rows.
|
||||
|
||||
## 9. Scaffolding
|
||||
|
||||
### `KvittScaffold`
|
||||
Top app bar (small, 64 dp; back or close), content, optional FAB slot, snackbar host, edge-to-edge insets.
|
||||
|
||||
### `KvittNavigationBar`
|
||||
Three items (ia.md §1). Expressive `ShortNavigationBar` (fallback `NavigationBar`). Selected icon filled. Labels always visible.
|
||||
|
||||
### `AddExpenseFab`
|
||||
Extended FAB "Lägg till utgift" (icon `add`), collapses to icon-only on scroll; Expressive size variants (fallback `ExtendedFloatingActionButton`).
|
||||
|
||||
### `EmptyState`
|
||||
Icon, title, body, optional primary action. Used by S-3, S-4, S-5, S-11, S-16, S-19.
|
||||
|
||||
### `ConfirmDialog`
|
||||
Title, body, confirm (destructive variant in `error`), dismiss.
|
||||
|
||||
### `SectionHeader`, `SettingsRow`, `CollapsibleSection`
|
||||
Settings rows (icon, title, optional trailing value/badge, chevron), section headers ("Konton" + trailing action), "Arkiverade (n)" collapse with rotation of the chevron.
|
||||
|
||||
### `InviteCodeDisplay`, `InviteCodeField`
|
||||
Code in three groups of four, mono/tabular, large; selectable; TalkBack spells characters ("7 K 3 M, Q X 9 D, 2 H P A"). Field variant normalises as typed (§8.3).
|
||||
|
||||
### `Skeleton`
|
||||
Shimmer placeholders for the first-sync loading state only.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Export every phone frame of one direction to <dir>/png/<frame-id>.png at 2x.
|
||||
# Usage: docs/ux/directions/export-png.sh docs/ux/directions/<n>-<slug> [frame-id ...]
|
||||
# Usage: [OUT_DIR=<dir>] docs/ux/directions/export-png.sh docs/ux/directions/<n>-<slug> [frame-id ...]
|
||||
# OUT_DIR (optional) is where the PNGs go; default <dir>/png.
|
||||
# Needs Google Chrome (macOS path below or CHROME=...). The page must implement ?list=1 and ?frame=<id>.
|
||||
set -euo pipefail
|
||||
dir="$(cd "$1" && pwd)"; shift
|
||||
@@ -13,8 +14,9 @@ if [ "$#" -gt 0 ]; then ids=("$@"); else
|
||||
| sed -n '/<pre id="frame-list">/,/<\/pre>/p' | sed -e 's/<[^>]*>//g' | grep -v '^[[:space:]]*$')
|
||||
fi
|
||||
[ "${#ids[@]}" -gt 0 ] || { echo "no frames listed by $page?list=1" >&2; exit 1; }
|
||||
mkdir -p "$dir/png"
|
||||
out="${OUT_DIR:-$dir/png}"
|
||||
mkdir -p "$out"
|
||||
for id in "${ids[@]}"; do
|
||||
run --window-size=411,891 --force-device-scale-factor=2 --screenshot="$dir/png/$id.png" "$page?frame=$id"
|
||||
run --window-size=411,891 --force-device-scale-factor=2 --screenshot="$out/$id.png" "$page?frame=$id"
|
||||
done
|
||||
echo "exported ${#ids[@]} frames to $dir/png"
|
||||
echo "exported ${#ids[@]} frames to $out"
|
||||
|
||||
+8
-8
@@ -1,6 +1,6 @@
|
||||
# Kvitt: flows
|
||||
|
||||
Status: U1 proposal (2026-10-17). Step-by-step flows with screen ids (`ia.md`, `screens.md`). Rules referenced by spec section; nothing here redefines them. Data from the shared mock household (Jacob = owner/admin, Anna = partner; today lördag 17 oktober 2026).
|
||||
Status: U1 (2026-10-17), updated in U2 (2026-10-18) for the owner's decisions (category suggestion UX-3 with no caption and "most often" history; 14 default categories UX-8). Step-by-step flows with screen ids (`ia.md`, `design-spec.md` §4). Rules referenced by spec section; nothing here redefines them. Data from the shared mock household (Jacob = owner/admin, Anna = partner; today lördag 17 oktober 2026).
|
||||
|
||||
Notation: **User** does · *System* does · `S-n` screen · ⟶ navigation.
|
||||
|
||||
@@ -18,7 +18,7 @@ Notation: **User** does · *System* does · `S-n` screen · ⟶ navigation.
|
||||
8. **Jacob** taps "Dela" ⟶ Android share sheet ⟶ sends the message to Anna (SMS, Signal, …). Back ⟶ `S-3` now shows the expiry.
|
||||
9. When Anna has joined (F-2), the next sync replaces `S-3` with `S-4` empty state "Skapa ert första konto" ⟶ **Jacob** taps "Nytt konto" ⟶ `S-6` "Vardag", 50/50 ⟶ Spara ⟶ `S-4` with the account and the FAB.
|
||||
|
||||
Error branches: `invite_invalid` / network / `rate_limited` inline on `S-1` (screens.md S-1). Lost every phone later: `kvitt-server pair-device` (AC-1.6) then F-1 steps 2–5.
|
||||
Error branches: `invite_invalid` / network / `rate_limited` inline on `S-1` (`design-spec.md` S-1). Lost every phone later: `kvitt-server pair-device` (AC-1.6) then F-1 steps 2–5.
|
||||
|
||||
## F-2 Join as spouse (US-2, AC-2.4, AC-2.10)
|
||||
|
||||
@@ -28,13 +28,13 @@ Error branches: `invite_invalid` / network / `rate_limited` inline on `S-1` (scr
|
||||
4. *Full sync* ⟶ `S-4` loading ⟶ notification permission prompt ⟶ `S-4` empty "Skapa ert första konto" (or Jacob's accounts, if he created any after she joined).
|
||||
5. On Jacob's phone the next sync (or push-triggered sync) removes `S-3`.
|
||||
|
||||
## F-3 Add an expense online (US-3, AC-8.6) — the ten-second flow
|
||||
## F-3 Add an expense online (US-3, AC-8.6, UX-3) — the ten-second flow
|
||||
|
||||
1. **Jacob** opens Kvitt ⟶ `S-4`.
|
||||
2. Taps FAB "Lägg till utgift" ⟶ `S-7` with amount focused, keypad up; defaults: Konto Vardag (last used), Kategori (last used in Vardag, else Övrigt), Datum Idag, Betalt av Du, Delning Kontots standard 50/50.
|
||||
2. Taps FAB "Lägg till utgift" ⟶ `S-7` with amount focused, keypad up; defaults: Konto Vardag (last used), Kategori (last used in Vardag, else Övrigt, until a description exists), Datum Idag, Betalt av Du, Delning Kontots standard 50/50. The category row shows the five categories used most in Vardag plus "Alla".
|
||||
3. Types `412`.
|
||||
4. Taps Beskrivning, types "Systembolaget".
|
||||
5. (optional, one tap each) taps the Kategori chip ⟶ grid ⟶ Nöje; taps Betalt av to switch to Anna; taps Delning to open the split section.
|
||||
4. Taps Beskrivning, types "Systembolaget". *While he types, the category is suggested from the description (`category-suggestion.md` CS-5: the category used most often for this description in the household's history, else the keyword table, else the default) and Nöje becomes selected; no caption or marker (CS-7).*
|
||||
5. (optional, one tap each) a wrong guess: taps the right category tile (or "Alla" ⟶ sheet ⟶ category); from then on the suggestion no longer changes the category (CS-2); taps Betalt av to switch to Anna; taps Delning to open the split section.
|
||||
6. Taps "Spara" ⟶ *saved locally, pending* ⟶ back to `S-4`, snackbar "Utgiften sparades"; Vardag's wording/amount and the global hero update immediately (AC-10.4).
|
||||
7. *2 s later, sync pushes (AC-15.3)*; the sync line returns to "Synkad nyss"; *the server pushes `expense_created` to Anna's phone*, which syncs and shows "Jacob lade till en utgift · Systembolaget · 412,00 kr · Vardag".
|
||||
|
||||
@@ -51,7 +51,7 @@ Error branches: `invite_invalid` / network / `rate_limited` inline on `S-1` (scr
|
||||
|
||||
1. **Jacob** gets a notification "Anna lade till en utgift · ICA Maxi Lindhagen · 1 147,60 kr · Vardag" ⟶ taps ⟶ `S-8` (after sync).
|
||||
2. Timeline: "Anna skapade den här · 16 okt. 14:02". He sees the receipt says 1 247,60.
|
||||
3. Taps "Ändra" ⟶ `S-7` "Ändra utgift" with all fields loaded; amount focused.
|
||||
3. Taps "Ändra" ⟶ `S-7` "Ändra utgift" with all fields loaded, nothing focused, keypad hidden; the category is kept (no suggestion when editing, CS-1). He taps the amount; the keypad appears.
|
||||
4. Changes 1 147,60 to 1 247,60 ⟶ "Spara" ⟶ back to `S-8`; timeline gains a last line "Din ändring väntar på synk".
|
||||
5. *Sync* ⟶ the pending line is replaced by "Du ändrade beloppet från 1 147,60 kr till 1 247,60 kr · 16 okt. 19:40".
|
||||
6. *Anna's phone* gets "Jacob ändrade en utgift · ICA Maxi Lindhagen · 1 247,60 kr"; her `S-8` shows the same timeline (her view: "Jacob ändrade beloppet …").
|
||||
@@ -80,7 +80,7 @@ Variant: "Lägg till konto" ⟶ picker (all non-deleted accounts, archived inclu
|
||||
## F-8 Create a recurring expense (US-10, FR-9)
|
||||
|
||||
1. **Jacob** ⟶ Inställningar `S-13` ⟶ "Återkommande utgifter" `S-19` ⟶ "Ny".
|
||||
2. Editor: amount 12 400,00; description "Hyra"; Konto Huset; Kategori Hem; Betalt av Anna (one tap); Delning Kontots standard 60/40; Dag i månaden 25; Första månad: *defaults to oktober 2026 because the 25th is still ahead (AC-9.8)*; Sista månad empty.
|
||||
2. Editor: amount 12 400,00; description "Hyra"; Konto Huset; Kategori Boende; Betalt av Anna (one tap); Delning Kontots standard 60/40; Dag i månaden 25; Första månad: *defaults to oktober 2026 because the 25th is still ahead (AC-9.8)*; Sista månad empty.
|
||||
3. Spara (online) ⟶ list row "Hyra · 12 400,00 kr · Huset · Varje månad den 25:e · Nästa: 25 okt.".
|
||||
4. *On 25 Oct at 06:00 the server creates the expense* ⟶ both phones get "Återkommande utgift tillagd · Hyra · 12 400,00 kr · Huset" (AC-9.6) ⟶ `S-8` shows "Återkommande: Hyra" and "Skapad automatiskt (återkommande) · 25 okt. 06:00".
|
||||
5. Offline at step 3 ⟶ Spara disabled with "Kräver anslutning".
|
||||
|
||||
+7
-7
@@ -1,10 +1,10 @@
|
||||
# Kvitt: information architecture
|
||||
|
||||
Status: U1 proposal (2026-10-17). Binding inputs: SPEC §13.2 (navigation model), §13.3 (screens S-1 to S-22), §14.2.4 (UI contract), §6.2 (module ownership). Screen ids are the spec's (`S-1` … `S-22`), not renumbered.
|
||||
Status: U1 (2026-10-17), updated in U2 (2026-10-18) for the owner's decisions (`proposed-decisions.md`: three tabs kept, UX-2; category suggestion, UX-3; 14 default categories, UX-8). Look and states of each screen: `design-spec.md`. Binding inputs: SPEC §13.2 (navigation model), §13.3 (screens S-1 to S-22), §14.2.4 (UI contract), §6.2 (module ownership). Screen ids are the spec's (`S-1` … `S-22`), not renumbered.
|
||||
|
||||
## 1. Bottom navigation
|
||||
|
||||
**Recommendation: keep the spec's three destinations.**
|
||||
**Decided (owner, 2026-10-18, UX-2): the spec's three destinations.**
|
||||
|
||||
| Destination | sv | en | Icon (Material Symbols) | Start screen |
|
||||
|---|---|---|---|---|
|
||||
@@ -18,10 +18,10 @@ Why three and not four:
|
||||
|
||||
1. **Home already is the account list.** The second most important question ("who owes whom", §13.1) is answered per account on Home (AC-10.2). A separate "Accounts" destination would split the global balance from its breakdown and add a tap to the most common path (Home → account → expense).
|
||||
2. **The add flow is the FAB, not a tab.** A tab "Add" would be a navigation item that is really an action; Material 3 discourages that, and the FAB is reachable with the thumb on both phones.
|
||||
3. **No fourth destination exists in the spec.** The only candidate that would earn the slot is an **Activity** feed (the other person's latest changes across all accounts, i.e. the audit log as a list). It would be useful together with push notifications (US-13), but it is a new feature, out of scope for the UX sprint (§13.5). It is listed as an owner question in the report, not designed.
|
||||
3. **No fourth destination exists in the spec.** The only candidate that would earn the slot is an **Activity** feed (the other person's latest changes across all accounts, i.e. the audit log as a list). It would be useful together with push notifications (US-13), but it is a new feature, out of scope for the UX sprint (§13.5). The owner did not take it for v1 (UX-2).
|
||||
4. Three items keep Swedish labels whole: "Inställningar" (13 characters) fits at 200 % font scale in a three-item bar but not in a four- or five-item bar of 411 dp.
|
||||
|
||||
Navigation bar style per direction (Expressive "short navigation bar" 64 dp or classic `NavigationBar` 80 dp) is stated on each direction's review page.
|
||||
Navigation bar style: Expressive `ShortNavigationBar` (64 dp; fallback `NavigationBar`), `design-spec.md` §3.11.
|
||||
|
||||
## 2. Screen inventory
|
||||
|
||||
@@ -35,7 +35,7 @@ Kind: **top** = bottom-nav destination; **pushed** = full screen with top app ba
|
||||
| S-4 | Översikt / Home | top | `:feature:home` | bottom nav; notification for deletions and `sync_batch` | `HomeRepository` → `HomeState` |
|
||||
| S-5 | Konto (account detail) | pushed | `:feature:account` | S-4 account row | `AccountRepository.detail` → `AccountDetailState` |
|
||||
| S-6 | Nytt konto / Ändra konto | pushed | `:feature:account` | S-4 "Nytt konto"; S-5 overflow "Ändra konto" | `AccountRepository` |
|
||||
| S-7 | Ny utgift / Ändra utgift | pushed, full screen (§13.2) | `:feature:expense` | FAB on S-4 or S-5; S-8 "Ändra"; S-19 editor reuses its fields | `ExpenseRepository.editorOptions`, `preview`, `save` |
|
||||
| S-7 | Ny utgift / Ändra utgift | pushed, full screen (§13.2) | `:feature:expense` | FAB on S-4 or S-5; S-8 "Ändra"; S-19 editor reuses its fields | `ExpenseRepository.editorOptions`, `preview`, `save`, `suggestCategory` (UX-3) |
|
||||
| S-8 | Utgift (expense detail) | pushed | `:feature:expense` | ledger rows (S-5, S-12, S-16); notification | `ExpenseRepository.detail` → `ExpenseDetailState` |
|
||||
| S-9 | Gör upp / Settle up | pushed | `:feature:settle` | S-4 "Gör upp" (global); S-5 "Gör upp det här kontot" (per account); S-10 "Ändra" | `SettlementRepository.start`, `recompute`, `save` → `SettleState` |
|
||||
| S-10 | Betalning (settlement detail) | pushed | `:feature:settle` | settlement rows; notification | `SettlementRepository.detail` |
|
||||
@@ -43,7 +43,7 @@ Kind: **top** = bottom-nav destination; **pushed** = full screen with top app ba
|
||||
| S-12 | Statistik, urval (drill-down) | pushed | `:feature:analytics` | tap on a bar or a breakdown row in S-11 (AC-14.8) | `AnalyticsRepository.drillDown` |
|
||||
| S-13 | Inställningar / Settings | top | `:feature:settings` | bottom nav | `ProfileRepository.household`, `SessionRepository` (role) |
|
||||
| S-14 | Profil | pushed | `:feature:settings` | S-13 | `ProfileRepository` |
|
||||
| S-15 | Kategorier (list + editor) | pushed (editor pushed from list) | `:feature:settings` | S-13 | `CategoryRepository` |
|
||||
| S-15 | Kategorier (list + editor; 14 default categories, UX-8) | pushed (editor pushed from list) | `:feature:settings` | S-13 | `CategoryRepository` |
|
||||
| S-16 | Synkstatus / Sync status | pushed | `:feature:home` | S-4 sync line; S-13 "Synkstatus" | `SyncRepository.status` |
|
||||
| S-17 | Uppdatering (banner + dialogs) | banner on S-4, dialogs | `:feature:home` | S-4 when `UpdateState != None`; S-22 "Sök efter uppdatering" | `UpdateRepository` |
|
||||
| S-18 | Telefonen har tagits bort / Device removed | flow, start when `Revoked` | `:feature:onboarding` | any 401 `device_revoked` (AC-3.3) | `SessionRepository.acknowledgeRevoked` |
|
||||
@@ -101,7 +101,7 @@ After saving in S-7 the app returns to where the editor was opened from (S-4 or
|
||||
| Own display name and picture | S-14 (and S-2 once for the partner) | S-4 hero, S-13, all avatars, timelines | no ("Kräver anslutning") |
|
||||
| Partner's name and picture | partner's own S-14 | everywhere | — |
|
||||
| Accounts (name, default ratio, archived, deleted) | S-6 (create, edit), S-5 overflow (archive, unarchive, delete) | S-4, S-7 picker (active only), S-9 rows and "Lägg till konto" (all), S-11/S-12, S-19 picker | no |
|
||||
| Categories | S-15 | S-7 grid (active only), S-8, ledger rows, S-11/S-12, S-19 | no |
|
||||
| Categories | S-15 | S-7 quick row and "Alla" sheet (active only; selected by the suggestion of UX-3 for new expenses), S-8, ledger rows, S-11/S-12, S-19 | no |
|
||||
| Recurring templates | S-19 | S-8 ("Återkommande: Hyra") | no |
|
||||
| Expenses | S-7 (create, edit), S-8 (delete) | S-4 (counts, balances), S-5, S-11/S-12, S-16 (pending) | **yes** |
|
||||
| Settlements | S-9 (create, edit), S-10 (delete) | S-4/S-5 balances and rows, S-16 (pending) | **yes** |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# UX sprint: owner answers, findings and proposed spec changes
|
||||
|
||||
**Recorded on `main`:** UX-1 … UX-8 became D-047 … D-054 in SPEC v0.8 (`7fb9f6f`), with the category contract v2 (`6b60ef7`). The two new colour tokens were named `slate` (insurance) and `amber` (fees) there; this file uses those names. Still open for a docs agent: the §9.7 hex values of UX-5 (`TODO(U2)` in SPEC v0.8; final values below) and UX-9.
|
||||
|
||||
The UX sprint runs in the owner's own session, which has no orchestrator. This file is how its results reach the main line. **No numbers are assigned here:** the decision ids and the spec version are handed out when a docs agent records these (CLAUDE.md › Numbering; next free at the time of writing: D-044, spec v0.6). Ids `UX-1` … are local labels only.
|
||||
|
||||
Every entry has: the owner's answer or the finding, the proposed decision text for `docs/decisions.md`, and the spec text to change.
|
||||
@@ -18,6 +20,7 @@ Every entry has: the owner's answer or the finding, the proposed decision text f
|
||||
| History match: most recent or most often? | **Most often.** |
|
||||
| Import granularity | Splitwise's categories are finer than Kvitt's ten defaults (several "Övrigt" under different groups); map them better. "Hemförsäkring" must not end up in Övrigt. |
|
||||
| Which new default categories? | **All four proposed:** Boende, Försäkringar, Barn, Avgifter & skatter. |
|
||||
| U2 design spec, screen catalogue and strings (2026-10-18) | **Approved** ("I approve everything"). |
|
||||
|
||||
## UX-1 Direction: Kalas, muted
|
||||
|
||||
@@ -39,7 +42,7 @@ Every entry has: the owner's answer or the finding, the proposed decision text f
|
||||
* AC-8.6: the category default becomes "the suggestion of §9.11, which falls back to the current rule".
|
||||
* New §9.11 "Category suggestion": the text CS-1 to CS-7 of `category-suggestion.md`.
|
||||
* New contract file `contracts/category-keywords.csv` (columns `keyword,match,default_key,source`; draft `docs/ux/category-keywords.csv`, 148 entries, none mapping to `other`), listed in §14.8.
|
||||
* §14.2.4: `ExpenseRepository` gains `suspend fun suggestCategory(accountId: AccountId?, description: String): CategoryId?` (computed in `:data` from Room and the keyword table; the editor calls it on description and account changes while the category was not picked by hand).
|
||||
* §14.2.4: `ExpenseRepository` gains `suspend fun suggestCategory(accountId: AccountId?, description: String): CategoryId?` (computed in `:data` from Room and the keyword table; the editor calls it on description and account changes while the category was not picked by hand). The editor's category row also needs `ExpenseEditorOptions.frequentCategoryIds` (UX-9).
|
||||
* §14.5: a pure Kotlin function `suggestCategory(description, history, keywords, activeCategories, fallback)` in `:domain` with test vectors (Kotlin only, like analytics).
|
||||
|
||||
## UX-7 Finer Splitwise import mapping (§10.8)
|
||||
@@ -81,17 +84,18 @@ Every entry has: the owner's answer or the finding, the proposed decision text f
|
||||
| **`housing`** | **Housing** | **Boende** | **`key`** | **`indigo`** | **25** |
|
||||
| `home` | Home | Hem | `home` | `brown` | 30 |
|
||||
| `utilities` | Utilities | Räkningar | `bolt` | `yellow` | 40 |
|
||||
| **`insurance`** | **Insurance** | **Försäkringar** | **`shield`** | **new token (UX-5)** | **45** |
|
||||
| **`insurance`** | **Insurance** | **Försäkringar** | **`shield`** | **`slate`** (new token, UX-5) | **45** |
|
||||
| `transport` | Transport | Transport | `directions_car` | `blue` | 50 |
|
||||
| `travel` | Travel | Resor | `flight` | `teal` | 60 |
|
||||
| `entertainment` | Entertainment | Nöje | `movie` | `purple` | 70 |
|
||||
| `health` | Health | Hälsa | `medical_services` | `red` | 80 |
|
||||
| **`children`** | **Children** | **Barn** | **`child_care`** | **`lime`** | **85** |
|
||||
| `shopping` | Shopping | Shopping | `shopping_bag` | `pink` | 90 |
|
||||
| **`fees`** | **Fees & taxes** | **Avgifter & skatter** | **`account_balance`** | **new token (UX-5)** | **95** |
|
||||
| **`fees`** | **Fees & taxes** | **Avgifter & skatter** | **`account_balance`** | **`amber`** (new token, UX-5) | **95** |
|
||||
| `other` | Other | Övrigt | `category` | `grey` | 100 |
|
||||
|
||||
* **Contract consequences** (one landing, numbered by the docs agent): AC-7.2 table; §9.7 icon set gains `key`, `shield`, `account_balance` (33 tokens) and the colour set gains two tokens (UX-5); §14.1 seed rows with four new fixed ids and a server schema **migration to version 2** inserting them (existing databases, including the development stack, get them on upgrade); `DefaultCategoryKey` and `ColorToken` in §14.2.4 gain the new values and `CategoryIcons.ALL` the new icons; the OpenAPI enums; default-name strings in both languages (`strings.csv`); test vectors that enumerate categories, if any. The Room schema is unchanged (category rows arrive through sync).
|
||||
* **Contract consequences** (one landing, numbered by the docs agent): AC-7.2 table; §9.7 icon set gains `key`, `shield`, `account_balance` (33 tokens) and the colour set gains `slate` and `amber` (UX-5); §14.1 seed rows with four new fixed ids and a server schema **migration to version 2** inserting them (existing databases, including the development stack, get them on upgrade); `DefaultCategoryKey` and `ColorToken` in §14.2.4 gain the new values and `CategoryIcons.ALL` the new icons; the OpenAPI enums; default-name strings in both languages (`strings.csv`); test vectors that enumerate categories, if any. The Room schema is unchanged (category rows arrive through sync).
|
||||
* **Fakes:** the D-018 empty scenario of `:core:fakes` (§6.2 "Fakes") then has exactly the **14** default categories, not ten; the §6.2 sentence changes with it.
|
||||
* **Order matters for existing milestones:** M3 (server) and M5 (`:data`) already landed with ten defaults, so this is a contract change against code on `main`, not only the spec.
|
||||
|
||||
## UX-4 S-1 asks for the display name
|
||||
@@ -102,9 +106,26 @@ Every entry has: the owner's answer or the finding, the proposed decision text f
|
||||
|
||||
## UX-5 Category colour values (§9.7)
|
||||
|
||||
* **Finding:** several provisional §9.7 values fail contrast as icon colours on tinted containers (light orange, yellow, lime).
|
||||
* **Decision:** replace the §9.7 hex values with the muted Kalas values, and add two colour tokens for the new categories of UX-8 (values and names from the Kalas revision in progress; filled in when it reports). Existing token names unchanged.
|
||||
* **Spec:** §9.7 table values.
|
||||
* **Finding:** several provisional §9.7 values fail contrast as icon colours on tinted containers (light orange, yellow, lime), and the bright set tires the eye over years of daily use (owner, UX-1).
|
||||
* **Decision:** replace the §9.7 hex values with the muted Kalas values below and add two colour tokens, `slate` (Försäkringar) and `amber` (Avgifter & skatter), for the new categories of UX-8. Existing token names unchanged. Light `brown` is #6B5244, darker than the first muted draft (#7A5F50), so it stays apart from `amber`. Values from `design-spec.md` §2.3, which also gives each token's container (the colour at 18 % over surface in light, 24 % in dark) and the contrast ratios (every value ≥ 3:1 as a graphic against surface, card and its container).
|
||||
* **Spec:** the §9.7 colour table becomes (14 tokens; the server's validation list and `ColorToken` gain `slate` and `amber`):
|
||||
|
||||
| Token | Light | Dark | §9.7 today (light / dark) |
|
||||
|---|---|---|---|
|
||||
| `green` | `#4E7A55` | `#8FB896` | `#2E7D32` / `#81C784` |
|
||||
| `orange` | `#B26A3E` | `#E8AA7E` | `#E65100` / `#FFB74D` |
|
||||
| `brown` | `#6B5244` | `#B8A090` | `#6D4C41` / `#BCAAA4` |
|
||||
| `yellow` | `#9C7C2A` | `#E6C67A` | `#F9A825` / `#FFF176` |
|
||||
| `blue` | `#4A78A8` | `#98C0E6` | `#1565C0` / `#90CAF9` |
|
||||
| `teal` | `#367F82` | `#8CCAC8` | `#00796B` / `#80CBC4` |
|
||||
| `purple` | `#865891` | `#C59CDA` | `#6A1B9A` / `#CE93D8` |
|
||||
| `red` | `#A84B45` | `#DC8F86` | `#C62828` / `#EF9A9A` |
|
||||
| `pink` | `#AE5D7C` | `#EFB0BE` | `#AD1457` / `#F48FB1` |
|
||||
| `grey` | `#6C7177` | `#B0B2B3` | `#546E7A` / `#B0BEC5` |
|
||||
| `indigo` | `#45508D` | `#969DD8` | `#283593` / `#9FA8DA` |
|
||||
| `lime` | `#73822F` | `#B9CC86` | `#827717` / `#DCE775` |
|
||||
| **`slate`** | **`#5E2D4E`** | **`#B0719A`** | new |
|
||||
| **`amber`** | **`#937D63`** | **`#DFCFC0`** | new |
|
||||
|
||||
## UX-6 Smaller UX rules found during U1 (no owner question needed)
|
||||
|
||||
@@ -113,12 +134,48 @@ Every entry has: the owner's answer or the finding, the proposed decision text f
|
||||
| Saving an expense shows "Utgiften sparades" without "Ångra" (an undo would be an audited create + delete that also notifies the partner). | §13.3 S-7 |
|
||||
| Account detail of an archived account hides the FAB (archived accounts are not offered for new expenses, AC-6.4); settling stays available. | §13.3 S-5 |
|
||||
| The per-account settle-up keeps the direction swap (§9.4: per-account settle-ups go either way) and lets the amount be edited (AC-11.4 "defaults to"); its one account row is fixed. | §13.3 S-9 |
|
||||
| Avatar initials colours are two tokens `avatar-1`, `avatar-2`; the slot is a stable function of the user id (AC-4.5). | AC-4.5 |
|
||||
| Avatar initials colours are two tokens `avatar-1`, `avatar-2`; the slot is a stable function of the user ids: slot 1 for the household user whose id sorts first (string comparison), slot 2 for the other, so the two never share a colour and the admin (created first, UUIDv7) is slot 1 on both phones (`design-spec.md` §2.5). | AC-4.5 |
|
||||
| `strings.csv` keys are renamed in U2 to the feature-module prefixes of D-017 (`onboarding_`, `home_`, `account_`, `expense_`, `settle_`, `analytics_`, `settings_`; shared keys without a feature prefix). | none (U2 work) |
|
||||
|
||||
## UX-9 Contract additions needed by the design
|
||||
|
||||
* **Finding (U2):** two binding states of `design-spec.md` need values that the UI contract (§14.2.4) does not carry. Both are computed on the phone from Room; no server or sync change.
|
||||
* **Decision (proposed):** add the two members below to `:core:model`, implement them in `:data` and `:core:fakes` (every fake scenario computes them from its own data). Recorded together with UX-3, since (1) is part of the same editor change.
|
||||
|
||||
**(1) `ExpenseEditorOptions.frequentCategoryIds`**
|
||||
|
||||
* **Why / where:** S-7's category row (and S-19's editor) shows five large tiles plus "Alla": the five categories used most in the selected account, so the usual category is one tap away even when the suggestion (UX-3) guesses wrong (`design-spec.md` §3.7 `CategoryQuickRow`; frames `S-7-editor`, `S-7-editor-200`, `S-19-recurring-editor`).
|
||||
* **Spec text (§14.2.4):**
|
||||
|
||||
```kotlin
|
||||
data class ExpenseEditorOptions(
|
||||
val me: User, val partner: User, val accounts: List<Account>, val categories: List<Category>,
|
||||
val defaultAccountId: AccountId?, val defaultCategoryFor: Map<AccountId, CategoryId>, val today: String,
|
||||
val frequentCategoryIds: Map<AccountId, List<CategoryId>>, // per active account: up to 5 active categories, most used first
|
||||
)
|
||||
```
|
||||
|
||||
Reading: for each active account, the active (non-archived, non-deleted) categories ordered by the number of non-deleted expenses in that account that use them, descending; ties by `sort_order` ascending; at most 5 entries; categories with no use in the account are left out (the list may be shorter or empty). Counted over every expense in the phone's Room copy (both users, imported and recurring included), so both phones show the same order after sync; it is not a per-phone history like AC-8.6's "last used on this phone". The flow re-emits when expenses or categories change; the editor reads it once when it opens (and on an account change) so tiles do not move while the user looks at them.
|
||||
* **UI rule (design, not contract):** the row shows the account's list, filled up to five from the remaining active categories in `sort_order`; the selected category (suggested, defaulted or picked in "Alla") takes the fifth place when it is not among them.
|
||||
* **Fallback until it exists:** the row shows the selected category first, then the first active categories in `sort_order` up to five.
|
||||
|
||||
**(2) `CategoryRepository.deletable(id)`**
|
||||
|
||||
* **Why / where:** the category editor shows "Ta bort kategori" disabled with the reason before any tap when deletion is not allowed (AC-7.6), the same way the account menu uses `AccountDetailState.deletable` (`design-spec.md` §4 S-15; frame `S-15-category-editor-custom`).
|
||||
* **Spec text (§14.2.4, `CategoryRepository`):**
|
||||
|
||||
```kotlin
|
||||
fun deletable(id: CategoryId): Flow<Boolean> // false for a default category (defaultKey != null) or one used by a non-deleted expense or non-deleted recurring template (AC-7.6)
|
||||
```
|
||||
|
||||
Computed from Room. The server stays the authority: `delete` can still return `Failed("category_is_default")` or `Failed("category_in_use")` (for example when the partner's new expense has not synced yet), and the UI shows the same reason inline. The editor uses `deletable` with the category's `defaultKey` to pick the text: default → `settings_category_delete_blocked_default_short`; in use and archived → `settings_category_delete_blocked_in_use_archived`; in use and active → `settings_category_delete_blocked_in_use`.
|
||||
* **Fallback until it exists:** for a custom category "Ta bort kategori" is enabled; after a tap, `Failed("category_in_use")` shows the in-use reason inline under the button (default categories need no data: `defaultKey` already says it).
|
||||
|
||||
* **Spec:** §14.2.4 as above (contract change: spec version bump and decision entry, numbered by the docs agent); `:core:fakes` and `:data` implement both in the same change (CLAUDE.md › The UI contract is the contract).
|
||||
|
||||
## Status
|
||||
|
||||
* U1: delivered (`d27f92e` rebased on `main`), direction picked.
|
||||
* Done: muted Kalas revision (approved); keyword list and replay for 14 categories and "most often".
|
||||
* In progress: Kalas mocks with the 14 categories and two new colour tokens;
|
||||
* Next: owner approves the muted palette → U2 `design-spec.md`, renamed `strings.csv`, per-screen PNGs, and the final values for UX-3, UX-5 and UX-8.
|
||||
* U2 (2026-10-18): `design-spec.md` (binding design), `strings.csv` with D-017 prefixes, the screen catalogue `screens/` (80 frames, light and dark); the final values of UX-5 above; UX-9 found while writing the design spec.
|
||||
* Next: a docs agent records UX-1 … UX-9 on `main` (decision numbers and spec version from the orchestrator).
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
# Kvitt: screens
|
||||
|
||||
Status: U1 proposal (2026-10-17), direction-independent. What each screen contains, in which order, in which states, and what each action does. How it looks is the chosen direction's job (`directions/`); the exact words are in `strings.csv`. Rules (rounding, allocation order, sync, limits) are the spec's and are referenced, never restated as new rules.
|
||||
|
||||
Conventions:
|
||||
|
||||
* **States** per SPEC §13.3: loaded, empty, loading (first sync only), offline (with pending changes), error (a failed online action), long content (long Swedish names, 7-digit amounts, 200 % font scale).
|
||||
* "Kräver anslutning" = the action is disabled while offline per §9.9, with that text as a supporting line or tooltip, never a silent disabled control.
|
||||
* "Kunde inte nå servern" = an online action failed with a network error; nothing changed locally (§9.9). Shown as a snackbar on the screen that triggered it, with "Försök igen" when the action is repeatable.
|
||||
* Server error codes (§14.3.2) map to inline text where a field is at fault (`name_taken` on the name field) and to a snackbar otherwise. Copy keys in `strings.csv` prefix `error_`.
|
||||
* Amounts: tabular figures, right-aligned in lists, never ellipsized; at 200 % they move under the description instead. Direction is always words plus colour (§13.1).
|
||||
* Pending-sync marker: a small `cloud_upload` (or `schedule`) icon with TalkBack label "Väntar på synk" on every row and detail whose `pending == true`.
|
||||
* Every screen reads local data, so after the first sync there is no loading state except where noted.
|
||||
|
||||
---
|
||||
|
||||
## S-1 Pairing (Para ihop)
|
||||
|
||||
**Purpose:** connect this phone to the household with an invite code (US-1, US-2, AC-2.6, AC-2.7).
|
||||
|
||||
Content, in priority order:
|
||||
1. App name "Kvitt" and the one-line purpose "Delade utgifter för två".
|
||||
2. Invite code field: 12 characters, displayed and typed in groups of 4 (`7K3M-QX9D-2HPA`); input is uppercased and normalised per §8.3 as the user types (hyphens inserted automatically, `O`→`0`, `I`/`L`→`1`). Trailing "Klistra in" button (reads the clipboard; accepts a full link and extracts the code and server).
|
||||
3. Explanation: "Koden får du av Jacob: från en länk eller från Inställningar › Bjud in partner." For the admin's first run: "Koden skrivs ut när servern sätts upp."
|
||||
4. "Ditt namn" field (1–40, §9.6), required on a first pairing: AC-2.4 sends the display name with the accept call and the app cannot know the invite kind beforehand (the server ignores it for `device` invites). Hidden in the "Para ihop telefonen igen" variant. The partner confirms it again on S-2.
|
||||
5. Server address field, prefilled `https://icehack.asuscomm.com:8443` (release), collapsed under "Serveradress" as a secondary row since it rarely changes; expanded automatically when the link carried a different server.
|
||||
6. Primary button "Para ihop".
|
||||
|
||||
States:
|
||||
* **Loaded/empty:** code field focused, keyboard open (uppercase letters and digits).
|
||||
* **From link:** both fields filled, focus on "Para ihop".
|
||||
* **Loading:** button shows progress "Parar ihop…", fields read-only. After success: full sync runs (S-4 loading state), then the notification permission prompt (AC-2.9).
|
||||
* **Error** (inline under the code field, button enabled again): `invite_invalid` "Koden fungerar inte. Den kan ha gått ut eller redan använts. Be Jacob om en ny." · network "Kunde inte nå servern. Kontrollera adressen och anslutningen." · `rate_limited` "För många försök. Vänta en stund och försök igen." · `http://` in release (AC-2.8) "Adressen måste börja med https://" on the server field.
|
||||
* **Unrecognised variant** (§14.4.8): header "Para ihop telefonen igen" and a line "Dina utgifter finns kvar på telefonen."
|
||||
* **Offline:** the button stays enabled; tapping it gives the network error (pairing never queues).
|
||||
* **Long content:** single column, scrolls; at 200 % the code field wraps to two lines of groups.
|
||||
|
||||
Actions: Para ihop (primary); Klistra in. No FAB, no bottom nav.
|
||||
|
||||
## S-2 Partner profile step (Din profil)
|
||||
|
||||
**Purpose:** the partner confirms their name and optionally adds a picture before entering the app (AC-2.10).
|
||||
|
||||
Content: 1. "Du går med i Jacobs hushåll" with Jacob's avatar and name; 2. own avatar (initials on `avatar-2` until a picture is chosen) with "Lägg till bild" (Android photo picker, AC-4.3); 3. display name field prefilled from the pairing call (1–40 characters, §9.6); 4. "Fortsätt".
|
||||
|
||||
States: **uploading picture** (progress on the avatar; Continue waits); **error** `image_invalid` / `unsupported_media_type` / `payload_too_large` as a snackbar "Bilden kunde inte användas" with the reason; network "Kunde inte nå servern" — the user may continue without the picture; **validation** empty name blocks Continue with "Skriv ett namn".
|
||||
|
||||
Actions: Fortsätt (primary, saves name if changed); Lägg till bild / Byt bild / Ta bort bild.
|
||||
|
||||
## S-3 Waiting for partner (Väntar på din partner)
|
||||
|
||||
**Purpose:** the admin's Home until the partner has joined (AC-5.1).
|
||||
|
||||
Content: illustration-free empty state on the Home destination: title "Väntar på din partner"; body "Kvitt behöver er båda innan ni kan börja. Bjud in din partner med en länk."; if an invite is pending: "Inbjudan gäller till mån 19 okt. 21:40"; primary "Bjud in partner" (→ S-21, creates a `partner` invite; if one is pending S-21 shows it and offers a new one). Bottom nav visible; no FAB; no balance; no "Nytt konto".
|
||||
|
||||
States: **offline** "Bjud in partner" disabled with "Kräver anslutning". **Partner joins** while the screen is open: the next sync replaces it with S-4 (empty state "Skapa ert första konto").
|
||||
|
||||
## S-4 Home (Översikt)
|
||||
|
||||
**Purpose:** answer "who owes whom" in one glance and start the two most common actions: add an expense and settle up (US-6, AC-10.1, AC-10.2).
|
||||
|
||||
Content, in priority order:
|
||||
1. **Update banner** (only when `UpdateState` is `Available`, `Downloading`, `Failed` or `NeedsInstallPermission`; S-17).
|
||||
2. **Balance hero:** both avatars; wording per §9.3 from the viewer's side: "Anna är skyldig dig", "Du är skyldig Anna", or "Ni är kvitt"; the amount (`amount-hero`) on its own line; "Gör upp" button when the global balance ≠ 0. TalkBack reads one sentence: "Du är skyldig Anna 3 250,00 kronor".
|
||||
3. **Sync line**, small, under the hero: "Synkad för 3 min sedan" (relative time per §9.8); appended "· 2 ändringar väntar på synk" when `pendingCount > 0`; "Offline" prefix when offline; "Synkar…" while running; warning style with "· 1 ändring synkades inte" when `rejectedCount > 0` or "Telefonens klocka går fel" when `clockSkew`. Tap → S-16. Never a blocking banner.
|
||||
4. **Konton** section header with trailing "Nytt konto" (text button; "Kräver anslutning" offline).
|
||||
5. **Active accounts** in `created_at` order (AC-6.6): name (ellipsize at one line, 2 lines at 200 %), wording ("Anna är skyldig dig", "Du är skyldig Anna", "Kvitt"), amount (omitted when Kvitt), "11 utgifter i oktober" (plural, current month name). Tap → S-5.
|
||||
6. **"Arkiverade (1)"** collapsed row; expands in place to the archived accounts with an "Arkiverat" badge (AC-6.4).
|
||||
7. FAB "Lägg till utgift" (extended, collapses to icon-only while scrolling down; TalkBack "Lägg till utgift").
|
||||
|
||||
States:
|
||||
* **Loading** (first sync after pairing only): hero and rows as skeletons, text "Hämtar era utgifter…"; the FAB is hidden until the first sync finishes or fails (then the local, possibly empty, state is shown).
|
||||
* **Empty** (partner joined, no accounts): no hero amount ("Ni är kvitt" is not meaningful yet: hero shows both avatars and "Välkommen, Anna och Jacob"), empty state "Skapa ert första konto" with one line ("Ett konto samlar utgifter som hör ihop, till exempel Vardag eller Japan 2027.") and the primary button "Nytt konto". No FAB (AC-8.6 needs an account).
|
||||
* **Settled:** hero "Ni är kvitt", no amount, no "Gör upp".
|
||||
* **Offline:** everything works; sync line "Offline · 2 ändringar väntar på synk"; "Nytt konto" disabled with "Kräver anslutning".
|
||||
* **Error:** not applicable (local data); a failed sync shows as the sync line in warning style ("Synk misslyckades · försök igen om en stund"), never a dialog.
|
||||
* **Long content:** account names up to 60 characters ellipsize in one line (two at 200 %); 7-digit amounts ("1 234 567,89 kr") fit the hero on one line at 100 % and wrap under the wording at 200 %.
|
||||
|
||||
Actions: FAB → S-7 (account defaulted per AC-8.6); Gör upp → S-9 global; account row → S-5; Nytt konto → S-6; sync line → S-16; banner → S-17.
|
||||
|
||||
## S-5 Account detail (Konto)
|
||||
|
||||
**Purpose:** one account's balance and its ledger; settle just this account (AC-10.3, US-8).
|
||||
|
||||
Content:
|
||||
1. Top app bar: account name (ellipsize), overflow menu: "Ändra konto" (→ S-6), "Arkivera" / "Återställ från arkiv", "Ta bort konto" (enabled only when `deletable`; otherwise disabled with the reason "Kontot kan inte tas bort så länge det har utgifter, betalningar eller återkommande utgifter"). All three "Kräver anslutning" offline.
|
||||
2. Header: default split "50 / 50 · Jacob och Anna" (names in percent order, viewer as "du" is not used here; names keep the line unambiguous); "Arkiverat" badge if archived; balance wording and amount (§9.3, account wording "Kvitt"); "Gör upp det här kontot" when non-zero.
|
||||
3. Ledger grouped by month, newest first: sticky month header "Oktober · 7 457,90 kr" (household spend of the month, settlements excluded); rows ordered per AC-8.10:
|
||||
* **Expense row:** category icon on its colour container, description (one line), date ("16 okt."), amount (right), effect line (§9.2: "Du lånade ut 432,17 kr" in owed colour, "Du lånade 623,80 kr" in owing colour, "Inte inblandad" / "Betalade för dig själv" neutral), pending marker. Tap → S-8.
|
||||
* **Settlement row:** `payments` icon, "Anna betalade dig 800,00 kr" or "Du betalade Anna 800,00 kr", "· 2 konton" when more than one allocation (AC-11.7), date, pending marker. Tap → S-10.
|
||||
4. FAB "Lägg till utgift" (account preset to this account).
|
||||
|
||||
States: **empty** (new account) "Inga utgifter än" + "Lägg till den första utgiften i Vardag." (FAB still there); **archived** badge, "Återställ från arkiv" in the header as a tonal button, FAB still offered? **No**: archived accounts are hidden from the account picker for new expenses (AC-6.4), so the FAB is hidden on an archived account; settling stays available; **offline** rows work, overflow actions disabled; **error** archive/delete failures as snackbar (`account_not_empty` "Kontot har fortfarande utgifter, betalningar eller återkommande utgifter"); **long content** month totals and row amounts at 7 digits stay on one line at 100 %; at 200 % the amount moves to the line under the description.
|
||||
|
||||
Actions: FAB → S-7; Gör upp det här kontot → S-9 (one fixed row); row → S-8/S-10; overflow actions; archive asks no confirmation (reversible) and shows a snackbar "Vardag arkiverades" with "Ångra"; delete asks "Ta bort kontot Vardag? Det går inte att ångra." (soft delete on the server, but not restorable from the app).
|
||||
|
||||
## S-6 Account editor (Nytt konto / Ändra konto)
|
||||
|
||||
**Purpose:** create or edit an account's name and default split (AC-6.1 to AC-6.3). Online only.
|
||||
|
||||
Content: 1. Name field (1–60, unique: `name_taken` → "Det finns redan ett konto som heter Vardag"); 2. "Standarddelning": a slider (0–100 % in steps of 1, snapping to 50) plus two percent fields "Jacob %" and "Anna %" that always sum to 100 (typing in one updates the other; two decimals allowed, comma or point); quick presets as chips "50/50", "60/40", "40/60"; 3. preview "Av 100,00 kr: Jacob 60,00 kr, Anna 40,00 kr"; 4. editing only: hint "Ändringen gäller nya utgifter. Befintliga utgifter behåller sin delning." (AC-6.3); 5. "Spara" (top app bar action and/or bottom button, per direction).
|
||||
|
||||
States: **offline** all fields disabled, banner "Kräver anslutning. Konton kan bara ändras när telefonen är online." , Spara disabled; **saving** progress on Spara; **error** network snackbar; validation inline.
|
||||
|
||||
Actions: Spara; Avbryt (back; asks "Slänga ändringarna?" only if something changed).
|
||||
|
||||
## S-7 Expense editor (Ny utgift / Ändra utgift) — the add-expense flow
|
||||
|
||||
**Purpose:** record an expense in ten seconds (SPEC §13.1): open app, tap add, type amount, type description, tap save. Full screen, not a sheet (§13.2). Works offline (AC-8.2).
|
||||
|
||||
### Layout, top to bottom
|
||||
|
||||
1. **Top app bar:** close (`close`, TalkBack "Avbryt"), title "Ny utgift" / "Ändra utgift", trailing "Spara" text button (enabled when valid). Edit mode adds overflow "Ta bort".
|
||||
2. **Amount** (focus on open): display-size `amount-large`, tabular, with "kr" suffix; input rules §9.6 (digits, one `,` or `.`, max two decimals, no sign, max 10 000 000,00). Empty shows "0,00" as a hint in `on-surface-variant`, not as a value.
|
||||
3. **Description** field "Beskrivning" (1–100), single line, IME action "Spara" (saves when valid, otherwise moves focus to the first invalid field).
|
||||
4. **Defaults row**: one chip per field, each showing its current value, each one tap to change:
|
||||
* **Konto:** "Vardag" (AC-8.6: last used on this phone if still active, else the oldest active). Opens a picker of active accounts (bottom sheet list with balances). Changing the account in mode `default` re-copies the ratio (AC-8.8) and changes the category default only if the user has not picked a category yet.
|
||||
* **Kategori:** icon + name of the default (AC-8.6: last used in that account on this phone if active, else Övrigt). Opens the category grid (icon + colour + name, active categories, `sort_order`).
|
||||
* **Datum:** "Idag" (also "Igår", else "16 okt."); opens the Material date picker; range per §9.6.
|
||||
* **Betalt av:** "Du" or "Anna" with the avatar; **one tap toggles** between the two users (no picker: there are exactly two) (AC-8.3).
|
||||
* **Delning:** "Kontots standard 50/50" — **one tap opens the split section** in place (below the chips) and scrolls it into view.
|
||||
5. **Split section** (collapsed by default; expanded when the mode is not `default` or the user tapped Delning):
|
||||
* Mode chooser (single-select connected buttons / segmented, wraps to two rows in Swedish): "Kontots standard 60/40" · "Egen fördelning" · "Exakta belopp" · "Allt på en person".
|
||||
* `ratio`: same control as S-6 (slider + two percent fields summing to 100).
|
||||
* `exact`: two money fields; the user types one, the other is read-only "amount minus typed" (AC-8.7). Typed > amount → inline error "Andelen är större än beloppet" and Save disabled.
|
||||
* `full`: choose the bearer: "Jacob" / "Anna" (two avatar toggles), independent of the payer.
|
||||
* **Live preview** for all modes, from `ExpenseRepository.preview` (the domain computes it; the UI never computes shares): "Du 206,00 kr · Anna 206,00 kr", plus the effect line that the row will show ("Du lånade ut 206,00 kr"). Hidden while `preview` returns null.
|
||||
6. **Anteckningar:** collapsed as "Lägg till anteckning"; expands to a multi-line field (0–1000).
|
||||
7. **Keypad / keyboard:** while the amount has focus, the numeric input is shown at the bottom (each direction states whether it is an in-app keypad with a guaranteed "," key or the system keyboard with `KeyboardType.Decimal`). Focus moves to the description with "Nästa"/IME or by tapping it; the system text keyboard replaces the keypad.
|
||||
8. Edit mode only, at the end: a destructive "Ta bort utgift". (Who created the expense is shown on S-8 as the first timeline line; `ExpenseDraft` carries no creator.)
|
||||
|
||||
### The ten-second path (binding interaction)
|
||||
|
||||
| Step | User does | Screen does |
|
||||
|---|---|---|
|
||||
| 1 | Taps FAB on S-4 | S-7 opens with amount focused, keypad up, all defaults set |
|
||||
| 2 | Types `412` | Amount shows "412", Save still disabled (description missing) |
|
||||
| 3 | Taps "Beskrivning", types "Systembolaget" | Text keyboard; Save enabled |
|
||||
| 4 | Taps "Spara" (app bar) or IME "Spara" | Saves locally (`SaveResult.Saved`), closes, snackbar "Utgiften sparades" (no "Ångra": an undo would be a create plus a delete, both audited and pushed to the partner), the new row is visible on return; sync runs after 2 s (AC-15.3) |
|
||||
|
||||
The split override is one tap from step 3 (the Delning chip). Payer change is one tap. Nothing else needs touching for the common case.
|
||||
|
||||
### States
|
||||
|
||||
* **Validation** (inline, from `SaveResult.Invalid` field errors, mapped by token): amount missing "Skriv ett belopp"; amount out of range "Beloppet måste vara mellan 0,01 kr och 10 000 000,00 kr"; description empty "Skriv en beskrivning"; date out of range "Datumet ligger för långt fram"; exact mismatch as above.
|
||||
* **Offline:** no difference; after saving, the snackbar says "Sparad. Synkas när du är online."
|
||||
* **No accounts:** the editor is not reachable (no FAB). **Only archived accounts:** same (FAB hidden; AC-6.4).
|
||||
* **Edit mode for an expense the other user created:** same editor, no warning; the save records the change in the timeline ("Du ändrade …").
|
||||
* **Discard:** close with changes asks "Slänga utgiften?" (Släng / Fortsätt redigera).
|
||||
* **Long content:** 7-digit amount scales down one type step rather than wrapping; at 200 % the chips wrap to several rows and the split section scrolls; Spara stays in the app bar so it is always reachable.
|
||||
|
||||
## S-8 Expense detail (Utgift)
|
||||
|
||||
**Purpose:** everything about one expense and who changed what (US-13, AC-12.4).
|
||||
|
||||
Content: 1. amount (large) and description; category chip; 2. facts list: Konto, Datum ("fredag 16 oktober 2026"), Betalt av (avatar + name), Delning (mode label, e.g. "Kontots standard 50/50", "Egen fördelning 70/30", "Exakta belopp", "Allt på Anna") and each user's share ("Du 623,80 kr · Anna 623,80 kr"), the effect line; "Återkommande: Hyra" when `recurringTemplateName != null` (AC-8.11); notes (full text); 3. **Historik** timeline, oldest first, newest last (AC-12.4): actor avatar, sentence, "· 16 okt. 19:40"; create line "Anna skapade den här"; update lines one per changed field ("Du ändrade beloppet från 1 147,60 kr till 1 247,60 kr"; split changes as "Du ändrade delningen från 50/50 till 70/30"); recurring source "Skapad automatiskt (återkommande)"; import source "Importerad från Splitwise"; pending last line "Din ändring väntar på synk" (AC-12.5).
|
||||
|
||||
Actions: "Ändra" (→ S-7 edit), "Ta bort" (confirm "Ta bort utgiften? Den försvinner för er båda." → soft delete, returns to the previous screen with snackbar "Utgiften togs bort").
|
||||
|
||||
States: **deleted** (opened from a deletion notification or the row was deleted while open): banner "Anna tog bort den här utgiften · 17 okt. 09:12", content muted, no Edit/Delete; **pending** marker next to the amount; **missing** (entity not in the database yet): "Utgiften finns inte på telefonen än" with "Synka nu"; **offline** fully readable, edit and delete allowed.
|
||||
|
||||
## S-9 Settle up (Gör upp)
|
||||
|
||||
**Purpose:** record a payment between the two users and spread it over accounts (US-7, US-8, AC-11.3 to AC-11.6). Works offline.
|
||||
|
||||
Content, in order:
|
||||
1. **Direction:** "Du betalar Anna" with a swap control "Byt riktning" (TalkBack). Default per AC-11.3 / AC-11.4.
|
||||
2. **Amount** (large money field), default = absolute global (or account) balance, empty when zero.
|
||||
3. **Fördelning** list, one row per account from `SettleState.lines`: account name; "Skuld 3 084,50 kr" (the payer's debt in that account, `payerNetOre`, words: "Du är skyldig 3 084,50 kr" / "Anna är skyldig dig 1 184,50 kr" when the payer is owed there); editable allocated amount; per-row remove; over-settle warning row text "Det här reglerar mer än som är skyldigt i Japan 2027" (AC-11.5) in warning colour with `warning` icon.
|
||||
4. **"Kvar att fördela 0,00 kr"** live indicator; when negative: "250,00 kr för mycket"; colour plus words.
|
||||
5. **"Lägg till konto"** → picker of all non-deleted accounts not yet listed (active and archived, AC-11.3).
|
||||
6. Info line when an account is left non-zero because the payer is owed there (§9.4 consequence): "Vardag påverkas inte: där är Anna skyldig dig 1 184,50 kr."
|
||||
7. Datum (Idag), Anteckning (0–200).
|
||||
8. **Bekräfta** (primary, enabled only when `canConfirm`), Avbryt.
|
||||
|
||||
Behaviour: editing the total amount re-runs the proposal (`recompute` with `allocations = null`) unless the user has edited a row, in which case it keeps the rows and updates "Kvar att fördela" (the user can tap "Föreslå igen" to re-run). Swap direction re-runs the proposal. Confirming with any warned row shows a dialog repeating the warning(s) (one line per warned row, the same text as on the row: "Det här reglerar mer än som är skyldigt i Japan 2027") with "Bekräfta ändå" / "Ändra".
|
||||
|
||||
Per-account variant (from S-5): one fixed row for that account (not removable), no "Lägg till konto", no info line.
|
||||
|
||||
States: **no accounts** cannot occur (button hidden); **zero balance** amount empty, rows appear after typing; **editing** title "Ändra betalning", proposal computed with this settlement excluded (AC-11.6); **offline** identical; **long content** at 200 % each row stacks name / debt / field vertically.
|
||||
|
||||
## S-10 Settlement detail (Betalning)
|
||||
|
||||
Content: "Anna betalade dig 800,00 kr" headline with both avatars and an arrow from payer to recipient; Datum ("tisdag 6 oktober 2026"); Anteckning; **Fördelning** per account with amounts; Historik timeline as in S-8 (diff lines "Du ändrade beloppet …", "… ändrade fördelningen"). Actions: Ändra (→ S-9 editing), Ta bort (confirm "Ta bort betalningen? Saldona räknas om för er båda."). States: deleted, pending, missing as S-8.
|
||||
|
||||
## S-11 Analytics (Statistik)
|
||||
|
||||
**Purpose:** how much the household spends, when, on what and in which account (US-11, FR-14).
|
||||
|
||||
Content:
|
||||
1. **Filters** (sticky under the app bar, wrap on two rows in Swedish):
|
||||
* Period chip "Senaste 6 mån" → menu with the presets of AC-14.2 (Denna månad, Senaste 3 mån, Senaste 6 mån, Senaste 12 mån, I år, All tid, Anpassad…); Anpassad opens the Material date range picker; the resolved range is shown under the chips ("1 maj – 17 okt. 2026").
|
||||
* Three binary toggles (connected buttons): Månad / Vecka · Hushållet / Min del · Kategori / Konto.
|
||||
2. **Totalt** for the range ("178 211,40 kr"), with the measure in words ("Hushållets utgifter" / "Din del").
|
||||
3. **Utgifter över tid:** stacked bars, one per bucket (all buckets, empty ones as zero, AC-14.7), stacked by group (category colours or account colours), y-axis in kronor (no decimals on the axis), x-axis "maj, jun …" or "v. 35"; legend below (group colour + name, only groups present); tap a bar → S-12 for that bucket; horizontally scrollable when there are more buckets than fit (weeks), newest at the right and scrolled into view.
|
||||
4. **Per kategori:** horizontal bars sorted descending, each row: icon, name, bar, amount, percent (one decimal, half-up, AC-14.10); tap → S-12.
|
||||
5. **Per konto:** same form; archived accounts included with "Arkiverat" badge (AC-14.9).
|
||||
|
||||
Grouping applies to chart 1 only; charts 2 and 3 are always shown (AC-14.6). Settlements never count (AC-14.6).
|
||||
|
||||
Accessibility: each chart has a content description that summarises it in one sentence ("Utgifter över tid, sex månader: högst i september med 41 484 kronor, lägst i maj med 24 710 kronor") and the bars are focusable one by one ("September: 41 484 kronor, Resor största kategori"). Breakdown rows are ordinary list items.
|
||||
|
||||
States: **empty** "Inga utgifter i den här perioden" with "Byt period" (charts hidden, filters remain); **loading** none (local); **offline** identical; **long content** category names ellipsize in breakdown rows, amounts never; at 200 % the breakdown row puts the bar on its own line.
|
||||
|
||||
No FAB on Statistik.
|
||||
|
||||
## S-12 Analytics drill-down
|
||||
|
||||
Content: title naming the filter ("Mat & hushåll, oktober 2026", "v. 41, Vardag", "Japan 2027, senaste 6 mån"), the measure ("Hushållet"/"Min del") as subtitle, total, then expense rows exactly as in S-5 (with the account name added to the row's secondary line, since rows come from several accounts), newest first. Tap → S-8. Empty cannot occur (only reachable from a non-zero number); if data changed meanwhile: "Inga utgifter längre".
|
||||
|
||||
## S-13 Settings hub (Inställningar)
|
||||
|
||||
Content: 1. household card: own avatar, name, "Du · Administratör" / "Du"; partner's avatar and name ("Anna · Partner"); 2. rows with leading icons and chevrons: Profil (`person`), Kategorier (`category`), Återkommande utgifter (`event_repeat`), Enheter (`devices`), Bjud in partner (`person_add`, admin before join) or Para ihop en enhet (`qr_code`/`add_link`, admin after join), Synkstatus (`sync`, with trailing "2 väntar" when pending), Om Kvitt (`info`, trailing "Uppdatering finns" when available). Rows that lead to online-only screens stay enabled; the screens themselves show their offline state.
|
||||
|
||||
## S-14 Profile (Profil)
|
||||
|
||||
Content: large avatar with "Byt bild" and "Ta bort bild" (when a picture exists); display name field (1–40); Spara. Picture changes apply immediately on success (no Save needed); name needs Spara. States: offline (all disabled, "Kräver anslutning"), uploading (progress over the avatar), error codes as in S-2.
|
||||
|
||||
## S-15 Categories (Kategorier)
|
||||
|
||||
**List:** active categories in `sort_order`: drag handle (long-press to drag, plus "Flytta upp"/"Flytta ned" TalkBack actions), icon on colour container, name (custom name or localised default), chevron; "Arkiverade (n)" collapsed section; app bar action "Ny kategori". Reorder saves on drop (online; offline: drag handles hidden and a line "Kräver anslutning för att ändra").
|
||||
|
||||
**Editor** (pushed): name field (1–40; for a default category, empty name means "use the default name"; "Återställ namn" button shown when a custom name exists, AC-7.4); live preview chip; **icon grid** of the 30 icons of §9.7 (6 per row at 411 dp, 48 dp targets, selected with outline + check); **colour swatches** of the 12 tokens (each 48 dp, selected with check, TalkBack names "Grön", "Orange" …); Spara; overflow "Arkivera" / "Återställ från arkiv"; "Ta bort kategori" only when allowed, otherwise disabled with the reason: default "Standardkategorier kan inte tas bort, bara arkiveras", in use "Kategorin används av utgifter eller återkommande utgifter. Arkivera den i stället."
|
||||
|
||||
## S-16 Sync status (Synkstatus)
|
||||
|
||||
Content: 1. status headline: "Synkad för 3 min sedan" / "Synkar…" / "Offline"; 2. clock-skew warning card when `clockSkew` ("Telefonens klocka går fel. Rätta den och synka igen.") — shown first; 3. "Väntande ändringar (2)" list of pending ledger rows (as in S-5, with "ny"/"ändrad"/"borttagen" tag), tap → S-8/S-10; 4. "Ändringar som inte synkades" list from `rejections`: summary (description · amount), the reason text per §14.4.6, the time; tap opens the current version (read-only S-8/S-10); "Ta bort från listan" per item; 5. "Synka nu" (primary; disabled with progress while running; offline "Kräver anslutning").
|
||||
|
||||
Empty: "Allt är synkat" with the last sync time.
|
||||
|
||||
## S-17 Update available (Uppdatering)
|
||||
|
||||
* **Banner on S-4** (top of content, dismissible for the session): "Version 1.2 finns" + "Uppdatera" / "Senare".
|
||||
* **First time only**, before downloading (`NeedsInstallPermission` or not yet granted): dialog "Tillåt installation" — "Android frågar en gång om Kvitt får installera appar. Välj Tillåt från den här källan och gå tillbaka." — buttons "Fortsätt" (opens the system setting) / "Avbryt".
|
||||
* **Downloading:** banner shows "Laddar ner… 64 %" with a linear progress indicator (determinate); the app stays usable.
|
||||
* **Verified:** the system installer appears (out of our hands).
|
||||
* **Failed** (AC-16.4): banner "Nedladdningen misslyckades. Försök igen." + "Försök igen".
|
||||
* **Offline:** "Uppdatera" disabled with "Kräver anslutning".
|
||||
|
||||
## S-18 Device removed (Telefonen har tagits bort)
|
||||
|
||||
Full screen, no navigation: icon `phonelink_erase`; title "Den här telefonen har tagits bort"; body "Åtkomsten togs bort och allt Kvitt-data på telefonen har raderats. Be Jacob om en ny kod för att para ihop den igen." (member) / "…Skapa en ny kod på servern med kvitt-server pair-device." (admin, rare); primary "Para ihop igen" → S-1.
|
||||
|
||||
## S-19 Recurring expenses (Återkommande utgifter)
|
||||
|
||||
**List:** rows: description, amount, account, "Varje månad den 25:e", "Nästa: 25 okt." (`next_occurrence_date`) or "Pausad" badge, payer avatar; app bar "Ny"; empty state "Inga återkommande utgifter" + "Hyra, el och streaming kan läggas in automatiskt varje månad." + "Ny återkommande utgift".
|
||||
|
||||
**Editor** (pushed): the S-7 fields without date (amount, description, Konto (active accounts), Kategori, Betalt av, Delning, Anteckningar) plus "Dag i månaden" (1–31 picker; for 29–31 a hint "I kortare månader används sista dagen"), "Första månad" (month picker, default per AC-9.8, back to 12 months), "Sista månad (valfritt)", "Pausad" switch; Spara; overflow "Ta bort" (confirm "Ta bort Hyra? Utgifter som redan skapats finns kvar."). Hint under the fields when editing: "Ändringen gäller kommande utgifter." (AC-9.4). Online only (offline state as S-6).
|
||||
|
||||
## S-20 Devices (Enheter)
|
||||
|
||||
Rows per AC-3.1: device name, owner avatar and name, "Parad 3 okt.", "Senast sedd nyss", "Kvitt 1.1", "Den här enheten" tag; trailing "Ta bort" (not on the current device). Confirmation dialog: "Ta bort Galaxy S26+? Telefonen loggas ut och dess Kvitt-data raderas nästa gång den ansluter." → "Ta bort" / "Avbryt". States: loading (online fetch: progress), offline ("Kräver anslutning" + nothing listed; this list is not stored locally), error `cannot_revoke_current_device` cannot be triggered from the UI.
|
||||
|
||||
## S-21 Invites (Bjud in partner / Para ihop en enhet)
|
||||
|
||||
Admin only. Content: actions "Bjud in partner" (only while one user exists, AC-2.1) and "Para ihop en enhet" with a choice "För mig" / "För Anna" (AC-2.2). **Created invite:** the code very large in groups of 4 (`7K3M-QX9D-2HPA`, mono/tabular, selectable), the link, "Gäller till tis 20 okt. 14:02", "Dela" (Android share sheet with a Swedish message: "Gå med i vårt hushåll i Kvitt: <länk> Koden är 7K3M-QX9D-2HPA."), "Kopiera kod". **Pending invites** list: kind ("Partner", "Enhet för Anna"), created, expiry, "Avbryt" (confirm). The code is only visible right after creation (the server stores hashes); later rows say "Koden visas bara när den skapas". States: offline ("Kräver anslutning"), errors `household_full`, network.
|
||||
|
||||
## S-22 About (Om Kvitt)
|
||||
|
||||
Rows: "Version 1.1 (11)", "Server: https://icehack.asuscomm.com:8443", "Serverversion 1.1.0" (or "Okänd" when `/health` failed), "Öppen källkod-licenser" (→ the standard licences screen), button "Sök efter uppdatering" (→ check; result as S-17 banner or a snackbar "Du har den senaste versionen"; debug builds: hidden, AC-16.5).
|
||||
|
||||
---
|
||||
|
||||
## Notifications (not screens)
|
||||
|
||||
Built locally per AC-13.5 and §13.3 (copy in `strings.csv`, prefix `notif_`). Channel "Aktivitet". Small icon: the app's monochrome icon. Large icon: actor avatar if cached. Tap targets per AC-13.6.
|
||||
File diff suppressed because it is too large
Load Diff
+557
-464
File diff suppressed because it is too large
Load Diff
@@ -1,223 +0,0 @@
|
||||
# Kvitt: design tokens
|
||||
|
||||
Status: U1 proposal (2026-10-17). One token **set of names** (binding once the owner picks a direction) with **values for all three directions** (`directions/1-skargard`, `2-kvitto`, `3-kalas`), so `:core:design` can be built from whichever is chosen. After the pick, U2 copies the chosen column into `design-spec.md` and deletes the other two here (one source of truth). Each direction's review page recomputes every contrast ratio below in JavaScript and marks failures; the values here were checked there.
|
||||
|
||||
Contrast targets (SPEC §13.1): text ≥ 4.5:1 against the surface it sits on; icons and graphics ≥ 3:1; nothing conveyed by colour alone.
|
||||
|
||||
## 1. How tokens map to Compose
|
||||
|
||||
| Token group | Compose home in `:core:design` | Notes |
|
||||
|---|---|---|
|
||||
| Material 3 colour roles (§2) | `lightColorScheme(...)` / `darkColorScheme(...)` | names map 1:1 (`on-primary-container` → `onPrimaryContainer`) |
|
||||
| Kvitt semantic roles (§3) | `KvittColors` data class in a `CompositionLocal`, light and dark instance | `owed*`, `owing*`, `settled*`, `warning*`, `pending`, `avatar*` |
|
||||
| Category colours (§4) | `KvittColors.category(ColorToken)` and `.categoryContainer(ColorToken)` | `ColorToken` enum from `:core:model` (§14.2.4) |
|
||||
| Chart colours (§5) | `KvittColors.chartCategorical[0..7]`, `chartSequential[0..4]`, `chartGrid`, `chartAxisLabel` | |
|
||||
| Type (§6) | `Typography(...)` + `KvittTypography(amountHero, amountLarge, amountMedium, amountSmall)` | amounts always `fontFeatureSettings = "tnum"` |
|
||||
| Spacing (§7) | `object KvittSpacing { val s1 = 4.dp … }` | |
|
||||
| Shapes (§8) | `Shapes(extraSmall…extraLarge)` + `KvittShapes` for the Expressive sizes and custom shapes | |
|
||||
| Elevation, motion (§9) | `MotionScheme` (Expressive) or durations/easings constants | |
|
||||
|
||||
Dynamic colour (Material You): offered only by Skärgård, as an off-by-default option that replaces §2 only; §3–§5 keep their values in every case.
|
||||
|
||||
## 2. Material 3 colour roles
|
||||
|
||||
`L` = light, `D` = dark.
|
||||
|
||||
| Token | Skärgård L | Skärgård D | Kvitto L | Kvitto D | Kalas L | Kalas D |
|
||||
|---|---|---|---|---|---|---|
|
||||
| primary | #1A6476 | #8ED0E2 | #23408E | #B3C3FF | #B23A1C | #FFB59F |
|
||||
| on-primary | #FFFFFF | #00363F | #FFFFFF | #112A6A | #FFFFFF | #5E1604 |
|
||||
| primary-container | #C2E8F3 | #0E4D5C | #DCE3F6 | #2B4590 | #FFDBD0 | #8A2A10 |
|
||||
| on-primary-container | #0B4C5C | #C2E8F3 | #14295E | #DDE3FF | #3D0B00 | #FFDBD0 |
|
||||
| secondary | #4E5F69 | #B6C9D4 | #4E5466 | #C2C6D8 | #4B6A2A | #B2D38A |
|
||||
| on-secondary | #FFFFFF | #20333B | #FFFFFF | #2B3040 | #FFFFFF | #1F3701 |
|
||||
| secondary-container | #D3E3EE | #374952 | #DFE2EB | #3B4050 | #D3EFAE | #344F15 |
|
||||
| on-secondary-container | #374851 | #D3E5F0 | #1B2030 | #DEE1F1 | #112000 | #D3EFAE |
|
||||
| tertiary | #7B5C24 | #E8C48A | #73552A | #DCC39A | #7A5900 | #F2C14E |
|
||||
| on-tertiary | #FFFFFF | #422C00 | #FFFFFF | #3E2D10 | #FFFFFF | #402D00 |
|
||||
| tertiary-container | #F7DFB4 | #5D4410 | #F1E2C8 | #574425 | #FFDF8E | #5A3F00 |
|
||||
| on-tertiary-container | #5A410E | #FFDEA8 | #2E1F06 | #F7E1BD | #261A00 | #FFDF8E |
|
||||
| error | #B3261E | #F2B8B5 | #B0261B | #FFB4A8 | #BA1A1A | #FFB4AB |
|
||||
| on-error | #FFFFFF | #601410 | #FFFFFF | #690004 | #FFFFFF | #690005 |
|
||||
| error-container | #F9DEDC | #8C1D18 | #F9DCD6 | #8E1E14 | #FFDAD6 | #93000A |
|
||||
| on-error-container | #8C1D18 | #F9DEDC | #5A0E08 | #FFDAD4 | #410002 | #FFDAD6 |
|
||||
| surface | #F6FAFC | #0F1417 | #F7F4EC | #1C1A17 | #FFF8F1 | #1D1415 |
|
||||
| on-surface | #171C1F | #DEE3E6 | #1D1B17 | #ECE6DA | #231918 | #F1DFDB |
|
||||
| surface-variant | #DBE4E8 | #40484C | #E7E2D6 | #4A463E | #F5DED6 | #53433F |
|
||||
| on-surface-variant | #40484C | #BFC8CC | #58534A | #C0B9AB | #53433F | #D8C2BB |
|
||||
| surface-container-lowest | #FFFFFF | #0A0F11 | #FFFDF8 | #161512 | #FFFFFF | #170F10 |
|
||||
| surface-container-low | #F0F4F7 | #171C1F | #F2EEE5 | #211F1B | #FFF1E8 | #251B1C |
|
||||
| surface-container | #EAEFF2 | #1B2124 | #EDE9DF | #26241F | #FAEADF | #2A1F20 |
|
||||
| surface-container-high | #E4E9EC | #252B2E | #E6E1D6 | #302D28 | #F4E4D8 | #352A2A |
|
||||
| surface-container-highest | #DEE3E6 | #303639 | #E0DBCF | #3B3832 | #EEDED2 | #403434 |
|
||||
| surface-dim | #D6DBDE | #0F1417 | #D9D4C8 | #1C1A17 | #E6D6CB | #1D1415 |
|
||||
| surface-bright | #F6FAFC | #353A3D | #FBF9F3 | #42403A | #FFF8F1 | #463A3A |
|
||||
| inverse-surface | #2C3134 | #DEE3E6 | #32302B | #ECE6DA | #392E2C | #F1DFDB |
|
||||
| inverse-on-surface | #EDF1F4 | #2C3134 | #F4F0E7 | #32302B | #FFEDE7 | #392E2C |
|
||||
| inverse-primary | #8ED0E2 | #1A6476 | #B3C3FF | #23408E | #FFB59F | #B23A1C |
|
||||
| outline | #70787C | #899296 | #7E786C | #8D877A | #85736E | #A08C87 |
|
||||
| outline-variant | #BFC8CC | #40484C | #D3CDC0 | #48443C | #D8C2BB | #53433F |
|
||||
| scrim | #000000 (40 %) | #000000 | #000000 (42 %) | #000000 | #000000 (38 %) | #000000 |
|
||||
|
||||
Key contrast (text role vs surface, L / D): on-surface 16.4 / 14.3 · 15.6 / 14.0 · 16.3 / 14.0; on-surface-variant 8.9 / 10.9 · 6.9 / 8.9 · 8.9 / 10.6; primary 6.4 / 10.8 · 8.7 / 10.1 · 5.7 / 10.6. `outline` is used for graphics only (3.99–5.84).
|
||||
|
||||
Direction-specific extra aliases: Kvitto `paper` (= surface-container-lowest in light, surface-container-high in dark; the receipt surface). Kalas `card` (#FFFFFF / #2A1F20) and `on-cat` (#FFFFFF / #1D1415, icon colour on solid category tiles).
|
||||
|
||||
## 3. Kvitt semantic roles
|
||||
|
||||
| Token | Use | Skärgård L | D | Kvitto L | D | Kalas L | D |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| owed | text/graphic: partner owes you | #00695C | #7DD8C5 | #2C6A38 | #92CC9A | #1B6B45 | #8BD8A8 |
|
||||
| on-owed | | #FFFFFF | #00382F | #FFFFFF | #0E3818 | #FFFFFF | #003920 |
|
||||
| owed-container | | #C2EDE4 | #005047 | #DCEBD8 | #1E3A24 | #C4EFD2 | #0F5233 |
|
||||
| on-owed-container | | #00443B | #A3F2E0 | #123A1C | #BFE8C4 | #00210F | #A7F5C3 |
|
||||
| owing | text/graphic: you owe partner | #A33E1C | #FFB59E | #A23B25 | #F2A890 | #7E2F84 | #F2AEF0 |
|
||||
| on-owing | | #FFFFFF | #5E1700 | #FFFFFF | #4F1708 | #FFFFFF | #4C0F53 |
|
||||
| owing-container | | #FFDBCF | #7E2C0E | #F6DDD3 | #4B2418 | #F9D7F6 | #66226C |
|
||||
| on-owing-container | | #792A0B | #FFDBCF | #4D170A | #FFDACD | #33003A | #FFD6F8 |
|
||||
| settled | "Kvitt" / "Ni är kvitt" | #4E5F69 | #B6C9D4 | #58534A | #C0B9AB | #6E5500 | #F2D45C |
|
||||
| settled-container | | #DCE5EA | #2C3B43 | #E6E1D6 | #302D28 | #FFE38F | #5A3F00 |
|
||||
| on-settled-container | | #36464F | #D3E5F0 | #2A2823 | #E5DFD2 | #241A00 | #FFE38F |
|
||||
| warning | over-settle, sync warnings | #855700 | #F5BD62 | #875400 | #EBC06C | #8A4D00 | #FFB86E |
|
||||
| on-warning | | #FFFFFF | #452B00 | #FFFFFF | #3E2900 | #FFFFFF | #4A2800 |
|
||||
| warning-container | | #FFDEAD | #633F00 | #F7E3BF | #4D3708 | #FFDDBA | #6A3B00 |
|
||||
| on-warning-container | | #633F00 | #FFDEAD | #3B2600 | #FCE3AE | #2C1600 | #FFDDBA |
|
||||
| pending | sync-pending marker | #7B5C24 | #E8C48A | #875400 | #EBC06C | #00658F | #86CFFF |
|
||||
| avatar-1 | initials background, user slot 1 | #C2E8F3 | #0E4D5C | #3F5E4C | #A5C9B0 | #9A4A00 | #FFB870 |
|
||||
| on-avatar-1 | | #0B4C5C | #C2E8F3 | #FFFFFF | #10291A | #FFFFFF | #2E1500 |
|
||||
| avatar-2 | initials background, user slot 2 | #F2DDB8 | #5D4410 | #6B4C8A | #CDB4E8 | #2C5F9E | #A8C8FF |
|
||||
| on-avatar-2 | | #5A400E | #F7DFB4 | #FFFFFF | #2F1B45 | #FFFFFF | #002F64 |
|
||||
|
||||
All owed/owing text values ≥ 5.6:1 on surface in both themes in all directions. Owed/owing hue pairs are chosen to stay distinct for red-green colour blindness (Skärgård sea-green vs coral, Kvitto green vs brick with a square marker, Kalas green vs purple); the words carry the meaning anyway. Avatar slot: a stable function of the user id (e.g. the last hex digit's parity), so each person keeps one colour on both phones (AC-4.5).
|
||||
|
||||
## 4. Category colours (SPEC §9.7 token names)
|
||||
|
||||
Use: the icon colour on its container (`cat-<token>` on `cat-<token>-container`), chart segments, Kalas solid tiles (`on-cat` icon on `cat-<token>`). Never text in light themes. Container rule: Skärgård 16 % (L) / 22 % (D) of the colour over surface; Kvitto 16 % / 24 %; Kalas 18 % / 24 %.
|
||||
|
||||
Baseline values (used unchanged by Kvitto and Kalas, and by Skärgård in dark):
|
||||
|
||||
| Token | Light | Dark | Skärgård light override |
|
||||
|---|---|---|---|
|
||||
| green | #2E7D32 | #81C784 | #2A732E |
|
||||
| orange | #B84A00 | #FFB74D | #A84300 |
|
||||
| brown | #795548 | #C8B4AC | — |
|
||||
| yellow | #8A6A00 | #F2D45C | #7D6000 |
|
||||
| blue | #1565C0 | #90CAF9 | — |
|
||||
| teal | #00796B | #80CBC4 | #006F62 |
|
||||
| purple | #7B1FA2 | #D1A0DB | — |
|
||||
| red | #C62828 | #EF9A9A | — |
|
||||
| pink | #AD1457 | #F48FB1 | — |
|
||||
| grey | #5F6B73 | #B0BEC5 | #58646C |
|
||||
| indigo | #3949AB | #A5AEE0 | — |
|
||||
| lime | #5B6B00 | #C5D86D | — |
|
||||
|
||||
Measured: icon vs its container ≥ 3.7:1 (light) and ≥ 4.8:1 (dark) in every direction; colour vs surface ≥ 4.6:1 (light) and ≥ 7.7:1 (dark). These differ from the provisional hex values in SPEC §9.7 (orange, brown, yellow, purple, grey, indigo, lime in light; brown, yellow, purple, indigo, lime in dark): adopting them is a spec change (decisions entry + spec version bump) that the orchestrator makes after the pick (SPEC §13.4 U2).
|
||||
|
||||
Per-direction container hex values are listed on each review page (computed, not hand-typed).
|
||||
|
||||
## 5. Chart colours
|
||||
|
||||
| Token | Skärgård L | Skärgård D | Kvitto L | Kvitto D | Kalas L | Kalas D |
|
||||
|---|---|---|---|---|---|---|
|
||||
| chart-cat-1 | #1A6476 | #8ED0E2 | #23408E | #B3C3FF | #C2462A | #FF9F86 |
|
||||
| chart-cat-2 | #A8741A | #E8C48A | #B4602A | #F0A673 | #4F7A2B | #A9D37F |
|
||||
| chart-cat-3 | #5B5FA8 | #B8BCF4 | #2D7A6C | #7DCAB8 | #8C6200 | #F2C14E |
|
||||
| chart-cat-4 | #4D8559 | #9BD3A3 | #7B4E9E | #CDA8EE | #2D63AE | #9CC2FF |
|
||||
| chart-cat-5 | #A0526E | #EDA9C1 | #7F7225 | #D6C66B | #8A3F92 | #E2A6EA |
|
||||
| chart-cat-6 | #2F86A0 | #5FB6D0 | #AE3A5C | #F29CB6 | #00796F | #7FD3C8 |
|
||||
| chart-cat-7 | #7A6A58 | #CDB9A1 | #4D6A7A | #A6BFCD | #B0306A | #FF9EC3 |
|
||||
| chart-cat-8 | #66737C | #AEBDC8 | #73552A | #DCC39A | #5B6670 | #B7C3CC |
|
||||
| chart-seq-1 | #D3ECF3 | #1E3A42 | #D6DDF2 | #2A3352 | #FBD8CC | #4A2A23 |
|
||||
| chart-seq-2 | #9FD3E2 | #245A68 | #A9B8E3 | #3C4C80 | #F4A68E | #7A3B2C |
|
||||
| chart-seq-3 | #62AFC5 | #2F7C8F | #7890CF | #5A70B0 | #E3765A | #B5563C |
|
||||
| chart-seq-4 | #2E869E | #5EADC3 | #4865B1 | #8499DC | #C24A2E | #E8826A |
|
||||
| chart-seq-5 | #165A6B | #A3DDEC | #23408E | #B3C3FF | #8E2A14 | #FFB59F |
|
||||
| chart-grid | #DEE3E6 | #303639 | #E3DED2 | #37342E | #EBDACF | #3E3233 |
|
||||
| chart-axis-label | #40484C | #BFC8CC | = on-surface-variant | = on-surface-variant | #53433F | #D8C2BB |
|
||||
|
||||
Rules: categorical colours are assigned to **accounts** in `created_at` order (Vardag 1, Huset 2, Gotland 2026 3, Japan 2027 4, cycling after 8); **categories** always use their own category colour, so a category looks the same in lists and charts. Sequential ramps are for single-series views (a highlighted bucket, "Min del" totals); seq-1 and seq-2 are backgrounds only (< 3:1). Stacked segments are separated by a 2 dp gap in the surface colour so adjacent similar hues stay distinct.
|
||||
|
||||
## 6. Typography
|
||||
|
||||
| Direction | Typeface(s) | Licence | Amounts |
|
||||
|---|---|---|---|
|
||||
| Skärgård | Roboto Flex (variable: wght, wdth, opsz) | OFL 1.1 | Roboto Flex with `tnum`; hero uses wdth 84 |
|
||||
| Kvitto | IBM Plex Sans (text), IBM Plex Mono (amounts, captions) | OFL 1.1 | Plex Mono (tabular by nature) |
|
||||
| Kalas | Nunito | OFL 1.1 | Nunito (default figures tabular, verified in renders) |
|
||||
|
||||
Type scale (sp / line height / weight):
|
||||
|
||||
| Style | Skärgård | Kvitto | Kalas |
|
||||
|---|---|---|---|
|
||||
| display-large | 57/64 400 | 57/64 400 (unused) | 57/64 800 |
|
||||
| display-medium | 45/52 400 | 45/52 400 (unused) | 45/52 800 |
|
||||
| display-small | 36/44 400 | 36/44 400 (unused) | 36/44 800 |
|
||||
| headline-large | 32/40 500 | 30/38 500 | 32/40 800 |
|
||||
| headline-medium | 28/36 500 | 26/34 500 | 28/36 800 |
|
||||
| headline-small | 24/32 500 | 22/30 500 | 24/32 800 |
|
||||
| title-large | 22/28 500 | 20/28 500 | 22/28 800 |
|
||||
| title-medium | 16/24 560 | 16/24 500 | 16/24 700 |
|
||||
| title-small | 14/20 560 | 14/20 600 | 14/20 800 |
|
||||
| body-large | 16/24 400 | 16/24 400 | 16/24 500 |
|
||||
| body-medium | 14/20 400 | 14/20 400 | 14/20 500 |
|
||||
| body-small | 12/16 400 | 12/16 400 | 12/16 600 |
|
||||
| label-large | 14/20 560 | 14/20 500 | 14/20 800 |
|
||||
| label-medium | 12/16 560 | 12/16 500 | 12/16 700 |
|
||||
| label-small | 11/16 560 | 11/16 500 Mono caps, tracking 0.08 em | 11/16 700 |
|
||||
| amount-hero | 60/64 720 wdth 84, decimals and kr at 50 % | 44/52 500 Mono | 44/52 900 |
|
||||
| amount-large | 32/40 650 wdth 92 | 28/36 500 Mono | 28/36 900 |
|
||||
| amount-medium | 16/24 620 | 15/22 500 Mono | 18/24 800 |
|
||||
| amount-small | 14/20 520 | 12/16 400 Mono | 15/20 800 |
|
||||
|
||||
Font scale: sp follows Android's non-linear scaling (Android 14+, Compose default): text ≥ 24 sp grows less than smaller text, so a 60 sp amount does not double at 200 %. The 200 % frames approximate this. Amounts never ellipsize: they wrap or step down one size.
|
||||
|
||||
## 7. Spacing (dp)
|
||||
|
||||
All three use one 4 dp scale: `space-1` 4, `space-2` 8, `space-3` 12, `space-4` 16, `space-5` 20, `space-6` 24, `space-8` 32, `space-10` 40, `space-12` 48, `space-16` 64 (Kvitto does not use 40 and 64). Screen gutter 16 in all directions.
|
||||
|
||||
| Measure | Skärgård | Kvitto | Kalas |
|
||||
|---|---|---|---|
|
||||
| card padding | 20 | 20 (receipt) | 20 |
|
||||
| home account row | ~72 (card row) | 60 | ~76 (card) |
|
||||
| ledger row | 72 | 60 | 72 |
|
||||
| settings row | 56 | 56 | 56 |
|
||||
| keypad key | 64 | 52 | 52 |
|
||||
| gap between grouped items | 2 | rule (1 dp dashed) | 12 |
|
||||
|
||||
## 8. Shapes (corner radius, dp)
|
||||
|
||||
M3 shape tokens: extraSmall 4, small 8, medium 12, large 16, largeIncreased 20, extraLarge 28, extraLargeIncreased 32, extraExtraLarge 48, full.
|
||||
|
||||
| Component | Skärgård | Kvitto | Kalas |
|
||||
|---|---|---|---|
|
||||
| buttons, chips | full | small 8 | full |
|
||||
| text fields | large 16 (outlined) | extraSmall 4 | large 16 (filled, no indicator) |
|
||||
| cards | extraLarge 28 | none (receipt zig-zag edge, 10 dp teeth) | 24–28 |
|
||||
| balance hero | extraLargeIncreased 32 | top corners 8, zig-zag bottom | extraLarge 28 |
|
||||
| grouped/segmented lists | outer 20, inner 4 | none, rules | outer 24, inner 8 |
|
||||
| category icon container | medium 12 | extraSmall 4 | MaterialShapes Cookie4Sided (fallback 30 %) |
|
||||
| avatars | MaterialShapes Cookie9/12 (fallback circle) | full | MaterialShapes Sunny (fallback circle) |
|
||||
| FAB | largeIncreased 20 | medium 12 | largeIncreased 20 |
|
||||
| dialogs | extraLarge 28 | large 16 | extraLarge 28 |
|
||||
| keypad keys | full, morph to 16 on press | small 8 | full |
|
||||
|
||||
## 9. Elevation and motion
|
||||
|
||||
| | Skärgård | Kvitto | Kalas |
|
||||
|---|---|---|---|
|
||||
| elevation model | tonal (surface-container levels); 2 dp shadow on menus, 3 dp on FAB | almost none: rules and tones; shadow only on menus, FAB, dialogs | tonal + soft warm shadow on light cards (0 1 2, 0 2 8, 10 % brown); no shadows in dark |
|
||||
| motion scheme | `MotionScheme.expressive()` springs: spatial fast 0.6/800, default 0.8/380, slow 0.8/200; effects 1.0/3800, 1600, 800 | standard easing curves: receipt print reveal 350 ms emphasizedDecelerate, number roll 250 ms per digit, "Kvitt" stamp 200 ms, push shared-axis X 300 ms, tabs fade-through 200 ms | `MotionScheme.expressive()`; seesaw re-tilt spring 0.55/300; amount count-up 400 ms emphasizedDecelerate; bunting drop with 40 ms stagger when settled |
|
||||
| stable fallback | `MotionScheme.standard()` / tween 200–500 ms emphasized | (already stable) | `MotionScheme.standard()` |
|
||||
| reduced motion | everything except fades snaps | everything becomes a 150 ms crossfade | springs critically damped |
|
||||
|
||||
## 10. Icons
|
||||
|
||||
Material Symbols (Apache 2.0), as a bundled variable font or vector drawables generated from it.
|
||||
|
||||
| | Skärgård | Kvitto | Kalas |
|
||||
|---|---|---|---|
|
||||
| style | Rounded | Sharp | Rounded |
|
||||
| weight / grade / optical size | 400 / 0 / 24 | 300 / 0 / 20–24 | 500–600 / 0 / 24 |
|
||||
| fill | 1 for selected nav item and category icons, else 0 | 0, except the selected nav item | 1 for category icons and selected nav item |
|
||||
|
||||
Category icons: the 30 tokens of SPEC §9.7. Other icons used by all directions: `home`, `bar_chart`, `settings`, `add`, `close`, `arrow_back`, `more_vert`, `edit`, `delete`, `swap_horiz`, `payments`, `cloud_upload` / `cloud_off` / `cloud_done`, `sync`, `warning`, `info`, `history`, `event_repeat`, `devices`, `person_add`, `content_paste`, `share`, `content_copy`, `drag_indicator`, `archive` / `unarchive`, `backspace`, `system_update`, `phonelink_erase`.
|
||||
Reference in New Issue
Block a user