mirror of
https://github.com/kbenestad/invoice.git
synced 2026-06-18 08:04:32 +00:00
Dynamic header: org brand, app wordmark, invoice number meta, ⓘ button
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
This commit is contained in:
parent
406ac77073
commit
206ed6184a
2 changed files with 84 additions and 22 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
</div>
|
||||
<span id="sz-label" style="font-size:12px;color:var(--text-muted);font-family:var(--font-mono)">100%</span>
|
||||
<button class="kb-iconbtn" id="btn-theme" onclick="toggleTheme()" title="Toggle light/dark" aria-label="Toggle dark mode">
|
||||
<svg viewBox="0 0 16 16" width="15" height="15" fill="currentColor">
|
||||
<path d="M8 1.5a6.5 6.5 0 1 0 0 13A6.5 6.5 0 0 0 8 1.5zM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8z"/>
|
||||
<path d="M8 1.5V14.5A6.5 6.5 0 0 1 8 1.5z"/>
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="kb-iconbtn" id="btn-about" onclick="openAbout()" aria-label="About" style="display:none">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Document header -->
|
||||
<!-- Document header (brand + title built by buildHeader() after config loads) -->
|
||||
<header class="kb-header">
|
||||
<div class="app-wordmark">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="28" height="28" role="img" aria-label="Invoice">
|
||||
<rect width="48" height="48" rx="12" fill="var(--accent)"/>
|
||||
<path d="M14 9 H29 L34 14 V39 H14 Z" fill="none" stroke="#fff" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M29 9 V14 H34" fill="none" stroke="#fff" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M18.5 20 H27" stroke="#fff" stroke-width="2.6" stroke-linecap="round"/>
|
||||
<path d="M18.5 25 H29.5" stroke="#fff" stroke-width="2.6" stroke-linecap="round" opacity=".55"/>
|
||||
<path d="M18.5 33 H29.5" stroke="#fff" stroke-width="3.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
invoice
|
||||
</div>
|
||||
<div id="hdr-brand"></div>
|
||||
<div class="kb-doctitle">
|
||||
<h1 id="inv-title">Invoice</h1>
|
||||
<h1 id="inv-title"></h1>
|
||||
<div class="meta" id="inv-meta"></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" aria-hidden="true">
|
||||
<rect width="48" height="48" rx="12" fill="var(--accent)"/>
|
||||
<path d="M14 9 H29 L34 14 V39 H14 Z" fill="none" stroke="#fff" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M29 9 V14 H34" fill="none" stroke="#fff" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M18.5 20 H27" stroke="#fff" stroke-width="2.6" stroke-linecap="round"/>
|
||||
<path d="M18.5 25 H29.5" stroke="#fff" stroke-width="2.6" stroke-linecap="round" opacity=".55"/>
|
||||
<path d="M18.5 33 H29.5" stroke="#fff" stroke-width="3.2" stroke-linecap="round"/>
|
||||
</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 = `<span class="logo">${LOGO_SVG}</span>
|
||||
<span class="org">${h(orgName)}${orgSub ? `<small>${h(orgSub)}</small>` : ""}</span>`;
|
||||
} 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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue