Created
March 7, 2026 04:56
-
-
Save EncodeTheCode/b12a3b5661189c0f77cdb54c7610d010 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="sv"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Ekonomi Uträkning - Notebook Strict Lines (Y offset applied)</title> | |
| <style> | |
| :root{ | |
| --page-width:210mm; | |
| --page-height:297mm; | |
| --line-spacing:8mm; | |
| --left-margin:25mm; | |
| --left-line-width:2px; | |
| --line-color: rgba(6,95,70,0.18); | |
| --left-line-color: rgba(220,38,38,0.18); | |
| --paper-bg: #fffdf9; | |
| --ink: #05202b; | |
| --text-y-offset: 4px; /* final Y offset used by JS */ | |
| } | |
| *{box-sizing:border-box} | |
| html,body{height:100%;margin:0;background:#f0f4f7;font-family:Segoe UI, Roboto, Inter, system-ui, -apple-system} | |
| .app{min-height:100%;display:flex;align-items:center;justify-content:center;padding:24px} | |
| .page{width:var(--page-width);min-height:var(--page-height);background:var(--paper-bg);position:relative;box-shadow:0 24px 48px rgba(2,6,23,0.28);border-radius:6px;overflow:hidden;padding:18mm 18mm 14mm;display:block} | |
| .header-bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:8mm} | |
| .title{display:flex;align-items:center;gap:12px;color:var(--ink)} | |
| .title .app-icon{font-size:22px} | |
| .title h1{font-size:18px;margin:0} | |
| .controls{display:flex;gap:8px;align-items:center} | |
| .select, .button, .file-input-label{font-size:13px;padding:8px 10px;border-radius:6px;border:1px solid rgba(2,6,23,0.06);background:rgba(255,255,255,0.9);cursor:pointer} | |
| .select{appearance:none} | |
| .button{background:linear-gradient(180deg,#0ea5a4,#0a8e85);color:#fff;border:none;box-shadow:0 8px 22px rgba(10,130,120,0.12)} | |
| .button.secondary{background:#fff;color:var(--ink);border:1px solid rgba(2,6,23,0.06)} | |
| .file-input-label{display:inline-flex;align-items:center;gap:8px} | |
| .notebook{position:relative;padding:12px 14px 18px 18px;background-image:linear-gradient(to right, var(--left-line-color) var(--left-line-width), transparent var(--left-line-width)),linear-gradient(to bottom, var(--line-color) 1px, transparent 1px);background-size: var(--left-margin) 100%, 100% var(--line-spacing);background-position: left top, left top;background-repeat: no-repeat, repeat-y;border-radius:6px;margin-bottom:8mm} | |
| .notebook::before{content:"";position:absolute;left:6px;top:12mm;bottom:12mm;width:20px; pointer-events:none;background-image: radial-gradient(circle at 50% 50%, rgba(2,6,23,0.12) 0 3px, transparent 4px);background-size: 20px 72mm; background-repeat: repeat-y;opacity:0.6} | |
| .table-wrap{width:100%;margin-top:6px} | |
| table{width:100%;border-collapse:collapse;background:transparent} | |
| thead th{color:var(--ink);opacity:0.95;text-align:left;padding:6px 6px;font-size:13px} | |
| tbody tr{height:var(--line-spacing);vertical-align:middle} | |
| tbody td{padding:0 6px;border:0;vertical-align:middle} | |
| .row-cell{display:flex;align-items:center;height:100%;overflow:hidden} | |
| .input{position:relative; top:4px;width:100%;border:none;background:transparent;color:var(--ink);font-size:14px;padding:0 6px;height:calc(var(--line-spacing) - 6px);line-height:calc(var(--line-spacing) - 6px)} | |
| .input.number{max-width:160px;text-align:right} | |
| .input{position:relative;top:9px;width:100%;border:none;background:transparent;color:var(--ink);font-size:14px;padding:0 6px;height:calc(var(--line-spacing) - 1px);line-height:calc(var(--line-spacing) - 1px);overflow-y:visible} | |
| .input:focus{outline:none;box-shadow:0 4px 14px rgba(10,130,120,0.06)} | |
| .smallbtn{padding:6px 8px;border-radius:6px;border:1px solid rgba(2,6,23,0.06);background:rgba(255,255,255,0.9);cursor:pointer} | |
| .total{display:flex;justify-content:space-between;align-items:center;margin-top:18px;padding:12px;border-radius:8px;background:linear-gradient(90deg,#fff,#fbfdff);font-weight:700;color:var(--ink)} | |
| .footer{margin-top:14px;color:rgba(2,6,23,0.54);font-size:12px} | |
| @media print{body{background:white}.app{padding:0}.page{box-shadow:none;border-radius:0;padding:18mm}} | |
| @media(max-width:900px){.page{transform:scale(0.9);transform-origin:top center;width:190mm}} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <div class="page" role="document" aria-label="Notebook strict lines A4 page with Y offset applied by JS"> | |
| <div class="header-bar"> | |
| <div class="title"> | |
| <div class="app-icon">🧾</div> | |
| <div> | |
| <h1 id="titleText">Ekonomi – Faktura Lista</h1> | |
| <div style="font-size:11px;color:rgba(2,6,23,0.5)">All invoices are placed strictly between the ruled lines. Y-offset is applied on creation.</div> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <select id="langSelect" class="select" aria-label="language select"> | |
| <option value="sv">Svenska (sv)</option> | |
| <option value="en">English (en)</option> | |
| </select> | |
| <button id="addBtn" class="button">➕ Lägg till faktura</button> | |
| <button id="exportBtn" class="button secondary">📤 Exportera</button> | |
| <label class="file-input-label" title="Importera JSON"> | |
| 📥 <span id="importBtnText">Importera</span> | |
| <input id="importFile" type="file" accept="application/json" style="display:none"> | |
| </label> | |
| <button id="resetBtn" class="button secondary">♻️ Reset</button> | |
| </div> | |
| </div> | |
| <div class="notebook"> | |
| <div class="table-wrap"> | |
| <table id="invoiceTable" aria-label="Invoice table"> | |
| <thead> | |
| <tr> | |
| <th style="width:44px"></th> | |
| <th id="thDesc">Beskrivning</th> | |
| <th id="thPrice" style="width:160px;text-align:right">Pris (SEK)</th> | |
| <th style="width:64px"></th> | |
| </tr> | |
| </thead> | |
| <tbody></tbody> | |
| </table> | |
| </div> | |
| <div class="total" aria-live="polite"> | |
| <div id="totalLabel">Total:</div> | |
| <div><span id="total">0.00</span> <span id="currency">SEK</span></div> | |
| </div> | |
| <div class="footer"> | |
| <small id="helpText">Endast fälden i anteckningsområdet kan innehålla fakturor — text placeras alltid mellan linjerna.</small> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const translations = { | |
| sv: { title: 'Ekonomi – Faktura Lista', add: '➕ Lägg till faktura', export: '📤 Exportera', import: 'Importera', reset: '♻️ Reset', desc: 'Beskrivning', price: 'Pris (SEK)', total: 'Total:', help: 'Du kan exportera/importera hela listan som en JSON-fil.', invoiceEmoji: '🧾', remove: 'Ta bort' }, | |
| en: { title: 'Finance – Invoice List', add: '➕ Add invoice', export: '📤 Export', import: 'Import', reset: '♻️ Reset', desc: 'Description', price: 'Price (SEK)', total: 'Total:', help: 'You can export/import the whole list as a JSON file.', invoiceEmoji: '🧾', remove: 'Remove' } | |
| }; | |
| let data = []; | |
| let lang = localStorage.getItem('ekonomi_lang') || 'sv'; | |
| const saveKey = 'ekonomi_data_v1'; | |
| const tbody = document.querySelector('#invoiceTable tbody'); | |
| const totalEl = document.getElementById('total'); | |
| const totalLabelEl = document.getElementById('totalLabel'); | |
| const titleText = document.getElementById('titleText'); | |
| const thDesc = document.getElementById('thDesc'); | |
| const thPrice = document.getElementById('thPrice'); | |
| const helpText = document.getElementById('helpText'); | |
| const langSelect = document.getElementById('langSelect'); | |
| const addBtn = document.getElementById('addBtn'); | |
| const exportBtn = document.getElementById('exportBtn'); | |
| const importFile = document.getElementById('importFile'); | |
| const importBtnText = document.getElementById('importBtnText'); | |
| const resetBtn = document.getElementById('resetBtn'); | |
| function t(k){ return translations[lang][k] || k } | |
| function fmtNumber(n){ try{ const locale = lang === 'sv' ? 'sv-SE' : 'en-US'; return new Intl.NumberFormat(locale,{minimumFractionDigits:2,maximumFractionDigits:2}).format(Number(n || 0)); }catch(e){ return Number(n || 0).toFixed(2) } } | |
| function save(){ try{ localStorage.setItem(saveKey, JSON.stringify(data)); localStorage.setItem('ekonomi_lang', lang);}catch(e){} } | |
| function load(){ const raw = localStorage.getItem(saveKey); if(raw){ try{ data = JSON.parse(raw) }catch(e){ data = [] } } else { data = [] } } | |
| // apply Y-offset precisely on each input element so the text baseline sits between lines | |
| function render(){ | |
| tbody.innerHTML = ''; | |
| let sum = 0; | |
| data.forEach((item, i)=>{ | |
| const tr = document.createElement('tr'); | |
| const descInput = `<div class="row-cell"><input class="input" value="${escapeHtml(item.desc)}" oninput="updateDesc(${i}, this.value)" placeholder="${escapeHtml(t('desc'))}"></div>`; | |
| const priceInput = `<div class="row-cell"><input class="input number" type="number" step="0.01" value="${Number(item.price).toFixed(2)}" oninput="updatePrice(${i}, this.value)" aria-label="price"></div>`; | |
| tr.innerHTML = ` | |
| <td style="width:48px;display:flex;align-items:center;justify-content:center">${t('invoiceEmoji')}</td> | |
| <td>${descInput}</td> | |
| <td style="text-align:right">${priceInput}</td> | |
| <td style="width:64px;text-align:center;vertical-align:middle"><button class="smallbtn" onclick="removeRow(${i})">${t('remove')}</button></td> | |
| `; | |
| tbody.appendChild(tr); | |
| // Immediately apply precise Y-offset to inputs of this row so they are positioned correctly | |
| sum += Number(item.price || 0); | |
| }); | |
| totalEl.textContent = fmtNumber(sum); | |
| save(); | |
| } | |
| function escapeHtml(s){ return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>') } | |
| window.updateDesc = function(i, v){ data[i].desc = v; save(); } | |
| window.updatePrice = function(i, v){ data[i].price = Number(v || 0); render(); } | |
| window.removeRow = function(i){ data.splice(i,1); render(); } | |
| function addRow(){ data.push({desc: '', price: 0}); render(); // focus newly added desc input (apply small delay to ensure DOM ready) | |
| setTimeout(()=>{ const inputs = tbody.querySelectorAll('input'); if(inputs.length){ const target = inputs[inputs.length-2]; if(target){ target.focus(); /* ensure offset is applied */ } } },80); | |
| } | |
| function exportData(){ const json = JSON.stringify(data, null, 2); const blob = new Blob([json], {type:'application/json'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'ekonomi.json'; a.click(); } | |
| function importDataFromFile(file){ const reader = new FileReader(); reader.onload = (e)=>{ try{ const parsed = JSON.parse(e.target.result); if(Array.isArray(parsed)){ data = parsed.map(x=>({desc:x.desc||'',price: Number(x.price||0)})); render(); } else { alert('Invalid file format (expected an array)'); } }catch(err){ alert('Could not parse file: ' + err.message) } }; reader.readAsText(file); } | |
| function importData(){ const file = importFile.files[0]; if(file) importDataFromFile(file); } | |
| function resetAll(){ if(confirm('Reset all data?')){ data = []; save(); render(); } } | |
| function applyTranslations(){ titleText.textContent = t('title'); addBtn.textContent = t('add'); exportBtn.textContent = t('export'); importBtnText.textContent = t('import'); resetBtn.textContent = t('reset'); thDesc.textContent = t('desc'); thPrice.textContent = t('price'); totalLabelEl.textContent = t('total'); helpText.textContent = t('help'); } | |
| langSelect.value = lang; | |
| langSelect.addEventListener('change', ()=>{ lang = langSelect.value; applyTranslations(); render(); save(); }); | |
| addBtn.addEventListener('click', addRow); | |
| exportBtn.addEventListener('click', exportData); | |
| importFile.addEventListener('change', importData); | |
| resetBtn.addEventListener('click', resetAll); | |
| load(); applyTranslations(); render(); | |
| document.querySelector('.file-input-label').addEventListener('click', ()=> importFile.click()); | |
| window.addEventListener('keydown',(e)=>{ if((e.ctrlKey||e.metaKey) && e.key.toLowerCase()==='n'){ e.preventDefault(); addRow(); } }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment