mirror of
https://github.com/kbenestad/invoice.git
synced 2026-06-18 08:04:32 +00:00
Flip FX rate direction to X-foreign=1-local; add A-/A+ zoom buttons
Exchange rate semantics changed from "1 foreign = X local" to "X foreign = 1 local" (divide instead of multiply). Updates the calculation, preview note, both PDF fxStr strings, and config labels. Adds accessible font-size A−/A+ buttons to the lang-bar (always visible). Zooms only #form-root; index persisted in localStorage. Language selector shown only when multiple languages are configured. https://claude.ai/code/session_015iyCBgoTXNNqaErR287U1u
This commit is contained in:
parent
7222152664
commit
89a310a7e5
2 changed files with 49 additions and 16 deletions
|
|
@ -351,10 +351,10 @@ translations:
|
|||
fr: Code devise
|
||||
"no": Valutakode
|
||||
exchange-rate:
|
||||
en: "Exchange rate (1 foreign = X local)"
|
||||
de: "Wechselkurs (1 Fremd = X Inland)"
|
||||
fr: "Taux de change (1 étranger = X local)"
|
||||
"no": "Valutakurs (1 utenlandsk = X lokal)"
|
||||
en: "Exchange rate (X foreign = 1 local)"
|
||||
de: "Wechselkurs (X Fremd = 1 Inland)"
|
||||
fr: "Taux de change (X étranger = 1 local)"
|
||||
"no": "Valutakurs (X utenlandsk = 1 lokal)"
|
||||
per-item:
|
||||
en: Price per item (foreign currency)
|
||||
de: Preis je Einheit (Fremdwährung)
|
||||
|
|
|
|||
|
|
@ -69,6 +69,14 @@
|
|||
}
|
||||
#lang-bar label { font-size: 12px; color: var(--text-muted); white-space: nowrap; }
|
||||
#lang-bar select { width: auto; }
|
||||
#lang-bar .lang-part { display: flex; align-items: center; gap: 8px; }
|
||||
.sz-btn {
|
||||
background: var(--white); border: 1px solid var(--border); border-radius: 4px;
|
||||
color: var(--text); font-size: 13px; font-weight: 600; padding: 2px 8px;
|
||||
line-height: 1.4; cursor: pointer;
|
||||
}
|
||||
.sz-btn:hover { background: var(--surface); }
|
||||
#sz-label { font-size: 11px; color: var(--text-muted); min-width: 28px; text-align: center; }
|
||||
|
||||
/* ── Invoice banner ─────────────────────────────────────────────────────── */
|
||||
#inv-banner {
|
||||
|
|
@ -311,12 +319,17 @@
|
|||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<!-- Language bar -->
|
||||
<div id="lang-bar" style="display:none">
|
||||
<span>🌐</span>
|
||||
<!-- Language / accessibility bar -->
|
||||
<div id="lang-bar">
|
||||
<button class="sz-btn" id="sz-down" onclick="bumpZoom(-1)" title="Decrease text size">A−</button>
|
||||
<span id="sz-label">100%</span>
|
||||
<button class="sz-btn" id="sz-up" onclick="bumpZoom(1)" title="Increase text size">A+</button>
|
||||
<div id="lang-part" class="lang-part" style="display:none">
|
||||
<span style="margin-left:6px">🌐</span>
|
||||
<label id="lbl-language" for="lang-sel">Language</label>
|
||||
<select id="lang-sel"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Banner -->
|
||||
<div id="inv-banner"><h1 id="inv-title">INVOICE</h1></div>
|
||||
|
|
@ -452,12 +465,33 @@ function boot() {
|
|||
document.getElementById("loading").style.display = "none";
|
||||
}
|
||||
|
||||
// ── Font-size accessibility ────────────────────────────────────────────────────
|
||||
const ZOOMS = [0.8, 0.85, 0.9, 0.95, 1.0, 1.1, 1.2, 1.3];
|
||||
const ZOOM_LABELS = ["80%", "85%", "90%", "95%", "100%", "110%", "120%", "130%"];
|
||||
let zoomIdx = +(localStorage.getItem("zoomIdx") ?? 4);
|
||||
|
||||
function applyZoom() {
|
||||
const fr = document.getElementById("form-root");
|
||||
if (fr) fr.style.zoom = ZOOMS[zoomIdx];
|
||||
const lbl = document.getElementById("sz-label");
|
||||
if (lbl) lbl.textContent = ZOOM_LABELS[zoomIdx];
|
||||
document.getElementById("sz-down").disabled = zoomIdx === 0;
|
||||
document.getElementById("sz-up").disabled = zoomIdx === ZOOMS.length - 1;
|
||||
}
|
||||
|
||||
function bumpZoom(dir) {
|
||||
zoomIdx = Math.max(0, Math.min(ZOOMS.length - 1, zoomIdx + dir));
|
||||
localStorage.setItem("zoomIdx", zoomIdx);
|
||||
applyZoom();
|
||||
}
|
||||
|
||||
// ── Language bar ──────────────────────────────────────────────────────────────
|
||||
function buildLangBar() {
|
||||
applyZoom();
|
||||
const langs = cfg.languages || [{ code: cfg["default-code"], name: cfg["default-name"] }];
|
||||
if (langs.length < 2) return;
|
||||
const bar = document.getElementById("lang-bar");
|
||||
bar.style.display = "flex";
|
||||
const part = document.getElementById("lang-part");
|
||||
if (part) part.style.display = "flex";
|
||||
document.getElementById("lbl-language").textContent = t("language");
|
||||
const sel = document.getElementById("lang-sel");
|
||||
sel.innerHTML = langs.map(l =>
|
||||
|
|
@ -840,8 +874,7 @@ function calcFxFromPer(i) {
|
|||
const per = pn(document.getElementById(`fper-${i}`)?.value);
|
||||
const rate = pn(document.getElementById(`frate-${i}`)?.value) || 1;
|
||||
const prEl = document.getElementById(`price-${i}`);
|
||||
// rate = "1 foreign = rate local", so local price = per * rate
|
||||
if (prEl) prEl.value = (per * rate).toFixed(6);
|
||||
if (prEl) prEl.value = (per / rate).toFixed(6);
|
||||
calcLine(i);
|
||||
}
|
||||
|
||||
|
|
@ -1128,7 +1161,7 @@ function buildPreviewHTML() {
|
|||
const linesHTML = rows.map(row => {
|
||||
const fxLine = row.fxNote
|
||||
? `<div class="d-fx-note">${h(row.fxNote.td("converted-from"))} ${h(row.fxNote.cur)}: `
|
||||
+ `1 ${h(row.fxNote.cur)} = ${(+row.fxNote.rate).toFixed(5)} ${h(iCur)}. `
|
||||
+ `${(+row.fxNote.rate).toFixed(5)} ${h(row.fxNote.cur)} = 1 ${h(iCur)}. `
|
||||
+ `${h(row.fxNote.td("per-item"))}: ${h(row.fxNote.cur)} ${fmt(row.fxNote.per)}, `
|
||||
+ `${h(row.fxNote.td("line-total")).toLowerCase()}: ${h(row.fxNote.cur)} ${fmt(row.fxNote.foreignTot)}</div>`
|
||||
: "";
|
||||
|
|
@ -1363,7 +1396,7 @@ function buildPDF() {
|
|||
const descH = Math.max(0, (dLines.length - 1) * 3.8);
|
||||
let fxH = 0;
|
||||
if (row.fxNote) {
|
||||
const fxStr = `${td("converted-from")} ${row.fxNote.cur}: 1 ${row.fxNote.cur} = ${(+row.fxNote.rate).toFixed(5)} ${iCur}. `
|
||||
const fxStr = `${td("converted-from")} ${row.fxNote.cur}: ${(+row.fxNote.rate).toFixed(5)} ${row.fxNote.cur} = 1 ${iCur}. `
|
||||
+ `${td("per-item")}: ${row.fxNote.cur} ${fmt(row.fxNote.per)}, `
|
||||
+ `${td("line-total").toLowerCase()}: ${row.fxNote.cur} ${fmt(row.fxNote.foreignTot)}`;
|
||||
const fxLines = sp(fxStr, CD + CP - 4);
|
||||
|
|
@ -1398,7 +1431,7 @@ function buildPDF() {
|
|||
|
||||
if (row.fxNote) {
|
||||
const fxStr = `${td("converted-from")} ${row.fxNote.cur}: `
|
||||
+ `1 ${row.fxNote.cur} = ${(+row.fxNote.rate).toFixed(5)} ${iCur}. `
|
||||
+ `${(+row.fxNote.rate).toFixed(5)} ${row.fxNote.cur} = 1 ${iCur}. `
|
||||
+ `${td("per-item")}: ${row.fxNote.cur} ${fmt(row.fxNote.per)}, `
|
||||
+ `${td("line-total").toLowerCase()}: ${row.fxNote.cur} ${fmt(row.fxNote.foreignTot)}`;
|
||||
fn(7); tc(107,114,128);
|
||||
|
|
|
|||
Loading…
Reference in a new issue