/** * @file Example demonstrating custom inspector panels and tabs * * This example shows three main customization patterns: * 1. Node-level renderInspector: Custom inspector content per node type * 2. InspectorPanel tabs: Adding custom tabs to the inspector * 3. InspectorPanel settingsPanels: Adding custom panels to the Settings tab * * All inspector UI is built using only the provided inspector parts components, * ensuring consistent theming across light and dark modes. */ import * as React from "react"; import { NodeEditor } from "../../../../../NodeEditor"; import { asNodeDefinition, type NodeDefinition, type InspectorRenderProps } from "../../../../../types/NodeDefinition"; import type { NodeEditorData } from "../../../../../types/core"; import { StandardNodeDefinition } from "../../../../../node-definitions/standard"; import { InspectorPanel, InspectorLayersTab, InspectorSettingsTab, PropertySection, InspectorInput, InspectorTextarea, InspectorButton, InspectorButtonGroup, InspectorDefinitionList, InspectorDefinitionItem, InspectorPropertiesTab, InspectorSection, InspectorField, InspectorLabel, InspectorNumberInput, InspectorSelect, PositionInputsGrid, ReadOnlyField, InspectorSectionTitle, type InspectorPanelTabConfig, type InspectorSettingsPanelConfig, type InspectorButtonGroupOption, } from "../../../../../inspector"; import { defaultEditorGridConfig } from "../../../../../core"; import { NodeCanvas } from "../../../../../components/canvas/NodeCanvas"; import classes from "./CustomInspectorExample.module.css"; // ============================================================================= // 1. Custom Node Data Types // ============================================================================= type PersonNodeData = { name: string; email: string; role: "developer" | "designer" | "manager"; bio: string; }; type ProjectNodeData = { projectName: string; status: "planning" | "active" | "completed"; priority: "low" | "medium" | "high"; deadline: string; }; type AnalyticsNodeData = { metricName: string; value: number; threshold: number; unit: string; createdAt: string; }; // ============================================================================= // 2. Custom Inspector Renderers (Pattern 1: renderInspector) // ============================================================================= /** * Custom inspector for Person nodes. * Uses the built-in inspector UI components for consistent styling. */ function PersonInspectorRenderer({ node, onUpdateNode, }: InspectorRenderProps): React.ReactElement { const data = node.data ?? ({} as PersonNodeData); const handleChange = (key: K, value: PersonNodeData[K]) => { onUpdateNode({ data: { ...data, [key]: value } }); }; return ( handleChange("name", e.target.value)} placeholder="Enter name" /> handleChange("email", e.target.value)} placeholder="email@example.com" /> handleChange("role", e.target.value as PersonNodeData["role"])} > handleChange("bio", e.target.value)} placeholder="Brief biography..." rows={3} /> ); } // Priority options for InspectorButtonGroup const priorityOptions: InspectorButtonGroupOption[] = [ { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High" }, ]; /** * Custom inspector for Project nodes. * Demonstrates different field types and styling. */ function ProjectInspectorRenderer({ node, onUpdateNode, onDeleteNode, }: InspectorRenderProps): React.ReactElement { const data = node.data ?? ({} as ProjectNodeData); const handleChange = (key: K, value: ProjectNodeData[K]) => { onUpdateNode({ data: { ...data, [key]: value } }); }; return ( <> handleChange("projectName", e.target.value)} placeholder="Project name" /> handleChange("status", e.target.value as ProjectNodeData["status"])} > handleChange("priority", value)} aria-label="Select priority" /> handleChange("deadline", e.target.value)} /> Delete Project ); } /** * Custom inspector for Analytics nodes. * Demonstrates additional inspector parts: InspectorField, InspectorLabel, * InspectorNumberInput, PositionInputsGrid, ReadOnlyField, InspectorSectionTitle. */ function AnalyticsInspectorRenderer({ node, onUpdateNode, }: InspectorRenderProps): React.ReactElement { const data = node.data ?? ({} as AnalyticsNodeData); const handleChange = (key: K, value: AnalyticsNodeData[K]) => { onUpdateNode({ data: { ...data, [key]: value } }); }; const isAboveThreshold = (data.value ?? 0) >= (data.threshold ?? 0); return ( <> {/* Pattern: Using InspectorSectionTitle for standalone title */} Metric Configuration {/* Pattern: Using InspectorField with InspectorLabel for vertical layout */} Metric Name}> handleChange("metricName", e.target.value)} placeholder="e.g., Page Views" /> Unit}> handleChange("unit", e.target.value)} placeholder="e.g., views/day" /> {/* Pattern: Using PositionInputsGrid for compact number inputs */} handleChange("value", value)} /> handleChange("threshold", value)} /> {/* Status indicator using InspectorDefinitionList */} {isAboveThreshold ? "Above Threshold" : "Below Threshold"} {/* Pattern: Using ReadOnlyField for non-editable data */} {node.id} {data.createdAt || "Unknown"} X: {Math.round(node.position.x)}, Y: {Math.round(node.position.y)} ); } // ============================================================================= // 3. Node Definitions with Custom Inspectors // ============================================================================= const PersonNodeDefinition: NodeDefinition = { type: "person", displayName: "Person", description: "Represents a team member", category: "Team", defaultData: { name: "", email: "", role: "developer", bio: "", }, ports: [ { id: "reports-to", type: "output", position: "top", label: "Reports To" }, { id: "works-on", type: "output", position: "right", label: "Works On" }, ], // Pattern 1: Provide a React component for custom inspector renderInspector: PersonInspectorRenderer, }; const ProjectNodeDefinition: NodeDefinition = { type: "project", displayName: "Project", description: "Represents a project", category: "Work", defaultData: { projectName: "", status: "planning", priority: "medium", deadline: "", }, ports: [ { id: "assigned", type: "input", position: "left", label: "Assigned" }, { id: "depends-on", type: "input", position: "top", label: "Depends On" }, { id: "blocks", type: "output", position: "bottom", label: "Blocks" }, ], renderInspector: ProjectInspectorRenderer, }; const AnalyticsNodeDefinition: NodeDefinition = { type: "analytics", displayName: "Analytics", description: "Represents a metric or KPI", category: "Data", defaultData: { metricName: "", value: 0, threshold: 100, unit: "", createdAt: new Date().toISOString().split("T")[0], }, ports: [ { id: "data-in", type: "input", position: "left", label: "Data In" }, { id: "data-out", type: "output", position: "right", label: "Data Out" }, ], renderInspector: AnalyticsInspectorRenderer, }; // ============================================================================= // 4. Custom Tab Components (Pattern 2: InspectorPanel tabs) // ============================================================================= /** * Custom "Statistics" tab showing editor statistics. */ const StatisticsTab: React.FC = () => { // In a real app, you'd use useEditorData() or similar to get actual counts return ( 5 3 0 2 1 1 1 ); }; /** * Custom "Help" tab with usage instructions. */ const HelpTab: React.FC = () => { return ( Click a node to edit properties Drag from ports to create connections Double-click canvas for new node Name, email, role, bio fields Status, priority, deadline fields Numeric inputs, read-only fields Default inspector (no renderInspector) ); }; // ============================================================================= // 5. Custom Settings Panel (Pattern 3: settingsPanels) // ============================================================================= /** * Custom settings panel for export options. */ const ExportSettingsPanel: React.FC = () => { const [format, setFormat] = React.useState<"json" | "yaml">("json"); const [includeMetadata, setIncludeMetadata] = React.useState(true); const handleExport = () => { alert(`Exporting as ${format.toUpperCase()}${includeMetadata ? " with metadata" : ""}`); }; return ( setFormat(e.target.value as "json" | "yaml")}> setIncludeMetadata(e.target.checked)} /> Export Data ); }; // ============================================================================= // 6. Custom InspectorPanel with All Patterns // ============================================================================= const CustomInspectorPanel: React.FC = () => { // Pattern 3: Custom settings panels added to the Settings tab const settingsPanels: InspectorSettingsPanelConfig[] = React.useMemo( () => [ { title: "Export Options", component: ExportSettingsPanel, }, ], [], ); // Pattern 2: Custom tabs replacing or extending the default tabs const tabs: InspectorPanelTabConfig[] = React.useMemo( () => [ { id: "layers", label: "Layers", render: () => , }, { id: "properties", label: "Properties", render: () => , }, { id: "statistics", label: "Stats", render: () => , }, { id: "settings", label: "Settings", render: () => , }, { id: "help", label: "Help", render: () => , }, ], [settingsPanels], ); return ; }; // ============================================================================= // 7. Minimal Custom Inspector (Simpler Pattern) // ============================================================================= /** * A simpler inspector that only shows node properties for selected nodes. * Uses InspectorPanel with single tab for consistent styling. */ const MinimalInspector: React.FC = () => { const tabs: InspectorPanelTabConfig[] = React.useMemo( () => [ { id: "properties", label: "Properties", render: () => , }, ], [], ); return ; }; // ============================================================================= // 8. Initial Data // ============================================================================= const initialData: NodeEditorData = { nodes: { "person-1": { id: "person-1", type: "person", position: { x: 100, y: 100 }, data: { name: "Alice Johnson", email: "alice@example.com", role: "developer", bio: "Senior frontend developer with 5 years of experience.", }, }, "person-2": { id: "person-2", type: "person", position: { x: 100, y: 300 }, data: { name: "Bob Smith", email: "bob@example.com", role: "designer", bio: "UI/UX designer passionate about accessibility.", }, }, "project-1": { id: "project-1", type: "project", position: { x: 400, y: 180 }, data: { projectName: "Node Editor v2", status: "active", priority: "high", deadline: "2024-06-30", }, }, "analytics-1": { id: "analytics-1", type: "analytics", position: { x: 700, y: 100 }, data: { metricName: "Daily Active Users", value: 1250, threshold: 1000, unit: "users/day", createdAt: "2024-01-15", }, }, "standard-1": { id: "standard-1", type: "standard", position: { x: 400, y: 380 }, data: { title: "Standard Node", content: "This node uses the default inspector (no renderInspector).", }, }, }, connections: { "conn-1": { id: "conn-1", fromNodeId: "person-1", fromPortId: "works-on", toNodeId: "project-1", toPortId: "assigned", }, "conn-2": { id: "conn-2", fromNodeId: "person-2", fromPortId: "works-on", toNodeId: "project-1", toPortId: "assigned", }, "conn-3": { id: "conn-3", fromNodeId: "project-1", fromPortId: "blocks", toNodeId: "analytics-1", toPortId: "data-in", }, }, }; // ============================================================================= // 9. Example Component with Mode Selection // ============================================================================= // Mode options for InspectorButtonGroup const modeOptions: InspectorButtonGroupOption<"full" | "minimal">[] = [ { value: "full", label: "Full" }, { value: "minimal", label: "Minimal" }, ]; export const CustomInspectorExample: React.FC = () => { const [mode, setMode] = React.useState<"full" | "minimal">("full"); const gridLayers = React.useMemo( () => [ { id: "canvas", component: , gridArea: "canvas" }, { id: "inspector", component: mode === "full" ? : , gridArea: "inspector", }, ], [mode], ); return (
{ console.log("Data changed:", data); }} />
); }; export default CustomInspectorExample;