Bug/gh issues fiz (#1103)

* refactor: optimize page loading and data fetching

* fix: resolve recurring production runtime errors

* feat: add MCP company and customer updates

* fix: handle year-end tax adjustments

* feat: harden annual report compliance

* fix: expand invoice logo and font support

* fix: sanitize API route error responses

* fix: sanitize user-facing error messages

* feat: persist onboarding and tax assessment notices

* fix: reduce cloud backup audit churn

* feat: refine invoice editor layout

* fix: show saved tax adjustments in INK2

* fix: complete annual report API mappings

* docs: record operational safeguards and decisions

* fix: harden annual report review findings

* fix: adjust column span for description based on VAT registration

* New css class name
This commit is contained in:
Mattsson
2026-07-21 23:00:15 +02:00
committed by GitHub
parent 702512437a
commit e11f70b347
467 changed files with 17569 additions and 2221 deletions
+305
View File
@@ -0,0 +1,305 @@
<!doctype html>
<html lang="sv">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PROTOTYP — Kundfaktura redesign (round 9: back to clean editorial, ALL fields)</title>
<!--
THROWAWAY PROTOTYPE — /grilling-frontend-prototyping.
ROUND 9 — back to the clean editorial language of rounds 2-3 (which Emil
liked), now carrying EVERY field. No new borders, no nested boxes, no chips,
no emoji, no ⋮ clutter. Advanced per-line bits (ROT/RUT, periodisering, konto)
render as a quiet indented line with one thin rule. Three early favourites,
all complete: Faktabladet, Rubrik, Stegduk. Opens on Fullt to verify nothing
is missing.
-->
<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=Hedvig+Letters+Serif:opsz@12..24&family=Inter:wght@400;450;500;600&display=swap" rel="stylesheet">
<style>
:root{
--bg:#ffffff; --fg:#1c1b19; --muted:#6f6b64; --faint:#a19c92;
--secondary-60:#f5f1e9; --border:#e3ded4; --border-strong:#cfc9bd;
--primary:#1c1b19; --primary-fg:#faf9f6;
--sans:'Inter',system-ui,-apple-system,Segoe UI,sans-serif; --serif:'Hedvig Letters Serif',Georgia,serif;
}
*{box-sizing:border-box} html,body{margin:0;height:100%}
body{background:#fff;color:var(--fg);font-family:var(--sans);font-size:14px;line-height:1.55;-webkit-font-smoothing:antialiased}
.tnum{font-variant-numeric:tabular-nums} .serif{font-family:var(--serif);font-weight:400;letter-spacing:-0.012em}
.muted{color:var(--muted)} .faint{color:var(--faint)} button{font-family:inherit}
.app{display:grid;grid-template-columns:248px 1fr;height:100vh;overflow:hidden}
.side{border-right:1px solid var(--border);padding:22px 16px;display:flex;flex-direction:column;gap:2px;background:#fff}
.brand{font-family:var(--serif);font-size:20px;letter-spacing:-0.02em;padding:6px 10px 20px}
.nav a{display:flex;align-items:center;gap:11px;padding:8px 10px;border-radius:8px;color:var(--muted);text-decoration:none;font-size:13.5px}
.nav a .dotnav{width:5px;height:5px;border-radius:50%;background:var(--border-strong)}
.nav a.active{background:#f1ede5;color:var(--fg)} .nav a.active .dotnav{background:var(--fg)}
.nav a:hover:not(.active){background:#f7f4ee} .side .spacer{flex:1}
.who{display:flex;align-items:center;gap:10px;padding:10px;border-top:1px solid var(--border);margin-top:8px}
.avatar{width:26px;height:26px;border-radius:50%;background:#eae5db;display:grid;place-items:center;font-size:11px;color:var(--muted)}
.main{overflow:auto;position:relative} .stage{min-height:100%;background:#faf8f4}
.inp,select.inp{width:100%;border:1px solid var(--border-strong);border-radius:8px;background:#fff;height:40px;padding:0 12px;font-family:inherit;font-size:14px;color:var(--fg)}
select.inp{appearance:none;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a19c92' stroke-width='2'><path d='M6 9l6 6 6-6'/></svg>");background-repeat:no-repeat;background-position:right 11px center;padding-right:30px;cursor:pointer}
.inp:focus,select.inp:focus{outline:0;border-color:var(--fg);box-shadow:0 0 0 3px rgba(28,27,25,.08)}
.inp::placeholder{color:var(--faint)} textarea.inp{height:auto;min-height:56px;padding:10px 12px;resize:vertical;line-height:1.55}
.lbl{display:block;font-size:12px;color:var(--muted);margin-bottom:6px} .req{color:#b4533f;margin-left:2px}
.btn{display:inline-flex;align-items:center;justify-content:center;height:44px;padding:0 22px;border-radius:9px;border:0;background:var(--primary);color:var(--primary-fg);font-size:14px;font-weight:500;cursor:pointer} .btn:hover{background:#322e26}
.obtn{display:inline-flex;align-items:center;justify-content:center;height:44px;padding:0 18px;border-radius:9px;border:1px solid var(--border-strong);background:#fff;color:var(--fg);font-size:14px;cursor:pointer} .obtn:hover{background:#f7f4ee}
.tbtn{background:0;border:0;color:var(--muted);font-size:13.5px;cursor:pointer;padding:8px 0} .tbtn:hover{color:var(--fg)}
.doclabel{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}
.surface{margin:0 auto;background:#fff;border:1px solid var(--border);border-radius:14px}
.wrap{padding:40px 24px 60px}
.sech{font-family:var(--serif);font-size:16px}
.metagrid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:18px 22px}
.tabs{display:flex;gap:22px} .tabs button{background:0;border:0;padding:5px 0;font-size:14px;color:var(--muted);cursor:pointer;border-bottom:1.5px solid transparent} .tabs button.on{color:var(--fg);border-bottom-color:var(--fg)}
/* clean line list (borderless cells, hairline rows) */
.lg{width:100%;border-collapse:collapse}
.lg th{font-weight:400;color:var(--faint);font-size:11px;text-align:left;padding:0 12px 10px 0}
.lg thead tr{border-bottom:1px solid var(--border)} .lg th.r,.lg td.r{text-align:right}
.lg td{padding:3px 12px 3px 0;border:0;vertical-align:middle} .lg tbody tr.l{border-bottom:1px solid var(--border)}
.lg .c{border:0;background:transparent;width:100%;padding:11px 0;font-family:inherit;font-size:14px;color:var(--fg)}
.lg .c.r{text-align:right;font-variant-numeric:tabular-nums} .lg .c:focus{outline:0;box-shadow:0 -2px 0 var(--fg) inset}
.lg .amt{padding:11px 0;font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap}
.lg .rm{opacity:0;color:var(--faint);cursor:pointer;font-size:15px} .lg tr.l:hover .rm{opacity:1}
.art{font-family:ui-monospace,Menlo,monospace;font-size:10.5px;color:var(--faint);margin-right:8px}
/* quiet advanced line: one thin left rule, no box/chip/emoji */
.adv{margin:0 0 4px;padding:6px 0 12px 14px;border-left:2px solid var(--border);display:flex;flex-wrap:wrap;align-items:flex-end;gap:14px 20px}
.advk{font-size:12px;color:var(--muted);font-weight:500;align-self:center}
.micro{display:block;font-size:11px;color:var(--faint);margin-bottom:3px}
.ins,select.ins{border:1px solid var(--border-strong);border-radius:7px;height:34px;padding:0 10px;font-size:13px;background:#fff;font-family:inherit;color:var(--fg)}
select.ins{appearance:none;padding-right:24px;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='11' height='11' viewBox='0 0 24 24' fill='none' stroke='%23a19c92' stroke-width='2'><path d='M6 9l6 6 6-6'/></svg>");background-repeat:no-repeat;background-position:right 8px center}
.advamt{font-size:12.5px;color:var(--muted);align-self:center} .advnote{width:100%;font-size:12px;color:var(--muted)}
.addrow{display:flex;gap:20px;margin-top:14px} .addrow .tbtn{padding-top:4px}
.sums{width:290px}
.sumrow{display:flex;justify-content:space-between;padding:5px 0;font-size:13.5px}
.sumtot{display:flex;justify-content:space-between;align-items:baseline;margin-top:10px;padding-top:14px;border-top:1px solid var(--border)} .sumtot .val{font-family:var(--serif);font-size:27px}
.sw{width:38px;height:22px;border-radius:999px;background:var(--border-strong);position:relative;cursor:pointer;flex:none} .sw.on{background:var(--fg)} .sw::after{content:'';position:absolute;width:18px;height:18px;border-radius:50%;background:#fff;top:2px;left:2px;transition:.15s} .sw.on::after{left:18px}
.step{padding:24px 34px;border-top:1px solid var(--border)} .stephead{display:flex;align-items:center;gap:10px;margin-bottom:16px}
.stepnum{width:22px;height:22px;border-radius:50%;background:var(--secondary-60);display:grid;place-items:center;font-size:12px;font-variant-numeric:tabular-nums}
.picker{position:fixed;right:22px;bottom:22px;width:344px;z-index:9999;background:#141311;color:#f4f0e8;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,.34);font-size:13px;user-select:none}
.picker-h{display:flex;align-items:center;gap:8px;padding:10px 12px;cursor:grab;border-bottom:1px solid #2a2823} .picker-h .dot{width:8px;height:8px;border-radius:50%;background:#c9a24a} .picker-h b{font-weight:600}
.picker-b{padding:12px} .picker-nav{display:flex;align-items:center;gap:10px;margin-bottom:8px}
.picker-nav button{width:34px;height:34px;border-radius:8px;border:1px solid #35322b;background:#211f1b;color:#f4f0e8;font-size:16px;cursor:pointer;display:grid;place-items:center} .picker-nav button:hover{background:#2c2924}
.picker-name{flex:1;text-align:center} .picker-name .n{font-family:var(--serif);font-size:16px} .picker-name .k{font-size:11px;color:#a49d8d;letter-spacing:.08em}
.picker-desc{color:#b8b1a1;font-size:12px;line-height:1.45;min-height:60px}
.picker-states{display:flex;gap:6px;margin-top:10px;border-top:1px solid #2a2823;padding-top:10px}
.picker-states button{flex:1;height:30px;border-radius:7px;border:1px solid #35322b;background:#211f1b;color:#cdc7b8;cursor:pointer;font-size:12px}
.picker-states button.on{background:#f4f0e8;color:#141311;border-color:#f4f0e8;font-weight:600} .picker-count{font-size:11px;color:#8f887a;margin-left:auto}
</style>
</head>
<body>
<div class="app">
<aside class="side">
<div class="brand">Accounted</div>
<nav class="nav">
<a href="#"><span class="dotnav"></span>Översikt</a>
<a href="#" class="active"><span class="dotnav"></span>Fakturor</a>
<a href="#"><span class="dotnav"></span>Kvitton</a>
<a href="#"><span class="dotnav"></span>Bokföring</a>
<a href="#"><span class="dotnav"></span>Rapporter</a>
<a href="#"><span class="dotnav"></span>Moms &amp; skatt</a>
<a href="#"><span class="dotnav"></span>Inställningar</a>
</nav>
<div class="spacer"></div>
<div class="who"><div class="avatar">KS</div><div style="font-size:12.5px"><div>Kvist Studio AB</div><div class="faint" style="font-size:11px">emil@kviststudio.se</div></div></div>
</aside>
<main class="main"><div class="stage" id="stage"></div></main>
</div>
<div class="picker" id="picker">
<div class="picker-h" id="pickerHandle"><span class="dot"></span><b>Redesign</b> · rond 9 · ren + alla fält<span class="picker-count" id="pcount"></span></div>
<div class="picker-b">
<div class="picker-nav">
<button id="prev" aria-label="Föregående">‹</button>
<div class="picker-name"><div class="n" id="pname">–</div><div class="k" id="pkey"></div></div>
<button id="next" aria-label="Nästa">›</button>
</div>
<div class="picker-desc" id="pdesc"></div>
<div class="picker-states">
<button data-state="empty">Tom</button>
<button data-state="filled">Ifylld</button>
<button data-state="full" class="on">Fullt</button>
</div>
</div>
</div>
<script>
const money=(n,dec=false)=>(n).toLocaleString('sv-SE',dec?{minimumFractionDigits:2,maximumFractionDigits:2}:{})+' kr';
const amt=n=>Number.isInteger(n)?money(n):money(n,true);
const CUST={name:'Björklund Design AB',org:'556677-8899',addr:'Kungsgatan 12, 111 43 Stockholm',ref:'Anna Björklund'};
const SIMPLE=[{desc:'Webbutveckling, sprint 3',qty:24,unit:'tim',price:950,vat:25},{desc:'UX-rådgivning',qty:6,unit:'tim',price:1200,vat:25}];
const FULLROWS=[
{art:'A102',desc:'Webbutveckling, sprint 3',qty:24,unit:'tim',price:950,vat:25,rot:true},
{desc:'UX-rådgivning',qty:6,unit:'tim',price:1200,vat:25,accrual:true},
{desc:'Serverdrift, 12 mån',qty:1,unit:'st',price:2999.60,vat:25,konto:true},
{textRow:true,desc:'Allt arbete är utfört enligt avtal 2026-05-14.'},
];
const TT={empty:{sub:0,vat:0,pay:0},filled:{sub:30000,vat:7500,pay:37500},full:{sub:32999.60,vat:8249.90,pay:34410,rot:6840,round:0.50,total:41249.50}};
const V=s=>s!=='empty';
function pv(p){return p===''?'':(Number.isInteger(p)?p:p.toLocaleString('sv-SE',{minimumFractionDigits:2,maximumFractionDigits:2}));}
function rowsFor(s){return s==='empty'?[{desc:'',qty:'',unit:'st',price:'',vat:25}]:(s==='full'?FULLROWS:SIMPLE);}
function advClean(l){
if(l.rot) return `<tr><td colspan="7" style="padding:0"><div class="adv"><span class="advk">ROT-avdrag 30 %</span>
<div><span class="micro">Arbetstyp</span><select class="ins"><option>Bygg och renovering</option></select></div>
<div><span class="micro">Timmar</span><input class="ins tnum" style="width:90px" value="24"></div>
<span class="advamt tnum">−${money(6840,true)}</span>
<span class="advnote">Endast arbetskostnad ger skattereduktion, fakturera material på egen rad.</span></div></td></tr>`;
if(l.accrual) return `<tr><td colspan="7" style="padding:0"><div class="adv"><span class="advk">Periodisering</span>
<div><span class="micro">Från</span><input class="ins tnum" style="width:120px" value="2026-07-01"></div>
<div><span class="micro">Till</span><input class="ins tnum" style="width:120px" value="2026-12-31"></div>
<div><span class="micro">Balanskonto</span><select class="ins"><option>2979 Upplupna intäkter</option></select></div></div></td></tr>`;
if(l.konto) return `<tr><td colspan="7" style="padding:0"><div class="adv"><span class="advk">Försäljningskonto</span>
<div><select class="ins" style="min-width:240px"><option>3540 Fakturerade kostnader</option></select></div>
<span class="advnote" style="width:auto;align-self:center">ersätter det momssatshärledda kontot</span></div></td></tr>`;
return '';
}
function lineList(state){
const rows=rowsFor(state).map(l=>{
if(l.textRow) return `<tr class="l"><td colspan="6"><input class="c" style="color:var(--muted);font-style:italic" value="${l.desc}" placeholder="Fritext, notering på raden"></td><td class="r"><span class="rm">×</span></td></tr>`;
const has=l.qty!==''&&l.price!==''; const lt=has?l.qty*l.price:0;
return `<tr class="l">
<td><div style="display:flex;align-items:center">${l.art?`<span class="art">${l.art}</span>`:''}<input class="c" value="${l.desc}" placeholder="Beskrivning"></div></td>
<td style="width:60px" class="r"><input class="c r" value="${l.qty}" placeholder="1"></td>
<td style="width:56px"><input class="c" value="${l.unit}"></td>
<td style="width:96px" class="r"><input class="c r" value="${pv(l.price)}" placeholder="0"></td>
<td style="width:54px" class="r"><input class="c r" value="${l.vat}%"></td>
<td style="width:116px" class="r amt">${has?amt(lt):'—'}</td>
<td style="width:20px" class="r"><span class="rm">×</span></td>
</tr>${state==='full'?advClean(l):''}`;
}).join('');
const addAdv = state==='full'?'':'<button class="tbtn" style="color:var(--faint)">Skattereduktion / periodisering</button>';
return `<table class="lg"><thead><tr><th>Beskrivning</th><th class="r">Antal</th><th>Enhet</th><th class="r">À-pris</th><th class="r">Moms</th><th class="r">Belopp</th><th></th></tr></thead><tbody>${rows}</tbody></table>
<div class="addrow"><button class="tbtn">+ Lägg till rad</button><button class="tbtn">Välj artikel</button><button class="tbtn">Fritext</button>${addAdv}</div>`;
}
function metaGrid(state){const v=V(state);return `<div class="metagrid">
<div><span class="lbl">Fakturadatum<span class="req">*</span></span><input class="inp tnum" value="${v?'2026-07-21':''}" placeholder="åååå-mm-dd"></div>
<div><span class="lbl">Förfallodatum<span class="req">*</span></span><input class="inp tnum" value="${v?'2026-08-20':''}" placeholder="+30 dagar"></div>
<div><span class="lbl">Leveransdatum</span><input class="inp tnum" value="${state==='full'?'2026-07-18':''}" placeholder="valfritt"></div>
<div><span class="lbl">Valuta</span><select class="inp"><option>SEK</option><option>EUR</option><option>USD</option></select></div>
<div><span class="lbl">Er referens</span><input class="inp" value="${v?CUST.ref:''}" placeholder="kontaktperson"></div>
<div><span class="lbl">Vår referens</span><input class="inp" value="${v?'Emil Mattsson':''}" placeholder="du"></div>
<div><span class="lbl">Betallänk</span><input class="inp" value="${state==='full'?'buy.stripe.com/…':''}" placeholder="https://…"></div>
<div><span class="lbl">Kostnadsställe</span><input class="inp" value="${state==='full'?'Konsult':''}" placeholder="valfritt"></div>
<div><span class="lbl">Projekt</span><input class="inp" value="${state==='full'?'Webb 2026':''}" placeholder="valfritt"></div>
</div>`;}
function rotClaim(state){
if(state!=='full') return '';
return `<div style="margin-top:8px"><div class="sech" style="margin-bottom:14px">Skattereduktion <span class="muted" style="font-size:12.5px">· uppgifter för Skatteverket (ROT)</span></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:18px 22px">
<div><span class="lbl">Personnummer (köpare)<span class="req">*</span></span><input class="inp" placeholder="ÅÅÅÅMMDD-XXXX"></div>
<div><span class="lbl">Fastighetsbeteckning<span class="req">*</span></span><input class="inp" value="Stockholm Väsby 1:23"></div>
</div></div>`;
}
function notes(state){return `<textarea class="inp" rows="2" placeholder="Valfri text som visas på fakturan">${state==='empty'?'':'Tack för förtroendet. Betalning sker via bankgiro 123-4567 senast på förfallodagen.'}</textarea>`;}
function summary(state){
const d=TT[state]; const m=n=>state==='full'?money(n,true):money(n);
return `<div class="sums">
<div class="sumrow"><span class="muted">Delsumma</span><span class="tnum">${m(d.sub)}</span></div>
<div class="sumrow"><span class="muted">Moms 25 %</span><span class="tnum">${m(d.vat)}</span></div>
${d.round?`<div class="sumrow"><span class="muted">Öresavrundning</span><span class="tnum">+${money(d.round,true)}</span></div>`:''}
${d.rot?`<div class="sumrow"><span class="muted">ROT-avdrag</span><span class="tnum">−${money(d.rot,true)}</span></div>`:''}
<div class="sumtot"><span>Att betala</span><span class="val tnum">${money(d.pay)}</span></div>
${d.rot?`<div class="sumrow" style="margin-top:6px"><span class="muted">Total inkl. moms</span><span class="tnum">${money(d.total,true)}</span></div>`:''}
<div class="sumrow" style="margin-top:14px;align-items:center"><span class="muted">Öresavrundning</span><span class="sw ${V(state)?'on':''}"></span></div>
</div>`;
}
const actions=`<div style="display:flex;gap:12px;align-items:center"><button class="btn">Granska &amp; skapa</button><button class="obtn">Spara utkast</button><button class="tbtn">Förhandsgranska</button></div>`;
const custBlock=(state)=> state==='empty'
? `<input class="inp" style="height:44px;max-width:440px;font-size:15px" placeholder="Sök kund, eller skapa ny">`
: `<div style="display:flex;justify-content:space-between;align-items:flex-start"><div><div style="font-weight:500;font-size:15px">${CUST.name}</div><div class="muted" style="font-size:12.5px">${CUST.org} · ${CUST.addr}</div></div><button class="tbtn">Byt kund</button></div>`;
/* A — Faktabladet (letterhead) */
function vLetter(state){
return `<div class="wrap"><div class="surface" style="max-width:880px;padding:44px 48px 40px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:30px">
<div class="tabs"><button class="on">Faktura</button><button>Proforma</button><button>Följesedel</button></div>
</div>
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:34px">
<div><div class="serif" style="font-size:21px">Kvist Studio AB</div><div class="muted" style="margin-top:4px;font-size:12.5px">Hornsgatan 1, 118 20 Stockholm · Org 559111-2233</div></div>
<div style="text-align:right"><div class="doclabel" style="letter-spacing:.22em">Faktura</div><div class="serif tnum" style="font-size:26px">F2026-014</div></div>
</div>
<div style="margin-bottom:26px"><div class="doclabel" style="margin-bottom:10px">Faktureras till</div>${custBlock(state)}</div>
<div style="margin-bottom:30px">${metaGrid(state)}</div>
<div class="sech" style="margin-bottom:12px">Rader</div>
${lineList(state)}
${state==='full'?`<div style="margin-top:26px">${rotClaim(state)}</div>`:''}
<div style="display:grid;grid-template-columns:1fr 300px;gap:36px;margin-top:32px">
<div><div class="sech" style="margin-bottom:12px">Anteckningar</div>${notes(state)}</div>
<div>${summary(state)}</div>
</div>
<hr style="border:0;border-top:1px solid var(--border);margin:28px 0 20px">${actions}
</div></div>`;
}
/* B — Rubrik (editorial title) */
function vTitle(state){
return `<div class="wrap"><div class="surface" style="max-width:880px;padding:0">
<div style="padding:30px 40px 24px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:flex-end">
<div><div class="doclabel" style="margin-bottom:8px">Faktura F2026-014</div>
${V(state)?`<div class="serif" style="font-size:28px">${CUST.name}</div><div class="muted" style="font-size:12.5px;margin-top:2px">${CUST.org} · ${CUST.addr}</div>`:`<div class="serif" style="font-size:28px;color:var(--faint)">Ny faktura</div>`}</div>
<div class="tabs" style="padding-bottom:4px"><button class="on">Faktura</button><button>Proforma</button><button>Följesedel</button></div>
</div>
${state==='empty'?`<div style="padding:20px 40px 0">${custBlock(state)}</div>`:''}
<div style="padding:24px 40px 0"><div class="doclabel" style="margin-bottom:14px">Detaljer</div>${metaGrid(state)}</div>
<div style="padding:24px 40px 0"><div class="sech" style="margin-bottom:12px">Rader</div>${lineList(state)}</div>
${state==='full'?`<div style="padding:26px 40px 0">${rotClaim(state)}</div>`:''}
<div style="padding:26px 40px 0"><div class="sech" style="margin-bottom:12px">Anteckningar</div>${notes(state)}</div>
<div style="padding:28px 40px 34px;margin-top:20px;border-top:1px solid var(--border);display:flex;justify-content:space-between;align-items:flex-end;gap:32px">
<div>${actions}</div><div>${summary(state)}</div>
</div>
</div></div>`;
}
/* C — Stegduk (numbered steps) */
function vSteps(state){
const step=(n,label,body,hint='')=>`<div class="step"><div class="stephead"><span class="stepnum">${n}</span><span class="sech">${label}</span>${hint?`<span class="muted" style="font-size:12px">${hint}</span>`:''}</div>${body}</div>`;
return `<div class="wrap"><div class="surface" style="max-width:820px;padding:0">
<div style="padding:24px 34px 8px;display:flex;justify-content:space-between;align-items:center"><div class="doclabel">Ny kundfaktura · F2026-014</div>
<div class="tabs"><button class="on">Faktura</button><button>Proforma</button><button>Följesedel</button></div></div>
${step(1,'Kund', custBlock(state))}
${step(2,'Rader', lineList(state))}
${step(3,'Villkor', metaGrid(state))}
${step(4,'Skattereduktion', state==='full'?rotClaim(state).replace('<div style="margin-top:8px">','<div>'):`<div class="muted" style="font-size:12.5px">Inga ROT/RUT-avdrag. Läggs till per rad under Rader.</div>`, state==='full'?'':'valfritt')}
${step(5,'Anteckningar', notes(state),'valfritt')}
<div class="step" style="display:flex;justify-content:space-between;align-items:flex-end;gap:32px"><div>${actions}</div><div>${summary(state)}</div></div>
</div></div>`;
}
const VARIANTS=[
{ key:'faktabladet', name:'Faktabladet', render:vLetter,
desc:'Fakturablad med brevhuvud: logga/org uppe, nr till höger, kund, sedan villkoren i ett luftigt fältrutnät, rader, summa. Alla fält, dokumentkänsla.' },
{ key:'rubrik', name:'Rubrik', render:vTitle,
desc:'Kundnamnet som stor serif-rubrik, all meta i en lugn "Detaljer"-yta, rena rader. Redaktionellt och lätt att läsa. Håller allt utan att kännas fullt.' },
{ key:'stegduk', name:'Stegduk', render:vSteps,
desc:'Numrerade steg: Kund → Rader → Villkor → Skattereduktion → Anteckningar → Summa. Varje fält får sin plats, inget trängs. Tydligast.' },
];
let idx=0, state='full';
const stage=document.getElementById('stage');
function draw(){
const v=VARIANTS[idx];
stage.innerHTML=v.render(state);
document.getElementById('pname').textContent=v.name;
document.getElementById('pkey').textContent=v.key.toUpperCase();
document.getElementById('pdesc').textContent=v.desc;
document.getElementById('pcount').textContent=` ${idx+1}/${VARIANTS.length}`;
stage.parentElement.scrollTop=0;
}
function go(d){ idx=(idx+d+VARIANTS.length)%VARIANTS.length; draw(); }
document.getElementById('prev').onclick=()=>go(-1);
document.getElementById('next').onclick=()=>go(1);
document.querySelectorAll('.picker-states button').forEach(b=>{ b.onclick=()=>{ state=b.dataset.state; document.querySelectorAll('.picker-states button').forEach(x=>x.classList.toggle('on',x===b)); draw(); }; });
document.addEventListener('keydown',e=>{ const t=e.target.tagName; if(t==='INPUT'||t==='TEXTAREA'||t==='SELECT'||e.target.isContentEditable) return; if(e.key==='ArrowLeft') go(-1); if(e.key==='ArrowRight') go(1); });
(function(){ const p=document.getElementById('picker'),h=document.getElementById('pickerHandle'); let dx=0,dy=0,drag=false;
h.addEventListener('mousedown',e=>{drag=true;dx=e.clientX-p.offsetLeft;dy=e.clientY-p.offsetTop;p.style.right='auto';p.style.bottom='auto';h.style.cursor='grabbing';});
document.addEventListener('mousemove',e=>{if(!drag)return;p.style.left=(e.clientX-dx)+'px';p.style.top=(e.clientY-dy)+'px';});
document.addEventListener('mouseup',()=>{drag=false;h.style.cursor='grab';}); })();
draw();
</script>
</body>
</html>
@@ -0,0 +1,281 @@
<!doctype html>
<html lang="sv">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PROTOTYPE — R4 finish (F1–F4)</title>
<!--
THROWAWAY PROTOTYPE. Round 5 (leaf): base = R4 (split rows, borderless,
grouped economy) with a FULL aligned header incl. Beskrivning + Belopp.
Body + header locked; only the add-row affordance and row-actions vary (2x2).
Switch: draggable picker (bottom-right) or ← / → keys.
-->
<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=Hedvig+Letters+Serif:opsz@12..24&family=Geist:wght@400;450;500;600&display=swap" rel="stylesheet">
<style>
:root{
--bg:#f4f3ef;--sheet:#ffffff;--fg:#211f1b;--muted:#7c766d;--faint:#a6a096;
--border:#e7e3db;--border-strong:#d9d4ca;--field-border:#dcd7cd;
--beige:#e9e5dc;--beige-hover:#f0ede6;--beige-soft:#f4f1ea;
--success:#5f7355;--destructive:#b0503a;--r-sm:6px;
--serif:'Hedvig Letters Serif',Georgia,serif;--sans:'Geist','Inter',system-ui,sans-serif;
--ring:0 0 0 3px rgba(120,113,108,.16);
}
*{box-sizing:border-box}html,body{margin:0;padding:0}
body{background:var(--bg);color:var(--fg);font-family:var(--sans);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;padding:40px 24px 150px}
.stagehead{display:flex;align-items:baseline;gap:12px;margin:0 auto 20px;max-width:1120px}
.stagehead h1{font-family:var(--serif);font-weight:400;font-size:20px;margin:0}.stagehead .sub{color:var(--muted);font-size:13px}
.stage{max-width:1120px;margin:0 auto}
.dialog{position:relative;background:var(--sheet);border:1px solid var(--border);border-radius:14px;box-shadow:0 22px 60px -28px rgba(30,27,22,.28),0 2px 8px -4px rgba(30,27,22,.12);overflow:hidden}
label{display:block;font-size:12.5px;color:var(--muted);margin-bottom:6px;font-weight:450}
.req{color:var(--destructive);margin-left:2px}
input,select,textarea{font-family:var(--sans);font-size:13.5px;color:var(--fg);width:100%;background:var(--sheet);border:1px solid var(--field-border);border-radius:var(--r-sm);height:38px;padding:0 11px;outline:none;transition:border-color .15s,box-shadow .15s}
textarea{height:auto;padding:9px 11px;resize:vertical;min-height:64px;line-height:1.55}
select{appearance:none;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%237c766d' stroke-width='2'><path d='M6 9l6 6 6-6'/></svg>");background-repeat:no-repeat;background-position:right 10px center;padding-right:30px;cursor:pointer}
input:focus,select:focus,textarea:focus{border-color:var(--border-strong);box-shadow:var(--ring)}
.num{text-align:right;font-variant-numeric:tabular-nums}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;height:38px;padding:0 15px;border-radius:var(--r-sm);border:1px solid transparent;background:transparent;font-family:var(--sans);font-size:13.5px;font-weight:500;color:var(--fg);cursor:pointer;transition:.15s;white-space:nowrap}
.btn svg{width:15px;height:15px}
.btn.primary{background:var(--fg);color:#fbfaf7;border-color:var(--fg)}.btn.primary:hover{background:#000}
.btn.secondary{background:var(--beige);color:var(--fg)}.btn.secondary:hover{background:var(--beige-hover)}
.btn.outline{border-color:var(--field-border);background:var(--sheet)}.btn.outline:hover{background:var(--beige-soft)}
.btn.ghost{color:var(--muted)}.btn.ghost:hover{background:var(--beige-soft);color:var(--fg)}
.btn.sm{height:32px;padding:0 11px;font-size:12.5px}
.iconbtn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:var(--r-sm);border:none;background:transparent;color:var(--muted);cursor:pointer;transition:.15s}
.iconbtn:hover{background:var(--beige-hover);color:var(--fg)}
.iconbtn.del:hover{color:var(--destructive)}
.tabs{display:inline-flex;background:var(--beige-soft);border:1px solid var(--border);border-radius:8px;padding:3px;gap:3px}
.tabs button{border:none;background:transparent;font-size:13px;font-weight:500;color:var(--muted);padding:6px 14px;border-radius:6px;cursor:pointer;font-family:var(--sans)}
.tabs button.on{background:var(--sheet);color:var(--fg);box-shadow:0 1px 2px rgba(30,27,22,.08)}
.seclabel{font-size:11px;font-weight:600;letter-spacing:.09em;text-transform:uppercase;color:var(--faint)}
.pill{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;color:var(--muted);background:var(--sheet);border:1px solid var(--field-border);border-radius:8px;padding:8px 12px;cursor:pointer;height:36px}
.pill:hover{background:var(--beige-soft);color:var(--fg)}.pill svg{width:13px;height:13px}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:13px}.grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:13px}
.d3top{display:flex;align-items:center;justify-content:space-between;padding:16px 26px 6px}
.d3top h2{font-family:var(--serif);font-weight:400;font-size:21px;margin:0}
.d3band{display:flex;align-items:flex-end;gap:26px;padding:10px 26px 16px;border-bottom:1px solid var(--border);flex-wrap:wrap}
.ul-lab{font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--faint);margin-bottom:3px;display:block}
.ul-cell{display:flex;flex-direction:column;gap:3px}
.ul-cell input,.ul-cell select{border:0;border-bottom:1px solid var(--field-border);border-radius:0;background:transparent;padding-left:2px;height:34px;min-width:120px}
.ul-cell input:focus,.ul-cell select:focus{box-shadow:none;border-bottom-color:var(--fg)}
.work{padding:20px 26px 12px}.addrow{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}
.menuwrap{position:relative;display:inline-block}
.menu{position:absolute;right:0;top:36px;z-index:40;min-width:224px;background:var(--sheet);border:1px solid var(--border-strong);border-radius:10px;box-shadow:0 16px 40px -18px rgba(30,27,22,.35);padding:5px;display:none}
.menu.open{display:block}
.menu .mlabel{font-size:10.5px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--faint);padding:8px 10px 4px}
.menu button.mi{display:flex;align-items:center;gap:9px;width:100%;background:none;border:none;font-family:var(--sans);font-size:13px;color:var(--fg);text-align:left;padding:8px 10px;border-radius:6px;cursor:pointer;height:auto}
.menu button.mi:hover{background:var(--beige-soft)}.menu button.mi.danger{color:var(--destructive)}
.menu .sep{height:1px;background:var(--border);margin:5px 6px}.menu svg{width:15px;height:15px;color:var(--muted)}
.menu button.mi.danger svg{color:var(--destructive)}.pop{min-width:320px;padding:16px}
.rotstrip{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:8px;padding:9px 11px;background:var(--beige-soft);border-radius:8px}
.rottag{font-size:11px;font-weight:600;color:var(--muted);font-variant-numeric:tabular-nums}
.rotstrip select,.rotstrip input{height:32px;background:var(--sheet)}
.rotstrip .amt{margin-left:auto;font-variant-numeric:tabular-nums;color:var(--success);font-weight:500}
/* ===== R4 split rows with FULL aligned header ===== */
.srow{display:flex;align-items:flex-start;gap:14px;padding:11px 6px}
.list>.srow{border-top:1px solid var(--border)}
.srow.line:hover{background:var(--beige-soft);border-radius:8px}
.headrow{border-bottom:1px solid var(--border);padding-bottom:9px;align-items:flex-end}
.headrow.srow:hover{background:transparent}
.hlab{font-size:9.5px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--faint)}
.sgrip{width:14px;flex:none;color:transparent;cursor:grab;padding-top:8px;text-align:center}
.srow.line:hover .sgrip{color:var(--faint)}
.sdesc{flex:1;min-width:130px}
.seco{display:flex;align-items:flex-start;gap:10px}
.seco .f{display:flex;flex-direction:column;gap:3px}
.seco .f>.hlab{margin-bottom:0}
.seco input,.seco select{height:34px;font-size:12.5px}
.sbel{width:104px;flex:none;text-align:right;font-variant-numeric:tabular-nums;font-family:var(--serif);font-size:16px;padding-top:6px}
.sact{width:34px;flex:none;display:flex;justify-content:flex-end;gap:2px;padding-top:1px}
.hovacts .sact{opacity:0;transition:opacity .15s}
.hovacts .srow.line:hover .sact,.hovacts .sact:has(.menu.open),.hovacts .sact.keep{opacity:1}
.textrow input{font-style:italic;color:var(--muted)}
.ghostrow{padding:2px 6px}
.ghostadd{display:flex;align-items:center;gap:8px;width:100%;background:none;border:none;color:var(--muted);font-family:var(--sans);font-size:13px;padding:10px;cursor:pointer;border-radius:6px;text-align:left}
.ghostadd:hover{background:var(--beige-soft);color:var(--fg)}.ghostadd svg{width:15px;height:15px}
.sumbar{position:sticky;bottom:0;display:flex;align-items:center;gap:20px;padding:15px 26px;border-top:1px solid var(--border-strong);background:rgba(255,255,255,.94);backdrop-filter:blur(6px)}
.sumbar .brk{color:var(--muted);font-size:12.5px;font-variant-numeric:tabular-nums;display:flex;gap:14px}
.sumbar .pay{margin-left:auto;display:flex;align-items:baseline;gap:9px}.sumbar .pay .k{font-size:12px;color:var(--muted)}
.sumbar .pay .v{font-family:var(--serif);font-size:23px;font-variant-numeric:tabular-nums}
.sumbar .acts{display:flex;gap:9px;margin-left:8px}
.warn{display:flex;gap:10px;align-items:flex-start;background:#fbf3ec;border:1px solid #efd9c6;border-radius:8px;padding:11px 13px;font-size:13px;color:#8a5a2f;margin:14px 26px 0}
.warn svg{width:16px;height:16px;flex:none;margin-top:2px}
#picker{position:fixed;right:22px;bottom:22px;z-index:100;width:290px;background:#1c1a17;color:#f4f1ea;border-radius:14px;box-shadow:0 18px 48px -16px rgba(0,0,0,.55);font-family:var(--sans);user-select:none}
#picker .ph{display:flex;align-items:center;gap:10px;padding:12px 14px;cursor:grab}
#picker .dot{width:8px;height:8px;border-radius:50%;background:#c9b8a3}#picker .ph b{font-size:12.5px;font-weight:600}#picker .ph .grip{margin-left:auto;color:#6b6459;letter-spacing:2px}
#picker .nav{display:flex;align-items:center;gap:10px;padding:2px 12px 12px}
#picker .arrow{width:34px;height:34px;border-radius:8px;border:1px solid #3a352e;background:#26231e;color:#f4f1ea;font-size:16px;cursor:pointer}#picker .arrow:hover{background:#332f28}
#picker .cur{flex:1;text-align:center}#picker .cur .k{font-size:10.5px;color:#8a8378;letter-spacing:.08em}#picker .cur .n{font-family:var(--serif);font-size:16px}
#picker .toggles{border-top:1px solid #302c26;padding:11px 12px;display:flex;flex-direction:column;gap:9px}
#picker .tg{display:flex;align-items:center;gap:8px}#picker .tg .tl{font-size:11px;color:#8a8378;width:44px}
#picker .seg{display:flex;background:#26231e;border-radius:7px;padding:2px;gap:2px;flex:1}
#picker .seg button{flex:1;border:none;background:transparent;color:#a9a296;font-size:11.5px;padding:5px 4px;border-radius:5px;cursor:pointer;font-family:var(--sans)}
#picker .seg button.on{background:#413b32;color:#f7f3ec}
#picker .hintbar{font-size:10.5px;color:#6b6459;text-align:center;padding:0 12px 11px}
</style>
</head>
<body>
<div class="stagehead"><h1>R4 finish — rubrikrad + radåtgärder</h1><span class="sub">kropp låst · header med Beskrivning · lägg-till &amp; ⋮ varieras (2×2)</span></div>
<div class="stage"><div class="dialog" id="mount"></div></div>
<div id="picker">
<div class="ph" id="pgrip"><span class="dot"></span><b>Finish</b><span class="grip">⠿</span></div>
<div class="nav"><button class="arrow" id="prev">‹</button><div class="cur"><div class="k" id="curk">F1</div><div class="n" id="curn">Spökrad + ⋮</div></div><button class="arrow" id="next">›</button></div>
<div class="toggles">
<div class="tg"><span class="tl">Läge</span><div class="seg" id="segMode"><button data-m="invoice" class="on">Faktura</button><button data-m="self_billed">Självfaktura</button></div></div>
<div class="tg"><span class="tl">Moms</span><div class="seg" id="segVat"><button data-v="1" class="on">Reg.</button><button data-v="0">Ej reg.</button></div></div>
</div>
<div class="hintbar">← / → för att byta variant</div>
</div>
<script>
const CUSTOMERS=['Nordisk Design AB','Studio Ljus HB','Kaffe &amp; Co AB'];
const ARTICLES=['Egen rad','KONS-01','DES-04','LIC-12'];
const UNITS=['tim','st','dag','km','kg','mån'];
const CUR=['SEK','EUR','USD','GBP','NOK','DKK'];const VAT=['25%','12%','6%','0%'];
const ROWS=[
{desc:'Konsultarvode september',qty:'24',unit:'tim',price:'1 150,00',vat:'25%',art:'KONS-01',tot:'27 600,00',rot:false},
{desc:'Utlägg resa (SJ)',qty:'1',unit:'st',price:'480,00',vat:'25%',art:'Egen rad',tot:'480,00',rot:true},
];
let S={vi:0,mode:'invoice',vat:true};
const isSB=()=>S.mode==='self_billed';
const opts=(a,s)=>a.map((o,i)=>`<option ${((s==null&&i===0)||o===s)?'selected':''}>${o}</option>`).join('');
const sel=(a,s)=>`<select>${opts(a,s)}</select>`;
const icon={
plus:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>',
dots:'<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="12" cy="19" r="1.6"/></svg>',
trash:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/></svg>',
cal:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="17" rx="2"/><path d="M3 9h18M8 2v4M16 2v4"/></svg>',
bank:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 10l9-6 9 6M5 10v9M19 10v9M9 10v9M15 10v9M3 21h18"/></svg>',
tag:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3h8l10 10-8 8L3 11V3z"/><circle cx="7" cy="7" r="1.4"/></svg>',
send:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/></svg>',
chev:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6"/></svg>',
sliders:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16"/></svg>',
warn:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l9 16H3l9-16zM12 10v4M12 17.5v.2"/></svg>',
more:'<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg>',
};
function rotStrip(){return isSB()?'':`<div class="rotstrip"><span class="rottag">ROT 30%</span>${sel(['Måleri','Snickeri','El','VVS'],'Snickeri')}<input class="num" value="8" style="max-width:70px"><span class="amt">−1 440,00 kr</span></div>`}
/* row actions: 'menu' = full ⋮ ; 'inline' = hover trash + compact ⋯ (no delete) */
function rowActions(style){
const compact=`<div class="mlabel">Skattereduktion</div><button class="mi">ROT-avdrag</button><button class="mi">RUT-avdrag</button><div class="sep"></div><button class="mi">${icon.cal} Periodisering</button><button class="mi">${icon.bank} Eget försäljningskonto</button><button class="mi">${icon.tag} Dimensioner</button>`;
if(style==='inline'){
return `<button class="iconbtn del" title="Ta bort rad">${icon.trash}</button>
<div class="menuwrap"><button class="iconbtn dotsbtn" title="Mer">${icon.more}</button><div class="menu">${compact}</div></div>`;
}
return `<div class="menuwrap"><button class="iconbtn dotsbtn" title="Radåtgärder">${icon.dots}</button><div class="menu">${compact}${isSB()?'':''}<div class="sep"></div><button class="mi danger">${icon.trash} Ta bort rad</button></div></div>`;
}
const FIELDS=()=>[
{lab:'Artikel',w:120,al:'left',ctrl:r=>sel(ARTICLES,r.art)},
{lab:'Antal',w:60,al:'right',ctrl:r=>`<input class="num" value="${r.qty}">`},
{lab:'Enhet',w:78,al:'left',ctrl:r=>sel(UNITS,r.unit)},
{lab:'à-pris',w:96,al:'right',ctrl:r=>`<input class="num" value="${r.price}">`},
...(S.vat?[{lab:'Moms',w:70,al:'left',ctrl:r=>sel(VAT,r.vat)}]:[]),
];
function headerRow(){
const eco=FIELDS().map(f=>`<div class="f" style="width:${f.w}px"><span class="hlab" style="text-align:${f.al}">${f.lab}</span></div>`).join('');
return `<div class="srow headrow">
<div class="sgrip"></div>
<div class="sdesc"><span class="hlab">Beskrivning</span></div>
<div class="seco">${eco}</div>
<div class="sbel"><span class="hlab">Belopp</span></div>
<div class="sact"></div>
</div>`;
}
function bodyRow(r,actStyle){
const eco=FIELDS().map(f=>`<div class="f" style="width:${f.w}px">${f.ctrl(r)}</div>`).join('');
return `<div class="srow line">
<div class="sgrip">⠿</div>
<div class="sdesc"><input value="${r.desc}" placeholder="Beskrivning">${r.rot?rotStrip():''}</div>
<div class="seco">${eco}</div>
<div class="sbel">${r.tot}</div>
<div class="sact ${actStyle==='menu'?'keep':''}">${rowActions(actStyle)}</div>
</div>`;
}
function textRow(actStyle){
if(isSB())return '';
return `<div class="srow line">
<div class="sgrip">⠿</div>
<div class="sdesc"><input value="Tack för ditt förtroende." style="font-style:italic;color:var(--muted)" placeholder="Fritext (bokförs inte)"></div>
<div class="seco"></div><div class="sbel"></div>
<div class="sact ${actStyle==='menu'?'keep':''}">${rowActions(actStyle)}</div>
</div>`;
}
/* add affordance: 'ghost' = inline ghost rows ; 'buttons' = outline buttons below */
function addAffordance(style){
if(style==='ghost'){
return `<div class="ghostrow"><button class="ghostadd">${icon.plus} Lägg till rad</button></div>${isSB()?'':`<div class="ghostrow"><button class="ghostadd">${icon.plus} Lägg till textrad</button></div>`}`;
}
return `<div class="addrow"><button class="btn outline sm">${icon.plus} Lägg till rad</button>${isSB()?'':`<button class="btn ghost sm">${icon.plus} Lägg till textrad</button>`}</div>`;
}
function frame(work){
const mc=(l,c)=>`<div class="ul-cell"><span class="ul-lab">${l}</span>${c}</div>`;
const meta=`${isSB()?'':mc('Typ',sel(['Faktura','Proforma','Följesedel']))}${mc('Valuta',sel(CUR))}${mc('Datum','<input type="date" value="2026-07-21">')}${mc('Förfaller','<input type="date" value="2026-08-20">')}
<div class="menuwrap"><button class="pill dotsbtn">${icon.sliders} Referenser &amp; mer ${icon.chev}</button>
<div class="menu pop"><div style="display:flex;flex-direction:column;gap:14px">
${isSB()?`<div><label>Avtalsreferens (självfakturering)</label><input placeholder="SB-2026-014"></div>`:`<div class="grid2"><div><label>Er referens</label><input placeholder="Kundens ref"></div><div><label>Vår referens</label><input value="Emil Mattsson"></div></div>`}
${isSB()?'':`<div><div class="seclabel" style="margin-bottom:8px">ROT/RUT-uppgifter</div><div class="grid3"><div><label>Fastighetsbet.</label><input placeholder="Gårda 12:3"></div><div><label>Lgh.nr</label><input placeholder="1101"></div><div><label>BRF org.nr</label><input placeholder="769600-0000"></div></div></div>`}
<div><label>Anteckningar</label><textarea>Betalning inom 30 dagar. Dröjsmålsränta enligt räntelagen.</textarea></div>
</div></div></div>`;
const send=isSB()?'':`<button class="btn secondary">${icon.send} Skicka</button>`;
return `<div class="d3top"><h2>${isSB()?'Ny självfaktura':'Ny faktura'}</h2>
<div class="tabs"><button class="${!isSB()?'on':''}" onclick="setMode('invoice')">Faktura</button><button class="${isSB()?'on':''}" onclick="setMode('self_billed')">Självfaktura</button></div></div>
<div class="d3band">
<div style="flex:1;min-width:0;max-width:340px"><span class="ul-lab">${isSB()?'Utställare':'Kund'}</span>
<div style="display:flex;gap:8px;align-items:center"><div style="flex:1">${sel(['Välj kund…',...CUSTOMERS],'Nordisk Design AB')}</div><button class="btn ghost sm">${icon.plus}</button></div></div>
<div style="display:flex;gap:20px;align-items:flex-end;flex-wrap:wrap">${meta}</div>
</div>
${isSB()?'':`<div class="warn">${icon.warn}<div>Inga bankuppgifter inlagda ännu. Fakturan kan skapas men saknar betalinfo.</div></div>`}
${work}
<div class="sumbar"><div class="brk"><span>Delsumma 28 080,00</span>${S.vat?'<span>Moms 7 020,00</span>':''}<span>ROT −1 440,00</span></div>
<div class="pay"><span class="k">Att betala</span><span class="v">${S.vat?'33 660,00':'26 640,00'} kr</span></div>
<div class="acts"><button class="btn ghost">Spara utkast</button>${send}<button class="btn primary">${isSB()?'Registrera självfaktura':'Granska &amp; skapa'}</button></div>
</div>`;
}
function build(addStyle,actStyle){
const rows=ROWS.map(r=>bodyRow(r,actStyle)).join('')+textRow(actStyle);
const listCls=actStyle==='inline'?'list hovacts':'list';
const work=`<div class="work"><div class="seclabel" style="margin-bottom:6px">Fakturarader</div>
${headerRow()}
<div class="${listCls}">${rows}</div>
${addAffordance(addStyle)}
</div>`;
return frame(work);
}
const VARIANTS=[
{k:'F1',n:'Spökrad + ⋮',r:()=>build('ghost','menu')},
{k:'F2',n:'Knappar + ⋮',r:()=>build('buttons','menu')},
{k:'F3',n:'Spökrad + hover-åtgärder',r:()=>build('ghost','inline')},
{k:'F4',n:'Knappar + hover-åtgärder',r:()=>build('buttons','inline')},
];
const mount=document.getElementById('mount');
function render(){const V=VARIANTS[S.vi];mount.innerHTML=V.r();document.getElementById('curk').textContent=V.k;document.getElementById('curn').textContent=V.n;wire();}
function setMode(m){S.mode=m;document.querySelectorAll('#segMode button').forEach(b=>b.classList.toggle('on',b.dataset.m===m));render();}
function step(d){S.vi=(S.vi+d+VARIANTS.length)%VARIANTS.length;render();}
document.getElementById('prev').onclick=()=>step(-1);document.getElementById('next').onclick=()=>step(1);
document.querySelectorAll('#segMode button').forEach(b=>b.onclick=()=>setMode(b.dataset.m));
document.querySelectorAll('#segVat button').forEach(b=>b.onclick=()=>{S.vat=b.dataset.v==='1';document.querySelectorAll('#segVat button').forEach(x=>x.classList.toggle('on',x===b));render();});
document.addEventListener('keydown',e=>{const t=e.target.tagName;if(t==='INPUT'||t==='SELECT'||t==='TEXTAREA'||e.target.isContentEditable)return;if(e.key==='ArrowLeft')step(-1);if(e.key==='ArrowRight')step(1);});
function wire(){document.querySelectorAll('.dotsbtn').forEach(btn=>btn.onclick=e=>{e.stopPropagation();const m=btn.parentElement.querySelector('.menu');const open=m.classList.contains('open');closeMenus();m.classList.toggle('open',!open);const s=btn.closest('.sact');if(s)s.classList.toggle('keep',!open);});}
function closeMenus(){document.querySelectorAll('.menu.open').forEach(m=>m.classList.remove('open'));document.querySelectorAll('.sact.keep').forEach(s=>{if(!s.querySelector('.rowActions'))return;});}
document.addEventListener('click',()=>{document.querySelectorAll('.menu.open').forEach(m=>m.classList.remove('open'));});
(function(){const p=document.getElementById('picker'),h=document.getElementById('pgrip');let dx,dy,drag=false;
h.addEventListener('pointerdown',e=>{drag=true;const r=p.getBoundingClientRect();dx=e.clientX-r.left;dy=e.clientY-r.top;h.setPointerCapture(e.pointerId)});
h.addEventListener('pointermove',e=>{if(!drag)return;p.style.left=(e.clientX-dx)+'px';p.style.top=(e.clientY-dy)+'px';p.style.right='auto';p.style.bottom='auto'});
h.addEventListener('pointerup',()=>drag=false);})();
render();
</script>
</body>
</html>