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 +20 -16
- package/dist/cli/cli.js +5 -3
- package/dist/mcp/index.js +1 -1
- package/dist/{snippet-BYzU_uSZ.js → snippet-3pBmpUTB.js} +109 -140
- package/editor/dist/editor.js +857 -75
- package/package.json +1 -1
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
/**
|
|
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("
|
|
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
|
|
1086
|
-
|
|
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/
|
|
1144
|
+
//#region src/sync/browserFunctions.ts
|
|
1160
1145
|
/**
|
|
1161
|
-
*
|
|
1162
|
-
*
|
|
1146
|
+
* Browser context functions for DOM manipulation.
|
|
1147
|
+
* These functions execute in the browser via page.evaluate(fn, ...args).
|
|
1163
1148
|
*
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
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
|
|
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
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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
|
|
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,
|
|
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
|
|
1512
|
+
const navResult = await navigateAndPrepare(page, url, variant.colorScheme);
|
|
1544
1513
|
if (!navResult.success) return {
|
|
1545
1514
|
...navResult,
|
|
1546
1515
|
filename
|