heroshot 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,22 +28,7 @@ npx heroshot
28
28
 
29
29
  First run opens a browser with a visual picker. Click what you want, name it, done. Screenshots land in `heroshots/`, config saves to `.heroshot/config.json`. Next run regenerates everything headlessly.
30
30
 
31
- <table align="center">
32
- <tr>
33
- <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-desktop-light.png?raw=true" alt="Desktop Light"></td>
34
- <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-desktop-dark.png?raw=true" alt="Desktop Dark"></td>
35
- </tr>
36
- <tr>
37
- <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-tablet-light.png?raw=true" alt="Tablet Light"></td>
38
- <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-tablet-dark.png?raw=true" alt="Tablet Dark"></td>
39
- </tr>
40
- <tr>
41
- <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-mobile-light.png?raw=true" alt="Mobile Light"></td>
42
- <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-mobile-dark.png?raw=true" alt="Mobile Dark"></td>
43
- </tr>
44
- </table>
45
-
46
- <p align="center"><em>6 screenshots from one config entry - always in sync with the live site.</em></p>
31
+ https://github.com/user-attachments/assets/f35600a6-9220-4bd2-a8c6-a6b4ee8a33d9
47
32
 
48
33
  ## Use in Your Docs
49
34
 
@@ -90,6 +75,25 @@ plugins:
90
75
 
91
76
  One component/macro, all variants - light/dark mode switches automatically, responsive sizes via srcset.
92
77
 
78
+ ## One Screenshot - All Variants
79
+
80
+ <table align="center">
81
+ <tr>
82
+ <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-desktop-light.png?raw=true" alt="Desktop Light"></td>
83
+ <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-desktop-dark.png?raw=true" alt="Desktop Dark"></td>
84
+ </tr>
85
+ <tr>
86
+ <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-tablet-light.png?raw=true" alt="Tablet Light"></td>
87
+ <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-tablet-dark.png?raw=true" alt="Tablet Dark"></td>
88
+ </tr>
89
+ <tr>
90
+ <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-mobile-light.png?raw=true" alt="Mobile Light"></td>
91
+ <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-mobile-dark.png?raw=true" alt="Mobile Dark"></td>
92
+ </tr>
93
+ </table>
94
+
95
+ <p align="center"><em>6 screenshots from one config entry - always in sync with the live site.</em></p>
96
+
93
97
  ## Learn More
94
98
 
95
99
  | | |
package/dist/cli/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as intro, D as setVerbose, E as outro, O as spinner, S as error, T as note, _ as loadConfig, a as launchBrowser, c as generateSessionKey, d as loadSession, f as saveLocalKey, g as getConfigPath, h as ensureHeroshotDirectory, k as verbose, l as getSessionPath, m as sessionExists, n as snippetAction, o as DEFAULT_VIEWPORT, p as saveSession, r as sync, s as EDITOR_DIR, u as loadLocalKey, v as saveConfig, w as log, x as generateUid, y as VIEWPORT_PRESETS } from "../snippet-BYzU_uSZ.js";
2
+ import { C as intro, D as setVerbose, E as outro, O as spinner, S as error, T as note, _ as loadConfig, a as launchBrowser, c as generateSessionKey, d as loadSession, f as saveLocalKey, g as getConfigPath, h as ensureHeroshotDirectory, k as verbose, l as getSessionPath, m as sessionExists, n as snippetAction, o as DEFAULT_VIEWPORT, p as saveSession, r as sync, s as EDITOR_DIR, u as loadLocalKey, v as saveConfig, w as log, x as generateUid, y as VIEWPORT_PRESETS } from "../snippet-3pBmpUTB.js";
3
3
  import { existsSync, readFileSync, rmSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { Command } from "commander";
@@ -518,7 +518,9 @@ function buildShotConfig(url, options, existingConfig) {
518
518
  browser: {
519
519
  viewport: getViewport(options, existingConfig),
520
520
  colorScheme: getColorScheme(options, false),
521
- deviceScaleFactor: getDeviceScaleFactor(options, existingConfig)
521
+ deviceScaleFactor: getDeviceScaleFactor(options, existingConfig),
522
+ reducedMotion: options?.reducedMotion ? "reduce" : existingConfig?.browser?.reducedMotion,
523
+ userAgent: options?.userAgent ?? existingConfig?.browser?.userAgent
522
524
  },
523
525
  screenshots: [screenshot]
524
526
  };
@@ -703,7 +705,7 @@ program.name("heroshot").description("Define your screenshots once, update them
703
705
  setVerbose(program.opts().verbose ?? false);
704
706
  intro(version);
705
707
  });
706
- program.command("oneshot [url]", { isDefault: true }).description("Capture URL directly, or sync all screenshots from config").option("--selector <selector...>", "CSS selector(s) to capture").option("-o, --output <file>", "Output filename").option("-p, --padding <pixels>", "Padding around element", Number.parseInt).option("-w, --width <pixels>", "Viewport width", Number.parseInt).option("--height <pixels>", "Viewport height", Number.parseInt).option("--mobile", "Use mobile viewport (430x932)").option("--tablet", "Use tablet viewport (768x1024)").option("--desktop", "Use desktop viewport (1280x800)").option("--dark", "Force dark color scheme").option("--light", "Force light color scheme").option("--scale <factor>", "Device scale factor (1, 2, 3)", Number.parseInt).option("--retina", "Use retina scale (2x)").option("-q, --quality <percent>", "JPEG quality (1-100), outputs JPEG", Number.parseInt).option("--viewport-only", "Capture only viewport (not full page)").option("--save", "Save screenshot definition to config").option("--clean", "Delete stale files in output directory").option("--workers <count>", "Number of parallel capture workers", Number.parseInt).option("--headed", "Run browser in headed mode (visible window) for debugging").action(async (url, options) => {
708
+ program.command("oneshot [url]", { isDefault: true }).description("Capture URL directly, or sync all screenshots from config").option("--selector <selector...>", "CSS selector(s) to capture").option("-o, --output <file>", "Output filename").option("-p, --padding <pixels>", "Padding around element", Number.parseInt).option("-w, --width <pixels>", "Viewport width", Number.parseInt).option("--height <pixels>", "Viewport height", Number.parseInt).option("--mobile", "Use mobile viewport (430x932)").option("--tablet", "Use tablet viewport (768x1024)").option("--desktop", "Use desktop viewport (1280x800)").option("--dark", "Force dark color scheme").option("--light", "Force light color scheme").option("--scale <factor>", "Device scale factor (1, 2, 3)", Number.parseInt).option("--retina", "Use retina scale (2x)").option("-q, --quality <percent>", "JPEG quality (1-100), outputs JPEG", Number.parseInt).option("--viewport-only", "Capture only viewport (not full page)").option("--reduced-motion", "Emulate prefers-reduced-motion: reduce (disables animations)").option("--user-agent <string>", "Custom user agent string").option("--save", "Save screenshot definition to config").option("--clean", "Delete stale files in output directory").option("--workers <count>", "Number of parallel capture workers", Number.parseInt).option("--headed", "Run browser in headed mode (visible window) for debugging").action(async (url, options) => {
707
709
  if (!await shotAction(url, options, program.opts())) process.exitCode = 1;
708
710
  });
709
711
  program.command("config").description("Open browser to add/edit screenshot definitions").option("--reset", "Clear existing session and start fresh").option("--only", "Only run config, skip sync afterwards").option("--light", "Force light mode (prefers-color-scheme: light)").option("--dark", "Force dark mode (prefers-color-scheme: dark)").action(async (options) => {
package/dist/mcp/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { _ as loadConfig, b as screenshotSchema, g as getConfigPath, i as filterScreenshots, r as sync, t as generateSnippets, v as saveConfig, x as generateUid } from "../snippet-BYzU_uSZ.js";
2
+ import { _ as loadConfig, b as screenshotSchema, g as getConfigPath, i as filterScreenshots, r as sync, t as generateSnippets, v as saveConfig, x as generateUid } from "../snippet-3pBmpUTB.js";
3
3
  import { z } from "zod";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -260,7 +260,11 @@ const paddingSchema = z.object({
260
260
  bottom: z.number().int().min(0).default(0).describe("Bottom padding in pixels"),
261
261
  left: z.number().int().min(0).default(0).describe("Left padding in pixels")
262
262
  });
263
- /** Scroll position to restore when capturing */
263
+ /**
264
+ * Scroll position saved from editor.
265
+ * NOTE: Currently not used during capture - we use scrollIntoView instead.
266
+ * Kept for potential future use (e.g., precise scroll offset from element).
267
+ */
264
268
  const scrollPositionSchema = z.object({
265
269
  x: z.number().int().min(0).default(0).describe("Horizontal scroll offset in pixels"),
266
270
  y: z.number().int().min(0).default(0).describe("Vertical scroll offset in pixels")
@@ -303,7 +307,7 @@ const screenshotSchema = z.object({
303
307
  url: z.url().describe("Full URL of the page to capture"),
304
308
  selector: z.string().optional().describe("Element selector for capture (omit for full-page). Supports Playwright selector formats: CSS (.class, #id), shadow DOM (host >> child), XPath (xpath=...), text (text=...), role (role=button[name=\"OK\"]), and chained selectors."),
305
309
  padding: paddingSchema.optional().describe("Expand capture area beyond element bounds"),
306
- scroll: scrollPositionSchema.optional().describe("Scroll position to restore before capturing"),
310
+ scroll: scrollPositionSchema.optional().describe("Saved scroll position (not used during capture - scrollIntoView is used instead)"),
307
311
  paddingFill: paddingFillSchema.optional().describe("Background fill for padding area: \"inherit\" (default) shows page content, \"solid\" fills with detected background color"),
308
312
  elementFill: elementFillSchema.optional().describe("Background fill for element area: \"original\" (default) keeps actual background, \"solid\" replaces with detected color"),
309
313
  viewports: z.array(viewportVariantSchema).optional().describe("Viewport variants to generate — preset names (\"desktop\", \"tablet\", \"mobile\") or custom \"WIDTHxHEIGHT\""),
@@ -334,7 +338,9 @@ const shotCliOptionsSchema = z.object({
334
338
  scale: z.number().min(1).max(3).optional(),
335
339
  retina: z.boolean().optional(),
336
340
  quality: z.number().int().min(1).max(100).optional(),
337
- viewportOnly: z.boolean().optional()
341
+ viewportOnly: z.boolean().optional(),
342
+ reducedMotion: z.boolean().optional(),
343
+ userAgent: z.string().optional()
338
344
  });
339
345
  /** CLI command options for URL capture (includes --save and --clean flags) */
340
346
  const shotCommandOptionsSchema = shotCliOptionsSchema.extend({
@@ -1039,32 +1045,6 @@ async function executeActions(page, actions) {
1039
1045
  }
1040
1046
  }
1041
1047
 
1042
- //#endregion
1043
- //#region src/sync/browserFunctions.ts
1044
- /**
1045
- * Browser context functions for DOM manipulation.
1046
- * These functions execute in the browser via page.evaluate(fn, ...args).
1047
- *
1048
- * IMPORTANT: These functions run in browser context, not Node.js.
1049
- * They cannot access Node modules or closures - all data must be passed as arguments.
1050
- *
1051
- * NOTE: Most functions that need nested helpers or module-level constants have been
1052
- * moved to use string-based evaluation in their respective wrapper files to avoid
1053
- * tsx __name wrapper issues. Only simple functions are exported here.
1054
- */
1055
- /**
1056
- * Apply or remove dark mode class on document element.
1057
- */
1058
- function applyColorScheme(isDark) {
1059
- document.documentElement.classList.toggle("dark", isDark);
1060
- }
1061
- /**
1062
- * Scroll the window to a specific position.
1063
- */
1064
- function scrollTo({ x, y }) {
1065
- window.scrollTo(x, y);
1066
- }
1067
-
1068
1048
  //#endregion
1069
1049
  //#region src/sync/elementFinder.ts
1070
1050
  /**
@@ -1077,13 +1057,18 @@ function normalizeSelector(selector) {
1077
1057
  /**
1078
1058
  * Find element using Playwright's locator API with retries.
1079
1059
  * Supports all Playwright selector formats including shadow DOM piercing.
1060
+ * Automatically scrolls the element into view once found.
1080
1061
  */
1081
1062
  async function findElement(page, selector, maxAttempts = 10, intervalMs = 500) {
1082
1063
  const normalizedSelector = normalizeSelector(selector);
1083
1064
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1084
1065
  try {
1085
- const element = await page.locator(normalizedSelector).elementHandle({ timeout: intervalMs });
1086
- if (element) return element;
1066
+ const locator = page.locator(normalizedSelector);
1067
+ const element = await locator.elementHandle({ timeout: intervalMs });
1068
+ if (element) {
1069
+ await locator.scrollIntoViewIfNeeded({ timeout: 5e3 });
1070
+ return element;
1071
+ }
1087
1072
  } catch {}
1088
1073
  if (attempt < maxAttempts) await page.waitForTimeout(intervalMs);
1089
1074
  }
@@ -1156,126 +1141,116 @@ async function removePaddingMask(page) {
1156
1141
  }
1157
1142
 
1158
1143
  //#endregion
1159
- //#region src/sync/pageScripts.ts
1144
+ //#region src/sync/browserFunctions.ts
1160
1145
  /**
1161
- * Shared helper function for shadow DOM piercing selectors.
1162
- * Defined as a string to be inlined into page.evaluate() calls.
1146
+ * Browser context functions for DOM manipulation.
1147
+ * These functions execute in the browser via page.evaluate(fn, ...args).
1163
1148
  *
1164
- * WHY STRING-BASED: If this were a regular function inlined into an exported
1165
- * function, esbuild/tsx would wrap it with __name() which doesn't exist in
1166
- * the browser, causing "ReferenceError: __name is not defined".
1167
- */
1168
- const QUERY_SELECTOR_DEEP = `
1169
- function querySelectorDeep(selector) {
1170
- var parts = selector.split('>>>').map(function(p) { return p.trim(); });
1171
- var current = document;
1172
- for (var i = 0; i < parts.length; i++) {
1173
- var part = parts[i];
1174
- if (!part) continue;
1175
- var root = (current instanceof Element && current.shadowRoot) ? current.shadowRoot : current;
1176
- var found = root.querySelector(part);
1177
- if (!found) return null;
1178
- current = found;
1179
- }
1180
- return current instanceof Element ? current : null;
1181
- }
1182
- `;
1149
+ * IMPORTANT: These functions run in browser context, not Node.js.
1150
+ * They cannot access Node modules or closures - all data must be passed as arguments.
1151
+ *
1152
+ * NOTE: Most functions that need nested helpers or module-level constants have been
1153
+ * moved to use string-based evaluation in their respective wrapper files to avoid
1154
+ * tsx __name wrapper issues. Only simple functions are exported here.
1155
+ */
1156
+ /**
1157
+ * Apply or remove dark mode class on document element.
1158
+ */
1159
+ function applyColorScheme(isDark) {
1160
+ document.documentElement.classList.toggle("dark", isDark);
1161
+ }
1162
+
1163
+ //#endregion
1164
+ //#region src/sync/pageScripts.ts
1183
1165
  /**
1184
1166
  * Store original background and apply new background color to element.
1167
+ * Uses Playwright's locator API to support all selector formats.
1185
1168
  */
1186
1169
  async function applyElementBackground(page, selector, bgColor) {
1187
- await page.evaluate(`(function(selector, bgColor) {
1188
- ${QUERY_SELECTOR_DEEP}
1189
- var element = querySelectorDeep(selector);
1190
- if (!element || !(element instanceof HTMLElement)) return;
1191
- element.dataset.heroshotOriginalBg = element.style.backgroundColor;
1192
- element.style.backgroundColor = bgColor;
1193
- })(${JSON.stringify(selector)}, ${JSON.stringify(bgColor)})`);
1170
+ const locator = page.locator(normalizeSelector(selector));
1171
+ try {
1172
+ await locator.evaluate((element, color) => {
1173
+ if (element instanceof HTMLElement) {
1174
+ element.dataset["heroshotOriginalBg"] = element.style.backgroundColor;
1175
+ element.style.backgroundColor = color;
1176
+ }
1177
+ }, bgColor, { timeout: 5e3 });
1178
+ } catch {}
1194
1179
  }
1195
1180
  /**
1196
1181
  * Restore original background on element.
1182
+ * Uses Playwright's locator API to support all selector formats.
1197
1183
  */
1198
1184
  async function restoreElementBackground(page, selector) {
1199
- await page.evaluate(`(function(selector) {
1200
- ${QUERY_SELECTOR_DEEP}
1201
- var element = querySelectorDeep(selector);
1202
- if (!element || !(element instanceof HTMLElement)) return;
1203
- var originalBg = element.dataset.heroshotOriginalBg;
1204
- if (originalBg !== undefined) {
1205
- element.style.backgroundColor = originalBg;
1206
- delete element.dataset.heroshotOriginalBg;
1207
- }
1208
- })(${JSON.stringify(selector)})`);
1185
+ const locator = page.locator(normalizeSelector(selector));
1186
+ try {
1187
+ await locator.evaluate((element) => {
1188
+ if (element instanceof HTMLElement) {
1189
+ const originalBg = element.dataset["heroshotOriginalBg"];
1190
+ if (originalBg !== void 0) {
1191
+ element.style.backgroundColor = originalBg;
1192
+ delete element.dataset["heroshotOriginalBg"];
1193
+ }
1194
+ }
1195
+ }, { timeout: 5e3 });
1196
+ } catch {}
1209
1197
  }
1210
1198
  /**
1211
1199
  * Apply text overrides to elements on the page.
1200
+ * Uses Playwright's locator API for the container, then CSS selectors for relative paths.
1212
1201
  */
1213
1202
  async function applyTextOverrides(page, selector, textOverrides) {
1214
- await page.evaluate(`(function(containerSelector, overrides) {
1215
- ${QUERY_SELECTOR_DEEP}
1216
- var container = querySelectorDeep(containerSelector);
1217
- if (!container) return;
1218
- var keys = Object.keys(overrides);
1219
- for (var i = 0; i < keys.length; i++) {
1220
- var relativeSelector = keys[i];
1221
- var newText = overrides[relativeSelector];
1222
- var textElement = container.querySelector(relativeSelector);
1223
- if (textElement) {
1224
- textElement.textContent = newText;
1225
- }
1226
- }
1227
- })(${JSON.stringify(selector)}, ${JSON.stringify(textOverrides)})`);
1203
+ const locator = page.locator(normalizeSelector(selector));
1204
+ try {
1205
+ await locator.evaluate((container, overrides) => {
1206
+ for (const [relativeSelector, newText] of Object.entries(overrides)) {
1207
+ const textElement = container.querySelector(relativeSelector);
1208
+ if (textElement) textElement.textContent = newText;
1209
+ }
1210
+ }, textOverrides, { timeout: 5e3 });
1211
+ } catch {}
1228
1212
  await page.waitForTimeout(50);
1229
1213
  }
1230
1214
  /**
1231
- * Get background color for an element via page.evaluate.
1215
+ * Get background color for an element.
1216
+ * Uses Playwright's locator API to support all selector formats.
1232
1217
  */
1233
1218
  async function getElementBackgroundColor(page, selector) {
1234
- const script = `(function(selector) {
1235
- ${QUERY_SELECTOR_DEEP}
1236
-
1237
- function rgbToHex(bgColor) {
1238
- var rgbMatch = ${"/rgba?" + String.raw`\((\d+),\s*(\d+),\s*(\d+)/`}.exec(bgColor);
1239
- if (rgbMatch && rgbMatch[1] && rgbMatch[2] && rgbMatch[3]) {
1240
- var red = parseInt(rgbMatch[1], 10);
1241
- var green = parseInt(rgbMatch[2], 10);
1242
- var blue = parseInt(rgbMatch[3], 10);
1243
- return '#' +
1244
- red.toString(16).padStart(2, '0') +
1245
- green.toString(16).padStart(2, '0') +
1246
- blue.toString(16).padStart(2, '0');
1247
- }
1248
- return bgColor;
1249
- }
1250
-
1251
- function isOpaqueColor(bgColor) {
1252
- return Boolean(bgColor && bgColor !== 'transparent' && !bgColor.startsWith('rgba(0, 0, 0, 0)'));
1253
- }
1254
-
1255
- var element = querySelectorDeep(selector);
1256
- if (!element) return '#ffffff';
1257
-
1258
- var current = element;
1259
- while (current) {
1260
- var backgroundColor = getComputedStyle(current).backgroundColor;
1261
- if (isOpaqueColor(backgroundColor)) {
1262
- return rgbToHex(backgroundColor);
1263
- }
1264
- var root = current.getRootNode();
1265
- current = (root instanceof ShadowRoot) ? root.host : current.parentElement;
1266
- }
1267
-
1268
- var bodyBg = getComputedStyle(document.body).backgroundColor;
1269
- if (isOpaqueColor(bodyBg)) return rgbToHex(bodyBg);
1270
-
1271
- var htmlBg = getComputedStyle(document.documentElement).backgroundColor;
1272
- if (isOpaqueColor(htmlBg)) return rgbToHex(htmlBg);
1273
-
1274
- return '#ffffff';
1275
- })(${JSON.stringify(selector)})`;
1276
- const bgColor = await page.evaluate(script);
1277
- verbose(`Detected background color: ${bgColor}`);
1278
- return bgColor;
1219
+ const locator = page.locator(normalizeSelector(selector));
1220
+ try {
1221
+ const bgColor = await locator.evaluate((element) => {
1222
+ function rgbToHex(color) {
1223
+ const rgbMatch = /rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(color);
1224
+ if (rgbMatch?.[1] && rgbMatch[2] && rgbMatch[3]) {
1225
+ const red = Number.parseInt(rgbMatch[1], 10);
1226
+ const green = Number.parseInt(rgbMatch[2], 10);
1227
+ const blue = Number.parseInt(rgbMatch[3], 10);
1228
+ return "#" + red.toString(16).padStart(2, "0") + green.toString(16).padStart(2, "0") + blue.toString(16).padStart(2, "0");
1229
+ }
1230
+ return color;
1231
+ }
1232
+ function isOpaqueColor(color) {
1233
+ return Boolean(color && color !== "transparent" && !color.startsWith("rgba(0, 0, 0, 0)"));
1234
+ }
1235
+ let current = element;
1236
+ while (current) {
1237
+ const { backgroundColor } = getComputedStyle(current);
1238
+ if (isOpaqueColor(backgroundColor)) return rgbToHex(backgroundColor);
1239
+ const root = current.getRootNode();
1240
+ current = root instanceof ShadowRoot ? root.host : current.parentElement;
1241
+ }
1242
+ const { backgroundColor: bodyBg } = getComputedStyle(document.body);
1243
+ if (isOpaqueColor(bodyBg)) return rgbToHex(bodyBg);
1244
+ const { backgroundColor: htmlBg } = getComputedStyle(document.documentElement);
1245
+ if (isOpaqueColor(htmlBg)) return rgbToHex(htmlBg);
1246
+ return "#ffffff";
1247
+ }, { timeout: 5e3 });
1248
+ verbose(`Detected background color: ${bgColor}`);
1249
+ return bgColor;
1250
+ } catch {
1251
+ verbose("Could not detect background color, using default #ffffff");
1252
+ return "#ffffff";
1253
+ }
1279
1254
  }
1280
1255
  /**
1281
1256
  * Apply dark mode class to document based on color scheme.
@@ -1481,8 +1456,9 @@ const RETRY_DELAYS = [
1481
1456
  ];
1482
1457
  /**
1483
1458
  * Navigate to URL and prepare page for screenshot.
1459
+ * Note: Scroll position is handled by findElement which scrolls the element into view.
1484
1460
  */
1485
- async function navigateAndPrepare(page, url, colorScheme, scroll) {
1461
+ async function navigateAndPrepare(page, url, colorScheme) {
1486
1462
  if (colorScheme) await page.emulateMedia({ colorScheme });
1487
1463
  try {
1488
1464
  await page.goto(url, {
@@ -1497,13 +1473,6 @@ async function navigateAndPrepare(page, url, colorScheme, scroll) {
1497
1473
  }
1498
1474
  if (colorScheme) await applyColorSchemeClass(page, colorScheme);
1499
1475
  await page.waitForTimeout(2e3);
1500
- if (scroll) {
1501
- await page.evaluate(scrollTo, {
1502
- x: scroll.x,
1503
- y: scroll.y
1504
- });
1505
- await page.waitForTimeout(100);
1506
- }
1507
1476
  return { success: true };
1508
1477
  }
1509
1478
  /**
@@ -1530,7 +1499,7 @@ async function capturePageScreenshot(page, outputPath, format, quality, fullPage
1530
1499
  * Capture a single screenshot.
1531
1500
  */
1532
1501
  async function captureScreenshot(page, screenshot, outputDirectory, captureOptions, variant = {}) {
1533
- const { name, url, selector, padding, scroll, paddingFill, elementFill, textOverrides } = screenshot;
1502
+ const { name, url, selector, padding, paddingFill, elementFill, textOverrides } = screenshot;
1534
1503
  const { format, quality, fullPage } = captureOptions;
1535
1504
  const filename = generateScreenshotFilename({
1536
1505
  name,
@@ -1540,7 +1509,7 @@ async function captureScreenshot(page, screenshot, outputDirectory, captureOptio
1540
1509
  });
1541
1510
  const suffix = buildVariantSuffix(variant.viewportName, variant.colorScheme);
1542
1511
  verbose(`Capturing: ${name}${suffix ? ` (${suffix})` : ""}`);
1543
- const navResult = await navigateAndPrepare(page, url, variant.colorScheme, scroll);
1512
+ const navResult = await navigateAndPrepare(page, url, variant.colorScheme);
1544
1513
  if (!navResult.success) return {
1545
1514
  ...navResult,
1546
1515
  filename