// fetch the team's screenshot deck (the copy / colors founders set up in the // org dashboard) and overlay it onto a screenshots plan, so the programmatic // `rnx screenshot appstore --plan … --org … --repo …` flow renders the org's // words + brand gradient instead of the local yaml's. the plan still owns // which screen each slide captures and the device frame; the deck owns copy + // colors. 3d scenes don't translate to the cli's 2d composer, so only copy and // colors carry over (the 3d hero look stays the in-browser screenshot mode). import { resolveBgColorValue } from 'sootsim-engine/screenshots/tokens' import type { BackgroundSpec } from './registry' import type { NormalizedScreenshotsPlan } from './schema' export interface OrgDeckFetchOptions { org: string repo: string origin: string authHeader: string } export interface OrgDeckFrame { title?: string subtitle?: string bgTop?: string bgBottom?: string titleColor?: string } function readString(value: unknown): string | undefined { return typeof value === 'string' && value.length > 0 ? value : undefined } // GET the team deck and return its first deck's frames, or null when the team // has no deck yet. throws on auth / network failure so the caller can surface a // clear message rather than silently falling back to the local copy. export async function fetchOrgDeckFrames( opts: OrgDeckFetchOptions, ): Promise { const base = opts.origin.replace(/\/$/, '') const url = `${base}/api/sootsim/screenshot-decks?org=${encodeURIComponent( opts.org, )}&repo=${encodeURIComponent(opts.repo)}` const res = await fetch(url, { headers: { Authorization: opts.authHeader } }) if (!res.ok) { const detail = await res.text().catch(() => '') throw new Error( `org deck fetch failed (${res.status} ${res.statusText}) for ${opts.org}/${opts.repo}${ detail ? ` — ${detail.slice(0, 200)}` : '' }`, ) } const body = (await res.json()) as { decks?: Array<{ config?: unknown }> } const deck = body.decks?.[0] if (!deck || !deck.config || typeof deck.config !== 'object') return null const frames = (deck.config as { frames?: unknown }).frames if (!Array.isArray(frames)) return null return frames.map((entry) => { const frame = (entry ?? {}) as Record return { title: readString(frame.title), subtitle: readString(frame.subtitle), bgTop: readString(frame.bgTop), bgBottom: readString(frame.bgBottom), titleColor: readString(frame.titleColor), } }) } // overlay deck frames onto the plan's compose slides, matched by order. a deck // with fewer frames than the plan leaves the trailing slides on their yaml copy. export function applyOrgDeckToPlan( plan: NormalizedScreenshotsPlan, frames: OrgDeckFrame[], ): void { plan.compose.slides.forEach((slide, index) => { const frame = frames[index] if (!frame) return if (frame.title != null) slide.headline = frame.title if (frame.subtitle != null) slide.subheadline = frame.subtitle if (frame.bgTop && frame.bgBottom) { const background: BackgroundSpec = { type: 'gradient', direction: 180, stops: [ { offset: 0, color: resolveBgColorValue(frame.bgTop) }, { offset: 1, color: resolveBgColorValue(frame.bgBottom) }, ], } slide.background = background } }) }