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