Skip to main content

Mind map

Spatial topic hierarchy with connections, custom renderers, and branch-from-selection.

Select a topic, then add a branch from it.

What happens

The root topic sits at the center with branches radiating outward. Each topic is a custom renderer that styles itself based on depth (root, branch, leaf). Connections use bezier routing to draw the edges between parent and child topics.

Select any topic and click "Add branch" to grow the map from that point. The new node auto-connects to its parent using the connections plugin. History tracks every change so you can undo freely.

Topic text stays as topic text. The renderer gets depth from the connection graph, so branch styling is derived from the map structure instead of hidden in the first line of node.text.

The code

app.vue
app.vue
<script setup lang="ts">
import { computed } from 'vue'
import { asNodeId, createBoardEngine } from '@lupinum/board-core'
import { connectionsPlugin } from '@lupinum/board-connections'
import { BoardConnectionLayer } from '@lupinum/board-connections/vue'
import { historyPlugin } from '@lupinum/board-history'
import TopicNode from './TopicNode.vue'

const ROOT_ID = asNodeId('root')
const engine = createBoardEngine({
  grid: { size: 20, snap: true, pattern: 'dot' },
  plugins: [historyPlugin(), connectionsPlugin({ routing: 'bezier' })],
})

const topicDepthsById = computed(() => {
  const depths = new Map([[ROOT_ID, 0]])
  for (const edge of engine.plugins.connections.getEdges()) {
    const fromDepth = depths.get(edge.from)
    if (fromDepth !== undefined) {
      depths.set(edge.to, fromDepth + 1)
    }
  }
  return depths
})
</script>

<template>
  <BoardRoot :engine="engine" style="height: 100vh">
    <template #node:text="{ node, selected }">
      <TopicNode
        :node="node"
        :selected="selected"
        :depth="topicDepthsById.get(node.id) ?? 2"
      />
    </template>
    <BoardConnectionLayer routing="bezier" />
  </BoardRoot>
</template>
TopicNode.vue
<script setup lang="ts">
import { computed } from 'vue'

const props = defineProps<{
  node: { text?: string }
  depth: number
}>()

const topic = computed(() => {
  const [title = 'Topic', detail = ''] = props.node.text?.split('\n') ?? []
  return { title, detail }
})
</script>

<template>
  <div class="topic-card" :class="depthClass(depth)">
    <strong>{{ topic.title }}</strong>
    <p>
      {{ topic.detail }}
    </p>
  </div>
</template>

Try these things

  • Select the root topic, then add a branch. The new node connects automatically.
  • Drag a branch away from the center. The bezier edge stretches to follow.
  • Undo a few times to watch the map shrink back.
  • Zoom out to see the full map structure.