diff --git a/docs/ux/README.md b/docs/ux/README.md index 120ee5c..754f278 100644 --- a/docs/ux/README.md +++ b/docs/ux/README.md @@ -17,6 +17,7 @@ The UX deliverables for Kvitt v1 (SPEC §13), produced 2026-10-17. Phase 1 (U1) | `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 `:one` and `:other`, which become one ``. 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` | ## The three directions diff --git a/docs/ux/category-keywords.csv b/docs/ux/category-keywords.csv new file mode 100644 index 0000000..ed6901a --- /dev/null +++ b/docs/ux/category-keywords.csv @@ -0,0 +1,150 @@ +keyword,match,default_key,source +bio,token,entertainment,data +bowling,token,entertainment,curated +entre,token,entertainment,data +entré,token,entertainment,data +filmstaden,token,entertainment,curated +gröna lund,phrase,entertainment,curated +karaoke,token,entertainment,data +konsert,prefix,entertainment,data +liseberg,token,entertainment,curated +museet,token,entertainment,curated +museum,token,entertainment,data +netflix,token,entertainment,curated +padel,token,entertainment,curated +spotify,token,entertainment,data +teater,prefix,entertainment,curated +viaplay,token,entertainment,curated +alkohol,token,groceries,data +bondens marknad,phrase,groceries,data +bröd,token,groceries,data +bubbel,token,groceries,data +city gross,phrase,groceries,curated +coop,token,groceries,data +frukt,token,groceries,curated +grönsaker,token,groceries,data +handling,prefix,groceries,data +hemköp,token,groceries,data +ica,token,groceries,data +julmat,token,groceries,data +lidl,token,groceries,curated +livs,token,groceries,data +livsmedel,prefix,groceries,curated +mat,token,groceries,data +mjölk,token,groceries,data +netto,token,groceries,data +ost,token,groceries,data +snacks,token,groceries,data +storhandling,token,groceries,data +systembolag,prefix,groceries,data +systemet,token,groceries,data +willys,token,groceries,data +apotek,prefix,health,data +kry,token,health,curated +läkare,token,health,curated +medicin,prefix,health,curated +sjukhus,token,health,curated +tandläkare,token,health,curated +tandvård,token,health,curated +vårdcentral,token,health,curated +amortering,prefix,home,curated +bauhaus,token,home,data +brf,token,home,data +byggmax,token,home,curated +clas ohlson,phrase,home,data +elgiganten,token,home,curated +färg,token,home,data +garderob,prefix,home,data +gardin,prefix,home,data +hylla,token,home,data +hyra,token,home,data +ikea,token,home,data +jula,token,home,curated +jysk,token,home,curated +lamp,prefix,home,data +lgh,token,home,data +möbler,token,home,curated +ohlson,token,home,data +rusta,token,home,data +ränta,token,home,data +barnförsäkring,token,other,data +förskola,token,other,data +försäkring,prefix,other,data +hemförsäkring,token,other,data +ränteåterbäring,token,other,data +bagel,prefix,restaurants,data +bageri,prefix,restaurants,data +bar,token,restaurants,curated +burger king,phrase,restaurants,curated +cafe,token,restaurants,curated +café,token,restaurants,data +dumplings,token,restaurants,data +fika,prefix,restaurants,data +foodora,token,restaurants,curated +glass,token,restaurants,data +hamburgare,token,restaurants,curated +kaffe,token,restaurants,data +kebab,token,restaurants,data +lunch,token,restaurants,data +max,token,restaurants,data +mcdonalds,token,restaurants,curated +middag,token,restaurants,data +pizz,prefix,restaurants,data +pub,token,restaurants,curated +ramen,token,restaurants,data +restaurang,prefix,restaurants,curated +seml,prefix,restaurants,data +streetfood,token,restaurants,data +sushi,token,restaurants,data +takeaway,token,restaurants,curated +thai,token,restaurants,data +uber eats,phrase,restaurants,curated +wolt,token,restaurants,curated +amazon,token,shopping,curated +h&m,token,shopping,curated +intersport,token,shopping,curated +kappahl,token,shopping,curated +lindex,token,shopping,curated +present,prefix,shopping,data +skor,token,shopping,curated +stadium,token,shopping,curated +zalando,token,shopping,curated +zara,token,shopping,curated +bensin,token,transport,curated +bil,token,transport,data +biltvätt,token,transport,curated +buss,prefix,transport,curated +diesel,token,transport,curated +easypark,token,transport,curated +flygbuss,token,transport,curated +färj,prefix,transport,data +laddning,prefix,transport,data +okq8,token,transport,curated +parkering,prefix,transport,data +pendeltåg,token,transport,curated +preem,token,transport,curated +sj,token,transport,curated +sl,token,transport,curated +tanka,token,transport,curated +taxi,token,transport,data +trängselskatt,token,transport,curated +tåg,token,transport,data +uber,token,transport,curated +västtrafik,token,transport,curated +airbnb,token,travel,data +boende,token,travel,data +booking,token,travel,curated +flyg,token,travel,curated +hotel,token,travel,data +hotell,token,travel,data +sas,token,travel,curated +scandic,token,travel,curated +vandrarhem,token,travel,curated +bredband,token,utilities,curated +elräkning,token,utilities,curated +internet,token,utilities,data +städning,prefix,utilities,curated +tele2,token,utilities,curated +telenor,token,utilities,curated +telia,token,utilities,curated +vattenfall,token,utilities,curated diff --git a/docs/ux/directions/3-kalas/index.html b/docs/ux/directions/3-kalas/index.html index 0791c43..189febe 100644 --- a/docs/ux/directions/3-kalas/index.html +++ b/docs/ux/directions/3-kalas/index.html @@ -6,7 +6,7 @@ Kvitt · 3 Kalas - + @@ -14,14 +14,14 @@ *{box-sizing:border-box;margin:0;padding:0} button{font:inherit;color:inherit;background:none;border:0;text-align:inherit;cursor:pointer} /* ---------- review page ---------- */ -body.review{background:#EFE8E2;color:#2A201E;font-family:Nunito,system-ui,sans-serif;font-size:15px;line-height:1.5;padding:40px 24px 80px} +body.review{background:#EFE8E2;color:#2A201E;font-family:Roboto,system-ui,sans-serif;font-size:15px;line-height:1.5;padding:40px 24px 80px} .review .wrap{max-width:1240px;margin:0 auto} -.review h1{font-size:40px;line-height:1.1;font-weight:900;letter-spacing:-.5px} -.review h1 small{display:block;font-size:15px;font-weight:800;letter-spacing:1.5px;text-transform:uppercase;color:#9C3B22;margin-bottom:6px} +.review header>h1{font-size:40px;line-height:1.1;font-weight:900;letter-spacing:-.5px} +.review header>h1 small{display:block;font-size:15px;font-weight:800;letter-spacing:1.5px;text-transform:uppercase;color:#9C3B22;margin-bottom:6px} .review .idea{font-size:20px;font-weight:700;margin:10px 0 8px;max-width:820px} -.review p{max-width:860px;margin:8px 0} -.review h2{font-size:26px;font-weight:900;margin:48px 0 12px;padding-top:16px;border-top:2px solid #DCCFC6} -.review h3{font-size:18px;font-weight:900;margin:26px 0 8px} +.review .wrap>p,.review header>p{max-width:860px;margin:8px 0} +.review .wrap>h2{font-size:26px;font-weight:900;margin:48px 0 12px;padding-top:16px;border-top:2px solid #DCCFC6} +.review .wrap>h3,.review .group>h3{font-size:18px;font-weight:900;margin:26px 0 8px} .review table.spec{border-collapse:collapse;font-size:13px;margin:8px 0 12px;background:#FFFBF7;border-radius:12px;overflow:hidden} .review table.spec th,.review table.spec td{padding:6px 10px;border-bottom:1px solid #EADFD7;text-align:left;vertical-align:middle} .review table.spec th{background:#F5ECE5;font-weight:800} @@ -30,12 +30,12 @@ body.review{background:#EFE8E2;color:#2A201E;font-family:Nunito,system-ui,sans-s .review .bad{color:#B3261E;font-weight:900} .review .ok{color:#1B6B45;font-weight:800} .review code{font-family:ui-monospace,Menlo,monospace;font-size:12px;background:#F3E9E1;padding:1px 4px;border-radius:4px} -.review ul{margin:6px 0 6px 22px;max-width:860px} +.review .wrap>ul{margin:6px 0 6px 22px;max-width:860px} .review .states{display:flex;flex-wrap:wrap;gap:16px;margin:8px 0} -.review .states .tk{width:379px;padding:16px;border-radius:24px;background:var(--surface);font-family:Nunito,sans-serif;--fs:1} +.review .states .tk{width:379px;padding:16px;border-radius:24px;background:var(--surface);font-family:Roboto,sans-serif;--fs:1} .review .states .cap{font-size:12px;font-weight:800;margin-bottom:6px;color:var(--on-surface-variant)} .review .group{margin:36px 0 0} -.review .group h3{font-size:20px;margin-bottom:2px} +.review .group>h3{font-size:20px;margin-bottom:2px} .review .pairs{display:flex;flex-wrap:wrap;gap:28px;margin-top:12px} .review figure{display:flex;flex-direction:column;gap:8px} .review figcaption{font-size:13px;max-width:411px} @@ -47,14 +47,14 @@ body.frame{margin:0;background:transparent} /* ---------- phone ---------- */ .phone{position:relative;width:411px;height:891px;overflow:hidden;background:var(--surface);color:var(--on-surface); - font-family:Nunito,system-ui,sans-serif;font-variant-numeric:tabular-nums;font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-weight:500; + font-family:Roboto,system-ui,sans-serif;font-variant-numeric:tabular-nums;font-feature-settings:'tnum';font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-weight:400; -webkit-font-smoothing:antialiased;flex:none;text-align:left} .ic{font-family:'Material Symbols Rounded';font-weight:normal;font-style:normal;font-size:24px;line-height:1;width:1em;height:1em;display:inline-block; overflow:hidden;white-space:nowrap;direction:ltr;font-feature-settings:'liga';flex:none;text-align:center; font-variation-settings:'FILL' 0,'wght' 500,'GRAD' 0,'opsz' 24;user-select:none} .ic.f{font-variation-settings:'FILL' 1,'wght' 600,'GRAD' 0,'opsz' 24} .sb{position:absolute;top:0;left:0;right:0;height:32px;display:flex;align-items:center;justify-content:space-between;padding:0 18px 0 24px;z-index:20; - font-size:calc(min(var(--fs),1.2)*14px);font-weight:700;color:var(--on-surface)} + font-size:calc(min(var(--fs),1.2)*14px);font-weight:500;color:var(--on-surface)} .sb .sbi{display:flex;gap:4px;align-items:center} .sb .ic{font-size:17px} .app{position:absolute;top:32px;left:0;right:0;bottom:0} @@ -79,12 +79,12 @@ body.frame{margin:0;background:transparent} .tab-a{display:flex;align-items:center;gap:2px;padding-right:4px} .ib{width:48px;height:48px;border-radius:50%;display:grid;place-items:center;flex:none;color:var(--on-surface-variant)} .ib.tonal{background:var(--secondary-container);color:var(--on-secondary-container)} -.wordmark{font-weight:900;font-size:calc(min(var(--fs),1.5)*24px);line-height:calc(min(var(--fs),1.5)*28px);color:var(--primary);letter-spacing:-.4px;display:flex;align-items:center;gap:8px} +.wordmark{font-weight:700;font-size:calc(min(var(--fs),1.5)*24px);line-height:calc(min(var(--fs),1.5)*28px);color:var(--primary);letter-spacing:-.4px;display:flex;align-items:center;gap:8px} .wordmark .dot{width:22px;height:22px;background:var(--primary);display:grid;place-items:center;color:var(--on-primary)} .wordmark .dot .ic{font-size:15px} /* buttons */ -.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:8px 20px;border-radius:999px;font-weight:800; +.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:8px 20px;border-radius:999px;font-weight:600; font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);flex:none;text-align:center} .btn .ic{font-size:20px} .btn.m{min-height:56px;padding:12px 26px;font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*24px)} @@ -104,7 +104,7 @@ body.frame{margin:0;background:transparent} /* chips */ .chips{display:flex;flex-wrap:wrap;gap:6px} .chip{display:inline-flex;align-items:center;gap:6px;min-height:36px;padding:4px 10px 4px 8px;border-radius:999px;background:var(--surface-container-high); - color:var(--on-surface);font-weight:700;font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*18px);max-width:100%} + color:var(--on-surface);font-weight:500;font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*18px);max-width:100%} .chip .ic{font-size:18px;color:var(--on-surface-variant)} .chip .lead-dot{width:12px;height:12px;border-radius:50%;flex:none} .chip.sel{background:var(--secondary-container);color:var(--on-secondary-container)} @@ -116,7 +116,7 @@ body.frame{margin:0;background:transparent} /* segmented / connected button group */ .seg{display:flex;gap:2px} .seg>span{flex:1;display:flex;align-items:center;justify-content:center;gap:4px;min-height:40px;padding:6px 12px;background:var(--surface-container-high); - font-weight:800;font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*18px);border-radius:8px;text-align:center;color:var(--on-surface-variant)} + font-weight:600;font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*18px);border-radius:8px;text-align:center;color:var(--on-surface-variant)} .seg>span:first-child{border-radius:999px 8px 8px 999px} .seg>span:last-child{border-radius:8px 999px 999px 8px} .seg>span.on{background:var(--secondary);color:var(--on-secondary);border-radius:999px} @@ -125,8 +125,8 @@ body.frame{margin:0;background:transparent} /* fields */ .fld{background:var(--surface-container-highest);border-radius:16px;padding:8px 16px;min-height:56px;display:flex;align-items:center;gap:12px;box-shadow:inset 0 0 0 0 transparent} .fld .fv{flex:1;min-width:0;display:flex;flex-direction:column} -.fld .lab{font-size:calc(var(--fs)*12px);line-height:calc(var(--fs)*16px);color:var(--on-surface-variant);font-weight:700} -.fld .val{font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*24px);font-weight:600;overflow-wrap:anywhere} +.fld .lab{font-size:calc(var(--fs)*12px);line-height:calc(var(--fs)*16px);color:var(--on-surface-variant);font-weight:500} +.fld .val{font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*24px);font-weight:400;overflow-wrap:anywhere} .fld .ph{color:var(--on-surface-variant);font-weight:500} .fld>.ic{color:var(--on-surface-variant)} .fld.focus{box-shadow:inset 0 0 0 2px var(--primary)} @@ -157,7 +157,7 @@ body.frame{margin:0;background:transparent} .mh{display:flex;align-items:baseline;justify-content:space-between;padding:12px 8px 2px;gap:8px} .sec-h{display:flex;align-items:center;justify-content:space-between;padding:8px 4px 0 8px;gap:8px;min-height:48px} .badge{display:inline-flex;align-items:center;gap:4px;padding:2px 10px;border-radius:999px;background:var(--surface-container-highest);color:var(--on-surface-variant); - font-weight:800;font-size:calc(var(--fs)*11px);line-height:calc(var(--fs)*16px);white-space:nowrap;vertical-align:middle} + font-weight:600;font-size:calc(var(--fs)*11px);line-height:calc(var(--fs)*16px);white-space:nowrap;vertical-align:middle} .badge .ic{font-size:14px} .badge.lantern{background:var(--tertiary-container);color:var(--on-tertiary-container)} .badge.pend{background:color-mix(in srgb,var(--pending) 14%,transparent);color:var(--pending)} @@ -167,7 +167,7 @@ body.frame{margin:0;background:transparent} .tile{display:grid;place-items:center;flex:none} .tile .ic{font-variation-settings:'FILL' 1,'wght' 600,'GRAD' 0,'opsz' 24} .av{display:inline-grid;place-items:center;flex:none;overflow:hidden;position:relative} -.av.ini{background:var(--avatar-2);color:var(--on-avatar-2);font-weight:900} +.av.ini{background:var(--avatar-2);color:var(--on-avatar-2);font-weight:700} .av.ini.a1{background:var(--avatar-1);color:var(--on-avatar-1)} .av.photo{background:linear-gradient(150deg,var(--photo-1),var(--photo-2))} .av.photo svg{position:absolute;inset:0;width:100%;height:100%} @@ -175,15 +175,15 @@ body.frame{margin:0;background:transparent} /* FAB */ .fab{position:absolute;right:16px;display:flex;align-items:center;gap:10px;min-height:56px;padding:12px 20px 12px 16px;border-radius:20px;background:var(--primary);color:var(--on-primary); - font-weight:800;font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*22px);box-shadow:0 2px 4px var(--shadow),0 6px 14px var(--shadow);z-index:8;max-width:calc(100% - 32px)} + font-weight:600;font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*22px);box-shadow:0 2px 4px var(--shadow),0 6px 14px var(--shadow);z-index:8;max-width:calc(100% - 32px)} .fab .ic{font-size:24px} /* navigation bar (Expressive short navigation bar, 64 dp) */ .nav{flex:none;background:var(--surface-container);padding-bottom:24px;display:flex;z-index:7} .nav .ni{flex:1;min-height:64px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;color:var(--on-surface-variant); - font-weight:700;font-size:calc(min(var(--fs),1.6)*12px);line-height:calc(min(var(--fs),1.6)*16px);padding:6px 2px;text-align:center} + font-weight:500;font-size:calc(min(var(--fs),1.6)*12px);line-height:calc(min(var(--fs),1.6)*16px);padding:6px 2px;text-align:center} .nav .ind{width:56px;height:32px;border-radius:16px;display:grid;place-items:center} -.nav .ni.on{color:var(--on-surface);font-weight:900} +.nav .ni.on{color:var(--on-surface);font-weight:700} .nav .ni.on .ind{background:var(--primary-container);color:var(--on-primary-container)} .nav .ni.on .ic{font-variation-settings:'FILL' 1,'wght' 600,'GRAD' 0,'opsz' 24} @@ -193,10 +193,10 @@ body.frame{margin:0;background:transparent} .see .plank{position:absolute;left:15px;top:66px;width:220px;height:8px;border-radius:4px;transform-origin:50% 50%} .see .fulc{position:absolute;left:107px;top:70px;width:36px;height:22px;clip-path:polygon(50% 0,100% 100%,0 100%);background:var(--outline-variant)} .see .seat{position:absolute;display:flex;flex-direction:column;align-items:center;gap:2px} -.see .nm{font-size:calc(min(var(--fs),1.15)*12px);line-height:calc(min(var(--fs),1.15)*16px);font-weight:800;color:var(--on-surface-variant)} +.see .nm{font-size:calc(min(var(--fs),1.15)*12px);line-height:calc(min(var(--fs),1.15)*16px);font-weight:600;color:var(--on-surface-variant)} .hero .words{margin-top:6px} .hero-amt{display:flex;flex-wrap:wrap;justify-content:center;align-items:baseline;column-gap:6px;margin-top:2px} -.hero-amt .u{font-weight:900;font-size:calc(min(var(--fs),1.5)*24px);line-height:calc(min(var(--fs),1.5)*30px)} +.hero-amt .u{font-weight:700;font-size:calc(min(var(--fs),1.5)*24px);line-height:calc(min(var(--fs),1.5)*30px)} .hero .btn{margin-top:14px} .bunting{position:absolute;left:0;right:0;top:0;height:30px} .bunting svg{width:100%;height:100%} @@ -206,7 +206,7 @@ body.frame{margin:0;background:transparent} .acard .an{flex:1 1 150px;min-width:0} .acard .ab{flex:0 0 auto;margin-left:auto;text-align:right;display:flex;flex-direction:column;align-items:flex-end} .acard.arch{opacity:.92} -.atile{font-weight:900;color:var(--on-cat);font-size:calc(min(var(--fs),1.3)*18px)} +.atile{font-weight:700;color:var(--on-cat);font-size:calc(min(var(--fs),1.3)*18px)} /* split bar (Expressive slider look) */ .split{position:relative;display:flex;align-items:center;gap:6px;height:16px} @@ -219,7 +219,7 @@ body.frame{margin:0;background:transparent} /* keypad */ .kp{flex:none;background:var(--surface-container);border-radius:28px 28px 0 0;padding:12px 12px 32px;display:grid;grid-template-columns:repeat(3,1fr);gap:8px} -.kp .k{min-height:52px;border-radius:999px;background:var(--surface-container-highest);display:grid;place-items:center;font-weight:800; +.kp .k{min-height:52px;border-radius:999px;background:var(--surface-container-highest);display:grid;place-items:center;font-weight:600; font-size:calc(min(var(--fs),1.5)*24px);line-height:calc(min(var(--fs),1.5)*30px);color:var(--on-surface)} .kp.sm .k{min-height:46px} .kp .k.fn{background:var(--primary-container);color:var(--on-primary-container)} @@ -227,8 +227,8 @@ body.frame{margin:0;background:transparent} /* category quick row */ .cq{display:grid;grid-template-columns:repeat(6,1fr);gap:4px} .cqi{display:flex;flex-direction:column;align-items:center;gap:6px;padding:4px 0;min-width:0;position:relative} -.cqi .cql{font-size:calc(var(--fs)*11px);line-height:calc(var(--fs)*14px);font-weight:700;color:var(--on-surface-variant);text-align:center;overflow-wrap:normal;hyphens:manual;max-width:100%} -.cqi.on .cql{color:var(--on-surface);font-weight:900} +.cqi .cql{font-size:calc(var(--fs)*11px);line-height:calc(var(--fs)*14px);font-weight:500;color:var(--on-surface-variant);text-align:center;overflow-wrap:normal;hyphens:manual;max-width:100%} +.cqi.on .cql{color:var(--on-surface);font-weight:700} .cqi .chk{position:absolute;top:-6px;right:-8px;width:20px;height:20px;border-radius:50%;background:var(--on-surface);color:var(--surface);display:grid;place-items:center;box-shadow:0 0 0 2px var(--surface)} .cqi .chk .ic{font-size:14px;font-variation-settings:'FILL' 1,'wght' 700,'GRAD' 0,'opsz' 20} @@ -244,7 +244,7 @@ body.frame{margin:0;background:transparent} /* sync line */ .syncl{align-self:center;display:inline-flex;align-items:center;gap:8px;min-height:40px;padding:6px 10px 6px 14px;border-radius:999px;background:var(--surface-container-high); - font-weight:700;font-size:calc(var(--fs)*13px);line-height:calc(var(--fs)*18px);color:var(--on-surface-variant);max-width:100%} + font-weight:500;font-size:calc(var(--fs)*13px);line-height:calc(var(--fs)*18px);color:var(--on-surface-variant);max-width:100%} .syncl .ic{font-size:18px} .syncl.pend{background:color-mix(in srgb,var(--pending) 13%,var(--surface));color:var(--on-surface)} @@ -276,15 +276,15 @@ body.frame{margin:0;background:transparent} /* charts */ .chart{position:relative;height:170px;margin:8px 0 0 44px} .chart .gl{position:absolute;left:0;right:0;height:1px;background:var(--chart-grid)} -.chart .yl{position:absolute;left:-46px;width:40px;text-align:right;transform:translateY(-50%);font-size:calc(var(--fs)*10px);line-height:calc(var(--fs)*12px);color:var(--chart-axis-label);font-weight:700} +.chart .yl{position:absolute;left:-46px;width:40px;text-align:right;transform:translateY(-50%);font-size:calc(var(--fs)*10px);line-height:calc(var(--fs)*12px);color:var(--chart-axis-label);font-weight:500} .chart .cols{position:absolute;inset:0;display:flex;align-items:flex-end;gap:10px;padding:0 4px} .chart .col{flex:1;display:flex;flex-direction:column-reverse;gap:2px;height:100%;justify-content:flex-start} .chart .col .stk{display:flex;flex-direction:column-reverse;gap:2px;border-radius:10px 10px 4px 4px;overflow:hidden;flex:none} .chart .col .stk>i{display:block;flex:none;min-height:3px} .xl{display:flex;gap:10px;margin:6px 0 0 44px;padding:0 4px} -.xl span{flex:1;text-align:center;font-size:calc(var(--fs)*11px);line-height:calc(var(--fs)*14px);color:var(--chart-axis-label);font-weight:700} +.xl span{flex:1;text-align:center;font-size:calc(var(--fs)*11px);line-height:calc(var(--fs)*14px);color:var(--chart-axis-label);font-weight:500} .legend{display:flex;flex-wrap:wrap;gap:4px 12px;margin-top:10px} -.legend span{display:inline-flex;align-items:center;gap:6px;font-size:calc(var(--fs)*12px);line-height:calc(var(--fs)*16px);font-weight:700;color:var(--on-surface-variant)} +.legend span{display:inline-flex;align-items:center;gap:6px;font-size:calc(var(--fs)*12px);line-height:calc(var(--fs)*16px);font-weight:500;color:var(--on-surface-variant)} .legend i{width:10px;height:10px;border-radius:3px;display:inline-block;flex:none} .brow{display:flex;flex-direction:column;gap:6px;padding:8px 0} .brow .bh{display:flex;align-items:center;gap:12px} @@ -300,7 +300,7 @@ body.frame{margin:0;background:transparent} .ntf .ap{width:36px;height:36px;border-radius:50%;background:var(--primary);color:var(--on-primary);display:grid;place-items:center;flex:none} .ntf .ap .ic{font-size:20px} .ntf .h{font-size:calc(var(--fs)*12px);line-height:calc(var(--fs)*16px);color:var(--sys-on-variant);font-weight:600} -.ntf .t{font-size:calc(var(--fs)*15px);line-height:calc(var(--fs)*20px);font-weight:700;margin-top:2px} +.ntf .t{font-size:calc(var(--fs)*15px);line-height:calc(var(--fs)*20px);font-weight:500;margin-top:2px} .ntf .b{font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);color:var(--sys-on-variant)} @@ -325,13 +325,65 @@ function lum(h){ return hexToRgb(h).map(v => v / 255).map(v => v <= 0.03928 ? v function contrast(a, b){ const x = lum(a), y = lum(b); return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05); } function mix(a, b, t){ const A = hexToRgb(a), B = hexToRgb(b); return '#' + A.map((v, i) => Math.round(v * t + B[i] * (1 - t)).toString(16).padStart(2, '0')).join('').toUpperCase(); } -/* ---------- tokens (single source: CSS + review tables + contrast) ---------- */ +/* ---------- tokens (single source: CSS + review tables + contrast). Muted revision 2026-10-18 ---------- */ const CAT_BASE = { - light: { green:'#2E7D32', orange:'#B84A00', brown:'#795548', yellow:'#8A6A00', blue:'#1565C0', teal:'#00796B', purple:'#7B1FA2', red:'#C62828', pink:'#AD1457', grey:'#5F6B73', indigo:'#3949AB', lime:'#5B6B00' }, - dark: { green:'#81C784', orange:'#FFB74D', brown:'#C8B4AC', yellow:'#F2D45C', blue:'#90CAF9', teal:'#80CBC4', purple:'#D1A0DB', red:'#EF9A9A', pink:'#F48FB1', grey:'#B0BEC5', indigo:'#A5AEE0', lime:'#C5D86D' } + light: { green:'#4E7A55', orange:'#B26A3E', brown:'#7A5F50', yellow:'#9C7C2A', blue:'#4A78A8', teal:'#367F82', purple:'#865891', red:'#A84B45', pink:'#AE5D7C', grey:'#6C7177', indigo:'#45508D', lime:'#73822F' }, + dark: { green:'#8FB896', orange:'#E8AA7E', brown:'#B8A090', yellow:'#E6C67A', blue:'#98C0E6', teal:'#8CCAC8', purple:'#C59CDA', red:'#DC8F86', pink:'#EFB0BE', grey:'#B0B2B3', indigo:'#969DD8', lime:'#B9CC86' } }; const CAT_CONTAINER_MIX = { light: 0.18, dark: 0.24 }; const TOK = { + light: { + 'primary':'#9C5238','on-primary':'#FFFFFF','primary-container':'#F2DED4','on-primary-container':'#3A1609', + 'secondary':'#5A6D52','on-secondary':'#FFFFFF','secondary-container':'#DDE6D3','on-secondary-container':'#172410', + 'tertiary':'#76612F','on-tertiary':'#FFFFFF','tertiary-container':'#EEE3C6','on-tertiary-container':'#261C04', + 'error':'#B3261E','on-error':'#FFFFFF','error-container':'#F9DEDA','on-error-container':'#410E0B', + 'surface':'#FAF7F2','on-surface':'#22201D','surface-variant':'#E8E2D9','on-surface-variant':'#4E4842', + 'surface-container-lowest':'#FFFFFF','surface-container-low':'#F5F1EB','surface-container':'#F0EBE4','surface-container-high':'#EAE5DD','surface-container-highest':'#E4DED6', + 'surface-dim':'#DCD6CE','surface-bright':'#FAF7F2','inverse-surface':'#36322E','inverse-on-surface':'#F6F1EA','inverse-primary':'#E4B39F', + 'outline':'#7E766E','outline-variant':'#D2CAC1','scrim':'#000000', + 'owed':'#3D6A4C','on-owed':'#FFFFFF','owed-container':'#D6E7D9','on-owed-container':'#0F2416', + 'owing':'#78497A','on-owing':'#FFFFFF','owing-container':'#EEDDEC','on-owing-container':'#2D1430', + 'settled':'#6A5A2B','settled-container':'#EEE3C6','on-settled-container':'#261C04', + 'warning':'#8A5522','on-warning':'#FFFFFF','warning-container':'#F5E1CA','on-warning-container':'#2C1800', + 'pending':'#3C6680', + 'avatar-1':'#8A5A33','on-avatar-1':'#FFFFFF','avatar-2':'#4A6587','on-avatar-2':'#FFFFFF', + 'chart-cat-1':'#A75B40','chart-cat-2':'#5E7A4E','chart-cat-3':'#8B6A2C','chart-cat-4':'#4C6E98','chart-cat-5':'#7C5A88','chart-cat-6':'#3F7774','chart-cat-7':'#A2566F','chart-cat-8':'#6A6F74', + 'chart-seq-1':'#F2DFD6','chart-seq-2':'#E2B7A5','chart-seq-3':'#C08068','chart-seq-4':'#A75B40','chart-seq-5':'#763A26', + 'chart-grid':'#E6E0D8','chart-axis-label':'#4E4842', + /* Kalas extras (not Material roles) */ + 'card':'#FFFFFF','on-cat':'#FFFFFF','shadow':'rgba(60,40,25,.08)', + 'photo-1':'#E8C2A2','photo-2':'#C0806A','photo-fg':'#6A3622', + 'sys-wall-1':'#D9D4DE','sys-wall-2':'#C9C3D0','sys-card':'#F7F2FA','sys-on':'#1C1B1F','sys-on-variant':'#49454F' + }, + dark: { + 'primary':'#E6B5A2','on-primary':'#4A1F10','primary-container':'#6A3826','on-primary-container':'#F6DCD0', + 'secondary':'#BCCBB1','on-secondary':'#27341F','secondary-container':'#3D4A35','on-secondary-container':'#D9E6CD', + 'tertiary':'#DCC690','on-tertiary':'#3A2E0B','tertiary-container':'#55432E','on-tertiary-container':'#F1E3C4', + 'error':'#F2B8B1','on-error':'#5F1410','error-container':'#73302A','on-error-container':'#F9DEDA', + 'surface':'#1B1917','on-surface':'#ECE6DF','surface-variant':'#4A4540','on-surface-variant':'#CDC5BC', + 'surface-container-lowest':'#151311','surface-container-low':'#221F1D','surface-container':'#272422','surface-container-high':'#312E2B','surface-container-highest':'#3C3835', + 'surface-dim':'#1B1917','surface-bright':'#423E3A','inverse-surface':'#ECE6DF','inverse-on-surface':'#312E2B','inverse-primary':'#9C5238', + 'outline':'#978E86','outline-variant':'#4A4540','scrim':'#000000', + 'owed':'#A2CCB0','on-owed':'#10331F','owed-container':'#2E4A37','on-owed-container':'#CBE7D3', + 'owing':'#D8B5D6','on-owing':'#3C1F3C','owing-container':'#553A55','on-owing-container':'#F1DDEF', + 'settled':'#E0CA94','settled-container':'#55432E','on-settled-container':'#F1E3C4', + 'warning':'#EDBE8C','on-warning':'#452B0E','warning-container':'#5B3F24','on-warning-container':'#F6DFC6', + 'pending':'#A3C7DE', + 'avatar-1':'#DDB693','on-avatar-1':'#3A230F','avatar-2':'#ADC3DD','on-avatar-2':'#1C2F45', + 'chart-cat-1':'#E3A68E','chart-cat-2':'#ABC79A','chart-cat-3':'#D8BD7E','chart-cat-4':'#9EB9DC','chart-cat-5':'#C8A6D2','chart-cat-6':'#8FC2BC','chart-cat-7':'#E3A6BA','chart-cat-8':'#B5B9BD', + 'chart-seq-1':'#3E2C25','chart-seq-2':'#6A4234','chart-seq-3':'#9A6049','chart-seq-4':'#CC8A70','chart-seq-5':'#E6B5A2', + 'chart-grid':'#36322F','chart-axis-label':'#CDC5BC', + 'card':'#272422','on-cat':'#1B1917','shadow':'rgba(0,0,0,.25)', + 'photo-1':'#C89A78','photo-2':'#8E5238','photo-fg':'#3E1E12', + 'sys-wall-1':'#26242B','sys-wall-2':'#141318','sys-card':'#2B2930','sys-on':'#E6E1E5','sys-on-variant':'#CAC4D0' + } +}; +/* ---------- previous bright palette (git 210ab5c), kept only for the Före / efter section ---------- */ +const CAT_BASE_OLD = { + light: { green:'#2E7D32', orange:'#B84A00', brown:'#795548', yellow:'#8A6A00', blue:'#1565C0', teal:'#00796B', purple:'#7B1FA2', red:'#C62828', pink:'#AD1457', grey:'#5F6B73', indigo:'#3949AB', lime:'#5B6B00' }, + dark: { green:'#81C784', orange:'#FFB74D', brown:'#C8B4AC', yellow:'#F2D45C', blue:'#90CAF9', teal:'#80CBC4', purple:'#D1A0DB', red:'#EF9A9A', pink:'#F48FB1', grey:'#B0BEC5', indigo:'#A5AEE0', lime:'#C5D86D' } +}; +const TOK_OLD = { light: { 'primary':'#B23A1C','on-primary':'#FFFFFF','primary-container':'#FFDBD0','on-primary-container':'#3D0B00', 'secondary':'#4B6A2A','on-secondary':'#FFFFFF','secondary-container':'#D3EFAE','on-secondary-container':'#112000', @@ -378,29 +430,59 @@ const TOK = { 'sys-wall-1':'#26242B','sys-wall-2':'#141318','sys-card':'#2B2930','sys-on':'#E6E1E5','sys-on-variant':'#CAC4D0' } }; -for (const th of ['light', 'dark']) { - for (const [k, v] of Object.entries(CAT_BASE[th])) { - TOK[th]['cat-' + k] = v; - TOK[th]['cat-' + k + '-container'] = mix(v, TOK[th]['surface'], CAT_CONTAINER_MIX[th]); +for (const [T, B] of [[TOK, CAT_BASE], [TOK_OLD, CAT_BASE_OLD]]) { + for (const th of ['light', 'dark']) { + for (const [k, v] of Object.entries(B[th])) { + T[th]['cat-' + k] = v; + T[th]['cat-' + k + '-container'] = mix(v, T[th]['surface'], CAT_CONTAINER_MIX[th]); + } } } document.getElementById('tokens').textContent = ['light', 'dark'].map(th => - `.phone[data-theme="${th}"],.tk[data-theme="${th}"]{` + Object.entries(TOK[th]).map(([k, v]) => `--${k}:${v}`).join(';') + '}').join('\n'); + `.phone[data-theme="${th}"],.tk[data-theme="${th}"]{` + Object.entries(TOK[th]).map(([k, v]) => `--${k}:${v}`).join(';') + '}\n' + + `.phone.old[data-theme="${th}"]{` + Object.entries(TOK_OLD[th]).map(([k, v]) => `--${k}:${v}`).join(';') + '}').join('\n'); + +/* CIEDE2000 between category tile colours (review page) */ +function toLab(h){ + const [r, g, b] = hexToRgb(h).map(v => v / 255).map(v => v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4); + const X = (0.4124 * r + 0.3576 * g + 0.1805 * b) / 0.95047, Y = 0.2126 * r + 0.7152 * g + 0.0722 * b, Z = (0.0193 * r + 0.1192 * g + 0.9505 * b) / 1.08883; + const f = t => t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116; + return [116 * f(Y) - 16, 500 * (f(X) - f(Y)), 200 * (f(Y) - f(Z))]; +} +function de2000(A, B){ + const rad = Math.PI / 180, [L1, a1, b1] = A, [L2, a2, b2] = B; + const Cb = (Math.hypot(a1, b1) + Math.hypot(a2, b2)) / 2, G = 0.5 * (1 - Math.sqrt(Cb ** 7 / (Cb ** 7 + 25 ** 7))); + const a1p = (1 + G) * a1, a2p = (1 + G) * a2, C1 = Math.hypot(a1p, b1), C2 = Math.hypot(a2p, b2); + const h1 = (Math.atan2(b1, a1p) / rad + 360) % 360, h2 = (Math.atan2(b2, a2p) / rad + 360) % 360; + let dh = C1 * C2 === 0 ? 0 : h2 - h1; if (dh > 180) dh -= 360; else if (dh < -180) dh += 360; + const dL = L2 - L1, dC = C2 - C1, dH = 2 * Math.sqrt(C1 * C2) * Math.sin(dh * rad / 2); + const Lb = (L1 + L2) / 2, Cbp = (C1 + C2) / 2; + const hb = C1 * C2 === 0 ? h1 + h2 : Math.abs(h1 - h2) <= 180 ? (h1 + h2) / 2 : (h1 + h2 < 360 ? (h1 + h2 + 360) / 2 : (h1 + h2 - 360) / 2); + const T = 1 - 0.17 * Math.cos((hb - 30) * rad) + 0.24 * Math.cos(2 * hb * rad) + 0.32 * Math.cos((3 * hb + 6) * rad) - 0.2 * Math.cos((4 * hb - 63) * rad); + const dth = 30 * Math.exp(-(((hb - 275) / 25) ** 2)), Rc = 2 * Math.sqrt(Cbp ** 7 / (Cbp ** 7 + 25 ** 7)); + const Sl = 1 + 0.015 * (Lb - 50) ** 2 / Math.sqrt(20 + (Lb - 50) ** 2), Sc = 1 + 0.045 * Cbp, Sh = 1 + 0.015 * Cbp * T, Rt = -Math.sin(2 * dth * rad) * Rc; + return Math.sqrt((dL / Sl) ** 2 + (dC / Sc) ** 2 + (dH / Sh) ** 2 + Rt * (dC / Sc) * (dH / Sh)); +} +function catPairs(base){ + const ks = Object.keys(base), out = []; + for (let i = 0; i < ks.length; i++) for (let j = i + 1; j < ks.length; j++) out.push([de2000(toLab(base[ks[i]]), toLab(base[ks[j]])), ks[i], ks[j]]); + return out.sort((a, b) => a[0] - b[0]); +} /* ---------- type scale ---------- */ const TYPE = [ - ['display-large',57,64,800,-0.25],['display-medium',45,52,800,0],['display-small',36,44,800,0], - ['headline-large',32,40,800,0],['headline-medium',28,36,800,0],['headline-small',24,32,800,0], - ['title-large',22,28,800,0],['title-medium',16,24,700,0.1],['title-small',14,20,800,0.1], - ['body-large',16,24,500,0.2],['body-medium',14,20,500,0.2],['body-small',12,16,600,0.3], - ['label-large',14,20,800,0.1],['label-medium',12,16,700,0.4],['label-small',11,16,700,0.4], - ['amount-hero',44,52,900,-0.5],['amount-large',28,36,900,-0.25],['amount-medium',18,24,800,0],['amount-small',15,20,800,0] + ['display-large',57,64,500,-0.25],['display-medium',45,52,500,0],['display-small',36,44,600,0], + ['headline-large',32,40,600,0],['headline-medium',28,36,600,0],['headline-small',24,32,600,0], + ['title-large',22,28,600,0],['title-medium',16,24,600,0.15],['title-small',14,20,600,0.1], + ['body-large',16,24,400,0.5],['body-medium',14,20,400,0.25],['body-small',12,16,400,0.4], + ['label-large',14,20,600,0.1],['label-medium',12,16,500,0.5],['label-small',11,16,500,0.5], + ['amount-hero',44,52,700,-0.5],['amount-large',28,36,700,-0.25],['amount-medium',18,24,700,0],['amount-small',15,20,600,0] ]; // Android 14+ non-linear font scaling (Compose default): styles of 24 sp and larger grow less than the scale; approximated as at most 1.5x. const CAP = Object.fromEntries(TYPE.filter(t => t[1] >= 24).map(t => [t[0], 1.5])); document.getElementById('type').textContent = TYPE.map(([n, s, lh, w, ls]) => { const f = CAP[n] ? `min(var(--fs),${CAP[n]})` : 'var(--fs)'; - return `.t-${n}{font-size:calc(${f}*${s}px);line-height:calc(${f}*${lh}px);font-weight:${w};letter-spacing:${ls}px${n.startsWith('amount') ? ';font-variant-numeric:tabular-nums;white-space:nowrap' : ''}}`; + return `.t-${n}{font-size:calc(${f}*${s}px);line-height:calc(${f}*${lh}px);font-weight:${w};letter-spacing:${ls}px${n.startsWith('amount') ? ";font-variant-numeric:tabular-nums;font-feature-settings:'tnum';white-space:nowrap" : ''}}`; }).join('\n'); /* ---------- Expressive MaterialShapes approximated as clip-path polygons ---------- */ @@ -717,6 +799,7 @@ function scrAccountEditor(off){ } /* expense editor */ +const autoCap = () => `
${ic('auto_awesome', { size: 16, color: 'secondary' })}Vald utifrån beskrivningen
`; function catQuick(sel, keys, big){ keys = keys || ['groceries', 'restaurants', 'entertainment', 'home', 'transport']; return `
${keys.map(k => ``).join('')}
`; @@ -732,6 +815,7 @@ function scrEditor(fsBig){ const b = `${amountBlock(41200, '412', { caret: true })} ${field({ label: 'Beskrivning', value: 'Systembolaget' })} ${catQuick('entertainment', null, fsBig)} + ${autoCap()}
${chip('Vardag', { dot: 'chart-cat-1', drop: true, aria: 'Konto: Vardag' })}${chip('Idag', { icon: 'event', drop: true, aria: 'Datum: Idag' })}${chip('Du', { k: 'Betalt av', avatar: 'jacob', drop: true })}${chip('Kontots standard 50/50', { k: 'Delning', icon: 'call_split', drop: true })}
${splitBar(50, { thin: true, aria: 'Hälften var' })}
Du ${kr(20600)} · Anna ${kr(20600)}
${btn('Lägg till anteckning', { kind: 'text', icon: 'notes', style: 'align-self:flex-start;margin-left:-8px' })}`; @@ -742,7 +826,7 @@ function splitModes(on){ return `
${m.map((x, i) => chip(x, { sel: i === on })).join('')}
`; } function scrEditorSplit(){ - const compact = `
${catTile('shopping', 36)}
Ullared
Vardag · Idag · Betalt av: Du
${kr(106000)}
`; + const compact = `
${catTile('shopping', 36)}
Ullared
${ic('auto_awesome', { size: 14, color: 'secondary' })}Shopping, vald utifrån beskrivningen
${kr(106000)}
`; const b = `
${chip('Exakta belopp', { k: 'Delning', icon: 'call_split', sel: true, drop: 'up' })}
${splitModes(2)} @@ -755,7 +839,9 @@ function scrEditorSplit(){ function scrEditorFull(){ const b = `${amountBlock(106000, '1' + NB + '060,00', { pad: '4px 0 2px' })} ${field({ label: 'Beskrivning', value: 'SL-kort 30 dagar, Anna' })} -
${chip('Vardag', { dot: 'chart-cat-1', drop: true })}${chip('14 okt.', { icon: 'event', drop: true })}${chip('Du', { k: 'Betalt av', avatar: 'jacob', drop: true })}${chip('Transport', { k: 'Kategori', icon: 'directions_car', drop: true })}
+ ${catQuick('transport')} + ${autoCap()} +
${chip('Vardag', { dot: 'chart-cat-1', drop: true })}${chip('14 okt.', { icon: 'event', drop: true })}${chip('Du', { k: 'Betalt av', avatar: 'jacob', drop: true })}
Delning${ic('expand_less', { color: 'on-surface-variant' })}
${splitModes(3)} @@ -782,7 +868,7 @@ function infoRows(rows, muted){ return `
${rows.map(([icn, lab, val]) => `
${ic(icn, { color: 'on-surface-variant' })}
${lab}
${val}
`).join('')}
`; } function timeline(lines){ - return `
Historik
${lines.map(l => `
${l.pend ? ic('cloud_upload', { size: 16, style: 'vertical-align:-3px;margin-right:4px' }) : ''}${l.t}
`).join('')}
`; + return `
Historik
${lines.map(l => `
${l.pend ? ic('cloud_upload', { size: 16, style: 'vertical-align:-3px;margin-right:4px' }) : ''}${l.t}
`).join('')}
`; } function sharesRow(a, b2, mode){ return `
${mode}${splitBar(Math.round(a / (a + b2) * 100), { thin: true, aria: mode })}Du ${kr(a)} · Anna ${kr(b2)}
`; @@ -972,7 +1058,7 @@ function scrDevices(revoke){ function scrInvites(){ const b = `
${ic('key', { f: true })}Ny enhet för Jacob
Inbjudningskod
-
${['7K3M', 'QX9D', '2HPA'].map(g => `${g}`).join('')}
+
${['7K3M', 'QX9D', '2HPA'].map(g => `${g}`).join('')}
https://icehack.asuscomm.com:8443/i/7K3MQX9D2HPA
${ic('schedule', { size: 18 })}Gäller till tis 20 okt. 14:02
${btn('Dela', { kind: 'tert', icon: 'share' })}${btn('Kopiera kod', { kind: 'outlined', icon: 'content_copy', style: 'color:var(--on-tertiary-container);box-shadow:inset 0 0 0 1px var(--on-tertiary-container)' })}
@@ -1002,24 +1088,24 @@ const FRAMES = [ { id:'S-2-profile-step', title:'S-2 Profilsteg (Anna)', note:'Annas telefon: Jacobs avatar (bild) och Annas initial på avatar-2 i Sunny-form.', themes:LD, render:scrProfileStep }, { id:'S-3-waiting', title:'S-3 Väntar på partner', note:'Gungbrädan med en tom plats: Jacob väger ner sin sida tills partnern sätter sig. Inställningar nås via navigeringen.', themes:LD, render:scrWaiting }, { id:'S-4-home', title:'S-4 Översikt', note:'Saldot som gungbräda: den som är skyldig sitter lägre, plankan har skuldens färg och orden säger riktningen. Varje konto har sin diagramfärg (samma som i Statistik).', themes:LD, render:() => scrHome({}) }, - { id:'S-4-home-offline', title:'S-4 Översikt · offline + uppdatering', note:'Synkraden blir en tonad pill med ikonen cloud_off; tryck öppnar S-16. Uppdateringsbannern är lyktgul (tertiary container).', themes:LD, render:() => scrHome({ offline:true, update:'available' }) }, - { id:'S-4-home-settled', title:'S-4 Översikt · kvitt', note:'Kalas: när ni är kvitt hängs vimpelgirlangen upp och kortet blir lyktgult. Ingen Gör upp-knapp. Arkiverade är utfällt.', themes:LD, render:() => scrHome({ settled:true, archOpen:true }) }, + { id:'S-4-home-offline', title:'S-4 Översikt · offline + uppdatering', note:'Synkraden blir en tonad pill med ikonen cloud_off; tryck öppnar S-16. Uppdateringsbannern är havregul (tertiary container).', themes:LD, render:() => scrHome({ offline:true, update:'available' }) }, + { id:'S-4-home-settled', title:'S-4 Översikt · kvitt', note:'Kalas: när ni är kvitt hängs vimpelgirlangen upp och kortet blir havregult. Ingen Gör upp-knapp. Arkiverade är utfällt.', themes:LD, render:() => scrHome({ settled:true, archOpen:true }) }, { id:'S-4-home-empty', title:'S-4 Översikt · inga konton', note:'Tom-läge med en hög kategoriplattor. Ingen FAB, det finns inget konto att lägga till i.', themes:LD, render:scrHomeEmpty }, { id:'S-4-home-loading', title:'S-4 Översikt · första synken', note:'Expressive LoadingIndicator (formskiftande cookie/sunny; reserv: CircularProgressIndicator) plus skelettkort.', themes:LD, render:scrHomeLoading }, { id:'S-4-home-en', title:'S-4 Home · English', note:'Same data in English: SEK 3,250.00; unit before the number.', themes:L, render:() => scrHome({ lang:'en' }) }, { id:'S-4-home-200', title:'S-4 Översikt · 200 %', note:'fontScale 2, listan scrollad 90 dp (appfältet får surface-container som scrollat) så att första kontot syns helt; FAB:en fälls ihop till en ikon-FAB (TalkBack: Lägg till utgift). Text ≥ 24 sp följer Androids icke-linjära skalning (ca 1,5× i stället för 2×), "kr" bryts till egen rad vid behov; kontobeloppen flyttar till en egen rad under namnet i stället för att kortas.', themes:L, fontScale:2, render:() => scrHome({ big:true }) }, - { id:'S-5-account', title:'S-5 Konto Vardag', note:'Huvudet: namn, 50 / 50 · Jacob och Anna, saldot och Gör upp det här kontot. Menyn visas öppen i S-5-account-menu. Raderna är segmenterade kort; betalningsraden har korall ton och handslagsikon. SL-kort har synkmarkören (moln med pil) vid datumet.', themes:LD, render:() => scrAccount({}) }, + { id:'S-5-account', title:'S-5 Konto Vardag', note:'Huvudet: namn, 50 / 50 · Jacob och Anna, saldot och Gör upp det här kontot. Menyn visas öppen i S-5-account-menu. Raderna är segmenterade kort; betalningsraden har lerröd ton och handslagsikon. SL-kort har synkmarkören (moln med pil) vid datumet.', themes:LD, render:() => scrAccount({}) }, { id:'S-5-account-menu', title:'S-5 Konto Vardag · meny (extra)', note:'Extra ram: överflödesmenyn öppen. Ta bort konto är avstängt och skälet står i menyraden.', themes:LD, render:() => scrAccount({ menu:true }) }, { id:'S-5-account-archived', title:'S-5 Konto Gotland 2026 · arkiverat', note:'Arkiverat-märke, Kvitt, Återställ från arkiv. Ingen FAB: arkiverade konton erbjuds inte för nya utgifter (AC-6.4).', themes:LD, render:() => scrAccount({ archived:true }) }, { id:'S-5-account-archived-menu', title:'S-5 Gotland 2026 · meny (extra)', note:'Extra ram: Ta bort konto är avstängt eftersom kontot har utgifter och en betalning.', themes:LD, render:() => scrAccount({ archived:true, menu:true }) }, { id:'S-6-account-editor', title:'S-6 Nytt konto', note:'Delningen som Expressive-reglage (tjock delad bana, smalt handtag) + två procentfält som alltid summerar till 100 + snabbval i en ButtonGroup.', themes:LD, render:() => scrAccountEditor(false) }, { id:'S-6-account-editor-offline', title:'S-6 Nytt konto · offline', note:'Kräver anslutning: fälten och Spara är avstängda.', themes:LD, render:() => scrAccountEditor(true) }, - { id:'S-7-editor', title:'S-7 Ny utgift · 10 sekunder', note:'Egen siffertangentbordspanel i appen (beskrivningen använder systemtangentbordet). Kategoriraden visar de mest använda; Nöje är förvald (senast använd i Vardag, AC-8.6). Delning-chippet öppnar delningen med ett tryck. Spara i appfältet.', themes:LD, render:() => scrEditor(false) }, - { id:'S-7-editor-split', title:'S-7 Ny utgift · exakta belopp med fel', note:'Beloppet fälls ihop till en kompakt rad när man scrollar. Alla fyra lägen syns som valchips. Annas andel räknas ut och visas skrivskyddad; Spara är avstängd.', themes:LD, render:scrEditorSplit }, - { id:'S-7-editor-full', title:'S-7 Ändra utgift · allt på en person', note:'Allt på Anna: personväljare som ButtonGroup, delningsstapeln helt i Annas färg, anteckningen utfälld, Ta bort i ändringsläget.', themes:LD, render:scrEditorFull }, + { id:'S-7-editor', title:'S-7 Ny utgift · 10 sekunder', note:'Egen siffertangentbordspanel i appen (beskrivningen använder systemtangentbordet). Kategoriraden visar de mest använda; Nöje valdes automatiskt utifrån beskrivningen (Systembolaget) och bildtexten "Vald utifrån beskrivningen" försvinner så fort man väljer kategori själv. Delning-chippet öppnar delningen med ett tryck. Spara i appfältet.', themes:LD, render:() => scrEditor(false) }, + { id:'S-7-editor-split', title:'S-7 Ny utgift · exakta belopp med fel', note:'Beloppet fälls ihop till en kompakt rad när man scrollar; raden visar också den automatiskt valda kategorin. Alla fyra lägen syns som valchips. Annas andel räknas ut och visas skrivskyddad; Spara är avstängd.', themes:LD, render:scrEditorSplit }, + { id:'S-7-editor-full', title:'S-7 Ändra utgift · allt på en person', note:'Transport valdes utifrån beskrivningen (SL-kort). Allt på Anna: personväljare som ButtonGroup, delningsstapeln helt i Annas färg, anteckningen utfälld, Ta bort i ändringsläget.', themes:LD, render:scrEditorFull }, { id:'S-7-editor-200', title:'S-7 Ny utgift · 200 %', note:'fontScale 2: beloppet och Spara syns, mellandelen scrollar, tangentbordet växer.', themes:L, fontScale:2, render:() => scrEditor(true) }, { id:'S-8-expense', title:'S-8 Utgift ICA Maxi Lindhagen', note:'Kategorifärgen bär kortet. Historiken har sista raden "Din ändring väntar på synk" med ihålig punkt i pending-färg.', themes:LD, render:() => scrExpense('normal') }, - { id:'S-8-expense-recurring', title:'S-8 Utgift · återkommande', note:'Återkommande: Hyra som lyktgul etikett.', themes:LD, render:() => scrExpense('recurring') }, + { id:'S-8-expense-recurring', title:'S-8 Utgift · återkommande', note:'Återkommande: Hyra som havregul etikett.', themes:LD, render:() => scrExpense('recurring') }, { id:'S-8-expense-deleted', title:'S-8 Utgift · borttagen', note:'Öppnad från en notis: banner överst, innehållet avfärgat, inga Ändra/Ta bort.', themes:LD, render:() => scrExpense('deleted') }, { id:'S-9-settle', title:'S-9 Gör upp · förslag', note:'Förslaget enligt §9.4: äldsta skulden först. Kvar att fördela 0,00 kr. Vardag-raden förklaras i en tyst info-rad.', themes:LD, render:() => scrSettle('proposal') }, { id:'S-9-settle-adjusted', title:'S-9 Gör upp · justerat', note:'Raden som reglerar mer än skulden får varningsram och varningstext.', themes:LD, render:() => scrSettle('adjusted') }, @@ -1046,9 +1132,9 @@ const FRAMES = [ { id:'S-19-recurring-editor', title:'S-19 Återkommande · Hyra', note:'Samma fält som S-7 utom datum, plus schema.', themes:LD, render:scrRecurringEditor }, { id:'S-20-devices', title:'S-20 Enheter', note:'', themes:LD, render:() => scrDevices(false) }, { id:'S-20-devices-revoke', title:'S-20 Enheter · bekräfta', note:'', themes:LD, render:() => scrDevices(true) }, - { id:'S-21-invites', title:'S-21 Para ihop en enhet', note:'Koden i tre grupper om fyra på lyktgult kort.', themes:LD, render:scrInvites }, + { id:'S-21-invites', title:'S-21 Para ihop en enhet', note:'Koden i tre grupper om fyra på havregult kort.', themes:LD, render:scrInvites }, { id:'S-22-about', title:'S-22 Om Kvitt', note:'', themes:LD, render:scrAbout }, - { id:'N-notifications', title:'Notiser', note:'Ritade neutralt som Android 16-notiser (inte One UI); appikonen i Kvitts korall.', themes:LD, render:scrNotifications } + { id:'N-notifications', title:'Notiser', note:'Ritade neutralt som Android 16-notiser (inte One UI); appikonen i Kvitts lerröda primärfärg.', themes:LD, render:scrNotifications } ]; /* ---------- phone shell ---------- */ @@ -1085,15 +1171,24 @@ function paletteTable(){ return `${P.map(row).join('')}
RoleLightContrastDarkContrast
`; } function catTable(){ - const rows = COLORS12.map(c => `--cat-${c}${['light','dark'].map(th => { const v = TOK[th]['cat-' + c], ct = TOK[th]['cat-' + c + '-container']; return `${v}${ct}${contrastCell(v, TOK[th].surface, 'graphic')} / ${contrastCell(v, TOK[th]['surface-container-high'], 'graphic')} / ${contrastCell(v, ct, 'graphic')} / ${contrastCell(TOK[th]['on-cat'], v, 'text')}`; }).join('')}`).join(''); - return `${rows}
TokenLightLight containervs surface / s-c-high / container / on-cat on itDarkDark containervs surface / s-c-high / container / on-cat on it
`; + const rows = COLORS12.map(c => `--cat-${c}${['light','dark'].map(th => { const v = TOK[th]['cat-' + c], ct = TOK[th]['cat-' + c + '-container']; return `${v}${ct}${contrastCell(v, TOK[th].surface, 'graphic')} / ${contrastCell(v, TOK[th]['surface-container-high'], 'graphic')} / ${contrastCell(v, ct, 'graphic')} / ${contrastCell(TOK[th]['on-cat'], v, 'graphic')}`; }).join('')}`).join(''); + return `${rows}
TokenLightLight containervs surface / s-c-high / container / on-cat icon on it (all graphics, ≥ 3:1)DarkDark containervs surface / s-c-high / container / on-cat icon on it (all graphics, ≥ 3:1)
`; } function chartTable(){ const keys = [...Array(8)].map((_, i) => 'chart-cat-' + (i + 1)).concat([...Array(5)].map((_, i) => 'chart-seq-' + (i + 1)), ['chart-grid', 'chart-axis-label']); return `${keys.map(k => `${['light','dark'].map(th => ``).join('')}`).join('')}
TokenLightvs card (graphics) · on-cat on it (text)Darkvs card · on-cat on it
--${k}${TOK[th][k]}${k.startsWith('chart-cat') ? contrastCell(TOK[th][k], TOK[th].card, 'graphic') + ' · ' + contrastCell(TOK[th]['on-cat'], TOK[th][k], 'text') : k === 'chart-axis-label' ? contrastCell(TOK[th][k], TOK[th].card, 'text') : k.startsWith('chart-seq') ? contrast(TOK[th][k], TOK[th].card).toFixed(2) + ' (sequential ramp: the light end is a background fill that always carries a label; marks use seq-3 to seq-5, all ≥ 3:1)' : 'decor'}
`; } +function deTable(){ + const cell = th => { const pr = catPairs(CAT_BASE[th]), po = catPairs(CAT_BASE_OLD[th]); return `${pr.slice(0, 5).map(([d, a, b]) => `${a}–${b} ${d.toFixed(1)}`).join('
')}${po[0][1]}–${po[0][2]} ${po[0][0].toFixed(1)}`; }; + return `${cell('light')}${cell('dark')}
Closest pairs now (CIEDE2000)Closest pair in the bright version
Light tiles
Dark tiles

A ΔE00 above about 10 is clearly distinguishable side by side; the muted set's closest pairs are further apart than the bright set's, because lightness now differs between neighbouring hues.

`; +} +function beforeAfter(){ + const pick = ['S-4-home', 'S-5-account', 'S-11-analytics'].map(id => FRAMES.find(f => f.id === id)); + const ph = (f, th, old) => `
${`
14:02${ic('signal_cellular_alt', { f: true }) + ic('wifi', { f: true }) + ic('battery_5_bar', { f: true })}
`}
${f.render()}
`; + return pick.map(f => `

${f.title}

${['light', 'dark'].map(th => `
${[true, false].map(old => `
${ph(f, th, old)}
${old ? 'Före (ljus, 210ab5c)' : 'Efter (dämpad)'} · ${th}
`).join('')}
`).join('')}
`).join(''); +} function typeTable(){ - return `${TYPE.map(([n, s, lh, w, ls]) => ``).join('')}
StyleSize (sp)Line heightWeightTrackingSample
${n}${s}${lh}${w}${ls}
${n.startsWith('amount') ? '3' + NB + '250,00' + NB + 'kr' : n.startsWith('display') ? 'Kvitt' : 'Gör upp'}
`; + return `${TYPE.map(([n, s, lh, w, ls]) => ``).join('')}
StyleSize (sp)Line heightWeightTrackingSample
${n}${s}${lh}${w}${ls}
${n.startsWith('amount') ? '3' + NB + '250,00' + NB + 'kr' : n.startsWith('display') ? 'Kvitt' : 'Gör upp'}
`; } function reviewPage(){ const groups = []; @@ -1102,22 +1197,24 @@ function reviewPage(){ document.body.className = 'review'; document.body.innerHTML = `

Kvitt · visual direction 3Kalas

-

A Swedish crayfish party: warm, cheerful cards where category colour carries the screen. Every expense is recognisable by its colour and icon before you read it.

-

Kalas is card-based and colourful without being noisy: cream (light) or warm plum-brown (dark) surfaces, white or tonal cards with very round corners, and one bold colour per thing — the category colour on every expense tile, the account's chart colour on every account card, coral for the one primary action. The balance is a playful two-person seesaw: whoever owes sits lower, the plank takes the colour of the debt, and the words always say who owes whom. When you are quits, bunting goes up and the card turns lantern yellow. Hierarchy stays strict: one hero, one primary button, numbers in heavy tabular Nunito.

+

A Swedish crayfish party, late in the evening: warm, calm cards where category colour still carries the screen. Every expense is recognisable by its colour and icon before you read it.

+

Revision 2026-10-18 (muted). The owner chose Kalas but asked for colours he will not tire of over years of daily use. The structure is unchanged (cards, very round shapes, MaterialShapes tiles, the seesaw, bunting, one solid colour tile per expense); every colour has lower chroma: clay/terracotta primary instead of coral, sage instead of dill, straw/oat instead of lantern yellow, a quieter warm off-white in light and a warm near-neutral brown-black in dark. The typeface is now Roboto (D-020, see Typography). The bright version is in git at 210ab5c and is compared below under Före / efter.

+

Kalas is card-based and colourful without being noisy: off-white (light) or warm brown-black (dark) surfaces, white or tonal cards with very round corners, and one solid colour per thing — the dusty category colour on every expense tile, the account's chart colour on every account card, clay for the one primary action. The balance is a playful two-person seesaw: whoever owes sits lower, the plank takes the colour of the debt, and the words always say who owes whom. When you are quits, bunting goes up and the card turns oat. Hierarchy stays strict: one hero, one primary button, amounts in bold tabular figures.

Direction spec

Colour roles (Material 3 + Kvitt semantic)

-

Contrast is computed in JavaScript from the same token object that themes the phones. Red marks text below 4.5:1 or graphics below 3:1. owed (green, partner owes you) and owing (plum, you owe) are a purple/green pair, distinguishable in deuteranopia, protanopia and tritanopia, and always come with words. settled uses the lantern yellow family. Extras beyond the brief's names: --card (card fill: white on cream in light, surface-container in dark), --on-cat (icon colour on a solid category/chart tile), --shadow, --photo-* (Jacob's picture stand-in), --sys-* (neutral Android notification shade).

+

Contrast is computed in JavaScript from the same token object that themes the phones. Red marks text below 4.5:1 or graphics below 3:1. owed (sage green, partner owes you) and owing (dusty plum, you owe) are a purple/green pair, distinguishable in deuteranopia, protanopia and tritanopia, and always come with words. settled uses the straw/oat family. Extras beyond the brief's names: --card (card fill: white on off-white in light, surface-container in dark), --on-cat (icon colour on a solid category/chart tile), --shadow, --photo-* (Jacob's picture stand-in), --sys-* (neutral Android notification shade). Dark containers for straw/oat and warning are pushed toward brown (hue ≈ 32°) so they never read olive.

${paletteTable()}

Category colours (§9.7 tokens)

-

Baseline values from the brief kept as is: they already pass on the cream and plum surfaces. In Kalas the category colour is used solid as the tile fill with the icon in --on-cat (white in light, plum-black in dark), so every row starts with a bold colour block. The container (--cat-*-container) is the colour at 18 % over surface in light and 24 % in dark; it fills unselected picker tiles (icon in the category colour) and the expense-detail hero card. Category colours are never used for text in light (several are below 4.5:1 on surface-container-high); labels stay on-surface.

+

Muted, dusty versions of all twelve §9.7 tokens (token names unchanged). In Kalas the category colour is used solid as the tile fill with the icon in --on-cat (white in light, brown-black in dark), so every row still starts with a colour block, just a quieter one. The container (--cat-*-container) is the colour at 18 % over surface in light and 24 % in dark; it fills unselected picker tiles (icon in the category colour) and the expense-detail hero card. Category colours are never used as text. To keep muted tiles apart, lightness is spread on purpose (indigo, brown and purple darker; yellow, orange and lime lighter) and every pair is checked with CIEDE2000 below.

+ ${deTable()} ${catTable()}

Chart colours

Accounts take chart-cat-n in created order (Vardag 1, Huset 2, Gotland 2026 3, Japan 2027 4): the same colour is the account's clover tile on Home, its card tint on the account screen and its series in Statistik. Categories use their own category colour in charts. Stacked bar segments are separated by 2 dp gaps so adjacent colours never touch.

${chartTable()}

Typography

-

Nunito (SIL Open Font License 1.1, variable weight 200–1000, bundleable as an Android font resource). A rounded terminal sans that reads friendly at heavy weights, has å/ä/ö with open counters, and whose default figures are tabular (verified: columns of amounts align in the rendered PNGs; Figtree and Plus Jakarta Sans were tested and do not). Amounts use weights 800–900 with font-variant-numeric: tabular-nums (Compose: fontFeatureSettings = "tnum"). All sizes are sp. The 200 % frames follow Android 14+ non-linear font scaling, which Compose applies by default: text of 24 sp and larger grows less than the system scale (approximated here as at most 1.5×, so the 44 sp hero amount becomes about 66 dp, not 88 dp); text below 24 sp scales fully. The hero unit "kr"/"SEK" may wrap to its own line, and nav-bar labels are capped at 1.6× so "Inställningar" fits.

+

Roboto, the Material 3 default typeface. SPEC v0.5 decision D-020: the app bundles no font file and uses the platform default, so there is no licence step and no font resource; this page previews it with Google Fonts Roboto so the owner sees what will ship. Roboto's figures are tabular by default; amounts also set tnum (Compose: fontFeatureSettings = "tnum"), and the columns of amounts in the ledger and breakdown frames align. The friendliness Nunito gave now comes from shape and colour: very round cards and buttons, MaterialShapes tiles and avatars, the seesaw and the bunting. Weights: 600 for titles and labels, 700 for amounts, 400 for body. Note for M6: FontFamily.Default resolves to the system sans-serif, which on a Galaxy with a custom One UI font style may not be Roboto; check on the S26 that amounts stay tabular. All sizes are sp. The 200 % frames follow Android 14+ non-linear font scaling, which Compose applies by default: text of 24 sp and larger grows less than the system scale (approximated here as at most 1.5×, so the 44 sp hero amount becomes about 66 dp, not 88 dp); text below 24 sp scales fully. The hero unit "kr"/"SEK" may wrap to its own line, and nav-bar labels are capped at 1.6× so "Inställningar" fits.

${typeTable()}

Spacing

${[1,2,3,4,5,6,8,10,12].map(n => ``).join('')}${[4,8,12,16,20,24,32,40,48].map(v => ``).join('')}
Token--space-${n}
dp${v}
@@ -1127,9 +1224,9 @@ function reviewPage(){ ${[['--radius-xs','4','progress track ends, chart segments (3)'],['--radius-sm','8','segmented list middle items, connected-button inner corners, menu items (12 → md)'],['--radius-md','12','menu items, warning strips inside cards'],['--radius-lg','16','text fields (all four corners, no indicator line), menus, inner info boxes'],['--radius-lg-inc','20','FAB (56 dp)'],['--radius-xl','28','cards, balance hero, dialogs, keypad sheet top'],['--radius-xl-inc','32','—'],['--radius-xxl','48','—'],['--radius-full','999','buttons, chips, keypad keys, sync pill, badges, nav indicator']].map(r => `${r[0]}${r[1]}${r[2]}`).join('')}

Account cards and segmented groups use 24 outer corners (between lg-inc and xl). Expressive MaterialShapes: Cookie4Sided for category tiles (fallback RoundedCornerShape(30%)), Clover4Leaf for account tiles (fallback RoundedCornerShape(16.dp)), Sunny for large avatars, app mark and dialog icons (fallback CircleShape), Cookie9Sided for the empty/removed illustrations and the loading indicator. Avatars in rows stay circles.

Elevation and motion

-
  • Elevation is tonal first: in light, white cards (--card) sit on cream with a soft warm shadow (0 1 2 and 0 2 8 at 10 % brown); in dark there are no visible shadows, cards are surface-container on surface. FAB and menus at level 3, dialogs at level 3 over a 38 % scrim.
  • +
    • Elevation is tonal first: in light, white cards (--card) sit on off-white with a soft warm shadow (0 1 2 and 0 2 8 at 8 % brown); in dark there are no visible shadows, cards are surface-container on surface. FAB and menus at level 3, dialogs at level 3 over a 38 % scrim.
    • Motion uses MotionScheme.expressive(): spatial springs bounce a little, effect springs never do. Spatial default: damping 0.8, stiffness 380 (≈ 350 ms). Fast spatial: damping 0.6, stiffness 800 (chips, tile selection). Effects: damping 1.0, stiffness 1600 (colour and opacity, ≈ 150 ms).
    • -
    • Seesaw: when the balance changes the plank re-tilts on a bouncy spring (damping 0.55, stiffness 300) and the amount counts to the new value over 400 ms (emphasized decelerate). Reaching zero levels the plank, then the bunting drops in one pennant at a time (40 ms stagger) and the card cross-fades to lantern yellow.
    • +
    • Seesaw: when the balance changes the plank re-tilts on a bouncy spring (damping 0.55, stiffness 300) and the amount counts to the new value over 400 ms (emphasized decelerate). Reaching zero levels the plank, then the bunting drops in one pennant at a time (40 ms stagger) and the card cross-fades to oat.
    • Category tile selection: the tile morphs from container tint to solid and the check badge pops (scale 0 → 1, fast spatial). Saving an expense: the new row slides into its month group with the FAB morphing back.
    • Reduced motion (system "Remove animations"): every spring becomes critically damped, staggered bunting appears at once, counting is skipped.

    Iconography

    @@ -1137,7 +1234,7 @@ function reviewPage(){

    Balance header, three states (real component)

    ${states('light')}
    ${states('dark')}

    Dynamic colour

    -

    Not offered. Kalas's identity is that colour means something: category colours identify expenses, account colours tie Home to Statistik, owed/owing are a checked colour-blind-safe pair, and settled is lantern yellow. A wallpaper-derived scheme would recolour the primary and containers but could not touch those semantic colours, so a Material You variant would end up with a random primary fighting fixed category colours. The cost of offering it is high and the value low for two users.

    +

    Not offered. Kalas's identity is that colour means something: category colours identify expenses, account colours tie Home to Statistik, owed/owing are a checked colour-blind-safe pair, and settled is oat. A wallpaper-derived scheme would recolour the primary and containers but could not touch those semantic colours, so a Material You variant would end up with a random primary fighting fixed category colours. The cost of offering it is high and the value low for two users.

    Material 3 Expressive components and stable fallbacks

    ${[['MaterialShapes (Cookie4Sided, Clover4Leaf, Sunny, Cookie9Sided) via RoundedPolygon.toShape()','category, account and avatar tiles, illustrations','RoundedCornerShape / CircleShape'],['ShortNavigationBar (64 dp)','bottom navigation','NavigationBar (80 dp)'],['ButtonGroup (connected)','Månad/Vecka, Hushållet/Min del, Kategori/Konto, split quick picks, "all on" person picker','SingleChoiceSegmentedButtonRow'],['Expressive Slider (thick track, thin handle)','default split on S-6 (split bar with handle)','Slider with custom track colours'],['LoadingIndicator (shape morph)','first sync on Home','CircularProgressIndicator'],['LinearWavyProgressIndicator','update download','LinearProgressIndicator'],['ExtendedFloatingActionButton, medium size (56 dp, 20 radius) in primary','Lägg till utgift','ExtendedFloatingActionButton'],['Button sizes (small 40 / medium 56) with full shape','all buttons','Button with heightIn'],['Segmented list items (ListItem with grouped shapes)','ledger, settings, devices','Card + ListItem with custom shape']].map(r => ``).join('')}
    ExpressiveWhereStable Material 3 fallback
    ${r[0]}${r[1]}${r[2]}
    @@ -1147,6 +1244,10 @@ function reviewPage(){
  • 200 %: see S-4-home-200 and S-7-editor-200. Account amounts drop below the name instead of truncating; category labels wrap; the hero amount follows non-linear scaling.
  • Extra frame: S-11-analytics-scrolled shows charts 2 and 3 whole, because one 891 dp screen cannot hold three charts.
+

Före / efter

+

The same markup rendered with the previous bright token set (git 210ab5c) and the new muted set. Only colour differs; both use Roboto. These comparison phones are review-only and are not in the frame list or the PNG export.

+ ${beforeAfter()} +

Frames

${groups.map(G => `

${G.g}

${G.items.map(f => `
${f.themes.map(th => `
${phone(f, th)}
${f.title} · ${th}${f.note || ''}
`).join('')}
`).join('')}
`).join('')}
`; diff --git a/docs/ux/proposed-decisions.md b/docs/ux/proposed-decisions.md new file mode 100644 index 0000000..194d054 --- /dev/null +++ b/docs/ux/proposed-decisions.md @@ -0,0 +1,66 @@ +# UX sprint: owner answers, findings and proposed spec changes + +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** | + +## 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 a muted palette (status: being revised; see `directions/3-kalas/`). 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, (2) built-in keyword list, (3) the AC-8.6 default. The user can always change it. 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). +* **Spec changes (to be completed from `category-suggestion.md` when the analysis finishes):** + * AC-8.6: add the suggestion as the first source of the category default. + * New rule section (proposed §9.11 "Category suggestion"): normalisation, history match and tie-break, keyword matching, skipped archived/deleted categories, when the suggestion stops. + * New contract file: the keyword list (proposed path `contracts/category-keywords.csv`, columns `keyword,match,default_key,source`; draft in `docs/ux/category-keywords.csv`), versioned like other contract files. + * §14.2.4 contract change: `ExpenseRepository` gains `suspend fun suggestCategory(description: String): CategoryId?` (computed in `:data` from Room history and the keyword list, with a pure function in `:domain`); the editor calls it on description changes. + * §14.5: the pure function and its test vectors (Kotlin only, like analytics). + * `strings.csv`: `expense_editor_category_suggested` "Vald utifrån beskrivningen" / "Chosen from the description". + +## 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). +* **Decision:** replace the §9.7 hex values with the muted Kalas values (pending; filled in when the revision is approved). Token names unchanged. +* **Spec:** §9.7 table values. + +## 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 id (AC-4.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) | + +## Status + +* U1: delivered (`d27f92e` rebased on `main`), direction picked. +* In progress: muted Kalas revision; keyword list and suggestion analysis from the Splitwise export. +* Next: owner approves the muted palette → U2 `design-spec.md`, renamed `strings.csv`, per-screen PNGs, and the final values for UX-3 and UX-5.