Skip to content

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
  • parent field on children referencing the container's blockName
  • getSubBlocks(block) for child discovery
  • order field 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 container

FormBlock 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 types

SliderBlock (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 appearance

SliderV2 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 integration

ConversationBlock 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 elements

GroupBlock 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 hooks

What This Consolidates

Before (current)

Each container has its own mutations in blocks.ts:

  • Form: createFormInputBlock, form-specific add/remove in addBlock/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:

  1. Not merged yet -- zero migration risk, no existing creatives to worry about
  2. Same pattern as V1 -- uses object property sub-blocks + slides[] overrides, so we know the pattern works
  3. Large codebase (~2080 lines CE, significant AF code) -- benefits most from shared infrastructure
  4. Adding new capabilities (video sub-blocks, 6 transitions) -- new code should be on new foundation
  5. 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 / removeChildBlock instead 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)
  • addChildBlock checks allowedSubBlocks to 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.

  • architecture/sub-block-patterns-comparison.md -- detailed comparison of current form/slider/group patterns
  • architecture/blocks-store-structure-analysis.md -- blocks.ts structure and extraction plan
  • todos/BlockGrouping/ordering-bugs-and-fractional-indexing.md -- the four ordering bugs and fractional indexing solution
  • todos/SliderV2/slider-v2-progress.md -- SliderV2 implementation status
  • AF/docs/superpowers/plans/2026-04-19-fractional-block-ordering.md -- fractional indexing implementation plan

Internal documentation