Appearance
Iframe DOM Inspector - Design Spec
Date: 2026-05-05 Status: Approved Branch: TBD (likely
dev-tools-dom-inspectoror continuation ofimprove-dev-tools)
Problem
Browser DevTools lose focus when the creative preview iframe refreshes. Switching between main page and iframe console is awkward. Developers need a way to inspect the creative DOM without fighting the browser.
Solution
A built-in DOM inspector inside the existing DevToolPanel system. Three tabs: Elements (DOM tree + element info), Console (eval + captured output), and Styles (computed styles + box model). The inspector is Cavai-aware - it maps DOM elements to block names and shortcodes.
Pop-out + fullscreen on a second monitor makes this a full replacement for browser DevTools when working with creatives.
Key Architectural Decision: Direct DOM Access
The preview iframe loads from /preview-frame.html (same-origin). This means AF can access iframe.contentDocument and iframe.contentWindow directly. This eliminates the need for serializing/deserializing the DOM tree via postMessage.
What this means:
- DOM tree: read directly from
iframe.contentDocument(no serialization) - Computed styles:
iframe.contentWindow.getComputedStyle(element)directly - Eval:
iframe.contentWindow.eval(expression)directly - Highlight overlay: inject elements directly into iframe DOM from AF
- MutationObserver: runs in AF, observing
iframe.contentDocument
What still needs postMessage:
- Console interception: must patch
consoleinside the iframe to capture output (CE-side, minimal code)
Trade-off: If the preview iframe ever becomes cross-origin, the DOM access approach would need to be replaced with postMessage serialization. This is unlikely given the local build architecture, and the simpler implementation outweighs the risk.
Architecture
New files
Creative-Engine (minimal):
src/devtools/consoleInterceptor.ts- console monkey-patching only- Gated behind
DataStore.devMode.valueat runtime (the#includeannotations are documentation markers, not preprocessor directives - all devtools code ships in bundle but is gated at runtime) - Must store original console references and restore on cleanup
- Re-initializes automatically after creative rebuild via
preview-iframe-readymessage
Application-Frontend:
src/components/DevTools/DomInspector.vue- main component with tab bar and three tabssrc/components/DevTools/DomTreeNode.vue- DOM-specific recursive tree (HTML-like rendering)src/components/DevTools/InspectorConsole.vue- console tabsrc/components/DevTools/InspectorStyles.vue- styles tab with box model diagram
Iframe access
DomInspector.vue gets the iframe reference from the builder. The iframe element is available via the preview component. On mount and on each preview-iframe-ready message, the inspector:
- Gets
iframe.contentDocumentandiframe.contentWindow - Attaches MutationObserver to observe DOM changes (debounced 500ms)
- Builds the initial DOM tree
- Sets up click listener for inspect mode
After creative rebuilds (detected via preview-iframe-ready postMessage), the observer and listeners are re-attached to the new document.
Console interception (CE-side)
The only CE-side code. In consoleInterceptor.ts:
ts
const originalConsole = { ...window.console }
export function initConsoleInterceptor() {
if (!DataStore.devMode?.value) return
for (const level of ['log', 'warn', 'error', 'info']) {
window.console[level] = (...args) => {
originalConsole[level](...args)
window.parent.postMessage({
type: 'cavai-dom-console',
level,
args: safeSerialize(args),
timestamp: Date.now(),
}, '*')
}
}
}
export function cleanupConsoleInterceptor() {
Object.assign(window.console, originalConsole)
}Original references are saved at module scope (before any patching). cleanupConsoleInterceptor restores them. Both are called from Creative.vue's mount/unmount lifecycle.
Elements Tab
DOM tree (DomTreeNode.vue)
Reads directly from iframe.contentDocument. Renders in Chrome DevTools-like format:
> <div .group-block .gp1> gp1
> <div .gp-inner>
> <div .t1-wrap> t1
v <div .text-block .t1-1>
"Hello world"
> <div .b1-wrap> b1
> <section .video-controls>- Tags in purple, classes in blue, text in gray, shortcode badge in blue (right-aligned)
- Collapsible nodes
- Hidden elements (
display: none/visibility: hidden) shown dimmed with "hidden" marker - Max tree depth: 20 levels (safety cap, creatives rarely exceed 10)
- Text content truncated to 80 characters
Shortcode and block mapping
Extract shortcode from element class names using the same patterns as extractShortcode() in devtoolsBridge.ts. Map shortcode back to blockName via the known prefix patterns (t=text, b=button, g=graphic, gp=group, etc.). This mapping runs in AF using the class names read directly from DOM elements.
Click-to-inspect
- "Inspect mode" toggle button (cursor icon, like Chrome)
- When active, clicks in the iframe are intercepted by a capture-phase listener added from AF
- Clicked element is identified, tree scrolls to and highlights the matching node
- Element info panel updates with selected element's data
Hover-highlight
- Hover on tree node in AF: AF directly creates/positions an overlay
<div>inside the iframe document - Overlay: semi-transparent blue (
rgba(66, 133, 244, 0.3)) withpointer-events: none,position: fixed,z-index: 2147483647(max int, above all creative content) - Shows dimensions label (e.g. "320 x 50") above or below the overlay
- Uses
element.getBoundingClientRect()for positioning - Mouse leave removes the overlay element
Element info panel (below tree)
For selected element:
- Tag, id, classes
- Block mapping: "Owned by textProperties-1 (t1)" - clicking dispatches
setSelectedBlockPathin Vuex to select the block in the builder sidebar - Dimensions: width x height, position (top, left) via
getBoundingClientRect() - Key styles: display, position, z-index, opacity, overflow
Console Tab
Output area
- Captured
console.log,.warn,.error,.infofrom iframe via postMessage - Each line shows: level icon (color-coded), timestamp, serialized arguments
- Objects/arrays are expandable (reuse TreeNode component)
- Auto-scroll to bottom, "Clear" button
Console interception lifecycle
The creative rebuilds on every block edit. After each rebuild:
- AF receives
preview-iframe-readymessage - CE's new Creative.vue instance calls
initConsoleInterceptor()on mount - Previous instance already called
cleanupConsoleInterceptor()on unmount - Console output continues uninterrupted in the inspector
Argument serialization
safeSerialize() handles:
- Primitives: pass through
- Objects/arrays:
JSON.stringifywith circular reference detection (replace with"[Circular]") - DOM nodes: serialize as CSS selector string (e.g.
<div.t1-wrap>) - Functions: serialize as
"function functionName() { ... }" - Errors: serialize message + stack
- Max depth: 5 levels for nested objects
- Max string length: 1000 characters
Input field
- Text input at bottom with
>prompt - Enter to execute, Shift+Enter for multiline
- Command history with up/down arrows (stored in component data, up to 50 entries)
- Eval via
iframe.contentWindow.eval(expression)(direct, same-origin) - Gated behind devMode check
- Results shown in output (green for return values, red for errors)
- Access to
DataStore,document, full iframe context
Styles Tab
For selected element, computed styles via iframe.contentWindow.getComputedStyle(element), grouped in collapsible sections:
| Section | Properties |
|---|---|
| Layout | display, position, flex-direction, flex-wrap, align-items, justify-content |
| Box Model | width, height, margin-, padding-, border-* |
| Typography | font-family, font-size, font-weight, color, line-height, text-align |
| Visual | background-color, background-image, opacity, z-index, overflow, box-shadow |
| Transform | transform, transition, animation |
- Filter out common defaults to reduce noise:
opacity: 1,z-index: auto,transform: none,overflow: visible,position: static,display: block(for div),display: inline(for span) - Color values show color swatches (reuse from TreeNode pattern)
Box model diagram
Simple nested-boxes visualization (like Chrome DevTools):
- Shows margin / border / padding / content with pixel values
- Color-coded layers (orange margin, green border, yellow padding, blue content)
- Positioned at top of Styles tab
- Values read directly via
getComputedStyle()
Integration
DevTools.vue
Add 5th tool with all three required integration points:
- Entry in
SWITCHABLE_TOOLS:
ts
{ key: 'domInspector', label: 'DOM Inspector' }- Entry in
toolsarray:
ts
{
text: this.$t('devTools.domInspector'),
disabled: () => false,
onClick: () => this.switchTool('domInspector'),
}- Conditional render in template:
html
<DomInspector
v-if="activeTool === 'domInspector'"
@close="activeTool = ''"
/>CE Creative.vue
Add #include block for console interceptor:
ts
// #include when devTools exists
import { initConsoleInterceptor, cleanupConsoleInterceptor } from '@/devtools/consoleInterceptor'
// mounted: initConsoleInterceptor()
// beforeUnmount: cleanupConsoleInterceptor()
// #endTab bar in DomInspector.vue
Three tabs rendered inside the DevToolPanel content slot. Simple tab bar with active state, no external dependency. Tabs: Elements (default), Console, Styles.
Existing infrastructure reused
- DevToolPanel: drag, resize, pop-out, fullscreen, tool switching
- TreeNode: expandable objects in console output
- JSONSearchBar pattern: search DOM nodes by tag/class/text
preview-iframe-readymessage: detect creative rebuilds for re-initializationextractShortcode()pattern from devtoolsBridge: map DOM classes to block shortcodes
Design principles
- Cavai-aware, not generic - map DOM to blocks, show shortcodes, link to config
- Direct DOM access - leverage same-origin iframe for simplicity and performance
- Minimal CE footprint - only console interception in CE, everything else in AF
- Runtime-gated - all devtools code gated behind
DataStore.devMode.value, not preprocessor - Rebuild-resilient - re-attach observers and listeners after each creative rebuild via
preview-iframe-ready - Familiar - Chrome DevTools visual language (colors, layout, interactions)
- Pop-out friendly - works in panel, pop-out, and fullscreen equally well