autokap 1.0.2 → 1.0.3

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 (83) hide show
  1. package/dist/cli-config.d.ts +13 -0
  2. package/dist/cli-config.js +42 -0
  3. package/dist/cli-utils.d.ts +0 -19
  4. package/dist/cli-utils.js +2 -65
  5. package/dist/cli.d.ts +0 -1
  6. package/dist/cli.js +266 -305
  7. package/package.json +23 -16
  8. package/assets/chrome/ios-statusbar-comparison-reference.jpg +0 -0
  9. package/assets/chrome/ios-statusbar-dark-reference.jpg +0 -0
  10. package/assets/chrome/ios-statusbar-light-reference.jpg +0 -0
  11. package/assets/devices/ipad-pro-11-m4.json +0 -52
  12. package/assets/devices/iphone-16-pro.json +0 -53
  13. package/assets/devices/macbook-air-13.json +0 -45
  14. package/assets/frames/MacBook Air 13.svg +0 -242
  15. package/assets/frames/Status bar - iPhone.png +0 -0
  16. package/assets/frames/Status bar and Menu bar- iPad.png +0 -0
  17. package/assets/frames/iPad Pro M4 11_.png +0 -0
  18. package/assets/frames/iPhone 16 Pro.png +0 -0
  19. package/assets/icons/Cellular Connection.svg +0 -3
  20. package/assets/icons/Union.svg +0 -6
  21. package/assets/icons/Wifi.svg +0 -3
  22. package/assets/icons/battery.svg +0 -5
  23. package/assets/icons/battery_charging.svg +0 -8
  24. package/dist/abort.d.ts +0 -5
  25. package/dist/abort.js +0 -44
  26. package/dist/agent.d.ts +0 -142
  27. package/dist/agent.js +0 -4504
  28. package/dist/browser-bar.d.ts +0 -40
  29. package/dist/browser-bar.js +0 -147
  30. package/dist/clip-orchestrator.d.ts +0 -148
  31. package/dist/clip-orchestrator.js +0 -950
  32. package/dist/clip-postprocess.d.ts +0 -42
  33. package/dist/clip-postprocess.js +0 -192
  34. package/dist/credential-templates.d.ts +0 -5
  35. package/dist/credential-templates.js +0 -60
  36. package/dist/element-capture.d.ts +0 -53
  37. package/dist/element-capture.js +0 -766
  38. package/dist/hybrid-navigator.d.ts +0 -138
  39. package/dist/hybrid-navigator.js +0 -468
  40. package/dist/index.d.ts +0 -15
  41. package/dist/index.js +0 -11
  42. package/dist/llm-usage.d.ts +0 -17
  43. package/dist/llm-usage.js +0 -45
  44. package/dist/mockup-html.d.ts +0 -119
  45. package/dist/mockup-html.js +0 -253
  46. package/dist/mockup.d.ts +0 -94
  47. package/dist/mockup.js +0 -604
  48. package/dist/mouse-animation.d.ts +0 -46
  49. package/dist/mouse-animation.js +0 -100
  50. package/dist/overlay-utils.d.ts +0 -14
  51. package/dist/overlay-utils.js +0 -13
  52. package/dist/posthog.d.ts +0 -4
  53. package/dist/posthog.js +0 -26
  54. package/dist/prompt-cache.d.ts +0 -10
  55. package/dist/prompt-cache.js +0 -24
  56. package/dist/prompts.d.ts +0 -167
  57. package/dist/prompts.js +0 -1165
  58. package/dist/security.d.ts +0 -20
  59. package/dist/security.js +0 -569
  60. package/dist/session-profile.d.ts +0 -86
  61. package/dist/session-profile.js +0 -1471
  62. package/dist/sf-pro-fonts.d.ts +0 -4
  63. package/dist/sf-pro-fonts.js +0 -7
  64. package/dist/status-bar-l10n.d.ts +0 -14
  65. package/dist/status-bar-l10n.js +0 -177
  66. package/dist/status-bar.d.ts +0 -44
  67. package/dist/status-bar.js +0 -336
  68. package/dist/tools.d.ts +0 -4
  69. package/dist/tools.js +0 -578
  70. package/dist/video-agent.d.ts +0 -143
  71. package/dist/video-agent.js +0 -4783
  72. package/dist/video-observation.d.ts +0 -36
  73. package/dist/video-observation.js +0 -192
  74. package/dist/video-planner.d.ts +0 -12
  75. package/dist/video-planner.js +0 -500
  76. package/dist/video-prompts.d.ts +0 -37
  77. package/dist/video-prompts.js +0 -554
  78. package/dist/video-tools.d.ts +0 -3
  79. package/dist/video-tools.js +0 -59
  80. package/dist/video-variant-state.d.ts +0 -29
  81. package/dist/video-variant-state.js +0 -80
  82. package/dist/vision-model.d.ts +0 -17
  83. package/dist/vision-model.js +0 -74
package/dist/mockup.js DELETED
@@ -1,604 +0,0 @@
1
- import { readFile, readdir } from 'fs/promises';
2
- import path from 'path';
3
- import sharp from 'sharp';
4
- import { fileURLToPath } from 'url';
5
- import { generateMockupPage, computeMockupLayout } from './mockup-html.js';
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = path.dirname(__filename);
8
- const DEVICES_DIR = path.join(__dirname, '..', 'assets', 'devices');
9
- const FRAMES_DIR = path.join(__dirname, '..', 'assets', 'frames');
10
- // Supabase config for DB-backed mockup loading
11
- const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
12
- const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
13
- const DEFAULT_MOCKUP_OPTIONS = {
14
- orientation: 'portrait',
15
- outputScale: 2,
16
- showStatusBar: true,
17
- showSafeAreaTop: true,
18
- showSafeAreaBottom: true,
19
- showSafeAreaLeft: true,
20
- showSafeAreaRight: true,
21
- showHomeIndicator: true,
22
- safeAreaTopColor: '',
23
- safeAreaBottomColor: '',
24
- safeAreaLeftColor: '',
25
- safeAreaRightColor: '',
26
- statusBar: {},
27
- browserBar: {},
28
- windowBorder: { color: '', width: 0, radius: 0 },
29
- };
30
- const CONFIG_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
31
- let configCache = null;
32
- // In-memory cache for downloaded frame asset Buffers
33
- const frameBufferCache = new Map();
34
- const FRAME_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
35
- export function invalidateDeviceConfigCache() {
36
- configCache = null;
37
- frameBufferCache.clear();
38
- }
39
- function resolveBrowserOrientation(config, requestedOrientation) {
40
- const supported = config.supportedOrientations ?? [];
41
- if (supported.length > 0) {
42
- return supported[0] ?? requestedOrientation;
43
- }
44
- const available = Object.keys(config.orientations ?? {});
45
- return available[0] ?? config.frameOrientation ?? requestedOrientation;
46
- }
47
- function normalizeBrowserSafeAreaTop(configuredTop, dpr, fallbackLogicalTop) {
48
- const rawTop = configuredTop ?? 0;
49
- if (rawTop <= 0) {
50
- return fallbackLogicalTop * dpr;
51
- }
52
- // Browser configs store safeArea.top in logical px, but some historical rows
53
- // may already contain a DPR-scaled physical value. Normalize both shapes.
54
- const logicalTop = rawTop > fallbackLogicalTop * 1.5
55
- ? rawTop / Math.max(dpr, 1)
56
- : rawTop;
57
- return logicalTop * dpr;
58
- }
59
- function normalizeBrowserWindowBorder(windowBorder, dpr) {
60
- if (!windowBorder) {
61
- return undefined;
62
- }
63
- return {
64
- ...windowBorder,
65
- radius: windowBorder.radius > 0
66
- ? windowBorder.radius * dpr
67
- : windowBorder.radius,
68
- };
69
- }
70
- function normalizeBrowserCornerRadius(cornerRadius, dpr) {
71
- if (!cornerRadius || cornerRadius <= 0) {
72
- return 0;
73
- }
74
- return cornerRadius * dpr;
75
- }
76
- /**
77
- * Resolve the orientation config for a given orientation.
78
- * Prefers per-orientation configs; falls back to rotation logic for legacy format.
79
- */
80
- function resolveOrientationConfig(config, requestedOrientation) {
81
- // Check for per-orientation config (new format)
82
- const orientationData = config.orientations?.[requestedOrientation];
83
- if (orientationData) {
84
- return {
85
- screen: orientationData.screen,
86
- safeArea: orientationData.safeArea ?? { top: 0, bottom: 0 },
87
- statusBar: orientationData.statusBar,
88
- homeIndicator: orientationData.homeIndicator,
89
- frame: orientationData.frame,
90
- frameUrl: orientationData.frameUrl ?? config._rowFrameUrl,
91
- frameDarkUrl: orientationData.frameDarkUrl,
92
- windowBorder: orientationData.windowBorder,
93
- browserBarZones: orientationData.browserBarZones,
94
- frameRotation: orientationData.frameRotation ?? 0,
95
- needsRotation: false,
96
- disableOverlays: false,
97
- frameBehindContent: orientationData.frameBehindContent ?? false,
98
- };
99
- }
100
- // Legacy: single config, may need auto-rotation
101
- const nativeOrientation = config.frameOrientation ?? 'portrait';
102
- const needsRotation = requestedOrientation !== nativeOrientation;
103
- const disableOverlays = config.category === 'phone' && needsRotation;
104
- return {
105
- screen: config.screen ?? { logicalWidth: 0, logicalHeight: 0, scale: 1, cornerRadius: 0 },
106
- safeArea: config.safeArea ?? { top: 0, bottom: 0 },
107
- statusBar: config.statusBar,
108
- homeIndicator: config.homeIndicator,
109
- frame: config.frame,
110
- frameRotation: 0,
111
- needsRotation,
112
- disableOverlays,
113
- frameBehindContent: false,
114
- };
115
- }
116
- async function loadDeviceConfigs() {
117
- if (configCache && configCache.expiresAt > Date.now()) {
118
- return configCache.configs;
119
- }
120
- // Try Supabase first
121
- if (SUPABASE_URL && SUPABASE_SERVICE_KEY) {
122
- try {
123
- const res = await fetch(`${SUPABASE_URL}/rest/v1/device_mockups?is_active=eq.true&order=category.asc,name.asc&select=id,config,frame_url`, {
124
- headers: {
125
- apikey: SUPABASE_SERVICE_KEY,
126
- Authorization: `Bearer ${SUPABASE_SERVICE_KEY}`,
127
- },
128
- });
129
- if (res.ok) {
130
- const rows = await res.json();
131
- const configs = new Map();
132
- for (const row of rows) {
133
- const config = row.config;
134
- if (row.frame_url)
135
- config._rowFrameUrl = row.frame_url;
136
- configs.set(row.id, config);
137
- }
138
- configCache = { configs, expiresAt: Date.now() + CONFIG_CACHE_TTL_MS };
139
- return configs;
140
- }
141
- }
142
- catch {
143
- // Fall through to filesystem
144
- }
145
- }
146
- // Fallback: local filesystem
147
- const files = await readdir(DEVICES_DIR);
148
- const configs = new Map();
149
- for (const file of files) {
150
- if (!file.endsWith('.json'))
151
- continue;
152
- const raw = await readFile(path.join(DEVICES_DIR, file), 'utf-8');
153
- const config = JSON.parse(raw);
154
- configs.set(config.id, config);
155
- }
156
- configCache = { configs, expiresAt: Date.now() + CONFIG_CACHE_TTL_MS };
157
- return configs;
158
- }
159
- async function loadFrameAsset(config, resolved) {
160
- // Check for orientation-specific frame URL first
161
- const frameUrl = resolved.frameUrl;
162
- const cacheKey = frameUrl ?? config.id;
163
- if (frameUrl) {
164
- const cached = frameBufferCache.get(cacheKey);
165
- if (cached && cached.expiresAt > Date.now())
166
- return cached.buffer;
167
- try {
168
- const res = await fetch(frameUrl);
169
- if (res.ok) {
170
- const buffer = Buffer.from(await res.arrayBuffer());
171
- frameBufferCache.set(cacheKey, { buffer, expiresAt: Date.now() + FRAME_CACHE_TTL_MS });
172
- return buffer;
173
- }
174
- }
175
- catch {
176
- // Fall through to filesystem
177
- }
178
- }
179
- // Fallback: local filesystem
180
- return readFile(path.join(FRAMES_DIR, resolved.frame.asset));
181
- }
182
- function computeResolvedFrameGeometry(resolved) {
183
- const rotation = resolved.frameRotation || (resolved.needsRotation ? 90 : 0);
184
- const is90or270 = rotation % 180 !== 0;
185
- if (rotation !== 0) {
186
- if (is90or270) {
187
- if (resolved.needsRotation) {
188
- const origSr = resolved.frame.screenRect;
189
- const origH = resolved.frame.height;
190
- return {
191
- frameWidth: resolved.frame.height,
192
- frameHeight: resolved.frame.width,
193
- frameRotation: rotation,
194
- screenRect: {
195
- x: origH - origSr.y - origSr.height,
196
- y: origSr.x,
197
- width: origSr.height,
198
- height: origSr.width,
199
- },
200
- };
201
- }
202
- return {
203
- frameWidth: resolved.frame.height,
204
- frameHeight: resolved.frame.width,
205
- frameRotation: rotation,
206
- screenRect: { ...resolved.frame.screenRect },
207
- };
208
- }
209
- return {
210
- frameWidth: resolved.frame.width,
211
- frameHeight: resolved.frame.height,
212
- frameRotation: rotation,
213
- screenRect: { ...resolved.frame.screenRect },
214
- };
215
- }
216
- return {
217
- frameWidth: resolved.frame.width,
218
- frameHeight: resolved.frame.height,
219
- frameRotation: 0,
220
- screenRect: { ...resolved.frame.screenRect },
221
- };
222
- }
223
- async function loadFrameAssetFromDescriptor(descriptor) {
224
- const cacheKey = descriptor.frameUrl ?? `${descriptor.id}:${descriptor.orientation}`;
225
- const cached = frameBufferCache.get(cacheKey);
226
- if (cached && cached.expiresAt > Date.now())
227
- return cached.buffer;
228
- if (descriptor.frameUrl) {
229
- try {
230
- const res = await fetch(descriptor.frameUrl);
231
- if (res.ok) {
232
- const buffer = Buffer.from(await res.arrayBuffer());
233
- frameBufferCache.set(cacheKey, {
234
- buffer,
235
- expiresAt: Date.now() + FRAME_CACHE_TTL_MS,
236
- });
237
- return buffer;
238
- }
239
- }
240
- catch {
241
- // Fall through to filesystem
242
- }
243
- }
244
- return readFile(path.join(FRAMES_DIR, descriptor.frame.asset));
245
- }
246
- export async function resolveDeviceFrameDescriptor(id, options) {
247
- const configs = await loadDeviceConfigs();
248
- const config = configs.get(id);
249
- if (!config)
250
- return null;
251
- const orientation = options?.orientation ?? config.frameOrientation ?? 'portrait';
252
- const resolved = resolveOrientationConfig(config, orientation);
253
- const geometry = computeResolvedFrameGeometry(resolved);
254
- return {
255
- id: config.id,
256
- name: config.name,
257
- category: config.category,
258
- orientation,
259
- screen: resolved.screen,
260
- safeArea: resolved.safeArea,
261
- statusBar: resolved.statusBar,
262
- homeIndicator: resolved.homeIndicator,
263
- frame: {
264
- type: resolved.frame.type,
265
- asset: resolved.frame.asset,
266
- width: resolved.frame.width,
267
- height: resolved.frame.height,
268
- },
269
- frameUrl: resolved.frameUrl,
270
- frameWidth: geometry.frameWidth,
271
- frameHeight: geometry.frameHeight,
272
- screenRect: geometry.screenRect,
273
- frameRotation: geometry.frameRotation,
274
- frameBehindContent: resolved.frameBehindContent,
275
- disableOverlays: resolved.disableOverlays,
276
- };
277
- }
278
- export async function rasterizeDeviceFrame(descriptor, outputScale) {
279
- const rawFrame = await loadFrameAssetFromDescriptor(descriptor);
280
- const isSvg = descriptor.frame.type === 'svg';
281
- const rasterWidth = Math.round(descriptor.frame.width * outputScale);
282
- const rasterHeight = Math.round(descriptor.frame.height * outputScale);
283
- const svgDensity = Math.max(72, Math.round(72 * outputScale));
284
- let rendered = isSvg
285
- ? await sharp(rawFrame, { density: svgDensity })
286
- .resize(rasterWidth, rasterHeight)
287
- .png()
288
- .toBuffer()
289
- : await sharp(rawFrame)
290
- .resize(rasterWidth, rasterHeight)
291
- .png()
292
- .toBuffer();
293
- if (descriptor.frameRotation !== 0) {
294
- rendered = await sharp(rendered).rotate(descriptor.frameRotation).png().toBuffer();
295
- }
296
- const renderedMeta = await sharp(rendered).metadata();
297
- if (renderedMeta.width !== Math.round(descriptor.frameWidth * outputScale) ||
298
- renderedMeta.height !== Math.round(descriptor.frameHeight * outputScale)) {
299
- rendered = await sharp(rendered)
300
- .resize(Math.round(descriptor.frameWidth * outputScale), Math.round(descriptor.frameHeight * outputScale))
301
- .png()
302
- .toBuffer();
303
- }
304
- return rendered;
305
- }
306
- // ── Public API ─────────────────────────────────────────────────────────
307
- export async function getDeviceFrames() {
308
- const configs = await loadDeviceConfigs();
309
- return Array.from(configs.values()).map(({ id, name, category, viewport }) => ({
310
- id, name, category, viewport,
311
- }));
312
- }
313
- export async function getDeviceFrame(id) {
314
- const configs = await loadDeviceConfigs();
315
- const c = configs.get(id);
316
- if (!c)
317
- return undefined;
318
- return { id: c.id, name: c.name, category: c.category, viewport: c.viewport };
319
- }
320
- export async function applyDeviceFrame(screenshot, deviceId, browserContext, options) {
321
- const configs = await loadDeviceConfigs();
322
- const config = configs.get(deviceId);
323
- if (!config)
324
- throw new Error(`Unknown device frame: ${deviceId}`);
325
- const opts = { ...DEFAULT_MOCKUP_OPTIONS, ...options };
326
- const requestedOrientation = opts.orientation ?? 'portrait';
327
- // Resolve orientation-specific config
328
- // Browser devices ignore requested orientation. Capture uses a single browser
329
- // geometry, and Studio must mirror that behavior even if stale configs contain
330
- // bogus extra orientations.
331
- const effectiveOrientation = config.category === 'browser'
332
- ? resolveBrowserOrientation(config, requestedOrientation)
333
- : requestedOrientation;
334
- const resolved = resolveOrientationConfig(config, effectiveOrientation);
335
- // Disable overlays if rotation requires it (legacy phone rotation)
336
- if (resolved.disableOverlays) {
337
- opts.showStatusBar = false;
338
- opts.showSafeAreaTop = false;
339
- opts.showSafeAreaBottom = false;
340
- opts.showSafeAreaLeft = false;
341
- opts.showSafeAreaRight = false;
342
- }
343
- const scale = resolved.screen?.scale ?? 1;
344
- const isBrowserDevice = config.category === 'browser';
345
- const hasFrameEarly = !!(resolved.frameUrl || resolved.frame.asset);
346
- // For frameless browser devices, the screenshot is already at final pixel resolution
347
- // (captured at the device's native viewport). outputScale would double it again.
348
- const os = (isBrowserDevice && !hasFrameEarly) ? 1 : Math.max(0.5, Math.min(4, opts.outputScale));
349
- console.log(`[mockup] applyDeviceFrame: id=${deviceId}, category=${config.category}, orientation=${requestedOrientation}`);
350
- console.log(`[mockup] hasOrientationConfig=${!!config.orientations?.[requestedOrientation]}, orientations=${JSON.stringify(Object.keys(config.orientations ?? {}))}`);
351
- console.log(`[mockup] resolved.screen:`, JSON.stringify(resolved.screen));
352
- console.log(`[mockup] resolved.frame: w=${resolved.frame.width} h=${resolved.frame.height} asset="${resolved.frame.asset}" url=${resolved.frameUrl ? 'yes' : 'no'}`);
353
- console.log(`[mockup] resolved.safeArea:`, JSON.stringify(resolved.safeArea));
354
- console.log(`[mockup] scale=${scale}, outputScale=${os}`);
355
- // Browser devices can work without a frame image
356
- const hasFrame = !!(resolved.frameUrl || resolved.frame.asset);
357
- let frameData = null;
358
- if (hasFrame) {
359
- // Load and potentially rotate frame asset
360
- const rawFrame = await loadFrameAsset(config, resolved);
361
- const isSvg = resolved.frame.type === 'svg';
362
- frameData = isSvg
363
- ? await sharp(rawFrame, { density: 72 * os })
364
- .resize(Math.round(resolved.frame.width * os), Math.round(resolved.frame.height * os))
365
- .png().toBuffer()
366
- : await sharp(rawFrame)
367
- .resize(Math.round(resolved.frame.width * os), Math.round(resolved.frame.height * os))
368
- .png().toBuffer();
369
- }
370
- // Apply frame rotation
371
- const geometry = computeResolvedFrameGeometry(resolved);
372
- let geo = {
373
- frameWidth: geometry.frameWidth,
374
- frameHeight: geometry.frameHeight,
375
- screenRect: geometry.screenRect,
376
- };
377
- let browserDeviceResolutionFactor = 1;
378
- // For frameless browser devices: derive dimensions from the actual screenshot
379
- // and ensure safe area top is set for the Chrome toolbar (86px @ 1x = 129px @ 1.5x)
380
- const BROWSER_BAR_HEIGHT = 86; // Chrome toolbar logical height at 1x
381
- if (isBrowserDevice && !hasFrame) {
382
- const sMeta = await sharp(screenshot).metadata();
383
- const sw = sMeta.width ?? 1440;
384
- const sh = sMeta.height ?? 900;
385
- // Detect the actual DPR from the screenshot dimensions vs the device's logical viewport.
386
- // This handles the case where the caller doesn't know the original capture DPR (e.g. Studio editor).
387
- const logicalW = resolved.screen?.logicalWidth || 1440;
388
- const inferredDpr = logicalW > 0 && sw > logicalW
389
- ? Math.round(sw / logicalW * 10) / 10
390
- : 1;
391
- // Use whichever is larger: the explicitly requested outputScale or the inferred DPR.
392
- // This ensures the browser bar is correctly sized even when outputScale=1 is passed
393
- // but the screenshot is at a higher pixel density.
394
- const dpr = Math.max(inferredDpr, Math.max(0.5, Math.min(4, opts.outputScale)));
395
- browserDeviceResolutionFactor = dpr;
396
- const safeAreaTop = normalizeBrowserSafeAreaTop(resolved.safeArea.top, dpr, BROWSER_BAR_HEIGHT);
397
- resolved.safeArea = { ...resolved.safeArea, top: safeAreaTop };
398
- resolved.screen = {
399
- ...resolved.screen,
400
- cornerRadius: normalizeBrowserCornerRadius(resolved.screen?.cornerRadius, dpr),
401
- };
402
- const saTop = resolved.safeArea.top * scale;
403
- // Frame = screenshot width × (screenshot height + safe area for toolbar)
404
- geo.frameWidth = sw;
405
- geo.frameHeight = sh + saTop;
406
- geo.screenRect = { x: 0, y: 0, width: sw, height: sh + saTop };
407
- console.log(`[mockup] frameless browser: screenshot=${sw}x${sh}, logicalW=${logicalW}, inferredDpr=${inferredDpr}, dpr=${dpr}, safeAreaTop=${saTop}, geo=${geo.frameWidth}x${geo.frameHeight}`);
408
- }
409
- else if (!hasFrame && geo.frameWidth === 0 && geo.frameHeight === 0) {
410
- // Non-browser frameless fallback
411
- geo.frameWidth = geo.screenRect.width || resolved.screen?.logicalWidth || 1440;
412
- geo.frameHeight = geo.screenRect.height || resolved.screen?.logicalHeight || 900;
413
- if (geo.screenRect.width === 0)
414
- geo.screenRect = { x: 0, y: 0, width: geo.frameWidth, height: geo.frameHeight };
415
- }
416
- if (frameData && geometry.frameRotation !== 0) {
417
- // Rotate the frame image by the specified angle
418
- frameData = await sharp(frameData).rotate(geometry.frameRotation).png().toBuffer();
419
- }
420
- // Always compute content area with all safe areas visible — this matches
421
- // the browser viewport the screenshot was captured at.
422
- // Safe area toggles only affect the visual overlays (fills, status bar),
423
- // NOT the screenshot sizing or placement.
424
- const layout = computeMockupLayout({
425
- frameSrc: '',
426
- frameWidth: geo.frameWidth,
427
- frameHeight: geo.frameHeight,
428
- frameRotation: 0,
429
- screenRect: geo.screenRect,
430
- cornerRadius: (resolved.screen?.cornerRadius ?? 0) * scale,
431
- safeArea: resolved.safeArea,
432
- scale,
433
- showSafeAreaTop: true,
434
- showSafeAreaBottom: true,
435
- showSafeAreaLeft: true,
436
- showSafeAreaRight: true,
437
- });
438
- const contentW = layout.contentArea.width;
439
- const contentH = layout.contentArea.height;
440
- console.log(`[mockup] hasFrame=${hasFrame}, geo: fw=${geo.frameWidth} fh=${geo.frameHeight} sr=${JSON.stringify(geo.screenRect)}`);
441
- console.log(`[mockup] layout: container=${layout.containerWidth}x${layout.containerHeight} content=${contentW}x${contentH} contentArea=${JSON.stringify(layout.contentArea)}`);
442
- // Get incoming screenshot dimensions for logging
443
- const screenshotMeta = await sharp(screenshot).metadata();
444
- console.log(`[mockup] input screenshot: ${screenshotMeta.width}x${screenshotMeta.height}`);
445
- console.log(`[mockup] resize target: ${Math.round(contentW * os)}x${Math.round(contentH * os)}`);
446
- // Resize screenshot to outputScale× of the content area for crisp rendering
447
- const screenshotForMockup = await sharp(screenshot)
448
- .resize(Math.round(contentW * os), Math.round(contentH * os), { fit: 'fill' })
449
- .png()
450
- .toBuffer();
451
- const screenshotBase64 = screenshotForMockup.toString('base64');
452
- // Sample edge colors for dynamic safe area fills
453
- const colors = await sampleEdgeColors(screenshotForMockup);
454
- if (opts.safeAreaTopColor)
455
- colors.topColor = opts.safeAreaTopColor;
456
- if (opts.safeAreaBottomColor)
457
- colors.bottomColor = opts.safeAreaBottomColor;
458
- if (opts.safeAreaLeftColor)
459
- colors.leftColor = opts.safeAreaLeftColor;
460
- if (opts.safeAreaRightColor)
461
- colors.rightColor = opts.safeAreaRightColor;
462
- // Determine color scheme from edge colors
463
- const autoColorScheme = isDarkBackground(colors.topColor) ? 'dark' : 'light';
464
- const lum = parseLuminance(colors.bottomColor);
465
- const hiColor = lum > 170 ? 'rgba(0,0,0,0.35)' : 'rgba(255,255,255,0.5)';
466
- // Browser devices: swap frame to dark variant if needed
467
- if (frameData && isBrowserDevice && autoColorScheme === 'dark' && resolved.frameDarkUrl) {
468
- try {
469
- const darkRes = await fetch(resolved.frameDarkUrl);
470
- if (darkRes.ok) {
471
- const darkRaw = Buffer.from(await darkRes.arrayBuffer());
472
- const isSvg = resolved.frame.type === 'svg';
473
- frameData = isSvg
474
- ? await sharp(darkRaw, { density: 72 * os })
475
- .resize(Math.round(resolved.frame.width * os), Math.round(resolved.frame.height * os))
476
- .png().toBuffer()
477
- : await sharp(darkRaw)
478
- .resize(Math.round(resolved.frame.width * os), Math.round(resolved.frame.height * os))
479
- .png().toBuffer();
480
- if (geometry.frameRotation !== 0) {
481
- frameData = await sharp(frameData).rotate(geometry.frameRotation).png().toBuffer();
482
- }
483
- }
484
- }
485
- catch {
486
- // Keep light frame as fallback
487
- }
488
- }
489
- const frameBase64 = frameData ? frameData.toString('base64') : '';
490
- const browserWindowBorder = isBrowserDevice
491
- ? normalizeBrowserWindowBorder(opts.windowBorder?.width ? opts.windowBorder : resolved.windowBorder, browserDeviceResolutionFactor)
492
- : undefined;
493
- // Safe area toggles: when hidden, make fills transparent instead of removing them.
494
- // This keeps the content area (screenshot placement) at the viewport-matching size.
495
- const safeAreaFillColors = {
496
- top: opts.showSafeAreaTop ? colors.topColor : 'transparent',
497
- bottom: opts.showSafeAreaBottom ? colors.bottomColor : 'transparent',
498
- left: opts.showSafeAreaLeft ? colors.leftColor : 'transparent',
499
- right: opts.showSafeAreaRight ? colors.rightColor : 'transparent',
500
- };
501
- // Use shared HTML generator (single source of truth for mockup rendering)
502
- // Always pass showSafeArea=true so the content area matches the viewport.
503
- const html = generateMockupPage({
504
- frameSrc: frameBase64 ? `data:image/png;base64,${frameBase64}` : undefined,
505
- frameWidth: geo.frameWidth,
506
- frameHeight: geo.frameHeight,
507
- frameRotation: 0, // frame already rotated by sharp
508
- frameBehindContent: resolved.frameBehindContent,
509
- screenRect: geo.screenRect,
510
- cornerRadius: (resolved.screen?.cornerRadius ?? 0) * scale,
511
- screenBackground: 'transparent',
512
- safeArea: resolved.safeArea,
513
- scale,
514
- showSafeAreaTop: true,
515
- showSafeAreaBottom: true,
516
- showSafeAreaLeft: true,
517
- showSafeAreaRight: true,
518
- safeAreaColors: safeAreaFillColors,
519
- statusBar: !isBrowserDevice && resolved.statusBar ? {
520
- height: resolved.statusBar.height,
521
- width: resolved.statusBar.width,
522
- type: resolved.statusBar.type ?? 'iphone-dynamic-island',
523
- layout: resolved.statusBar.layout,
524
- } : undefined,
525
- showStatusBar: !isBrowserDevice && opts.showStatusBar,
526
- statusBarConfig: { ...opts.statusBar, colorScheme: autoColorScheme },
527
- colorScheme: autoColorScheme,
528
- showBrowserBar: isBrowserDevice,
529
- browserBarConfig: isBrowserDevice ? { ...opts.browserBar, colorScheme: autoColorScheme } : undefined,
530
- homeIndicator: resolved.homeIndicator,
531
- showHomeIndicator: opts.showHomeIndicator,
532
- homeIndicatorColor: hiColor,
533
- windowBorder: browserWindowBorder,
534
- contentHtml: `<img style="width:100%;height:100%;display:block" src="data:image/png;base64,${screenshotBase64}">`,
535
- pixelScale: os,
536
- });
537
- // Compute final render size — must match the HTML page container (including window border)
538
- const wbw = browserWindowBorder?.width ?? 0;
539
- const renderW = Math.round((geo.frameWidth + wbw * 2) * os);
540
- const renderH = Math.round((geo.frameHeight + wbw * 2) * os);
541
- console.log(`[mockup] renderMockup: ${renderW}x${renderH}, showBrowserBar=${isBrowserDevice}, windowBorderWidth=${wbw}, browserDpr=${browserDeviceResolutionFactor}`);
542
- return renderMockup(browserContext, html, renderW, renderH);
543
- }
544
- // ── Edge Color Sampling ────────────────────────────────────────────────
545
- async function sampleEdgeColors(preparedScreenshot) {
546
- const meta = await sharp(preparedScreenshot).metadata();
547
- const w = meta.width ?? 1;
548
- const h = meta.height ?? 1;
549
- const [topStats, bottomStats, leftStats, rightStats] = await Promise.all([
550
- sharp(preparedScreenshot).extract({ left: 0, top: 0, width: w, height: 1 }).stats(),
551
- sharp(preparedScreenshot).extract({ left: 0, top: h - 1, width: w, height: 1 }).stats(),
552
- sharp(preparedScreenshot).extract({ left: 0, top: 0, width: 1, height: h }).stats(),
553
- sharp(preparedScreenshot).extract({ left: w - 1, top: 0, width: 1, height: h }).stats(),
554
- ]);
555
- return {
556
- topColor: channelsToRgb(topStats.channels),
557
- bottomColor: channelsToRgb(bottomStats.channels),
558
- leftColor: channelsToRgb(leftStats.channels),
559
- rightColor: channelsToRgb(rightStats.channels),
560
- };
561
- }
562
- function channelsToRgb(channels) {
563
- const r = Math.round(channels[0].mean);
564
- const g = Math.round(channels[1].mean);
565
- const b = Math.round(channels[2].mean);
566
- return `rgb(${r},${g},${b})`;
567
- }
568
- function isDarkBackground(rgbStr) {
569
- const match = rgbStr.match(/\d+/g);
570
- if (!match)
571
- return false;
572
- const [r, g, b] = match.map(Number);
573
- return r * 0.299 + g * 0.587 + b * 0.114 < 128;
574
- }
575
- function parseLuminance(rgbStr) {
576
- const match = rgbStr.match(/\d+/g);
577
- if (!match)
578
- return 128;
579
- const [r, g, b] = match.map(Number);
580
- return r * 0.299 + g * 0.587 + b * 0.114;
581
- }
582
- // ── Playwright Renderer ────────────────────────────────────────────────
583
- async function renderMockup(browserContext, html, width, height) {
584
- const browser = browserContext.browser();
585
- if (!browser)
586
- throw new Error('Browser not available for mockup rendering');
587
- const renderCtx = await browser.newContext({
588
- viewport: { width, height },
589
- deviceScaleFactor: 1,
590
- });
591
- const page = await renderCtx.newPage();
592
- try {
593
- await page.setContent(html, { waitUntil: 'load' });
594
- await page.evaluate(() => document.fonts.ready);
595
- await page.waitForTimeout(50);
596
- const buffer = await page.screenshot({ type: 'png', fullPage: false, omitBackground: true });
597
- return Buffer.from(buffer);
598
- }
599
- finally {
600
- await page.close();
601
- await renderCtx.close();
602
- }
603
- }
604
- //# sourceMappingURL=mockup.js.map
@@ -1,46 +0,0 @@
1
- import type { Page } from 'playwright';
2
- export interface BezierMoveOptions {
3
- /** Total duration in milliseconds. Default: proportional to distance (300–800ms) */
4
- durationMs?: number;
5
- /** Number of intermediate points. Default: 30 */
6
- steps?: number;
7
- }
8
- /**
9
- * Move the mouse from `from` to `to` along a cubic Bezier curve with natural
10
- * human-like motion: ease-in-out timing, randomized control points, micro-jitter.
11
- */
12
- export declare function moveMouse(page: Page, from: {
13
- x: number;
14
- y: number;
15
- }, to: {
16
- x: number;
17
- y: number;
18
- }, options?: BezierMoveOptions): Promise<void>;
19
- /**
20
- * Move the mouse to `target` with Bezier curve animation and click.
21
- *
22
- * @param fromCurrent - Optional current mouse position. If omitted, the click
23
- * is performed at `target` without a preceding Bezier move (first action).
24
- */
25
- export declare function animatedClick(page: Page, target: {
26
- x: number;
27
- y: number;
28
- }, fromCurrent?: {
29
- x: number;
30
- y: number;
31
- }, options?: BezierMoveOptions): Promise<void>;
32
- /**
33
- * Move the mouse to `target` (for hover/highlight actions) without clicking.
34
- */
35
- export declare function animatedHover(page: Page, target: {
36
- x: number;
37
- y: number;
38
- }, fromCurrent?: {
39
- x: number;
40
- y: number;
41
- }, options?: BezierMoveOptions): Promise<void>;
42
- /**
43
- * Type text into the currently focused element at a human-like typing speed.
44
- * Assumes the field is already focused (via a preceding click).
45
- */
46
- export declare function humanType(page: Page, text: string): Promise<void>;