Appearance
Block Grouping System
How GROUP blocks work across Application-Frontend (AF) and Creative-Engine (CE).
Overview
GROUP blocks wrap child visual elements in a positionable flex container. Use cases: scroll-sections for double midscroll, custom CSS targeting via group name, layout control with flex alignment.
Storage Pattern
In AF (runtime -- flat model)
AF uses a flat data model: group children are top-level entries in creativeBlocks with a parent field pointing to their group. This avoids the dual-data-model bugs that plagued earlier implementations (syncGroupChildren).
creativeBlocks: {
baseProperties: { ... },
blockGroupProperties-1: {
blockName: 'blockGroupProperties-1',
blockType: 'blockGroupProperties',
displayName: 'Block Group',
order: 'a0', // fractional string key
groupName: 'Block Group',
allowedSubBlocks: ['textProperties', 'buttonProperties', 'graphicProperties', 'videoProperties', 'htmlProperties'],
// NO children stored here in AF runtime
},
textProperties-1: {
blockName: 'textProperties-1',
parent: 'blockGroupProperties-1', // child of group
order: 'a0', // fractional order within group
...
},
buttonProperties-1: {
blockName: 'buttonProperties-1',
parent: 'blockGroupProperties-1',
order: 'a1',
...
},
}In CE/Backend (serialized -- nested model)
CE and backend expect children nested inside the group object with integer orders. serializeOrdersForSave() converts before save/preview:
creativeBlocks: {
blockGroupProperties-1: {
blockName: 'blockGroupProperties-1',
blockType: 'blockGroupProperties',
order: 0, // integer
textProperties-1: { blockName: 'textProperties-1', parent: 'blockGroupProperties-1', order: 0, ... },
buttonProperties-1: { blockName: 'buttonProperties-1', parent: 'blockGroupProperties-1', order: 1, ... },
}
}Data Flow (load/save boundary)
Backend JSON (nested, integers)
--> setCreativeBlocks()
--> migrateNumericOrders() // integers -> fractional string keys
--> promoteGroupChildren() // nested children -> top-level with parent field
--> AF runtime (flat, fractional)
AF runtime (flat, fractional)
--> serializeOrdersForSave() // flat -> nested, fractional -> integers
--> Backend/CE (nested, integers)Key points:
- Fractional indexing is AF-internal only. CE and backend never see fractional keys.
promoteGroupChildren()clones children before promoting to avoid mutating input.serializeOrdersForSave()deep-clones viaJSON.parse(JSON.stringify(...))so it is non-destructive.
Ordering System
Block ordering uses the fractional-indexing npm library. See src/utils/orderUtils.ts.
| Function | Purpose |
|---|---|
insertBetween(above, below) | Key between two neighbors |
insertAfter(lastKey) | Key after last item (append) |
insertBefore(firstKey) | Key before first item (prepend) |
insertNBetween(above, below, n) | N evenly-spaced keys |
compareOrder(a, b) | Sort comparator for fractional keys |
migrateNumericOrders(blocks) | Legacy integer -> fractional migration |
promoteGroupChildren(blocks) | Nested group children -> flat top-level |
serializeOrdersForSave(blocks) | Flat/fractional -> nested/integer for save |
Engine Rendering
Wrap + container + inner approach:
html
<div class="gp1-wrap"> <!-- flexbox positioning (alignment, z-index) -->
<div class="gp-block gp1"> <!-- styling (size, bg, border, shadow, animation) -->
<div class="gp-inner"> <!-- padding inset container -->
<CreativeTextBlock />
<CreativeButtonBlock />
</div>
</div>
</div>CSS override for children -- child visual elements normally render with position: absolute wraps. The group overrides this via CSS specificity:
javascript
// Group styles():
[groupSelector + ' > div']: { // Override children's wraps
position: 'relative',
width: 'auto',
height: 'auto',
pointerEvents: 'auto',
},
[groupSelector + ' > div > div']: { // Override children's inner blocks
position: 'relative',
},This works because .gp-block.gp1 > .gp-inner > div beats .t1-wrap in specificity. Children participate in flex flow without modifying their own components.
Filtering -- CreativeVisualElements.vue filters out group children with !isGroupChild(block) to prevent double-rendering. CreativeBody.vue renders group blocks separately with a tree-shaking #include guard.
Important: The #include annotation must match the actual block key: #include when blocks have "blockGroupProperties" (not "groupProperties").
Class Name Prefix
Groups use gp (not g) to avoid clashing with graphic blocks:
graphicProperties-1->g1blockGroupProperties-1->gp1
Handled in: getBlockPrefix() (engine), classNameFromBlockName() (frontend).
Builder UI (Application-Frontend)
Block List
- Multi-select: Cmd+click to toggle block selection
- Group: Toolbar button appears when 1+ blocks selected, or Shift+G keyboard shortcut
- Ungroup: Right-click a group -> "Ungroup"
- Collapse: Click chevron on group to collapse/expand children
- Drag into group: Drag a visual element onto a group's sort area
- Drag out of group: Drag a child to a top-level sort area
- Drag between groups: Atomic
moveBlockBetweenGroupsmutation (single undo snapshot) - Shortcodes on hover: Block type abbreviation displayed on hover (T1, GP1, etc.)
DnD Constraint System
CONTAINER_BLOCK_TYPES: blockGroupProperties, formProperties, sliderProperties, conversationPropertiescanDropHere(): Prevents drops across incompatible containerscanDropInContainer(): Shows disabled overlay on containers that can't accept the dragged block- Container-locked blocks: Form inputs, slider slides, conversation blocks can't leave their parent
- Extended drop zones: When container is disabled, absolute-positioned catchers allow dropping above/below
Configuration Panel
BlockGroupConfiguration.vueusingconfigurationLogicmixin- Sections: Size, BackgroundStyle, Border (color + width + radius), BoxShadow, Padding, FlexAlignment
- Alignment controls group position within the creative (on the wrap), not children layout
Store Mutations
| Mutation | Purpose |
|---|---|
createBlockGroup({ selectedBlocks, groupName }) | Create group from selected blocks |
ungroupBlocks(groupId) | Dissolve group, promote children to top-level |
moveBlockToGroup({ blockName, groupId, targetOrder }) | Move standalone block into group |
moveBlockOutOfGroup({ blockName, targetOrder }) | Move child out of group (auto-deletes empty group) |
moveBlockBetweenGroups({ blockName, targetGroupId, targetOrder }) | Atomic cross-group move (single undo snapshot) |
Undo/Redo
- Snapshot-based:
pushUndoSnapshot()storescloneDeep(creativeBlocks)before each mutation - Covers: add, remove, duplicate, reorder, group, ungroup, move
- Tab-scoped: stack cleared when leaving Design tab to avoid conflicts with CavaiFlow
Key Files
| File | Purpose |
|---|---|
AF/src/utils/orderUtils.ts | Fractional indexing utilities, serialization, migration |
AF/src/store/modules/blocks.ts | All group/ordering mutations, sortedBlocks getter, undo/redo |
AF/src/pages/.../Blocks/BlocksList.vue | DnD with groups, multi-select, keyboard shortcuts |
AF/src/pages/.../Blocks/BlockItem.vue | Shortcodes on hover, selection UI |
AF/src/pages/.../Blocks/SortArea.vue | Drop zones with parent/side props |
AF/src/pages/.../Blocks/BlockActionsMenu.vue | Context menu (group, ungroup, bring forward, etc.) |
AF/src/pages/.../Configuration/configs/BlockGroupConfiguration.vue | Group config panel |
AF/src/pages/.../Blocks/data/defaults.ts | blockGroupDefaults |
AF/src/pages/.../Blocks/data/types.ts | BlockGroupProperties type |
CE/src/components/creative/CreativeGroupBlock/CreativeGroupBlock.vue | Engine group component |
CE/src/components/conversationflow/CreativeBody.vue | Renders groups with tree-shaking guard |
CE/src/components/creative/VisualElements/CreativeVisualElements.vue | Filters out group children |
CE/src/utils/constants.ts | BLOCKS.GROUP = 'blockGroupProperties' |
Limitations (v1)
- No nested groups (group inside group) -- prevented by
moveBlockToGroupguard +canDropHere - Only visual elements can be grouped (text, graphic, button, video, html) -- not form, slider, conversation
- Group children are not targetable by change operators
Related Docs
architecture/block-ordering-and-reordering-system.md-- pre-fractional ordering analysis (historical)architecture/sub-block-patterns-comparison.md-- how group sub-blocks compare to form and slider patternsarchitecture/blocks-store-structure-analysis.md-- blocks.ts structuretodos/BlockGrouping/feasibility.md-- original design spectodos/BlockGrouping/ordering-bugs-and-fractional-indexing.md-- bug analysis that led to fractional indexing