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>
12 KiB
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 3ColorScheme+KvittColorsextension +KvittTypography); no literal values. - Strings only from resources (
strings.csvkeys); components that need words take them as resolvedStringparameters or take the model value and resolve inside:core:design(shared strings live there, §14.7). - Touch targets ≥ 48 dp (
minimumInteractiveComponentSize), TalkBackcontentDescription/semanticsas stated per component. - Material 3 Expressive APIs (
@ExperimentalMaterial3ExpressiveApi) are used only behind a:core:designwrapper 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 forHero/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") andAccount(… "Kvitt"). - Semantics: merged into one sentence.
2. People
PersonAvatar
- Params:
user: User,size(24, 32, 40, 56, 96 dp),shape(direction token; fallbackCircleShape). - Picture via Coil keyed by
user.id+pictureVersion(AC-4.6); else initials (first letter of the first two words, uppercase, AC-4.5) onavatar-1/avatar-2chosen 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 (warningicon + "Det här reglerar mer än som är skyldigt i Japan 2027") whenoverSettles. VariantFixed(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 perFieldChange, sentence built from field + old → new, values formatted byMoneyFormatter/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.