ghost-paper 0.2.0 → 0.3.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/README.md +5 -3
- package/dist/cli.js +3 -2
- package/package.json +2 -2
- package/dist/-5YRZFEDB.js +0 -10
- package/dist/-6A745MKY.js +0 -10
- package/dist/-ANANGS26.js +0 -10
- package/dist/-EON5YM73.js +0 -10
- package/dist/-FCFRRCIG.js +0 -10
- package/dist/-HRRPL4SN.js +0 -10
- package/dist/-K6SQE2E5.js +0 -10
- package/dist/-N6RDWVBC.js +0 -10
- package/dist/-NRPITI2A.js +0 -10
- package/dist/-OKZHAHO3.js +0 -10
- package/dist/-RMMSGBXC.js +0 -10
- package/dist/-RZB6N6OZ.js +0 -10
- package/dist/-TG5MSQYZ.js +0 -10
- package/dist/-TTLTI6LQ.js +0 -10
- package/dist/-U5MQJGM2.js +0 -10
- package/dist/-YCMFHAET.js +0 -10
- package/dist/chunk-2QOHE4NF.js +0 -1464
- package/dist/chunk-3HX4SVJH.js +0 -1504
- package/dist/chunk-4P2YAHD2.js +0 -1522
- package/dist/chunk-6WF3MSUR.js +0 -1362
- package/dist/chunk-DSRRH4T2.js +0 -1425
- package/dist/chunk-J2XA7LOH.js +0 -1463
- package/dist/chunk-MNZVZ2ZA.js +0 -1464
- package/dist/chunk-NUWIGEJ3.js +0 -1462
- package/dist/chunk-QMHJ3BGD.js +0 -1471
- package/dist/chunk-RMODJYHI.js +0 -1526
- package/dist/chunk-SOKYK7YV.js +0 -1496
- package/dist/chunk-TV6ABXBU.js +0 -1534
- package/dist/chunk-TW7HQTU6.js +0 -1444
- package/dist/chunk-U2G5K2PR.js +0 -1465
- package/dist/chunk-UWUILD43.js +0 -1471
- package/dist/chunk-ZRSWDAXH.js +0 -1433
- package/dist/pdf-ADH6BBSB.js +0 -71
- package/dist/pdf-EDRHZK4V.js +0 -72
- package/dist/pdf-HC6MMT72.js +0 -62
- package/dist/pdf-SKUNOP45.js +0 -62
- package/dist/pdf-TQDMPPSC.js +0 -71
- package/dist/pdf-VCMDLIDX.js +0 -71
- package/dist/pdf-WLYWKSHR.js +0 -71
package/dist/chunk-3HX4SVJH.js
DELETED
|
@@ -1,1504 +0,0 @@
|
|
|
1
|
-
// src/parse.ts
|
|
2
|
-
import { unified } from "unified";
|
|
3
|
-
import remarkParse from "remark-parse";
|
|
4
|
-
import remarkGfm from "remark-gfm";
|
|
5
|
-
import remarkFrontmatter from "remark-frontmatter";
|
|
6
|
-
import YAML from "yaml";
|
|
7
|
-
|
|
8
|
-
// src/classify.ts
|
|
9
|
-
var TIME_PATTERNS = [
|
|
10
|
-
/^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\b/i,
|
|
11
|
-
/^(January|February|March|April|May|June|July|August|September|October|November|December)\b/i,
|
|
12
|
-
/^Q[1-4]\b/i,
|
|
13
|
-
/^W\d{1,2}$/i,
|
|
14
|
-
/^\d{4}[-\/]\d{2}/,
|
|
15
|
-
/^\d{4}$/,
|
|
16
|
-
/^(Week|Wk)\s*\d+/i,
|
|
17
|
-
/^(H[12])\b/i,
|
|
18
|
-
/^(FY\s*\d{2,4})/i
|
|
19
|
-
];
|
|
20
|
-
function isNumericValue(s) {
|
|
21
|
-
if (!s || s === "\u2014" || s === "\u2013" || s === "-" || s === "N/A" || s === "n/a") return false;
|
|
22
|
-
const stripped = s.replace(/\s*\(.*?\)\s*/g, "").trim();
|
|
23
|
-
if (!stripped) return false;
|
|
24
|
-
const cleaned = stripped.replace(/[$€£¥%,+\s]/g, "").replace(/(K|M|B|pp|bps|x|mo)$/i, "").replace(/^[+-]/, "");
|
|
25
|
-
if (!cleaned) return false;
|
|
26
|
-
return !isNaN(Number(cleaned)) && isFinite(Number(cleaned));
|
|
27
|
-
}
|
|
28
|
-
function parseNumericValue(s) {
|
|
29
|
-
if (!s) return null;
|
|
30
|
-
const stripped = s.replace(/\s*\(.*?\)\s*/g, "").trim();
|
|
31
|
-
const cleaned = stripped.replace(/[$€£¥%,\s]/g, "").replace(/(pp|bps|mo)$/i, "");
|
|
32
|
-
const match = cleaned.match(/^([+-]?\d+\.?\d*)\s*(K|M|B)?$/i);
|
|
33
|
-
if (!match) return null;
|
|
34
|
-
return parseFloat(match[1]);
|
|
35
|
-
}
|
|
36
|
-
function isTimeValue(s) {
|
|
37
|
-
return TIME_PATTERNS.some((p) => p.test(s.trim()));
|
|
38
|
-
}
|
|
39
|
-
function columnIsNumeric(rows, colIndex) {
|
|
40
|
-
const values = rows.map((r) => r[colIndex]).filter((v) => v && v !== "\u2014" && v !== "\u2013");
|
|
41
|
-
if (values.length === 0) return false;
|
|
42
|
-
const numericCount = values.filter(isNumericValue).length;
|
|
43
|
-
return numericCount / values.length >= 0.7;
|
|
44
|
-
}
|
|
45
|
-
function columnIsTime(rows, colIndex) {
|
|
46
|
-
const values = rows.map((r) => r[colIndex]).filter(Boolean);
|
|
47
|
-
if (values.length === 0) return false;
|
|
48
|
-
const timeCount = values.filter(isTimeValue).length;
|
|
49
|
-
return timeCount / values.length >= 0.6;
|
|
50
|
-
}
|
|
51
|
-
function countNumericColumns(table) {
|
|
52
|
-
let count = 0;
|
|
53
|
-
for (let i = 0; i < table.headers.length; i++) {
|
|
54
|
-
if (columnIsNumeric(table.rows, i)) count++;
|
|
55
|
-
}
|
|
56
|
-
return count;
|
|
57
|
-
}
|
|
58
|
-
function classifyTable(table) {
|
|
59
|
-
const { headers, rows } = table;
|
|
60
|
-
const rowCount = rows.length;
|
|
61
|
-
const colCount = headers.length;
|
|
62
|
-
if (rowCount === 0 || colCount === 0) return "table";
|
|
63
|
-
if (rowCount >= 2 && rowCount <= 6 && colCount >= 2 && colCount <= 4) {
|
|
64
|
-
const numericCols = [];
|
|
65
|
-
const textCols = [];
|
|
66
|
-
for (let i = 0; i < colCount; i++) {
|
|
67
|
-
if (columnIsNumeric(rows, i)) numericCols.push(i);
|
|
68
|
-
else textCols.push(i);
|
|
69
|
-
}
|
|
70
|
-
if (textCols.length >= 1 && numericCols.length >= 1) {
|
|
71
|
-
const headerStr = headers.join(" ").toLowerCase();
|
|
72
|
-
const hasMetricHeader = /metric|name|label|kpi|measure|indicator/i.test(headerStr);
|
|
73
|
-
const hasValueHeader = /value|amount|number|result|total|figure/i.test(headerStr);
|
|
74
|
-
const hasChangeHeader = /change|delta|trend|vs|growth|yoy|qoq|mom|diff/i.test(headerStr);
|
|
75
|
-
if (hasMetricHeader || hasValueHeader && colCount <= 3 || hasChangeHeader && hasValueHeader) {
|
|
76
|
-
return "kpi";
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (colCount >= 2 && rowCount >= 3 && columnIsTime(rows, 0)) {
|
|
81
|
-
const numericAfterFirst = countNumericColumns({ headers: headers.slice(1), rows: rows.map((r) => r.slice(1)) });
|
|
82
|
-
const totalDataCells = rowCount * (colCount - 1);
|
|
83
|
-
let emptyCells = 0;
|
|
84
|
-
for (let r = 0; r < rowCount; r++) {
|
|
85
|
-
for (let c = 1; c < colCount; c++) {
|
|
86
|
-
const v = rows[r][c];
|
|
87
|
-
if (!v || v === "\u2014" || v === "\u2013" || v === "-" || v === "N/A" || v === "n/a") emptyCells++;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
const sparseRatio = emptyCells / totalDataCells;
|
|
91
|
-
if (numericAfterFirst >= 1 && sparseRatio < 0.25) return "line";
|
|
92
|
-
}
|
|
93
|
-
if (colCount >= 2 && rowCount >= 3) {
|
|
94
|
-
const firstIsText = !columnIsNumeric(rows, 0) && !columnIsTime(rows, 0);
|
|
95
|
-
if (firstIsText) {
|
|
96
|
-
const afterFirst = { headers: headers.slice(1), rows: rows.map((r) => r.slice(1)) };
|
|
97
|
-
const numericAfterFirst = countNumericColumns(afterFirst);
|
|
98
|
-
const textAfterFirst = colCount - 1 - numericAfterFirst;
|
|
99
|
-
if (numericAfterFirst === 1 && textAfterFirst === 0) return "bar";
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (colCount === 2 && rowCount >= 2 && rowCount <= 8) {
|
|
103
|
-
const firstIsText = !columnIsNumeric(rows, 0);
|
|
104
|
-
const secondIsNumeric = columnIsNumeric(rows, 1);
|
|
105
|
-
if (firstIsText && secondIsNumeric) {
|
|
106
|
-
const allPositive = rows.every((r) => {
|
|
107
|
-
const v = parseNumericValue(r[1]);
|
|
108
|
-
return v !== null && v > 0;
|
|
109
|
-
});
|
|
110
|
-
if (allPositive) return "pie";
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return "table";
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// src/parse.ts
|
|
117
|
-
var chartCounter = 0;
|
|
118
|
-
function inlineToHtml(nodes) {
|
|
119
|
-
return nodes.map((node) => {
|
|
120
|
-
switch (node.type) {
|
|
121
|
-
case "text":
|
|
122
|
-
return escapeHtml(node.value);
|
|
123
|
-
case "strong":
|
|
124
|
-
return `<strong>${inlineToHtml(node.children)}</strong>`;
|
|
125
|
-
case "emphasis":
|
|
126
|
-
return `<em>${inlineToHtml(node.children)}</em>`;
|
|
127
|
-
case "inlineCode":
|
|
128
|
-
return `<code>${escapeHtml(node.value)}</code>`;
|
|
129
|
-
case "link":
|
|
130
|
-
return `<a href="${escapeHtml(node.url)}">${inlineToHtml(node.children)}</a>`;
|
|
131
|
-
case "break":
|
|
132
|
-
return "<br>";
|
|
133
|
-
default:
|
|
134
|
-
if ("children" in node) {
|
|
135
|
-
return inlineToHtml(node.children);
|
|
136
|
-
}
|
|
137
|
-
if ("value" in node) {
|
|
138
|
-
return escapeHtml(node.value);
|
|
139
|
-
}
|
|
140
|
-
return "";
|
|
141
|
-
}
|
|
142
|
-
}).join("");
|
|
143
|
-
}
|
|
144
|
-
function inlineToText(nodes) {
|
|
145
|
-
return nodes.map((node) => {
|
|
146
|
-
switch (node.type) {
|
|
147
|
-
case "text":
|
|
148
|
-
return node.value;
|
|
149
|
-
case "strong":
|
|
150
|
-
return inlineToText(node.children);
|
|
151
|
-
case "emphasis":
|
|
152
|
-
return inlineToText(node.children);
|
|
153
|
-
case "inlineCode":
|
|
154
|
-
return node.value;
|
|
155
|
-
case "link":
|
|
156
|
-
return inlineToText(node.children);
|
|
157
|
-
default:
|
|
158
|
-
if ("children" in node) return inlineToText(node.children);
|
|
159
|
-
if ("value" in node) return node.value;
|
|
160
|
-
return "";
|
|
161
|
-
}
|
|
162
|
-
}).join("");
|
|
163
|
-
}
|
|
164
|
-
function headingText(node) {
|
|
165
|
-
return inlineToText(node.children);
|
|
166
|
-
}
|
|
167
|
-
function extractTable(node) {
|
|
168
|
-
const rows = node.children;
|
|
169
|
-
if (rows.length === 0) return { headers: [], rows: [] };
|
|
170
|
-
const headers = rows[0].children.map((cell) => inlineToText(cell.children));
|
|
171
|
-
const dataRows = rows.slice(1).map(
|
|
172
|
-
(row) => row.children.map((cell) => inlineToText(cell.children))
|
|
173
|
-
);
|
|
174
|
-
return { headers, rows: dataRows };
|
|
175
|
-
}
|
|
176
|
-
function blockquoteToHtml(node) {
|
|
177
|
-
return node.children.map((child) => {
|
|
178
|
-
if (child.type === "paragraph") {
|
|
179
|
-
return inlineToHtml(child.children);
|
|
180
|
-
}
|
|
181
|
-
return "";
|
|
182
|
-
}).join("\n");
|
|
183
|
-
}
|
|
184
|
-
function paragraphToHtml(node) {
|
|
185
|
-
return inlineToHtml(node.children);
|
|
186
|
-
}
|
|
187
|
-
function isCaption(node) {
|
|
188
|
-
if (node.type !== "paragraph") return null;
|
|
189
|
-
const para = node;
|
|
190
|
-
if (para.children.length === 1 && para.children[0].type === "emphasis") {
|
|
191
|
-
return inlineToText(para.children[0].children);
|
|
192
|
-
}
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
function slugify(s) {
|
|
196
|
-
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
197
|
-
}
|
|
198
|
-
function parse(markdown) {
|
|
199
|
-
chartCounter = 0;
|
|
200
|
-
const tree = unified().use(remarkParse).use(remarkGfm).use(remarkFrontmatter, ["yaml"]).parse(markdown);
|
|
201
|
-
let frontmatter = { title: "Report" };
|
|
202
|
-
const fmNode = tree.children.find((n) => n.type === "yaml");
|
|
203
|
-
if (fmNode && fmNode.type === "yaml") {
|
|
204
|
-
try {
|
|
205
|
-
const parsed = YAML.parse(fmNode.value);
|
|
206
|
-
if (parsed.title) frontmatter.title = parsed.title;
|
|
207
|
-
if (parsed.subtitle) frontmatter.subtitle = parsed.subtitle;
|
|
208
|
-
} catch {
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
const nodes = tree.children.filter((n) => n.type !== "yaml");
|
|
212
|
-
const h1Count = nodes.filter((n) => n.type === "heading" && n.depth === 1).length;
|
|
213
|
-
const h2Count = nodes.filter((n) => n.type === "heading" && n.depth === 2).length;
|
|
214
|
-
const tabDepth = h1Count === 0 && h2Count >= 2 ? 2 : 1;
|
|
215
|
-
const sectionDepth = tabDepth + 1;
|
|
216
|
-
const tabs = [];
|
|
217
|
-
let currentTab = null;
|
|
218
|
-
if (tabDepth === 2 && h1Count === 1) {
|
|
219
|
-
const h1Node = nodes.find((n) => n.type === "heading" && n.depth === 1);
|
|
220
|
-
if (h1Node) {
|
|
221
|
-
const h1Title = headingText(h1Node);
|
|
222
|
-
if (frontmatter.title === "Report") {
|
|
223
|
-
frontmatter.title = h1Title;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
228
|
-
const node = nodes[i];
|
|
229
|
-
if (tabDepth === 2 && node.type === "heading" && node.depth === 1) {
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
if (node.type === "heading" && node.depth === tabDepth) {
|
|
233
|
-
const title = headingText(node);
|
|
234
|
-
currentTab = { title, elements: [] };
|
|
235
|
-
tabs.push(currentTab);
|
|
236
|
-
if (i + 1 < nodes.length && nodes[i + 1].type === "blockquote") {
|
|
237
|
-
currentTab.subtitle = blockquoteToHtml(nodes[i + 1]);
|
|
238
|
-
i++;
|
|
239
|
-
}
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
if (!currentTab) {
|
|
243
|
-
currentTab = { title: frontmatter.title, elements: [] };
|
|
244
|
-
tabs.push(currentTab);
|
|
245
|
-
}
|
|
246
|
-
if (node.type === "heading" && node.depth === sectionDepth) {
|
|
247
|
-
const text = headingText(node);
|
|
248
|
-
currentTab.elements.push({ kind: "heading2", text, id: slugify(text) });
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
if (node.type === "heading" && node.depth > sectionDepth) {
|
|
252
|
-
const text = headingText(node);
|
|
253
|
-
currentTab.elements.push({ kind: "paragraph", html: `<strong>${escapeHtml(text)}</strong>` });
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
if (node.type === "table") {
|
|
257
|
-
let caption;
|
|
258
|
-
const prev = i > 0 ? nodes[i - 1] : null;
|
|
259
|
-
if (prev) {
|
|
260
|
-
const cap = isCaption(prev);
|
|
261
|
-
if (cap) {
|
|
262
|
-
caption = cap;
|
|
263
|
-
const lastEl = currentTab.elements[currentTab.elements.length - 1];
|
|
264
|
-
if (lastEl && lastEl.kind === "paragraph") {
|
|
265
|
-
currentTab.elements.pop();
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
const tableData = extractTable(node);
|
|
270
|
-
const chartType = classifyTable(tableData);
|
|
271
|
-
if (chartType === "kpi") {
|
|
272
|
-
currentTab.elements.push({ kind: "kpi", table: tableData });
|
|
273
|
-
} else if (chartType === "table") {
|
|
274
|
-
currentTab.elements.push({ kind: "table", table: tableData });
|
|
275
|
-
} else {
|
|
276
|
-
const chartId = `chart-${chartCounter++}`;
|
|
277
|
-
currentTab.elements.push({
|
|
278
|
-
kind: "chart",
|
|
279
|
-
chartType,
|
|
280
|
-
table: tableData,
|
|
281
|
-
chartId,
|
|
282
|
-
caption
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
if (node.type === "blockquote") {
|
|
288
|
-
const html = blockquoteToHtml(node);
|
|
289
|
-
currentTab.elements.push({ kind: "aside", html });
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
if (node.type === "paragraph") {
|
|
293
|
-
const html = paragraphToHtml(node);
|
|
294
|
-
currentTab.elements.push({ kind: "paragraph", html });
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
if (node.type === "list") {
|
|
298
|
-
const listTag = node.ordered ? "ol" : "ul";
|
|
299
|
-
const items = node.children.map((item) => {
|
|
300
|
-
const content = item.children.map((child) => {
|
|
301
|
-
if (child.type === "paragraph") return inlineToHtml(child.children);
|
|
302
|
-
return "";
|
|
303
|
-
}).join("");
|
|
304
|
-
return `<li>${content}</li>`;
|
|
305
|
-
}).join("\n");
|
|
306
|
-
currentTab.elements.push({ kind: "paragraph", html: `<${listTag}>
|
|
307
|
-
${items}
|
|
308
|
-
</${listTag}>` });
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
if (tabs.length === 0) {
|
|
313
|
-
tabs.push({ title: frontmatter.title, elements: [] });
|
|
314
|
-
}
|
|
315
|
-
if (frontmatter.title === "Report" && tabs.length > 0) {
|
|
316
|
-
frontmatter.title = tabs[0].title;
|
|
317
|
-
}
|
|
318
|
-
return { frontmatter, tabs };
|
|
319
|
-
}
|
|
320
|
-
function escapeHtml(s) {
|
|
321
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// src/charts.ts
|
|
325
|
-
function chartPreamble(printMode = false) {
|
|
326
|
-
const labelFont = printMode ? "Geist Sans, sans-serif" : "Geist Mono, monospace";
|
|
327
|
-
const labelSize = printMode ? 10 : 12;
|
|
328
|
-
const printModeJs = printMode ? "true" : "false";
|
|
329
|
-
return `
|
|
330
|
-
var PRINT_MODE = ${printModeJs};
|
|
331
|
-
const ACCENT = '#D6F500';
|
|
332
|
-
const CHARCOAL = '#2A2A28';
|
|
333
|
-
const CHARCOAL_MID = '#6B6B64';
|
|
334
|
-
const CHARCOAL_LIGHT = '#A8A8A0';
|
|
335
|
-
const WARM_GRAY = '#D5D5D0';
|
|
336
|
-
const POSITIVE = '#22C55E';
|
|
337
|
-
const NEGATIVE = '#EF4444';
|
|
338
|
-
const TEXT_PRI = '#141414';
|
|
339
|
-
const TEXT_SEC = '#555550';
|
|
340
|
-
const TEXT_MUT = '#8C8C86';
|
|
341
|
-
const GRID_LINE = '#EDEDEA';
|
|
342
|
-
const BORDER_C = '#E0E0DB';
|
|
343
|
-
var LABEL_FONT = '${labelFont}';
|
|
344
|
-
var LABEL_SIZE = ${labelSize};
|
|
345
|
-
|
|
346
|
-
const PALETTE = [CHARCOAL, ACCENT, CHARCOAL_MID, CHARCOAL_LIGHT, WARM_GRAY];
|
|
347
|
-
|
|
348
|
-
// In multi-column print, fix chart container widths to actual column width
|
|
349
|
-
if (PRINT_MODE) {
|
|
350
|
-
(function() {
|
|
351
|
-
var tc = document.querySelector('.tab-content');
|
|
352
|
-
if (!tc) return;
|
|
353
|
-
var cs = getComputedStyle(tc);
|
|
354
|
-
var cols = parseInt(cs.columnCount) || 1;
|
|
355
|
-
if (cols <= 1) return;
|
|
356
|
-
var gap = parseFloat(cs.columnGap) || 0;
|
|
357
|
-
var colW = Math.floor((tc.clientWidth - gap * (cols - 1)) / cols);
|
|
358
|
-
document.querySelectorAll('.chart-container').forEach(function(el) {
|
|
359
|
-
el.style.width = colW + 'px';
|
|
360
|
-
el.style.maxWidth = colW + 'px';
|
|
361
|
-
});
|
|
362
|
-
})();
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function makeChart(id) {
|
|
366
|
-
var el = document.getElementById(id);
|
|
367
|
-
if (!el) return null;
|
|
368
|
-
var chart = echarts.init(el);
|
|
369
|
-
if (!PRINT_MODE) {
|
|
370
|
-
new ResizeObserver(function() { chart.resize(); }).observe(el);
|
|
371
|
-
}
|
|
372
|
-
return chart;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
var ax = {
|
|
376
|
-
axisLine: { lineStyle: { color: BORDER_C } },
|
|
377
|
-
axisTick: { show: false },
|
|
378
|
-
axisLabel: { color: TEXT_SEC, fontFamily: LABEL_FONT, fontSize: LABEL_SIZE },
|
|
379
|
-
splitLine: { lineStyle: { color: GRID_LINE } }
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
var tt = {
|
|
383
|
-
backgroundColor: '#fff',
|
|
384
|
-
borderColor: BORDER_C,
|
|
385
|
-
borderWidth: 1,
|
|
386
|
-
textStyle: { color: TEXT_PRI, fontFamily: 'Geist Sans, sans-serif', fontSize: 14, lineHeight: 20 },
|
|
387
|
-
extraCssText: 'border-radius: 8px; box-shadow: 0 4px 24px rgba(0,0,0,0.1); padding: 12px 14px;'
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
var lg = function(data) {
|
|
391
|
-
return {
|
|
392
|
-
data: data, top: 0, right: 0,
|
|
393
|
-
textStyle: { color: TEXT_SEC, fontFamily: LABEL_FONT, fontSize: LABEL_SIZE },
|
|
394
|
-
icon: 'roundRect', itemWidth: 10, itemHeight: 5, itemGap: 12
|
|
395
|
-
};
|
|
396
|
-
};
|
|
397
|
-
`;
|
|
398
|
-
}
|
|
399
|
-
function detectFormat(values) {
|
|
400
|
-
let dollarCount = 0;
|
|
401
|
-
let pctCount = 0;
|
|
402
|
-
let kCount = 0;
|
|
403
|
-
let mCount = 0;
|
|
404
|
-
let bCount = 0;
|
|
405
|
-
for (const v of values) {
|
|
406
|
-
if (v.includes("$")) dollarCount++;
|
|
407
|
-
if (v.includes("%")) pctCount++;
|
|
408
|
-
if (/\d+K\b/i.test(v)) kCount++;
|
|
409
|
-
if (/\d+M\b/i.test(v) || /\.\d+M\b/i.test(v)) mCount++;
|
|
410
|
-
if (/\d+B\b/i.test(v) || /\.\d+B\b/i.test(v)) bCount++;
|
|
411
|
-
}
|
|
412
|
-
const total = values.length;
|
|
413
|
-
const pickSuffix = () => {
|
|
414
|
-
if (mCount / total > 0.3) return "M";
|
|
415
|
-
if (bCount / total > 0.3) return "B";
|
|
416
|
-
if (kCount / total > 0.3) return "K";
|
|
417
|
-
return "";
|
|
418
|
-
};
|
|
419
|
-
if (dollarCount / total > 0.5) return { prefix: "$", suffix: pickSuffix() };
|
|
420
|
-
if (pctCount / total > 0.5) return { prefix: "", suffix: "%" };
|
|
421
|
-
if (kCount / total > 0.5) return { prefix: "", suffix: "K" };
|
|
422
|
-
if (mCount / total > 0.5) return { prefix: "", suffix: "M" };
|
|
423
|
-
return { prefix: "", suffix: "" };
|
|
424
|
-
}
|
|
425
|
-
function extractColumnData(rows, colIndex) {
|
|
426
|
-
return rows.map((r) => parseNumericValue(r[colIndex]));
|
|
427
|
-
}
|
|
428
|
-
function lineChartJs(chartId, table) {
|
|
429
|
-
const xData = table.rows.map((r) => r[0]);
|
|
430
|
-
const seriesNames = [];
|
|
431
|
-
const seriesData = [];
|
|
432
|
-
for (let i = 1; i < table.headers.length; i++) {
|
|
433
|
-
seriesNames.push(table.headers[i]);
|
|
434
|
-
seriesData.push(extractColumnData(table.rows, i));
|
|
435
|
-
}
|
|
436
|
-
let useDualAxis = false;
|
|
437
|
-
if (seriesData.length === 2) {
|
|
438
|
-
const max0 = Math.max(...seriesData[0].filter((v) => v !== null));
|
|
439
|
-
const max1 = Math.max(...seriesData[1].filter((v) => v !== null));
|
|
440
|
-
if (max0 > 0 && max1 > 0) {
|
|
441
|
-
const ratio = Math.max(max0, max1) / Math.min(max0, max1);
|
|
442
|
-
if (ratio >= 2.5) useDualAxis = true;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
const formats = [];
|
|
446
|
-
for (let i = 1; i < table.headers.length; i++) {
|
|
447
|
-
formats.push(detectFormat(table.rows.map((r) => r[i])));
|
|
448
|
-
}
|
|
449
|
-
const xDataStr = JSON.stringify(xData);
|
|
450
|
-
let yAxisStr;
|
|
451
|
-
if (useDualAxis) {
|
|
452
|
-
const f0 = formats[0];
|
|
453
|
-
const f1 = formats[1];
|
|
454
|
-
yAxisStr = `[
|
|
455
|
-
{ ...ax, type: 'value', position: 'left',
|
|
456
|
-
axisLabel: { ...ax.axisLabel, formatter: function(v) { return '${f0.prefix}' + v + '${f0.suffix}'; } }
|
|
457
|
-
},
|
|
458
|
-
{ ...ax, type: 'value', position: 'right', splitLine: { show: false },
|
|
459
|
-
axisLabel: { ...ax.axisLabel, formatter: function(v) { return '${f1.prefix}' + v + '${f1.suffix}'; } }
|
|
460
|
-
}
|
|
461
|
-
]`;
|
|
462
|
-
} else {
|
|
463
|
-
const f = formats[0] || { prefix: "", suffix: "" };
|
|
464
|
-
yAxisStr = `{ ...ax, type: 'value',
|
|
465
|
-
axisLabel: { ...ax.axisLabel, formatter: function(v) { return '${f.prefix}' + v + '${f.suffix}'; } }
|
|
466
|
-
}`;
|
|
467
|
-
}
|
|
468
|
-
const colors = ["CHARCOAL", "ACCENT", "CHARCOAL_MID", "CHARCOAL_LIGHT", "WARM_GRAY"];
|
|
469
|
-
const seriesStr = seriesData.map((data, i) => {
|
|
470
|
-
const color = colors[i % colors.length];
|
|
471
|
-
const yAxisIndex = useDualAxis ? `, yAxisIndex: ${i}` : "";
|
|
472
|
-
const area = i === 0 ? `,
|
|
473
|
-
areaStyle: {
|
|
474
|
-
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
475
|
-
{ offset: 0, color: 'rgba(42,42,40,0.1)' },
|
|
476
|
-
{ offset: 1, color: 'rgba(42,42,40,0.01)' }
|
|
477
|
-
])
|
|
478
|
-
}` : "";
|
|
479
|
-
return `{
|
|
480
|
-
name: ${JSON.stringify(seriesNames[i])}, type: 'line'${yAxisIndex}, data: ${JSON.stringify(data)}, smooth: 0.3,
|
|
481
|
-
symbol: 'circle', symbolSize: ${i === 0 ? 6 : 5},
|
|
482
|
-
lineStyle: { color: ${color}, width: 2.5 },
|
|
483
|
-
itemStyle: { color: ${color}${i === 1 && useDualAxis ? ", borderColor: CHARCOAL, borderWidth: 1" : ""} }${area}
|
|
484
|
-
}`;
|
|
485
|
-
}).join(",\n ");
|
|
486
|
-
const legendStr = seriesNames.length > 1 ? `legend: lg(${JSON.stringify(seriesNames)}),` : "";
|
|
487
|
-
const fmtArray = JSON.stringify(formats.map((f) => ({ prefix: f.prefix, suffix: f.suffix })));
|
|
488
|
-
return `
|
|
489
|
-
(() => {
|
|
490
|
-
var chart = makeChart(${JSON.stringify(chartId)});
|
|
491
|
-
if (!chart) return;
|
|
492
|
-
var fmts = ${fmtArray};
|
|
493
|
-
chart.setOption({
|
|
494
|
-
tooltip: { ...tt, trigger: 'axis',
|
|
495
|
-
formatter: function(params) {
|
|
496
|
-
var lines = '<strong>' + params[0].axisValue + '</strong>';
|
|
497
|
-
params.forEach(function(p) {
|
|
498
|
-
var f = fmts[p.seriesIndex] || { prefix: '', suffix: '' };
|
|
499
|
-
lines += '<br/>' + p.marker + ' ' + p.seriesName + ' <strong>' + f.prefix + p.value + f.suffix + '</strong>';
|
|
500
|
-
});
|
|
501
|
-
return lines;
|
|
502
|
-
}
|
|
503
|
-
},
|
|
504
|
-
${legendStr}
|
|
505
|
-
grid: { left: PRINT_MODE ? 36 : 50, right: PRINT_MODE ? 36 : ${useDualAxis ? 50 : 12}, top: ${seriesNames.length > 1 ? 32 : 16}, bottom: 24, containLabel: PRINT_MODE },
|
|
506
|
-
xAxis: { ...ax, type: 'category', data: ${xDataStr}, boundaryGap: false },
|
|
507
|
-
yAxis: ${yAxisStr},
|
|
508
|
-
series: [
|
|
509
|
-
${seriesStr}
|
|
510
|
-
]
|
|
511
|
-
});
|
|
512
|
-
})();`;
|
|
513
|
-
}
|
|
514
|
-
function barChartJs(chartId, table) {
|
|
515
|
-
const categories = table.rows.map((r) => r[0]);
|
|
516
|
-
let numColIndex = 1;
|
|
517
|
-
for (let i = 1; i < table.headers.length; i++) {
|
|
518
|
-
const vals = table.rows.map((r) => r[i]);
|
|
519
|
-
if (vals.some((v) => parseNumericValue(v) !== null)) {
|
|
520
|
-
numColIndex = i;
|
|
521
|
-
break;
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
const values = table.rows.map((r) => parseNumericValue(r[numColIndex]) ?? 0);
|
|
525
|
-
const fmt = detectFormat(table.rows.map((r) => r[numColIndex]));
|
|
526
|
-
const maxLabelLen = Math.max(...categories.map((c) => c.length));
|
|
527
|
-
const leftMargin = Math.min(300, Math.max(80, maxLabelLen * 8 + 16));
|
|
528
|
-
return `
|
|
529
|
-
(() => {
|
|
530
|
-
var chart = makeChart(${JSON.stringify(chartId)});
|
|
531
|
-
if (!chart) return;
|
|
532
|
-
var categories = ${JSON.stringify(categories)};
|
|
533
|
-
var values = ${JSON.stringify(values)};
|
|
534
|
-
chart.setOption({
|
|
535
|
-
tooltip: { ...tt, trigger: 'axis', axisPointer: { type: 'shadow' },
|
|
536
|
-
formatter: function(p) { return '<strong>' + p[0].axisValue + '</strong><br/>${table.headers[numColIndex]}: <strong>${fmt.prefix}' + p[0].value + '${fmt.suffix}</strong>'; }
|
|
537
|
-
},
|
|
538
|
-
grid: { left: PRINT_MODE ? '42%' : ${leftMargin}, right: PRINT_MODE ? 36 : 52, top: 28, bottom: 4 },
|
|
539
|
-
title: {
|
|
540
|
-
text: ${JSON.stringify(table.headers[numColIndex])},
|
|
541
|
-
left: PRINT_MODE ? '42%' : ${leftMargin},
|
|
542
|
-
top: 0,
|
|
543
|
-
textStyle: { fontFamily: LABEL_FONT, fontSize: 13, fontWeight: 600, color: TEXT_SEC }
|
|
544
|
-
},
|
|
545
|
-
xAxis: { type: 'value', show: false },
|
|
546
|
-
yAxis: { ...ax, type: 'category', data: categories, axisLine: { show: false }, splitLine: { show: false } },
|
|
547
|
-
series: [{
|
|
548
|
-
type: 'bar',
|
|
549
|
-
data: (function() {
|
|
550
|
-
var maxVal = Math.max.apply(null, values);
|
|
551
|
-
return values.map(function(v) {
|
|
552
|
-
return {
|
|
553
|
-
value: v,
|
|
554
|
-
itemStyle: {
|
|
555
|
-
color: v === maxVal ? ACCENT : CHARCOAL,
|
|
556
|
-
borderRadius: [0, 4, 4, 0]
|
|
557
|
-
}
|
|
558
|
-
};
|
|
559
|
-
});
|
|
560
|
-
})(),
|
|
561
|
-
barWidth: 24,
|
|
562
|
-
barCategoryGap: '20%',
|
|
563
|
-
label: { show: true, position: 'right',
|
|
564
|
-
color: TEXT_SEC,
|
|
565
|
-
fontFamily: LABEL_FONT, fontSize: LABEL_SIZE, fontWeight: 600,
|
|
566
|
-
formatter: function(p) { return '${fmt.prefix}' + p.value + '${fmt.suffix}'; }
|
|
567
|
-
}
|
|
568
|
-
}]
|
|
569
|
-
});
|
|
570
|
-
})();`;
|
|
571
|
-
}
|
|
572
|
-
function pieChartJs(chartId, table) {
|
|
573
|
-
const data = table.rows.map((r) => ({
|
|
574
|
-
name: r[0],
|
|
575
|
-
value: parseNumericValue(r[1]) ?? 0
|
|
576
|
-
}));
|
|
577
|
-
return `
|
|
578
|
-
(() => {
|
|
579
|
-
var chart = makeChart(${JSON.stringify(chartId)});
|
|
580
|
-
if (!chart) return;
|
|
581
|
-
var data = ${JSON.stringify(data)};
|
|
582
|
-
chart.setOption({
|
|
583
|
-
tooltip: { ...tt, trigger: 'item',
|
|
584
|
-
formatter: function(p) { return '<strong>' + p.name + '</strong><br/>' + p.value + '%'; }
|
|
585
|
-
},
|
|
586
|
-
series: [{
|
|
587
|
-
type: 'pie', radius: ['46%', '76%'], center: ['50%', '46%'],
|
|
588
|
-
padAngle: 2, itemStyle: { borderRadius: 4, borderColor: '#F4F4F1', borderWidth: 2 },
|
|
589
|
-
label: {
|
|
590
|
-
color: TEXT_SEC, fontFamily: LABEL_FONT, fontSize: LABEL_SIZE,
|
|
591
|
-
formatter: '{b}\\n{d}%', lineHeight: 16
|
|
592
|
-
},
|
|
593
|
-
labelLine: { lineStyle: { color: BORDER_C }, length: 10, length2: 14 },
|
|
594
|
-
data: data.map(function(d, i) { return { value: d.value, name: d.name, itemStyle: { color: PALETTE[i % PALETTE.length] } }; }),
|
|
595
|
-
emphasis: { itemStyle: { shadowBlur: 12, shadowColor: 'rgba(0,0,0,0.12)' } }
|
|
596
|
-
}]
|
|
597
|
-
});
|
|
598
|
-
})();`;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// src/template.ts
|
|
602
|
-
var CSS = `
|
|
603
|
-
@font-face {
|
|
604
|
-
font-family: 'Geist Sans';
|
|
605
|
-
src: url('https://cdn.jsdelivr.net/npm/geist@1.2.0/dist/fonts/geist-sans/Geist-Regular.woff2') format('woff2');
|
|
606
|
-
font-weight: 400;
|
|
607
|
-
}
|
|
608
|
-
@font-face {
|
|
609
|
-
font-family: 'Geist Sans';
|
|
610
|
-
src: url('https://cdn.jsdelivr.net/npm/geist@1.2.0/dist/fonts/geist-sans/Geist-Medium.woff2') format('woff2');
|
|
611
|
-
font-weight: 500;
|
|
612
|
-
}
|
|
613
|
-
@font-face {
|
|
614
|
-
font-family: 'Geist Sans';
|
|
615
|
-
src: url('https://cdn.jsdelivr.net/npm/geist@1.2.0/dist/fonts/geist-sans/Geist-SemiBold.woff2') format('woff2');
|
|
616
|
-
font-weight: 600;
|
|
617
|
-
}
|
|
618
|
-
@font-face {
|
|
619
|
-
font-family: 'Geist Sans';
|
|
620
|
-
src: url('https://cdn.jsdelivr.net/npm/geist@1.2.0/dist/fonts/geist-sans/Geist-Bold.woff2') format('woff2');
|
|
621
|
-
font-weight: 700;
|
|
622
|
-
}
|
|
623
|
-
@font-face {
|
|
624
|
-
font-family: 'Geist Sans';
|
|
625
|
-
src: url('https://cdn.jsdelivr.net/npm/geist@1.2.0/dist/fonts/geist-sans/Geist-Black.woff2') format('woff2');
|
|
626
|
-
font-weight: 900;
|
|
627
|
-
}
|
|
628
|
-
@font-face {
|
|
629
|
-
font-family: 'Geist Mono';
|
|
630
|
-
src: url('https://cdn.jsdelivr.net/npm/geist@1.2.0/dist/fonts/geist-mono/GeistMono-Regular.woff2') format('woff2');
|
|
631
|
-
font-weight: 400;
|
|
632
|
-
}
|
|
633
|
-
@font-face {
|
|
634
|
-
font-family: 'Geist Mono';
|
|
635
|
-
src: url('https://cdn.jsdelivr.net/npm/geist@1.2.0/dist/fonts/geist-mono/GeistMono-Medium.woff2') format('woff2');
|
|
636
|
-
font-weight: 500;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
:root {
|
|
640
|
-
--bg-base: #F4F4F1;
|
|
641
|
-
--bg-sidebar: #FAFAF8;
|
|
642
|
-
--border: #E0E0DB;
|
|
643
|
-
--border-strong: #C8C8C2;
|
|
644
|
-
--text-primary: #141414;
|
|
645
|
-
--text-body: #1A1A18;
|
|
646
|
-
--text-secondary: #555550;
|
|
647
|
-
--text-muted: #8C8C86;
|
|
648
|
-
--accent: #D6F500;
|
|
649
|
-
--accent-on: #141414;
|
|
650
|
-
--charcoal: #2A2A28;
|
|
651
|
-
--positive: #22C55E;
|
|
652
|
-
--negative: #EF4444;
|
|
653
|
-
--sidebar-width: 320px;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
657
|
-
|
|
658
|
-
body {
|
|
659
|
-
background: var(--bg-base);
|
|
660
|
-
color: var(--text-body);
|
|
661
|
-
font-family: 'Geist Sans', -apple-system, system-ui, sans-serif;
|
|
662
|
-
font-size: 16px;
|
|
663
|
-
line-height: 1.6;
|
|
664
|
-
-webkit-font-smoothing: antialiased;
|
|
665
|
-
text-rendering: optimizeLegibility;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
.sidebar {
|
|
669
|
-
position: fixed;
|
|
670
|
-
top: 0; left: 0;
|
|
671
|
-
width: var(--sidebar-width);
|
|
672
|
-
height: 100vh;
|
|
673
|
-
background: var(--bg-sidebar);
|
|
674
|
-
border-right: 1px solid var(--border);
|
|
675
|
-
display: flex;
|
|
676
|
-
flex-direction: column;
|
|
677
|
-
z-index: 100;
|
|
678
|
-
overflow-y: auto;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
.sidebar-header {
|
|
682
|
-
padding: 32px 24px 28px;
|
|
683
|
-
border-bottom: 1px solid var(--border);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
.sidebar-brand {
|
|
687
|
-
font-family: 'Geist Mono', monospace;
|
|
688
|
-
font-size: 11px;
|
|
689
|
-
font-weight: 500;
|
|
690
|
-
letter-spacing: 2.5px;
|
|
691
|
-
text-transform: uppercase;
|
|
692
|
-
color: var(--text-muted);
|
|
693
|
-
margin-bottom: 18px;
|
|
694
|
-
display: flex;
|
|
695
|
-
align-items: center;
|
|
696
|
-
gap: 7px;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
.sidebar-brand::before {
|
|
700
|
-
content: '';
|
|
701
|
-
width: 8px; height: 8px;
|
|
702
|
-
border-radius: 2px;
|
|
703
|
-
background: var(--accent);
|
|
704
|
-
border: 1px solid rgba(0,0,0,0.1);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
.sidebar-title {
|
|
708
|
-
font-size: 26px;
|
|
709
|
-
font-weight: 900;
|
|
710
|
-
color: var(--text-primary);
|
|
711
|
-
letter-spacing: -0.8px;
|
|
712
|
-
line-height: 1.15;
|
|
713
|
-
padding-bottom: 16px;
|
|
714
|
-
border-bottom: 3px solid var(--accent);
|
|
715
|
-
margin-bottom: 4px;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
.sidebar-subtitle {
|
|
719
|
-
font-size: 13px;
|
|
720
|
-
color: var(--text-secondary);
|
|
721
|
-
margin-top: 12px;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
.sidebar-nav {
|
|
725
|
-
flex: 1;
|
|
726
|
-
padding: 12px 10px;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
.nav-item {
|
|
730
|
-
display: flex;
|
|
731
|
-
align-items: center;
|
|
732
|
-
gap: 8px;
|
|
733
|
-
padding: 7px 12px;
|
|
734
|
-
border-radius: 7px;
|
|
735
|
-
cursor: pointer;
|
|
736
|
-
font-size: 14px;
|
|
737
|
-
font-weight: 500;
|
|
738
|
-
color: var(--text-secondary);
|
|
739
|
-
transition: all 0.12s ease;
|
|
740
|
-
margin-bottom: 1px;
|
|
741
|
-
user-select: none;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
.nav-item:hover {
|
|
745
|
-
background: rgba(0,0,0,0.04);
|
|
746
|
-
color: var(--text-primary);
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
.nav-item.active {
|
|
750
|
-
background: var(--text-primary);
|
|
751
|
-
color: #fff;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
.nav-number {
|
|
755
|
-
font-family: 'Geist Mono', monospace;
|
|
756
|
-
font-size: 10px;
|
|
757
|
-
font-weight: 500;
|
|
758
|
-
width: 20px; height: 20px;
|
|
759
|
-
display: flex;
|
|
760
|
-
align-items: center;
|
|
761
|
-
justify-content: center;
|
|
762
|
-
border-radius: 5px;
|
|
763
|
-
background: rgba(0,0,0,0.06);
|
|
764
|
-
color: var(--text-muted);
|
|
765
|
-
flex-shrink: 0;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
.nav-item.active .nav-number {
|
|
769
|
-
background: var(--accent);
|
|
770
|
-
color: var(--text-primary);
|
|
771
|
-
font-weight: 600;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
.sidebar-footer {
|
|
775
|
-
padding: 18px 24px 22px;
|
|
776
|
-
border-top: 1px solid var(--border);
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
.sidebar-footer-text {
|
|
780
|
-
font-family: 'Geist Mono', monospace;
|
|
781
|
-
font-size: 10.5px;
|
|
782
|
-
color: var(--text-muted);
|
|
783
|
-
letter-spacing: 0.5px;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
.main {
|
|
787
|
-
margin-left: var(--sidebar-width);
|
|
788
|
-
min-height: 100vh;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
.tab-content {
|
|
792
|
-
display: none;
|
|
793
|
-
padding: 40px 48px 72px;
|
|
794
|
-
max-width: 680px;
|
|
795
|
-
margin: 0 auto;
|
|
796
|
-
animation: fadeIn 0.2s ease;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
.tab-content.active { display: block; }
|
|
800
|
-
|
|
801
|
-
@keyframes fadeIn {
|
|
802
|
-
from { opacity: 0; transform: translateY(4px); }
|
|
803
|
-
to { opacity: 1; transform: translateY(0); }
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
.tab-content h1 {
|
|
807
|
-
font-size: 46px;
|
|
808
|
-
font-weight: 600;
|
|
809
|
-
letter-spacing: -1.8px;
|
|
810
|
-
color: var(--text-primary);
|
|
811
|
-
margin-bottom: 24px;
|
|
812
|
-
padding-bottom: 20px;
|
|
813
|
-
border-bottom: 1px solid var(--border);
|
|
814
|
-
line-height: 1.05;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
.tab-content .subtitle {
|
|
818
|
-
font-size: 18px;
|
|
819
|
-
color: var(--text-secondary);
|
|
820
|
-
margin-bottom: 28px;
|
|
821
|
-
line-height: 1.5;
|
|
822
|
-
font-weight: 400;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
.tab-content h2 {
|
|
826
|
-
font-size: 21px;
|
|
827
|
-
font-weight: 700;
|
|
828
|
-
letter-spacing: -0.3px;
|
|
829
|
-
color: var(--text-primary);
|
|
830
|
-
margin-top: 28px;
|
|
831
|
-
margin-bottom: 12px;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
.tab-content p {
|
|
835
|
-
font-size: 16px;
|
|
836
|
-
color: var(--text-body);
|
|
837
|
-
line-height: 1.8;
|
|
838
|
-
margin-bottom: 20px;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
.tab-content ul, .tab-content ol {
|
|
842
|
-
font-size: 16px;
|
|
843
|
-
color: var(--text-body);
|
|
844
|
-
line-height: 1.8;
|
|
845
|
-
margin-bottom: 20px;
|
|
846
|
-
padding-left: 24px;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
.tab-content li {
|
|
850
|
-
margin-bottom: 6px;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
.tab-content li:last-child {
|
|
854
|
-
margin-bottom: 0;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
.kpi-strip {
|
|
858
|
-
display: grid;
|
|
859
|
-
grid-auto-columns: 1fr;
|
|
860
|
-
grid-auto-flow: column;
|
|
861
|
-
margin: 28px 0;
|
|
862
|
-
padding: 20px 0;
|
|
863
|
-
border-top: 1px solid var(--border);
|
|
864
|
-
border-bottom: 1px solid var(--border);
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
.kpi {
|
|
868
|
-
min-width: 0;
|
|
869
|
-
padding: 0 20px;
|
|
870
|
-
border-left: 1px solid var(--border);
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
.kpi:first-child {
|
|
874
|
-
padding-left: 0;
|
|
875
|
-
border-left: none;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
.kpi-value {
|
|
879
|
-
font-size: 26px;
|
|
880
|
-
font-weight: 900;
|
|
881
|
-
letter-spacing: -0.8px;
|
|
882
|
-
color: var(--text-primary);
|
|
883
|
-
line-height: 1.1;
|
|
884
|
-
margin-bottom: 4px;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
.kpi-label {
|
|
888
|
-
font-size: 13px;
|
|
889
|
-
color: var(--text-secondary);
|
|
890
|
-
line-height: 1.3;
|
|
891
|
-
margin-bottom: 2px;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
.kpi-change {
|
|
895
|
-
font-family: 'Geist Mono', monospace;
|
|
896
|
-
font-size: 12px;
|
|
897
|
-
color: var(--text-muted);
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
figure {
|
|
901
|
-
margin: 28px 0;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
figcaption {
|
|
905
|
-
font-size: 13px;
|
|
906
|
-
color: var(--text-muted);
|
|
907
|
-
margin-top: 8px;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
.chart-container { width: 100%; height: 280px; }
|
|
911
|
-
.chart-container.tall { height: 340px; }
|
|
912
|
-
|
|
913
|
-
.tab-content aside {
|
|
914
|
-
border-left: 3px solid var(--accent);
|
|
915
|
-
padding: 2px 0 2px 20px;
|
|
916
|
-
margin: 24px 0;
|
|
917
|
-
font-size: 15px;
|
|
918
|
-
color: var(--text-body);
|
|
919
|
-
line-height: 1.6;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
.tab-content aside strong {
|
|
923
|
-
font-weight: 700;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
.tab-content table {
|
|
927
|
-
width: 100%;
|
|
928
|
-
border-collapse: collapse;
|
|
929
|
-
font-size: 15px;
|
|
930
|
-
margin: 24px 0;
|
|
931
|
-
font-variant-numeric: tabular-nums;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
.tab-content thead th {
|
|
935
|
-
font-family: 'Geist Mono', monospace;
|
|
936
|
-
font-size: 11px;
|
|
937
|
-
font-weight: 600;
|
|
938
|
-
letter-spacing: 1px;
|
|
939
|
-
text-transform: uppercase;
|
|
940
|
-
color: var(--text-muted);
|
|
941
|
-
text-align: left;
|
|
942
|
-
padding: 8px 16px 8px 0;
|
|
943
|
-
border-bottom: 2px solid var(--charcoal);
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
.tab-content tbody td {
|
|
947
|
-
padding: 10px 16px 10px 0;
|
|
948
|
-
border-bottom: 1px solid var(--border);
|
|
949
|
-
color: var(--text-body);
|
|
950
|
-
font-weight: 500;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
.tab-content tbody tr:last-child td { border-bottom: none; }
|
|
954
|
-
|
|
955
|
-
.tab-content tbody td:first-child {
|
|
956
|
-
font-weight: 600;
|
|
957
|
-
color: var(--text-primary);
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
.tab-content .footnote {
|
|
961
|
-
font-size: 13px;
|
|
962
|
-
color: var(--text-muted);
|
|
963
|
-
margin-top: -16px;
|
|
964
|
-
margin-bottom: 16px;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
@media (max-width: 1024px) {
|
|
968
|
-
.sidebar { display: none; }
|
|
969
|
-
.main { margin-left: 0; }
|
|
970
|
-
.tab-content { padding: 24px 16px 48px; }
|
|
971
|
-
.kpi-strip { flex-wrap: wrap; }
|
|
972
|
-
.kpi { flex: 1 1 45%; margin-bottom: 16px; }
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
.mobile-header {
|
|
976
|
-
display: none;
|
|
977
|
-
position: fixed;
|
|
978
|
-
top: 0; left: 0; right: 0;
|
|
979
|
-
height: 48px;
|
|
980
|
-
background: #fff;
|
|
981
|
-
border-bottom: 1px solid var(--border);
|
|
982
|
-
align-items: center;
|
|
983
|
-
justify-content: space-between;
|
|
984
|
-
padding: 0 16px;
|
|
985
|
-
z-index: 200;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
.mobile-header-title {
|
|
989
|
-
font-size: 14px;
|
|
990
|
-
font-weight: 700;
|
|
991
|
-
color: var(--text-primary);
|
|
992
|
-
letter-spacing: -0.3px;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
.mobile-menu-btn {
|
|
996
|
-
width: 36px; height: 36px;
|
|
997
|
-
display: flex;
|
|
998
|
-
align-items: center;
|
|
999
|
-
justify-content: center;
|
|
1000
|
-
border: none;
|
|
1001
|
-
background: none;
|
|
1002
|
-
cursor: pointer;
|
|
1003
|
-
border-radius: 6px;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
.mobile-menu-btn:hover { background: rgba(0,0,0,0.05); }
|
|
1007
|
-
|
|
1008
|
-
.mobile-menu-btn svg { width: 20px; height: 20px; color: var(--text-primary); }
|
|
1009
|
-
|
|
1010
|
-
.mobile-dropdown {
|
|
1011
|
-
display: none;
|
|
1012
|
-
position: fixed;
|
|
1013
|
-
top: 48px; left: 0; right: 0;
|
|
1014
|
-
background: #fff;
|
|
1015
|
-
border-bottom: 1px solid var(--border);
|
|
1016
|
-
padding: 8px;
|
|
1017
|
-
z-index: 199;
|
|
1018
|
-
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
|
|
1019
|
-
max-height: calc(100vh - 48px);
|
|
1020
|
-
overflow-y: auto;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
.mobile-dropdown.open { display: block; }
|
|
1024
|
-
|
|
1025
|
-
.mobile-nav-item {
|
|
1026
|
-
display: flex;
|
|
1027
|
-
align-items: center;
|
|
1028
|
-
gap: 10px;
|
|
1029
|
-
font-size: 15px;
|
|
1030
|
-
font-weight: 500;
|
|
1031
|
-
color: var(--text-secondary);
|
|
1032
|
-
padding: 10px 12px;
|
|
1033
|
-
border-radius: 8px;
|
|
1034
|
-
cursor: pointer;
|
|
1035
|
-
transition: all 0.12s;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
.mobile-nav-item:hover { background: rgba(0,0,0,0.04); }
|
|
1039
|
-
|
|
1040
|
-
.mobile-nav-item.active {
|
|
1041
|
-
background: var(--text-primary);
|
|
1042
|
-
color: #fff;
|
|
1043
|
-
font-weight: 600;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
.mobile-nav-number {
|
|
1047
|
-
font-family: 'Geist Mono', monospace;
|
|
1048
|
-
font-size: 11px;
|
|
1049
|
-
font-weight: 500;
|
|
1050
|
-
width: 22px; height: 22px;
|
|
1051
|
-
display: flex;
|
|
1052
|
-
align-items: center;
|
|
1053
|
-
justify-content: center;
|
|
1054
|
-
border-radius: 5px;
|
|
1055
|
-
background: rgba(0,0,0,0.06);
|
|
1056
|
-
color: var(--text-muted);
|
|
1057
|
-
flex-shrink: 0;
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
.mobile-nav-item.active .mobile-nav-number {
|
|
1061
|
-
background: var(--accent);
|
|
1062
|
-
color: var(--text-primary);
|
|
1063
|
-
font-weight: 600;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
.mobile-overlay {
|
|
1067
|
-
display: none;
|
|
1068
|
-
position: fixed;
|
|
1069
|
-
inset: 0;
|
|
1070
|
-
background: rgba(0,0,0,0.2);
|
|
1071
|
-
z-index: 198;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
.mobile-overlay.open { display: block; }
|
|
1075
|
-
|
|
1076
|
-
@media (max-width: 1024px) {
|
|
1077
|
-
.mobile-header { display: flex; }
|
|
1078
|
-
.tab-content { padding-top: 64px; }
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
::-webkit-scrollbar { width: 4px; }
|
|
1082
|
-
::-webkit-scrollbar-track { background: transparent; }
|
|
1083
|
-
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
|
1084
|
-
`;
|
|
1085
|
-
var TAB_SWITCHING_JS = `
|
|
1086
|
-
document.querySelectorAll('.nav-item, .mobile-nav-item').forEach(function(item) {
|
|
1087
|
-
item.addEventListener('click', function() {
|
|
1088
|
-
var tab = item.getAttribute('data-tab');
|
|
1089
|
-
document.querySelectorAll('.nav-item').forEach(function(n) { n.classList.remove('active'); });
|
|
1090
|
-
document.querySelectorAll('.mobile-nav-item').forEach(function(n) { n.classList.remove('active'); });
|
|
1091
|
-
document.querySelectorAll('.tab-content').forEach(function(t) { t.classList.remove('active'); });
|
|
1092
|
-
document.querySelectorAll('[data-tab="' + tab + '"]').forEach(function(el) { el.classList.add('active'); });
|
|
1093
|
-
// Update mobile header title
|
|
1094
|
-
var titleEl = document.querySelector('.mobile-header-title');
|
|
1095
|
-
if (titleEl && item.classList.contains('mobile-nav-item')) {
|
|
1096
|
-
titleEl.textContent = item.querySelector('.mobile-nav-label') ? item.querySelector('.mobile-nav-label').textContent : item.textContent;
|
|
1097
|
-
}
|
|
1098
|
-
// Close mobile dropdown
|
|
1099
|
-
var dd = document.querySelector('.mobile-dropdown');
|
|
1100
|
-
var ov = document.querySelector('.mobile-overlay');
|
|
1101
|
-
if (dd) dd.classList.remove('open');
|
|
1102
|
-
if (ov) ov.classList.remove('open');
|
|
1103
|
-
window.scrollTo(0, 0);
|
|
1104
|
-
window.dispatchEvent(new Event('resize'));
|
|
1105
|
-
});
|
|
1106
|
-
});
|
|
1107
|
-
|
|
1108
|
-
// Hamburger toggle
|
|
1109
|
-
var menuBtn = document.querySelector('.mobile-menu-btn');
|
|
1110
|
-
var dropdown = document.querySelector('.mobile-dropdown');
|
|
1111
|
-
var overlay = document.querySelector('.mobile-overlay');
|
|
1112
|
-
if (menuBtn && dropdown) {
|
|
1113
|
-
menuBtn.addEventListener('click', function() {
|
|
1114
|
-
dropdown.classList.toggle('open');
|
|
1115
|
-
if (overlay) overlay.classList.toggle('open');
|
|
1116
|
-
});
|
|
1117
|
-
}
|
|
1118
|
-
if (overlay) {
|
|
1119
|
-
overlay.addEventListener('click', function() {
|
|
1120
|
-
dropdown.classList.remove('open');
|
|
1121
|
-
overlay.classList.remove('open');
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
`;
|
|
1125
|
-
var PRINT_CSS = `
|
|
1126
|
-
/* \u2500\u2500 Print overrides: two-column layout \u2500\u2500 */
|
|
1127
|
-
body { background: #fff; }
|
|
1128
|
-
.sidebar, .mobile-header, .mobile-dropdown, .mobile-overlay { display: none !important; }
|
|
1129
|
-
.main { margin-left: 0; min-height: auto; }
|
|
1130
|
-
|
|
1131
|
-
/* \u2500\u2500 Cover page \u2500\u2500 */
|
|
1132
|
-
.print-header {
|
|
1133
|
-
min-height: calc(100vh - 1.4in);
|
|
1134
|
-
display: flex;
|
|
1135
|
-
flex-direction: column;
|
|
1136
|
-
justify-content: flex-start;
|
|
1137
|
-
background: var(--charcoal);
|
|
1138
|
-
padding: 48px 48px 56px;
|
|
1139
|
-
margin: 0;
|
|
1140
|
-
page-break-after: always;
|
|
1141
|
-
break-after: page;
|
|
1142
|
-
}
|
|
1143
|
-
.print-title {
|
|
1144
|
-
font-size: 88px;
|
|
1145
|
-
font-weight: 700;
|
|
1146
|
-
letter-spacing: -3.5px;
|
|
1147
|
-
color: #fff;
|
|
1148
|
-
padding-bottom: 28px;
|
|
1149
|
-
border-bottom: 5px solid var(--accent);
|
|
1150
|
-
line-height: 0.95;
|
|
1151
|
-
word-spacing: 100vw;
|
|
1152
|
-
}
|
|
1153
|
-
.print-subtitle {
|
|
1154
|
-
font-size: 15px;
|
|
1155
|
-
color: rgba(255,255,255,0.6);
|
|
1156
|
-
margin-top: 20px;
|
|
1157
|
-
line-height: 1.5;
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
/* \u2500\u2500 Two-column content \u2500\u2500 */
|
|
1161
|
-
.tab-content {
|
|
1162
|
-
display: block;
|
|
1163
|
-
max-width: none;
|
|
1164
|
-
padding: 0 0 16px;
|
|
1165
|
-
animation: none;
|
|
1166
|
-
column-count: 2;
|
|
1167
|
-
column-gap: 28px;
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
.tab-content + .tab-content {
|
|
1171
|
-
border-top: 3px solid var(--accent);
|
|
1172
|
-
padding-top: 16px;
|
|
1173
|
-
margin-top: 20px;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
/* Only section headers span both columns */
|
|
1177
|
-
.tab-content h1,
|
|
1178
|
-
.tab-content .subtitle { column-span: all; }
|
|
1179
|
-
|
|
1180
|
-
.tab-content h1 {
|
|
1181
|
-
font-size: 20px;
|
|
1182
|
-
font-weight: 700;
|
|
1183
|
-
letter-spacing: -0.5px;
|
|
1184
|
-
margin-bottom: 10px;
|
|
1185
|
-
padding-bottom: 8px;
|
|
1186
|
-
border-bottom-width: 1px;
|
|
1187
|
-
break-after: avoid;
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
.tab-content .subtitle {
|
|
1191
|
-
font-size: 12px;
|
|
1192
|
-
margin-bottom: 12px;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
.tab-content h2 {
|
|
1196
|
-
font-size: 14px;
|
|
1197
|
-
font-weight: 700;
|
|
1198
|
-
margin-top: 16px;
|
|
1199
|
-
margin-bottom: 6px;
|
|
1200
|
-
break-after: avoid;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
.tab-content p {
|
|
1204
|
-
font-size: 12px;
|
|
1205
|
-
line-height: 1.6;
|
|
1206
|
-
margin-bottom: 10px;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
.tab-content ul, .tab-content ol {
|
|
1210
|
-
font-size: 12px;
|
|
1211
|
-
line-height: 1.6;
|
|
1212
|
-
margin-bottom: 10px;
|
|
1213
|
-
padding-left: 18px;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
.tab-content aside {
|
|
1217
|
-
font-size: 11.5px;
|
|
1218
|
-
margin: 10px 0;
|
|
1219
|
-
padding: 2px 0 2px 10px;
|
|
1220
|
-
line-height: 1.5;
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
/* Tables: in-column with light grey box */
|
|
1224
|
-
.tab-content table {
|
|
1225
|
-
font-size: 10px;
|
|
1226
|
-
margin: 12px 0;
|
|
1227
|
-
break-inside: avoid;
|
|
1228
|
-
width: calc(100% - 24px);
|
|
1229
|
-
background: #F6F6F4;
|
|
1230
|
-
padding: 12px;
|
|
1231
|
-
border-radius: 4px;
|
|
1232
|
-
}
|
|
1233
|
-
.tab-content thead th { font-size: 8px; letter-spacing: 0.8px; padding: 5px 8px 5px 0; border-bottom-color: #D0D0CB; }
|
|
1234
|
-
.tab-content tbody td { padding: 6px 8px 6px 0; font-weight: 400; border-bottom-color: #E8E8E4; }
|
|
1235
|
-
|
|
1236
|
-
/* KPIs: compact grid for column width */
|
|
1237
|
-
.kpi-strip {
|
|
1238
|
-
margin: 12px 0; padding: 8px 0;
|
|
1239
|
-
display: grid;
|
|
1240
|
-
grid-template-columns: repeat(2, 1fr);
|
|
1241
|
-
gap: 6px 10px;
|
|
1242
|
-
break-inside: avoid;
|
|
1243
|
-
}
|
|
1244
|
-
.kpi { padding: 4px 0; border-left: none; }
|
|
1245
|
-
.kpi-value { font-size: 16px; }
|
|
1246
|
-
.kpi-label { font-size: 9px; }
|
|
1247
|
-
.kpi-change { font-size: 8px; }
|
|
1248
|
-
|
|
1249
|
-
/* Charts: clipped to column with border */
|
|
1250
|
-
figure {
|
|
1251
|
-
margin: 14px 0;
|
|
1252
|
-
break-inside: avoid; page-break-inside: avoid; break-before: avoid;
|
|
1253
|
-
overflow: hidden;
|
|
1254
|
-
border: 1px solid var(--border);
|
|
1255
|
-
border-radius: 4px;
|
|
1256
|
-
padding: 8px;
|
|
1257
|
-
}
|
|
1258
|
-
figcaption { font-size: 10px; margin-top: 6px; }
|
|
1259
|
-
.chart-container { height: 190px; overflow: hidden; }
|
|
1260
|
-
|
|
1261
|
-
.kpi-strip { break-inside: avoid; page-break-inside: avoid; }
|
|
1262
|
-
table { break-inside: auto; }
|
|
1263
|
-
thead { display: table-header-group; }
|
|
1264
|
-
tr { break-inside: avoid; }
|
|
1265
|
-
|
|
1266
|
-
@media print {
|
|
1267
|
-
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
1268
|
-
}
|
|
1269
|
-
`;
|
|
1270
|
-
function htmlTemplate(opts) {
|
|
1271
|
-
const printMode = opts.printMode ?? false;
|
|
1272
|
-
const styleBlock = printMode ? `${CSS}
|
|
1273
|
-
${PRINT_CSS}` : CSS;
|
|
1274
|
-
const jsBlock = printMode ? opts.scriptContent : `${TAB_SWITCHING_JS}
|
|
1275
|
-
${opts.scriptContent}`;
|
|
1276
|
-
return `<!DOCTYPE html>
|
|
1277
|
-
<html lang="en">
|
|
1278
|
-
<head>
|
|
1279
|
-
<meta charset="UTF-8">
|
|
1280
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1281
|
-
<title>${escapeHtml2(opts.title)}</title>
|
|
1282
|
-
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
|
|
1283
|
-
<style>${styleBlock}</style>
|
|
1284
|
-
</head>
|
|
1285
|
-
<body>
|
|
1286
|
-
${opts.sidebarHtml}
|
|
1287
|
-
${opts.mobileNavHtml}
|
|
1288
|
-
<main class="main">
|
|
1289
|
-
${opts.contentHtml}
|
|
1290
|
-
</main>
|
|
1291
|
-
<script>
|
|
1292
|
-
${jsBlock}
|
|
1293
|
-
</script>
|
|
1294
|
-
</body>
|
|
1295
|
-
</html>`;
|
|
1296
|
-
}
|
|
1297
|
-
function escapeHtml2(s) {
|
|
1298
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
// src/render.ts
|
|
1302
|
-
function renderKpi(table) {
|
|
1303
|
-
const colCount = table.headers.length;
|
|
1304
|
-
let labelCol = 0;
|
|
1305
|
-
let valueCol = 1;
|
|
1306
|
-
let changeCol = colCount >= 3 ? 2 : -1;
|
|
1307
|
-
const kpis = table.rows.map((row) => {
|
|
1308
|
-
const label = row[labelCol] || "";
|
|
1309
|
-
const value = row[valueCol] || "";
|
|
1310
|
-
const change = changeCol >= 0 ? row[changeCol] || "" : "";
|
|
1311
|
-
return ` <div class="kpi">
|
|
1312
|
-
<div class="kpi-value">${escapeHtml3(value)}</div>
|
|
1313
|
-
<div class="kpi-label">${escapeHtml3(label)}</div>
|
|
1314
|
-
${change ? `<div class="kpi-change">${escapeHtml3(change)}</div>` : ""}
|
|
1315
|
-
</div>`;
|
|
1316
|
-
}).join("\n");
|
|
1317
|
-
return ` <div class="kpi-strip">
|
|
1318
|
-
${kpis}
|
|
1319
|
-
</div>`;
|
|
1320
|
-
}
|
|
1321
|
-
function renderTable(table) {
|
|
1322
|
-
const thead = table.headers.map((h) => `<th>${escapeHtml3(h)}</th>`).join("");
|
|
1323
|
-
const tbody = table.rows.map((row) => {
|
|
1324
|
-
const cells = row.map((cell) => {
|
|
1325
|
-
return `<td>${escapeHtml3(cell)}</td>`;
|
|
1326
|
-
}).join("");
|
|
1327
|
-
return `<tr>${cells}</tr>`;
|
|
1328
|
-
}).join("\n ");
|
|
1329
|
-
return ` <table>
|
|
1330
|
-
<thead><tr>${thead}</tr></thead>
|
|
1331
|
-
<tbody>
|
|
1332
|
-
${tbody}
|
|
1333
|
-
</tbody>
|
|
1334
|
-
</table>`;
|
|
1335
|
-
}
|
|
1336
|
-
function renderChart(chartId, caption, chartType, rowCount) {
|
|
1337
|
-
let styleAttr = "";
|
|
1338
|
-
if (chartType === "bar" && rowCount) {
|
|
1339
|
-
const height = Math.max(120, rowCount * 44 + 40);
|
|
1340
|
-
styleAttr = ` style="height:${height}px"`;
|
|
1341
|
-
}
|
|
1342
|
-
return ` <figure>
|
|
1343
|
-
<div class="chart-container" id="${chartId}"${styleAttr}></div>
|
|
1344
|
-
${caption ? `<figcaption>${escapeHtml3(caption)}</figcaption>` : ""}
|
|
1345
|
-
</figure>`;
|
|
1346
|
-
}
|
|
1347
|
-
function renderElement(el) {
|
|
1348
|
-
switch (el.kind) {
|
|
1349
|
-
case "paragraph":
|
|
1350
|
-
return ` <p>${el.html}</p>`;
|
|
1351
|
-
case "heading2":
|
|
1352
|
-
return ` <h2>${escapeHtml3(el.text)}</h2>`;
|
|
1353
|
-
case "kpi":
|
|
1354
|
-
return renderKpi(el.table);
|
|
1355
|
-
case "chart":
|
|
1356
|
-
return renderChart(el.chartId, el.caption, el.chartType, el.table.rows.length);
|
|
1357
|
-
case "table":
|
|
1358
|
-
return renderTable(el.table);
|
|
1359
|
-
case "aside":
|
|
1360
|
-
return ` <aside>${el.html}</aside>`;
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
function buildSidebar(doc) {
|
|
1364
|
-
const navItems = doc.tabs.map(
|
|
1365
|
-
(tab, i) => ` <div class="nav-item${i === 0 ? " active" : ""}" data-tab="${i}">
|
|
1366
|
-
<span class="nav-number">${i + 1}</span>
|
|
1367
|
-
${escapeHtml3(tab.title)}
|
|
1368
|
-
</div>`
|
|
1369
|
-
).join("\n");
|
|
1370
|
-
const subtitle = doc.frontmatter.subtitle ? `
|
|
1371
|
-
<div class="sidebar-subtitle">${escapeHtml3(doc.frontmatter.subtitle)}</div>` : "";
|
|
1372
|
-
return `<aside class="sidebar">
|
|
1373
|
-
<div class="sidebar-header">
|
|
1374
|
-
<div class="sidebar-brand">Ghost Paper</div>
|
|
1375
|
-
<div class="sidebar-title">${escapeHtml3(doc.frontmatter.title)}</div>${subtitle}
|
|
1376
|
-
</div>
|
|
1377
|
-
<nav class="sidebar-nav">
|
|
1378
|
-
${navItems}
|
|
1379
|
-
</nav>
|
|
1380
|
-
<div class="sidebar-footer">
|
|
1381
|
-
<div class="sidebar-footer-text">Built with Ghost Paper</div>
|
|
1382
|
-
</div>
|
|
1383
|
-
</aside>`;
|
|
1384
|
-
}
|
|
1385
|
-
function buildMobileNav(doc) {
|
|
1386
|
-
const items = doc.tabs.map(
|
|
1387
|
-
(tab, i) => ` <div class="mobile-nav-item${i === 0 ? " active" : ""}" data-tab="${i}">
|
|
1388
|
-
<span class="mobile-nav-number">${i + 1}</span>
|
|
1389
|
-
<span class="mobile-nav-label">${escapeHtml3(tab.title)}</span>
|
|
1390
|
-
</div>`
|
|
1391
|
-
).join("\n");
|
|
1392
|
-
const firstTitle = escapeHtml3(doc.tabs[0]?.title || "");
|
|
1393
|
-
return `<div class="mobile-header">
|
|
1394
|
-
<span class="mobile-header-title">${firstTitle}</span>
|
|
1395
|
-
<button class="mobile-menu-btn" aria-label="Menu">
|
|
1396
|
-
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="3" y1="5" x2="17" y2="5"/><line x1="3" y1="10" x2="17" y2="10"/><line x1="3" y1="15" x2="17" y2="15"/></svg>
|
|
1397
|
-
</button>
|
|
1398
|
-
</div>
|
|
1399
|
-
<div class="mobile-overlay"></div>
|
|
1400
|
-
<div class="mobile-dropdown">
|
|
1401
|
-
${items}
|
|
1402
|
-
</div>`;
|
|
1403
|
-
}
|
|
1404
|
-
function buildContent(doc) {
|
|
1405
|
-
return doc.tabs.map((tab, i) => {
|
|
1406
|
-
const elements = tab.elements.map(renderElement).join("\n\n");
|
|
1407
|
-
const subtitle = tab.subtitle ? `
|
|
1408
|
-
<div class="subtitle">${tab.subtitle}</div>` : "";
|
|
1409
|
-
return ` <div class="tab-content${i === 0 ? " active" : ""}" data-tab="${i}">
|
|
1410
|
-
<h1>${escapeHtml3(tab.title)}</h1>${subtitle}
|
|
1411
|
-
|
|
1412
|
-
${elements}
|
|
1413
|
-
</div>`;
|
|
1414
|
-
}).join("\n\n");
|
|
1415
|
-
}
|
|
1416
|
-
function buildChartScript(doc, printMode = false) {
|
|
1417
|
-
const chartCalls = [];
|
|
1418
|
-
for (const tab of doc.tabs) {
|
|
1419
|
-
for (const el of tab.elements) {
|
|
1420
|
-
if (el.kind === "chart") {
|
|
1421
|
-
switch (el.chartType) {
|
|
1422
|
-
case "line":
|
|
1423
|
-
chartCalls.push(lineChartJs(el.chartId, el.table));
|
|
1424
|
-
break;
|
|
1425
|
-
case "bar":
|
|
1426
|
-
chartCalls.push(barChartJs(el.chartId, el.table));
|
|
1427
|
-
break;
|
|
1428
|
-
case "pie":
|
|
1429
|
-
chartCalls.push(pieChartJs(el.chartId, el.table));
|
|
1430
|
-
break;
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
if (chartCalls.length === 0) {
|
|
1436
|
-
if (printMode) return "window.__ghostPaperChartsReady = true;";
|
|
1437
|
-
return "";
|
|
1438
|
-
}
|
|
1439
|
-
const script = chartPreamble(printMode) + "\n" + chartCalls.join("\n");
|
|
1440
|
-
if (printMode) return script + "\nwindow.__ghostPaperChartsReady = true;";
|
|
1441
|
-
return script;
|
|
1442
|
-
}
|
|
1443
|
-
function buildPrintContent(doc) {
|
|
1444
|
-
const subtitle = doc.frontmatter.subtitle ? `
|
|
1445
|
-
<div class="print-subtitle">${escapeHtml3(doc.frontmatter.subtitle)}</div>` : "";
|
|
1446
|
-
const header = ` <div class="print-header">
|
|
1447
|
-
<div class="print-title">${escapeHtml3(doc.frontmatter.title)}</div>${subtitle}
|
|
1448
|
-
</div>`;
|
|
1449
|
-
const tabs = doc.tabs.map((tab, i) => {
|
|
1450
|
-
const elements = tab.elements.map(renderElement).join("\n\n");
|
|
1451
|
-
const tabSubtitle = tab.subtitle ? `
|
|
1452
|
-
<div class="subtitle">${tab.subtitle}</div>` : "";
|
|
1453
|
-
return ` <div class="tab-content active" data-tab="${i}">
|
|
1454
|
-
<h1>${escapeHtml3(tab.title)}</h1>${tabSubtitle}
|
|
1455
|
-
|
|
1456
|
-
${elements}
|
|
1457
|
-
</div>`;
|
|
1458
|
-
}).join("\n\n");
|
|
1459
|
-
return `${header}
|
|
1460
|
-
|
|
1461
|
-
${tabs}`;
|
|
1462
|
-
}
|
|
1463
|
-
function render(doc, opts) {
|
|
1464
|
-
const printMode = opts?.printMode ?? false;
|
|
1465
|
-
return htmlTemplate({
|
|
1466
|
-
title: doc.frontmatter.title,
|
|
1467
|
-
sidebarHtml: printMode ? "" : buildSidebar(doc),
|
|
1468
|
-
mobileNavHtml: printMode ? "" : buildMobileNav(doc),
|
|
1469
|
-
contentHtml: printMode ? buildPrintContent(doc) : buildContent(doc),
|
|
1470
|
-
scriptContent: buildChartScript(doc, printMode),
|
|
1471
|
-
printMode
|
|
1472
|
-
});
|
|
1473
|
-
}
|
|
1474
|
-
function escapeHtml3(s) {
|
|
1475
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
// src/index.ts
|
|
1479
|
-
function build(markdown) {
|
|
1480
|
-
const doc = parse(markdown);
|
|
1481
|
-
return render(doc);
|
|
1482
|
-
}
|
|
1483
|
-
function buildPrintHtml(markdown) {
|
|
1484
|
-
const doc = parse(markdown);
|
|
1485
|
-
return render(doc, { printMode: true });
|
|
1486
|
-
}
|
|
1487
|
-
async function buildPdf(markdown, outputPath, options) {
|
|
1488
|
-
const doc = parse(markdown);
|
|
1489
|
-
const html = render(doc, { printMode: true });
|
|
1490
|
-
const { generatePdf } = await import("./pdf-PXNSBFN6.js");
|
|
1491
|
-
await generatePdf({
|
|
1492
|
-
html,
|
|
1493
|
-
outputPath,
|
|
1494
|
-
title: doc.frontmatter.title,
|
|
1495
|
-
pageSize: options?.pageSize,
|
|
1496
|
-
landscape: options?.landscape
|
|
1497
|
-
});
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
export {
|
|
1501
|
-
build,
|
|
1502
|
-
buildPrintHtml,
|
|
1503
|
-
buildPdf
|
|
1504
|
-
};
|