@opendatalabs/darshana 1.3.0 → 1.4.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 +1 -1
- package/package.json +1 -1
- package/src/auth.mjs +91 -5
- package/src/html.mjs +96 -17
- package/src/index.mjs +7 -5
- package/src/pdf.mjs +3 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Crawl any web app and generate a labeled PDF, HTML viewer, or image set for desi
|
|
|
7
7
|
## Try it now
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npx @opendatalabs/darshana --url https://vana.org --public
|
|
10
|
+
npx @opendatalabs/darshana --url https://vana.org --public --max-pages 10
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Output lands in `./darshana-output/` — a PDF and a self-contained HTML viewer with sidebar nav, filters, and keyboard navigation.
|
package/package.json
CHANGED
package/src/auth.mjs
CHANGED
|
@@ -7,7 +7,7 @@ const AUTH_MAX_AGE_MS = 12 * 60 * 60 * 1000;
|
|
|
7
7
|
export async function ensureAuth(config) {
|
|
8
8
|
if (config.public === true) {
|
|
9
9
|
console.log('[auth] Public app — skipping auth.');
|
|
10
|
-
return null;
|
|
10
|
+
return { storageStatePath: null, authCaptures: [] };
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const storagePath = config.authStorage;
|
|
@@ -18,7 +18,7 @@ export async function ensureAuth(config) {
|
|
|
18
18
|
if (ageMs < AUTH_MAX_AGE_MS) {
|
|
19
19
|
const ageMin = Math.round(ageMs / 60000);
|
|
20
20
|
console.log(`[auth] Using cached auth (${ageMin}m old): ${storagePath}`);
|
|
21
|
-
return storagePath;
|
|
21
|
+
return { storageStatePath: storagePath, authCaptures: [] };
|
|
22
22
|
}
|
|
23
23
|
console.log('[auth] Cached auth is stale (>12h) — re-authenticating.');
|
|
24
24
|
}
|
|
@@ -31,25 +31,77 @@ export async function ensureAuth(config) {
|
|
|
31
31
|
throw new Error(`authScript must export a default function, got: ${typeof fn}`);
|
|
32
32
|
}
|
|
33
33
|
const browser = await chromium.launch({ headless: true });
|
|
34
|
+
let storagePath;
|
|
34
35
|
try {
|
|
35
36
|
const result = await fn(browser);
|
|
36
37
|
if (typeof result !== 'string') {
|
|
37
38
|
throw new Error('authScript must return a storageState file path string');
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
+
storagePath = result;
|
|
40
41
|
} finally {
|
|
41
42
|
await browser.close();
|
|
42
43
|
}
|
|
44
|
+
|
|
45
|
+
// Post-auth verification screenshot using the saved storageState
|
|
46
|
+
const authCaptures = [];
|
|
47
|
+
try {
|
|
48
|
+
console.log('[auth] Capturing post-auth landing page...');
|
|
49
|
+
const verifyBrowser = await chromium.launch({ headless: true });
|
|
50
|
+
try {
|
|
51
|
+
const verifyContext = await verifyBrowser.newContext({ storageState: storagePath });
|
|
52
|
+
const verifyPage = await verifyContext.newPage();
|
|
53
|
+
const landingUrl = config.url + (config.start ?? '/');
|
|
54
|
+
await verifyPage.goto(landingUrl, { waitUntil: 'networkidle', timeout: 30000 });
|
|
55
|
+
const imageBuffer = await verifyPage.screenshot({ fullPage: true, type: 'png' });
|
|
56
|
+
authCaptures.push({
|
|
57
|
+
url: verifyPage.url(),
|
|
58
|
+
pathname: '/_auth/landing',
|
|
59
|
+
theme: 'system',
|
|
60
|
+
viewport: 'desktop',
|
|
61
|
+
section: 'auth',
|
|
62
|
+
label: 'Auth · post-login landing',
|
|
63
|
+
imageBuffer,
|
|
64
|
+
});
|
|
65
|
+
await verifyPage.close();
|
|
66
|
+
await verifyContext.close();
|
|
67
|
+
} finally {
|
|
68
|
+
await verifyBrowser.close();
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.warn(`[auth] WARNING: post-auth screenshot failed: ${err.message}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { storageStatePath: storagePath, authCaptures };
|
|
43
75
|
}
|
|
44
76
|
|
|
45
77
|
console.log('\n[auth] Launching headed browser for manual login...');
|
|
46
78
|
console.log(`[auth] Navigate to: ${config.url}`);
|
|
47
79
|
console.log('[auth] Log in, then press ENTER here to capture session.\n');
|
|
48
80
|
|
|
81
|
+
const startPath = config.start ?? '/';
|
|
82
|
+
const startUrl = config.url + startPath;
|
|
83
|
+
|
|
49
84
|
const browser = await chromium.launch({ headless: false });
|
|
50
85
|
const context = await browser.newContext();
|
|
51
86
|
const page = await context.newPage();
|
|
52
|
-
await page.goto(
|
|
87
|
+
await page.goto(startUrl, { waitUntil: 'load' });
|
|
88
|
+
|
|
89
|
+
// Pre-auth screenshot (captures the login/redirect page)
|
|
90
|
+
let preAuthCapture = null;
|
|
91
|
+
try {
|
|
92
|
+
const preAuthImageBuffer = await page.screenshot({ fullPage: true, type: 'png' });
|
|
93
|
+
preAuthCapture = {
|
|
94
|
+
url: page.url(),
|
|
95
|
+
pathname: '/_auth/login',
|
|
96
|
+
theme: 'system',
|
|
97
|
+
viewport: 'desktop',
|
|
98
|
+
section: 'auth',
|
|
99
|
+
label: 'Auth · login page',
|
|
100
|
+
imageBuffer: preAuthImageBuffer,
|
|
101
|
+
};
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.warn(`[auth] WARNING: pre-auth screenshot failed: ${err.message}`);
|
|
104
|
+
}
|
|
53
105
|
|
|
54
106
|
await waitForEnter();
|
|
55
107
|
|
|
@@ -58,7 +110,41 @@ export async function ensureAuth(config) {
|
|
|
58
110
|
await browser.close();
|
|
59
111
|
|
|
60
112
|
console.log('[auth] Session saved.\n');
|
|
61
|
-
|
|
113
|
+
|
|
114
|
+
// Post-auth verification screenshot using a new page with saved storageState
|
|
115
|
+
let postAuthCapture = null;
|
|
116
|
+
try {
|
|
117
|
+
console.log('[auth] Capturing post-auth landing page...');
|
|
118
|
+
const verifyBrowser = await chromium.launch({ headless: true });
|
|
119
|
+
try {
|
|
120
|
+
const verifyContext = await verifyBrowser.newContext({ storageState: storagePath });
|
|
121
|
+
const verifyPage = await verifyContext.newPage();
|
|
122
|
+
await verifyPage.goto(startUrl, { waitUntil: 'networkidle', timeout: 30000 });
|
|
123
|
+
const postAuthImageBuffer = await verifyPage.screenshot({ fullPage: true, type: 'png' });
|
|
124
|
+
postAuthCapture = {
|
|
125
|
+
url: verifyPage.url(),
|
|
126
|
+
pathname: '/_auth/landing',
|
|
127
|
+
theme: 'system',
|
|
128
|
+
viewport: 'desktop',
|
|
129
|
+
section: 'auth',
|
|
130
|
+
label: 'Auth · post-login landing',
|
|
131
|
+
imageBuffer: postAuthImageBuffer,
|
|
132
|
+
};
|
|
133
|
+
await verifyPage.close();
|
|
134
|
+
await verifyContext.close();
|
|
135
|
+
} finally {
|
|
136
|
+
await verifyBrowser.close();
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.warn(`[auth] WARNING: post-auth screenshot failed: ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const authCaptures = [
|
|
143
|
+
...(preAuthCapture ? [preAuthCapture] : []),
|
|
144
|
+
...(postAuthCapture ? [postAuthCapture] : []),
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
return { storageStatePath: storagePath, authCaptures };
|
|
62
148
|
}
|
|
63
149
|
|
|
64
150
|
function waitForEnter() {
|
package/src/html.mjs
CHANGED
|
@@ -13,26 +13,78 @@ export async function assembleHtml(pages, config, outputDir) {
|
|
|
13
13
|
return `img[data-viewport="${vp}"] { width: ${cssWidth}; max-width: ${preset.width}px; }`;
|
|
14
14
|
}).join('\n ');
|
|
15
15
|
|
|
16
|
-
const
|
|
16
|
+
const authPages = pages.filter(p => p.section === 'auth');
|
|
17
|
+
const appPages = pages.filter(p => p.section !== 'auth');
|
|
18
|
+
const hasAuthCaptures = authPages.length > 0;
|
|
19
|
+
|
|
17
20
|
const pageSections = [];
|
|
18
21
|
|
|
19
22
|
pages.forEach((capture, i) => {
|
|
20
23
|
const idx = i + 1;
|
|
21
24
|
const pageId = `page-${idx}`;
|
|
22
25
|
const base64 = capture.imageBuffer.toString('base64');
|
|
26
|
+
const sectionAttr = capture.section === 'auth' ? ' data-section="auth"' : '';
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
`<li data-theme="${escHtml(capture.theme)}" data-viewport="${escHtml(capture.viewport)}"><a href="#${pageId}" data-theme="${escHtml(capture.theme)}" data-viewport="${escHtml(capture.viewport)}">${escHtml(capture.label)}</a></li>`
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
pageSections.push(`<div class="page" id="${pageId}" data-theme="${escHtml(capture.theme)}" data-viewport="${escHtml(capture.viewport)}">
|
|
28
|
+
pageSections.push(`<div class="page" id="${pageId}" data-theme="${escHtml(capture.theme)}" data-viewport="${escHtml(capture.viewport)}"${sectionAttr}>
|
|
29
29
|
<div class="label"><span class="idx">${idx}</span>${escHtml(capture.label)}</div>
|
|
30
|
-
<img src="data:image/png;base64,${base64}" alt="${escHtml(capture.label)}" data-viewport="${escHtml(capture.viewport)}" loading="lazy">
|
|
30
|
+
<img src="data:image/png;base64,${base64}" alt="${escHtml(capture.label)}" data-viewport="${escHtml(capture.viewport)}"${sectionAttr} loading="lazy">
|
|
31
31
|
</div>`);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// Build sidebar nav items
|
|
35
|
+
let navHtml;
|
|
36
|
+
if (hasAuthCaptures) {
|
|
37
|
+
// Auth group
|
|
38
|
+
const authItems = [];
|
|
39
|
+
pages.forEach((capture, i) => {
|
|
40
|
+
if (capture.section !== 'auth') return;
|
|
41
|
+
const idx = i + 1;
|
|
42
|
+
const pageId = `page-${idx}`;
|
|
43
|
+
authItems.push(
|
|
44
|
+
`<li data-section="auth"><a href="#${pageId}">${escHtml(capture.label)}</a></li>`
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Pages group
|
|
49
|
+
const pageItems = [];
|
|
50
|
+
pages.forEach((capture, i) => {
|
|
51
|
+
if (capture.section === 'auth') return;
|
|
52
|
+
const idx = i + 1;
|
|
53
|
+
const pageId = `page-${idx}`;
|
|
54
|
+
pageItems.push(
|
|
55
|
+
`<li data-theme="${escHtml(capture.theme)}" data-viewport="${escHtml(capture.viewport)}"><a href="#${pageId}" data-theme="${escHtml(capture.theme)}" data-viewport="${escHtml(capture.viewport)}">${escHtml(capture.label)}</a></li>`
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
navHtml = `
|
|
60
|
+
<div class="section-group">
|
|
61
|
+
<div class="section-header" onclick="toggleSection(this)">
|
|
62
|
+
<span class="chevron">▼</span> Auth
|
|
63
|
+
</div>
|
|
64
|
+
<ul class="section-list">
|
|
65
|
+
${authItems.join('\n ')}
|
|
66
|
+
</ul>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="section-group">
|
|
69
|
+
<div class="section-header" onclick="toggleSection(this)">
|
|
70
|
+
<span class="chevron">▼</span> Pages
|
|
71
|
+
</div>
|
|
72
|
+
<ul class="section-list" id="pages-section-list">
|
|
73
|
+
${pageItems.join('\n ')}
|
|
74
|
+
</ul>
|
|
75
|
+
</div>`;
|
|
76
|
+
} else {
|
|
77
|
+
// Flat nav — same as original behavior
|
|
78
|
+
const flatItems = pages.map((capture, i) => {
|
|
79
|
+
const idx = i + 1;
|
|
80
|
+
const pageId = `page-${idx}`;
|
|
81
|
+
return `<li data-theme="${escHtml(capture.theme)}" data-viewport="${escHtml(capture.viewport)}"><a href="#${pageId}" data-theme="${escHtml(capture.theme)}" data-viewport="${escHtml(capture.viewport)}">${escHtml(capture.label)}</a></li>`;
|
|
82
|
+
});
|
|
83
|
+
navHtml = `<ul id="nav-list">\n ${flatItems.join('\n ')}\n </ul>`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const themes = [...new Set(appPages.map(p => p.theme))];
|
|
87
|
+
const viewports = [...new Set(appPages.map(p => p.viewport))];
|
|
36
88
|
|
|
37
89
|
const themeCheckboxes = themes.map(t =>
|
|
38
90
|
`<label><input type="checkbox" data-filter="theme" value="${escHtml(t)}" checked> ${escHtml(t)}</label>`
|
|
@@ -64,6 +116,19 @@ export async function assembleHtml(pages, config, outputDir) {
|
|
|
64
116
|
#nav-list li a:hover, #nav-list li a.active { background: #1e1e1e; color: #fff; }
|
|
65
117
|
#nav-list li[data-hidden] { display: none; }
|
|
66
118
|
|
|
119
|
+
.section-group { flex-shrink: 0; }
|
|
120
|
+
.section-header { position: sticky; top: 0; background: #111; z-index: 1; padding: 8px 16px; font-size: 10px; font-weight: 600; color: #888; text-transform: uppercase; letter-spacing: 0.08em; border-bottom: 1px solid #2a2a2a; cursor: pointer; user-select: none; display: flex; align-items: center; gap: 6px; }
|
|
121
|
+
.section-header:hover { color: #bbb; }
|
|
122
|
+
.section-header .chevron { font-size: 8px; transition: transform 0.15s; display: inline-block; }
|
|
123
|
+
.section-header.collapsed .chevron { transform: rotate(-90deg); }
|
|
124
|
+
.section-list { list-style: none; margin: 0; padding: 4px 0; }
|
|
125
|
+
.section-list.collapsed { display: none; }
|
|
126
|
+
.section-list li a { display: block; padding: 5px 16px; font-size: 11px; color: #666; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
127
|
+
.section-list li a:hover, .section-list li a.active { background: #1e1e1e; color: #fff; }
|
|
128
|
+
.section-list li[data-hidden] { display: none; }
|
|
129
|
+
.section-list li[data-section="auth"] a { color: #7a8aaa; }
|
|
130
|
+
.section-list li[data-section="auth"] a:hover { color: #aab8d4; background: #1e1e1e; }
|
|
131
|
+
|
|
67
132
|
#content { margin-left: 240px; flex: 1; padding: 32px; min-width: 0; }
|
|
68
133
|
.cover { padding: 48px 0 40px; border-bottom: 1px solid #2a2a2a; margin-bottom: 48px; }
|
|
69
134
|
.cover h1 { font-size: 1.75rem; margin: 0 0 8px; font-weight: 600; }
|
|
@@ -73,6 +138,7 @@ export async function assembleHtml(pages, config, outputDir) {
|
|
|
73
138
|
.page[data-hidden] { display: none; }
|
|
74
139
|
.label { background: #0d0d0d; border: 1px solid #2a2a2a; border-bottom: none; padding: 8px 14px; font-size: 12px; color: #ccc; font-family: 'SF Mono', 'Fira Code', monospace; display: flex; align-items: center; gap: 10px; border-radius: 6px 6px 0 0; }
|
|
75
140
|
.label .idx { background: #2a2a2a; color: #888; padding: 1px 6px; border-radius: 3px; font-size: 10px; min-width: 24px; text-align: center; }
|
|
141
|
+
.page[data-section="auth"] .label { background: #0d0f1a; border-color: #2a2e42; color: #9aadcc; }
|
|
76
142
|
.page img { display: block; border: 1px solid #2a2a2a; border-radius: 0 0 6px 6px; box-shadow: 0 4px 24px rgba(0,0,0,0.4); }
|
|
77
143
|
img[data-viewport="mobile"] { border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); }
|
|
78
144
|
|
|
@@ -94,9 +160,7 @@ export async function assembleHtml(pages, config, outputDir) {
|
|
|
94
160
|
<div class="filter-group-label" style="margin-top:10px">Viewport</div>
|
|
95
161
|
${viewportCheckboxes}
|
|
96
162
|
</div>
|
|
97
|
-
|
|
98
|
-
${navItems.join('\n ')}
|
|
99
|
-
</ul>
|
|
163
|
+
${navHtml}
|
|
100
164
|
</nav>
|
|
101
165
|
<main id="content">
|
|
102
166
|
<div class="cover">
|
|
@@ -107,15 +171,30 @@ export async function assembleHtml(pages, config, outputDir) {
|
|
|
107
171
|
${pageSections.join('\n ')}
|
|
108
172
|
</main>
|
|
109
173
|
<script>
|
|
174
|
+
function toggleSection(header) {
|
|
175
|
+
header.classList.toggle('collapsed');
|
|
176
|
+
const list = header.nextElementSibling;
|
|
177
|
+
if (list && list.classList.contains('section-list')) {
|
|
178
|
+
list.classList.toggle('collapsed');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
110
182
|
function applyFilters() {
|
|
111
183
|
const checked = { theme: new Set(), viewport: new Set() };
|
|
112
184
|
document.querySelectorAll('input[data-filter]').forEach(cb => {
|
|
113
185
|
if (cb.checked) checked[cb.dataset.filter].add(cb.value);
|
|
114
186
|
});
|
|
115
|
-
document.querySelectorAll('.page').forEach(
|
|
187
|
+
document.querySelectorAll('.page').forEach(page => {
|
|
188
|
+
// Auth captures are always visible — not part of the theme/viewport matrix
|
|
189
|
+
if (page.dataset.section === 'auth') return;
|
|
116
190
|
const visible = checked.theme.has(page.dataset.theme) && checked.viewport.has(page.dataset.viewport);
|
|
117
191
|
page.toggleAttribute('data-hidden', !visible);
|
|
118
|
-
|
|
192
|
+
// Also hide/show the corresponding nav item in the Pages section list
|
|
193
|
+
const link = document.querySelector('.section-list a[href="#' + page.id + '"]');
|
|
194
|
+
if (link) link.closest('li')?.toggleAttribute('data-hidden', !visible);
|
|
195
|
+
// Flat nav fallback
|
|
196
|
+
const flatLink = document.querySelector('#nav-list a[href="#' + page.id + '"]');
|
|
197
|
+
if (flatLink) flatLink.closest('li')?.toggleAttribute('data-hidden', !visible);
|
|
119
198
|
});
|
|
120
199
|
}
|
|
121
200
|
document.querySelectorAll('input[data-filter]').forEach(cb => cb.addEventListener('change', applyFilters));
|
|
@@ -129,8 +208,8 @@ export async function assembleHtml(pages, config, outputDir) {
|
|
|
129
208
|
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') currentIdx = Math.max(currentIdx - 1, 0);
|
|
130
209
|
else return;
|
|
131
210
|
ps[currentIdx].scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
132
|
-
document.querySelectorAll('#nav-list li a').forEach(a => a.classList.remove('active'));
|
|
133
|
-
const link = document.querySelector('
|
|
211
|
+
document.querySelectorAll('.section-list li a, #nav-list li a').forEach(a => a.classList.remove('active'));
|
|
212
|
+
const link = document.querySelector('a[href="#' + ps[currentIdx].id + '"]');
|
|
134
213
|
if (link) { link.classList.add('active'); link.scrollIntoView({ block: 'nearest' }); }
|
|
135
214
|
});
|
|
136
215
|
|
|
@@ -138,7 +217,7 @@ export async function assembleHtml(pages, config, outputDir) {
|
|
|
138
217
|
entries.forEach(entry => {
|
|
139
218
|
if (entry.isIntersecting && !entry.target.hasAttribute('data-hidden')) {
|
|
140
219
|
const id = entry.target.id;
|
|
141
|
-
document.querySelectorAll('#nav-list li a').forEach(a => {
|
|
220
|
+
document.querySelectorAll('.section-list li a, #nav-list li a').forEach(a => {
|
|
142
221
|
a.classList.toggle('active', a.getAttribute('href') === '#' + id);
|
|
143
222
|
});
|
|
144
223
|
}
|
package/src/index.mjs
CHANGED
|
@@ -315,7 +315,7 @@ function runMain() {
|
|
|
315
315
|
config = configFromArgs(args);
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
const storageStatePath = await ensureAuth(config);
|
|
318
|
+
const { storageStatePath, authCaptures } = await ensureAuth(config);
|
|
319
319
|
config._storageStatePath = storageStatePath;
|
|
320
320
|
|
|
321
321
|
printConfigSummary(config);
|
|
@@ -365,7 +365,9 @@ function runMain() {
|
|
|
365
365
|
|
|
366
366
|
console.log(`\n[darshana] Captured ${captures.length} page(s).`);
|
|
367
367
|
|
|
368
|
-
|
|
368
|
+
const allCaptures = [...authCaptures, ...captures];
|
|
369
|
+
|
|
370
|
+
if (allCaptures.length === 0) {
|
|
369
371
|
console.error('[darshana] No pages captured. Exiting.');
|
|
370
372
|
process.exit(1);
|
|
371
373
|
}
|
|
@@ -376,15 +378,15 @@ function runMain() {
|
|
|
376
378
|
const outputs = config.outputs ?? ['pdf', 'html'];
|
|
377
379
|
|
|
378
380
|
if (outputs.includes('pdf')) {
|
|
379
|
-
await assemblePdf(
|
|
381
|
+
await assemblePdf(allCaptures, config, outputDir);
|
|
380
382
|
}
|
|
381
383
|
|
|
382
384
|
if (outputs.includes('html')) {
|
|
383
|
-
await assembleHtml(
|
|
385
|
+
await assembleHtml(allCaptures, config, outputDir);
|
|
384
386
|
}
|
|
385
387
|
|
|
386
388
|
if (outputs.includes('images')) {
|
|
387
|
-
await writeImages(
|
|
389
|
+
await writeImages(allCaptures, outputDir);
|
|
388
390
|
}
|
|
389
391
|
|
|
390
392
|
console.log('\n[darshana] Done.');
|
package/src/pdf.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
|
|
|
4
4
|
|
|
5
5
|
const HEADER_HEIGHT = 28;
|
|
6
6
|
const HEADER_BG = rgb(0, 0, 0);
|
|
7
|
+
const HEADER_BG_AUTH = rgb(0.25, 0.25, 0.35);
|
|
7
8
|
const HEADER_FG = rgb(1, 1, 1);
|
|
8
9
|
const FONT_SIZE = 11;
|
|
9
10
|
|
|
@@ -49,8 +50,9 @@ export async function assemblePdf(pages, config, outputDir) {
|
|
|
49
50
|
page.drawImage(img, { x: 0, y: 0, width: pgWidth, height: imgHeight });
|
|
50
51
|
|
|
51
52
|
// Header bar at top (y=0 is bottom in pdf-lib, so header sits at y=imgHeight)
|
|
53
|
+
const headerColor = capture.section === 'auth' ? HEADER_BG_AUTH : HEADER_BG;
|
|
52
54
|
page.drawRectangle({
|
|
53
|
-
x: 0, y: imgHeight, width: pgWidth, height: HEADER_HEIGHT, color:
|
|
55
|
+
x: 0, y: imgHeight, width: pgWidth, height: HEADER_HEIGHT, color: headerColor,
|
|
54
56
|
});
|
|
55
57
|
page.drawText(capture.label, {
|
|
56
58
|
x: 8, y: imgHeight + 8, font, size: FONT_SIZE, color: HEADER_FG, maxWidth: pgWidth - 16,
|