docs: use vitepress
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
.data.json
|
||||||
|
.temp
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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'
|
||||||
@@ -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' },
|
||||||
|
]
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as CustomConnectionLineApp } from './App.vue?raw'
|
||||||
|
export { default as CustomConnectionLine } from './CustomConnectionLine.vue?raw'
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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'
|
||||||
@@ -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',
|
||||||
|
}
|
||||||
@@ -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%
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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'
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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'
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as EmptyApp } from './App.vue?raw'
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as HiddenApp } from './App.vue?raw'
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as HorizontalApp } from './App.vue?raw'
|
||||||
|
export { default as HorizontalElements } from './initial-elements.js?raw'
|
||||||
@@ -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' },
|
||||||
|
]
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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'
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<script setup>
|
||||||
|
import Flow from './Flow.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="multiflows">
|
||||||
|
<Flow />
|
||||||
|
<Flow />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -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>
|
||||||
@@ -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'
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as NestedApp } from './App.vue?raw'
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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'
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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'
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.vue-flow__node.dark {
|
||||||
|
background: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<script setup>
|
||||||
|
//
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<aside>
|
||||||
|
<div class="description">Teleport destination</div>
|
||||||
|
<div id="port" class="port"></div>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
@@ -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>
|
||||||
@@ -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'
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as UpdateEdgeApp } from './App.vue?raw'
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as UpdateNodeApp } from './App.vue?raw'
|
||||||
|
export { default as UpdateNodeCSS } from './style.css'
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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'
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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
@@ -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']
|
||||||
|
}
|
||||||
Vendored
+13
@@ -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 {}
|
||||||
@@ -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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -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'),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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' }],
|
||||||
|
]
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 |
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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
Reference in New Issue
Block a user