Files
kvitt/docs/ux/components.md
T
jacobh2 b72ac5bb9e UX sprint U1: three directions, IA, screens, flows, components, tokens, strings
Three visual directions (Skärgård, Kvitto, Kalas) as self-contained HTML pages with
every SPEC §13.3 screen in light and dark, Swedish, plus PNG exports and the exporter
script. Direction-independent docs: ia.md, screens.md, flows.md, components.md,
tokens.md (values for all three directions), strings.csv (key,en,sv,notes), and the
comparison with a recommendation in directions/README.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 08:27:51 +02:00

12 KiB
Raw Blame History

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 PersonChips 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 PersonAvatars, 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 TextFields 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 PersonChips: "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 AllocationRows + "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 RangePresets), 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.