heroshot 0.2.0 → 0.3.0

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
@@ -5,18 +5,43 @@
5
5
  <h1 align="center">heroshot</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>Screenshots that stay true.</strong><br>
9
- Your UI evolves. Heroshot keeps your docs and marketing in sync - automatically.
8
+ <a href="https://www.npmjs.com/package/heroshot"><img src="https://img.shields.io/npm/v/heroshot?style=for-the-badge&logo=npm" alt="npm version"></a>
9
+ <a href="https://github.com/omachala/heroshot/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/heroshot?style=for-the-badge" alt="license"></a>
10
+ <a href="https://codecov.io/gh/omachala/heroshot"><img src="https://img.shields.io/codecov/c/github/omachala/heroshot?style=for-the-badge" alt="coverage"></a>
11
+ <a href="https://sonarcloud.io/summary/new_code?id=omachala_heroshot"><img src="https://img.shields.io/sonar/quality_gate/omachala_heroshot?server=https%3A%2F%2Fsonarcloud.io&style=for-the-badge&logo=sonarcloud" alt="quality gate"></a>
12
+ <a href="https://heroshot.sh"><img src="https://img.shields.io/badge/docs-heroshot.sh-blue?style=for-the-badge" alt="docs"></a>
10
13
  </p>
11
14
 
12
- <p align="center">
13
- <a href="https://www.npmjs.com/package/heroshot"><img src="https://img.shields.io/npm/v/heroshot" alt="npm"></a>
14
- <a href="https://heroshot.sh"><img src="https://img.shields.io/badge/docs-heroshot.sh-blue" alt="docs"></a>
15
- </p>
16
-
17
- <p align="center">
18
- <img src="https://github.com/omachala/heroshot/blob/main/toolbar/tests/snapshots/manage-screenshots.test.ts/after-rename.png?raw=true" alt="heroshot toolbar demo" width="800">
19
- </p>
15
+ Documentation screenshots rot. Your UI changes, but the images in your README, docs, and tutorials become outdated. The manual fix is painful: navigate, log in, screenshot, crop, save, commit. Repeat for every image.
16
+
17
+ **Heroshot treats screenshots as code** - define them once, regenerate with one command.
18
+
19
+ - **Visual picker** - Point and click to select elements, generates config for you
20
+ - **Multi-variant** - Desktop, tablet, mobile + light/dark from a single definition
21
+ - **CI/CD ready** - Automate updates with encrypted session support
22
+
23
+ <table align="center">
24
+ <tr>
25
+ <th></th>
26
+ <th>Light</th>
27
+ <th>Dark</th>
28
+ </tr>
29
+ <tr>
30
+ <th>Desktop</th>
31
+ <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-desktop-light.png?raw=true" alt="Desktop Light"></td>
32
+ <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-desktop-dark.png?raw=true" alt="Desktop Dark"></td>
33
+ </tr>
34
+ <tr>
35
+ <th>Tablet</th>
36
+ <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-tablet-light.png?raw=true" alt="Tablet Light"></td>
37
+ <td><img src="https://github.com/omachala/heroshot/blob/main/docs/public/screenshots/hero-tablet-dark.png?raw=true" alt="Tablet Dark"></td>
38
+ </tr>
39
+ <tr>
40
+ <th>Mobile</th>
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>
20
45
 
21
46
  ## Get Started
22
47
 
@@ -58,7 +83,7 @@ Heroshot automates **documentation screenshots** - not visual regression testing
58
83
 
59
84
  ## Automated Updates
60
85
 
61
- Run heroshot in CI to keep screenshots always current. See the [full guide](https://heroshot.sh/guide/automated-updates).
86
+ Run heroshot in CI to keep screenshots always current. See the [full guide](https://heroshot.sh/docs/guide/automated-updates).
62
87
 
63
88
  **Quick setup:**
64
89
 
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { existsSync as existsSync4, readFileSync as readFileSync4, rmSync } from "fs";
5
- import path5 from "path";
5
+ import path6 from "path";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/browser.ts
@@ -18,9 +18,18 @@ import path from "path";
18
18
 
19
19
  // src/schema.ts
20
20
  import { z } from "zod";
21
+
22
+ // src/utils/generateUid.ts
21
23
  function generateUid() {
22
- return Math.random().toString(36).slice(2, 10);
24
+ return crypto.randomUUID().slice(0, 8);
23
25
  }
26
+
27
+ // src/schema.ts
28
+ var VIEWPORT_PRESETS = {
29
+ desktop: { width: 1280, height: 800 },
30
+ tablet: { width: 768, height: 1024 },
31
+ mobile: { width: 375, height: 667 }
32
+ };
24
33
  var viewportSchema = z.object({
25
34
  width: z.number().int().positive().default(1280),
26
35
  height: z.number().int().positive().default(800)
@@ -39,7 +48,7 @@ var scrollPositionSchema = z.object({
39
48
  });
40
49
  var viewportVariantSchema = z.string().refine(
41
50
  (value) => {
42
- if (["desktop", "tablet", "mobile"].includes(value)) return true;
51
+ if (value in VIEWPORT_PRESETS) return true;
43
52
  const match = /^(\d+)x(\d+)$/.exec(value);
44
53
  if (!match) return false;
45
54
  const width = parseInt(match[1] ?? "0", 10);
@@ -70,6 +79,42 @@ var browserSchema = z.object({
70
79
  /** Device scale factor for retina/high-DPI screenshots (1 = standard, 2 = retina) */
71
80
  deviceScaleFactor: z.number().min(1).max(3).optional()
72
81
  });
82
+ var shotCliOptionsSchema = z.object({
83
+ /** CSS selector(s) to capture - if multiple, captures bounding box of all */
84
+ selector: z.array(z.string()).optional(),
85
+ /** Output filename (auto-generated from URL if not provided) */
86
+ output: z.string().optional(),
87
+ /** Padding around element in pixels */
88
+ padding: z.number().int().min(0).optional(),
89
+ /** Viewport width */
90
+ width: z.number().int().positive().optional(),
91
+ /** Viewport height */
92
+ height: z.number().int().positive().optional(),
93
+ /** Use mobile viewport preset (375x667) */
94
+ mobile: z.boolean().optional(),
95
+ /** Use tablet viewport preset (768x1024) */
96
+ tablet: z.boolean().optional(),
97
+ /** Use desktop viewport preset (1280x800) */
98
+ desktop: z.boolean().optional(),
99
+ /** Force dark color scheme */
100
+ dark: z.boolean().optional(),
101
+ /** Force light color scheme */
102
+ light: z.boolean().optional(),
103
+ /** Device scale factor (1, 2, 3) */
104
+ scale: z.number().min(1).max(3).optional(),
105
+ /** Shortcut for scale=2 */
106
+ retina: z.boolean().optional(),
107
+ /** JPEG quality (1-100) - outputs JPEG instead of PNG */
108
+ quality: z.number().int().min(1).max(100).optional(),
109
+ /** Omit background for transparent PNG */
110
+ omitBackground: z.boolean().optional(),
111
+ /** Timeout in milliseconds */
112
+ timeout: z.number().int().positive().optional()
113
+ });
114
+ var shotCommandOptionsSchema = shotCliOptionsSchema.extend({
115
+ /** Save screenshot definition to config file */
116
+ save: z.boolean().optional()
117
+ });
73
118
  var configSchema = z.object({
74
119
  /** Output directory for screenshots (relative to config file) */
75
120
  outputDirectory: z.string().default("heroshots"),
@@ -622,19 +667,35 @@ async function setup(options = {}) {
622
667
 
623
668
  // src/sync.ts
624
669
  import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
670
+ import path5 from "path";
671
+
672
+ // src/utils/addSuffix.ts
625
673
  import path4 from "path";
626
- var VIEWPORT_DESKTOP = { width: 1280, height: 800 };
627
- var VIEWPORT_TABLET = { width: 768, height: 1024 };
628
- var VIEWPORT_MOBILE = { width: 375, height: 667 };
674
+ function addSuffix(filename, suffix) {
675
+ const extension = path4.extname(filename);
676
+ const base = path4.basename(filename, extension);
677
+ const directory = path4.dirname(filename);
678
+ return path4.join(directory, `${base}${suffix}${extension}`);
679
+ }
680
+
681
+ // src/utils/getColorSchemes.ts
682
+ function getColorSchemes(setting) {
683
+ if (setting === "auto") return [];
684
+ if (setting === "light") return ["light"];
685
+ if (setting === "dark") return ["dark"];
686
+ return ["light", "dark"];
687
+ }
688
+
689
+ // src/utils/parseViewport.ts
629
690
  function parseViewport(variant) {
630
691
  if (variant === "desktop") {
631
- return { name: "desktop", ...VIEWPORT_DESKTOP };
692
+ return { name: "desktop", ...VIEWPORT_PRESETS.desktop };
632
693
  }
633
694
  if (variant === "tablet") {
634
- return { name: "tablet", ...VIEWPORT_TABLET };
695
+ return { name: "tablet", ...VIEWPORT_PRESETS.tablet };
635
696
  }
636
697
  if (variant === "mobile") {
637
- return { name: "mobile", ...VIEWPORT_MOBILE };
698
+ return { name: "mobile", ...VIEWPORT_PRESETS.mobile };
638
699
  }
639
700
  const match = /^(\d+)x(\d+)$/.exec(variant);
640
701
  if (match) {
@@ -645,8 +706,10 @@ function parseViewport(variant) {
645
706
  return { name: variant, width, height };
646
707
  }
647
708
  }
648
- return { name: "desktop", ...VIEWPORT_DESKTOP };
709
+ return { name: "desktop", ...VIEWPORT_PRESETS.desktop };
649
710
  }
711
+
712
+ // src/sync.ts
650
713
  var GET_BACKGROUND_COLOR_SCRIPT = String.raw`
651
714
  (element) => {
652
715
  const toHex = (bgColor) => {
@@ -777,12 +840,6 @@ async function findElement(page, selector, maxAttempts = 10, intervalMs = 500) {
777
840
  }
778
841
  return null;
779
842
  }
780
- function addFilenameSuffix(filename, suffix) {
781
- const extension = path4.extname(filename);
782
- const base = path4.basename(filename, extension);
783
- const directory = path4.dirname(filename);
784
- return path4.join(directory, `${base}${suffix}${extension}`);
785
- }
786
843
  async function takeScreenshot(target, outputPath, format, quality, clip) {
787
844
  const isPage = "goto" in target;
788
845
  if (format === "jpeg") {
@@ -804,7 +861,7 @@ async function takeScreenshot(target, outputPath, format, quality, clip) {
804
861
  async function captureScreenshot(page, screenshot, outputDirectory, captureOptions, filenameSuffix = "") {
805
862
  const { name, url, selector, filename, padding, scroll, maskPadding } = screenshot;
806
863
  const { format, quality } = captureOptions;
807
- const finalFilename = filenameSuffix ? addFilenameSuffix(filename, filenameSuffix) : filename;
864
+ const finalFilename = filenameSuffix ? addSuffix(filename, filenameSuffix) : filename;
808
865
  verbose(`Capturing: ${name}${filenameSuffix}`);
809
866
  try {
810
867
  await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
@@ -817,8 +874,8 @@ async function captureScreenshot(page, screenshot, outputDirectory, captureOptio
817
874
  await page.evaluate(`window.scrollTo(${scroll.x}, ${scroll.y})`);
818
875
  await page.waitForTimeout(100);
819
876
  }
820
- const outputPath = path4.join(outputDirectory, finalFilename);
821
- const outputDirectoryPath = path4.dirname(outputPath);
877
+ const outputPath = path5.join(outputDirectory, finalFilename);
878
+ const outputDirectoryPath = path5.dirname(outputPath);
822
879
  if (!existsSync3(outputDirectoryPath)) {
823
880
  mkdirSync3(outputDirectoryPath, { recursive: true });
824
881
  }
@@ -882,15 +939,9 @@ async function captureScreenshot(page, screenshot, outputDirectory, captureOptio
882
939
  }
883
940
  return { success: true };
884
941
  }
885
- function getColorSchemes(setting) {
886
- if (setting === "auto") return [];
887
- if (setting === "light") return ["light"];
888
- if (setting === "dark") return ["dark"];
889
- return ["light", "dark"];
890
- }
891
942
  var RETRY_DELAYS = [500, 1e3, 2e3, 3e3, 5e3];
892
943
  async function captureAndLog(page, screenshot, outputDirectory, captureOptions, suffix) {
893
- const filename = suffix ? addFilenameSuffix(screenshot.filename, suffix) : screenshot.filename;
944
+ const filename = suffix ? addSuffix(screenshot.filename, suffix) : screenshot.filename;
894
945
  const { length: maxRetries } = RETRY_DELAYS;
895
946
  let result = { success: false };
896
947
  for (let attempt = 0; attempt < maxRetries; attempt++) {
@@ -948,7 +999,7 @@ function showResults(results, outputDirectory) {
948
999
  }
949
1000
  async function sync(options = {}) {
950
1001
  const configPath = options.configPath ?? getConfigPath();
951
- const config = loadConfig(configPath);
1002
+ const config = options.config ?? loadConfig(configPath);
952
1003
  if (config.screenshots.length === 0) {
953
1004
  warn("No screenshots defined.");
954
1005
  outro('Run "heroshot config" to add screenshots');
@@ -967,9 +1018,14 @@ async function sync(options = {}) {
967
1018
  const names = screenshots.map(({ name }) => name).join(", ");
968
1019
  verbose(`Matched ${screenshots.length}: ${names}`);
969
1020
  }
970
- const configDirectory = path4.dirname(configPath);
971
- const projectRoot = path4.dirname(configDirectory);
972
- const outputDirectory = path4.resolve(projectRoot, config.outputDirectory);
1021
+ let outputDirectory;
1022
+ if (options.outputDirectory) {
1023
+ outputDirectory = path5.resolve(options.outputDirectory);
1024
+ } else {
1025
+ const configDirectory = path5.dirname(configPath);
1026
+ const projectRoot = path5.dirname(configDirectory);
1027
+ outputDirectory = path5.resolve(projectRoot, config.outputDirectory);
1028
+ }
973
1029
  const storageState = loadEncryptedSession(options.sessionKey);
974
1030
  const captureSpinner = spinner();
975
1031
  captureSpinner.start("Launching browser...");
@@ -1057,8 +1113,24 @@ async function sync(options = {}) {
1057
1113
  return showResults(results, config.outputDirectory);
1058
1114
  }
1059
1115
 
1116
+ // src/utils/generateScreenshotFilename.ts
1117
+ function generateScreenshotFilename(url, selector) {
1118
+ try {
1119
+ const parsed = new URL(url);
1120
+ const parts = [parsed.hostname, ...parsed.pathname.split("/").filter(Boolean)];
1121
+ let base = parts.join("-").replaceAll(/[^\w-]/g, "-").replaceAll(/-+/g, "-");
1122
+ if (selector) {
1123
+ const selectorPart = selector.replaceAll(/[^\w-]/g, "-").replaceAll(/-+/g, "-").slice(0, 20);
1124
+ base = `${base}-${selectorPart}`;
1125
+ }
1126
+ return `${base || "screenshot"}.png`;
1127
+ } catch {
1128
+ return "screenshot.png";
1129
+ }
1130
+ }
1131
+
1060
1132
  // src/cli.ts
1061
- var packageJsonPath = path5.join(import.meta.dirname, "..", "package.json");
1133
+ var packageJsonPath = path6.join(import.meta.dirname, "..", "package.json");
1062
1134
  var packageJson = JSON.parse(readFileSync4(packageJsonPath, "utf8"));
1063
1135
  var version = packageJson && typeof packageJson === "object" && "version" in packageJson ? String(packageJson.version) : "0.0.0";
1064
1136
  var program = new Command();
@@ -1067,27 +1139,138 @@ program.name("heroshot").description("Define your screenshots once, update them
1067
1139
  setVerbose(options.verbose ?? false);
1068
1140
  intro(version);
1069
1141
  });
1070
- program.command("run", { isDefault: true, hidden: true }).description("Run heroshot (setup if no config, otherwise sync)").action(async () => {
1071
- const options = program.opts();
1072
- const configPath = options.config ? path5.resolve(options.config) : getConfigPath();
1073
- if (existsSync4(configPath)) {
1074
- const result = await sync({ configPath, sessionKey: options.sessionKey });
1075
- if (result.failed > 0) {
1076
- process.exitCode = 1;
1142
+ function buildScreenshotEntry(url, options) {
1143
+ const selectorValue = options?.selector?.[0];
1144
+ const filename = options?.output ?? generateScreenshotFilename(url, selectorValue);
1145
+ const screenshot = {
1146
+ id: generateUid(),
1147
+ name: path6.basename(filename, path6.extname(filename)),
1148
+ url,
1149
+ filename,
1150
+ selector: selectorValue
1151
+ };
1152
+ if (options?.padding) {
1153
+ screenshot.padding = {
1154
+ top: options.padding,
1155
+ right: options.padding,
1156
+ bottom: options.padding,
1157
+ left: options.padding
1158
+ };
1159
+ }
1160
+ if (options?.mobile) {
1161
+ screenshot.viewports = ["mobile"];
1162
+ } else if (options?.tablet) {
1163
+ screenshot.viewports = ["tablet"];
1164
+ } else if (options?.desktop) {
1165
+ screenshot.viewports = ["desktop"];
1166
+ }
1167
+ return screenshot;
1168
+ }
1169
+ function getColorScheme(options) {
1170
+ if (options?.dark) return "dark";
1171
+ if (options?.light) return "light";
1172
+ return void 0;
1173
+ }
1174
+ function getDeviceScaleFactor(options, existingConfig) {
1175
+ if (options?.retina) return 2;
1176
+ if (options?.scale) return options.scale;
1177
+ return existingConfig?.browser?.deviceScaleFactor;
1178
+ }
1179
+ function getViewport(options, existingConfig) {
1180
+ if (options?.mobile) return VIEWPORT_PRESETS.mobile;
1181
+ if (options?.tablet) return VIEWPORT_PRESETS.tablet;
1182
+ if (options?.desktop) return VIEWPORT_PRESETS.desktop;
1183
+ if (options?.width || options?.height) {
1184
+ const base = existingConfig?.browser?.viewport;
1185
+ return {
1186
+ width: options?.width ?? base?.width ?? 1280,
1187
+ height: options?.height ?? base?.height ?? 800
1188
+ };
1189
+ }
1190
+ return existingConfig?.browser?.viewport;
1191
+ }
1192
+ function buildShotConfig(url, options, existingConfig) {
1193
+ const screenshot = buildScreenshotEntry(url, options);
1194
+ const outputFormat = options?.quality ? "jpeg" : existingConfig?.outputFormat ?? "png";
1195
+ return {
1196
+ outputDirectory: ".",
1197
+ outputFormat,
1198
+ jpegQuality: options?.quality ?? existingConfig?.jpegQuality ?? 80,
1199
+ browser: {
1200
+ viewport: getViewport(options, existingConfig),
1201
+ colorScheme: getColorScheme(options),
1202
+ deviceScaleFactor: getDeviceScaleFactor(options, existingConfig)
1203
+ },
1204
+ screenshots: [screenshot]
1205
+ };
1206
+ }
1207
+ function saveScreenshotToConfig(configPath, screenshot, shotConfig, existingConfig) {
1208
+ const configForSave = existingConfig ?? loadConfig("");
1209
+ if (shotConfig.browser?.colorScheme) {
1210
+ configForSave.browser = {
1211
+ ...configForSave.browser,
1212
+ colorScheme: shotConfig.browser.colorScheme
1213
+ };
1214
+ }
1215
+ if (shotConfig.browser?.deviceScaleFactor) {
1216
+ configForSave.browser = {
1217
+ ...configForSave.browser,
1218
+ deviceScaleFactor: shotConfig.browser.deviceScaleFactor
1219
+ };
1220
+ }
1221
+ configForSave.screenshots.push(screenshot);
1222
+ saveConfig(configPath, configForSave);
1223
+ verbose(`Saved to config: ${screenshot.name}`);
1224
+ }
1225
+ async function handleUrlCapture(url, options, configPath, sessionKey) {
1226
+ const existingConfig = existsSync4(configPath) ? loadConfig(configPath) : void 0;
1227
+ const shotConfig = buildShotConfig(url, options, existingConfig);
1228
+ const outputDirectory = options?.output ? path6.dirname(path6.resolve(options.output)) : process.cwd();
1229
+ const result = await sync({
1230
+ config: shotConfig,
1231
+ outputDirectory,
1232
+ sessionKey
1233
+ });
1234
+ if (options?.save && result.failed === 0) {
1235
+ const screenshot = shotConfig.screenshots[0];
1236
+ if (screenshot) {
1237
+ saveScreenshotToConfig(configPath, screenshot, shotConfig, existingConfig);
1077
1238
  }
1239
+ }
1240
+ return result.failed === 0;
1241
+ }
1242
+ async function handleDefaultCommand(configPath, sessionKey, hasExplicitConfig) {
1243
+ if (existsSync4(configPath)) {
1244
+ const result = await sync({ configPath, sessionKey });
1245
+ return result.failed === 0;
1246
+ }
1247
+ if (hasExplicitConfig) {
1248
+ error(`Config file not found: ${configPath}`);
1249
+ return false;
1250
+ }
1251
+ const { hasScreenshots } = await setup();
1252
+ if (hasScreenshots) {
1253
+ const result = await sync({});
1254
+ return result.failed === 0;
1255
+ }
1256
+ return true;
1257
+ }
1258
+ program.command("shot [url]", { isDefault: true, hidden: true }).description("Take a screenshot (URL capture mode, or sync if no URL)").option("--selector <selector...>", "CSS selector(s) to capture").option("-o, --output <file>", "Output filename").option("-p, --padding <pixels>", "Padding around element", parseInt).option("-w, --width <pixels>", "Viewport width", parseInt).option("--height <pixels>", "Viewport height", parseInt).option("--mobile", "Use mobile viewport (375x667)").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)", parseInt).option("--retina", "Use retina scale (2x)").option("-q, --quality <percent>", "JPEG quality (1-100), outputs JPEG", parseInt).option("--omit-background", "Transparent background (PNG only)").option("--timeout <ms>", "Timeout in milliseconds", parseInt).option("--save", "Save screenshot definition to config").action(async (url, options) => {
1259
+ const globalOptions = program.opts();
1260
+ const configPath = globalOptions.config ? path6.resolve(globalOptions.config) : getConfigPath();
1261
+ if (url?.startsWith("http")) {
1262
+ const success2 = await handleUrlCapture(url, options, configPath, globalOptions.sessionKey);
1263
+ if (!success2) process.exitCode = 1;
1264
+ } else if (url) {
1265
+ const result = await sync({ configPath, sessionKey: globalOptions.sessionKey, filter: url });
1266
+ if (result.failed > 0) process.exitCode = 1;
1078
1267
  } else {
1079
- if (options.config) {
1080
- error(`Config file not found: ${configPath}`);
1081
- process.exitCode = 1;
1082
- return;
1083
- }
1084
- const { hasScreenshots } = await setup();
1085
- if (hasScreenshots) {
1086
- const result = await sync({});
1087
- if (result.failed > 0) {
1088
- process.exitCode = 1;
1089
- }
1090
- }
1268
+ const success2 = await handleDefaultCommand(
1269
+ configPath,
1270
+ globalOptions.sessionKey,
1271
+ !!globalOptions.config
1272
+ );
1273
+ if (!success2) process.exitCode = 1;
1091
1274
  }
1092
1275
  });
1093
1276
  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(
@@ -1105,7 +1288,7 @@ program.command("config").description("Open browser to add/edit screenshot defin
1105
1288
  else if (commandOptions.dark) colorScheme = "dark";
1106
1289
  const { hasScreenshots } = await setup({ colorScheme });
1107
1290
  if (hasScreenshots && !commandOptions.only) {
1108
- const configPath = globalOptions.config ? path5.resolve(globalOptions.config) : void 0;
1291
+ const configPath = globalOptions.config ? path6.resolve(globalOptions.config) : void 0;
1109
1292
  const result = await sync({ configPath, sessionKey: globalOptions.sessionKey });
1110
1293
  if (result.failed > 0) {
1111
1294
  process.exitCode = 1;
@@ -1115,7 +1298,7 @@ program.command("config").description("Open browser to add/edit screenshot defin
1115
1298
  );
1116
1299
  program.command("sync [pattern]").description("Capture screenshots (optionally filter by pattern)").action(async (pattern) => {
1117
1300
  const options = program.opts();
1118
- const configPath = options.config ? path5.resolve(options.config) : getConfigPath();
1301
+ const configPath = options.config ? path6.resolve(options.config) : getConfigPath();
1119
1302
  if (!existsSync4(configPath)) {
1120
1303
  error('No config found. Run "heroshot config" first.');
1121
1304
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heroshot",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Define your screenshots once, update them forever with one command",
5
5
  "type": "module",
6
6
  "author": "Ondrej Machala",
@@ -73,6 +73,7 @@
73
73
  "build:toolbar": "vite build --config toolbar/vite.config.ts",
74
74
  "test": "vitest",
75
75
  "test:run": "vitest run",
76
+ "test:cli": "vitest run --config vitest.cli.config.ts",
76
77
  "test:toolbar": "vitest run --config toolbar/vite.config.ts",
77
78
  "test:toolbar:coverage": "vitest run --config toolbar/vite.config.ts --coverage",
78
79
  "test:toolbar:e2e": "playwright test --config toolbar/playwright.config.ts",
@@ -80,6 +81,7 @@
80
81
  "typecheck:toolbar": "tsc --noEmit -p toolbar/tsconfig.json --incremental --tsBuildInfoFile node_modules/.cache/tsbuildinfo-toolbar",
81
82
  "lint": "eslint --cache --cache-location node_modules/.cache/eslint src/",
82
83
  "lint:toolbar": "eslint --cache --cache-location node_modules/.cache/eslint-toolbar toolbar/src/",
84
+ "check:svelte": "pnpm build:toolbar 2>&1 | grep -q '\\[vite-plugin-svelte\\].*warning' && echo 'Svelte warnings found!' && exit 1 || exit 0",
83
85
  "lint:fix": "eslint --cache --cache-location node_modules/.cache/eslint src/ --fix",
84
86
  "format": "prettier --write . --cache --cache-location node_modules/.cache/prettier",
85
87
  "format:check": "prettier --check . --cache --cache-location node_modules/.cache/prettier",
@@ -3783,10 +3783,10 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
3783
3783
  function findElementBySelector(selector) {
3784
3784
  return selector.includes(">>>") ? querySelectorPiercing(selector) : document.querySelector(selector);
3785
3785
  }
3786
- var root_4$1 = /* @__PURE__ */ from_html(`<div class="fixed pointer-events-auto cursor-pointer"></div>`);
3787
- var root_5$1 = /* @__PURE__ */ from_html(`<div class="fixed pointer-events-auto cursor-pointer"></div>`);
3788
- var root_6$1 = /* @__PURE__ */ from_html(`<div class="fixed pointer-events-auto cursor-pointer"></div>`);
3789
- var root_7$1 = /* @__PURE__ */ from_html(`<div class="fixed pointer-events-auto cursor-pointer"></div>`);
3786
+ var root_4$1 = /* @__PURE__ */ from_html(`<div role="button" tabindex="-1" class="fixed pointer-events-auto cursor-pointer"></div>`);
3787
+ var root_5$1 = /* @__PURE__ */ from_html(`<div role="button" tabindex="-1" class="fixed pointer-events-auto cursor-pointer"></div>`);
3788
+ var root_6$1 = /* @__PURE__ */ from_html(`<div role="button" tabindex="-1" class="fixed pointer-events-auto cursor-pointer"></div>`);
3789
+ var root_7$1 = /* @__PURE__ */ from_html(`<div role="button" tabindex="-1" class="fixed pointer-events-auto cursor-pointer"></div>`);
3790
3790
  var root_8$1 = /* @__PURE__ */ from_html(`<div class="fixed h-0.5 bg-heroshot-primary/50 pointer-events-none"></div>`);
3791
3791
  var root_9$1 = /* @__PURE__ */ from_html(`<div class="fixed h-0.5 bg-heroshot-primary/50 pointer-events-none"></div>`);
3792
3792
  var root_10$1 = /* @__PURE__ */ from_html(`<div class="fixed w-0.5 bg-heroshot-primary/50 pointer-events-none"></div>`);
@@ -3794,7 +3794,7 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
3794
3794
  var root_3 = /* @__PURE__ */ from_html(`<!> <!> <!> <!> <!> <!> <!> <!>`, 1);
3795
3795
  var root_2$1 = /* @__PURE__ */ from_html(`<!> <div></div> <div class="fixed bg-white rounded-sm pointer-events-auto -translate-x-1/2 -translate-y-1/2" role="button" tabindex="0"></div> <div class="fixed bg-white rounded-sm pointer-events-auto -translate-x-1/2 -translate-y-1/2" role="button" tabindex="0"></div> <div class="fixed bg-white rounded-sm pointer-events-auto -translate-x-1/2 -translate-y-1/2" role="button" tabindex="0"></div> <div class="fixed bg-white rounded-sm pointer-events-auto -translate-x-1/2 -translate-y-1/2" role="button" tabindex="0"></div> <div class="fixed bg-white rounded-sm pointer-events-auto -translate-x-1/2 -translate-y-1/2" role="button" tabindex="0"></div> <div class="fixed bg-white rounded-sm pointer-events-auto -translate-x-1/2 -translate-y-1/2" role="button" tabindex="0"></div> <div class="fixed bg-white rounded-sm pointer-events-auto -translate-x-1/2 -translate-y-1/2" role="button" tabindex="0"></div> <div class="fixed bg-white rounded-sm pointer-events-auto -translate-x-1/2 -translate-y-1/2" role="button" tabindex="0"></div>`, 1);
3796
3796
  var root_12 = /* @__PURE__ */ from_html(`<div class="fixed border-[3px] pointer-events-none box-border border-heroshot-primary bg-heroshot-primary/10"></div>`);
3797
- var root_1$3 = /* @__PURE__ */ from_html(`<div class="fixed inset-0 w-screen h-screen z-[2147483646] pointer-events-none"><div></div> <div></div> <div></div> <div></div> <!></div>`);
3797
+ var root_1$3 = /* @__PURE__ */ from_html(`<div class="fixed inset-0 w-screen h-screen z-[2147483646] pointer-events-none"><div role="button" tabindex="-1"></div> <div role="button" tabindex="-1"></div> <div role="button" tabindex="-1"></div> <div role="button" tabindex="-1"></div> <!></div>`);
3798
3798
  var root_14 = /* @__PURE__ */ from_html(`<span style="color:#fbbf24;"> </span>`);
3799
3799
  var root_15 = /* @__PURE__ */ from_html(`<span style="color:#67e8f9;"> </span>`);
3800
3800
  var root_16 = /* @__PURE__ */ from_html(`<span style="color:#22c55e;"> </span>`);
@@ -4220,12 +4220,16 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
4220
4220
  var div = root_1$3();
4221
4221
  var div_1 = child(div);
4222
4222
  div_1.__click = handleOverlayClick;
4223
+ div_1.__keydown = (event2) => event2.key === "Enter" && handleOverlayClick();
4223
4224
  var div_2 = sibling(div_1, 2);
4224
4225
  div_2.__click = handleOverlayClick;
4226
+ div_2.__keydown = (event2) => event2.key === "Enter" && handleOverlayClick();
4225
4227
  var div_3 = sibling(div_2, 2);
4226
4228
  div_3.__click = handleOverlayClick;
4229
+ div_3.__keydown = (event2) => event2.key === "Enter" && handleOverlayClick();
4227
4230
  var div_4 = sibling(div_3, 2);
4228
4231
  div_4.__click = handleOverlayClick;
4232
+ div_4.__keydown = (event2) => event2.key === "Enter" && handleOverlayClick();
4229
4233
  var node_1 = sibling(div_4, 2);
4230
4234
  {
4231
4235
  var consequent_9 = ($$anchor3) => {
@@ -4239,6 +4243,7 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
4239
4243
  var consequent = ($$anchor5) => {
4240
4244
  var div_5 = root_4$1();
4241
4245
  div_5.__click = handlePaddingClick;
4246
+ div_5.__keydown = (event2) => event2.key === "Enter" && handlePaddingClick();
4242
4247
  div_5.__mousemove = handlePaddingMouseMove;
4243
4248
  template_effect(() => set_style(div_5, `top:${get(expandedRect).top ?? ""}px;left:${get(expandedRect).left ?? ""}px;width:${get(expandedRect).width ?? ""}px;height:${get(selectedPadding).top ?? ""}px;background:${(get(maskPadding) ? get(detectedBgColor) : "rgba(34, 197, 94, 0.25)") ?? ""};`));
4244
4249
  event("mouseenter", div_5, handlePaddingMouseMove);
@@ -4254,6 +4259,7 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
4254
4259
  var consequent_1 = ($$anchor5) => {
4255
4260
  var div_6 = root_5$1();
4256
4261
  div_6.__click = handlePaddingClick;
4262
+ div_6.__keydown = (event2) => event2.key === "Enter" && handlePaddingClick();
4257
4263
  div_6.__mousemove = handlePaddingMouseMove;
4258
4264
  template_effect(() => set_style(div_6, `top:${get(overlayRects).highlight.top + get(overlayRects).highlight.height}px;left:${get(expandedRect).left ?? ""}px;width:${get(expandedRect).width ?? ""}px;height:${get(selectedPadding).bottom ?? ""}px;background:${(get(maskPadding) ? get(detectedBgColor) : "rgba(34, 197, 94, 0.25)") ?? ""};`));
4259
4265
  event("mouseenter", div_6, handlePaddingMouseMove);
@@ -4269,6 +4275,7 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
4269
4275
  var consequent_2 = ($$anchor5) => {
4270
4276
  var div_7 = root_6$1();
4271
4277
  div_7.__click = handlePaddingClick;
4278
+ div_7.__keydown = (event2) => event2.key === "Enter" && handlePaddingClick();
4272
4279
  div_7.__mousemove = handlePaddingMouseMove;
4273
4280
  template_effect(() => set_style(div_7, `top:${get(overlayRects).highlight.top ?? ""}px;left:${get(expandedRect).left ?? ""}px;width:${get(selectedPadding).left ?? ""}px;height:${get(overlayRects).highlight.height ?? ""}px;background:${(get(maskPadding) ? get(detectedBgColor) : "rgba(34, 197, 94, 0.25)") ?? ""};`));
4274
4281
  event("mouseenter", div_7, handlePaddingMouseMove);
@@ -4284,6 +4291,7 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
4284
4291
  var consequent_3 = ($$anchor5) => {
4285
4292
  var div_8 = root_7$1();
4286
4293
  div_8.__click = handlePaddingClick;
4294
+ div_8.__keydown = (event2) => event2.key === "Enter" && handlePaddingClick();
4287
4295
  div_8.__mousemove = handlePaddingMouseMove;
4288
4296
  template_effect(() => set_style(div_8, `top:${get(overlayRects).highlight.top ?? ""}px;left:${get(overlayRects).highlight.left + get(overlayRects).highlight.width}px;width:${get(selectedPadding).right ?? ""}px;height:${get(overlayRects).highlight.height ?? ""}px;background:${(get(maskPadding) ? get(detectedBgColor) : "rgba(34, 197, 94, 0.25)") ?? ""};`));
4289
4297
  event("mouseenter", div_8, handlePaddingMouseMove);
@@ -4464,7 +4472,7 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
4464
4472
  append($$anchor, fragment);
4465
4473
  return pop($$exports);
4466
4474
  }
4467
- delegate(["click", "mousemove", "mousedown"]);
4475
+ delegate(["click", "keydown", "mousemove", "mousedown"]);
4468
4476
  var root_2 = /* @__PURE__ */ from_html(`<p class="text-xs text-slate-500 mt-2">Will capture two screenshots: -light and -dark variants</p>`);
4469
4477
  var root_1$2 = /* @__PURE__ */ from_html(`<div class="fixed inset-0 bg-black/50 z-[2147483647] flex items-center justify-center pointer-events-auto" role="button" tabindex="0"><div class="bg-slate-800 rounded-lg p-6 w-80 shadow-2xl" role="dialog" aria-modal="true" aria-label="Settings" tabindex="-1"><h2 class="text-lg font-semibold text-white mb-4">Settings</h2> <div class="mb-4"><span class="block text-sm text-slate-400 mb-2">Viewport Size</span> <div class="flex gap-2 items-center"><label class="sr-only" for="viewport-width">Width</label> <input id="viewport-width" type="number" class="w-20 px-2 py-1 bg-slate-700 text-white rounded border border-slate-600 focus:border-blue-500 focus:outline-none" min="320" max="3840"/> <span class="text-slate-400">x</span> <label class="sr-only" for="viewport-height">Height</label> <input id="viewport-height" type="number" class="w-20 px-2 py-1 bg-slate-700 text-white rounded border border-slate-600 focus:border-blue-500 focus:outline-none" min="200" max="2160"/> <span class="text-slate-500 text-sm">px</span></div></div> <div class="mb-4"><span class="block text-sm text-slate-400 mb-2">Scale (Retina)</span> <div class="flex gap-2"><button type="button">1x</button> <button type="button">2x</button> <button type="button">3x</button></div> <p class="text-xs text-slate-500 mt-2">Higher scale = sharper images, larger file size</p></div> <div class="mb-6"><span class="block text-sm text-slate-400 mb-2">Color Scheme</span> <div class="flex gap-2"><button type="button" title="Capture both light and dark versions (default)">Both</button> <button type="button" title="Use browser's color scheme preference">Auto</button> <button type="button">Light</button> <button type="button">Dark</button></div> <!></div> <div class="flex justify-end gap-2"><button type="button" class="px-4 py-2 rounded bg-slate-700 text-white hover:bg-slate-600 transition-colors">Cancel</button> <button type="button" class="px-4 py-2 rounded bg-green-500 text-white hover:bg-green-600 transition-colors">Save</button></div></div></div>`);
4470
4478
  function SettingsModal($$anchor, $$props) {