Appearance
Mobile Lite Preview — Design Spec
Lightweight mobile preview for the standalone creative preview. Replaces the heavy panZoom-based PreviewPanel on mobile devices with a minimal component optimized for performance and UX.
Related: GitHub #1848 | Linear CAV-164 | Linear CAV-33 (next step: in-context preview modes)
Problem
The standalone preview (CreativePreview.vue → PreviewPanel.vue) uses panZoom, scaling calculations, resize observers, and device frame SVGs. On mobile devices this causes crashes and poor performance. Fullscreen formats shown inside an iPhone frame on an actual phone is redundant. Clients sharing preview links with stakeholders get a broken experience on mobile.
Architecture
CreativePreview.vue detects the device and renders one of two components:
CreativePreview.vue (data fetching, whitelabel, format selection)
├── isMobileDevice === true → MobileLitePreview
└── isMobileDevice === false → PreviewPanel (unchanged)CreativePreview owns all data fetching, whitelabel logic, and initial format selection. MobileLitePreview receives the selected creative's props and renders an iframe.
Mobile Detection
Computed property isMobileDevice in CreativePreview:
- Primary:
window.matchMedia('(pointer: coarse)')— detects touch devices - Fallback: Viewport width <= 768px
- Dev override:
?mobile=truequery parameter forces mobile view on desktop;?mobile=falseforces desktop view on mobile (useful for debugging)
Data Flow
Format List
CreativePreview.previewFormats is currently declared in data() but never populated — it stays as []. The actual format list lives in Vuex at preview.previewCreatives, which PreviewFormatSelector reads directly.
MobileLitePreview and MobileFormatDropdown follow the same pattern: read previewCreatives from Vuex directly rather than receiving formats as a prop. This is consistent with how the desktop format selector already works.
Iframe Rendering
MobileLitePreview renders a raw <iframe> with selectedPreviewCreative.tagFile as src. It does NOT reuse PreviewIframe because that component internally renders MobilePreviewContainer (iPhone frames, resize handles, dummy website backgrounds) for fullscreen/percentage formats — exactly the overhead we want to avoid on mobile. The only thing needed from PreviewIframe is the URL, which is simply tagFile from Vuex.
Mobile Format Selection
After fetchPreviewData completes in CreativePreview's mounted(), the Vuex store sets the default to the largest format (last(sortedFormats)). On mobile, CreativePreview overrides this by calling setSelectedPreviewCreative with the mobile-preferred format:
- Has a fullscreen format? → Select it (best mobile experience)
- No fullscreen? → Select format closest to viewport width
- Only one format? → Select it
Desktop default selection (largest format) remains unchanged.
Loading State
CreativePreview already shows "Building your creative..." when creativesBuilding is true. This sits above the component branch — it shows regardless of whether MobileLitePreview or PreviewPanel is rendered.
Fullscreen Format Detection
Formats come in multiple shapes. A format is considered "fullscreen" when:
- The format string equals
CREATIVE_FORMATS.FULLSCREEN(the string"fullscreen"), OR - Both
widthandheightare percentage strings (e.g."100%")
This matches how PreviewPanel already detects fullscreen via selectedFormat === CREATIVE_FORMATS.FULLSCREEN.
A format is "percent" when at least one dimension is a percentage string but it's not fullscreen (e.g. width "100%", height 250).
A format is "fixed" when both dimensions are numbers (e.g. 300, 600).
MobileLitePreview Component
No panZoom mixin. No device frames. Wraps PreviewIframe with CSS layout.
Props
creativeId— Selected creative IDchatbotHash— Build hashcreativeType— Creative type stringcreativeFormat— Format object ({ width, height }) or stringmaxWidth/maxHeight— Numeric dimensions (null for percentage/fullscreen formats)customLogo— Whitelabel logo URL (Cavai logo as fallback)
Format list for the dropdown is read from Vuex (preview.previewCreatives), not passed as a prop.
Display Modes
The component determines display mode from the format (see Fullscreen Format Detection above):
Fullscreen Format
- Iframe:
100dvw x 100dvh - No scaling, no background visible
- Creative fills the entire screen
- Format dropdown: semi-transparent button only, no background bar
Percent Format (e.g. halfscreen: "100%" width, 250 height)
- Width: CSS percentage of container (e.g.
width: 100%on a full-width container) - Height: fixed pixel value (
height: 250px) - Container:
100dvw x 100dvh, flexbox centering - Background: existing standalone color visible around the creative
Fixed Format (e.g. 300x600, 980x300)
- Scaled down with
transform: scale()to fit viewport with padding - Scale factor:
Math.min((viewportWidth - 32) / formatWidth, (viewportHeight - 32) / formatHeight, 1) - Never scales up, only down
- Centered vertically and horizontally via flexbox
- 16px padding on each side
- Background: existing standalone color
Orientation Changes
Fullscreen and percent formats use dvw/dvh units which auto-update on rotation. Fixed format scale calculation uses a CSS-only approach where possible; if transform: scale() requires JS, a resize event listener recalculates. No heavy observers — just one listener.
UI Overlay
Logo (top left):
- Whitelabel logo or Cavai logo fallback
- Always visible
- No back-to-builder navigation (builder doesn't work well on mobile)
Format dropdown (bottom center):
- Hidden when only one format exists
- Small button showing current format name (e.g. "Fullscreen", "300x600")
- Opens a list upward on tap with all available formats
- Styling inspired by existing PreviewFormatSelector (same colors, typography) but as a dropdown interaction pattern instead of a horizontal tab bar
- In fullscreen mode: semi-transparent button without the dark background bar used in other modes
- Format switch calls
setSelectedPreviewCreativemutation in Vuex; CreativePreview's computed props update; iframe reloads
What Does NOT Change
- PreviewPanel: Completely untouched. Desktop path is identical.
- Vuex preview store: Same mutations/actions. Mobile just overrides which creative is selected initially.
- Preview route: Same URL (
/creatives/:routeId/:creativeType/:creatives?/:hash). Device detection happens at render time. - Whitelabel logic: Stays in CreativePreview. MobileLitePreview just receives the logo URL.
- PreviewIframe: Not reused — MobileLitePreview uses a raw iframe to avoid MobilePreviewContainer overhead.
Scalability to Context Modes
This architecture prepares for in-context preview (CAV-33):
MobileLitePreview
├── mode: "clean" → iframe fills screen (this spec)
├── mode: "article" → iframe in mock article page (skins, standard)
├── mode: "scroll" → iframe in scrollable page (double fullscreen, midscroll)
└── mode: "topscroll" → iframe with topscroll placementContext modes are future wrappers around the iframe. The component structure supports this without refactoring.
File Changes
| File | Change |
|---|---|
src/pages/Chatbots/CreativePreview.vue | Add isMobileDevice computed, conditional rendering, mobile format override after fetch |
src/pages/Chatbots/components/BuilderVisuals/Preview/MobileLitePreview.vue | New file — lightweight preview component wrapping PreviewIframe |
src/pages/Chatbots/components/BuilderVisuals/Preview/MobileFormatDropdown.vue | New file — dropdown format selector for mobile |