@fyit/crouton-flow
v0.1.0
Published
Vue Flow integration for nuxt-crouton - graph/DAG visualization for collections
Maintainers
Readme
nuxt-crouton-flow
Vue Flow integration layer for Nuxt Crouton - graph/DAG visualization with real-time multiplayer collaboration.
Features
- Graph Visualization: Render collection data as interactive node graphs
- Automatic Layout: Dagre-based tree/DAG layout for hierarchical data
- Real-time Sync: Multiplayer collaboration via Yjs CRDTs
- Presence Awareness: See other users' cursors and selections
- Position Persistence: Drag-and-drop with automatic position saving
- Custom Nodes: Use collection-specific node components
Installation
pnpm add @fyit/crouton-flowAdd to your nuxt.config.ts:
export default defineNuxtConfig({
extends: [
'@fyit/crouton',
'@fyit/crouton-flow'
]
})Basic Usage
<template>
<CroutonFlow
:rows="decisions"
collection="decisions"
parent-field="parentId"
position-field="position"
/>
</template>
<script setup lang="ts">
const { data: decisions } = await useCollectionQuery('decisions')
</script>Real-time Multiplayer Sync
Enable multiplayer collaboration with the sync prop:
<template>
<CroutonFlow
collection="decisions"
sync
:flow-id="projectId"
/>
</template>
<script setup lang="ts">
const route = useRoute()
const projectId = route.params.projectId as string
</script>How It Works
When sync is enabled:
- Yjs CRDT: All node state is managed by a Yjs document
- Durable Objects: A Cloudflare Durable Object handles WebSocket connections
- Real-time Sync: Changes are broadcast to all connected clients
- Dual Persistence: State is saved both as Yjs blob and individual rows
Prerequisites
- D1 Database: Run the migration to create the
yjs_flow_statestable:
-- server/database/migrations/0001_yjs_flow_states.sql
CREATE TABLE IF NOT EXISTS yjs_flow_states (
flow_id TEXT PRIMARY KEY,
collection_name TEXT NOT NULL,
state BLOB NOT NULL,
version INTEGER DEFAULT 1,
created_at INTEGER DEFAULT (unixepoch()),
updated_at INTEGER DEFAULT (unixepoch())
);- Wrangler Config: Configure Durable Objects in
wrangler.toml:
[[durable_objects.bindings]]
name = "FLOW_ROOMS"
class_name = "FlowRoom"
[[migrations]]
tag = "v1"
new_classes = ["FlowRoom"]- Authentication: Users must be authenticated via
useUserSession()for presence features.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| rows | Record<string, unknown>[] | - | Collection rows (not needed with sync) |
| collection | string | required | Collection name |
| parentField | string | 'parentId' | Field for parent relationships |
| positionField | string | 'position' | Field for node positions |
| labelField | string | 'title' | Field for node labels |
| sync | boolean | false | Enable real-time sync |
| flowId | string | - | Flow ID (required with sync) |
| controls | boolean | true | Show zoom controls |
| minimap | boolean | false | Show minimap |
| background | boolean | true | Show background pattern |
| draggable | boolean | true | Enable node dragging |
Events
| Event | Payload | Description |
|-------|---------|-------------|
| nodeClick | (nodeId, data) | Node clicked |
| nodeDblClick | (nodeId, data) | Node double-clicked |
| nodeMove | (nodeId, position) | Node position changed |
| edgeClick | (edgeId) | Edge clicked |
Custom Node Components
Create a [Collection]Node.vue component to customize node rendering:
<!-- app/components/DecisionsNode.vue -->
<script setup lang="ts">
defineProps<{
data: Record<string, unknown>
selected: boolean
dragging: boolean
label?: string
}>()
</script>
<template>
<div class="custom-node" :class="{ selected, dragging }">
<h3>{{ data.title }}</h3>
<p>{{ data.status }}</p>
</div>
</template>Composables
useFlowSync
Direct access to the sync state for advanced use cases:
const {
nodes, // Readonly ref of all nodes
connected, // WebSocket connected
synced, // Initial sync complete
error, // Connection error
users, // Online users
createNode, // Create a new node
updateNode, // Update node data
updatePosition, // Update node position
deleteNode, // Delete a node
selectNode, // Broadcast node selection
updateCursor, // Broadcast cursor position
} = useFlowSync({
flowId: 'my-flow',
collection: 'decisions'
})useFlowPresence
Utilities for presence UI:
const { otherUsers, getUsersSelectingNode, getNodePresenceStyle } = useFlowPresence({
users: computed(() => syncState.users),
currentUserId: currentUser.id
})Architecture
┌─────────────────────────────────────────────────────────────┐
│ Clients │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Client A │ │ Client B │ │ Client C │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────┼─────────────┘ │
│ │ WebSocket │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Cloudflare Durable Object │ │
│ │ (FlowRoom) │ │
│ │ - Manages Yjs Y.Doc per flow │ │
│ │ - Handles WebSocket connections │ │
│ │ - Merges updates from all clients │ │
│ │ - Persists to D1 on changes │ │
│ └──────────────────────┬──────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ D1 (SQLite) │ │
│ │ yjs_flow_states │ collection tables │ │
│ │ (fast reload) │ (queryable records) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘License
MIT
