Group nodes
Create groups, assign children, capture nodes by dragging, and keep z-order correct.
Groups are board nodes with type: 'group'. Children point at a group through parentId, and the engine moves the whole subtree when the group moves.
Create a group
A group stores its display name in label. Its visual color lives on the top-level color field.
const group = engine.createNode({
type: 'group',
x: 50,
y: 50,
width: 520,
height: 360,
color: '6',
label: 'Group',
})The default Vue renderer uses the color for the group border, tinted fill, and label chip.
Assign children
Set parentId when creating or updating a node:
const card = engine.createNode({
type: 'text',
x: 90,
y: 110,
width: 260,
height: 140,
parentId: group.id,
text: 'Node',
})
engine.updateNode(card.id, { parentId: undefined })Clearing parentId removes the node from the group.
Capture nodes by dragging a group
When a group is dragged over visible, unlocked nodes, the engine checks final node bounds. A node that is fully contained by the moved group becomes a child of that group.
This is the behavior users expect from spatial tools: drop a group over cards, then move the group again and the captured cards follow.
engine.select([group.id])
engine.translateSelectedNodes(320, 0)translateSelectedNodes() uses the same hierarchy logic as pointer dragging.
Wrap the current selection
Use selection helpers to create a group around selected nodes:
import { getSelectionBounds, getSelectionNodes } from '@lupinum/board-core'
function wrapSelectionInGroup(engine: BoardEngine) {
const selected = getSelectionNodes(engine)
const bounds = getSelectionBounds(engine)
if (selected.length === 0 || !bounds) {
return
}
const padding = 24
const group = engine.createNode({
type: 'group',
x: bounds.minX - padding,
y: bounds.minY - padding,
width: bounds.maxX - bounds.minX + padding * 2,
height: bounds.maxY - bounds.minY + padding * 2,
color: '5',
label: 'Group',
select: false,
})
for (const node of selected) {
engine.updateNode(node.id, { parentId: group.id })
}
engine.sendToBack(group.id)
engine.select([group.id, ...selected.map((node) => node.id)])
}The important steps are creating the group, assigning parentId, and sending
the group behind its children.
Keep groups behind children
Groups should render behind their children. Send the group behind once; the engine maintains descendant ordering as hierarchy changes:
engine.sendToBack(group.id)The engine also keeps descendants above their group during drag and front/back commands.
Nested groups
Nested groups work through the same parentId field:
const outer = engine.createNode({
type: 'group',
width: 600,
height: 420,
label: 'Group',
})
const inner = engine.createNode({
type: 'group',
x: 80,
y: 80,
width: 320,
height: 220,
parentId: outer.id,
label: 'Group',
})When a node could belong to multiple groups, containment helpers choose the smallest visible group that fully contains the node bounds.
Custom group rendering
Custom renderers receive the same node record as other node types. Read group title from node.label and color from node.color.
<script setup lang="ts">
import type { BoardNode } from '@lupinum/board-core'
const props = defineProps<{
node: BoardNode
selected: boolean
}>()
</script>
<template>
<div class="group-shell" :class="{ 'is-selected': selected }">
<span>{{ props.node.label ?? 'Untitled group' }}</span>
</div>
</template>Use CSS variables from the board shell when you want your renderer to match the built-in color system:
.group-shell {
border: 2px solid var(--board-node-color-soft, var(--board-group-border));
background: var(--board-node-tint, var(--board-group-bg));
}