@ai-matrx/print 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1686 @@
1
+ // src/core.ts
2
+ var PRINT_STYLES = `
3
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
4
+
5
+ body {
6
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
7
+ font-size: 11pt;
8
+ line-height: 1.6;
9
+ color: #1a1a1a;
10
+ background: #fff;
11
+ max-width: 860px;
12
+ margin: 0 auto;
13
+ padding: 28px 36px;
14
+ }
15
+
16
+ .print-actions {
17
+ display: flex;
18
+ gap: 10px;
19
+ margin-bottom: 24px;
20
+ padding-bottom: 18px;
21
+ border-bottom: 2px solid #e5e7eb;
22
+ }
23
+
24
+ .print-btn {
25
+ display: inline-flex;
26
+ align-items: center;
27
+ gap: 6px;
28
+ padding: 8px 20px;
29
+ border-radius: 8px;
30
+ font-size: 13px;
31
+ font-family: inherit;
32
+ cursor: pointer;
33
+ border: none;
34
+ font-weight: 600;
35
+ }
36
+
37
+ .print-btn-primary { background: #2563eb; color: #fff; }
38
+ .print-btn-secondary { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; }
39
+
40
+ h1 { font-size: 22pt; font-weight: 700; margin-bottom: 0.5em; }
41
+ h2 { font-size: 16pt; font-weight: 700; margin: 1.2em 0 0.4em; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.2em; }
42
+ h3 { font-size: 13pt; font-weight: 600; margin: 1em 0 0.3em; }
43
+ p { margin-bottom: 0.7em; }
44
+
45
+ table {
46
+ width: 100%;
47
+ border-collapse: collapse;
48
+ margin: 1em 0;
49
+ font-size: 10pt;
50
+ }
51
+ th {
52
+ background: #1e293b;
53
+ color: #f8fafc;
54
+ font-weight: 600;
55
+ text-align: left;
56
+ padding: 7px 12px;
57
+ border: 1px solid #334155;
58
+ }
59
+ td {
60
+ padding: 6px 12px;
61
+ border: 1px solid #e2e8f0;
62
+ vertical-align: top;
63
+ }
64
+ tr:nth-child(even) td { background: #f8fafc; }
65
+
66
+ @media print {
67
+ body { max-width: 100%; padding: 0; }
68
+ .print-actions { display: none !important; }
69
+ th { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
70
+ }
71
+ `;
72
+ function escapeHtml(str) {
73
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
74
+ }
75
+ function buildPrintDocument(bodyHtml, title = "AI Response", extraStyles = "") {
76
+ return `<!DOCTYPE html>
77
+ <html lang="en">
78
+ <head>
79
+ <meta charset="UTF-8">
80
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
81
+ <title>${escapeHtml(title)}</title>
82
+ <style>
83
+ ${PRINT_STYLES}
84
+ ${extraStyles}
85
+ </style>
86
+ </head>
87
+ <body>
88
+ <div class="print-actions">
89
+ <button class="print-btn print-btn-primary" onclick="window.print()">Print / Save as PDF</button>
90
+ <button class="print-btn print-btn-secondary" onclick="window.close()">Close</button>
91
+ </div>
92
+ <div class="content">
93
+ ${bodyHtml}
94
+ </div>
95
+ </body>
96
+ </html>`;
97
+ }
98
+ function openPrintWindow(htmlDocument, filename = "print") {
99
+ const win = window.open("", "_blank", "width=920,height=720,scrollbars=yes");
100
+ if (!win) {
101
+ const blob = new Blob([htmlDocument], { type: "text/html;charset=utf-8" });
102
+ const url = URL.createObjectURL(blob);
103
+ const a = document.createElement("a");
104
+ a.href = url;
105
+ a.download = `${filename.replace(/\s+/g, "-").toLowerCase()}.html`;
106
+ document.body.appendChild(a);
107
+ a.click();
108
+ document.body.removeChild(a);
109
+ URL.revokeObjectURL(url);
110
+ return;
111
+ }
112
+ win.document.open();
113
+ win.document.write(htmlDocument);
114
+ win.document.close();
115
+ }
116
+ function buildFitTextScript(selectors) {
117
+ const selectorsJson = JSON.stringify([...selectors]);
118
+ return `
119
+ <script>
120
+ (function() {
121
+ function fitText(el, container) {
122
+ // container is the bounding box we must fit inside
123
+ var maxW = container.clientWidth;
124
+ var maxH = container.clientHeight;
125
+ var size = parseFloat(window.getComputedStyle(el).fontSize);
126
+ var minSize = 6;
127
+ // Give the element a chance to reflow at current size before measuring
128
+ el.style.fontSize = size + "pt";
129
+ while (size > minSize && (el.scrollWidth > maxW + 1 || el.scrollHeight > maxH + 1)) {
130
+ size -= 0.5;
131
+ el.style.fontSize = size + "pt";
132
+ }
133
+ }
134
+ function runAll() {
135
+ var selectors = ${selectorsJson};
136
+ for (var s = 0; s < selectors.length; s++) {
137
+ var els = document.querySelectorAll(selectors[s]);
138
+ for (var i = 0; i < els.length; i++) {
139
+ fitText(els[i], els[i].parentElement);
140
+ }
141
+ }
142
+ }
143
+ if (document.readyState === "loading") {
144
+ document.addEventListener("DOMContentLoaded", runAll);
145
+ } else {
146
+ // document.write pages may already be loaded
147
+ setTimeout(runAll, 0);
148
+ }
149
+ })();
150
+ <\/script>`;
151
+ }
152
+
153
+ // src/flashcards.ts
154
+ var SETTING_DEFAULTS = {
155
+ showSideLabel: false,
156
+ showCardNumber: false,
157
+ showTickMarks: false,
158
+ showCutLines: false,
159
+ lightBackText: false,
160
+ mirrorBack: false,
161
+ showImages: true
162
+ };
163
+ function resolveSettings(raw) {
164
+ if (!raw) return { ...SETTING_DEFAULTS };
165
+ const b = (key) => raw[key] !== void 0 ? Boolean(raw[key]) : SETTING_DEFAULTS[key];
166
+ return {
167
+ showSideLabel: b("showSideLabel"),
168
+ showCardNumber: b("showCardNumber"),
169
+ showTickMarks: b("showTickMarks"),
170
+ showCutLines: b("showCutLines"),
171
+ lightBackText: b("lightBackText"),
172
+ mirrorBack: b("mirrorBack"),
173
+ showImages: b("showImages")
174
+ };
175
+ }
176
+ function extractCards(data) {
177
+ if (!data) return [];
178
+ if (typeof data === "object" && data !== null && "cards" in data) {
179
+ const cards = data.cards;
180
+ return Array.isArray(cards) ? cards : [];
181
+ }
182
+ return [];
183
+ }
184
+ function faceImageHtml(card, face, showImages = true) {
185
+ if (!showImages) return "";
186
+ const url = face === "front" ? card.frontImageUrl : card.backImageUrl;
187
+ if (!url || !/^https?:\/\//.test(url)) return "";
188
+ const alt = (face === "front" ? card.frontImageAlt : card.backImageAlt) ?? "";
189
+ return `<img class="face-image" src="${escapeHtml(url)}" alt="${escapeHtml(alt)}">`;
190
+ }
191
+ var BASE_STYLES = `
192
+ img.face-image {
193
+ display: block;
194
+ max-width: 100%;
195
+ max-height: 1.6in;
196
+ margin: 0 auto 6px auto;
197
+ object-fit: contain;
198
+ }
199
+ h1.deck-title {
200
+ font-size: 13pt !important;
201
+ font-weight: 700;
202
+ color: #1e293b;
203
+ margin: 0 0 4px 0;
204
+ letter-spacing: -0.01em;
205
+ border-bottom: none !important;
206
+ }
207
+ h2 { border-bottom: none !important; }
208
+ .deck-meta {
209
+ font-size: 8.5pt;
210
+ color: #64748b;
211
+ margin-bottom: 16px;
212
+ }
213
+ .deck-header {
214
+ border-bottom: 1.5px solid #e2e8f0;
215
+ padding-bottom: 10px;
216
+ margin-bottom: 16px;
217
+ }
218
+
219
+ @media print {
220
+ @page { margin: 14mm 12mm; }
221
+ }
222
+ `;
223
+ var CUT_CARD_STYLES = `
224
+ ${BASE_STYLES}
225
+
226
+ .instructions {
227
+ font-size: 8pt;
228
+ color: #64748b;
229
+ background: #f8fafc;
230
+ border: 1px solid #e2e8f0;
231
+ border-radius: 6px;
232
+ padding: 7px 10px;
233
+ margin-bottom: 14px;
234
+ line-height: 1.5;
235
+ }
236
+
237
+ .cards-grid {
238
+ display: grid;
239
+ grid-template-columns: repeat(2, 1fr);
240
+ gap: 0;
241
+ }
242
+
243
+ .card-cell {
244
+ position: relative;
245
+ border: 1px dashed #cbd5e1;
246
+ padding: 0;
247
+ min-height: 120px;
248
+ }
249
+
250
+ /* Cut-line corner markers */
251
+ .card-cell::before, .card-cell::after {
252
+ content: "";
253
+ position: absolute;
254
+ width: 8px;
255
+ height: 8px;
256
+ border-color: #94a3b8;
257
+ border-style: solid;
258
+ }
259
+ .card-cell::before { top: 3px; left: 3px; border-width: 1px 0 0 1px; }
260
+ .card-cell::after { bottom: 3px; right: 3px; border-width: 0 1px 1px 0; }
261
+
262
+ .card-inner {
263
+ display: flex;
264
+ flex-direction: column;
265
+ height: 100%;
266
+ min-height: 120px;
267
+ }
268
+
269
+ .card-face {
270
+ flex: 1;
271
+ display: flex;
272
+ flex-direction: column;
273
+ justify-content: center;
274
+ align-items: center;
275
+ padding: 12px 14px;
276
+ text-align: center;
277
+ }
278
+
279
+ .card-front-face {
280
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%);
281
+ color: #ffffff;
282
+ border-radius: 0;
283
+ }
284
+ .card-back-face {
285
+ background: #ffffff;
286
+ border-top: 1.5px solid #e2e8f0;
287
+ }
288
+
289
+ .card-side-label {
290
+ font-size: 7pt;
291
+ font-weight: 700;
292
+ letter-spacing: 0.1em;
293
+ text-transform: uppercase;
294
+ margin-bottom: 6px;
295
+ opacity: 0.65;
296
+ }
297
+ .card-front-face .card-side-label { color: #bfdbfe; }
298
+ .card-back-face .card-side-label { color: #94a3b8; }
299
+
300
+ .card-number-badge {
301
+ position: absolute;
302
+ top: 6px;
303
+ right: 8px;
304
+ font-size: 7pt;
305
+ color: #94a3b8;
306
+ font-weight: 600;
307
+ }
308
+
309
+ .front-text {
310
+ font-size: 11pt;
311
+ font-weight: 600;
312
+ line-height: 1.4;
313
+ color: #ffffff;
314
+ }
315
+ .front-text.long { font-size: 9.5pt; }
316
+ .front-text.xlong { font-size: 8.5pt; }
317
+
318
+ .back-text {
319
+ font-size: 10pt;
320
+ line-height: 1.5;
321
+ color: #1a1a1a;
322
+ white-space: pre-line;
323
+ text-align: left;
324
+ width: 100%;
325
+ }
326
+ .back-text.long { font-size: 9pt; }
327
+ .back-text.xlong { font-size: 8pt; }
328
+
329
+ .fold-hint {
330
+ font-size: 7pt;
331
+ color: #94a3b8;
332
+ text-align: center;
333
+ margin-top: 10px;
334
+ letter-spacing: 0.04em;
335
+ }
336
+
337
+ .page-break { page-break-before: always; }
338
+
339
+ @media print {
340
+ .card-front-face { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
341
+ .card-cell { page-break-inside: avoid; break-inside: avoid; }
342
+ .instructions { display: none; }
343
+ }
344
+ `;
345
+ function sizeClass(text) {
346
+ const len = text.length;
347
+ if (len > 200) return "xlong";
348
+ if (len > 100) return "long";
349
+ return "";
350
+ }
351
+ function renderCutCards(cards, title, s, startIndex = 0) {
352
+ const PAGE_SIZE = 4;
353
+ const pages = [];
354
+ for (let p = 0; p < Math.ceil(cards.length / PAGE_SIZE); p++) {
355
+ const batch = cards.slice(p * PAGE_SIZE, (p + 1) * PAGE_SIZE);
356
+ const cells = batch.map((card, i) => {
357
+ const globalIdx = startIndex + p * PAGE_SIZE + i + 1;
358
+ const frontSize = sizeClass(card.front);
359
+ const backSize = sizeClass(card.back ?? "");
360
+ const backContent = (card.back ?? "").split("\n").map((line) => escapeHtml(line)).join("<br>");
361
+ return `<div class="card-cell">
362
+ <span class="card-number-badge">#${globalIdx}</span>
363
+ <div class="card-inner">
364
+ <div class="card-face card-front-face">
365
+ <div class="card-side-label">Question / Term</div>
366
+ ${faceImageHtml(card, "front", s.showImages)}<div class="front-text ${frontSize}">${escapeHtml(card.front)}</div>
367
+ </div>
368
+ <div class="card-face card-back-face">
369
+ <div class="card-side-label">Answer / Definition</div>
370
+ ${faceImageHtml(card, "back", s.showImages)}<div class="back-text ${backSize}">${backContent}</div>
371
+ </div>
372
+ </div>
373
+ </div>`;
374
+ }).join("\n");
375
+ const isFirst = p === 0;
376
+ const pageClass = isFirst ? "" : ' class="page-break"';
377
+ pages.push(`<div${pageClass}>
378
+ ${isFirst ? `<div class="deck-header">
379
+ <h1 class="deck-title">${escapeHtml(title)}</h1>
380
+ <div class="deck-meta">${cards.length} card${cards.length !== 1 ? "s" : ""} &nbsp;\xB7&nbsp; Cut along dashed lines</div>
381
+ </div>
382
+ <div class="instructions">
383
+ \u2702 Print this page, then cut along the dashed lines. Each cell is one card \u2014 front (blue) on top, answer on the bottom.
384
+ Fold in half for a double-sided card, or cut each half separately for separate front/back cards.
385
+ </div>` : `<div class="deck-meta" style="margin-bottom:10px;">Cards ${startIndex + p * PAGE_SIZE + 1}\u2013${startIndex + Math.min((p + 1) * PAGE_SIZE, cards.length)}</div>`}
386
+ <div class="cards-grid">
387
+ ${cells}
388
+ </div>
389
+ <div class="fold-hint">\u2702 cut along dashed borders \xB7 fold on the center line for double-sided cards</div>
390
+ </div>`);
391
+ }
392
+ return pages.join("\n");
393
+ }
394
+ var BOTH_SIDES_STYLES = `
395
+ ${BASE_STYLES}
396
+
397
+ .cards-grid {
398
+ display: grid;
399
+ grid-template-columns: repeat(2, 1fr);
400
+ gap: 10px;
401
+ }
402
+ .card-cell {
403
+ border: 1.5px solid #e2e8f0;
404
+ border-radius: 8px;
405
+ overflow: hidden;
406
+ page-break-inside: avoid;
407
+ break-inside: avoid;
408
+ background: #fff;
409
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06);
410
+ }
411
+ .card-front {
412
+ background: #1e293b;
413
+ color: #f1f5f9;
414
+ padding: 10px 14px;
415
+ font-weight: 600;
416
+ font-size: 10.5pt;
417
+ line-height: 1.4;
418
+ position: relative;
419
+ }
420
+ .card-front .card-number { font-size: 7.5pt; color: #64748b; margin-bottom: 4px; }
421
+ .card-back {
422
+ padding: 10px 14px;
423
+ font-size: 10pt;
424
+ line-height: 1.5;
425
+ color: #1a1a1a;
426
+ background: #fff;
427
+ border-top: 1px solid #e2e8f0;
428
+ white-space: pre-line;
429
+ }
430
+
431
+ @media print {
432
+ .card-front { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
433
+ }
434
+ `;
435
+ function renderBothSides(cards, title, s, startIndex = 0) {
436
+ const cells = cards.map(
437
+ (card, i) => `<div class="card-cell">
438
+ <div class="card-front">
439
+ <div class="card-number">Card ${startIndex + i + 1}</div>
440
+ ${faceImageHtml(card, "front", s.showImages)}${escapeHtml(card.front)}
441
+ </div>
442
+ <div class="card-back">${faceImageHtml(card, "back", s.showImages)}${escapeHtml(card.back ?? "")}</div>
443
+ </div>`
444
+ ).join("\n");
445
+ return `<div class="deck-header">
446
+ <h1 class="deck-title">${escapeHtml(title)}</h1>
447
+ <div class="deck-meta">${cards.length} card${cards.length !== 1 ? "s" : ""} &nbsp;\xB7&nbsp; Front and back</div>
448
+ </div>
449
+ <div class="cards-grid">
450
+ ${cells}
451
+ </div>`;
452
+ }
453
+ var STUDY_SHEET_STYLES = `
454
+ ${BASE_STYLES}
455
+
456
+ table {
457
+ width: 100%;
458
+ border-collapse: collapse;
459
+ margin-top: 8px;
460
+ font-size: 10pt;
461
+ }
462
+ thead th {
463
+ background: #1e293b;
464
+ color: #f1f5f9;
465
+ padding: 8px 12px;
466
+ text-align: left;
467
+ font-size: 8.5pt;
468
+ font-weight: 700;
469
+ letter-spacing: 0.05em;
470
+ text-transform: uppercase;
471
+ }
472
+ thead th:first-child { width: 38%; border-radius: 0; }
473
+
474
+ tbody tr { page-break-inside: avoid; break-inside: avoid; }
475
+ tbody tr:nth-child(even) td { background: #f8fafc; }
476
+
477
+ tbody td {
478
+ padding: 8px 12px;
479
+ vertical-align: top;
480
+ border-bottom: 1px solid #e2e8f0;
481
+ line-height: 1.5;
482
+ white-space: pre-line;
483
+ }
484
+ tbody td:first-child {
485
+ font-weight: 600;
486
+ border-right: 1px solid #e2e8f0;
487
+ color: #1e3a5f;
488
+ }
489
+ .row-num {
490
+ display: inline-block;
491
+ width: 18px;
492
+ font-size: 8pt;
493
+ color: #94a3b8;
494
+ font-weight: 400;
495
+ margin-right: 4px;
496
+ }
497
+
498
+ @media print {
499
+ thead th { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
500
+ tbody tr:nth-child(even) td { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
501
+ }
502
+ `;
503
+ function renderStudySheet(cards, title, s, startIndex = 0) {
504
+ const rows = cards.map(
505
+ (card, i) => ` <tr>
506
+ <td><span class="row-num">${startIndex + i + 1}.</span>${faceImageHtml(card, "front", s.showImages)}${escapeHtml(card.front)}</td>
507
+ <td>${faceImageHtml(card, "back", s.showImages)}${escapeHtml(card.back ?? "")}</td>
508
+ </tr>`
509
+ ).join("\n");
510
+ return `<div class="deck-header">
511
+ <h1 class="deck-title">${escapeHtml(title)}</h1>
512
+ <div class="deck-meta">${cards.length} card${cards.length !== 1 ? "s" : ""} &nbsp;\xB7&nbsp; Study sheet</div>
513
+ </div>
514
+ <table>
515
+ <thead>
516
+ <tr>
517
+ <th>Question / Term</th>
518
+ <th>Answer / Definition</th>
519
+ </tr>
520
+ </thead>
521
+ <tbody>
522
+ ${rows}
523
+ </tbody>
524
+ </table>`;
525
+ }
526
+ var FRONT_ONLY_STYLES = `
527
+ ${BASE_STYLES}
528
+
529
+ .cards-grid {
530
+ display: grid;
531
+ grid-template-columns: repeat(2, 1fr);
532
+ gap: 10px;
533
+ }
534
+ .card-cell {
535
+ border: 1.5px solid #bfdbfe;
536
+ border-radius: 8px;
537
+ overflow: hidden;
538
+ page-break-inside: avoid;
539
+ break-inside: avoid;
540
+ background: #fff;
541
+ }
542
+ .card-front {
543
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%);
544
+ color: #ffffff;
545
+ padding: 12px 14px;
546
+ font-weight: 600;
547
+ font-size: 10.5pt;
548
+ line-height: 1.4;
549
+ }
550
+ .card-front .card-number { font-size: 7.5pt; color: #bfdbfe; margin-bottom: 4px; }
551
+ .answer-blank {
552
+ height: 52px;
553
+ border-top: 1px dashed #cbd5e1;
554
+ background: #fafafa;
555
+ position: relative;
556
+ }
557
+ .answer-blank::after {
558
+ content: "write your answer here";
559
+ position: absolute;
560
+ bottom: 6px;
561
+ left: 12px;
562
+ font-size: 7pt;
563
+ color: #cbd5e1;
564
+ letter-spacing: 0.04em;
565
+ }
566
+
567
+ @media print {
568
+ .card-front { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
569
+ }
570
+ `;
571
+ function renderFrontOnly(cards, title, s, startIndex = 0) {
572
+ const cells = cards.map(
573
+ (card, i) => `<div class="card-cell">
574
+ <div class="card-front">
575
+ <div class="card-number">Card ${startIndex + i + 1}</div>
576
+ ${faceImageHtml(card, "front", s.showImages)}${escapeHtml(card.front)}
577
+ </div>
578
+ <div class="answer-blank"></div>
579
+ </div>`
580
+ ).join("\n");
581
+ return `<div class="deck-header">
582
+ <h1 class="deck-title">${escapeHtml(title)}</h1>
583
+ <div class="deck-meta">${cards.length} card${cards.length !== 1 ? "s" : ""} &nbsp;\xB7&nbsp; Self-test \u2014 write your answers in the blank space</div>
584
+ </div>
585
+ <div class="cards-grid">
586
+ ${cells}
587
+ </div>`;
588
+ }
589
+ var BACK_ONLY_STYLES = `
590
+ ${BASE_STYLES}
591
+
592
+ .cards-grid {
593
+ display: grid;
594
+ grid-template-columns: repeat(2, 1fr);
595
+ gap: 10px;
596
+ }
597
+ .card-cell {
598
+ border: 1.5px solid #a7f3d0;
599
+ border-radius: 8px;
600
+ overflow: hidden;
601
+ page-break-inside: avoid;
602
+ break-inside: avoid;
603
+ background: #fff;
604
+ }
605
+ .card-back-header {
606
+ background: linear-gradient(135deg, #065f46 0%, #0f766e 100%);
607
+ color: #ecfdf5;
608
+ padding: 10px 14px;
609
+ font-size: 7.5pt;
610
+ font-weight: 700;
611
+ text-transform: uppercase;
612
+ letter-spacing: 0.08em;
613
+ }
614
+ .card-back-content {
615
+ padding: 10px 14px;
616
+ font-size: 10pt;
617
+ line-height: 1.5;
618
+ color: #1a1a1a;
619
+ white-space: pre-line;
620
+ }
621
+
622
+ @media print {
623
+ .card-back-header { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
624
+ }
625
+ `;
626
+ function renderBackOnly(cards, title, s, startIndex = 0) {
627
+ const cells = cards.map(
628
+ (card, i) => `<div class="card-cell">
629
+ <div class="card-back-header">Card ${startIndex + i + 1}</div>
630
+ <div class="card-back-content">${faceImageHtml(card, "back", s.showImages)}${escapeHtml(card.back ?? "")}</div>
631
+ </div>`
632
+ ).join("\n");
633
+ return `<div class="deck-header">
634
+ <h1 class="deck-title">${escapeHtml(title)}</h1>
635
+ <div class="deck-meta">${cards.length} card${cards.length !== 1 ? "s" : ""} &nbsp;\xB7&nbsp; Answers and definitions only</div>
636
+ </div>
637
+ <div class="cards-grid">
638
+ ${cells}
639
+ </div>`;
640
+ }
641
+ var LANDSCAPE_STYLES = `
642
+ @page { size: landscape; margin: 0; }
643
+
644
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
645
+
646
+ html, body {
647
+ width: 11in;
648
+ background: #fff;
649
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
650
+ color: #000;
651
+ /* Screen: scroll to see all pages. Print: locked to one page height per .page. */
652
+ }
653
+
654
+ /* The page wrapper takes the full physical page */
655
+ .page {
656
+ width: 11in;
657
+ height: 8.5in;
658
+ position: relative;
659
+ page-break-after: always;
660
+ break-after: page;
661
+ display: grid;
662
+ grid-template-columns: 1fr 1fr;
663
+ grid-template-rows: 1fr 1fr;
664
+ gap: 0;
665
+ padding: 0.5in;
666
+ /* Thin separator visible on screen between pages */
667
+ border-bottom: 2px solid #e2e8f0;
668
+ }
669
+ .page:last-child {
670
+ page-break-after: auto;
671
+ break-after: auto;
672
+ border-bottom: none;
673
+ }
674
+
675
+ @media print {
676
+ /* Do NOT constrain html/body height \u2014 that clips pages 2+.
677
+ Page sizing is handled by @page + .page { height: 8.5in } + break-after: page. */
678
+ html, body { width: 11in; height: auto; overflow: visible; }
679
+ .page { border-bottom: none; }
680
+ }
681
+
682
+ /* Each quadrant cell \u2014 full 5.5" \xD7 4.25" including its half-gutter */
683
+ .quad {
684
+ display: flex;
685
+ align-items: stretch;
686
+ justify-content: stretch;
687
+ padding: 0 0.25in 0.25in 0; /* right + bottom inner half-gutters */
688
+ }
689
+ /* Top row: add top inner half-gutter via bottom padding of the cell above */
690
+ .quad.top { padding-bottom: 0.25in; padding-top: 0; }
691
+ .quad.bottom { padding-bottom: 0; padding-top: 0.25in; }
692
+ .quad.left { padding-left: 0; padding-right: 0.25in; }
693
+ .quad.right { padding-left: 0.25in; padding-right: 0; }
694
+
695
+ /* The actual card face \u2014 no border, no lines. Cuts are the separation. */
696
+ .card-face {
697
+ flex: 1;
698
+ border: none;
699
+ display: flex;
700
+ flex-direction: column;
701
+ align-items: center;
702
+ justify-content: center;
703
+ padding: 0.18in 0.22in;
704
+ overflow: hidden;
705
+ position: relative;
706
+ }
707
+
708
+ /*
709
+ * Cut tick marks \u2014 4 short marks at the exact center intersection.
710
+ * These sit on the .page element (outside all card faces).
711
+ * Each tick is a 0.18in line. Two ticks per axis: one on each side of center.
712
+ */
713
+
714
+ /* Horizontal axis ticks: sit at vertical midpoint, on the left and right edges */
715
+ .tick-h-left, .tick-h-right {
716
+ position: absolute;
717
+ top: 50%;
718
+ width: 0.18in;
719
+ height: 0;
720
+ border-top: 0.6px solid #bbb;
721
+ pointer-events: none;
722
+ }
723
+ .tick-h-left { left: 0.1in; }
724
+ .tick-h-right { right: 0.1in; }
725
+
726
+ /* Vertical axis ticks: sit at horizontal midpoint, on the top and bottom edges */
727
+ .tick-v-top, .tick-v-bottom {
728
+ position: absolute;
729
+ left: 50%;
730
+ height: 0.18in;
731
+ width: 0;
732
+ border-left: 0.6px solid #bbb;
733
+ pointer-events: none;
734
+ }
735
+ .tick-v-top { top: 0.1in; }
736
+ .tick-v-bottom { bottom: 0.1in; }
737
+
738
+ /* Full dashed cut lines (optional \u2014 only rendered when showCutLines = true) */
739
+ .cut-line-h {
740
+ position: absolute;
741
+ left: 0;
742
+ right: 0;
743
+ top: 50%;
744
+ height: 0;
745
+ border-top: 0.6px dashed #bbb;
746
+ pointer-events: none;
747
+ }
748
+ .cut-line-v {
749
+ position: absolute;
750
+ top: 0;
751
+ bottom: 0;
752
+ left: 50%;
753
+ width: 0;
754
+ border-left: 0.6px dashed #bbb;
755
+ pointer-events: none;
756
+ }
757
+
758
+ /* Card number badge */
759
+ .card-num {
760
+ position: absolute;
761
+ top: 5px;
762
+ right: 7px;
763
+ font-size: 6.5pt;
764
+ color: #999;
765
+ font-weight: 600;
766
+ }
767
+
768
+ /* Side label (FRONT / BACK) */
769
+ .side-label {
770
+ font-size: 6.5pt;
771
+ font-weight: 700;
772
+ letter-spacing: 0.12em;
773
+ text-transform: uppercase;
774
+ color: #888;
775
+ margin-bottom: 8px;
776
+ align-self: flex-start;
777
+ }
778
+
779
+ /* The text container \u2014 fills remaining space, centers content */
780
+ .card-text {
781
+ flex: 1;
782
+ width: 100%;
783
+ display: flex;
784
+ align-items: center;
785
+ justify-content: center;
786
+ text-align: center;
787
+ overflow: hidden;
788
+ }
789
+
790
+ .card-text-inner {
791
+ font-size: 22pt; /* fitText JS shrinks this to fit \u2014 start bigger for short fronts */
792
+ font-weight: 600;
793
+ line-height: 1.3;
794
+ white-space: pre-wrap;
795
+ word-break: break-word;
796
+ text-align: center;
797
+ -webkit-print-color-adjust: exact;
798
+ print-color-adjust: exact;
799
+ }
800
+
801
+ /* Multiline prose back: left-align, slightly smaller start */
802
+ .card-text-inner.multiline {
803
+ text-align: left;
804
+ font-size: 15pt;
805
+ font-weight: 400;
806
+ line-height: 1.45;
807
+ }
808
+
809
+ /* Bullet list back */
810
+ ul.card-text-inner.card-list {
811
+ text-align: left;
812
+ font-size: 15pt;
813
+ font-weight: 400;
814
+ line-height: 1.5;
815
+ padding-left: 1.1em;
816
+ margin: 0;
817
+ list-style-type: disc;
818
+ white-space: normal;
819
+ width: 100%;
820
+ }
821
+ ul.card-text-inner.card-list li {
822
+ margin-bottom: 0.25em;
823
+ }
824
+ ul.card-text-inner.card-list li:last-child {
825
+ margin-bottom: 0;
826
+ }
827
+
828
+ /* Print-only: hide the browser action bar */
829
+ @media print {
830
+ .screen-only { display: none !important; }
831
+ }
832
+ `;
833
+ var FIT_TEXT_SCRIPT = buildFitTextScript([
834
+ ".card-text .card-text-inner",
835
+ ".avery-text-wrap .avery-text"
836
+ ]);
837
+ function isBulletContent(text) {
838
+ const lines = text.split("\n").filter((l) => l.trim().length > 0);
839
+ if (lines.length < 2) return false;
840
+ const bulletLines = lines.filter((l) => /^\s*[-•*]\s/.test(l));
841
+ return bulletLines.length >= lines.length * 0.6;
842
+ }
843
+ function isMultiline(text) {
844
+ return text.includes("\n") || text.length > 120;
845
+ }
846
+ function renderCardText(text, isFront, extraStyle = "") {
847
+ if (isFront) {
848
+ const styleAttr2 = extraStyle ? ` style="${extraStyle}"` : "";
849
+ return {
850
+ html: `<div class="card-text-inner"${styleAttr2}>${escapeHtml(text).replace(/\n/g, "<br>")}</div>`,
851
+ isList: false
852
+ };
853
+ }
854
+ const bullet = isBulletContent(text);
855
+ if (bullet) {
856
+ const lines = text.split("\n");
857
+ const items = lines.map((l) => l.trim()).filter((l) => l.length > 0).map((l) => l.replace(/^[-•*]\s*/, "")).map((l) => `<li>${escapeHtml(l)}</li>`).join("");
858
+ const styleAttr2 = extraStyle ? ` style="${extraStyle}"` : "";
859
+ return {
860
+ html: `<ul class="card-text-inner card-list"${styleAttr2}>${items}</ul>`,
861
+ isList: true
862
+ };
863
+ }
864
+ const ml = isMultiline(text);
865
+ const cls = ml ? "card-text-inner multiline" : "card-text-inner";
866
+ const styleAttr = extraStyle ? ` style="${extraStyle}"` : "";
867
+ return {
868
+ html: `<div class="${cls}"${styleAttr}>${escapeHtml(text).replace(/\n/g, "<br>")}</div>`,
869
+ isList: false
870
+ };
871
+ }
872
+ function buildCardFace(text, cardNum, isFront, s) {
873
+ const numBadge = s.showCardNumber ? `<span class="card-num">#${cardNum}</span>` : "";
874
+ const sideLabel = s.showSideLabel ? `<span class="side-label">${isFront ? "Front" : "Back"}</span>` : "";
875
+ const textColor = !isFront && s.lightBackText ? "color:#777;" : "";
876
+ const mirrorXform = !isFront && s.mirrorBack ? "transform:scaleX(-1);display:block;" : "";
877
+ const innerStyle = textColor + mirrorXform;
878
+ const { html, isList } = renderCardText(text, isFront, innerStyle);
879
+ const cardTextStyle = isList ? ' style="align-items:flex-start;"' : "";
880
+ return `<div class="card-face">
881
+ ${numBadge}
882
+ ${sideLabel}
883
+ <div class="card-text"${cardTextStyle}>
884
+ ${html}
885
+ </div>
886
+ </div>`;
887
+ }
888
+ var QUAD_CLASSES = [
889
+ "quad top left",
890
+ "quad top right",
891
+ "quad bottom left",
892
+ "quad bottom right"
893
+ ];
894
+ function buildPage(cells, s) {
895
+ const ticks = s.showTickMarks ? `<div class="tick-h-left"></div>
896
+ <div class="tick-h-right"></div>
897
+ <div class="tick-v-top"></div>
898
+ <div class="tick-v-bottom"></div>` : "";
899
+ const lines = s.showCutLines ? `<div class="cut-line-h"></div>
900
+ <div class="cut-line-v"></div>` : "";
901
+ return `<div class="page">
902
+ ${ticks}
903
+ ${lines}
904
+ ${cells.map((c, i) => `<div class="${QUAD_CLASSES[i]}">${c}</div>`).join("\n ")}
905
+ </div>`;
906
+ }
907
+ function renderLandscape(cards, title, mode, s, startIndex = 0) {
908
+ const BATCH = 4;
909
+ const pages = [];
910
+ const backPages = [];
911
+ const empty = `<div class="card-face"></div>`;
912
+ for (let p = 0; p < Math.ceil(cards.length / BATCH); p++) {
913
+ const batch = cards.slice(p * BATCH, (p + 1) * BATCH);
914
+ while (batch.length < BATCH) batch.push({ front: "", back: "" });
915
+ const fronts = batch.map(
916
+ (c, i) => c.front ? buildCardFace(c.front, startIndex + p * BATCH + i + 1, true, s) : empty
917
+ );
918
+ const backs = batch.map(
919
+ (c, i) => c.back ? buildCardFace(c.back ?? "", startIndex + p * BATCH + i + 1, false, s) : empty
920
+ );
921
+ if (mode === "duplex") {
922
+ const backsFlipped = [backs[1], backs[0], backs[3], backs[2]];
923
+ pages.push(buildPage(fronts, s));
924
+ pages.push(buildPage(backsFlipped, s));
925
+ } else {
926
+ pages.push(buildPage(fronts, s));
927
+ backPages.push(buildPage(backs, s));
928
+ }
929
+ }
930
+ return [...pages, ...backPages].join("\n");
931
+ }
932
+ function openLandscapeWindow(bodyHtml, title) {
933
+ const html = `<!DOCTYPE html>
934
+ <html lang="en">
935
+ <head>
936
+ <meta charset="UTF-8">
937
+ <title>${escapeHtml(title)}</title>
938
+ <style>${LANDSCAPE_STYLES}</style>
939
+ </head>
940
+ <body>
941
+ <div class="screen-only" style="font-family:sans-serif;font-size:12px;padding:12px 16px;background:#f8fafc;border-bottom:1px solid #e2e8f0;display:flex;gap:10px;align-items:center;">
942
+ <button onclick="window.print()" style="padding:7px 18px;background:#111;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">Print / Save PDF</button>
943
+ <button onclick="window.close()" style="padding:7px 14px;background:#f1f5f9;border:1px solid #cbd5e1;border-radius:6px;font-size:13px;cursor:pointer;">Close</button>
944
+ <span style="color:#64748b;font-size:11px;">Tip: set your printer to <strong>Landscape</strong> with no additional margins</span>
945
+ </div>
946
+ ${bodyHtml}
947
+ ${FIT_TEXT_SCRIPT}
948
+ </body>
949
+ </html>`;
950
+ const win = window.open("", "_blank", "width=1100,height=720,scrollbars=yes");
951
+ if (!win) {
952
+ const blob = new Blob([html], { type: "text/html;charset=utf-8" });
953
+ const url = URL.createObjectURL(blob);
954
+ const a = document.createElement("a");
955
+ a.href = url;
956
+ a.download = `${title.replace(/\s+/g, "-").toLowerCase()}-flashcards.html`;
957
+ document.body.appendChild(a);
958
+ a.click();
959
+ document.body.removeChild(a);
960
+ URL.revokeObjectURL(url);
961
+ return;
962
+ }
963
+ win.document.open();
964
+ win.document.write(html);
965
+ win.document.close();
966
+ }
967
+ var AVERY_5388_STYLES = `
968
+ @page { size: portrait; margin: 0; }
969
+
970
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
971
+
972
+ html, body {
973
+ width: 8.5in;
974
+ background: #fff;
975
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
976
+ color: #000;
977
+ }
978
+
979
+ .avery-page {
980
+ width: 8.5in;
981
+ height: 11in;
982
+ padding: 0.5in 1.75in;
983
+ display: flex;
984
+ flex-direction: column;
985
+ gap: 0.5in;
986
+ page-break-after: always;
987
+ break-after: page;
988
+ border-bottom: 1px solid #e2e8f0; /* screen only separator */
989
+ }
990
+ .avery-page:last-child {
991
+ page-break-after: auto;
992
+ break-after: auto;
993
+ border-bottom: none;
994
+ }
995
+
996
+ /* Each card zone: exactly 5" \xD7 3" \u2014 position:relative so the inner text can be absolutely centered */
997
+ .avery-card {
998
+ width: 5in;
999
+ height: 3in;
1000
+ position: relative;
1001
+ overflow: hidden;
1002
+ flex-shrink: 0;
1003
+ }
1004
+
1005
+ /* Text centering wrapper \u2014 fills the card and centers content in both axes */
1006
+ .avery-text-wrap {
1007
+ position: absolute;
1008
+ inset: 0.2in 0.3in; /* inner padding \u2014 0.2in top/bottom, 0.3in left/right */
1009
+ display: flex;
1010
+ align-items: center;
1011
+ justify-content: center;
1012
+ overflow: hidden;
1013
+ }
1014
+
1015
+ /* Side label (tiny, top-left) */
1016
+ .avery-side-label {
1017
+ position: absolute;
1018
+ top: 6px;
1019
+ left: 8px;
1020
+ font-size: 6pt;
1021
+ font-weight: 700;
1022
+ letter-spacing: 0.1em;
1023
+ text-transform: uppercase;
1024
+ color: #aaa;
1025
+ }
1026
+
1027
+ /* Card number (tiny, top-right) */
1028
+ .avery-card-num {
1029
+ position: absolute;
1030
+ top: 6px;
1031
+ right: 8px;
1032
+ font-size: 6pt;
1033
+ color: #aaa;
1034
+ font-weight: 600;
1035
+ }
1036
+
1037
+ /* The text itself \u2014 starts large, fitText JS shrinks it to fit the wrap container */
1038
+ .avery-text {
1039
+ font-size: 22pt;
1040
+ font-weight: 600;
1041
+ line-height: 1.3;
1042
+ white-space: pre-wrap;
1043
+ word-break: break-word;
1044
+ text-align: center;
1045
+ max-width: 100%;
1046
+ }
1047
+ .avery-text.multiline {
1048
+ font-size: 15pt;
1049
+ font-weight: 400;
1050
+ text-align: left;
1051
+ line-height: 1.45;
1052
+ }
1053
+
1054
+ /* Bullet list back */
1055
+ ul.avery-text.card-list {
1056
+ text-align: left;
1057
+ font-size: 15pt;
1058
+ font-weight: 400;
1059
+ line-height: 1.5;
1060
+ padding-left: 1.1em;
1061
+ margin: 0;
1062
+ list-style-type: disc;
1063
+ white-space: normal;
1064
+ width: 100%;
1065
+ }
1066
+ ul.avery-text.card-list li {
1067
+ margin-bottom: 0.3em;
1068
+ }
1069
+ ul.avery-text.card-list li:last-child {
1070
+ margin-bottom: 0;
1071
+ }
1072
+
1073
+ @media print {
1074
+ /* Ensure gray text isn't converted to black by color management */
1075
+ .avery-text { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
1076
+ }
1077
+
1078
+ @media print {
1079
+ html, body { width: 8.5in; height: auto; overflow: visible; }
1080
+ .avery-page { border-bottom: none; }
1081
+ .screen-only { display: none !important; }
1082
+ }
1083
+ `;
1084
+ function buildAveryCard(text, cardNum, isFront, s) {
1085
+ const numBadge = s.showCardNumber ? `<span class="avery-card-num">#${cardNum}</span>` : "";
1086
+ const sideLabel = s.showSideLabel ? `<span class="avery-side-label">${isFront ? "Front" : "Back"}</span>` : "";
1087
+ const textColor = !isFront && s.lightBackText ? "color:#777;" : "";
1088
+ const mirrorXform = !isFront && s.mirrorBack ? "transform:scaleX(-1);" : "";
1089
+ const innerStyle = textColor + mirrorXform;
1090
+ const isBullet = !isFront && isBulletContent(text);
1091
+ const isML = !isFront && isMultiline(text);
1092
+ const cls = isBullet ? "avery-text card-list" : isML ? "avery-text multiline" : "avery-text";
1093
+ const styleAttr = innerStyle ? ` style="${innerStyle}"` : "";
1094
+ let innerHtml;
1095
+ if (isBullet) {
1096
+ const items = text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).map((l) => l.replace(/^[-•*]\s*/, "")).map((l) => `<li>${escapeHtml(l)}</li>`).join("");
1097
+ innerHtml = `<ul class="${cls}"${styleAttr}>${items}</ul>`;
1098
+ } else {
1099
+ const escaped = escapeHtml(text).replace(/\n/g, "<br>");
1100
+ innerHtml = `<div class="${cls}"${styleAttr}>${escaped}</div>`;
1101
+ }
1102
+ const wrapExtra = isBullet ? ' style="align-items:flex-start;"' : "";
1103
+ return `<div class="avery-card">
1104
+ ${numBadge}
1105
+ ${sideLabel}
1106
+ <div class="avery-text-wrap"${wrapExtra}>
1107
+ ${innerHtml}
1108
+ </div>
1109
+ </div>`;
1110
+ }
1111
+ function renderAvery5388(cards, title, s, startIndex = 0) {
1112
+ const BATCH = 3;
1113
+ const pages = [];
1114
+ for (let p = 0; p < Math.ceil(cards.length / BATCH); p++) {
1115
+ const batch = cards.slice(p * BATCH, (p + 1) * BATCH);
1116
+ const frontCells = batch.map(
1117
+ (c, i) => buildAveryCard(c.front, startIndex + p * BATCH + i + 1, true, s)
1118
+ ).join("\n");
1119
+ const backCells = batch.map(
1120
+ (c, i) => buildAveryCard(c.back ?? "", startIndex + p * BATCH + i + 1, false, s)
1121
+ ).join("\n");
1122
+ const padCount = BATCH - batch.length;
1123
+ const padCells = Array(padCount).fill(`<div class="avery-card"></div>`).join("\n");
1124
+ pages.push(`<div class="avery-page">${frontCells}${padCells}</div>`);
1125
+ pages.push(`<div class="avery-page">${backCells}${padCells}</div>`);
1126
+ }
1127
+ return pages.join("\n");
1128
+ }
1129
+ function openAveryWindow(bodyHtml, title) {
1130
+ const html = `<!DOCTYPE html>
1131
+ <html lang="en">
1132
+ <head>
1133
+ <meta charset="UTF-8">
1134
+ <title>${escapeHtml(title)} \u2014 Avery 5388</title>
1135
+ <style>${AVERY_5388_STYLES}</style>
1136
+ </head>
1137
+ <body>
1138
+ <div class="screen-only" style="font-family:sans-serif;font-size:12px;padding:10px 16px;background:#f8fafc;border-bottom:1px solid #e2e8f0;display:flex;gap:10px;align-items:center;">
1139
+ <button onclick="window.print()" style="padding:7px 18px;background:#111;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">Print / Save PDF</button>
1140
+ <button onclick="window.close()" style="padding:7px 14px;background:#f1f5f9;border:1px solid #cbd5e1;border-radius:6px;font-size:13px;cursor:pointer;">Close</button>
1141
+ <span style="color:#64748b;font-size:11px;">Avery 5388 \xB7 3\xD75" index cards \xB7 3 per page \xB7 set printer to <strong>Portrait, no margins</strong></span>
1142
+ </div>
1143
+ ${bodyHtml}
1144
+ ${FIT_TEXT_SCRIPT}
1145
+ </body>
1146
+ </html>`;
1147
+ const win = window.open("", "_blank", "width=900,height=720,scrollbars=yes");
1148
+ if (!win) {
1149
+ const blob = new Blob([html], { type: "text/html;charset=utf-8" });
1150
+ const url = URL.createObjectURL(blob);
1151
+ const a = document.createElement("a");
1152
+ a.href = url;
1153
+ a.download = `${title.replace(/\s+/g, "-").toLowerCase()}-avery5388.html`;
1154
+ document.body.appendChild(a);
1155
+ a.click();
1156
+ document.body.removeChild(a);
1157
+ URL.revokeObjectURL(url);
1158
+ return;
1159
+ }
1160
+ win.document.open();
1161
+ win.document.write(html);
1162
+ win.document.close();
1163
+ }
1164
+ var PORTRAIT_6UP_STYLES = `
1165
+ @page { size: portrait; margin: 0; }
1166
+
1167
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
1168
+
1169
+ html, body {
1170
+ width: 8.5in;
1171
+ background: #fff;
1172
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
1173
+ color: #000;
1174
+ }
1175
+
1176
+ .page-6up {
1177
+ width: 8.5in;
1178
+ height: 11in;
1179
+ position: relative;
1180
+ page-break-after: always;
1181
+ break-after: page;
1182
+ display: grid;
1183
+ grid-template-columns: 1fr 1fr;
1184
+ grid-template-rows: 1fr 1fr 1fr;
1185
+ gap: 0;
1186
+ padding: 0;
1187
+ border-bottom: 2px solid #e2e8f0;
1188
+ }
1189
+ .page-6up:last-child {
1190
+ page-break-after: auto;
1191
+ break-after: auto;
1192
+ border-bottom: none;
1193
+ }
1194
+
1195
+ @media print {
1196
+ html, body { width: 8.5in; height: auto; overflow: visible; }
1197
+ .page-6up { border-bottom: none; }
1198
+ }
1199
+
1200
+ /* Each of the 6 cells \u2014 clean 4.25" \xD7 3.667" zone split by the cuts */
1201
+ .cell-6 {
1202
+ display: flex;
1203
+ align-items: stretch;
1204
+ justify-content: stretch;
1205
+ padding: 0.2in 0.22in;
1206
+ overflow: hidden;
1207
+ }
1208
+
1209
+ /* The actual card face \u2014 no border. Cuts are the separation. */
1210
+ .card-face {
1211
+ flex: 1;
1212
+ border: none;
1213
+ display: flex;
1214
+ flex-direction: column;
1215
+ align-items: center;
1216
+ justify-content: center;
1217
+ padding: 0.1in 0.12in;
1218
+ overflow: hidden;
1219
+ position: relative;
1220
+ }
1221
+
1222
+ /* Cut tick marks \u2014 short marks at each cut intersection with the page edge */
1223
+ .tick-v-top, .tick-v-bottom {
1224
+ position: absolute;
1225
+ left: 50%;
1226
+ height: 0.18in;
1227
+ width: 0;
1228
+ border-left: 0.6px solid #bbb;
1229
+ pointer-events: none;
1230
+ }
1231
+ .tick-v-top { top: 0.1in; }
1232
+ .tick-v-bottom { bottom: 0.1in; }
1233
+
1234
+ .tick-h-1-left, .tick-h-1-right,
1235
+ .tick-h-2-left, .tick-h-2-right {
1236
+ position: absolute;
1237
+ width: 0.18in;
1238
+ height: 0;
1239
+ border-top: 0.6px solid #bbb;
1240
+ pointer-events: none;
1241
+ }
1242
+ .tick-h-1-left { top: 33.333%; left: 0.1in; }
1243
+ .tick-h-1-right { top: 33.333%; right: 0.1in; }
1244
+ .tick-h-2-left { top: 66.667%; left: 0.1in; }
1245
+ .tick-h-2-right { top: 66.667%; right: 0.1in; }
1246
+
1247
+ /* Full dashed cut lines (optional) */
1248
+ .cut-line-v {
1249
+ position: absolute;
1250
+ top: 0; bottom: 0;
1251
+ left: 50%;
1252
+ width: 0;
1253
+ border-left: 0.6px dashed #bbb;
1254
+ pointer-events: none;
1255
+ }
1256
+ .cut-line-h-1, .cut-line-h-2 {
1257
+ position: absolute;
1258
+ left: 0; right: 0;
1259
+ height: 0;
1260
+ border-top: 0.6px dashed #bbb;
1261
+ pointer-events: none;
1262
+ }
1263
+ .cut-line-h-1 { top: 33.333%; }
1264
+ .cut-line-h-2 { top: 66.667%; }
1265
+
1266
+ /* Card number badge */
1267
+ .card-num {
1268
+ position: absolute;
1269
+ top: 5px;
1270
+ right: 7px;
1271
+ font-size: 6.5pt;
1272
+ color: #999;
1273
+ font-weight: 600;
1274
+ }
1275
+
1276
+ /* Side label (FRONT / BACK) */
1277
+ .side-label {
1278
+ font-size: 6.5pt;
1279
+ font-weight: 700;
1280
+ letter-spacing: 0.12em;
1281
+ text-transform: uppercase;
1282
+ color: #888;
1283
+ margin-bottom: 6px;
1284
+ align-self: flex-start;
1285
+ }
1286
+
1287
+ /* Centered text container \u2014 fills the card, both axes centered */
1288
+ .card-text {
1289
+ flex: 1;
1290
+ width: 100%;
1291
+ display: flex;
1292
+ align-items: center;
1293
+ justify-content: center;
1294
+ text-align: center;
1295
+ overflow: hidden;
1296
+ }
1297
+
1298
+ .card-text-inner {
1299
+ font-size: 18pt; /* fitText JS shrinks this to fit */
1300
+ font-weight: 600;
1301
+ line-height: 1.3;
1302
+ white-space: pre-wrap;
1303
+ word-break: break-word;
1304
+ text-align: center;
1305
+ -webkit-print-color-adjust: exact;
1306
+ print-color-adjust: exact;
1307
+ }
1308
+
1309
+ .card-text-inner.multiline {
1310
+ text-align: left;
1311
+ font-size: 13pt;
1312
+ font-weight: 400;
1313
+ line-height: 1.45;
1314
+ }
1315
+
1316
+ ul.card-text-inner.card-list {
1317
+ text-align: left;
1318
+ font-size: 13pt;
1319
+ font-weight: 400;
1320
+ line-height: 1.5;
1321
+ padding-left: 1.1em;
1322
+ margin: 0;
1323
+ list-style-type: disc;
1324
+ white-space: normal;
1325
+ width: 100%;
1326
+ }
1327
+ ul.card-text-inner.card-list li { margin-bottom: 0.2em; }
1328
+ ul.card-text-inner.card-list li:last-child { margin-bottom: 0; }
1329
+
1330
+ @media print {
1331
+ .screen-only { display: none !important; }
1332
+ }
1333
+ `;
1334
+ function buildPage6Up(cells, s) {
1335
+ const ticks = s.showTickMarks ? `<div class="tick-v-top"></div>
1336
+ <div class="tick-v-bottom"></div>
1337
+ <div class="tick-h-1-left"></div>
1338
+ <div class="tick-h-1-right"></div>
1339
+ <div class="tick-h-2-left"></div>
1340
+ <div class="tick-h-2-right"></div>` : "";
1341
+ const lines = s.showCutLines ? `<div class="cut-line-v"></div>
1342
+ <div class="cut-line-h-1"></div>
1343
+ <div class="cut-line-h-2"></div>` : "";
1344
+ return `<div class="page-6up">
1345
+ ${ticks}
1346
+ ${lines}
1347
+ ${cells.map((c) => `<div class="cell-6">${c}</div>`).join("\n ")}
1348
+ </div>`;
1349
+ }
1350
+ function renderPortrait6Up(cards, title, mode, s, startIndex = 0) {
1351
+ const BATCH = 6;
1352
+ const pages = [];
1353
+ const backPages = [];
1354
+ const empty = `<div class="card-face"></div>`;
1355
+ for (let p = 0; p < Math.ceil(cards.length / BATCH); p++) {
1356
+ const batch = cards.slice(p * BATCH, (p + 1) * BATCH);
1357
+ while (batch.length < BATCH) batch.push({ front: "", back: "" });
1358
+ const fronts = batch.map(
1359
+ (c, i) => c.front ? buildCardFace(c.front, startIndex + p * BATCH + i + 1, true, s) : empty
1360
+ );
1361
+ const backs = batch.map(
1362
+ (c, i) => c.back ? buildCardFace(c.back ?? "", startIndex + p * BATCH + i + 1, false, s) : empty
1363
+ );
1364
+ if (mode === "duplex") {
1365
+ const backsFlipped = [
1366
+ backs[1],
1367
+ backs[0],
1368
+ backs[3],
1369
+ backs[2],
1370
+ backs[5],
1371
+ backs[4]
1372
+ ];
1373
+ pages.push(buildPage6Up(fronts, s));
1374
+ pages.push(buildPage6Up(backsFlipped, s));
1375
+ } else {
1376
+ pages.push(buildPage6Up(fronts, s));
1377
+ backPages.push(buildPage6Up(backs, s));
1378
+ }
1379
+ }
1380
+ return [...pages, ...backPages].join("\n");
1381
+ }
1382
+ function openPortrait6UpWindow(bodyHtml, title) {
1383
+ const html = `<!DOCTYPE html>
1384
+ <html lang="en">
1385
+ <head>
1386
+ <meta charset="UTF-8">
1387
+ <title>${escapeHtml(title)} \u2014 6 per page</title>
1388
+ <style>${PORTRAIT_6UP_STYLES}</style>
1389
+ </head>
1390
+ <body>
1391
+ <div class="screen-only" style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#fff;border-bottom:1px solid #e2e8f0;padding:14px 20px;">
1392
+ <div style="display:flex;gap:10px;align-items:center;margin-bottom:10px;">
1393
+ <button onclick="window.print()" style="padding:8px 20px;background:#111;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;">Print / Save PDF</button>
1394
+ <button onclick="window.close()" style="padding:8px 14px;background:#f1f5f9;border:1px solid #cbd5e1;border-radius:6px;font-size:13px;cursor:pointer;">Close</button>
1395
+ <span style="color:#111;font-size:12px;font-weight:600;margin-left:4px;">6 flashcards per page \xB7 cut into 6</span>
1396
+ </div>
1397
+ <div style="background:#fef3c7;border:1px solid #fbbf24;border-radius:6px;padding:10px 14px;font-size:12px;line-height:1.55;color:#78350f;max-width:720px;">
1398
+ <div style="font-weight:700;margin-bottom:4px;color:#92400e;">\u26A0 Printer settings for duplex alignment</div>
1399
+ <strong>- Page Size:</strong> Letter (8.5" \xD7 11")<br>
1400
+ <strong>- Orientation:</strong> Portrait<br>
1401
+ <strong>- Two-sided (Duplex):</strong> On<br>
1402
+ <strong>- Flip on:</strong> Long edge<br>
1403
+ <strong>- Pages per sheet:</strong> 1<br>
1404
+ <strong>- Scale:</strong> 100% / Actual size<br>
1405
+ <strong>- Margins:</strong> None<br>
1406
+ </div>
1407
+ </div>
1408
+ ${bodyHtml}
1409
+ ${FIT_TEXT_SCRIPT}
1410
+ </body>
1411
+ </html>`;
1412
+ const win = window.open("", "_blank", "width=900,height=720,scrollbars=yes");
1413
+ if (!win) {
1414
+ const blob = new Blob([html], { type: "text/html;charset=utf-8" });
1415
+ const url = URL.createObjectURL(blob);
1416
+ const a = document.createElement("a");
1417
+ a.href = url;
1418
+ a.download = `${title.replace(/\s+/g, "-").toLowerCase()}-flashcards-6up.html`;
1419
+ document.body.appendChild(a);
1420
+ a.click();
1421
+ document.body.removeChild(a);
1422
+ URL.revokeObjectURL(url);
1423
+ return;
1424
+ }
1425
+ win.document.open();
1426
+ win.document.write(html);
1427
+ win.document.close();
1428
+ }
1429
+ function deriveTitle(data) {
1430
+ if (data && typeof data === "object" && "title" in data) {
1431
+ const t = data.title;
1432
+ if (typeof t === "string" && t.trim()) return t.trim();
1433
+ }
1434
+ return "Flashcards";
1435
+ }
1436
+ var flashcardsPrinter = {
1437
+ label: "Print flashcards",
1438
+ variants: [
1439
+ {
1440
+ id: "portrait-6up-duplex",
1441
+ label: "Portrait 6-up \u2014 duplex (recommended)",
1442
+ description: "6 cards per page (2 columns \xD7 3 rows), portrait. Split down the middle and into thirds. Fronts and backs on alternating pages \u2014 backs are mirrored so they align perfectly when duplex-printed (flip on long edge) and cut into 6"
1443
+ },
1444
+ {
1445
+ id: "portrait-6up-stacked",
1446
+ label: "Portrait 6-up \u2014 all fronts then all backs",
1447
+ description: "6 cards per page (2 columns \xD7 3 rows), portrait. All front pages first, then all back pages \u2014 cut all sheets together for a matching deck"
1448
+ },
1449
+ {
1450
+ id: "landscape-duplex",
1451
+ label: "Landscape cut sheet \u2014 duplex (double-sided)",
1452
+ description: "4 cards per page, landscape. Fronts and backs on alternating pages \u2014 backs are mirrored so they align perfectly when you flip and cut"
1453
+ },
1454
+ {
1455
+ id: "landscape-stacked",
1456
+ label: "Landscape cut sheet \u2014 all fronts then all backs",
1457
+ description: "4 cards per page, landscape. All front pages first, then all back pages \u2014 cut all sheets together for a matching deck"
1458
+ },
1459
+ {
1460
+ id: "avery-5388",
1461
+ label: 'Avery 5388 \u2014 3\xD75" index cards (3 per page)',
1462
+ description: "Prints to Avery 5388 card stock. 3 cards per page, portrait. Front page then back page per set of 3 \u2014 load the sheet twice to print both sides"
1463
+ },
1464
+ {
1465
+ id: "cut-cards",
1466
+ label: "Cut-out cards (portrait)",
1467
+ description: "4 per page with cut lines \u2014 front and back shown together per card"
1468
+ },
1469
+ {
1470
+ id: "both-sides",
1471
+ label: "Both sides per card",
1472
+ description: "Front and back shown together \u2014 best for review or sharing"
1473
+ },
1474
+ {
1475
+ id: "study-sheet",
1476
+ label: "Study sheet (table)",
1477
+ description: "Compact two-column table \u2014 all cards on one or two pages"
1478
+ },
1479
+ {
1480
+ id: "front-only",
1481
+ label: "Questions only (self-test)",
1482
+ description: "Front side with blank space to write answers \u2014 print and quiz yourself"
1483
+ },
1484
+ {
1485
+ id: "back-only",
1486
+ label: "Answers only",
1487
+ description: "Definitions / answers without the questions"
1488
+ }
1489
+ ],
1490
+ settings: [
1491
+ {
1492
+ type: "range",
1493
+ id: "cardRange",
1494
+ label: "Cards to print",
1495
+ description: "Leave blank to print the whole deck",
1496
+ fromId: "fromCard",
1497
+ toId: "toCard",
1498
+ defaultFrom: 0,
1499
+ defaultTo: 0,
1500
+ min: 1,
1501
+ fromPlaceholder: "1",
1502
+ toPlaceholder: "end"
1503
+ },
1504
+ {
1505
+ type: "boolean",
1506
+ id: "showSideLabel",
1507
+ label: "Show side label",
1508
+ description: 'Prints "Front" or "Back" in the corner of each card',
1509
+ defaultValue: false,
1510
+ appliesTo: [
1511
+ "landscape-duplex",
1512
+ "landscape-stacked",
1513
+ "portrait-6up-duplex",
1514
+ "portrait-6up-stacked",
1515
+ "avery-5388"
1516
+ ]
1517
+ },
1518
+ {
1519
+ type: "boolean",
1520
+ id: "showCardNumber",
1521
+ label: "Show card number",
1522
+ description: "Prints #1, #2 \u2026 in the corner of each card",
1523
+ defaultValue: false,
1524
+ appliesTo: [
1525
+ "landscape-duplex",
1526
+ "landscape-stacked",
1527
+ "portrait-6up-duplex",
1528
+ "portrait-6up-stacked",
1529
+ "avery-5388"
1530
+ ]
1531
+ },
1532
+ {
1533
+ type: "boolean",
1534
+ id: "showTickMarks",
1535
+ label: "Show cut tick marks",
1536
+ description: "Small marks at each cut intersection \u2014 where to make each cut",
1537
+ defaultValue: false,
1538
+ appliesTo: [
1539
+ "landscape-duplex",
1540
+ "landscape-stacked",
1541
+ "portrait-6up-duplex",
1542
+ "portrait-6up-stacked"
1543
+ ]
1544
+ },
1545
+ {
1546
+ type: "boolean",
1547
+ id: "showCutLines",
1548
+ label: "Show full cut lines",
1549
+ description: "Dashed lines all the way across the page \u2014 helps if you don't have a ruler",
1550
+ defaultValue: false,
1551
+ appliesTo: [
1552
+ "landscape-duplex",
1553
+ "landscape-stacked",
1554
+ "portrait-6up-duplex",
1555
+ "portrait-6up-stacked"
1556
+ ]
1557
+ },
1558
+ {
1559
+ type: "boolean",
1560
+ id: "lightBackText",
1561
+ label: "Light back text (reduce show-through)",
1562
+ description: "Prints the back side in dark gray instead of black \u2014 ink is lighter and less visible through the paper",
1563
+ defaultValue: false,
1564
+ appliesTo: [
1565
+ "landscape-duplex",
1566
+ "landscape-stacked",
1567
+ "portrait-6up-duplex",
1568
+ "portrait-6up-stacked",
1569
+ "avery-5388"
1570
+ ]
1571
+ },
1572
+ {
1573
+ type: "boolean",
1574
+ id: "mirrorBack",
1575
+ label: "Mirror back text (best show-through prevention)",
1576
+ description: "Flips back text horizontally \u2014 any ink that bleeds through reads backwards and is unrecognizable from the front",
1577
+ defaultValue: false,
1578
+ appliesTo: [
1579
+ "landscape-duplex",
1580
+ "landscape-stacked",
1581
+ "portrait-6up-duplex",
1582
+ "portrait-6up-stacked",
1583
+ "avery-5388"
1584
+ ]
1585
+ },
1586
+ {
1587
+ type: "boolean",
1588
+ id: "showImages",
1589
+ label: "Print face images",
1590
+ description: "Includes each card's front/back image when it has one. The fixed-geometry formats (landscape, 6-up, Avery) are text-only by design",
1591
+ defaultValue: true,
1592
+ appliesTo: [
1593
+ "cut-cards",
1594
+ "both-sides",
1595
+ "study-sheet",
1596
+ "front-only",
1597
+ "back-only"
1598
+ ]
1599
+ }
1600
+ ],
1601
+ print(data, variantId = "portrait-6up-duplex", rawSettings) {
1602
+ const allCards = extractCards(data);
1603
+ const title = deriveTitle(data);
1604
+ const s = resolveSettings(rawSettings);
1605
+ if (allCards.length === 0) {
1606
+ openPrintWindow(
1607
+ buildPrintDocument(
1608
+ "<p>No flashcard data available to print.</p>",
1609
+ title,
1610
+ BASE_STYLES
1611
+ ),
1612
+ "flashcards"
1613
+ );
1614
+ return;
1615
+ }
1616
+ const rawFrom = typeof rawSettings?.fromCard === "number" ? rawSettings.fromCard : 0;
1617
+ const rawTo = typeof rawSettings?.toCard === "number" ? rawSettings.toCard : 0;
1618
+ const rangeStart = rawFrom > 0 ? Math.min(rawFrom, allCards.length) : 1;
1619
+ const rangeEnd = rawTo > 0 ? Math.min(rawTo, allCards.length) : allCards.length;
1620
+ const [fromIdx, toIdx] = rangeStart <= rangeEnd ? [rangeStart, rangeEnd] : [rangeEnd, rangeStart];
1621
+ const startIndex = fromIdx - 1;
1622
+ const cards = allCards.slice(startIndex, toIdx);
1623
+ if (variantId === "landscape-duplex") {
1624
+ openLandscapeWindow(
1625
+ renderLandscape(cards, title, "duplex", s, startIndex),
1626
+ title
1627
+ );
1628
+ return;
1629
+ }
1630
+ if (variantId === "landscape-stacked") {
1631
+ openLandscapeWindow(
1632
+ renderLandscape(cards, title, "stacked", s, startIndex),
1633
+ title
1634
+ );
1635
+ return;
1636
+ }
1637
+ if (variantId === "portrait-6up-duplex") {
1638
+ openPortrait6UpWindow(
1639
+ renderPortrait6Up(cards, title, "duplex", s, startIndex),
1640
+ title
1641
+ );
1642
+ return;
1643
+ }
1644
+ if (variantId === "portrait-6up-stacked") {
1645
+ openPortrait6UpWindow(
1646
+ renderPortrait6Up(cards, title, "stacked", s, startIndex),
1647
+ title
1648
+ );
1649
+ return;
1650
+ }
1651
+ if (variantId === "avery-5388") {
1652
+ openAveryWindow(renderAvery5388(cards, title, s, startIndex), title);
1653
+ return;
1654
+ }
1655
+ let bodyHtml;
1656
+ let styles;
1657
+ switch (variantId) {
1658
+ case "cut-cards":
1659
+ bodyHtml = renderCutCards(cards, title, s, startIndex);
1660
+ styles = CUT_CARD_STYLES;
1661
+ break;
1662
+ case "front-only":
1663
+ bodyHtml = renderFrontOnly(cards, title, s, startIndex);
1664
+ styles = FRONT_ONLY_STYLES;
1665
+ break;
1666
+ case "back-only":
1667
+ bodyHtml = renderBackOnly(cards, title, s, startIndex);
1668
+ styles = BACK_ONLY_STYLES;
1669
+ break;
1670
+ case "study-sheet":
1671
+ bodyHtml = renderStudySheet(cards, title, s, startIndex);
1672
+ styles = STUDY_SHEET_STYLES;
1673
+ break;
1674
+ case "both-sides":
1675
+ default:
1676
+ bodyHtml = renderBothSides(cards, title, s, startIndex);
1677
+ styles = BOTH_SIDES_STYLES;
1678
+ break;
1679
+ }
1680
+ openPrintWindow(buildPrintDocument(bodyHtml, title, styles), "flashcards");
1681
+ }
1682
+ };
1683
+ export {
1684
+ flashcardsPrinter
1685
+ };
1686
+ //# sourceMappingURL=flashcards.js.map