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 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-3pBmpUTB.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-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.textOverrides && { textOverrides: screenshot.textOverrides }
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
- textOverrides: z.record(z.string(), z.string()).optional()
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: data.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.textOverrides && Object.keys(data.textOverrides).length > 0 && { textOverrides: data.textOverrides }
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.dirname, "..", "..", "package.json");
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-3pBmpUTB.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-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().int().min(0).default(0).describe("Top padding in pixels"),
259
- right: z.number().int().min(0).default(0).describe("Right padding in pixels"),
260
- bottom: z.number().int().min(0).default(0).describe("Bottom padding in pixels"),
261
- left: z.number().int().min(0).default(0).describe("Left padding in pixels")
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 README_TEMPLATE_PATH = path.join(import.meta.dirname, "templates", "heroshotReadme.txt");
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.dirname), "editor");
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, outputPath, format, quality, padding, paddingFill, elementFill, textOverrides } = options;
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
- outputPath,
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
- x: Math.max(0, box.x - padding.left),
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 hasPadding = padding && (padding.top > 0 || padding.right > 0 || padding.bottom > 0 || padding.left > 0);
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 (needsBgColor) bgColor = await getElementBackgroundColor(page, selector);
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 && padding) {
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,