#!/usr/bin/env bun // Real desktop-app frames for the teaser's three desktop scenes: 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, // with the two dev-build markers hidden (they are not in a release build). // Writes media/teaser/out/desktop/*.jpg plus boxes.json (cursor and callout targets, as fractions of the frame). // portfolio.jpg Portfolio: every app of every studio, grouped by studio // releases.jpg Prism Puddle's release timeline, both stores rolling out at 20% // releases-empty.jpg the same frame with the rollout bars' fill and "20%" hidden (the video fills the bar) // checks.jpg the Checks tab: the "Rejection risks" card with its findings // checks-clean.jpg the same tab after the fixes: the fixture's scan answer with no findings (the app's own // "No rejection risks found" state; the fixture module is served with an empty findings list) // 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 boxes: Record = {}; const errors: string[] = []; const session = async (clean: boolean) => { const ctx = await browser.newContext({ viewport: VIEW, deviceScaleFactor: 2, colorScheme: 'dark', reducedMotion: 'reduce' }); if (clean) { // after the fixes: the fixture's scan of Prism Puddle answers with no findings (the UI is unchanged) await ctx.route(/\/src\/dev\/fixtures\.ts/, async (route: any) => { const res = await route.fetch(); const src = await res.text(); const body = src.replace(/findings:\s*SCAN_PRISM,\s*counts:\s*scanCounts\(SCAN_PRISM\)/, 'findings: [], counts: scanCounts([])'); if (body === src) errors.push('fixtures.ts: the Prism Puddle scan entry moved (desktop.ts clean capture)'); await route.fulfill({ response: res, body }); }); } const page = await ctx.newPage(); page.on('pageerror', (e: unknown) => errors.push(String(e))); return { ctx, page }; }; const open = async (page: any, start: string): Promise => { await page.goto(`${base}/?start=${encodeURIComponent(start)}&theme=dark&lang=en`); await page.waitForLoadState('networkidle'); await page.addStyleTag({ content: '.brand small, .dev-pill { display: none !important; }' }); await page.waitForTimeout(1200); }; const box = async (page: any, 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 shot = async (page: any, name: string): Promise => { await page.mouse.move(VIEW.width - 2, VIEW.height - 2); await page.waitForTimeout(200); await page.screenshot({ path: join(OUT, `${name}.jpg`), type: 'jpeg', quality: 93 }); }; const a = await session(false); await open(a.page, 'portfolio'); await box(a.page, 'studios', a.page.locator('.fchips')); await box(a.page, 'table', a.page.locator('table').first()); await shot(a.page, 'portfolio'); await open(a.page, 'app:releases'); // the release timeline (both stores, rollout bars) at the top of the frame await a.page.locator('.tl-lane').first().evaluate((el: HTMLElement) => { el.scrollIntoView({ block: 'start' }); document.getElementById('content')?.scrollBy({ top: -150 }); }); await a.page.waitForTimeout(300); await box(a.page, 'roll', a.page.locator('.tl-roll').first()); await box(a.page, 'roll2', a.page.locator('.tl-roll').nth(1)); await box(a.page, 'timeline', a.page.locator('.tl-lane').first()); await shot(a.page, 'releases'); await a.page.addStyleTag({ content: '.tl-roll .rbar rect:not(.track) { display: none !important; } .tl-roll .pct { visibility: hidden !important; }' }); await a.page.waitForTimeout(150); await shot(a.page, 'releases-empty'); await open(a.page, 'app:checks'); await box(a.page, 'scan', a.page.locator('.card').first()); await box(a.page, 'finding', a.page.locator('.card').first().locator('.li').first()); await box(a.page, 'scanAgain', a.page.locator('.card').first().locator('.btn').first()); await shot(a.page, 'checks'); await a.ctx.close(); const b = await session(true); await open(b.page, 'app:checks'); await box(b.page, 'scanClean', b.page.locator('.card').first()); await shot(b.page, 'checks-clean'); await b.ctx.close(); 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(`5 frames + boxes.json -> ${OUT}`); } finally { vite.kill(); } } if (import.meta.main) await captureDesktop();