heroshot 0.12.1 → 0.13.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/dist/cli/cli.js +35 -6
- package/dist/mcp/index.js +1 -1
- package/dist/{snippet-3pBmpUTB.js → snippet-CqBg-092.js} +380 -33
- package/editor/dist/editor.js +1754 -346
- package/package.json +4 -1
package/dist/cli/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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-CqBg-092.js";
|
|
3
3
|
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { Command } from "commander";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
|
|
@@ -20,8 +21,14 @@ function configToScreenshotData(screenshots) {
|
|
|
20
21
|
...screenshot.padding && { padding: screenshot.padding },
|
|
21
22
|
...screenshot.scroll && { scroll: screenshot.scroll },
|
|
22
23
|
...screenshot.paddingFill && { paddingFill: screenshot.paddingFill },
|
|
24
|
+
...screenshot.paddingColor && { paddingColor: screenshot.paddingColor },
|
|
23
25
|
...screenshot.elementFill && { elementFill: screenshot.elementFill },
|
|
24
|
-
...screenshot.
|
|
26
|
+
...screenshot.elementColor && { elementColor: screenshot.elementColor },
|
|
27
|
+
...screenshot.textOverrides && { textOverrides: screenshot.textOverrides },
|
|
28
|
+
...screenshot.annotations && { annotations: screenshot.annotations },
|
|
29
|
+
...screenshot.borderWidth && { borderWidth: screenshot.borderWidth },
|
|
30
|
+
...screenshot.borderColor && { borderColor: screenshot.borderColor },
|
|
31
|
+
...screenshot.borderRadius && { borderRadius: screenshot.borderRadius }
|
|
25
32
|
}));
|
|
26
33
|
}
|
|
27
34
|
|
|
@@ -86,12 +93,23 @@ const screenshotDataSchema = z.object({
|
|
|
86
93
|
"solid",
|
|
87
94
|
"transparent"
|
|
88
95
|
]).optional(),
|
|
96
|
+
paddingColor: z.string().optional(),
|
|
89
97
|
elementFill: z.enum([
|
|
90
98
|
"original",
|
|
91
99
|
"solid",
|
|
92
100
|
"transparent"
|
|
93
101
|
]).optional(),
|
|
94
|
-
|
|
102
|
+
elementColor: z.string().optional(),
|
|
103
|
+
textOverrides: z.record(z.string(), z.string()).optional(),
|
|
104
|
+
annotations: z.array(z.object({
|
|
105
|
+
id: z.string(),
|
|
106
|
+
type: z.string(),
|
|
107
|
+
points: z.array(z.number()),
|
|
108
|
+
style: z.record(z.string(), z.union([z.string(), z.number()])).optional()
|
|
109
|
+
})).optional(),
|
|
110
|
+
borderWidth: z.number().optional(),
|
|
111
|
+
borderColor: z.string().optional(),
|
|
112
|
+
borderRadius: z.number().optional()
|
|
95
113
|
});
|
|
96
114
|
/** Schema for browser settings from toolbar */
|
|
97
115
|
const browserSettingsSchema = z.object({
|
|
@@ -232,11 +250,22 @@ function toConfigScreenshot(data) {
|
|
|
232
250
|
name: data.name,
|
|
233
251
|
url: data.url,
|
|
234
252
|
selector: data.selector,
|
|
235
|
-
...data.padding && { padding:
|
|
253
|
+
...data.padding && { padding: {
|
|
254
|
+
top: Math.round(data.padding.top),
|
|
255
|
+
right: Math.round(data.padding.right),
|
|
256
|
+
bottom: Math.round(data.padding.bottom),
|
|
257
|
+
left: Math.round(data.padding.left)
|
|
258
|
+
} },
|
|
236
259
|
...data.scroll && { scroll: data.scroll },
|
|
237
260
|
...data.paddingFill && { paddingFill: data.paddingFill },
|
|
261
|
+
...data.paddingColor && { paddingColor: data.paddingColor },
|
|
238
262
|
...data.elementFill && { elementFill: data.elementFill },
|
|
239
|
-
...data.
|
|
263
|
+
...data.elementColor && { elementColor: data.elementColor },
|
|
264
|
+
...data.textOverrides && Object.keys(data.textOverrides).length > 0 && { textOverrides: data.textOverrides },
|
|
265
|
+
...data.annotations && data.annotations.length > 0 && { annotations: data.annotations },
|
|
266
|
+
...data.borderWidth && { borderWidth: data.borderWidth },
|
|
267
|
+
...data.borderColor && { borderColor: data.borderColor },
|
|
268
|
+
...data.borderRadius && { borderRadius: data.borderRadius }
|
|
240
269
|
};
|
|
241
270
|
}
|
|
242
271
|
|
|
@@ -693,7 +722,7 @@ function listAction(options, globalOptions) {
|
|
|
693
722
|
function getVersion() {
|
|
694
723
|
if (typeof HEROSHOT_VERSION !== "undefined") return HEROSHOT_VERSION;
|
|
695
724
|
try {
|
|
696
|
-
const packageJsonPath = path.join(import.meta.
|
|
725
|
+
const packageJsonPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
697
726
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
698
727
|
if (packageJson && typeof packageJson === "object" && "version" in packageJson) return String(packageJson.version);
|
|
699
728
|
} catch {}
|
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-CqBg-092.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";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { intro, log, note, outro, spinner } from "@clack/prompts";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
import { createCipheriv, createDecipheriv, createHash, randomBytes, scryptSync } from "node:crypto";
|
|
@@ -255,10 +256,10 @@ const colorSchemeSchema = z.enum(["light", "dark"]);
|
|
|
255
256
|
const outputFormatSchema = z.enum(["png", "jpeg"]).default("png");
|
|
256
257
|
/** Padding around element (can expand capture area) */
|
|
257
258
|
const paddingSchema = z.object({
|
|
258
|
-
top: z.number().
|
|
259
|
-
right: z.number().
|
|
260
|
-
bottom: z.number().
|
|
261
|
-
left: z.number().
|
|
259
|
+
top: z.number().min(0).default(0).describe("Top padding in pixels"),
|
|
260
|
+
right: z.number().min(0).default(0).describe("Right padding in pixels"),
|
|
261
|
+
bottom: z.number().min(0).default(0).describe("Bottom padding in pixels"),
|
|
262
|
+
left: z.number().min(0).default(0).describe("Left padding in pixels")
|
|
262
263
|
});
|
|
263
264
|
/**
|
|
264
265
|
* Scroll position saved from editor.
|
|
@@ -300,6 +301,13 @@ const viewportVariantSchema = z.string().refine((value) => {
|
|
|
300
301
|
const height = Number.parseInt(match[2] ?? "0", 10);
|
|
301
302
|
return width > 0 && height > 0;
|
|
302
303
|
}, { message: "Must be \"desktop\", \"tablet\", \"mobile\", or \"WIDTHxHEIGHT\" (e.g., \"400x500\")" });
|
|
304
|
+
/** Visual annotation drawn over a screenshot */
|
|
305
|
+
const annotationSchema = z.object({
|
|
306
|
+
id: z.string().min(1).default(generateUid).describe("Unique identifier (auto-generated if omitted)"),
|
|
307
|
+
type: z.string().describe("Annotation type: arrow, rect, or ellipse"),
|
|
308
|
+
points: z.array(z.number()).describe("Geometry points - meaning depends on type"),
|
|
309
|
+
style: z.record(z.string(), z.union([z.string(), z.number()])).optional().describe("CSS/SVG style properties (stroke, stroke-width, fill, opacity, etc.)")
|
|
310
|
+
});
|
|
303
311
|
/** Single screenshot definition */
|
|
304
312
|
const screenshotSchema = z.object({
|
|
305
313
|
id: z.string().min(1).default(generateUid).describe("Unique identifier (auto-generated if omitted)"),
|
|
@@ -309,9 +317,15 @@ const screenshotSchema = z.object({
|
|
|
309
317
|
padding: paddingSchema.optional().describe("Expand capture area beyond element bounds"),
|
|
310
318
|
scroll: scrollPositionSchema.optional().describe("Saved scroll position (not used during capture - scrollIntoView is used instead)"),
|
|
311
319
|
paddingFill: paddingFillSchema.optional().describe("Background fill for padding area: \"inherit\" (default) shows page content, \"solid\" fills with detected background color"),
|
|
320
|
+
paddingColor: z.string().optional().describe("Custom color for padding fill when set to \"solid\" (hex, defaults to auto-detected background)"),
|
|
312
321
|
elementFill: elementFillSchema.optional().describe("Background fill for element area: \"original\" (default) keeps actual background, \"solid\" replaces with detected color"),
|
|
322
|
+
elementColor: z.string().optional().describe("Custom color for element fill when set to \"solid\" (hex, defaults to auto-detected background)"),
|
|
313
323
|
viewports: z.array(viewportVariantSchema).optional().describe("Viewport variants to generate — preset names (\"desktop\", \"tablet\", \"mobile\") or custom \"WIDTHxHEIGHT\""),
|
|
314
324
|
textOverrides: z.record(z.string(), z.string()).optional().describe("Replace text content before capture. Keys are CSS selectors, values are replacement text"),
|
|
325
|
+
annotations: z.array(annotationSchema).optional().describe("Visual annotations drawn over the screenshot (arrows, rectangles, ellipses)"),
|
|
326
|
+
borderWidth: z.number().int().min(0).optional().describe("Border width around capture area in pixels (default 0)"),
|
|
327
|
+
borderColor: z.string().optional().describe("Border color (hex, default \"#000000\")"),
|
|
328
|
+
borderRadius: z.number().int().min(0).optional().describe("Corner radius in pixels — rounds the screenshot corners with transparency (PNG only)"),
|
|
315
329
|
actions: actionsSchema.optional()
|
|
316
330
|
});
|
|
317
331
|
/** Browser settings */
|
|
@@ -374,7 +388,8 @@ function parseConfig(input) {
|
|
|
374
388
|
//#region src/configFile.ts
|
|
375
389
|
const HEROSHOT_DIRECTORY_NAME = ".heroshot";
|
|
376
390
|
const CONFIG_FILENAME = "config.json";
|
|
377
|
-
const
|
|
391
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
392
|
+
const README_TEMPLATE_PATH = path.join(__dirname, "templates", "heroshotReadme.txt");
|
|
378
393
|
/**
|
|
379
394
|
* Get the .heroshot directory path for a project
|
|
380
395
|
*/
|
|
@@ -563,7 +578,7 @@ function findPackageRoot(startDirectory) {
|
|
|
563
578
|
return startDirectory;
|
|
564
579
|
}
|
|
565
580
|
/** Path to editor directory */
|
|
566
|
-
const EDITOR_DIR = path.join(findPackageRoot(import.meta.
|
|
581
|
+
const EDITOR_DIR = path.join(findPackageRoot(path.dirname(fileURLToPath(import.meta.url))), "editor");
|
|
567
582
|
|
|
568
583
|
//#endregion
|
|
569
584
|
//#region src/browser/browserDetect.ts
|
|
@@ -1045,6 +1060,310 @@ async function executeActions(page, actions) {
|
|
|
1045
1060
|
}
|
|
1046
1061
|
}
|
|
1047
1062
|
|
|
1063
|
+
//#endregion
|
|
1064
|
+
//#region src/sync/annotationOverlay.ts
|
|
1065
|
+
const OVERLAY_ID$1 = "heroshot-annotation-overlay";
|
|
1066
|
+
/** Default style values */
|
|
1067
|
+
const DEFAULT_STYLE = {
|
|
1068
|
+
stroke: "#ef4444",
|
|
1069
|
+
"stroke-width": 3,
|
|
1070
|
+
opacity: 1
|
|
1071
|
+
};
|
|
1072
|
+
/**
|
|
1073
|
+
* Calculate the required padding expansion for annotations that extend beyond element bounds.
|
|
1074
|
+
*/
|
|
1075
|
+
function calculateAnnotationPadding(annotations, tolerance = 20) {
|
|
1076
|
+
let minX = 0;
|
|
1077
|
+
let minY = 0;
|
|
1078
|
+
let maxX = 0;
|
|
1079
|
+
let maxY = 0;
|
|
1080
|
+
for (const ann of annotations) {
|
|
1081
|
+
const bbox = getAnnotationBBox(ann);
|
|
1082
|
+
minX = Math.min(minX, bbox.minX);
|
|
1083
|
+
minY = Math.min(minY, bbox.minY);
|
|
1084
|
+
maxX = Math.max(maxX, bbox.maxX);
|
|
1085
|
+
maxY = Math.max(maxY, bbox.maxY);
|
|
1086
|
+
}
|
|
1087
|
+
return {
|
|
1088
|
+
top: Math.max(0, -(minY - tolerance)),
|
|
1089
|
+
right: Math.max(0, tolerance),
|
|
1090
|
+
bottom: Math.max(0, tolerance),
|
|
1091
|
+
left: Math.max(0, -(minX - tolerance))
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
/** Get bounding box for a single annotation */
|
|
1095
|
+
function getAnnotationBBox(ann) {
|
|
1096
|
+
const { points } = ann;
|
|
1097
|
+
switch (ann.type) {
|
|
1098
|
+
case "arrow": {
|
|
1099
|
+
const [x1 = 0, y1 = 0, x2 = 0, y2 = 0] = points;
|
|
1100
|
+
return {
|
|
1101
|
+
minX: Math.min(x1, x2),
|
|
1102
|
+
minY: Math.min(y1, y2),
|
|
1103
|
+
maxX: Math.max(x1, x2),
|
|
1104
|
+
maxY: Math.max(y1, y2)
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
case "rect": {
|
|
1108
|
+
const [x = 0, y = 0, w = 0, h = 0] = points;
|
|
1109
|
+
return {
|
|
1110
|
+
minX: x,
|
|
1111
|
+
minY: y,
|
|
1112
|
+
maxX: x + w,
|
|
1113
|
+
maxY: y + h
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
case "ellipse": {
|
|
1117
|
+
const [cx = 0, cy = 0, rx = 0, ry = 0] = points;
|
|
1118
|
+
return {
|
|
1119
|
+
minX: cx - rx,
|
|
1120
|
+
minY: cy - ry,
|
|
1121
|
+
maxX: cx + rx,
|
|
1122
|
+
maxY: cy + ry
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
default: return {
|
|
1126
|
+
minX: 0,
|
|
1127
|
+
minY: 0,
|
|
1128
|
+
maxX: 0,
|
|
1129
|
+
maxY: 0
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Inject SVG annotation overlay at element position.
|
|
1135
|
+
*/
|
|
1136
|
+
async function injectAnnotationOverlay(page, element, annotations, padding) {
|
|
1137
|
+
if (annotations.length === 0) return;
|
|
1138
|
+
const box = await element.boundingBox();
|
|
1139
|
+
if (!box) return;
|
|
1140
|
+
await page.evaluate(`(function(config) {
|
|
1141
|
+
var OVERLAY_ID = ${JSON.stringify(OVERLAY_ID$1)};
|
|
1142
|
+
var DEFAULT_STYLE = ${JSON.stringify(DEFAULT_STYLE)};
|
|
1143
|
+
var box = config.box;
|
|
1144
|
+
var annotations = config.annotations;
|
|
1145
|
+
var padding = config.padding;
|
|
1146
|
+
|
|
1147
|
+
// Remove existing overlay
|
|
1148
|
+
var existing = document.querySelector('#' + OVERLAY_ID);
|
|
1149
|
+
if (existing) existing.remove();
|
|
1150
|
+
|
|
1151
|
+
function buildStyle(ann) {
|
|
1152
|
+
var merged = { fill: 'none' };
|
|
1153
|
+
for (var k in DEFAULT_STYLE) merged[k] = DEFAULT_STYLE[k];
|
|
1154
|
+
if (ann.style) {
|
|
1155
|
+
for (var k in ann.style) merged[k] = ann.style[k];
|
|
1156
|
+
}
|
|
1157
|
+
var parts = [];
|
|
1158
|
+
for (var k in merged) parts.push(k + ':' + merged[k]);
|
|
1159
|
+
return parts.join(';');
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function renderAnnotation(ann) {
|
|
1163
|
+
var style = buildStyle(ann);
|
|
1164
|
+
switch (ann.type) {
|
|
1165
|
+
case 'arrow': {
|
|
1166
|
+
var x1 = ann.points[0] || 0, y1 = ann.points[1] || 0;
|
|
1167
|
+
var x2 = ann.points[2] || 0, y2 = ann.points[3] || 0;
|
|
1168
|
+
var strokeWidth = (ann.style && ann.style['stroke-width']) || 3;
|
|
1169
|
+
var headLength = 8 + strokeWidth * 2;
|
|
1170
|
+
var headAngle = Math.PI / 7;
|
|
1171
|
+
var angle = Math.atan2(y2 - y1, x2 - x1);
|
|
1172
|
+
var a1x = x2 - headLength * Math.cos(angle - headAngle);
|
|
1173
|
+
var a1y = y2 - headLength * Math.sin(angle - headAngle);
|
|
1174
|
+
var a2x = x2 - headLength * Math.cos(angle + headAngle);
|
|
1175
|
+
var a2y = y2 - headLength * Math.sin(angle + headAngle);
|
|
1176
|
+
var baseFactor = Math.cos(headAngle);
|
|
1177
|
+
var baseX = x2 - headLength * baseFactor * Math.cos(angle);
|
|
1178
|
+
var baseY = y2 - headLength * baseFactor * Math.sin(angle);
|
|
1179
|
+
var strokeColor = (ann.style && ann.style.stroke) || '#ef4444';
|
|
1180
|
+
var opacity = (ann.style && ann.style.opacity) || 1;
|
|
1181
|
+
var lineStyle = 'stroke:' + strokeColor + ';stroke-width:' + strokeWidth + ';fill:none';
|
|
1182
|
+
return '<g style="opacity:' + opacity + '">'
|
|
1183
|
+
+ '<line x1="' + x1 + '" y1="' + y1 + '" x2="' + baseX + '" y2="' + baseY + '" style="' + lineStyle + '" />'
|
|
1184
|
+
+ '<polygon points="' + x2 + ',' + y2 + ' ' + a1x + ',' + a1y + ' ' + a2x + ',' + a2y + '" '
|
|
1185
|
+
+ 'fill="' + strokeColor + '" />'
|
|
1186
|
+
+ '</g>';
|
|
1187
|
+
}
|
|
1188
|
+
case 'rect': {
|
|
1189
|
+
var x = ann.points[0] || 0, y = ann.points[1] || 0;
|
|
1190
|
+
var w = ann.points[2] || 0, h = ann.points[3] || 0;
|
|
1191
|
+
var borderRadius = (ann.style && ann.style['border-radius'] != null) ? ann.style['border-radius'] : 4;
|
|
1192
|
+
return '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '" rx="' + borderRadius + '" style="' + style + '" />';
|
|
1193
|
+
}
|
|
1194
|
+
case 'ellipse': {
|
|
1195
|
+
var cx = ann.points[0] || 0, cy = ann.points[1] || 0;
|
|
1196
|
+
var rx = ann.points[2] || 0, ry = ann.points[3] || 0;
|
|
1197
|
+
return '<ellipse cx="' + cx + '" cy="' + cy + '" rx="' + rx + '" ry="' + ry + '" style="' + style + '" />';
|
|
1198
|
+
}
|
|
1199
|
+
default:
|
|
1200
|
+
return '';
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Create container at element position
|
|
1205
|
+
var container = document.createElement('div');
|
|
1206
|
+
container.id = OVERLAY_ID;
|
|
1207
|
+
container.style.cssText = 'position:fixed;pointer-events:none;z-index:2147483646;'
|
|
1208
|
+
+ 'top:' + (box.y - padding.top) + 'px;'
|
|
1209
|
+
+ 'left:' + (box.x - padding.left) + 'px;'
|
|
1210
|
+
+ 'width:' + (box.width + padding.left + padding.right) + 'px;'
|
|
1211
|
+
+ 'height:' + (box.height + padding.top + padding.bottom) + 'px;';
|
|
1212
|
+
|
|
1213
|
+
// Build SVG content
|
|
1214
|
+
var svgParts = [];
|
|
1215
|
+
var svgWidth = box.width + padding.left + padding.right;
|
|
1216
|
+
var svgHeight = box.height + padding.top + padding.bottom;
|
|
1217
|
+
svgParts.push('<svg xmlns="http://www.w3.org/2000/svg" width="' + svgWidth + '" height="' + svgHeight + '" style="overflow:visible;">');
|
|
1218
|
+
// Translate so (0,0) = element top-left
|
|
1219
|
+
svgParts.push('<g transform="translate(' + padding.left + ',' + padding.top + ')">');
|
|
1220
|
+
for (var i = 0; i < annotations.length; i++) {
|
|
1221
|
+
svgParts.push(renderAnnotation(annotations[i]));
|
|
1222
|
+
}
|
|
1223
|
+
svgParts.push('</g></svg>');
|
|
1224
|
+
|
|
1225
|
+
container.innerHTML = svgParts.join('');
|
|
1226
|
+
document.body.appendChild(container);
|
|
1227
|
+
})(${JSON.stringify({
|
|
1228
|
+
box,
|
|
1229
|
+
annotations,
|
|
1230
|
+
padding
|
|
1231
|
+
})})`);
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Remove injected annotation overlay.
|
|
1235
|
+
*/
|
|
1236
|
+
async function removeAnnotationOverlay(page) {
|
|
1237
|
+
await page.evaluate(`(function() {
|
|
1238
|
+
var OVERLAY_ID = ${JSON.stringify(OVERLAY_ID$1)};
|
|
1239
|
+
var existing = document.querySelector('#' + OVERLAY_ID);
|
|
1240
|
+
if (existing) existing.remove();
|
|
1241
|
+
})()`);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
//#endregion
|
|
1245
|
+
//#region src/sync/borderOverlay.ts
|
|
1246
|
+
const OVERLAY_ID = "heroshot-border-overlay";
|
|
1247
|
+
/**
|
|
1248
|
+
* Inject a border overlay div positioned over the clip region.
|
|
1249
|
+
*/
|
|
1250
|
+
async function injectBorderOverlay(page, clip, borderWidth, borderColor, borderRadius) {
|
|
1251
|
+
await page.evaluate(`(function(config) {
|
|
1252
|
+
var OVERLAY_ID = ${JSON.stringify(OVERLAY_ID)};
|
|
1253
|
+
var existing = document.querySelector('#' + OVERLAY_ID);
|
|
1254
|
+
if (existing) existing.remove();
|
|
1255
|
+
|
|
1256
|
+
var div = document.createElement('div');
|
|
1257
|
+
div.id = OVERLAY_ID;
|
|
1258
|
+
div.style.cssText = 'position:fixed;'
|
|
1259
|
+
+ 'left:' + config.x + 'px;'
|
|
1260
|
+
+ 'top:' + config.y + 'px;'
|
|
1261
|
+
+ 'width:' + config.width + 'px;'
|
|
1262
|
+
+ 'height:' + config.height + 'px;'
|
|
1263
|
+
+ 'border:' + config.borderWidth + 'px solid ' + config.borderColor + ';'
|
|
1264
|
+
+ 'border-radius:' + config.borderRadius + 'px;'
|
|
1265
|
+
+ 'box-sizing:border-box;'
|
|
1266
|
+
+ 'pointer-events:none;'
|
|
1267
|
+
+ 'z-index:2147483646;';
|
|
1268
|
+
document.body.appendChild(div);
|
|
1269
|
+
})(${JSON.stringify({
|
|
1270
|
+
...clip,
|
|
1271
|
+
borderWidth,
|
|
1272
|
+
borderColor,
|
|
1273
|
+
borderRadius
|
|
1274
|
+
})})`);
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Remove border overlay.
|
|
1278
|
+
*/
|
|
1279
|
+
async function removeBorderOverlay(page) {
|
|
1280
|
+
await page.evaluate(`(function() {
|
|
1281
|
+
var el = document.querySelector('#${OVERLAY_ID}');
|
|
1282
|
+
if (el) el.remove();
|
|
1283
|
+
})()`);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
//#endregion
|
|
1287
|
+
//#region src/sync/borderRadiusMask.ts
|
|
1288
|
+
/**
|
|
1289
|
+
* Apply CSS clip-path with rounded inset to <html> and remove backgrounds.
|
|
1290
|
+
* Coordinates are viewport-relative (from Playwright boundingBox/clip).
|
|
1291
|
+
*/
|
|
1292
|
+
async function injectBorderRadiusMask(page, clip, borderRadius) {
|
|
1293
|
+
await page.evaluate(`(function(config) {
|
|
1294
|
+
var scrollX = window.scrollX || 0;
|
|
1295
|
+
var scrollY = window.scrollY || 0;
|
|
1296
|
+
var docWidth = document.documentElement.scrollWidth;
|
|
1297
|
+
var docHeight = document.documentElement.scrollHeight;
|
|
1298
|
+
|
|
1299
|
+
// Convert viewport coords to document coords for inset calculation
|
|
1300
|
+
var top = config.y + scrollY;
|
|
1301
|
+
var left = config.x + scrollX;
|
|
1302
|
+
var bottom = docHeight - (top + config.height);
|
|
1303
|
+
var right = docWidth - (left + config.width);
|
|
1304
|
+
|
|
1305
|
+
// Clamp to non-negative (avoid invalid inset values)
|
|
1306
|
+
top = Math.max(0, top);
|
|
1307
|
+
left = Math.max(0, left);
|
|
1308
|
+
bottom = Math.max(0, bottom);
|
|
1309
|
+
right = Math.max(0, right);
|
|
1310
|
+
|
|
1311
|
+
var html = document.documentElement;
|
|
1312
|
+
var body = document.body;
|
|
1313
|
+
|
|
1314
|
+
// Save original styles
|
|
1315
|
+
html.dataset.heroshotOriginalClip = html.style.clipPath || '';
|
|
1316
|
+
html.dataset.heroshotOriginalHtmlBg = html.style.background || '';
|
|
1317
|
+
html.dataset.heroshotOriginalBodyBg = body.style.background || '';
|
|
1318
|
+
html.dataset.heroshotOriginalHtmlBgColor = html.style.backgroundColor || '';
|
|
1319
|
+
html.dataset.heroshotOriginalBodyBgColor = body.style.backgroundColor || '';
|
|
1320
|
+
|
|
1321
|
+
// Apply clip-path with rounded corners
|
|
1322
|
+
html.style.clipPath = 'inset(' + top + 'px ' + right + 'px ' + bottom + 'px ' + left + 'px round ' + config.borderRadius + 'px)';
|
|
1323
|
+
|
|
1324
|
+
// Remove html/body backgrounds so canvas is transparent
|
|
1325
|
+
// (CSS spec: root element bg propagates to canvas, bypassing clip-path)
|
|
1326
|
+
html.style.setProperty('background', 'transparent', 'important');
|
|
1327
|
+
html.style.setProperty('background-color', 'transparent', 'important');
|
|
1328
|
+
body.style.setProperty('background', 'transparent', 'important');
|
|
1329
|
+
body.style.setProperty('background-color', 'transparent', 'important');
|
|
1330
|
+
})(${JSON.stringify({
|
|
1331
|
+
...clip,
|
|
1332
|
+
borderRadius
|
|
1333
|
+
})})`);
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Remove border radius clip mask and restore original styles.
|
|
1337
|
+
*/
|
|
1338
|
+
async function removeBorderRadiusMask(page) {
|
|
1339
|
+
await page.evaluate(`(function() {
|
|
1340
|
+
var html = document.documentElement;
|
|
1341
|
+
var body = document.body;
|
|
1342
|
+
|
|
1343
|
+
// Restore clip-path
|
|
1344
|
+
if (html.dataset.heroshotOriginalClip !== undefined) {
|
|
1345
|
+
html.style.clipPath = html.dataset.heroshotOriginalClip;
|
|
1346
|
+
delete html.dataset.heroshotOriginalClip;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Restore html background
|
|
1350
|
+
if (html.dataset.heroshotOriginalHtmlBg !== undefined) {
|
|
1351
|
+
html.style.background = html.dataset.heroshotOriginalHtmlBg;
|
|
1352
|
+
html.style.backgroundColor = html.dataset.heroshotOriginalHtmlBgColor || '';
|
|
1353
|
+
delete html.dataset.heroshotOriginalHtmlBg;
|
|
1354
|
+
delete html.dataset.heroshotOriginalHtmlBgColor;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// Restore body background
|
|
1358
|
+
if (html.dataset.heroshotOriginalBodyBg !== undefined) {
|
|
1359
|
+
body.style.background = html.dataset.heroshotOriginalBodyBg;
|
|
1360
|
+
body.style.backgroundColor = html.dataset.heroshotOriginalBodyBgColor || '';
|
|
1361
|
+
delete html.dataset.heroshotOriginalBodyBg;
|
|
1362
|
+
delete html.dataset.heroshotOriginalBodyBgColor;
|
|
1363
|
+
}
|
|
1364
|
+
})()`);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1048
1367
|
//#endregion
|
|
1049
1368
|
//#region src/sync/elementFinder.ts
|
|
1050
1369
|
/**
|
|
@@ -1310,7 +1629,7 @@ async function takeScreenshot(options) {
|
|
|
1310
1629
|
* Capture element with selector and optional text overrides.
|
|
1311
1630
|
*/
|
|
1312
1631
|
async function captureElementWithOptions(options) {
|
|
1313
|
-
const { page, selector,
|
|
1632
|
+
const { page, selector, textOverrides, ...rest } = options;
|
|
1314
1633
|
const element = await findElement(page, selector);
|
|
1315
1634
|
if (!element) return {
|
|
1316
1635
|
success: false,
|
|
@@ -1321,64 +1640,88 @@ async function captureElementWithOptions(options) {
|
|
|
1321
1640
|
page,
|
|
1322
1641
|
element,
|
|
1323
1642
|
selector,
|
|
1324
|
-
|
|
1325
|
-
format,
|
|
1326
|
-
quality,
|
|
1327
|
-
padding,
|
|
1328
|
-
paddingFill,
|
|
1329
|
-
elementFill
|
|
1643
|
+
...rest
|
|
1330
1644
|
});
|
|
1331
1645
|
}
|
|
1332
|
-
/**
|
|
1333
|
-
* Capture element with padding using clip region.
|
|
1334
|
-
*/
|
|
1646
|
+
/** Capture element with padding using clip region. */
|
|
1335
1647
|
async function captureWithPadding(options) {
|
|
1336
|
-
const { page, element, padding, paddingFill, bgColor, outputPath, format, quality, needsTransparent } = options;
|
|
1648
|
+
const { page, element, padding, paddingFill, bgColor, outputPath, format, quality, needsTransparent, annotations, borderWidth, borderColor, borderRadius } = options;
|
|
1337
1649
|
const box = await element.boundingBox();
|
|
1338
1650
|
if (!box) return {
|
|
1339
1651
|
success: false,
|
|
1340
1652
|
error: "Could not get element bounding box"
|
|
1341
1653
|
};
|
|
1342
1654
|
if (paddingFill === "solid") await injectPaddingMask(page, element, padding, bgColor);
|
|
1655
|
+
const hasAnnotations = annotations && annotations.length > 0;
|
|
1656
|
+
if (hasAnnotations) await injectAnnotationOverlay(page, element, annotations, padding);
|
|
1657
|
+
const clip = {
|
|
1658
|
+
x: Math.max(0, box.x - padding.left),
|
|
1659
|
+
y: Math.max(0, box.y - padding.top),
|
|
1660
|
+
width: box.width + padding.left + padding.right,
|
|
1661
|
+
height: box.height + padding.top + padding.bottom
|
|
1662
|
+
};
|
|
1663
|
+
const hasBorder = borderWidth != null && borderWidth > 0 && borderColor != null;
|
|
1664
|
+
if (hasBorder) await injectBorderOverlay(page, clip, borderWidth, borderColor, borderRadius ?? 0);
|
|
1665
|
+
const hasBorderRadius = borderRadius != null && borderRadius > 0;
|
|
1666
|
+
if (hasBorderRadius) await injectBorderRadiusMask(page, clip, borderRadius);
|
|
1343
1667
|
await takeScreenshot({
|
|
1344
1668
|
target: page,
|
|
1345
1669
|
outputPath,
|
|
1346
1670
|
format,
|
|
1347
1671
|
quality,
|
|
1348
|
-
clip
|
|
1349
|
-
|
|
1350
|
-
y: Math.max(0, box.y - padding.top),
|
|
1351
|
-
width: box.width + padding.left + padding.right,
|
|
1352
|
-
height: box.height + padding.top + padding.bottom
|
|
1353
|
-
},
|
|
1354
|
-
omitBackground: needsTransparent
|
|
1672
|
+
clip,
|
|
1673
|
+
omitBackground: needsTransparent || hasBorderRadius
|
|
1355
1674
|
});
|
|
1675
|
+
if (hasBorderRadius) await removeBorderRadiusMask(page);
|
|
1676
|
+
if (hasBorder) await removeBorderOverlay(page);
|
|
1677
|
+
if (hasAnnotations) await removeAnnotationOverlay(page);
|
|
1356
1678
|
if (paddingFill === "solid") await removePaddingMask(page);
|
|
1357
1679
|
return { success: true };
|
|
1358
1680
|
}
|
|
1681
|
+
/** Expand padding to fit annotations that extend beyond current bounds. */
|
|
1682
|
+
function expandPaddingForAnnotations(padding, annotations) {
|
|
1683
|
+
if (!annotations || annotations.length === 0) return padding;
|
|
1684
|
+
const ap = calculateAnnotationPadding(annotations);
|
|
1685
|
+
return {
|
|
1686
|
+
top: Math.max(padding.top, ap.top),
|
|
1687
|
+
right: Math.max(padding.right, ap.right),
|
|
1688
|
+
bottom: Math.max(padding.bottom, ap.bottom),
|
|
1689
|
+
left: Math.max(padding.left, ap.left)
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1359
1692
|
/**
|
|
1360
1693
|
* Capture element screenshot with optional padding and background fill modes.
|
|
1361
1694
|
*/
|
|
1362
1695
|
async function captureElementScreenshot(options) {
|
|
1363
|
-
const { page, element, selector, outputPath, format, quality, padding, paddingFill, elementFill } = options;
|
|
1364
|
-
const
|
|
1696
|
+
const { page, element, selector, outputPath, format, quality, padding, paddingFill, elementFill, annotations, borderWidth, borderColor, borderRadius } = options;
|
|
1697
|
+
const effectivePadding = expandPaddingForAnnotations(padding ?? {
|
|
1698
|
+
top: 0,
|
|
1699
|
+
right: 0,
|
|
1700
|
+
bottom: 0,
|
|
1701
|
+
left: 0
|
|
1702
|
+
}, annotations);
|
|
1703
|
+
const hasAnnotations = annotations && annotations.length > 0;
|
|
1704
|
+
const hasPadding = effectivePadding.top + effectivePadding.right + effectivePadding.bottom + effectivePadding.left > 0;
|
|
1365
1705
|
const needsTransparent = format === "png" && (paddingFill === "transparent" || elementFill === "transparent");
|
|
1366
|
-
const needsBgColor = paddingFill === "solid" || elementFill === "solid";
|
|
1367
1706
|
let bgColor = "#ffffff";
|
|
1368
|
-
if (
|
|
1707
|
+
if (paddingFill === "solid" || elementFill === "solid") bgColor = await getElementBackgroundColor(page, selector);
|
|
1369
1708
|
if (elementFill === "solid") await applyElementBackground(page, selector, bgColor);
|
|
1370
1709
|
else if (elementFill === "transparent") await applyElementBackground(page, selector, "transparent");
|
|
1371
|
-
if (hasPadding &&
|
|
1710
|
+
if (hasPadding || hasAnnotations || borderRadius != null && borderRadius > 0 || borderWidth != null && borderWidth > 0) {
|
|
1372
1711
|
const result = await captureWithPadding({
|
|
1373
1712
|
page,
|
|
1374
1713
|
element,
|
|
1375
|
-
padding,
|
|
1714
|
+
padding: effectivePadding,
|
|
1376
1715
|
paddingFill,
|
|
1377
1716
|
bgColor,
|
|
1378
1717
|
outputPath,
|
|
1379
1718
|
format,
|
|
1380
1719
|
quality,
|
|
1381
|
-
needsTransparent
|
|
1720
|
+
needsTransparent,
|
|
1721
|
+
annotations,
|
|
1722
|
+
borderWidth,
|
|
1723
|
+
borderColor,
|
|
1724
|
+
borderRadius
|
|
1382
1725
|
});
|
|
1383
1726
|
if (!result.success) return result;
|
|
1384
1727
|
} else await takeScreenshot({
|
|
@@ -1499,7 +1842,7 @@ async function capturePageScreenshot(page, outputPath, format, quality, fullPage
|
|
|
1499
1842
|
* Capture a single screenshot.
|
|
1500
1843
|
*/
|
|
1501
1844
|
async function captureScreenshot(page, screenshot, outputDirectory, captureOptions, variant = {}) {
|
|
1502
|
-
const { name, url, selector, padding, paddingFill, elementFill, textOverrides } = screenshot;
|
|
1845
|
+
const { name, url, selector, padding, paddingFill, elementFill, textOverrides, annotations, borderWidth, borderColor, borderRadius } = screenshot;
|
|
1503
1846
|
const { format, quality, fullPage } = captureOptions;
|
|
1504
1847
|
const filename = generateScreenshotFilename({
|
|
1505
1848
|
name,
|
|
@@ -1536,7 +1879,11 @@ async function captureScreenshot(page, screenshot, outputDirectory, captureOptio
|
|
|
1536
1879
|
padding,
|
|
1537
1880
|
paddingFill,
|
|
1538
1881
|
elementFill,
|
|
1539
|
-
textOverrides
|
|
1882
|
+
textOverrides,
|
|
1883
|
+
annotations,
|
|
1884
|
+
borderWidth,
|
|
1885
|
+
borderColor,
|
|
1886
|
+
borderRadius
|
|
1540
1887
|
}) : await capturePageScreenshot(page, outputPath, format, quality, fullPage ?? true);
|
|
1541
1888
|
return captureResult.success ? {
|
|
1542
1889
|
success: true,
|