Appearance
Animation System V2 — Progress & Architecture
Status: Multi-target triggers implemented, UX polish ongoing
Branch: add-animation-system (all 3 repos — was add-slider-v2, moved to dedicated branch) Issue: Cavai/Application-Frontend#1804 PRs: AF #1806, CE #706, Composer PR neededDate started: March 2026
What was built
A composable, effect-based animation system for ALL block types. Replaces the never-shipped V1 preset-based system.
Core concept
Instead of fixed presets ("fadeIn", "bounce"), users compose animations from individual effects (fade, slideX, slideY, scale, rotate, blur, skew). Each effect has multi-step keyframes ({percent, value} stops). Effects combine freely — e.g., fade + slideY + scale = fade-slide-scale-up.
Presets still exist as quick-fill shortcuts that populate the effects array. Users can tweak values after loading a preset.
Key design decisions
- No backward compatibility — V1 was never live, so types/defaults were replaced in-place
- Shared AnimationConfigCard — one component used by all 8 block configs (reduced ~538 lines to ~138)
- Multi-step keyframes — not just from→to, but arbitrary percent stops (e.g., bounce: 0%→60%→80%→100%)
- Dynamic CSS @keyframes — generated at runtime by merging all effects at each unique percent stop
- State-aware — animations pause during hover/active/focus states on stateful blocks (Button, Form)
- Flow-triggered — operators in CavaiFlow can trigger animations on specific blocks (like OperatorStyles pattern)
- Skew added as effect type per user request
Architecture
Data model
typescript
// types.ts
type AnimationEffectType = 'fade' | 'slideX' | 'slideY' | 'scale' | 'rotate' | 'blur' | 'skew'
type AnimationKeyframe = { percent: number; value: number }
type AnimationEffect = { type: AnimationEffectType; keyframes: AnimationKeyframe[] }
type AnimationTrigger = 'load' | 'hover' | 'click' | 'scroll'
type AnimationDirection = 'normal' | 'reverse' | 'alternate' | 'alternate-reverse'
type AnimationConfig = {
effects: AnimationEffect[]
trigger: AnimationTrigger
duration: number; delay: number; loop: boolean; iterationCount: number
direction: AnimationDirection; easing: string
}AnimationConfig lives on BlockBase.animation — inherited by all block types.
Keyframe generation (CE — helpers.ts)
getAllPercentStops(effects)— collects unique percent stops across all effectsinterpolateEffect(keyframes, percent)— linearly interpolates between stopsgetEffectCSS(effect, percent)— maps effect type to CSS property (opacity, transform, filter)mergeEffectsAtPercent(effects, percent)— merges all effects, combines transformsbuildKeyframes(effects)— generates CSS@keyframesobject from all percent stops
AnimationMixin (CE)
Provides to every block component:
animationConfig— validated config fromblock.animationshouldPlayAnimation— trigger logic (load/scroll/click/hover)animationStyleProps— CSS animation stringanimationKeyframeRule— dynamic @keyframes for StyleTreeParseranimationHoverStyle— hover-triggered animationanimationPauseStyles— pauses animation during state transitionshandleFlowAnimation()— receives flow-triggered animation events- Trigger setup: IntersectionObserver for scroll, click handler, etc.
UI Components (AF)
| Component | Purpose |
|---|---|
AnimationEffectRow | Single effect: type dropdown + keyframe rows (percent% → value) |
AnimationEffectList | List of effects + "Add effect" button + custom preset dropdown with hover preview |
AnimationPreviewBox | Inline animation preview with replay + loop toggle, uses actual timing settings |
BezierEasingEditor | SVG cubic-bezier curve editor with dynamic viewBox, CTM-based drag, copy button |
AnimationConfigCard | Complete animation card shared by all 8 configs |
AnimationTriggerSection | Trigger selector with excludeTriggers prop |
AnimationDirectionSection | Direction selector |
OperatorAnimations | Flow-triggered animation panel on operators |
animationPreviewHelpers.ts | Shared buildPreviewCSS() — generates CSS @keyframes from effects array |
Vuex additions
blocks.animationClipboard— state for copy/paste between blockssetAnimationClipboardmutationhasAnimationClipboard/animationClipboardgetterscopyAnimation()/pasteAnimation()methods in configurationLogic mixin
Files changed
Application-Frontend (24 files, +1405 lines)
New files (8):
Configuration/components/AnimationEffectRow.vueConfiguration/components/AnimationEffectList.vueConfiguration/components/AnimationPreviewBox.vueConfiguration/components/BezierEasingEditor.vueConfiguration/components/AnimationConfigCard.vueConfiguration/components/AnimationTriggerSection.vue(existed as V1, rewritten)Configuration/components/animationPreviewHelpers.tsCavaiFlow/flowactors/OperatorAnimations.vue
Modified files (18):
Blocks/data/types.ts— new effect-based typesBlocks/data/defaults.ts—animationDefaults(),EFFECT_UNITS,EFFECT_DEFAULTS,ANIMATION_PRESETSBlocks/utils.ts—sectionSettingsentries for new animation sectionsConfiguration/mixins/configurationLogic.ts— animation clipboard, copy/pasteConfiguration/configs/*Configuration.vue(×8) — addedAnimationConfigCardConfiguration/components/ConversationAnimationSection.vue— renamed to avoid i18n collisionassets/i18n/en.js— effect type labels, UI stringsstore/modules/blocks.ts— animation clipboard stateCavaiFlow/CavaiFlow.vue— animation panel positioning/show/hideCavaiFlow/flowactors/OperatorBase.vue— animation icon + panel trigger
Deleted files (1):
Configuration/components/AnimationPresetSection.vue— replaced by AnimationConfigCard
Creative-Engine (15 files, +380 lines)
New files (2):
styles/animations/helpers.ts— dynamic keyframe generationmixins/AnimationMixin.ts— animation logic mixin
Modified files (13):
interfaces/jsonTypes/payload-v2/index.ts— effect-based typesinterfaces/operator/OperatorPropertiesInterface.ts—animationTriggerfieldinterfaces/components/FlowComponent/FlowComponentInterface.ts— addedanimationTriggerfield for flow pipelinelogic-system/processors/conversationFlow.ts—emitAnimationTriggers()method, emitsanimation:flowevents via DataStore.emitterstyles/animations/index.ts— new exportscomponents/creative/VisualElements/CreativeButtonBlock.vue— pause stylescomponents/creative/VisualElements/CreativeTextBlock.vue— mixincomponents/creative/VisualElements/CreativeGraphicBlock.vue— mixincomponents/creative/VisualElements/CreativeHtmlBlock.vue— mixincomponents/creative/CreativeVideoBlock/CreativeVideoBlock.vue— mixincomponents/creative/CreativeConversationBlock/CreativeConversationBlock.vue— mixincomponents/creative/CreativeFormBlock/CreativeFormBlock.vue— mixincomponents/creative/CreativeARBlock/CreativeARBlock.vue— mixin
Creative-Composer (3 files, +15 lines)
Modified files (3):
src/remapper/remapData.ts— parsesbody.animationTrigger?.valueJSON into FlowComponentsrc/remapper/newTypes.ts— addedanimationTriggertoFlowComponentBaseinterfacesrc/remapper/jsonTypes.ts— addedanimationTrigger?: { value: string }to OperatorProperties body
Commits (AF, animation-only, chronological)
953731aaAdd AnimationConfig types and add animation to BlockBase816da930Add animation defaults to all block typesba08791fAdd AnimationPresetSection, AnimationTriggerSection, AnimationDirectionSection3edd4c39Add animation config Card to all block configurationsde674c50Add animation system i18n translationsce173d25Fix duplicate animation i18n key by renaming conversation animation keyef6f3514Add animation section settings to fix override lookup crash1c79572fReplace native select with InputSelect, use animConfig computedd210c499Add full-width to direction and trigger option lists5f5199efReplace preset-based AnimationConfig with composable effect-based types and defaults42fa5749Add AnimationEffectRow, AnimationEffectList, BezierEasingEditor, AnimationConfigCarddfc88172Add animation clipboard, copy/paste, exclude-triggers, V2 i18ne4b9987cReplace inline animation sections with AnimationConfigCard in all 8 configsa19d1fb2Remove unused AnimationPresetSectione6a73a22Add flow-triggered animation panel to flow operatorsd24b9e67Fix CIcon import path in animation componentsb4211c1aFix BezierEasingEditor section settings key
Commits (CE, animation-only, chronological)
- Various — AnimationMixin, helpers.ts, payload-v2 types, block integrations
79c5b157Add flow-triggered animation support (operator interface + mixin)4fe34f2fPause animation during hover/active states on button blockfc94d149Fix Vue 3 compatibility: remove $root.$on/$off, use beforeUnmount7ecd2ebdFix hover and click animation triggers for all block types2e1573cfWire flow-triggered animations from conversationFlow to visual blocks
Commits (Composer, animation-only)
1d0e8f0Add animationTrigger field to flow component remapping
Bugs fixed during testing
| Bug | Root cause | Fix |
|---|---|---|
| CIcon import crash | Wrong path @/components/common/Icon/CIcon.vue | Changed to @/components/common/CIcon.vue in 3 files |
| BezierEasingEditor crash | getSectionSettings() strips "Section" from name, key was "BezierEasing" but needed "BezierEasingEditor" | Renamed key in sectionSettings |
| CE crash on button add | Vue 3 has no $root.$on/$off, and beforeDestroy doesn't exist | Removed event bus calls, changed to beforeUnmount |
| CE build export error | Build stale after Vue 3 fix | Rebuilt CE |
Bugs fixed during manual testing (session 2)
| Bug | Root cause | Fix |
|---|---|---|
| OperatorBase animation icon invisible | "play" not in CIcon library | Changed to "video-player" |
| Effect buttons hidden when disabled | v-if="!disabled" removed elements | Changed to :disabled prop |
| Add effect / presets do nothing | inputLocked passed to AnimationEffectList | Removed disabled binding from AnimationConfigCard |
| Bezier drag doesn't work | SVG viewBox too small, events not captured outside box | Extended viewBox to -5 -55 110 210, moved mouse events to wrapper div |
| Bezier only shows on Bounce | Only cubic-bezier(...) values show SVG | Added "Custom" preset that starts as cubic-bezier(0.25,0.1,0.25,1) |
| Flow trigger opens branding toolbar | Missing showAnimationsEditor guards in CavaiFlow | Added to all 6 guard locations + pan-close |
| Unit shown at dropdown, not at value | unit span was in effect-header | Moved unit to each keyframe row after value |
| Trash button barely visible | Low opacity (0.5), no border | Added border, better hover state with red highlight |
UX polish (sessions 4–5)
Custom preset dropdown with hover preview
Replaced InputSelect for preset selection with a custom dropdown component in AnimationEffectList:
- Hover-based preview — 60px preview area at top of dropdown shows a looping animation of the hovered preset (18px blue dot)
- v-click-outside — closes dropdown when clicking elsewhere
- Modified indicator — orange dot appears when effects have been manually tweaked from a preset
- Uses
buildPreviewCSS()from sharedanimationPreviewHelpers.tsto generate inline@keyframes - Preview uses
<component :is="'style'">pattern for dynamic CSS injection
AnimationPreviewBox
Reusable preview widget used both in the effect list and the preset dropdown:
- Replay button — SVG refresh icon, replays animation once
- Loop toggle — toggles infinite looping (button turns blue when active)
- Timing-aware — receives
duration,easing,delay,direction,iterationCountfrom AnimationConfigCard and uses actual animation settings in preview - Auto-replay — triggers replay when any timing prop or effects change
- Overflow clipping —
overflow: hiddenon.preview-stageclips translateY/slideX animations - External loop mode (for dropdown) uses fixed 1000ms/ease/infinite
BezierEasingEditor improvements
- Dynamic viewBox — shows only 0-1 range by default, expands automatically when control points overshoot. Computed from actual bezier Y values:
svgMinY = 100 - max(1, y1, y2) * 100 svgMaxY = 100 - min(0, y1, y2) * 100 - SVG CTM-based drag — uses
getScreenCTM().inverse()for pixel-to-SVG coordinate conversion. Always accurate regardless of viewBox changes (no feedback loop, no snapshot needed) - Document-level drag —
addEventListener('mousemove/mouseup')on document for dragging beyond SVG bounds - Copy-to-clipboard — inline SVG copy icon → green checkmark animation on click
- Full-width canvas — removed max-width: 240px, height 150px (compact)
- Y clamped to -1..2 — CSS spec allows overshoot, but clamped for practicality
Sensible default keyframes per effect type
Added EFFECT_DEFAULTS map in defaults.ts:
| Type | Default keyframes | Rationale |
|---|---|---|
| fade | 0 → 1 | invisible → visible |
| slideX | 30px → 0 | slide in from right |
| slideY | 30px → 0 | slide in from below |
| scale | 0.5 → 1 | half → full size |
| rotate | 0 → 360deg | full rotation |
| blur | 10px → 0 | blurry → sharp |
| skew | 15deg → 0 | skewed → straight |
- When adding a new effect, it starts with the correct defaults for its type
- When changing effect type in dropdown, keyframes reset to the new type's defaults
- Previously all types defaulted to 0 → 1 (meaningless for slideX, rotate, etc.)
New files (session 4–5)
Configuration/components/animationPreviewHelpers.ts— sharedbuildPreviewCSS()and keyframe interpolation used by AnimationPreviewBox and AnimationEffectList dropdown
Additional commits (sessions 4–5)
495e6ce0Add animation preset preview box and fix easing button wrapping128f5e06Fix custom bezier editor not visible by using additional-content slot0bfa3291Fix preset persistence, panel click propagation, add Self target8801d7f0Add 12 new animation presets with multi-effect combinationsf0952343Redesign animation preset and easing UI237b2588Fix percent sign overlapping input in keyframe row0073293aFix bezier SVG white space and move keyframe add button75f04d10Restyle animation panel to light theme matching builder UI
Additional commits (session 6 — flow wiring + UX fixes)
b9fb9e2bPolish animation preview box, bezier editor, and effect list UI4d69216dFix animation editor panel positioning in flow view9463784dFix animation trigger panel UX issues in flow view2972e064Fix animation panel positioning, scroll, click-outside, and icon size
Additional commits (session 7 — UX bug fixes + trigger types)
3184fc4aFix animation panel scroll, click-outside, and add flow trigger types (AF)b34d4eb1Add flow animation trigger types — appear, click, hover (CE)
Bugs fixed (sessions 4–5)
| Bug | Root cause | Fix |
|---|---|---|
| BezierEasingEditor custom canvas not visible | OptionRow #additional-content slot + Vue 2 $slots reactivity | Moved canvas outside OptionRow as sibling |
| Easing buttons not right-aligned | OptionList max-width: 240px but items left-aligned | Added justify-content: flex-end |
| Drag stops at canvas edge | @mouseleave="stopDrag" fired | Document-level addEventListener('mousemove/mouseup') |
| Copy icon looked like download | Used CIcon "export" icon | Inline SVG copy icon with check animation |
| Control points lost outside canvas | overflow: visible leaked into whole panel | Dynamic viewBox covering actual Y range |
| Bezier drag jumpy on viewBox resize | Snapshot viewBox at drag start → stale mapping | SVG getScreenCTM().inverse() for live mapping |
| Preset dropdown height changes on hover | min-height: 52px grew when preview rendered | Fixed height: 60px inline preview area |
| Preview animation alternate direction | Animation used alternate | Changed to regular infinite restart |
| Preview too fast in dropdown | 600ms loop | Increased to 1000ms for dropdown loop |
| Dropdown preview overflows on slide effects | .preset-menu-preview no overflow clip | Added overflow: hidden |
| Preview ignores timing settings | Hardcoded 600ms/ease | Props from AnimationConfigCard: duration, easing, delay, direction, iterationCount |
| Default keyframes meaningless for non-fade | All types had 0→1 | EFFECT_DEFAULTS map with sensible values per type; type change resets keyframes |
Known remaining work
Must do — PRs & housekeeping
- Create Composer PR — branch
add-animation-systemis pushed with commits but no PR exists yet - Remove debug console.logs — CE conversationFlow.ts still has startup/progress debug logs
- Remove on-scroll trigger from MVP —
scrollis in theAnimationTriggertype and the UI exposes it, but it can't work in nested iframes (no host page scroll access). Remove from trigger options for now. The architecture (IntersectionObserver setup in AnimationMixin) stays in place so it can be re-enabled later with Advantage's scroll progress API
Needs verification
- Copy/paste animation between blocks — Vuex clipboard +
copyAnimation()/pasteAnimation()in configurationLogic are implemented but untested in browser. Needs manual verification: copy from one block, paste to another, confirm config transfers correctly - Multi-target UI — collapsible cards, add/remove targets, per-target effects/duration/easing
- Vuetify red underline — user reported 3 times across sessions, each fix was more aggressive. Current approach: unscoped CSS in CavaiFlow.vue with
content: none !importanton::before/::afterpseudo-elements - Session 7 fixes — scroll inside panel, click-outside, dropdown behavior
- Session 6 fixes — custom animation trigger icon, clipboard error handling, Cmd+Shift+S guard
Bugs fixed during manual testing (session 3)
| Bug | Root cause | Fix |
|---|---|---|
| CIcon crash in AnimationEffectRow, AnimationConfigCard, BezierEasingEditor, OperatorAnimations | Used name prop but CIcon only has icon prop; name went to $attrs, causing $slots.default[0] crash | Changed all name="..." to icon="..." |
| i18n "visuals.duration" not a string | Key visuals.duration is nested object, not a leaf | Changed to visuals.transitionSettings.duration |
visualElementBlocks.map is not a function | Vuex getter returns object (pickBy), not array | Added Object.values() wrap |
| Dark/light UI mismatch in flow panel | Panel had #2d2d2d background with white inputs | Restyled to light theme (#fff) matching builder UI |
Preset revert button (from template system, March 2026)
The template system now uses a purple Revert button instead of the orange modified dot. The animation preset system should match this pattern. See todos/TemplateSystem/template-revert-button.md for full details.
Summary of changes needed:
- Replace orange
modified-dotinAnimationEffectList.vuewith a purple revert button - Add
selectedPreset: stringtoAnimationConfigtype and defaults — stores which preset was loaded - Revert button re-applies the stored preset with confirmation dialog
- Reuse the same
$echo-*color scheme and styling asTemplateButton.vue
Nice to have / later
- Flow animation end-to-end test (operator → block)
- Stagger/sequence for Block Groups (#1805)
- More presets (wiggle, flip, elastic, etc.)
- Create GitHub issues: text-shadow for text blocks, better gradient support
- Preview loop button: consider hiding entirely (instead of disabled) when config loop is on
- Keyframe UX: consider labels like "Start → End" instead of "0% → 100%"
Planned: Slide-enter trigger type (SliderV2 synergy)
Concept: A new animation trigger slide-enter that fires when a slider slide becomes the active slide.
Why it works already (partially): SliderV2 uses v-if="canShowSlide()" on slides, so child blocks (text, graphic, button, HTML) get mounted fresh when a slide enters the visible buffer. This means load-trigger animations already re-play on every slide entry — but this is accidental, not intentional.
What a dedicated trigger adds:
- Explicit intent: user chooses "animate on slide enter" vs "animate on page load"
- Per-slide animation overrides: slide 1 fades in, slide 2 slides in from left
- Differentiation from
loadtrigger (which fires once on mount, not on re-entry for non-slider blocks)
Implementation plan:
- Add
'slide-enter'toAnimationTriggertype (CEpayload-v2/index.ts) - Add to
AnimationTriggerSectionexcludeTriggers for non-slider blocks - In CE SliderV2, emit a
animation:slide-enterevent onactiveIndexchange with the slide's child block names - AnimationMixin listens for this event and triggers animation if trigger === 'slide-enter'
- AF: Add trigger option to AnimationConfigCard (only visible when block is child of slider)
Per-slide animation overrides: SliderV2 already has a per-slide override system where each slide can override ANY sub-block property via {...subBlock, ...slide[subBlock.blockName]}. Since animation lives on BlockBase, this already works — a slide override can include { textProperties_0: { animation: { effects: [...], trigger: 'slide-enter' } } } and it will merge correctly.
No code changes needed for the override mechanism itself — it's purely additive. The UI for configuring per-slide animation overrides would be part of the SliderV2 config (per-slide settings tab).
will-change optimization (implemented)
Dynamic will-change management in AnimationMixin via animationstart/animationend event listeners. Sets will-change: transform, opacity, filter when animation starts, resets to auto when it ends. Avoids permanent GPU memory cost while still getting hardware acceleration during animations.
Session 6 — Flow animation wiring + UX fixes
Flow animation pipeline (end-to-end, working)
The full pipeline across 3 repos is now wired and confirmed working:
OperatorAnimations panel (AF)
→ saves JSON to op.properties.body.animationTrigger.value
→ Creative-Composer remapData.ts parses JSON into FlowComponentInterface.animationTrigger
→ CE conversationFlow.ts calls emitAnimationTriggers() when components are added to flow
→ DataStore.emitter.emit('animation:flow', { targetBlock, effects, duration, easing })
→ AnimationMixin.handleFlowAnimation() on matching block receives event
→ Block plays the animationKey implementation details:
emitAnimationTriggers()is called from all 3 places in conversationFlow.ts where components enter the live flow: (1) flowQueue shift, (2) consent/textinput first component, (3) regular building blocks__self__target is resolved tocomp.name(the operator's own block name) before emitting- AnimationMixin registers listener in
mounted()and unregisters inbeforeUnmount()viaDataStore.emitter.on/off('animation:flow')
UX fixes attempted (session 6)
| Issue | Approach | Status |
|---|---|---|
| Panel enormous/full screen | CSS position: absolute + content-class attribute selector (matching styles-editor pattern) | Fixed |
| Panel overlaps operator | Positioned via animationsEditorY/X computed from operator position | Fixed |
| Animation trigger icon too small | Created new animationTrigger icon (play triangle) in CIcon.vue | Needs verification |
| Scroll blocked in flow | @wheel.stop on panel wrapper | Still broken |
| Scroll blocked inside panel | overflow-y: auto + max-height: 470px on .panel-content | Still broken |
| Click-outside doesn't close | v-click-outside with include for Vuetify overlays | Needs verification |
| Dropdowns close panel | Added .v-menu__content etc to include function | Needs verification |
| Clipboard error | try/catch around navigator.clipboard.writeText() | Needs verification |
| Cmd+Shift+S triggers save | !e.shiftKey guard in ChatbotBuilder.vue | Needs verification |
Pattern note: styles-editor vs animations-editor
The animations panel follows the same pattern as the existing styles-editor in CavaiFlow:
- Uses
content-classHTML attribute (notclass) for CSS targeting via[content-class*='...']selector position: absolute(not fixed) so it scrolls with the flow canvas- Wrapper div with computed
top/leftstyle from operator position v-click-outsidefor closing
The key difference is that animations panel has Vuetify dropdowns (InputSelect) inside it, which render overlay elements outside the panel DOM. This requires the include option on v-click-outside to prevent those overlays from triggering close.
Session 8 — Multi-target triggers, per-choice isolation, UX polish
Multi-target animation triggers (major feature)
Changed data model from single-target to multi-target. One operator can now trigger animations on multiple blocks simultaneously.
Old format (single target):
typescript
animationTrigger?: {
targetBlock: string
effects: any[]
duration: number
easing: string
triggerType?: 'appear' | 'click' | 'hover'
}New format (multi-target):
typescript
animationTriggers?: {
triggerType: 'appear' | 'click' | 'hover' // shared per operator
targets: {
targetBlock: string
effects: any[]
duration: number
easing: string
}[]
}Changes across repos:
- CE FlowComponentInterface.ts — new
animationTriggerstype - CE conversationFlow.ts —
emitAnimationTriggers()iteratestargetsarray, emits oneanimation:flowevent per target - CE Choice.vue —
doSelection(idx)iterates targets for click/hover triggers - Composer newTypes.ts — updated type
- Composer remapData.ts — parses new format, with legacy single-target migration
- AF OperatorAnimations.vue — complete UI redesign with collapsible target cards
UI design:
- Shared trigger type dropdown at top (appear/click/hover)
- Collapsible cards per target (chevron toggle)
- Each card: target block dropdown, effect list, duration, easing
- "+ Add target" dashed button
- Remove button per card (hidden when only 1 target)
- Card header shows resolved block display name
Per-choice trigger isolation (architecture redesign)
Problem: All choices triggered the same animation regardless of which was clicked.
Root cause: Original architecture had conversationFlow emit triggers when components entered the flow. But for click/hover triggers, the SOURCE component (e.g., Choice) needs to emit — not the flow processor.
Fix: Redesigned trigger architecture:
conversationFlow.emitAnimationTriggers()now skips click/hover triggersChoice.vue doSelection(idx)checksblockArray[idx].animationTriggersdirectly and emits per-target- Each choice button fires only its own operator's animation config
Animation restart fix
Problem: Sequential animations on same block (e.g., fade out → fade in) didn't restart.
Root cause: Same @keyframes name reused — browser doesn't restart identical animation.
Fix: Added flowAnimationCounter in AnimationMixin. Each trigger increments counter, generating unique keyframe name: cavai-flow-anim-{blockName}-{counter}.
Flow/block animation duration conflict
Problem: When a block already had a block-level animation and received a flow trigger, the block animation's duration was used instead of the flow trigger's duration.
Fix: animationStyleProps in AnimationMixin now returns early with ONLY flow animation styles when flowAnimationActive is true. Flow animation takes full priority.
UX polish
| Feature | Implementation |
|---|---|
| Pan-to-center accounts for panel height | panToOperatorAndShow() calculates targetTop = (containerHeight - totalHeight) / 2 where totalHeight = operator + panel |
| Caret centered on trigger icon | Changed left: 14px → left: 3.5px (centered on 17px-wide icon) |
| Panel flip with caret | detectPanelFlip() checks if panel overflows viewport bottom, passes flipped prop, CSS flips caret direction |
| Escape to close | Added to handleKeyDown in CavaiFlow.vue |
| Entrance animation | @keyframes panel-enter { from { opacity: 0; translateY(-4px) } to { opacity: 1; translateY(0) } } |
| Removed dead caret code | Removed caretAdjusted computed and BGCOMPONENTLIST import from OperatorStyles |
| Vuetify underline removal | Unscoped CSS in CavaiFlow.vue: .v-input__slot::before/::after { content: none !important } |
Commits (session 8)
AF:
7b51910fFix duplicate loop key in animation i18n2972e064Fix animation panel positioning, scroll, click-outside, and icon size9463784dFix animation trigger panel UX issues in flow view4d69216dFix animation editor panel positioning in flow viewb9fb9e2bPolish animation preview box, bezier editor, and effect list UI3ed0ced6Add multi-target animation trigger UI with collapsible cards
CE:
202cc86dAdd multi-target animation triggers support
Composer:
2b370a2Add multi-target animation triggers with legacy migration
Known issues (session 8)
| Issue | Status |
|---|---|
| Vuetify red underline under dropdowns | .operator-animations-panel targets ::before/::after with content:none, display:none, border:none (all !important), plus border-bottom:none on .v-input__slot. No error prop passed to InputSelect. |
| Debug console.logs in CE | ✅ Fixed — 5 debug logs removed from conversationFlow.ts (39f66293) |
| Composer PR | ✅ Created — Cavai/Creative-Composer#105 (draft) |
Session 9 — Verification & cleanup
Verified
Multi-target UI — OperatorAnimations.vue has: shared trigger type dropdown, collapsible target cards with chevron toggle, per-target effect list/duration/easing, add/remove target buttons, legacy single-target migration. All correct.
Vuetify red underline — CavaiFlow.vue unscoped CSS (lines 5212-5226) is comprehensive:
.operator-animations-panel .v-input__slot { border-bottom: none !important }::beforeand::afterpseudo-elements:content: none,display: none,border: none(all!important)- InputSelect doesn't pass
errorprop in OperatorAnimations, so no validation state triggering red
End-to-end pipeline — traced across all 3 repos:
- AF
OperatorAnimations.saveConfig()→ writes{ triggerType, targets: [...] }JSON - Composer
remapData.ts→ parses to FlowComponentInterface with legacy migration - CE
conversationFlow.emitAnimationTriggers()→ iterates targets, emits oneanimation:flowper target - CE
Choice.doSelection()→ handles click/hover triggers per choice - CE
AnimationMixin.handleFlowAnimation()→ receives events, filters by blockName - Data format consistent across all repos.
__self__resolution, defaults, and trigger type filtering all correct.
- AF
Commits (session 9)
CE:
39f66293Remove debug console.logs from conversationFlow
Known remaining work
| Item | Status |
|---|---|
| Manual browser testing on external site | Pending |
| Vuetify underline — visual confirmation in browser | Pending (CSS verified correct in code) |
Session 10 — Status review (2026-03-14)
Full status review of the branch before manual testing phase.
Summary
Code is feature-complete across all 3 repos (51 commits on AF). Cloudflare preview deploys successfully. No code work remains — the blocker is manual browser testing.
PRs
| Repo | PR | Status |
|---|---|---|
| Application-Frontend | #1806 | Draft |
| Creative-Engine | #706 | Open |
| Creative-Composer | #105 | Draft |
Must verify in browser before merge
| Item | Notes |
|---|---|
| Block-level animations (all 8 types) | Add effects, change timing, confirm animation plays in preview and on external site |
| Flow-triggered animations | Set animation on operator → confirm block animates when message appears |
| Per-choice isolation | Set different animations on different choice operators → confirm each choice triggers only its own |
| Multi-target triggers | One operator animating multiple blocks simultaneously |
| Copy/paste between blocks | Vuex clipboard implemented but never tested in browser |
Remove scroll trigger from MVP | Still in UI but can't work in nested iframes — needs to be hidden |
| Scroll inside flow panel | Reported "still broken" in sessions 6-7 |
| Click-outside closing | Including Vuetify overlay exclusions |
| Vuetify red underline on dropdowns | CSS fix written, needs visual confirmation |
| Bezier easing editor drag | CTM-based drag, dynamic viewBox |
| Preset dropdown hover preview | 60px preview area with looping animation |
Nice-to-have (post-merge)
- Flow animation end-to-end visual test
- Text-level animations (V3 concept)
Stagger Animation for Block Groups (2026-06-02)
Branch: theming-system-redesignStatus: Infrastructure done, UI paused for reconsideration Plan: Cavai-Documentation/src/DocumentationTexts/plans/2026-06-02-stagger-animation-redesign.md
What was built
Per-child AnimationConfig approach: stagger writes animation configs to each child block, making the group stagger a "shortcut" that populates children's animation fields.
Infrastructure (all implemented, on branch):
StaggerSettingstype:{ preset, duration, easing, staggerDelay, loop, iterations, reverse, excludedChildren }AnimationConfigadditions:staggerSource?: string,repeatDelay?: numberbuildStaggerChildConfigs()utility inutils.tsconfigurationLogic.ts:updateAndClearStaggerhelper, paste stripsstaggerSource/repeatDelay- 10 stagger presets in
defaults.ts(pulse, pop, heartbeat, elastic, bounce, nod, nudge, shake, wiggle, swing) - Engine
AnimationMixin:isStaggerControlledsuppresses CSS animation for stagger-assigned blocks - Engine
CreativeGroupBlock: reads fromstaggerSettings+ children's animation configs, WAAPI round-robin - i18n keys for stagger UI
Design decisions:
- Child groups excluded from stagger targets (only visual elements)
staggerSourceauto-cleared when user manually edits a child's animation- Flow animation takes priority over stagger suppression
- Engine calculates its own
repeatDelayfromStaggerSettings(not from child config) repeatDelaykept onAnimationConfigtype for conversation choice stagger (different system)
UI (removed by user):
Full stagger config Card was built (preset, duration, easing, delay, loop, iterations, child include/exclude list, reverse toggle, preview dots, clear button) but removed from BlockGroupConfiguration.vue. Only AnimationConfigCard remains. UI approach to be reconsidered.
What remains
- Decide on stagger UI approach (simpler? different location?)
- Document
blockGroupPropertiesandStaggerSettingsinblocks.md - Browser testing when UI is re-added
Design: Block-level vs. flow-level animation (override model)
Block-level animation is the default — it applies uniformly to every instance. For a conversation block, this means every message gets the same animation (e.g., fade-in on appear).
Flow-level animation is a per-operator override. When set on an operator, it takes full priority over the block-level animation for that specific message. This allows differentiation within a single conversation:
Block animation: fade-in (applies to all messages by default)
Operator 3: blur + slide-in (overrides fade-in for this message only)
Operator 7: scale + rotate (overrides fade-in for this message only)
All others: fade-in (block default)How it works:
AnimationMixin.handleFlowAnimation()setsflowAnimationActive = trueand returns early with flow animation styles, bypassing the block-level config entirely- The override is scoped to the specific message — other messages in the same block continue using the block-level animation
- Per-choice isolation: each choice button can have its own animation config, firing only when that specific choice is interacted with
Implications for trigger types:
- On appear: Block default plays for every message. Flow override replaces it for specific operators — ideal for highlighting key moments in the conversation
- On click / on hover: Only relevant at the flow level (per-choice isolation). Block-level click/hover animates the block container itself
Design: Extensibility — scroll triggers and Slider V2
On-scroll is excluded from MVP because ads typically run in nested iframes where we don't have access to the host page's scroll position. The architecture is designed for future extension:
- Scroll-triggered animations via Advantage — Advantage format has direct page access, making IntersectionObserver viable for scroll-based triggers
- Slide-enter animations for Slider V2 — trigger effects when a slide becomes the active slide (e.g., fade-in content on slide enter, stagger child blocks per slide transition)
Future idea: Text-level animations (V3?)
Block-level animations affect the whole block. Text-level animations would target inline text ranges — the same selections you can already bold/italic/strikethrough in the text editor.
Concept: If you can mark text as bold, you could also mark it as "write-on", "strikethrough-animate", "gradual-italic", etc. These would be text decorations that animate rather than being static.
Possible text animation types:
- Write-on / typewriter — characters appear one by one (classic typewriter effect)
- Strikethrough animation — line draws across text over time
- Gradual bold/italic — font-weight or style transitions in over duration
- Highlight sweep — background color sweeps across text like a marker
- Character stagger — each character fades/slides in with a small delay (like Framer Motion
staggerChildren) - Blur reveal — text starts blurred, sharpens character by character
- Color shift — text color transitions through a gradient over time
Block-specific animation types:
- Video block: animate-to-timestamp, auto-seek on scroll
- Form block: field-by-field reveal, submit button pulse
- Slider block: slide-count-driven animations on child elements
Scroll-driven variant: All of the above could be tied to scroll progress instead of time — e.g., text writes on as user scrolls, strikethrough progresses with scroll position. This connects to the broader scroll-driven animations concept (CSS animation-timeline: scroll()).
Architecture consideration: Text-level animations would need to integrate with the rich text editor (marks/decorations system) rather than the block-level AnimationConfig. Could potentially extend the existing inline formatting system (bold, italic, strikethrough marks) with animation-aware marks. Separate issue recommended — could share the same PR if scope stays reasonable, but likely its own feature.
Session 9 — Preview redesign, keyframes UX, duplicate prevention, block fixes
AnimationPreviewBox redesign — hero mode
Redesigned the animation preview as a full-width "hero" area at the top of AnimationConfigCard (before the effect list). New hero prop enables the larger display mode.
Hero mode styling:
- Dotted grid background (
$grey-95base,$grey-85dots viaradial-gradient) - 100px height, full width, 8px border-radius
- 28px blue preview block with rounded corners and box-shadow
- Replay button positioned absolute bottom-right with white background and subtle shadow
- Compact mode (no
heroprop) still used inside preset dropdown
Wiring: AnimationConfigCard renders AnimationPreviewBox with hero prop above AnimationEffectList. Preview receives all timing props (duration, easing, delay, direction, iterationCount).
Keyframes collapsible group header
Added a "Keyframes" group header in AnimationEffectList that wraps all effect rows:
- Expand/collapse toggle with chevron icon
- Summary text showing effect count ("3 effects")
keyframesCollapseddata controls visibility of effect rows and add button- i18n key:
visuals.animation.keyframes
Duplicate effect type prevention
Effects of the same type are now prevented (adding two opacity effects is meaningless — they'd overwrite each other in buildKeyframes()):
AnimationEffectRowreceivesusedTypesprop, filters dropdown to show only unused types (plus current type)AnimationEffectListcomputesusedTypesandallTypesUsed- "Add effect" button disabled when all 7 types are in use (visual:
opacity: 0.4,cursor: not-allowed) addEffect()picks the first unused type instead of always defaulting to 'fade'
FormBlock animation fix
Problem: AnimationMixin reads this.block?.animation but FormBlock doesn't use VisualElementMixin (which provides block as a prop via the block system).
Fix: Added block() computed to CreativeFormBlock.vue returning this.typedBlock.
ConversationBlock animation fix
Same issue and fix as FormBlock — added block() computed returning this.typedBlock.
SliderBlock animation wiring (CE)
Added AnimationMixin to CreativeSliderBlock.vue:
- Import and mixin registration
block()computed returningtypedBlockanimationStylePropsandanimationHoverStylespread into block stylesanimationKeyframeRulemerged into returned styles object
Animation tab disabled for Slider and Conversation
After testing, animation tabs were removed from both SliderConfiguration.vue and ConversationConfiguration.vue:
- Slider: Has hardcoded
opacity: 0on wrapper (line 529) and inner wrap (line 588) for tease/image-loading management. These override CSS animations — fade-in appears as an abrupt pop after the duration elapses. - Conversation: Own rendering flow (message-by-message with typing animation, scroll management) conflicts with block-level CSS animations. Styles are wired correctly but animation has no visible effect.
- Decision: Ship animation for blocks where it works cleanly (text, graphic, button, HTML, video, AR, form). Slider and conversation need deeper engine work to support block-level animations.
Files changed (session 9)
AF modified:
AnimationPreviewBox.vue— hero mode with dotted grid, repositioned replay buttonAnimationConfigCard.vue— hero preview at top, removed timing props from AnimationEffectListAnimationEffectList.vue— keyframes group header, duplicate prevention, removed embedded previewAnimationEffectRow.vue—usedTypesprop, computedeffectTypeItemsfilterOperatorAnimations.vue— removed stale timing props from AnimationEffectListen.js— addedkeyframesi18n key
CE modified:
CreativeFormBlock.vue— addedblock()computedCreativeConversationBlock.vue— addedblock()computedCreativeSliderBlock.vue— added AnimationMixin +block()computed + animation styles
Bugs fixed (session 9)
| Bug | Root cause | Fix |
|---|---|---|
$grey-97 doesn't exist | No such SCSS variable in color system | Used $grey-95 (lightest available) and $grey-85 for dots |
| Preview not visually different | Hero class not distinct enough from compact mode | Full redesign with dotted grid, larger block, rounded corners |
| Replay button flush against edge | right: 8px too tight | Changed to right: 16px |
| "Add effect" not visually disabled | No disabled CSS styles | Added opacity: 0.4, cursor: not-allowed, hover guard |
| FormBlock animation broken | this.block undefined — no VisualElementMixin | Added block() computed returning typedBlock |
| ConversationBlock animation broken | Same as FormBlock | Same fix |
CE build missing buildCreative | Built without VITE_LIBRARY_MODE=true | Rebuilt with env var |
Future idea: ShowHide animated transitions
Currently, the ShowHide operator uses DataStore.runtimeHidden with hard display: none toggling — blocks appear/disappear instantly.
Concept: Add animated transitions to ShowHide so blocks can fade out, slide away, etc. instead of popping in/out.
Implementation approach:
AF — ShowHide operator UI: Add animation type selector (fade, slideUp, slideDown, scale, etc.) and duration input to the ShowHide operator panel.
Composer → CE data: Extend
runtimeHiddenfrombooleanto:typescript{ hidden: boolean; animation?: string; duration?: number }Or keep
runtimeHiddenas boolean and add a separateruntimeHiddenAnimationfield.CE BlockMixin — animated hide/show:
- Show: Remove
display: none, apply enter animation, block is visible immediately - Hide: Play exit animation, listen for
animationend, THEN setdisplay: none - This prevents the current problem where
display: nonekills any running animation
- Show: Remove
Considerations:
- Must handle rapid show/hide toggles (cancel pending
animationendif toggled again before animation completes) - Could reuse the same
AnimationEffecttypes from the animation system for consistency - Layout shift: hiding a block may cause content reflow.
visibility: hidden+height: 0with animation might be better thandisplay: nonefor some layouts
- Must handle rapid show/hide toggles (cancel pending
Future idea: Cross-block animation triggers
Currently, cross-block animation is only possible via the flow (OperatorAnimations). A potential improvement would be letting blocks trigger animations on other blocks directly from their own click/hover events — without going through the flow.
Concept: When a block's animation trigger is set to click or hover, a target block dropdown appears. Selecting a target causes the source block's event to fire the animation on the target instead of on itself.
Implementation approach:
AF — AnimationConfigCard: Add
targetBlockfield toAnimationConfigtype and defaults. Show a target block dropdown (usingInputSelect) when trigger is click or hover. Populate with all animatable blocks except the current one, plus a "Self (this block)" default option.AF — configurationLogic: Add
animatableBlockOptionscomputed that filterscreativeBlocksagainstANIMATABLE_BLOCKSconstant, excluding the current block.CE — AnimationMixin: When
targetBlockis set,triggerAnimation()emitsanimation:flowevent to the target block instead of playing locally. For hover triggers with a target, use JSmouseenterevent (not CSS:hover). Reuses existing flow animation infrastructure — no new event system needed.
Why deferred: The flow already handles cross-block triggers with more flexibility (per-choice isolation, appear-trigger, multi-target from one operator). Adding it at block level too may cause confusion about which system to use. Revisit if users need simpler cross-block triggers without flow involvement.
Session 10 (2026-03-18)
Cross-block triggers — attempted and deferred:
- Fully implemented cross-block animation triggers in AF and CE
- Reverted after deciding flow-triggered approach is sufficient for now
- Documented as future idea above
Bug fix — click animation not re-triggering:
- Click-triggered animations only played once; subsequent clicks did nothing
- Root cause:
$nextTicktoggle ofanimationTriggered(false→true) didn't force a browser reflow, so the CSS animation wasn't restarted - Fix: Added
void this.$el?.offsetHeightbetween removing and re-adding the animation to force reflow
Session 11 (2026-03-20) — Change operator animation simplification
Problem: Change operators already have their own target selection via TargetOpSelector. The full OperatorAnimations panel (trigger type, target block selector, multi-target, "Add target" button) was redundant — change operators just need effects, duration, and easing.
Solution: Added isChangeOperator prop to OperatorAnimations panel. When true:
Hidden elements:
- Trigger type dropdown (change operators don't need appear/click/hover — they trigger on the change event)
- Target block selector (TargetOpSelector already handles target selection)
- Target card header (collapse/expand chevron, card title, remove button) — since there's always exactly 1 target
- "Add target" button — only 1 implicit target (
__self__)
Auto-set target:
loadConfig()forcestargetBlock: '__self__'for change operators, ensuring the animation applies to the change operator's own target blockInverted color palette: Added
.invertclass with dark theme matching other change operator styling:- Background:
$global-white-invert-alt - Border:
rgba($bravo-65, 0.4) - Text:
rgba(#fff, 0.9) - Labels:
rgba(#fff, 0.6) - Caret color matches panel background
- Background:
CavaiFlow integration: Added
animationsEditorIsChangecomputed that checks ifanimationsEditorOpKeystarts with'change', passed as:is-change-operatorprop
Result: Change operator animation panel shows only: effects list, duration, easing — compact and clean with inverted dark palette matching the operator's visual identity.
Files changed:
AF OperatorAnimations.vue—isChangeOperatorprop, conditional rendering, inverted stylesAF CavaiFlow.vue—animationsEditorIsChangecomputed, prop binding