Appearance
Block Grouping — Feasibility & Design Spec
Date: 2026-04-08 GitHub Issue: #1805 Status: Design approved, ready for implementation planning
Summary
Add a GROUP block type that wraps child blocks in a named <div> with a CSS class. Groups have size, position, and flex alignment. This enables scroll-sections (double midscroll), shared animation (future), and custom CSS targeting.
MVP Scope
Included
GROUPblock type withgroupName, size, position, flex alignment- Multi-select in block list (Cmd/Ctrl+click)
- Group/Ungroup via context menu and keyboard shortcut (Cmd+G)
- Collapsible group rendering in block list with drag in/out
CreativeGroupBlock.vuein engine — renders wrapper div with CSS class fromgroupName- Config panel: name, width, height, top, left, justify-content, align-items
Not included (future)
- Nesting (groups inside groups) — test if trivial during implementation
- Group-level animation / childAnimation / stagger
- Background color, overflow, opacity, border, etc.
- Scrub-on-input (blocked by theming branch)
Data Model
New block type
blocks.ts constants:
typescript
GROUP: 'groupProperties'types.ts:
typescript
interface GroupProperties extends BlockBase {
blockType: 'groupProperties'
groupName: string // User-chosen name → sanitized CSS class
width: string // Default: '100%'
height: string // Default: '100%'
top: string // Default: '0'
left: string // Default: '0'
justifyContent: string // Default: 'flex-start'
alignItems: string // Default: 'flex-start'
allowedSubBlocks: ['*'] // All block types allowed as children
subBlocksReorderable: true
}Parent/child relationship
Uses the existing parent field on BlockBase and the nested property storage pattern (same as form inputs inside formProperties). Children are stored as properties of the group block object, not as top-level entries:
creativeBlocks: {
"groupProperties-1": {
groupName: "section-1", order: 0, ...,
"textProperties-1": { parent: "groupProperties-1", order: 0, ... },
"graphicProperties-1": { parent: "groupProperties-1", order: 1, ... },
},
"buttonProperties-1": { order: 0, ... } // ungrouped, top-level
}This matches how the engine's getSubBlocks() function discovers children (filters object properties with blockName field) and how the builder's BlocksList renders nested sub-blocks.
No backend changes — stored in creativeBlob JSON like all other blocks.
Engine Rendering
New component: CreativeGroupBlock.vue
html
<div :class="[blockClassNames.wrap, sanitizedGroupName]"
:style="groupStyles">
<!-- Reuse CreativeVisualElements with a group filter prop, or inline the same pattern -->
<creative-text-block v-for="block in childTextBlocks" :block="block" />
<creative-graphic-block v-for="block in childGraphicBlocks" :block="block" />
<creative-button-block v-for="block in childButtonBlocks" :block="block" />
<creative-html-block v-for="block in childHtmlBlocks" :block="block" />
</div>sanitizedGroupName:groupNamesanitized to valid CSS class (lowercase, hyphens, no spaces)groupStyles: computed withposition: relative, width, height, top, left, display: flex, justifyContent, alignItems- Children filtered from
creativeBlockswhereblock.parent === this.blockName - Ordered by child
orderfield
Changes to existing rendering
CreativeBody.vue: Add group blocks to the rendering loop alongside conversation, video, slider, form blocks. Add // #include when blocks have "groupProperties" guard so group code is tree-shaken from creatives that don't use groups.
CreativeVisualElements.vue: Add a group-specific filter to the visualElements computed. The filter must check if block.parent matches a group block specifically (e.g., isGroupChild(block)), not just any parent — because parent is already used by other block relationships (form inputs, slider sub-blocks, expandable blocks). Blocks with a group parent are skipped here and rendered inside their group instead.
GROUP is NOT a visual element: Do not add GROUP to the VISUAL_ELEMENTS constant. Groups are containers, not visual elements.
Child positioning: Children inside a group are positioned relative to the group div (since the group has position: relative). When ungrouping, child position values remain unchanged — the user may need to adjust them manually. This is the expected Figma-like behavior.
Key principle: blocks with a group parent render inside the group div, not at the top level. Ungrouped blocks render as they do today.
Builder UI
Multi-select (new capability)
Cmd/Ctrl + clickto toggle block selection in block list- New store state:
selectedBlocks: string[]— coexists with existingselectedBlockPath(single selection for config panel).selectedBlocksis for multi-select actions only;selectedBlockPathcontinues to drive which config panel is shown. - Selected blocks get visual highlight
- Useful beyond grouping (future: bulk delete, bulk hide, etc.)
Group action
- Select 2+ blocks → right-click → "Group" (or Cmd+G)
- Creates
groupProperties-Nwith default name "Group N" - Sets
parenton all selected blocks - Group defaults to 100% width/height of creative
Ungroup action
- Right-click group → "Ungroup"
- Removes
parentfrom all children - Deletes the group block
Block list rendering
▾ Group 1 ← collapsible, draggable
Text Block 1 ← indented, draggable within group
Graphic Block 1
▾ Group 2
Text Block 2
Button Block 1
Video Block ← ungrouped, draggable as today- Drag blocks in/out of groups to change parent
- Drag entire group to change z-order
- Collapse/expand with arrow icon (same pattern as form blocks today)
Config panel
When a group is selected, show config with:
- Name — text input, updates
groupNameand CSS class - Size — width/height (standard InputField)
- Position — top/left (standard InputField)
- Alignment — justifyContent/alignItems (OptionList or InputSelect)
Changes Per Repo
Application-Frontend
src/constants/blocks.ts— addGROUPconstantsrc/pages/Chatbots/components/BuilderVisuals/Blocks/data/types.ts— addGroupPropertiestypesrc/store/modules/blocks.ts— multi-select state, group/ungroup mutations, filter grouped blocks from top-levelsrc/pages/Chatbots/components/BuilderVisuals/Blocks/BlocksList.vue— multi-select UI, group context menu, collapsible group renderingsrc/pages/Chatbots/components/BuilderVisuals/Blocks/BlockItem.vue— multi-select click handling, group drag targets- New config component for group blocks (name, size, position, alignment sections)
Creative-Engine
- New
CreativeGroupBlock.vuecomponent src/components/conversationflow/CreativeBody.vue— render group blocks in loopsrc/components/creative/VisualElements/CreativeVisualElements.vue— skip blocks with group parentsrc/utils/blockUtils.ts— group class name generation if needed
Application-Backend
- No changes
Nesting (stretch goal)
If implementation is trivial, test groups inside groups. The parent field already supports any block as parent. Main considerations:
- Recursive child filtering in engine
- Indentation depth in block list UI
- Config panel shows correct parent context
Risk Assessment
Low risk: The parent field, allowedSubBlocks, subBlocksReorderable, and sub-block rendering patterns all exist. Group is conceptually "a block without visual content that wraps children in a div."
Medium risk: Multi-select is new UI behavior. Drag-in/out of groups needs careful UX for drop zones.
Lessons Learned (post-implementation)
selectedBlockPathgoes stale when blocks move between containers. Any mutation that relocates a block (groupBlocks,ungroupBlocks,moveBlockToGroup,moveBlockOutOfGroup) must rewriteselectedBlockPathto match the block's new location. Config components retain the stale path otherwise.updateBlockValueuseslodash.set, which silently creates missing keys. A stale path like'buttonProperties-1.isStatePreview'(after the button was moved into a group) will produce a ghost top-levelbuttonProperties-1: { isStatePreview: false }. On save+reload,setCreativeBlocksthen runsdefaultsDeepand fills in the full default shape — visible as a duplicate block with emptydisplayNameand the defaultparent. Guard: reject paths whose root key isn't increativeBlocks.fixBlockNameMismatchesmust cover every sub-block container. It already handledformPropertiesandsliderProperties; groups needed the same treatment.- i18n key lookup uses
blockType, notblockName. Keys arevisuals.blocks.buttonProperties, notvisuals.blocks.buttonProperties-1.BlockItem.vuename fallback must useblock.blockType. generateVisualElementNamedisplay-name switch must list every type. Missing cases (VIDEO, CONVERSATION, SLIDER, FORM, AR, GROUP) produced empty display names.
No risk: Backend is unchanged — group blocks are just another entry in the creativeBlob JSON.