/** * @file Grid toolbox component */ import * as React from "react"; import { useNodeCanvas, useNodeCanvasActions } from "../../contexts/composed/canvas/viewport/context"; import { useNodeEditor } from "../../contexts/composed/node-editor/context"; import { useEditorActionState } from "../../contexts/composed/EditorActionStateContext"; import { applyZoomDelta, clampZoomScale } from "../../utils/zoomUtils"; import styles from "./GridToolbox.module.css"; // eslint-disable-next-line @typescript-eslint/no-empty-object-type export type GridToolboxProps = { // No props needed - layout is handled by GridLayout }; const ZOOM_PRESETS = [5, 10, 25, 50, 100, 200, 400, 800, 1000] as const; // Memoized zoom button component const ZoomButton = React.memo<{ onClick: () => void; title: string; ariaLabel: string; disabled?: boolean; children: React.ReactNode; }>(({ onClick, title, ariaLabel, disabled, children }) => ( )); ZoomButton.displayName = "ZoomButton"; // Memoized toggle button component const ToggleButton = React.memo<{ active: boolean; onClick: () => void; title: string; ariaLabel: string; children: React.ReactNode; }>(({ active, onClick, title, ariaLabel, children }) => ( )); ToggleButton.displayName = "ToggleButton"; // Memoized status indicator component const StatusIndicator = React.memo<{ label: string; value: number; }>(({ label, value }) => (
{label}: {value}
)); StatusIndicator.displayName = "StatusIndicator"; export const GridToolbox: React.FC = React.memo(() => { const { state: canvasState } = useNodeCanvas(); const { actions: canvasActions } = useNodeCanvasActions(); const { state: editorState } = useNodeEditor(); const { state: actionState } = useEditorActionState(); const zoomPercentage = React.useMemo( () => Math.round(canvasState.viewport.scale * 100), [canvasState.viewport.scale], ); const nodeCount = React.useMemo(() => Object.keys(editorState.nodes).length, [editorState.nodes]); const connectionCount = React.useMemo(() => Object.keys(editorState.connections).length, [editorState.connections]); const selectedNodeCount = React.useMemo(() => actionState.selectedNodeIds.length, [actionState.selectedNodeIds]); const handleZoomIn = React.useCallback(() => { const newScale = applyZoomDelta(canvasState.viewport.scale, 1); canvasActions.zoomViewport(newScale); }, [canvasState.viewport.scale, canvasActions]); const handleZoomOut = React.useCallback(() => { const newScale = applyZoomDelta(canvasState.viewport.scale, -1); canvasActions.zoomViewport(newScale); }, [canvasState.viewport.scale, canvasActions]); const handleZoomReset = React.useCallback(() => { canvasActions.setViewport({ ...canvasState.viewport, scale: 1, }); }, [canvasState.viewport, canvasActions]); const handleZoomToFit = React.useCallback(() => { const nodes = Object.values(editorState.nodes); if (nodes.length === 0) { return; } // Calculate bounding box of all nodes let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; nodes.forEach((node) => { minX = Math.min(minX, node.position.x); minY = Math.min(minY, node.position.y); maxX = Math.max(maxX, node.position.x + (node.size?.width ?? 150)); maxY = Math.max(maxY, node.position.y + (node.size?.height ?? 50)); }); const padding = 50; const contentWidth = maxX - minX + padding * 2; const contentHeight = maxY - minY + padding * 2; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const scaleX = viewportWidth / contentWidth; const scaleY = viewportHeight / contentHeight; const scale = clampZoomScale(Math.min(scaleX, scaleY, 1)); const centerX = (minX + maxX) / 2; const centerY = (minY + maxY) / 2; canvasActions.setViewport({ scale, offset: { x: -(centerX * scale - viewportWidth / 2), y: -(centerY * scale - viewportHeight / 2), }, }); }, [editorState.nodes, canvasActions]); const handleZoomToSelection = React.useCallback(() => { if (selectedNodeCount === 0) { return; } const selectedNodes = actionState.selectedNodeIds .map((id) => editorState.nodes[id]) .filter((node): node is NonNullable => node != null); if (selectedNodes.length === 0) { return; } // Calculate bounding box of selected nodes let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; selectedNodes.forEach((node) => { minX = Math.min(minX, node.position.x); minY = Math.min(minY, node.position.y); maxX = Math.max(maxX, node.position.x + (node.size?.width ?? 150)); maxY = Math.max(maxY, node.position.y + (node.size?.height ?? 50)); }); const padding = 50; const contentWidth = maxX - minX + padding * 2; const contentHeight = maxY - minY + padding * 2; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const scaleX = viewportWidth / contentWidth; const scaleY = viewportHeight / contentHeight; const scale = clampZoomScale(Math.min(scaleX, scaleY, 2)); const centerX = (minX + maxX) / 2; const centerY = (minY + maxY) / 2; canvasActions.setViewport({ scale, offset: { x: -(centerX * scale - viewportWidth / 2), y: -(centerY * scale - viewportHeight / 2), }, }); }, [editorState.nodes, actionState.selectedNodeIds, selectedNodeCount, canvasActions]); const handlePresetZoom = React.useCallback( (preset: number) => { canvasActions.setViewport({ ...canvasState.viewport, scale: preset / 100, }); }, [canvasState.viewport, canvasActions], ); const handleToggleGrid = React.useCallback(() => { canvasActions.updateGridSettings({ showGrid: !canvasState.gridSettings.showGrid, }); }, [canvasState.gridSettings.showGrid, canvasActions]); const handleToggleSnapToGrid = React.useCallback(() => { canvasActions.updateGridSettings({ snapToGrid: !canvasState.gridSettings.snapToGrid, }); }, [canvasState.gridSettings.snapToGrid, canvasActions]); // Memoized zoom select options const zoomOptions = React.useMemo(() => { const options = ZOOM_PRESETS.map((preset) => ( )); if (!ZOOM_PRESETS.includes(zoomPercentage as (typeof ZOOM_PRESETS)[number])) { options.push( , ); } return options; }, [zoomPercentage]); return (
{/* Zoom controls */}
{/* Zoom presets */} 1:1
{/* Grid toggle */} {/* Snap to grid toggle */}
{/* Node count indicator */} {/* Connection count indicator */}
); }); GridToolbox.displayName = "GridToolbox"; /** * Debug notes: * - Reviewed src/components/canvas/CanvasBase.tsx to mirror toolbar zoom controls with canvas handlers. * - Reviewed src/contexts/NodeCanvasContext.tsx to confirm shared clamping semantics for toolbar adjustments. */