feat: add node shape variants with five distinct shapes

Implements configurable shape variants for actor nodes, allowing visual
differentiation of node types beyond just color.

Features:
- Five shape options: rectangle, circle, rounded rectangle, ellipse, pill
- All shapes use pure CSS (border-radius) for consistent behavior
- Auto-grow with content
- Perfect shadow and selection/highlight effects
- Proper React Flow handle alignment
- Shape selector UI with visual previews
- Migration logic for existing documents (defaults to rectangle)

Shape characteristics:
- Rectangle: Standard, general purpose
- Circle: Round, for people and concepts
- Rounded Rectangle: Soft edges, friendly appearance
- Ellipse: Oval/horizontal, for processes and stages
- Pill: Capsule, compact for tags and labels

Technical approach:
- Uses border-radius for all shapes (no clip-path)
- Ensures boxShadow follows shape contours properly
- Each shape component maintains consistent props interface
- NodeShapeRenderer routes to appropriate shape component

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik Bruhn 2025-10-16 15:34:24 +02:00
parent 084a3bb486
commit e0784ff3d8
14 changed files with 633 additions and 48 deletions

View file

@ -25,6 +25,7 @@ const NodeTypeConfigModal = ({ isOpen, onClose }: Props) => {
const [newTypeName, setNewTypeName] = useState('');
const [newTypeColor, setNewTypeColor] = useState('#6366f1');
const [newTypeShape, setNewTypeShape] = useState<import('../../types').NodeShape>('rectangle');
const [newTypeDescription, setNewTypeDescription] = useState('');
const [newTypeIcon, setNewTypeIcon] = useState('');
@ -32,6 +33,7 @@ const NodeTypeConfigModal = ({ isOpen, onClose }: Props) => {
const [editingId, setEditingId] = useState<string | null>(null);
const [editLabel, setEditLabel] = useState('');
const [editColor, setEditColor] = useState('');
const [editShape, setEditShape] = useState<import('../../types').NodeShape>('rectangle');
const [editIcon, setEditIcon] = useState('');
const [editDescription, setEditDescription] = useState('');
@ -53,6 +55,7 @@ const NodeTypeConfigModal = ({ isOpen, onClose }: Props) => {
id,
label: newTypeName.trim(),
color: newTypeColor,
shape: newTypeShape,
icon: newTypeIcon || undefined,
description: newTypeDescription.trim() || undefined,
};
@ -62,6 +65,7 @@ const NodeTypeConfigModal = ({ isOpen, onClose }: Props) => {
// Reset form
setNewTypeName('');
setNewTypeColor('#6366f1');
setNewTypeShape('rectangle');
setNewTypeDescription('');
setNewTypeIcon('');
};
@ -82,6 +86,7 @@ const NodeTypeConfigModal = ({ isOpen, onClose }: Props) => {
setEditingId(type.id);
setEditLabel(type.label);
setEditColor(type.color);
setEditShape(type.shape || 'rectangle');
setEditIcon(type.icon || '');
setEditDescription(type.description || '');
};
@ -92,6 +97,7 @@ const NodeTypeConfigModal = ({ isOpen, onClose }: Props) => {
updateNodeType(editingId, {
label: editLabel.trim(),
color: editColor,
shape: editShape,
icon: editIcon || undefined,
description: editDescription.trim() || undefined,
});
@ -124,10 +130,12 @@ const NodeTypeConfigModal = ({ isOpen, onClose }: Props) => {
<NodeTypeForm
name={newTypeName}
color={newTypeColor}
shape={newTypeShape}
icon={newTypeIcon}
description={newTypeDescription}
onNameChange={setNewTypeName}
onColorChange={setNewTypeColor}
onShapeChange={setNewTypeShape}
onIconChange={setNewTypeIcon}
onDescriptionChange={setNewTypeDescription}
/>
@ -154,10 +162,12 @@ const NodeTypeConfigModal = ({ isOpen, onClose }: Props) => {
<NodeTypeForm
name={editLabel}
color={editColor}
shape={editShape}
icon={editIcon}
description={editDescription}
onNameChange={setEditLabel}
onColorChange={setEditColor}
onShapeChange={setEditShape}
onIconChange={setEditIcon}
onDescriptionChange={setEditDescription}
/>

View file

@ -1,4 +1,6 @@
import IconSelector from './IconSelector';
import ShapeSelector from './ShapeSelector';
import type { NodeShape } from '../../types';
/**
* NodeTypeForm - Reusable form fields for creating/editing node types
@ -6,6 +8,7 @@ import IconSelector from './IconSelector';
* Features:
* - Name input
* - Color picker (visual + text input)
* - Shape selector
* - Icon selector
* - Description input
*/
@ -13,10 +16,12 @@ import IconSelector from './IconSelector';
interface Props {
name: string;
color: string;
shape: NodeShape;
icon: string;
description: string;
onNameChange: (value: string) => void;
onColorChange: (value: string) => void;
onShapeChange: (value: NodeShape) => void;
onIconChange: (value: string) => void;
onDescriptionChange: (value: string) => void;
}
@ -24,10 +29,12 @@ interface Props {
const NodeTypeForm = ({
name,
color,
shape,
icon,
description,
onNameChange,
onColorChange,
onShapeChange,
onIconChange,
onDescriptionChange,
}: Props) => {
@ -67,6 +74,8 @@ const NodeTypeForm = ({
</div>
</div>
<ShapeSelector value={shape} onChange={onShapeChange} color={color} />
<IconSelector selectedIcon={icon} onSelect={onIconChange} />
<div>

View file

@ -0,0 +1,196 @@
import type { NodeShape } from '../../types';
interface ShapeSelectorProps {
value: NodeShape;
onChange: (shape: NodeShape) => void;
color?: string;
}
interface ShapeOption {
id: NodeShape;
label: string;
description: string;
}
const SHAPE_OPTIONS: ShapeOption[] = [
{
id: 'rectangle',
label: 'Rectangle',
description: 'Standard rectangular shape, good for general purpose use',
},
{
id: 'circle',
label: 'Circle',
description: 'Circular shape, best for people and concepts',
},
{
id: 'roundedRectangle',
label: 'Rounded Rectangle',
description: 'Soft rounded rectangle, suitable for teams and groups',
},
{
id: 'ellipse',
label: 'Ellipse',
description: 'Oval shape, ideal for processes and stages',
},
{
id: 'pill',
label: 'Pill',
description: 'Capsule shape, perfect for tags and labels',
},
];
/**
* ShapeSelector - Visual selector for node shapes
*
* Displays all available shape options with visual previews
* and allows users to select a shape for their node type.
*/
const ShapeSelector = ({ value, onChange, color = '#3b82f6' }: ShapeSelectorProps) => {
return (
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
Shape
</label>
<div className="grid grid-cols-3 gap-3">
{SHAPE_OPTIONS.map((option) => {
const isSelected = value === option.id;
return (
<button
key={option.id}
type="button"
onClick={() => onChange(option.id)}
className={`
relative p-3 rounded-lg border-2 transition-all
hover:border-blue-400 hover:bg-blue-50
${isSelected ? 'border-blue-500 bg-blue-50' : 'border-gray-300 bg-white'}
`}
title={option.description}
>
{/* Shape Preview */}
<div className="flex justify-center items-center h-12 mb-2">
<ShapePreview shape={option.id} color={color} size={40} />
</div>
{/* Shape Label */}
<div className="text-xs text-center text-gray-700 font-medium">
{option.label}
</div>
{/* Selected Indicator */}
{isSelected && (
<div className="absolute top-1 right-1 w-4 h-4 bg-blue-500 rounded-full flex items-center justify-center">
<svg
className="w-3 h-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
)}
</button>
);
})}
</div>
</div>
);
};
/**
* ShapePreview - Renders a small preview of a shape
*/
const ShapePreview = ({ shape, color, size }: { shape: NodeShape; color: string; size: number }) => {
const svgSize = size;
const strokeWidth = 2;
switch (shape) {
case 'rectangle':
return (
<svg width={svgSize} height={svgSize * 0.7} viewBox="0 0 60 42">
<rect
x={strokeWidth}
y={strokeWidth}
width={60 - strokeWidth * 2}
height={42 - strokeWidth * 2}
fill={color}
stroke={color}
strokeWidth={strokeWidth}
rx="3"
/>
</svg>
);
case 'circle':
return (
<svg width={svgSize} height={svgSize} viewBox="0 0 50 50">
<circle
cx="25"
cy="25"
r={25 - strokeWidth}
fill={color}
stroke={color}
strokeWidth={strokeWidth}
/>
</svg>
);
case 'roundedRectangle':
return (
<svg width={svgSize} height={svgSize * 0.7} viewBox="0 0 60 42">
<rect
x={strokeWidth}
y={strokeWidth}
width={60 - strokeWidth * 2}
height={42 - strokeWidth * 2}
fill={color}
stroke={color}
strokeWidth={strokeWidth}
rx="12"
/>
</svg>
);
case 'ellipse':
return (
<svg width={svgSize * 1.2} height={svgSize * 0.7} viewBox="0 0 60 35">
<ellipse
cx="30"
cy="17.5"
rx={30 - strokeWidth}
ry={17.5 - strokeWidth}
fill={color}
stroke={color}
strokeWidth={strokeWidth}
/>
</svg>
);
case 'pill':
return (
<svg width={svgSize} height={svgSize * 0.5} viewBox="0 0 60 30">
<rect
x={strokeWidth}
y={strokeWidth}
width={60 - strokeWidth * 2}
height={30 - strokeWidth * 2}
fill={color}
stroke={color}
strokeWidth={strokeWidth}
rx="15"
/>
</svg>
);
default:
return null;
}
};
export default ShapeSelector;

View file

@ -8,6 +8,7 @@ import {
} from "../../utils/colorUtils";
import { getIconComponent } from "../../utils/iconUtils";
import type { ActorData } from "../../types";
import NodeShapeRenderer from "./Shapes/NodeShapeRenderer";
/**
* CustomNode - Represents an actor in the constellation graph
@ -32,6 +33,7 @@ const CustomNode = ({ data, selected }: NodeProps<ActorData>) => {
const nodeTypeConfig = nodeTypes.find((nt) => nt.id === data.type);
const nodeColor = nodeTypeConfig?.color || "#6b7280";
const nodeLabel = nodeTypeConfig?.label || "Unknown";
const nodeShape = nodeTypeConfig?.shape || "rectangle";
const IconComponent = getIconComponent(nodeTypeConfig?.icon);
// Determine text color based on background
@ -86,23 +88,9 @@ const CustomNode = ({ data, selected }: NodeProps<ActorData>) => {
return (
<div
className={`
px-4 py-3 rounded-lg shadow-md min-w-[120px]
transition-all duration-200
${selected ? "shadow-xl" : "shadow-md"}
`}
className="relative"
style={{
backgroundColor: nodeColor,
borderWidth: "3px", // Keep consistent border width
borderStyle: "solid",
borderColor: borderColor,
color: textColor,
opacity: nodeOpacity,
boxShadow: selected
? `0 0 0 3px ${nodeColor}40` // Add outer glow when selected (40 = ~25% opacity)
: isHighlighted
? `0 0 0 3px ${nodeColor}80, 0 0 12px ${nodeColor}60` // Highlight glow for search matches
: undefined,
}}
>
{/* Connection handles - shown only when selected or connecting */}
@ -166,34 +154,43 @@ const CustomNode = ({ data, selected }: NodeProps<ActorData>) => {
}}
/>
{/* Node content */}
<div className="space-y-1">
{/* Icon (if available) */}
{IconComponent && (
{/* Node content with shape renderer */}
<NodeShapeRenderer
shape={nodeShape}
color={nodeColor}
borderColor={borderColor}
textColor={textColor}
selected={selected}
isHighlighted={isHighlighted}
>
<div className="space-y-1">
{/* Icon (if available) */}
{IconComponent && (
<div
className="flex justify-center mb-1"
style={{ color: textColor, fontSize: "2rem" }}
>
<IconComponent />
</div>
)}
{/* Main label */}
<div
className="flex justify-center mb-1"
style={{ color: textColor, fontSize: "2rem" }}
className="text-base font-bold text-center break-words leading-tight"
style={{ color: textColor }}
>
<IconComponent />
{data.label}
</div>
)}
{/* Main label */}
<div
className="text-base font-bold text-center break-words leading-tight"
style={{ color: textColor }}
>
{data.label}
{/* Type as subtle subtitle */}
<div
className="text-xs text-center opacity-70 font-medium leading-tight"
style={{ color: textColor }}
>
{nodeLabel}
</div>
</div>
{/* Type as subtle subtitle */}
<div
className="text-xs text-center opacity-70 font-medium leading-tight"
style={{ color: textColor }}
>
{nodeLabel}
</div>
</div>
</NodeShapeRenderer>
</div>
);
};

View file

@ -0,0 +1,53 @@
import { ReactNode } from 'react';
interface CircleShapeProps {
color: string;
borderColor: string;
textColor: string;
selected?: boolean;
isHighlighted?: boolean;
children: ReactNode;
className?: string;
}
/**
* CircleShape - Circular/elliptical node shape
*
* Best for: People, concepts, end states
* Characteristics: Compact, no directional bias, works well in radial layouts
* Handles: Positioned at cardinal points (top, right, bottom, left) of bounding box
*/
const CircleShape = ({
color,
borderColor,
textColor,
selected = false,
isHighlighted = false,
children,
className = '',
}: CircleShapeProps) => {
const shadowStyle = selected
? `0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1), 0 0 0 3px ${color}40`
: isHighlighted
? `0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1), 0 0 0 3px ${color}80, 0 0 12px ${color}60`
: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)';
return (
<div
className={`px-4 py-3 rounded-full min-w-[120px] flex items-center justify-center transition-shadow duration-200 ${className}`}
style={{
backgroundColor: color,
borderWidth: '3px',
borderStyle: 'solid',
borderColor: borderColor,
color: textColor,
aspectRatio: '1 / 1',
boxShadow: shadowStyle,
}}
>
{children}
</div>
);
};
export default CircleShape;

View file

@ -0,0 +1,53 @@
import { ReactNode } from 'react';
interface EllipseShapeProps {
color: string;
borderColor: string;
textColor: string;
selected?: boolean;
isHighlighted?: boolean;
children: ReactNode;
className?: string;
}
/**
* EllipseShape - Elliptical/oval node shape
*
* Best for: Processes, stages, states, horizontal groupings
* Characteristics: Wider than tall, smooth edges, good for labeled stages
* Handles: Positioned at cardinal points (top, right, bottom, left) of bounding box
*/
const EllipseShape = ({
color,
borderColor,
textColor,
selected = false,
isHighlighted = false,
children,
className = '',
}: EllipseShapeProps) => {
const shadowStyle = selected
? `0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1), 0 0 0 3px ${color}40`
: isHighlighted
? `0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1), 0 0 0 3px ${color}80, 0 0 12px ${color}60`
: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)';
return (
<div
className={`px-6 py-3 min-w-[140px] min-h-[80px] flex items-center justify-center transition-shadow duration-200 ${className}`}
style={{
backgroundColor: color,
borderWidth: '3px',
borderStyle: 'solid',
borderColor: borderColor,
color: textColor,
borderRadius: '50%',
boxShadow: shadowStyle,
}}
>
{children}
</div>
);
};
export default EllipseShape;

View file

@ -0,0 +1,80 @@
import { ReactNode } from 'react';
import type { NodeShape } from '../../../types';
import RectangleShape from './RectangleShape';
import CircleShape from './CircleShape';
import RoundedRectangleShape from './RoundedRectangleShape';
import EllipseShape from './EllipseShape';
import PillShape from './PillShape';
interface NodeShapeRendererProps {
shape: NodeShape;
color: string;
borderColor: string;
textColor: string;
selected?: boolean;
isHighlighted?: boolean;
children: ReactNode;
className?: string;
}
/**
* NodeShapeRenderer - Renders the appropriate shape component based on shape type
*
* This component acts as a router that selects the correct shape component
* to render based on the NodeTypeConfig.shape property.
*
* All shapes maintain consistent sizing and layout behavior to ensure
* proper alignment with React Flow's edge routing and layout algorithms.
*
* Usage:
* <NodeShapeRenderer
* shape="circle"
* color="#3b82f6"
* borderColor="#1e40af"
* textColor="white"
* >
* <YourNodeContent />
* </NodeShapeRenderer>
*/
const NodeShapeRenderer = ({
shape,
color,
borderColor,
textColor,
selected = false,
isHighlighted = false,
children,
className = '',
}: NodeShapeRendererProps) => {
const shapeProps = {
color,
borderColor,
textColor,
selected,
isHighlighted,
className,
};
switch (shape) {
case 'rectangle':
return <RectangleShape {...shapeProps}>{children}</RectangleShape>;
case 'circle':
return <CircleShape {...shapeProps}>{children}</CircleShape>;
case 'roundedRectangle':
return <RoundedRectangleShape {...shapeProps}>{children}</RoundedRectangleShape>;
case 'ellipse':
return <EllipseShape {...shapeProps}>{children}</EllipseShape>;
case 'pill':
return <PillShape {...shapeProps}>{children}</PillShape>;
default:
// Fallback to rectangle for unknown shapes
return <RectangleShape {...shapeProps}>{children}</RectangleShape>;
}
};
export default NodeShapeRenderer;

View file

@ -0,0 +1,53 @@
import { ReactNode } from 'react';
interface PillShapeProps {
color: string;
borderColor: string;
textColor: string;
selected?: boolean;
isHighlighted?: boolean;
children: ReactNode;
className?: string;
}
/**
* PillShape - Capsule/pill node shape
*
* Best for: Tags, labels, flow elements, actions
* Characteristics: Fully rounded ends, compact, modern look
* Handles: Positioned at cardinal points (top, right, bottom, left) of bounding box
*/
const PillShape = ({
color,
borderColor,
textColor,
selected = false,
isHighlighted = false,
children,
className = '',
}: PillShapeProps) => {
const shadowStyle = selected
? `0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1), 0 0 0 3px ${color}40`
: isHighlighted
? `0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1), 0 0 0 3px ${color}80, 0 0 12px ${color}60`
: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)';
return (
<div
className={`px-6 py-3 min-w-[120px] flex items-center justify-center transition-shadow duration-200 ${className}`}
style={{
backgroundColor: color,
borderWidth: '3px',
borderStyle: 'solid',
borderColor: borderColor,
color: textColor,
borderRadius: '999px',
boxShadow: shadowStyle,
}}
>
{children}
</div>
);
};
export default PillShape;

View file

@ -0,0 +1,52 @@
import { ReactNode } from 'react';
interface RectangleShapeProps {
color: string;
borderColor: string;
textColor: string;
selected?: boolean;
isHighlighted?: boolean;
children: ReactNode;
className?: string;
}
/**
* RectangleShape - Standard rectangular node shape
*
* This is the default shape, maintaining the current node appearance.
* Handles are positioned at the midpoints of each side (top, right, bottom, left).
*/
const RectangleShape = ({
color,
borderColor,
textColor,
selected = false,
isHighlighted = false,
children,
className = '',
}: RectangleShapeProps) => {
// Build shadow style
const shadowStyle = selected
? `0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1), 0 0 0 3px ${color}40`
: isHighlighted
? `0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1), 0 0 0 3px ${color}80, 0 0 12px ${color}60`
: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)';
return (
<div
className={`px-4 py-3 rounded-lg min-w-[120px] transition-shadow duration-200 ${className}`}
style={{
backgroundColor: color,
borderWidth: '3px',
borderStyle: 'solid',
borderColor: borderColor,
color: textColor,
boxShadow: shadowStyle,
}}
>
{children}
</div>
);
};
export default RectangleShape;

View file

@ -0,0 +1,53 @@
import { ReactNode } from 'react';
interface RoundedRectangleShapeProps {
color: string;
borderColor: string;
textColor: string;
selected?: boolean;
isHighlighted?: boolean;
children: ReactNode;
className?: string;
}
/**
* RoundedRectangleShape - Rounded rectangular node shape
*
* Best for: Softer entities (teams, groups, communities)
* Characteristics: Friendly appearance, similar layout behavior to rectangle
* Handles: Positioned at the midpoints of each side (top, right, bottom, left)
*/
const RoundedRectangleShape = ({
color,
borderColor,
textColor,
selected = false,
isHighlighted = false,
children,
className = '',
}: RoundedRectangleShapeProps) => {
const shadowStyle = selected
? `0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1), 0 0 0 3px ${color}40`
: isHighlighted
? `0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1), 0 0 0 3px ${color}80, 0 0 12px ${color}60`
: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)';
return (
<div
className={`px-4 py-3 min-w-[120px] transition-shadow duration-200 ${className}`}
style={{
backgroundColor: color,
borderWidth: '3px',
borderStyle: 'solid',
borderColor: borderColor,
color: textColor,
borderRadius: '24px', // More rounded than regular rectangle
boxShadow: shadowStyle,
}}
>
{children}
</div>
);
};
export default RoundedRectangleShape;

View file

@ -31,12 +31,12 @@ interface GraphStore {
edgeTypes: EdgeTypeConfig[];
}
// Default node types
// Default node types with semantic shape assignments
const defaultNodeTypes: NodeTypeConfig[] = [
{ id: 'person', label: 'Person', color: '#3b82f6', icon: 'Person', description: 'Individual person' },
{ id: 'organization', label: 'Organization', color: '#10b981', icon: 'Business', description: 'Company or group' },
{ id: 'system', label: 'System', color: '#f59e0b', icon: 'Computer', description: 'Technical system' },
{ id: 'concept', label: 'Concept', color: '#8b5cf6', icon: 'Lightbulb', description: 'Abstract concept' },
{ id: 'person', label: 'Person', color: '#3b82f6', shape: 'circle', icon: 'Person', description: 'Individual person' },
{ id: 'organization', label: 'Organization', color: '#10b981', shape: 'rectangle', icon: 'Business', description: 'Company or group' },
{ id: 'system', label: 'System', color: '#f59e0b', shape: 'roundedRectangle', icon: 'Computer', description: 'Technical system' },
{ id: 'concept', label: 'Concept', color: '#8b5cf6', shape: 'roundedRectangle', icon: 'Lightbulb', description: 'Abstract concept' },
];
// Default edge types

View file

@ -140,6 +140,23 @@ export function getCurrentGraphFromDocument(document: ConstellationDocument): {
}
}
// Migrate node types to include shape property if missing
function migrateNodeTypes(nodeTypes: NodeTypeConfig[]): NodeTypeConfig[] {
return nodeTypes.map(nodeType => {
// If shape property already exists, return as-is
if (nodeType.shape) {
return nodeType;
}
// Otherwise, add default shape (rectangle) for backward compatibility
console.log(`Migrating node type "${nodeType.id}" to include shape property (defaulting to rectangle)`);
return {
...nodeType,
shape: 'rectangle' as const,
};
});
}
// Deserialize graph state from a document
export function deserializeGraphState(document: ConstellationDocument): {
nodes: Actor[];
@ -156,10 +173,13 @@ export function deserializeGraphState(document: ConstellationDocument): {
const nodes = deserializeActors(currentGraph.nodes);
const edges = deserializeRelations(currentGraph.edges);
// Migrate node types to include shape property
const migratedNodeTypes = migrateNodeTypes(currentGraph.nodeTypes);
return {
nodes,
edges,
nodeTypes: currentGraph.nodeTypes,
nodeTypes: migratedNodeTypes,
edgeTypes: currentGraph.edgeTypes,
};
} catch (error) {

View file

@ -39,10 +39,10 @@ const defaultSettings: WorkspaceSettings = {
maxOpenDocuments: 10,
autoSaveEnabled: true,
defaultNodeTypes: [
{ id: 'person', label: 'Person', color: '#3b82f6', icon: 'Person', description: 'Individual person' },
{ id: 'organization', label: 'Organization', color: '#10b981', icon: 'Business', description: 'Company or group' },
{ id: 'system', label: 'System', color: '#f59e0b', icon: 'Computer', description: 'Technical system' },
{ id: 'concept', label: 'Concept', color: '#8b5cf6', icon: 'Lightbulb', description: 'Abstract concept' },
{ id: 'person', label: 'Person', color: '#3b82f6', shape: 'circle', icon: 'Person', description: 'Individual person' },
{ id: 'organization', label: 'Organization', color: '#10b981', shape: 'rectangle', icon: 'Business', description: 'Company or group' },
{ id: 'system', label: 'System', color: '#f59e0b', shape: 'roundedRectangle', icon: 'Computer', description: 'Technical system' },
{ id: 'concept', label: 'Concept', color: '#8b5cf6', shape: 'roundedRectangle', icon: 'Lightbulb', description: 'Abstract concept' },
],
defaultEdgeTypes: [
{ id: 'collaborates', label: 'Collaborates', color: '#3b82f6', style: 'solid' },

View file

@ -23,11 +23,20 @@ export interface RelationData {
export type Relation = Edge<RelationData>;
// Node Shape Types
export type NodeShape =
| 'rectangle'
| 'circle'
| 'roundedRectangle'
| 'ellipse'
| 'pill';
// Node Type Configuration
export interface NodeTypeConfig {
id: string;
label: string;
color: string;
shape: NodeShape;
icon?: string;
description?: string;
}