@ai-matrx/print 0.3.1 → 0.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/CHANGELOG.md +44 -0
- package/dist/certificate.cjs +746 -0
- package/dist/certificate.cjs.map +1 -0
- package/dist/certificate.d.cts +101 -0
- package/dist/certificate.d.ts +101 -0
- package/dist/certificate.js +723 -0
- package/dist/certificate.js.map +1 -0
- package/dist/education.cjs +928 -0
- package/dist/education.cjs.map +1 -0
- package/dist/education.d.cts +79 -0
- package/dist/education.d.ts +79 -0
- package/dist/education.js +905 -0
- package/dist/education.js.map +1 -0
- package/dist/exam.cjs +648 -0
- package/dist/exam.cjs.map +1 -0
- package/dist/exam.d.cts +69 -0
- package/dist/exam.d.ts +69 -0
- package/dist/exam.js +625 -0
- package/dist/exam.js.map +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/zpl.cjs +1039 -0
- package/dist/zpl.cjs.map +1 -0
- package/dist/zpl.d.cts +160 -0
- package/dist/zpl.d.ts +160 -0
- package/dist/zpl.js +1006 -0
- package/dist/zpl.js.map +1 -0
- package/package.json +41 -1
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
// src/core.ts
|
|
2
|
+
function escapeHtml(str) {
|
|
3
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
4
|
+
}
|
|
5
|
+
function openPrintWindow(htmlDocument, filename = "print", windowFeatures = "width=920,height=720,scrollbars=yes") {
|
|
6
|
+
const win = window.open("", "_blank", windowFeatures);
|
|
7
|
+
if (!win) {
|
|
8
|
+
const blob = new Blob([htmlDocument], { type: "text/html;charset=utf-8" });
|
|
9
|
+
const url = URL.createObjectURL(blob);
|
|
10
|
+
const a = document.createElement("a");
|
|
11
|
+
a.href = url;
|
|
12
|
+
a.download = `${filename.replace(/\s+/g, "-").toLowerCase()}.html`;
|
|
13
|
+
document.body.appendChild(a);
|
|
14
|
+
a.click();
|
|
15
|
+
document.body.removeChild(a);
|
|
16
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
17
|
+
return "downloaded";
|
|
18
|
+
}
|
|
19
|
+
win.document.open();
|
|
20
|
+
win.document.write(htmlDocument);
|
|
21
|
+
win.document.close();
|
|
22
|
+
return "opened";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/booklet.ts
|
|
26
|
+
var PAGES_PER_SHEET = 4;
|
|
27
|
+
var SADDLE_STITCH_MAX_PAGES = 48;
|
|
28
|
+
function imposeBooklet(pageCount) {
|
|
29
|
+
if (!Number.isInteger(pageCount) || pageCount < 1) {
|
|
30
|
+
throw new RangeError(
|
|
31
|
+
`imposeBooklet: pageCount must be a positive integer, received ${String(pageCount)}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const padded = Math.ceil(pageCount / PAGES_PER_SHEET) * PAGES_PER_SHEET;
|
|
35
|
+
const slot = (n) => n > pageCount ? 0 : n;
|
|
36
|
+
const sheets = [];
|
|
37
|
+
for (let i = 1; i <= padded / PAGES_PER_SHEET; i += 1) {
|
|
38
|
+
sheets.push({
|
|
39
|
+
front: [slot(padded - 2 * i + 2), slot(2 * i - 1)],
|
|
40
|
+
back: [slot(2 * i), slot(padded - 2 * i + 1)]
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return { paddedPageCount: padded, sheets, requiresShortEdgeDuplex: true };
|
|
44
|
+
}
|
|
45
|
+
function bookletSizeWarning(pageCount) {
|
|
46
|
+
if (!Number.isInteger(pageCount) || pageCount < 1) return null;
|
|
47
|
+
const padded = Math.ceil(pageCount / PAGES_PER_SHEET) * PAGES_PER_SHEET;
|
|
48
|
+
if (padded <= SADDLE_STITCH_MAX_PAGES) return null;
|
|
49
|
+
return `This booklet is ${padded} pages (${padded / PAGES_PER_SHEET} sheets). Past about ${SADDLE_STITCH_MAX_PAGES} pages a saddle-stitch booklet gets too thick to fold flat and the inner pages creep out at the fore-edge. Consider printing it as a plain workbook instead, or splitting it into ${SADDLE_STITCH_MAX_PAGES}-page signatures.`;
|
|
50
|
+
}
|
|
51
|
+
var BOOKLET_STYLES = `
|
|
52
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
53
|
+
|
|
54
|
+
@page { size: 11in 8.5in; margin: 0; }
|
|
55
|
+
|
|
56
|
+
html, body {
|
|
57
|
+
background: #fff;
|
|
58
|
+
color: #1a1a1a;
|
|
59
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.bk-side {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: row;
|
|
65
|
+
width: 11in;
|
|
66
|
+
height: 8.5in;
|
|
67
|
+
page-break-after: always;
|
|
68
|
+
break-after: page;
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
}
|
|
71
|
+
.bk-side:last-of-type { page-break-after: auto; break-after: auto; }
|
|
72
|
+
|
|
73
|
+
.bk-page {
|
|
74
|
+
width: 5.5in;
|
|
75
|
+
height: 8.5in;
|
|
76
|
+
overflow: hidden;
|
|
77
|
+
position: relative;
|
|
78
|
+
}
|
|
79
|
+
.bk-blank { /* intentionally empty \u2014 a padded slot */ }
|
|
80
|
+
|
|
81
|
+
.bk-banner {
|
|
82
|
+
background: #fef3c7;
|
|
83
|
+
border: 2px solid #f59e0b;
|
|
84
|
+
border-radius: 8px;
|
|
85
|
+
padding: 14px 18px;
|
|
86
|
+
margin: 18px;
|
|
87
|
+
font-size: 13px;
|
|
88
|
+
line-height: 1.55;
|
|
89
|
+
color: #78350f;
|
|
90
|
+
max-width: 11in;
|
|
91
|
+
}
|
|
92
|
+
.bk-banner h2 { font-size: 15px; margin-bottom: 6px; }
|
|
93
|
+
.bk-banner ol { margin: 6px 0 6px 20px; }
|
|
94
|
+
.bk-banner strong { font-weight: 700; }
|
|
95
|
+
.bk-banner .bk-size-warning {
|
|
96
|
+
display: block;
|
|
97
|
+
margin-top: 10px;
|
|
98
|
+
padding-top: 10px;
|
|
99
|
+
border-top: 1px solid #f59e0b;
|
|
100
|
+
color: #7f1d1d;
|
|
101
|
+
font-weight: 600;
|
|
102
|
+
}
|
|
103
|
+
.bk-actions { display: flex; gap: 10px; margin: 0 18px 18px; }
|
|
104
|
+
.bk-btn {
|
|
105
|
+
padding: 8px 20px; border-radius: 8px; font-size: 13px; font-family: inherit;
|
|
106
|
+
font-weight: 600; cursor: pointer; border: none;
|
|
107
|
+
}
|
|
108
|
+
.bk-btn-primary { background: #2563eb; color: #fff; }
|
|
109
|
+
.bk-btn-secondary { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; }
|
|
110
|
+
|
|
111
|
+
@media print {
|
|
112
|
+
.screen-only { display: none !important; }
|
|
113
|
+
.bk-side { page-break-after: always; break-after: page; }
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
function renderBox(page, pagesHtml) {
|
|
117
|
+
const html = page === 0 ? void 0 : pagesHtml[page - 1];
|
|
118
|
+
if (html === void 0) {
|
|
119
|
+
return `<div class="bk-page bk-blank" data-page="0"></div>`;
|
|
120
|
+
}
|
|
121
|
+
return `<div class="bk-page" data-page="${page}">${html}</div>`;
|
|
122
|
+
}
|
|
123
|
+
function renderSide(spread, pagesHtml) {
|
|
124
|
+
return `<section class="bk-side">${renderBox(spread[0], pagesHtml)}${renderBox(spread[1], pagesHtml)}</section>`;
|
|
125
|
+
}
|
|
126
|
+
function renderBanner(imposition, warning) {
|
|
127
|
+
const sheetCount = imposition.sheets.length;
|
|
128
|
+
const warningHtml = warning ? `<span class="bk-size-warning">${escapeHtml(warning)}</span>` : "";
|
|
129
|
+
return `<div class="bk-banner screen-only">
|
|
130
|
+
<h2>Booklet print settings \u2014 set these exactly</h2>
|
|
131
|
+
<ol>
|
|
132
|
+
<li>Orientation: <strong>Landscape</strong></li>
|
|
133
|
+
<li>Two-sided printing: <strong>ON</strong></li>
|
|
134
|
+
<li>Flip on <strong>SHORT edge</strong> (also called "tumble" or "short-edge binding") \u2014 long-edge flip prints every back side upside-down</li>
|
|
135
|
+
<li>Scale: <strong>100%</strong> (never "fit to page")</li>
|
|
136
|
+
<li>Margins: <strong>None</strong></li>
|
|
137
|
+
</ol>
|
|
138
|
+
<p><strong>Print all pages, fold the whole stack in half, staple the spine.</strong>
|
|
139
|
+
${sheetCount} sheet${sheetCount === 1 ? "" : "s"} of paper, ${imposition.paddedPageCount} booklet pages.</p>
|
|
140
|
+
${warningHtml}
|
|
141
|
+
</div>`;
|
|
142
|
+
}
|
|
143
|
+
function buildBookletDocument(pagesHtml, opts) {
|
|
144
|
+
const title = opts?.title ?? "Booklet";
|
|
145
|
+
const pageStyles = opts?.pageStyles ?? "";
|
|
146
|
+
const imposition = imposeBooklet(Math.max(1, pagesHtml.length));
|
|
147
|
+
const warning = bookletSizeWarning(pagesHtml.length);
|
|
148
|
+
const sides = imposition.sheets.map((sheet) => renderSide(sheet.front, pagesHtml) + renderSide(sheet.back, pagesHtml)).join("\n");
|
|
149
|
+
return `<!DOCTYPE html>
|
|
150
|
+
<html lang="en">
|
|
151
|
+
<head>
|
|
152
|
+
<meta charset="UTF-8">
|
|
153
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
154
|
+
<title>${escapeHtml(title)}</title>
|
|
155
|
+
<style>
|
|
156
|
+
${BOOKLET_STYLES}
|
|
157
|
+
${pageStyles}
|
|
158
|
+
</style>
|
|
159
|
+
</head>
|
|
160
|
+
<body>
|
|
161
|
+
${renderBanner(imposition, warning)}
|
|
162
|
+
<div class="bk-actions screen-only">
|
|
163
|
+
<button class="bk-btn bk-btn-primary" onclick="window.print()">Print / Save as PDF</button>
|
|
164
|
+
<button class="bk-btn bk-btn-secondary" onclick="window.close()">Close</button>
|
|
165
|
+
</div>
|
|
166
|
+
${sides}
|
|
167
|
+
</body>
|
|
168
|
+
</html>`;
|
|
169
|
+
}
|
|
170
|
+
function printBooklet(pagesHtml, opts) {
|
|
171
|
+
const title = opts?.title ?? "Booklet";
|
|
172
|
+
return openPrintWindow(buildBookletDocument(pagesHtml, opts), title);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/certificate.ts
|
|
176
|
+
function asString(value) {
|
|
177
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
178
|
+
}
|
|
179
|
+
function parseSignature(raw) {
|
|
180
|
+
if (!raw || typeof raw !== "object") return null;
|
|
181
|
+
const obj = raw;
|
|
182
|
+
const role = asString(obj["role"]);
|
|
183
|
+
if (role === void 0) return null;
|
|
184
|
+
const name = asString(obj["name"]);
|
|
185
|
+
return name === void 0 ? { role } : { name, role };
|
|
186
|
+
}
|
|
187
|
+
function extractCertificate(data) {
|
|
188
|
+
if (!data || typeof data !== "object") {
|
|
189
|
+
return { recipientName: "", title: "Certificate" };
|
|
190
|
+
}
|
|
191
|
+
const obj = data;
|
|
192
|
+
const cert = {
|
|
193
|
+
recipientName: asString(obj["recipientName"]) ?? "",
|
|
194
|
+
title: asString(obj["title"]) ?? "Certificate"
|
|
195
|
+
};
|
|
196
|
+
const subtitle = asString(obj["subtitle"]);
|
|
197
|
+
if (subtitle !== void 0) cert.subtitle = subtitle;
|
|
198
|
+
const body = asString(obj["body"]);
|
|
199
|
+
if (body !== void 0) cert.body = body;
|
|
200
|
+
const date = asString(obj["date"]);
|
|
201
|
+
if (date !== void 0) cert.date = date;
|
|
202
|
+
const rawSignatures = obj["signatures"];
|
|
203
|
+
if (Array.isArray(rawSignatures)) {
|
|
204
|
+
const signatures = rawSignatures.map(parseSignature).filter((s) => s !== null);
|
|
205
|
+
if (signatures.length > 0) cert.signatures = signatures;
|
|
206
|
+
}
|
|
207
|
+
return cert;
|
|
208
|
+
}
|
|
209
|
+
var CERT_RESET = `
|
|
210
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
211
|
+
html, body {
|
|
212
|
+
background: #fff;
|
|
213
|
+
color: #111827;
|
|
214
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
|
215
|
+
}
|
|
216
|
+
.screen-only { padding: 14px 20px; border-bottom: 1px solid #e2e8f0; display: flex; gap: 10px; }
|
|
217
|
+
.cert-btn {
|
|
218
|
+
padding: 8px 20px; border-radius: 8px; font-size: 13px; font-family: inherit;
|
|
219
|
+
font-weight: 600; cursor: pointer; border: none;
|
|
220
|
+
}
|
|
221
|
+
.cert-btn-primary { background: #2563eb; color: #fff; }
|
|
222
|
+
.cert-btn-secondary { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; }
|
|
223
|
+
@media print { .screen-only { display: none !important; } }
|
|
224
|
+
|
|
225
|
+
.cert-signatures {
|
|
226
|
+
display: flex;
|
|
227
|
+
justify-content: center;
|
|
228
|
+
gap: 0.8in;
|
|
229
|
+
margin-top: 0.5in;
|
|
230
|
+
}
|
|
231
|
+
.cert-sig { min-width: 2.2in; text-align: center; }
|
|
232
|
+
.cert-sig-rule {
|
|
233
|
+
border-bottom: 1.5px solid #111827;
|
|
234
|
+
height: 0.42in;
|
|
235
|
+
-webkit-print-color-adjust: exact;
|
|
236
|
+
print-color-adjust: exact;
|
|
237
|
+
}
|
|
238
|
+
.cert-sig-name { font-size: 11pt; font-weight: 600; margin-top: 4px; }
|
|
239
|
+
.cert-sig-role {
|
|
240
|
+
font-size: 8.5pt;
|
|
241
|
+
letter-spacing: 0.1em;
|
|
242
|
+
text-transform: uppercase;
|
|
243
|
+
color: #4b5563;
|
|
244
|
+
margin-top: 2px;
|
|
245
|
+
}
|
|
246
|
+
`;
|
|
247
|
+
var FORMAL_STYLES = `
|
|
248
|
+
${CERT_RESET}
|
|
249
|
+
@page { size: 11in 8.5in; margin: 0; }
|
|
250
|
+
|
|
251
|
+
.cert-page {
|
|
252
|
+
width: 11in;
|
|
253
|
+
height: 8.5in;
|
|
254
|
+
padding: 0.45in;
|
|
255
|
+
position: relative;
|
|
256
|
+
break-after: page;
|
|
257
|
+
page-break-after: always;
|
|
258
|
+
}
|
|
259
|
+
.cert-page:last-of-type { break-after: auto; page-break-after: auto; }
|
|
260
|
+
|
|
261
|
+
.cert-frame {
|
|
262
|
+
position: relative;
|
|
263
|
+
width: 100%;
|
|
264
|
+
height: 100%;
|
|
265
|
+
border: 6px double #7c5c1e;
|
|
266
|
+
padding: 0.28in;
|
|
267
|
+
-webkit-print-color-adjust: exact;
|
|
268
|
+
print-color-adjust: exact;
|
|
269
|
+
}
|
|
270
|
+
.cert-frame-inner {
|
|
271
|
+
width: 100%;
|
|
272
|
+
height: 100%;
|
|
273
|
+
border: 1px solid #b08b3f;
|
|
274
|
+
padding: 0.5in 0.7in;
|
|
275
|
+
display: flex;
|
|
276
|
+
flex-direction: column;
|
|
277
|
+
align-items: center;
|
|
278
|
+
justify-content: center;
|
|
279
|
+
text-align: center;
|
|
280
|
+
-webkit-print-color-adjust: exact;
|
|
281
|
+
print-color-adjust: exact;
|
|
282
|
+
}
|
|
283
|
+
/* Corner flourishes \u2014 two borders per pseudo-element, rotated by placement. */
|
|
284
|
+
.cert-corner {
|
|
285
|
+
position: absolute;
|
|
286
|
+
width: 0.55in;
|
|
287
|
+
height: 0.55in;
|
|
288
|
+
-webkit-print-color-adjust: exact;
|
|
289
|
+
print-color-adjust: exact;
|
|
290
|
+
}
|
|
291
|
+
.cert-corner-tl { top: 0.1in; left: 0.1in; border-top: 3px solid #b08b3f; border-left: 3px solid #b08b3f; }
|
|
292
|
+
.cert-corner-tr { top: 0.1in; right: 0.1in; border-top: 3px solid #b08b3f; border-right: 3px solid #b08b3f; }
|
|
293
|
+
.cert-corner-bl { bottom: 0.1in; left: 0.1in; border-bottom: 3px solid #b08b3f; border-left: 3px solid #b08b3f; }
|
|
294
|
+
.cert-corner-br { bottom: 0.1in; right: 0.1in; border-bottom: 3px solid #b08b3f; border-right: 3px solid #b08b3f; }
|
|
295
|
+
|
|
296
|
+
.cert-eyebrow {
|
|
297
|
+
font-size: 11pt;
|
|
298
|
+
letter-spacing: 0.34em;
|
|
299
|
+
text-transform: uppercase;
|
|
300
|
+
color: #7c5c1e;
|
|
301
|
+
}
|
|
302
|
+
.cert-title { font-size: 30pt; font-weight: 700; margin-top: 0.1in; letter-spacing: 0.02em; }
|
|
303
|
+
.cert-subtitle { font-size: 12pt; color: #4b5563; margin-top: 0.08in; }
|
|
304
|
+
.cert-presented { font-size: 10.5pt; color: #4b5563; margin-top: 0.32in; letter-spacing: 0.16em; text-transform: uppercase; }
|
|
305
|
+
.cert-recipient {
|
|
306
|
+
font-size: 34pt;
|
|
307
|
+
font-weight: 400;
|
|
308
|
+
font-family: 'Palatino Linotype', Palatino, 'Book Antiqua', Georgia, serif;
|
|
309
|
+
margin-top: 0.06in;
|
|
310
|
+
padding-bottom: 0.08in;
|
|
311
|
+
border-bottom: 1px solid #b08b3f;
|
|
312
|
+
min-width: 5in;
|
|
313
|
+
-webkit-print-color-adjust: exact;
|
|
314
|
+
print-color-adjust: exact;
|
|
315
|
+
}
|
|
316
|
+
.cert-body { font-size: 11pt; line-height: 1.6; max-width: 6.6in; margin-top: 0.24in; color: #1f2937; }
|
|
317
|
+
.cert-date { font-size: 10pt; color: #4b5563; margin-top: 0.24in; }
|
|
318
|
+
`;
|
|
319
|
+
var MODERN_STYLES = `
|
|
320
|
+
${CERT_RESET}
|
|
321
|
+
@page { size: 11in 8.5in; margin: 0; }
|
|
322
|
+
|
|
323
|
+
.cert-page {
|
|
324
|
+
width: 11in;
|
|
325
|
+
height: 8.5in;
|
|
326
|
+
padding: 0.9in 1.1in;
|
|
327
|
+
display: flex;
|
|
328
|
+
flex-direction: column;
|
|
329
|
+
justify-content: center;
|
|
330
|
+
break-after: page;
|
|
331
|
+
page-break-after: always;
|
|
332
|
+
}
|
|
333
|
+
.cert-page:last-of-type { break-after: auto; page-break-after: auto; }
|
|
334
|
+
|
|
335
|
+
.cert-rule-top {
|
|
336
|
+
width: 1.6in;
|
|
337
|
+
border-top: 4px solid #111827;
|
|
338
|
+
margin-bottom: 0.3in;
|
|
339
|
+
-webkit-print-color-adjust: exact;
|
|
340
|
+
print-color-adjust: exact;
|
|
341
|
+
}
|
|
342
|
+
.cert-eyebrow {
|
|
343
|
+
font-size: 9.5pt;
|
|
344
|
+
letter-spacing: 0.3em;
|
|
345
|
+
text-transform: uppercase;
|
|
346
|
+
color: #6b7280;
|
|
347
|
+
}
|
|
348
|
+
.cert-title { font-size: 22pt; font-weight: 600; margin-top: 0.06in; }
|
|
349
|
+
.cert-subtitle { font-size: 11pt; color: #6b7280; margin-top: 0.05in; }
|
|
350
|
+
.cert-recipient { font-size: 40pt; font-weight: 700; letter-spacing: -0.01em; margin-top: 0.3in; }
|
|
351
|
+
.cert-body { font-size: 11pt; line-height: 1.65; max-width: 6.2in; margin-top: 0.2in; color: #374151; }
|
|
352
|
+
.cert-date { font-size: 10pt; color: #6b7280; margin-top: 0.24in; }
|
|
353
|
+
.cert-signatures { justify-content: flex-start; gap: 0.7in; }
|
|
354
|
+
.cert-sig { text-align: left; }
|
|
355
|
+
`;
|
|
356
|
+
var ACK_STYLES = `
|
|
357
|
+
${CERT_RESET}
|
|
358
|
+
@page { size: 8.5in 11in; margin: 0.75in; }
|
|
359
|
+
|
|
360
|
+
.cert-page {
|
|
361
|
+
break-after: page;
|
|
362
|
+
page-break-after: always;
|
|
363
|
+
}
|
|
364
|
+
.cert-page:last-of-type { break-after: auto; page-break-after: auto; }
|
|
365
|
+
|
|
366
|
+
.cert-title { font-size: 17pt; font-weight: 700; }
|
|
367
|
+
.cert-subtitle { font-size: 11pt; color: #4b5563; margin-top: 3px; }
|
|
368
|
+
.cert-body { font-size: 11pt; line-height: 1.7; margin-top: 0.28in; color: #1f2937; }
|
|
369
|
+
.cert-ack-statement {
|
|
370
|
+
margin-top: 0.3in;
|
|
371
|
+
padding: 0.16in 0.2in;
|
|
372
|
+
border: 1.5px solid #111827;
|
|
373
|
+
font-size: 11pt;
|
|
374
|
+
line-height: 1.65;
|
|
375
|
+
-webkit-print-color-adjust: exact;
|
|
376
|
+
print-color-adjust: exact;
|
|
377
|
+
}
|
|
378
|
+
.cert-ack-fields { margin-top: 0.4in; display: flex; gap: 0.4in; }
|
|
379
|
+
.cert-ack-field { flex: 1; }
|
|
380
|
+
.cert-ack-field-date { flex: 0 0 2in; }
|
|
381
|
+
.cert-ack-rule {
|
|
382
|
+
border-bottom: 1.5px solid #111827;
|
|
383
|
+
height: 0.4in;
|
|
384
|
+
-webkit-print-color-adjust: exact;
|
|
385
|
+
print-color-adjust: exact;
|
|
386
|
+
}
|
|
387
|
+
.cert-ack-label {
|
|
388
|
+
font-size: 8.5pt;
|
|
389
|
+
letter-spacing: 0.1em;
|
|
390
|
+
text-transform: uppercase;
|
|
391
|
+
color: #4b5563;
|
|
392
|
+
margin-top: 3px;
|
|
393
|
+
}
|
|
394
|
+
.cert-signatures { justify-content: flex-start; gap: 0.6in; }
|
|
395
|
+
.cert-sig { text-align: left; }
|
|
396
|
+
`;
|
|
397
|
+
function renderSignatures(signatures) {
|
|
398
|
+
if (signatures.length === 0) return "";
|
|
399
|
+
const lines = signatures.map(
|
|
400
|
+
(sig) => `<div class="cert-sig">
|
|
401
|
+
<div class="cert-sig-rule"></div>
|
|
402
|
+
${sig.name ? `<div class="cert-sig-name">${escapeHtml(sig.name)}</div>` : ""}
|
|
403
|
+
<div class="cert-sig-role">${escapeHtml(sig.role)}</div>
|
|
404
|
+
</div>`
|
|
405
|
+
).join("");
|
|
406
|
+
return `<div class="cert-signatures">${lines}</div>`;
|
|
407
|
+
}
|
|
408
|
+
function renderFormal(cert) {
|
|
409
|
+
return `<div class="cert-page cert-formal">
|
|
410
|
+
<div class="cert-frame">
|
|
411
|
+
<span class="cert-corner cert-corner-tl"></span>
|
|
412
|
+
<span class="cert-corner cert-corner-tr"></span>
|
|
413
|
+
<span class="cert-corner cert-corner-bl"></span>
|
|
414
|
+
<span class="cert-corner cert-corner-br"></span>
|
|
415
|
+
<div class="cert-frame-inner">
|
|
416
|
+
<div class="cert-eyebrow">Certificate</div>
|
|
417
|
+
<div class="cert-title">${escapeHtml(cert.title)}</div>
|
|
418
|
+
${cert.subtitle ? `<div class="cert-subtitle">${escapeHtml(cert.subtitle)}</div>` : ""}
|
|
419
|
+
<div class="cert-presented">This certifies that</div>
|
|
420
|
+
<div class="cert-recipient">${escapeHtml(cert.recipientName)}</div>
|
|
421
|
+
${cert.body ? `<div class="cert-body">${escapeHtml(cert.body)}</div>` : ""}
|
|
422
|
+
${cert.date ? `<div class="cert-date">${escapeHtml(cert.date)}</div>` : ""}
|
|
423
|
+
${renderSignatures(cert.signatures ?? [])}
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
</div>`;
|
|
427
|
+
}
|
|
428
|
+
function renderModernMinimal(cert) {
|
|
429
|
+
return `<div class="cert-page cert-modern">
|
|
430
|
+
<div class="cert-rule-top"></div>
|
|
431
|
+
<div class="cert-eyebrow">Certificate of completion</div>
|
|
432
|
+
<div class="cert-title">${escapeHtml(cert.title)}</div>
|
|
433
|
+
${cert.subtitle ? `<div class="cert-subtitle">${escapeHtml(cert.subtitle)}</div>` : ""}
|
|
434
|
+
<div class="cert-recipient">${escapeHtml(cert.recipientName)}</div>
|
|
435
|
+
${cert.body ? `<div class="cert-body">${escapeHtml(cert.body)}</div>` : ""}
|
|
436
|
+
${cert.date ? `<div class="cert-date">${escapeHtml(cert.date)}</div>` : ""}
|
|
437
|
+
${renderSignatures(cert.signatures ?? [])}
|
|
438
|
+
</div>`;
|
|
439
|
+
}
|
|
440
|
+
var ACK_STATEMENT = "I acknowledge that I have received, read and understand this document, and I agree to comply with its contents.";
|
|
441
|
+
function renderAcknowledgment(cert) {
|
|
442
|
+
return `<div class="cert-page cert-acknowledgment">
|
|
443
|
+
<div class="cert-title">${escapeHtml(cert.title)}</div>
|
|
444
|
+
${cert.subtitle ? `<div class="cert-subtitle">${escapeHtml(cert.subtitle)}</div>` : ""}
|
|
445
|
+
${cert.body ? `<div class="cert-body">${escapeHtml(cert.body)}</div>` : ""}
|
|
446
|
+
<div class="cert-ack-statement">${escapeHtml(ACK_STATEMENT)}</div>
|
|
447
|
+
<div class="cert-ack-fields">
|
|
448
|
+
<div class="cert-ack-field">
|
|
449
|
+
<div class="cert-ack-rule"></div>
|
|
450
|
+
<div class="cert-ack-label">Printed name${cert.recipientName ? ` \u2014 ${escapeHtml(cert.recipientName)}` : ""}</div>
|
|
451
|
+
</div>
|
|
452
|
+
<div class="cert-ack-field">
|
|
453
|
+
<div class="cert-ack-rule"></div>
|
|
454
|
+
<div class="cert-ack-label">Signature</div>
|
|
455
|
+
</div>
|
|
456
|
+
<div class="cert-ack-field cert-ack-field-date">
|
|
457
|
+
<div class="cert-ack-rule"></div>
|
|
458
|
+
<div class="cert-ack-label">Date${cert.date ? ` \u2014 ${escapeHtml(cert.date)}` : ""}</div>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
${renderSignatures(cert.signatures ?? [])}
|
|
462
|
+
</div>`;
|
|
463
|
+
}
|
|
464
|
+
function stylesFor(variant) {
|
|
465
|
+
switch (variant) {
|
|
466
|
+
case "modern-minimal":
|
|
467
|
+
return MODERN_STYLES;
|
|
468
|
+
case "acknowledgment":
|
|
469
|
+
return ACK_STYLES;
|
|
470
|
+
case "formal":
|
|
471
|
+
default:
|
|
472
|
+
return FORMAL_STYLES;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function renderCertificatePage(cert, variant = "formal") {
|
|
476
|
+
switch (variant) {
|
|
477
|
+
case "modern-minimal":
|
|
478
|
+
return renderModernMinimal(cert);
|
|
479
|
+
case "acknowledgment":
|
|
480
|
+
return renderAcknowledgment(cert);
|
|
481
|
+
case "formal":
|
|
482
|
+
default:
|
|
483
|
+
return renderFormal(cert);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function buildCertificateDocument(cert, variant) {
|
|
487
|
+
return `<!DOCTYPE html>
|
|
488
|
+
<html lang="en">
|
|
489
|
+
<head>
|
|
490
|
+
<meta charset="UTF-8">
|
|
491
|
+
<title>${escapeHtml(cert.title)}</title>
|
|
492
|
+
<style>
|
|
493
|
+
${stylesFor(variant)}
|
|
494
|
+
</style>
|
|
495
|
+
</head>
|
|
496
|
+
<body>
|
|
497
|
+
<div class="screen-only">
|
|
498
|
+
<button class="cert-btn cert-btn-primary" onclick="window.print()">Print / Save as PDF</button>
|
|
499
|
+
<button class="cert-btn cert-btn-secondary" onclick="window.close()">Close</button>
|
|
500
|
+
</div>
|
|
501
|
+
${renderCertificatePage(cert, variant)}
|
|
502
|
+
</body>
|
|
503
|
+
</html>`;
|
|
504
|
+
}
|
|
505
|
+
var certificatePrinter = {
|
|
506
|
+
label: "Print certificate",
|
|
507
|
+
variants: [
|
|
508
|
+
{
|
|
509
|
+
id: "formal",
|
|
510
|
+
label: "Formal (ornamental border)",
|
|
511
|
+
description: "Landscape letter with a double gold rule and corner flourishes \u2014 the classic course/award certificate. Pure CSS, no images"
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
id: "modern-minimal",
|
|
515
|
+
label: "Modern minimal",
|
|
516
|
+
description: "Landscape letter, left-aligned, no ornament \u2014 a large recipient name over a single rule"
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
id: "acknowledgment",
|
|
520
|
+
label: "Acknowledgment / signature page (HR)",
|
|
521
|
+
description: 'Portrait letter: policy body text, an "I acknowledge\u2026" statement, and printed-name / signature / date rules. The HR twin of the certificate page'
|
|
522
|
+
}
|
|
523
|
+
],
|
|
524
|
+
print(data, variantId = "formal", _settings) {
|
|
525
|
+
const cert = extractCertificate(data);
|
|
526
|
+
const variant = ["formal", "modern-minimal", "acknowledgment"].includes(variantId) ? variantId : "formal";
|
|
527
|
+
return openPrintWindow(
|
|
528
|
+
buildCertificateDocument(cert, variant),
|
|
529
|
+
cert.title,
|
|
530
|
+
"width=1100,height=760,scrollbars=yes"
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
var WORKBOOK_STYLES = `
|
|
535
|
+
.wb-cover {
|
|
536
|
+
height: 100%;
|
|
537
|
+
display: flex;
|
|
538
|
+
flex-direction: column;
|
|
539
|
+
justify-content: center;
|
|
540
|
+
align-items: center;
|
|
541
|
+
text-align: center;
|
|
542
|
+
padding: 0.9in 0.7in;
|
|
543
|
+
}
|
|
544
|
+
.wb-cover-title { font-size: 26pt; font-weight: 700; line-height: 1.2; }
|
|
545
|
+
.wb-cover-subtitle { font-size: 12pt; color: #4b5563; margin-top: 0.16in; }
|
|
546
|
+
.wb-cover-rule {
|
|
547
|
+
width: 1.4in;
|
|
548
|
+
border-top: 3px solid #111827;
|
|
549
|
+
margin: 0.28in 0;
|
|
550
|
+
-webkit-print-color-adjust: exact;
|
|
551
|
+
print-color-adjust: exact;
|
|
552
|
+
}
|
|
553
|
+
.wb-toc { padding: 0.7in 0.7in; }
|
|
554
|
+
.wb-toc h2 { font-size: 15pt; font-weight: 700; margin-bottom: 0.2in; }
|
|
555
|
+
.wb-toc-row {
|
|
556
|
+
display: flex;
|
|
557
|
+
align-items: baseline;
|
|
558
|
+
gap: 6px;
|
|
559
|
+
font-size: 11pt;
|
|
560
|
+
margin-bottom: 8px;
|
|
561
|
+
}
|
|
562
|
+
.wb-toc-label { white-space: nowrap; }
|
|
563
|
+
.wb-toc-dots {
|
|
564
|
+
flex: 1;
|
|
565
|
+
border-bottom: 1px dotted #9ca3af;
|
|
566
|
+
transform: translateY(-3px);
|
|
567
|
+
-webkit-print-color-adjust: exact;
|
|
568
|
+
print-color-adjust: exact;
|
|
569
|
+
}
|
|
570
|
+
.wb-toc-page { font-variant-numeric: tabular-nums; font-weight: 600; }
|
|
571
|
+
.wb-section-title-page {
|
|
572
|
+
height: 100%;
|
|
573
|
+
display: flex;
|
|
574
|
+
flex-direction: column;
|
|
575
|
+
justify-content: center;
|
|
576
|
+
padding: 0.9in 0.8in;
|
|
577
|
+
}
|
|
578
|
+
.wb-section-eyebrow {
|
|
579
|
+
font-size: 9pt;
|
|
580
|
+
letter-spacing: 0.28em;
|
|
581
|
+
text-transform: uppercase;
|
|
582
|
+
color: #6b7280;
|
|
583
|
+
}
|
|
584
|
+
.wb-section-name { font-size: 22pt; font-weight: 700; margin-top: 0.08in; }
|
|
585
|
+
`;
|
|
586
|
+
var WORKBOOK_SEQUENTIAL_STYLES = `
|
|
587
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
588
|
+
@page { size: 8.5in 11in; margin: 0; }
|
|
589
|
+
html, body {
|
|
590
|
+
background: #fff;
|
|
591
|
+
color: #111827;
|
|
592
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
|
593
|
+
}
|
|
594
|
+
.wb-page {
|
|
595
|
+
width: 8.5in;
|
|
596
|
+
height: 11in;
|
|
597
|
+
overflow: hidden;
|
|
598
|
+
position: relative;
|
|
599
|
+
break-after: page;
|
|
600
|
+
page-break-after: always;
|
|
601
|
+
}
|
|
602
|
+
.wb-page:last-of-type { break-after: auto; page-break-after: auto; }
|
|
603
|
+
.screen-only { padding: 14px 20px; border-bottom: 1px solid #e2e8f0; display: flex; gap: 10px; }
|
|
604
|
+
.wb-btn {
|
|
605
|
+
padding: 8px 20px; border-radius: 8px; font-size: 13px; font-family: inherit;
|
|
606
|
+
font-weight: 600; cursor: pointer; border: none;
|
|
607
|
+
}
|
|
608
|
+
.wb-btn-primary { background: #2563eb; color: #fff; }
|
|
609
|
+
.wb-btn-secondary { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; }
|
|
610
|
+
@media print { .screen-only { display: none !important; } }
|
|
611
|
+
${WORKBOOK_STYLES}
|
|
612
|
+
${CERT_RESET_FOR_WORKBOOK()}
|
|
613
|
+
`;
|
|
614
|
+
function CERT_RESET_FOR_WORKBOOK() {
|
|
615
|
+
return `
|
|
616
|
+
.cert-page { width: 100%; height: 100%; padding: 0.5in; break-after: auto; page-break-after: auto; }
|
|
617
|
+
${CERT_RESET.split("@media print")[0] ?? ""}
|
|
618
|
+
.cert-frame { position: relative; width: 100%; height: 100%; border: 6px double #7c5c1e; padding: 0.24in; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
619
|
+
.cert-frame-inner { width: 100%; height: 100%; border: 1px solid #b08b3f; padding: 0.4in; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
|
|
620
|
+
.cert-corner { position: absolute; width: 0.5in; height: 0.5in; }
|
|
621
|
+
.cert-corner-tl { top: 0.1in; left: 0.1in; border-top: 3px solid #b08b3f; border-left: 3px solid #b08b3f; }
|
|
622
|
+
.cert-corner-tr { top: 0.1in; right: 0.1in; border-top: 3px solid #b08b3f; border-right: 3px solid #b08b3f; }
|
|
623
|
+
.cert-corner-bl { bottom: 0.1in; left: 0.1in; border-bottom: 3px solid #b08b3f; border-left: 3px solid #b08b3f; }
|
|
624
|
+
.cert-corner-br { bottom: 0.1in; right: 0.1in; border-bottom: 3px solid #b08b3f; border-right: 3px solid #b08b3f; }
|
|
625
|
+
.cert-eyebrow { font-size: 10pt; letter-spacing: 0.3em; text-transform: uppercase; color: #7c5c1e; }
|
|
626
|
+
.cert-title { font-size: 22pt; font-weight: 700; margin-top: 0.08in; }
|
|
627
|
+
.cert-subtitle { font-size: 11pt; color: #4b5563; margin-top: 0.06in; }
|
|
628
|
+
.cert-presented { font-size: 10pt; color: #4b5563; margin-top: 0.24in; letter-spacing: 0.16em; text-transform: uppercase; }
|
|
629
|
+
.cert-recipient { font-size: 26pt; font-family: 'Palatino Linotype', Palatino, 'Book Antiqua', Georgia, serif; margin-top: 0.06in; padding-bottom: 0.06in; border-bottom: 1px solid #b08b3f; min-width: 3.4in; }
|
|
630
|
+
.cert-body { font-size: 10.5pt; line-height: 1.6; margin-top: 0.2in; }
|
|
631
|
+
.cert-date { font-size: 10pt; color: #4b5563; margin-top: 0.2in; }
|
|
632
|
+
`;
|
|
633
|
+
}
|
|
634
|
+
function renderCoverPage(spec) {
|
|
635
|
+
return `<div class="wb-cover">
|
|
636
|
+
<div class="wb-cover-title">${escapeHtml(spec.title)}</div>
|
|
637
|
+
<div class="wb-cover-rule"></div>
|
|
638
|
+
${spec.subtitle ? `<div class="wb-cover-subtitle">${escapeHtml(spec.subtitle)}</div>` : ""}
|
|
639
|
+
</div>`;
|
|
640
|
+
}
|
|
641
|
+
function renderSectionTitlePage(section, index) {
|
|
642
|
+
return `<div class="wb-section-title-page" data-section="${index + 1}">
|
|
643
|
+
<div class="wb-section-eyebrow">Section ${index + 1}</div>
|
|
644
|
+
<div class="wb-section-name">${escapeHtml(section.title)}</div>
|
|
645
|
+
</div>`;
|
|
646
|
+
}
|
|
647
|
+
function renderTocPage(spec, entries) {
|
|
648
|
+
const rows = entries.map(
|
|
649
|
+
(entry) => `<div class="wb-toc-row" data-toc-page="${entry.page}">
|
|
650
|
+
<span class="wb-toc-label">${escapeHtml(entry.title)}</span>
|
|
651
|
+
<span class="wb-toc-dots"></span>
|
|
652
|
+
<span class="wb-toc-page">${entry.page}</span>
|
|
653
|
+
</div>`
|
|
654
|
+
).join("");
|
|
655
|
+
return `<div class="wb-toc">
|
|
656
|
+
<h2>${escapeHtml(spec.title)} \u2014 Contents</h2>
|
|
657
|
+
${rows}
|
|
658
|
+
</div>`;
|
|
659
|
+
}
|
|
660
|
+
function composeWorkbookPages(spec) {
|
|
661
|
+
const hasToc = spec.includeToc === true;
|
|
662
|
+
const firstSectionPage = hasToc ? 3 : 2;
|
|
663
|
+
const entries = [];
|
|
664
|
+
let cursor = firstSectionPage;
|
|
665
|
+
for (const section of spec.sections) {
|
|
666
|
+
entries.push({ title: section.title, page: cursor });
|
|
667
|
+
cursor += 1 + section.pagesHtml.length;
|
|
668
|
+
}
|
|
669
|
+
const pages = [renderCoverPage(spec)];
|
|
670
|
+
if (hasToc) pages.push(renderTocPage(spec, entries));
|
|
671
|
+
spec.sections.forEach((section, i) => {
|
|
672
|
+
pages.push(renderSectionTitlePage(section, i));
|
|
673
|
+
for (const pageHtml of section.pagesHtml) pages.push(pageHtml);
|
|
674
|
+
});
|
|
675
|
+
if (spec.finalPage) {
|
|
676
|
+
pages.push(
|
|
677
|
+
spec.finalPage.kind === "certificate" ? renderCertificatePage(spec.finalPage.data, "formal") : spec.finalPage.html
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
return pages;
|
|
681
|
+
}
|
|
682
|
+
function printWorkbook(spec, opts) {
|
|
683
|
+
const pages = composeWorkbookPages(spec);
|
|
684
|
+
const extra = opts?.pageStyles ?? "";
|
|
685
|
+
if (opts?.booklet === true) {
|
|
686
|
+
return printBooklet(pages, {
|
|
687
|
+
title: spec.title,
|
|
688
|
+
pageStyles: `${WORKBOOK_STYLES}
|
|
689
|
+
${CERT_RESET_FOR_WORKBOOK()}
|
|
690
|
+
${extra}`
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
const body = pages.map(
|
|
694
|
+
(html, i) => `<div class="wb-page" data-page="${i + 1}">${html}</div>`
|
|
695
|
+
).join("\n");
|
|
696
|
+
const doc = `<!DOCTYPE html>
|
|
697
|
+
<html lang="en">
|
|
698
|
+
<head>
|
|
699
|
+
<meta charset="UTF-8">
|
|
700
|
+
<title>${escapeHtml(spec.title)}</title>
|
|
701
|
+
<style>
|
|
702
|
+
${WORKBOOK_SEQUENTIAL_STYLES}
|
|
703
|
+
${extra}
|
|
704
|
+
</style>
|
|
705
|
+
</head>
|
|
706
|
+
<body>
|
|
707
|
+
<div class="screen-only">
|
|
708
|
+
<button class="wb-btn wb-btn-primary" onclick="window.print()">Print / Save as PDF</button>
|
|
709
|
+
<button class="wb-btn wb-btn-secondary" onclick="window.close()">Close</button>
|
|
710
|
+
</div>
|
|
711
|
+
${body}
|
|
712
|
+
</body>
|
|
713
|
+
</html>`;
|
|
714
|
+
return openPrintWindow(doc, spec.title, "width=920,height=760,scrollbars=yes");
|
|
715
|
+
}
|
|
716
|
+
export {
|
|
717
|
+
certificatePrinter,
|
|
718
|
+
composeWorkbookPages,
|
|
719
|
+
extractCertificate,
|
|
720
|
+
printWorkbook,
|
|
721
|
+
renderCertificatePage
|
|
722
|
+
};
|
|
723
|
+
//# sourceMappingURL=certificate.js.map
|