10 KiB
Mobile Prototype Agent Guide
Prototype Instructions
In ChatGPT Work Mode, run sites-preview start "$PWD", open http://terminal.local:4173/ in the cloud browser, and verify the rendered app and its primary interactions. Keep that preview open and tell the user to inspect it in the cloud browser; do not present the local URL as a user-facing chat link. In Codex Desktop, run the local server yourself, open the preview in the in-app browser, and provide the clickable local URL. Do not deploy to Sites unless the user explicitly asks to share, publish, or deploy. Do not give the user server-start instructions when you can run it.
Before planning or implementing any mobile-app change, read this AGENTS.md in full. It is the source of truth for the template's runtime and component guidance.
Before making substantial visual changes, use the Product Design plugin's get-context skill when the visual source is unclear or no longer matches the current goal. When the user gives durable prototype-specific design feedback, preferences, or decisions, record them in AGENTS.md.
When implementing from a selected generated mock, treat that image as the source of truth for layout, component anatomy, density, spacing, color, typography, visible content, and hierarchy.
Editing Boundary
- Build app-specific UI in
src/Prototype.tsxandsrc/prototype.css. - Treat
src/App.tsx,src/main.tsx,src/styles.css,src/mobile/,public/assets/iphone/,public/assets/android/,public/assets/status/,vite.config.ts,worker/index.js, andscripts/prepare-sites-build.mjsas protected runtime files. Do not edit, replace, remove, or recreate them unless the user explicitly asks to change the mobile runtime itself. For an explicit runtime change, update the affected lock hashes only after verifying the new runtime behavior. - Run
npm run check:runtimebefore preview or handoff. If it fails, restore the protected runtime instead of weakening or bypassing the check. npm run buildpreserves the mobile runtime and prepares the static Cloudflare Worker output required by Sites. Before a Sites handoff, confirmdist/client/index.html,dist/server/index.js,dist/.openai/hosting.json, and source.openai/hosting.jsonexist, then runnpm run test:sites. Do not replace this project with a Vinext starter.
Runtime Contract
- Preserve the mobile device runtime unless the user's task explicitly asks otherwise. Do not replace it with a standalone page. Visual fidelity applies to app-owned content inside the device screen, not to template-owned device chrome.
- Keep
Appcomposed aroundPhoneFrame->KeyboardProvider, withStatusBar, app content,HomeIndicator, andKeyboardDockmounted inside the phone frame.StatusBarand the iOS home indicator are overlaid device chrome. When the Android keyboard is closed, the app viewport reserves the protected navigation-bar region instead of painting behind it. When the Android keyboard is open, preserve the current full-screen keyboard layout: its asset includes the IME navigation strip and the separate black navigation bar is hidden. iOS screens continue to paint behind the home-indicator area and own their safe-area content padding. - Preserve the
iPhone/Pixel 10device picker and both calibrated device presets. The Pixel screen is427 x 952; its32 x 32camera circle andpublic/assets/android/navigation-bar.svgbottom navigation bar are protected device chrome, not app content. - Preserve the device picker's intentionally lightweight Codex styling in the top-right corner: its trigger wrapper is borderless and transparent, its trigger sizes to content, and its right-aligned menu uses the compact 3px inset plus the specified hairline and elevation shadow layers. Keep the prototype root and default app screen white.
- Preserve
StatusBaras live device chrome, including its platform-specific typography, source status-icon assets, and spacing. Pixel 10 uses Roboto, Android indicators, and 32px top, left, and right padding. iPhone uses its iOS indicators, system typography, and calibrated spacing. Do not hardcode screenshot times like9:41into the status bar, replace its real-time clock, or move status bar content into app markup unless the user explicitly asks for a fixed/mock device time. PhoneFrameowns the calibrated device frame, screen portal, device picker, camera cutout, and custom cursor. Keep device assets inpublic/assets/iphone/andpublic/assets/android/; if an asset fails to load, repair the asset path or restore the asset instead of removing the frame, keyboard, or image render.- Use
MobileScrolldirectly for simple single-screen prototypes. UseFlowStackfor conventional multi-screen flows whose routes can own their fixed header and footer; when using it, define each route as aFlowScreen:{ id, header?, headerHeight?, footer?, footerHeight?, render }, and useflow.push(screen),flow.pop(), andflow.replace(screen)fromFlowStackrender callbacks oruseFlow()instead of introducing another router. - Use
Carouselfor a carousel, horizontal rail, swipeable cards, image or media strip, horizontally scrollable cards, chip rail, or other horizontal collection. - For a layered app shell—such as a persistent composer, independently presented sheet, pushed/peek sidebar, or app-wide transition—compose directly in
Prototype.tsxrather than forcing it throughFlowStack. Keep app-owned fixed chrome as sibling layers outsideMobileScroll. - When using
FlowScreen, put route-owned fixed headers or footers inFlowScreen.headerorFlowScreen.footer. SetheaderHeightto the visible app-toolbar height;FlowStackadds the device's top safe-area/status-bar inset automatically. Do not includeStatusBaror its height in the header. SetfooterHeightto the full app-footer height.FlowScreen.footeris an overlay, not reserved layout space; screens using it must add their own bottom content padding such aspadding-bottom: calc(var(--flow-footer-height) + var(--mobile-safe-area-height) + 24px)so final content can scroll above the footer while still painting behind it. - Render only scrollable content inside
MobileScroll; it is for content that should move with scroll and rubber-band overscroll. Keep app-owned headers, nav bars, tabs, composers, and overlays outside it. This keeps scroll physics, safe areas, keyboard insets, scrollbars, and drag click suppression active without letting content paint under fixed chrome. - Buttons, links, cards, and images inside
MobileScrollshould still allow drag scrolling when the pointer moves beyond tap slop. Usedata-scroll-drag="ignore"only for rare controls that must own the drag gesture themselves. - Do not add
var(--keyboard-height)to ordinary screen/content padding insideMobileScroll; the scroll viewport already shrinks above the simulated keyboard. For custom fixed composers, search bars, or toast chrome, useuseKeyboardInsets().bottomInset. It is relative to the app viewport: Android returns0while the closed-keyboard viewport already reserves navigation, then returns the keyboard height while open; iOS continues to clear the home indicator while closed and ride directly above the keyboard while open. Do not pin custom bottom chrome tobottom: 0or onlykeyboardHeight. - Use
KeyboardInput,KeyboardTextarea, orMobileTextFieldfor every text-entry control. A rawinputortextareadisconnects focus, keyboard animation, safe-area insets, and attached surfaces. - Use
BottomSheetfor phone-scoped sheets. Its props areopen,onOpenChange,title, optionaldescription, optionalsnap, andchildren; it renders through the phone screen portal and dismisses the keyboard before opening.
Horizontal Carousels
- Use
Carouselfor horizontally draggable cards, images, media, chips, or other horizontal collections. Do not recreate these withoverflow-x, custom pointer handlers, or a generic div. Carouselcan be nested directly insideMobileScroll. It owns horizontal gestures and automatically yields vertical gestures to the parent.- Never put
data-scroll-drag="ignore"on or around aCarousel; doing so prevents vertical parent scrolling when a gesture begins inside it. - Do not add CSS scroll snapping to
Carousel; its runtime owns momentum and release motion. - Use
data-scroll-drag="ignore"only when a control must prevent parent scrolling in every drag direction.
See src/mobile/COMPONENTS.md for the full component and gesture contract.
Keyboard Rule
The simulated keyboard is a separate top-layer component. Before presenting anything that behaves like iOS navigation or modal UI, dismiss it first.
Call keyboard.hide() before:
- pushing, popping, or replacing FlowStack routes
- opening bottom sheets, action sheets, dialogs, menus, or navigation sheets
- starting transitions where the destination should not inherit text-input focus
FlowStack already hides the keyboard for push, pop, and replace. BottomSheet already hides it before opening. If you add new modal/sheet/navigation primitives, follow the same rule.
When a composer, search surface, or other keyboard-attached component closes, call keyboard.hide() in the same event before changing that component's open state. Position attached surfaces from useKeyboardInsets() rather than a separate timer or visibility flag so both dismiss together.
When any text-entry control loses focus, dismiss the simulated keyboard. If the control is custom or does not use the runtime's keyboard-aware fields, handle its blur event and call keyboard.hide() explicitly. Keep the keyboard open only when focus is moving directly to another text-entry control that should share the same keyboard session.
Interaction Rules
- Do not trigger buttons or inputs after a pointer has become a drag. Preserve the drag suppression behavior in
MobileScroll. - Do not allow native browser image/file dragging inside the phone frame. Preserve the phone-level
dragstartsuppression and non-draggable image styles so scroll drags that begin on images still scroll the prototype. - Use
KeyboardInput,KeyboardTextarea, orMobileTextFieldfor text entry so the simulated keyboard and safe-area insets stay connected. - Fixed phone chrome should not animate with pushed screens. Screen content can animate; the status bar, camera cutout, and preview chrome should stay put.
- Keep the keyboard below the home indicator/safe area layer in z-index, and above ordinary app UI while visible.
- Keep the home indicator as the topmost safe-area layer in the z-index above everything else in the prototype.