Skip to content

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

  1. No backward compatibility — V1 was never live, so types/defaults were replaced in-place
  2. Shared AnimationConfigCard — one component used by all 8 block configs (reduced ~538 lines to ~138)
  3. Multi-step keyframes — not just from→to, but arbitrary percent stops (e.g., bounce: 0%→60%→80%→100%)
  4. Dynamic CSS @keyframes — generated at runtime by merging all effects at each unique percent stop
  5. State-aware — animations pause during hover/active/focus states on stateful blocks (Button, Form)
  6. Flow-triggered — operators in CavaiFlow can trigger animations on specific blocks (like OperatorStyles pattern)
  7. 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)

  1. getAllPercentStops(effects) — collects unique percent stops across all effects
  2. interpolateEffect(keyframes, percent) — linearly interpolates between stops
  3. getEffectCSS(effect, percent) — maps effect type to CSS property (opacity, transform, filter)
  4. mergeEffectsAtPercent(effects, percent) — merges all effects, combines transforms
  5. buildKeyframes(effects) — generates CSS @keyframes object from all percent stops

AnimationMixin (CE)

Provides to every block component:

  • animationConfig — validated config from block.animation
  • shouldPlayAnimation — trigger logic (load/scroll/click/hover)
  • animationStyleProps — CSS animation string
  • animationKeyframeRule — dynamic @keyframes for StyleTreeParser
  • animationHoverStyle — hover-triggered animation
  • animationPauseStyles — pauses animation during state transitions
  • handleFlowAnimation() — receives flow-triggered animation events
  • Trigger setup: IntersectionObserver for scroll, click handler, etc.

UI Components (AF)

ComponentPurpose
AnimationEffectRowSingle effect: type dropdown + keyframe rows (percent% → value)
AnimationEffectListList of effects + "Add effect" button + custom preset dropdown with hover preview
AnimationPreviewBoxInline animation preview with replay + loop toggle, uses actual timing settings
BezierEasingEditorSVG cubic-bezier curve editor with dynamic viewBox, CTM-based drag, copy button
AnimationConfigCardComplete animation card shared by all 8 configs
AnimationTriggerSectionTrigger selector with excludeTriggers prop
AnimationDirectionSectionDirection selector
OperatorAnimationsFlow-triggered animation panel on operators
animationPreviewHelpers.tsShared buildPreviewCSS() — generates CSS @keyframes from effects array

Vuex additions

  • blocks.animationClipboard — state for copy/paste between blocks
  • setAnimationClipboard mutation
  • hasAnimationClipboard / animationClipboard getters
  • copyAnimation() / pasteAnimation() methods in configurationLogic mixin

Files changed

Application-Frontend (24 files, +1405 lines)

New files (8):

  • Configuration/components/AnimationEffectRow.vue
  • Configuration/components/AnimationEffectList.vue
  • Configuration/components/AnimationPreviewBox.vue
  • Configuration/components/BezierEasingEditor.vue
  • Configuration/components/AnimationConfigCard.vue
  • Configuration/components/AnimationTriggerSection.vue (existed as V1, rewritten)
  • Configuration/components/animationPreviewHelpers.ts
  • CavaiFlow/flowactors/OperatorAnimations.vue

Modified files (18):

  • Blocks/data/types.ts — new effect-based types
  • Blocks/data/defaults.tsanimationDefaults(), EFFECT_UNITS, EFFECT_DEFAULTS, ANIMATION_PRESETS
  • Blocks/utils.tssectionSettings entries for new animation sections
  • Configuration/mixins/configurationLogic.ts — animation clipboard, copy/paste
  • Configuration/configs/*Configuration.vue (×8) — added AnimationConfigCard
  • Configuration/components/ConversationAnimationSection.vue — renamed to avoid i18n collision
  • assets/i18n/en.js — effect type labels, UI strings
  • store/modules/blocks.ts — animation clipboard state
  • CavaiFlow/CavaiFlow.vue — animation panel positioning/show/hide
  • CavaiFlow/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 generation
  • mixins/AnimationMixin.ts — animation logic mixin

Modified files (13):

  • interfaces/jsonTypes/payload-v2/index.ts — effect-based types
  • interfaces/operator/OperatorPropertiesInterface.tsanimationTrigger field
  • interfaces/components/FlowComponent/FlowComponentInterface.ts — added animationTrigger field for flow pipeline
  • logic-system/processors/conversationFlow.tsemitAnimationTriggers() method, emits animation:flow events via DataStore.emitter
  • styles/animations/index.ts — new exports
  • components/creative/VisualElements/CreativeButtonBlock.vue — pause styles
  • components/creative/VisualElements/CreativeTextBlock.vue — mixin
  • components/creative/VisualElements/CreativeGraphicBlock.vue — mixin
  • components/creative/VisualElements/CreativeHtmlBlock.vue — mixin
  • components/creative/CreativeVideoBlock/CreativeVideoBlock.vue — mixin
  • components/creative/CreativeConversationBlock/CreativeConversationBlock.vue — mixin
  • components/creative/CreativeFormBlock/CreativeFormBlock.vue — mixin
  • components/creative/CreativeARBlock/CreativeARBlock.vue — mixin

Creative-Composer (3 files, +15 lines)

Modified files (3):

  • src/remapper/remapData.ts — parses body.animationTrigger?.value JSON into FlowComponent
  • src/remapper/newTypes.ts — added animationTrigger to FlowComponentBase interface
  • src/remapper/jsonTypes.ts — added animationTrigger?: { value: string } to OperatorProperties body

Commits (AF, animation-only, chronological)

  1. 953731aa Add AnimationConfig types and add animation to BlockBase
  2. 816da930 Add animation defaults to all block types
  3. ba08791f Add AnimationPresetSection, AnimationTriggerSection, AnimationDirectionSection
  4. 3edd4c39 Add animation config Card to all block configurations
  5. de674c50 Add animation system i18n translations
  6. ce173d25 Fix duplicate animation i18n key by renaming conversation animation key
  7. ef6f3514 Add animation section settings to fix override lookup crash
  8. 1c79572f Replace native select with InputSelect, use animConfig computed
  9. d210c499 Add full-width to direction and trigger option lists
  10. 5f5199ef Replace preset-based AnimationConfig with composable effect-based types and defaults
  11. 42fa5749 Add AnimationEffectRow, AnimationEffectList, BezierEasingEditor, AnimationConfigCard
  12. dfc88172 Add animation clipboard, copy/paste, exclude-triggers, V2 i18n
  13. e4b9987c Replace inline animation sections with AnimationConfigCard in all 8 configs
  14. a19d1fb2 Remove unused AnimationPresetSection
  15. e6a73a22 Add flow-triggered animation panel to flow operators
  16. d24b9e67 Fix CIcon import path in animation components
  17. b4211c1a Fix BezierEasingEditor section settings key

Commits (CE, animation-only, chronological)

  1. Various — AnimationMixin, helpers.ts, payload-v2 types, block integrations
  2. 79c5b157 Add flow-triggered animation support (operator interface + mixin)
  3. 4fe34f2f Pause animation during hover/active states on button block
  4. fc94d149 Fix Vue 3 compatibility: remove $root.$on/$off, use beforeUnmount
  5. 7ecd2ebd Fix hover and click animation triggers for all block types
  6. 2e1573cf Wire flow-triggered animations from conversationFlow to visual blocks

Commits (Composer, animation-only)

  1. 1d0e8f0 Add animationTrigger field to flow component remapping

Bugs fixed during testing

BugRoot causeFix
CIcon import crashWrong path @/components/common/Icon/CIcon.vueChanged to @/components/common/CIcon.vue in 3 files
BezierEasingEditor crashgetSectionSettings() strips "Section" from name, key was "BezierEasing" but needed "BezierEasingEditor"Renamed key in sectionSettings
CE crash on button addVue 3 has no $root.$on/$off, and beforeDestroy doesn't existRemoved event bus calls, changed to beforeUnmount
CE build export errorBuild stale after Vue 3 fixRebuilt CE

Bugs fixed during manual testing (session 2)

BugRoot causeFix
OperatorBase animation icon invisible"play" not in CIcon libraryChanged to "video-player"
Effect buttons hidden when disabledv-if="!disabled" removed elementsChanged to :disabled prop
Add effect / presets do nothinginputLocked passed to AnimationEffectListRemoved disabled binding from AnimationConfigCard
Bezier drag doesn't workSVG viewBox too small, events not captured outside boxExtended viewBox to -5 -55 110 210, moved mouse events to wrapper div
Bezier only shows on BounceOnly cubic-bezier(...) values show SVGAdded "Custom" preset that starts as cubic-bezier(0.25,0.1,0.25,1)
Flow trigger opens branding toolbarMissing showAnimationsEditor guards in CavaiFlowAdded to all 6 guard locations + pan-close
Unit shown at dropdown, not at valueunit span was in effect-headerMoved unit to each keyframe row after value
Trash button barely visibleLow opacity (0.5), no borderAdded 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 shared animationPreviewHelpers.ts to 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, iterationCount from AnimationConfigCard and uses actual animation settings in preview
  • Auto-replay — triggers replay when any timing prop or effects change
  • Overflow clippingoverflow: hidden on .preview-stage clips translateY/slideX animations
  • External loop mode (for dropdown) uses fixed 1000ms/ease/infinite

BezierEasingEditor improvements

  1. 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
  2. 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)
  3. Document-level dragaddEventListener('mousemove/mouseup') on document for dragging beyond SVG bounds
  4. Copy-to-clipboard — inline SVG copy icon → green checkmark animation on click
  5. Full-width canvas — removed max-width: 240px, height 150px (compact)
  6. 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:

TypeDefault keyframesRationale
fade0 → 1invisible → visible
slideX30px → 0slide in from right
slideY30px → 0slide in from below
scale0.5 → 1half → full size
rotate0 → 360degfull rotation
blur10px → 0blurry → sharp
skew15deg → 0skewed → 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 — shared buildPreviewCSS() and keyframe interpolation used by AnimationPreviewBox and AnimationEffectList dropdown

Additional commits (sessions 4–5)

  1. 495e6ce0 Add animation preset preview box and fix easing button wrapping
  2. 128f5e06 Fix custom bezier editor not visible by using additional-content slot
  3. 0bfa3291 Fix preset persistence, panel click propagation, add Self target
  4. 8801d7f0 Add 12 new animation presets with multi-effect combinations
  5. f0952343 Redesign animation preset and easing UI
  6. 237b2588 Fix percent sign overlapping input in keyframe row
  7. 0073293a Fix bezier SVG white space and move keyframe add button
  8. 75f04d10 Restyle animation panel to light theme matching builder UI

Additional commits (session 6 — flow wiring + UX fixes)

  1. b9fb9e2b Polish animation preview box, bezier editor, and effect list UI
  2. 4d69216d Fix animation editor panel positioning in flow view
  3. 9463784d Fix animation trigger panel UX issues in flow view
  4. 2972e064 Fix animation panel positioning, scroll, click-outside, and icon size

Additional commits (session 7 — UX bug fixes + trigger types)

  1. 3184fc4a Fix animation panel scroll, click-outside, and add flow trigger types (AF)
  2. b34d4eb1 Add flow animation trigger types — appear, click, hover (CE)

Bugs fixed (sessions 4–5)

BugRoot causeFix
BezierEasingEditor custom canvas not visibleOptionRow #additional-content slot + Vue 2 $slots reactivityMoved canvas outside OptionRow as sibling
Easing buttons not right-alignedOptionList max-width: 240px but items left-alignedAdded justify-content: flex-end
Drag stops at canvas edge@mouseleave="stopDrag" firedDocument-level addEventListener('mousemove/mouseup')
Copy icon looked like downloadUsed CIcon "export" iconInline SVG copy icon with check animation
Control points lost outside canvasoverflow: visible leaked into whole panelDynamic viewBox covering actual Y range
Bezier drag jumpy on viewBox resizeSnapshot viewBox at drag start → stale mappingSVG getScreenCTM().inverse() for live mapping
Preset dropdown height changes on hovermin-height: 52px grew when preview renderedFixed height: 60px inline preview area
Preview animation alternate directionAnimation used alternateChanged to regular infinite restart
Preview too fast in dropdown600ms loopIncreased to 1000ms for dropdown loop
Dropdown preview overflows on slide effects.preset-menu-preview no overflow clipAdded overflow: hidden
Preview ignores timing settingsHardcoded 600ms/easeProps from AnimationConfigCard: duration, easing, delay, direction, iterationCount
Default keyframes meaningless for non-fadeAll types had 0→1EFFECT_DEFAULTS map with sensible values per type; type change resets keyframes

Known remaining work

Must do — PRs & housekeeping

  • Create Composer PR — branch add-animation-system is 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 MVPscroll is in the AnimationTrigger type 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 !important on ::before/::after pseudo-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)

BugRoot causeFix
CIcon crash in AnimationEffectRow, AnimationConfigCard, BezierEasingEditor, OperatorAnimationsUsed name prop but CIcon only has icon prop; name went to $attrs, causing $slots.default[0] crashChanged all name="..." to icon="..."
i18n "visuals.duration" not a stringKey visuals.duration is nested object, not a leafChanged to visuals.transitionSettings.duration
visualElementBlocks.map is not a functionVuex getter returns object (pickBy), not arrayAdded Object.values() wrap
Dark/light UI mismatch in flow panelPanel had #2d2d2d background with white inputsRestyled 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:

  1. Replace orange modified-dot in AnimationEffectList.vue with a purple revert button
  2. Add selectedPreset: string to AnimationConfig type and defaults — stores which preset was loaded
  3. Revert button re-applies the stored preset with confirmation dialog
  4. Reuse the same $echo-* color scheme and styling as TemplateButton.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 load trigger (which fires once on mount, not on re-entry for non-slider blocks)

Implementation plan:

  1. Add 'slide-enter' to AnimationTrigger type (CE payload-v2/index.ts)
  2. Add to AnimationTriggerSection excludeTriggers for non-slider blocks
  3. In CE SliderV2, emit a animation:slide-enter event on activeIndex change with the slide's child block names
  4. AnimationMixin listens for this event and triggers animation if trigger === 'slide-enter'
  5. 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 animation

Key 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 to comp.name (the operator's own block name) before emitting
  • AnimationMixin registers listener in mounted() and unregisters in beforeUnmount() via DataStore.emitter.on/off('animation:flow')

UX fixes attempted (session 6)

IssueApproachStatus
Panel enormous/full screenCSS position: absolute + content-class attribute selector (matching styles-editor pattern)Fixed
Panel overlaps operatorPositioned via animationsEditorY/X computed from operator positionFixed
Animation trigger icon too smallCreated new animationTrigger icon (play triangle) in CIcon.vueNeeds verification
Scroll blocked in flow@wheel.stop on panel wrapperStill broken
Scroll blocked inside paneloverflow-y: auto + max-height: 470px on .panel-contentStill broken
Click-outside doesn't closev-click-outside with include for Vuetify overlaysNeeds verification
Dropdowns close panelAdded .v-menu__content etc to include functionNeeds verification
Clipboard errortry/catch around navigator.clipboard.writeText()Needs verification
Cmd+Shift+S triggers save!e.shiftKey guard in ChatbotBuilder.vueNeeds verification

Pattern note: styles-editor vs animations-editor

The animations panel follows the same pattern as the existing styles-editor in CavaiFlow:

  • Uses content-class HTML attribute (not class) for CSS targeting via [content-class*='...'] selector
  • position: absolute (not fixed) so it scrolls with the flow canvas
  • Wrapper div with computed top/left style from operator position
  • v-click-outside for 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 animationTriggers type
  • CE conversationFlow.tsemitAnimationTriggers() iterates targets array, emits one animation:flow event per target
  • CE Choice.vuedoSelection(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:

  1. conversationFlow.emitAnimationTriggers() now skips click/hover triggers
  2. Choice.vue doSelection(idx) checks blockArray[idx].animationTriggers directly and emits per-target
  3. 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

FeatureImplementation
Pan-to-center accounts for panel heightpanToOperatorAndShow() calculates targetTop = (containerHeight - totalHeight) / 2 where totalHeight = operator + panel
Caret centered on trigger iconChanged left: 14pxleft: 3.5px (centered on 17px-wide icon)
Panel flip with caretdetectPanelFlip() checks if panel overflows viewport bottom, passes flipped prop, CSS flips caret direction
Escape to closeAdded to handleKeyDown in CavaiFlow.vue
Entrance animation@keyframes panel-enter { from { opacity: 0; translateY(-4px) } to { opacity: 1; translateY(0) } }
Removed dead caret codeRemoved caretAdjusted computed and BGCOMPONENTLIST import from OperatorStyles
Vuetify underline removalUnscoped CSS in CavaiFlow.vue: .v-input__slot::before/::after { content: none !important }

Commits (session 8)

AF:

  • 7b51910f Fix duplicate loop key in animation i18n
  • 2972e064 Fix animation panel positioning, scroll, click-outside, and icon size
  • 9463784d Fix animation trigger panel UX issues in flow view
  • 4d69216d Fix animation editor panel positioning in flow view
  • b9fb9e2b Polish animation preview box, bezier editor, and effect list UI
  • 3ed0ced6 Add multi-target animation trigger UI with collapsible cards

CE:

  • 202cc86d Add multi-target animation triggers support

Composer:

  • 2b370a2 Add multi-target animation triggers with legacy migration

Known issues (session 8)

IssueStatus
Vuetify red underline under dropdownsUser reported 3 times — CSS fix verified correct: unscoped .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 CEFixed — 5 debug logs removed from conversationFlow.ts (39f66293)
Composer PRCreated — Cavai/Creative-Composer#105 (draft)

Session 9 — Verification & cleanup

Verified

  1. 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.

  2. Vuetify red underline — CavaiFlow.vue unscoped CSS (lines 5212-5226) is comprehensive:

    • .operator-animations-panel .v-input__slot { border-bottom: none !important }
    • ::before and ::after pseudo-elements: content: none, display: none, border: none (all !important)
    • InputSelect doesn't pass error prop in OperatorAnimations, so no validation state triggering red
  3. 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 one animation:flow per 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.

Commits (session 9)

CE:

  • 39f66293 Remove debug console.logs from conversationFlow

Known remaining work

ItemStatus
Manual browser testing on external sitePending
Vuetify underline — visual confirmation in browserPending (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

RepoPRStatus
Application-Frontend#1806Draft
Creative-Engine#706Open
Creative-Composer#105Draft

Must verify in browser before merge

ItemNotes
Block-level animations (all 8 types)Add effects, change timing, confirm animation plays in preview and on external site
Flow-triggered animationsSet animation on operator → confirm block animates when message appears
Per-choice isolationSet different animations on different choice operators → confirm each choice triggers only its own
Multi-target triggersOne operator animating multiple blocks simultaneously
Copy/paste between blocksVuex clipboard implemented but never tested in browser
Remove scroll trigger from MVPStill in UI but can't work in nested iframes — needs to be hidden
Scroll inside flow panelReported "still broken" in sessions 6-7
Click-outside closingIncluding Vuetify overlay exclusions
Vuetify red underline on dropdownsCSS fix written, needs visual confirmation
Bezier easing editor dragCTM-based drag, dynamic viewBox
Preset dropdown hover preview60px 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):

  • StaggerSettings type: { preset, duration, easing, staggerDelay, loop, iterations, reverse, excludedChildren }
  • AnimationConfig additions: staggerSource?: string, repeatDelay?: number
  • buildStaggerChildConfigs() utility in utils.ts
  • configurationLogic.ts: updateAndClearStagger helper, paste strips staggerSource/repeatDelay
  • 10 stagger presets in defaults.ts (pulse, pop, heartbeat, elastic, bounce, nod, nudge, shake, wiggle, swing)
  • Engine AnimationMixin: isStaggerControlled suppresses CSS animation for stagger-assigned blocks
  • Engine CreativeGroupBlock: reads from staggerSettings + children's animation configs, WAAPI round-robin
  • i18n keys for stagger UI

Design decisions:

  • Child groups excluded from stagger targets (only visual elements)
  • staggerSource auto-cleared when user manually edits a child's animation
  • Flow animation takes priority over stagger suppression
  • Engine calculates its own repeatDelay from StaggerSettings (not from child config)
  • repeatDelay kept on AnimationConfig type 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 blockGroupProperties and StaggerSettings in blocks.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() sets flowAnimationActive = true and 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-95 base, $grey-85 dots via radial-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 hero prop) 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")
  • keyframesCollapsed data 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()):

  • AnimationEffectRow receives usedTypes prop, filters dropdown to show only unused types (plus current type)
  • AnimationEffectList computes usedTypes and allTypesUsed
  • "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 returning typedBlock
  • animationStyleProps and animationHoverStyle spread into block styles
  • animationKeyframeRule merged 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: 0 on 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 button
  • AnimationConfigCard.vue — hero preview at top, removed timing props from AnimationEffectList
  • AnimationEffectList.vue — keyframes group header, duplicate prevention, removed embedded preview
  • AnimationEffectRow.vueusedTypes prop, computed effectTypeItems filter
  • OperatorAnimations.vue — removed stale timing props from AnimationEffectList
  • en.js — added keyframes i18n key

CE modified:

  • CreativeFormBlock.vue — added block() computed
  • CreativeConversationBlock.vue — added block() computed
  • CreativeSliderBlock.vue — added AnimationMixin + block() computed + animation styles

Bugs fixed (session 9)

BugRoot causeFix
$grey-97 doesn't existNo such SCSS variable in color systemUsed $grey-95 (lightest available) and $grey-85 for dots
Preview not visually differentHero class not distinct enough from compact modeFull redesign with dotted grid, larger block, rounded corners
Replay button flush against edgeright: 8px too tightChanged to right: 16px
"Add effect" not visually disabledNo disabled CSS stylesAdded opacity: 0.4, cursor: not-allowed, hover guard
FormBlock animation brokenthis.block undefined — no VisualElementMixinAdded block() computed returning typedBlock
ConversationBlock animation brokenSame as FormBlockSame fix
CE build missing buildCreativeBuilt without VITE_LIBRARY_MODE=trueRebuilt 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:

  1. AF — ShowHide operator UI: Add animation type selector (fade, slideUp, slideDown, scale, etc.) and duration input to the ShowHide operator panel.

  2. Composer → CE data: Extend runtimeHidden from boolean to:

    typescript
    { hidden: boolean; animation?: string; duration?: number }

    Or keep runtimeHidden as boolean and add a separate runtimeHiddenAnimation field.

  3. CE BlockMixin — animated hide/show:

    • Show: Remove display: none, apply enter animation, block is visible immediately
    • Hide: Play exit animation, listen for animationend, THEN set display: none
    • This prevents the current problem where display: none kills any running animation
  4. Considerations:

    • Must handle rapid show/hide toggles (cancel pending animationend if toggled again before animation completes)
    • Could reuse the same AnimationEffect types from the animation system for consistency
    • Layout shift: hiding a block may cause content reflow. visibility: hidden + height: 0 with animation might be better than display: none for some layouts

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:

  1. AF — AnimationConfigCard: Add targetBlock field to AnimationConfig type and defaults. Show a target block dropdown (using InputSelect) when trigger is click or hover. Populate with all animatable blocks except the current one, plus a "Self (this block)" default option.

  2. AF — configurationLogic: Add animatableBlockOptions computed that filters creativeBlocks against ANIMATABLE_BLOCKS constant, excluding the current block.

  3. CE — AnimationMixin: When targetBlock is set, triggerAnimation() emits animation:flow event to the target block instead of playing locally. For hover triggers with a target, use JS mouseenter event (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: $nextTick toggle of animationTriggered (false→true) didn't force a browser reflow, so the CSS animation wasn't restarted
  • Fix: Added void this.$el?.offsetHeight between 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:

  1. 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__)
  2. Auto-set target: loadConfig() forces targetBlock: '__self__' for change operators, ensuring the animation applies to the change operator's own target block

  3. Inverted color palette: Added .invert class 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
  4. CavaiFlow integration: Added animationsEditorIsChange computed that checks if animationsEditorOpKey starts with 'change', passed as :is-change-operator prop

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.vueisChangeOperator prop, conditional rendering, inverted styles
  • AF CavaiFlow.vueanimationsEditorIsChange computed, prop binding

Internal documentation