b72ac5bb9e
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>
1354 lines
128 KiB
HTML
1354 lines
128 KiB
HTML
<!doctype html>
|
||
<html lang="sv">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Kvitt · 1 Skärgård</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wdth,wght@8..144,25..151,100..1000&display=block" rel="stylesheet">
|
||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,400,0..1,0&display=block" rel="stylesheet">
|
||
<style id="theme-css"></style>
|
||
<style>
|
||
/* ===================== Review page ===================== */
|
||
html,body{margin:0}
|
||
body{background:#EEF2F4;color:#1B2226;font-family:'Roboto Flex',Roboto,system-ui,sans-serif;font-optical-sizing:auto}
|
||
body.frame-mode{margin:0;background:transparent}
|
||
.rv{max-width:1000px;margin:0 auto;padding:48px 24px 96px}
|
||
.rv h1{font-size:44px;font-stretch:90%;font-weight:700;letter-spacing:-.5px;margin:0 0 4px}
|
||
.rv h2{font-size:26px;font-weight:600;margin:56px 0 12px;padding-top:16px;border-top:1px solid #CBD4D9}
|
||
.rv h3{font-size:18px;font-weight:600;margin:28px 0 8px}
|
||
.rv p,.rv li{font-size:15px;line-height:1.55;max-width:760px}
|
||
.rv .idea{font-size:20px;color:#1A6476;margin:0 0 16px}
|
||
.rv table{border-collapse:collapse;font-size:13px;margin:8px 0 16px;width:100%;max-width:960px}
|
||
.rv th,.rv td{text-align:left;padding:6px 8px;border-bottom:1px solid #D5DDE1;vertical-align:middle}
|
||
.rv th{font-weight:600;color:#40484C}
|
||
.rv td.n{font-variant-numeric:tabular-nums;white-space:nowrap}
|
||
.sw{display:inline-block;width:28px;height:20px;border-radius:6px;border:1px solid rgba(0,0,0,.15);vertical-align:middle;margin-right:6px}
|
||
.bad{color:#B3261E;font-weight:700}
|
||
.ok{color:#1F6B3A}
|
||
.cols{display:flex;flex-wrap:wrap;gap:24px}
|
||
.cols>div{flex:1 1 440px;min-width:0}
|
||
.frames{display:flex;flex-wrap:wrap;gap:32px;margin:12px 0 8px}
|
||
.fr{display:flex;flex-direction:column;gap:8px}
|
||
.fr .cap{font-size:13px;color:#40484C;max-width:411px}
|
||
.fr .cap b{color:#1B2226}
|
||
.rv .phone{border-radius:28px;box-shadow:0 1px 3px rgba(0,0,0,.18),0 10px 28px rgba(16,40,52,.14)}
|
||
.note{font-size:14px;color:#40484C;max-width:860px;margin:4px 0 0}
|
||
.heroes{display:flex;flex-wrap:wrap;gap:16px}
|
||
.heroes .hbox{width:411px;border-radius:28px;overflow:hidden}
|
||
|
||
/* ===================== Phone ===================== */
|
||
.phone,.hbox{--space-1:4px;--space-2:8px;--space-3:12px;--space-4:16px;--space-5:20px;--space-6:24px;--space-8:32px;--space-10:40px;--space-12:48px;--space-16:64px;
|
||
--radius-xs:4px;--radius-sm:8px;--radius-md:12px;--radius-lg:16px;--radius-lg-inc:20px;--radius-xl:28px;--radius-xl-inc:32px;--radius-xxl:48px;--radius-full:9999px}
|
||
.phone{width:411px;height:891px;position:relative;overflow:hidden;display:flex;flex-direction:column;
|
||
background:var(--surface);color:var(--on-surface);font-family:'Roboto Flex',Roboto,system-ui,sans-serif;font-optical-sizing:auto;
|
||
-webkit-font-smoothing:antialiased;text-align:left}
|
||
.phone *{box-sizing:border-box}
|
||
:where(.phone,.hbox) button{font:inherit;color:inherit;border:0;background:none;padding:0;margin:0;cursor:pointer;text-align:inherit}
|
||
.hbox{position:relative;background:var(--surface);color:var(--on-surface);font-family:'Roboto Flex',Roboto,sans-serif;padding:16px}
|
||
.hbox *{box-sizing:border-box}
|
||
.ms{font-family:'Material Symbols Rounded';font-weight:normal;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;
|
||
font-variation-settings:'FILL' 0,'wght' 400,'GRAD' 0,'opsz' 24;-webkit-font-feature-settings:'liga';font-feature-settings:'liga';flex:none;overflow:hidden;width:1em;height:1em}
|
||
.ms.f{font-variation-settings:'FILL' 1,'wght' 400,'GRAD' 0,'opsz' 24}
|
||
.num{font-variant-numeric:tabular-nums;font-feature-settings:'tnum' 1}
|
||
|
||
/* type scale (sp: scaled by --fs; display/headline/hero use Android 14+ non-linear factors --fsh/--fsd/--fsx) */
|
||
.dl{font-size:calc(var(--fsd)*57px);line-height:calc(var(--fsd)*64px);font-weight:400;letter-spacing:-.25px}
|
||
.dm{font-size:calc(var(--fsd)*45px);line-height:calc(var(--fsd)*52px);font-weight:400}
|
||
.ds{font-size:calc(var(--fsd)*36px);line-height:calc(var(--fsd)*44px);font-weight:400}
|
||
.hl{font-size:calc(var(--fsh)*32px);line-height:calc(var(--fsh)*40px);font-weight:500}
|
||
.hm{font-size:calc(var(--fsh)*28px);line-height:calc(var(--fsh)*36px);font-weight:500}
|
||
.hs{font-size:calc(var(--fsh)*24px);line-height:calc(var(--fsh)*32px);font-weight:500}
|
||
.tl{font-size:calc(var(--fs)*22px);line-height:calc(var(--fs)*28px);font-weight:500}
|
||
.tm{font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*24px);font-weight:560;letter-spacing:.15px}
|
||
.ts{font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-weight:560;letter-spacing:.1px}
|
||
.bl{font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*24px);font-weight:400;letter-spacing:.3px}
|
||
.bm{font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-weight:400;letter-spacing:.2px}
|
||
.bs{font-size:calc(var(--fs)*12px);line-height:calc(var(--fs)*16px);font-weight:400;letter-spacing:.3px}
|
||
.ll{font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-weight:560;letter-spacing:.1px}
|
||
.lm{font-size:calc(var(--fs)*12px);line-height:calc(var(--fs)*16px);font-weight:560;letter-spacing:.4px}
|
||
.lsm{font-size:calc(var(--fs)*11px);line-height:calc(var(--fs)*16px);font-weight:560;letter-spacing:.4px}
|
||
.ah{font-size:calc(var(--fsx)*60px);line-height:calc(var(--fsx)*64px);font-weight:720;font-stretch:84%;letter-spacing:-1.2px;font-variant-numeric:tabular-nums}
|
||
.ah .dec{font-size:.5em;font-weight:620;letter-spacing:-.3px;font-stretch:90%}
|
||
.al{font-size:calc(var(--fsh)*32px);line-height:calc(var(--fsh)*40px);font-weight:650;font-stretch:92%;letter-spacing:-.4px;font-variant-numeric:tabular-nums}
|
||
.am{font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*24px);font-weight:620;font-variant-numeric:tabular-nums;letter-spacing:0}
|
||
.as{font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-weight:520;font-variant-numeric:tabular-nums;letter-spacing:.1px}
|
||
.nw{white-space:nowrap}
|
||
.v{color:var(--on-surface-variant)}
|
||
.c-owed{color:var(--owed)} .c-owing{color:var(--owing)} .c-settled{color:var(--settled)} .c-warn{color:var(--warning)} .c-err{color:var(--error)} .c-pend{color:var(--pending)} .c-pri{color:var(--primary)}
|
||
|
||
/* system chrome */
|
||
.sb{height:32px;flex:none;display:flex;align-items:center;justify-content:space-between;padding:0 20px 0 24px;color:var(--on-surface);
|
||
font-size:calc(var(--fs)*14px);font-weight:500;letter-spacing:.2px;position:relative;z-index:3}
|
||
.sb .ic{display:flex;gap:4px;align-items:center}
|
||
.sb .bat{transform:rotate(90deg)}
|
||
.gest{height:24px;flex:none;display:flex;align-items:center;justify-content:center;position:relative;z-index:3}
|
||
.gest i{width:108px;height:4px;border-radius:2px;background:var(--on-surface);opacity:.55}
|
||
.app{flex:1;min-height:0;display:flex;flex-direction:column}
|
||
.scroll{flex:1;min-height:0;overflow:hidden}
|
||
.pad{padding-left:16px;padding-right:16px}
|
||
|
||
/* top app bar */
|
||
.tab{min-height:64px;flex:none;display:flex;align-items:center;gap:4px;padding:0 8px 0 4px}
|
||
.tab .tt{flex:1;min-width:0;padding-left:4px}
|
||
.tab .tt .tl{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.tab .tt.wrap .tl{white-space:normal}
|
||
.tab .acts{display:flex;align-items:center;gap:4px}
|
||
.tab.root{padding-left:16px}
|
||
.ib{width:48px;height:48px;border-radius:50%;display:inline-grid;place-items:center;color:var(--on-surface-variant);flex:none}
|
||
.ib.on{color:var(--on-surface)}
|
||
.ib.ton{background:var(--secondary-container);color:var(--on-secondary-container);width:48px;height:48px}
|
||
|
||
/* short navigation bar (Expressive) */
|
||
.nav{flex:none;min-height:64px;padding:4px 0;background:var(--surface-container);display:flex}
|
||
.nav .it{flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;color:var(--on-surface-variant)}
|
||
.nav .ind{width:56px;height:32px;border-radius:16px;display:grid;place-items:center}
|
||
.nav .it.on .ind{background:var(--secondary-container);color:var(--on-secondary-container)}
|
||
.nav .it.on .lab{color:var(--secondary);font-weight:650}
|
||
.nav .lab{font-size:calc(var(--fs)*12px);line-height:calc(var(--fs)*16px);font-weight:560;letter-spacing:.3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;padding:0 4px}
|
||
.big .nav .lab{font-stretch:72%;letter-spacing:-.2px}
|
||
|
||
/* FAB */
|
||
.fab{position:absolute;right:16px;bottom:104px;z-index:4;display:inline-flex;align-items:center;gap:12px;background:var(--primary-container);color:var(--on-primary-container);
|
||
box-shadow:0 3px 6px rgba(0,0,0,.16),0 1px 2px rgba(0,0,0,.12)}
|
||
.fab.m{min-height:80px;border-radius:20px;padding:0 28px 0 24px}
|
||
.fab.m .lb{font-size:calc(var(--fs)*20px);line-height:calc(var(--fs)*26px);font-weight:600;letter-spacing:0}
|
||
.fab.s{min-height:56px;border-radius:16px;padding:0 20px 0 16px}
|
||
.fab.s .lb{font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*22px);font-weight:600}
|
||
.fab.nonav{bottom:40px}
|
||
|
||
/* buttons */
|
||
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:0 20px;border-radius:999px;font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-weight:600;letter-spacing:.1px;flex:none;white-space:nowrap}
|
||
.btn.m{min-height:56px;padding:0 28px;font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*24px);gap:8px}
|
||
.btn.xs{min-height:32px;padding:0 12px;font-size:calc(var(--fs)*13px)}
|
||
.btn.full{width:100%}
|
||
.btn.wrap{white-space:normal;text-align:center;padding-top:6px;padding-bottom:6px}
|
||
.btn.filled{background:var(--primary);color:var(--on-primary)}
|
||
.btn.tonal{background:var(--secondary-container);color:var(--on-secondary-container)}
|
||
.btn.outl{border:1px solid var(--outline-variant);color:var(--on-surface-variant)}
|
||
.btn.outl.pri{color:var(--primary);border-color:var(--outline)}
|
||
.btn.text{color:var(--primary);padding:0 12px}
|
||
.btn.err{color:var(--error)}
|
||
.btn.errf{background:var(--error);color:var(--on-error)}
|
||
.btn.owing{background:var(--owing);color:var(--on-owing)}
|
||
.btn[disabled],.btn.dis{background:color-mix(in srgb,var(--on-surface) 10%,transparent);color:color-mix(in srgb,var(--on-surface) 38%,transparent);border-color:transparent}
|
||
.btn.text[disabled]{background:none}
|
||
|
||
/* cards, groups */
|
||
.card{background:var(--surface-container-low);border-radius:28px;padding:20px}
|
||
.card.hi{background:var(--surface-container)}
|
||
.grp{display:flex;flex-direction:column;gap:2px}
|
||
.grp>.gi{background:var(--surface-container);border-radius:4px}
|
||
.grp>.gi:first-child{border-top-left-radius:20px;border-top-right-radius:20px}
|
||
.grp>.gi:last-child{border-bottom-left-radius:20px;border-bottom-right-radius:20px}
|
||
.grp.low>.gi{background:var(--surface-container-low)}
|
||
.sec{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:48px;padding:8px 4px 4px 4px}
|
||
.sec .ts{color:var(--on-surface-variant)}
|
||
.li{display:flex;align-items:center;gap:16px;min-height:56px;padding:8px 16px}
|
||
.li .m{flex:1;min-width:0}
|
||
.li .m>*{overflow-wrap:anywhere}
|
||
.li .lead{width:40px;height:40px;border-radius:50%;display:grid;place-items:center;background:var(--secondary-container);color:var(--on-secondary-container);flex:none}
|
||
.li .ch{color:var(--on-surface-variant)}
|
||
.divider{height:1px;background:var(--outline-variant);margin:0 16px}
|
||
|
||
/* chips */
|
||
.chips{display:flex;flex-wrap:wrap;gap:8px}
|
||
.chip{display:inline-flex;align-items:center;gap:8px;min-height:40px;padding:4px 16px 4px 10px;border-radius:999px;border:1px solid var(--outline-variant);
|
||
color:var(--on-surface-variant);font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-weight:560;max-width:100%}
|
||
.chip .ms{color:var(--primary)}
|
||
.chip .cl{color:var(--on-surface-variant);font-weight:450}
|
||
.chip .cv{color:var(--on-surface)}
|
||
.chip.sel{background:var(--secondary-container);border-color:transparent;color:var(--on-secondary-container)}
|
||
.chip.sel .ms,.chip.sel .cl,.chip.sel .cv{color:var(--on-secondary-container)}
|
||
.chip.sm{min-height:32px;padding:2px 12px 2px 8px}
|
||
.chip.dd{padding-right:10px}
|
||
.badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:999px;background:var(--surface-container-highest);color:var(--on-surface-variant);
|
||
font-size:calc(var(--fs)*11px);line-height:calc(var(--fs)*16px);font-weight:600;letter-spacing:.4px;white-space:nowrap}
|
||
.badge.tert{background:var(--tertiary-container);color:var(--on-tertiary-container)}
|
||
.badge.pri{background:var(--primary-container);color:var(--on-primary-container)}
|
||
|
||
/* fields (outlined, large radius) */
|
||
.fld{display:flex;flex-direction:column;gap:4px;min-width:0}
|
||
.fbox{position:relative;min-height:56px;border:1px solid var(--outline);border-radius:16px;display:flex;align-items:center;gap:12px;padding:8px 16px;color:var(--on-surface)}
|
||
.fld.foc .fbox{border:2px solid var(--primary);padding:7px 15px}
|
||
.fld.er .fbox{border:2px solid var(--error);padding:7px 15px}
|
||
.fld.dis .fbox{border-color:color-mix(in srgb,var(--on-surface) 12%,transparent);color:color-mix(in srgb,var(--on-surface) 38%,transparent)}
|
||
.fld.ro .fbox{border-style:dashed;border-color:var(--outline-variant)}
|
||
.flab{position:absolute;left:12px;top:calc(var(--fs)*-8px);padding:0 4px;background:var(--fbg,var(--surface));color:var(--on-surface-variant);
|
||
font-size:calc(var(--fs)*12px);line-height:calc(var(--fs)*16px);font-weight:500;letter-spacing:.3px;white-space:nowrap;max-width:calc(100% - 24px);overflow:hidden;text-overflow:ellipsis}
|
||
.fld.foc .flab{color:var(--primary)} .fld.er .flab{color:var(--error)}
|
||
.fld.dis .flab{color:color-mix(in srgb,var(--on-surface) 38%,transparent)}
|
||
.fval{flex:1;min-width:0;font-size:calc(var(--fs)*16px);line-height:calc(var(--fs)*24px);letter-spacing:.3px;overflow-wrap:anywhere}
|
||
.fval.r{text-align:right}
|
||
.fval.ph{color:var(--on-surface-variant)}
|
||
.caret{display:inline-block;width:2px;height:1.1em;background:var(--primary);vertical-align:-.15em;margin-left:1px;border-radius:1px}
|
||
.fsup{padding:0 16px;font-size:calc(var(--fs)*12px);line-height:calc(var(--fs)*16px);color:var(--on-surface-variant);letter-spacing:.3px}
|
||
.fld.er .fsup{color:var(--error)}
|
||
.fbox .ms{color:var(--on-surface-variant)}
|
||
.fld.er .fbox .ms.tr{color:var(--error)}
|
||
|
||
/* avatars */
|
||
.av{display:inline-grid;place-items:center;flex:none;overflow:hidden;font-weight:650;letter-spacing:0;line-height:1}
|
||
.av svg{width:100%;height:100%;display:block}
|
||
.pair{display:inline-flex;align-items:center}
|
||
.pair .av+.av{margin-left:-10px}
|
||
.pair .ring{border-radius:50%;box-shadow:0 0 0 3px var(--ringc,var(--surface))}
|
||
|
||
/* category icon */
|
||
.ci{width:40px;height:40px;border-radius:12px;display:grid;place-items:center;background:var(--cc);color:var(--c);flex:none}
|
||
.ci.s{width:32px;height:32px;border-radius:10px}
|
||
.ci.l{width:56px;height:56px;border-radius:16px}
|
||
|
||
/* ledger rows */
|
||
.mh{display:flex;align-items:baseline;justify-content:space-between;flex-wrap:wrap;gap:0 8px;padding:16px 16px 6px 20px;color:var(--on-surface-variant)}
|
||
.mh .ts{color:var(--on-surface)}
|
||
.row{display:flex;align-items:center;flex-wrap:wrap;gap:4px 14px;padding:10px 16px;min-height:72px}
|
||
.row .rm{flex:1 1 150px;min-width:0}
|
||
.row .rtop{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 12px}
|
||
.row .rt{flex:1 1 120px;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.row .rs{display:flex;align-items:center;gap:4px;flex-wrap:wrap;color:var(--on-surface-variant)}
|
||
.row .ra{flex:0 0 auto;margin-left:auto;text-align:right}
|
||
.eff-lent{color:var(--owed)} .eff-borrowed{color:var(--owing)}
|
||
.pend{display:inline-flex;align-items:center;gap:2px;color:var(--pending)}
|
||
|
||
/* hero */
|
||
.hero{position:relative;border-radius:32px;background:var(--surface-container);padding:20px 20px 24px;overflow:hidden}
|
||
.hero .top{display:flex;align-items:center;gap:12px;position:relative;z-index:1}
|
||
.hero .word{position:relative;z-index:1;margin-top:16px}
|
||
.hero .amt{position:relative;z-index:1;margin-top:2px;overflow-wrap:anywhere}
|
||
.hero .acts{position:relative;z-index:1;margin-top:16px;display:flex;gap:8px;flex-wrap:wrap}
|
||
.hero .deco{position:absolute;right:-4px;top:-6px;width:200px;height:96px;z-index:0;pointer-events:none}
|
||
.hero.settled .amt{color:var(--settled)}
|
||
|
||
/* banners */
|
||
.banner{border-radius:24px;background:var(--tertiary-container);color:var(--on-tertiary-container);padding:16px 16px 12px 16px}
|
||
.banner .bh{display:flex;gap:12px;align-items:flex-start}
|
||
.banner .ba{display:flex;justify-content:flex-end;gap:8px;margin-top:8px;flex-wrap:wrap}
|
||
.banner .btn.text{color:var(--on-tertiary-container)}
|
||
.banner .btn.filled{background:var(--tertiary);color:var(--on-tertiary)}
|
||
.banner.err{background:var(--error-container);color:var(--on-error-container)}
|
||
.banner.err .btn.filled{background:var(--error);color:var(--on-error)}
|
||
.banner.err .btn.text{color:var(--on-error-container)}
|
||
.banner.warn{background:var(--warning-container);color:var(--on-warning-container)}
|
||
|
||
.syncrow{display:flex;align-items:center;gap:8px;min-height:48px;padding:4px 12px 4px 16px;border-radius:999px;color:var(--on-surface-variant);width:100%}
|
||
.syncrow .t{flex:1;min-width:0}
|
||
.syncrow.off{background:var(--surface-container-high);color:var(--on-surface)}
|
||
.syncrow.off .ms.l{color:var(--pending)}
|
||
|
||
/* overlays */
|
||
.scrim{position:absolute;inset:0;background:color-mix(in srgb,var(--scrim) 40%,transparent);z-index:8}
|
||
.dlg{position:absolute;left:28px;right:28px;top:50%;transform:translateY(-50%);z-index:9;background:var(--surface-container-high);border-radius:28px;padding:24px;--fbg:var(--surface-container-high)}
|
||
.dlg .di{display:grid;place-items:center;color:var(--secondary);margin-bottom:16px}
|
||
.dlg .dt{margin:0 0 16px}
|
||
.dlg .db{color:var(--on-surface-variant)}
|
||
.dlg .da{display:flex;justify-content:flex-end;gap:8px;margin-top:24px;flex-wrap:wrap}
|
||
.menu{position:absolute;z-index:9;background:var(--surface-container);border-radius:16px;padding:8px 0;min-width:200px;
|
||
box-shadow:0 2px 6px rgba(0,0,0,.14),0 6px 20px rgba(0,0,0,.12)}
|
||
.menu .mi{display:flex;align-items:flex-start;gap:12px;min-height:48px;padding:12px 16px;color:var(--on-surface)}
|
||
.menu .mi .ms{color:var(--on-surface-variant)}
|
||
.menu .mi.dis{color:color-mix(in srgb,var(--on-surface) 38%,transparent)}
|
||
.menu .mi.dis .ms{color:color-mix(in srgb,var(--on-surface) 38%,transparent)}
|
||
.menu .mi .sup{color:var(--on-surface-variant);margin-top:2px}
|
||
|
||
/* keypad */
|
||
.kp{flex:none;padding:8px 12px 8px;display:grid;grid-template-columns:repeat(3,1fr);gap:8px;background:var(--surface-container-low)}
|
||
.key{height:64px;border-radius:999px;background:var(--surface-container-highest);color:var(--on-surface);display:grid;place-items:center;
|
||
font-size:calc(var(--fsh)*26px);line-height:1;font-weight:500;font-variant-numeric:tabular-nums}
|
||
.key.sec2{background:var(--secondary-container);color:var(--on-secondary-container);border-radius:20px}
|
||
.key.del{background:var(--tertiary-container);color:var(--on-tertiary-container);border-radius:20px}
|
||
.key.pressed{border-radius:16px;background:var(--primary);color:var(--on-primary)}
|
||
.kp.sm .key{height:52px}
|
||
|
||
/* segmented / button group */
|
||
.bg{display:inline-flex;gap:2px}
|
||
.bg .b{min-height:40px;padding:0 14px;display:inline-flex;align-items:center;justify-content:center;gap:6px;background:var(--surface-container-high);color:var(--on-surface-variant);
|
||
font-size:calc(var(--fs)*14px);line-height:calc(var(--fs)*20px);font-weight:560;border-radius:8px;white-space:nowrap}
|
||
.bg .b:first-child{border-top-left-radius:20px;border-bottom-left-radius:20px}
|
||
.bg .b:last-child{border-top-right-radius:20px;border-bottom-right-radius:20px}
|
||
.bg .b.on{background:var(--secondary);color:var(--on-secondary);border-radius:20px}
|
||
.bg.full{display:flex}.bg.full .b{flex:1}
|
||
|
||
/* misc */
|
||
.tl-list{position:relative;padding-left:4px}
|
||
.tl-it{display:flex;gap:14px;position:relative;padding-bottom:14px}
|
||
.tl-dot{width:12px;height:12px;border-radius:50%;background:var(--outline);margin-top:4px;flex:none;position:relative;z-index:1;box-shadow:0 0 0 4px var(--tlbg,var(--surface))}
|
||
.tl-it:not(:last-child)::before{content:"";position:absolute;left:5px;top:12px;bottom:-2px;width:2px;background:var(--outline-variant)}
|
||
.tl-it.pending .tl-dot{background:var(--pending)}
|
||
.tl-it.first .tl-dot{background:var(--primary)}
|
||
.kv{display:flex;gap:16px;align-items:center;min-height:56px;padding:8px 16px}
|
||
.kv .k{color:var(--on-surface-variant)}
|
||
.sk{background:var(--surface-container-high);border-radius:12px}
|
||
.sw-sel{outline:3px solid var(--on-surface);outline-offset:3px}
|
||
.switch{width:52px;height:32px;border-radius:16px;background:var(--surface-container-highest);border:2px solid var(--outline);position:relative;flex:none}
|
||
.switch::after{content:"";position:absolute;left:6px;top:6px;width:16px;height:16px;border-radius:50%;background:var(--outline)}
|
||
.switch.on{background:var(--primary);border-color:var(--primary)}
|
||
.switch.on::after{left:22px;top:2px;width:24px;height:24px;background:var(--on-primary)}
|
||
.slider{position:relative;height:44px;display:flex;align-items:center}
|
||
.slider .tr{position:absolute;left:0;right:0;height:16px;display:flex;gap:6px;align-items:center}
|
||
.slider .a{height:16px;border-radius:8px 4px 4px 8px;background:var(--primary)}
|
||
.slider .b2{flex:1;height:16px;border-radius:4px 8px 8px 4px;background:var(--secondary-container)}
|
||
.slider .h{width:4px;height:44px;border-radius:2px;background:var(--primary);flex:none}
|
||
.stop{position:absolute;right:4px;width:4px;height:4px;border-radius:50%;background:var(--on-secondary-container)}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<script>
|
||
'use strict';
|
||
/* =====================================================================
|
||
Tokens
|
||
===================================================================== */
|
||
const TOKENS = {
|
||
light: {
|
||
'primary':'#1A6476','on-primary':'#FFFFFF','primary-container':'#C2E8F3','on-primary-container':'#0B4C5C',
|
||
'secondary':'#4E5F69','on-secondary':'#FFFFFF','secondary-container':'#D3E3EE','on-secondary-container':'#374851',
|
||
'tertiary':'#7B5C24','on-tertiary':'#FFFFFF','tertiary-container':'#F7DFB4','on-tertiary-container':'#5A410E',
|
||
'error':'#B3261E','on-error':'#FFFFFF','error-container':'#F9DEDC','on-error-container':'#8C1D18',
|
||
'surface':'#F6FAFC','on-surface':'#171C1F','surface-variant':'#DBE4E8','on-surface-variant':'#40484C',
|
||
'surface-container-lowest':'#FFFFFF','surface-container-low':'#F0F4F7','surface-container':'#EAEFF2',
|
||
'surface-container-high':'#E4E9EC','surface-container-highest':'#DEE3E6','surface-dim':'#D6DBDE','surface-bright':'#F6FAFC',
|
||
'inverse-surface':'#2C3134','inverse-on-surface':'#EDF1F4','inverse-primary':'#8ED0E2',
|
||
'outline':'#70787C','outline-variant':'#BFC8CC','scrim':'#000000',
|
||
'owed':'#00695C','on-owed':'#FFFFFF','owed-container':'#C2EDE4','on-owed-container':'#00443B',
|
||
'owing':'#A33E1C','on-owing':'#FFFFFF','owing-container':'#FFDBCF','on-owing-container':'#792A0B',
|
||
'settled':'#4E5F69','settled-container':'#DCE5EA','on-settled-container':'#36464F',
|
||
'warning':'#855700','on-warning':'#FFFFFF','warning-container':'#FFDEAD','on-warning-container':'#633F00',
|
||
'pending':'#7B5C24',
|
||
'avatar-1':'#C2E8F3','on-avatar-1':'#0B4C5C','avatar-2':'#F2DDB8','on-avatar-2':'#5A400E',
|
||
'cat-green':'#2A732E','cat-orange':'#A84300','cat-brown':'#795548','cat-yellow':'#7D6000','cat-blue':'#1565C0','cat-teal':'#006F62',
|
||
'cat-purple':'#7B1FA2','cat-red':'#C62828','cat-pink':'#AD1457','cat-grey':'#58646C','cat-indigo':'#3949AB','cat-lime':'#5B6B00',
|
||
'chart-cat-1':'#1A6476','chart-cat-2':'#A8741A','chart-cat-3':'#5B5FA8','chart-cat-4':'#4D8559','chart-cat-5':'#A0526E','chart-cat-6':'#2F86A0','chart-cat-7':'#7A6A58','chart-cat-8':'#66737C',
|
||
'chart-seq-1':'#D3ECF3','chart-seq-2':'#9FD3E2','chart-seq-3':'#62AFC5','chart-seq-4':'#2E869E','chart-seq-5':'#165A6B',
|
||
'chart-grid':'#DEE3E6','chart-axis-label':'#40484C'
|
||
},
|
||
dark: {
|
||
'primary':'#8ED0E2','on-primary':'#00363F','primary-container':'#0E4D5C','on-primary-container':'#C2E8F3',
|
||
'secondary':'#B6C9D4','on-secondary':'#20333B','secondary-container':'#374952','on-secondary-container':'#D3E5F0',
|
||
'tertiary':'#E8C48A','on-tertiary':'#422C00','tertiary-container':'#5D4410','on-tertiary-container':'#FFDEA8',
|
||
'error':'#F2B8B5','on-error':'#601410','error-container':'#8C1D18','on-error-container':'#F9DEDC',
|
||
'surface':'#0F1417','on-surface':'#DEE3E6','surface-variant':'#40484C','on-surface-variant':'#BFC8CC',
|
||
'surface-container-lowest':'#0A0F11','surface-container-low':'#171C1F','surface-container':'#1B2124',
|
||
'surface-container-high':'#252B2E','surface-container-highest':'#303639','surface-dim':'#0F1417','surface-bright':'#353A3D',
|
||
'inverse-surface':'#DEE3E6','inverse-on-surface':'#2C3134','inverse-primary':'#1A6476',
|
||
'outline':'#899296','outline-variant':'#40484C','scrim':'#000000',
|
||
'owed':'#7DD8C5','on-owed':'#00382F','owed-container':'#005047','on-owed-container':'#A3F2E0',
|
||
'owing':'#FFB59E','on-owing':'#5E1700','owing-container':'#7E2C0E','on-owing-container':'#FFDBCF',
|
||
'settled':'#B6C9D4','settled-container':'#2C3B43','on-settled-container':'#D3E5F0',
|
||
'warning':'#F5BD62','on-warning':'#452B00','warning-container':'#633F00','on-warning-container':'#FFDEAD',
|
||
'pending':'#E8C48A',
|
||
'avatar-1':'#0E4D5C','on-avatar-1':'#C2E8F3','avatar-2':'#5D4410','on-avatar-2':'#F7DFB4',
|
||
'cat-green':'#81C784','cat-orange':'#FFB74D','cat-brown':'#C8B4AC','cat-yellow':'#F2D45C','cat-blue':'#90CAF9','cat-teal':'#80CBC4',
|
||
'cat-purple':'#D1A0DB','cat-red':'#EF9A9A','cat-pink':'#F48FB1','cat-grey':'#B0BEC5','cat-indigo':'#A5AEE0','cat-lime':'#C5D86D',
|
||
'chart-cat-1':'#8ED0E2','chart-cat-2':'#E8C48A','chart-cat-3':'#B8BCF4','chart-cat-4':'#9BD3A3','chart-cat-5':'#EDA9C1','chart-cat-6':'#5FB6D0','chart-cat-7':'#CDB9A1','chart-cat-8':'#AEBDC8',
|
||
'chart-seq-1':'#1E3A42','chart-seq-2':'#245A68','chart-seq-3':'#2F7C8F','chart-seq-4':'#5EADC3','chart-seq-5':'#A3DDEC',
|
||
'chart-grid':'#303639','chart-axis-label':'#BFC8CC'
|
||
}
|
||
};
|
||
const CAT_TOKENS = ['green','orange','brown','yellow','blue','teal','purple','red','pink','grey','indigo','lime'];
|
||
/* category container rule: 16 % (light) / 22 % (dark) of the category colour over --surface */
|
||
const CAT_MIX = {light:.16, dark:.22};
|
||
|
||
/* ----- colour maths ----- */
|
||
function hex2rgb(h){h=h.replace('#','');return [0,2,4].map(i=>parseInt(h.substr(i,2),16));}
|
||
function rgb2hex(c){return '#'+c.map(v=>Math.round(v).toString(16).padStart(2,'0')).join('').toUpperCase();}
|
||
function mix(a,b,t){const x=hex2rgb(a),y=hex2rgb(b);return rgb2hex(x.map((v,i)=>v*t+y[i]*(1-t)));}
|
||
function lum(h){return hex2rgb(h).map(v=>{v/=255;return v<=0.03928?v/12.92:Math.pow((v+0.055)/1.055,2.4);}).reduce((s,v,i)=>s+v*[0.2126,0.7152,0.0722][i],0);}
|
||
function cr(a,b){const l1=lum(a),l2=lum(b);return (Math.max(l1,l2)+0.05)/(Math.min(l1,l2)+0.05);}
|
||
for(const th of ['light','dark']){ for(const c of CAT_TOKENS){ TOKENS[th]['cat-'+c+'-container']=mix(TOKENS[th]['cat-'+c],TOKENS[th]['surface'],CAT_MIX[th]); } }
|
||
|
||
/* theme CSS from tokens */
|
||
(function(){
|
||
let css='';
|
||
for(const th of ['light','dark']){
|
||
const sel=`.phone[data-theme="${th}"],.hbox[data-theme="${th}"]`;
|
||
css+=sel+'{'+Object.entries(TOKENS[th]).map(([k,v])=>`--${k}:${v}`).join(';')+'}\n';
|
||
}
|
||
document.getElementById('theme-css').textContent=css;
|
||
})();
|
||
|
||
/* =====================================================================
|
||
Formatting helpers
|
||
===================================================================== */
|
||
const NB=' ';
|
||
function grp(n,sep){return String(n).replace(/\B(?=(\d{3})+(?!\d))/g,sep);}
|
||
function krn(ore){ore=Math.abs(Math.round(ore));return grp(Math.floor(ore/100),NB)+','+String(ore%100).padStart(2,'0');}
|
||
function kr(ore){return krn(ore)+NB+'kr';}
|
||
function sek(ore){ore=Math.abs(Math.round(ore));return 'SEK'+NB+grp(Math.floor(ore/100),',')+'.'+String(ore%100).padStart(2,'0');}
|
||
function pct1(part,total){ // half-up to one decimal (AC-14.10), integer maths on öre
|
||
const x=Math.floor((part*2000+total)/(2*total)); // round(part*1000/total)
|
||
return String(Math.floor(x/10))+','+(x%10)+NB+'%';
|
||
}
|
||
/* non-linear font scaling (Android 14+ FontScaleConverter table for 200 %), used for text >= 24 sp only */
|
||
const NL2=[[8,16],[10,20],[12,24],[14,26],[18,30],[20,34],[24,36],[30,38],[100,100]];
|
||
function nlf(sp,fs){ if(fs<=1) return 1; let out=sp*2;
|
||
for(let i=0;i<NL2.length-1;i++){const [a,A]=NL2[i],[b,B]=NL2[i+1]; if(sp>=a&&sp<=b){out=A+(B-A)*(sp-a)/(b-a);break;}}
|
||
if(sp>100) out=sp; const f2=out/sp; return +(1+(f2-1)*(fs-1)).toFixed(3); }
|
||
|
||
/* =====================================================================
|
||
Shapes (Material 3 Expressive MaterialShapes, approximated as masks)
|
||
===================================================================== */
|
||
function polarPath(n,amp,rot){const pts=[];const N=240;for(let i=0;i<N;i++){const t=i/N*Math.PI*2;const r=46*(1+amp*Math.cos(n*(t+rot)));pts.push([50+r*Math.cos(t),50+r*Math.sin(t)]);}
|
||
return 'M'+pts.map(p=>p[0].toFixed(2)+' '+p[1].toFixed(2)).join('L')+'Z';}
|
||
function maskUrl(d){return `url('data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><path d='${d}'/></svg>`).replace(/'/g,'%27')}')`;}
|
||
const SHAPE={ cookie9: maskUrl(polarPath(9,0.065,0)), cookie12: maskUrl(polarPath(12,0.045,0)), clover4: maskUrl(polarPath(4,0.16,Math.PI/4)) };
|
||
function shp(name){return `-webkit-mask:${SHAPE[name]} center/100% 100% no-repeat;mask:${SHAPE[name]} center/100% 100% no-repeat;`;}
|
||
|
||
/* =====================================================================
|
||
Primitive components
|
||
===================================================================== */
|
||
const esc=s=>String(s).replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"');
|
||
function I(n,o={}){const s=o.s||24;return `<span class="ms${o.f?' f':''}${o.c?' '+o.c:''}" style="font-size:${s}px${o.st?';'+o.st:''}"${o.l?` role="img" aria-label="${esc(o.l)}"`:' aria-hidden="true"'}>${n}</span>`;}
|
||
function IB(icon,label,cls='',o={}){return `<button class="ib ${cls}" aria-label="${esc(label)}">${I(icon,o)}</button>`;}
|
||
/* avatars: who = 'J' (picture stand-in) or 'A' (initials on --avatar-2) */
|
||
function av(who,size,shape){
|
||
const sh = shape ? shp(shape) : 'border-radius:50%;';
|
||
const fsz = Math.round(size*0.42);
|
||
if(who==='A') return `<span class="av" role="img" aria-label="Anna" style="width:${size}px;height:${size}px;${sh}background:var(--avatar-2);color:var(--on-avatar-2);font-size:min(calc(var(--fs)*${fsz}px),${Math.round(size*0.6)}px)">A</span>`;
|
||
if(who==='J?') return `<span class="av" role="img" aria-label="Din partner" style="width:${size}px;height:${size}px;${sh}background:var(--surface-container-highest);color:var(--on-surface-variant);border:2px dashed var(--outline)">${I('person_add',{s:Math.round(size*0.45)})}</span>`;
|
||
return `<span class="av" role="img" aria-label="Jacob" style="width:${size}px;height:${size}px;${sh}background:linear-gradient(165deg,var(--tertiary-container) 0%,var(--primary-container) 52%,var(--primary) 100%)"><svg viewBox="0 0 40 40" aria-hidden="true"><circle cx="20" cy="16.5" r="6.6" style="fill:var(--on-primary-container)"/><path d="M6.5 41c0-9 6-13.6 13.5-13.6S33.5 32 33.5 41z" style="fill:var(--on-primary-container)"/></svg></span>`;
|
||
}
|
||
function pairAv(size,shape,ring){return `<span class="pair" style="--ringc:var(${ring||'--surface'})">${av('J',size,shape)}${av('A',size,shape)}</span>`;}
|
||
function catIc(key,cls=''){const c=CAT[key];return `<span class="ci ${cls}" style="--c:var(--cat-${c.c});--cc:var(--cat-${c.c}-container)">${I(c.icon,{f:1,s:cls==='s'?18:(cls==='l'?28:22)})}</span>`;}
|
||
function btn(label,cls='filled',icon,o={}){return `<button class="btn ${cls}"${o.dis?' disabled':''}${o.al?` aria-label="${esc(o.al)}"`:''}>${icon?I(icon,{s:cls.includes(' m')?24:18}):''}${label}</button>`;}
|
||
function field(o){
|
||
const cls=['fld',o.foc?'foc':'',o.err?'er':'',o.dis?'dis':'',o.ro?'ro':''].join(' ');
|
||
const sup=o.err?`<div class="fsup">${o.err}</div>`:(o.sup?`<div class="fsup">${o.sup}</div>`:'');
|
||
return `<div class="${cls}" style="${o.st||''}"><div class="fbox">${o.label?`<span class="flab">${o.label}</span>`:''}${o.lead?I(o.lead,{s:22}):''}<span class="fval${o.r?' r':''}${o.ph?' ph':''}${o.num?' num':''}" ${o.vcls?`style="${o.vcls}"`:''}>${o.value||''}${o.foc&&!o.nocaret?'<span class="caret"></span>':''}</span>${o.trail||''}${o.err?I('error',{f:1,s:22,c:'tr'}):''}</div>${sup}</div>`;
|
||
}
|
||
function chip(o){return `<button class="chip${o.sel?' sel':''}${o.sm?' sm':''}${o.dd?' dd':''}"${o.al?` aria-label="${esc(o.al)}"`:''}>${o.lead||''}${o.icon?I(o.icon,{s:18,f:o.f}):''}<span>${o.k?`<span class="cl">${o.k} </span>`:''}<span class="cv">${o.v}</span></span>${o.dd?I(o.ddi||'arrow_drop_down',{s:18}):''}</button>`;}
|
||
function statusBar(){return `<div class="sb" style="background:var(--sbbg)"><span class="num">14:02</span><span class="ic">${I('signal_cellular_alt',{s:17})}${I('wifi',{s:17,f:1})}${I('battery_full',{s:17,f:1,c:'bat'})}</span></div>`;}
|
||
function appBar(o){
|
||
return `<header class="tab${o.root?' root':''}" style="${o.bg?`background:var(--${o.bg})`:''}">${o.nav?IB(o.nav,o.navL||'Tillbaka','on'):''}<div class="tt${o.wrap?' wrap':''}"><div class="tl">${o.title||''}</div>${o.sub?`<div class="bm v">${o.sub}</div>`:''}</div><div class="acts">${o.acts||''}</div></header>`;
|
||
}
|
||
function navBar(active,en){
|
||
const items=[['home','Översikt','Home'],['bar_chart','Statistik','Analytics'],['settings','Inställningar','Settings']];
|
||
return `<nav class="nav" aria-label="${en?'Navigation':'Navigering'}">${items.map(([ic,sv,e],i)=>`<button class="it${i===active?' on':''}"><span class="ind">${I(ic,{f:i===active})}</span><span class="lab">${en?e:sv}</span></button>`).join('')}</nav>`;
|
||
}
|
||
function fab(label,o={}){return `<button class="fab ${o.s?'s':'m'}${o.nonav?' nonav':''}"${o.st?` style="${o.st}"`:''}>${I('add',{s:o.s?24:28})}<span class="lb">${label}</span></button>`;}
|
||
function kvittMark(size){return `<span style="width:${size}px;height:${size}px;${shp('cookie12')}background:var(--primary);display:inline-grid;place-items:center;flex:none"><svg viewBox="0 0 40 40" width="${size*0.62}" height="${size*0.62}" aria-hidden="true"><path d="M7 23c4-4 7-4 11 0s7 4 11 0 5-3 5-3" style="fill:none;stroke:var(--on-primary);stroke-width:3.4;stroke-linecap:round"/><path d="M7 15c4-4 7-4 11 0s7 4 11 0 5-3 5-3" style="fill:none;stroke:var(--on-primary);stroke-width:3.4;stroke-linecap:round;opacity:.6"/></svg></span>`;}
|
||
function islands(){ /* decorative archipelago in the balance hero: granite islets and a calm swell */
|
||
return `<svg class="deco" viewBox="0 0 210 120" preserveAspectRatio="xMaxYMin meet" aria-hidden="true">
|
||
<path d="M96 88c10-17 24-26 38-26 16 0 22 12 34 14 12 2 22-6 42-4v40H96z" style="fill:var(--surface-container-highest)"/>
|
||
<path d="M150 60c6-12 15-18 24-18 10 0 14 8 22 8 6 0 10-3 14-3v36h-62z" style="fill:var(--surface-container-high)"/>
|
||
<path d="M34 104c10-6 20-6 30 0s20 6 30 0 20-6 30 0 20 6 30 0 20-6 30 0 20 6 26 2" style="fill:none;stroke:var(--outline-variant);stroke-width:2;stroke-linecap:round"/>
|
||
<path d="M74 116c10-6 20-6 30 0s20 6 30 0 20-6 30 0 20 6 30 0" style="fill:none;stroke:var(--outline-variant);stroke-width:2;stroke-linecap:round;opacity:.6"/>
|
||
</svg>`;
|
||
}
|
||
function scroll(inner,st=''){return `<div class="scroll" style="${st}">${inner}</div>`;}
|
||
|
||
/* =====================================================================
|
||
Shared data (brief §5)
|
||
===================================================================== */
|
||
const CAT={
|
||
groceries:{sv:'Mat & hushåll',en:'Groceries',icon:'shopping_cart',c:'green'},
|
||
restaurants:{sv:'Restaurang & café',en:'Restaurants',icon:'restaurant',c:'orange'},
|
||
home:{sv:'Hem',en:'Home',icon:'home',c:'brown'},
|
||
utilities:{sv:'Räkningar',en:'Utilities',icon:'bolt',c:'yellow'},
|
||
transport:{sv:'Transport',en:'Transport',icon:'directions_car',c:'blue'},
|
||
travel:{sv:'Resor',en:'Travel',icon:'flight',c:'teal'},
|
||
entertainment:{sv:'Nöje',en:'Entertainment',icon:'movie',c:'purple'},
|
||
health:{sv:'Hälsa',en:'Health',icon:'medical_services',c:'red'},
|
||
shopping:{sv:'Shopping',en:'Shopping',icon:'shopping_bag',c:'pink'},
|
||
other:{sv:'Övrigt',en:'Other',icon:'category',c:'grey'},
|
||
pant:{sv:'Pant',icon:'water_drop',c:'lime'}
|
||
};
|
||
const ACCOUNTS=[
|
||
{name:'Vardag',net:118450,oct:11,arch:false,split:'50 / 50'},
|
||
{name:'Huset',net:-308450,oct:4,arch:false,split:'60 / 40'},
|
||
{name:'Gotland 2026',net:0,oct:0,arch:true,split:'50 / 50'},
|
||
{name:'Japan 2027',net:-135000,oct:2,arch:false,split:'50 / 50'}
|
||
];
|
||
const GLOBAL_NET=ACCOUNTS.reduce((s,a)=>s+a.net,0); // -325000
|
||
// effect: ['lent',ore] ['borrowed',ore] ['none'] ['self']
|
||
const VARDAG=[
|
||
{m:'Oktober',total:745790,rows:[
|
||
{d:'16 okt.',t:'ICA Maxi Lindhagen',c:'groceries',a:124760,e:['borrowed',62380]},
|
||
{d:'15 okt.',t:'Systembolaget',c:'entertainment',a:41200,e:['lent',20600]},
|
||
{d:'14 okt.',t:'SL-kort 30 dagar, Anna',c:'transport',a:106000,e:['lent',106000],p:1},
|
||
{d:'12 okt.',t:'Willys Hornstull',c:'groceries',a:86435,e:['lent',43217]},
|
||
{d:'10 okt.',t:'Babette',c:'restaurants',a:118000,e:['borrowed',59000]},
|
||
{d:'9 okt.',t:'Apotek Hjärtat',c:'health',a:18990,e:['self']},
|
||
{d:'7 okt.',t:'ICA Nära Hornstull',c:'groceries',a:78560,e:['borrowed',39280]},
|
||
{d:'6 okt.',settle:'Anna betalade dig',a:80000},
|
||
{d:'4 okt.',t:'Clas Ohlson',c:'home',a:45900,e:['borrowed',22950]},
|
||
{d:'3 okt.',t:'Netflix',c:'entertainment',a:17900,e:['lent',8950]},
|
||
{d:'2 okt.',t:'Pressbyrån',c:'restaurants',a:6800,e:['none']},
|
||
{d:'1 okt.',t:'Lidl Liljeholmen',c:'groceries',a:101245,e:['lent',50622]}]},
|
||
{m:'September',total:941240,rows:[
|
||
{d:'28 sep.',t:'Coop Södermalm',c:'groceries',a:73420,e:['borrowed',36710]},
|
||
{d:'26 sep.',t:'Hemköp',c:'groceries',a:51280,e:['lent',25640]}]}
|
||
];
|
||
const GOTLAND=[
|
||
{m:'Augusti',total:0,rows:[{d:'3 aug.',settle:'Du betalade Anna',a:35000}]},
|
||
{m:'Juli',total:438000,rows:[
|
||
{d:'18 jul.',t:'Clarion Hotel Wisby, 3 nätter',c:'travel',a:254000,e:['borrowed',127000]},
|
||
{d:'15 jul.',t:'Destination Gotland, färja t/r',c:'travel',a:184000,e:['lent',92000]}]}
|
||
];
|
||
function effText(e,en){
|
||
if(e[0]==='lent') return `<span class="eff-lent">${en?'You lent '+sek(e[1]):'Du lånade ut '+kr(e[1])}</span>`;
|
||
if(e[0]==='borrowed') return `<span class="eff-borrowed">${en?'You borrowed '+sek(e[1]):'Du lånade '+kr(e[1])}</span>`;
|
||
if(e[0]==='none') return 'Inte inblandad';
|
||
return 'Betalade för dig själv';
|
||
}
|
||
function ledgerRow(r){
|
||
const pend = r.p?`<span class="pend" role="img" aria-label="Väntar på synk">${I('cloud_upload',{s:16})}</span>`:'';
|
||
if(r.settle) return `<div class="row"><span class="ci" style="--c:var(--settled);--cc:var(--settled-container)">${I('handshake',{f:1,s:22})}</span><div class="rm"><div class="tm" style="overflow-wrap:anywhere">${r.settle} <span class="num nw">${kr(r.a)}</span></div><div class="rs bm">${pend}${r.d} · Betalning</div></div></div>`;
|
||
return `<div class="row">${catIc(r.c)}<div class="rm"><div class="rtop"><span class="tm rt">${r.t}</span><span class="ra am">${kr(r.a)}</span></div><div class="rs bm">${pend}<span class="nw">${r.d}</span><span aria-hidden="true">·</span>${effText(r.e)}</div></div></div>`;
|
||
}
|
||
function monthHead(m,total){return `<div class="mh" aria-label="${m}${total?' · '+kr(total):''}"><span class="ts">${m}</span><span class="as num">${total?kr(total):''}</span></div>`;}
|
||
|
||
/* =====================================================================
|
||
Balance hero (S-4 header): owing / owed / settled
|
||
===================================================================== */
|
||
function heroAmount(ore,en){
|
||
if(en){const s=sek(ore).replace('SEK'+NB,'');const i=s.lastIndexOf('.');return `<span class="dec">SEK${NB}</span><span>${s.slice(0,i)}</span><span class="dec">${s.slice(i)}</span>`;}
|
||
const s=krn(ore);const i=s.indexOf(',');return `<span>${s.slice(0,i)}</span><span class="dec">${s.slice(i)}${NB}kr</span>`;
|
||
}
|
||
function hero(state,ore,o={}){
|
||
const en=o.en;
|
||
let word,amt,cls,act='';
|
||
if(state==='owing'){word=en?'You owe Anna':'Du är skyldig Anna';amt=`<div class="amt ah c-owing" aria-label="${en?sek(ore):kr(ore)}">${heroAmount(ore,en)}</div>`;cls='owing';
|
||
act=`<div class="acts">${btn(en?'Settle up':'Gör upp','filled m',null)}</div>`;}
|
||
else if(state==='owed'){word=en?'Anna owes you':'Anna är skyldig dig';amt=`<div class="amt ah c-owed" aria-label="${en?sek(ore):kr(ore)}">${heroAmount(ore,en)}</div>`;cls='owed';
|
||
act=`<div class="acts">${btn(en?'Settle up':'Gör upp','filled m',null)}</div>`;}
|
||
else {word=en?'Jacob and Anna':'Jacob och Anna';amt=`<div class="amt ds" style="font-weight:700;font-stretch:88%;letter-spacing:-.6px;display:flex;align-items:center;gap:10px;flex-wrap:wrap"><span style="${shp('cookie9')}background:var(--settled);color:var(--surface);width:44px;height:44px;display:inline-grid;place-items:center;flex:none">${I('check',{s:26})}</span>${en?'All settled up':'Ni är kvitt'}</div>`;cls='settled';}
|
||
if(cls==='settled') return `<section class="hero settled" aria-label="${en?'Balance':'Saldo'}">${islands()}
|
||
<div class="top">${pairAv(44,'cookie9','--surface-container')}<div class="tm" style="color:var(--on-surface-variant)">${word}</div></div>${amt.replace('class="amt ds"','class="amt ds" ').replace('display:flex;','display:flex;margin-top:14px;')}</section>`;
|
||
return `<section class="hero ${cls}" aria-label="${en?'Balance':'Saldo'}">${islands()}
|
||
<div class="top">${pairAv(44,'cookie9','--surface-container')}</div>
|
||
<div class="word tl" style="color:var(--on-surface)">${word}</div>${amt}${act}</section>`;
|
||
}
|
||
|
||
/* =====================================================================
|
||
S-4 Home
|
||
===================================================================== */
|
||
const EN={konton:'Accounts',ny:'New account',arch:n=>`Archived (${n})`,cnt:n=>`${n} expense${n===1?'':'s'} in October`,sync:'Synced 3 min ago',fab:'Add expense',
|
||
owes:o=>['Anna owes you',sek(o)],owe:o=>['You owe Anna',sek(o)],settled:'Settled up'};
|
||
const SV={konton:'Konton',ny:'Nytt konto',arch:n=>`Arkiverade (${n})`,cnt:n=>`${n} ${n===1?'utgift':'utgifter'} i oktober`,sync:'Synkad för 3 min sedan',fab:'Lägg till utgift',
|
||
owes:o=>['Anna är skyldig dig',kr(o)],owe:o=>['Du är skyldig Anna',kr(o)],settled:'Kvitt'};
|
||
function accItem(a,L,settledAll){
|
||
const net=settledAll?0:a.net;
|
||
let right;
|
||
if(net>0){const [w,v]=L.owes(net);right=`<div class="ab"><div class="lm c-owed">${w}</div><div class="am c-owed">${v}</div></div>`;}
|
||
else if(net<0){const [w,v]=L.owe(-net);right=`<div class="ab"><div class="lm c-owing">${w}</div><div class="am c-owing">${v}</div></div>`;}
|
||
else right=`<div class="ab"><div class="lm c-settled" style="display:flex;align-items:center;gap:4px;justify-content:flex-end">${I('check',{s:16})}${L.settled}</div></div>`;
|
||
return `<button class="gi" style="display:flex;flex-wrap:wrap;align-items:center;gap:6px 14px;padding:14px 16px 14px 14px;min-height:72px;width:100%">
|
||
<span style="width:44px;height:44px;border-radius:14px;background:var(--secondary-container);color:var(--on-secondary-container);display:grid;place-items:center;flex:none" class="tm" aria-hidden="true">${a.name[0]}</span>
|
||
<span style="flex:1 1 120px;min-width:0"><span class="tm" style="display:block;overflow-wrap:anywhere">${a.name}${a.arch?` <span class="badge" style="vertical-align:2px">Arkiverat</span>`:''}</span><span class="bs v" style="display:block">${a.arch&&!a.oct?'Inga utgifter i oktober':L.cnt(a.oct)}</span></span>
|
||
<span style="flex:0 1 auto;margin-left:auto;text-align:right">${right}</span></button>`;
|
||
}
|
||
function home(o={}){
|
||
const en=!!o.en; const L=en?EN:SV;
|
||
const settled=o.state==='settled';
|
||
const active=ACCOUNTS.filter(a=>!a.arch), archived=ACCOUNTS.filter(a=>a.arch);
|
||
let banner='';
|
||
if(o.banner==='update') banner=`<div class="banner" style="margin:0 16px 12px">${`<div class="bh">${I('system_update',{s:24})}<div style="flex:1"><div class="ts">Version 1.2 finns</div><div class="bm">Ny version av Kvitt · 18,4 MB</div></div></div><div class="ba">${btn('Senare','text')}${btn('Uppdatera','filled')}</div>`}</div>`;
|
||
if(o.banner==='progress') banner=`<div class="banner" style="margin:0 16px 12px"><div class="bh">${I('downloading',{s:24})}<div style="flex:1;min-width:0"><div class="ts">Version 1.2 finns</div><div class="bm">Laddar ner… 64${NB}%</div>
|
||
<svg viewBox="0 0 300 12" style="width:100%;height:12px;margin-top:10px;display:block" role="progressbar" aria-label="Laddar ner, 64 procent" aria-valuenow="64"><path d="${wave(2,186,6,5,6)}" style="fill:none;stroke:var(--tertiary);stroke-width:4;stroke-linecap:round"/><path d="M196 6H296" style="stroke:var(--surface-container-lowest);stroke-width:4;stroke-linecap:round;opacity:.7"/><circle cx="296" cy="6" r="2" style="fill:var(--tertiary)"/></svg></div></div></div>`;
|
||
if(o.banner==='failed') banner=`<div class="banner err" style="margin:0 16px 12px"><div class="bh">${I('error',{s:24,f:1})}<div style="flex:1"><div class="ts">Version 1.2 finns</div><div class="bm">Nedladdningen misslyckades. Försök igen.</div></div></div><div class="ba">${btn('Senare','text')}${btn('Försök igen','filled')}</div></div>`;
|
||
const sync = o.offline
|
||
? `<button class="syncrow off" aria-label="Synkstatus: Offline, 2 ändringar väntar på synk">${I('cloud_off',{s:20,c:'l'})}<span class="t bm">Offline · 2 ändringar väntar på synk</span>${I('chevron_right',{s:20})}</button>`
|
||
: `<button class="syncrow">${I('cloud_done',{s:20})}<span class="t bm">${L.sync}</span>${I('chevron_right',{s:20})}</button>`;
|
||
let body;
|
||
if(o.state==='empty'){
|
||
body=`<div class="pad" style="padding-top:4px">
|
||
<section class="hero settled" style="padding-bottom:20px">${islands()}<div class="top">${pairAv(44,'cookie9','--surface-container')}<div><div class="tm">Jacob och Anna</div><div class="bm v">Ni är båda med</div></div></div></section>
|
||
<div style="padding:${o.fsBig?20:40}px 12px 0;text-align:center;display:flex;flex-direction:column;align-items:center">
|
||
<div style="position:relative;width:132px;height:112px;margin-bottom:20px" aria-hidden="true">
|
||
<span style="position:absolute;left:0;top:18px;width:84px;height:84px;${shp('clover4')}background:var(--primary-container)"></span>
|
||
<span style="position:absolute;right:0;top:0;width:68px;height:68px;${shp('cookie9')}background:var(--tertiary-container)"></span>
|
||
<span style="position:absolute;left:30px;top:44px;width:48px;height:48px;border-radius:16px;background:var(--primary);color:var(--on-primary);display:grid;place-items:center">${I('account_balance_wallet',{s:26,f:1})}</span>
|
||
</div>
|
||
<div class="hs">Skapa ert första konto</div>
|
||
<p class="bm v" style="margin:8px 0 24px;max-width:320px">Ett konto samlar det ni delar på, till exempel Vardag, Huset eller en resa. Ni väljer hur kostnaderna delas.</p>
|
||
${btn('Nytt konto','filled m','add')}
|
||
</div></div>`;
|
||
} else if(o.state==='loading'){
|
||
body=`<div class="pad" style="padding-top:4px">
|
||
<div class="hero" style="height:200px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:16px">
|
||
<span style="width:48px;height:48px;border-radius:50%;background:var(--primary-container);display:grid;place-items:center" role="progressbar" aria-label="Hämtar era utgifter">
|
||
<span style="width:34px;height:34px;${shp('cookie9')}background:var(--primary);transform:rotate(18deg)"></span></span>
|
||
<div class="tm">Hämtar era utgifter…</div>
|
||
<div class="bm v" style="text-align:center;max-width:280px">Första synken efter parning kan ta en liten stund.</div>
|
||
</div>
|
||
<div class="sec"><span class="ts">Konton</span></div>
|
||
<div class="grp">${[0,1,2].map(()=>`<div class="gi" style="display:flex;gap:14px;align-items:center;padding:14px;min-height:72px"><span class="sk" style="width:44px;height:44px;border-radius:14px"></span><span style="flex:1"><span class="sk" style="display:block;width:46%;height:14px;border-radius:7px"></span><span class="sk" style="display:block;width:64%;height:10px;border-radius:5px;margin-top:10px"></span></span><span class="sk" style="width:76px;height:14px;border-radius:7px"></span></div>`).join('')}</div>
|
||
</div>`;
|
||
} else {
|
||
const heroH = settled ? hero('settled',0,{en}) : hero('owing',-GLOBAL_NET,{en});
|
||
const archOpen = o.archOpen;
|
||
body=`<div class="pad" style="padding-top:4px">${heroH}
|
||
<div style="margin:8px 0 0">${sync}</div>
|
||
<div class="sec"><h2 class="ts" style="margin:0">${L.konton}</h2>${btn(L.ny,'text','add')}</div>
|
||
<div class="grp">${active.map(a=>accItem(a,L,settled)).join('')}</div>
|
||
<button class="sec" style="width:100%;margin-top:6px;min-height:56px" aria-expanded="${archOpen?'true':'false'}"><span class="ts" style="color:var(--on-surface-variant)">${L.arch(archived.length)}</span>${I(archOpen?'expand_less':'expand_more',{st:'color:var(--on-surface-variant)'})}</button>
|
||
${archOpen?`<div class="grp low">${archived.map(a=>accItem(a,L,true)).join('')}</div>`:''}
|
||
<div style="height:120px"></div></div>`;
|
||
}
|
||
const topbar=`<header class="tab root" style="gap:12px">${kvittMark(32)}<div class="tt"><div class="tl" style="font-weight:650">Kvitt</div></div></header>`;
|
||
const showFab = !(o.state==='empty'||o.state==='loading');
|
||
return {html: topbar+(banner?`<div style="flex:none">${banner}</div>`:'')+scroll(body)+(showFab?fab(L.fab):'')+navBar(0,en), gb:'surface-container', over:o.over||''};
|
||
}
|
||
function wave(x0,x1,y,amp,len){ let d=`M${x0} ${y}`; for(let x=x0;x+len*2<=x1;x+=len*2){ d+=` q${len/2} ${-amp} ${len} 0 t${len} 0`; } return d; }
|
||
|
||
/* =====================================================================
|
||
S-1 … S-3, S-18 onboarding
|
||
===================================================================== */
|
||
function pairing(err){
|
||
const body=`<div class="pad" style="padding-top:28px;display:flex;flex-direction:column;min-height:100%">
|
||
<div style="display:flex;align-items:center;gap:16px">${kvittMark(64)}<div><div class="dm" style="font-weight:700;font-stretch:90%;letter-spacing:-.8px">Kvitt</div></div></div>
|
||
<p class="bl v" style="margin:8px 0 28px">Delade utgifter för två</p>
|
||
<div style="display:flex;flex-direction:column;gap:${err?16:20}px">
|
||
${field({label:'Inbjudningskod',value:'<span class="num" style="letter-spacing:1.6px">7K3M-QX9D-2HPA</span>',err:err?'Koden fungerar inte. Den kan ha gått ut eller redan använts. Be Jacob om en ny.':'',foc:!!err,nocaret:1,trail:err?'':btn('Klistra in','text','content_paste')})}
|
||
${field({label:'Ditt namn',value:'Anna'})}
|
||
<p class="bm v" style="margin:0 4px;display:flex;gap:10px">${I('info',{s:20})}<span>Koden får du av Jacob: från en länk eller från Inställningar › Bjud in partner.</span></p>
|
||
<div class="grp low" style="margin-top:4px"><button class="gi li" style="width:100%;padding:12px 16px"><span class="lead" style="background:var(--surface-container-highest);color:var(--on-surface-variant)">${I('dns',{s:22})}</span><span class="m"><span class="bs v" style="display:block">Serveradress</span><span class="bm" style="display:block;overflow-wrap:anywhere">https://icehack.asuscomm.com:8443</span></span>${I('edit',{s:20,c:'ch',l:'Ändra serveradress'})}</button></div>
|
||
</div>
|
||
<div style="flex:1;min-height:24px;position:relative" aria-hidden="true"><svg viewBox="0 50 210 70" style="position:absolute;left:-16px;right:-16px;bottom:8px;width:calc(100% + 32px);height:auto">
|
||
<path d="M0 98c18-22 34-30 50-30 18 0 26 14 40 14s24-12 40-12 30 16 44 16 22-6 36-6v40H0z" style="fill:var(--surface-container-high)"/>
|
||
<path d="M118 84c8-14 18-20 28-20 12 0 16 10 26 10 8 0 14-4 20-4 8 0 12 4 18 4v46h-92z" style="fill:var(--surface-container-highest)"/>
|
||
<path d="M0 112c10-5 20-5 30 0s20 5 30 0 20-5 30 0 20 5 30 0 20-5 30 0 20 5 30 0 20-5 30 0" style="fill:none;stroke:var(--outline-variant);stroke-width:1.6;stroke-linecap:round"/></svg></div>
|
||
${btn('Para ihop','filled m full')}
|
||
<div style="height:16px"></div>
|
||
</div>`;
|
||
return {html:scroll(body,'display:flex;flex-direction:column')};
|
||
}
|
||
function profileStep(){
|
||
const body=`<div class="pad" style="padding-top:20px;display:flex;flex-direction:column;height:100%">
|
||
<div class="card" style="display:flex;align-items:center;gap:14px;padding:14px 16px;border-radius:20px">${av('J',48,'cookie9')}<div><div class="bs v">Hushåll</div><div class="tm">Du går med i Jacobs hushåll</div></div></div>
|
||
<div class="hm" style="margin:32px 0 6px">Så här syns du</div>
|
||
<p class="bm v" style="margin:0 0 28px">Jacob ser ditt namn och din bild bredvid det du lägger till.</p>
|
||
<div style="display:flex;flex-direction:column;align-items:center;gap:12px;margin-bottom:28px">
|
||
${av('A',120,'cookie9')}
|
||
${btn('Lägg till bild','tonal','add_a_photo')}
|
||
</div>
|
||
${field({label:'Ditt namn',value:'Anna',foc:1,sup:'Visas för Jacob. Du kan ändra det senare.'})}
|
||
<div style="flex:1"></div>
|
||
${btn('Fortsätt','filled m full')}<div style="height:16px"></div></div>`;
|
||
return {html:scroll(body)};
|
||
}
|
||
function waiting(){
|
||
const body=`<div class="pad" style="padding-top:4px">
|
||
<section class="hero" style="padding:28px 24px 28px">${islands()}
|
||
<div class="top" style="gap:0"><span class="pair" style="--ringc:var(--surface-container)">${av('J',56,'cookie9')}${av('J?',56)}</span></div>
|
||
<div class="hm" style="margin-top:20px;position:relative">Väntar på din partner</div>
|
||
<p class="bm v" style="margin:8px 0 0;position:relative">Kvitt behöver er båda innan ni kan börja. Bjud in din partner med en länk.</p>
|
||
<div style="margin-top:20px;position:relative">${btn('Bjud in partner','filled m','person_add')}</div>
|
||
</section>
|
||
<div class="grp" style="margin-top:16px"><div class="gi li">${I('schedule',{s:22,st:'color:var(--on-surface-variant)'})}<div class="m"><div class="ts">Inbjudan skickad</div><div class="bm v">Inbjudan gäller till mån 19 okt. 21:40</div></div></div></div>
|
||
<p class="bs v" style="margin:16px 8px">Konton, utgifter och betalningar blir tillgängliga när din partner har gått med.</p>
|
||
</div>`;
|
||
return {html:`<header class="tab root" style="gap:12px">${kvittMark(32)}<div class="tt"><div class="tl" style="font-weight:650">Kvitt</div></div></header>`+scroll(body)+navBar(0),gb:'surface-container'};
|
||
}
|
||
function removed(){
|
||
const body=`<div class="pad" style="height:100%;display:flex;flex-direction:column;padding-top:96px">
|
||
<div style="display:flex;justify-content:center;margin-bottom:32px"><span style="width:120px;height:120px;${shp('cookie9')}background:var(--secondary-container);color:var(--on-secondary-container);display:grid;place-items:center">${I('phonelink_erase',{s:52})}</span></div>
|
||
<div class="hm" style="text-align:center">Den här telefonen har tagits bort</div>
|
||
<p class="bl v" style="text-align:center;margin:12px 8px 0">Åtkomsten togs bort och allt Kvitt-data på telefonen har raderats.</p>
|
||
<div style="flex:1"></div>${btn('Para ihop igen','filled m full')}<div style="height:16px"></div></div>`;
|
||
return {html:scroll(body)};
|
||
}
|
||
|
||
/* =====================================================================
|
||
S-5 Account detail
|
||
===================================================================== */
|
||
function accountHeader(kind){
|
||
if(kind==='gotland') return `<div class="card hi" style="border-radius:28px;margin:4px 16px 0;padding:20px">
|
||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap"><span class="badge">${I('inventory_2',{s:14})}Arkiverat</span><span class="bm v">50 / 50 · Jacob och Anna</span></div>
|
||
<div style="display:flex;align-items:center;gap:12px;margin-top:14px">${pairAv(36,null,'--surface-container')}<div class="hs c-settled" style="display:flex;align-items:center;gap:6px">${I('check',{s:24})}Kvitt</div></div>
|
||
<div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap">${btn('Återställ från arkiv','tonal','unarchive')}</div>
|
||
<div class="bs v" style="display:flex;gap:6px;margin-top:12px">${I('info',{s:16})}<span>Kontot kan inte tas bort så länge det har utgifter, betalningar eller återkommande utgifter</span></div></div>`;
|
||
return `<div class="card hi" style="border-radius:28px;margin:4px 16px 0;padding:20px">
|
||
<div class="bm v">50 / 50 · Jacob och Anna</div>
|
||
<div style="display:flex;align-items:flex-start;gap:12px;margin-top:14px">${pairAv(36,null,'--surface-container')}<div style="min-width:0"><div class="ts">Anna är skyldig dig</div><div class="al c-owed">${kr(118450)}</div></div></div>
|
||
<div style="margin-top:16px">${btn('Gör upp det här kontot','tonal','handshake')}</div></div>`;
|
||
}
|
||
function account(o={}){
|
||
const g=o.kind==='gotland';
|
||
const months=g?GOTLAND:VARDAG;
|
||
const list=months.map(m=>monthHead(m.m,m.total)+m.rows.map(ledgerRow).join('')).join('');
|
||
const acts=IB('edit','Ändra konto')+IB('more_vert','Fler åtgärder','on');
|
||
let over='';
|
||
if(o.menu) over=`<div class="menu" style="right:12px;top:84px;width:282px" role="menu">
|
||
<div class="mi">${I('edit',{s:22})}<span class="bl">Ändra konto</span></div>
|
||
<div class="mi">${I('inventory_2',{s:22})}<span class="bl">Arkivera</span></div>
|
||
<div class="mi dis" aria-disabled="true">${I('delete',{s:22})}<span><span class="bl" style="display:block">Ta bort konto</span><span class="bs sup" style="display:block">Kontot kan inte tas bort så länge det har utgifter, betalningar eller återkommande utgifter</span></span></div></div>`;
|
||
if(false) over=`<div class="menu" style="right:12px;top:84px;width:282px" role="menu">
|
||
<div class="mi">${I('edit',{s:22})}<span class="bl">Ändra konto</span></div>
|
||
<div class="mi">${I('unarchive',{s:22})}<span class="bl">Återställ från arkiv</span></div>
|
||
<div class="mi dis" aria-disabled="true">${I('delete',{s:22})}<span><span class="bl" style="display:block">Ta bort konto</span><span class="bs sup" style="display:block">Kontot kan inte tas bort så länge det har utgifter, betalningar eller återkommande utgifter</span></span></div></div>`;
|
||
return {html:appBar({nav:'arrow_back',title:g?'Gotland 2026':'Vardag',acts})+scroll(accountHeader(g?'gotland':'')+list+'<div style="height:120px"></div>')+(g?'':fab('Lägg till utgift',{nonav:1,s:1})), over};
|
||
}
|
||
|
||
/* =====================================================================
|
||
S-6 Account editor
|
||
===================================================================== */
|
||
function accountEditor(off){
|
||
const d=off;
|
||
const body=`<div class="pad" style="padding-top:12px;display:flex;flex-direction:column;gap:20px">
|
||
${off?`<div class="banner warn" style="border-radius:20px"><div class="bh">${I('cloud_off',{s:22})}<div><div class="ts">Kräver anslutning</div><div class="bm">Konton sparas på servern. Anslut telefonen till internet för att spara.</div></div></div></div>`:''}
|
||
${field({label:'Namn',value:'Sommarstugan',foc:!off,dis:d})}
|
||
<div class="card" style="${d?'opacity:.5':''}">
|
||
<div class="ts">Standardfördelning</div>
|
||
<p class="bs v" style="margin:2px 0 12px">Gäller nya utgifter i kontot. Fältet för Anna räknas ut så att summan blir 100.</p>
|
||
<div style="display:flex;justify-content:space-between" class="lm v"><span>Jacob</span><span>Anna</span></div>
|
||
<div class="slider" role="slider" aria-label="Jacobs andel" aria-valuenow="60"><div class="tr"><div class="a" style="width:58%"></div><div class="h"></div><div class="b2"></div></div></div>
|
||
<div style="display:flex;gap:12px;margin-top:12px;--fbg:var(--surface-container-low)">
|
||
${field({label:'Jacob',value:'60,00',r:1,num:1,trail:'<span class="bl v">%</span>',st:'flex:1',dis:d})}
|
||
${field({label:'Anna',value:'40,00',r:1,num:1,trail:'<span class="bl v">%</span>',st:'flex:1',ro:1,dis:d})}
|
||
</div>
|
||
</div>
|
||
<div class="card hi" style="border-radius:20px;padding:16px;display:flex;gap:12px;align-items:flex-start;${d?'opacity:.5':''}">${I('calculate',{s:22,st:'color:var(--primary)'})}<div><div class="lm v">Förhandsvisning</div><div class="bm num">Av 100,00${NB}kr: Jacob 60,00${NB}kr, Anna 40,00${NB}kr</div></div></div>
|
||
</div>`;
|
||
return {html:appBar({nav:'close',navL:'Avbryt',title:'Nytt konto',acts:btn('Spara','filled',null,{dis:off})})+scroll(body)};
|
||
}
|
||
|
||
/* =====================================================================
|
||
S-7 Expense editor
|
||
===================================================================== */
|
||
function keypad(o={}){
|
||
const k=(t,cls='',al)=>`<button class="key ${cls}"${al?` aria-label="${al}"`:''}>${t}</button>`;
|
||
return `<div class="kp${o.sm?' sm':''}" role="group" aria-label="Sifferknappar">${['1','2','3','4','5','6','7','8','9'].map(n=>k(n,o.pressed===n?'pressed':'')).join('')}${k(',','sec2','Decimaltecken')}${k('0')}${k(I('backspace',{s:26}),'del','Radera')}</div>`;
|
||
}
|
||
function amountBlock(ore,typed,o={}){
|
||
const val = typed!=null ? typed : krn(ore);
|
||
return `<div style="padding:${o.compact?'4px 20px 12px':'12px 20px 16px'}">
|
||
<div class="lm v">Belopp</div>
|
||
<div style="display:flex;align-items:baseline;gap:8px;flex-wrap:wrap"><span class="${o.compact?'al':'ah'}" style="color:var(--on-surface)">${val}${o.caret?'<span class="caret" style="width:3px;margin-left:3px"></span>':''}</span><span class="${o.compact?'hs':'hm'} v" style="font-weight:500">kr</span></div></div>`;
|
||
}
|
||
function defaultsChips(o){
|
||
return `<div class="chips" role="group" aria-label="Standardval">
|
||
${chip({icon:'account_balance_wallet',v:o.acc||'Vardag',al:'Konto: '+(o.acc||'Vardag')})}
|
||
${chip({lead:catIc(o.cat||'entertainment','s').replace('class="ci s"','class="ci s" style="width:24px;height:24px;border-radius:8px;--c:var(--cat-'+CAT[o.cat||'entertainment'].c+');--cc:var(--cat-'+CAT[o.cat||'entertainment'].c+'-container)"'),v:CAT[o.cat||'entertainment'].sv,al:'Kategori: '+CAT[o.cat||'entertainment'].sv})}
|
||
${chip({icon:'event',v:'Idag',al:'Datum: Idag'})}
|
||
${chip({lead:av('J',24),k:'Betalt av',v:'Du'})}
|
||
${chip({icon:'call_split',k:'Delning',v:o.split||'Kontots standard 50/50',sel:o.splitOpen,dd:1,ddi:o.splitOpen?'expand_less':'expand_more'})}
|
||
${o.noNotes?'':chip({icon:'notes',v:'Lägg till anteckning'})}
|
||
</div>`;
|
||
}
|
||
function editor(o={}){
|
||
const fsBig=o.big;
|
||
const top=appBar({nav:'close',navL:'Avbryt',title:'Ny utgift',acts:btn('Spara','filled',null,{dis:o.dis})});
|
||
let mid,bottom='';
|
||
if(o.mode==='split'){
|
||
mid=`${amountBlock(106000,null,{compact:1})}
|
||
<div class="pad" style="display:flex;flex-direction:column;gap:12px">
|
||
${field({label:'Beskrivning',value:'Biljetter Dramaten'})}
|
||
${defaultsChips({cat:'entertainment',split:'Exakta belopp',splitOpen:1,noNotes:1})}
|
||
<section class="card" style="padding:16px;border-radius:28px;--fbg:var(--surface-container-low)" aria-label="Delning">
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px" role="radiogroup" aria-label="Delningsläge">
|
||
${[['Kontots standard 50/50',0],['Egen fördelning',0],['Exakta belopp',1],['Allt på en person',0]].map(([t,on],i)=>`<button role="radio" aria-checked="${on?'true':'false'}" class="bm" style="min-height:44px;padding:6px 12px;display:flex;align-items:center;gap:6px;border-radius:${on?'22px':(['20px 8px 8px 8px','8px 20px 8px 8px','8px 8px 8px 20px','8px 8px 20px 8px'][i])};background:var(${on?'--secondary':'--surface-container-high'});color:var(${on?'--on-secondary':'--on-surface'});font-weight:${on?600:450}">${on?I('check',{s:18}):''}${t}</button>`).join('')}
|
||
</div>
|
||
<div style="display:flex;gap:10px;margin-top:16px;align-items:flex-start">
|
||
${field({label:'Du',value:'1 200,00',num:1,r:1,foc:1,err:'Andelen är större än beloppet',trail:'<span class="bl v">kr</span>',st:'flex:1.15'})}
|
||
${field({label:'Anna',value:'—',r:1,ro:1,trail:'<span class="bl v">kr</span>',st:'flex:.85',sup:'Räknas ut'})}
|
||
</div>
|
||
<div class="bm v" style="margin-top:10px;display:flex;gap:8px;align-items:center">${I('calculate',{s:18})}<span>Du 1${NB}200,00${NB}kr · Anna —</span></div>
|
||
</section></div>`;
|
||
bottom=keypad({sm:1});
|
||
} else if(o.mode==='full'){
|
||
mid=`${amountBlock(106000,null,{compact:1})}
|
||
<div class="pad" style="display:flex;flex-direction:column;gap:12px">
|
||
${field({label:'Beskrivning',value:'SL-kort 30 dagar, Anna'})}
|
||
${defaultsChips({cat:'transport',split:'Allt på Anna',splitOpen:1,noNotes:1})}
|
||
<section class="card" style="padding:16px;border-radius:28px" aria-label="Delning">
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px" role="radiogroup" aria-label="Delningsläge">
|
||
${[['Kontots standard 50/50',0],['Egen fördelning',0],['Exakta belopp',0],['Allt på en person',1]].map(([t,on],i)=>`<button role="radio" aria-checked="${on?'true':'false'}" class="bm" style="min-height:44px;padding:6px 12px;display:flex;align-items:center;gap:6px;border-radius:${on?'22px':(['20px 8px 8px 8px','8px 20px 8px 8px','8px 8px 8px 20px','8px 8px 20px 8px'][i])};background:var(${on?'--secondary':'--surface-container-high'});color:var(${on?'--on-secondary':'--on-surface'});font-weight:${on?600:450}">${on?I('check',{s:18}):''}${t}</button>`).join('')}
|
||
</div>
|
||
<div class="lm v" style="margin:16px 0 8px">Vem står för hela kostnaden?</div>
|
||
<div class="bg full" role="radiogroup"><button class="b" role="radio" aria-checked="false">${av('J',24)} Du</button><button class="b on" role="radio" aria-checked="true">${I('check',{s:18})}${av('A',24)} Anna</button></div>
|
||
<div class="bm" style="margin-top:14px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">${I('calculate',{s:18,st:'color:var(--primary)'})}<span class="num">Du 0,00${NB}kr · Anna 1${NB}060,00${NB}kr</span></div>
|
||
</section>
|
||
${field({label:'Anteckningar',value:'Annas månadskort för oktober–november.',foc:1})}
|
||
<div style="height:24px"></div></div>`;
|
||
} else {
|
||
mid=`${amountBlock(41200,'412',{caret:1})}
|
||
<div class="pad" style="display:flex;flex-direction:column;gap:14px">
|
||
${field({label:'Beskrivning',value:'Systembolaget'})}
|
||
${defaultsChips({cat:'entertainment'})}
|
||
<div class="bm v" style="display:flex;gap:8px;align-items:center;padding:0 4px">${I('calculate',{s:18})}<span class="num">Du 206,00${NB}kr · Anna 206,00${NB}kr</span></div>
|
||
</div>`;
|
||
bottom=keypad({pressed:'2'});
|
||
}
|
||
return {html:top+scroll(mid)+bottom, gb: bottom?'surface-container-low':'surface'};
|
||
}
|
||
|
||
/* =====================================================================
|
||
S-8 Expense detail, S-10 Settlement detail
|
||
===================================================================== */
|
||
function timeline(lines){
|
||
return `<ol class="tl-list" style="list-style:none;margin:0;padding:4px 0 0 4px">${lines.map((l,i)=>`<li class="tl-it${l.p?' pending':''}${i===0?' first':''}"><span class="tl-dot"></span><div><div class="bm"${l.p?' style="color:var(--pending);font-weight:560"':''}>${l.p?I('cloud_upload',{s:16,st:'vertical-align:-3px;margin-right:4px'}):''}${l.t}</div>${l.at?`<div class="bs v num">${l.at}</div>`:''}</div></li>`).join('')}</ol>`;
|
||
}
|
||
function kvRow(icon,k,v,extra=''){return `<div class="gi kv">${I(icon,{s:22,st:'color:var(--on-surface-variant)'})}<div style="flex:1;min-width:0"><div class="bs v">${k}</div><div class="bl" style="overflow-wrap:anywhere">${v}</div></div>${extra}</div>`;}
|
||
function expenseDetail(kind){
|
||
const dl=kind==='deleted', rec=kind==='recurring';
|
||
const d = rec ? {t:'Hyra september',a:1240000,acc:'Huset',c:'home',date:'fredag 25 september 2026',payer:'A',mode:'Kontots standard 60/40',sh:`Du ${kr(744000)} · Anna ${kr(496000)}`,note:'',
|
||
tl:[{t:'Skapad automatiskt (återkommande)',at:'25 sep. 06:00'}]}
|
||
: dl ? {t:'Pressbyrån',a:6800,acc:'Vardag',c:'restaurants',date:'fredag 2 oktober 2026',payer:'A',mode:'Allt på en person',sh:`Du ${kr(0)} · Anna ${kr(6800)}`,note:'',
|
||
tl:[{t:'Anna skapade den här',at:'2 okt. 08:14'},{t:'Anna tog bort den här',at:'17 okt. 09:12'}]}
|
||
: {t:'ICA Maxi Lindhagen',a:124760,acc:'Vardag',c:'groceries',date:'fredag 16 oktober 2026',payer:'A',mode:'Kontots standard 50/50',sh:`Du ${kr(62380)} · Anna ${kr(62380)}`,note:'Storhandling inför middagen med Lindqvists',
|
||
tl:[{t:'Anna skapade den här',at:'16 okt. 14:02'},{t:`Du ändrade beloppet från ${kr(114760)} till ${kr(124760)}`,at:'16 okt. 19:40'},{t:'Anna ändrade kategorin från Övrigt till Mat & hushåll',at:'16 okt. 20:05'},{t:'Din ändring väntar på synk',p:1}]};
|
||
const acts = dl?'':IB('edit','Ändra')+IB('delete','Ta bort');
|
||
const muted = dl?'opacity:.62;':'';
|
||
const body=`
|
||
${dl?`<div class="banner err" style="margin:4px 16px 16px;border-radius:20px"><div class="bh">${I('delete',{s:22})}<div class="ts">Anna tog bort den här utgiften · 17 okt. 09:12</div></div></div>`:''}
|
||
<div class="pad" style="${muted}">
|
||
<div style="display:flex;align-items:center;gap:16px;padding:4px 4px 0">${`<span style="width:64px;height:64px;${shp('cookie9')}background:var(--cat-${CAT[d.c].c}-container);color:var(--cat-${CAT[d.c].c});display:grid;place-items:center;flex:none">${I(CAT[d.c].icon,{f:1,s:30})}</span>`}
|
||
<div style="min-width:0"><div class="hs" style="overflow-wrap:anywhere;${dl?'text-decoration:line-through;text-decoration-thickness:1px':''}">${d.t}</div>${rec?`<span class="badge tert" style="margin-top:4px">${I('event_repeat',{s:14})}Återkommande: Hyra</span>`:''}</div></div>
|
||
<div class="al" style="margin:16px 4px 16px">${kr(d.a)}</div>
|
||
<div class="grp">
|
||
${kvRow('account_balance_wallet','Konto',d.acc)}
|
||
${kvRow(CAT[d.c].icon,'Kategori',CAT[d.c].sv)}
|
||
${kvRow('event','Datum',d.date)}
|
||
${kvRow('person','Betalt av','Anna',av('A',32))}
|
||
${kvRow('call_split','Delning · '+d.mode,`<span class="num">${d.sh}</span>`)}
|
||
${d.note?kvRow('notes','Anteckning',d.note):''}
|
||
</div>
|
||
<h2 class="ts" style="margin:24px 4px 12px">Historik</h2>
|
||
${timeline(d.tl)}
|
||
<div style="height:24px"></div>
|
||
</div>`;
|
||
return {html:appBar({nav:'arrow_back',title:'',acts})+scroll(body)};
|
||
}
|
||
function settlementDetail(){
|
||
const body=`<div class="pad">
|
||
<section class="hero owed" style="padding:24px">${islands()}
|
||
<div class="top">${av('A',48,'cookie9')}${I('arrow_forward',{s:22,st:'color:var(--on-surface-variant)'})}${av('J',48,'cookie9')}</div>
|
||
<div class="word tl">Anna betalade dig</div>
|
||
<div class="amt ah c-owed" style="font-size:calc(var(--fsx)*52px);line-height:calc(var(--fsx)*58px)">${heroAmount(80000)}</div>
|
||
</section>
|
||
<div class="grp" style="margin-top:16px">
|
||
${kvRow('event','Datum','tisdag 6 oktober 2026')}
|
||
${kvRow('notes','Anteckning','Swish')}
|
||
</div>
|
||
<h2 class="ts" style="margin:24px 4px 8px">Fördelning</h2>
|
||
<div class="grp"><div class="gi kv"><span style="width:32px;height:32px;border-radius:10px;background:var(--secondary-container);color:var(--on-secondary-container);display:grid;place-items:center" class="ts" aria-hidden="true">V</span><div class="bl" style="flex:1">Vardag</div><div class="am">${kr(80000)}</div></div></div>
|
||
<h2 class="ts" style="margin:24px 4px 12px">Historik</h2>
|
||
${timeline([{t:'Anna skapade den här',at:'6 okt. 21:15'}])}
|
||
</div>`;
|
||
return {html:appBar({nav:'arrow_back',title:'Betalning',acts:IB('edit','Ändra')+IB('delete','Ta bort')})+scroll(body)};
|
||
}
|
||
|
||
/* =====================================================================
|
||
S-9 Settle up
|
||
===================================================================== */
|
||
function settle(o={}){
|
||
const rows = o.account ? [['Huset',308450,308450]]
|
||
: o.v==='adjusted' ? [['Huset',308450,175000],['Japan 2027',135000,150000,1]]
|
||
: o.v==='remaining' ? [['Huset',308450,200000],['Japan 2027',135000,100000]]
|
||
: [['Huset',308450,308450],['Japan 2027',135000,16550]];
|
||
const total=325000, alloc=rows.reduce((s,r)=>s+r[2],0), rem=(o.account?308450:total)-alloc;
|
||
const amt=o.account?308450:total;
|
||
const rowH=rows.map(([n,debt,a,warn])=>`
|
||
<div class="gi" style="padding:12px 8px 12px 16px">
|
||
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||
<div style="flex:1 1 120px;min-width:0"><div class="tm">${n}</div><div class="bs v num">Skuld ${kr(debt)}</div></div>
|
||
${field({value:krn(a),num:1,r:1,trail:'<span class="bm v">kr</span>',st:'width:148px;--fbg:var(--surface-container)',err:'',foc:o.v==='adjusted'&&warn})}
|
||
${o.account?'<span style="width:4px"></span>':IB('close','Ta bort '+n+' från fördelningen','',{s:20})}
|
||
</div>
|
||
${warn?`<div class="bs" style="display:flex;gap:6px;align-items:flex-start;color:var(--warning);margin-top:8px;font-weight:560">${I('warning',{s:16,f:1})}<span>Det här reglerar mer än som är skyldigt i ${n}</span></div>`:''}
|
||
</div>`).join('');
|
||
const remRow = `<div style="display:flex;align-items:center;gap:8px;padding:12px 16px;border-radius:16px;background:var(${rem===0?'--surface-container-low':'--error-container'});color:var(${rem===0?'--on-surface-variant':'--on-error-container'})">${I(rem===0?'check_circle':'error',{s:20,f:1,st:rem===0?'color:var(--owed)':''})}<span class="bm" style="flex:1">Kvar att fördela</span><span class="am">${kr(Math.abs(rem))}</span></div>`;
|
||
const body=`<div class="pad" style="display:flex;flex-direction:column;gap:14px;padding-top:4px">
|
||
<div class="card hi" style="padding:16px 12px 16px 16px;border-radius:28px;display:flex;align-items:center;gap:12px">
|
||
<span class="pair" style="--ringc:var(--surface-container)">${av('J',40,'cookie9')}${av('A',40,'cookie9')}</span>
|
||
<div style="flex:1;min-width:0"><div class="lm v">Riktning</div><div class="tl">Du betalar Anna</div></div>
|
||
<button class="ib ton" aria-label="Byt riktning">${I('swap_vert')}</button>
|
||
</div>
|
||
${field({label:'Belopp',value:`<span class="al" style="color:var(--on-surface)">${krn(amt)}</span>`,trail:'<span class="tl v">kr</span>'})}
|
||
<div><div class="sec" style="padding-top:0"><h2 class="ts" style="margin:0">Fördelning</h2></div>
|
||
<div class="grp">${rowH}</div></div>
|
||
${remRow}
|
||
${o.account?'':btn('Lägg till konto','text','add')}
|
||
${o.account?'':`<p class="bs v" style="margin:-6px 4px 0;display:flex;gap:8px">${I('info',{s:16})}<span>Vardag ändras inte. Där är det Anna som är skyldig dig.</span></p>`}
|
||
<div style="display:flex;gap:10px">${field({label:'Datum',value:'Idag',lead:'event',st:'flex:1'})}${field({label:'Anteckning',value:'Swish',st:'flex:1.3'})}</div>
|
||
<div style="height:8px"></div>
|
||
</div>`;
|
||
const ok=rem===0;
|
||
const footer=`<div style="flex:none;padding:12px 16px 12px;background:var(--surface)">${btn('Bekräfta','filled m full',null,{dis:!ok})}</div>`;
|
||
let over='';
|
||
if(o.dialog){ over=`<div class="scrim"></div><div class="dlg" role="dialog" aria-label="Bekräfta betalningen">
|
||
<div class="di">${I('handshake',{s:28})}</div>
|
||
<div class="hs dt" style="text-align:center">Bekräfta betalningen?</div>
|
||
<div class="bm db">Du betalar Anna <span class="num nw">${kr(325000)}</span>, idag.</div>
|
||
<div class="bm db num" style="margin-top:8px">Huset ${kr(175000)}<br>Japan 2027 ${kr(150000)}</div>
|
||
<div class="bs" style="display:flex;gap:8px;margin-top:16px;padding:12px;border-radius:16px;background:var(--warning-container);color:var(--on-warning-container);font-weight:560">${I('warning',{s:18,f:1})}<span>Det här reglerar mer än som är skyldigt i Japan 2027</span></div>
|
||
<div class="da">${btn('Avbryt','text')}${btn('Bekräfta','filled')}</div></div>`; }
|
||
return {html:appBar({nav:'close',navL:'Avbryt',title:o.account?'Gör upp Huset':'Gör upp'})+scroll(body)+footer,over};
|
||
}
|
||
|
||
/* =====================================================================
|
||
S-11 Analytics, S-12 Drill-down
|
||
===================================================================== */
|
||
const MONTHS=['maj','jun','jul','aug','sep','okt'];
|
||
const AN={ // kronor*100 (öre)
|
||
groceries:[642000,698000,587000,661000,694000,391000],
|
||
restaurants:[189000,264000,398000,172000,211000,124800],
|
||
home:[1315000,1382000,1299000,1346000,1338000,393350],
|
||
utilities:[178000,169000,154000,162000,187400,147500],
|
||
transport:[106000,106000,245000,106000,106000,106000],
|
||
travel:[0,125000,438000,0,1486000,1216000],
|
||
entertainment:[41000,59000,64000,38000,52000,59100],
|
||
health:[0,29000,0,45000,0,18990],
|
||
shopping:[0,62000,0,136000,74000,0],
|
||
other:[0,0,0,0,0,0]
|
||
};
|
||
const ANACC=[['Huset',7986000,'chart-cat-2'],['Vardag',6695140,'chart-cat-1'],['Japan 2027',2702000,'chart-cat-3'],['Gotland 2026',438000,'chart-cat-4']];
|
||
const WEEKS=['35','36','37','38','39','40','41','42'];
|
||
const WK={Vardag:[124000,151500,112000,186000,139000,118000,193000,145000],Huset:[98000,41000,163000,54000,225000,69000,128000,76000],'Japan 2027':[0,0,0,120000,0,140000,0,234000]};
|
||
const WKCAT=[['groceries',624000],['home',541000],['travel',494000],['utilities',313000],['restaurants',218000],['transport',159000],['entertainment',90500],['health',47000],['shopping',30000]];
|
||
const ACC_COL={'Vardag':'chart-cat-1','Huset':'chart-cat-2','Japan 2027':'chart-cat-3','Gotland 2026':'chart-cat-4'};
|
||
function niceMax(v,step){return Math.ceil(v/step)*step;}
|
||
function stackedChart(labels,series,o={}){ // series: [{name,col,vals}]
|
||
const W=347,H=o.h||176,L=44,B=22,T=6;
|
||
const totals=labels.map((_,i)=>series.reduce((s,x)=>s+x.vals[i],0));
|
||
const step=o.step, max=niceMax(Math.max(...totals),step);
|
||
const pw=W-L-4, bw=Math.min(34,pw/labels.length*0.56), gap=pw/labels.length;
|
||
const y=v=>T+(H-T-B)*(1-v/max);
|
||
let s='';
|
||
for(let t=0;t<=max;t+=step){ s+=`<line x1="${L}" x2="${W}" y1="${y(t)}" y2="${y(t)}" style="stroke:var(--chart-grid);stroke-width:1"/><text x="${L-8}" y="${y(t)+4}" text-anchor="end" style="fill:var(--chart-axis-label);font-size:calc(var(--fs)*11px);font-family:inherit" class="num">${t===0?'0':(t/100000)+' tkr'}</text>`; }
|
||
labels.forEach((lab,i)=>{
|
||
const cx=L+gap*i+gap/2; let acc=0;
|
||
const segs=series.filter(x=>x.vals[i]>0);
|
||
segs.forEach((x,j)=>{const v=x.vals[i];const y0=y(acc),y1=y(acc+v);acc+=v;const h=Math.max(0,y0-y1-(j<segs.length-1?2:0));
|
||
s+=`<rect x="${cx-bw/2}" y="${y1}" width="${bw}" height="${Math.max(h,1)}" rx="${j===segs.length-1?Math.min(8,h/2):3}" style="fill:var(--${x.col})"/>`;});
|
||
s+=`<text x="${cx}" y="${H-5}" text-anchor="middle" style="fill:var(--chart-axis-label);font-size:calc(var(--fs)*11px);font-family:inherit">${lab}</text>`;
|
||
});
|
||
return `<svg viewBox="0 0 ${W} ${H}" width="100%" style="display:block;overflow:visible" role="img" aria-label="${esc(o.summary||'')}">${s}</svg>`;
|
||
}
|
||
function legend(items){return `<div style="display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:10px">${items.map(([n,col])=>`<span class="lsm v" style="display:inline-flex;align-items:center;gap:6px"><span style="width:10px;height:10px;border-radius:3px;background:var(--${col})"></span>${n}</span>`).join('')}</div>`;}
|
||
function breakdown(rows,total){ // rows: [{label, ore, col, icon?}]
|
||
const max=Math.max(...rows.map(r=>r.ore));
|
||
return rows.map(r=>`<button class="gi" style="display:flex;gap:12px;align-items:center;padding:10px 16px 10px 12px;width:100%">${r.lead}<span style="flex:1;min-width:0"><span style="display:flex;justify-content:space-between;gap:8px;align-items:baseline;flex-wrap:wrap"><span class="bm" style="overflow-wrap:anywhere">${r.label}</span><span class="nw"><span class="as">${kr(r.ore)}</span> <span class="bs v num" style="display:inline-block;min-width:44px;text-align:right">${pct1(r.ore,total)}</span></span></span>
|
||
<span style="display:block;height:6px;border-radius:3px;background:var(--surface-container-highest);margin-top:6px"><span style="display:block;height:6px;border-radius:3px;width:${(r.ore/max*100).toFixed(1)}%;background:var(--${r.col})"></span></span></span></button>`).join('');
|
||
}
|
||
function accLead(n){return `<span style="width:32px;height:32px;border-radius:10px;background:var(--secondary-container);color:var(--on-secondary-container);display:grid;place-items:center;flex:none" class="ts" aria-hidden="true">${n[0]}</span>`;}
|
||
function analytics(o={}){
|
||
const wk=o.week, empty=o.empty;
|
||
const filters=`<div class="pad" style="display:flex;flex-direction:column;gap:8px;padding-bottom:4px">
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">${chip({icon:'date_range',v:empty?'1–31 mars 2025':(wk?'24 aug.–17 okt.':'Senaste 6 mån'),k:empty||wk?'Anpassad:':'',dd:1,sm:1,sel:1})}
|
||
<div class="bg" role="radiogroup" aria-label="Indelning"><button class="b${wk?'':' on'}">Månad</button><button class="b${wk?' on':''}">Vecka</button></div></div>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<div class="bg" role="radiogroup" aria-label="Mått"><button class="b${wk?'':' on'}">Hushållet</button><button class="b${wk?' on':''}">Min del</button></div>
|
||
<div class="bg" role="radiogroup" aria-label="Gruppera efter"><button class="b${wk?'':' on'}">Kategori</button><button class="b${wk?' on':''}">Konto</button></div></div></div>`;
|
||
let body;
|
||
if(empty){
|
||
body=`${filters}<div class="pad" style="padding-top:12px"><div class="card" style="text-align:center;padding:40px 24px">
|
||
<div style="display:flex;justify-content:center;margin-bottom:16px"><span style="width:88px;height:88px;${shp('clover4')}background:var(--surface-container-highest);color:var(--on-surface-variant);display:grid;place-items:center">${I('bar_chart',{s:40})}</span></div>
|
||
<div class="lm v">Totalt</div><div class="al">${kr(0)}</div>
|
||
<div class="tm" style="margin-top:16px">Inga utgifter i den här perioden</div>
|
||
<p class="bm v" style="margin:6px 0 20px">Välj en annan period för att se era utgifter.</p>${btn('Senaste 6 mån','tonal','date_range')}</div></div>`;
|
||
} else if(wk){
|
||
const series=Object.keys(WK).map(n=>({name:n,col:ACC_COL[n],vals:WK[n]}));
|
||
const tot=WEEKS.reduce((s,_,i)=>s+series.reduce((a,x)=>a+x.vals[i],0),0);
|
||
body=`${filters}<div class="pad" style="padding-top:12px;display:flex;flex-direction:column;gap:12px">
|
||
<div style="padding:4px 4px 0"><div class="lm v">Totalt · min del · 24 aug.–17 okt.</div><div class="al">${kr(tot)}</div></div>
|
||
<section class="card" style="padding:16px 16px 14px"><h2 class="ts" style="margin:0 0 10px">Utgifter över tid</h2>${stackedChart(WEEKS.map(w=>'v.'+NB+w),series,{step:150000,summary:'Utgifter över tid, min del per vecka: högst vecka 42, 4 550 kr; lägst vecka 36, 1 925 kr.'})}${legend(series.map(x=>[x.name,x.col]))}</section>
|
||
<section><h2 class="ts" style="margin:8px 4px 8px">Per kategori</h2><div class="grp">${breakdown(WKCAT.slice(0,4).map(([k,v])=>({label:CAT[k].sv,ore:v,col:'cat-'+CAT[k].c,lead:catIc(k,'s')})),tot)}</div></section>
|
||
<div style="height:20px"></div></div>`;
|
||
} else {
|
||
const keys=Object.keys(AN).filter(k=>AN[k].some(v=>v>0));
|
||
const series=keys.map(k=>({name:CAT[k].sv,col:'cat-'+CAT[k].c,vals:AN[k]}));
|
||
const tot=keys.reduce((s,k)=>s+AN[k].reduce((a,b)=>a+b,0),0);
|
||
const byCat=keys.map(k=>({k,ore:AN[k].reduce((a,b)=>a+b,0)})).sort((a,b)=>b.ore-a.ore);
|
||
body=`${filters}<div class="pad" style="padding-top:12px;display:flex;flex-direction:column;gap:12px">
|
||
<div style="padding:4px 4px 0"><div class="lm v">Totalt · maj–okt 2026</div><div class="al">${kr(tot)}</div></div>
|
||
<section class="card" style="padding:16px 16px 14px"><h2 class="ts" style="margin:0 0 10px">Utgifter över tid</h2>${stackedChart(MONTHS,series,{step:1500000,h:168,summary:'Utgifter över tid: högst i september, 41 484 kr; lägst i maj, 24 710 kr.'})}${legend(series.map(x=>[x.name,x.col]))}</section>
|
||
<section><h2 class="ts" style="margin:8px 4px 8px">Per kategori</h2><div class="grp">${breakdown(byCat.map(r=>({label:CAT[r.k].sv,ore:r.ore,col:'cat-'+CAT[r.k].c,lead:catIc(r.k,'s')})),tot)}</div></section>
|
||
<section><h2 class="ts" style="margin:8px 4px 8px">Per konto</h2><div class="grp">${breakdown(ANACC.map(([n,v,col])=>({label:n+(n==='Gotland 2026'?' <span class="badge">Arkiverat</span>':''),ore:v,col,lead:accLead(n)})),tot)}</div></section>
|
||
<div style="height:24px"></div></div>`;
|
||
}
|
||
if(o.scrolled) body=`<div style="transform:translateY(-590px)">${body}</div>`;
|
||
return {html:appBar({title:'Statistik',root:1,sub:o.scrolled?'Senaste 6 mån · Månad · Hushållet · Kategori':''})+scroll(body)+navBar(1),gb:'surface-container'};
|
||
}
|
||
function drill(){
|
||
const rows=VARDAG[0].rows.filter(r=>r.c==='groceries');
|
||
const body=`<div class="pad" style="padding-top:4px"><div class="card hi" style="display:flex;align-items:center;gap:14px;border-radius:24px;padding:16px">${catIc('groceries','l')}<div><div class="lm v">Totalt · 4 utgifter</div><div class="al">${kr(391000)}</div></div></div></div>
|
||
<div style="padding-top:8px">${rows.map(ledgerRow).join('')}</div>`;
|
||
return {html:appBar({nav:'arrow_back',title:'Mat & hushåll, oktober 2026',wrap:1})+scroll(body)};
|
||
}
|
||
|
||
/* =====================================================================
|
||
Settings and sub-screens
|
||
===================================================================== */
|
||
function settingsRow(icon,label,sub,o={}){return `<button class="gi li" style="width:100%;min-height:${sub?72:56}px"><span class="lead" style="${o.leadSt||''}">${I(icon,{s:22})}</span><span class="m"><span class="bl" style="display:block">${label}</span>${sub?`<span class="bm v" style="display:block">${sub}</span>`:''}</span>${'trail' in o?o.trail:I('chevron_right',{s:22,c:'ch'})}</button>`;}
|
||
function settings(){
|
||
const body=`<div class="pad" style="display:flex;flex-direction:column;gap:16px;padding-top:4px">
|
||
<div class="card hi" style="border-radius:28px;padding:16px">
|
||
<div style="display:flex;align-items:center;gap:14px">${av('J',56,'cookie9')}<div style="flex:1"><div class="tm">Jacob</div><div class="bm v">Du · Administratör</div></div></div>
|
||
<div style="height:1px;background:var(--outline-variant);margin:14px 0"></div>
|
||
<div style="display:flex;align-items:center;gap:14px">${av('A',56,'cookie9')}<div style="flex:1"><div class="tm">Anna</div><div class="bm v">Partner</div></div></div>
|
||
</div>
|
||
<div class="grp">
|
||
${settingsRow('person','Profil','Namn och bild')}
|
||
${settingsRow('category','Kategorier','10 kategorier')}
|
||
${settingsRow('event_repeat','Återkommande utgifter','4 mallar, 1 pausad')}
|
||
</div>
|
||
<div class="grp">
|
||
${settingsRow('devices','Enheter','2 enheter')}
|
||
${settingsRow('qr_code_2','Para ihop en enhet','')}
|
||
</div>
|
||
<div class="grp">
|
||
${settingsRow('sync','Synkstatus','Synkad för 3 min sedan')}
|
||
${settingsRow('info','Om Kvitt','Version 1.1')}
|
||
</div><div style="height:16px"></div></div>`;
|
||
return {html:appBar({title:'Inställningar',root:1})+scroll(body)+navBar(2),gb:'surface-container'};
|
||
}
|
||
function profile(){
|
||
const body=`<div class="pad" style="display:flex;flex-direction:column;align-items:center;gap:16px;padding-top:24px;height:100%">
|
||
${av('J',144,'cookie9')}
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap;justify-content:center">${btn('Byt bild','tonal','photo_library')}${btn('Ta bort bild','outl','delete')}</div>
|
||
<div style="width:100%;margin-top:16px">${field({label:'Namn',value:'Jacob',foc:1,sup:'Visas för Anna i hela appen.'})}</div>
|
||
<div style="flex:1"></div>${btn('Spara','filled m full')}<div style="height:16px"></div></div>`;
|
||
return {html:appBar({nav:'arrow_back',title:'Profil'})+scroll(body)};
|
||
}
|
||
function categories(){
|
||
const keys=['groceries','restaurants','home','utilities','transport','travel','entertainment','health','shopping','other'];
|
||
const row=k=>`<div class="gi li" style="min-height:56px;padding:8px 8px 8px 16px">${catIc(k)}<span class="m bl">${CAT[k].sv}</span><button class="ib" aria-label="Flytta ${CAT[k].sv}">${I('drag_indicator',{s:22})}</button></div>`;
|
||
const body=`<div class="pad" style="padding-top:4px"><div class="grp">${keys.map(row).join('')}</div>
|
||
<div class="sec" style="margin-top:8px"><span class="ts">Arkiverade (1)</span>${I('expand_less',{st:'color:var(--on-surface-variant)'})}</div>
|
||
<div class="grp low"><div class="gi li" style="padding:8px 16px">${catIc('pant')}<span class="m"><span class="bl" style="display:block">Pant</span></span><span class="badge">Arkiverad</span></div></div>
|
||
<div style="height:24px"></div></div>`;
|
||
return {html:appBar({nav:'arrow_back',title:'Kategorier',acts:IB('add','Ny kategori','on')})+scroll(body)};
|
||
}
|
||
const ICONS30=['shopping_cart','restaurant','local_cafe','home','bolt','water_drop','wifi','directions_car','train','local_gas_station','flight','hotel','luggage','movie','music_note','sports_esports','fitness_center','medical_services','medication','shopping_bag','checkroom','pets','child_care','school','card_giftcard','celebration','build','chair','subscriptions','category'];
|
||
function categoryEditor(){
|
||
const body=`<div class="pad" style="display:flex;flex-direction:column;gap:14px;padding-top:12px">
|
||
<div style="display:flex;gap:14px;align-items:center">${`<span style="width:56px;height:56px;${shp('cookie9')}background:var(--cat-green-container);color:var(--cat-green);display:grid;place-items:center;flex:none">${I('shopping_cart',{f:1,s:28})}</span>`}
|
||
${field({label:'Namn',value:'Matvaror',foc:1,st:'flex:1'})}</div>
|
||
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:-6px;flex-wrap:wrap"><span class="bs v" style="padding-left:70px">Standardnamn: Mat & hushåll</span>${btn('Återställ namn','text','restart_alt')}</div>
|
||
<div><div class="ts" style="margin:0 4px 8px">Ikon</div>
|
||
<div style="display:grid;grid-template-columns:repeat(6,1fr);gap:4px;background:var(--surface-container-low);padding:8px;border-radius:24px" role="radiogroup" aria-label="Ikon">${ICONS30.map(n=>`<button role="radio" aria-checked="${n==='shopping_cart'}" aria-label="${n}" style="height:48px;display:grid;place-items:center;border-radius:${n==='shopping_cart'?'16px':'50%'};${n==='shopping_cart'?'background:var(--cat-green-container);color:var(--cat-green);box-shadow:inset 0 0 0 2px var(--cat-green)':'color:var(--on-surface-variant)'}">${I(n,{s:24,f:n==='shopping_cart'})}</button>`).join('')}</div></div>
|
||
<div><div class="ts" style="margin:0 4px 8px">Färg</div>
|
||
<div style="display:grid;grid-template-columns:repeat(6,1fr);gap:10px;padding:4px 8px" role="radiogroup" aria-label="Färg">${CAT_TOKENS.map(c=>`<button role="radio" aria-checked="${c==='green'}" aria-label="${c}" style="height:44px;display:grid;place-items:center"><span style="width:40px;height:40px;border-radius:50%;background:var(--cat-${c});display:grid;place-items:center;color:var(--surface)${c==='green'?';box-shadow:0 0 0 3px var(--surface),0 0 0 5px var(--on-surface)':''}">${c==='green'?I('check',{s:22}):''}</span></button>`).join('')}</div></div>
|
||
<div class="grp" style="margin-top:4px">
|
||
${settingsRow('inventory_2','Arkivera','Visas inte för nya utgifter',{trail:''})}
|
||
<div class="gi li" style="opacity:.6"><span class="lead" style="background:var(--surface-container-highest);color:var(--on-surface-variant)">${I('delete',{s:22})}</span><span class="m"><span class="bl" style="display:block">Ta bort</span><span class="bm v" style="display:block">Kan inte tas bort: standardkategori</span></span></div>
|
||
</div><div style="height:16px"></div></div>`;
|
||
return {html:appBar({nav:'close',navL:'Avbryt',title:'Ändra kategori',acts:btn('Spara','filled')})+scroll(body)};
|
||
}
|
||
function sync(clock){
|
||
const body=`<div class="pad" style="display:flex;flex-direction:column;gap:16px;padding-top:4px">
|
||
${clock?`<div class="banner warn" style="border-radius:24px"><div class="bh">${I('schedule',{s:24})}<div><div class="ts">Telefonens klocka går fel. Rätta den och synka igen.</div><div class="bm" style="margin-top:4px">Dina ändringar är sparade på telefonen och skickas när klockan stämmer.</div></div></div></div>`:''}
|
||
<div class="card hi" style="border-radius:28px;display:flex;flex-direction:column;gap:14px">
|
||
<div style="display:flex;gap:14px;align-items:center"><span style="width:48px;height:48px;${shp('cookie9')}background:var(--primary-container);color:var(--on-primary-container);display:grid;place-items:center">${I(clock?'sync_problem':'cloud_done',{s:26})}</span><div><div class="tm">${clock?'Synken stoppades':'Synkad för 3 min sedan'}</div><div class="bm v">${clock?'Senast synkad 17 okt. 13:59':'17 okt. 13:59'}</div></div></div>
|
||
${btn('Synka nu','filled','sync')}
|
||
</div>
|
||
<section><h2 class="ts" style="margin:4px 4px 8px">Väntande ändringar (2)</h2>
|
||
<div class="grp">
|
||
<div class="gi row">${catIc('transport')}<div class="rm"><div class="rtop"><span class="tm rt">SL-kort 30 dagar, Anna</span><span class="ra am">${kr(106000)}</span></div><div class="rs bm"><span class="pend">${I('cloud_upload',{s:16})}</span>Ny · 14 okt.</div></div></div>
|
||
<div class="gi row">${catIc('groceries')}<div class="rm"><div class="rtop"><span class="tm rt">Willys Hornstull</span><span class="ra am">${kr(86435)}</span></div><div class="rs bm"><span class="pend">${I('cloud_upload',{s:16})}</span>Ändrad · 12 okt.</div></div></div>
|
||
</div></section>
|
||
${clock?'':`<section><h2 class="ts" style="margin:4px 4px 8px">Ändringar som inte synkades</h2>
|
||
<div class="card" style="border-radius:24px;padding:16px">
|
||
<div style="display:flex;gap:12px;align-items:flex-start">${catIc('restaurants')}<div style="flex:1;min-width:0"><div class="tm">Babette</div><div class="bm" style="color:var(--on-surface)">Din ändring ersattes av en nyare ändring från Anna</div><div class="bs v">17 okt. 08:31</div></div></div>
|
||
<p class="bs v" style="margin:10px 0 0 52px">Annas version gäller nu för er båda. Öppna för att se vad du hade ändrat.</p>
|
||
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:8px;flex-wrap:wrap">${btn('Visa','text')}${btn('Ta bort från listan','tonal')}</div>
|
||
</div></section>`}
|
||
<div style="height:16px"></div></div>`;
|
||
return {html:appBar({nav:'arrow_back',title:'Synkstatus'})+scroll(body)};
|
||
}
|
||
function recurring(){
|
||
const items=[['Hyra',1240000,'home','Huset · Anna betalar','Varje månad den 25:e · Nästa 25 okt.'],['Bahnhof bredband',39900,'utilities','Huset · Du betalar','Varje månad den 28:e · Nästa 28 okt.'],['Netflix',17900,'entertainment','Vardag · Du betalar','Varje månad den 3:e · Nästa 3 nov.'],['Spotify Family',25900,'entertainment','Vardag · Anna betalar','Varje månad den 12:e',1]];
|
||
const body=`<div class="pad" style="padding-top:4px"><div class="grp">${items.map(([t,a,c,s1,s2,p])=>`<button class="gi" style="display:flex;gap:14px;align-items:flex-start;flex-wrap:wrap;padding:14px 16px;width:100%;${p?'':''}"><span style="${p?'opacity:.55':''}">${catIc(c)}</span><span style="flex:1 1 150px;min-width:0"><span style="display:flex;flex-wrap:wrap;gap:0 12px;align-items:baseline"><span class="tm" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;flex:1 1 120px">${t}${p?`<span class="badge tert">${I('pause',{s:14})}Pausad</span>`:''}</span><span class="am" style="margin-left:auto">${kr(a)}</span></span><span class="bm v" style="display:block">${s1}</span><span class="bs v" style="display:block">${s2}</span></span></button>`).join('')}</div><div style="height:110px"></div></div>`;
|
||
return {html:appBar({nav:'arrow_back',title:'Återkommande utgifter'})+scroll(body)+fab('Ny mall',{s:1,nonav:1})};
|
||
}
|
||
function recurringEditor(){
|
||
const body=`${amountBlock(1240000,null,{compact:1})}<div class="pad" style="display:flex;flex-direction:column;gap:16px">
|
||
${field({label:'Beskrivning',value:'Hyra'})}
|
||
<div class="chips">${chip({icon:'account_balance_wallet',v:'Huset'})}${chip({lead:catIc('home','s').replace('class="ci s"','class="ci s" style="width:24px;height:24px;border-radius:8px;--c:var(--cat-brown);--cc:var(--cat-brown-container)"'),v:'Hem'})}${chip({lead:av('A',24),k:'Betalt av',v:'Anna'})}${chip({icon:'call_split',k:'Delning',v:'Kontots standard 60/40',dd:1,ddi:'expand_more'})}</div>
|
||
<div class="card" style="display:flex;flex-direction:column;gap:18px;--fbg:var(--surface-container-low);border-radius:28px">
|
||
<div class="ts">Upprepning</div>
|
||
${field({label:'Dag i månaden',value:'25',lead:'event_repeat',sup:'Kortare månader använder sista dagen.'})}
|
||
<div style="display:flex;gap:10px">${field({label:'Första månad',value:'september 2026',st:'flex:1'})}${field({label:'Sista månad (valfritt)',value:'Ingen',ph:1,st:'flex:1'})}</div>
|
||
<div style="display:flex;align-items:center;gap:16px;min-height:48px"><div style="flex:1"><div class="bl">Pausad</div><div class="bs v">Inga nya utgifter skapas medan mallen är pausad</div></div><span class="switch" role="switch" aria-checked="false" aria-label="Pausad"></span></div>
|
||
</div>
|
||
${btn('Ta bort mall','outl err','delete')}
|
||
<div style="height:16px"></div></div>`;
|
||
return {html:appBar({nav:'close',navL:'Avbryt',title:'Ändra återkommande',acts:btn('Spara','filled')})+scroll(body)};
|
||
}
|
||
function devices(dialog){
|
||
const dev=(name,who,seen,cur)=>`<div class="gi" style="padding:16px;display:flex;gap:14px;align-items:flex-start;flex-wrap:wrap">
|
||
<span class="lead" style="width:44px;height:44px;border-radius:14px;background:var(--secondary-container);color:var(--on-secondary-container);display:grid;place-items:center;flex:none">${I('smartphone',{s:24})}</span>
|
||
<div style="flex:1 1 180px;min-width:0"><div class="tm" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">${name}${cur?'<span class="badge pri">Den här enheten</span>':''}</div>
|
||
<div class="bm v" style="display:flex;align-items:center;gap:6px;margin-top:2px">${av(who,20)} ${who==='J'?'Jacob':'Anna'}</div>
|
||
<div class="bs v" style="margin-top:6px">Parad 3 okt. · ${seen}<br>Kvitt 1.1</div></div>
|
||
${cur?'':`<div style="margin-left:auto;align-self:center">${btn('Återkalla','outl err',null,{al:'Återkalla Galaxy S26+'})}</div>`}</div>`;
|
||
const body=`<div class="pad" style="padding-top:4px"><div class="grp">${dev('Galaxy S26','J','Senast sedd nyss',1)}${dev('Galaxy S26+','A','Senast sedd 17 okt. 13:47',0)}</div>
|
||
<p class="bs v" style="margin:12px 8px">En återkallad enhet loggas ut och allt Kvitt-data på den raderas.</p></div>`;
|
||
const over=dialog?`<div class="scrim"></div><div class="dlg" role="dialog"><div class="di">${I('phonelink_erase',{s:28})}</div><div class="hs dt" style="text-align:center">Återkalla Galaxy S26+?</div><div class="bm db">Annas telefon förlorar åtkomsten direkt och allt Kvitt-data på den raderas. Anna kan para ihop den igen med en ny kod.</div><div class="da">${btn('Avbryt','text')}${btn('Återkalla','errf')}</div></div>`:'';
|
||
return {html:appBar({nav:'arrow_back',title:'Enheter'})+scroll(body),over};
|
||
}
|
||
function invites(){
|
||
const body=`<div class="pad" style="display:flex;flex-direction:column;gap:16px;padding-top:4px">
|
||
<section class="hero" style="padding:24px;border-radius:32px">
|
||
<div style="position:relative"><div class="lm v">Ny enhet för Jacob</div>
|
||
<div class="ds num" style="font-weight:650;letter-spacing:2px;margin:10px 0 4px;display:flex;gap:14px;flex-wrap:wrap" aria-label="7K3M QX9D 2HPA"><span>7K3M</span><span>QX9D</span><span>2HPA</span></div>
|
||
<div class="bm" style="overflow-wrap:anywhere;color:var(--primary)">https://icehack.asuscomm.com:8443/i/7K3MQX9D2HPA</div>
|
||
<div class="bm v" style="display:flex;gap:6px;align-items:center;margin-top:10px">${I('schedule',{s:18})}Gäller till tis 20 okt. 14:02</div>
|
||
<div style="display:flex;gap:8px;margin-top:20px;flex-wrap:wrap">${btn('Dela','filled m','share')}${btn('Kopiera','outl pri','content_copy')}</div></div>
|
||
</section>
|
||
<section><h2 class="ts" style="margin:4px 4px 8px">Väntande inbjudningar</h2>
|
||
<div class="grp"><div class="gi li" style="padding:12px 8px 12px 16px"><span class="lead">${I('qr_code_2',{s:22})}</span><span class="m"><span class="bl" style="display:block">Ny enhet för Jacob</span><span class="bm v" style="display:block">Skapad 17 okt. 14:02 · Gäller till tis 20 okt. 14:02</span></span>${btn('Avbryt','text',null,{al:'Avbryt inbjudan'})}</div></div></section>
|
||
<p class="bs v" style="margin:0 8px">Koden fungerar en gång och går ut efter 72 timmar.</p>
|
||
</div>`;
|
||
return {html:appBar({nav:'arrow_back',title:'Para ihop en enhet'})+scroll(body)};
|
||
}
|
||
function about(){
|
||
const body=`<div class="pad" style="display:flex;flex-direction:column;gap:16px;padding-top:12px">
|
||
<div style="display:flex;flex-direction:column;align-items:center;gap:10px;padding:16px 0 8px">${kvittMark(88)}<div class="hm" style="font-weight:700">Kvitt</div><div class="bm v num">Version 1.1 (11)</div></div>
|
||
<div class="grp">
|
||
${kvRow('dns','Server','https://icehack.asuscomm.com:8443')}
|
||
${kvRow('memory','Serverversion','1.1.0')}
|
||
${settingsRow('description','Licenser för öppen källkod','',{leadSt:'background:none;color:var(--on-surface-variant);width:22px'})}
|
||
</div>
|
||
${btn('Sök efter uppdatering','tonal m','system_update')}
|
||
</div>`;
|
||
return {html:appBar({nav:'arrow_back',title:'Om Kvitt'})+scroll(body)};
|
||
}
|
||
function updateDialog(){
|
||
const h=home({banner:'update'});
|
||
h.over=`<div class="scrim"></div><div class="dlg" role="dialog"><div class="di">${I('install_mobile',{s:28})}</div><div class="hs dt" style="text-align:center">Tillåt installation</div>
|
||
<div class="bm db">Kvitt uppdateras utan Play Butik. Första gången frågar Android om Kvitt får <b>Installera okända appar</b>.</div>
|
||
<div class="bm db" style="margin-top:10px">Slå på <b>Tillåt från den här källan</b> och gå sedan tillbaka till Kvitt. Du behöver bara göra det en gång.</div>
|
||
<div class="da">${btn('Senare','text')}${btn('Fortsätt','filled')}</div></div>`;
|
||
return h;
|
||
}
|
||
function notifications(){
|
||
const n=(title,body,time,icon)=>`<div style="background:var(--surface-container-lowest);border-radius:24px;padding:14px 16px;display:flex;gap:12px">
|
||
<span style="width:32px;height:32px;border-radius:50%;background:var(--primary);color:var(--on-primary);display:grid;place-items:center;flex:none">${I(icon,{s:18})}</span>
|
||
<div style="flex:1;min-width:0"><div class="bs v">Kvitt · ${time}</div><div class="ts" style="margin-top:2px">${title}</div><div class="bm v" style="overflow-wrap:anywhere">${body}</div></div>${I('expand_more',{s:20,st:'color:var(--on-surface-variant)'})}</div>`;
|
||
const qs=['wifi','bluetooth','flashlight_on','do_not_disturb_on'];
|
||
const body=`<div class="pad" style="padding-top:16px">
|
||
<div style="display:flex;justify-content:space-between;align-items:flex-end;padding:0 8px 16px"><div><div class="dm num" style="font-weight:400">14:02</div><div class="bm v">lördag 17 oktober</div></div></div>
|
||
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:20px">${qs.map((q,i)=>`<span style="height:56px;border-radius:20px;display:grid;place-items:center;background:var(${i===0?'--primary':'--surface-container-high'});color:var(${i===0?'--on-primary':'--on-surface-variant'})">${I(q,{s:24})}</span>`).join('')}</div>
|
||
<div style="display:flex;flex-direction:column;gap:4px">
|
||
${n('Anna lade till en utgift',`ICA Maxi Lindhagen · ${kr(124760)} · Vardag`,'nu','receipt_long')}
|
||
${n('Anna registrerade en betalning',`Anna betalade dig ${kr(80000)}`,'12 min','handshake')}
|
||
</div>
|
||
<div style="display:flex;justify-content:flex-end;margin-top:12px">${btn('Rensa alla','tonal')}</div></div>`;
|
||
return {html:scroll(body), sb:'surface-dim', bg:'surface-dim', gb:'surface-dim'};
|
||
}
|
||
|
||
/* =====================================================================
|
||
FRAMES
|
||
===================================================================== */
|
||
const LD=['light','dark'];
|
||
const FRAMES=[
|
||
{id:'S-1-pairing',title:'S-1 Parning',note:'Betraktaren är Anna som går med. Fälten fyllda från länken. Serveradressen är en hopfälld sekundär rad (tryck för att ändra).',themes:LD,render:()=>pairing(false)},
|
||
{id:'S-1-pairing-error',title:'S-1 Parning, fel',note:'invite_invalid: inline-fel under koden, Para ihop aktiv igen.',themes:LD,render:()=>pairing(true)},
|
||
{id:'S-2-profile-step',title:'S-2 Profilsteg (Anna)',note:'Visas efter en partner-parning (AC-2.10). Betraktaren är Anna.',themes:LD,render:profileStep},
|
||
{id:'S-3-waiting',title:'S-3 Väntar på partner',note:'Ingen FAB: utgifter kan inte läggas till förrän partnern är med (AC-5.1).',themes:LD,render:waiting},
|
||
{id:'S-4-home',title:'S-4 Översikt',note:'Saldot är hjälten: ord ovanför, belopp i amount-hero (Roboto Flex, smal bredd, tung vikt), skärgårdsmotiv i hörnet. Kontolista som Expressive segmenterad grupp.',themes:LD,render:()=>home({})},
|
||
{id:'S-4-home-offline',title:'S-4 Översikt, offline + uppdatering',note:'Synkraden blir en tonad pill (tryck → S-16). Uppdateringsbanner i tertiär sand-färg. Allt annat fungerar som vanligt.',themes:LD,render:()=>home({offline:1,banner:'update'})},
|
||
{id:'S-4-home-settled',title:'S-4 Översikt, kvitt',note:'"Ni är kvitt" i stället för belopp, ingen Gör upp-knapp. Arkiverade utfällda.',themes:LD,render:()=>home({state:'settled',archOpen:1})},
|
||
{id:'S-4-home-empty',title:'S-4 Översikt, inga konton',note:'Ingen FAB: det finns inget konto att lägga utgiften i.',themes:LD,render:()=>home({state:'empty'})},
|
||
{id:'S-4-home-loading',title:'S-4 Översikt, första synk',note:'Expressive LoadingIndicator (formskiftande) i behållare; fallback CircularProgressIndicator. Skelett för kontolistan.',themes:LD,render:()=>home({state:'loading'})},
|
||
{id:'S-4-home-en',title:'S-4 Home (English)',note:'Same data, English copy and SEK 3,250.00 format.',themes:['light'],render:()=>home({en:1})},
|
||
{id:'S-4-home-200',title:'S-4 Översikt, 200 % teckenstorlek',note:'Text ≤ 22 sp skalas linjärt (×2); rubriker och saldot följer Android 14+ icke-linjär skalning (60 sp → ~64 dp). Belopp bryts till egen rad i stället för att kortas.',themes:['light'],fontScale:2,render:()=>home({})},
|
||
{id:'S-5-account',title:'S-5 Konto Vardag',note:'Överflödesmenyn öppen: Ta bort är inaktiverad med skäl. SL-kort har synk-markör (moln-ikon, "Väntar på synk" för TalkBack).',themes:LD,render:()=>account({menu:1})},
|
||
{id:'S-5-account-archived',title:'S-5 Konto Gotland 2026, arkiverat',note:'Arkiverat-märke, Kvitt, Återställ från arkiv. Menyn visar att Ta bort inte är tillåtet (kontot har utgifter).',themes:LD,render:()=>account({kind:'gotland'})},
|
||
{id:'S-6-account-editor',title:'S-6 Nytt konto',note:'Expressive slider med stopp-markör + två procentfält (Anna räknas ut, summan alltid 100).',themes:LD,render:()=>accountEditor(false)},
|
||
{id:'S-6-account-editor-offline',title:'S-6 Nytt konto, offline',note:'Kräver anslutning: fält och Spara inaktiverade.',themes:LD,render:()=>accountEditor(true)},
|
||
{id:'S-7-editor',title:'S-7 Ny utgift (10-sekundersflödet)',note:'Egen siffertangentbord i appen (Expressive tangentformer; nedtryckt tangent morfar till fyrkant). Standardval som chips direkt ovanför; Delning öppnas med ett tryck. Kategori = senast använd i Vardag (AC-8.6).',themes:LD,render:()=>editor({})},
|
||
{id:'S-7-editor-split',title:'S-7 Ny utgift, Exakta belopp med fel',note:'Delningschippen öppnar sektionen ovanför tangentbordet, som nu skriver i Du-fältet. Anna räknas ut (skrivskyddad). Spara inaktiverad.',themes:LD,render:()=>editor({mode:'split',dis:1})},
|
||
{id:'S-7-editor-full',title:'S-7 Ny utgift, Allt på en person',note:'Anteckningar utfällda och i fokus (systemtangentbordet skulle visas här).',themes:LD,render:()=>editor({mode:'full'})},
|
||
{id:'S-7-editor-200',title:'S-7 Ny utgift, 200 %',note:'Chips bryts på fler rader; mellandelen rullar medan tangentbordet och Spara (i toppraden) alltid syns.',themes:['light'],fontScale:2,render:()=>editor({big:1})},
|
||
{id:'S-8-expense',title:'S-8 Utgift ICA Maxi Lindhagen',note:'Tidslinje äldst först, väntande ändring sist i --pending.',themes:LD,render:()=>expenseDetail('')},
|
||
{id:'S-8-expense-recurring',title:'S-8 Utgift Hyra september (återkommande)',note:'',themes:LD,render:()=>expenseDetail('recurring')},
|
||
{id:'S-8-expense-deleted',title:'S-8 Utgift, borttagen',note:'Öppnad från en notis. Innehållet dämpat, inga åtgärder.',themes:LD,render:()=>expenseDetail('deleted')},
|
||
{id:'S-9-settle',title:'S-9 Gör upp, förslag',note:'Förslaget enligt §9.4: äldsta skulden först.',themes:LD,render:()=>settle({})},
|
||
{id:'S-9-settle-adjusted',title:'S-9 Gör upp, justerat',note:'Varning per rad (AC-11.5) i --warning med ikon.',themes:LD,render:()=>settle({v:'adjusted'})},
|
||
{id:'S-9-settle-remaining',title:'S-9 Gör upp, summan stämmer inte',note:'Kvar att fördela i error-container; Bekräfta inaktiverad.',themes:LD,render:()=>settle({v:'remaining'})},
|
||
{id:'S-9-settle-account',title:'S-9 Gör upp Huset (per konto)',note:'En fast rad och ingen Lägg till konto. Riktningen kan bytas (§9.4: per konto i båda riktningar).',themes:LD,render:()=>settle({account:1})},
|
||
{id:'S-9-confirm-dialog',title:'S-9 Bekräfta',note:'Dialogen upprepar varningen.',themes:LD,render:()=>settle({v:'adjusted',dialog:1})},
|
||
{id:'S-10-settlement',title:'S-10 Betalning',note:'',themes:LD,render:settlementDetail},
|
||
{id:'S-11-analytics',title:'S-11 Statistik',note:'Filter som ButtonGroup (fallback SegmentedButton). TalkBack: "Utgifter över tid: högst i september, 41 484 kr; lägst i maj, 24 710 kr." "Per kategori: störst Hem, 70 733,50 kr, 39,7 %." "Per konto: störst Huset, 79 860 kr, 44,8 %." Listan rullar till Per konto (se S-11-analytics-scrolled).',themes:LD,render:()=>analytics({})},
|
||
{id:'S-11-analytics-scrolled',title:'S-11 Statistik, rullad till Per konto',note:'Extra ram (inte i briefens lista): samma skärm rullad så att diagram 2 och 3 syns. Filterraden ligger kvar som en kompakt rad.',themes:LD,render:()=>analytics({scrolled:1})},
|
||
{id:'S-11-analytics-week',title:'S-11 Statistik, vecka · min del · konto',note:'Påhittade veckovärden. TalkBack: "Utgifter över tid, min del per vecka: högst vecka 42, 4 550 kr; lägst vecka 36, 1 925 kr."',themes:LD,render:()=>analytics({week:1})},
|
||
{id:'S-11-analytics-empty',title:'S-11 Statistik, tom period',note:'',themes:LD,render:()=>analytics({empty:1})},
|
||
{id:'S-12-drilldown',title:'S-12 Mat & hushåll, oktober 2026',note:'Titeln får bryta på två rader.',themes:LD,render:drill},
|
||
{id:'S-13-settings',title:'S-13 Inställningar',note:'Grupperade Expressive-listor.',themes:LD,render:settings},
|
||
{id:'S-14-profile',title:'S-14 Profil',note:'',themes:LD,render:profile},
|
||
{id:'S-15-categories',title:'S-15 Kategorier',note:'Dra i handtaget för att sortera (TalkBack: anpassade åtgärder Flytta upp/ned).',themes:LD,render:categories},
|
||
{id:'S-15-category-editor',title:'S-15 Ändra kategori',note:'30 ikoner, 12 färger. Vald ikon får morfad form.',themes:LD,render:categoryEditor},
|
||
{id:'S-16-sync',title:'S-16 Synkstatus',note:'',themes:LD,render:()=>sync(false)},
|
||
{id:'S-16-sync-clock',title:'S-16 Synkstatus, klockan fel',note:'',themes:LD,render:()=>sync(true)},
|
||
{id:'S-17-update',title:'S-17 Tillåt installation',note:'Visas före första installationen.',themes:LD,render:updateDialog},
|
||
{id:'S-17-update-progress',title:'S-17 Laddar ner',note:'Expressive vågig LinearProgressIndicator (fallback rak).',themes:LD,render:()=>home({banner:'progress'})},
|
||
{id:'S-17-update-failed',title:'S-17 Nedladdning misslyckades',note:'',themes:LD,render:()=>home({banner:'failed'})},
|
||
{id:'S-18-removed',title:'S-18 Enheten borttagen',note:'',themes:LD,render:removed},
|
||
{id:'S-19-recurring',title:'S-19 Återkommande utgifter',note:'',themes:LD,render:recurring},
|
||
{id:'S-19-recurring-editor',title:'S-19 Ändra återkommande Hyra',note:'Samma fält som S-7 utom datum.',themes:LD,render:recurringEditor},
|
||
{id:'S-20-devices',title:'S-20 Enheter',note:'',themes:LD,render:()=>devices(false)},
|
||
{id:'S-20-devices-revoke',title:'S-20 Återkalla enhet',note:'',themes:LD,render:()=>devices(true)},
|
||
{id:'S-21-invites',title:'S-21 Para ihop en enhet',note:'Koden i grupper om fyra, länken, giltighet, Dela (systemets delningsark).',themes:LD,render:invites},
|
||
{id:'S-22-about',title:'S-22 Om Kvitt',note:'',themes:LD,render:about},
|
||
{id:'N-notifications',title:'Notiser',note:'Ritade neutralt som Android 16 (inte One UI).',themes:LD,render:notifications}
|
||
];
|
||
|
||
function renderPhone(f,theme){
|
||
const fs=f.fontScale||1;
|
||
let r=f.render(theme); if(typeof r==='string') r={html:r};
|
||
const vars=`--fs:${fs};--fsh:${nlf(28,fs)};--fsd:${nlf(45,fs)};--fsx:${nlf(60,fs)};--sbbg:var(--${r.sb||'surface'});`;
|
||
return `<div class="phone${fs>1.5?' big':''}" data-frame-id="${f.id}-${theme}" data-theme="${theme}" lang="${f.id==='S-4-home-en'?'en':'sv'}" style="${vars}${r.bg?`background:var(--${r.bg})`:''}">${statusBar()}<div class="app">${r.html}</div><div class="gest" style="background:var(--${r.gb||'surface'})"><i></i></div>${r.over||''}</div>`;
|
||
}
|
||
|
||
/* =====================================================================
|
||
Review page
|
||
===================================================================== */
|
||
function swRow(th,name,against,min){
|
||
const v=TOKENS[th][name];const a=against?TOKENS[th][against]:null;const c=a?cr(v,a):null;
|
||
return `<td><span class="sw" style="background:${v}"></span><code>${v}</code></td><td class="n">${c?`<span class="${c<min?'bad':'ok'}">${c.toFixed(2)}</span> <span style="color:#70787C">vs ${against}</span>`:'—'}</td>`;
|
||
}
|
||
function colourTable(){
|
||
const P=[ // [role, against, min]
|
||
['primary','surface',4.5],['on-primary','primary',4.5],['primary-container','surface',0],['on-primary-container','primary-container',4.5],
|
||
['secondary','surface',4.5],['on-secondary','secondary',4.5],['secondary-container','surface',0],['on-secondary-container','secondary-container',4.5],
|
||
['tertiary','surface',4.5],['on-tertiary','tertiary',4.5],['tertiary-container','surface',0],['on-tertiary-container','tertiary-container',4.5],
|
||
['error','surface',4.5],['on-error','error',4.5],['error-container','surface',0],['on-error-container','error-container',4.5],
|
||
['surface',null],['on-surface','surface',4.5],['surface-variant',null],['on-surface-variant','surface',4.5],['on-surface-variant','surface-container-high',4.5],
|
||
['surface-container-lowest',null],['surface-container-low',null],['surface-container',null],['surface-container-high',null],['surface-container-highest',null],['surface-dim',null],['surface-bright',null],
|
||
['inverse-surface',null],['inverse-on-surface','inverse-surface',4.5],['inverse-primary','inverse-surface',3],
|
||
['outline','surface',3],['outline-variant','surface',0],['scrim',null],
|
||
['owed','surface',4.5],['owed','surface-container',4.5],['on-owed','owed',4.5],['owed-container','surface',0],['on-owed-container','owed-container',4.5],
|
||
['owing','surface',4.5],['owing','surface-container',4.5],['on-owing','owing',4.5],['owing-container','surface',0],['on-owing-container','owing-container',4.5],
|
||
['settled','surface',4.5],['settled-container','surface',0],['on-settled-container','settled-container',4.5],
|
||
['warning','surface',4.5],['on-warning','warning',4.5],['warning-container','surface',0],['on-warning-container','warning-container',4.5],
|
||
['pending','surface',4.5],
|
||
['avatar-1','surface',0],['on-avatar-1','avatar-1',4.5],['avatar-2','surface',0],['on-avatar-2','avatar-2',4.5]
|
||
];
|
||
return `<table><tr><th>Role</th><th>Light</th><th>Contrast (light)</th><th>Dark</th><th>Contrast (dark)</th></tr>${P.map(([n,a,m])=>`<tr><td><code>--${n}</code></td>${swRow('light',n,m?a:null,m)}${swRow('dark',n,m?a:null,m)}</tr>`).join('')}</table>`;
|
||
}
|
||
function catTable(){
|
||
return `<table><tr><th>Token</th><th>Light</th><th>vs surface</th><th>vs s-c-high</th><th>vs own container</th><th>Dark</th><th>vs surface</th><th>vs s-c-high</th><th>vs own container</th></tr>${CAT_TOKENS.map(c=>{
|
||
const cells=th=>{const v=TOKENS[th]['cat-'+c],cc=TOKENS[th]['cat-'+c+'-container'];const a=cr(v,TOKENS[th].surface),b=cr(v,TOKENS[th]['surface-container-high']),d=cr(v,cc);
|
||
return `<td><span class="sw" style="background:${v}"></span><code>${v}</code><br><span class="sw" style="background:${cc}"></span><code style="color:#70787C">${cc}</code></td><td class="n ${a<4.5?'bad':'ok'}">${a.toFixed(2)}</td><td class="n ${b<4.5?'bad':'ok'}">${b.toFixed(2)}</td><td class="n ${d<3?'bad':'ok'}">${d.toFixed(2)}</td>`;};
|
||
return `<tr><td><code>--cat-${c}</code></td>${cells('light')}${cells('dark')}</tr>`;}).join('')}</table>`;
|
||
}
|
||
function chartTable(){
|
||
const ks=[...Array(8)].map((_,i)=>'chart-cat-'+(i+1)).concat([...Array(5)].map((_,i)=>'chart-seq-'+(i+1)),['chart-grid','chart-axis-label']);
|
||
return `<table><tr><th>Token</th><th>Light</th><th>vs surface-container-low</th><th>Dark</th><th>vs surface-container-low</th></tr>${ks.map(k=>{const min=k==='chart-axis-label'?4.5:(k==='chart-grid'||k.startsWith('chart-seq')?0:3);
|
||
const cell=th=>{const v=TOKENS[th][k];const c=cr(v,TOKENS[th]['surface-container-low']);return `<td><span class="sw" style="background:${v}"></span><code>${v}</code></td><td class="n">${min?`<span class="${c<min?'bad':'ok'}">${c.toFixed(2)}</span>`:`${c.toFixed(2)} <span style="color:#70787C">(decorative/ramp)</span>`}</td>`;};
|
||
return `<tr><td><code>--${k}</code></td>${cell('light')}${cell('dark')}</tr>`;}).join('')}</table>`;
|
||
}
|
||
const TYPE=[
|
||
['display-large',57,64,400,'—'],['display-medium',45,52,400,'Kvitt wordmark (pairing)'],['display-small',36,44,400,'Invite code, "Ni är kvitt"'],
|
||
['headline-large',32,40,500,'—'],['headline-medium',28,36,500,'Onboarding titles, "kr" suffix in editor'],['headline-small',24,32,500,'Expense title, empty-state titles, dialog titles'],
|
||
['title-large',22,28,500,'Top app bar, hero wording'],['title-medium',16,24,560,'List item titles, account names'],['title-small',14,20,560,'Section headers'],
|
||
['body-large',16,24,400,'Field values, settings rows'],['body-medium',14,20,400,'Supporting text, effect lines'],['body-small',12,16,400,'Meta, timestamps'],
|
||
['label-large',14,20,560,'Buttons, chips'],['label-medium',12,16,560,'Nav labels, overlines'],['label-small',11,16,560,'Badges, chart legend/axes'],
|
||
['amount-hero',60,64,'720, wdth 84','Global balance on Home (decimals + kr at 50 %)'],['amount-large',32,40,'650, wdth 92','Account balance, detail amount, settle amount, totals'],
|
||
['amount-medium',16,24,620,'Amounts in lists'],['amount-small',14,20,520,'Month totals, breakdown amounts']
|
||
];
|
||
function reviewPage(){
|
||
const groups=[];FRAMES.forEach(f=>{const g=f.id.split('-').slice(0,2).join('-');let G=groups.find(x=>x.g===g);if(!G){G={g,items:[]};groups.push(G);}G.items.push(f);});
|
||
const heroBox=(th,st)=>`<div class="hbox" data-theme="${th}" style="--fs:1;--fsh:1;--fsd:1;--fsx:1">${hero(st,st==='owing'?325000:(st==='owed'?118450:0))}</div>`;
|
||
return `<div class="rv">
|
||
<div style="display:flex;align-items:center;gap:16px"><div class="phone" data-theme="light" style="width:auto;height:auto;background:none;box-shadow:none;--fs:1">${kvittMark(56)}</div><div><div style="font-size:14px;color:#40484C;letter-spacing:.5px">KVITT · UX-RIKTNING 1</div><h1>Skärgård</h1></div></div>
|
||
<p class="idea">Stockholms skärgård en stilla dag: Material 3 Expressive med hav-och-granit-palett, stora rundade former och ett stort saldo som hjälte. Lekfullt men tyst.</p>
|
||
<p>Varje skärm har få element och mycket luft. Översikten domineras av det globala saldot: orden ovanför ("Du är skyldig Anna"), beloppet i en smal, tung Roboto Flex-siffra och båda avatarerna i en Expressive "cookie"-form. Bakom saldot ligger ett diskret skärgårdsmotiv (granitholmar och dyning) i ytans egna toner. Konton och inställningar är segmenterade grupper med stora ytterhörn (20 dp) och små innerhörn (4 dp). Utgiftseditorn är byggd för tio sekunder: beloppet först i displaystorlek, ett eget siffertangentbord med Expressive-tangenter och alla standardval som chips direkt ovanför. Färgen bär lite: havsblå primärfärg, granitgrå sekundär, en varm lav/sand-accent som bara används för uppdateringar, väntande synk och diagram.</p>
|
||
|
||
<h2>Färger</h2>
|
||
<p>Alla färger är roller (CSS custom properties per tema; Compose: <code>ColorScheme</code> + en <code>KvittColors</code>-CompositionLocal för de semantiska rollerna). Kontrast beräknas här i JavaScript mot ytan rollen används på. Röd = under 4,5:1 för text eller 3:1 för grafik.</p>
|
||
<p><b>Skyldig/fordran utan rött/grönt:</b> "Anna är skyldig dig" använder havsgrön-blå <code>--owed</code>, "Du är skyldig Anna" använder korall/rost <code>--owing</code>. Paret skiljer sig i både nyans (blå-grön mot orange) och ljushet, så det håller för protanopi och deuteranopi; riktningen står dessutom alltid i ord.</p>
|
||
${colourTable()}
|
||
<h3>Kategorifärger (§9.7)</h3>
|
||
<p>Utgår från briefens baslinje. Ljust tema: green, orange, yellow, teal och grey är något mörkare än baslinjen så att alla klarar 4,5:1 även mot <code>--surface-container-high</code> (Skärgårds kalla grå ytor är mörkare än baslinjens referens). Mörkt tema: baslinjen oförändrad. Behållaren bakom ikonen = ${CAT_MIX.light*100} % av färgen över <code>--surface</code> (ljust) och ${CAT_MIX.dark*100} % (mörkt); ikonen fylls (FILL 1).</p>
|
||
${catTable()}
|
||
<h3>Diagram</h3>
|
||
<p><code>--chart-cat-*</code> används för konton i ordningen Vardag, Huset, Japan 2027, Gotland 2026 …; kategorier använder sina egna kategorifärger. Staplade segment skiljs av 2 dp mellanrum så att färgen aldrig är enda gränsen.</p>
|
||
${chartTable()}
|
||
<h3>Dynamisk färg (Material You)</h3>
|
||
<p>Erbjuds som ett val i Inställningar › Profil (av som standard). Riktningen bygger helt på roller, så en bakgrundsbild-palett byter bara havsblått mot användarens färg; ytor, former, typografi och saldots komposition bär karaktären. <b>Undantag som alltid behåller Skärgårds värden:</b> <code>--owed</code>/<code>--owing</code>/<code>--warning</code>/<code>--pending</code>, kategorifärgerna och diagramfärgerna, eftersom deras betydelse och kontrast inte får bero på en tapet.</p>
|
||
|
||
<h2>Typografi</h2>
|
||
<p><b>Roboto Flex</b> (SIL Open Font License 1.1, Google Fonts; kan buntas som variabel <code>res/font</code>-resurs med <code>FontVariation.Settings</code>). Varför: det är Androids eget formspråk, har tabellsiffror (<code>tnum</code>), och axlarna bredd (wdth 25–151), vikt och optisk storlek låter saldot bli uttrycksfullt utan en andra familj. Bredd/vikt används <b>bara</b> för belopp (amount-hero wdth 84 / vikt 720, amount-large wdth 92 / 650); allt annat är standard-Roboto-proportioner. Alla belopp: <code>font-variant-numeric: tabular-nums</code> (Compose: <code>fontFeatureSettings = "tnum"</code>).</p>
|
||
<table><tr><th>Style</th><th>Size (sp)</th><th>Line height</th><th>Weight / axes</th><th>Used for</th></tr>${TYPE.map(t=>`<tr><td><code>${t[0]}</code></td><td class="n">${t[1]}</td><td class="n">${t[2]}</td><td>${t[3]}</td><td>${t[4]}</td></tr>`).join('')}</table>
|
||
<p><b>200 %:</b> text upp till 22 sp skalas linjärt i ramarna (×2, hårdare än Android). Rubriker (≥ 24 sp) och amount-hero följer Android 14+:s icke-linjära skalning (<code>FontScaleConverter</code>, som Compose använder automatiskt för sp), annars skulle 60 sp bli 120 dp och inget belopp skulle få plats. Belopp bryts aldrig inuti (hårt mellanslag) och kortas aldrig; de flyttar till en egen rad.</p>
|
||
|
||
<h2>Form, avstånd, upphöjning, rörelse, ikoner</h2>
|
||
<div class="cols"><div>
|
||
<h3>Form</h3>
|
||
<table><tr><th>Token</th><th>dp</th><th>Used by</th></tr>
|
||
<tr><td><code>--radius-xs</code> extraSmall</td><td class="n">4</td><td>Inner corners of segmented groups, stacked bar segments (3)</td></tr>
|
||
<tr><td><code>--radius-sm</code> small</td><td class="n">8</td><td>Inner corners of connected button groups, small category tile in chips</td></tr>
|
||
<tr><td><code>--radius-md</code> medium</td><td class="n">12</td><td>Category icon container (40 dp), skeletons</td></tr>
|
||
<tr><td><code>--radius-lg</code> large</td><td class="n">16</td><td>Text fields, menus, small FAB, pressed keypad key</td></tr>
|
||
<tr><td><code>--radius-lg-inc</code> largeIncreased</td><td class="n">20</td><td>Outer corners of segmented lists, medium FAB, keypad function keys, banners in content</td></tr>
|
||
<tr><td><code>--radius-xl</code> extraLarge</td><td class="n">28</td><td>Cards, dialogs, split section</td></tr>
|
||
<tr><td><code>--radius-xl-inc</code> extraLargeIncreased</td><td class="n">32</td><td>Balance hero, invite card</td></tr>
|
||
<tr><td><code>--radius-xxl</code> extraExtraLarge</td><td class="n">48</td><td>Bottom sheets (date picker), not used in frames</td></tr>
|
||
<tr><td><code>--radius-full</code> full</td><td class="n">50 %</td><td>Buttons, chips, digit keys, nav indicator, avatars in lists</td></tr></table>
|
||
<p>MaterialShapes: <b>Cookie9Sided</b> for large avatars (≥ 40 dp), category hero and confirmation icons; <b>Cookie12Sided</b> for the Kvitt mark; <b>Clover4Leaf</b> for empty-state art. Stable fallback: <code>CircleShape</code> (avatars) and <code>RoundedCornerShape(28.dp)</code> (art).</p>
|
||
<h3>Spacing (dp)</h3>
|
||
<table><tr><th>Token</th><th>dp</th></tr>${[[1,4],[2,8],[3,12],[4,16],[5,20],[6,24],[8,32],[10,40],[12,48],[16,64]].map(([a,b])=>`<tr><td><code>--space-${a}</code></td><td class="n">${b}</td></tr>`).join('')}</table>
|
||
<p>Side gutter 16; card padding 20 (hero 20/24); 2 dp between segmented items; 12–16 between sections.</p>
|
||
</div><div>
|
||
<h3>Elevation</h3>
|
||
<p>Tonal, not shadowed. Level 0 surface (screens, nav bar uses surface-container), level 1 surface-container-low (cards, keypad tray), level 2 surface-container (hero, segmented lists, menus + 2 dp shadow), level 3 surface-container-high (dialogs; FAB with 3 dp shadow). Scrim 40 % of <code>--scrim</code>.</p>
|
||
<h3>Motion</h3>
|
||
<table><tr><th>Token</th><th>Expressive spring (Compose MotionScheme.expressive)</th><th>Fallback (standard)</th></tr>
|
||
<tr><td>spatial-fast</td><td>damping 0.6, stiffness 800</td><td>200 ms, emphasized (0.2, 0, 0, 1)</td></tr>
|
||
<tr><td>spatial-default</td><td>damping 0.8, stiffness 380</td><td>300 ms, emphasized</td></tr>
|
||
<tr><td>spatial-slow</td><td>damping 0.8, stiffness 200</td><td>500 ms, emphasized</td></tr>
|
||
<tr><td>effects-fast</td><td>damping 1.0, stiffness 3800</td><td>150 ms, standard (0.2, 0, 0, 1)</td></tr>
|
||
<tr><td>effects-default</td><td>damping 1.0, stiffness 1600</td><td>200 ms, standard</td></tr>
|
||
<tr><td>effects-slow</td><td>damping 1.0, stiffness 800</td><td>300 ms, standard</td></tr></table>
|
||
<p>Principles: springs only for things the finger moves (key press morphs round → square 16 dp, button groups widen the pressed segment, the Delning section expands from its chip, FAB collapses to icon on scroll). The hero amount counts to a new value over effects-slow; colour changes cross-fade. Screen transitions: shared-axis X for push, fade-through between nav destinations. Reduced motion: springs replaced by 0 ms snaps except fades.</p>
|
||
<h3>Iconography</h3>
|
||
<p>Material Symbols <b>Rounded</b>, weight 400, grade 0, optical size 24 (20 for 18–20 dp). Fill 1 for: selected navigation item, category icons, status/error icons; fill 0 elsewhere. Icon size 24 dp in bars/lists, 18 in chips and buttons, 16 inline with text. Every icon-only control carries a Swedish <code>aria-label</code> (TalkBack <code>contentDescription</code>).</p>
|
||
</div></div>
|
||
|
||
<h2>Saldohuvudet i tre lägen</h2>
|
||
<div class="heroes">${['owing','owed','settled'].map(s=>heroBox('light',s)).join('')}</div>
|
||
<div class="heroes" style="margin-top:16px">${['owing','owed','settled'].map(s=>heroBox('dark',s)).join('')}</div>
|
||
<p class="note">Ord först, belopp i amount-hero i semantisk färg, Gör upp (filled, 56 dp) bara när saldot inte är noll. Kvitt-läget byter beloppet mot "Ni är kvitt" med en cookie-bock i granitgrått.</p>
|
||
|
||
<h2>Material 3 Expressive-komponenter och stabila reserver</h2>
|
||
<table><tr><th>Expressive component</th><th>Where</th><th>Stable Material 3 fallback</th></tr>
|
||
<tr><td>ShortNavigationBar (64 dp)</td><td>Bottom navigation</td><td>NavigationBar (80 dp)</td></tr>
|
||
<tr><td>Medium extended FAB (80 dp, 20 dp corners)</td><td>Home "Lägg till utgift"</td><td>ExtendedFloatingActionButton (56 dp)</td></tr>
|
||
<tr><td>Small extended FAB</td><td>Account detail, categories, recurring</td><td>ExtendedFloatingActionButton</td></tr>
|
||
<tr><td>ButtonGroup, connected toggle buttons</td><td>Analytics: Månad/Vecka, Hushållet/Min del, Kategori/Konto; "Vem står för kostnaden"</td><td>SingleChoiceSegmentedButtonRow</td></tr>
|
||
<tr><td>LoadingIndicator (contained)</td><td>Home first sync</td><td>CircularProgressIndicator</td></tr>
|
||
<tr><td>Wavy LinearProgressIndicator</td><td>Update download</td><td>LinearProgressIndicator</td></tr>
|
||
<tr><td>MaterialShapes (Cookie9, Cookie12, Clover4)</td><td>Avatars ≥ 40 dp, icons, art</td><td>CircleShape / RoundedCornerShape</td></tr>
|
||
<tr><td>Slider with inset handle and stop indicator</td><td>Account editor</td><td>Slider (2023 style)</td></tr>
|
||
<tr><td>Segmented list (Expressive grouped ListItem shapes)</td><td>Accounts, settings, details</td><td>ListItem in Card with dividers</td></tr>
|
||
<tr><td>Menu with supporting text, 16 dp corners</td><td>Account overflow</td><td>DropdownMenu + DropdownMenuItem</td></tr>
|
||
<tr><td>MotionScheme.expressive()</td><td>All</td><td>MotionScheme.standard()</td></tr>
|
||
<tr><td>Button sizes (S 40, M 56) with shape morph on press</td><td>All buttons</td><td>Button (40 dp), no morph</td></tr></table>
|
||
<p>Custom (no Material component): the in-app numeric keypad (a <code>LazyVerticalGrid</code> of 12 <code>Surface</code> buttons, shape animated on press) and the balance hero with its archipelago <code>Canvas</code> decoration.</p>
|
||
|
||
<h2>Skärmar</h2>
|
||
${groups.map(G=>`<h3>${G.items[0].title.split(/[ ,]/)[0]} ${G.items[0].title.replace(/^\S+\s/,'').split(',')[0]}</h3>${G.items.map(f=>`<div class="frames">${f.themes.map(th=>`<div class="fr">${renderPhone(f,th)}<div class="cap"><b>${f.title}</b> · ${th}${f.note?'<br>'+f.note:''}</div></div>`).join('')}</div>`).join('')}`).join('')}
|
||
</div>`;
|
||
}
|
||
|
||
/* =====================================================================
|
||
Boot: URL modes
|
||
===================================================================== */
|
||
(function(){
|
||
const q=new URLSearchParams(location.search);
|
||
const root=document.getElementById('root');
|
||
if(q.get('list')){
|
||
document.body.innerHTML='<pre id="frame-list">'+FRAMES.flatMap(f=>f.themes.map(t=>f.id+'-'+t)).join('\n')+'</pre>';
|
||
return;
|
||
}
|
||
const fr=q.get('frame');
|
||
if(fr){
|
||
document.body.classList.add('frame-mode');
|
||
const m=fr.match(/^(.*)-(light|dark)$/);
|
||
const f=m&&FRAMES.find(x=>x.id===m[1]);
|
||
root.innerHTML=f?renderPhone(f,m[2]):'<p>Unknown frame</p>';
|
||
return;
|
||
}
|
||
root.innerHTML=reviewPage();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|