@reshotdev/screenshot 0.0.1-beta.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.
Files changed (59) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +388 -0
  3. package/package.json +64 -0
  4. package/src/commands/auth.js +259 -0
  5. package/src/commands/chrome.js +140 -0
  6. package/src/commands/ci-run.js +123 -0
  7. package/src/commands/ci-setup.js +288 -0
  8. package/src/commands/drifts.js +423 -0
  9. package/src/commands/import-tests.js +309 -0
  10. package/src/commands/ingest.js +458 -0
  11. package/src/commands/init.js +633 -0
  12. package/src/commands/publish.js +1721 -0
  13. package/src/commands/pull.js +303 -0
  14. package/src/commands/record.js +94 -0
  15. package/src/commands/run.js +476 -0
  16. package/src/commands/setup-wizard.js +740 -0
  17. package/src/commands/setup.js +137 -0
  18. package/src/commands/status.js +275 -0
  19. package/src/commands/sync.js +621 -0
  20. package/src/commands/ui.js +248 -0
  21. package/src/commands/validate-docs.js +529 -0
  22. package/src/index.js +462 -0
  23. package/src/lib/api-client.js +815 -0
  24. package/src/lib/capture-engine.js +1623 -0
  25. package/src/lib/capture-script-runner.js +3120 -0
  26. package/src/lib/ci-detect.js +137 -0
  27. package/src/lib/config.js +1240 -0
  28. package/src/lib/diff-engine.js +642 -0
  29. package/src/lib/hash.js +74 -0
  30. package/src/lib/image-crop.js +396 -0
  31. package/src/lib/matrix.js +89 -0
  32. package/src/lib/output-path-template.js +318 -0
  33. package/src/lib/playwright-runner.js +252 -0
  34. package/src/lib/polished-clip.js +553 -0
  35. package/src/lib/privacy-engine.js +408 -0
  36. package/src/lib/progress-tracker.js +142 -0
  37. package/src/lib/record-browser-injection.js +654 -0
  38. package/src/lib/record-cdp.js +612 -0
  39. package/src/lib/record-clip.js +343 -0
  40. package/src/lib/record-config.js +623 -0
  41. package/src/lib/record-screenshot.js +360 -0
  42. package/src/lib/record-terminal.js +123 -0
  43. package/src/lib/recorder-service.js +781 -0
  44. package/src/lib/secrets.js +51 -0
  45. package/src/lib/selector-strategies.js +859 -0
  46. package/src/lib/standalone-mode.js +400 -0
  47. package/src/lib/storage-providers.js +569 -0
  48. package/src/lib/style-engine.js +684 -0
  49. package/src/lib/ui-api.js +4677 -0
  50. package/src/lib/ui-assets.js +373 -0
  51. package/src/lib/ui-executor.js +587 -0
  52. package/src/lib/variant-injector.js +591 -0
  53. package/src/lib/viewport-presets.js +454 -0
  54. package/src/lib/worker-pool.js +118 -0
  55. package/web/cropper/index.html +436 -0
  56. package/web/manager/dist/assets/index--ZgioErz.js +507 -0
  57. package/web/manager/dist/assets/index-n468W0Wr.css +1 -0
  58. package/web/manager/dist/index.html +27 -0
  59. package/web/subtitle-editor/index.html +295 -0
@@ -0,0 +1,454 @@
1
+ // viewport-presets.js - Robust viewport configuration with named presets
2
+ // Provides flexible viewport sizing with device presets, custom sizes, and crop regions
3
+
4
+ /**
5
+ * Standard device viewport presets
6
+ * Each preset includes viewport dimensions and optional deviceScaleFactor
7
+ */
8
+ const VIEWPORT_PRESETS = {
9
+ // Desktop presets
10
+ "desktop-hd": {
11
+ name: "Desktop HD",
12
+ category: "desktop",
13
+ width: 1920,
14
+ height: 1080,
15
+ deviceScaleFactor: 1,
16
+ description: "Full HD desktop (1920×1080)",
17
+ },
18
+ "desktop": {
19
+ name: "Desktop",
20
+ category: "desktop",
21
+ width: 1280,
22
+ height: 720,
23
+ deviceScaleFactor: 1,
24
+ description: "Standard desktop (1280×720)",
25
+ },
26
+ "desktop-small": {
27
+ name: "Desktop Small",
28
+ category: "desktop",
29
+ width: 1024,
30
+ height: 768,
31
+ deviceScaleFactor: 1,
32
+ description: "Small desktop/laptop (1024×768)",
33
+ },
34
+ "desktop-retina": {
35
+ name: "Desktop Retina",
36
+ category: "desktop",
37
+ width: 1280,
38
+ height: 720,
39
+ deviceScaleFactor: 2,
40
+ description: "Retina desktop (2x scale)",
41
+ },
42
+ "desktop-4k": {
43
+ name: "Desktop 4K",
44
+ category: "desktop",
45
+ width: 3840,
46
+ height: 2160,
47
+ deviceScaleFactor: 1,
48
+ description: "4K UHD desktop (3840×2160)",
49
+ },
50
+
51
+ // Tablet presets
52
+ "tablet-landscape": {
53
+ name: "Tablet Landscape",
54
+ category: "tablet",
55
+ width: 1024,
56
+ height: 768,
57
+ deviceScaleFactor: 2,
58
+ description: "iPad-like landscape (1024×768)",
59
+ },
60
+ "tablet-portrait": {
61
+ name: "Tablet Portrait",
62
+ category: "tablet",
63
+ width: 768,
64
+ height: 1024,
65
+ deviceScaleFactor: 2,
66
+ description: "iPad-like portrait (768×1024)",
67
+ },
68
+ "tablet-pro-landscape": {
69
+ name: "Tablet Pro Landscape",
70
+ category: "tablet",
71
+ width: 1366,
72
+ height: 1024,
73
+ deviceScaleFactor: 2,
74
+ description: "iPad Pro-like landscape",
75
+ },
76
+ "tablet-pro-portrait": {
77
+ name: "Tablet Pro Portrait",
78
+ category: "tablet",
79
+ width: 1024,
80
+ height: 1366,
81
+ deviceScaleFactor: 2,
82
+ description: "iPad Pro-like portrait",
83
+ },
84
+
85
+ // Mobile presets
86
+ "mobile": {
87
+ name: "Mobile",
88
+ category: "mobile",
89
+ width: 375,
90
+ height: 667,
91
+ deviceScaleFactor: 2,
92
+ description: "iPhone-like (375×667)",
93
+ },
94
+ "mobile-small": {
95
+ name: "Mobile Small",
96
+ category: "mobile",
97
+ width: 320,
98
+ height: 568,
99
+ deviceScaleFactor: 2,
100
+ description: "Small mobile (320×568)",
101
+ },
102
+ "mobile-large": {
103
+ name: "Mobile Large",
104
+ category: "mobile",
105
+ width: 414,
106
+ height: 896,
107
+ deviceScaleFactor: 3,
108
+ description: "Large mobile / iPhone Pro Max-like",
109
+ },
110
+ "mobile-landscape": {
111
+ name: "Mobile Landscape",
112
+ category: "mobile",
113
+ width: 667,
114
+ height: 375,
115
+ deviceScaleFactor: 2,
116
+ description: "Mobile landscape orientation",
117
+ },
118
+
119
+ // Documentation-specific presets
120
+ "docs-wide": {
121
+ name: "Docs Wide",
122
+ category: "docs",
123
+ width: 1200,
124
+ height: 800,
125
+ deviceScaleFactor: 2,
126
+ description: "Wide documentation screenshots",
127
+ },
128
+ "docs-standard": {
129
+ name: "Docs Standard",
130
+ category: "docs",
131
+ width: 960,
132
+ height: 640,
133
+ deviceScaleFactor: 2,
134
+ description: "Standard documentation screenshots",
135
+ },
136
+ "docs-narrow": {
137
+ name: "Docs Narrow",
138
+ category: "docs",
139
+ width: 720,
140
+ height: 480,
141
+ deviceScaleFactor: 2,
142
+ description: "Narrow documentation screenshots",
143
+ },
144
+
145
+ // Social/Marketing presets
146
+ "social-og": {
147
+ name: "Open Graph",
148
+ category: "social",
149
+ width: 1200,
150
+ height: 630,
151
+ deviceScaleFactor: 2,
152
+ description: "Open Graph / Facebook sharing (1200×630)",
153
+ },
154
+ "social-twitter": {
155
+ name: "Twitter Card",
156
+ category: "social",
157
+ width: 1200,
158
+ height: 600,
159
+ deviceScaleFactor: 2,
160
+ description: "Twitter card image (1200×600)",
161
+ },
162
+ "social-linkedin": {
163
+ name: "LinkedIn",
164
+ category: "social",
165
+ width: 1200,
166
+ height: 627,
167
+ deviceScaleFactor: 2,
168
+ description: "LinkedIn post image (1200×627)",
169
+ },
170
+ };
171
+
172
+ /**
173
+ * Crop region presets for common UI sections
174
+ */
175
+ const CROP_PRESETS = {
176
+ // Navigation regions
177
+ "header": {
178
+ name: "Header Only",
179
+ description: "Top navigation header region",
180
+ // Percentage-based - will be calculated based on viewport
181
+ percentBased: true,
182
+ region: { x: 0, y: 0, widthPercent: 100, heightPercent: 10 },
183
+ },
184
+ "sidebar": {
185
+ name: "Sidebar Only",
186
+ description: "Left sidebar navigation",
187
+ percentBased: true,
188
+ region: { x: 0, y: 0, widthPercent: 20, heightPercent: 100 },
189
+ },
190
+ "main-content": {
191
+ name: "Main Content",
192
+ description: "Main content area (excluding sidebar)",
193
+ percentBased: true,
194
+ region: { xPercent: 20, y: 0, widthPercent: 80, heightPercent: 100 },
195
+ },
196
+ "center-modal": {
197
+ name: "Center Modal",
198
+ description: "Centered modal dialog area",
199
+ percentBased: true,
200
+ region: { xPercent: 15, yPercent: 15, widthPercent: 70, heightPercent: 70 },
201
+ },
202
+ "full": {
203
+ name: "Full Viewport",
204
+ description: "No cropping - full viewport",
205
+ percentBased: false,
206
+ enabled: false,
207
+ },
208
+ };
209
+
210
+ /**
211
+ * Get a viewport preset by name
212
+ * @param {string} presetName - Name of the preset
213
+ * @returns {Object|null} Viewport configuration or null if not found
214
+ */
215
+ function getViewportPreset(presetName) {
216
+ return VIEWPORT_PRESETS[presetName] || null;
217
+ }
218
+
219
+ /**
220
+ * Get all viewport presets
221
+ * @returns {Object} All viewport presets keyed by name
222
+ */
223
+ function getAllViewportPresets() {
224
+ return { ...VIEWPORT_PRESETS };
225
+ }
226
+
227
+ /**
228
+ * Get viewport presets grouped by category
229
+ * @returns {Object} Presets grouped by category
230
+ */
231
+ function getViewportPresetsByCategory() {
232
+ const grouped = {};
233
+ for (const [key, preset] of Object.entries(VIEWPORT_PRESETS)) {
234
+ const category = preset.category || "other";
235
+ if (!grouped[category]) {
236
+ grouped[category] = [];
237
+ }
238
+ grouped[category].push({ key, ...preset });
239
+ }
240
+ return grouped;
241
+ }
242
+
243
+ /**
244
+ * Resolve viewport configuration from various inputs
245
+ * Supports:
246
+ * - Preset name: "desktop", "mobile", etc.
247
+ * - Shorthand: "1280x720" or "1280x720@2x"
248
+ * - Object: { width: 1280, height: 720, deviceScaleFactor: 2 }
249
+ *
250
+ * @param {string|Object} input - Viewport specification
251
+ * @param {Object} defaults - Default values to use
252
+ * @returns {Object} Resolved viewport config { width, height, deviceScaleFactor, presetName? }
253
+ */
254
+ function resolveViewport(input, defaults = { width: 1280, height: 720, deviceScaleFactor: 2 }) {
255
+ // Handle null/undefined - return defaults
256
+ if (input == null) {
257
+ return { ...defaults, presetName: null };
258
+ }
259
+
260
+ // Handle preset name string
261
+ if (typeof input === "string") {
262
+ // Check if it's a preset name
263
+ const preset = VIEWPORT_PRESETS[input];
264
+ if (preset) {
265
+ return {
266
+ width: preset.width,
267
+ height: preset.height,
268
+ deviceScaleFactor: preset.deviceScaleFactor || defaults.deviceScaleFactor,
269
+ presetName: input,
270
+ };
271
+ }
272
+
273
+ // Check for shorthand format: "WIDTHxHEIGHT" or "WIDTHxHEIGHT@Nx"
274
+ const shorthandMatch = input.match(/^(\d+)x(\d+)(?:@(\d+)x)?$/i);
275
+ if (shorthandMatch) {
276
+ const [, width, height, scale] = shorthandMatch;
277
+ return {
278
+ width: parseInt(width, 10),
279
+ height: parseInt(height, 10),
280
+ deviceScaleFactor: scale ? parseInt(scale, 10) : defaults.deviceScaleFactor,
281
+ presetName: null,
282
+ };
283
+ }
284
+
285
+ // Unknown string - return defaults
286
+ console.warn(`Unknown viewport specification: "${input}", using defaults`);
287
+ return { ...defaults, presetName: null };
288
+ }
289
+
290
+ // Handle object format
291
+ if (typeof input === "object") {
292
+ return {
293
+ width: input.width || defaults.width,
294
+ height: input.height || defaults.height,
295
+ deviceScaleFactor: input.deviceScaleFactor ?? input.dpr ?? defaults.deviceScaleFactor,
296
+ presetName: input.preset || input.presetName || null,
297
+ };
298
+ }
299
+
300
+ return { ...defaults, presetName: null };
301
+ }
302
+
303
+ /**
304
+ * Validate viewport configuration
305
+ * @param {Object} viewport - Viewport object to validate
306
+ * @returns {{ valid: boolean, error?: string }}
307
+ */
308
+ function validateViewport(viewport) {
309
+ if (!viewport) {
310
+ return { valid: false, error: "Viewport is required" };
311
+ }
312
+
313
+ const { width, height, deviceScaleFactor } = viewport;
314
+
315
+ if (typeof width !== "number" || width < 100 || width > 10000) {
316
+ return { valid: false, error: "Viewport width must be between 100 and 10000" };
317
+ }
318
+
319
+ if (typeof height !== "number" || height < 100 || height > 10000) {
320
+ return { valid: false, error: "Viewport height must be between 100 and 10000" };
321
+ }
322
+
323
+ if (deviceScaleFactor !== undefined) {
324
+ if (typeof deviceScaleFactor !== "number" || deviceScaleFactor < 1 || deviceScaleFactor > 4) {
325
+ return { valid: false, error: "deviceScaleFactor must be between 1 and 4" };
326
+ }
327
+ }
328
+
329
+ return { valid: true };
330
+ }
331
+
332
+ /**
333
+ * Resolve a crop region, supporting both absolute and percentage-based regions
334
+ * @param {Object} cropConfig - Crop configuration
335
+ * @param {Object} viewport - Current viewport { width, height }
336
+ * @returns {Object} Resolved absolute crop region { x, y, width, height }
337
+ */
338
+ function resolveCropRegion(cropConfig, viewport) {
339
+ if (!cropConfig || !cropConfig.region) {
340
+ return null;
341
+ }
342
+
343
+ const region = cropConfig.region;
344
+
345
+ // Check if this is a preset
346
+ if (cropConfig.preset && CROP_PRESETS[cropConfig.preset]) {
347
+ const presetConfig = CROP_PRESETS[cropConfig.preset];
348
+ if (!presetConfig.enabled && presetConfig.enabled !== undefined) {
349
+ return null; // Preset explicitly disabled (like "full")
350
+ }
351
+ return resolveCropRegion({ ...cropConfig, ...presetConfig }, viewport);
352
+ }
353
+
354
+ // Handle percentage-based regions
355
+ if (cropConfig.percentBased) {
356
+ const x = region.xPercent !== undefined
357
+ ? Math.round((region.xPercent / 100) * viewport.width)
358
+ : (region.x || 0);
359
+ const y = region.yPercent !== undefined
360
+ ? Math.round((region.yPercent / 100) * viewport.height)
361
+ : (region.y || 0);
362
+ const width = region.widthPercent !== undefined
363
+ ? Math.round((region.widthPercent / 100) * viewport.width)
364
+ : region.width;
365
+ const height = region.heightPercent !== undefined
366
+ ? Math.round((region.heightPercent / 100) * viewport.height)
367
+ : region.height;
368
+
369
+ return { x, y, width, height };
370
+ }
371
+
372
+ // Absolute region
373
+ return {
374
+ x: region.x || 0,
375
+ y: region.y || 0,
376
+ width: region.width,
377
+ height: region.height,
378
+ };
379
+ }
380
+
381
+ /**
382
+ * Get a crop preset by name
383
+ * @param {string} presetName - Name of the crop preset
384
+ * @returns {Object|null} Crop preset configuration or null
385
+ */
386
+ function getCropPreset(presetName) {
387
+ return CROP_PRESETS[presetName] || null;
388
+ }
389
+
390
+ /**
391
+ * Get all crop presets
392
+ * @returns {Object} All crop presets
393
+ */
394
+ function getAllCropPresets() {
395
+ return { ...CROP_PRESETS };
396
+ }
397
+
398
+ /**
399
+ * Create a custom viewport configuration
400
+ * @param {Object} options - Custom viewport options
401
+ * @returns {Object} Viewport configuration object
402
+ */
403
+ function createCustomViewport(options = {}) {
404
+ const {
405
+ width = 1280,
406
+ height = 720,
407
+ deviceScaleFactor = 2,
408
+ name = "Custom",
409
+ description = "",
410
+ } = options;
411
+
412
+ return {
413
+ name,
414
+ category: "custom",
415
+ width,
416
+ height,
417
+ deviceScaleFactor,
418
+ description: description || `${width}×${height}${deviceScaleFactor > 1 ? ` @${deviceScaleFactor}x` : ""}`,
419
+ };
420
+ }
421
+
422
+ /**
423
+ * Parse viewport matrix configuration for multi-viewport captures
424
+ * @param {Array|Object|string} viewportConfig - Viewport configuration
425
+ * @returns {Array<Object>} Array of resolved viewport configs
426
+ */
427
+ function parseViewportMatrix(viewportConfig) {
428
+ if (!viewportConfig) {
429
+ return [resolveViewport(null)];
430
+ }
431
+
432
+ // Single viewport
433
+ if (typeof viewportConfig === "string" || !Array.isArray(viewportConfig)) {
434
+ return [resolveViewport(viewportConfig)];
435
+ }
436
+
437
+ // Array of viewports - resolve each
438
+ return viewportConfig.map(v => resolveViewport(v));
439
+ }
440
+
441
+ module.exports = {
442
+ VIEWPORT_PRESETS,
443
+ CROP_PRESETS,
444
+ getViewportPreset,
445
+ getAllViewportPresets,
446
+ getViewportPresetsByCategory,
447
+ resolveViewport,
448
+ validateViewport,
449
+ resolveCropRegion,
450
+ getCropPreset,
451
+ getAllCropPresets,
452
+ createCustomViewport,
453
+ parseViewportMatrix,
454
+ };
@@ -0,0 +1,118 @@
1
+ // worker-pool.js - Streaming worker pool for concurrent task execution
2
+ // Replaces batch-based Promise.all — when any worker finishes, the next
3
+ // queued task starts immediately. No batch blocking.
4
+
5
+ class WorkerPool {
6
+ /**
7
+ * @param {number} concurrency - Max concurrent workers
8
+ * @param {Object} options
9
+ * @param {Function} options.onProgress - Called after each task completes:
10
+ * ({ completed, total, active, durationMs, result, error }) => void
11
+ */
12
+ constructor(concurrency, options = {}) {
13
+ this.concurrency = Math.max(1, concurrency);
14
+ this.onProgress = options.onProgress || null;
15
+ }
16
+
17
+ /**
18
+ * Execute all tasks with streaming concurrency.
19
+ * Returns results in the same order as the input tasks array.
20
+ *
21
+ * @param {Array} tasks - Array of task items
22
+ * @param {Function} executor - async (task, index) => result
23
+ * @returns {Promise<Array>} Results in input order
24
+ */
25
+ async runAll(tasks, executor) {
26
+ const total = tasks.length;
27
+ const results = new Array(total);
28
+ let nextIndex = 0;
29
+ let completed = 0;
30
+ let active = 0;
31
+
32
+ return new Promise((resolve, reject) => {
33
+ const startNext = () => {
34
+ while (active < this.concurrency && nextIndex < total) {
35
+ const index = nextIndex++;
36
+ active++;
37
+
38
+ const taskStart = Date.now();
39
+
40
+ executor(tasks[index], index)
41
+ .then((result) => {
42
+ results[index] = result;
43
+ active--;
44
+ completed++;
45
+
46
+ // Start next task BEFORE reporting progress so active count
47
+ // reflects the replacement worker already launched
48
+ if (completed < total) {
49
+ startNext();
50
+ }
51
+
52
+ if (this.onProgress) {
53
+ try {
54
+ this.onProgress({
55
+ completed,
56
+ total,
57
+ active,
58
+ durationMs: Date.now() - taskStart,
59
+ result,
60
+ error: null,
61
+ task: tasks[index],
62
+ index,
63
+ });
64
+ } catch (_e) {
65
+ // Don't let progress callback errors break the pool
66
+ }
67
+ }
68
+
69
+ if (completed === total) {
70
+ resolve(results);
71
+ }
72
+ })
73
+ .catch((error) => {
74
+ // Store error as a failed result rather than rejecting the whole pool
75
+ results[index] = { success: false, error: error.message };
76
+ active--;
77
+ completed++;
78
+
79
+ // Start next task BEFORE reporting progress
80
+ if (completed < total) {
81
+ startNext();
82
+ }
83
+
84
+ if (this.onProgress) {
85
+ try {
86
+ this.onProgress({
87
+ completed,
88
+ total,
89
+ active,
90
+ durationMs: Date.now() - taskStart,
91
+ result: null,
92
+ error,
93
+ task: tasks[index],
94
+ index,
95
+ });
96
+ } catch (_e) {
97
+ // Don't let progress callback errors break the pool
98
+ }
99
+ }
100
+
101
+ if (completed === total) {
102
+ resolve(results);
103
+ }
104
+ });
105
+ }
106
+ };
107
+
108
+ if (total === 0) {
109
+ resolve([]);
110
+ return;
111
+ }
112
+
113
+ startNext();
114
+ });
115
+ }
116
+ }
117
+
118
+ module.exports = { WorkerPool };