fuzzyworld

when-am-i / commit

Add README screenshots via a dedicated cropped-screenshot script

76ecf55eac6f5e6d2317216330a813b7acbf0a7a · Pavel Safronov · 2026-06-20 22:59:05 -0700 · parents c571572f1347

Add README screenshots via a dedicated cropped-screenshot script

Full-page app screenshots are too busy for a README hero image, so
screenshot-readme.mts captures a narrow landing shot plus an example state
clipped to just the map+answers column (skipping the header/intro text).
Also fixes a stale "default 1700" comment in generate-screenshots.mts left
over from the earlier landing-page date change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
 README.md                         |  14 ++++++++++
 package.json                      |   1 +
 screenshots/readme/01-landing.png | Bin 0 -> 223588 bytes
 screenshots/readme/02-example.png | Bin 0 -> 1051464 bytes
 scripts/generate-screenshots.mts  |   2 +-
 scripts/screenshot-readme.mts     |  57 ++++++++++++++++++++++++++++++++++++++
 6 files changed, 73 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 8b3846e..68b0932 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,13 @@
 
 Imagine you're a time traveler who ends up in a random place and time in history. What does a local say when you ask, "When am I?" This project is a fun and educational React app that lets you explore different calendar systems around the world by selecting a location on an interactive map and choosing any year in history. It then shows you the local calendar year and system for that place and time.
 
+![Landing page: a map and date picker, no spot chosen yet](screenshots/readme/01-landing.png)
+
+Pin a spot and a date, and see every valid answer a local could give — including
+when more than one calendar applies at once:
+
+![Boston, 1650: both the colonial Julian calendar and the Massachusett lunar reckoning shown as valid answers](screenshots/readme/02-example.png)
+
 ## Features
 
 - Interactive world map for selecting a location
@@ -122,6 +129,7 @@ running dev server or start one automatically.
 npm run screenshot                                   # app states → screenshots/app/
 npm run screenshot:calendar -- src/data/calendars/massachusett.json   # one calendar → screenshots/calendars/<file>/
 npm run screenshot:calendars                         # regenerate ALL calendars → screenshots/calendars/
+npm run screenshot:readme                            # the two README images → screenshots/readme/
 ```
 
 `screenshot:calendar` pins a point inside the region and a representative date
@@ -130,6 +138,12 @@ in each era, capturing the answer panel and shaded map per era.
 (reusing one dev server + browser) — run it after adding or editing calendar
 data so the checked-in screenshots stay current.
 
+`screenshot:readme` regenerates the two images embedded above: a full-viewport
+landing shot, and an example state cropped to just the map+answers column
+(skipping the header/intro, which a README image doesn't need) — narrower and
+more deliberately cropped than the full-page shots above, since a hero image
+needs to read at a glance.
+
 ## Reporting issues
 
 The app footer links to the [source repository](https://gitlab.com/PavelSafronov/when-am-i)
diff --git a/package.json b/package.json
index afbeb5e..5093ff0 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
         "test": "vitest run",
         "test:watch": "vitest",
         "screenshot": "tsx scripts/generate-screenshots.mts",
+        "screenshot:readme": "tsx scripts/screenshot-readme.mts",
         "screenshot:calendar": "tsx scripts/screenshot-calendar.mts",
         "screenshot:calendars": "tsx scripts/screenshot-calendars.mts",
         "gen:badges": "tsx scripts/gen-badges.mts",
diff --git a/screenshots/readme/01-landing.png b/screenshots/readme/01-landing.png
new file mode 100644
index 0000000..22f7dfd
Binary files /dev/null and b/screenshots/readme/01-landing.png differ
diff --git a/screenshots/readme/02-example.png b/screenshots/readme/02-example.png
new file mode 100644
index 0000000..7ecb944
Binary files /dev/null and b/screenshots/readme/02-example.png differ
diff --git a/scripts/generate-screenshots.mts b/scripts/generate-screenshots.mts
index 787a27e..40e3698 100644
--- a/scripts/generate-screenshots.mts
+++ b/scripts/generate-screenshots.mts
@@ -12,7 +12,7 @@ import { startDevServer, BASE_URL, REPO_ROOT } from './lib/server.mts';
 const OUT_DIR = path.join(REPO_ROOT, 'screenshots', 'app');
 
 const STATES = [
-    { name: '01-landing', query: '', desc: 'Landing (default 1700, no pin)' },
+    { name: '01-landing', query: '', desc: "Landing (today's date, no pin)" },
     {
         name: '02-boston-1650',
         query: '?lat=42.32&lng=-71.06&zoom=10&year=1650&month=6&day=15',
diff --git a/scripts/screenshot-readme.mts b/scripts/screenshot-readme.mts
new file mode 100644
index 0000000..0722996
--- /dev/null
+++ b/scripts/screenshot-readme.mts
@@ -0,0 +1,57 @@
+// Generate the two screenshots embedded in the README.
+//
+// Unlike generate-screenshots.mts (full-page app-state captures for internal
+// review) these are deliberately small and cropped — a README hero image
+// should read at a glance, not reproduce the whole page. The landing shot is
+// the full viewport (narrow, so the header/intro/map fit together); the
+// example shot clips to just the map+answers column (`.app-cols`), skipping
+// the header and intro text since by then the reader already has context.
+//
+//   npm run screenshot:readme
+//
+// Output: screenshots/readme/*.png — committed, embedded via README.md.
+
+import { chromium } from 'playwright';
+import { mkdir, rm } from 'node:fs/promises';
+import path from 'node:path';
+import { startDevServer, BASE_URL, REPO_ROOT } from './lib/server.mts';
+
+const OUT_DIR = path.join(REPO_ROOT, 'screenshots', 'readme');
+const VIEWPORT = { width: 1000, height: 700 };
+
+async function run() {
+    console.log('\n📸  Generating README screenshots…\n');
+    const server = await startDevServer();
+    try {
+        await rm(OUT_DIR, { recursive: true, force: true });
+        await mkdir(OUT_DIR, { recursive: true });
+
+        const browser = await chromium.launch();
+        const page = await browser.newPage({ viewport: VIEWPORT, deviceScaleFactor: 2 });
+
+        // 1. Landing: no pin yet, today's date — the first thing a visitor sees.
+        await page.goto(BASE_URL, { waitUntil: 'networkidle' });
+        await page.waitForTimeout(500);
+        await page.screenshot({ path: path.join(OUT_DIR, '01-landing.png') });
+        console.log('  ✅  01-landing.png');
+
+        // 2. Example: Boston, 1650 — two coexisting calendars (Massachusett +
+        // colonial Julian), cropped to the map+answers column only.
+        await page.goto(`${BASE_URL}?lat=42.32&lng=-71.06&zoom=10&year=1650&month=6&day=15`, {
+            waitUntil: 'networkidle',
+        });
+        await page.waitForTimeout(800); // let map tiles settle
+        await page.locator('.app-cols').screenshot({ path: path.join(OUT_DIR, '02-example.png') });
+        console.log('  ✅  02-example.png');
+
+        await browser.close();
+        console.log('\n✨  Done — screenshots/readme/\n');
+    } finally {
+        server.stop();
+    }
+}
+
+run().catch((err) => {
+    console.error(err);
+    process.exit(1);
+});