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,119 @@
1
+ import dns from "node:dns/promises";
2
+ import net from "node:net";
3
+ const guardedPages = new WeakSet();
4
+ const resolutionCache = new Map();
5
+ export function targetNetworkPolicyFromEnv() {
6
+ return {
7
+ // Localhost capture is a core local-development feature. Disable this in SaaS.
8
+ allowLocalhost: process.env.ALLOW_LOCALHOST_TARGETS !== "0",
9
+ allowPrivateNetworks: process.env.ALLOW_PRIVATE_TARGETS === "1",
10
+ };
11
+ }
12
+ export async function installNetworkGuard(page, policy = targetNetworkPolicyFromEnv()) {
13
+ if (guardedPages.has(page))
14
+ return;
15
+ guardedPages.add(page);
16
+ await page.route("**/*", async (route) => {
17
+ try {
18
+ await assertSafeTargetUrl(route.request().url(), policy);
19
+ await route.continue();
20
+ }
21
+ catch (error) {
22
+ await abortBlockedRoute(route, error);
23
+ }
24
+ });
25
+ }
26
+ export async function assertSafeTargetUrl(value, policy = targetNetworkPolicyFromEnv()) {
27
+ const url = new URL(value);
28
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
29
+ throw new Error("Only HTTP(S) URLs are supported");
30
+ }
31
+ if (url.username || url.password) {
32
+ throw new Error("Target URLs cannot contain credentials");
33
+ }
34
+ const hostname = url.hostname.toLowerCase().replace(/\.$/, "");
35
+ if (isCloudMetadataHostname(hostname)) {
36
+ throw new Error("Cloud metadata targets are blocked");
37
+ }
38
+ const addresses = await resolveAddresses(hostname);
39
+ if (addresses.length === 0)
40
+ throw new Error(`Could not resolve target host: ${hostname}`);
41
+ for (const address of addresses)
42
+ assertAddressAllowed(address, hostname, policy);
43
+ return url;
44
+ }
45
+ export function assertAddressAllowed(address, hostname, policy) {
46
+ const normalized = normalizeMappedIpv4(address);
47
+ if (isCloudMetadataAddress(normalized)) {
48
+ throw new Error("Cloud metadata targets are blocked");
49
+ }
50
+ const category = addressCategory(normalized);
51
+ if (category === "public")
52
+ return;
53
+ if (category === "loopback" && policy.allowLocalhost && isLocalHostname(hostname, normalized)) {
54
+ return;
55
+ }
56
+ if (category === "private" && policy.allowPrivateNetworks)
57
+ return;
58
+ throw new Error(`Target resolves to a blocked ${category} address (${normalized})`);
59
+ }
60
+ export function addressCategory(address) {
61
+ const version = net.isIP(address);
62
+ if (version === 4) {
63
+ const [a, b] = address.split(".").map(Number);
64
+ if (a === 127)
65
+ return "loopback";
66
+ if (a === 0 ||
67
+ a === 10 ||
68
+ (a === 100 && b >= 64 && b <= 127) ||
69
+ (a === 169 && b === 254) ||
70
+ (a === 172 && b >= 16 && b <= 31) ||
71
+ (a === 192 && b === 168) ||
72
+ (a === 198 && (b === 18 || b === 19)) ||
73
+ a >= 224)
74
+ return "private";
75
+ return "public";
76
+ }
77
+ if (version === 6) {
78
+ const lower = address.toLowerCase();
79
+ if (lower === "::" || lower === "::1")
80
+ return "loopback";
81
+ if (/^(fc|fd)/.test(lower) || /^fe[89ab]/.test(lower) || lower.startsWith("ff")) {
82
+ return "private";
83
+ }
84
+ return "public";
85
+ }
86
+ throw new Error(`Invalid resolved IP address: ${address}`);
87
+ }
88
+ async function resolveAddresses(hostname) {
89
+ if (net.isIP(hostname))
90
+ return [hostname];
91
+ const cached = resolutionCache.get(hostname);
92
+ if (cached && cached.expiresAt > Date.now())
93
+ return cached.addresses;
94
+ const resolved = await dns.lookup(hostname, { all: true, verbatim: true });
95
+ const addresses = [...new Set(resolved.map((item) => item.address))];
96
+ resolutionCache.set(hostname, { expiresAt: Date.now() + 30_000, addresses });
97
+ return addresses;
98
+ }
99
+ function isLocalHostname(hostname, address) {
100
+ return (hostname === "localhost" ||
101
+ hostname.endsWith(".localhost") ||
102
+ hostname === address ||
103
+ hostname === `[${address}]`);
104
+ }
105
+ function isCloudMetadataHostname(hostname) {
106
+ return hostname === "metadata.google.internal" || hostname.endsWith(".metadata.google.internal");
107
+ }
108
+ function isCloudMetadataAddress(address) {
109
+ return address === "169.254.169.254" || address.toLowerCase() === "fd00:ec2::254";
110
+ }
111
+ function normalizeMappedIpv4(address) {
112
+ const match = address.toLowerCase().match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
113
+ return match?.[1] ?? address;
114
+ }
115
+ async function abortBlockedRoute(route, error) {
116
+ console.warn(`Blocked browser request to ${route.request().url()}: ${error instanceof Error ? error.message : "network policy violation"}`);
117
+ await route.abort("blockedbyclient");
118
+ }
119
+ //# sourceMappingURL=networkPolicy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"networkPolicy.js","sourceRoot":"","sources":["../../src/browser/networkPolicy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Page } from 'playwright';
2
+ export declare function primeLazyAssets(page: Page): Promise<void>;
3
+ //# sourceMappingURL=prime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prime.d.ts","sourceRoot":"","sources":["../../src/browser/prime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAEtC,wBAAsB,eAAe,CAAC,IAAI,EAAE,IAAI,iBAgB/C"}
@@ -0,0 +1,13 @@
1
+ export async function primeLazyAssets(page) {
2
+ await page.evaluate(async () => {
3
+ const lazyImages = document.querySelectorAll('img[loading="lazy"], img[data-src], img[data-lazy-src], img[data-original]');
4
+ for (const img of lazyImages) {
5
+ img.loading = 'eager';
6
+ const lazySrc = img.dataset.src ?? img.dataset.lazySrc ?? img.dataset.original;
7
+ if (lazySrc)
8
+ img.src = lazySrc;
9
+ }
10
+ await Promise.all(Array.from(document.images).map((img) => img.decode().catch(() => undefined)));
11
+ });
12
+ }
13
+ //# sourceMappingURL=prime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prime.js","sourceRoot":"","sources":["../../src/browser/prime.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAU;IAC9C,MAAM,IAAI,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE;QAC7B,MAAM,UAAU,GAAG,QAAQ,CAAC,gBAAgB,CAC1C,4EAA4E,CAC7E,CAAA;QAED,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;YAC7B,GAAG,CAAC,OAAO,GAAG,OAAO,CAAA;YACrB,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAA;YAC9E,IAAI,OAAO;gBAAE,GAAG,CAAC,GAAG,GAAG,OAAO,CAAA;QAChC,CAAC;QAED,MAAM,OAAO,CAAC,GAAG,CACf,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,CAC9E,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Page } from "playwright";
2
+ export declare function sanitizeDom(page: Page, removeOverlayElements: boolean): Promise<void>;
3
+ //# sourceMappingURL=sanitize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/browser/sanitize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAYvC,wBAAsB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,qBAAqB,EAAE,OAAO,iBA6B3E"}
@@ -0,0 +1,37 @@
1
+ const OVERLAY_SELECTORS = [
2
+ '[id*="cookie"]',
3
+ '[class*="cookie"]',
4
+ '[id*="consent"]',
5
+ '[class*="consent"]',
6
+ ".newsletter-popup",
7
+ '[class*="popup"]',
8
+ '[aria-modal="true"]',
9
+ ];
10
+ export async function sanitizeDom(page, removeOverlayElements) {
11
+ await page.evaluate(({ selectors, remove }) => {
12
+ const style = document.createElement("style");
13
+ style.innerHTML = `
14
+ ::-webkit-scrollbar { display: none !important; width: 0 !important; height: 0 !important; }
15
+ html, body {
16
+ scroll-behavior: auto !important;
17
+ scrollbar-width: none !important;
18
+ overflow: hidden !important;
19
+ margin: 0 !important;
20
+ padding: 0 !important;
21
+ width: 100% !important;
22
+ min-height: 100% !important;
23
+ }
24
+ `;
25
+ document.head.appendChild(style);
26
+ if (!remove)
27
+ return;
28
+ for (const selector of selectors) {
29
+ document.querySelectorAll(selector).forEach((el) => {
30
+ if (el === document.documentElement || el === document.body)
31
+ return;
32
+ el.remove();
33
+ });
34
+ }
35
+ }, { selectors: OVERLAY_SELECTORS, remove: removeOverlayElements });
36
+ }
37
+ //# sourceMappingURL=sanitize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../../src/browser/sanitize.ts"],"names":[],"mappings":"AAEA,MAAM,iBAAiB,GAAG;IACxB,gBAAgB;IAChB,mBAAmB;IACnB,iBAAiB;IACjB,oBAAoB;IACpB,mBAAmB;IACnB,kBAAkB;IAClB,qBAAqB;CACtB,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAU,EAAE,qBAA8B;IAC1E,MAAM,IAAI,CAAC,QAAQ,CACjB,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE;QACxB,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC9C,KAAK,CAAC,SAAS,GAAG;;;;;;;;;;;KAWnB,CAAC;QACA,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAEjC,IAAI,CAAC,MAAM;YAAE,OAAO;QAEpB,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YACjC,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;gBACjD,IAAI,EAAE,KAAK,QAAQ,CAAC,eAAe,IAAI,EAAE,KAAK,QAAQ,CAAC,IAAI;oBAAE,OAAO;gBACpE,EAAE,CAAC,MAAM,EAAE,CAAC;YACd,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC,EACD,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAChE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,29 @@
1
+ import type { Page } from "playwright";
2
+ import type { FrameRecorder } from "../capture/frameRecorder.js";
3
+ import type { AnimationConfig, PauseTrigger, ResolvedMotionPlan, ScrollMode } from "../types.js";
4
+ import type { BezierControlPoints } from "./curves.js";
5
+ export interface RunScrollOptions {
6
+ pixelsPerFrame: number;
7
+ pauseTriggers: PauseTrigger[];
8
+ bezier: BezierControlPoints;
9
+ scrollMode?: ScrollMode;
10
+ animationConfig?: AnimationConfig;
11
+ viewportWidth: number;
12
+ viewportHeight: number;
13
+ fastMode?: boolean;
14
+ frameRecorder?: FrameRecorder;
15
+ signal?: AbortSignal;
16
+ onProgress?: (completedFrames: number, totalFrames: number) => void | Promise<void>;
17
+ }
18
+ export interface RunScrollResult {
19
+ scrollStrategy: "document" | "virtual";
20
+ maxScroll: number;
21
+ frames?: Array<{
22
+ file: string;
23
+ y?: number;
24
+ progress?: number;
25
+ }>;
26
+ motionPlan: ResolvedMotionPlan;
27
+ }
28
+ export declare function runScroll(page: Page, options: RunScrollOptions): Promise<RunScrollResult>;
29
+ //# sourceMappingURL=scroll.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scroll.d.ts","sourceRoot":"","sources":["../../src/browser/scroll.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEjE,OAAO,KAAK,EACV,eAAe,EAGf,YAAY,EAEZ,kBAAkB,EAElB,UAAU,EACX,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAmBvD,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,MAAM,EAAE,mBAAmB,CAAC;IAC5B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrF;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,UAAU,GAAG,SAAS,CAAC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChE,UAAU,EAAE,kBAAkB,CAAC;CAChC;AAED,wBAAsB,SAAS,CAC7B,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,eAAe,CAAC,CAQ1B"}
@@ -0,0 +1,420 @@
1
+ import { resolveVirtualScrollSettings } from "../config/virtualScroll.js";
2
+ import { DEFAULT_DIRECTED_START_HOLD_MS } from "../types.js";
3
+ import { resolveScrollCurve } from "./curves.js";
4
+ import { detectScrollMode } from "./detectScrollMode.js";
5
+ import { buildMotionTimeline } from "./motion.js";
6
+ import { runVirtualTimeline } from "./virtualScroll.js";
7
+ import { alignedDocumentPosition, collectSemanticAnchors, detectSafeViewport, nearestSemanticAnchor, resolvePauseFraming, } from "./composition.js";
8
+ import { normalizeResolvedBeats } from "./directionGuardrails.js";
9
+ import { ComponentInteractionAnimator } from "./componentInteraction.js";
10
+ const DEFAULT_BEAT_CURVE = { preset: "ease-in-out-cubic" };
11
+ export async function runScroll(page, options) {
12
+ const mode = await detectScrollMode(page, options.scrollMode ?? "auto");
13
+ await settleCaptureAtTop(page);
14
+ if (mode === "virtual") {
15
+ return runDirectedVirtualScroll(page, options);
16
+ }
17
+ return runDirectedDocumentScroll(page, options);
18
+ }
19
+ async function runDirectedDocumentScroll(page, options) {
20
+ const maxScroll = await page.evaluate(() => Math.max(0, document.documentElement.scrollHeight - window.innerHeight));
21
+ const direction = options.animationConfig?.direction;
22
+ const startHoldMs = direction
23
+ ? direction.startHoldMs ?? DEFAULT_DIRECTED_START_HOLD_MS
24
+ : options.animationConfig?.heroHoldMs ?? 0;
25
+ const safeViewport = await detectSafeViewport(page, options.viewportWidth, options.viewportHeight);
26
+ const anchors = direction
27
+ ? await collectSemanticAnchors(page, safeViewport, options.viewportHeight, maxScroll)
28
+ : [];
29
+ const resolved = direction
30
+ ? await resolveDirectedDocumentBeats(page, direction.beats, maxScroll, options.viewportHeight, safeViewport, anchors, startHoldMs)
31
+ : await resolveLegacyDocumentBeats(page, options, maxScroll, safeViewport);
32
+ const fps = options.frameRecorder?.getFps() ?? 30;
33
+ const samples = buildMotionTimeline({
34
+ fps,
35
+ startHoldMs,
36
+ beats: resolved.timeline,
37
+ });
38
+ const frames = [];
39
+ let lastY = -1;
40
+ const interactionAnimator = new ComponentInteractionAnimator(page);
41
+ for (const [index, sample] of samples.entries()) {
42
+ options.signal?.throwIfAborted();
43
+ const y = Math.max(0, Math.min(maxScroll, Math.round(sample.position)));
44
+ if (y !== lastY) {
45
+ await settleScrollPaint(page, y);
46
+ lastY = y;
47
+ }
48
+ const activeBeat = sample.beatIndex >= 0 ? resolved.beats[sample.beatIndex] : undefined;
49
+ if (sample.phase === "hold"
50
+ && activeBeat?.interaction
51
+ && activeBeat.target.type === "selector") {
52
+ await interactionAnimator.render({
53
+ beatIndex: sample.beatIndex,
54
+ selector: activeBeat.target.selector,
55
+ interaction: activeBeat.interaction,
56
+ progress: sample.phaseProgress ?? 0,
57
+ });
58
+ }
59
+ else {
60
+ await interactionAnimator.reset();
61
+ }
62
+ if (options.frameRecorder) {
63
+ const frameNumber = options.frameRecorder.getFrameCount();
64
+ await options.frameRecorder.writeFrame(page);
65
+ frames.push({
66
+ file: `frame-${String(frameNumber).padStart(6, "0")}.jpg`,
67
+ y,
68
+ });
69
+ }
70
+ else {
71
+ await page.waitForTimeout(1000 / fps);
72
+ }
73
+ await options.onProgress?.(index + 1, samples.length);
74
+ }
75
+ const durationMs = Math.round((samples.length / fps) * 1000);
76
+ await interactionAnimator.reset();
77
+ return {
78
+ scrollStrategy: "document",
79
+ maxScroll,
80
+ frames: options.frameRecorder ? frames : undefined,
81
+ motionPlan: {
82
+ mode: "document",
83
+ startHoldMs,
84
+ durationMs,
85
+ beats: resolved.beats,
86
+ adjustments: resolved.adjustments,
87
+ },
88
+ };
89
+ }
90
+ async function runDirectedVirtualScroll(page, options) {
91
+ const virtual = resolveVirtualScrollSettings(options.animationConfig ?? {}, options.viewportHeight, options.pixelsPerFrame, options.fastMode ?? false);
92
+ const direction = options.animationConfig?.direction;
93
+ if (!direction && (options.pauseTriggers?.length ?? 0) > 0) {
94
+ throw new Error("Selector pause triggers are not supported on virtual-scroll pages. Inspect the page and use direction beats with progress targets.");
95
+ }
96
+ const startHoldMs = direction?.startHoldMs
97
+ ?? (direction ? DEFAULT_DIRECTED_START_HOLD_MS : options.animationConfig?.heroHoldMs ?? 0);
98
+ const resolved = direction
99
+ ? resolveDirectedVirtualBeats(direction.beats, startHoldMs, options.viewportHeight, virtual.wheelBudget)
100
+ : resolveLegacyVirtualBeats(virtual.durationMs, options.bezier);
101
+ const fps = options.frameRecorder?.getFps() ?? 30;
102
+ const samples = buildMotionTimeline({
103
+ fps,
104
+ startHoldMs,
105
+ beats: resolved.timeline,
106
+ });
107
+ const frames = await runVirtualTimeline(page, {
108
+ viewportWidth: options.viewportWidth,
109
+ viewportHeight: options.viewportHeight,
110
+ wheelBudget: virtual.wheelBudget,
111
+ samples,
112
+ frameRecorder: options.frameRecorder,
113
+ signal: options.signal,
114
+ onProgress: options.onProgress,
115
+ });
116
+ const durationMs = Math.round((samples.length / fps) * 1000);
117
+ return {
118
+ scrollStrategy: "virtual",
119
+ maxScroll: 0,
120
+ frames: options.frameRecorder ? frames : undefined,
121
+ motionPlan: {
122
+ mode: "virtual",
123
+ startHoldMs,
124
+ durationMs,
125
+ beats: resolved.beats,
126
+ adjustments: resolved.adjustments,
127
+ },
128
+ };
129
+ }
130
+ async function resolveDirectedDocumentBeats(page, beats, maxScroll, viewportHeight, safeViewport, anchors, startHoldMs) {
131
+ if (beats.length === 0)
132
+ throw new Error("direction.beats must contain at least one beat");
133
+ const resolved = [];
134
+ const adjustments = [];
135
+ for (const [beatIndex, beat] of beats.entries()) {
136
+ validateBeat(beat);
137
+ let target = beat.target;
138
+ let position = await resolveDocumentTarget(page, target, maxScroll, viewportHeight, safeViewport);
139
+ if (target.type === "progress" && (beat.holdMs ?? 0) > 0) {
140
+ const nearest = nearestSemanticAnchor(anchors, position, viewportHeight);
141
+ if (nearest) {
142
+ const requested = target;
143
+ target = { type: "selector", selector: nearest.selector, align: nearest.recommendedAlign };
144
+ position = nearest.position;
145
+ adjustments.push({
146
+ beatIndex,
147
+ code: "promoted-progress-target",
148
+ message: `Promoted a held progress target to the nearby “${nearest.label}” section`,
149
+ requested,
150
+ resolved: target,
151
+ });
152
+ }
153
+ }
154
+ const curve = beat.curve ?? DEFAULT_BEAT_CURVE;
155
+ resolved.push({
156
+ target,
157
+ position,
158
+ transitionMs: beat.transitionMs,
159
+ holdMs: beat.holdMs ?? 0,
160
+ curve,
161
+ interaction: beat.interaction,
162
+ });
163
+ }
164
+ await applyHeldBeatFraming(page, resolved, adjustments, safeViewport, viewportHeight, maxScroll);
165
+ const normalized = normalizeResolvedBeats({
166
+ beats: resolved,
167
+ startHoldMs,
168
+ viewportHeight,
169
+ adjustments,
170
+ });
171
+ normalized.beats.forEach((beat, index) => {
172
+ if (index > 0 && beat.position + 1 < normalized.beats[index - 1].position) {
173
+ throw new Error(`Direction target ${describeTarget(beat.target)} resolves behind the previous beat`);
174
+ }
175
+ });
176
+ return {
177
+ ...normalized,
178
+ timeline: normalized.beats.map((beat) => ({
179
+ position: beat.position,
180
+ transitionMs: beat.transitionMs,
181
+ holdMs: beat.holdMs,
182
+ bezier: resolveScrollCurve(beat.curve),
183
+ })),
184
+ };
185
+ }
186
+ async function applyHeldBeatFraming(page, beats, adjustments, safeViewport, viewportHeight, maxScroll) {
187
+ for (const [beatIndex, beat] of beats.entries()) {
188
+ if (beat.holdMs <= 0 || beat.target.type !== "selector")
189
+ continue;
190
+ const metrics = await page.evaluate((selector) => {
191
+ let element = null;
192
+ try {
193
+ element = document.querySelector(selector);
194
+ }
195
+ catch {
196
+ return null;
197
+ }
198
+ if (!element)
199
+ return null;
200
+ const rect = element.getBoundingClientRect();
201
+ if (rect.width <= 0 || rect.height <= 0)
202
+ return null;
203
+ return {
204
+ y: rect.top + window.scrollY,
205
+ height: rect.height,
206
+ };
207
+ }, beat.target.selector);
208
+ if (!metrics)
209
+ continue;
210
+ const align = beat.target.align ?? "center";
211
+ const framing = resolvePauseFraming({
212
+ y: metrics.y,
213
+ height: metrics.height,
214
+ position: beat.position,
215
+ align,
216
+ safeViewport,
217
+ viewportHeight,
218
+ maxScroll,
219
+ });
220
+ const requestedPosition = beat.position;
221
+ beat.position = framing.targetY;
222
+ beat.framing = {
223
+ selector: beat.target.selector,
224
+ align: framing.align,
225
+ targetY: framing.targetY,
226
+ safeTopPx: framing.safeTopPx,
227
+ safeBottomPx: framing.safeBottomPx,
228
+ verified: framing.verified,
229
+ };
230
+ if (Math.abs(requestedPosition - beat.position) > 1) {
231
+ adjustments.push({
232
+ beatIndex,
233
+ code: "corrected-pause-framing",
234
+ message: "Adjusted a held target to keep its focal element inside the safe viewport",
235
+ requested: requestedPosition,
236
+ resolved: beat.position,
237
+ });
238
+ }
239
+ }
240
+ }
241
+ function resolveDirectedVirtualBeats(beats, startHoldMs, viewportHeight, wheelBudget) {
242
+ if (beats.length === 0)
243
+ throw new Error("direction.beats must contain at least one beat");
244
+ let previous = 0;
245
+ const resolved = [];
246
+ for (const beat of beats) {
247
+ validateBeat(beat);
248
+ if (beat.interaction) {
249
+ throw new Error("Component interactions are only supported on document-scroll pages");
250
+ }
251
+ if (beat.target.type === "selector") {
252
+ if (beat.target.fallbackProgress === undefined) {
253
+ throw new Error(`Selector target ${beat.target.selector} cannot direct a virtual-scroll page; use a progress target from inspect_website`);
254
+ }
255
+ }
256
+ const position = beat.target.type === "page-end"
257
+ ? 1
258
+ : beat.target.type === "selector"
259
+ ? beat.target.fallbackProgress
260
+ : beat.target.value;
261
+ if (position < 0 || position > 1)
262
+ throw new Error("Virtual progress targets must be between 0 and 1");
263
+ if (position + 1e-6 < previous)
264
+ throw new Error("Virtual progress beats must be ordered from 0 to 1");
265
+ previous = position;
266
+ const curve = beat.curve ?? DEFAULT_BEAT_CURVE;
267
+ resolved.push({ target: beat.target, position, transitionMs: beat.transitionMs, holdMs: beat.holdMs ?? 0, curve });
268
+ }
269
+ const normalized = normalizeResolvedBeats({
270
+ beats: resolved,
271
+ startHoldMs,
272
+ viewportHeight,
273
+ positionScale: wheelBudget,
274
+ });
275
+ return {
276
+ ...normalized,
277
+ timeline: normalized.beats.map((beat) => ({
278
+ position: beat.position,
279
+ transitionMs: beat.transitionMs,
280
+ holdMs: beat.holdMs,
281
+ bezier: resolveScrollCurve(beat.curve),
282
+ })),
283
+ };
284
+ }
285
+ async function resolveLegacyDocumentBeats(page, options, maxScroll, safeViewport) {
286
+ const pauses = normalizePauseTriggers(options.pauseTriggers ?? []);
287
+ const pauseTargets = [];
288
+ for (const pause of pauses) {
289
+ const target = { type: "selector", selector: pause.selector, align: "center" };
290
+ const position = await resolveDocumentTarget(page, target, maxScroll, options.viewportHeight, safeViewport);
291
+ pauseTargets.push({ target, position, holdMs: pause.durationMs });
292
+ }
293
+ pauseTargets.sort((a, b) => a.position - b.position);
294
+ const targets = [
295
+ ...pauseTargets,
296
+ { target: { type: "page-end" }, position: maxScroll, holdMs: 0 },
297
+ ].filter((target, index, all) => index === 0 || target.position > all[index - 1].position + 1 || target.holdMs > 0);
298
+ const defaultMovementMs = Math.max(1000, (Math.ceil(maxScroll / Math.max(1, options.pixelsPerFrame)) / (options.frameRecorder?.getFps() ?? 30)) * 1000);
299
+ let movementMs = options.animationConfig?.durationMs ?? defaultMovementMs;
300
+ const scrollSync = options.animationConfig?.scrollSync;
301
+ if (scrollSync && scrollSync.refMaxScroll > 0 && scrollSync.refDurationMs > 0) {
302
+ const refSpeed = scrollSync.refMaxScroll / scrollSync.refDurationMs;
303
+ if (refSpeed > 0) {
304
+ const calculatedMs = Math.round(maxScroll / refSpeed);
305
+ const maxAllowedMs = Math.min(90000, (options.animationConfig?.durationMs ?? 18000) * 5);
306
+ movementMs = Math.max(1000, Math.min(maxAllowedMs, calculatedMs));
307
+ }
308
+ }
309
+ const curve = options.animationConfig?.scrollCurve ?? { preset: "linear" };
310
+ const resolved = [];
311
+ const timeline = [];
312
+ let previous = 0;
313
+ for (const target of targets) {
314
+ const distance = Math.max(0, target.position - previous);
315
+ const transitionMs = maxScroll > 0 ? Math.max(1, Math.round(movementMs * distance / maxScroll)) : 1;
316
+ resolved.push({ target: target.target, position: target.position, transitionMs, holdMs: target.holdMs, curve });
317
+ timeline.push({ position: target.position, transitionMs, holdMs: target.holdMs, bezier: options.bezier });
318
+ previous = target.position;
319
+ }
320
+ return { beats: resolved, timeline, adjustments: [] };
321
+ }
322
+ function resolveLegacyVirtualBeats(durationMs, bezier) {
323
+ const target = { type: "page-end" };
324
+ return {
325
+ beats: [{ target, position: 1, transitionMs: durationMs, holdMs: 0, curve: { preset: "linear" } }],
326
+ timeline: [{ position: 1, transitionMs: durationMs, holdMs: 0, bezier }],
327
+ adjustments: [],
328
+ };
329
+ }
330
+ async function resolveDocumentTarget(page, target, maxScroll, viewportHeight, safeViewport) {
331
+ if (target.type === "page-end")
332
+ return maxScroll;
333
+ if (target.type === "progress") {
334
+ if (target.value < 0 || target.value > 1)
335
+ throw new Error("Progress targets must be between 0 and 1");
336
+ return maxScroll * target.value;
337
+ }
338
+ const metrics = await page.evaluate((selector) => {
339
+ let element = null;
340
+ try {
341
+ element = document.querySelector(selector);
342
+ }
343
+ catch {
344
+ return null;
345
+ }
346
+ if (!element)
347
+ return null;
348
+ const rect = element.getBoundingClientRect();
349
+ if (rect.width <= 0 || rect.height <= 0)
350
+ return null;
351
+ return { top: rect.top + window.scrollY, height: rect.height };
352
+ }, target.selector);
353
+ if (!metrics) {
354
+ if (target.fallbackProgress !== undefined)
355
+ return maxScroll * target.fallbackProgress;
356
+ throw new Error(`Direction selector was not found or visible: ${target.selector}`);
357
+ }
358
+ return alignedDocumentPosition({
359
+ y: metrics.top,
360
+ height: metrics.height,
361
+ align: target.align ?? "center",
362
+ offsetPx: target.offsetPx ?? 0,
363
+ safeViewport,
364
+ viewportHeight,
365
+ maxScroll,
366
+ });
367
+ }
368
+ function validateBeat(beat) {
369
+ if (!Number.isFinite(beat.transitionMs) || beat.transitionMs < 250 || beat.transitionMs > 60_000) {
370
+ throw new Error("Each direction transitionMs must be between 250 and 60000");
371
+ }
372
+ const holdMs = beat.holdMs ?? 0;
373
+ if (!Number.isFinite(holdMs) || holdMs < 0 || holdMs > 15_000) {
374
+ throw new Error("Each direction holdMs must be between 0 and 15000");
375
+ }
376
+ if (beat.interaction) {
377
+ if (beat.target.type !== "selector") {
378
+ throw new Error("Component interactions require a selector target from inspect_website");
379
+ }
380
+ if (holdMs < 900) {
381
+ throw new Error("Interactive beats require holdMs of at least 900ms");
382
+ }
383
+ const zoomScale = beat.interaction.zoomScale ?? 1.25;
384
+ if (!Number.isFinite(zoomScale) || zoomScale < 1 || zoomScale > 1.8) {
385
+ throw new Error("Interaction zoomScale must be between 1 and 1.8");
386
+ }
387
+ }
388
+ if (beat.target.type === "selector"
389
+ && beat.target.fallbackProgress !== undefined
390
+ && (!Number.isFinite(beat.target.fallbackProgress) || beat.target.fallbackProgress < 0 || beat.target.fallbackProgress > 1)) {
391
+ throw new Error("Selector fallbackProgress must be between 0 and 1");
392
+ }
393
+ resolveScrollCurve(beat.curve ?? DEFAULT_BEAT_CURVE);
394
+ }
395
+ function normalizePauseTriggers(triggers) {
396
+ return triggers
397
+ .map((trigger) => ({ selector: trigger.selector.trim(), durationMs: Math.max(0, Math.round(trigger.durationMs)) }))
398
+ .filter((trigger) => trigger.selector.length > 0 && trigger.durationMs >= 100);
399
+ }
400
+ function describeTarget(target) {
401
+ if (target.type === "selector")
402
+ return target.selector;
403
+ if (target.type === "progress")
404
+ return `progress ${target.value}`;
405
+ return "page end";
406
+ }
407
+ async function settleScrollPaint(page, scrollY) {
408
+ await page.evaluate(async (y) => {
409
+ window.scrollTo({ top: y, left: 0, behavior: "instant" });
410
+ await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
411
+ }, scrollY);
412
+ }
413
+ async function settleCaptureAtTop(page) {
414
+ await page.evaluate(async () => {
415
+ window.scrollTo({ top: 0, left: 0, behavior: "instant" });
416
+ await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
417
+ });
418
+ await page.waitForTimeout(250);
419
+ }
420
+ //# sourceMappingURL=scroll.js.map