feat: add node calssification

This commit is contained in:
2026-03-10 21:34:39 +01:00
parent eed880e7ef
commit 87c46c25c6
8 changed files with 79 additions and 19 deletions

View File

@@ -30,6 +30,7 @@ import {
ContextMenu, ContextMenu,
ContextMenuContent, ContextMenuContent,
ContextMenuItem, ContextMenuItem,
ContextMenuLabel,
ContextMenuSeparator, ContextMenuSeparator,
ContextMenuSub, ContextMenuSub,
ContextMenuSubContent, ContextMenuSubContent,
@@ -52,6 +53,7 @@ import { ClipboardPaste, FolderOpen, FileStack, CircleDotDashed } from 'lucide-r
import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from '@/lib/flowUtils' import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from '@/lib/flowUtils'
import { import {
getRegisteredNodeTypes, getRegisteredNodeTypes,
getRegisteredNodeTypesGroupedByClassification,
getRegisteredNodeTypeIds, getRegisteredNodeTypeIds,
getDefaultStyle, getDefaultStyle,
isConnectionAllowed, isConnectionAllowed,
@@ -594,17 +596,25 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
<ContextMenuSub> <ContextMenuSub>
<ContextMenuSubTrigger>Create Node</ContextMenuSubTrigger> <ContextMenuSubTrigger>Create Node</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-44"> <ContextMenuSubContent className="w-44">
<ContextMenuGroup> {getRegisteredNodeTypesGroupedByClassification().map(
{getRegisteredNodeTypes().map((desc, index) => ( (group, groupIndex) =>
<React.Fragment key={desc.id}> group.types.length > 0 && (
{index === 2 ? <ContextMenuSeparator /> : null} <React.Fragment key={group.classification}>
<ContextMenuItem onSelect={() => createNode(desc.id)}> {groupIndex > 0 && <ContextMenuSeparator />}
{desc.menuIcon} <ContextMenuGroup>
{desc.menuLabel} <ContextMenuLabel className="text-muted-foreground">
</ContextMenuItem> {group.label}
</React.Fragment> </ContextMenuLabel>
))} {group.types.map((desc) => (
</ContextMenuGroup> <ContextMenuItem key={desc.id} onSelect={() => createNode(desc.id)}>
{desc.menuIcon}
{desc.menuLabel}
</ContextMenuItem>
))}
</ContextMenuGroup>
</React.Fragment>
)
)}
</ContextMenuSubContent> </ContextMenuSubContent>
</ContextMenuSub> </ContextMenuSub>
<ContextMenuSeparator /> <ContextMenuSeparator />

View File

@@ -21,7 +21,7 @@ import { Plus, ListTodo } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useSidebar } from '@/components/ui/sidebar' import { useSidebar } from '@/components/ui/sidebar'
import { usePlatform } from './platformContext' import { usePlatform } from './platformContext'
import { getProjectIcon } from './icon-map' import { getProjectIcon } from '../../lib/iconMap'
import { NewProjectDialog } from './NewProjectDialog' import { NewProjectDialog } from './NewProjectDialog'
import type { Project } from './types' import type { Project } from './types'
@@ -132,7 +132,7 @@ export function AppSidebar() {
<SidebarMenuButton asChild tooltip="All projects" isActive={!selectedProjectId}> <SidebarMenuButton asChild tooltip="All projects" isActive={!selectedProjectId}>
<Link to="/projects"> <Link to="/projects">
<ListTodo className="size-4" /> <ListTodo className="size-4" />
<span>All projects</span> <span>Pleroma</span>
</Link> </Link>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>

View File

@@ -17,7 +17,7 @@ import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Plus } from 'lucide-react' import { Plus } from 'lucide-react'
import { PROJECT_ICON_IDS, type Project, type ProjectIconId } from './types' import { PROJECT_ICON_IDS, type Project, type ProjectIconId } from './types'
import { getProjectIcon } from './icon-map' import { getProjectIcon } from '../../lib/iconMap'
type NewProjectDialogProps = { type NewProjectDialogProps = {
onCreate: (project: Project) => void onCreate: (project: Project) => void

View File

@@ -49,7 +49,7 @@ import {
Network, Network,
} from 'lucide-react' } from 'lucide-react'
import { usePlatform } from './platformContext' import { usePlatform } from './platformContext'
import { getProjectIcon } from './icon-map' import { getProjectIcon } from '../../lib/iconMap'
import { import {
loadGraphFromStorage, loadGraphFromStorage,
removeGraphFromStorage, removeGraphFromStorage,
@@ -319,7 +319,7 @@ export function ProjectsPage() {
<TooltipProvider> <TooltipProvider>
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<h1 className="p-3 scroll-m-20 text-4xl font-extrabold tracking-tight text-balance"> <h1 className="p-3 scroll-m-20 text-4xl font-extrabold tracking-tight text-balance">
Hi, David Hi, Demiurge
</h1> </h1>
</div> </div>
<div className="flex flex-col gap-4 rounded-lg border bg-card p-4 text-card-foreground shadow-sm"> <div className="flex flex-col gap-4 rounded-lg border bg-card p-4 text-card-foreground shadow-sm">
@@ -393,7 +393,7 @@ export function ProjectsPage() {
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') { if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault() e.preventDefault()
;(e.currentTarget as HTMLElement).click() ; (e.currentTarget as HTMLElement).click()
} }
}} }}
aria-label="Create new project" aria-label="Create new project"

View File

@@ -2,7 +2,7 @@ import React, { useContext, useMemo } from 'react'
import FlowContext from '../../lib/flowContext' import FlowContext from '../../lib/flowContext'
import { ArrowDownLeft, ArrowUpRight } from 'lucide-react' import { ArrowDownLeft, ArrowUpRight } from 'lucide-react'
import { NodeHelpPopover } from './NodeHelpPopover' import { NodeHelpPopover } from './NodeHelpPopover'
import { getNodeType } from '../../lib/nodeRegistry' import { getNodeType, getNodeClassificationLabel } from '../../lib/nodeRegistry'
type Props = { type Props = {
nodeId: string nodeId: string
@@ -27,6 +27,7 @@ export function NodeFooterEdgeIndicators({ nodeId, nodeType, children }: Props)
const descriptor = getNodeType(nodeType) const descriptor = getNodeType(nodeType)
const showInput = descriptor?.hasInput ?? false const showInput = descriptor?.hasInput ?? false
const showOutput = descriptor?.hasOutput ?? false const showOutput = descriptor?.hasOutput ?? false
const classificationLabel = getNodeClassificationLabel(nodeType)
return ( return (
<div className="flex items-center gap-2 w-full text-xs text-muted-foreground"> <div className="flex items-center gap-2 w-full text-xs text-muted-foreground">
@@ -48,6 +49,12 @@ export function NodeFooterEdgeIndicators({ nodeId, nodeType, children }: Props)
<span className="min-w-0 truncate">{children}</span> <span className="min-w-0 truncate">{children}</span>
</> </>
)} )}
{classificationLabel != null && (
<>
{(showInput || showOutput || children != null) && <span className="shrink-0">|</span>}
<span className="shrink-0" title="Node classification">{classificationLabel}</span>
</>
)}
<span className="shrink-0 ml-auto"> <span className="shrink-0 ml-auto">
<NodeHelpPopover nodeType={nodeType} /> <NodeHelpPopover nodeType={nodeType} />
</span> </span>

View File

@@ -18,7 +18,7 @@ import {
Turtle, Turtle,
type LucideIcon, type LucideIcon,
} from 'lucide-react' } from 'lucide-react'
import type { ProjectIconId } from './types' import type { ProjectIconId } from '../app/platform/types'
export const PROJECT_ICON_MAP: Record<ProjectIconId, LucideIcon> = { export const PROJECT_ICON_MAP: Record<ProjectIconId, LucideIcon> = {
bird: Bird, bird: Bird,

View File

@@ -7,6 +7,15 @@
import type React from 'react' import type React from 'react'
import type { Node } from '@xyflow/react' import type { Node } from '@xyflow/react'
/** Node classification for UI: Psyche (Form/Config, Variable, Behavior/Function), Pneuma (Rendering), Physis (TBD). */
export type NodeClassification = 'psyche' | 'pneuma' | 'physis'
export const NODE_CLASSIFICATION_LABELS: Record<NodeClassification, string> = {
psyche: 'Psyche',
pneuma: 'Pneuma',
physis: 'Physis',
}
export type NodeHelpEntry = { export type NodeHelpEntry = {
title: string title: string
content: React.ReactNode content: React.ReactNode
@@ -20,6 +29,8 @@ export type NodeTypeDescriptor = {
idPrefix: string idPrefix: string
hasInput: boolean hasInput: boolean
hasOutput: boolean hasOutput: boolean
/** Classification for creation menu and node footer: Psyche, Pneuma, Physis. */
classification?: NodeClassification
/** When this type is the connection target, which source types are allowed. Omit = all. */ /** When this type is the connection target, which source types are allowed. Omit = all. */
allowedSourceTypes?: string[] allowedSourceTypes?: string[]
/** When this type is the connection source, which target types are allowed. Omit = all. */ /** When this type is the connection source, which target types are allowed. Omit = all. */
@@ -48,10 +59,31 @@ export function getNodeType(id: string): NodeTypeDescriptor | undefined {
return registry.get(id) return registry.get(id)
} }
const CLASSIFICATION_ORDER: NodeClassification[] = ['psyche', 'pneuma', 'physis']
export function getRegisteredNodeTypes(): NodeTypeDescriptor[] { export function getRegisteredNodeTypes(): NodeTypeDescriptor[] {
return Array.from(registry.values()) return Array.from(registry.values())
} }
/** Node types grouped by classification for the Create Node menu. Order: Psyche, Pneuma, Physis. */
export function getRegisteredNodeTypesGroupedByClassification(): {
classification: NodeClassification
label: string
types: NodeTypeDescriptor[]
}[] {
const byClass = new Map<NodeClassification, NodeTypeDescriptor[]>()
for (const c of CLASSIFICATION_ORDER) byClass.set(c, [])
for (const desc of registry.values()) {
const c = desc.classification ?? 'physis'
byClass.get(c)!.push(desc)
}
return CLASSIFICATION_ORDER.map((classification) => ({
classification,
label: NODE_CLASSIFICATION_LABELS[classification],
types: byClass.get(classification)!,
}))
}
export function getRegisteredNodeTypeIds(): string[] { export function getRegisteredNodeTypeIds(): string[] {
return Array.from(registry.keys()) return Array.from(registry.keys())
} }
@@ -114,3 +146,10 @@ export function getNodeHelp(nodeType: string): NodeHelpEntry {
const desc = getNodeType(nodeType) const desc = getNodeType(nodeType)
return desc?.help ?? { title: nodeType, content: null } return desc?.help ?? { title: nodeType, content: null }
} }
/** Classification label for a node type (e.g. "Psyche"). Shown in node footer before help button. */
export function getNodeClassificationLabel(nodeType: string): string | null {
const desc = getNodeType(nodeType)
const c = desc?.classification
return c ? NODE_CLASSIFICATION_LABELS[c] : null
}

View File

@@ -23,6 +23,7 @@ export function registerBuiltinNodes(): void {
idPrefix: 'cfg_', idPrefix: 'cfg_',
hasInput: true, hasInput: true,
hasOutput: true, hasOutput: true,
classification: 'psyche',
allowedSourceTypes: ['config', 'variable', 'function'], allowedSourceTypes: ['config', 'variable', 'function'],
allowedTargetTypes: ['config', 'render'], allowedTargetTypes: ['config', 'render'],
help: NODE_HELP.config, help: NODE_HELP.config,
@@ -49,6 +50,7 @@ export function registerBuiltinNodes(): void {
idPrefix: 'rnd_', idPrefix: 'rnd_',
hasInput: true, hasInput: true,
hasOutput: false, hasOutput: false,
classification: 'pneuma',
allowedSourceTypes: ['config'], allowedSourceTypes: ['config'],
help: NODE_HELP.render, help: NODE_HELP.render,
menuLabel: 'Renderer', menuLabel: 'Renderer',
@@ -64,6 +66,7 @@ export function registerBuiltinNodes(): void {
idPrefix: 'var_', idPrefix: 'var_',
hasInput: false, hasInput: false,
hasOutput: true, hasOutput: true,
classification: 'psyche',
allowedTargetTypes: ['config', 'function'], allowedTargetTypes: ['config', 'function'],
help: NODE_HELP.variable, help: NODE_HELP.variable,
menuLabel: 'Variable', menuLabel: 'Variable',
@@ -82,6 +85,7 @@ export function registerBuiltinNodes(): void {
idPrefix: 'fn_', idPrefix: 'fn_',
hasInput: true, hasInput: true,
hasOutput: true, hasOutput: true,
classification: 'psyche',
allowedSourceTypes: ['config', 'variable', 'function'], allowedSourceTypes: ['config', 'variable', 'function'],
allowedTargetTypes: ['config', 'function'], allowedTargetTypes: ['config', 'function'],
help: NODE_HELP.function, help: NODE_HELP.function,