modern-document 0.1.0
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/brands/default/brand.json +21 -0
- package/package.json +55 -0
- package/src/cli/index.js +119 -0
- package/src/core/brand.js +53 -0
- package/src/core/build.js +318 -0
- package/src/core/components.js +146 -0
- package/src/core/index.js +5 -0
- package/src/core/markdown.js +69 -0
- package/src/core/pdf.js +24 -0
- package/src/mcp/server.js +225 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Modern Document",
|
|
3
|
+
"tagline": "Dokumenty, které stojí za přečtení.",
|
|
4
|
+
"web": "moderndocument.app",
|
|
5
|
+
"email": "hello@moderndocument.app",
|
|
6
|
+
"colors": {
|
|
7
|
+
"primary": "#0F0F0F",
|
|
8
|
+
"accent": "#2563EB",
|
|
9
|
+
"muted": "#868B93",
|
|
10
|
+
"surface": "#F6F6F8"
|
|
11
|
+
},
|
|
12
|
+
"fonts": {
|
|
13
|
+
"heading": "Georgia, serif",
|
|
14
|
+
"body": "-apple-system, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif"
|
|
15
|
+
},
|
|
16
|
+
"style": "modern",
|
|
17
|
+
"footer": {
|
|
18
|
+
"showContact": true,
|
|
19
|
+
"note": ""
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "modern-document",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Markdown → branded HTML/PDF documents. CLI, MCP server, and web app.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/core/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/core/index.js",
|
|
9
|
+
"./mcp": "./src/mcp/server.js"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"moddoc": "src/cli/index.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src/core/",
|
|
16
|
+
"src/mcp/",
|
|
17
|
+
"src/cli/",
|
|
18
|
+
"brands/"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"dev": "vite",
|
|
22
|
+
"build": "vite build",
|
|
23
|
+
"mcp": "node src/mcp/server.js"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"markdown",
|
|
27
|
+
"document",
|
|
28
|
+
"pdf",
|
|
29
|
+
"html",
|
|
30
|
+
"brand",
|
|
31
|
+
"mcp",
|
|
32
|
+
"mcp-server",
|
|
33
|
+
"claude",
|
|
34
|
+
"proposal",
|
|
35
|
+
"invoice"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": ""
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
44
|
+
"zod": "^3.24.4"
|
|
45
|
+
},
|
|
46
|
+
"optionalDependencies": {
|
|
47
|
+
"puppeteer": "^24.0.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"react": "^19.1.0",
|
|
51
|
+
"react-dom": "^19.1.0",
|
|
52
|
+
"@vitejs/plugin-react": "^4.5.2",
|
|
53
|
+
"vite": "^6.3.5"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { resolve, extname, basename } from "node:path";
|
|
4
|
+
import { build, mergeBrand, loadBrand, listComponents, exportPdf } from "../core/index.js";
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const cmd = args[0];
|
|
8
|
+
|
|
9
|
+
function flag(name) {
|
|
10
|
+
const i = args.indexOf(name);
|
|
11
|
+
if (i === -1) return null;
|
|
12
|
+
return args[i + 1] || true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function usage() {
|
|
16
|
+
console.log(`
|
|
17
|
+
moddoc — Modern Document CLI
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
moddoc build <file.md> Build HTML from markdown
|
|
21
|
+
moddoc pdf <file.html> Convert HTML to PDF
|
|
22
|
+
moddoc pipe <file.md> Full pipeline: MD → HTML + PDF
|
|
23
|
+
moddoc components List available components
|
|
24
|
+
moddoc styles List available style presets
|
|
25
|
+
moddoc mcp Start MCP server (for Claude Code)
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
-o <path> Output file path
|
|
29
|
+
--brand <path> Path to brand.json
|
|
30
|
+
--style <name> Style preset: modern | formal | minimal | executive | creative | technical | invoice | compact
|
|
31
|
+
--title <text> Document title
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function getBrand() {
|
|
36
|
+
const brandPath = flag("--brand");
|
|
37
|
+
const style = flag("--style");
|
|
38
|
+
if (brandPath) {
|
|
39
|
+
const b = await loadBrand(resolve(brandPath));
|
|
40
|
+
if (style) b.style = style;
|
|
41
|
+
return b;
|
|
42
|
+
}
|
|
43
|
+
return mergeBrand(style ? { style } : {});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function main() {
|
|
47
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
48
|
+
usage();
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (cmd === "build") {
|
|
53
|
+
const input = args[1];
|
|
54
|
+
if (!input) { console.error("Error: specify input file"); process.exit(1); }
|
|
55
|
+
const md = await readFile(resolve(input), "utf-8");
|
|
56
|
+
const brand = await getBrand();
|
|
57
|
+
const title = flag("--title");
|
|
58
|
+
const html = build(md, brand, { title });
|
|
59
|
+
const out = flag("-o") || input.replace(extname(input), ".html");
|
|
60
|
+
await writeFile(resolve(out), html, "utf-8");
|
|
61
|
+
console.log(`✓ ${out} (${(Buffer.byteLength(html, "utf-8") / 1024).toFixed(1)} KB)`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
else if (cmd === "pdf") {
|
|
65
|
+
const input = args[1];
|
|
66
|
+
if (!input) { console.error("Error: specify input HTML file"); process.exit(1); }
|
|
67
|
+
const html = await readFile(resolve(input), "utf-8");
|
|
68
|
+
const out = flag("-o") || input.replace(extname(input), ".pdf");
|
|
69
|
+
await exportPdf(html, resolve(out));
|
|
70
|
+
console.log(`✓ ${out}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
else if (cmd === "pipe") {
|
|
74
|
+
const input = args[1];
|
|
75
|
+
if (!input) { console.error("Error: specify input file"); process.exit(1); }
|
|
76
|
+
const md = await readFile(resolve(input), "utf-8");
|
|
77
|
+
const brand = await getBrand();
|
|
78
|
+
const title = flag("--title");
|
|
79
|
+
const html = build(md, brand, { title });
|
|
80
|
+
const base = basename(input, extname(input));
|
|
81
|
+
const htmlOut = flag("-o") || `${base}.html`;
|
|
82
|
+
const pdfOut = htmlOut.replace(extname(htmlOut), ".pdf");
|
|
83
|
+
await writeFile(resolve(htmlOut), html, "utf-8");
|
|
84
|
+
console.log(`✓ ${htmlOut} (${(Buffer.byteLength(html, "utf-8") / 1024).toFixed(1)} KB)`);
|
|
85
|
+
await exportPdf(html, resolve(pdfOut));
|
|
86
|
+
console.log(`✓ ${pdfOut}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
else if (cmd === "components") {
|
|
90
|
+
const components = listComponents();
|
|
91
|
+
for (const c of components) {
|
|
92
|
+
console.log(`\n${c.name} — ${c.description}`);
|
|
93
|
+
console.log(` Variants: ${c.variants.join(", ")}`);
|
|
94
|
+
console.log(` Example: ${c.example}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
else if (cmd === "styles") {
|
|
99
|
+
const { STYLE_PRESETS } = await import("../core/build.js");
|
|
100
|
+
for (const [key, s] of Object.entries(STYLE_PRESETS)) {
|
|
101
|
+
console.log(` ${key.padEnd(12)} ${s.label} — ${s.desc}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
else if (cmd === "mcp") {
|
|
106
|
+
await import("../mcp/server.js");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
else {
|
|
110
|
+
console.error(`Unknown command: ${cmd}`);
|
|
111
|
+
usage();
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
main().catch((err) => {
|
|
117
|
+
console.error(err.message);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const DEFAULT_BRAND = {
|
|
2
|
+
name: "Modern Document",
|
|
3
|
+
tagline: "Dokumenty, které stojí za přečtení.",
|
|
4
|
+
web: "moderndocument.app",
|
|
5
|
+
email: "hello@moderndocument.app",
|
|
6
|
+
logo: null,
|
|
7
|
+
icon: null,
|
|
8
|
+
colors: {
|
|
9
|
+
primary: "#0F0F0F",
|
|
10
|
+
accent: "#2563EB",
|
|
11
|
+
muted: "#868B93",
|
|
12
|
+
surface: "#F6F6F8",
|
|
13
|
+
},
|
|
14
|
+
fonts: {
|
|
15
|
+
heading: "Georgia, serif",
|
|
16
|
+
body: "-apple-system, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif",
|
|
17
|
+
},
|
|
18
|
+
style: "modern",
|
|
19
|
+
footer: {
|
|
20
|
+
showContact: true,
|
|
21
|
+
note: "",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function mergeBrand(custom = {}) {
|
|
26
|
+
return {
|
|
27
|
+
...DEFAULT_BRAND,
|
|
28
|
+
...custom,
|
|
29
|
+
colors: { ...DEFAULT_BRAND.colors, ...custom.colors },
|
|
30
|
+
fonts: { ...DEFAULT_BRAND.fonts, ...custom.fonts },
|
|
31
|
+
footer: { ...DEFAULT_BRAND.footer, ...custom.footer },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function loadBrand(brandPath) {
|
|
36
|
+
const { readFile } = await import("node:fs/promises");
|
|
37
|
+
const { resolve, dirname } = await import("node:path");
|
|
38
|
+
const raw = JSON.parse(await readFile(brandPath, "utf-8"));
|
|
39
|
+
|
|
40
|
+
const brandDir = dirname(resolve(brandPath));
|
|
41
|
+
|
|
42
|
+
for (const key of ["logo", "icon"]) {
|
|
43
|
+
if (raw[key] && !raw[key].startsWith("data:")) {
|
|
44
|
+
const filePath = resolve(brandDir, raw[key]);
|
|
45
|
+
const buf = await readFile(filePath);
|
|
46
|
+
const ext = filePath.split(".").pop().toLowerCase();
|
|
47
|
+
const mime = ext === "svg" ? "image/svg+xml" : `image/${ext}`;
|
|
48
|
+
raw[key] = `data:${mime};base64,${buf.toString("base64")}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return mergeBrand(raw);
|
|
53
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { parseMd } from "./markdown.js";
|
|
2
|
+
|
|
3
|
+
// ─── Style presets ───
|
|
4
|
+
// Styles define LAYOUT PERSONALITY only (sizes, weights, spacing, density).
|
|
5
|
+
// They never override brand colors/fonts — those always flow through CSS vars.
|
|
6
|
+
const STYLE_PRESETS = {
|
|
7
|
+
// ── General document styles ──
|
|
8
|
+
modern: {
|
|
9
|
+
label: "Moderní",
|
|
10
|
+
desc: "Vzdušný, velké lehké nadpisy. Pro nabídky a prezentace.",
|
|
11
|
+
h1Size: "2.75rem", h1Weight: "400", h1Spacing: "-0.025em",
|
|
12
|
+
h2Size: "1.75rem", h2Weight: "400", h2Border: "line", h2Accent: false,
|
|
13
|
+
h3Size: "1.0625rem", h4Style: "uppercase",
|
|
14
|
+
bodySize: "0.9375rem", lineHeight: "1.75",
|
|
15
|
+
tableStyle: "minimal", boxRadius: "8px",
|
|
16
|
+
coverStyle: "center", headerStyle: "border",
|
|
17
|
+
sectionSpacing: "3rem", componentSpacing: "1.5rem",
|
|
18
|
+
},
|
|
19
|
+
formal: {
|
|
20
|
+
label: "Formální",
|
|
21
|
+
desc: "Konzervativní, kompaktní. Smlouvy, právní dokumenty.",
|
|
22
|
+
h1Size: "2.25rem", h1Weight: "400", h1Spacing: "-0.01em",
|
|
23
|
+
h2Size: "1.5rem", h2Weight: "400", h2Border: "line", h2Accent: false,
|
|
24
|
+
h3Size: "1rem", h4Style: "uppercase",
|
|
25
|
+
bodySize: "0.875rem", lineHeight: "1.7",
|
|
26
|
+
tableStyle: "bordered", boxRadius: "4px",
|
|
27
|
+
coverStyle: "top", headerStyle: "border",
|
|
28
|
+
sectionSpacing: "2.5rem", componentSpacing: "1.25rem",
|
|
29
|
+
},
|
|
30
|
+
minimal: {
|
|
31
|
+
label: "Minimální",
|
|
32
|
+
desc: "Čistý, tučné nadpisy, max whitespace. Reporty, tech docs.",
|
|
33
|
+
h1Size: "2rem", h1Weight: "600", h1Spacing: "-0.02em",
|
|
34
|
+
h2Size: "1.375rem", h2Weight: "600", h2Border: "none", h2Accent: false,
|
|
35
|
+
h3Size: "1.0625rem", h4Style: "normal",
|
|
36
|
+
bodySize: "0.9375rem", lineHeight: "1.8",
|
|
37
|
+
tableStyle: "clean", boxRadius: "6px",
|
|
38
|
+
coverStyle: "center", headerStyle: "clean",
|
|
39
|
+
sectionSpacing: "3rem", componentSpacing: "1.5rem",
|
|
40
|
+
},
|
|
41
|
+
executive: {
|
|
42
|
+
label: "Executive",
|
|
43
|
+
desc: "Autoritativní, accent linky. Board reporty, C-level.",
|
|
44
|
+
h1Size: "2.5rem", h1Weight: "700", h1Spacing: "-0.02em",
|
|
45
|
+
h2Size: "1.5rem", h2Weight: "600", h2Border: "accent", h2Accent: true,
|
|
46
|
+
h3Size: "1.0625rem", h4Style: "uppercase",
|
|
47
|
+
bodySize: "0.9375rem", lineHeight: "1.7",
|
|
48
|
+
tableStyle: "bordered", boxRadius: "6px",
|
|
49
|
+
coverStyle: "hero", headerStyle: "border",
|
|
50
|
+
sectionSpacing: "3rem", componentSpacing: "1.5rem",
|
|
51
|
+
},
|
|
52
|
+
creative: {
|
|
53
|
+
label: "Kreativní",
|
|
54
|
+
desc: "Výrazný, velký kontrast. Agenturní nabídky, portfolio.",
|
|
55
|
+
h1Size: "3.25rem", h1Weight: "800", h1Spacing: "-0.03em",
|
|
56
|
+
h2Size: "1.875rem", h2Weight: "700", h2Border: "none", h2Accent: true,
|
|
57
|
+
h3Size: "1.125rem", h4Style: "uppercase",
|
|
58
|
+
bodySize: "1rem", lineHeight: "1.75",
|
|
59
|
+
tableStyle: "clean", boxRadius: "12px",
|
|
60
|
+
coverStyle: "hero", headerStyle: "clean",
|
|
61
|
+
sectionSpacing: "3.5rem", componentSpacing: "2rem",
|
|
62
|
+
},
|
|
63
|
+
technical: {
|
|
64
|
+
label: "Technický",
|
|
65
|
+
desc: "Data-dense, monospace akcenty. Analýzy, audity, API docs.",
|
|
66
|
+
h1Size: "1.875rem", h1Weight: "600", h1Spacing: "-0.01em",
|
|
67
|
+
h2Size: "1.375rem", h2Weight: "600", h2Border: "line", h2Accent: false,
|
|
68
|
+
h3Size: "1rem", h4Style: "mono",
|
|
69
|
+
bodySize: "0.875rem", lineHeight: "1.65",
|
|
70
|
+
tableStyle: "bordered", boxRadius: "4px",
|
|
71
|
+
coverStyle: "top", headerStyle: "border",
|
|
72
|
+
sectionSpacing: "2rem", componentSpacing: "1rem",
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// ── Specialized document styles ──
|
|
76
|
+
invoice: {
|
|
77
|
+
label: "Faktura",
|
|
78
|
+
desc: "Specifický layout pro faktury — from/to, line items, totals.",
|
|
79
|
+
h1Size: "2rem", h1Weight: "700", h1Spacing: "-0.01em",
|
|
80
|
+
h2Size: "1.25rem", h2Weight: "600", h2Border: "none", h2Accent: false,
|
|
81
|
+
h3Size: "1rem", h4Style: "uppercase",
|
|
82
|
+
bodySize: "0.875rem", lineHeight: "1.6",
|
|
83
|
+
tableStyle: "invoice", boxRadius: "4px",
|
|
84
|
+
coverStyle: "none", headerStyle: "invoice",
|
|
85
|
+
sectionSpacing: "2rem", componentSpacing: "1rem",
|
|
86
|
+
},
|
|
87
|
+
compact: {
|
|
88
|
+
label: "Kompaktní",
|
|
89
|
+
desc: "Max obsahu na stránku. SOW, smlouvy, detailní specifikace.",
|
|
90
|
+
h1Size: "1.75rem", h1Weight: "600", h1Spacing: "-0.01em",
|
|
91
|
+
h2Size: "1.25rem", h2Weight: "600", h2Border: "line", h2Accent: false,
|
|
92
|
+
h3Size: "0.9375rem", h4Style: "normal",
|
|
93
|
+
bodySize: "0.8125rem", lineHeight: "1.6",
|
|
94
|
+
tableStyle: "bordered", boxRadius: "4px",
|
|
95
|
+
coverStyle: "none", headerStyle: "border",
|
|
96
|
+
sectionSpacing: "1.75rem", componentSpacing: "0.75rem",
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export { STYLE_PRESETS };
|
|
101
|
+
|
|
102
|
+
// ─── Table CSS variants ───
|
|
103
|
+
function tableCSS(style) {
|
|
104
|
+
if (style === "bordered")
|
|
105
|
+
return `table{border:1px solid var(--border)} thead th{border-bottom:2px solid var(--primary);background:var(--surface);padding:0.625rem 1rem} tbody td{padding:0.625rem 1rem;border-bottom:1px solid var(--border)}`;
|
|
106
|
+
if (style === "clean")
|
|
107
|
+
return `thead th{border-bottom:1px solid var(--border-strong);padding:0 1rem 0.5rem 0;font-weight:600;text-transform:none;letter-spacing:0;font-size:0.8125rem;color:var(--primary)} tbody td{padding:0.625rem 1rem 0.625rem 0;border-bottom:1px solid var(--border)}`;
|
|
108
|
+
if (style === "invoice")
|
|
109
|
+
return `table{border:1px solid var(--border);border-radius:4px;overflow:hidden} thead th{background:var(--primary);color:#fff;padding:0.625rem 1rem;font-size:0.75rem;font-weight:600;letter-spacing:0.04em;text-transform:uppercase;text-align:left} thead th:last-child{text-align:right} tbody td{padding:0.625rem 1rem;border-bottom:1px solid var(--border);font-size:0.8125rem} tbody td:last-child{text-align:right;font-weight:600;font-variant-numeric:tabular-nums} tbody tr:last-child td{border-bottom:none}`;
|
|
110
|
+
// minimal (default)
|
|
111
|
+
return `thead th{font-weight:700;text-align:left;color:var(--text-light);font-size:0.6875rem;letter-spacing:0.08em;text-transform:uppercase;padding:0 1rem 0.75rem 0;border-bottom:2px solid var(--primary)} tbody td{padding:0.75rem 1rem 0.75rem 0;border-bottom:1px solid var(--border)} tbody tr:last-child td{border-bottom:2px solid var(--primary)}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Style-specific CSS overrides ───
|
|
115
|
+
function styleCSS(s) {
|
|
116
|
+
let css = "";
|
|
117
|
+
|
|
118
|
+
// h2 accent bar (short colored line instead of full border)
|
|
119
|
+
if (s.h2Accent) {
|
|
120
|
+
css += `h2::before{content:"";display:block;width:40px;height:3px;background:var(--accent);margin-bottom:0.75rem;border-radius:2px}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// h4 mono style for technical preset
|
|
124
|
+
if (s.h4Style === "mono") {
|
|
125
|
+
css += `h4{font-family:var(--mono, 'SF Mono', Consolas, monospace);font-size:0.75rem;font-weight:600;letter-spacing:0.02em;text-transform:none;color:var(--accent);margin:1.5rem 0 0.5rem}`;
|
|
126
|
+
} else if (s.h4Style === "normal") {
|
|
127
|
+
css += `h4{font-family:var(--body);font-size:0.8125rem;font-weight:600;letter-spacing:0;text-transform:none;color:var(--primary);margin:1.5rem 0 0.5rem}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Cover hero style (colored background block)
|
|
131
|
+
if (s.coverStyle === "hero") {
|
|
132
|
+
css += `.cover{background:var(--primary);color:#fff;border-radius:${s.boxRadius};padding:3rem 2.5rem;margin:-3rem -2rem 2.5rem;min-height:50vh;display:flex;flex-direction:column;justify-content:center}
|
|
133
|
+
.cover h1,.cover h2,.cover h3,.cover h4,.cover p,.cover strong{color:#fff}
|
|
134
|
+
.cover p{opacity:0.85}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Header clean (no border)
|
|
138
|
+
if (s.headerStyle === "clean") {
|
|
139
|
+
css += `.doc-header{border-bottom:none;padding-bottom:0.5rem}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Invoice-specific components
|
|
143
|
+
if (s.headerStyle === "invoice") {
|
|
144
|
+
css += `.doc-header{display:flex;justify-content:space-between;align-items:flex-start;border-bottom:none;padding-bottom:1.5rem;margin-bottom:1.5rem}
|
|
145
|
+
.invoice-label{font-family:var(--heading);font-size:2.5rem;font-weight:700;color:var(--primary);letter-spacing:-0.02em;text-transform:uppercase;line-height:1}
|
|
146
|
+
.invoice-meta{text-align:right}
|
|
147
|
+
.invoice-meta .kv-row{justify-content:flex-end;gap:1rem;border:none;padding:0.2rem 0}
|
|
148
|
+
.invoice-parties{display:grid;grid-template-columns:1fr 1fr;gap:2rem;margin:0 0 2rem}
|
|
149
|
+
.invoice-party h4{margin-top:0}
|
|
150
|
+
.invoice-total{display:flex;justify-content:flex-end;margin:1.5rem 0}
|
|
151
|
+
.invoice-total-box{background:var(--surface);border-radius:${s.boxRadius};padding:1rem 1.5rem;min-width:250px}
|
|
152
|
+
.invoice-total-box .kv-row{border-color:var(--border)}
|
|
153
|
+
.invoice-total-box .kv-row:last-child{border-bottom:none;font-size:1.25rem;padding-top:0.75rem;margin-top:0.25rem;border-top:2px solid var(--primary)}
|
|
154
|
+
.invoice-total-box .kv-row:last-child .kv-val{font-size:1.25rem}
|
|
155
|
+
.invoice-payment{background:var(--surface);border-radius:${s.boxRadius};padding:1.25rem 1.5rem;margin:2rem 0}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return css;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─── Base component CSS ───
|
|
162
|
+
function componentCSS(s) {
|
|
163
|
+
const r = s.boxRadius;
|
|
164
|
+
const cs = s.componentSpacing;
|
|
165
|
+
return `
|
|
166
|
+
.box{background:var(--surface);border-radius:${r};padding:1.25rem 1.5rem;margin:${cs} 0}
|
|
167
|
+
.box-dark{background:var(--primary);color:#fff;border-radius:${r};padding:1.25rem 1.5rem;margin:${cs} 0}
|
|
168
|
+
.box-dark h3,.box-dark h4,.box-dark strong{color:#fff}
|
|
169
|
+
.box-outline{border:1.5px solid var(--border);border-radius:${r};padding:1.25rem 1.5rem;margin:${cs} 0}
|
|
170
|
+
.box-accent{border-left:3px solid var(--accent);padding:1rem 1.5rem;margin:${cs} 0;background:var(--surface)}
|
|
171
|
+
.note{font-size:0.8125rem;color:var(--muted);border-left:2px solid var(--border);padding:0.5rem 1rem;margin:1rem 0}
|
|
172
|
+
.note-important{border-left-color:var(--accent);color:var(--text)}
|
|
173
|
+
.kv{margin:${cs} 0}
|
|
174
|
+
.kv-row{display:flex;justify-content:space-between;padding:0.5rem 0;border-bottom:1px solid var(--border);font-size:0.875rem}
|
|
175
|
+
.kv-key{color:var(--muted)}.kv-val{font-weight:600;color:var(--primary)}
|
|
176
|
+
.metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:1.5rem;margin:2rem 0}
|
|
177
|
+
.metric{text-align:center}
|
|
178
|
+
.metric-value{font-family:var(--heading);font-size:2.5rem;font-weight:700;color:var(--primary);line-height:1.1}
|
|
179
|
+
.metric-label{font-size:0.75rem;color:var(--muted);margin-top:0.25rem;text-transform:uppercase;letter-spacing:0.06em}
|
|
180
|
+
.stats-row{display:flex;gap:2rem;margin:${cs} 0}
|
|
181
|
+
.stat{text-align:center;flex:1}
|
|
182
|
+
.progress{margin:${cs} 0}
|
|
183
|
+
.progress-row{margin-bottom:0.75rem}
|
|
184
|
+
.progress-row label{display:flex;justify-content:space-between;font-size:0.8125rem;margin-bottom:0.25rem}
|
|
185
|
+
.progress-bar{height:6px;background:var(--surface);border-radius:3px;overflow:hidden}
|
|
186
|
+
.progress-fill{height:100%;background:var(--accent);border-radius:3px}
|
|
187
|
+
.timeline{position:relative;padding-left:2rem;margin:2rem 0}
|
|
188
|
+
.timeline::before{content:"";position:absolute;left:0;top:0;bottom:0;width:2px;background:var(--border)}
|
|
189
|
+
.timeline-item{position:relative;margin-bottom:2rem}
|
|
190
|
+
.timeline-item::before{content:"";position:absolute;left:-2rem;top:0.3rem;width:10px;height:10px;border-radius:50%;background:var(--accent);border:2px solid var(--bg)}
|
|
191
|
+
.timeline-date{font-size:0.75rem;color:var(--muted);margin-bottom:0.25rem;text-transform:uppercase;letter-spacing:0.06em}
|
|
192
|
+
.steps{counter-reset:step;margin:2rem 0}
|
|
193
|
+
.step{display:flex;gap:1rem;margin-bottom:1.5rem;align-items:flex-start}
|
|
194
|
+
.step-num{width:32px;height:32px;border-radius:50%;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:0.8125rem;font-weight:700;flex-shrink:0}
|
|
195
|
+
.step-content{flex:1}
|
|
196
|
+
.cols{display:grid;gap:2rem;margin:${cs} 0}
|
|
197
|
+
.cols-2{grid-template-columns:1fr 1fr}
|
|
198
|
+
.cols-3{grid-template-columns:1fr 1fr 1fr}
|
|
199
|
+
.cols-60-40{grid-template-columns:3fr 2fr}
|
|
200
|
+
.comparison{display:grid;grid-template-columns:1fr 1fr;gap:0;margin:2rem 0;border:1px solid var(--border);border-radius:${r};overflow:hidden}
|
|
201
|
+
.comp-a,.comp-b{padding:1.25rem 1.5rem}
|
|
202
|
+
.comp-a{border-right:1px solid var(--border)}
|
|
203
|
+
ul.checklist{list-style:none;padding-left:0}
|
|
204
|
+
ul.checklist li{padding-left:1.75rem;position:relative;margin-bottom:0.5rem}
|
|
205
|
+
ul.checklist li::before{content:"☐";position:absolute;left:0;color:var(--muted)}
|
|
206
|
+
ul.checklist li.done::before{content:"☑";color:var(--accent)}
|
|
207
|
+
.badge{display:inline-block;font-size:0.6875rem;font-weight:600;padding:0.2rem 0.6rem;border-radius:4px;text-transform:uppercase;letter-spacing:0.04em;background:var(--surface);color:var(--muted)}
|
|
208
|
+
.badge-success{background:#E8F5E9;color:#2E7D32}
|
|
209
|
+
.badge-warning{background:#FFF8E1;color:#F57F17}
|
|
210
|
+
.badge-danger{background:#FFEBEE;color:#C62828}
|
|
211
|
+
.cover{min-height:60vh;display:flex;flex-direction:column;justify-content:center;padding:3rem 0;margin-bottom:2rem}
|
|
212
|
+
.page-break{break-after:page;height:0;margin:0;border:none}
|
|
213
|
+
.divider-logo{text-align:center;margin:${s.sectionSpacing} 0;opacity:0.15}
|
|
214
|
+
.divider-logo img{height:24px}
|
|
215
|
+
.pull-quote{font-family:var(--heading);font-size:1.5rem;font-weight:400;line-height:1.4;color:var(--primary);border:none;padding:2rem 0;margin:2rem 0;text-align:center;font-style:italic}
|
|
216
|
+
.signatures{display:flex;gap:3rem;margin:3rem 0}
|
|
217
|
+
.sig{flex:1;text-align:center}
|
|
218
|
+
.sig-line{border-top:1px solid var(--primary);margin-bottom:0.5rem;margin-top:3rem}
|
|
219
|
+
.sig-name{font-size:0.8125rem;font-weight:600}
|
|
220
|
+
.doc-figure{margin:2rem 0;text-align:center}
|
|
221
|
+
.doc-figure img{max-width:100%;height:auto;border-radius:6px;display:block;margin:0 auto}
|
|
222
|
+
.doc-figure figcaption{font-size:0.75rem;color:var(--muted);margin-top:0.5rem;font-style:italic}
|
|
223
|
+
.doc-img-inline{max-height:1.5em;vertical-align:middle;display:inline}
|
|
224
|
+
.doc-img-full{width:100%;height:auto;border-radius:6px;margin:${cs} 0}
|
|
225
|
+
.doc-img-half{width:50%;height:auto;border-radius:6px;margin:1rem}
|
|
226
|
+
.doc-img-float-left{float:left;max-width:40%;margin:0.5rem 1.5rem 1rem 0;border-radius:6px}
|
|
227
|
+
.doc-img-float-right{float:right;max-width:40%;margin:0.5rem 0 1rem 1.5rem;border-radius:6px}
|
|
228
|
+
.doc-gallery{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:0.75rem;margin:2rem 0}
|
|
229
|
+
.doc-gallery img{width:100%;height:auto;border-radius:6px;object-fit:cover;aspect-ratio:4/3}
|
|
230
|
+
.doc-nav{position:sticky;top:0;z-index:10;background:var(--bg);border-bottom:1px solid var(--border);padding:0.75rem 0;margin-bottom:2rem}
|
|
231
|
+
@media(max-width:600px){.cols-2,.cols-3,.cols-60-40{grid-template-columns:1fr}.comparison{grid-template-columns:1fr}.comp-a{border-right:none;border-bottom:1px solid var(--border)}.signatures{flex-direction:column;gap:1.5rem}}
|
|
232
|
+
@media print{.doc-nav{display:none}.cover{min-height:auto;page-break-after:always}.page-break{break-after:page}.box,.box-dark,.box-outline,.box-accent,.note,.kv,.metrics,.timeline-item,.step,.comparison,.doc-figure{break-inside:avoid}.doc-figure img{max-height:40vh}.doc-img-float-left,.doc-img-float-right{float:none;max-width:60%;margin:1rem auto;display:block}}
|
|
233
|
+
`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ─── Build HTML ───
|
|
237
|
+
export function build(content, brand, { title, isHtml = false } = {}) {
|
|
238
|
+
const body = isHtml ? content : parseMd(content);
|
|
239
|
+
const s = STYLE_PRESETS[brand.style] || STYLE_PRESETS.modern;
|
|
240
|
+
const c = brand.colors;
|
|
241
|
+
const f = brand.fonts;
|
|
242
|
+
|
|
243
|
+
const logo = brand.logo
|
|
244
|
+
? `<div class="doc-header"><img src="${brand.logo}" alt="${brand.name}" class="doc-logo"/></div>`
|
|
245
|
+
: brand.name
|
|
246
|
+
? `<div class="doc-header"><div class="doc-logo-text">${brand.name}</div>${brand.tagline ? `<div class="doc-tagline">${brand.tagline}</div>` : ""}</div>`
|
|
247
|
+
: "";
|
|
248
|
+
|
|
249
|
+
const footerParts = [];
|
|
250
|
+
if (brand.name) footerParts.push(brand.name);
|
|
251
|
+
if (brand.footer?.showContact && brand.web) footerParts.push(brand.web);
|
|
252
|
+
if (brand.footer?.showContact && brand.email) footerParts.push(brand.email);
|
|
253
|
+
const footerIcon = brand.icon
|
|
254
|
+
? `<img src="${brand.icon}" alt="" class="doc-footer-icon"/>`
|
|
255
|
+
: "";
|
|
256
|
+
const footerNote = brand.footer?.note
|
|
257
|
+
? `<div class="doc-footer-note">${brand.footer.note}</div>`
|
|
258
|
+
: "";
|
|
259
|
+
const footer = footerParts.length
|
|
260
|
+
? `<footer class="doc-footer">${footerIcon}<div class="doc-footer-meta">${footerParts.join(" · ")}</div>${footerNote}</footer>`
|
|
261
|
+
: "";
|
|
262
|
+
|
|
263
|
+
// h2 border style
|
|
264
|
+
let h2BorderCSS = "";
|
|
265
|
+
if (s.h2Border === "line") h2BorderCSS = `padding-top:2rem;border-top:1px solid var(--border);`;
|
|
266
|
+
else if (s.h2Border === "accent") h2BorderCSS = `padding-top:2rem;`;
|
|
267
|
+
|
|
268
|
+
return `<!DOCTYPE html>
|
|
269
|
+
<html lang="cs">
|
|
270
|
+
<head>
|
|
271
|
+
<meta charset="UTF-8">
|
|
272
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
273
|
+
<title>${title || brand.name || "Dokument"}</title>
|
|
274
|
+
<style>
|
|
275
|
+
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
|
276
|
+
:root{--primary:${c.primary};--accent:${c.accent};--muted:${c.muted};--surface:${c.surface};--bg:#FFFFFF;--border:#E8E8ED;--border-strong:#D2D2D7;--text:#2C2C2E;--text-light:#636366;--heading:${f.heading};--body:${f.body}}
|
|
277
|
+
html{font-size:16px;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
|
|
278
|
+
body{font-family:var(--body);font-size:${s.bodySize};font-weight:400;line-height:${s.lineHeight};color:var(--text);background:var(--bg);max-width:720px;margin:0 auto;padding:3rem 2rem 4rem}
|
|
279
|
+
.doc-header{margin-bottom:2.5rem;padding-bottom:1.75rem;border-bottom:1px solid var(--border)}
|
|
280
|
+
.doc-logo{max-height:40px;width:auto;display:block}
|
|
281
|
+
.doc-logo-text{font-family:var(--heading);font-size:1.25rem;font-weight:700;color:var(--primary);letter-spacing:-0.01em}
|
|
282
|
+
.doc-tagline{font-size:0.8125rem;color:var(--muted);margin-top:0.25rem}
|
|
283
|
+
h1{font-family:var(--heading);font-size:${s.h1Size};font-weight:${s.h1Weight};line-height:1.1;letter-spacing:${s.h1Spacing};color:var(--primary);margin-bottom:1.5rem}
|
|
284
|
+
h2{font-family:var(--heading);font-size:${s.h2Size};font-weight:${s.h2Weight};line-height:1.2;letter-spacing:-0.01em;color:var(--primary);margin:${s.sectionSpacing} 0 1.25rem;${h2BorderCSS}}
|
|
285
|
+
h2:first-child,h1+h2{border-top:none;padding-top:0}
|
|
286
|
+
h2:first-child::before{display:none}
|
|
287
|
+
h3{font-family:var(--body);font-size:${s.h3Size};font-weight:600;line-height:1.35;color:var(--primary);margin:2rem 0 0.625rem}
|
|
288
|
+
h4{font-family:var(--body);font-size:0.75rem;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;color:var(--muted);margin:1.5rem 0 0.5rem}
|
|
289
|
+
p{margin-bottom:1rem} strong{font-weight:600;color:var(--primary)} em{font-style:italic}
|
|
290
|
+
a{color:var(--accent);text-decoration:underline;text-underline-offset:3px;text-decoration-thickness:1px}
|
|
291
|
+
hr{border:none;height:1px;background:var(--border);margin:${s.sectionSpacing} 0}
|
|
292
|
+
ul,ol{margin:0 0 1rem;padding-left:1.25rem} li{margin-bottom:0.5rem;line-height:1.65} li::marker{color:var(--muted)}
|
|
293
|
+
table{width:100%;border-collapse:collapse;margin:${s.componentSpacing} 0 2rem;font-size:0.8125rem;line-height:1.5}
|
|
294
|
+
${tableCSS(s.tableStyle)}
|
|
295
|
+
${componentCSS(s)}
|
|
296
|
+
${styleCSS(s)}
|
|
297
|
+
.doc-footer{margin-top:4rem;padding-top:2rem;border-top:1px solid var(--border);text-align:center}
|
|
298
|
+
.doc-footer-icon{height:20px;width:auto;margin-bottom:0.75rem;opacity:0.25}
|
|
299
|
+
.doc-footer-meta{font-size:0.75rem;color:var(--muted);letter-spacing:0.02em}
|
|
300
|
+
.doc-footer-note{font-size:0.6875rem;color:var(--muted);margin-top:0.5rem;opacity:0.7}
|
|
301
|
+
@media print{
|
|
302
|
+
@page{size:A4;margin:20mm 18mm 22mm 18mm}
|
|
303
|
+
body{max-width:none;padding:0;font-size:10pt;line-height:1.6}
|
|
304
|
+
h1{font-size:26pt} h2{font-size:15pt;margin-top:1.5rem;padding-top:1rem;break-before:auto}
|
|
305
|
+
h2,h3,h4{break-after:avoid} table,blockquote{break-inside:avoid} tr{break-inside:avoid}
|
|
306
|
+
thead{display:table-header-group} p{orphans:3;widows:3} .doc-footer{break-inside:avoid}
|
|
307
|
+
a{text-decoration:none;color:var(--text)}
|
|
308
|
+
*{-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
|
309
|
+
}
|
|
310
|
+
</style>
|
|
311
|
+
</head>
|
|
312
|
+
<body>
|
|
313
|
+
${logo}
|
|
314
|
+
${body}
|
|
315
|
+
${footer}
|
|
316
|
+
</body>
|
|
317
|
+
</html>`;
|
|
318
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export const COMPONENTS = [
|
|
2
|
+
{
|
|
3
|
+
name: "box",
|
|
4
|
+
description: "Zvýrazněný box s pozadím",
|
|
5
|
+
variants: ["box", "box-dark", "box-outline", "box-accent"],
|
|
6
|
+
example: '<div class="box"><h3>Titulek</h3><p>Obsah boxu</p></div>',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: "note",
|
|
10
|
+
description: "Poznámka / upozornění",
|
|
11
|
+
variants: ["note", "note-important"],
|
|
12
|
+
example: '<div class="note"><p>Důležitá poznámka</p></div>',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "kv",
|
|
16
|
+
description: "Páry klíč–hodnota",
|
|
17
|
+
variants: ["kv"],
|
|
18
|
+
example:
|
|
19
|
+
'<div class="kv"><div class="kv-row"><span class="kv-key">Klient</span><span class="kv-val">ACME s.r.o.</span></div></div>',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "metrics",
|
|
23
|
+
description: "Velká čísla v gridu",
|
|
24
|
+
variants: ["metrics"],
|
|
25
|
+
example:
|
|
26
|
+
'<div class="metrics"><div class="metric"><div class="metric-value">42</div><div class="metric-label">Projektů</div></div></div>',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "timeline",
|
|
30
|
+
description: "Časová osa",
|
|
31
|
+
variants: ["timeline"],
|
|
32
|
+
example:
|
|
33
|
+
'<div class="timeline"><div class="timeline-item"><div class="timeline-date">Leden 2026</div><div class="timeline-content"><h4>Kick-off</h4><p>Zahájení projektu</p></div></div></div>',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "steps",
|
|
37
|
+
description: "Číslované kroky",
|
|
38
|
+
variants: ["steps"],
|
|
39
|
+
example:
|
|
40
|
+
'<div class="steps"><div class="step"><div class="step-num">1</div><div class="step-content"><h4>Analýza</h4><p>Zmapujeme potřeby</p></div></div></div>',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "columns",
|
|
44
|
+
description: "Vícesloupcový layout",
|
|
45
|
+
variants: ["cols cols-2", "cols cols-3", "cols cols-60-40"],
|
|
46
|
+
example:
|
|
47
|
+
'<div class="cols cols-2"><div>Levý sloupec</div><div>Pravý sloupec</div></div>',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "comparison",
|
|
51
|
+
description: "Porovnání dvou variant",
|
|
52
|
+
variants: ["comparison"],
|
|
53
|
+
example:
|
|
54
|
+
'<div class="comparison"><div class="comp-a"><h4>Varianta A</h4><p>Popis</p></div><div class="comp-b"><h4>Varianta B</h4><p>Popis</p></div></div>',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "badge",
|
|
58
|
+
description: "Status štítek",
|
|
59
|
+
variants: ["badge", "badge-success", "badge-warning", "badge-danger"],
|
|
60
|
+
example: '<span class="badge badge-success">Aktivní</span>',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "checklist",
|
|
64
|
+
description: "Seznam s checkboxy",
|
|
65
|
+
variants: ["checklist"],
|
|
66
|
+
example:
|
|
67
|
+
'<ul class="checklist"><li class="done">Hotovo</li><li>Zbývá</li></ul>',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "pull-quote",
|
|
71
|
+
description: "Velký citát",
|
|
72
|
+
variants: ["pull-quote"],
|
|
73
|
+
example:
|
|
74
|
+
'<blockquote class="pull-quote">Design is not how it looks, but how it works.</blockquote>',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "cover",
|
|
78
|
+
description: "Titulní strana dokumentu",
|
|
79
|
+
variants: ["cover"],
|
|
80
|
+
example:
|
|
81
|
+
'<div class="cover"><h1>Název dokumentu</h1><p>Podtitulek</p></div>',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "page-break",
|
|
85
|
+
description: "Zalomení stránky při tisku",
|
|
86
|
+
variants: ["page-break"],
|
|
87
|
+
example: '<div class="page-break"></div>',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "figure",
|
|
91
|
+
description: "Obrázek s volitelným popiskem (markdown syntaxe)",
|
|
92
|
+
variants: ["doc-figure"],
|
|
93
|
+
example: '',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "image-layout",
|
|
97
|
+
description: "CSS třídy pro pozicování obrázků",
|
|
98
|
+
variants: ["doc-img-full", "doc-img-half", "doc-img-float-left", "doc-img-float-right"],
|
|
99
|
+
example: '<img src="foto.jpg" class="doc-img-float-left" alt="Foto"/>',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "gallery",
|
|
103
|
+
description: "Grid galerie obrázků",
|
|
104
|
+
variants: ["doc-gallery"],
|
|
105
|
+
example: '<div class="doc-gallery"><img src="a.jpg" alt=""/><img src="b.jpg" alt=""/><img src="c.jpg" alt=""/></div>',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "signatures",
|
|
109
|
+
description: "Podpisové bloky",
|
|
110
|
+
variants: ["signatures"],
|
|
111
|
+
example:
|
|
112
|
+
'<div class="signatures"><div class="sig"><div class="sig-line"></div><div class="sig-name">Jan Novák</div></div></div>',
|
|
113
|
+
},
|
|
114
|
+
// ── Invoice-specific components ──
|
|
115
|
+
{
|
|
116
|
+
name: "invoice-label",
|
|
117
|
+
description: "Velký nápis FAKTURA (pro styl invoice)",
|
|
118
|
+
variants: ["invoice-label"],
|
|
119
|
+
example: '<div class="invoice-label">FAKTURA</div>',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "invoice-parties",
|
|
123
|
+
description: "Dva sloupce dodavatel/odběratel (pro styl invoice)",
|
|
124
|
+
variants: ["invoice-parties"],
|
|
125
|
+
example:
|
|
126
|
+
'<div class="invoice-parties"><div><h4>Dodavatel</h4><p><strong>Firma s.r.o.</strong><br>Ulice 123<br>Praha</p></div><div><h4>Odběratel</h4><p><strong>Klient a.s.</strong><br>Ulice 456<br>Brno</p></div></div>',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "invoice-total",
|
|
130
|
+
description: "Součet faktury — mezisoučet, DPH, celkem (pro styl invoice)",
|
|
131
|
+
variants: ["invoice-total", "invoice-total-box"],
|
|
132
|
+
example:
|
|
133
|
+
'<div class="invoice-total"><div class="invoice-total-box"><div class="kv"><div class="kv-row"><span class="kv-key">Mezisoučet</span><span class="kv-val">50 000 Kč</span></div><div class="kv-row"><span class="kv-key">DPH 21%</span><span class="kv-val">10 500 Kč</span></div><div class="kv-row"><span class="kv-key">Celkem</span><span class="kv-val">60 500 Kč</span></div></div></div></div>',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "invoice-payment",
|
|
137
|
+
description: "Platební údaje (pro styl invoice)",
|
|
138
|
+
variants: ["invoice-payment"],
|
|
139
|
+
example:
|
|
140
|
+
'<div class="invoice-payment"><h4>Platební údaje</h4><div class="kv"><div class="kv-row"><span class="kv-key">Banka</span><span class="kv-val">Fio banka</span></div><div class="kv-row"><span class="kv-key">Číslo účtu</span><span class="kv-val">1234567890/2010</span></div><div class="kv-row"><span class="kv-key">VS</span><span class="kv-val">20260001</span></div></div></div>',
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
export function listComponents() {
|
|
145
|
+
return COMPONENTS;
|
|
146
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export function parseMd(md) {
|
|
2
|
+
return md
|
|
3
|
+
// Images:  and 
|
|
4
|
+
.replace(
|
|
5
|
+
/^!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)\s*$/gm,
|
|
6
|
+
(_, alt, src, caption) => {
|
|
7
|
+
const img = `<img src="${src}" alt="${alt}" loading="lazy"/>`;
|
|
8
|
+
if (caption) {
|
|
9
|
+
return `<figure class="doc-figure">${img}<figcaption>${caption}</figcaption></figure>`;
|
|
10
|
+
}
|
|
11
|
+
return `<figure class="doc-figure">${img}</figure>`;
|
|
12
|
+
},
|
|
13
|
+
)
|
|
14
|
+
// Inline images (within text): 
|
|
15
|
+
.replace(
|
|
16
|
+
/!\[([^\]]*)\]\(([^)]+)\)/g,
|
|
17
|
+
(_, alt, src) => `<img src="${src}" alt="${alt}" class="doc-img-inline" loading="lazy"/>`,
|
|
18
|
+
)
|
|
19
|
+
// Tables
|
|
20
|
+
.replace(
|
|
21
|
+
/^(\|.+\|)\n(\|[-| :]+\|)\n((?:\|.+\|\n?)*)/gm,
|
|
22
|
+
(_, header, _sep, body) => {
|
|
23
|
+
const ths = header
|
|
24
|
+
.split("|")
|
|
25
|
+
.filter((c) => c.trim())
|
|
26
|
+
.map((c) => `<th>${c.trim()}</th>`)
|
|
27
|
+
.join("");
|
|
28
|
+
const rows = body
|
|
29
|
+
.trim()
|
|
30
|
+
.split("\n")
|
|
31
|
+
.map((row) => {
|
|
32
|
+
const cells = row
|
|
33
|
+
.split("|")
|
|
34
|
+
.filter((c) => c.trim())
|
|
35
|
+
.map((c) => {
|
|
36
|
+
const t = c.trim();
|
|
37
|
+
const bold = t.startsWith("**") && t.endsWith("**");
|
|
38
|
+
return `<td>${bold ? `<strong>${t.slice(2, -2)}</strong>` : t}</td>`;
|
|
39
|
+
})
|
|
40
|
+
.join("");
|
|
41
|
+
return `<tr>${cells}</tr>`;
|
|
42
|
+
})
|
|
43
|
+
.join("\n");
|
|
44
|
+
return `<table><thead><tr>${ths}</tr></thead><tbody>${rows}</tbody></table>`;
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
// Headings
|
|
48
|
+
.replace(/^#### (.+)$/gm, "<h4>$1</h4>")
|
|
49
|
+
.replace(/^### (.+)$/gm, "<h3>$1</h3>")
|
|
50
|
+
.replace(/^## (.+)$/gm, "<h2>$1</h2>")
|
|
51
|
+
.replace(/^# (.+)$/gm, "<h1>$1</h1>")
|
|
52
|
+
// Horizontal rule
|
|
53
|
+
.replace(/^---+$/gm, "<hr/>")
|
|
54
|
+
// Inline formatting
|
|
55
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, "<strong><em>$1</em></strong>")
|
|
56
|
+
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
|
|
57
|
+
.replace(/\*(.+?)\*/g, "<em>$1</em>")
|
|
58
|
+
// Lists
|
|
59
|
+
.replace(/^(\d+)\. (.+)$/gm, "<oli>$2</oli>")
|
|
60
|
+
.replace(/^[-*] (.+)$/gm, "<uli>$1</uli>")
|
|
61
|
+
.replace(/((?:<uli>.+<\/uli>\n?)+)/g, (m) =>
|
|
62
|
+
`<ul>${m.replace(/<\/?uli>/g, (s) => (s === "<uli>" ? "<li>" : "</li>"))}</ul>`,
|
|
63
|
+
)
|
|
64
|
+
.replace(/((?:<oli>.+<\/oli>\n?)+)/g, (m) =>
|
|
65
|
+
`<ol>${m.replace(/<\/?oli>/g, (s) => (s === "<oli>" ? "<li>" : "</li>"))}</ol>`,
|
|
66
|
+
)
|
|
67
|
+
// Paragraphs (lines not starting with HTML tag)
|
|
68
|
+
.replace(/^(?!<[a-z/]|$)(.+)$/gm, "<p>$1</p>");
|
|
69
|
+
}
|
package/src/core/pdf.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function exportPdf(html, outputPath, { format = "A4" } = {}) {
|
|
2
|
+
let puppeteer;
|
|
3
|
+
try {
|
|
4
|
+
puppeteer = await import("puppeteer");
|
|
5
|
+
} catch {
|
|
6
|
+
throw new Error(
|
|
7
|
+
"Puppeteer is required for PDF export. Install it: npm install puppeteer",
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const browser = await puppeteer.default.launch({ headless: true });
|
|
12
|
+
try {
|
|
13
|
+
const page = await browser.newPage();
|
|
14
|
+
await page.setContent(html, { waitUntil: "networkidle0" });
|
|
15
|
+
await page.pdf({
|
|
16
|
+
path: outputPath,
|
|
17
|
+
format,
|
|
18
|
+
printBackground: true,
|
|
19
|
+
margin: { top: "20mm", right: "18mm", bottom: "22mm", left: "18mm" },
|
|
20
|
+
});
|
|
21
|
+
} finally {
|
|
22
|
+
await browser.close();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { writeFile, readFile } from "node:fs/promises";
|
|
5
|
+
import { resolve, dirname, basename, extname } from "node:path";
|
|
6
|
+
import { build, mergeBrand, loadBrand, listComponents, exportPdf } from "../core/index.js";
|
|
7
|
+
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: "modern-document",
|
|
10
|
+
version: "0.1.0",
|
|
11
|
+
description: "Build branded HTML/PDF documents from markdown",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Load default brand from MODDOC_BRAND env var if set
|
|
15
|
+
let defaultBrandConfig = null;
|
|
16
|
+
if (process.env.MODDOC_BRAND) {
|
|
17
|
+
try {
|
|
18
|
+
defaultBrandConfig = await loadBrand(resolve(process.env.MODDOC_BRAND));
|
|
19
|
+
} catch (e) {
|
|
20
|
+
console.error(`Warning: Could not load brand from MODDOC_BRAND="${process.env.MODDOC_BRAND}": ${e.message}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── build_document ───
|
|
25
|
+
server.tool(
|
|
26
|
+
"build_document",
|
|
27
|
+
"Build a branded HTML document from markdown content. Returns the HTML and optionally saves to file.",
|
|
28
|
+
{
|
|
29
|
+
content: z.string().describe("Markdown or HTML content to render"),
|
|
30
|
+
title: z.string().optional().describe("Document title"),
|
|
31
|
+
brand: z.string().optional().describe("Path to brand.json file"),
|
|
32
|
+
brandOverrides: z
|
|
33
|
+
.object({
|
|
34
|
+
name: z.string().optional(),
|
|
35
|
+
tagline: z.string().optional(),
|
|
36
|
+
web: z.string().optional(),
|
|
37
|
+
email: z.string().optional(),
|
|
38
|
+
colors: z
|
|
39
|
+
.object({
|
|
40
|
+
primary: z.string().optional(),
|
|
41
|
+
accent: z.string().optional(),
|
|
42
|
+
muted: z.string().optional(),
|
|
43
|
+
surface: z.string().optional(),
|
|
44
|
+
})
|
|
45
|
+
.optional(),
|
|
46
|
+
fonts: z
|
|
47
|
+
.object({
|
|
48
|
+
heading: z.string().optional(),
|
|
49
|
+
body: z.string().optional(),
|
|
50
|
+
})
|
|
51
|
+
.optional(),
|
|
52
|
+
style: z.enum(["modern", "formal", "minimal", "executive", "creative", "technical", "invoice", "compact"]).optional(),
|
|
53
|
+
})
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("Inline brand overrides"),
|
|
56
|
+
outputPath: z.string().optional().describe("Where to save the HTML file"),
|
|
57
|
+
isHtml: z
|
|
58
|
+
.boolean()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("Set to true if content is already HTML, not markdown"),
|
|
61
|
+
},
|
|
62
|
+
async ({ content, title, brand: brandPath, brandOverrides, outputPath, isHtml }) => {
|
|
63
|
+
let brandConfig;
|
|
64
|
+
if (brandPath) {
|
|
65
|
+
brandConfig = await loadBrand(resolve(brandPath));
|
|
66
|
+
if (brandOverrides) brandConfig = mergeBrand({ ...brandConfig, ...brandOverrides });
|
|
67
|
+
} else if (defaultBrandConfig) {
|
|
68
|
+
brandConfig = brandOverrides ? mergeBrand({ ...defaultBrandConfig, ...brandOverrides }) : defaultBrandConfig;
|
|
69
|
+
} else {
|
|
70
|
+
brandConfig = mergeBrand(brandOverrides);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const html = build(content, brandConfig, { title, isHtml });
|
|
74
|
+
const sizeKB = (Buffer.byteLength(html, "utf-8") / 1024).toFixed(1);
|
|
75
|
+
|
|
76
|
+
if (outputPath) {
|
|
77
|
+
const fullPath = resolve(outputPath);
|
|
78
|
+
await writeFile(fullPath, html, "utf-8");
|
|
79
|
+
return {
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: "text",
|
|
83
|
+
text: `Document saved to ${fullPath} (${sizeKB} KB)\n\nThe HTML file is self-contained — it can be opened directly in any browser, sent via email/chat, or printed to PDF.`,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: "text", text: html }],
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// ─── export_pdf ───
|
|
96
|
+
server.tool(
|
|
97
|
+
"export_pdf",
|
|
98
|
+
"Convert an HTML document to A4 PDF using headless browser rendering.",
|
|
99
|
+
{
|
|
100
|
+
htmlPath: z.string().describe("Path to the HTML file to convert"),
|
|
101
|
+
outputPath: z.string().optional().describe("Where to save the PDF (default: same name .pdf)"),
|
|
102
|
+
format: z.enum(["A4", "Letter"]).optional().describe("Paper format"),
|
|
103
|
+
},
|
|
104
|
+
async ({ htmlPath, outputPath, format }) => {
|
|
105
|
+
const fullHtmlPath = resolve(htmlPath);
|
|
106
|
+
const html = await readFile(fullHtmlPath, "utf-8");
|
|
107
|
+
const pdfPath = outputPath
|
|
108
|
+
? resolve(outputPath)
|
|
109
|
+
: fullHtmlPath.replace(extname(fullHtmlPath), ".pdf");
|
|
110
|
+
|
|
111
|
+
await exportPdf(html, pdfPath, { format: format || "A4" });
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
content: [
|
|
115
|
+
{
|
|
116
|
+
type: "text",
|
|
117
|
+
text: `PDF saved to ${pdfPath}`,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// ─── full_pipeline ───
|
|
125
|
+
server.tool(
|
|
126
|
+
"full_pipeline",
|
|
127
|
+
"Complete pipeline: markdown → branded HTML → PDF in one step.",
|
|
128
|
+
{
|
|
129
|
+
content: z.string().describe("Markdown content"),
|
|
130
|
+
title: z.string().optional().describe("Document title"),
|
|
131
|
+
brand: z.string().optional().describe("Path to brand.json"),
|
|
132
|
+
brandOverrides: z
|
|
133
|
+
.object({
|
|
134
|
+
name: z.string().optional(),
|
|
135
|
+
colors: z
|
|
136
|
+
.object({
|
|
137
|
+
primary: z.string().optional(),
|
|
138
|
+
accent: z.string().optional(),
|
|
139
|
+
})
|
|
140
|
+
.optional(),
|
|
141
|
+
style: z.enum(["modern", "formal", "minimal", "executive", "creative", "technical", "invoice", "compact"]).optional(),
|
|
142
|
+
})
|
|
143
|
+
.optional(),
|
|
144
|
+
outputDir: z.string().optional().describe("Output directory (default: current)"),
|
|
145
|
+
filename: z.string().optional().describe("Base filename without extension"),
|
|
146
|
+
formats: z
|
|
147
|
+
.array(z.enum(["html", "pdf"]))
|
|
148
|
+
.optional()
|
|
149
|
+
.describe("Which formats to generate (default: both)"),
|
|
150
|
+
},
|
|
151
|
+
async ({ content, title, brand: brandPath, brandOverrides, outputDir, filename, formats }) => {
|
|
152
|
+
let brandConfig;
|
|
153
|
+
if (brandPath) {
|
|
154
|
+
brandConfig = await loadBrand(resolve(brandPath));
|
|
155
|
+
if (brandOverrides) brandConfig = mergeBrand({ ...brandConfig, ...brandOverrides });
|
|
156
|
+
} else if (defaultBrandConfig) {
|
|
157
|
+
brandConfig = brandOverrides ? mergeBrand({ ...defaultBrandConfig, ...brandOverrides }) : defaultBrandConfig;
|
|
158
|
+
} else {
|
|
159
|
+
brandConfig = mergeBrand(brandOverrides);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const base = filename || "document";
|
|
163
|
+
const dir = outputDir ? resolve(outputDir) : process.cwd();
|
|
164
|
+
const wantHtml = !formats || formats.includes("html");
|
|
165
|
+
const wantPdf = !formats || formats.includes("pdf");
|
|
166
|
+
|
|
167
|
+
const html = build(content, brandConfig, { title });
|
|
168
|
+
const results = [];
|
|
169
|
+
|
|
170
|
+
if (wantHtml) {
|
|
171
|
+
const htmlPath = resolve(dir, `${base}.html`);
|
|
172
|
+
await writeFile(htmlPath, html, "utf-8");
|
|
173
|
+
results.push(`HTML: ${htmlPath} (${(Buffer.byteLength(html, "utf-8") / 1024).toFixed(1)} KB)`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (wantPdf) {
|
|
177
|
+
const pdfPath = resolve(dir, `${base}.pdf`);
|
|
178
|
+
await exportPdf(html, pdfPath);
|
|
179
|
+
results.push(`PDF: ${pdfPath}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
content: [{ type: "text", text: results.join("\n") }],
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// ─── list_components ───
|
|
189
|
+
server.tool(
|
|
190
|
+
"list_components",
|
|
191
|
+
"List all available HTML components you can use in documents (boxes, timelines, metrics, etc.).",
|
|
192
|
+
{},
|
|
193
|
+
async () => {
|
|
194
|
+
const components = listComponents();
|
|
195
|
+
const text = components
|
|
196
|
+
.map(
|
|
197
|
+
(c) =>
|
|
198
|
+
`### ${c.name}\n${c.description}\nVariants: ${c.variants.join(", ")}\n\`\`\`html\n${c.example}\n\`\`\``,
|
|
199
|
+
)
|
|
200
|
+
.join("\n\n");
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: "text", text }],
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// ─── list_styles ───
|
|
208
|
+
server.tool(
|
|
209
|
+
"list_styles",
|
|
210
|
+
"List available document style presets.",
|
|
211
|
+
{},
|
|
212
|
+
async () => {
|
|
213
|
+
const { STYLE_PRESETS } = await import("../core/build.js");
|
|
214
|
+
const lines = Object.entries(STYLE_PRESETS).map(
|
|
215
|
+
([key, s]) => `**${key}** — ${s.label}: ${s.desc}`
|
|
216
|
+
);
|
|
217
|
+
return {
|
|
218
|
+
content: [{ type: "text", text: lines.join("\n\n") }],
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// ─── Start ───
|
|
224
|
+
const transport = new StdioServerTransport();
|
|
225
|
+
await server.connect(transport);
|