resuml 1.14.2 → 1.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resuml",
3
- "version": "1.14.2",
3
+ "version": "1.15.0",
4
4
  "description": "Generate JSON resumes from YAML with theme support",
5
5
  "type": "module",
6
6
  "main": "./dist/api.js",
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Render a small PNG thumbnail of every theme's `.snapshot.html` so the
4
+ * theme picker can show visual previews instead of "Stackoverfl…" ×5.
5
+ *
6
+ * For each theme in docs/themes/manifest.json that has hasSnapshot=true,
7
+ * open the snapshot in Playwright at 1200×1600 (full page), then
8
+ * screenshot-scale to ~320×420. Output to docs/themes/<name>.thumb.jpg.
9
+ *
10
+ * Requires the dev server to be running (defaults to :3010) so relative
11
+ * URLs inside the snapshot (fonts, CSS) resolve correctly.
12
+ *
13
+ * Usage:
14
+ * node scripts/render-theme-thumbs.mjs
15
+ * node scripts/render-theme-thumbs.mjs --only stackoverflow,even
16
+ * node scripts/render-theme-thumbs.mjs --force # re-render existing
17
+ */
18
+
19
+ import { readFileSync, existsSync, statSync } from 'node:fs';
20
+ import { resolve, dirname } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { chromium } from 'playwright';
23
+
24
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ const ROOT = resolve(__dirname, '..');
26
+ const THEMES_DIR = resolve(ROOT, 'docs/themes');
27
+ const MANIFEST = resolve(THEMES_DIR, 'manifest.json');
28
+ const DEV_URL = 'http://localhost:3010';
29
+
30
+ const args = process.argv.slice(2);
31
+ const onlyArg = args.find((a) => a.startsWith('--only='));
32
+ const only = onlyArg ? new Set(onlyArg.split('=')[1].split(',')) : null;
33
+ const force = args.includes('--force');
34
+
35
+ function pickTargets() {
36
+ const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'));
37
+ const out = [];
38
+ for (const t of manifest) {
39
+ if (only && !only.has(t.name)) continue;
40
+ if (!t.hasSnapshot) continue;
41
+ const thumbPath = resolve(THEMES_DIR, `${t.name}.thumb.jpg`);
42
+ if (!force && existsSync(thumbPath)) {
43
+ // Skip if thumb is newer than the snapshot
44
+ const snapMtime = statSync(resolve(THEMES_DIR, `${t.name}.snapshot.html`)).mtimeMs;
45
+ const thumbMtime = statSync(thumbPath).mtimeMs;
46
+ if (thumbMtime >= snapMtime) continue;
47
+ }
48
+ out.push(t.name);
49
+ }
50
+ return out;
51
+ }
52
+
53
+ async function probe() {
54
+ try {
55
+ const r = await fetch(DEV_URL);
56
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
57
+ } catch {
58
+ console.error(`Dev server not responding on ${DEV_URL}. Start it with:`);
59
+ console.error(' node scripts/dev-server.js --port 3010');
60
+ process.exit(1);
61
+ }
62
+ }
63
+
64
+ async function main() {
65
+ await probe();
66
+ const targets = pickTargets();
67
+ if (targets.length === 0) {
68
+ console.log('Nothing to render (use --force to regenerate existing).');
69
+ return;
70
+ }
71
+ console.log(`Rendering ${targets.length} thumbnails…`);
72
+
73
+ const browser = await chromium.launch();
74
+ // Render at ~2x the display size (display ≈ 240×320 in the picker grid)
75
+ // so thumbnails still look crisp on retina. JPEG keeps repo size down
76
+ // compared to PNG — ~15–25 KB per image vs 150+ for PNG.
77
+ const ctx = await browser.newContext({
78
+ viewport: { width: 480, height: 640 },
79
+ deviceScaleFactor: 1,
80
+ });
81
+ const page = await ctx.newPage();
82
+ page.on('pageerror', () => {});
83
+ page.on('console', () => {});
84
+
85
+ let done = 0;
86
+ let failed = 0;
87
+ for (const name of targets) {
88
+ try {
89
+ await page.goto(`${DEV_URL}/themes/${name}.snapshot.html`, {
90
+ waitUntil: 'networkidle',
91
+ timeout: 15_000,
92
+ });
93
+ // Small wait so webfonts/CSS transitions settle.
94
+ await page.waitForTimeout(400);
95
+ // Clamp to the hero portion — long resumes lose legibility when
96
+ // the whole page is scaled down to thumb size.
97
+ await page.screenshot({
98
+ path: resolve(THEMES_DIR, `${name}.thumb.jpg`),
99
+ clip: { x: 0, y: 0, width: 480, height: 640 },
100
+ type: 'jpeg',
101
+ quality: 72,
102
+ });
103
+ done++;
104
+ if (done % 20 === 0) process.stdout.write(` ${done}/${targets.length}\r`);
105
+ } catch (e) {
106
+ failed++;
107
+ console.log(` ⚠ ${name}: ${e.message?.slice(0, 80) ?? 'failed'}`);
108
+ }
109
+ }
110
+ await browser.close();
111
+ console.log(`\n✅ Rendered ${done}/${targets.length} thumbnails (${failed} failed)`);
112
+ }
113
+
114
+ main().catch((e) => {
115
+ console.error(e);
116
+ process.exit(1);
117
+ });