docs: use vitepress

This commit is contained in:
braks
2022-07-28 00:09:56 +02:00
committed by Braks
parent bd36253cd0
commit e6bef4a9f1
140 changed files with 8166 additions and 0 deletions

2
docs2/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.data.json
.temp

110
docs2/components/Repl.vue Normal file
View File

@@ -0,0 +1,110 @@
<script lang="ts" setup>
import { Repl, ReplStore } from '@vue/repl'
import { useVueFlow } from '@braks/vue-flow'
import '@vue/repl/style.css'
import { exampleImports } from './examples'
const props = defineProps<{ example: keyof typeof exampleImports; mainFile?: string; dependencies?: Record<string, string> }>()
const { vueFlowVersion } = useVueFlow()
let css = `@import 'https://cdn.jsdelivr.net/npm/@braks/vue-flow@${vueFlowVersion.value}/dist/style.css';
@import 'https://cdn.jsdelivr.net/npm/@braks/vue-flow@${vueFlowVersion.value}/dist/theme-default.css';
html,
body,
#app {
margin: 0;
height: 100%;
}
#app {
text-transform: uppercase;
font-family: 'JetBrains Mono', monospace;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
.vue-flow__minimap {
transform: scale(75%);
transform-origin: bottom right;
}
\n`
const store = new ReplStore({
showOutput: true,
outputMode: 'preview',
})
watchEffect(
() => {
document.documentElement.style.setProperty('--vh', `${window.innerHeight}px`)
},
{ flush: 'post' },
)
const files: any = {}
const imports = exampleImports[props.example]
for (const example of Object.keys(imports)) {
if (example.includes('css')) {
css += `${imports[example as keyof typeof imports]}`
} else {
files[example] = imports[example as keyof typeof imports]
}
}
onMounted(async () => {
await store.setVueVersion('3.2.25')
await store.setFiles(
{
...files,
'main.css': css,
},
props.mainFile ?? 'App.vue',
)
// pre-set import map
store.setImportMap({
imports: {
'@braks/vue-flow': `${location.origin}/vue-flow.es.js`,
},
})
})
const sfcOptions = {
script: {
reactivityTransform: true,
},
}
</script>
<template>
<Repl
:clear-console="true"
:auto-resize="true"
:store="store"
:show-compile-output="false"
:show-import-map="false"
:sfc-options="sfcOptions"
@keydown.ctrl.s.prevent
@keydown.meta.s.prevent
/>
</template>
<style>
.file-selector {
@apply scrollbar scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-green-500 scrollbar-track-black;
}
.vue-repl {
@apply rounded-lg border-1 border-solid dark:border-gray-200/10 border-gray-200;
height: calc(var(--vh) - var(--navbar-height));
}
.msg.err {
display: none;
}
</style>

View File

@@ -0,0 +1,84 @@
<script setup>
import { Background, Controls, MiniMap, VueFlow, isNode, useVueFlow } from '@braks/vue-flow'
import { ref } from 'vue'
import { initialElements } from './initial-elements.js'
/**
* useVueFlow provides all event handlers and store properties
* You can pass the composable an object that has the same properties as the VueFlow component props
*/
const { onPaneReady, onNodeDragStop, onConnect, addEdges, setTransform, toObject } = useVueFlow()
/**
* Our elements
*/
const elements = ref(initialElements)
/**
* This is a Vue Flow event-hook which can be listened to from anywhere you call the composable, instead of only on the main component
*
* onPaneReady is called when viewpane & nodes have visible dimensions
*/
onPaneReady(({ fitView }) => {
fitView()
})
onNodeDragStop((e) => console.log('drag stop', e))
/**
* onConnect is called when a new connection is created.
* You can add additional properties to your new edge (like a type or label) or block the creation altogether
*/
onConnect((params) => addEdges([params]))
const dark = ref(false)
/**
* To update node properties you can simply use your elements v-model and mutate the elements directly
* Changes should always be reflected on the graph reactively, without the need to overwrite the elements
*/
const updatePos = () =>
elements.value.forEach((el) => {
if (isNode(el)) {
el.position = {
x: Math.random() * 400,
y: Math.random() * 400,
}
}
})
/**
* toObject transforms your current graph data to an easily persist-able object
*/
const logToObject = () => console.log(toObject())
/**
* Resets the current viewpane transformation (zoom & pan)
*/
const resetTransform = () => setTransform({ x: 0, y: 0, zoom: 1 })
const toggleClass = () => {
dark.value = !dark.value
elements.value.forEach((el) => (el.class = dark.value ? 'dark' : 'light'))
}
</script>
<template>
<VueFlow v-model="elements" class="basicflow" :default-zoom="1.5" :min-zoom="0.2" :max-zoom="4">
<Background pattern-color="#aaa" gap="8" />
<MiniMap />
<Controls />
<div class="controls">
<button style="background-color: #113285; color: white" @click="resetTransform">reset transform</button>
<button style="background-color: #6f3381; color: white" @click="updatePos">update positions</button>
<button
:style="{ backgroundColor: dark ? '#FFFFFB' : '#1C1C1C', color: dark ? '#1C1C1C' : '#FFFFFB' }"
@click="toggleClass"
>
toggle {{ dark ? 'light' : 'dark' }}
</button>
<button @click="logToObject">log toObject</button>
</div>
</VueFlow>
</template>

View File

@@ -0,0 +1,3 @@
export { default as BasicApp } from './App.vue?raw'
export { default as BasicElements } from './initial-elements.js?raw'
export { default as BasicCSS } from './style.css'

View File

@@ -0,0 +1,25 @@
import { MarkerType } from '@braks/vue-flow'
/**
* You can pass elements together as a v-model value
* or split them up into nodes and edges and pass them to the `nodes` and `edges` props of Vue Flow (or useVueFlow composable)
*/
export const initialElements = [
{ id: '1', type: 'input', label: 'Node 1', position: { x: 250, y: 5 }, class: 'light' },
{ id: '2', type: 'output', label: 'Node 2', position: { x: 100, y: 100 }, class: 'light' },
{ id: '3', label: 'Node 3', position: { x: 400, y: 100 }, class: 'light' },
{ id: '4', label: 'Node 4', position: { x: 150, y: 200 }, class: 'light' },
{ id: '5', type: 'output', label: 'Node 5', position: { x: 300, y: 300 }, class: 'light' },
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', label: 'edge with arrowhead', source: '1', target: '3', markerEnd: MarkerType.Arrow },
{
id: 'e4-5',
type: 'step',
label: 'step-edge',
source: '4',
target: '5',
style: { stroke: 'orange' },
labelBgStyle: { fill: 'orange' },
},
{ id: 'e3-4', type: 'smoothstep', label: 'smoothstep-edge', source: '3', target: '4' },
]

View File

@@ -0,0 +1,30 @@
.basicflow .vue-flow__node.dark {
background: #1C1C1C;
color: #FFFFFB;
}
.basicflow .controls {
position: absolute;
left: 10px;
top: 10px;
z-index: 4;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
.basicflow .controls button {
padding: 5px;
border-radius: 5px;
font-weight: 500;
-webkit-box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3);
box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3);
cursor: pointer;
}
.basicflow .controls button:hover {
opacity: 0.8;
transform: scale(105%);
transition: 250ms all ease;
}

View File

@@ -0,0 +1,24 @@
<script setup>
import { Background, BackgroundVariant, VueFlow } from '@braks/vue-flow'
import { ref } from 'vue'
import CustomConnectionLine from './CustomConnectionLine.vue'
const elements = ref([
{
id: '1',
type: 'input',
label: 'Node 1',
position: { x: 250, y: 5 },
},
])
</script>
<template>
<VueFlow v-model="elements">
<template #connection-line="props">
<CustomConnectionLine v-bind="props" />
</template>
<Background :variant="BackgroundVariant.Lines" />
</VueFlow>
</template>

View File

@@ -0,0 +1,33 @@
<script setup>
const props = defineProps({
sourceX: {
type: Number,
required: true,
},
sourceY: {
type: Number,
required: true,
},
targetX: {
type: Number,
required: true,
},
targetY: {
type: Number,
required: true,
},
})
</script>
<template>
<g>
<path
class="animated"
fill="none"
stroke="#6F3381"
:stroke-width="2.5"
:d="`M${sourceX},${sourceY} C ${sourceX} ${targetY} ${sourceX} ${targetY} ${targetX},${targetY}`"
/>
<circle :cx="targetX" :cy="targetY" fill="#fff" :r="5" stroke="#6F3381" :stroke-width="1.5" />
</g>
</template>

View File

@@ -0,0 +1,2 @@
export { default as CustomConnectionLineApp } from './App.vue?raw'
export { default as CustomConnectionLine } from './CustomConnectionLine.vue?raw'

View File

@@ -0,0 +1,117 @@
<script setup>
import { ConnectionMode, MiniMap, Position, VueFlow, useVueFlow } from '@braks/vue-flow'
import { computed, h, onMounted, ref } from 'vue'
import ColorSelectorNode from './CustomNode.vue'
import { presets } from './presets.js'
const { getNode } = useVueFlow()
const outputColorNode = computed(() => getNode.value('3'))
const elements = ref([])
const gradient = ref(false)
const bgColor = ref(presets.ayame)
const bgName = ref('AYAME')
const connectionLineStyle = { stroke: '#fff' }
// minimap stroke color functions
const nodeStroke = (n) => {
if (n.type === 'input') return '#0041d0'
if (n.type === 'custom') return presets.sumi
if (n.type === 'output') return '#ff0072'
return '#eee'
}
const nodeColor = (n) => {
if (n.type === 'custom') return bgColor.value
return '#fff'
}
// output labels
const outputColorLabel = () => h('div', {}, bgColor.value)
const outputNameLabel = () => h('div', {}, bgName.value)
const onChange = (color) => {
gradient.value = false
bgColor.value = color.value
bgName.value = color.name
outputColorNode.value.hidden = false
}
const onGradient = () => {
gradient.value = true
bgColor.value = null
bgName.value = 'gradient'
outputColorNode.value.hidden = true
}
onMounted(() => {
elements.value = [
{
id: '1',
type: 'custom',
data: { color: bgColor },
position: { x: 0, y: 50 },
},
{
id: '2',
type: 'output',
label: outputNameLabel,
position: { x: 350, y: 25 },
targetPosition: Position.Left,
},
{
id: '3',
type: 'output',
label: outputColorLabel,
position: { x: 350, y: 200 },
targetPosition: Position.Left,
},
{
id: 'e1a-2',
source: '1',
sourceHandle: 'a',
target: '2',
animated: true,
style: () => ({
stroke: bgColor.value,
filter: 'invert(100%)',
}),
},
{
id: 'e1b-3',
source: '1',
sourceHandle: 'b',
target: '3',
animated: true,
style: () => ({
stroke: bgColor.value,
filter: 'invert(100%)',
}),
},
]
})
</script>
<template>
<VueFlow
v-model="elements"
class="customnodeflow"
:class="[gradient ? 'animated-bg-gradient' : '']"
:style="{ backgroundColor: bgColor }"
:connection-mode="ConnectionMode.Loose"
:connection-line-style="connectionLineStyle"
:default-zoom="1.5"
:fit-view-on-init="true"
>
<template #node-custom="props">
<ColorSelectorNode :data="props.data" @change="onChange" @gradient="onGradient" />
</template>
<MiniMap :node-stroke-color="nodeStroke" :node-color="nodeColor" />
</VueFlow>
</template>

View File

@@ -0,0 +1,59 @@
<script setup>
import { Handle, Position } from '@braks/vue-flow'
import { computed } from 'vue'
import { presets } from './presets.js'
const props = defineProps({
data: {
type: Object,
required: true,
},
})
const emit = defineEmits(['change', 'gradient'])
const onConnect = (params) => console.log('handle onConnect', params)
const onSelect = (color) => {
emit('change', color)
}
const onGradient = () => {
emit('gradient')
}
const colors = computed(() => {
return Object.keys(presets).map((color) => {
return {
name: color,
value: presets[color],
}
})
})
const selectedColor = computed(() => {
return colors.value.find((color) => color.value === props.data.color)
})
const sourceHandleStyleA = computed(() => ({ backgroundColor: props.data.color, filter: 'invert(100%)', top: '10px' }))
const sourceHandleStyleB = computed(() => ({
backgroundColor: props.data.color,
filter: 'invert(100%)',
bottom: '10px',
top: 'auto',
}))
</script>
<template>
<div>Select a color</div>
<div
style="display: flex; flex-direction: row; flex-wrap: wrap; justify-content: center; max-width: 90%; margin: auto; gap: 3px"
>
<template v-for="color of colors" :key="color.name">
<button :title="color.name" :style="{ backgroundColor: color.value }" type="button" @click="onSelect(color)"></button>
</template>
<button class="animated-bg-gradient" title="gradient" type="button" @click="onGradient"></button>
</div>
<Handle id="a" type="source" :position="Position.Right" :style="sourceHandleStyleA" />
<Handle id="b" type="source" :position="Position.Right" :style="sourceHandleStyleB" />
</template>

View File

@@ -0,0 +1,4 @@
export { default as CustomNodeApp } from './App.vue?raw'
export { default as CustomNode } from './CustomNode.vue?raw'
export { default as CustomNodeCSS } from './style.css'
export { default as ColorPresets } from './presets.js?raw'

View File

@@ -0,0 +1,23 @@
export const presets = {
sumi: '#1C1C1C',
gofun: '#FFFFFB',
byakuroku: '#A8D8B9',
mizu: '#81C7D4',
asagi: '#33A6B8',
ukon: '#EFBB24',
mushikuri: '#D9CD90',
hiwa: '#BEC23F',
ichigo: '#B5495B',
kurenai: '#CB1B45',
syojyohi: '#E83015',
konjyo: '#113285',
fuji: '#8B81C3',
ayame: '#6F3381',
torinoko: '#DAC9A6',
kurotsurubami: '#0B1013',
ohni: '#F05E1C',
kokikuchinashi: '#FB9966',
beniukon: '#E98B2A',
sakura: '#FEDFE1',
toki: '#EEA9A9',
}

View File

@@ -0,0 +1,73 @@
.customnodeflow .vue-flow__node-custom {
border: 1px solid #777;
padding: 10px;
border-radius: 10px;
background: whitesmoke;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 10px;
max-width: 250px;
}
.customnodeflow button {
padding: 5px;
width: 25px;
height: 25px;
border-radius: 25px;
-webkit-box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3);
box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3);
cursor: pointer;
}
.customnodeflow button:hover {
opacity: 0.9;
transform: scale(105%);
transition: 250ms all ease;
}
.animated-bg-gradient {
background: linear-gradient(122deg, #6f3381, #81c7d4, #fedfe1, #fffffb);
background-size: 800% 800%;
-webkit-animation: gradient 4s ease infinite;
-moz-animation: gradient 4s ease infinite;
animation: gradient 4s ease infinite;
}
@-webkit-keyframes gradient {
0% {
background-position: 0% 22%
}
50% {
background-position: 100% 79%
}
100% {
background-position: 0% 22%
}
}
@-moz-keyframes gradient {
0% {
background-position: 0% 22%
}
50% {
background-position: 100% 79%
}
100% {
background-position: 0% 22%
}
}
@keyframes gradient {
0% {
background-position: 0% 22%
}
50% {
background-position: 100% 79%
}
100% {
background-position: 0% 22%
}
}

View File

@@ -0,0 +1,45 @@
<script setup>
import { VueFlow, useVueFlow } from '@braks/vue-flow'
import Sidebar from './Sidebar.vue'
let id = 0
const getId = () => `dndnode_${id++}`
const { onConnect, nodes, edges, addEdges, addNodes, viewport, project } = useVueFlow({
nodes: [
{
id: '1',
type: 'input',
label: 'input node',
position: { x: 250, y: 25 },
},
],
})
const onDragOver = (event) => {
event.preventDefault()
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move'
}
}
onConnect((params) => addEdges([params]))
const onDrop = (event) => {
const type = event.dataTransfer?.getData('application/vueflow')
const position = project({ x: event.clientX - 40, y: event.clientY - 18 })
const newNode = {
id: getId(),
type,
position,
label: `${type} node`,
}
addNodes([newNode])
}
</script>
<template>
<div class="dndflow" @drop="onDrop">
<VueFlow @dragover="onDragOver" />
<Sidebar />
</div>
</template>

View File

@@ -0,0 +1,19 @@
<script setup>
const onDragStart = (event, nodeType) => {
if (event.dataTransfer) {
event.dataTransfer.setData('application/vueflow', nodeType)
event.dataTransfer.effectAllowed = 'move'
}
}
</script>
<template>
<aside>
<div class="description">You can drag these nodes to the pane.</div>
<div class="nodes">
<div class="vue-flow__node-input" :draggable="true" @dragstart="onDragStart($event, 'input')">Input Node</div>
<div class="vue-flow__node-default" :draggable="true" @dragstart="onDragStart($event, 'default')">Default Node</div>
<div class="vue-flow__node-output" :draggable="true" @dragstart="onDragStart($event, 'output')">Output Node</div>
</div>
</aside>
</template>

View File

@@ -0,0 +1,3 @@
export { default as DndApp } from './App.vue?raw'
export { default as DndSidebar } from './Sidebar.vue?raw'
export { default as DndCSS } from './style.css'

View File

@@ -0,0 +1,51 @@
.dndflow {
flex-direction: column;
display: flex;
height: 100%;
}
.dndflow aside {
color: white;
font-weight: 700;
border-right: 1px solid #eee;
padding: 15px 10px;
font-size: 12px;
background: rgba(16, 185, 129, 0.75);
-webkit-box-shadow: 0px 5px 10px 0px rgba(0,0,0,0.3);
box-shadow: 0px 5px 10px 0px rgba(0,0,0,0.3);
}
.dndflow aside .nodes > * {
margin-bottom: 10px;
cursor: grab;
font-weight: 500;
-webkit-box-shadow: 5px 5px 10px 2px rgba(0,0,0,0.25);
box-shadow: 5px 5px 10px 2px rgba(0,0,0,0.25);
}
.dndflow aside .description {
margin-bottom: 10px;
}
.dndflow .vue-flow-wrapper {
flex-grow: 1;
height: 100%;
}
@media screen and (min-width: 640px) {
.dndflow {
flex-direction: row;
}
.dndflow aside {
min-width: 25%;
}
}
@media screen and (max-width: 639px) {
.dndflow aside .nodes {
display: flex;
flex-direction: row;
gap: 5px;
}
}

View File

@@ -0,0 +1,74 @@
<script setup>
import { Background, Controls, MarkerType, MiniMap, VueFlow } from '@braks/vue-flow'
import { h, ref } from 'vue'
import CustomEdge from './CustomEdge.vue'
import CustomEdge2 from './CustomEdge2.vue'
import CustomEdgeLabel from './CustomEdgeLabel.vue'
const elements = ref([
{ id: '1', type: 'input', label: 'Start', position: { x: 50, y: 0 }, style: { borderColor: '#10b981' } },
{ id: '2', label: 'Node 2', position: { x: 150, y: 100 } },
{ id: '2a', label: 'Node 2a', position: { x: 0, y: 180 } },
{ id: '3', label: 'Node 3', position: { x: 250, y: 200 } },
{ id: '4', label: 'Node 4', position: { x: 400, y: 300 } },
{ id: '3a', label: 'Node 3a', position: { x: 175, y: 300 } },
{ id: '5', label: 'Node 5', position: { x: 200, y: 400 } },
{ id: '6', type: 'output', label: 'Output 6', position: { x: 0, y: 350 } },
{ id: '7', type: 'output', label: 'Output 7', position: { x: 50, y: 600 } },
{ id: '8', type: 'output', label: 'Output 8', position: { x: 350, y: 600 } },
{ id: '9', type: 'output', label: 'Output 9', position: { x: 550, y: 400 } },
{ id: 'e1-2', source: '1', target: '2', label: 'bezier edge (default)', class: 'normal-edge' },
{ id: 'e2-2a', source: '2', target: '2a', type: 'smoothstep', label: 'smoothstep edge' },
{ id: 'e2-3', source: '2', target: '3', type: 'step', label: 'step edge' },
{ id: 'e3-4', source: '3', target: '4', type: 'straight', label: 'straight edge' },
{ id: 'e3-3a', source: '3', target: '3a', type: 'straight', label: 'label only edge', style: { stroke: 'none' } },
{ id: 'e3-5', source: '4', target: '5', animated: true, label: 'animated styled edge', style: { stroke: '#10b981' } },
{
id: 'e2a-6',
source: '2a',
target: '6',
label: () => h(CustomEdgeLabel, { label: 'custom label text' }),
labelStyle: { fill: '#10b981', fontWeight: 700 },
markerEnd: MarkerType.Arrow,
},
{
id: 'e5-7',
source: '5',
target: '7',
label: 'label with bg',
labelBgPadding: [8, 4],
labelBgBorderRadius: 4,
labelBgStyle: { fill: '#FFCC00', color: '#fff', fillOpacity: 0.7 },
markerEnd: MarkerType.ArrowClosed,
},
{
id: 'e5-8',
source: '5',
target: '8',
type: 'custom',
data: { text: 'custom edge' },
markerEnd: MarkerType.ArrowClosed,
},
{
id: 'e4-9',
source: '4',
target: '9',
type: 'custom2',
data: { text: 'styled custom edge label' },
},
])
</script>
<template>
<VueFlow v-model="elements" :fit-view-on-init="true" :snap-to-grid="true">
<template #edge-custom="props">
<CustomEdge v-bind="props" />
</template>
<template #edge-custom2="props">
<CustomEdge2 v-bind="props" />
</template>
<MiniMap />
<Controls />
<Background />
</VueFlow>
</template>

View File

@@ -0,0 +1,100 @@
<script setup>
import { getBezierPath, getEdgeCenter, useVueFlow } from '@braks/vue-flow'
import { computed } from 'vue'
const props = defineProps({
id: {
type: String,
required: true,
},
sourceX: {
type: Number,
required: true,
},
sourceY: {
type: Number,
required: true,
},
targetX: {
type: Number,
required: true,
},
targetY: {
type: Number,
required: true,
},
sourcePosition: {
type: String,
required: true,
},
targetPosition: {
type: String,
required: true,
},
data: {
type: Object,
required: false,
},
markerEnd: {
type: String,
required: false,
},
style: {
type: Object,
required: false,
},
})
const { applyEdgeChanges } = useVueFlow()
const foreignObjectSize = 40
const onClick = (evt, id) => {
applyEdgeChanges([{ type: 'remove', id }])
evt.stopPropagation()
}
const edgePath = computed(() =>
getBezierPath({
sourceX: props.sourceX,
sourceY: props.sourceY,
sourcePosition: props.sourcePosition,
targetX: props.targetX,
targetY: props.targetY,
targetPosition: props.targetPosition,
}),
)
const center = computed(() =>
getEdgeCenter({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
}),
)
</script>
<script>
export default {
inheritAttrs: false,
}
</script>
<template>
<path :id="id" :style="style" class="vue-flow__edge-path" :d="edgePath" :marker-end="markerEnd" />
<foreignObject
:width="foreignObjectSize"
:height="foreignObjectSize"
:x="center[0] - foreignObjectSize / 2"
:y="center[1] - foreignObjectSize / 2"
class="edgebutton-foreignobject"
requiredExtensions="http://www.w3.org/1999/xhtml"
>
<body style="display: flex; align-items: center; justify-content: center">
<div>
<button ref="btn" class="edgebutton" @click="(event) => onClick(event, id)">×</button>
</div>
</body>
</foreignObject>
</template>

View File

@@ -0,0 +1,97 @@
<script setup>
import { EdgeText, getBezierPath, getEdgeCenter } from '@braks/vue-flow'
import { computed } from 'vue'
const props = defineProps({
id: {
type: String,
required: true,
},
sourceX: {
type: Number,
required: true,
},
sourceY: {
type: Number,
required: true,
},
targetX: {
type: Number,
required: true,
},
targetY: {
type: Number,
required: true,
},
sourcePosition: {
type: String,
required: true,
},
targetPosition: {
type: String,
required: true,
},
data: {
type: Object,
required: false,
},
markerEnd: {
type: String,
required: false,
},
style: {
type: Object,
required: false,
},
sourceHandleId: {
type: String,
required: false,
},
targetHandleId: {
type: String,
required: false,
},
})
const edgePath = computed(() =>
getBezierPath({
sourceX: props.sourceX,
sourceY: props.sourceY,
sourcePosition: props.sourcePosition,
targetX: props.targetX,
targetY: props.targetY,
targetPosition: props.targetPosition,
}),
)
const center = computed(() =>
getEdgeCenter({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
}),
)
const onClick = () => console.log(props.data)
</script>
<script>
export default {
inheritAttrs: false,
}
</script>
<template>
<path :id="props.id" class="vue-flow__edge-path" :d="edgePath" :marker-end="props.markerEnd" />
<EdgeText
:x="center[0]"
:y="center[1]"
:label="props.data?.text"
:label-style="{ fill: 'white' }"
:label-show-bg="true"
:label-bg-style="{ fill: '#10b981' }"
:label-bg-padding="[2, 4]"
:label-bg-border-radius="2"
@click="onClick"
/>
</template>

View File

@@ -0,0 +1,12 @@
<script setup>
const props = defineProps({
label: {
type: String,
required: true,
},
})
</script>
<template>
<tspan dy="10" x="0">{{ props.label }}</tspan>
</template>

View File

@@ -0,0 +1,5 @@
export { default as EdgesApp } from './App.vue?raw'
export { default as CustomEdge } from './CustomEdge.vue?raw'
export { default as CustomEdge2 } from './CustomEdge2.vue?raw'
export { default as CustomEdgeLabel } from './CustomEdgeLabel.vue?raw'
export { default as EdgeCSS } from './style.css'

View File

@@ -0,0 +1,10 @@
.edgebutton {
border-radius: 999px;
cursor: pointer;
}
.edgebutton:hover {
transform: scale(110%);
transition: all ease 500ms;
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.5), 0 0 0 4px #10b981;
}

View File

@@ -0,0 +1,31 @@
<script setup>
import { Background, BackgroundVariant, Controls, MiniMap, VueFlow, useVueFlow } from '@braks/vue-flow'
const { nodes, addNodes, edges, addEdges, onConnect, onPaneReady, onNodeDragStop, dimensions } = useVueFlow()
onConnect((params) => addEdges([params]))
onPaneReady((flowInstance) => console.log('flow loaded:', flowInstance))
onNodeDragStop((node) => console.log('drag stop', node))
const addRandomNode = () => {
const nodeId = (nodes.value.length + 1).toString()
const newNode = {
id: nodeId,
label: `Node: ${nodeId}`,
position: { x: Math.random() * dimensions.value.width, y: Math.random() * dimensions.value.height },
}
addNodes([newNode])
}
</script>
<template>
<VueFlow>
<MiniMap />
<Controls />
<Background :variant="BackgroundVariant.Lines" />
<button type="button" :style="{ position: 'absolute', left: '10px', top: '10px', zIndex: 4 }" @click="addRandomNode">
add node
</button>
</VueFlow>
</template>

View File

@@ -0,0 +1 @@
export { default as EmptyApp } from './App.vue?raw'

View File

@@ -0,0 +1,41 @@
<script setup>
import { Controls, MiniMap, VueFlow, useVueFlow } from '@braks/vue-flow'
import { ref, watchEffect } from 'vue'
const isHidden = ref(false)
const { nodes, edges } = useVueFlow({
nodes: [
{ id: '1', type: 'input', label: 'Node 1', position: { x: 250, y: 5 } },
{ id: '2', label: 'Node 2', position: { x: 100, y: 100 } },
{ id: '3', label: 'Node 3', position: { x: 400, y: 100 } },
{ id: '4', label: 'Node 4', position: { x: 400, y: 200 } },
],
edges: [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e3-4', source: '3', target: '4' },
],
})
watchEffect(() => {
nodes.value.forEach((n) => (n.hidden = isHidden.value))
edges.value.forEach((e) => (e.hidden = isHidden.value))
})
</script>
<template>
<VueFlow>
<MiniMap />
<Controls />
<div :style="{ position: 'absolute', left: '10px', top: '10px', zIndex: 4 }">
<div>
<label for="ishidden">
hidden
<input id="ishidden" v-model="isHidden" type="checkbox" />
</label>
</div>
</div>
</VueFlow>
</template>

View File

@@ -0,0 +1 @@
export { default as HiddenApp } from './App.vue?raw'

View File

@@ -0,0 +1,27 @@
<script setup>
import { Background, Controls, MiniMap, VueFlow, useVueFlow } from '@braks/vue-flow'
import { ref } from 'vue'
import { initialElements } from './initial-elements.js'
const { onConnect, addEdges } = useVueFlow()
const elements = ref(initialElements)
onConnect((params) => addEdges([params]))
</script>
<template>
<VueFlow
v-model="elements"
class="basicflow"
:default-edge-options="{ type: 'smoothstep' }"
:default-zoom="1.5"
:min-zoom="0.2"
:max-zoom="4"
:fit-view-on-init="true"
>
<Background pattern-color="#aaa" gap="8" />
<MiniMap />
<Controls />
</VueFlow>
</template>

View File

@@ -0,0 +1,2 @@
export { default as HorizontalApp } from './App.vue?raw'
export { default as HorizontalElements } from './initial-elements.js?raw'

View File

@@ -0,0 +1,38 @@
import { MarkerType, Position } from '@braks/vue-flow'
export const initialElements = [
{
id: '1',
type: 'input',
label: 'Node 1',
position: { x: 0, y: 50 },
sourcePosition: Position.Right,
},
{
id: '2',
type: 'output',
label: 'Node 2',
position: { x: 250, y: 0 },
targetPosition: Position.Left,
},
{ id: '3', label: 'Node 3', position: { x: 250, y: 100 }, sourcePosition: Position.Right, targetPosition: Position.Left },
{ id: '4', label: 'Node 4', position: { x: 500, y: 150 }, sourcePosition: Position.Right, targetPosition: Position.Left },
{
id: '5',
type: 'output',
label: 'Node 5',
position: { x: 750, y: 50 },
sourcePosition: Position.Right,
targetPosition: Position.Left,
},
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3', markerEnd: MarkerType.Arrow },
{
id: 'e4-5',
source: '4',
target: '5',
style: { stroke: 'orange' },
labelBgStyle: { fill: 'orange' },
},
{ id: 'e3-4', source: '3', target: '4' },
]

View File

@@ -0,0 +1,100 @@
import { BasicApp, BasicCSS, BasicElements } from './basic'
import { ColorPresets, CustomNode, CustomNodeApp, CustomNodeCSS } from './custom-node'
import { CustomConnectionLine, CustomConnectionLineApp } from './connectionline'
import { CustomEdge, CustomEdge2, CustomEdgeLabel, EdgeCSS, EdgesApp } from './edges'
import { NestedApp } from './nested'
import { StressApp, StressCSS, StressUtils } from './stress'
import { UpdateEdgeApp } from './update-edge'
import { UpdateNodeApp, UpdateNodeCSS } from './update-node'
import { ValidationApp, ValidationCSS, ValidationCustomInput, ValidationCustomNode } from './validation'
import { SaveRestoreApp, SaveRestoreCSS, SaveRestoreControls } from './save-restore'
import { DndApp, DndCSS, DndSidebar } from './dnd'
import { EmptyApp } from './empty'
import { HiddenApp } from './hidden'
import { InteractionApp, InteractionCSS, InteractionControls } from './interaction'
import { MultiApp, MultiCSS, MultiFlow } from './multi'
import { HorizontalApp, HorizontalElements } from './horizontal'
import { TeleportApp, TeleportCSS, TeleportSidebar, TeleportableNode, TeleportableUseTransition } from './teleport'
export const exampleImports = {
basic: {
'App.vue': BasicApp,
'initial-elements.js': BasicElements,
'style.css': BasicCSS,
},
customNode: {
'App.vue': CustomNodeApp,
'CustomNode.vue': CustomNode,
'style.css': CustomNodeCSS,
'presets.js': ColorPresets,
},
connectionline: {
'App.vue': CustomConnectionLineApp,
'CustomConnectionLine.vue': CustomConnectionLine,
},
edges: {
'App.vue': EdgesApp,
'CustomEdge.vue': CustomEdge,
'CustomEdge2.vue': CustomEdge2,
'CustomEdgeLabel.vue': CustomEdgeLabel,
'style.css': EdgeCSS,
},
nested: {
'App.vue': NestedApp,
},
stress: {
'App.vue': StressApp,
'utils.js': StressUtils,
'style.css': StressCSS,
},
updateEdge: {
'App.vue': UpdateEdgeApp,
},
updateNode: {
'App.vue': UpdateNodeApp,
'style.css': UpdateNodeCSS,
},
validation: {
'App.vue': ValidationApp,
'CustomInput.vue': ValidationCustomInput,
'CustomNode.vue': ValidationCustomNode,
'style.css': ValidationCSS,
},
saveRestore: {
'App.vue': SaveRestoreApp,
'Controls.vue': SaveRestoreControls,
'style.css': SaveRestoreCSS,
},
dnd: {
'App.vue': DndApp,
'Sidebar.vue': DndSidebar,
'style.css': DndCSS,
},
empty: {
'App.vue': EmptyApp,
},
hidden: {
'App.vue': HiddenApp,
},
interaction: {
'App.vue': InteractionApp,
'InteractionControls.vue': InteractionControls,
'style.css': InteractionCSS,
},
multi: {
'App.vue': MultiApp,
'Flow.vue': MultiFlow,
'style.css': MultiCSS,
},
horizontal: {
'App.vue': HorizontalApp,
'initial-elements.js': HorizontalElements,
},
teleport: {
'App.vue': TeleportApp,
'Sidebar.vue': TeleportSidebar,
'TeleportableNode.vue': TeleportableNode,
'useTransition.js': TeleportableUseTransition,
'style.css': TeleportCSS,
},
}

View File

@@ -0,0 +1,20 @@
<script setup>
import { VueFlow } from '@braks/vue-flow'
import { ref } from 'vue'
import InteractionControls from './InteractionControls.vue'
const elements = ref([
{ id: '1', type: 'input', label: 'Node 1', position: { x: 250, y: 5 } },
{ id: '2', label: 'Node 2', position: { x: 100, y: 100 } },
{ id: '3', label: 'Node 3', position: { x: 400, y: 100 } },
{ id: '4', label: 'Node 4', position: { x: 400, y: 200 } },
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
])
</script>
<template>
<VueFlow class="interactionflow" v-model="elements" :fit-view-on-init="true">
<InteractionControls />
</VueFlow>
</template>

View File

@@ -0,0 +1,110 @@
<script setup>
import { useVueFlow } from '@braks/vue-flow'
import { ref } from 'vue'
const {
nodesDraggable,
nodesConnectable,
elementsSelectable,
zoomOnScroll,
zoomOnDoubleClick,
zoomOnPinch,
panOnScroll,
panOnScrollMode,
panOnDrag,
onConnect,
onNodeDragStop,
onPaneClick,
onPaneScroll,
onPaneContextMenu,
onNodeDragStart,
onMoveEnd,
addEdges,
} = useVueFlow()
const captureZoomClick = ref(false)
const captureZoomScroll = ref(false)
onConnect((params) => addEdges([params]))
onNodeDragStart((e) => console.log('drag start', e))
onNodeDragStop((e) => console.log('drag stop', e))
onPaneClick((event) => captureZoomClick.value && console.log('pane click', event))
onPaneScroll((event) => captureZoomScroll.value && console.log('pane scroll', event))
onPaneContextMenu((event) => captureZoomClick.value && console.log('pane ctx menu', event))
onMoveEnd((flowTransform) => console.log('move end', flowTransform))
</script>
<template>
<div class="controls">
<div>
<label class="label" for="draggable">
nodesDraggable
<input id="draggable" v-model="nodesDraggable" type="checkbox" class="vue-flow__draggable" />
</label>
</div>
<div>
<label class="label" for="connectable">
nodesConnectable
<input id="connectable" v-model="nodesConnectable" type="checkbox" class="vue-flow__connectable" />
</label>
</div>
<div>
<label class="label" for="selectable">
elementsSelectable
<input id="selectable" v-model="elementsSelectable" type="checkbox" class="vue-flow__selectable" />
</label>
</div>
<div>
<label class="label" for="zoomonscroll">
zoomOnScroll
<input id="zoomonscroll" v-model="zoomOnScroll" type="checkbox" class="vue-flow__zoomonscroll" />
</label>
</div>
<div>
<label class="label" for="zoomonpinch">
zoomOnPinch
<input id="zoomonpinch" v-model="zoomOnPinch" type="checkbox" class="vue-flow__zoomonpinch" />
</label>
</div>
<div>
<label class="label" for="panonscroll">
panOnScroll
<input id="panonscroll" v-model="panOnScroll" type="checkbox" class="vue-flow__panonscroll" />
</label>
</div>
<div>
<label class="label">
panOnScrollMode
<select id="panonscrollmode" v-model="panOnScrollMode" class="vue-flow__panonscrollmode">
<option value="free">free</option>
<option value="horizontal">horizontal</option>
<option value="vertical">vertical</option>
</select>
</label>
</div>
<div>
<label class="label" for="zoomondbl">
zoomOnDoubleClick
<input id="zoomondbl" v-model="zoomOnDoubleClick" type="checkbox" class="vue-flow__zoomondbl" />
</label>
</div>
<div>
<label class="label" for="panemoveable">
paneMovable
<input id="panemoveable" v-model="panOnDrag" type="checkbox" class="vue-flow__panemoveable" />
</label>
</div>
<div>
<label class="label" for="capturezoompaneclick">
capture onPaneClick
<input id="capturezoompaneclick" v-model="captureZoomClick" type="checkbox" class="vue-flow__capturezoompaneclick" />
</label>
</div>
<div>
<label class="label" for="capturezoompanescroll">
capture onPaneScroll
<input id="capturezoompanescroll" v-model="captureZoomScroll" type="checkbox" class="vue-flow__capturezoompanescroll" />
</label>
</div>
</div>
</template>

View File

@@ -0,0 +1,3 @@
export { default as InteractionApp } from './App.vue?raw'
export { default as InteractionControls } from './InteractionControls.vue?raw'
export { default as InteractionCSS } from './style.css'

View File

@@ -0,0 +1,22 @@
.interactionflow .controls {
position: absolute;
left: 0;
top: 0;
z-index: 4;
font-size: 11px;
background-color: lightgray;
border-bottom-right-radius: 10px;
padding: 12px;
}
.interactionflow .controls .label {
display: flex;
justify-content: space-between;
align-items: center;
gap: 5px;
cursor: pointer;
}
.interactionflow .controls .label input {
cursor: pointer;
}

View File

@@ -0,0 +1,10 @@
<script setup>
import Flow from './Flow.vue'
</script>
<template>
<div class="multiflows">
<Flow />
<Flow />
</div>
</template>

View File

@@ -0,0 +1,36 @@
<script setup>
import { Background, VueFlow, isNode } from '@braks/vue-flow'
import { ref } from 'vue'
const elements = ref([
{ id: '1', type: 'input', label: 'Node 1', position: { x: 250, y: 5 }, class: 'light' },
{ id: '2', label: 'Node 2', position: { x: 100, y: 100 }, class: 'light' },
{ id: '3', label: 'Node 3', position: { x: 400, y: 100 }, class: 'light' },
{ id: '4', label: 'Node 4', position: { x: 400, y: 200 }, class: 'light' },
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
])
const toggleClass = () => elements.value.forEach((el) => (el.class = el.class === 'light' ? 'dark' : 'light'))
const updatePos = () =>
elements.value.forEach((el) => {
if (isNode(el)) {
el.position = {
x: Math.random() * 400,
y: Math.random() * 400,
}
}
})
</script>
<template>
<VueFlow v-model="elements" :fit-view-on-init="true">
<Background />
<div style="position: absolute; right: 10px; top: 10px; z-index: 4">
<button style="margin-right: 5px" @click="updatePos">update positions</button>
<button style="margin-right: 5px" @click="toggleClass">toggle class</button>
</div>
</VueFlow>
</template>

View File

@@ -0,0 +1,3 @@
export { default as MultiApp } from './App.vue?raw'
export { default as MultiFlow } from './Flow.vue?raw'
export { default as MultiCSS } from './style.css'

View File

@@ -0,0 +1,18 @@
.vue-flow__node.dark {
background: #000000;
color: #ffffff;
}
.multiflows {
display: flex;
height: 100%;
}
.multiflows .vue-flow {
width: 100%;
height: 100%;
}
.multiflows .vue-flow:first-child {
border-right: 2px solid #333;
}

View File

@@ -0,0 +1,93 @@
<script setup>
import { Background, ConnectionMode, Controls, MiniMap, VueFlow, useVueFlow } from '@braks/vue-flow'
import { onMounted } from 'vue'
const { onConnect, nodes, edges, addEdges, addNodes } = useVueFlow({
fitViewOnInit: true,
connectionMode: ConnectionMode.Loose,
// set this to true so edges get elevated when selected, defaults to false
elevateEdgesOnSelect: true,
nodes: [
{ id: '1', type: 'input', label: 'node', position: { x: 250, y: 0 } },
{
id: '2',
label: 'parent node',
position: { x: 100, y: 100 },
style: { backgroundColor: 'rgba(16, 185, 129, 0.5)', width: '200px', height: '200px' },
},
{
id: '2a',
label: 'child node',
position: { x: 10, y: 50 },
parentNode: '2',
},
{
id: '4',
label: 'parent node',
position: { x: 320, y: 175 },
style: { backgroundColor: 'rgba(16, 185, 129, 0.5)', width: '400px', height: '300px' },
},
{
id: '4a',
label: 'child node',
position: { x: 15, y: 65 },
extent: 'parent',
parentNode: '4',
},
{
id: '4b',
label: 'nested parent node',
position: { x: 15, y: 120 },
style: { backgroundColor: 'rgba(139, 92, 246, 0.5)', height: '150px', width: '270px' },
parentNode: '4',
},
{
id: '4b1',
label: 'nested child node',
position: { x: 20, y: 40 },
parentNode: '4b',
},
{
id: '4b2',
label: 'nested child node',
position: { x: 100, y: 100 },
parentNode: '4b',
},
{ id: '4c', label: 'child node', position: { x: 200, y: 65 }, parentNode: '4' },
],
edges: [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e1-4', source: '1', target: '4' },
{ id: 'e1-4c', source: '1', target: '4c' },
{ id: 'e2a-4a', source: '2a', target: '4a' },
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
{ id: 'e4a-4b2', source: '4a', target: '4b2' },
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
],
})
onConnect((params) => addEdges([params]))
onMounted(() => {
// add nodes to parent
addNodes([
{
id: '999',
type: 'input',
label: 'Drag me to extend area!',
position: { x: 20, y: 100 },
class: 'light',
expandParent: true,
parentNode: '2',
},
])
})
</script>
<template>
<VueFlow>
<MiniMap />
<Controls />
<Background />
</VueFlow>
</template>

View File

@@ -0,0 +1 @@
export { default as NestedApp } from './App.vue?raw'

View File

@@ -0,0 +1,23 @@
<script setup>
import sdk from '@stackblitz/sdk'
const el = ref()
onMounted(() => {
sdk.embedProjectId(el.value, 'vitejs-vite-wyfpsj', {
height: 750,
forceEmbedLayout: true,
openFile: 'src/App.vue',
})
})
</script>
<template>
<div ref="el" class="outline-none"></div>
</template>
<style>
.application {
@apply h-[75vh];
}
</style>

View File

@@ -0,0 +1,13 @@
<script lang="ts" setup>
import { VueFlow } from '@braks/vue-flow'
import { ref } from 'vue'
import Controls from './Controls.vue'
const elements = ref([{ id: '1', label: 'Node 1', position: { x: 100, y: 100 } }])
</script>
<template>
<VueFlow v-model="elements">
<Controls />
</VueFlow>
</template>

View File

@@ -0,0 +1,40 @@
<script setup>
import { useVueFlow } from '@braks/vue-flow'
const flowKey = 'example-flow'
const { nodes, addNodes, setNodes, setEdges, dimensions, setTransform, toObject } = useVueFlow()
const onSave = () => {
localStorage.setItem(flowKey, JSON.stringify(toObject()))
}
const onRestore = () => {
const flow = JSON.parse(localStorage.getItem(flowKey))
if (flow) {
const [x = 0, y = 0] = flow.position
setNodes(flow.nodes)
setEdges(flow.edges)
setTransform({ x, y, zoom: flow.zoom || 0 })
}
}
const onAdd = () => {
const id = nodes.value.length + 1
const newNode = {
id: `random_node-${id}`,
label: `Node ${id}`,
position: { x: Math.random() * dimensions.value.width, y: Math.random() * dimensions.value.height },
}
addNodes([newNode])
}
</script>
<template>
<div class="save__controls">
<button style="background-color: #33a6b8" @click="onSave">save</button>
<button style="background-color: #113285" @click="onRestore">restore</button>
<button style="background-color: #6f3381" @click="onAdd">add node</button>
</div>
</template>

View File

@@ -0,0 +1,3 @@
export { default as SaveRestoreApp } from './App.vue?raw'
export { default as SaveRestoreControls } from './Controls.vue?raw'
export { default as SaveRestoreCSS } from './style.css'

View File

@@ -0,0 +1,25 @@
.save__controls {
position: absolute;
left: 10px;
top: 10px;
z-index: 4;
font-size: 12px;
}
.save__controls button {
margin-left: 5px;
padding: 5px;
border-radius: 5px;
font-weight: 700;
text-transform: uppercase;
color: white;
-webkit-box-shadow: 0px 5px 10px 0px rgba(0,0,0,0.3);
box-shadow: 0px 5px 10px 0px rgba(0,0,0,0.3);
cursor: pointer;
}
.save__controls button:hover {
opacity: 0.8;
transform: scale(105%);
transition: 250ms all ease-in-out;
}

View File

@@ -0,0 +1,42 @@
<script setup>
import { VueFlow, isNode, useVueFlow } from '@braks/vue-flow'
import { nextTick, ref } from 'vue'
import { getElements } from './utils.js'
const { nodes, edges } = getElements(15, 15)
const elements = ref([...nodes, ...edges])
const { onPaneReady, dimensions, onNodeClick, getEdges, fitView } = useVueFlow()
onPaneReady((i) => {
i.fitView({
padding: 0.2,
})
console.log(i.getElements.value)
})
const toggleClass = () => elements.value.forEach((el) => (el.class = el.class === 'light' ? 'dark' : 'light'))
const updatePos = () => {
elements.value.forEach((el) => {
if (isNode(el)) {
el.position = {
x: Math.random() * 10 * dimensions.value.width,
y: Math.random() * 10 * dimensions.value.height,
}
}
})
nextTick(() => {
fitView({ duration: 1000, padding: 0.5 })
})
}
</script>
<template>
<VueFlow v-model="elements" :min-zoom="0.1">
<div style="position: absolute; right: 10px; top: 10px; z-index: 4">
<button style="margin-right: 5px" @click="updatePos">update positions</button>
<button style="margin-right: 5px" @click="toggleClass">toggle class</button>
</div>
</VueFlow>
</template>

View File

@@ -0,0 +1,3 @@
export { default as StressApp } from './App.vue?raw'
export { default as StressUtils } from './utils.js?raw'
export { default as StressCSS } from './style.css'

View File

@@ -0,0 +1,4 @@
.vue-flow__node.dark {
background: #000000;
color: #ffffff;
}

View File

@@ -0,0 +1,40 @@
export function getElements(xElements = 10, yElements = 10) {
const initialNodes = []
const initialEdges = []
let nodeId = 1
let recentNodeId = null
for (let y = 0; y < yElements; y++) {
for (let x = 0; x < xElements; x++) {
const position = { x: x * 75, y: y * 75 }
const node = {
id: nodeId.toString(),
style: { width: `50px`, fontSize: `11px`, zIndex: 1 },
label: `Node ${nodeId}`,
class: 'light',
position,
}
initialNodes.push(node)
if (recentNodeId && nodeId <= xElements * yElements) {
initialEdges.push({
id: `${x}-${y}`,
source: recentNodeId.toString(),
target: nodeId.toString(),
style: (edge) => {
if (!edge.sourceNode.selected && !edge.targetNode.selected) return
return { stroke: '#10b981', strokeWidth: 3 }
},
})
}
recentNodeId = nodeId
nodeId++
}
}
return {
nodes: initialNodes,
edges: initialEdges,
}
}

View File

@@ -0,0 +1,46 @@
<script setup>
import { VueFlow } from '@braks/vue-flow'
import { ref } from 'vue'
import Sidebar from './Sidebar.vue'
import TeleportableNode from './TeleportableNode.vue'
const elements = ref([
{
id: '1',
label: 'Click to teleport',
type: 'teleportable',
position: { x: 125, y: 0 },
data: {},
},
{
id: '2',
label: 'Click to teleport',
type: 'teleportable',
position: { x: 350, y: 200 },
data: {},
},
{
id: '3',
label: 'Click to teleport',
type: 'teleportable',
position: { x: 0, y: 200 },
data: {},
},
{
id: 'e1-2',
source: '1',
target: '2',
},
])
</script>
<template>
<div class="teleportflow">
<VueFlow v-model="elements" :fit-view-on-init="true">
<template #node-teleportable="props">
<TeleportableNode v-bind="props" />
</template>
</VueFlow>
<Sidebar />
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup>
//
</script>
<template>
<aside>
<div class="description">Teleport destination</div>
<div id="port" class="port"></div>
</aside>
</template>

View File

@@ -0,0 +1,40 @@
<script setup>
import { Handle, Position } from '@braks/vue-flow'
import { useTransition } from './useTransition.js'
const props = defineProps({
id: {
type: String,
required: true,
},
})
const { animation, transition, teleport, onClick } = useTransition(props.id)
const changeAnimation = () => {
animation.value = animation.value === 'fade' ? 'shrink' : 'fade'
}
</script>
<script>
export default {
inheritAttrs: false,
}
</script>
<template>
<teleport :disabled="!teleport" :to="teleport">
<transition :name="animation">
<div v-if="!transition" class="teleportable">
<Handle type="target" :position="Position.Top" />
[Node {{ id }}]
<div class="buttons">
<div v-if="teleport !== '#port'" class="button" @click.prevent="onClick('#port')">Teleport To Sidebar</div>
<div v-if="teleport !== null" class="button" @click.prevent="onClick(null)">Teleport To Main Graph</div>
<div class="button" @click.prevent="changeAnimation">Animation: {{ animation }}</div>
</div>
<Handle type="source" :position="Position.Bottom" />
</div>
</transition>
</teleport>
</template>

View File

@@ -0,0 +1,5 @@
export { default as TeleportApp } from './App.vue?raw'
export { default as TeleportSidebar } from './Sidebar.vue?raw'
export { default as TeleportableNode } from './TeleportableNode.vue?raw'
export { default as TeleportableUseTransition } from './useTransition.js?raw'
export { default as TeleportCSS } from './style.css'

View File

@@ -0,0 +1,125 @@
.teleportflow {
flex-direction: column;
display: flex;
height: 100%;
}
.teleportflow aside {
display: flex;
flex-direction: column;
gap: 5px;
color: white;
font-weight: 700;
border-right: 1px solid #eee;
padding: 10px 10px;
font-size: 12px;
background: rgba(16, 185, 129, 0.75);
-webkit-box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3);
box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.3);
}
.teleportflow aside .port > * {
position: relative;
margin-bottom: 10px;
cursor: grab;
font-weight: 500;
-webkit-box-shadow: 5px 5px 10px 2px rgba(0, 0, 0, 0.25);
box-shadow: 5px 5px 10px 2px rgba(0, 0, 0, 0.25);
}
.teleportflow aside .description {
margin-bottom: 10px;
}
.teleportflow .vue-flow-wrapper {
flex-grow: 1;
height: 100%;
}
@media screen and (min-width: 640px) {
.teleportflow {
flex-direction: row;
}
.teleportflow aside {
min-width: 25%;
}
}
@media screen and (max-width: 639px) {
.teleportflow aside .port {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 5px;
}
}
.teleportable {
padding: 10px;
background: white;
border: 1px solid black;
border-radius: 10px;
color: black
}
.buttons {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 5px;
gap: 5px;
}
.button {
background-color: whitesmoke;
cursor: pointer;
padding: 5px 10px;
border: 1px solid black;
border-radius: 10px;
color: black;
font-weight: 700;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}
.button:hover {
background: black;
color: white;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.shrink-leave-active {
animation: shrink 0.5s;
}
.shrink-enter-active {
animation: grow 0.5s;
}
@keyframes grow {
from {
transform: scale(0.1);
}
to {
transform: scale(1);
}
}
@keyframes shrink {
from {
transform: scale(1);
}
to {
transform: scale(0.1);
}
}

View File

@@ -0,0 +1,107 @@
import { getConnectedEdges, useVueFlow } from '@braks/vue-flow'
import { nextTick, ref } from 'vue'
/**
* Utility composable for specifying animations
*
* Animations that resize a node need to call the `updateNodeDimensions` function from store to update node handle positions
* Otherwise edges do not connect properly
*/
export const useTransition = (id) => {
const animation = ref('fade')
const transition = ref(false)
const teleport = ref(null)
const { updateNodeInternals, getNode, edges } = useVueFlow()
/**
* specify a selector to teleport to
*
* teleported elements still behave like they're at their position before,
* i.e. if they emit events, they will still emit them up their regular tree
*/
const fade = (destination, onFinish) => {
setTimeout(() => {
// teleport to destination or disable teleport
teleport.value = destination
setTimeout(() => {
transition.value = false
// if destination is null, defer hiding edges until node is teleported back
if (!destination) {
onFinish()
}
}, 500)
}, 500)
}
const shrink = (destination, onFinish) => {
setTimeout(() => {
// teleport to destination or disable teleport
teleport.value = destination
setTimeout(() => {
transition.value = false
setTimeout(() => {
// if destination is null, defer hiding edges until node is teleported back
if (!destination) {
updateNodeInternals([id])
nextTick(() => {
onFinish()
})
}
}, 500)
}, 500)
}, 500)
}
/**
* specify a selector to teleport to
*
* teleported elements still behave like they're at their position before,
* i.e. if they emit events, they will still emit them up their regular tree
*/
const onClick = (destination) => {
const node = getNode.value(id)
transition.value = true
// save current teleport destination to data of node
node.data.destination = destination
// hide connected edges when teleporting
const connectedEdges = getConnectedEdges([node], edges.value)
// if destination is not null, hide edges immediately
// check if nodes connected to edge are teleported and hide edge if one of them is
if (destination) {
connectedEdges.forEach(
(edge) => (edge.hidden = !!getNode.value(edge.source).data.destination || !!getNode.value(edge.target).data.destination),
)
}
const onFinish = () => {
connectedEdges.forEach(
(edge) => (edge.hidden = !!getNode.value(edge.source).data.destination || !!getNode.value(edge.target).data.destination),
)
}
switch (animation.value) {
case 'fade':
fade(destination, onFinish)
break
case 'shrink':
shrink(destination, onFinish)
}
}
return {
animation,
transition,
teleport,
onClick,
}
}

View File

@@ -0,0 +1,47 @@
<script setup>
import { ConnectionMode, Controls, VueFlow, addEdge, updateEdge } from '@braks/vue-flow'
import { ref } from 'vue'
const elements = ref([
{
id: '1',
type: 'input',
label: 'Node <strong>A</strong>',
position: { x: 250, y: 0 },
},
{
id: '2',
label: 'Node <strong>B</strong>',
position: { x: 100, y: 100 },
},
{
id: '3',
label: 'Node <strong>C</strong>',
position: { x: 400, y: 100 },
style: { background: '#D6D5E6', color: '#333', border: '1px solid #222138', width: 180 },
},
{ id: 'e1-2', source: '1', target: '2', label: 'Updateable edge', updatable: true },
])
const onLoad = (flowInstance) => flowInstance.fitView()
const onEdgeUpdateStart = (edge) => console.log('start update', edge)
const onEdgeUpdateEnd = (edge) => console.log('end update', edge)
const onEdgeUpdate = ({ edge, connection }) => {
elements.value = updateEdge(edge, connection, elements.value)
}
const onConnect = (params) => (elements.value = addEdge(params, elements.value))
</script>
<template>
<VueFlow
v-model="elements"
:connection-mode="ConnectionMode.Loose"
@pane-ready="onLoad"
@edge-update="onEdgeUpdate"
@connect="onConnect"
@edge-update-start="onEdgeUpdateStart"
@edge-update-end="onEdgeUpdateEnd"
>
<Controls />
</VueFlow>
</template>

View File

@@ -0,0 +1 @@
export { default as UpdateEdgeApp } from './App.vue?raw'

View File

@@ -0,0 +1,48 @@
<script setup>
import { VueFlow, useVueFlow } from '@braks/vue-flow'
import { reactive } from 'vue'
const defaultLabel = '-'
const { onPaneReady, getNode } = useVueFlow({
nodes: [
{ id: '1', label: defaultLabel, position: { x: 100, y: 100 } },
{ id: '2', label: 'Node 2', position: { x: 100, y: 200 } },
],
edges: [{ id: 'e1-2', source: '1', target: '2' }],
})
const opts = reactive({
bg: '#eeeeee',
label: 'Node 1',
hidden: false,
})
const updateNode = () => {
const node = getNode.value('1')
node.label = opts.label.trim() !== '' ? opts.label : defaultLabel
node.style = { backgroundColor: opts.bg }
node.hidden = opts.hidden
}
onPaneReady(({ fitView }) => {
fitView()
updateNode()
})
</script>
<template>
<VueFlow>
<div class="updatenode__controls">
<label>label:</label>
<input v-model="opts.label" @input="updateNode" />
<label class="updatenode__bglabel">background:</label>
<input v-model="opts.bg" type="color" @input="updateNode" />
<div class="updatenode__checkboxwrapper">
<label>hidden:</label>
<input v-model="opts.hidden" type="checkbox" @change="updateNode" />
</div>
</div>
</VueFlow>
</template>

View File

@@ -0,0 +1,2 @@
export { default as UpdateNodeApp } from './App.vue?raw'
export { default as UpdateNodeCSS } from './style.css'

View File

@@ -0,0 +1,30 @@
.updatenode__controls {
position: absolute;
right: 10px;
top: 10px;
z-index: 4;
font-size: 11px;
background-color: lightgray;
border-radius: 10px;
padding: 8px;
}
.updatenode__controls label {
display: blocK;
}
.updatenode__controls input {
padding: 2px;
border-radius: 5px;
}
.updatenode__bglabel {
margin-top: 8px;
}
.updatenode__checkboxwrapper {
display: flex;
justify-content: center;
align-items: center;
margin-top: 8px;
}

View File

@@ -0,0 +1,47 @@
<script setup>
import { VueFlow, addEdge } from '@braks/vue-flow'
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
import CustomNode from './CustomNode.vue'
const elements = ref([
{ id: '0', type: 'custominput', position: { x: 0, y: 150 }, isValidTargetPos: (connection) => connection.target === 'B' },
{
id: 'A',
type: 'custom',
position: { x: 250, y: 0 },
isValidSourcePos: () => false,
},
{ id: 'B', type: 'custom', position: { x: 250, y: 150 }, isValidSourcePos: (connection) => connection.target === 'B' },
{ id: 'C', type: 'custom', position: { x: 250, y: 300 }, isValidSourcePos: (connection) => connection.target === 'B' },
])
const onLoad = (flowInstance) => flowInstance.fitView()
const onConnectStart = ({ nodeId, handleType }) => console.log('on connect start', { nodeId, handleType })
const onConnectStop = (event) => console.log('on connect stop', event)
const onConnectEnd = (event) => console.log('on connect end', event)
const onConnect = (params) => {
console.log('on connect', params)
addEdge(params, elements.value)
}
</script>
<template>
<VueFlow
class="validationflow"
v-model="elements"
@connect="onConnect"
@pane-ready="onLoad"
@connect-start="onConnectStart"
@connect-stop="onConnectStop"
@connect-end="onConnectEnd"
>
<template #node-custominput="props">
<CustomInput v-bind="props" />
</template>
<template #node-custom="props">
<CustomNode v-bind="props" />
</template>
</VueFlow>
</template>

View File

@@ -0,0 +1,21 @@
<script setup>
import { Handle, Position } from '@braks/vue-flow'
const props = defineProps({
isValidTargetPos: {
type: Function,
required: false,
},
})
</script>
<script>
export default {
inheritAttrs: false,
}
</script>
<template>
<div>Only connectable with B</div>
<Handle type="source" :position="Position.Right" :is-valid-connection="props.isValidTargetPos" />
</template>

View File

@@ -0,0 +1,25 @@
<script setup>
import { Handle, Position } from '@braks/vue-flow'
const props = defineProps({
id: {
type: String,
required: true,
},
isValidSourcePos: {
type: Function,
required: false,
},
})
</script>
<script>
export default {
inheritAttrs: false,
}
</script>
<template>
<Handle type="target" :position="Position.Left" :is-valid-connection="props.isValidSourcePos" />
<div>{{ props.id }}</div>
</template>

View File

@@ -0,0 +1,4 @@
export { default as ValidationApp } from './App.vue?raw'
export { default as ValidationCustomInput } from './CustomInput.vue?raw'
export { default as ValidationCustomNode } from './CustomNode.vue?raw'
export { default as ValidationCSS } from './style.css'

View File

@@ -0,0 +1,31 @@
.validationflow .vue-flow__node {
width: 150px;
border-radius: 5px;
padding: 10px;
color: #555;
border: 1px solid #ddd;
text-align: center;
font-size: 12px;
}
.validationflow .vue-flow__node-customnode {
background: #e6e6e9;
border: 1px solid #ddd;
}
.validationflow .vue-flow__node-custominput .vue-flow__handle {
background: #e6e6e9;
}
.validationflow .vue-flow__node-custominput {
background: #fff;
}
.validationflow .vue-flow__handle-connecting {
background: #ff6060;
}
.validationflow .vue-flow__handle-valid {
background: #55dd99;
}

View File

@@ -0,0 +1,27 @@
<script lang="ts" setup>
import Heart from '~icons/mdi/heart'
</script>
<template>
<div id="acknowledgement" class="w-full dark:(bg-black text-white) border-y-1 border-white">
<div class="max-w-11/12 md:max-w-6/12 m-auto py-4 md:(pb-12 pt-6) text-center">
<div>
<h1 class="md:mb-8 flex justify-center items-center"><Heart class="text-red-500" /> Acknowledgement</h1>
<strong>First off</strong>: A big thank you to the developers of
<a href="https://webkid.io" target="_blank" class="text-green-500">Webkid</a>. Their amazing work made it possible for me
to create this port to Vue 3 - without them there is no Vue Flow.
</div>
<br />
<div>
Vue Flow is built on top of existing features and code taken from
<a href="https://reactflow.dev" target="_blank" class="text-green-500">React Flow</a>. It replicates the basic features
found in React Flow (zoom, pan, graph, additional components and more) and brings them to the Vue 3 experience, with all
the fun reactivity and features like template slots etc. you know and love from Vue. If you're happy with Vue Flow,
<br />
<a href="https://github.com/sponsors/wbkd" target="_blank" class="text-lg font-bold text-green-500">
please consider supporting Webkid by donating.
</a>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,31 @@
<script lang="ts" setup>
import { $fetch } from 'ohmyfetch'
import Star from '~icons/carbon/star'
import Download from '~icons/carbon/download'
const githubData = await $fetch('https://api.github.com/repos/bcakmakoglu/vue-flow?page=$i&per_page=100')
const npmData = await $fetch('https://api.npmjs.org/downloads/point/last-month/@braks/vue-flow')
</script>
<template>
<div class="w-full dark:(bg-black text-white border-white) border-black border-y-1">
<div class="max-w-full md:max-w-11/12 m-auto py-4 md:py-12 <md:(dark:border-t-1 border-white)">
<div class="grid md:grid-cols-3 gap-3 text-center <md:divide-y md:divide-x dark:divide-white divide-black">
<div class="grid grid-rows-auto gap-2 py-4 md:py-0">
<div class="text-gray-400 font-semibold text-lg">Stargazers</div>
<div class="font-bold text-3xl flex gap-2 items-center justify-center"><Star /> {{ githubData.stargazers_count }}</div>
</div>
<div class="grid grid-rows-2 gap-2 py-4 md:py-0">
<div class="text-gray-400 font-semibold text-lg">Downloads (last month)</div>
<div class="font-bold text-3xl flex gap-2 items-center justify-center"><Download /> {{ npmData.downloads }}</div>
</div>
<div class="grid grid-rows-2 gap-2 py-4 md:py-0">
<div class="text-gray-400 font-semibold text-lg">License</div>
<div class="font-bold text-3xl">
{{ githubData.license.key.toUpperCase() }}
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,56 @@
<script lang="ts" setup>
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import type { VueFlowStore } from '@braks/vue-flow'
const breakpoints = useBreakpoints(breakpointsTailwind)
const el = ref()
const instances: VueFlowStore[] = []
const onLoad = (instance: VueFlowStore) => {
instances.push(instance)
instance.fitView()
}
const fitViews = () => {
instances.forEach((i) => i.fitView())
}
const { stop } = useResizeObserver(
el,
useDebounceFn(() => fitViews(), 5),
)
onBeforeUnmount(stop)
</script>
<template>
<div ref="el" class="w-full dark:(bg-black text-white)">
<div
class="flex flex-col divide-y divide-gray-500 md:divide-y-0 gap-12 md:gap-24 lg:gap-36 max-w-9/12 md:max-w-11/12 lg:max-w-9/12 m-auto py-12 md:py-24 text-center md:text-left"
>
<XyzTransition appear-visible xyz="fade down ease-out-back">
<div class="flex flex-col md:flex-row gap-12 md:gap-24">
<Basic @pane="onLoad" />
</div>
</XyzTransition>
<XyzTransition appear-visible xyz="fade down ease-out-back">
<div class="flex flex-col-reverse md:flex-row flex-unwrap gap-12 md:gap-24">
<RGB @pane="onLoad" />
</div>
</XyzTransition>
<XyzTransition appear-visible xyz="fade down ease-out-back">
<div class="flex flex-col md:flex-row flex-unwrap gap-12 md:gap-24">
<Nested @pane="onLoad" />
</div>
</XyzTransition>
<XyzTransition appear-visible xyz="fade down ease-out-back">
<div class="flex flex-col-reverse md:flex-row flex-unwrap gap-12 md:gap-24">
<Additional @pane="onLoad" />
</div>
</XyzTransition>
</div>
</div>
</template>

View File

@@ -0,0 +1,83 @@
<script lang="ts" setup>
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import Blobity from 'blobity'
import Intro from './flows/Intro.vue'
const breakpoints = useBreakpoints(breakpointsTailwind)
const usesDark = useDark({
storageKey: 'vuepress-color-scheme',
selector: 'html',
})
const dark = ref(false)
onMounted(() => {
const html = document.getElementsByTagName('html')![0]
usesDark.value = html.classList.contains('dark')
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
dark.value = html.classList.contains('dark')
}
})
observer.observe(html, {
attributes: true,
attributeOldValue: true,
attributeFilter: ['class'],
})
})
onMounted(() => {
if (!breakpoints.isSmaller('md')) {
const blobity = new Blobity({
color: dark.value ? '#ffffff' : '#000000',
invert: true,
zIndex: 0,
magnetic: false,
dotColor: '#10b981',
radius: 8,
focusableElementsOffsetX: 5,
focusableElementsOffsetY: 4,
mode: 'normal',
focusableElements:
'[data-blobity], a:not([data-no-blobity]), button:not([data-no-blobity]), [data-blobity-tooltip], .back-to-top, .intro',
})
onBeforeUnmount(() => {
blobity.destroy()
})
}
})
</script>
<template>
<div class="relative h-[90vh] md:h-[75vh]">
<Intro />
</div>
</template>
<style>
button:focus {
outline: none;
}
h1 {
@apply text-xl lg:text-4xl mb-4 font-bold;
}
h2 {
@apply text-lg lg:text-2xl mb-4 font-semibold;
}
p {
@apply text-md lg:text-lg;
}
p ~ h1,
p ~ h2 {
@apply mt-6;
}
</style>

View File

@@ -0,0 +1,57 @@
<script lang="ts" setup>
import { computed } from 'vue'
import type { EdgeProps, MarkerType, Position } from '@braks/vue-flow'
import { getBezierPath } from '@braks/vue-flow'
interface CustomEdgeProps extends EdgeProps {
source: string
target: string
sourceHandleId?: string
targetHandleId?: string
id: string
sourceX: number
sourceY: number
targetX: number
targetY: number
sourcePosition: Position
targetPosition: Position
markerEnd?: MarkerType
data?: {
text?: string
color?: 'red' | 'green' | 'blue'
}
}
const props = defineProps<CustomEdgeProps>()
const edgePath = computed(() =>
getBezierPath({
sourceX: props.sourceX,
sourceY: props.sourceY,
sourcePosition: props.sourcePosition,
targetX: props.targetX,
targetY: props.targetY,
targetPosition: props.targetPosition,
}),
)
</script>
<script lang="ts">
export default {
inheritAttrs: false,
}
</script>
<template>
<path
:id="props.id"
class="vue-flow__edge-path"
:style="{ stroke: props.data?.color }"
:d="edgePath"
:marker-end="props.markerEnd"
/>
<text>
<textPath :href="`#${props.id}`" :style="{ fontSize: '1.25rem', fill: 'white' }" startOffset="50%" text-anchor="middle">
{{ props.data?.text }}
</textPath>
</text>
</template>

View File

@@ -0,0 +1,112 @@
<script lang="ts" setup>
import { ref } from 'vue'
import type { Elements } from '@braks/vue-flow'
import { Background, Controls, MiniMap, Position, VueFlow, useVueFlow } from '@braks/vue-flow'
const emit = defineEmits(['pane'])
const elements = ref<Elements>([
{
id: '1',
style: { width: '75px' },
type: 'input',
sourcePosition: Position.Right,
label: 'input',
position: { x: 25, y: 120 },
},
{
id: '2',
label: 'A',
targetPosition: Position.Left,
sourcePosition: Position.Right,
position: { x: 150, y: 25 },
style: { width: '75px' },
},
{
id: '3',
label: 'B',
targetPosition: Position.Left,
sourcePosition: Position.Right,
position: { x: 250, y: 25 },
style: { width: '75px' },
},
{
id: '4',
label: 'C',
targetPosition: Position.Left,
sourcePosition: Position.Right,
position: { x: 350, y: 25 },
style: { width: '75px' },
},
{
id: '5',
label: 'D',
targetPosition: Position.Left,
sourcePosition: Position.Right,
position: { x: 150, y: 220 },
style: { width: '75px' },
},
{
id: '6',
label: 'E',
targetPosition: Position.Left,
sourcePosition: Position.Right,
position: { x: 250, y: 220 },
style: { width: '75px' },
},
{
id: '7',
label: 'F',
targetPosition: Position.Left,
sourcePosition: Position.Right,
position: { x: 350, y: 220 },
style: { width: '75px' },
},
{
id: '8',
type: 'output',
label: 'Output',
targetPosition: Position.Left,
position: { x: 500, y: 120 },
style: { width: '75px' },
},
{ id: 'e1-2', type: 'step', source: '1', target: '2' },
{ id: 'e2-3', type: 'step', source: '2', target: '3' },
{ id: 'e3-4', type: 'step', source: '3', target: '4' },
{ id: 'e4-8', type: 'step', source: '4', target: '8' },
{ id: 'e1-5', type: 'step', source: '1', target: '5', animated: true },
{ id: 'e5-6', type: 'step', source: '5', target: '6', animated: true },
{ id: 'e6-7', type: 'step', source: '6', target: '7', animated: true },
{ id: 'e6-8', type: 'step', source: '7', target: '8', animated: true },
])
const { onPaneReady } = useVueFlow({
modelValue: elements.value,
zoomOnScroll: false,
panOnDrag: false,
})
onPaneReady((i) => emit('pane', i))
</script>
<template>
<div class="w-full h-[300px] md:min-h-[400px] shadow-xl rounded-xl font-mono uppercase overflow-hidden border-2">
<VueFlow>
<Controls :show-interactive="false" />
<MiniMap mask-color="rgba(16, 185, 129, 0.5)" class="transform scale-60 origin-bottom-right opacity-75" />
<Background variant="lines" pattern-color="#aaa" :gap="46" />
</VueFlow>
</div>
<div class="md:max-w-1/3 flex flex-col gap-12 justify-center <md:pt-12">
<div class="flex flex-col gap-2 items-center md:items-start">
<h1>Additional Features</h1>
<p>
On top of all the features Vue Flow comes with several components like a Background, Minimap or Controls.
<br />
Plus it's built for composition, making the access of the internal state easy as can be!
</p>
<router-link class="docs-button max-w-max" to="/guide/"> Documentation </router-link>
</div>
</div>
</template>

View File

@@ -0,0 +1,143 @@
<script lang="ts" setup>
import type { ClassFunc, GraphEdge, GraphNode, StyleFunc } from '@braks/vue-flow'
import { Background, ConnectionLineType, Controls, VueFlow, useVueFlow } from '@braks/vue-flow'
import Cross from '~icons/mdi/window-close'
const emit = defineEmits(['pane'])
const getNodeClass: ClassFunc<GraphNode> = (el) => {
const classes = ['font-semibold', '!border-2', 'transition-colors', 'duration-300', 'ease-in-out']
if (el.selected)
classes.push(
...['!border-green-500/80', '!shadow-md', '!shadow-green-500/50', '!bg-green-100/80 dark:(!bg-white)', '!text-gray-700'],
)
return classes.join(' ')
}
const getEdgeClass: ClassFunc<GraphEdge> = (el) => {
const classes = ['transition-colors duration-300', el.sourceNode.selected ? 'font-semibold' : '']
return classes.join(' ')
}
const getEdgeStyle: StyleFunc<GraphEdge> = (el) => {
const sourceNodeSelected = el.sourceNode.selected
return {
transition: 'stroke ease-in-out 300ms',
stroke: el.selected || sourceNodeSelected ? 'var(--secondary)' : '',
}
}
const { onPaneReady, onConnect, addEdges, viewport } = useVueFlow({
connectionLineType: ConnectionLineType.SmoothStep,
connectionLineStyle: {
strokeDasharray: 5,
animation: 'dashdraw 0.5s linear infinite',
},
modelValue: [
{
id: '1',
type: 'input',
label: 'Start',
position: { x: 250, y: 5 },
class: getNodeClass,
},
{
id: '2',
label: 'Waypoint',
position: { x: 100, y: 100 },
class: getNodeClass,
},
{ id: '3', label: 'Waypoint', position: { x: 400, y: 100 }, class: getNodeClass },
{
id: '4',
type: 'output',
label: 'End',
position: { x: 250, y: 225 },
class: getNodeClass,
},
{
id: 'e1-2',
source: '1',
label: 'animated edge',
target: '2',
animated: true,
class: getEdgeClass,
style: getEdgeStyle,
},
{
id: 'e1-3',
source: '1',
target: '3',
label: 'default edge',
class: getEdgeClass,
style: (el: GraphEdge) => {
const sourceNodeSelected = el.sourceNode.selected
return {
transition: 'stroke ease-in-out 300ms',
stroke: el.selected || sourceNodeSelected ? 'red' : '',
}
},
},
{
id: 'e2-4',
source: '2',
target: '4',
type: 'step',
animated: true,
class: getEdgeClass,
style: getEdgeStyle,
},
],
})
onPaneReady((i) => emit('pane', i))
onConnect((param) => {
addEdges([
{
...param,
type: 'smoothstep',
animated: true,
},
])
})
</script>
<template>
<div class="md:max-w-1/3 flex flex-col justify-center">
<div class="flex flex-col items-center md:items-start">
<h1>Interactive Graphs</h1>
<p>
Vue Flow comes with built-in features like zoom & pan and dedicated controls, single & multi-selections, draggable
elements, customizable nodes and edges and a bunch of event handlers.
</p>
<router-link class="docs-button max-w-max" to="/guide/"> Documentation </router-link>
</div>
</div>
<div
class="w-full h-[300px] md:min-h-[400px] shadow-xl rounded-xl font-mono uppercase border-1 border-secondary overflow-hidden"
>
<VueFlow class="basic">
<Controls class="md:(!left-auto !right-[10px])" />
<Background :gap="60">
<template #pattern>
<Cross :style="{ fontSize: `${8 * viewport.zoom || 1}px` }" class="text-[#10b981] opacity-50" />
</template>
</Background>
</VueFlow>
</div>
</template>
<style>
.basic .vue-flow__node-input.selected .vue-flow__handle {
@apply bg-green-500;
}
.basic .vue-flow__node-default.selected .vue-flow__handle {
@apply bg-green-500;
}
.basic .vue-flow__node-output.selected .vue-flow__handle {
@apply bg-green-500;
}
</style>

View File

@@ -0,0 +1,451 @@
<script lang="ts" setup>
import { Background, Handle, Position, VueFlow, useVueFlow } from '@braks/vue-flow'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import confetti from 'canvas-confetti'
import colors from 'windicss/colors'
import { cheer, fireworks } from './confetti'
import Heart from '~icons/mdi/heart'
const breakpoints = useBreakpoints(breakpointsTailwind)
const dark = ref(false)
const animatedBackground = ref(false)
onMounted(() => {
const html = document.getElementsByTagName('html')![0]
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
dark.value = html.classList.contains('dark')
}
})
observer.observe(html, {
attributes: true,
attributeOldValue: true,
attributeFilter: ['class'],
})
})
const initialEdges = [
{
id: 'eintro-examples',
sourceHandle: 'a',
source: 'intro',
target: 'examples',
animated: true,
style: { strokeWidth: 4, stroke: '#ef467e' },
},
{
id: 'eintro-documentation',
sourceHandle: 'a',
source: 'intro',
target: 'documentation',
animated: true,
style: { strokeWidth: 4, stroke: '#f97316' },
},
{
id: 'eintro-acknowledgement',
sourceHandle: 'a',
source: 'intro',
target: 'acknowledgement',
animated: true,
style: { strokeWidth: 4, stroke: '#0ea5e9' },
},
]
const { dimensions, onNodeClick, getNodes, fitView, getNode, getEdge, updateEdge, edges, setEdges } = useVueFlow({
nodes: [
{ id: 'intro', type: 'box', position: { x: 0, y: 0 } },
{ id: 'examples', type: 'box', position: { x: -50, y: 400 } },
{ id: 'documentation', type: 'box', position: { x: 300, y: 400 } },
{ id: 'acknowledgement', type: 'box', position: { x: 150, y: 500 } },
],
edges: initialEdges,
elementsSelectable: true,
panOnDrag: false,
zoomOnScroll: false,
zoomOnDoubleClick: false,
zoomOnPinch: false,
elevateEdgesOnSelect: true,
})
const clickInterval = ref()
const clicks = ref(0)
const disabled = ref(false)
const confettiColors = Object.values(colors).flatMap((color) => {
if (typeof color === 'string') return color
else return Object.values(color).flatMap((c) => c)
})
onNodeClick(async ({ node }) => {
if (node.id === 'intro') {
if (disabled.value) return
animatedBackground.value = !animatedBackground.value
if (clickInterval.value) {
clearInterval(clickInterval.value)
}
clickInterval.value = setInterval(() => {
clearInterval(clickInterval.value)
clicks.value = 0
}, 1000)
clicks.value++
if (clicks.value < 10) {
confetti({
startVelocity: 25,
spread: 360,
angle: 270,
origin: { y: 0.1 },
colors: confettiColors,
})
} else if (clicks.value < 20) {
await cheer(['#10b981', dark.value ? '#ffffff' : '#000000'])
} else if (clicks.value === 20) {
disabled.value = true
await fireworks(confettiColors)
disabled.value = false
} else {
clicks.value = 0
}
}
})
const el = templateRef<HTMLDivElement>('el', null)
const setNodes = () => {
if (breakpoints.isSmaller('md')) {
const mainNode = getNode.value('intro')!
getNodes.value.forEach((node) => {
switch (node.id) {
case 'intro':
node.position = { x: 0, y: 0 }
break
case 'examples':
node.position = {
x: mainNode.dimensions.width / 2 - node.dimensions.width / 2,
y: mainNode.dimensions.height * 1.5,
}
break
case 'documentation':
node.position = {
x: mainNode.dimensions.width / 2 - node.dimensions.width / 2,
y: mainNode.dimensions.height * 2 + 50,
}
break
case 'acknowledgement':
node.position = {
x: mainNode.dimensions.width / 2 - node.dimensions.width / 2,
y: mainNode.dimensions.height * 3,
}
break
}
})
setEdges(() => {
return [
{
id: 'eintro-examples',
sourceHandle: 'a',
source: 'intro',
target: 'examples',
animated: true,
style: { strokeWidth: 4, stroke: '#ef467e' },
},
{
id: 'eexamples-documentation',
source: 'examples',
target: 'documentation',
animated: true,
style: { strokeWidth: 4, stroke: '#f97316' },
},
{
id: 'edocumentation-acknowledgement',
source: 'documentation',
target: 'acknowledgement',
animated: true,
style: { strokeWidth: 4, stroke: '#0ea5e9' },
},
]
})
} else {
getNodes.value.forEach((node) => {
const mainNode = getNode.value('intro')!
switch (node.id) {
case 'intro':
node.position = { x: 0, y: 0 }
break
case 'examples':
node.position = { x: -node.dimensions.width / 2, y: mainNode.dimensions.height * 1.5 }
break
case 'documentation':
node.position = {
x: mainNode.dimensions.width - node.dimensions.width / 2,
y: mainNode.dimensions.height * 1.5,
}
break
case 'acknowledgement':
node.position = {
x: mainNode.dimensions.width / 2 - node.dimensions.width / 2,
y: mainNode.dimensions.height * 2,
}
break
}
})
setEdges(initialEdges)
}
nextTick(() => {
fitView()
})
}
const { stop } = useResizeObserver(el, useDebounceFn(setNodes, 5))
onBeforeUnmount(stop)
const scrollTo = () => {
const el = document.getElementById('acknowledgement')
if (el) {
el.scrollIntoView({ behavior: 'smooth' })
}
}
const animationClassNames = ['checker-gb', 'checker-op', 'checker-yg', 'checker-ss']
const shuffle = (a: any[]) => {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[a[i], a[j]] = [a[j], a[i]]
}
return a
}
const createAnimationDurations = () => {
return animationClassNames.map((className) => {
const duration = 5 + Math.random() * 5
return {
className,
duration,
}
})
}
const animations = ref<{ className: string; duration: number }[]>(shuffle(createAnimationDurations()))
</script>
<template>
<VueFlow ref="el" class="dark:bg-black bg-white transition-colors duration-200 ease-in-out">
<XyzTransition xyz="fade down ease-out-back duration-10" mode="out-in">
<Background v-if="animatedBackground">
<template #pattern-container="{ id }">
<pattern :id="id" x="0" y="0" width="200" height="200" patternUnits="userSpaceOnUse">
<rect
:class="animations[0].className"
:style="{ '--animation-duration': `${animations[0].duration}s` }"
x="0"
y="0"
width="50"
height="50"
></rect>
<rect
:class="animations[1].className"
:style="{ '--animation-duration': `${animations[1].duration}s` }"
x="0"
y="100"
width="50"
height="50"
></rect>
<rect
:class="animations[2].className"
:style="{ '--animation-duration': `${animations[2].duration}s` }"
x="100"
y="0"
width="50"
height="50"
></rect>
<rect
:class="animations[3].className"
:style="{ '--animation-duration': `${animations[3].duration}s` }"
x="100"
y="100"
width="50"
height="50"
></rect>
</pattern>
</template>
</Background>
</XyzTransition>
<XyzTransition xyz="fade down ease-out-back duration-20" mode="out-in">
<Background
v-if="!animatedBackground"
variant="lines"
:pattern-color="dark ? '#ffffff' : '#000000'"
:size="0.7"
:gap="100"
/>
</XyzTransition>
<template #node-box="props">
<template v-if="props.id === 'intro'">
<div class="box max-w-[500px]">
<div class="intro px-4 py-2 shadow-lg rounded-md border-2 border-solid border-black">
<div class="font-mono flex flex-col gap-4 p-4 items-center text-center">
<h1 class="text-2xl lg:text-4xl !my-0 !pt-0 font-bold">Vue Flow</h1>
<h2 class="text-lg lg:text-xl font-normal !border-0">
The customizable Vue 3 component bringing interactivity to flowcharts and graphs.
</h2>
</div>
<Handle id="a" type="source" :position="Position.Bottom" />
</div>
</div>
</template>
<template v-else-if="props.id === 'documentation'">
<div class="flex">
<router-link class="link group bg-[#f15a16]" to="/guide/"> Read The Documentation </router-link>
</div>
<Handle type="target" :position="Position.Top" />
<Handle class="block md:hidden" type="source" :position="Position.Bottom" />
</template>
<template v-else-if="props.id === 'examples'">
<div class="flex">
<router-link class="link group bg-[#ef467e]" to="/examples/"> Check The Examples </router-link>
</div>
<Handle type="target" :position="Position.Top" />
<Handle class="block md:hidden" type="source" :position="Position.Bottom" />
</template>
<template v-else-if="props.id === 'acknowledgement'">
<div class="flex" @click="scrollTo">
<button class="link group bg-sky-500"><Heart class="text-red-500" /> Acknowledgement</button>
</div>
<Handle type="target" :position="Position.Top" />
</template>
</template>
</VueFlow>
</template>
<style>
.checker-gb {
animation: fill-green-blue var(--animation-duration) alternate infinite;
}
.checker-gb path {
animation: fill-green-blue var(--animation-duration) alternate infinite;
}
.checker-op {
animation: fill-orange-purple var(--animation-duration) alternate infinite;
}
.checker-op path {
animation: fill-orange-purple var(--animation-duration) alternate infinite;
}
.checker-yg {
animation: fill-yellow-green var(--animation-duration) alternate infinite;
}
.checker-yg path {
animation: fill-yellow-green var(--animation-duration) alternate infinite;
}
.checker-ss {
animation: fill-sky-red var(--animation-duration) alternate infinite;
}
.checker-ss path {
animation: fill-sky-red var(--animation-duration) alternate infinite;
}
@keyframes fill-green-blue {
0% {
@apply fill-transparent;
}
35% {
@apply fill-green-500/50;
}
65% {
@apply fill-transparent;
}
100% {
@apply fill-blue-500/50;
}
}
@keyframes fill-orange-purple {
0% {
@apply fill-transparent;
}
35% {
@apply fill-orange-500/50;
}
65% {
@apply fill-transparent;
}
100% {
@apply fill-purple-500/50;
}
}
@keyframes fill-yellow-green {
0% {
@apply fill-transparent;
}
35% {
@apply fill-yellow-500/50;
}
65% {
@apply fill-transparent;
}
100% {
@apply fill-green-500/50;
}
}
@keyframes fill-sky-red {
0% {
@apply fill-transparent;
}
35% {
@apply fill-sky-500/50;
}
65% {
@apply fill-transparent;
}
100% {
@apply fill-red-500/50;
}
}
.intro {
@apply cursor-pointer
bg-green-500
text-white
transform
transition-transform
duration-300
hover:(ring ring-white);
}
.link {
@apply flex
gap-3
items-center
p-4
shadow-lg
transform
transition-transform
duration-300
hover:(scale-102)
transition-colors
ease-in-out
rounded-lg
text-white
font-semibold
text-lg;
}
</style>

View File

@@ -0,0 +1,85 @@
<script lang="ts" setup>
import { Background, ConnectionMode, Controls, VueFlow, useVueFlow } from '@braks/vue-flow'
import { breakpointsTailwind } from '@vueuse/core'
const emit = defineEmits(['pane'])
const breakpoints = useBreakpoints(breakpointsTailwind)
const nodeClasses = ['!normal-case font-semibold !text-white', '!border-1', 'shadow-md'].join(' ')
const childClasses = `${nodeClasses} !bg-green-500/70 !border-white`
const { onPaneReady, panOnDrag } = useVueFlow({
fitViewOnInit: true,
connectionMode: ConnectionMode.Loose,
zoomOnScroll: false,
preventScrolling: false,
translateExtent: [
[-500, -100],
[600, 500],
],
nodes: [
{ id: '1', type: 'input', label: 'Outer Node', position: { x: 0, y: 0 }, class: childClasses },
{
id: '2',
label: 'Parent Node',
position: { x: -125, y: 100 },
class: `${nodeClasses} !bg-green-500/30 !border-green-500`,
style: { width: '400px', height: '160px' },
},
{
id: '2a',
label: 'Child Node',
position: { x: 17, y: 30 },
parentNode: '2',
extent: 'parent',
class: childClasses,
},
{ id: '2b', label: 'Child Node', position: { x: 225, y: 30 }, parentNode: '2', extent: 'parent', class: childClasses },
{ id: '2c', label: 'Child Node', position: { x: 125, y: 100 }, parentNode: '2', extent: 'parent', class: childClasses },
{ id: '3', type: 'output', label: 'Outer Node', position: { x: 0, y: 300 }, class: childClasses },
],
edges: [
{ id: 'e1-2a', source: '1', target: '2a', type: 'smoothstep', style: { stroke: 'white', strokeWidth: '2' } },
{ id: 'e2-3', source: '2', target: '3', type: 'smoothstep', style: { stroke: 'white', strokeWidth: '2' } },
],
})
watch(
[breakpoints.sm, breakpoints.md, breakpoints.lg, breakpoints.xl, breakpoints['2xl']],
() => {
panOnDrag.value = !breakpoints.isSmaller('md')
},
{ immediate: true },
)
onPaneReady((i) => emit('pane', i))
</script>
<template>
<div class="md:max-w-1/3 flex flex-col justify-center <md:pt-12">
<div class="flex flex-col gap-2 items-center md:items-start">
<h1>Complex Flows</h1>
<p>
You want to visualize more complex ideas?
<br />
No worries! Vue Flow supports creating nested nodes and nested graphs out-of-the-box.
</p>
<router-link class="docs-button max-w-max" to="/guide/"> Documentation </router-link>
</div>
</div>
<div
class="w-full h-[300px] md:min-h-[400px] shadow-xl rounded-xl font-mono uppercase border-1 border-green-500 overflow-hidden"
>
<VueFlow class="nested">
<Controls class="md:(!left-auto !right-[10px])" />
<Background pattern-color="#aaa" class="!bg-gray-800" :gap="18" />
</VueFlow>
</div>
</template>
<style>
.nested .vue-flow__handle {
opacity: 0;
}
</style>

View File

@@ -0,0 +1,114 @@
<script lang="ts" setup>
import type { MiniMapNodeFunc } from '@braks/vue-flow'
import { Background, BackgroundVariant, Controls, MiniMap, VueFlow, useVueFlow } from '@braks/vue-flow'
import { breakpointsTailwind } from '@vueuse/core'
import CustomEdge from '../edges/Custom.vue'
import RGBNode from '../nodes/Input.vue'
import RGBOutputNode from '../nodes/Output.vue'
const emit = defineEmits(['pane'])
const breakpoints = useBreakpoints(breakpointsTailwind)
interface Colors {
red: number
green: number
blue: number
}
const { onPaneReady, getNode, panOnDrag } = useVueFlow({
id: 'rgb-flow',
nodes: [
{ id: '1', type: 'rgb', data: { color: 'g' }, position: { x: -25, y: 0 } },
{ id: '2', type: 'rgb', data: { color: 'r' }, position: { x: 50, y: -110 } },
{ id: '3', type: 'rgb', data: { color: 'b' }, position: { x: 0, y: 110 } },
{ id: '4', type: 'rgb-output', label: 'RGB', position: { x: 400, y: -25 } },
],
edges: [
{ id: 'e1-4', type: 'rgb-line', data: { color: 'green' }, source: '1', target: '4', animated: true },
{ id: 'e2-4', type: 'rgb-line', data: { color: 'red' }, source: '2', target: '4', animated: true },
{ id: 'e3-4', type: 'rgb-line', data: { color: 'blue' }, source: '3', target: '4', animated: true },
],
zoomOnScroll: false,
nodeExtent: [
[-50, -150],
[500, 300],
],
})
onPaneReady((i) => emit('pane', i))
const el = templateRef<HTMLDivElement>('el', null)
const color = ref<Colors>({
red: 222,
green: 45,
blue: 140,
})
watch(
[breakpoints.sm, breakpoints.md, breakpoints.lg, breakpoints.xl, breakpoints['2xl']],
() => {
const mobile = breakpoints.isSmaller('md')
if (mobile) {
getNode.value('4')!.position = { x: 300, y: -25 }
}
panOnDrag.value = !mobile
},
{ immediate: true },
)
const onChange = ({ color: c, val }: { color: keyof Colors; val: number }) => (color.value[c] = Number(val))
const nodeColor: MiniMapNodeFunc = (node) => {
switch (node.id) {
case '1':
return 'green'
case '2':
return 'red'
case '3':
return 'blue'
case '4':
return `rgb(${color.value.red}, ${color.value.green}, ${color.value.blue})`
}
return ''
}
</script>
<template>
<div
ref="el"
class="w-full h-[300px] md:min-h-[400px] shadow-xl rounded-xl font-mono uppercase overflow-hidden bg-gray-800 border-2"
:style="{ borderColor: `rgb(${color.red}, ${color.green}, ${color.blue})` }"
>
<VueFlow class="relative font-mono">
<template #edge-rgb-line="rgbLineProps">
<CustomEdge v-bind="{ ...rgbLineProps, data: { text: color[rgbLineProps.data?.color], ...rgbLineProps.data } }" />
</template>
<template #node-rgb="rgbProps">
<RGBNode v-bind="rgbProps" :amount="color" @change="onChange" />
</template>
<template #node-rgb-output="rgbOutputProps">
<RGBOutputNode v-bind="rgbOutputProps" :rgb="`rgb(${color.red}, ${color.green}, ${color.blue})`" />
</template>
<Controls class="hidden md:block" />
<Background
:variant="BackgroundVariant.Lines"
:pattern-color="`rgb(${color.red}, ${color.green}, ${color.blue})`"
:gap="48"
:size="1"
/>
<MiniMap class="hidden sm:block transform scale-25 md:scale-50 lg:scale-75 origin-bottom-right" :node-color="nodeColor" />
</VueFlow>
</div>
<div class="md:max-w-1/3 flex flex-col gap-12 justify-center <md:pt-12">
<div class="flex flex-col gap-2 items-center md:items-start">
<h1>Customizable</h1>
<p>
You can expand on the existing features by using your own custom nodes and edges and implement any design and
functionality you want.
</p>
<router-link class="docs-button max-w-max" to="/guide/">Documentation</router-link>
</div>
</div>
</template>

View File

@@ -0,0 +1,69 @@
import confetti from 'canvas-confetti'
export const fireworks = (colors?: string[]) => {
return new Promise((resolve) => {
const duration = 15 * 1000
const animationEnd = Date.now() + duration
const defaults = { startVelocity: 60, spread: 360, ticks: 20, zIndex: 0 }
function randomInRange(min: number, max: number) {
return Math.random() * (max - min) + min
}
const interval: any = setInterval(function () {
const timeLeft = animationEnd - Date.now()
if (timeLeft <= 0) {
clearInterval(interval)
return resolve(true)
}
const particleCount = 50 * (timeLeft / duration)
// since particles fall down, start a bit higher than random
confetti(
Object.assign({}, defaults, {
particleCount,
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
colors,
}),
)
confetti(
Object.assign({}, defaults, {
particleCount,
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
colors,
}),
)
}, 250)
})
}
export const cheer = (colors: string[]) => {
return new Promise((resolve) => {
const end = Date.now() + 2 * 1000
function frame() {
confetti({
particleCount: 2,
angle: 60,
spread: 75,
origin: { x: 0 },
colors,
})
confetti({
particleCount: 2,
angle: 120,
spread: 75,
origin: { x: 1 },
colors,
})
if (Date.now() < end) {
requestAnimationFrame(frame)
} else {
resolve(true)
}
}
frame()
})
}

View File

@@ -0,0 +1,46 @@
<script lang="ts" setup>
import type { NodeProps } from '@braks/vue-flow'
import { Handle, Position } from '@braks/vue-flow'
interface RGBNodeProps extends NodeProps {
data: {
color: 'r' | 'g' | 'b'
}
amount: {
red: number
green: number
blue: number
}
}
const props = defineProps<RGBNodeProps>()
const emit = defineEmits(['change'])
let color = 'red'
switch (props.data?.color) {
case 'r':
color = 'red'
break
case 'g':
color = 'green'
break
case 'b':
color = 'blue'
break
}
const onChange = (e: any) => emit('change', { color, val: e.target.value })
</script>
<template>
<div class="px-4 py-2 bg-white rounded-md border-2 border-solid border-black text-left transform scale-75 lg:scale-100">
<div class="text-md" :style="{ color }">{{ `${color} Amount`.toUpperCase() }}</div>
<input
v-model="props.amount[color]"
class="slider nodrag"
:style="{ '--color': color }"
type="range"
min="0"
max="255"
@input="onChange"
/>
<Handle type="source" :position="Position.Right" :style="{ backgroundColor: color }" />
</div>
</template>

View File

@@ -0,0 +1,21 @@
<script lang="ts" setup>
import type { NodeProps } from '@braks/vue-flow'
import { Handle, Position } from '@braks/vue-flow'
interface RBGOutputNodeProps extends NodeProps {
rgb: string
}
const props = defineProps<RBGOutputNodeProps>()
</script>
<template>
<div
:style="{ backgroundColor: props.rgb }"
class="px-6 py-2 rounded-xl text-white text-center min-w-[220px] border-2 border-white"
>
<div class="text-xl font-bold">Color Output</div>
<div class="font-semibold">{{ props.rgb }}</div>
<Handle type="target" :position="Position.Left" />
</div>
</template>

37
docs2/package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "docs2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "vitepress dev src",
"build": "vitepress build src",
"serve": "vitepress serve src",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"@animxyz/core": "^0.6.6",
"@animxyz/vue3": "^0.6.7",
"@braks/vue-flow": "workspace:*",
"@stackblitz/sdk": "^1.8.0",
"@vue/repl": "1.1.2",
"blobity": "^0.1.7",
"canvas-confetti": "^1.5.1"
},
"devDependencies": {
"@iconify/json": "^2.1.81",
"@types/canvas-confetti": "^1.4.3",
"@types/node": "^18.0.6",
"@windicss/plugin-scrollbar": "^1.2.3",
"dotenv": "^16.0.1",
"ohmyfetch": "^0.4.18",
"typedoc": "^0.22.18",
"typedoc-plugin-markdown": "^3.13.4",
"unplugin-auto-import": "^0.9.5",
"unplugin-icons": "^0.14.7",
"unplugin-vue-components": "^0.19.9",
"vite-plugin-windicss": "^1.8.7",
"vitepress": "^1.0.0-alpha.4",
"windicss": "^3.5.6"
}
}

248
docs2/src/.vitepress/auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,248 @@
// Generated by 'unplugin-auto-import'
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const logicAnd: typeof import('@vueuse/core')['logicAnd']
const logicNot: typeof import('@vueuse/core')['logicNot']
const logicOr: typeof import('@vueuse/core')['logicOr']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClamp: typeof import('@vueuse/core')['useClamp']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}

13
docs2/src/.vitepress/components.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
declare module '@vue/runtime-core' {
export interface GlobalComponents {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}
export {}

View File

@@ -0,0 +1,154 @@
import { resolve } from 'path'
import { readdirSync, statSync } from 'fs'
import type { HeadConfig } from 'vitepress'
import { DefaultTheme, defineConfigWithTheme } from 'vitepress'
import WindiCSS from 'vite-plugin-windicss'
import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'
import Components from 'unplugin-vue-components/vite'
import AutoImport from 'unplugin-auto-import/vite'
import { useVueFlow } from '@braks/vue-flow'
import { copyVueFlowPlugin } from './copy-plugin'
import head from './head'
import SidebarGroup = DefaultTheme.SidebarGroup
const { vueFlowVersion } = useVueFlow()
function capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
const typedocSidebarEntries = (): SidebarGroup[] => {
const filePath = resolve(__dirname, '../../typedocs')
const docsModules = readdirSync(filePath).filter((name) => statSync(`${filePath}/${name}`).isDirectory())
const sidebarItems = docsModules.map((module) => {
let children = readdirSync(`${filePath}/${module}/`).map((entry) => `/typedocs/${module}/${entry}`)
if (module === 'variables') {
children = children.filter((child) => {
return child.includes('default')
})
}
return { text: capitalize(module), children }
})
return [
{
text: 'Modules',
link: '/typedocs/',
items: [...sidebarItems],
},
]
}
export default defineConfigWithTheme<DefaultTheme.Config>({
title: 'Vue Flow',
description: 'Visualize your ideas with Vue Flow, a highly customizable Vue3 Flowchart library.',
head: head as HeadConfig[],
outDir: resolve(__dirname, '../../dist'),
vite: {
optimizeDeps: {
exclude: ['@animxyz/vue3'],
},
plugins: [
copyVueFlowPlugin(),
AutoImport({
imports: ['vue', '@vueuse/core'],
dts: resolve(__dirname, './auto-imports.d.ts'),
}),
WindiCSS({
transformCSS: 'pre',
config: resolve(__dirname, './windi.config.ts'),
}),
Components({
dirs: [resolve(__dirname, '../../components')],
deep: true,
// allow auto load markdown components under `./src/components/`
extensions: ['vue', 'md'],
// allow auto import and register components used in markdown
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
dts: resolve(__dirname, './components.d.ts'),
resolvers: [IconsResolver()],
}),
Icons({
compiler: 'vue3',
}),
],
},
themeConfig: {
logo: '/favicons/android-chrome-512x512.png',
nav: [
{ text: `v${vueFlowVersion.value}`, link: '/' },
{ text: 'Guide', link: '/guide/', activeMatch: '^/guide/' },
{
text: 'Examples',
link: '/examples/',
activeMatch: '^/examples/',
},
{
text: 'TypeDocs',
link: '/typedocs/',
activeMatch: '^/typedocs/',
},
],
sidebar: {
'/guide/': [
{
text: 'Guide',
items: [
{ text: 'Introduction', link: '/guide/' },
{ text: 'Getting Started', link: '/guide/getting-started' },
{ text: 'Theming', link: '/guide/theming' },
{ text: 'Nodes', link: '/guide/node' },
{ text: 'Edges', link: '/guide/edge' },
{ text: 'Composables', link: '/guide/composables' },
],
},
{
text: 'Vue Flow',
items: [
{ text: 'Config / Props', link: '/guide/vue-flow/config' },
{ text: 'State', link: '/guide/vue-flow/state' },
{
text: 'Actions',
link: '/typedocs/interfaces/Actions.html',
},
{
text: 'Getters',
link: '/typedocs/interfaces/Getters.html',
},
{
text: 'Events',
link: '/typedocs/interfaces/FlowEvents.html',
},
{ text: 'Slots', link: '/guide/vue-flow/slots' },
],
},
{
text: 'Utilities',
items: [
{ text: 'Graph', link: '/guide/utils/graph' },
{ text: 'Viewport', link: '/guide/utils/instance' },
{ text: 'Edges', link: '/guide/utils/edge' },
],
},
{
text: 'Components',
items: [
{ text: 'Background', link: '/guide/components/background' },
{ text: 'MiniMap', link: '/guide/components/minimap' },
{ text: 'MiniMapNode', link: '/guide/components/minimap-node' },
{ text: 'Controls', link: '/guide/components/controls' },
{ text: 'Control Button', link: '/guide/components/control-button' },
],
},
],
},
},
})

View File

@@ -0,0 +1,21 @@
import { existsSync, readFileSync } from 'fs'
import { resolve } from 'path'
import type { Plugin } from 'vite'
export function copyVueFlowPlugin(): Plugin {
return {
name: 'copy-vue-flow',
generateBundle() {
const filePath = resolve(__dirname, '../../node_modules/@braks/vue-flow/dist/vue-flow.es.js')
if (!existsSync(filePath)) {
throw new Error(`@braks/vue-flow/dist/vue-flow.es.js not built. ` + `Run "pnpm -w build" first.`)
}
this.emitFile({
type: 'asset',
fileName: 'vue-flow.es.js',
source: readFileSync(filePath, 'utf-8'),
})
},
}
}

View File

@@ -0,0 +1,132 @@
export const meta = {
title: '🌊 Vue Flow - The customizable Vue3 Flowchart Library',
description: `Bring interactivity to your flowcharts and graphs with Vue Flow. It's a highly customizable Vue3 Flowchart library that can be used to visualize your ideas with Vue3 Flowchart. Features seamless zoom & pan and a ton of more features!`,
keywords: 'vue, flowchart, graph, vue3, vuejs, vite, nuxt3, nuxtjs, quasar',
img: 'https://images.prismic.io/bcakmakoglu/8fbdad18-3cd4-46a9-83cf-dbd9fbf60484_vue-flow.png?auto=compress,format',
url: 'https://vueflow.dev/',
}
export default [
[
'meta',
{
name: 'description',
content: meta.description,
},
],
['meta', { hid: 'og:title', name: 'og:title', content: meta.title }],
[
'meta',
{
hid: 'og:description',
property: 'og:description',
content: meta.description,
},
],
[
'meta',
{
hid: 'og:image',
property: 'og:image',
content: `http://${meta.img}`,
},
],
[
'meta',
{
hid: 'og:image:secure_url',
property: 'og:image:secure_url',
content: `https://${meta.img}`,
},
],
[
'meta',
{
hid: 'og:image:type',
property: 'og:image:type',
content: 'image/png',
},
],
[
'meta',
{
hid: 'og:image:width',
property: 'og:image:width',
content: '2428',
},
],
[
'meta',
{
hid: 'og:image:height',
property: 'og:image:height',
content: '1280',
},
],
['meta', { hid: 'og:url', property: 'og:url', content: meta.url }],
[
'meta',
{
hid: 'twitter:card',
name: 'twitter:card',
content: 'summary_large_image',
},
],
[
'meta',
{
hid: 'twitter:url',
name: 'twitter:url',
content: meta.url,
},
],
[
'meta',
{
hid: 'twitter:title',
name: 'twitter:title',
content: meta.title,
},
],
[
'meta',
{
hid: 'twitter:description',
name: 'twitter:description',
content: meta.description,
},
],
[
'meta',
{
hid: 'twitter:image',
name: 'twitter:image',
content: meta.img,
},
],
[
'link',
{
hid: 'canonical',
rel: 'canonical',
href: meta.url,
},
],
[
'link',
{
hid: 'image_src',
rel: 'image_src',
href: meta.img,
},
],
['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/favicons/apple-touch-icon.png' }],
['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicons/favicon-32x32.png' }],
['link', { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicons/favicon-16x16.png' }],
['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicons/android-chrome-192x192.png' }],
['link', { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicons/android-chrome-512x512.png' }],
['link', { rel: 'manifest', href: '/favicons/site.webmanifest' }],
['link', { rel: 'shortcut icon', href: '/favicons/favicon.ico' }],
['meta', { name: 'msapplication-TileColor', content: '#10b981' }],
['meta', { name: 'theme-color', content: '#10b981' }],
]

View File

@@ -0,0 +1,12 @@
import type { Theme } from 'vitepress'
import VueAnimXyz from '@animxyz/vue3'
import DefaultTheme from 'vitepress/theme'
const CustomTheme = {
...DefaultTheme,
enhanceApp: ({ app }) => {
app.use(VueAnimXyz)
},
} as Theme
export default CustomTheme

View File

@@ -0,0 +1,83 @@
<script>
import 'virtual:windi.css'
import '@animxyz/core'
import '@braks/vue-flow/dist/style.css'
import '@braks/vue-flow/dist/theme-default.css'
import '../../assets/index.css'
</script>
<script setup>
import DefaultTheme from 'vitepress/theme'
import ReactIcon from '~icons/logos/react'
import GithubIcon from '~icons/carbon/logo-github'
import DiscordIcon from '~icons/logos/discord-icon'
const { Layout: ParentLayout } = DefaultTheme
</script>
<template>
<ParentLayout>
<template #nav-bar-title-before>
<XyzTransition appear-visible xyz="fade perspective-1 down ease-out-back duration-10">
<div
class="border-1 border-gray-300 bg-white dark:bg-gray-800 dark:(shadow-gray-700 shadow-md) shadow-lg select-none flex justify-center items-center mx-2 gap-6 px-4 py-2 rounded-lg"
>
<a
href="https://github.com/bcakmakoglu/vue-flow"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
class="bg-white rounded-full relative group cursor-pointer flex justify-center items-center"
>
<GithubIcon class="text-black text-xl transform group-hover:scale-125 transition transition-all ease duration-300" />
<span
class="transition-opacity ease-in duration-200 opacity-0 group-hover:opacity-100 bg-black dark:bg-white text-white dark:text-black rounded-md p-2 absolute top-[25px] right-0 mt-2 mr-2 text-xs"
>
Contribute on GitHub!
</span>
</a>
<a
href="https://discord.gg/F4v6qE4Fuq"
target="_blank"
rel="noopener noreferrer"
aria-label="Discord"
class="relative group cursor-pointer flex justify-center items-center"
>
<DiscordIcon class="text-xl transform group-hover:scale-125 transition transition-all ease duration-300" />
<span
class="transition-opacity ease-in duration-200 opacity-0 group-hover:opacity-100 bg-black dark:bg-white text-white dark:text-black rounded-md p-2 absolute top-[25px] right-0 mt-2 mr-2 text-xs"
>
Join the Discord server!
</span>
</a>
<a
href="https://reactflow.dev/"
target="_blank"
rel="noopener noreferrer"
aria-label="React Flow"
class="relative group cursor-pointer flex justify-center items-center"
>
<ReactIcon class="text-xl transform group-hover:scale-125 transition transition-all ease duration-300" />
<span
class="transition-opacity ease-in duration-200 opacity-0 group-hover:opacity-100 bg-black dark:bg-white text-white dark:text-black rounded-md p-2 absolute top-[25px] right-0 mt-2 mr-2 text-xs"
>
Check out the original React Flow by Wbkd!
</span>
</a>
</div>
</XyzTransition>
<div class="hidden md:(block w-8) lg:w-12" />
</template>
<template #layout-bottom>
<div class="page-footer">MIT Licensed | Copyright © 2021-present Burak Cakmakoglu</div>
</template>
</ParentLayout>
</template>
<style>
.page-footer {
@apply dark:(text-white bg-black) pt-[2rem] text-center;
border-top: 1px solid var(--c-border);
transition: border-color var(--t-color);
}
</style>

View File

@@ -0,0 +1,73 @@
import { resolve } from 'path'
import { defineConfig } from 'windicss/helpers'
import typography from 'windicss/plugin/typography'
import scrollbar from '@windicss/plugin-scrollbar'
export default defineConfig({
extract: {
include: [
resolve(__dirname, './theme/**/*.{ts,md,vue}'),
resolve(__dirname, '../../components/**/*.{ts,md,vue}'),
resolve(__dirname, '../**/*.{ts,md,vue}'),
],
},
darkMode: 'class',
plugins: [
typography({
dark: true,
}),
scrollbar,
],
shortcuts: {
'primary-gradient': 'bg-gradient-to-b from-accent-500 via-accent-700 to-accent-900',
},
theme: {
extend: {
colors: {
primary: {
'50': '#fcf9ff',
'100': '#f8f3ff',
'200': '#eee1fe',
'300': '#e4cffe',
'400': '#cfaafd',
'500': '#BB86FC',
'600': '#a879e3',
'700': '#8c65bd',
'800': '#705097',
'900': '#5c427b',
'DEFAULT': '#BB86FC',
},
secondary: {
'50': '#c4fef9',
'100': '#9df7ef',
'200': '#77f0e4',
'300': '#50e8da',
'400': '#2ae1cf',
'500': '#03dac5',
'600': '#03ae9d',
'700': '#028276',
'800': '#02564e',
'900': '#012a26',
'DEFAULT': '#03dac5',
},
accent: {
'50': '#e6d4ff',
'100': '#ccaafc',
'200': '#b17ff8',
'300': '#9755f5',
'400': '#7c2af1',
'500': '#6200ee',
'600': '#5000c1',
'700': '#3d0095',
'800': '#2b0068',
'900': '#18003b',
'DEFAULT': '#6200ee',
},
},
},
},
})

111
docs2/src/assets/index.css Normal file
View File

@@ -0,0 +1,111 @@
:root {
--primary: #BB86FC;
--secondary: #03dac5;
}
.dark {
--c-text-lighter: white !important;
--c-text: white !important;
--c-bg: black !important;
}
.dark .tip {
--c-bg-light: gray;
}
.tip {
--c-bg-light: white;
}
html, body {
@apply scrollbar scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-green-500 scrollbar-track-black;
}
.home {
--content-width: 100%;
max-width: 100% !important;
@apply !px-0;
}
.page a {
@apply text-green-500 font-semibold;
}
.examples .page, .examples .page-meta, .footer, .sidebar {
@apply dark:(!bg-black text-white);
}
.examples .page {
--content-width: 100%;
}
.theme-default-content {
@apply overflow-scroll md:overflow-hidden;
}
.navbar {
@apply dark:bg-gray-900;
}
h1 {
@apply text-xl lg:text-4xl mb-4 font-bold;
}
h2 {
@apply text-lg lg:text-2xl mb-4 font-semibold;
}
p {
line-height: inherit;
margin: revert;
}
ul li {
@apply mt-2;
}
.docs-button {
@apply z-1 shadow-lg transition-colors duration-200 text-white font-semibold text-lg mt-4 px-5 py-3 rounded-lg bg-green-500;
}
.vue-flow__node.dark {
@apply bg-gray-800 text-white;
}
.vue-flow__node-selector {
font-size: 14px;
background: #f0f2f3;
border: 1px solid #555;
border-radius: 5px;
text-align: center;
}
.vue-flow__node-selector .vue-flow__handle {
border-color: #f0f2f3;
}
.slider {
--color: red;
@apply bg-gray-200 w-full h-[10px] outline-none rounded-full;
-webkit-appearance: none;
appearance: none;
&::-moz-range-thumb {
@apply w-[15px] h-[15px] cursor-pointer border-1 border-solid border-white rounded-full;
-webkit-appearance: none;
background: var(--color);
}
&::-webkit-slider-thumb {
@apply w-[15px] h-[15px] cursor-pointer border-1 border-solid border-white rounded-full;
-webkit-appearance: none;
background: var(--color);
}
}
.theme-container.examples .theme-default-content .vue-repl pre, .theme-default-content .vue-repl pre[class*=language-] {
padding: unset;
margin: unset;
border-radius: unset;
line-height: unset;
}

View File

@@ -0,0 +1 @@
<svg id="visual" viewBox="0 0 960 540" width="960" height="540" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><rect width="960" height="540" fill="#31475e"></rect><g><g transform="translate(57 45)"><path d="M0 -106.9L83.6 -66.7L104.2 23.8L46.4 96.3L-46.4 96.3L-104.2 23.8L-83.6 -66.7Z" fill="#3fb984"></path></g><g transform="translate(708 88)"><path d="M0 -66L51.6 -41.2L64.3 14.7L28.6 59.5L-28.6 59.5L-64.3 14.7L-51.6 -41.2Z" fill="#3fb984"></path></g><g transform="translate(823 510)"><path d="M0 -77L60.2 -48L75.1 17.1L33.4 69.4L-33.4 69.4L-75.1 17.1L-60.2 -48Z" fill="#3fb984"></path></g><g transform="translate(896 280)"><path d="M0 -45L35.2 -28.1L43.9 10L19.5 40.5L-19.5 40.5L-43.9 10L-35.2 -28.1Z" fill="#3fb984"></path></g><g transform="translate(287 458)"><path d="M0 -58L45.3 -36.2L56.5 12.9L25.2 52.3L-25.2 52.3L-56.5 12.9L-45.3 -36.2Z" fill="#3fb984"></path></g><g transform="translate(12 380)"><path d="M0 -101L79 -63L98.5 22.5L43.8 91L-43.8 91L-98.5 22.5L-79 -63Z" fill="#3fb984"></path></g><g transform="translate(609 440)"><path d="M0 -60L46.9 -37.4L58.5 13.4L26 54.1L-26 54.1L-58.5 13.4L-46.9 -37.4Z" fill="#3fb984"></path></g><g transform="translate(457 42)"><path d="M0 -81L63.3 -50.5L79 18L35.1 73L-35.1 73L-79 18L-63.3 -50.5Z" fill="#3fb984"></path></g></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

24
docs2/src/examples/dnd.md Normal file
View File

@@ -0,0 +1,24 @@
---
pageClass: examples
---
# Drag & Drop
Adding nodes to an already existing graph can be done multiple ways. To create an interactive editor, you would probably
like them to be inserted with drag and drop.
In this example we demonstrate how to build a basic Sidebar and implement drag and drop handlers to create new nodes.
## State access
This example shows another key feature of Vue Flow. You can initialize the Flow state at any point before the `VueFlow` component
is actually mounted. This can be achieved by using the [`useVueFlow`](/guide/composables.html#usevueflow) composable.
<div class="mt-6">
<client-only>
<Suspense>
<Repl example="dnd"></Repl>
</Suspense>
</client-only>
</div>

View File

@@ -0,0 +1,19 @@
---
pageClass: examples
---
# Custom Connection Line
If the default connection lines aren't to your liking, or you want to expand on the existing
functionality you can use your own custom connection line.
Simply pass a component in the designated template slot, and you're good to go.
<div class="mt-6">
<client-only>
<Suspense>
<Repl example="connectionline"></Repl>
</Suspense>
</client-only>
</div>

View File

@@ -0,0 +1,17 @@
---
pageClass: examples
---
# Edges
Vue Flow comes with four pre-defined edge types - bezier-, step-, smoothstep and straight-edges.
In addition to the built-in edge types you can create your own custom edges. You can find more information on edge types [here](/guide/edge.html#default-edge-types).
<div class="mt-6">
<client-only>
<Suspense>
<Repl example="edges"></Repl>
</Suspense>
</client-only>
</div>

View File

@@ -0,0 +1,22 @@
---
pageClass: examples
---
# Updatable Edge
Existing edges can be updated, meaning their source / target position can be changed interactively.
Update an edge by simply dragging it from one node to another at the edge-anchor (handles).
You can enable updating edges either globally by passing the `edgesUpdatable` prop or you can enable it
for specific edges by using the `updatable` attribute.
<div class="mt-6">
<client-only>
<Suspense>
<Repl example="updateEdge"></Repl>
</Suspense>
</client-only>
</div>

View File

@@ -0,0 +1,39 @@
---
pageClass: examples
---
# Connection Validation
Connections can be validated before edges are created and nodes get connected.
## Using a handle in a custom node
```vue:no-line-numbers
<div>
[ ... ]
<Handle type="source" :is-valid-connection="isValidConnection" />
</div>
```
## Passing as node option
```ts
const nodes = [
{
id: '1',
label: 'Node 1',
position: { x: 0, y: 0 },
isValidSourcePos: (connection) => {
return connection.target === '2'
},
},
]
```
<div class="mt-6">
<client-only>
<Suspense>
<Repl example="validation"></Repl>
</Suspense>
</client-only>
</div>

View File

@@ -0,0 +1,17 @@
---
pageClass: examples
---
# Empty
Similar to the drag and drop example, we can also add nodes to an empty graph on a button click,
which triggers a push into our elements / nodes array.
<div class="mt-6">
<client-only>
<Suspense>
<Repl example="empty"></Repl>
</Suspense>
</client-only>
</div>

View File

@@ -0,0 +1,25 @@
---
pageClass: examples
---
This example demonstrates "floating edges".
The positions of the source and target nodes are calculated dynamically,
the caveat here is, that edge-anchors are still tied to Handles (even though invisible in this example),
so connections would, by default, only be triggered when hovering over the handles (bottom / top by default).
<div class="mt-6">
<iframe src="https://codesandbox.io/embed/vue-flow-floating-edges-zfxdok?eslint=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2Fcomponents%2FFlow.vue&theme=dark"
class="hidden dark:block bg-black h-full w-full min-h-[75vh]"
title="Vue Flow: Floating Edges"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>
<iframe src="https://codesandbox.io/embed/vue-flow-floating-edges-zfxdok?eslint=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2Fcomponents%2FFlow.vue&theme=light"
class="block dark:hidden h-full w-full min-h-[75vh]"
title="Vue Flow: Floating Edges"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>
</div>

View File

@@ -0,0 +1,24 @@
---
pageClass: examples
---
# Hidden
You can toggle the visibility of nodes by simply setting their `hidden` attribute to either `true` or `false`.
Edges that are connected to a hidden node will also be hidden.
If you set the `onlyRenderVisibleElements` prop to `true`, only elements that are visible inside the current viewpane will be rendered.
This behavior is disabled by default, meaning all elements are rendered whether they are inside the view or not.
This can save you some time on initial renders but consider that moving the pane can cause multiple renders of nodes into the dom,
which can cause performance spikes depending on the complexity of the components that have to be mounted.
<div class="mt-6">
<client-only>
<Suspense>
<Repl example="hidden"></Repl>
</Suspense>
</client-only>
</div>

View File

@@ -0,0 +1,19 @@
---
pageClass: examples
---
# Horizontal Layout
Vue Flow doesn't require you to use vertical layouts.
You can use horizontal layouts with the default node types as well.
You can define where source or target handle positions are with the `sourcePosition` or `targetPosition` properties of nodes.
<div class="mt-6">
<client-only>
<Suspense>
<Repl example="horizontal"></Repl>
</Suspense>
</client-only>
</div>

Some files were not shown because too many files have changed in this diff Show More