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:
2026-10-18 08:27:37 +02:00
parent 0006bd79be
commit 6da6b1b023
11 changed files with 3264 additions and 1208 deletions
+67 -10
View File
@@ -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).