deio-scroll 1.0.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 (205) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -0
  3. package/dist/api/server.d.ts +3 -0
  4. package/dist/api/server.d.ts.map +1 -0
  5. package/dist/api/server.js +393 -0
  6. package/dist/api/server.js.map +1 -0
  7. package/dist/artifacts/store.d.ts +24 -0
  8. package/dist/artifacts/store.d.ts.map +1 -0
  9. package/dist/artifacts/store.js +43 -0
  10. package/dist/artifacts/store.js.map +1 -0
  11. package/dist/browser/componentInteraction.d.ts +24 -0
  12. package/dist/browser/componentInteraction.d.ts.map +1 -0
  13. package/dist/browser/componentInteraction.js +328 -0
  14. package/dist/browser/componentInteraction.js.map +1 -0
  15. package/dist/browser/composition.d.ts +42 -0
  16. package/dist/browser/composition.d.ts.map +1 -0
  17. package/dist/browser/composition.js +132 -0
  18. package/dist/browser/composition.js.map +1 -0
  19. package/dist/browser/cookies.d.ts +3 -0
  20. package/dist/browser/cookies.d.ts.map +1 -0
  21. package/dist/browser/cookies.js +20 -0
  22. package/dist/browser/cookies.js.map +1 -0
  23. package/dist/browser/curves.d.ts +4 -0
  24. package/dist/browser/curves.d.ts.map +1 -0
  25. package/dist/browser/curves.js +35 -0
  26. package/dist/browser/curves.js.map +1 -0
  27. package/dist/browser/detectScrollMode.d.ts +5 -0
  28. package/dist/browser/detectScrollMode.d.ts.map +1 -0
  29. package/dist/browser/detectScrollMode.js +61 -0
  30. package/dist/browser/detectScrollMode.js.map +1 -0
  31. package/dist/browser/directionGuardrails.d.ts +22 -0
  32. package/dist/browser/directionGuardrails.d.ts.map +1 -0
  33. package/dist/browser/directionGuardrails.js +99 -0
  34. package/dist/browser/directionGuardrails.js.map +1 -0
  35. package/dist/browser/goto.d.ts +9 -0
  36. package/dist/browser/goto.d.ts.map +1 -0
  37. package/dist/browser/goto.js +62 -0
  38. package/dist/browser/goto.js.map +1 -0
  39. package/dist/browser/hydrate.d.ts +8 -0
  40. package/dist/browser/hydrate.d.ts.map +1 -0
  41. package/dist/browser/hydrate.js +73 -0
  42. package/dist/browser/hydrate.js.map +1 -0
  43. package/dist/browser/mediaClock.d.ts +9 -0
  44. package/dist/browser/mediaClock.d.ts.map +1 -0
  45. package/dist/browser/mediaClock.js +86 -0
  46. package/dist/browser/mediaClock.js.map +1 -0
  47. package/dist/browser/motion.d.ts +21 -0
  48. package/dist/browser/motion.d.ts.map +1 -0
  49. package/dist/browser/motion.js +42 -0
  50. package/dist/browser/motion.js.map +1 -0
  51. package/dist/browser/networkPolicy.d.ts +11 -0
  52. package/dist/browser/networkPolicy.d.ts.map +1 -0
  53. package/dist/browser/networkPolicy.js +119 -0
  54. package/dist/browser/networkPolicy.js.map +1 -0
  55. package/dist/browser/prime.d.ts +3 -0
  56. package/dist/browser/prime.d.ts.map +1 -0
  57. package/dist/browser/prime.js +13 -0
  58. package/dist/browser/prime.js.map +1 -0
  59. package/dist/browser/sanitize.d.ts +3 -0
  60. package/dist/browser/sanitize.d.ts.map +1 -0
  61. package/dist/browser/sanitize.js +37 -0
  62. package/dist/browser/sanitize.js.map +1 -0
  63. package/dist/browser/scroll.d.ts +29 -0
  64. package/dist/browser/scroll.d.ts.map +1 -0
  65. package/dist/browser/scroll.js +420 -0
  66. package/dist/browser/scroll.js.map +1 -0
  67. package/dist/browser/scrollEasing.d.ts +5 -0
  68. package/dist/browser/scrollEasing.d.ts.map +1 -0
  69. package/dist/browser/scrollEasing.js +40 -0
  70. package/dist/browser/scrollEasing.js.map +1 -0
  71. package/dist/browser/scrollReadiness.d.ts +25 -0
  72. package/dist/browser/scrollReadiness.d.ts.map +1 -0
  73. package/dist/browser/scrollReadiness.js +105 -0
  74. package/dist/browser/scrollReadiness.js.map +1 -0
  75. package/dist/browser/urlGuard.d.ts +4 -0
  76. package/dist/browser/urlGuard.d.ts.map +1 -0
  77. package/dist/browser/urlGuard.js +22 -0
  78. package/dist/browser/urlGuard.js.map +1 -0
  79. package/dist/browser/virtualScroll.d.ts +19 -0
  80. package/dist/browser/virtualScroll.d.ts.map +1 -0
  81. package/dist/browser/virtualScroll.js +50 -0
  82. package/dist/browser/virtualScroll.js.map +1 -0
  83. package/dist/capture/detectMargins.d.ts +6 -0
  84. package/dist/capture/detectMargins.d.ts.map +1 -0
  85. package/dist/capture/detectMargins.js +145 -0
  86. package/dist/capture/detectMargins.js.map +1 -0
  87. package/dist/capture/frameRecorder.d.ts +29 -0
  88. package/dist/capture/frameRecorder.d.ts.map +1 -0
  89. package/dist/capture/frameRecorder.js +74 -0
  90. package/dist/capture/frameRecorder.js.map +1 -0
  91. package/dist/capture/stitchFrames.d.ts +9 -0
  92. package/dist/capture/stitchFrames.d.ts.map +1 -0
  93. package/dist/capture/stitchFrames.js +62 -0
  94. package/dist/capture/stitchFrames.js.map +1 -0
  95. package/dist/cli.d.ts +3 -0
  96. package/dist/cli.d.ts.map +1 -0
  97. package/dist/cli.js +329 -0
  98. package/dist/cli.js.map +1 -0
  99. package/dist/config/browserLaunch.d.ts +13 -0
  100. package/dist/config/browserLaunch.d.ts.map +1 -0
  101. package/dist/config/browserLaunch.js +63 -0
  102. package/dist/config/browserLaunch.js.map +1 -0
  103. package/dist/config/recordingProfile.d.ts +10 -0
  104. package/dist/config/recordingProfile.d.ts.map +1 -0
  105. package/dist/config/recordingProfile.js +25 -0
  106. package/dist/config/recordingProfile.js.map +1 -0
  107. package/dist/config/runtimeMode.d.ts +2 -0
  108. package/dist/config/runtimeMode.d.ts.map +1 -0
  109. package/dist/config/runtimeMode.js +4 -0
  110. package/dist/config/runtimeMode.js.map +1 -0
  111. package/dist/config/virtualScroll.d.ts +8 -0
  112. package/dist/config/virtualScroll.d.ts.map +1 -0
  113. package/dist/config/virtualScroll.js +19 -0
  114. package/dist/config/virtualScroll.js.map +1 -0
  115. package/dist/contracts.d.ts +157 -0
  116. package/dist/contracts.d.ts.map +1 -0
  117. package/dist/contracts.js +136 -0
  118. package/dist/contracts.js.map +1 -0
  119. package/dist/editor/compileVideo.d.ts +21 -0
  120. package/dist/editor/compileVideo.d.ts.map +1 -0
  121. package/dist/editor/compileVideo.js +104 -0
  122. package/dist/editor/compileVideo.js.map +1 -0
  123. package/dist/editor/composeComparison.d.ts +30 -0
  124. package/dist/editor/composeComparison.d.ts.map +1 -0
  125. package/dist/editor/composeComparison.js +315 -0
  126. package/dist/editor/composeComparison.js.map +1 -0
  127. package/dist/editor/composeResponsiveness.d.ts +19 -0
  128. package/dist/editor/composeResponsiveness.d.ts.map +1 -0
  129. package/dist/editor/composeResponsiveness.js +198 -0
  130. package/dist/editor/composeResponsiveness.js.map +1 -0
  131. package/dist/editor/frameVideo.d.ts +11 -0
  132. package/dist/editor/frameVideo.d.ts.map +1 -0
  133. package/dist/editor/frameVideo.js +181 -0
  134. package/dist/editor/frameVideo.js.map +1 -0
  135. package/dist/index.d.ts +3 -0
  136. package/dist/index.d.ts.map +1 -0
  137. package/dist/index.js +2 -0
  138. package/dist/index.js.map +1 -0
  139. package/dist/inspection/inspectWebsite.d.ts +21 -0
  140. package/dist/inspection/inspectWebsite.d.ts.map +1 -0
  141. package/dist/inspection/inspectWebsite.js +412 -0
  142. package/dist/inspection/inspectWebsite.js.map +1 -0
  143. package/dist/jobs/manager.d.ts +123 -0
  144. package/dist/jobs/manager.d.ts.map +1 -0
  145. package/dist/jobs/manager.js +378 -0
  146. package/dist/jobs/manager.js.map +1 -0
  147. package/dist/jobs/store.d.ts +150 -0
  148. package/dist/jobs/store.d.ts.map +1 -0
  149. package/dist/jobs/store.js +446 -0
  150. package/dist/jobs/store.js.map +1 -0
  151. package/dist/jobs/thumbnail.d.ts +2 -0
  152. package/dist/jobs/thumbnail.d.ts.map +1 -0
  153. package/dist/jobs/thumbnail.js +36 -0
  154. package/dist/jobs/thumbnail.js.map +1 -0
  155. package/dist/pipeline/processRecordingRequest.d.ts +4 -0
  156. package/dist/pipeline/processRecordingRequest.d.ts.map +1 -0
  157. package/dist/pipeline/processRecordingRequest.js +17 -0
  158. package/dist/pipeline/processRecordingRequest.js.map +1 -0
  159. package/dist/pipeline/recordComparison.d.ts +4 -0
  160. package/dist/pipeline/recordComparison.d.ts.map +1 -0
  161. package/dist/pipeline/recordComparison.js +99 -0
  162. package/dist/pipeline/recordComparison.js.map +1 -0
  163. package/dist/pipeline/recordResponsiveness.d.ts +4 -0
  164. package/dist/pipeline/recordResponsiveness.d.ts.map +1 -0
  165. package/dist/pipeline/recordResponsiveness.js +122 -0
  166. package/dist/pipeline/recordResponsiveness.js.map +1 -0
  167. package/dist/pipeline/recordWebsite.d.ts +11 -0
  168. package/dist/pipeline/recordWebsite.d.ts.map +1 -0
  169. package/dist/pipeline/recordWebsite.js +446 -0
  170. package/dist/pipeline/recordWebsite.js.map +1 -0
  171. package/dist/pipeline/styleRecording.d.ts +12 -0
  172. package/dist/pipeline/styleRecording.d.ts.map +1 -0
  173. package/dist/pipeline/styleRecording.js +59 -0
  174. package/dist/pipeline/styleRecording.js.map +1 -0
  175. package/dist/transcode/ffmpeg.d.ts +4 -0
  176. package/dist/transcode/ffmpeg.d.ts.map +1 -0
  177. package/dist/transcode/ffmpeg.js +62 -0
  178. package/dist/transcode/ffmpeg.js.map +1 -0
  179. package/dist/transcode/probe.d.ts +7 -0
  180. package/dist/transcode/probe.d.ts.map +1 -0
  181. package/dist/transcode/probe.js +91 -0
  182. package/dist/transcode/probe.js.map +1 -0
  183. package/dist/transcode/quality.d.ts +8 -0
  184. package/dist/transcode/quality.d.ts.map +1 -0
  185. package/dist/transcode/quality.js +13 -0
  186. package/dist/transcode/quality.js.map +1 -0
  187. package/dist/types.d.ts +302 -0
  188. package/dist/types.d.ts.map +1 -0
  189. package/dist/types.js +2 -0
  190. package/dist/types.js.map +1 -0
  191. package/dist/worker.d.ts +3 -0
  192. package/dist/worker.d.ts.map +1 -0
  193. package/dist/worker.js +22 -0
  194. package/dist/worker.js.map +1 -0
  195. package/package.json +63 -0
  196. package/public/assets/index-C63t4P1L.css +1 -0
  197. package/public/assets/index-Sfgf7dSH.js +9 -0
  198. package/public/background_presets/gray_noise_gradient.png +0 -0
  199. package/public/background_presets/paper_blue.png +0 -0
  200. package/public/background_presets/red_blocks_gradient.png +0 -0
  201. package/public/deio-scroll-mark.svg +12 -0
  202. package/public/favicon.svg +1 -0
  203. package/public/icons.svg +24 -0
  204. package/public/index.html +16 -0
  205. package/public/scrollizard-mark.png +0 -0
@@ -0,0 +1,181 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { randomUUID } from "node:crypto";
6
+ import { probeVideoDurationMs, probeVideoFps, probeVideoSize } from "../transcode/probe.js";
7
+ const PRESET_FILES = {
8
+ gray_noise_gradient: "gray_noise_gradient.png",
9
+ paper_blue: "paper_blue.png",
10
+ red_blocks_gradient: "red_blocks_gradient.png",
11
+ };
12
+ const CONTENT_SCALE = 0.84;
13
+ export async function frameVideoOnBackground(options) {
14
+ const { inputPath, outputPath, preset, addShadow, roundedCorners = false, encode } = options;
15
+ const size = await probeVideoSize(inputPath);
16
+ if (!size)
17
+ throw new Error("Could not read video dimensions for background export");
18
+ const fps = (await probeVideoFps(inputPath)) ?? 30;
19
+ const durationMs = await probeVideoDurationMs(inputPath);
20
+ const width = even(size.width);
21
+ const height = even(size.height);
22
+ const contentWidth = even(Math.round(width * CONTENT_SCALE));
23
+ const contentHeight = even(Math.round(height * CONTENT_SCALE));
24
+ const x = Math.floor((width - contentWidth) / 2);
25
+ const y = Math.floor((height - contentHeight) / 2);
26
+ const cornerRadius = roundedCorners
27
+ ? Math.max(6, Math.round(Math.min(contentWidth, contentHeight) * 0.014))
28
+ : 0;
29
+ const backgroundPath = await resolvePresetPath(PRESET_FILES[preset]);
30
+ // Corners and shadows do not change from frame to frame. Building them once
31
+ // avoids running geq/gblur thousands of times on long recordings.
32
+ const staticLayers = await createStaticLayers({
33
+ outputPath,
34
+ width,
35
+ height,
36
+ contentWidth,
37
+ contentHeight,
38
+ x,
39
+ y,
40
+ cornerRadius,
41
+ addShadow,
42
+ });
43
+ try {
44
+ const filters = [
45
+ `[1:v]scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height},setsar=1[bg]`,
46
+ `[0:v]scale=${contentWidth}:${contentHeight}:flags=lanczos,setsar=1,format=rgba[card-base]`,
47
+ ];
48
+ const inputs = ["-y", "-i", inputPath, "-loop", "1", "-framerate", String(fps), "-i", backgroundPath];
49
+ let inputIndex = 2;
50
+ if (staticLayers.maskPath) {
51
+ inputs.push("-loop", "1", "-framerate", String(fps), "-i", staticLayers.maskPath);
52
+ filters.push(`[${inputIndex}:v]format=gray[mask]`, `[card-base][mask]alphamerge[card]`);
53
+ inputIndex += 1;
54
+ }
55
+ else {
56
+ filters.push("[card-base]copy[card]");
57
+ }
58
+ if (staticLayers.shadowPath) {
59
+ inputs.push("-loop", "1", "-framerate", String(fps), "-i", staticLayers.shadowPath);
60
+ filters.push(`[${inputIndex}:v]format=rgba[shadow]`, "[bg][shadow]overlay=0:0[canvas]", `[canvas][card]overlay=${x}:${y}:shortest=1,fps=${fps},format=yuv420p[output]`);
61
+ }
62
+ else {
63
+ filters.push(`[bg][card]overlay=${x}:${y}:shortest=1,fps=${fps},format=yuv420p[output]`);
64
+ }
65
+ await runFfmpeg([
66
+ ...inputs,
67
+ "-filter_complex",
68
+ filters.join(";"),
69
+ "-map",
70
+ "[output]",
71
+ "-an",
72
+ "-c:v",
73
+ "libx264",
74
+ "-preset",
75
+ encode.preset,
76
+ "-crf",
77
+ String(encode.crf),
78
+ "-pix_fmt",
79
+ "yuv420p",
80
+ ...(durationMs ? ["-t", (durationMs / 1000).toFixed(3)] : []),
81
+ "-shortest",
82
+ "-movflags",
83
+ "+faststart",
84
+ outputPath,
85
+ ]);
86
+ }
87
+ finally {
88
+ await Promise.all(staticLayers.files.map((file) => fs.unlink(file).catch(() => undefined)));
89
+ }
90
+ }
91
+ async function createStaticLayers(options) {
92
+ const { outputPath, width, height, contentWidth, contentHeight, x, y, cornerRadius, addShadow } = options;
93
+ if (!cornerRadius && !addShadow)
94
+ return { files: [], maskPath: undefined, shadowPath: undefined };
95
+ const id = randomUUID();
96
+ const dir = path.dirname(outputPath);
97
+ const maskPath = path.join(dir, `.style-mask-${id}.png`);
98
+ const shadowPath = addShadow ? path.join(dir, `.style-shadow-${id}.png`) : undefined;
99
+ const maskFilter = cornerRadius ? roundedMaskFilter(cornerRadius) : "format=gray";
100
+ await runFfmpeg([
101
+ "-y", "-f", "lavfi", "-i", `color=c=white:s=${contentWidth}x${contentHeight}:r=1`,
102
+ "-vf", maskFilter,
103
+ "-frames:v", "1", "-c:v", "png", maskPath,
104
+ ]);
105
+ if (shadowPath) {
106
+ // Soft bottom drop-shadow: mainly below the card so it reads on light and busy
107
+ // backgrounds. Slight X inset + wider blur keeps it natural without a hard halo.
108
+ const shadowOffsetY = Math.max(22, Math.round(height * 0.032));
109
+ const shadowY = y + shadowOffsetY;
110
+ const shadowBlur = Math.max(28, Math.round(Math.min(width, height) * 0.034));
111
+ const shadowAlpha = 0.38;
112
+ // Grow the shadow plate a touch so blur doesn't thin out at the sides.
113
+ const shadowPadX = Math.max(8, Math.round(contentWidth * 0.012));
114
+ const shadowW = even(contentWidth + shadowPadX * 2);
115
+ const shadowH = even(contentHeight + Math.round(shadowOffsetY * 0.35));
116
+ const shadowX = Math.max(0, x - shadowPadX);
117
+ await runFfmpeg([
118
+ "-y", "-i", maskPath,
119
+ "-f", "lavfi", "-i", `color=c=black:s=${contentWidth}x${contentHeight}:r=1`,
120
+ "-filter_complex",
121
+ // Soft plate → pad into full frame (offset down) → opacity → blur
122
+ `[0:v]format=gray,scale=${shadowW}:${shadowH}:flags=bilinear[mask];` +
123
+ `[1:v]format=rgba,scale=${shadowW}:${shadowH}:flags=bilinear[black];` +
124
+ `[black][mask]alphamerge,` +
125
+ `pad=${width}:${height}:${shadowX}:${shadowY}:color=black@0,` +
126
+ `format=rgba,colorchannelmixer=aa=${shadowAlpha},` +
127
+ `gblur=sigma=${shadowBlur}:steps=3[output]`,
128
+ "-map", "[output]", "-frames:v", "1", "-c:v", "png", shadowPath,
129
+ ]);
130
+ }
131
+ return {
132
+ files: shadowPath ? [maskPath, shadowPath] : [maskPath],
133
+ maskPath: cornerRadius ? maskPath : undefined,
134
+ shadowPath,
135
+ };
136
+ }
137
+ function roundedMaskFilter(radius) {
138
+ const right = `W-${radius}`;
139
+ const bottom = `H-${radius}`;
140
+ const outsideCorner = [
141
+ `lt(X,${radius})*lt(Y,${radius})*gt(hypot(X-${radius},Y-${radius}),${radius})`,
142
+ `gt(X,${right})*lt(Y,${radius})*gt(hypot(X-(${right}),Y-${radius}),${radius})`,
143
+ `lt(X,${radius})*gt(Y,${bottom})*gt(hypot(X-${radius},Y-(${bottom})),${radius})`,
144
+ `gt(X,${right})*gt(Y,${bottom})*gt(hypot(X-(${right}),Y-(${bottom})),${radius})`,
145
+ ].join("+");
146
+ return `format=gray,geq=lum='if(gt(${outsideCorner},0),0,255)'`;
147
+ }
148
+ async function resolvePresetPath(filename) {
149
+ const apiPublicDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../public/background_presets");
150
+ const builtPath = path.join(apiPublicDir, filename);
151
+ if (await exists(builtPath))
152
+ return builtPath;
153
+ // During Vite development, its public directory is the authoritative source.
154
+ const webPublicPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../web/public/background_presets", filename);
155
+ if (await exists(webPublicPath))
156
+ return webPublicPath;
157
+ throw new Error(`Background preset is unavailable: ${filename}`);
158
+ }
159
+ function even(value) {
160
+ return value % 2 === 0 ? value : value - 1;
161
+ }
162
+ async function exists(filePath) {
163
+ return fs.access(filePath).then(() => true).catch(() => false);
164
+ }
165
+ function runFfmpeg(args) {
166
+ return new Promise((resolve, reject) => {
167
+ const child = spawn("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
168
+ let stderr = "";
169
+ child.stderr.on("data", (chunk) => {
170
+ stderr += chunk.toString();
171
+ });
172
+ child.on("error", reject);
173
+ child.on("close", (code) => {
174
+ if (code === 0)
175
+ resolve();
176
+ else
177
+ reject(new Error(`Background render failed with code ${code}: ${stderr.slice(-2000)}`));
178
+ });
179
+ });
180
+ }
181
+ //# sourceMappingURL=frameVideo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frameVideo.js","sourceRoot":"","sources":["../../src/editor/frameVideo.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { recordWebsite } from './pipeline/recordWebsite.js';
2
+ export type * from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAC3D,mBAAmB,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { recordWebsite } from './pipeline/recordWebsite.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA"}
@@ -0,0 +1,21 @@
1
+ import type { WebsiteInspection, WebsiteSection } from "../types.js";
2
+ export interface RawWebsiteSection {
3
+ label: string;
4
+ selector: string;
5
+ kind: "heading" | "landmark";
6
+ y: number;
7
+ height: number;
8
+ stable: number;
9
+ }
10
+ export declare function inspectWebsite(options: {
11
+ targetUrl: string;
12
+ viewport?: {
13
+ width: number;
14
+ height: number;
15
+ };
16
+ }): Promise<WebsiteInspection>;
17
+ export declare function normalizeInspectionSections(raw: RawWebsiteSection[], safeViewport: {
18
+ topInsetPx: number;
19
+ bottomInsetPx: number;
20
+ }, viewportHeight: number, maxScroll: number): WebsiteSection[];
21
+ //# sourceMappingURL=inspectWebsite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspectWebsite.d.ts","sourceRoot":"","sources":["../../src/inspection/inspectWebsite.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,iBAAiB,EAEjB,cAAc,EACf,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,GAAG,UAAU,CAAC;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA2E7B;AAwED,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,iBAAiB,EAAE,EACxB,YAAY,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,EAC3D,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,MAAM,GAChB,cAAc,EAAE,CAoFlB"}
@@ -0,0 +1,412 @@
1
+ import { chromium } from "playwright";
2
+ import { gotoReachablePage } from "../browser/goto.js";
3
+ export async function inspectWebsite(options) {
4
+ const target = assertHttpUrl(options.targetUrl);
5
+ const viewport = options.viewport ?? { width: 1280, height: 720 };
6
+ const browser = await chromium.launch({ headless: true });
7
+ try {
8
+ const page = await browser.newPage({ viewport });
9
+ await gotoReachablePage(page, target.href);
10
+ await page
11
+ .waitForLoadState("networkidle", { timeout: 10_000 })
12
+ .catch(() => undefined);
13
+ const pageHeight = await page.evaluate(() => Math.max(document.body.scrollHeight, document.documentElement.scrollHeight));
14
+ const scrollMode = await detectInspectionScrollMode(page, pageHeight, viewport.height);
15
+ const safeViewport = await detectInspectionSafeViewport(page, viewport);
16
+ const sections = normalizeInspectionSections(await collectSections(page), safeViewport, viewport.height, Math.max(0, pageHeight - viewport.height));
17
+ const interactions = scrollMode === "document"
18
+ ? await collectInteractionCandidates(page)
19
+ : [];
20
+ const { screenshots, storyboard } = scrollMode === "virtual"
21
+ ? await takeVirtualStoryboardScreenshots(page, viewport)
22
+ : await takeDocumentStoryboardScreenshots(page, pageHeight, viewport.height);
23
+ if (scrollMode === "document") {
24
+ await attachSectionPreviewScreenshots(page, sections, screenshots, viewport.height);
25
+ }
26
+ const warnings = [];
27
+ if (page.url() !== target.href)
28
+ warnings.push(`Redirected to ${page.url()}`);
29
+ if (pageHeight <= viewport.height + 48 && scrollMode === "document") {
30
+ warnings.push("This page has little or no scrollable content.");
31
+ }
32
+ if (scrollMode === "virtual") {
33
+ warnings.push("Virtual scrolling detected; direction uses storyboard progress points.");
34
+ }
35
+ if (safeViewport.topInsetPx > 0) {
36
+ warnings.push(`A ${safeViewport.topInsetPx}px fixed header will be kept clear.`);
37
+ }
38
+ if (scrollMode === "document" && sections.length === 0) {
39
+ warnings.push("No semantic sections were detected; storyboard waypoints will be used.");
40
+ }
41
+ return {
42
+ url: page.url(),
43
+ title: await page.title(),
44
+ pageHeight,
45
+ viewport,
46
+ scrollMode,
47
+ safeViewport,
48
+ sections,
49
+ interactions,
50
+ storyboard,
51
+ screenshots,
52
+ warnings,
53
+ };
54
+ }
55
+ finally {
56
+ await browser.close();
57
+ }
58
+ }
59
+ async function collectInteractionCandidates(page) {
60
+ return page.evaluate(`(() => {
61
+ const blocked = /\\b(delete|remove|unsubscribe|sign out|log out|purchase|buy|pay|checkout|submit|send|publish|confirm|destroy|upload|download)\\b/i;
62
+ const pathFor = (element) => {
63
+ if (element.id) return "#" + CSS.escape(element.id);
64
+ for (const name of ["data-testid", "data-test", "data-qa"]) {
65
+ const value = element.getAttribute(name);
66
+ if (value) return "[" + name + "=\\"" + CSS.escape(value) + "\\"]";
67
+ }
68
+ const segments = [];
69
+ let current = element;
70
+ while (current && current !== document.body && segments.length < 5) {
71
+ const parent = current.parentElement;
72
+ const tag = current.tagName.toLowerCase();
73
+ const siblings = parent ? Array.from(parent.children).filter((child) => child.tagName === current.tagName) : [];
74
+ const suffix = siblings.length > 1 ? ":nth-of-type(" + (siblings.indexOf(current) + 1) + ")" : "";
75
+ segments.unshift(tag + suffix);
76
+ current = parent;
77
+ }
78
+ return "body > " + segments.join(" > ");
79
+ };
80
+ return Array.from(document.querySelectorAll("button,a[href],[role='button'],[role='tab'],[role='switch'],[role='menuitem'],[aria-expanded],summary,input:not([type='hidden'])"))
81
+ .flatMap((element, index) => {
82
+ const rect = element.getBoundingClientRect();
83
+ const style = getComputedStyle(element);
84
+ const label = (element.getAttribute("aria-label") || element.getAttribute("title") || element.textContent || element.value || "").replace(/\\s+/g, " ").trim().slice(0, 120);
85
+ if (!label || blocked.test(label) || rect.width < 28 || rect.height < 20 || style.display === "none" || style.visibility === "hidden" || Number(style.opacity) < 0.05 || element.hasAttribute("disabled") || element.getAttribute("aria-disabled") === "true") return [];
86
+ const tag = element.tagName.toLowerCase();
87
+ const role = element.getAttribute("role") || (tag === "a" ? "link" : tag);
88
+ const type = (element.getAttribute("type") || "").toLowerCase();
89
+ const semanticClick = ["tab", "switch", "menuitem", "option"].includes(role) || element.hasAttribute("aria-expanded") || tag === "summary" || (tag === "button" && type === "button") || (tag === "button" && !element.closest("form") && !type);
90
+ const focusable = element.tabIndex >= 0 || ["button", "a", "input", "summary"].includes(tag);
91
+ const actions = ["hover", ...(focusable ? ["focus"] : []), ...(semanticClick && tag !== "a" ? ["click"] : [])];
92
+ const selector = pathFor(element);
93
+ const id = "interaction_" + String(index + 1).padStart(2, "0");
94
+ const maxScroll = Math.max(0, document.documentElement.scrollHeight - innerHeight);
95
+ const progress = maxScroll === 0 ? 0 : Math.max(0, Math.min(1, (rect.top + scrollY - innerHeight / 2) / maxScroll));
96
+ const recommendedZoomScale = rect.width > innerWidth * 0.4 ? 1.12 : 1.28;
97
+ return [{
98
+ id,
99
+ selector,
100
+ label,
101
+ tag,
102
+ role,
103
+ actions,
104
+ rect: {
105
+ x: Math.round(rect.left + window.scrollX),
106
+ y: Math.round(rect.top + window.scrollY),
107
+ width: Math.round(rect.width),
108
+ height: Math.round(rect.height),
109
+ },
110
+ recommendedTarget: { type: "selector", selector, align: "center", fallbackProgress: progress },
111
+ recommendedHoldMs: semanticClick ? 1600 : 1300,
112
+ recommendedZoomScale,
113
+ recommendedInteraction: {
114
+ action: "hover",
115
+ candidateId: id,
116
+ label,
117
+ role,
118
+ zoomScale: recommendedZoomScale,
119
+ showCursor: true,
120
+ },
121
+ }];
122
+ })
123
+ .slice(0, 30);
124
+ })()`);
125
+ }
126
+ export function normalizeInspectionSections(raw, safeViewport, viewportHeight, maxScroll) {
127
+ const safeTop = safeViewport.topInsetPx + 24;
128
+ const safeBottom = viewportHeight - safeViewport.bottomInsetPx;
129
+ const headingLeadPx = Math.round((safeBottom - safeTop) * 0.14);
130
+ const scored = raw
131
+ .map((section) => ({
132
+ ...section,
133
+ score: (section.kind === "heading" ? 4 : 0) + section.stable * 2,
134
+ recommendedAlign: "top",
135
+ recommendedOffsetPx: section.kind === "heading" ? -headingLeadPx : 0,
136
+ targetY: Math.max(0, Math.min(maxScroll, section.kind === "heading"
137
+ ? section.y - safeTop - headingLeadPx
138
+ : section.y - safeTop)),
139
+ }))
140
+ .sort((a, b) => a.y - b.y || b.score - a.score);
141
+ const selected = [];
142
+ for (const section of scored) {
143
+ const nearby = selected.findIndex((candidate) => Math.abs(candidate.y - section.y) <= 160);
144
+ if (nearby < 0)
145
+ selected.push(section);
146
+ else {
147
+ const candidate = selected[nearby];
148
+ const sameVisualRow = candidate.kind === "heading"
149
+ && section.kind === "heading"
150
+ && Math.abs(candidate.y - section.y) <= 48;
151
+ const candidateLabel = candidate.label.toLowerCase();
152
+ const sectionLabel = section.label.toLowerCase();
153
+ const distinctLabels = candidateLabel !== sectionLabel
154
+ && !candidateLabel.includes(sectionLabel)
155
+ && !sectionLabel.includes(candidateLabel);
156
+ if (sameVisualRow && distinctLabels) {
157
+ candidate.label = `${candidate.label} · ${section.label}`.slice(0, 120);
158
+ }
159
+ else if (section.score > candidate.score) {
160
+ selected[nearby] = section;
161
+ }
162
+ }
163
+ }
164
+ let previousTarget = 0;
165
+ return selected
166
+ .sort((a, b) => a.y - b.y)
167
+ .slice(0, 30)
168
+ .map((section) => {
169
+ const distanceFromPrevious = Math.max(0, section.targetY - previousTarget);
170
+ previousTarget = section.targetY;
171
+ const recommendedTransitionMs = Math.max(600, Math.ceil((((distanceFromPrevious * 1.9) /
172
+ Math.max(1, viewportHeight * 1.5)) *
173
+ 1000) /
174
+ 50) * 50);
175
+ return {
176
+ label: section.label,
177
+ selector: section.selector,
178
+ kind: section.kind,
179
+ y: section.y,
180
+ height: section.height,
181
+ targetY: section.targetY,
182
+ progress: maxScroll === 0 ? 0 : section.targetY / maxScroll,
183
+ distanceFromPrevious,
184
+ recommendedTransitionMs,
185
+ recommendedTarget: {
186
+ type: "selector",
187
+ selector: section.selector,
188
+ align: section.recommendedAlign,
189
+ ...(section.recommendedOffsetPx
190
+ ? { offsetPx: section.recommendedOffsetPx }
191
+ : {}),
192
+ },
193
+ };
194
+ });
195
+ }
196
+ async function collectSections(page) {
197
+ return page.evaluate(`(() => {
198
+ const pathFor = (element) => {
199
+ if (element.id) return "#" + CSS.escape(element.id);
200
+ const framerName = element.getAttribute("data-framer-name");
201
+ if (framerName) {
202
+ const namedSelector = "[data-framer-name=\\"" + CSS.escape(framerName) + "\\"]";
203
+ if (document.querySelectorAll(namedSelector).length === 1) return namedSelector;
204
+ }
205
+ const segments = [];
206
+ let current = element;
207
+ while (current && current !== document.body && segments.length < 12) {
208
+ const parent = current.parentElement;
209
+ const tag = current.tagName.toLowerCase();
210
+ const index = parent ? Array.from(parent.children).filter((child) => child.tagName === current.tagName).indexOf(current) + 1 : 1;
211
+ segments.unshift(tag + ":nth-of-type(" + index + ")");
212
+ current = parent;
213
+ }
214
+ return current === document.body
215
+ ? "body > " + segments.join(" > ")
216
+ : "body " + segments.join(" > ");
217
+ };
218
+ const genericFramerNames = new Set([
219
+ "app", "background", "bg", "border", "bottom", "container", "content",
220
+ "default", "desktop", "footer", "header", "interface", "layout", "left",
221
+ "main", "mobile", "new layout", "right", "section", "shade", "tablet",
222
+ "top", "ui", "visual", "wrapper",
223
+ ]);
224
+ const semantic = Array.from(document.querySelectorAll("h1,h2,h3,main,section,article,footer,[role='main'],[role='region']"))
225
+ // A footer inside main/section/article is commonly part of a UI mockup
226
+ // or card. Treat only page-level footers as navigation landmarks.
227
+ .filter((element) => element.tagName.toLowerCase() !== "footer" || !element.closest("main,section,article"))
228
+ .map((element) => {
229
+ const tag = element.tagName.toLowerCase();
230
+ const heading = /^h[1-3]$/.test(tag) ? element : element.querySelector("h1,h2,h3");
231
+ const focus = heading || element;
232
+ const rect = focus.getBoundingClientRect();
233
+ const framerName = element.getAttribute("data-framer-name") || "";
234
+ const namedLabel = genericFramerNames.has(framerName.toLowerCase()) ? "" : framerName;
235
+ const text = (
236
+ element.getAttribute("aria-label")
237
+ || heading?.innerText
238
+ || heading?.textContent
239
+ || namedLabel
240
+ || (tag === "footer" ? "Footer" : element.innerText || element.textContent)
241
+ || ""
242
+ ).replace(/\\s+/g, " ").trim();
243
+ return { label: text.slice(0, 120) || tag, selector: pathFor(focus), kind: heading ? "heading" : "landmark", y: Math.max(0, Math.round(rect.top + window.scrollY)), height: Math.max(1, Math.round(rect.height)), stable: focus.id ? 1 : 0, visible: rect.width > 0 && rect.height > 0 };
244
+ })
245
+ .filter((section) => section.visible && section.label.length > 0 && Number.isFinite(section.y));
246
+ const namedBands = Array.from(document.querySelectorAll("main [data-framer-name]"))
247
+ .flatMap((element) => {
248
+ const name = (element.getAttribute("data-framer-name") || "").replace(/\\s+/g, " ").trim();
249
+ const normalizedName = name.toLowerCase();
250
+ if (
251
+ name.length < 3
252
+ || name.length > 60
253
+ || genericFramerNames.has(normalizedName)
254
+ || element.querySelector("h1,h2,h3")
255
+ ) return [];
256
+ const main = element.closest("main");
257
+ let depth = 0;
258
+ let current = element;
259
+ while (current && current !== main) {
260
+ depth += 1;
261
+ current = current.parentElement;
262
+ }
263
+ const rect = element.getBoundingClientRect();
264
+ const style = getComputedStyle(element);
265
+ const visible =
266
+ rect.width >= innerWidth * 0.65
267
+ && rect.height >= Math.max(280, innerHeight * 0.34)
268
+ && style.display !== "none"
269
+ && style.visibility !== "hidden"
270
+ && Number(style.opacity || 1) > 0
271
+ && style.position !== "fixed"
272
+ && style.position !== "sticky";
273
+ if (!main || depth > 2 || !visible) return [];
274
+ return [{
275
+ label: name,
276
+ selector: pathFor(element),
277
+ kind: "landmark",
278
+ y: Math.max(0, Math.round(rect.top + window.scrollY)),
279
+ height: Math.max(1, Math.round(rect.height)),
280
+ stable: element.id || document.querySelectorAll("[data-framer-name=\\"" + CSS.escape(name) + "\\"]").length === 1 ? 1 : 0,
281
+ visible: true,
282
+ }];
283
+ });
284
+ return [...semantic, ...namedBands];
285
+ })()`);
286
+ }
287
+ async function detectInspectionSafeViewport(page, viewport) {
288
+ const topInsetPx = await page.evaluate(`(() => Array.from(document.querySelectorAll("body *")).reduce((inset, element) => {
289
+ const style = getComputedStyle(element); const rect = element.getBoundingClientRect();
290
+ const pinned = style.position === "fixed" || style.position === "sticky";
291
+ const visible = style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity || 1) > 0;
292
+ const topBar = rect.top <= 8 && rect.bottom > 0 && rect.width >= ${viewport.width} * 0.5 && rect.height >= 24 && rect.height <= ${viewport.height} * 0.3;
293
+ return pinned && visible && topBar ? Math.max(inset, Math.round(rect.bottom)) : inset;
294
+ }, 0))()`);
295
+ return { topInsetPx, bottomInsetPx: 24 };
296
+ }
297
+ async function takeDocumentStoryboardScreenshots(page, pageHeight, viewportHeight) {
298
+ const maxScroll = Math.max(0, pageHeight - viewportHeight);
299
+ const positions = [
300
+ ...new Set([
301
+ 0,
302
+ Math.round(maxScroll / 3),
303
+ Math.round((maxScroll * 2) / 3),
304
+ maxScroll,
305
+ ]),
306
+ ];
307
+ const screenshots = [];
308
+ const storyboard = [];
309
+ for (const position of positions) {
310
+ await page.evaluate(`window.scrollTo(0, ${position})`);
311
+ await page.waitForTimeout(250);
312
+ screenshots.push((await page.screenshot({ type: "jpeg", quality: 60 })).toString("base64"));
313
+ storyboard.push({
314
+ imageIndex: screenshots.length - 1,
315
+ target: { type: "progress", value: maxScroll === 0 ? 0 : position / maxScroll },
316
+ y: position,
317
+ });
318
+ }
319
+ return { screenshots, storyboard };
320
+ }
321
+ async function attachSectionPreviewScreenshots(page, sections, screenshots, viewportHeight) {
322
+ if (sections.length === 0)
323
+ return;
324
+ const allPreviewPositions = [];
325
+ const minimumSpacing = Math.max(180, viewportHeight * 0.28);
326
+ for (const section of sections) {
327
+ const nearby = allPreviewPositions.findIndex((position) => Math.abs(position - section.targetY) < minimumSpacing);
328
+ if (nearby < 0) {
329
+ allPreviewPositions.push(section.targetY);
330
+ }
331
+ }
332
+ allPreviewPositions.sort((a, b) => a - b);
333
+ const maximumPreviewCount = 16;
334
+ const previewPositions = allPreviewPositions.length <= maximumPreviewCount
335
+ ? allPreviewPositions
336
+ : [...new Set(Array.from({ length: maximumPreviewCount }, (_, index) => allPreviewPositions[Math.round((index * (allPreviewPositions.length - 1)) / (maximumPreviewCount - 1))]))];
337
+ const indexes = new Map();
338
+ for (const position of previewPositions) {
339
+ await page.evaluate(`window.scrollTo({ top: ${Math.round(position)}, left: 0, behavior: "instant" })`);
340
+ await page.waitForTimeout(120);
341
+ screenshots.push((await page.screenshot({ type: "jpeg", quality: 68 })).toString("base64"));
342
+ indexes.set(position, screenshots.length - 1);
343
+ }
344
+ for (const section of sections) {
345
+ const closest = previewPositions.reduce((best, position) => {
346
+ if (best === undefined)
347
+ return position;
348
+ return Math.abs(position - section.targetY) < Math.abs(best - section.targetY)
349
+ ? position
350
+ : best;
351
+ }, undefined);
352
+ if (closest !== undefined)
353
+ section.imageIndex = indexes.get(closest);
354
+ }
355
+ }
356
+ async function takeVirtualStoryboardScreenshots(page, viewport) {
357
+ const progressPoints = [0, 0.25, 0.5, 0.75, 1];
358
+ const wheelBudget = viewport.height * 8;
359
+ const centerX = Math.floor(viewport.width / 2);
360
+ const centerY = Math.floor(viewport.height / 2);
361
+ const cdp = await page.context().newCDPSession(page);
362
+ const screenshots = [];
363
+ const storyboard = [];
364
+ let dispatched = 0;
365
+ try {
366
+ await page.mouse.move(centerX, centerY);
367
+ await page.mouse.click(centerX, centerY);
368
+ for (const value of progressPoints) {
369
+ const target = value * wheelBudget;
370
+ if (target > dispatched) {
371
+ const delta = (target - dispatched) / 20;
372
+ for (let tick = 0; tick < 20; tick += 1) {
373
+ await cdp.send("Input.dispatchMouseEvent", {
374
+ type: "mouseWheel",
375
+ x: centerX,
376
+ y: centerY,
377
+ deltaX: 0,
378
+ deltaY: delta,
379
+ });
380
+ await page.waitForTimeout(16);
381
+ }
382
+ dispatched = target;
383
+ await page.waitForTimeout(250);
384
+ }
385
+ screenshots.push((await page.screenshot({ type: "jpeg", quality: 60 })).toString("base64"));
386
+ storyboard.push({ imageIndex: screenshots.length - 1, target: { type: "progress", value } });
387
+ }
388
+ }
389
+ finally {
390
+ await cdp.detach().catch(() => undefined);
391
+ }
392
+ return { screenshots, storyboard };
393
+ }
394
+ async function detectInspectionScrollMode(page, pageHeight, viewportHeight) {
395
+ if (pageHeight - viewportHeight > 48)
396
+ return "document";
397
+ const virtual = await page.evaluate(`(() => {
398
+ const body = getComputedStyle(document.body); const html = getComputedStyle(document.documentElement);
399
+ const locked = body.overflow === "hidden" || body.overflowY === "hidden" || html.overflow === "hidden" || html.overflowY === "hidden";
400
+ const fixedShell = Array.from(document.querySelectorAll("body *")).some((element) => { const style = getComputedStyle(element); const rect = element.getBoundingClientRect(); return (style.position === "fixed" || style.position === "sticky") && rect.height >= innerHeight * 0.85 && rect.width >= innerWidth * 0.85; });
401
+ return locked || fixedShell || document.querySelectorAll("canvas").length >= 3;
402
+ })()`);
403
+ return virtual ? "virtual" : "document";
404
+ }
405
+ function assertHttpUrl(value) {
406
+ const url = new URL(value);
407
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
408
+ throw new Error("Only HTTP(S) URLs are supported");
409
+ }
410
+ return url;
411
+ }
412
+ //# sourceMappingURL=inspectWebsite.js.map