Appearance
Live CSS Preview During Scrub — Design Spec
Goal
Provide instant visual feedback in the preview iframe when scrubbing property values, without full engine rebuild. The preview updates reactively during drag; one full rebuild fires on mouseup to sync everything.
Architecture
Leverage the Creative Engine's existing Vue 3 reactivity. During scrub, update DataStore.creativeSettings.creativeBlocks directly in the iframe — Vue's reactive pipeline automatically recomputes the affected block's styles computed property, which triggers a watcher that updates the block's <style> tag. No custom CSS computation needed.
Data Flow
During scrub drag
InputField/OptionRow scrub
→ Section component → Vuex mutation → creativeBlocks changes
→ LocalBuildPreview watcher detects scrubbing-active class
→ sendBlockUpdate(): get changed block from Vuex
→ Dispatch CustomEvent('creative-block-update') to iframe
→ preview-iframe.ts receives event
→ Update DataStore.creativeSettings.creativeBlocks[blockName]
→ Vue 3 deep reactivity:
styles computed re-evaluates
→ watcher fires
→ object2Css(styles)
→ styleElement.innerHTML = css
→ Preview updates visually (no blinking)On scrub end (mouseup)
MutationObserver detects scrubbing-active class removed
→ rebuildPending = true → localBuildCreative()
→ Full rebuild (unmount + buildCreative + mount) syncs everythingChanges Per File
Creative Engine
src/entry.ts — Export DataStore:
ts
export { DataStore } from './services/dataStore'No other engine changes needed. All CSS mapping logic stays where it is.
Application-Frontend
src/preview-iframe.ts — New event listener for incremental block updates:
ts
import { DataStore } from '@cavai/creative-engine/src/services/dataStore'
import { set } from 'lodash-es'
document.addEventListener('creative-block-update', (e) => {
if (!creative) return // Ignore if creative not mounted
const { blockPath, blockData } = e.detail
const blocks = DataStore.creativeSettings.creativeBlocks
if (blockPath.includes('.')) {
// Sub-block (e.g., formProperties.formInput-1)
set(blocks, blockPath, blockData)
} else {
Object.assign(blocks[blockPath], blockData)
}
})src/pages/.../LocalBuildPreview.vue — Send block update during scrub:
ts
// In watcher, when scrubbing-active:
if (document.documentElement.classList.contains('scrubbing-active')) {
this.rebuildPending = true
this.sendBlockUpdate(value)
return
}
// New method:
sendBlockUpdate(blocks) {
const blockPath = this.$store.state.blocks.selectedBlockPath
if (!blockPath) return
const iframe = this.$refs.creativePreview
if (!iframe?.contentDocument) return
const block = get(blocks, blockPath)
if (!block) return
iframe.contentDocument.dispatchEvent(new CustomEvent('creative-block-update', {
detail: { blockPath, blockData: JSON.parse(JSON.stringify(block)) }
}))
}Why This Works
DataStore.creativeSettingsis created with Vue 3reactive()— deep reactivity- Block components read from DataStore via provide/inject + computed properties
StyleAndClassNameGenerationMixinhas a deep watcher onstylesthat regenerates CSS- The engine already knows which CSS property goes on which element (wrap/block/inner-wrap)
- We just update the data — Vue handles everything else
Edge Cases
- Sub-blocks (dotted paths like
formProperties.formInput-1): Use lodashset()for nested assignment - Creative not mounted: Guard with
if (!creative) returnin event handler - Multiple properties changing: Vue batches reactive updates in the same microtask — no extra cost
- Non-CSS properties (delay, duration, iterations): DataStore update is harmless — no visual CSS change, full rebuild on mouseup syncs animation state
What We Don't Change
- Engine-internal CSS pipeline (getStyleProps, StyleComposer, object2Css, StyleTreeParser)
- Existing
creative-data-updateevent (still used for full rebuild) - useScrubInput / InputField scrub mechanics
- MutationObserver rebuild-on-scrub-end logic (already committed)
Key Files
| File | Repo | Change |
|---|---|---|
src/entry.ts | Creative-Engine | Add DataStore export |
src/preview-iframe.ts | Application-Frontend | Add creative-block-update event handler |
src/pages/.../LocalBuildPreview.vue | Application-Frontend | Add sendBlockUpdate() method, wire into watcher |