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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import React, { useContext, useMemo } from 'react'
import FlowContext from '../../lib/flowContext'
import { ArrowDownLeft, ArrowUpRight } from 'lucide-react'
import { NodeHelpPopover } from './NodeHelpPopover'
import { getNodeType } from '../../lib/nodeRegistry'
import { getNodeType, getNodeClassificationLabel } from '../../lib/nodeRegistry'
type Props = {
nodeId: string
@@ -27,6 +27,7 @@ export function NodeFooterEdgeIndicators({ nodeId, nodeType, children }: Props)
const descriptor = getNodeType(nodeType)
const showInput = descriptor?.hasInput ?? false
const showOutput = descriptor?.hasOutput ?? false
const classificationLabel = getNodeClassificationLabel(nodeType)
return (
<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>
</>
)}
{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">
<NodeHelpPopover nodeType={nodeType} />
</span>

View File

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

View File

@@ -7,6 +7,15 @@
import type React from '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 = {
title: string
content: React.ReactNode
@@ -20,6 +29,8 @@ export type NodeTypeDescriptor = {
idPrefix: string
hasInput: 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. */
allowedSourceTypes?: string[]
/** 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)
}
const CLASSIFICATION_ORDER: NodeClassification[] = ['psyche', 'pneuma', 'physis']
export function getRegisteredNodeTypes(): NodeTypeDescriptor[] {
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[] {
return Array.from(registry.keys())
}
@@ -114,3 +146,10 @@ export function getNodeHelp(nodeType: string): NodeHelpEntry {
const desc = getNodeType(nodeType)
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_',
hasInput: true,
hasOutput: true,
classification: 'psyche',
allowedSourceTypes: ['config', 'variable', 'function'],
allowedTargetTypes: ['config', 'render'],
help: NODE_HELP.config,
@@ -49,6 +50,7 @@ export function registerBuiltinNodes(): void {
idPrefix: 'rnd_',
hasInput: true,
hasOutput: false,
classification: 'pneuma',
allowedSourceTypes: ['config'],
help: NODE_HELP.render,
menuLabel: 'Renderer',
@@ -64,6 +66,7 @@ export function registerBuiltinNodes(): void {
idPrefix: 'var_',
hasInput: false,
hasOutput: true,
classification: 'psyche',
allowedTargetTypes: ['config', 'function'],
help: NODE_HELP.variable,
menuLabel: 'Variable',
@@ -82,6 +85,7 @@ export function registerBuiltinNodes(): void {
idPrefix: 'fn_',
hasInput: true,
hasOutput: true,
classification: 'psyche',
allowedSourceTypes: ['config', 'variable', 'function'],
allowedTargetTypes: ['config', 'function'],
help: NODE_HELP.function,