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:
Claude 2026-05-19 09:55:31 +00:00
parent 7222152664
commit 89a310a7e5
No known key found for this signature in database
2 changed files with 49 additions and 16 deletions

View file

@ -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)

View file

@ -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>&#127760;</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">&#127760;</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);