diff --git a/app/config.yml b/app/config.yml
index df0ed57..fc98d12 100644
--- a/app/config.yml
+++ b/app/config.yml
@@ -21,8 +21,14 @@ languages:
name: Norsk
direction: ltr
+# ── Organisation brand ────────────────────────────────────────────────────────
+# Optional. Set org-name to show a branded header (org logo + name + subheading).
+# Leave org-name absent or empty to show the plain "invoice" wordmark instead.
+# org-name: "Acme Corporation"
+# org-subheading: "Freelance invoicing"
+
# ── About modal ───────────────────────────────────────────────────────────────
-# Optional. Remove this section entirely to hide the About link in the footer.
+# Optional. Remove this section entirely to hide the ⓘ button and footer link.
about:
en:
title: "About"
diff --git a/app/index.html b/app/index.html
index 8996e78..08fa4bd 100644
--- a/app/index.html
+++ b/app/index.html
@@ -153,19 +153,34 @@
/* ── Document header ────────────────────────────────────────────────────── */
.kb-header {
- display: flex; justify-content: space-between; align-items: center;
- gap: 16px; padding-bottom: 14px; margin-bottom: 20px;
+ display: flex; justify-content: space-between; align-items: flex-start;
+ gap: 24px; padding-bottom: 20px; margin-bottom: 20px;
border-bottom: 1px solid var(--border);
}
+ /* Full brand (org name visible) */
+ .kb-brand { display: flex; align-items: center; gap: 14px; min-width: 0; }
+ .kb-brand .logo {
+ height: 46px; width: 46px; flex: 0 0 46px; border-radius: 10px;
+ display: grid; place-items: center; overflow: hidden;
+ }
+ .kb-brand .logo svg { width: 100%; height: 100%; display: block; }
+ .kb-brand .org { font-size: 17px; font-weight: 700; color: var(--text); letter-spacing: -0.01em; }
+ .kb-brand .org small { display: block; font-size: var(--fs-small); font-weight: 500; color: var(--text-muted); letter-spacing: 0; margin-top: 1px; }
+ /* Minimal wordmark (no org) */
.app-wordmark {
display: inline-flex; align-items: center; gap: 8px;
font: 700 var(--fs-h1)/1 var(--font-sans);
color: var(--text); letter-spacing: -0.01em; user-select: none;
}
.app-wordmark svg { flex-shrink: 0; }
-
+ /* Right: app name + meta */
.kb-doctitle { text-align: right; }
- .kb-doctitle h1 { margin: 0; font-size: var(--fs-h1); font-weight: 700; letter-spacing: -0.01em; color: var(--text); }
+ .kb-doctitle h1 {
+ margin: 0; font-size: var(--fs-h1); font-weight: 700; letter-spacing: -0.01em;
+ color: var(--text); display: inline-flex; align-items: center; gap: 9px;
+ }
+ .kb-doctitle h1 svg { flex-shrink: 0; }
+ .kb-doctitle .meta { margin-top: 4px; font-size: var(--fs-small); color: var(--text-muted); font-family: var(--font-mono); }
/* ── Cards ──────────────────────────────────────────────────────────────── */
.kb-card {
@@ -365,28 +380,23 @@
100%
+
-
+
@@ -593,13 +603,54 @@ async function loadCfg() {
// ── Boot ──────────────────────────────────────────────────────────────────────
function boot() {
buildLangBar();
+ buildHeader();
buildForm();
buildFooter();
restoreStorage();
+ updateInvMeta();
if (!loadLines()) addLine();
document.getElementById("loading").style.display = "none";
}
+// ── Document header ───────────────────────────────────────────────────────────
+const LOGO_SVG = `
+
+
+
+
+
+
+`;
+
+function buildHeader() {
+ const orgName = cfg["org-name"];
+ const orgSub = cfg["org-subheading"];
+
+ // Left brand
+ const brandEl = document.getElementById("hdr-brand");
+ if (orgName) {
+ brandEl.className = "kb-brand";
+ brandEl.innerHTML = `${LOGO_SVG}
+ ${h(orgName)}${orgSub ? `${h(orgSub)}` : ""}`;
+ } else {
+ brandEl.className = "app-wordmark";
+ brandEl.innerHTML = LOGO_SVG + " invoice";
+ }
+
+ // Right: app name h1 (icon + lowercase app name)
+ const titleEl = document.getElementById("inv-title");
+ titleEl.innerHTML = LOGO_SVG.replace("48 48", "24 24") + " invoice";
+
+ // Meta: invoice number (updated live)
+ updateInvMeta();
+}
+
+function updateInvMeta() {
+ const el = document.getElementById("inv-meta");
+ const ino = (document.getElementById("ino")?.value || "").trim();
+ if (el) el.textContent = ino || "";
+}
+
// ── 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%"];
@@ -623,6 +674,10 @@ function bumpZoom(dir) {
// ── Language bar ──────────────────────────────────────────────────────────────
function buildLangBar() {
applyZoom();
+ if (cfg.about) {
+ const btn = document.getElementById("btn-about");
+ if (btn) btn.style.display = "";
+ }
const langs = cfg.languages || [{ code: cfg["default-code"], name: cfg["default-name"] }];
if (langs.length < 2) return;
const part = document.getElementById("lang-part");
@@ -905,6 +960,7 @@ function buildForm() {
document.getElementById("pcode").addEventListener("change", function () {
document.getElementById("pcode-other-wrap").style.display = this.value === "__other__" ? "block" : "none";
});
+ document.getElementById("ino").addEventListener("input", updateInvMeta);
document.getElementById("ct-pick").addEventListener("change", e => fillChargeTo(e.target.value));
document.getElementById("idate").addEventListener("change", calcPayBy);
document.getElementById("paid-inp").addEventListener("input", calcTotals);
@@ -1305,7 +1361,7 @@ function bumpNum(s) {
// ── Relabel on language switch ────────────────────────────────────────────────
function relabel() {
const lm = {
- "inv-title":"invoice","lbl-language":"language",
+ "lbl-language":"language",
"sec-sender":"sender-section","sec-invdet":"invoice-details-section",
"sec-ct":"charge-to","sec-lines":"invoice-lines",
"lbl-sn":"sender-name","lbl-sa1":"sender-address1","lbl-sa2":"sender-address2",