From 206ed6184ae63a6b7144a577d3bd468f900ed2b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 15:59:46 +0000 Subject: [PATCH] =?UTF-8?q?Dynamic=20header:=20org=20brand,=20app=20wordma?= =?UTF-8?q?rk,=20invoice=20number=20meta,=20=E2=93=98=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Left brand: - org-name set in config → kb-brand with 46px logo, org name, optional subheading (org-subheading) - org-name absent/empty → plain app-wordmark (icon + "invoice") Right doctitle: - h1: 24px icon inline + lowercase "invoice" (built by buildHeader()) - .meta: invoice number, live-updates as user types in the ino field Toolbar: - Theme toggle updated to moon-phase icon (matches timesheet) - ⓘ button added; shown only when cfg.about is configured config.yml: org-name / org-subheading keys documented (commented out) About modal comment updated to mention ⓘ button https://claude.ai/code/session_01MkM7p8Us3L8YAfLKGA13NS --- app/config.yml | 8 ++++- app/index.html | 98 +++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 84 insertions(+), 22 deletions(-) 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% + - +
-
- - - - - - - - - invoice -
+
-

Invoice

+

+
@@ -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 = ` + ${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",