Skip to content

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.vuePreviewPanel.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:

  1. Primary: window.matchMedia('(pointer: coarse)') — detects touch devices
  2. Fallback: Viewport width <= 768px
  3. Dev override: ?mobile=true query parameter forces mobile view on desktop; ?mobile=false forces 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:

  1. Has a fullscreen format? → Select it (best mobile experience)
  2. No fullscreen? → Select format closest to viewport width
  3. 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 width and height are 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 ID
  • chatbotHash — Build hash
  • creativeType — Creative type string
  • creativeFormat — Format object ({ width, height }) or string
  • maxWidth / 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 setSelectedPreviewCreative mutation 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 placement

Context modes are future wrappers around the iframe. The component structure supports this without refactoring.

File Changes

FileChange
src/pages/Chatbots/CreativePreview.vueAdd isMobileDevice computed, conditional rendering, mobile format override after fetch
src/pages/Chatbots/components/BuilderVisuals/Preview/MobileLitePreview.vueNew file — lightweight preview component wrapping PreviewIframe
src/pages/Chatbots/components/BuilderVisuals/Preview/MobileFormatDropdown.vueNew file — dropdown format selector for mobile

Internal documentation