sootsim 0.1.83 → 0.1.84

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 (207) hide show
  1. package/README.md +0 -1
  2. package/detox/colors.ts +54 -0
  3. package/detox/config-loader.ts +135 -0
  4. package/detox/expectations.ts +477 -0
  5. package/detox/gestures.ts +442 -0
  6. package/detox/index.ts +1436 -0
  7. package/detox/jest-environment.ts +86 -0
  8. package/detox/jest-preset.cjs +50 -0
  9. package/detox/matchers.ts +29 -0
  10. package/detox/navigation.ts +43 -0
  11. package/detox/run-test.ts +113 -0
  12. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  13. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  14. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  15. package/detox/screenshots/theme-blue-toggle.png +0 -0
  16. package/detox/screenshots/theme-blue.png +0 -0
  17. package/detox/screenshots/theme-red-toggle.png +0 -0
  18. package/detox/screenshots/theme-red.png +0 -0
  19. package/dist-cli/bin.js +3 -3
  20. package/dist-cli/chunks/{agent-MQ7GLVIB.js → agent-2CWD6W6P.js} +2 -2
  21. package/dist-cli/chunks/{agent-wrapper-7KAFDQCN.js → agent-wrapper-5W3LOX6S.js} +2 -2
  22. package/dist-cli/chunks/{assert-TV46GUNU.js → assert-ZOMAMKRT.js} +2 -2
  23. package/dist-cli/chunks/auto-bootstrap-NYYSMTIM.js +2 -0
  24. package/dist-cli/chunks/beta-4K2SQACK.js +2 -0
  25. package/dist-cli/chunks/chunk-3HXQ7MJK.js +79 -0
  26. package/dist-cli/chunks/{chunk-4LS5MZAI.js → chunk-4K7BH2D4.js} +3 -3
  27. package/dist-cli/chunks/{chunk-FJYT7XL2.js → chunk-4OPRODFA.js} +2 -2
  28. package/dist-cli/chunks/{chunk-DP7O5MHK.js → chunk-4OWVPRZV.js} +2 -2
  29. package/dist-cli/chunks/{chunk-PM5NVKLP.js → chunk-5XCXOLG2.js} +2 -2
  30. package/dist-cli/chunks/chunk-67ZZ2CM5.js +1 -0
  31. package/dist-cli/chunks/{chunk-WN7M3QON.js → chunk-73UZXB4B.js} +2 -2
  32. package/dist-cli/chunks/{chunk-5DJXZIFZ.js → chunk-7NWNTUJF.js} +1 -1
  33. package/dist-cli/chunks/{chunk-Y2VJBRSP.js → chunk-7YHDJLO2.js} +1 -1
  34. package/dist-cli/chunks/{chunk-6NN2D4EJ.js → chunk-AJVTY6KY.js} +1 -1
  35. package/dist-cli/chunks/chunk-AWSQUOAS.js +67 -0
  36. package/dist-cli/chunks/{chunk-CJY3AVI7.js → chunk-BCBNVJVG.js} +1 -1
  37. package/dist-cli/chunks/{chunk-OYMFNU3M.js → chunk-BKBL6K2G.js} +1 -1
  38. package/dist-cli/chunks/{chunk-IBNRRAES.js → chunk-C3DPQZ4J.js} +2 -2
  39. package/dist-cli/chunks/chunk-D3ZSBIIY.js +2 -0
  40. package/dist-cli/chunks/{chunk-2AWQ7OB2.js → chunk-D4HUVLZR.js} +1 -1
  41. package/dist-cli/chunks/{chunk-F3HP444U.js → chunk-DUUSJDES.js} +1 -1
  42. package/dist-cli/chunks/{chunk-277XAALA.js → chunk-ELJLF4SG.js} +3 -3
  43. package/dist-cli/chunks/{chunk-RH4F2TF7.js → chunk-EQ7TFQ2F.js} +1 -1
  44. package/dist-cli/chunks/{chunk-HNWEELAE.js → chunk-EQCKGC4B.js} +1 -1
  45. package/dist-cli/chunks/chunk-FUCGLWNN.js +1 -0
  46. package/dist-cli/chunks/{chunk-FRM355UL.js → chunk-HYPJW65U.js} +2 -2
  47. package/dist-cli/chunks/chunk-IILJQCZA.js +2 -0
  48. package/dist-cli/chunks/{chunk-Y4BUVURT.js → chunk-KU6MSPAH.js} +2 -2
  49. package/dist-cli/chunks/{chunk-DM6WT7QM.js → chunk-OOOR7NT2.js} +1 -1
  50. package/dist-cli/chunks/{chunk-HAWOAQAG.js → chunk-P7WDNKOS.js} +3 -3
  51. package/dist-cli/chunks/{chunk-6TNANCQC.js → chunk-PPKKA5VW.js} +2 -2
  52. package/dist-cli/chunks/{chunk-JQ7ZXOXJ.js → chunk-PS2G44GT.js} +2 -2
  53. package/dist-cli/chunks/{chunk-ECJBV65H.js → chunk-QMSJR5R2.js} +2 -2
  54. package/dist-cli/chunks/{chunk-J2GYISVJ.js → chunk-RF4R2U46.js} +2 -2
  55. package/dist-cli/chunks/{chunk-VMXWC2JO.js → chunk-RIXUH3NK.js} +2 -2
  56. package/dist-cli/chunks/{chunk-2PY3UZVO.js → chunk-SFGUPL2X.js} +2 -2
  57. package/dist-cli/chunks/{chunk-572VSFNP.js → chunk-SQX5CAYG.js} +1 -1
  58. package/dist-cli/chunks/{chunk-NXATOWWF.js → chunk-SQZAC7C4.js} +1 -1
  59. package/dist-cli/chunks/{chunk-WTKTOL3C.js → chunk-SV7FOGJ3.js} +2 -2
  60. package/dist-cli/chunks/{chunk-JHJNODXN.js → chunk-TK3OJSEO.js} +2 -2
  61. package/dist-cli/chunks/{chunk-KASUZ5XV.js → chunk-TL7SIZ7S.js} +1 -1
  62. package/dist-cli/chunks/{chunk-6XZOEBTZ.js → chunk-V2GQ4WXJ.js} +2 -2
  63. package/dist-cli/chunks/{chunk-IP3QJLRH.js → chunk-VH7F45CN.js} +1 -1
  64. package/dist-cli/chunks/chunk-WNVNU2OW.js +4 -0
  65. package/dist-cli/chunks/{chunk-YUELRHGB.js → chunk-XQ2OBHBE.js} +2 -2
  66. package/dist-cli/chunks/{chunk-CYV6Y6YV.js → chunk-YCIA4BHJ.js} +2 -2
  67. package/dist-cli/chunks/chunk-ZSMMJMPA.js +1 -0
  68. package/dist-cli/chunks/cli-version-QB4VH24H.js +2 -0
  69. package/dist-cli/chunks/{compat-QLLWBTS3.js → compat-FWSEEGEH.js} +3 -3
  70. package/dist-cli/chunks/{config-2DSLDCXV.js → config-CYI2WAGP.js} +2 -2
  71. package/dist-cli/chunks/control-UXY7YQVX.js +2 -0
  72. package/dist-cli/chunks/{cpu-profile-GEIKHCPC.js → cpu-profile-IKAE3KTY.js} +2 -2
  73. package/dist-cli/chunks/{daemon-4EBUFN4D.js → daemon-ZUMF53YB.js} +2 -2
  74. package/dist-cli/chunks/{debug-WGD6XWOF.js → debug-P6KULKKS.js} +3 -3
  75. package/dist-cli/chunks/{detox-LNKGRZU6.js → detox-SPWAZCYG.js} +2 -2
  76. package/dist-cli/chunks/{device-AYKXKVIQ.js → device-JWEPK6I2.js} +2 -2
  77. package/dist-cli/chunks/{diagnose-TMXSDOOC.js → diagnose-IZODTXV2.js} +2 -2
  78. package/dist-cli/chunks/drivers-MK6WJKBC.js +2 -0
  79. package/dist-cli/chunks/{electron-QFPF7TBY.js → electron-R5GP6RVB.js} +3 -3
  80. package/dist-cli/chunks/flow-6O4GEOPJ.js +2 -0
  81. package/dist-cli/chunks/{hints-MXKRR4TG.js → hints-DYDNYX7N.js} +2 -2
  82. package/dist-cli/chunks/{home-paths-REMWQDAO.js → home-paths-GLMX5OKL.js} +2 -2
  83. package/dist-cli/chunks/{inspect-XGSQNFV7.js → inspect-FJOPCTY2.js} +3 -3
  84. package/dist-cli/chunks/install-A3TUGGHN.js +2 -0
  85. package/dist-cli/chunks/{install-desktop-NQG3RZSA.js → install-desktop-YPJZMZM5.js} +3 -3
  86. package/dist-cli/chunks/{keys-5QZWXL3F.js → keys-GSYPHWNY.js} +2 -2
  87. package/dist-cli/chunks/{launch-SBXOZWKO.js → launch-4G2PKW5X.js} +3 -3
  88. package/dist-cli/chunks/{login-EACQXE24.js → login-KJQGHA64.js} +4 -4
  89. package/dist-cli/chunks/{logout-IBQLMUML.js → logout-XM2SYH5C.js} +2 -2
  90. package/dist-cli/chunks/{maestro-LFYXUX7O.js → maestro-EOWGI7DG.js} +2 -2
  91. package/dist-cli/chunks/{preview-U4SBOEGQ.js → preview-F73TKK37.js} +2 -2
  92. package/dist-cli/chunks/{profile-GWS5ECMY.js → profile-22FDKBUO.js} +2 -2
  93. package/dist-cli/chunks/{react-QDHLMVYL.js → react-5L6VPFUP.js} +2 -2
  94. package/dist-cli/chunks/{record-BUEUWPDI.js → record-JZXCQ4IN.js} +2 -2
  95. package/dist-cli/chunks/runtime-EEBX7CFV.js +2 -0
  96. package/dist-cli/chunks/{runtime-delivery-G7L6RVZ7.js → runtime-delivery-LXUM3R4A.js} +2 -2
  97. package/dist-cli/chunks/{screenshot-T2HBA3VI.js → screenshot-HDRRG33Q.js} +2 -2
  98. package/dist-cli/chunks/{screenshot-mode-EG5HMIH3.js → screenshot-mode-WY63LZIX.js} +2 -2
  99. package/dist-cli/chunks/{screenshots-S52AFHTV.js → screenshots-MPV2ENL5.js} +2 -2
  100. package/dist-cli/chunks/{server-MFFVYUGG.js → server-5LBMCJ3G.js} +2 -2
  101. package/dist-cli/chunks/setup-repo-SZSYNKNI.js +2 -0
  102. package/dist-cli/chunks/{skills-HQGWBS2O.js → skills-BQ73YOBF.js} +2 -2
  103. package/dist-cli/chunks/{start-E3DRYY7W.js → start-2WU4W6ZU.js} +4 -4
  104. package/dist-cli/chunks/store-RE45SUBF.js +2 -0
  105. package/dist-cli/chunks/telemetry-DG6GJLCP.js +2 -0
  106. package/dist-cli/chunks/{test-ZY3EF62K.js → test-OVO4CQTG.js} +3 -3
  107. package/dist-cli/chunks/{three-mode-WSPKQCJ5.js → three-mode-BKM3KFM7.js} +2 -2
  108. package/dist-cli/chunks/{timeline-3XAB5EWZ.js → timeline-MDXGEDQL.js} +2 -2
  109. package/dist-cli/chunks/{upgrade-WNENPFM5.js → upgrade-JGQABWVF.js} +2 -2
  110. package/dist-cli/chunks/upload-UJNUA4ZV.js +2 -0
  111. package/dist-cli/chunks/{web-D2AOZY44.js → web-WYFAYQ72.js} +2 -2
  112. package/dist-cli/chunks/{what-happened-F43KNSG6.js → what-happened-PZW2KW6A.js} +2 -2
  113. package/dist-cli/chunks/{whoami-T22VBR7C.js → whoami-7ATWJQS6.js} +2 -2
  114. package/dist-lib/agent-daemon-client.cjs +1 -1
  115. package/dist-lib/agent-events.cjs +1 -1
  116. package/dist-lib/agent-sessions.cjs +1 -1
  117. package/dist-lib/attached-projects.cjs +1 -1
  118. package/dist-lib/auth/shared-session.cjs +1 -1
  119. package/dist-lib/backend-origin.cjs +1 -1
  120. package/dist-lib/beta.cjs +44 -0
  121. package/dist-lib/bridge-constants.cjs +1 -1
  122. package/dist-lib/cli-constants.cjs +1 -1
  123. package/dist-lib/config.cjs +1 -1
  124. package/dist-lib/detox/index.cjs +1770 -0
  125. package/dist-lib/detox/jest-preset.cjs +50 -0
  126. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  127. package/dist-lib/home-paths.cjs +1 -1
  128. package/dist-lib/host/bridge-host.cjs +1 -1
  129. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  130. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  131. package/dist-lib/index.cjs +1 -1
  132. package/dist-lib/metro.cjs +1 -1
  133. package/dist-lib/profiles.cjs +1 -1
  134. package/dist-lib/render-mode.cjs +1 -1
  135. package/dist-lib/scripts/demo-app-registry.cjs +809 -0
  136. package/dist-lib/scripts/dev-server-scanner.cjs +1269 -0
  137. package/dist-lib/skills.cjs +8322 -0
  138. package/dist-lib/vite-base.cjs +3 -3
  139. package/dist-lib/vite.cjs +1 -1
  140. package/package.json +39 -10
  141. package/scripts/demo-app-registry.ts +989 -0
  142. package/scripts/dev-server-scanner.ts +674 -0
  143. package/src/agent-daemon-client.ts +390 -0
  144. package/src/agent-events.ts +71 -0
  145. package/src/agent-prompt.ts +71 -0
  146. package/src/agent-sessions.ts +572 -0
  147. package/src/attached-projects.ts +536 -0
  148. package/src/auth/shared-session.ts +199 -0
  149. package/src/backend-origin.ts +49 -0
  150. package/src/beta.ts +21 -0
  151. package/src/bridge-constants.ts +10 -0
  152. package/src/cli-constants.ts +1 -0
  153. package/src/cli-version.ts +30 -0
  154. package/src/codex-client.ts +215 -0
  155. package/src/config.ts +110 -0
  156. package/src/dev-bundle-resolution.ts +180 -0
  157. package/src/home-paths.ts +382 -0
  158. package/src/host/agent-host.ts +576 -0
  159. package/src/host/bridge-host.ts +2293 -0
  160. package/src/host/fetch-proxy-handler.ts +288 -0
  161. package/src/host/fetch-proxy-overrides.ts +39 -0
  162. package/src/host/open-url.ts +234 -0
  163. package/src/index.ts +9 -0
  164. package/src/metro-plugin.ts +207 -0
  165. package/src/native-dev-bundle-url.ts +62 -0
  166. package/src/native-seam-manifest.ts +313 -0
  167. package/src/profiles.ts +179 -0
  168. package/src/render-mode.ts +27 -0
  169. package/src/runtime-delivery.ts +334 -0
  170. package/src/screenshots/compose.ts +422 -0
  171. package/src/screenshots/frame-compose.ts +438 -0
  172. package/src/screenshots/orchestrate.ts +244 -0
  173. package/src/screenshots/registry.ts +58 -0
  174. package/src/screenshots/schema.ts +364 -0
  175. package/src/skills/builtin/a11y-review.ts +126 -0
  176. package/src/skills/builtin/compat-check.ts +104 -0
  177. package/src/skills/builtin/perf-profile.ts +84 -0
  178. package/src/skills/builtin/screenshot-all.ts +46 -0
  179. package/src/skills/builtin/test-flow.ts +118 -0
  180. package/src/skills/builtin/visual-diff.ts +94 -0
  181. package/src/skills/registry.ts +107 -0
  182. package/src/skills/types.ts +41 -0
  183. package/src/vite-plugin-one.ts +187 -0
  184. package/src/vite-plugin.ts +1381 -0
  185. package/src/worklets-babel.ts +132 -0
  186. package/dist-cli/chunks/auto-bootstrap-FQS4ZD2K.js +0 -2
  187. package/dist-cli/chunks/beta-VG7CDY2U.js +0 -2
  188. package/dist-cli/chunks/chunk-2OIBDYHW.js +0 -1
  189. package/dist-cli/chunks/chunk-6BNLVMXA.js +0 -1
  190. package/dist-cli/chunks/chunk-6XD6CBJM.js +0 -2
  191. package/dist-cli/chunks/chunk-CHQTO426.js +0 -1
  192. package/dist-cli/chunks/chunk-FAPYGVIU.js +0 -4
  193. package/dist-cli/chunks/chunk-PEHFE3LG.js +0 -64
  194. package/dist-cli/chunks/chunk-RXH2SLKF.js +0 -2
  195. package/dist-cli/chunks/chunk-UXQWC5ZR.js +0 -79
  196. package/dist-cli/chunks/chunk-XFQL74PF.js +0 -5
  197. package/dist-cli/chunks/cli-version-PWF6I6LY.js +0 -2
  198. package/dist-cli/chunks/control-UIOXGYXU.js +0 -2
  199. package/dist-cli/chunks/demo-app-registry-G3BDOFWC.js +0 -2
  200. package/dist-cli/chunks/drivers-IDQF34HP.js +0 -2
  201. package/dist-cli/chunks/flow-3JN3Y7RF.js +0 -2
  202. package/dist-cli/chunks/install-2N3YOOSN.js +0 -2
  203. package/dist-cli/chunks/runtime-PVB4VGUH.js +0 -2
  204. package/dist-cli/chunks/setup-repo-YOF7NV5D.js +0 -2
  205. package/dist-cli/chunks/store-MAI6D3UO.js +0 -2
  206. package/dist-cli/chunks/telemetry-RCQKCJTH.js +0 -2
  207. package/dist-cli/chunks/upload-YLJ4RA73.js +0 -2
@@ -0,0 +1,422 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'fs'
2
+ import path from 'path'
3
+ import {
4
+ DEFAULT_FRAME_SHADOW,
5
+ toAssetKey,
6
+ type BackgroundSpec,
7
+ type CanvasPreset,
8
+ type FrameComposeSpec,
9
+ type PosePresetName,
10
+ type TextPresetName,
11
+ } from './registry'
12
+ import type { NormalizedComposePlan, NormalizedScreenshotSlide } from './schema'
13
+
14
+ export interface ComposeSlideSource {
15
+ slide: NormalizedScreenshotSlide
16
+ imagePath: string
17
+ }
18
+
19
+ export interface ComposeManifestEntry {
20
+ slideId: string
21
+ canvas: string
22
+ locale: string
23
+ outputPath: string
24
+ sourcePath: string
25
+ }
26
+
27
+ export interface ComposeResult {
28
+ manifestPath: string
29
+ outputs: string[]
30
+ entries: ComposeManifestEntry[]
31
+ }
32
+
33
+ export type MarketingBrowserLike = {
34
+ newContext: (opts: { viewport: { width: number; height: number } }) => Promise<{
35
+ newPage: () => Promise<{
36
+ setViewportSize: (size: { width: number; height: number }) => Promise<void>
37
+ setContent: (html: string, opts: { waitUntil: 'load' }) => Promise<void>
38
+ waitForTimeout: (ms: number) => Promise<void>
39
+ screenshot: (opts: {
40
+ type: 'png'
41
+ clip: { x: number; y: number; width: number; height: number }
42
+ }) => Promise<Buffer>
43
+ close: () => Promise<void>
44
+ }>
45
+ close: () => Promise<void>
46
+ }>
47
+ close?: () => Promise<void>
48
+ }
49
+
50
+ function escapeHtml(value: string): string {
51
+ return value
52
+ .replaceAll('&', '&amp;')
53
+ .replaceAll('<', '&lt;')
54
+ .replaceAll('>', '&gt;')
55
+ .replaceAll('"', '&quot;')
56
+ }
57
+
58
+ function normalizeHexColor(color: string): string {
59
+ const value = color.trim()
60
+ if (/^#[0-9a-f]{3}$/i.test(value)) {
61
+ return `#${value[1]}${value[1]}${value[2]}${value[2]}${value[3]}${value[3]}`
62
+ }
63
+ return value
64
+ }
65
+
66
+ function toRgba(color: string, alpha: number): string {
67
+ const value = normalizeHexColor(color)
68
+ const clampedAlpha = Math.max(0, Math.min(1, alpha))
69
+ const hexMatch = value.match(/^#([0-9a-f]{6})$/i)
70
+ if (hexMatch) {
71
+ const hex = hexMatch[1]
72
+ const r = Number.parseInt(hex.slice(0, 2), 16)
73
+ const g = Number.parseInt(hex.slice(2, 4), 16)
74
+ const b = Number.parseInt(hex.slice(4, 6), 16)
75
+ return `rgba(${r}, ${g}, ${b}, ${clampedAlpha})`
76
+ }
77
+ if (value.startsWith('rgb(')) {
78
+ return value.replace(/^rgb\((.+)\)$/i, `rgba($1, ${clampedAlpha})`)
79
+ }
80
+ return value
81
+ }
82
+
83
+ function backgroundCss(background: BackgroundSpec): string {
84
+ if (background.type === 'solid') {
85
+ return background.color || '#000000'
86
+ }
87
+ const direction = background.direction ?? 180
88
+ const stops =
89
+ background.stops?.map((stop) => `${stop.color} ${stop.offset}%`).join(', ') ||
90
+ '#000000 0%, #111111 100%'
91
+ return `linear-gradient(${direction}deg, ${stops})`
92
+ }
93
+
94
+ function glowCss(background: BackgroundSpec): string {
95
+ const glow = background.glow
96
+ if (!glow) return 'none'
97
+ return `radial-gradient(circle at 50% 24%, ${toRgba(glow, 0.46)} 0%, ${toRgba(glow, 0.2)} 24%, rgba(0,0,0,0) 62%)`
98
+ }
99
+
100
+ function poseTransform(pose: PosePresetName): string {
101
+ switch (pose) {
102
+ case 'tilted-left':
103
+ return 'perspective(2400px) rotateY(10deg) rotateZ(-2deg)'
104
+ case 'tilted-right':
105
+ return 'perspective(2400px) rotateY(-10deg) rotateZ(2deg)'
106
+ case 'cut-bottom':
107
+ return 'translateY(10%) scale(1.08)'
108
+ case 'cut-top':
109
+ return 'translateY(-16%) scale(1.08)'
110
+ case 'straight':
111
+ default:
112
+ return 'none'
113
+ }
114
+ }
115
+
116
+ function combineTransforms(...parts: string[]): string {
117
+ const active = parts
118
+ .map((part) => part.trim())
119
+ .filter((part) => part.length > 0 && part !== 'none')
120
+ return active.length > 0 ? active.join(' ') : 'none'
121
+ }
122
+
123
+ function shadowFilter(shadow: FrameComposeSpec['shadow']): string {
124
+ const glowColor = toRgba(shadow.color, shadow.opacity)
125
+ const lowerGlow = toRgba(shadow.color, shadow.opacity * 0.52)
126
+ return `drop-shadow(0 34px ${shadow.spread}px ${lowerGlow}) drop-shadow(0 0 ${shadow.blur}px ${glowColor})`
127
+ }
128
+
129
+ function resolveSlideShadow(
130
+ shadow: FrameComposeSpec['shadow'],
131
+ background: BackgroundSpec,
132
+ ): FrameComposeSpec['shadow'] {
133
+ if (!background.glow) return shadow
134
+ if (shadow.color && shadow.color !== DEFAULT_FRAME_SHADOW.color) return shadow
135
+ return {
136
+ ...shadow,
137
+ color: background.glow,
138
+ }
139
+ }
140
+
141
+ function textLayout(
142
+ preset: TextPresetName,
143
+ canvas: CanvasPreset,
144
+ ): {
145
+ copyStyle: string
146
+ titleStyle: string
147
+ subStyle: string
148
+ eyebrowStyle: string
149
+ deviceStyle: (
150
+ frame: FrameComposeSpec,
151
+ slide: NormalizedScreenshotSlide,
152
+ background: BackgroundSpec,
153
+ ) => string
154
+ } {
155
+ if (preset === 'editorial-left') {
156
+ const headline = Math.round(canvas.height * 0.056)
157
+ const subhead = Math.round(canvas.height * 0.022)
158
+ return {
159
+ copyStyle: [
160
+ 'position:absolute',
161
+ `top:${Math.round(canvas.height * 0.12)}px`,
162
+ `left:${Math.round(canvas.width * 0.085)}px`,
163
+ `width:${Math.round(canvas.width * 0.42)}px`,
164
+ 'text-align:left',
165
+ 'z-index:3',
166
+ ].join(';'),
167
+ titleStyle: `font-size:${headline}px;line-height:0.92;`,
168
+ subStyle: `font-size:${subhead}px;line-height:1.36;max-width:${Math.round(canvas.width * 0.36)}px;`,
169
+ eyebrowStyle: 'align-items:flex-start;',
170
+ deviceStyle: (frame, slide, background) => {
171
+ const width = Math.round(canvas.width * 0.68 * (slide.scale ?? frame.scale))
172
+ const offsetY = Math.round(
173
+ canvas.height * (0.03 + frame.offsetY + (slide.offsetY ?? 0)),
174
+ )
175
+ const pose = slide.pose || frame.pose || 'tilted-right'
176
+ const shadow = resolveSlideShadow(frame.shadow, background)
177
+ return [
178
+ 'position:absolute',
179
+ `top:${Math.round(canvas.height * 0.18) + offsetY}px`,
180
+ `left:${Math.round(canvas.width * 0.56)}px`,
181
+ `width:${width}px`,
182
+ 'transform-origin:center center',
183
+ `transform:${poseTransform(pose)}`,
184
+ `filter:${shadowFilter(shadow)}`,
185
+ 'z-index:2',
186
+ ].join(';')
187
+ },
188
+ }
189
+ }
190
+
191
+ if (preset === 'minimal-bottom') {
192
+ const headline = Math.round(canvas.height * 0.041)
193
+ const subhead = Math.round(canvas.height * 0.019)
194
+ return {
195
+ copyStyle: [
196
+ 'position:absolute',
197
+ `left:${Math.round(canvas.width * 0.12)}px`,
198
+ `right:${Math.round(canvas.width * 0.12)}px`,
199
+ `bottom:${Math.round(canvas.height * 0.085)}px`,
200
+ 'text-align:center',
201
+ 'z-index:3',
202
+ ].join(';'),
203
+ titleStyle: `font-size:${headline}px;line-height:0.96;`,
204
+ subStyle: `font-size:${subhead}px;line-height:1.34;max-width:${Math.round(canvas.width * 0.56)}px;margin:0 auto;`,
205
+ eyebrowStyle: 'align-items:center;',
206
+ deviceStyle: (frame, slide, background) => {
207
+ const width = Math.round(canvas.width * 0.84 * (slide.scale ?? frame.scale))
208
+ const offsetY = Math.round(canvas.height * (frame.offsetY + (slide.offsetY ?? 0)))
209
+ const pose = slide.pose || frame.pose
210
+ const shadow = resolveSlideShadow(frame.shadow, background)
211
+ return [
212
+ 'position:absolute',
213
+ `top:${Math.round(canvas.height * 0.075) + offsetY}px`,
214
+ 'left:50%',
215
+ `width:${width}px`,
216
+ 'transform-origin:center center',
217
+ `transform:${combineTransforms('translateX(-50%)', poseTransform(pose))}`,
218
+ `filter:${shadowFilter(shadow)}`,
219
+ 'z-index:2',
220
+ ].join(';')
221
+ },
222
+ }
223
+ }
224
+
225
+ const headline = Math.round(canvas.height * 0.04)
226
+ const subhead = Math.round(canvas.height * 0.019)
227
+ return {
228
+ copyStyle: [
229
+ 'position:absolute',
230
+ `top:${Math.round(canvas.height * 0.084)}px`,
231
+ 'left:50%',
232
+ `width:${Math.round(canvas.width * 0.88)}px`,
233
+ 'transform:translateX(-50%)',
234
+ 'text-align:center',
235
+ 'z-index:3',
236
+ ].join(';'),
237
+ titleStyle: `font-size:${headline}px;line-height:0.92;`,
238
+ subStyle: `font-size:${subhead}px;line-height:1.34;max-width:${Math.round(canvas.width * 0.6)}px;margin:0 auto;`,
239
+ eyebrowStyle: 'align-items:center;',
240
+ deviceStyle: (frame, slide, background) => {
241
+ const widthMultiplier = canvas.name === 'ipad-13' ? 0.54 : 0.64
242
+ const width = Math.round(
243
+ canvas.width * widthMultiplier * (slide.scale ?? frame.scale),
244
+ )
245
+ const offsetY = Math.round(canvas.height * (frame.offsetY + (slide.offsetY ?? 0)))
246
+ const pose = slide.pose || frame.pose
247
+ const shadow = resolveSlideShadow(frame.shadow, background)
248
+ return [
249
+ 'position:absolute',
250
+ `top:${Math.round(canvas.height * (canvas.name === 'ipad-13' ? 0.25 : 0.28)) + offsetY}px`,
251
+ 'left:50%',
252
+ `width:${width}px`,
253
+ 'transform-origin:center top',
254
+ `transform:${combineTransforms('translateX(-50%)', poseTransform(pose))}`,
255
+ `filter:${shadowFilter(shadow)}`,
256
+ 'z-index:2',
257
+ ].join(';')
258
+ },
259
+ }
260
+ }
261
+
262
+ function renderSlideHtml({
263
+ canvas,
264
+ compose,
265
+ slide,
266
+ imageDataUrl,
267
+ }: {
268
+ canvas: CanvasPreset
269
+ compose: NormalizedComposePlan
270
+ slide: NormalizedScreenshotSlide
271
+ imageDataUrl: string
272
+ }): string {
273
+ const background = slide.background ?? compose.background
274
+ const layout = textLayout(compose.text.preset, canvas)
275
+ const copyPresent =
276
+ compose.text.preset !== 'none' &&
277
+ Boolean(slide.eyebrow || slide.headline || slide.subheadline)
278
+ const imageStyle = layout.deviceStyle(compose.frame, slide, background)
279
+ const titleHtml = slide.headline
280
+ ? `<div style="font-weight:780;white-space:pre-line;text-wrap:balance;${layout.titleStyle}">${escapeHtml(slide.headline)}</div>`
281
+ : ''
282
+ const subHtml = slide.subheadline
283
+ ? `<div style="margin-top:${Math.round(canvas.height * 0.018)}px;color:${compose.text.subColor};white-space:pre-line;text-wrap:balance;${layout.subStyle}">${escapeHtml(slide.subheadline)}</div>`
284
+ : ''
285
+ const eyebrowHtml = slide.eyebrow
286
+ ? `<div style="display:flex;${layout.eyebrowStyle}margin-bottom:${Math.round(canvas.height * 0.016)}px;"><div style="font-size:${Math.round(canvas.height * 0.014)}px;font-weight:700;letter-spacing:0.18em;text-transform:uppercase;color:${compose.text.eyebrowColor};">${escapeHtml(slide.eyebrow)}</div></div>`
287
+ : ''
288
+
289
+ return `<!doctype html>
290
+ <html>
291
+ <head>
292
+ <meta charset="utf-8" />
293
+ <style>
294
+ html, body {
295
+ margin: 0;
296
+ width: ${canvas.width}px;
297
+ height: ${canvas.height}px;
298
+ overflow: hidden;
299
+ background: transparent;
300
+ }
301
+ body {
302
+ font-family: "SF Pro Display", "Helvetica Neue", system-ui, sans-serif;
303
+ }
304
+ #canvas {
305
+ position: relative;
306
+ width: ${canvas.width}px;
307
+ height: ${canvas.height}px;
308
+ overflow: hidden;
309
+ background: ${backgroundCss(background)};
310
+ color: ${compose.text.color};
311
+ isolation: isolate;
312
+ }
313
+ #glow {
314
+ position: absolute;
315
+ inset: 0;
316
+ background: ${glowCss(background)};
317
+ pointer-events: none;
318
+ }
319
+ #copy {
320
+ ${layout.copyStyle};
321
+ }
322
+ #device {
323
+ ${imageStyle};
324
+ }
325
+ #device img {
326
+ display: block;
327
+ width: 100%;
328
+ height: auto;
329
+ }
330
+ </style>
331
+ </head>
332
+ <body>
333
+ <div id="canvas">
334
+ <div id="glow"></div>
335
+ ${copyPresent ? `<div id="copy">${eyebrowHtml}${titleHtml}${subHtml}</div>` : ''}
336
+ <div id="device"><img src="${imageDataUrl}" alt="" /></div>
337
+ </div>
338
+ </body>
339
+ </html>`
340
+ }
341
+
342
+ export async function composeMarketingScreenshots(
343
+ compose: NormalizedComposePlan,
344
+ slides: ComposeSlideSource[],
345
+ browserOverride?: MarketingBrowserLike,
346
+ ): Promise<ComposeResult> {
347
+ const entries: ComposeManifestEntry[] = []
348
+ const browser =
349
+ browserOverride ??
350
+ (await (async () => {
351
+ const { chromium } = await import('playwright')
352
+ return chromium.launch({ headless: true })
353
+ })())
354
+ try {
355
+ const context = await browser.newContext({ viewport: { width: 1280, height: 720 } })
356
+ try {
357
+ for (const slideSource of slides) {
358
+ const buffer = readFileSync(slideSource.imagePath)
359
+ const imageDataUrl = `data:image/png;base64,${buffer.toString('base64')}`
360
+ for (const canvas of compose.canvases) {
361
+ const page = await context.newPage()
362
+ try {
363
+ await page.setViewportSize({ width: canvas.width, height: canvas.height })
364
+ await page.setContent(
365
+ renderSlideHtml({
366
+ canvas,
367
+ compose,
368
+ slide: slideSource.slide,
369
+ imageDataUrl,
370
+ }),
371
+ { waitUntil: 'load' },
372
+ )
373
+ await page.waitForTimeout(20)
374
+ const outputDir = path.join(compose.outDir, canvas.name, compose.locale)
375
+ mkdirSync(outputDir, { recursive: true })
376
+ const outputPath = path.join(outputDir, `${slideSource.slide.assetKey}.png`)
377
+ const png = (await page.screenshot({
378
+ type: 'png',
379
+ clip: { x: 0, y: 0, width: canvas.width, height: canvas.height },
380
+ })) as Buffer
381
+ writeFileSync(outputPath, png)
382
+ entries.push({
383
+ slideId: slideSource.slide.id,
384
+ canvas: canvas.name,
385
+ locale: compose.locale,
386
+ outputPath,
387
+ sourcePath: slideSource.imagePath,
388
+ })
389
+ } finally {
390
+ await page.close()
391
+ }
392
+ }
393
+ }
394
+ } finally {
395
+ await context.close()
396
+ }
397
+ } finally {
398
+ if (!browserOverride && typeof browser.close === 'function') {
399
+ await browser.close()
400
+ }
401
+ }
402
+
403
+ mkdirSync(compose.outDir, { recursive: true })
404
+ const manifestPath = path.join(compose.outDir, 'manifest.json')
405
+ writeFileSync(
406
+ manifestPath,
407
+ JSON.stringify(
408
+ {
409
+ generatedAt: new Date().toISOString(),
410
+ locale: compose.locale,
411
+ outputs: entries,
412
+ },
413
+ null,
414
+ 2,
415
+ ),
416
+ )
417
+ return {
418
+ manifestPath,
419
+ outputs: entries.map((entry) => entry.outputPath),
420
+ entries,
421
+ }
422
+ }