feat: add node calssification
This commit is contained in:
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user