Skip to content

Instantly share code, notes, and snippets.

@EncodeTheCode
Created March 7, 2026 04:56
Show Gist options
  • Select an option

  • Save EncodeTheCode/b12a3b5661189c0f77cdb54c7610d010 to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/b12a3b5661189c0f77cdb54c7610d010 to your computer and use it in GitHub Desktop.
<!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,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;') }
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