#!/usr/bin/env bun // Real desktop-app frames for the teaser's desktop scene: the Vite dev server of apps/desktop in fixture mode // (VITE_FIXTURES=1: example.com-style fixture data, no Tauri, no store accounts), captured headlessly, dark theme. // Writes media/teaser/out/desktop/*.jpg plus boxes.json (where the cursor goes, as fractions of the frame). // render.ts runs it when out/desktop/ is missing; run it by hand after a UI change: bun media/teaser/desktop.ts import { mkdirSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { launch } from '../../cli/shots/browser'; const DIR = import.meta.dir; const ROOT = join(DIR, '../..'); const OUT = join(DIR, 'out/desktop'); export const VIEW = { width: 1280, height: 800 }; const log = (m: string): void => console.log(`${new Date().toISOString().slice(11, 19)} [desktop] ${m}`); type Box = { x: number; y: number; w: number; h: number }; export async function captureDesktop(): Promise { mkdirSync(OUT, { recursive: true }); const port = 14_300 + Math.floor(Math.random() * 500); const vite = Bun.spawn(['bunx', 'vite', '--host', '127.0.0.1', '--port', String(port), '--strictPort'], { cwd: join(ROOT, 'apps/desktop'), env: { ...process.env, VITE_FIXTURES: '1' }, stdout: 'ignore', stderr: 'pipe', }); const base = `http://127.0.0.1:${port}`; try { for (let i = 0; ; i++) { try { if ((await fetch(base)).ok) break; } catch { /* not up yet */ } if (i > 120) throw new Error('vite dev server did not start'); await Bun.sleep(250); } const browser = await launch(ROOT, { headless: true }); const ctx = await browser.newContext({ viewport: VIEW, deviceScaleFactor: 2, colorScheme: 'dark', reducedMotion: 'reduce' }); const page = await ctx.newPage(); const errors: string[] = []; page.on('pageerror', (e: unknown) => errors.push(String(e))); const boxes: Record = {}; const box = async (name: string, loc: any): Promise => { const b = await loc.first().boundingBox(); if (!b) throw new Error(`no box for ${name}`); boxes[name] = { x: b.x / VIEW.width, y: b.y / VIEW.height, w: b.width / VIEW.width, h: b.height / VIEW.height }; }; const open = async (start: string): Promise => { await page.goto(`${base}/?start=${encodeURIComponent(start)}&theme=dark&lang=en`); await page.waitForLoadState('networkidle'); // the two dev-build markers ("DEV" by the name, the "DEV FIXTURES" pill) are not in a release build await page.addStyleTag({ content: '.brand small, .dev-pill { display: none !important; }' }); await page.waitForTimeout(900); }; const shot = async (name: string): Promise => { await page.mouse.move(VIEW.width - 2, VIEW.height - 2); await page.waitForTimeout(150); await page.screenshot({ path: join(OUT, `${name}.jpg`), type: 'jpeg', quality: 92 }); }; await open('portfolio'); await shot('portfolio'); await open('app:releases'); await box('console', page.getByRole('button', { name: /App Store Connect/ })); await shot('releases'); await open('app:growth'); const chart = page.locator('#tabpanel .lchart-plot').first(); await box('chart', chart); await shot('growth'); const c = boxes.chart!; const hx = (c.x + c.w * 0.68) * VIEW.width, hy = (c.y + c.h * 0.5) * VIEW.height; await page.mouse.move(hx, hy, { steps: 4 }); await page.waitForTimeout(400); boxes.hover = { x: hx / VIEW.width, y: hy / VIEW.height, w: 0, h: 0 }; await page.screenshot({ path: join(OUT, 'growth-hover.jpg'), type: 'jpeg', quality: 92 }); await open('palette'); await shot('palette'); await open('app:prices'); await page.getByRole('button', { name: /Compare store\/prices.csv/ }).click(); await page.waitForTimeout(5200); // the "compared" result toast closes by itself after 4.5 s await page.getByText(/against the stores/).first().evaluate((el: HTMLElement) => { el.scrollIntoView({ block: 'start' }); document.getElementById('content')?.scrollBy({ top: -24 }); }); await page.waitForTimeout(300); await box('apply', page.getByRole('button', { name: /^Apply \d+ changes/ })); await shot('prices-diff'); writeFileSync(join(OUT, 'boxes.json'), `${JSON.stringify(boxes, null, 1)}\n`); await browser.close(); if (errors.length) throw new Error(`page errors:\n ${[...new Set(errors)].join('\n ')}`); log(`6 frames + boxes.json -> ${OUT}`); } finally { vite.kill(); } } if (import.meta.main) await captureDesktop();