Improve graphfx-ui node UX
This commit is contained in:
@@ -19,6 +19,8 @@ const edges = ref<Edge[]>([])
|
||||
const warnings = ref<GraphFxFlowWarning[]>([])
|
||||
const layoutDirection = ref<'LR' | 'TB'>('LR')
|
||||
const preferSavedPositions = ref(false)
|
||||
const selectedSaveSlot = ref(localStorage.getItem('GRAPHFX_UI_SELECTED_SAVE_SLOT') || 'My first graph')
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const { fitView } = useVueFlow()
|
||||
|
||||
const stats = computed(() => ({
|
||||
@@ -51,6 +53,49 @@ function loadSample() {
|
||||
refreshFlow()
|
||||
}
|
||||
|
||||
function saveSlotKey(name = selectedSaveSlot.value) {
|
||||
return `GRAPHFX_UI_SAVE_${name}`
|
||||
}
|
||||
|
||||
function saveGraph() {
|
||||
refreshFlow()
|
||||
if (parseError.value) return
|
||||
localStorage.setItem('GRAPHFX_UI_SELECTED_SAVE_SLOT', selectedSaveSlot.value)
|
||||
localStorage.setItem(saveSlotKey(), JSON.stringify(graph.value, null, 2))
|
||||
}
|
||||
|
||||
function loadGraph() {
|
||||
const saved = localStorage.getItem(saveSlotKey())
|
||||
if (!saved) {
|
||||
parseError.value = `No saved graph named "${selectedSaveSlot.value}".`
|
||||
return
|
||||
}
|
||||
localStorage.setItem('GRAPHFX_UI_SELECTED_SAVE_SLOT', selectedSaveSlot.value)
|
||||
editorText.value = saved
|
||||
refreshFlow()
|
||||
}
|
||||
|
||||
function exportGraph() {
|
||||
refreshFlow()
|
||||
if (parseError.value) return
|
||||
const blob = new Blob([JSON.stringify(graph.value, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${selectedSaveSlot.value || 'graphfx-graph'}.json`
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
async function importGraph(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
editorText.value = await file.text()
|
||||
input.value = ''
|
||||
refreshFlow()
|
||||
}
|
||||
|
||||
function relayout() {
|
||||
const model = graphFxToVueFlow(graph.value)
|
||||
nodes.value = layoutGraph(model.nodes, model.edges, {
|
||||
@@ -86,6 +131,15 @@ refreshFlow()
|
||||
<button type="button" class="secondary" @click="loadSample">Load sample</button>
|
||||
</div>
|
||||
|
||||
<div class="savebar">
|
||||
<input v-model="selectedSaveSlot" type="text" aria-label="Save slot name" />
|
||||
<button type="button" class="secondary" @click="loadGraph">Load</button>
|
||||
<button type="button" class="secondary" @click="saveGraph">Save</button>
|
||||
<button type="button" class="secondary" @click="fileInput?.click()">Import</button>
|
||||
<button type="button" class="secondary" @click="exportGraph">Export</button>
|
||||
<input ref="fileInput" class="visually-hidden" type="file" accept="application/json,.json" @change="importGraph" />
|
||||
</div>
|
||||
|
||||
<div class="options">
|
||||
<label>
|
||||
Layout
|
||||
|
||||
@@ -7,45 +7,64 @@ const props = defineProps<{
|
||||
data: GraphFxNodeData
|
||||
}>()
|
||||
|
||||
function typeClass(port: GraphFxPort) {
|
||||
return `io-type-${port.type.toLowerCase()}`
|
||||
}
|
||||
|
||||
function valuePreview(port: GraphFxPort) {
|
||||
if (port.output) return 'connected'
|
||||
if (port.value === null || port.value === undefined) return 'empty'
|
||||
if (typeof port.value === 'string') return port.value.length > 28 ? `${port.value.slice(0, 28)}…` : port.value
|
||||
if (port.output) return ''
|
||||
if (port.value === null || port.value === undefined) return ''
|
||||
if (typeof port.value === 'string') return port.value.length > 24 ? `${port.value.slice(0, 24)}…` : port.value
|
||||
if (typeof port.value === 'number' || typeof port.value === 'boolean') return String(port.value)
|
||||
if (typeof port.value === 'object') return 'object'
|
||||
return String(port.value)
|
||||
}
|
||||
|
||||
function hasCustomLabel(port: GraphFxPort) {
|
||||
return port.label && port.label !== port.name
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article class="graphfx-node">
|
||||
<header class="graphfx-node__header">
|
||||
<p class="graphfx-node__kind">GraphFX</p>
|
||||
<h3>{{ data.name }}</h3>
|
||||
<small>{{ data.id }}</small>
|
||||
</header>
|
||||
|
||||
<section class="graphfx-node__ports" :class="{ 'graphfx-node__ports--empty': !data.inputs.length && !data.outputs.length }">
|
||||
<div class="graphfx-node__column">
|
||||
<p class="graphfx-node__column-title">Inputs</p>
|
||||
<div v-if="!data.inputs.length" class="graphfx-node__empty">none</div>
|
||||
<div v-for="port in data.inputs" :key="port.name" class="graphfx-node__port graphfx-node__port--input">
|
||||
<div class="graphfx-node__column graphfx-node__column--inputs">
|
||||
<div v-if="!data.inputs.length" class="graphfx-node__empty">no inputs</div>
|
||||
<div
|
||||
v-for="port in data.inputs"
|
||||
:key="port.name"
|
||||
class="graphfx-node__port graphfx-node__port--input"
|
||||
:class="[typeClass(port), { 'graphfx-node__port--connected': port.output, 'graphfx-node__port--labeled': hasCustomLabel(port) }]"
|
||||
:title="`${port.name} · ${port.type}${port.output ? ` · ${port.output}` : ''}`"
|
||||
>
|
||||
<Handle
|
||||
type="target"
|
||||
:id="getInputHandleId(data.id, port.name)"
|
||||
:position="Position.Left"
|
||||
class="graphfx-node__handle graphfx-node__handle--input"
|
||||
/>
|
||||
<span class="graphfx-node__port-label">{{ port.label }}</span>
|
||||
<span class="graphfx-node__port-value" :title="String(port.value ?? '')">{{ valuePreview(port) }}</span>
|
||||
<span class="graphfx-node__port-name">{{ port.name }}</span>
|
||||
<span v-if="hasCustomLabel(port)" class="graphfx-node__port-label">{{ port.label }}</span>
|
||||
<span v-if="valuePreview(port)" class="graphfx-node__port-value">{{ valuePreview(port) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="graphfx-node__column graphfx-node__column--outputs">
|
||||
<p class="graphfx-node__column-title">Outputs</p>
|
||||
<div v-if="!data.outputs.length" class="graphfx-node__empty">none</div>
|
||||
<div v-for="port in data.outputs" :key="port.name" class="graphfx-node__port graphfx-node__port--output">
|
||||
<span class="graphfx-node__port-label">{{ port.label }}</span>
|
||||
<div v-if="!data.outputs.length" class="graphfx-node__empty">no outputs</div>
|
||||
<div
|
||||
v-for="port in data.outputs"
|
||||
:key="port.name"
|
||||
class="graphfx-node__port graphfx-node__port--output"
|
||||
:class="[typeClass(port), { 'graphfx-node__port--labeled': hasCustomLabel(port) }]"
|
||||
:title="`${port.name} · ${port.type}`"
|
||||
>
|
||||
<span class="graphfx-node__port-name">{{ port.name }}</span>
|
||||
<span v-if="hasCustomLabel(port)" class="graphfx-node__port-label">{{ port.label }}</span>
|
||||
<Handle
|
||||
type="source"
|
||||
:id="getOutputHandleId(data.id, port.name)"
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { Edge, Node } from '@vue-flow/core'
|
||||
import { MarkerType, type Edge, type Node } from '@vue-flow/core'
|
||||
import { ioTypeFor, type GraphFxIoType } from './nodeDefinitions'
|
||||
|
||||
export type GraphFxIoValue = {
|
||||
label?: string | null
|
||||
value?: unknown
|
||||
output?: string | null
|
||||
type?: GraphFxIoType
|
||||
definition?: { type?: GraphFxIoType }
|
||||
}
|
||||
|
||||
export type GraphFxSerializedNode = {
|
||||
@@ -27,7 +30,7 @@ export type GraphFxSerializedGraph = GraphFxSerializedNode[]
|
||||
export type GraphFxPort = {
|
||||
name: string
|
||||
label: string
|
||||
type?: string
|
||||
type: GraphFxIoType
|
||||
output?: string | null
|
||||
value?: unknown
|
||||
}
|
||||
@@ -63,14 +66,25 @@ export function getOutputHandleId(nodeId: string, outputName: string) {
|
||||
return `${nodeId}:out:${outputName}`
|
||||
}
|
||||
|
||||
const ioTypeColors: Record<GraphFxIoType, string> = {
|
||||
Image: '#4dd7ff',
|
||||
Number: '#f6c85f',
|
||||
String: '#9ee66f',
|
||||
Boolean: '#c084fc',
|
||||
Color: '#ff78b7',
|
||||
Font: '#fb923c',
|
||||
Unknown: '#94a3b8',
|
||||
}
|
||||
|
||||
function labelFor(name: string, io?: GraphFxIoValue) {
|
||||
return io?.label || name
|
||||
}
|
||||
|
||||
function toPorts(record: Record<string, GraphFxIoValue> | undefined, direction: 'in' | 'out') {
|
||||
function toPorts(nodeName: string, record: Record<string, GraphFxIoValue> | undefined, direction: 'in' | 'out') {
|
||||
return Object.entries(record || {}).map(([name, io]) => ({
|
||||
name,
|
||||
label: labelFor(name, io),
|
||||
type: ioTypeFor(nodeName, direction, name, io?.type || io?.definition?.type),
|
||||
output: direction === 'in' ? io?.output || null : null,
|
||||
value: direction === 'in' ? io?.value : undefined,
|
||||
}))
|
||||
@@ -121,8 +135,8 @@ export function graphFxToVueFlow(graph: GraphFxSerializedGraph): GraphFxFlowMode
|
||||
.filter((item) => item?.node?.id && item.node.name)
|
||||
.map((item, index) => {
|
||||
const { hasSavedPosition, position } = savedPosition(item, index)
|
||||
const inputs = toPorts(item.node.options?.in, 'in')
|
||||
const outputs = toPorts(item.node.options?.out, 'out')
|
||||
const inputs = toPorts(item.node.name, item.node.options?.in, 'in')
|
||||
const outputs = toPorts(item.node.name, item.node.options?.out, 'out')
|
||||
|
||||
return {
|
||||
id: item.node.id,
|
||||
@@ -159,15 +173,20 @@ export function graphFxToVueFlow(graph: GraphFxSerializedGraph): GraphFxFlowMode
|
||||
continue
|
||||
}
|
||||
|
||||
const sourceNode = nodes.find((node) => node.id === source.nodeId)
|
||||
const sourcePort = sourceNode?.data?.outputs.find((output) => output.name === source.outputName)
|
||||
const edgeColor = ioTypeColors[sourcePort?.type || 'Unknown']
|
||||
|
||||
edges.push({
|
||||
id: `${source.nodeId}:${source.outputName}->${item.node.id}:${inputName}`,
|
||||
source: source.nodeId,
|
||||
target: item.node.id,
|
||||
sourceHandle: getOutputHandleId(source.nodeId, source.outputName),
|
||||
targetHandle: getInputHandleId(item.node.id, inputName),
|
||||
label: inputName,
|
||||
type: 'smoothstep',
|
||||
animated: false,
|
||||
style: { stroke: edgeColor, strokeWidth: 2.8 },
|
||||
markerEnd: { type: MarkerType.ArrowClosed, color: edgeColor },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
137
graphfx-ui/src/lib/nodeDefinitions.ts
Normal file
137
graphfx-ui/src/lib/nodeDefinitions.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
export type GraphFxIoType = 'Image' | 'Number' | 'String' | 'Boolean' | 'Color' | 'Font' | 'Unknown'
|
||||
|
||||
export type GraphFxNodeDefinition = {
|
||||
in?: Record<string, GraphFxIoType>
|
||||
out?: Record<string, GraphFxIoType>
|
||||
}
|
||||
|
||||
const imageDimensions = {
|
||||
image: 'Image',
|
||||
width: 'Number',
|
||||
height: 'Number',
|
||||
} as const
|
||||
|
||||
const canvas2dBase = {
|
||||
in: { image: 'Image', width: 'Number' },
|
||||
out: imageDimensions,
|
||||
} as const
|
||||
|
||||
const webglBase = {
|
||||
in: { image: 'Image' },
|
||||
out: imageDimensions,
|
||||
} as const
|
||||
|
||||
export const graphFxNodeDefinitions: Record<string, GraphFxNodeDefinition> = {
|
||||
Webcam: { out: { image: 'Image', width: 'Number', height: 'Number', camera: 'String' } },
|
||||
Resize: { in: { ...canvas2dBase.in, height: 'Number' }, out: canvas2dBase.out },
|
||||
Compose: {
|
||||
in: {
|
||||
...canvas2dBase.in,
|
||||
height: 'Number',
|
||||
fg: 'Image',
|
||||
fgX: 'Number',
|
||||
fgY: 'Number',
|
||||
bg: 'Image',
|
||||
bgX: 'Number',
|
||||
bgY: 'Number',
|
||||
background: 'Image',
|
||||
overlay: 'Image',
|
||||
qr: 'Image',
|
||||
},
|
||||
out: { image: 'Image' },
|
||||
},
|
||||
Fill: { in: { ...canvas2dBase.in, height: 'Number', color: 'Color' }, out: canvas2dBase.out },
|
||||
Flip: { in: { ...canvas2dBase.in, horizontal: 'Boolean', vertical: 'Boolean' }, out: canvas2dBase.out },
|
||||
Rotate: { in: { ...canvas2dBase.in, angle: 'Number' }, out: canvas2dBase.out },
|
||||
Text: {
|
||||
in: {
|
||||
...canvas2dBase.in,
|
||||
text: 'String',
|
||||
fontSize: 'Number',
|
||||
fontStyle: 'String',
|
||||
font: 'Font',
|
||||
color: 'Color',
|
||||
x: 'Number',
|
||||
y: 'Number',
|
||||
},
|
||||
out: canvas2dBase.out,
|
||||
},
|
||||
EmptySpace: { out: { w: 'Number', h: 'Number', top: 'Number', right: 'Number', bottom: 'Number', left: 'Number' } },
|
||||
|
||||
BrightnessContrast: { in: { ...webglBase.in, brightness: 'Number', contrast: 'Number' }, out: webglBase.out },
|
||||
HSV: { in: { ...webglBase.in, hue: 'Number', saturation: 'Number', value: 'Number' }, out: webglBase.out },
|
||||
Sepia: { in: { ...webglBase.in, amount: 'Number' }, out: webglBase.out },
|
||||
GreenScreen: {
|
||||
in: {
|
||||
...webglBase.in,
|
||||
mask: 'Image',
|
||||
feather: 'Number',
|
||||
balance: 'Number',
|
||||
screen: 'Color',
|
||||
screenWeight: 'Number',
|
||||
clipBlack: 'Number',
|
||||
clipWhite: 'Number',
|
||||
},
|
||||
out: webglBase.out,
|
||||
},
|
||||
Channels: { in: { ...webglBase.in, red: 'Number', green: 'Number', blue: 'Number', alpha: 'Number', amount: 'Number' }, out: webglBase.out },
|
||||
GradientMap: { in: { ...webglBase.in, amount: 'Number', map: 'Image' }, out: webglBase.out },
|
||||
ColorLookupTable: { in: { ...webglBase.in, amount: 'Number', map: 'Image' }, out: webglBase.out },
|
||||
Vignette: { in: { ...webglBase.in, amount: 'Number', size: 'Number' }, out: webglBase.out },
|
||||
Blur: { in: { ...webglBase.in, radius: 'Number', sigma: 'Number', passes: 'Number' }, out: webglBase.out },
|
||||
SoftEdge: { in: { ...webglBase.in, radius: 'Number', sigma: 'Number', passes: 'Number' }, out: webglBase.out },
|
||||
Edge: { in: { ...webglBase.in, dx: 'Number', dy: 'Number' }, out: webglBase.out },
|
||||
KernelPreset: { in: { ...webglBase.in, preset: 'String', passes: 'Number', multiplier: 'Number' }, out: webglBase.out },
|
||||
Levels: { in: { ...webglBase.in, shadow: 'Number', gamma: 'Number', highlight: 'Number', dark: 'Number', light: 'Number' }, out: webglBase.out },
|
||||
LevelsAuto: { in: webglBase.in, out: webglBase.out },
|
||||
Tint: { in: { ...webglBase.in, amount: 'Number', color: 'Color' }, out: webglBase.out },
|
||||
|
||||
BodySegmentation: { in: { image: 'Image', maskBlurAmount: 'Number', foregroundThreshold: 'Number' }, out: imageDimensions },
|
||||
BodyPartSegmentation: { in: { image: 'Image', maskBlurAmount: 'Number', foregroundThreshold: 'Number' }, out: imageDimensions },
|
||||
FaceFeaturePosition: {
|
||||
in: { image: 'Image', modelUrl: 'String' },
|
||||
out: {
|
||||
image: 'Image',
|
||||
faceX: 'Number',
|
||||
faceY: 'Number',
|
||||
faceWidth: 'Number',
|
||||
faceHeight: 'Number',
|
||||
rightEyeX: 'Number',
|
||||
rightEyeY: 'Number',
|
||||
leftEyeX: 'Number',
|
||||
leftEyeY: 'Number',
|
||||
},
|
||||
},
|
||||
|
||||
Numbers: { in: { x: 'Number', y: 'Number', z: 'Number' }, out: { x: 'Number', y: 'Number', z: 'Number' } },
|
||||
NumberBinaryOperation: { in: { x: 'Number', y: 'Number', operation: 'String' }, out: { result: 'Number' } },
|
||||
Font: { in: { fontSize: 'Number', fontStyle: 'String', font: 'Font' }, out: { fontSize: 'Number', fontStyle: 'String', font: 'Font' } },
|
||||
Disable: { in: { image: 'Image', disabled: 'Boolean' }, out: imageDimensions },
|
||||
Api: {
|
||||
in: Object.fromEntries([...Array(8)].flatMap((_, i) => [[`name${i}`, 'String'], [`image${i}`, 'Image']]).concat([['image', 'Image'], ['endpoint', 'String']])) as Record<string, GraphFxIoType>,
|
||||
out: { image: 'Image', response: 'String' },
|
||||
},
|
||||
ApiJson: {
|
||||
in: Object.fromEntries([...Array(8)].map((_, i) => [`propertyPath${i}`, 'String']).concat([['url', 'String']])) as Record<string, GraphFxIoType>,
|
||||
out: Object.fromEntries([...Array(8)].map((_, i) => [`value${i}`, 'String'])) as Record<string, GraphFxIoType>,
|
||||
},
|
||||
Eval: {
|
||||
in: Object.fromEntries([['image', 'Image'], ...Array.from({ length: 8 }, (_, i) => [`i${i}`, 'Number']), ['script', 'String']]) as Record<string, GraphFxIoType>,
|
||||
out: { image: 'Image', result: 'Number', resultString: 'String' },
|
||||
},
|
||||
QRCodeGenerator: { in: { text: 'String', size: 'Number', correction: 'String', margin: 'Number' }, out: { image: 'Image', size: 'Number', margin: 'Number' } },
|
||||
QRCodeDetector: { in: { image: 'Image', filter: 'String' }, out: { image: 'Image', rotation: 'Number', x: 'Number', y: 'Number', size: 'Number' } },
|
||||
MarkerDetector: { in: { image: 'Image', markerId: 'Number', pivotPoint: 'String' }, out: { image: 'Image', markerId: 'Number', x: 'Number', y: 'Number', rotation: 'Number', x1: 'Number', y1: 'Number', x2: 'Number', y2: 'Number', x3: 'Number', y3: 'Number', x4: 'Number', y4: 'Number' } },
|
||||
WaitForAll: { in: { image0: 'Image', image1: 'Image' }, out: { image0: 'Image', image1: 'Image' } },
|
||||
Base64ToImg: { in: { base64: 'String' }, out: imageDimensions },
|
||||
ImgToBase64: { in: { image: 'Image', type: 'String' }, out: { base64: 'String' } },
|
||||
}
|
||||
|
||||
export function ioTypeFor(nodeName: string, direction: 'in' | 'out', ioName: string, fallback?: unknown): GraphFxIoType {
|
||||
const explicit = typeof fallback === 'string' ? fallback : undefined
|
||||
if (explicit && ['Image', 'Number', 'String', 'Boolean', 'Color', 'Font'].includes(explicit)) {
|
||||
return explicit as GraphFxIoType
|
||||
}
|
||||
|
||||
return graphFxNodeDefinitions[nodeName]?.[direction]?.[ioName] || 'Unknown'
|
||||
}
|
||||
@@ -112,6 +112,39 @@ code {
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.savebar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr repeat(4, auto);
|
||||
gap: 0.45rem;
|
||||
padding: 0.62rem;
|
||||
border: 1px solid rgb(255 255 255 / 8%);
|
||||
border-radius: 16px;
|
||||
background: rgb(255 255 255 / 4%);
|
||||
}
|
||||
|
||||
.savebar input[type="text"] {
|
||||
min-width: 0;
|
||||
border: 1px solid rgb(255 255 255 / 12%);
|
||||
border-radius: 10px;
|
||||
padding: 0.55rem 0.68rem;
|
||||
color: #f5f8ff;
|
||||
background: #101827;
|
||||
}
|
||||
|
||||
.savebar button {
|
||||
padding: 0.55rem 0.62rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: grid;
|
||||
gap: 0.72rem;
|
||||
@@ -266,97 +299,136 @@ code {
|
||||
}
|
||||
|
||||
.graphfx-node {
|
||||
min-width: 280px;
|
||||
min-width: 276px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgb(157 177 255 / 30%);
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(180deg, rgb(28 39 63 / 96%), rgb(13 20 34 / 96%));
|
||||
border: 1px solid rgb(157 177 255 / 24%);
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(180deg, rgb(24 34 55 / 97%), rgb(11 17 29 / 97%));
|
||||
box-shadow:
|
||||
0 18px 48px rgb(0 0 0 / 34%),
|
||||
inset 0 1px 0 rgb(255 255 255 / 8%);
|
||||
}
|
||||
|
||||
.graphfx-node__header {
|
||||
padding: 0.9rem 1rem 0.75rem;
|
||||
padding: 0.72rem 0.86rem 0.66rem;
|
||||
border-bottom: 1px solid rgb(255 255 255 / 8%);
|
||||
background:
|
||||
radial-gradient(circle at top right, rgb(55 213 255 / 20%), transparent 10rem),
|
||||
radial-gradient(circle at top right, rgb(55 213 255 / 16%), transparent 9rem),
|
||||
rgb(255 255 255 / 3%);
|
||||
}
|
||||
|
||||
.graphfx-node__kind {
|
||||
margin: 0 0 0.18rem;
|
||||
color: #33e1ba;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 950;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.graphfx-node h3 {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-size: 1.05rem;
|
||||
font-size: 1.02rem;
|
||||
line-height: 1.1;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.graphfx-node small {
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
margin-top: 0.18rem;
|
||||
max-width: 240px;
|
||||
overflow: hidden;
|
||||
color: #8fa4c7;
|
||||
font-size: 0.7rem;
|
||||
color: #8296b8;
|
||||
font-size: 0.66rem;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.graphfx-node__ports {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.55rem;
|
||||
padding: 0.75rem 0.6rem 0.85rem;
|
||||
gap: 0.5rem;
|
||||
padding: 0.58rem 0.56rem 0.68rem;
|
||||
}
|
||||
|
||||
.graphfx-node__ports--empty { grid-template-columns: 1fr; }
|
||||
|
||||
.graphfx-node__column-title {
|
||||
margin: 0 0 0.35rem;
|
||||
padding: 0 0.4rem;
|
||||
color: #7f93b6;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 950;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
.graphfx-node__column {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 0.28rem;
|
||||
}
|
||||
|
||||
.graphfx-node__port {
|
||||
--io-color: #94a3b8;
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 0.08rem;
|
||||
min-height: 31px;
|
||||
padding: 0.38rem 0.45rem;
|
||||
border-radius: 10px;
|
||||
background: rgb(255 255 255 / 5%);
|
||||
color: #dbe7ff;
|
||||
min-height: 34px;
|
||||
padding: 0.36rem 0.48rem;
|
||||
border: 1px solid color-mix(in srgb, var(--io-color) 28%, transparent);
|
||||
border-radius: 11px;
|
||||
background:
|
||||
linear-gradient(90deg, color-mix(in srgb, var(--io-color) 16%, transparent), transparent 46%),
|
||||
rgb(255 255 255 / 4%);
|
||||
color: #eef4ff;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.graphfx-node__port + .graphfx-node__port { margin-top: 0.25rem; }
|
||||
.graphfx-node__port--input {
|
||||
padding-left: 0.72rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.graphfx-node__port--input { padding-left: 0.62rem; }
|
||||
.graphfx-node__port--output { padding-right: 0.62rem; text-align: right; }
|
||||
.graphfx-node__port--output {
|
||||
padding-right: 0.72rem;
|
||||
text-align: right;
|
||||
background:
|
||||
linear-gradient(270deg, color-mix(in srgb, var(--io-color) 16%, transparent), transparent 46%),
|
||||
rgb(255 255 255 / 4%);
|
||||
}
|
||||
|
||||
.graphfx-node__port-label {
|
||||
.graphfx-node__port--labeled {
|
||||
min-height: 44px;
|
||||
background:
|
||||
linear-gradient(90deg, color-mix(in srgb, var(--io-color) 26%, transparent), transparent 58%),
|
||||
rgb(255 255 255 / 6%);
|
||||
}
|
||||
|
||||
.graphfx-node__port--output.graphfx-node__port--labeled {
|
||||
background:
|
||||
linear-gradient(270deg, color-mix(in srgb, var(--io-color) 26%, transparent), transparent 58%),
|
||||
rgb(255 255 255 / 6%);
|
||||
}
|
||||
|
||||
.graphfx-node__port-name {
|
||||
overflow: hidden;
|
||||
color: #f8fbff;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 950;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1.08;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.graphfx-node__port-label {
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
margin-top: 0.2rem;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
padding: 0.11rem 0.42rem;
|
||||
background: color-mix(in srgb, var(--io-color) 88%, white 12%);
|
||||
color: #06101f;
|
||||
font-size: 0.66rem;
|
||||
font-weight: 950;
|
||||
line-height: 1.15;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.graphfx-node__port--output .graphfx-node__port-label {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.graphfx-node__port-value {
|
||||
overflow: hidden;
|
||||
color: #8397ba;
|
||||
font-size: 0.64rem;
|
||||
color: color-mix(in srgb, var(--io-color) 68%, white 32%);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 750;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -370,18 +442,30 @@ code {
|
||||
}
|
||||
|
||||
.graphfx-node__handle {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border: 2px solid #0c1322;
|
||||
background: #33e1ba;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid #0a1020;
|
||||
background: transparent;
|
||||
box-shadow: 0 0 0 2px var(--io-color), 0 0 14px color-mix(in srgb, var(--io-color) 45%, transparent);
|
||||
}
|
||||
|
||||
.graphfx-node__handle--input {
|
||||
left: -6px;
|
||||
background: #7da2ff;
|
||||
}
|
||||
|
||||
.graphfx-node__handle--output {
|
||||
right: -6px;
|
||||
background: #33e1ba;
|
||||
background: var(--io-color);
|
||||
}
|
||||
|
||||
.graphfx-node__port--connected .graphfx-node__handle--input {
|
||||
background: var(--io-color);
|
||||
}
|
||||
|
||||
.io-type-image { --io-color: #4dd7ff; }
|
||||
.io-type-number { --io-color: #f6c85f; }
|
||||
.io-type-string { --io-color: #9ee66f; }
|
||||
.io-type-boolean { --io-color: #c084fc; }
|
||||
.io-type-color { --io-color: #ff78b7; }
|
||||
.io-type-font { --io-color: #fb923c; }
|
||||
.io-type-unknown { --io-color: #94a3b8; }
|
||||
|
||||
Reference in New Issue
Block a user