1import {
2 MOBILE_DEFAULT_THEME_ID,
3 MOBILE_THEME_IDS,
4 type MobileThemeId,
5} from "@t3tools/shared/themePalettes";
6
7import { SHOWCASE_SCENES, type ShowcaseScene } from "./mobile-showcase-environment.ts";
8
9export { SHOWCASE_SCENES };
10export type { ShowcaseScene };
11
12export type ShowcaseAppearance = "light" | "dark";
13
14/** Every palette the mobile appearance settings can select. */
15export const SHOWCASE_THEMES = MOBILE_THEME_IDS;
16export const DEFAULT_SHOWCASE_THEME = MOBILE_DEFAULT_THEME_ID;
17export type ShowcaseTheme = MobileThemeId;
18
19export interface ShowcaseStoreAssetSpec {
20 readonly store: "apple" | "google-play";
21 /** Device directory relative to ShowcaseConfig.outputDirectory. */
22 readonly directory: string;
23 readonly width: number;
24 readonly height: number;
25 readonly minimumUploadCount: number;
26 readonly maximumUploadCount: number;
27 readonly maximumFileSizeBytes?: number;
28}
29
30export interface ShowcaseIosDevice {
31 readonly id: string;
32 readonly platform: "ios";
33 /** Exact name from `xcrun simctl list devices available`. */
34 readonly simulator: string;
35 /** Device type used to create a disposable simulator when the named one is absent. */
36 readonly simulatorDeviceType?: string;
37 /** Appearance used when the CLI does not pass --appearance. */
38 readonly appearance: ShowcaseAppearance;
39 /** Palette used when the CLI does not pass --theme. */
40 readonly theme: ShowcaseTheme;
41 /** Orientation applied by the capture harness. Defaults to portrait. */
42 readonly orientation?: "portrait" | "landscape";
43 readonly scenes: ReadonlyArray<ShowcaseScene>;
44 readonly storeAsset: ShowcaseStoreAssetSpec;
45}
46
47export interface ShowcaseAndroidDevice {
48 readonly id: string;
49 readonly platform: "android";
50 /** Exact name from `emulator -list-avds`. */
51 readonly avd: string;
52 /** Appearance used when the CLI does not pass --appearance. */
53 readonly appearance: ShowcaseAppearance;
54 /** Palette used when the CLI does not pass --theme. */
55 readonly theme: ShowcaseTheme;
56 /** Native ABI used by the AVD, from its config.ini `abi.type`. */
57 readonly abi?: "arm64-v8a" | "x86_64" | "x86" | "armeabi-v7a";
58 readonly scenes: ReadonlyArray<ShowcaseScene>;
59 /** Optional capture viewport. Omit to use the AVD's native size and density. */
60 readonly viewport?: {
61 readonly width: number;
62 readonly height: number;
63 readonly density?: number;
64 };
65 readonly storeAsset: ShowcaseStoreAssetSpec;
66}
67
68export type ShowcaseDevice = ShowcaseIosDevice | ShowcaseAndroidDevice;
69
70export interface ShowcaseConfig {
71 readonly outputDirectory: string;
72 readonly metroPort: number;
73 readonly settleDelayMs: number;
74 readonly devices: ReadonlyArray<ShowcaseDevice>;
75}
76
77const ANDROID_ABIS = ["arm64-v8a", "x86_64", "x86", "armeabi-v7a"] as const;
78
79export function resolveShowcaseAndroidAbi(
80 value: string | undefined,
81): NonNullable<ShowcaseAndroidDevice["abi"]> {
82 if (!value) return "arm64-v8a";
83 if (ANDROID_ABIS.some((abi) => abi === value)) {
84 return value as NonNullable<ShowcaseAndroidDevice["abi"]>;
85 }
86 throw new Error(
87 `Unsupported T3_SHOWCASE_ANDROID_ABI '${value}'. Use ${ANDROID_ABIS.join(", ")}.`,
88 );
89}
90
91/**
92 * The defaults cover every App Store Connect and Google Play upload slot used
93 * by the mobile app. Edit this matrix (or pass --device / --scene) without
94 * changing the runner. Every target declares and validates its exact upload
95 * dimensions so SDK or emulator changes cannot silently produce invalid files.
96 */
97const config: ShowcaseConfig = {
98 outputDirectory: "artifacts/app-store/screenshots",
99 // Dedicated port so the harness cannot attach to a normal mobile dev server
100 // (or a second worktree) and capture the wrong bundle.
101 metroPort: 8199,
102 settleDelayMs: 2_500,
103 devices: [
104 {
105 id: "iphone-6.9",
106 platform: "ios",
107 // A disposable device lands on the newest runtime, whose default lock
108 // screen wallpaper suits both appearances; a stock one may be older.
109 simulator: "T3 Showcase iPhone 17 Pro Max",
110 simulatorDeviceType: "com.apple.CoreSimulator.SimDeviceType.iPhone-17-Pro-Max",
111 appearance: "dark",
112 theme: DEFAULT_SHOWCASE_THEME,
113 scenes: ["thread", "terminal", "review", "threads", "environments", "agent-activity"],
114 storeAsset: {
115 store: "apple",
116 directory: "apple/iphone-6.9",
117 width: 1320,
118 height: 2868,
119 minimumUploadCount: 1,
120 maximumUploadCount: 10,
121 },
122 },
123 {
124 id: "iphone-6.5",
125 platform: "ios",
126 simulator: "T3 Showcase iPhone 14 Plus",
127 simulatorDeviceType: "com.apple.CoreSimulator.SimDeviceType.iPhone-14-Plus",
128 appearance: "dark",
129 theme: DEFAULT_SHOWCASE_THEME,
130 scenes: ["thread", "terminal", "review", "threads", "environments", "agent-activity"],
131 storeAsset: {
132 store: "apple",
133 directory: "apple/iphone-6.5",
134 width: 1284,
135 height: 2778,
136 minimumUploadCount: 1,
137 maximumUploadCount: 10,
138 },
139 },
140 {
141 id: "ipad-13",
142 platform: "ios",
143 simulator: "iPad Pro 13-inch (M5)",
144 simulatorDeviceType: "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-13-inch-M5-16GB",
145 appearance: "dark",
146 theme: DEFAULT_SHOWCASE_THEME,
147 orientation: "landscape",
148 // The lock screen does not follow the app's self-rotation, so a
149 // headless landscape capture of it would come out sideways.
150 scenes: ["thread", "terminal", "review", "threads", "environments"],
151 storeAsset: {
152 store: "apple",
153 directory: "apple/ipad-13",
154 width: 2752,
155 height: 2064,
156 minimumUploadCount: 1,
157 maximumUploadCount: 10,
158 },
159 },
160 {
161 id: "pixel",
162 platform: "android",
163 avd: "Pixel_10_Pro",
164 // Apple Silicon uses ARM64 locally; CI overrides this with x86_64 so its
165 // Blacksmith Linux runner can use KVM acceleration.
166 abi: resolveShowcaseAndroidAbi(process.env.T3_SHOWCASE_ANDROID_ABI),
167 appearance: "dark",
168 theme: DEFAULT_SHOWCASE_THEME,
169 viewport: {
170 width: 1080,
171 height: 1920,
172 density: 420,
173 },
174 scenes: ["thread", "terminal", "review", "threads", "environments", "agent-activity"],
175 storeAsset: {
176 store: "google-play",
177 directory: "google-play/phone",
178 width: 1080,
179 height: 1920,
180 minimumUploadCount: 2,
181 maximumUploadCount: 8,
182 maximumFileSizeBytes: 8 * 1024 * 1024,
183 },
184 },
185 {
186 id: "android-tablet-7",
187 platform: "android",
188 avd: "Pixel_10_Pro",
189 abi: resolveShowcaseAndroidAbi(process.env.T3_SHOWCASE_ANDROID_ABI),
190 appearance: "dark",
191 theme: DEFAULT_SHOWCASE_THEME,
192 viewport: {
193 width: 1080,
194 height: 1920,
195 density: 288,
196 },
197 scenes: ["thread", "terminal", "review", "threads", "environments", "agent-activity"],
198 storeAsset: {
199 store: "google-play",
200 directory: "google-play/tablet-7",
201 width: 1080,
202 height: 1920,
203 minimumUploadCount: 4,
204 maximumUploadCount: 8,
205 maximumFileSizeBytes: 8 * 1024 * 1024,
206 },
207 },
208 {
209 id: "android-tablet-10",
210 platform: "android",
211 avd: "Pixel_10_Pro",
212 abi: resolveShowcaseAndroidAbi(process.env.T3_SHOWCASE_ANDROID_ABI),
213 appearance: "dark",
214 theme: DEFAULT_SHOWCASE_THEME,
215 viewport: {
216 width: 1440,
217 height: 2560,
218 density: 288,
219 },
220 scenes: ["thread", "terminal", "review", "threads", "environments", "agent-activity"],
221 storeAsset: {
222 store: "google-play",
223 directory: "google-play/tablet-10",
224 width: 1440,
225 height: 2560,
226 minimumUploadCount: 4,
227 maximumUploadCount: 8,
228 maximumFileSizeBytes: 8 * 1024 * 1024,
229 },
230 },
231 ],
232};
233
234export default config;