autokap 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-config.d.ts +13 -0
- package/dist/cli-config.js +42 -0
- package/dist/cli-utils.d.ts +0 -19
- package/dist/cli-utils.js +2 -65
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +266 -305
- package/package.json +26 -19
- package/assets/chrome/ios-statusbar-comparison-reference.jpg +0 -0
- package/assets/chrome/ios-statusbar-dark-reference.jpg +0 -0
- package/assets/chrome/ios-statusbar-light-reference.jpg +0 -0
- package/assets/devices/ipad-pro-11-m4.json +0 -52
- package/assets/devices/iphone-16-pro.json +0 -53
- package/assets/devices/macbook-air-13.json +0 -45
- package/assets/frames/MacBook Air 13.svg +0 -242
- package/assets/frames/Status bar - iPhone.png +0 -0
- package/assets/frames/Status bar and Menu bar- iPad.png +0 -0
- package/assets/frames/iPad Pro M4 11_.png +0 -0
- package/assets/frames/iPhone 16 Pro.png +0 -0
- package/assets/icons/Cellular Connection.svg +0 -3
- package/assets/icons/Union.svg +0 -6
- package/assets/icons/Wifi.svg +0 -3
- package/assets/icons/battery.svg +0 -5
- package/assets/icons/battery_charging.svg +0 -8
- package/dist/abort.d.ts +0 -5
- package/dist/abort.js +0 -44
- package/dist/agent.d.ts +0 -142
- package/dist/agent.js +0 -4504
- package/dist/browser-bar.d.ts +0 -40
- package/dist/browser-bar.js +0 -147
- package/dist/clip-orchestrator.d.ts +0 -148
- package/dist/clip-orchestrator.js +0 -950
- package/dist/clip-postprocess.d.ts +0 -42
- package/dist/clip-postprocess.js +0 -192
- package/dist/credential-templates.d.ts +0 -5
- package/dist/credential-templates.js +0 -60
- package/dist/element-capture.d.ts +0 -53
- package/dist/element-capture.js +0 -766
- package/dist/hybrid-navigator.d.ts +0 -138
- package/dist/hybrid-navigator.js +0 -468
- package/dist/index.d.ts +0 -15
- package/dist/index.js +0 -11
- package/dist/llm-usage.d.ts +0 -17
- package/dist/llm-usage.js +0 -45
- package/dist/mockup-html.d.ts +0 -119
- package/dist/mockup-html.js +0 -253
- package/dist/mockup.d.ts +0 -94
- package/dist/mockup.js +0 -604
- package/dist/mouse-animation.d.ts +0 -46
- package/dist/mouse-animation.js +0 -100
- package/dist/overlay-utils.d.ts +0 -14
- package/dist/overlay-utils.js +0 -13
- package/dist/posthog.d.ts +0 -4
- package/dist/posthog.js +0 -26
- package/dist/prompt-cache.d.ts +0 -10
- package/dist/prompt-cache.js +0 -24
- package/dist/prompts.d.ts +0 -167
- package/dist/prompts.js +0 -1165
- package/dist/security.d.ts +0 -20
- package/dist/security.js +0 -569
- package/dist/session-profile.d.ts +0 -86
- package/dist/session-profile.js +0 -1471
- package/dist/sf-pro-fonts.d.ts +0 -4
- package/dist/sf-pro-fonts.js +0 -7
- package/dist/status-bar-l10n.d.ts +0 -14
- package/dist/status-bar-l10n.js +0 -177
- package/dist/status-bar.d.ts +0 -44
- package/dist/status-bar.js +0 -336
- package/dist/tools.d.ts +0 -4
- package/dist/tools.js +0 -578
- package/dist/video-agent.d.ts +0 -143
- package/dist/video-agent.js +0 -4783
- package/dist/video-observation.d.ts +0 -36
- package/dist/video-observation.js +0 -192
- package/dist/video-planner.d.ts +0 -12
- package/dist/video-planner.js +0 -500
- package/dist/video-prompts.d.ts +0 -37
- package/dist/video-prompts.js +0 -554
- package/dist/video-tools.d.ts +0 -3
- package/dist/video-tools.js +0 -59
- package/dist/video-variant-state.d.ts +0 -29
- package/dist/video-variant-state.js +0 -80
- package/dist/vision-model.d.ts +0 -17
- 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>;
|