Files
kvitt/docs/ux/proposed-decisions.md
T
jacobh2 6da6b1b023 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
2026-10-18 08:27:51 +02:00

18 KiB
Raw Blame History

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.

Owner answers (2026-10-18)

Question (U1 report) Owner's answer
Which direction? 3 Kalas, with less bright colours ("I'm nervous that I will get tired of too bright colors over time").
Bottom navigation Three tabs for now (Översikt, Statistik, Inställningar), as SPEC §13.2. No change.
Category from the description Yes: the category is auto-selected from the description and the user can still change it. Rule: history first, then a built-in keyword list, then the AC-8.6 default. The keyword list is learned from the household's Splitwise export.
S-1 "Ditt namn" field (partner types the name on S-1 and confirms it on S-2) OK
Swedish wording "Ni är kvitt", "Gör upp", "Du lånade ut / Du lånade", "Inte inblandad" Yes
Muted Kalas (2026-10-18, second round) Approved ("let's go with that for now").
Mark the suggested category ("Vald utifrån beskrivningen")? No: the category is simply selected; a wrong guess is changed with one tap; simpler UI.
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

  • Decision: the app's visual direction is U1's direction 3 "Kalas" (cards, very round shapes, a solid category-colour tile on every expense, the seesaw balance), with the muted palette of directions/3-kalas/ (owner approved 2026-10-18). Typeface per D-020 (Material 3 default, Roboto); Kalas's preview font Nunito is dropped.
  • Why: owner's pick; muted because bright colours tire over years of daily use.
  • Spec: none beyond UX-5 (category colours). U2's design-spec.md carries the tokens.

UX-2 Navigation stays at three destinations

  • Decision: no change to SPEC §13.2. An "Aktivitet" feed (the other person's changes across accounts) was considered as a fourth tab and not taken for v1.
  • Spec: none.

UX-3 Category suggested from the description

  • Decision (owner, 2026-10-18): for a new expense, the editor selects a category from the description as the user types, while the user has not picked a category by hand in this editor session: (1) history match, the category used most often among matching expenses, (2) built-in keyword list, (3) the AC-8.6 default. The selected category is not marked as a suggestion (no caption); the user changes it with one tap. Editing an existing expense never changes its category automatically.
  • Why: most expenses repeat the same shops; fewer taps in the ten-second flow (§13.1).
  • Evidence (chronological replay of the household's 1 775 Splitwise expenses with the 14 categories of UX-8 and the mapping of UX-7, aggregates only, category-suggestion.md): history step ("most often") used for 71.5 % of rows and right 92.8 % of the time; keyword step 92.4 % on 8.2 %; overall 79.7 % correct versus 30.5 % for today's AC-8.6 default alone ("most recent" would give 79.0 %). Cold start (no history): keywords fire on 77 % of rows and are right 96 % of the time.
  • Spec changes:
    • 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). 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)

  • Finding (export analysis): 21 rows use Swedish Splitwise category names that the §10.8 table lacks (Utbildning 4, Tjänster 3, Livet - Övrigt 3, Kläder 3, Städning 2, Flyg 2, Spel 2, Verktyg - Övrigt 1, Cykel 1); and §10.8 collapses Splitwise's finer categories, so e.g. Försäkringar (26 rows), Barnomsorg (73) and Skatter (19) land in other and Hyra (106) and Avbetalning/Amortering (48) in home.
  • Decision (owner, 2026-10-18): map Splitwise categories as finely as Kvitt's categories allow, with the four new defaults of UX-8.
  • Spec: the §10.8 table and server/crates/kvitt-import/data/splitwise_categories.csv become:
Splitwise category (English) Splitwise category (Swedish) Kvitt default_key
Groceries, Food and drink - Other, Liquor Livsmedel, Mat och dryck - Övrigt, Alkohol groceries
Dining out Restaurangbesök restaurants
Rent, Mortgage Hyra, Avbetalning/Amortering housing
Electronics, Furniture, Household supplies, Maintenance, Services, Pets, Home - Other Elektronik, Möbler, Förbrukningsvaror, Skötsel/underhåll, Tjänster, Husdjur, Hem - Övrigt home
Electricity, Heat/gas, Water, Trash, TV/Phone/Internet, Cleaning, Utilities - Other El, Värme/gas, Vatten, Sophämtning, TV/telefon/internet, Städning, Verktyg - Övrigt utilities
Insurance Försäkringar insurance
Bicycle, Bus/train, Car, Gas/fuel, Parking, Taxi, Transportation - Other Cykel, Buss/tåg, Bil, Bensin/bränsle, Parkering, Taxi, Transport - Övrigt transport
Hotel, Plane Hotell, Flyg travel
Games, Movies, Music, Sports, Entertainment - Other Spel, Filmer, Musik, Sport, Underhållning - Övrigt entertainment
Medical expenses Sjukvård/medicin health
Childcare Barnomsorg children
Clothing, Gifts Kläder, Presenter shopping
Taxes Skatter fees
General, Education, Life - Other, Other Allmänt, Utbildning, Livet - Övrigt other
any other value any other value other, counted as unmapped

Swedish names not seen in the export (El, Värme/gas, Vatten, Sophämtning, Husdjur) are Splitwise's Swedish labels by analogy and must be checked against Splitwise before M9 relies on them; an unseen name simply stays unmapped.

UX-8 Four new default categories

  • Decision (owner, 2026-10-18): Kvitt seeds 14 default categories instead of 10. New: Boende, Försäkringar, Barn, Avgifter & skatter. Hem keeps furniture, supplies, maintenance, electronics and services; rent and mortgage move to Boende.
  • Why: the household's history shows these as separate, large groups (Hyra + Amortering 154 rows, Barnomsorg 73, Försäkringar 26, Skatter 19) that were collapsed into Hem and Övrigt; separate categories make analytics and the category suggestion meaningful.
  • Spec (AC-7.2 table, full new list in sort order; new rows in bold):
default_key English name Swedish name icon color sort_order
groceries Groceries Mat & hushåll shopping_cart green 10
restaurants Restaurants Restaurang & café restaurant orange 20
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 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 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 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

  • Finding: AC-2.4 sends a display name with the accept call, but §13.3's S-1 lists no name field.
  • Decision (owner OK, 2026-10-18): S-1 has a required "Ditt namn" field on a first pairing (hidden in the "Para ihop telefonen igen" variant); the partner confirms it again on S-2 (AC-2.10).
  • Spec: §13.3 S-1 "Must show": add "display name field (1–40, §9.6), required on a first pairing".

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), 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)

Rule Spec place
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 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):
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):
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".
  • 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).