@facturion/invoice-renderer 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +1 -1
- package/package.json +5 -2
- package/src/document.js +8 -11
- package/src/render.js +4 -1
- package/src/styles.js +6 -0
package/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type Lang = "en" | "de";
|
|
|
5
5
|
/** Label resolver: `(key, vars?) => string`, returning the key unchanged on a miss. */
|
|
6
6
|
export type TFunction = (key: string, vars?: Record<string, unknown>) => string;
|
|
7
7
|
|
|
8
|
-
export { lineNet, computeTotals } from "@facturion/invoice";
|
|
8
|
+
export { lineNet, computeTotals } from "@facturion/invoice/model";
|
|
9
9
|
|
|
10
10
|
/** HTML-escape a value (`&`, `<`, `>`, `"`). */
|
|
11
11
|
export function esc(s: unknown): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@facturion/invoice-renderer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Render the @facturion/invoice simplified-JSON model to a human-readable HTML invoice — the presentation layer behind Facturion's invoice PDFs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"en16931",
|
|
@@ -42,12 +42,15 @@
|
|
|
42
42
|
"NOTICE"
|
|
43
43
|
],
|
|
44
44
|
"scripts": {
|
|
45
|
+
"generate": "node scripts/generate-styles.mjs",
|
|
46
|
+
"prepare": "npm run generate",
|
|
47
|
+
"pretest": "npm run generate",
|
|
45
48
|
"test": "node --test test/*.test.js",
|
|
46
49
|
"prepublishOnly": "npm test"
|
|
47
50
|
},
|
|
48
51
|
"dependencies": {
|
|
49
52
|
"@facturion/codelists": "^0.1.1",
|
|
50
|
-
"@facturion/invoice": "^0.1.
|
|
53
|
+
"@facturion/invoice": "^0.1.1"
|
|
51
54
|
},
|
|
52
55
|
"engines": {
|
|
53
56
|
"node": ">=20"
|
package/src/document.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
// Batteries-included wrapper: a standalone HTML document for an invoice, with
|
|
2
2
|
// the bundled stylesheet inlined and a default label resolver. This is what an
|
|
3
3
|
// HTML→PDF service (e.g. headless Chromium) consumes.
|
|
4
|
+
//
|
|
5
|
+
// The CSS is embedded as JS strings (./styles.js, generated from styles/*.css),
|
|
6
|
+
// so this module has no filesystem dependency and works in the browser as well
|
|
7
|
+
// as in Node.
|
|
4
8
|
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
6
|
-
import { dirname, join } from "node:path";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { renderInvoice } from "./render.js";
|
|
9
10
|
import { makeT, DEFAULT_LANG } from "./i18n.js";
|
|
10
|
-
|
|
11
|
-
const _stylesDir = join(dirname(fileURLToPath(import.meta.url)), "..", "styles");
|
|
12
|
-
const _variablesCss = readFileSync(join(_stylesDir, "variables.css"), "utf-8");
|
|
13
|
-
const _utilitiesCss = readFileSync(join(_stylesDir, "utilities.css"), "utf-8");
|
|
14
|
-
const _viewCss = readFileSync(join(_stylesDir, "view.css"), "utf-8");
|
|
11
|
+
import { variablesCss, utilitiesCss, viewCss } from "./styles.js";
|
|
15
12
|
|
|
16
13
|
const _HTML_LANG = { en: "en", de: "de" };
|
|
17
14
|
|
|
@@ -32,9 +29,9 @@ export function renderInvoiceDocument(invoice, { lang = DEFAULT_LANG, t } = {})
|
|
|
32
29
|
<head>
|
|
33
30
|
<meta charset="utf-8">
|
|
34
31
|
<style>
|
|
35
|
-
${
|
|
36
|
-
${
|
|
37
|
-
${
|
|
32
|
+
${variablesCss}
|
|
33
|
+
${utilitiesCss}
|
|
34
|
+
${viewCss}
|
|
38
35
|
body { margin: 0; padding: 0; background: #fff; }
|
|
39
36
|
</style>
|
|
40
37
|
</head>
|
package/src/render.js
CHANGED
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
// and `units.*` / `paymentMeans.*` vocabulary. `renderInvoiceDocument` supplies
|
|
11
11
|
// a default `t` (bundled strings + @facturion/codelists); advanced callers pass
|
|
12
12
|
// their own. The net/total math comes from @facturion/invoice.
|
|
13
|
-
|
|
13
|
+
// Import from the pure ./model subpath, not the package root — the root eagerly
|
|
14
|
+
// compiles Ajv validators at load, which we don't want dragged into the renderer
|
|
15
|
+
// (and, transitively, the browser) bundle just for the net/total math.
|
|
16
|
+
import { computeTotals, lineNet } from "@facturion/invoice/model";
|
|
14
17
|
|
|
15
18
|
export { computeTotals, lineNet };
|
|
16
19
|
|
package/src/styles.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Generated by scripts/generate-styles.mjs from styles/*.css.
|
|
2
|
+
// Do not edit by hand.
|
|
3
|
+
|
|
4
|
+
export const variablesCss = "/* ── Design tokens ─────────────────────────────────────────────────────────── */\n\n:root {\n /* Primary */\n --primary: #5a7fbf;\n --primary-light: #7fa0d8;\n --primary-dim: #47659c;\n --primary-ghost: rgba(90, 127, 191, 0.13);\n\n /* Surfaces (calm product-slate — luminance-only depth steps) */\n --bg-base: #0d1422;\n --bg-2: #111a2c;\n --bg-card: #18223a;\n --bg-elevated: #1d2742;\n --bg-input: #131c2e;\n --bg-hover: rgba(255, 255, 255, 0.04);\n\n /* Borders — translucent-white hairlines (calm) */\n --border: rgba(255, 255, 255, 0.08);\n --border-light: rgba(255, 255, 255, 0.14);\n --border-focus: var(--primary);\n\n /* Text */\n --text: #e7ecf6;\n --text-secondary: #99a6bf;\n --text-muted: #647190;\n --text-inverse: #0d1422;\n\n /* Semantic — calm muted (green = valid only; blue = info/action) */\n --success: #36c08b;\n --success-dim: rgba(54, 192, 139, 0.13);\n --warning: #e0a73c;\n --warning-dim: rgba(224, 167, 60, 0.13);\n --error: #e0606e;\n --error-dim: rgba(224, 96, 110, 0.13);\n --info: #7fa0d8;\n --info-dim: rgba(127, 160, 216, 0.13);\n\n /* Typography — calm direction: Geist (display) / Hanken Grotesk (body) / JetBrains Mono (code) */\n --font-display: 'Geist', 'Hanken Grotesk', system-ui, sans-serif;\n --font-sans: 'Hanken Grotesk', system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif;\n --font-mono: 'JetBrains Mono', ui-monospace, 'Cascadia Code', 'Fira Mono', 'Consolas', monospace;\n\n --text-xs: 0.8125rem;\n --text-sm: 0.875rem;\n --text-base: 1rem;\n --text-lg: 1.1875rem;\n --text-xl: 1.3rem;\n --text-2xl: 1.75rem;\n --text-3xl: 2.25rem;\n\n /* Spacing */\n --space-1: 0.25rem;\n --space-2: 0.5rem;\n --space-3: 0.75rem;\n --space-4: 1rem;\n --space-5: 1.25rem;\n --space-6: 1.5rem;\n --space-8: 2rem;\n --space-10: 2.5rem;\n --space-12: 3rem;\n --space-16: 4rem;\n\n /* Radii */\n --radius-sm: 4px;\n --radius-md: 8px;\n --radius-lg: 12px;\n --radius-xl: 16px;\n --radius-full: 9999px;\n\n /* Shadows */\n --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);\n --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.3);\n --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.4);\n\n /* Transitions */\n --ease-out: cubic-bezier(0.16, 1, 0.3, 1);\n --duration: 180ms;\n\n /* Layout */\n --nav-height: 3.5rem;\n --max-width: 1600px;\n\n /* Shared dropdown caret — reused by both native <select> and the custom\n listbox button so the closed trigger looks identical across them. The\n stroke colour is hard-coded to --text-muted (#647190); changing one\n means changing both. */\n --caret-svg: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6' fill='none' stroke='%23647190' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'><path d='M1 1l4 4 4-4'/></svg>\");\n}\n";
|
|
5
|
+
export const utilitiesCss = "/* ── Utilities ─────────────────────────────────────────────────────────────── */\n\n/* Data table */\n.data-table {\n width: 100%;\n border-collapse: collapse;\n font-size: var(--text-sm);\n}\n\n.data-table th,\n.data-table td {\n text-align: left;\n padding: var(--space-2) var(--space-3);\n border-bottom: 1px solid var(--border);\n}\n\n.data-table th {\n color: var(--text-secondary);\n font-weight: 500;\n font-size: var(--text-xs);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n.data-table td {\n color: var(--text-secondary);\n}\n\n.data-table code {\n font-size: var(--text-xs);\n color: var(--primary-light);\n}\n\n/* Alert */\n.alert {\n padding: var(--space-3) var(--space-4);\n border-radius: var(--radius-md);\n margin-bottom: var(--space-4);\n font-size: var(--text-sm);\n}\n\n.alert-info {\n background: var(--info-dim);\n color: var(--info);\n border: 1px solid rgba(59, 130, 246, 0.2);\n}\n\n.alert-error {\n background: var(--error-dim);\n color: var(--error);\n border: 1px solid rgba(239, 68, 68, 0.2);\n}\n\n.alert code {\n display: block;\n margin-top: var(--space-2);\n word-break: break-all;\n font-size: var(--text-xs);\n}\n\n/* Transitions for interactive elements */\n.card {\n transition: border-color var(--duration) var(--ease-out);\n}\n\n.btn {\n transition: background var(--duration) var(--ease-out),\n border-color var(--duration) var(--ease-out),\n opacity var(--duration) var(--ease-out),\n transform 80ms var(--ease-out);\n}\n\n.btn:active:not(:disabled) {\n transform: scale(0.98);\n}\n\n/* Inline spinner (for buttons) */\n@keyframes spin { to { transform: rotate(360deg); } }\n\n.spinner {\n display: inline-block;\n width: 16px;\n height: 16px;\n border: 2px solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n animation: spin 0.6s linear infinite;\n vertical-align: middle;\n}\n\n/* ── Info tooltip ─────────────────────────────────────────────────────────\n Shared component for inline help affordances. Markup pattern:\n\n <span class=\"info-tip\" tabindex=\"0\" role=\"button\" aria-label=\"More info\">\n <span class=\"info-tip-icon\" aria-hidden=\"true\">i</span>\n <span class=\"info-tip-popover\" role=\"tooltip\">…content…</span>\n </span>\n\n Hover or focus on the wrapper reveals the popover. The popover sits flush\n against the icon's bottom edge so the cursor doesn't cross a non-hovered\n gap on its way from the icon down into the popover (which would close it\n prematurely). Content can be plain text or include rich children\n (links, source notes, etc.). */\n.info-tip {\n position: relative;\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n cursor: help;\n outline: none;\n vertical-align: middle;\n}\n\n.info-tip-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n border-radius: var(--radius-full);\n background: var(--bg-elevated);\n color: var(--text-muted);\n font-size: 10px;\n font-style: italic;\n font-weight: 700;\n /* Pin line-height to font-size so the character sits at the geometric\n center of the circle. Without this, the line-box inherits the parent's\n line-height (~1.5) and the \"i\" rides high inside the icon, looking\n misaligned with adjacent text. */\n line-height: 1;\n user-select: none;\n}\n\n.info-tip:hover .info-tip-icon,\n.info-tip:focus-visible .info-tip-icon {\n background: var(--border);\n color: var(--text);\n}\n\n/* Popover is `position: fixed` so it escapes any parent overflow/clip\n context (cards, scroll containers, etc.) and can be JS-positioned in\n viewport coordinates. `installInfoTipPositioning` in `$lib/info-tip.ts`\n computes top/left/max-width on mouseover/focusin and on scroll/resize.\n Hidden state uses opacity + visibility (not display:none) so the\n popover always has measurable dimensions for the positioning math. */\n.info-tip-popover {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 10;\n display: flex;\n flex-direction: column;\n gap: var(--space-2);\n min-width: 240px;\n max-width: 340px;\n padding: var(--space-3);\n background: var(--bg-card);\n border: 1px solid var(--border);\n border-radius: var(--radius-md);\n box-shadow: 0 8px 16px rgba(0, 0, 0, 0.18);\n font-family: var(--font-sans);\n font-size: var(--text-xs);\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n color: var(--text);\n text-align: left;\n white-space: normal;\n cursor: default;\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n transition: opacity 0.1s ease;\n}\n\n.info-tip:hover .info-tip-popover,\n.info-tip:focus-within .info-tip-popover {\n opacity: 1;\n visibility: visible;\n pointer-events: auto;\n}\n\n/* Subdued source line + KB link inside an info-tip popover (used by the\n step-annotation tooltips). Optional. */\n.info-tip-source {\n color: var(--text-secondary);\n}\n\n.info-tip-link {\n color: var(--primary-light);\n text-decoration: none;\n}\n\n.info-tip-link:hover {\n text-decoration: underline;\n}\n\n/* Site footer */\n.site-footer {\n display: flex;\n justify-content: center;\n gap: var(--space-6);\n padding: var(--space-6) var(--space-4);\n border-top: 1px solid var(--border);\n font-size: var(--text-xs);\n}\n\n.site-footer a {\n color: var(--text-secondary);\n text-decoration: none;\n}\n\n.site-footer a:hover {\n color: var(--text-secondary);\n}\n\n.site-footer-note {\n color: var(--text-secondary);\n font-style: italic;\n}\n\n/* Responsive helpers */\n@media (max-width: 640px) {\n #app {\n padding: var(--space-4) var(--space-4);\n }\n}\n";
|
|
6
|
+
export const viewCss = "/* ── View page ──────────────────────────────────────────────────────────────── */\n\n.view-layout {\n max-width: 880px;\n margin: 0 auto;\n}\n\n.view-drop-card {\n margin-bottom: var(--space-5);\n}\n\n/* Modeled on .validate-tabs: the toolbar acts as the tab bar — the active\n tab's underline overlaps the toolbar's border-bottom for a unified strip. */\n.view-toolbar {\n display: flex;\n align-items: end;\n gap: var(--space-3);\n margin-bottom: var(--space-4);\n border-bottom: 1px solid var(--border);\n}\n\n.view-toolbar .mode-tabs { margin-right: auto; }\n\n/* Action buttons sit centered against the bar so they don't touch the\n separator line — same treatment as .tab-download-btn in /validate. */\n.view-toolbar .print-btn { align-self: center; }\n\n/* ── Invoice paper ─────────────────────────────────────────────────────────── */\n\n.invoice-paper {\n background: #fdf8f0;\n color: #111;\n border-radius: var(--radius-md);\n padding: var(--space-10);\n box-shadow: var(--shadow-lg);\n font-size: var(--text-sm);\n line-height: 1.6;\n}\n\n/* ── Invoice header ────────────────────────────────────────────────────────── */\n\n.inv-header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n margin-bottom: var(--space-8);\n padding-bottom: var(--space-6);\n border-bottom: 2px solid #e2e8f0;\n}\n\n.inv-type {\n font-size: var(--text-xs);\n font-weight: 700;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n color: #64748b;\n margin-bottom: var(--space-1);\n}\n\n.inv-id {\n font-size: var(--text-xl);\n font-weight: 700;\n color: #0f172a;\n}\n\n.inv-header-right {\n text-align: right;\n color: #475569;\n}\n\n.inv-header-right > div {\n margin-bottom: var(--space-1);\n}\n\n/* ── Parties ───────────────────────────────────────────────────────────────── */\n\n.inv-parties {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: var(--space-8);\n margin-bottom: var(--space-8);\n}\n\n.inv-party-label {\n font-size: var(--text-xs);\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: #94a3b8;\n margin-bottom: var(--space-2);\n}\n\n.inv-party-name {\n font-weight: 600;\n color: #0f172a;\n margin-bottom: var(--space-1);\n}\n\n.inv-party > div {\n color: #475569;\n}\n\n.inv-party-trading {\n font-style: italic;\n color: #64748b !important;\n margin-bottom: var(--space-1);\n}\n\n.inv-party-legal {\n margin-top: var(--space-2);\n font-size: var(--text-xs);\n color: #64748b !important;\n line-height: 1.4;\n}\n\n.inv-party-contact {\n margin-top: var(--space-2);\n font-size: var(--text-xs);\n color: #475569 !important;\n}\n\n.inv-parties-secondary {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));\n gap: var(--space-6);\n margin-bottom: var(--space-8);\n}\n\n/* ── References ────────────────────────────────────────────────────────────── */\n\n.inv-references {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));\n gap: var(--space-1) var(--space-6);\n margin-bottom: var(--space-6);\n padding: var(--space-3) var(--space-4);\n background: #f5ede0;\n border-radius: var(--radius-sm);\n font-size: var(--text-xs);\n}\n\n.inv-ref {\n display: flex;\n gap: var(--space-2);\n color: #475569;\n}\n\n.inv-ref-label {\n font-weight: 600;\n letter-spacing: 0.04em;\n color: #64748b;\n text-transform: uppercase;\n flex-shrink: 0;\n}\n\n.inv-ref-value {\n color: #0f172a;\n word-break: break-all;\n}\n\n/* ── Period / delivery strip ───────────────────────────────────────────────── */\n\n.inv-period {\n margin-bottom: var(--space-6);\n padding: var(--space-2) var(--space-4);\n background: #f5ede0;\n border-left: 3px solid #94a3b8;\n border-radius: var(--radius-sm);\n color: #334155;\n font-size: var(--text-sm);\n}\n\n/* ── Notes / payment ───────────────────────────────────────────────────────── */\n\n.inv-notes,\n.inv-payment {\n margin-bottom: var(--space-6);\n padding: var(--space-4);\n background: #f5ede0;\n border-radius: var(--radius-sm);\n color: #334155;\n}\n\n/* Keep coherent cards, the totals block, table rows, and short labels on a\n * single page in PDF output — Chromium's paginator respects break-inside. */\n.inv-references,\n.inv-period,\n.inv-notes,\n.inv-payment,\n.inv-payment-terms,\n.inv-totals-wrap,\n.inv-lines tr {\n break-inside: avoid;\n}\n\n.inv-notes p {\n margin: 0 0 var(--space-2) 0;\n}\n\n.inv-notes p:last-child {\n margin-bottom: 0;\n}\n\n.inv-payment > div {\n margin-bottom: var(--space-1);\n}\n\n.inv-section-label {\n font-size: var(--text-xs);\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: #64748b;\n margin-bottom: var(--space-2);\n}\n\n.inv-label {\n font-size: var(--text-xs);\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: #94a3b8;\n margin-right: 0.3em;\n}\n\n/* ── Line items ────────────────────────────────────────────────────────────── */\n\n.inv-lines-wrap {\n margin-bottom: var(--space-6);\n overflow-x: auto;\n}\n\n.inv-lines {\n color: #1e293b;\n}\n\n.inv-lines thead th {\n background: #f5ede0;\n color: #64748b;\n}\n\n.inv-col-id {\n width: 3rem;\n color: #94a3b8;\n}\n\n.inv-col-desc {\n /* takes remaining space */\n}\n\n.inv-col-num {\n text-align: right;\n white-space: nowrap;\n}\n\n/* ── Line cell internals ───────────────────────────────────────────────────── */\n\n.inv-line-name {\n font-weight: 500;\n color: #0f172a;\n}\n\n.inv-line-desc,\n.inv-line-meta,\n.inv-line-allowance,\n.inv-line-charge {\n font-size: var(--text-xs);\n color: #64748b;\n line-height: 1.4;\n}\n\n.inv-line-meta {\n margin-top: var(--space-1);\n}\n\n.inv-line-allowance {\n color: #b45309; /* amber-700 */\n}\n\n.inv-line-charge {\n color: #475569;\n}\n\n.inv-line-price-gross {\n font-size: var(--text-xs);\n color: #94a3b8;\n text-decoration: line-through;\n}\n\n/* ── VAT breakdown sub-rows in the totals table ───────────────────────────── */\n\n.inv-vat-base {\n font-size: var(--text-xs);\n color: #94a3b8;\n font-weight: 400;\n}\n\n.inv-vat-reason td {\n padding-top: 0 !important;\n padding-bottom: var(--space-2) !important;\n font-size: var(--text-xs);\n font-style: italic;\n color: #64748b;\n line-height: 1.3;\n}\n\n/* ── Totals ────────────────────────────────────────────────────────────────── */\n\n.inv-totals-wrap {\n display: flex;\n justify-content: flex-end;\n margin-bottom: var(--space-6);\n}\n\n.inv-totals {\n min-width: 280px;\n border-collapse: collapse;\n color: #1e293b;\n}\n\n.inv-totals td {\n padding: var(--space-1) var(--space-3);\n}\n\n.inv-totals td:first-child {\n color: #475569;\n}\n\n.inv-totals td.inv-col-num {\n font-variant-numeric: tabular-nums;\n}\n\n.inv-totals tr:first-child td {\n padding-top: var(--space-3);\n border-top: 1px solid #e2e8f0;\n}\n\n.inv-totals-payable td {\n padding-top: var(--space-3);\n border-top: 2px solid #e2e8f0;\n font-weight: 700;\n font-size: var(--text-base);\n color: #0f172a;\n}\n\n/* ── Invoice paper dark mode ────────────────────────────────────────────────── */\n\n.invoice-paper--dark {\n background: var(--bg-elevated);\n color: var(--text);\n}\n\n/* Header */\n.invoice-paper--dark .inv-header {\n border-bottom-color: var(--border);\n}\n.invoice-paper--dark .inv-type {\n color: var(--text-secondary);\n}\n.invoice-paper--dark .inv-id {\n color: var(--text);\n}\n.invoice-paper--dark .inv-header-right {\n color: var(--text-secondary);\n}\n\n/* Parties */\n.invoice-paper--dark .inv-party-name {\n color: var(--text);\n}\n.invoice-paper--dark .inv-party > div {\n color: var(--text-secondary);\n}\n.invoice-paper--dark .inv-party-label,\n.invoice-paper--dark .inv-label {\n color: var(--text-secondary);\n}\n\n/* Lines */\n.invoice-paper--dark .inv-lines {\n color: var(--text);\n}\n.invoice-paper--dark .inv-lines thead th {\n background: var(--bg-card);\n color: var(--text-secondary);\n}\n.invoice-paper--dark .data-table td {\n color: var(--text-secondary);\n}\n.invoice-paper--dark .inv-col-id {\n color: var(--text-muted);\n}\n\n/* Totals */\n.invoice-paper--dark .inv-totals {\n color: var(--text);\n}\n.invoice-paper--dark .inv-totals td:first-child {\n color: var(--text-secondary);\n}\n.invoice-paper--dark .inv-totals tr:first-child td {\n border-top-color: var(--border);\n}\n.invoice-paper--dark .inv-totals-payable td {\n border-top-color: var(--border-light);\n color: var(--text);\n}\n\n/* Payment terms */\n.invoice-paper--dark .inv-payment-terms {\n color: var(--text-secondary);\n}\n\n/* Secondary party cards + labels */\n.invoice-paper--dark .inv-party-trading,\n.invoice-paper--dark .inv-party-legal,\n.invoice-paper--dark .inv-party-contact {\n color: var(--text-secondary) !important;\n}\n\n/* References / period / notes / payment — neutral info cards */\n.invoice-paper--dark .inv-references,\n.invoice-paper--dark .inv-period,\n.invoice-paper--dark .inv-notes,\n.invoice-paper--dark .inv-payment {\n background: var(--bg-card);\n color: var(--text-secondary);\n}\n.invoice-paper--dark .inv-ref {\n color: var(--text-secondary);\n}\n.invoice-paper--dark .inv-ref-label,\n.invoice-paper--dark .inv-section-label {\n color: var(--text-secondary);\n}\n.invoice-paper--dark .inv-ref-value {\n color: var(--text);\n}\n.invoice-paper--dark .inv-period {\n border-left-color: var(--border-light);\n color: var(--text-secondary);\n}\n\n/* Line cell internals */\n.invoice-paper--dark .inv-line-name {\n color: var(--text);\n}\n.invoice-paper--dark .inv-line-desc,\n.invoice-paper--dark .inv-line-meta,\n.invoice-paper--dark .inv-line-charge {\n color: var(--text-secondary);\n}\n.invoice-paper--dark .inv-line-price-gross {\n color: var(--text-secondary);\n}\n\n/* VAT breakdown sub-rows */\n.invoice-paper--dark .inv-vat-base,\n.invoice-paper--dark .inv-vat-reason td {\n color: var(--text-secondary);\n}\n\n/* ── Detailed view ─────────────────────────────────────────────────────────── */\n\n/* Power-user / educational rendering: every populated EN 16931 field beside\n its BT/BG code + DE/EN human label. Grouped by BG group. Styled with the\n site's dark theme tokens — deliberately not document-shaped. */\n\n.inv-detailed {\n display: flex;\n flex-direction: column;\n gap: var(--space-5);\n color: var(--text);\n}\n\n.inv-d-group {\n background: var(--bg-card);\n border: 1px solid var(--border);\n border-radius: var(--radius-lg);\n overflow: hidden;\n}\n\n.inv-d-group-title {\n display: flex;\n align-items: baseline;\n gap: var(--space-3);\n flex-wrap: wrap;\n margin: 0;\n padding: var(--space-3) var(--space-4);\n background: var(--bg-elevated);\n border-bottom: 1px solid var(--border);\n font-size: var(--text-base);\n font-weight: 600;\n color: var(--text);\n}\n\n/* Two-column auto-flow: rows pair up side-by-side once there's enough room\n (>= ~720px container), otherwise collapse to a single column. The 360px\n minimum is tuned so the create-view's narrower preview pane stays single\n column. */\n.inv-d-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));\n column-gap: 1px; /* hairline divider between columns via background */\n background: var(--border);\n}\n\n.inv-d-row {\n display: grid;\n grid-template-columns: minmax(11rem, max-content) 1fr;\n column-gap: var(--space-4);\n align-items: center;\n padding: var(--space-2) var(--space-4);\n background: var(--bg-card);\n font-size: var(--text-sm);\n border-bottom: 1px solid var(--border);\n}\n\n.inv-d-key {\n display: flex;\n flex-direction: column;\n gap: 2px;\n word-break: break-word;\n}\n\n.inv-d-code {\n font-family: var(--font-mono);\n font-size: var(--text-xs);\n color: var(--primary-light);\n letter-spacing: 0.04em;\n}\n\n.inv-d-name {\n font-size: var(--text-sm);\n color: var(--text);\n}\n\n.inv-d-value {\n color: var(--text-secondary);\n text-align: left;\n word-break: break-word;\n}\n\n/* Empty rows: muted, italic placeholder. Used when \"Show empty fields\" is\n on so users debugging or building an invoice can see the full structure. */\n.inv-d-row--empty .inv-d-name,\n.inv-d-row--empty .inv-d-code {\n color: var(--text-muted);\n}\n\n.inv-d-row--empty .inv-d-value {\n color: var(--text-muted);\n font-style: italic;\n}\n\n/* \"Show empty fields\" toggle — embedded as a checkbox label in the first\n section header. Sits flush right via margin-left: auto. */\n.inv-d-show-empty {\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n gap: var(--space-2);\n font-size: var(--text-xs);\n font-weight: 400;\n color: var(--text-secondary);\n text-transform: none;\n letter-spacing: normal;\n cursor: pointer;\n user-select: none;\n}\n\n.inv-d-show-empty input {\n accent-color: var(--primary);\n cursor: pointer;\n margin: 0;\n}\n\n.inv-d-hint {\n color: var(--text-secondary);\n font-size: var(--text-xs);\n margin-left: var(--space-1);\n}\n\n.inv-d-scheme {\n color: var(--text-muted);\n font-size: var(--text-xs);\n}\n\n.inv-d-line {\n background: var(--bg-card);\n border: 1px solid var(--border);\n border-left: 3px solid var(--text-muted);\n border-radius: var(--radius-md);\n padding: 0;\n margin-bottom: var(--space-3);\n overflow: hidden;\n}\n\n.inv-d-lines .inv-d-line {\n border-radius: var(--radius-md);\n margin: var(--space-3) var(--space-4);\n}\n\n.inv-d-line summary {\n cursor: pointer;\n display: flex;\n align-items: baseline;\n gap: var(--space-3);\n padding: var(--space-3) var(--space-4);\n background: var(--bg-elevated);\n border-bottom: 1px solid var(--border);\n list-style: none;\n}\n\n.inv-d-line summary::-webkit-details-marker { display: none; }\n\n.inv-d-line summary::before {\n content: \"▸\";\n color: var(--text-muted);\n transition: transform var(--duration) var(--ease-out);\n}\n\n.inv-d-line[open] summary::before {\n transform: rotate(90deg);\n display: inline-block;\n}\n\n.inv-d-line > .inv-d-grid {\n margin: var(--space-3) var(--space-4);\n}\n\n.inv-d-line > .inv-d-subgroup {\n margin: var(--space-3) 0 0;\n}\n\n/* Subgroup separation: matches the top-level array-item pattern — only the\n title strip is tonally elevated; the body inherits the line card's tone.\n A top border above each subgroup gives a clean structural break without\n the body re-shading that previously felt visually busy on deep nests. */\n.inv-d-subgroup {\n border-top: 1px solid var(--border);\n padding-top: var(--space-3);\n}\n\n.inv-d-subgroup-title {\n display: flex;\n align-items: baseline;\n gap: var(--space-2);\n margin: 0 0 var(--space-2);\n padding: var(--space-2) var(--space-4);\n background: var(--bg-elevated);\n font-size: var(--text-xs);\n font-weight: 600;\n color: var(--text-secondary);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n.inv-d-subgroup .inv-d-row:last-child {\n border-bottom: 0;\n}\n\n/* Mode tabs — reuse .tab-btn from validate.css. The natural margin-bottom:\n -1px on .tab-btn lets the active 2px underline overlap the parent's 1px\n border-bottom for a unified tab-bar look. */\n.mode-tabs {\n display: inline-flex;\n gap: var(--space-1);\n align-items: end;\n}\n\n.mode-tabs .tab-btn {\n padding: var(--space-2) var(--space-3);\n}\n\n/* ── Print ─────────────────────────────────────────────────────────────────── */\n\n@media print {\n @page {\n margin: 1.5cm;\n }\n\n nav,\n .site-footer,\n .view-drop-card,\n .view-toolbar,\n #view-error,\n .mode-tabs,\n .inv-detailed {\n display: none !important;\n }\n\n body {\n background: white;\n }\n\n .view-layout {\n max-width: 100%;\n }\n\n .invoice-paper {\n box-shadow: none;\n border-radius: 0;\n padding: 0;\n background: #fff;\n }\n}\n\n/* ── Editor (editable detailed view) ─────────────────────────────────────── */\n/* The detailed renderer's editable mode replaces value cells with inputs.\n Styling keeps the inputs visually flush with the read-only mode — same\n typography, no chrome — so the BT-code / label / value alignment stays\n intact. The whole row is a <label>, so clicking anywhere in the row\n focuses the input. */\n\n.inv-detailed--editable label.inv-d-row {\n cursor: text;\n}\n\n.inv-detailed--editable .inv-d-value input,\n.inv-detailed--editable .inv-d-value select {\n font: inherit;\n color: var(--text);\n background: transparent;\n border: 1px solid transparent;\n border-radius: var(--radius-sm);\n padding: 2px 4px;\n margin: -3px -4px; /* keep baseline alignment with surrounding rows */\n width: 100%;\n min-width: 0;\n}\n\n/* Vacant style: muted text for empty fields so populated content stands\n out at scanning distance. Driven by `.inv-d-row--vacant`, which the\n state listener keeps in sync as the user edits. */\n.inv-detailed--editable .inv-d-row--vacant .inv-d-name,\n.inv-detailed--editable .inv-d-row--vacant .inv-d-code {\n color: var(--text-muted);\n}\n\n.inv-detailed--editable .inv-d-row--vacant .inv-d-value input,\n.inv-detailed--editable .inv-d-row--vacant .inv-d-value select {\n color: var(--text-muted);\n}\n\n.inv-detailed--editable .inv-d-value input:hover,\n.inv-detailed--editable .inv-d-value select:hover {\n border-color: var(--border);\n}\n\n.inv-detailed--editable .inv-d-value input:focus,\n.inv-detailed--editable .inv-d-value select:focus {\n outline: none;\n border-color: var(--primary);\n background: var(--bg-card);\n color: var(--text);\n}\n\n.inv-detailed--editable .inv-d-row--derived .inv-d-value input,\n.inv-detailed--editable .inv-d-row--derived .inv-d-value select {\n color: var(--text-muted);\n cursor: not-allowed;\n}\n\n/* Array item subcards (top-level array members: notes, allowances, credit\n transfers, VAT breakdown rows, …). Mirror the line-item card treatment:\n each item gets its own bordered card with a 3px neutral left accent so\n it reads as a discrete unit within the section. */\n.inv-d-arr-items {\n display: flex;\n flex-direction: column;\n gap: var(--space-3);\n padding: var(--space-3) var(--space-4);\n}\n\n.inv-d-arr-item {\n background: var(--bg-card);\n border: 1px solid var(--border);\n border-left: 3px solid var(--text-muted);\n border-radius: var(--radius-md);\n overflow: hidden;\n}\n\n/* Title strip uses the elevated tone so it visually mirrors a line item's\n summary header. Body rows below stay on the card tone. */\n.inv-d-arr-item-title {\n display: flex;\n align-items: baseline;\n gap: var(--space-2);\n padding: var(--space-2) var(--space-4);\n background: var(--bg-elevated);\n border-bottom: 1px solid var(--border);\n font-size: var(--text-xs);\n font-weight: 600;\n color: var(--text-secondary);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n/* Drop the divider line on the last row of a card (the card's own bottom\n border already terminates the stack — the row border would just create\n a doubled line). Field-row padding is preserved; the trailing-gap\n tightening happens at the container level below. */\n.inv-d-arr-item > .inv-d-grid > .inv-d-row:last-child,\n.inv-d-group > .inv-d-grid:last-child > .inv-d-row:last-child,\n.inv-d-line > .inv-d-grid:last-child > .inv-d-row:last-child {\n border-bottom: 0;\n}\n\n/* Standalone + Add row used after a line's per-item subgroups (allowances,\n charges, attributes), where there's no enclosing _group to host the\n button. */\n.inv-d-subgroup-add {\n padding: var(--space-2) var(--space-4);\n}\n\n/* When the +Add button trails an .inv-d-arr-items list (e.g. credit\n transfers), the parent already supplies horizontal padding — strip the\n inner padding so the button aligns flush with the cards above. */\n.inv-d-arr-items > .inv-d-subgroup-add {\n padding: 0;\n}\n\n/* Suggestion popup for codelist-driven inputs (BT-130 unit, country,\n VAT category, …). Reuses the .listbox-panel / .listbox-option visual\n styles from create.css; this block only adds combobox-specific bits:\n a scrollable max-height for long lists and a monospace \"code\" column\n so cryptic UN/CEFACT codes line up next to their friendly labels. */\n.bt-suggest-panel {\n max-height: 280px;\n overflow-y: auto;\n min-width: 240px;\n}\n\n.bt-suggest-panel:hover .listbox-option:not(:hover) {\n background: transparent;\n}\n\n.bt-suggest-panel .listbox-option:hover {\n background: var(--bg-elevated);\n}\n\n.bt-suggest-code {\n font-family: var(--font-mono);\n font-size: var(--text-xs);\n color: var(--text-secondary);\n flex-shrink: 0;\n min-width: 4em;\n}\n\n/* + Add / × Remove buttons. Small, secondary visual weight — they live\n alongside group titles and item headers, so they should fade into the\n structure when not hovered. */\n.inv-d-group-add,\n.inv-d-row-remove {\n appearance: none;\n background: transparent;\n border: 1px solid var(--border);\n border-radius: var(--radius-sm);\n color: var(--text-secondary);\n font: inherit;\n font-size: var(--text-xs);\n padding: 2px 8px;\n cursor: pointer;\n margin-left: auto;\n text-transform: none;\n letter-spacing: normal;\n}\n\n.inv-d-group-add:hover {\n background: var(--bg-elevated);\n color: var(--text);\n border-color: var(--text-secondary);\n}\n\n.inv-d-row-remove {\n padding: 0 6px;\n line-height: 1.6;\n}\n\n.inv-d-row-remove:hover {\n background: var(--error-dim, rgba(220, 53, 53, 0.08));\n color: var(--error);\n border-color: var(--error);\n}\n\n/* ── Mobile ────────────────────────────────────────────────────────────────── */\n\n@media (max-width: 600px) {\n .inv-parties {\n grid-template-columns: 1fr;\n }\n\n .inv-header {\n flex-direction: column;\n gap: var(--space-3);\n }\n\n .inv-header-right {\n text-align: left;\n }\n}\n";
|