Appearance
Stagger Animation Redesign -- Implementation Plan
For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Redesign stagger animation so it writes animation configs to each child block (making stagger a "shortcut"), with child inclusion/exclusion, reverse order, and per-child override capability.
Architecture: When the user applies a stagger preset from the group config, the system writes an AnimationConfig to each included child's animation field in Vuex. The group stores staggerSettings metadata (preset, timing, loop, reverse, excluded children) for recalculation. The engine's CreativeGroupBlock continues to coordinate WAAPI round-robin across children (for hover-pause synchronization and tab-visibility restart), reading effect data from children's animation configs. Each child's AnimationMixin suppresses its own CSS animation when stagger is active to avoid double-animation.
Tech Stack: Vue 2 (Options API), Vuex, TypeScript, Web Animations API, SCSS with CSS custom properties.
Status (2026-06-02): Infrastructure (Tasks 1-8) implemented on theming-system-redesign. UI was built but removed by user for reconsideration. Key post-implementation fixes: child groups excluded from stagger targets, paste strips staggerSource/repeatDelay, dead repeatDelay removed from buildStaggerChildConfigs, updateAndClearStagger helper extracted in configurationLogic.
Current State Reference
These files already have stagger implementation (uncommitted on theming-system-redesign):
| File | Role |
|---|---|
AF/src/pages/.../Blocks/data/types.ts | StaggerAnimationConfig, StaggerPresetConfig types |
AF/src/pages/.../Blocks/data/defaults.ts | STAGGER_PRESETS (10 presets), FLOW_EASING_OPTIONS |
AF/src/pages/.../Configuration/configs/BlockGroupConfiguration.vue | Group config with stagger tab: preset, duration, easing, delay, loop, preview dots |
CE/src/components/creative/CreativeGroupBlock/CreativeGroupBlock.vue | WAAPI round-robin, hover pause, visibility restart |
CE/src/mixins/AnimationMixin.ts | CSS @keyframes animation for all blocks (no stagger awareness) |
CE/src/styles/animations/helpers.ts | buildKeyframes(), buildAnimationString(), getAnimationStyles() |
CE/src/interfaces/jsonTypes/payload-v2.ts | AnimationConfig, BasicConfig.animation? |
AF/src/pages/.../CavaiFlow/flowactors/StaggerAnimationPanel.vue | Flow stagger panel (writes to operators, reference pattern) |
File Structure
Types (data model changes)
- Modify:
AF/src/pages/Chatbots/components/BuilderVisuals/Blocks/data/types.ts - Modify:
CE/src/interfaces/jsonTypes/payload-v2.ts
AF -- Group Config UI
- Modify:
AF/src/pages/Chatbots/components/BuilderVisuals/Configuration/configs/BlockGroupConfiguration.vue - Modify:
AF/src/pages/Chatbots/components/BuilderVisuals/Blocks/utils.ts
AF -- configurationLogic mixin
- Modify:
AF/src/pages/Chatbots/components/BuilderVisuals/Configuration/mixins/configurationLogic.ts
CE -- Engine Animation
- Modify:
CE/src/mixins/AnimationMixin.ts - Modify:
CE/src/components/creative/CreativeGroupBlock/CreativeGroupBlock.vue
i18n
- Modify:
AF/src/assets/i18n/en.js
Chunk 1: Data Model & Write-to-Children
Task 1: Update type definitions
Files:
- Modify:
AF/src/pages/Chatbots/components/BuilderVisuals/Blocks/data/types.ts - Modify:
CE/src/interfaces/jsonTypes/payload-v2.ts
Replace StaggerAnimationConfig with StaggerSettings and add stagger fields to AnimationConfig.
- [ ] Step 1: Update AF types.ts
Replace the StaggerAnimationConfig type and add staggerSource + repeatDelay to AnimationConfig:
ts
// Replace StaggerAnimationConfig (lines 89-97) with:
export type StaggerSettings = {
preset: string
duration: number
easing: string
staggerDelay: number
loop: boolean
iterations: number
reverse: boolean
excludedChildren: string[] // blockNames to skip
}
// Add to AnimationConfig (after line 114, inside the type):
repeatDelay?: number // ms between iterations for round-robin stagger
staggerSource?: string // blockName of the parent group that assigned this animationUpdate BlockGroupProperties (line 994): change staggerAnimation?: StaggerAnimationConfig to staggerSettings?: StaggerSettings.
- [ ] Step 2: Update CE payload-v2.ts
Add repeatDelay?: number and staggerSource?: string to the CE AnimationConfig type. These fields are needed for the engine to:
repeatDelay: Switch to WAAPI round-robin mode in AnimationMixinstaggerSource: Know the animation was stagger-assigned (for suppression logic)[ ] Step 3: Commit
feat: add stagger settings types and animation stagger fieldsTask 2: Write stagger configs to children
Files:
- Modify:
AF/src/pages/Chatbots/components/BuilderVisuals/Configuration/configs/BlockGroupConfiguration.vue - Modify:
AF/src/pages/Chatbots/components/BuilderVisuals/Blocks/utils.ts
When the user applies a stagger preset, write animation to each child block in Vuex (same pattern as flow's StaggerAnimationPanel.emitApply).
- [ ] Step 1: Add helper to utils.ts
Add a function to build stagger animation configs for children:
ts
import type { AnimationConfig, AnimationEffect, StaggerSettings } from './types'
/**
* Build AnimationConfig for each child in a stagger group.
* Returns an array of { blockName, animation } pairs, one per included child.
*/
export const buildStaggerChildConfigs = (
children: { blockName: string }[],
settings: StaggerSettings,
effects: AnimationEffect[],
): { blockName: string, animation: AnimationConfig }[] => {
const ordered = settings.reverse ? [...children].reverse() : children
const step = settings.duration + settings.staggerDelay
const count = ordered.length
const totalCycle = count * step
const repeatDelay = settings.loop ? totalCycle - settings.duration : undefined
return ordered.map((child, i) => ({
blockName: child.blockName,
animation: {
effects: JSON.parse(JSON.stringify(effects)),
trigger: 'appear' as const,
duration: settings.duration,
delay: i * step,
loop: settings.loop,
iterationCount: settings.loop ? 0 : settings.iterations,
direction: 'normal' as const,
easing: settings.easing,
...(repeatDelay ? { repeatDelay } : {}),
staggerSource: '__pending__', // replaced with actual group blockName by caller
},
}))
}- [ ] Step 2: Update BlockGroupConfiguration to write to children
Replace the current onStaggerPresetChange and updateStaggerField methods. When stagger settings change, the component must:
- Write
staggerSettingsto the group block - Write
animationto each included child block
Key changes to BlockGroupConfiguration.vue:
ts
// New computed: get child blocks
includedChildren() {
if (!this.blockData?.blockName) return []
const all = getGroupChildren(this.blockData.blockName, this.$store.state.blocks.creativeBlocks)
const excluded = this.blockData?.staggerSettings?.excludedChildren || []
return all.filter((b) => !excluded.includes(b.blockName))
},
// Rewrite onStaggerPresetChange
onStaggerPresetChange(value) {
if (value === 'none') {
this.clearStagger()
return
}
const presetConfig = STAGGER_PRESETS[value]
if (!presetConfig) return
const settings = {
preset: value,
duration: presetConfig.duration,
easing: presetConfig.easing,
staggerDelay: presetConfig.staggerDelay,
loop: this.staggerLoop,
iterations: this.staggerIterations,
reverse: this.staggerReverse,
excludedChildren: this.blockData?.staggerSettings?.excludedChildren || [],
}
this.applyStaggerToChildren(settings, presetConfig.effects)
},
// New method: apply stagger to all included children
applyStaggerToChildren(settings, effects) {
// 1. Write stagger settings to group
this.updateValue(settings, 'staggerSettings')
// 2. Build per-child animation configs
const groupName = this.blockData.blockName
const childConfigs = buildStaggerChildConfigs(this.includedChildren, settings, effects)
// 3. Write animation to each child
for (const { blockName, animation } of childConfigs) {
animation.staggerSource = groupName
this.$store.commit('updateBlockValue', {
path: `${blockName}.animation`,
value: animation,
})
}
},
// Rewrite updateStaggerField to recalculate children
updateStaggerField(field, value) {
const current = this.blockData?.staggerSettings
if (!current) return
const settings = { ...current, [field]: value }
const presetConfig = STAGGER_PRESETS[settings.preset]
if (!presetConfig) return
this.applyStaggerToChildren(settings, presetConfig.effects)
},
// Rewrite clearStagger to also clear children's animations
clearStagger() {
// Clear stagger-assigned animations from all children
const groupName = this.blockData?.blockName
const all = getGroupChildren(groupName, this.$store.state.blocks.creativeBlocks)
for (const child of all) {
const childAnim = child.animation
if (childAnim?.staggerSource === groupName) {
this.$store.commit('updateBlockValue', {
path: `${child.blockName}.animation`,
value: undefined,
})
}
}
// Clear group settings
this.updateValue(undefined, 'staggerSettings')
},- [ ] Step 3: Update computed properties for new field names
Change all staggerAnimation references to staggerSettings:
hasStaggerAnimation-> checksstaggerSettings?.presetstaggerPreset-> reads fromstaggerSettingsstaggerDuration/Easing/Delay/Loop/Iterations-> reads fromstaggerSettingsAdd
staggerReversecomputed[ ] Step 4: Update watch and preview to use staggerSettings
The watchers and restartStaggerPreview already work with the computed values, so they should work without changes. Verify the preview dots still animate correctly.
- [ ] Step 5: Commit
feat: stagger writes animation configs to child blocksTask 3: Child inclusion/exclusion and reverse
Files:
- Modify:
AF/src/pages/Chatbots/components/BuilderVisuals/Configuration/configs/BlockGroupConfiguration.vue - Modify:
AF/src/assets/i18n/en.js
Add UI for toggling which children participate in stagger, and a reverse toggle.
- [ ] Step 1: Add child list UI to template
After the iterations OptionRow and before the preview strip, add a child toggle section:
html
<OptionRow :title="$t('visuals.animation.staggerChildren')" no-toggle>
<div class="stagger-children-list">
<label
v-for="child in allGroupChildren"
:key="child.blockName"
class="stagger-child-toggle"
>
<input
type="checkbox"
:checked="!isChildExcluded(child.blockName)"
:disabled="inputLocked"
@change="toggleChildInclusion(child.blockName)"
>
<span :class="{ 'stagger-child-overridden': isChildOverridden(child) }">
{{ child.displayName || child.blockName }}
</span>
</label>
</div>
</OptionRow>
<OptionRow :title="$t('visuals.animation.staggerReverse')" no-toggle>
<button
:class="['stagger-reverse-toggle', { active: staggerReverse }]"
:disabled="inputLocked"
@click="updateStaggerField('reverse', !staggerReverse)"
>
{{ staggerReverse ? $t('visuals.animation.reversed') : $t('visuals.animation.inOrder') }}
</button>
</OptionRow>- [ ] Step 2: Add computed properties and methods
ts
// Computed
allGroupChildren() {
if (!this.blockData?.blockName) return []
return getGroupChildren(this.blockData.blockName, this.$store.state.blocks.creativeBlocks)
},
staggerReverse() {
return this.blockData?.staggerSettings?.reverse ?? false
},
// Methods
isChildExcluded(blockName) {
return (this.blockData?.staggerSettings?.excludedChildren || []).includes(blockName)
},
isChildOverridden(child) {
const groupName = this.blockData?.blockName
return child.animation && child.animation.staggerSource !== groupName
},
toggleChildInclusion(blockName) {
const current = this.blockData?.staggerSettings
if (!current) return
const excluded = [...(current.excludedChildren || [])]
const idx = excluded.indexOf(blockName)
if (idx >= 0) {
excluded.splice(idx, 1)
} else {
excluded.push(blockName)
// Clear stagger animation from excluded child
const groupName = this.blockData.blockName
const child = this.allGroupChildren.find((c) => c.blockName === blockName)
if (child?.animation?.staggerSource === groupName) {
this.$store.commit('updateBlockValue', {
path: `${blockName}.animation`,
value: undefined,
})
}
}
const settings = { ...current, excludedChildren: excluded }
const presetConfig = STAGGER_PRESETS[settings.preset]
if (presetConfig) {
this.applyStaggerToChildren(settings, presetConfig.effects)
}
},- [ ] Step 3: Add i18n keys
Add to en.js under visuals.animation:
js
staggerChildren: 'Include',
staggerReverse: 'Direction',
reversed: 'Reversed',
inOrder: 'In order',- [ ] Step 4: Add styles
scss
.stagger-children-list {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 120px;
overflow-y: auto;
}
.stagger-child-toggle {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text-secondary);
cursor: pointer;
input[type="checkbox"] {
accent-color: var(--accent-primary);
}
}
.stagger-child-overridden {
font-style: italic;
color: var(--text-muted);
}
.stagger-reverse-toggle {
background: var(--surface-hover);
border: 1px solid var(--border-default);
border-radius: 4px;
padding: 4px 10px;
font-size: 12px;
color: var(--text-secondary);
cursor: pointer;
transition: all 150ms ease;
&:hover {
border-color: var(--text-muted);
color: var(--text-primary);
}
&.active {
background: var(--accent-primary);
border-color: var(--accent-primary);
color: var(--surface-base);
}
&:disabled {
opacity: 0.5;
pointer-events: none;
}
}- [ ] Step 5: Commit
feat: add child inclusion/exclusion and reverse toggle for staggerChunk 2: Engine Changes & Override Detection
Task 4: AnimationMixin stagger awareness
Files:
- Modify:
CE/src/mixins/AnimationMixin.ts
When a block's animation.staggerSource is set, the AnimationMixin should suppress its own CSS animation to avoid double-animation (the parent group handles WAAPI).
- [ ] Step 1: Add stagger suppression to animationStyleProps
In the animationStyleProps computed, check for staggerSource:
ts
animationStyleProps(): Record<string, string> {
// Flow animation takes full priority over block animation
if (this.flowAnimationActive && this.flowAnimationConfig) {
const { duration, easing } = this.flowAnimationConfig
const name: string = this.flowAnimationName
return { animation: `${name} ${duration}ms ${easing} both` }
}
// Stagger source present = parent group handles WAAPI, suppress CSS animation
if (this.animationConfig?.staggerSource) {
return {}
}
const styles: Record<string, string> = {}
if (this.shouldPlayAnimation) {
Object.assign(styles, this.animationResult?.animationStyle || {})
}
return styles
},Also suppress keyframes and hover styles when stagger is active:
ts
animationKeyframeRule(): Record<string, any> {
if (this.animationConfig?.staggerSource) {
// Still emit flow animation keyframes if active
if (this.flowAnimationActive && this.flowAnimationConfig) {
const keyframes = buildKeyframes(this.flowAnimationConfig.effects)
const name: string = this.flowAnimationName
return { [`@keyframes ${name}`]: keyframes }
}
return {}
}
const rules = this.animationResult?.keyframeRule || {}
// ... rest unchanged
},
animationHoverStyle(): Record<string, string> {
if (this.animationConfig?.staggerSource) return {}
return getHoverAnimationStyle(this.animationConfig, this.animationBlockName) || {}
},- [ ] Step 2: Commit
feat(engine): suppress CSS animation when block has stagger sourceTask 5: Refactor CreativeGroupBlock stagger to read from children
Files:
- Modify:
CE/src/components/creative/CreativeGroupBlock/CreativeGroupBlock.vue
Change playStaggerAnimation to read staggerSettings (instead of staggerAnimation) and get effects from children's animation configs.
- [ ] Step 1: Update playStaggerAnimation
ts
playStaggerAnimation() {
const settings = this.typedBlock.staggerSettings
if (!settings?.preset) return
const children = this.getStaggerChildren()
if (!children.length) return
this.setupStaggerHover(children)
// Read each child's animation config for its effects
// Fall back to group's preset if child has no animation
const childBlocks = Object.values(this.typedBlock)
.filter((v: any) => v?.blockName)
.sort((a: any, b: any) => (Number(b.order) || 0) - (Number(a.order) || 0))
const excludedSet = new Set(settings.excludedChildren || [])
const orderedBlocks = settings.reverse ? [...childBlocks].reverse() : childBlocks
const includedBlocks = orderedBlocks.filter((b: any) => !excludedSet.has(b.blockName))
const duration = settings.duration || 500
const easing = settings.easing || 'ease'
const staggerDelay = settings.staggerDelay || 200
const loop = settings.loop ?? true
const iterations = loop ? Infinity : (settings.iterations || 1)
const step = duration + staggerDelay
const count = includedBlocks.length
const totalCycle = count * step
const repeatDelay = loop ? totalCycle - duration : 0
// Map DOM children to their block data
// Children render in the same sort order as subBlocks computed
const childEls = this.getStaggerChildren()
for (let i = 0; i < count; i++) {
const block = includedBlocks[i]
// Find the DOM element for this block
const elIndex = childBlocks.indexOf(block)
const el = childEls[elIndex]
if (!el) continue
// Get animation effects: prefer child's own, fall back to preset
const childAnim = block.animation
const effects = childAnim?.effects?.length
? childAnim.effects
: STAGGER_PRESETS[settings.preset]?.effects
if (!effects?.length) continue
const keyframeObj = buildKeyframes(effects)
const webKeyframes: Keyframe[] = Object.entries(keyframeObj).map(([stop, props]) => ({
offset: stop === 'from' ? 0 : stop === 'to' ? 1 : parseFloat(stop) / 100,
...props as Record<string, string>,
})).sort((a, b) => (a.offset as number) - (b.offset as number))
// ... rest of WAAPI round-robin logic stays the same
// (trackTimeout, playOnce pattern, etc.)
}
},- [ ] Step 2: Update mounted/beforeUnmount to use staggerSettings
Change condition check from staggerAnimation to staggerSettings:
ts
// In visibilityHandler:
if (document.visibilityState === 'visible' && this.typedBlock.staggerSettings?.preset) {
this.cancelStaggerAnimation()
this.playStaggerAnimation()
}- [ ] Step 3: Remove staggerAnimation references
Search for any remaining staggerAnimation references in CreativeGroupBlock and replace with staggerSettings.
- [ ] Step 4: Commit
feat(engine): read stagger from children's animation + group staggerSettingsTask 6: Override detection in group config
Files:
- Modify:
AF/src/pages/Chatbots/components/BuilderVisuals/Configuration/configs/BlockGroupConfiguration.vue
When a child's animation was stagger-assigned but the user manually edited it (changed effects, duration, etc.), show a visual indicator in the child list.
- [ ] Step 1: Add override detection computed
ts
childStaggerStatus() {
const groupName = this.blockData?.blockName
if (!groupName) return {}
const status = {}
for (const child of this.allGroupChildren) {
const anim = child.animation
if (!anim) {
status[child.blockName] = 'none'
} else if (anim.staggerSource === groupName) {
status[child.blockName] = 'stagger'
} else {
status[child.blockName] = 'overridden'
}
}
return status
},- [ ] Step 2: Update isChildOverridden to use status
ts
isChildOverridden(child) {
return this.childStaggerStatus[child.blockName] === 'overridden'
},- [ ] Step 3: Add visual indicator in template
Next to the child name in the toggle list, show a small "(modified)" label:
html
<span v-if="isChildOverridden(child)" class="stagger-modified-badge">
{{ $t('visuals.animation.modified') }}
</span>Add i18n key: modified: 'modified'
- [ ] Step 4: Style the badge
scss
.stagger-modified-badge {
font-size: 10px;
color: var(--text-muted);
font-style: italic;
margin-left: 4px;
}- [ ] Step 5: Commit
feat: show override indicator for stagger-assigned childrenTask 7: Clean up old staggerAnimation field
Files:
- Modify:
AF/src/pages/Chatbots/components/BuilderVisuals/Blocks/data/types.ts - Modify:
AF/src/pages/Chatbots/components/BuilderVisuals/Blocks/data/defaults.ts - Modify:
CE/src/components/creative/CreativeGroupBlock/CreativeGroupBlock.vue
Remove the old StaggerAnimationConfig type (now StaggerSettings) and ensure no references to the old staggerAnimation field remain.
- [ ] Step 1: Search and replace
Grep both AF and CE repos for staggerAnimation and replace any remaining references with staggerSettings.
- [ ] Step 2: Remove old type if still present
If StaggerAnimationConfig wasn't already replaced in Task 1, remove it now.
- [ ] Step 3: Verify build
Run npm run build in both AF and CE to confirm no type errors.
- [ ] Step 4: Commit
refactor: clean up old staggerAnimation referencesChunk 3: Polish & Edge Cases
Task 8: Handle child animation editing (clear staggerSource)
Files:
- Modify:
AF/src/pages/Chatbots/components/BuilderVisuals/Configuration/mixins/configurationLogic.ts
When a child block's animation is edited via AnimationConfigCard, clear staggerSource to indicate it's been overridden.
- [ ] Step 1: Update animationCardListeners
In configurationLogic.ts, modify the animation update listeners to clear staggerSource when the user manually changes animation properties:
ts
// In animationCardListeners computed, wrap each update handler:
'update:effects': ($event) => {
this.updateValue($event, 'animation.effects')
if (this.blockData?.animation?.staggerSource) {
this.updateValue(undefined, 'animation.staggerSource')
}
},
// Same for update:duration, update:easing, update:trigger, update:delay, etc.This way, any manual edit to a stagger-assigned animation clears the source marker, signaling override to the group.
- [ ] Step 2: Commit
feat: clear stagger source when child animation is manually editedTask 9: Stagger preset change preserves excluded children
Files:
- Modify:
AF/src/pages/Chatbots/components/BuilderVisuals/Configuration/configs/BlockGroupConfiguration.vue
Verify that when changing stagger preset, excluded children stay excluded and their animations are not overwritten.
- [ ] Step 1: Verify applyStaggerToChildren skips excluded
The includedChildren computed already filters out excluded. Verify that applyStaggerToChildren only writes to included children and doesn't touch excluded ones.
- [ ] Step 2: Verify clearStagger clears only stagger-assigned
When clearing stagger, only clear animations that have staggerSource matching this group. Don't clear manually-set animations on excluded children.
- [ ] Step 3: Commit (if changes needed)
fix: preserve excluded children state when changing stagger presetTask 10: Manual test checklist
Before merging:
- [ ] Apply stagger preset from group config -- verify child blocks show animation in their own AnimationConfigCard
- [ ] Change stagger duration/easing/delay -- verify all children update
- [ ] Toggle a child off -- verify its animation is cleared and stagger recalculates delays
- [ ] Toggle a child back on -- verify it gets animation with correct delay
- [ ] Enable reverse -- verify stagger order reverses
- [ ] Manually edit a child's animation -- verify "(modified)" badge appears in group config
- [ ] Clear stagger -- verify all stagger-assigned animations are removed, manually-edited ones preserved
- [ ] Preview dots animate correctly in group config panel
- [ ] Engine: stagger plays correctly in creative preview (round-robin, hover pause, tab restart)
- [ ] Engine: child with manually-edited animation still participates in group stagger (or doesn't, depending on exclusion)
- [ ] Flow stagger still works unchanged (StaggerAnimationPanel)
- [ ] No console errors in builder or preview