Appearance
Unified Container Block Vision
Date: 2026-04-20 Status: Proposed direction, not yet implemented Prerequisite: Fractional indexing (plan at
AF/docs/superpowers/plans/2026-04-19-fractional-block-ordering.md)
Core Idea
All block types that contain sub-blocks (Form, Slider, SliderV2, Conversation, Group) share the same foundation. Instead of each having its own mutations, helpers, and creation logic, treat them all as container blocks with specialization hooks.
What They Already Share
Every container block in Cavai uses:
- Children stored as object properties on the parent
parentfield on children referencing the container's blockNamegetSubBlocks(block)for child discoveryorderfield on children for sorting- Own CE rendering component
The Unified Model
ContainerBlock (base)
- allowedSubBlocks: string[]
- subBlocksReorderable: boolean
- children as object properties with parent + order (fractional keys)
Generic mutations:
- addChildBlock(parentBlockName, childDefaults, hooks?)
- removeChildBlock(parentBlockName, childBlockName, hooks?)
- reorderChildBlock(parentBlockName, childBlockName, newOrderKey, hooks?)
- moveBlockToContainer(blockName, targetContainerBlockName, orderKey)
- moveBlockOutOfContainer(blockName, sourceContainerBlockName, targetOrderKey)
- dissolveContainer(containerBlockName) // move children out, delete containerFormBlock extends ContainerBlock
allowedSubBlocks: ['formInputProperties', 'formSubmitButtonProperties']
hooks:
afterMutation -> renumberFormInputDisplayNames()
createChild -> calculateTypeIndex(), generateDisplayName()
CE: renders <form> with input fields, validation, submission
Special: triple numbering (blockName, typeIndex, displayName), 11 input typesSliderBlock (V1) extends ContainerBlock
allowedSubBlocks: ['textProperties', 'graphicProperties', 'buttonProperties', 'htmlProperties']
extra data: slides[] (per-slide override objects -- slider-specific, not part of container system)
hooks:
createChild -> apply template defaults
duplicate -> uniquefySlideBlockNames()
CE: renders slides with navigation, transitions, merges sub-block defaults with per-slide overrides
Special: sub-blocks are templates, not instances; slides[] overrides change per-slide appearanceSliderV2 extends ContainerBlock
allowedSubBlocks: ['textProperties', 'graphicProperties', 'buttonProperties', 'htmlProperties', 'videoProperties']
extra data: slides[] (same override pattern as V1, plus per-slide video fields)
hooks:
createChild -> apply template defaults (same as V1)
CE: 6 transition modes, thumbnail nav, video sub-blocks, ~2080 lines
Special: same slides[] override pattern as V1, plus video integrationConversationBlock extends ContainerBlock
allowedSubBlocks: ['conversationStepProperties']
hooks:
createChild -> set up flow node with conditions
CE: renders flow steps with branching, conditional logic, state machine
Special: most divergent from the group model -- children are flow nodes, not visual elementsGroupBlock extends ContainerBlock
allowedSubBlocks: ['textProperties', 'graphicProperties', 'buttonProperties', 'htmlProperties']
hooks: none
CE: renders flex div wrapper, children positioned inside
Special: simplest container -- no extra data, no special hooksWhat This Consolidates
Before (current)
Each container has its own mutations in blocks.ts:
- Form:
createFormInputBlock, form-specific add/remove inaddBlock/removeBlock - Slider:
createSliderTemplateBlocks, slider-specific duplication - Group:
createBlockGroup,moveBlockToGroup,moveBlockOutOfGroup,ungroupBlocks - Each: ~150-300 lines of type-specific mutation code
Total: ~800+ lines across blocks.ts
After (unified)
Generic mutations: ~100-150 lines Type-specific hooks: ~100-200 lines each (form numbering, slider templates, etc.) Total: ~500 lines, with clear separation of generic vs specific
The generic mutations handle the common operations (add child with fractional order key, remove child, reorder). The hooks handle type-specific concerns (form renumbering, slider override sync).
What This Does NOT Unify
- CE rendering: Each container type has genuinely different rendering needs. CreativeFormBlock, CreativeSliderBlock, CreativeGroupBlock stay separate.
- Configuration panels: Each has its own config UI. FormConfiguration, SliderConfiguration, GroupConfiguration stay separate.
- Slider slides[] array: The per-slide override mechanism is slider-specific. It sits alongside the container system, not inside it.
- Form triple numbering: The blockName/typeIndex/displayName system is form-specific. It becomes a hook, not part of the base.
- Conversation flow logic: Branching, conditions, state machine are conversation-specific.
Implementation Strategy
Step 1: Fractional indexing (ordering foundation)
Unifies ordering for all container types. After this, all containers use string-based order keys. No normalizeOrders needed.
Step 2: Extract utilities from blocks.ts
Move form utilities (~471 lines) and slider templates (~155 lines) to separate files. Makes the shared vs specific boundary visible.
Step 3: Introduce generic container mutations
Replace type-specific add/remove/reorder with generic versions that take hooks. Group uses them without hooks, form adds renumber hook, slider adds template hook.
Step 4: Apply to SliderV2 before merge
SliderV2 is on branch add-slider-v2, not yet merged. Perfect opportunity to build it on the new foundation instead of duplicating V1 patterns. Adopt generic container mutations + fractional ordering from the start.
Step 5: Migrate V1 containers incrementally
Once generic mutations are proven on Group + SliderV2, migrate Form and SliderV1 one at a time. Each migration is a PR that replaces type-specific mutations with generic + hooks.
SliderV2 as First Adopter
SliderV2 is the ideal first adopter because:
- Not merged yet -- zero migration risk, no existing creatives to worry about
- Same pattern as V1 -- uses object property sub-blocks + slides[] overrides, so we know the pattern works
- Large codebase (~2080 lines CE, significant AF code) -- benefits most from shared infrastructure
- Adding new capabilities (video sub-blocks, 6 transitions) -- new code should be on new foundation
- V1 stays unchanged -- if anything goes wrong with V2's new foundation, V1 is unaffected
Concrete changes for SliderV2:
- Use fractional order keys for sub-blocks instead of integers
- Use generic
addChildBlock/removeChildBlockinstead of slider-specific mutations - Keep slides[] override array as slider-specific (it's a property of the container, not the sub-block system)
- Template creation becomes a hook that provides child defaults to
addChildBlock
Deep Nesting
The unified container model makes nesting straightforward:
- Each container has its own ordering space (fractional keys)
addChildBlockchecksallowedSubBlocksto validate- Groups could allow other groups:
allowedSubBlocks: ['textProperties', ..., 'groupProperties'] - No special nesting code needed -- just recursive application of the same pattern
Risk Assessment
Low risk: Fractional indexing + utility extraction (Steps 1-2). No behavior change, just better organization.
Medium risk: Generic container mutations (Step 3). Requires careful hook design. Test with Group first (simplest case).
Low risk: SliderV2 adoption (Step 4). New code on new foundation, V1 unaffected.
Medium risk: V1 migration (Step 5). Existing creatives depend on current mutations. Requires thorough testing.
Related Docs
architecture/sub-block-patterns-comparison.md-- detailed comparison of current form/slider/group patternsarchitecture/blocks-store-structure-analysis.md-- blocks.ts structure and extraction plantodos/BlockGrouping/ordering-bugs-and-fractional-indexing.md-- the four ordering bugs and fractional indexing solutiontodos/SliderV2/slider-v2-progress.md-- SliderV2 implementation statusAF/docs/superpowers/plans/2026-04-19-fractional-block-ordering.md-- fractional indexing implementation plan