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 +36 -11
- package/dist/cli.js +235 -52
- package/package.json +3 -1
- package/toolbar/dist/toolbar.js +14 -6
package/README.md
CHANGED
|
@@ -5,18 +5,43 @@
|
|
|
5
5
|
<h1 align="center">heroshot</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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", ...
|
|
692
|
+
return { name: "desktop", ...VIEWPORT_PRESETS.desktop };
|
|
632
693
|
}
|
|
633
694
|
if (variant === "tablet") {
|
|
634
|
-
return { name: "tablet", ...
|
|
695
|
+
return { name: "tablet", ...VIEWPORT_PRESETS.tablet };
|
|
635
696
|
}
|
|
636
697
|
if (variant === "mobile") {
|
|
637
|
-
return { name: "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", ...
|
|
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 ?
|
|
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 =
|
|
821
|
-
const outputDirectoryPath =
|
|
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 ?
|
|
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
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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 =
|
|
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
|
-
|
|
1071
|
-
const
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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 ?
|
|
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 ?
|
|
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.
|
|
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",
|
package/toolbar/dist/toolbar.js
CHANGED
|
@@ -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) {
|