From e7ff53dcd7c1f0f3fa31a0963dfbcce3884546ef Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Fri, 10 Oct 2025 18:13:18 +0200 Subject: [PATCH] feat: side panels for properties and tools --- docs/SIDE_PANELS_UX_DESIGN.md | 2826 +++++++++++++++++++++++++ src/App.tsx | 85 +- src/components/Editor/GraphEditor.tsx | 145 +- src/components/Panels/LeftPanel.tsx | 288 +++ src/components/Panels/RightPanel.tsx | 492 +++++ src/hooks/useDocumentHistory.ts | 37 +- src/stores/panelStore.ts | 137 ++ 7 files changed, 3945 insertions(+), 65 deletions(-) create mode 100644 docs/SIDE_PANELS_UX_DESIGN.md create mode 100644 src/components/Panels/LeftPanel.tsx create mode 100644 src/components/Panels/RightPanel.tsx create mode 100644 src/stores/panelStore.ts diff --git a/docs/SIDE_PANELS_UX_DESIGN.md b/docs/SIDE_PANELS_UX_DESIGN.md new file mode 100644 index 0000000..fb80e7f --- /dev/null +++ b/docs/SIDE_PANELS_UX_DESIGN.md @@ -0,0 +1,2826 @@ +# Side Panels UI/UX Design Specification +## Constellation Analyzer - Collapsible Panels Implementation Guide + +**Version:** 1.0 +**Date:** 2025-10-10 +**Status:** Design Specification +**Related:** UX_ANALYSIS.md Section 2.6, 4.1 + +--- + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [Design Goals](#design-goals) +3. [Layout Architecture](#layout-architecture) +4. [Left Panel: Tools Panel](#left-panel-tools-panel) +5. [Right Panel: Properties Panel](#right-panel-properties-panel) +6. [Panel Controls & Interactions](#panel-controls--interactions) +7. [Responsive Behavior](#responsive-behavior) +8. [Visual Design Specifications](#visual-design-specifications) +9. [User Workflow Improvements](#user-workflow-improvements) +10. [Implementation Guide](#implementation-guide) +11. [Accessibility Requirements](#accessibility-requirements) +12. [Migration Strategy](#migration-strategy) + +--- + +## Executive Summary + +This document specifies the design for implementing collapsible left and right side panels in Constellation Analyzer, replacing the current horizontal toolbar and modal property dialogs with a more professional, space-efficient panel-based layout. + +**Key Changes:** +- **Left Panel (Tools):** Moves toolbar contents to collapsible sidebar with improved organization +- **Right Panel (Properties):** Replaces modal dialogs with persistent, context-aware panel +- **Canvas:** Gains more space when panels collapse, improving focus and usability +- **State Persistence:** Panel states (open/closed, width) saved per user in localStorage + +**User Benefits:** +- 40-60% more canvas space when panels collapsed +- Non-modal property editing maintains context +- Better visual hierarchy and tool organization +- Professional appearance matching modern IDEs +- Keyboard-driven panel control + +--- + +## Design Goals + +### Primary Objectives +1. **Maximize Canvas Space:** More room for complex graphs +2. **Maintain Context:** Non-modal panels keep graph visible while editing +3. **Improve Discoverability:** Organized, always-visible tool categories +4. **Professional Appearance:** Match VS Code, Figma, Blender-style panel systems +5. **Flexible Workspace:** User controls their preferred layout + +### Success Metrics +- Canvas space increases by 40-60% when both panels collapsed +- Property editing doesn't require closing panel to see results +- New users can find all tools within 30 seconds +- 80% of users keep preferred panel state across sessions + +--- + +## Layout Architecture + +### Overall Screen Structure + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ HEADER: Constellation Analyzer Logo + Title [×] │ ← 80px +├──────────────────────────────────────────────────────────────────────┤ +│ MENU BAR: File Edit View Layout Help │ ← 40px +├──────────────────────────────────────────────────────────────────────┤ +│ DOCUMENT TABS: Doc1 Doc2 [+] │ ← 44px +├────┬──────────────────────────────────────────────────────────┬─────┤ +│ │ │ │ +│ L │ CANVAS │ R │ +│ E │ (ReactFlow) │ I │ +│ F │ │ G │ +│ T │ Graph Editor Area │ H │ +│ │ │ T │ +│ P │ [Nodes, Edges, MiniMap, Controls] │ │ +│ A │ │ P │ +│ N │ │ A │ +│ E │ │ N │ +│ L │ │ E │ +│ │ │ L │ +└────┴──────────────────────────────────────────────────────────┴─────┘ + ↑ ↑ + 280px default 320px default + (collapsible to 40px icon bar) (collapsible to 0px) +``` + +### Dimension Specifications + +**Left Panel (Tools Panel)** +``` +Expanded: + - Default Width: 280px + - Min Width: 220px + - Max Width: 400px + - Collapsed Width: 40px (icon bar visible) + - Height: 100% of available space (below tabs) + +Collapsed Icon Bar: + - Width: 40px + - Shows vertical icons for quick access + - Tooltip on hover with panel name +``` + +**Right Panel (Properties Panel)** +``` +Expanded: + - Default Width: 320px + - Min Width: 280px + - Max Width: 600px + - Collapsed Width: 0px (completely hidden) + - Height: 100% of available space (below tabs) + +Collapsed State: + - Completely hidden to maximize canvas space + - Toggle button remains visible on canvas edge +``` + +**Canvas Area** +``` +Both Panels Open: + - Width: viewport - 280px - 320px = ~40% of 1920px screen + - Comfortable for medium graphs (10-20 nodes) + +Left Open, Right Closed: + - Width: viewport - 280px = ~60% of 1920px screen + - Good for active editing + +Both Panels Closed: + - Width: viewport - 40px = ~98% of 1920px screen + - Maximum space for large graphs (50+ nodes) + - Icon bar persists for quick tool access +``` + +### Responsive Breakpoints + +``` +≥ 1920px (Large Desktop): + - Both panels default open + - Canvas: ~1260px width + +1440px - 1919px (Desktop): + - Both panels default open + - Canvas: ~840px width + - Warning if both open and graph is large + +1024px - 1439px (Small Desktop/Laptop): + - Left panel default open + - Right panel default closed + - Canvas: ~744px width + +768px - 1023px (Tablet): + - Both panels default closed + - Panels overlay canvas when opened + - Canvas: ~728px width + +< 768px (Mobile): + - Panels not supported (show warning) + - Recommend desktop browser +``` + +--- + +## Left Panel: Tools Panel + +### Purpose +Centralized location for all graph creation and editing tools, replacing the horizontal toolbar. + +### Content Organization + +The left panel is divided into collapsible sections for easy scanning and organization: + +``` +┌─────────────────────────────┐ +│ [<] TOOLS [×] │ ← Header with collapse/close +├─────────────────────────────┤ +│ │ +│ ▼ HISTORY │ ← Collapsible section +│ [↶ Undo] [↷ Redo] │ +│ Move Actor │ ← Action description +│ │ +│ ▼ ADD ACTORS │ +│ ┌─────────────┐ │ +│ │ Person │ [●] │ ← Color indicator +│ └─────────────┘ │ +│ ┌─────────────┐ │ +│ │ Organization│ [●] │ +│ └─────────────┘ │ +│ ┌─────────────┐ │ +│ │ System │ [●] │ +│ └─────────────┘ │ +│ ┌─────────────┐ │ +│ │ Concept │ [●] │ +│ └─────────────┘ │ +│ [+ Manage Types...] │ +│ │ +│ ▼ RELATIONS │ +│ Active Type: │ +│ ┌─────────────────────┐ │ +│ │ Collaborates [▼] │ │ ← Dropdown +│ └─────────────────────┘ │ +│ ────────────────── │ ← Visual preview +│ [+ Manage Types...] │ +│ │ +│ ▼ LAYOUT │ +│ ┌─────────────────────┐ │ +│ │ Auto Layout [▼] │ │ +│ └─────────────────────┘ │ +│ • Force-Directed │ +│ • Hierarchical │ +│ • Circular │ +│ • Grid │ +│ │ +│ [Align Selected] │ +│ [Distribute Selected] │ +│ │ +│ ▼ VIEW │ +│ [Fit to Content] │ +│ [Reset Zoom] │ +│ ☑ Show Grid │ +│ ☑ Snap to Grid │ +│ ☑ Show MiniMap │ +│ │ +│ ▼ SEARCH │ +│ ┌─────────────────────┐ │ +│ │ 🔍 Search... │ │ +│ └─────────────────────┘ │ +│ No filters active │ +│ [Advanced Filters...] │ +│ │ +└─────────────────────────────┘ +``` + +### Section Specifications + +#### 1. History Section +**Purpose:** Quick access to undo/redo with visual feedback + +```typescript +interface HistorySection { + title: "HISTORY"; + collapsible: true; + defaultExpanded: true; + + content: { + undoButton: { + label: "Undo"; + icon: UndoIcon; + disabled: !canUndo; + tooltip: canUndo ? `Undo: ${lastAction}` : "Nothing to undo"; + shortcut: "Ctrl+Z"; + }; + redoButton: { + label: "Redo"; + icon: RedoIcon; + disabled: !canRedo; + tooltip: canRedo ? `Redo: ${nextAction}` : "Nothing to redo"; + shortcut: "Ctrl+Y"; + }; + actionDescription: { + text: lastAction; + style: "text-xs text-gray-500"; + }; + }; +} +``` + +**Visual Design:** +- Buttons side-by-side with icons +- Description text below shows last action +- Disabled state: 40% opacity +- Tooltips show action + keyboard shortcut + +#### 2. Add Actors Section +**Purpose:** Visual palette for creating nodes + +```typescript +interface AddActorsSection { + title: "ADD ACTORS"; + collapsible: true; + defaultExpanded: true; + + content: { + actorButtons: NodeTypeConfig[]; + manageTypesLink: { + label: "+ Manage Types..."; + action: openNodeTypeManager; + }; + }; + + buttonStyle: { + width: "100%"; + height: "48px"; + display: "flex"; + justifyContent: "space-between"; + padding: "12px"; + backgroundColor: nodeType.color; + borderRadius: "6px"; + marginBottom: "8px"; + }; +} +``` + +**Interactions:** +- Click button: Adds node to center of viewport +- Drag button: Start drag, drop on canvas to place (FUTURE) +- Hover: Shows full type description in tooltip +- Color dot on right shows node color + +**Visual Improvements over Current Toolbar:** +- Vertical layout allows more descriptive labels +- Color preview is larger and clearer +- Can show icons in future iterations +- Tooltips have more space for descriptions + +#### 3. Relations Section +**Purpose:** Manage active relation type for connections + +```typescript +interface RelationsSection { + title: "RELATIONS"; + collapsible: true; + defaultExpanded: true; + + content: { + activeTypeLabel: "Active Type:"; + dropdown: { + value: selectedRelationType; + options: edgeTypes; + onChange: setSelectedRelationType; + }; + visualPreview: { + // SVG line showing current type style + strokeColor: currentEdgeType.color; + strokeStyle: currentEdgeType.style; // solid/dashed/dotted + }; + manageTypesLink: { + label: "+ Manage Types..."; + action: openEdgeTypeManager; + }; + }; +} +``` + +**Visual Preview:** +- SVG line rendered below dropdown +- Shows actual edge color and style +- Updates live when dropdown changes +- Helps users visualize before creating + +#### 4. Layout Section +**Purpose:** One-click layout algorithms + +```typescript +interface LayoutSection { + title: "LAYOUT"; + collapsible: true; + defaultExpanded: false; // Collapsed by default + + content: { + autoLayoutDropdown: { + options: [ + "Force-Directed", + "Hierarchical (Top-Down)", + "Hierarchical (Left-Right)", + "Circular", + "Grid" + ]; + onSelect: applyLayout; + }; + alignButton: { + label: "Align Selected"; + enabled: selectedNodes.length >= 2; + submenu: ["Left", "Right", "Top", "Bottom", "Center H", "Center V"]; + }; + distributeButton: { + label: "Distribute Selected"; + enabled: selectedNodes.length >= 3; + submenu: ["Horizontally", "Vertically"]; + }; + }; +} +``` + +**Note:** This section implements recommendation from UX_ANALYSIS.md Section 2.3 + +#### 5. View Section +**Purpose:** Canvas view controls and settings + +```typescript +interface ViewSection { + title: "VIEW"; + collapsible: true; + defaultExpanded: false; + + content: { + fitViewButton: { + label: "Fit to Content"; + shortcut: "F"; + action: fitView; + }; + resetZoomButton: { + label: "Reset Zoom"; + shortcut: "Ctrl+0"; + action: () => setViewport({ zoom: 1 }); + }; + showGridToggle: { + label: "Show Grid"; + checked: showGrid; + onChange: toggleGrid; + }; + snapToGridToggle: { + label: "Snap to Grid"; + checked: snapToGrid; + onChange: toggleSnapToGrid; + }; + showMinimapToggle: { + label: "Show MiniMap"; + checked: showMinimap; + onChange: toggleMinimap; + }; + }; +} +``` + +**Improvement:** Consolidates view settings in one place + +#### 6. Search Section +**Purpose:** Find and filter actors/relations + +```typescript +interface SearchSection { + title: "SEARCH"; + collapsible: true; + defaultExpanded: false; + + content: { + searchInput: { + placeholder: "🔍 Search actors..."; + value: searchQuery; + onChange: handleSearch; + clearButton: true; + }; + filterStatus: { + text: activeFilters.length > 0 + ? `${filteredCount} of ${totalCount} actors` + : "No filters active"; + }; + advancedFiltersButton: { + label: "Advanced Filters..."; + action: openFilterPanel; + }; + }; +} +``` + +**Note:** Implements UX_ANALYSIS.md Section 1.4 (Search and Filter) + +### Collapsed State: Icon Bar + +When left panel collapses, it becomes a vertical icon bar: + +``` +┌──┐ +│≡ │ ← Menu toggle (expands panel) +├──┤ +│↶ │ ← Undo (with tooltip) +├──┤ +│+ │ ← Add Actor (with dropdown on click) +├──┤ +│⟿ │ ← Relations +├──┤ +│⊞ │ ← Layout +├──┤ +│👁│ ← View +├──┤ +│🔍│ ← Search +├──┤ +│ │ +│ │ +└──┘ +``` + +**Specifications:** +- Width: 40px +- Icons: 24x24px Material Icons +- Padding: 8px vertical between icons +- Background: Same as panel (white/gray) +- Border: Right border to separate from canvas +- Tooltips: Show section name + "Click to expand" + +**Interactions:** +- Click menu icon (≡): Expand full panel +- Click section icon: Expand panel and auto-scroll to that section +- Hover: Show tooltip with section name + +--- + +## Right Panel: Properties Panel + +### Purpose +Context-aware property inspector for selected graph elements, replacing modal dialogs with persistent panel. + +### Selection State Management + +The right panel content changes based on what's selected: + +```typescript +type SelectionState = + | { type: 'none' } + | { type: 'single-node'; node: Actor } + | { type: 'multiple-nodes'; nodes: Actor[] } + | { type: 'single-edge'; edge: Relation } + | { type: 'multiple-edges'; edges: Relation[] } + | { type: 'mixed'; nodes: Actor[]; edges: Relation[] }; +``` + +### State 1: Nothing Selected + +``` +┌─────────────────────────────┐ +│ PROPERTIES [×] │ +├─────────────────────────────┤ +│ │ +│ ╭─────────╮ │ +│ │ ··· │ │ +│ ╰─────────╯ │ +│ │ +│ No Selection │ +│ │ +│ Select a node or edge │ +│ to view properties │ +│ │ +│ Tips: │ +│ • Double-click to edit │ +│ • Right-click for menu │ +│ • Shift+click to multi- │ +│ select │ +│ │ +│ │ +│ │ +│ │ +└─────────────────────────────┘ +``` + +**Purpose:** +- Guide users on how to select items +- Provide helpful tips +- Clean, uncluttered state + +### State 2: Single Node Selected + +``` +┌─────────────────────────────┐ +│ ACTOR PROPERTIES [×] │ +├─────────────────────────────┤ +│ │ +│ Label * │ +│ ┌─────────────────────────┐ │ +│ │ John Doe │ │ ← Auto-focus, auto-select +│ └─────────────────────────┘ │ +│ │ +│ Type │ +│ ┌─────────────────────────┐ │ +│ │ Person [▼] │ │ +│ └─────────────────────────┘ │ +│ ┌─────────────────────────┐ │ +│ │ Color Preview │ │ ← Shows node color +│ └─────────────────────────┘ │ +│ │ +│ Description │ +│ ┌─────────────────────────┐ │ +│ │ Team lead for the │ │ +│ │ engineering department │ │ +│ │ │ │ +│ └─────────────────────────┘ │ +│ │ +│ ▼ METADATA (0) │ ← Collapsible +│ [+ Add Custom Field] │ +│ │ +│ ▼ CONNECTIONS (3) │ +│ → Collaborates │ +│ • Jane Smith │ +│ • Bob Johnson │ +│ → Reports To │ +│ • Alice Brown │ +│ │ +│ [View in Graph] │ ← Highlights connections +│ │ +├─────────────────────────────┤ +│ Node: actor-123 │ ← Footer with metadata +│ Position: (350, 240) │ +│ │ +│ [Delete Actor] │ +└─────────────────────────────┘ +``` + +**Specifications:** + +```typescript +interface NodePropertiesPanel { + header: { + title: "ACTOR PROPERTIES"; + closeButton: true; + }; + + fields: { + label: { + type: "text"; + required: true; + autoFocus: true; + autoSelect: true; + placeholder: "Enter actor name"; + onChange: (value) => updateNodeLive(node.id, { label: value }); + }; + + type: { + type: "select"; + options: nodeTypes; + onChange: (typeId) => updateNodeLive(node.id, { type: typeId }); + }; + + colorPreview: { + type: "visual"; + color: currentNodeType.color; + height: "40px"; + }; + + description: { + type: "textarea"; + rows: 4; + placeholder: "Add description..."; + onChange: debounce((value) => updateNodeLive(node.id, { description: value }), 500); + }; + }; + + sections: { + metadata: { + title: "METADATA"; + collapsible: true; + defaultExpanded: false; + badge: metadataCount; + content: MetadataEditor; + }; + + connections: { + title: "CONNECTIONS"; + collapsible: true; + defaultExpanded: true; + badge: connectionCount; + content: ConnectionList; + }; + }; + + footer: { + nodeInfo: { + id: node.id; + position: `(${Math.round(node.position.x)}, ${Math.round(node.position.y)})`; + }; + deleteButton: { + label: "Delete Actor"; + variant: "danger"; + confirmation: true; + }; + }; +} +``` + +**Key Features:** +1. **Live Updates:** Changes apply immediately (no Save button) +2. **Auto-focus:** Label field focused when panel opens +3. **Connection Preview:** Shows related edges and nodes +4. **Collapsible Sections:** Keep panel clean and scannable +5. **Non-modal:** Graph remains visible and interactive + +### State 3: Multiple Nodes Selected + +``` +┌─────────────────────────────┐ +│ BULK EDIT (3 ACTORS) [×] │ +├─────────────────────────────┤ +│ │ +│ Selected: │ +│ • John Doe (Person) │ +│ • Acme Corp (Organization) │ +│ • API Gateway (System) │ +│ │ +│ ────────────────────────── │ +│ │ +│ Bulk Actions: │ +│ │ +│ Change Type │ +│ ┌─────────────────────────┐ │ +│ │ (Keep Individual) [▼] │ │ +│ └─────────────────────────┘ │ +│ • Person │ +│ • Organization │ +│ • System │ +│ • Concept │ +│ │ +│ [Apply to All] │ +│ │ +│ ────────────────────────── │ +│ │ +│ Layout: │ +│ [Align Left] [Align Top] │ +│ [Align Center] [Align Right]│ +│ [Distribute Horizontally] │ +│ [Distribute Vertically] │ +│ │ +│ ────────────────────────── │ +│ │ +│ [Delete All Selected] │ +│ │ +└─────────────────────────────┘ +``` + +**Purpose:** Bulk operations for efficiency (implements UX_ANALYSIS.md 1.5) + +### State 4: Single Edge Selected + +``` +┌─────────────────────────────┐ +│ RELATION PROPERTIES [×] │ +├─────────────────────────────┤ +│ │ +│ From │ +│ ┌─────────────────────────┐ │ +│ │ John Doe (Person) │ │ ← Read-only, click to jump +│ └─────────────────────────┘ │ +│ ↓ │ +│ To │ +│ ┌─────────────────────────┐ │ +│ │ Jane Smith (Person) │ │ +│ └─────────────────────────┘ │ +│ │ +│ Type │ +│ ┌─────────────────────────┐ │ +│ │ Collaborates [▼] │ │ +│ └─────────────────────────┘ │ +│ ────────────────── │ ← Visual preview of style +│ │ +│ Custom Label (optional) │ +│ ┌─────────────────────────┐ │ +│ │ │ │ +│ └─────────────────────────┘ │ +│ Leave empty to use type │ +│ label: "Collaborates" │ +│ │ +│ ▼ METADATA (0) │ +│ [+ Add Custom Field] │ +│ │ +├─────────────────────────────┤ +│ Edge: edge-456 │ +│ │ +│ [Delete Relation] │ +└─────────────────────────────┘ +``` + +**Key Difference from Modal:** +- Graph stays visible +- Can see connection while editing +- Click source/target to jump to that node + +### State 5: Multiple Edges Selected + +``` +┌─────────────────────────────┐ +│ BULK EDIT (2 RELATIONS) [×] │ +├─────────────────────────────┤ +│ │ +│ Selected: │ +│ • John → Jane (Collaborates)│ +│ • Alice → Bob (Reports To) │ +│ │ +│ ────────────────────────── │ +│ │ +│ Change Type │ +│ ┌─────────────────────────┐ │ +│ │ (Keep Individual) [▼] │ │ +│ └─────────────────────────┘ │ +│ │ +│ [Apply to All] │ +│ │ +│ ────────────────────────── │ +│ │ +│ [Delete All Selected] │ +│ │ +└─────────────────────────────┘ +``` + +### Panel Behavior Specifications + +**Opening:** +- Double-click node/edge: Auto-opens right panel (if closed) +- Panel slides in from right (200ms ease-out) +- Previous content fades out, new content fades in (150ms) + +**Closing:** +- Click [×] button: Panel closes completely +- Click canvas: Selection clears, panel shows "Nothing Selected" +- Press Escape: Clears selection and closes panel + +**Live Updates:** +- Text inputs: Debounced 500ms, then update graph +- Dropdowns: Immediate update on change +- Checkboxes: Immediate update +- Visual feedback: Changed field highlights briefly (blue border pulse) + +**Validation:** +- Required fields: Red border if empty on blur +- Invalid values: Inline error message below field +- Block delete if it would orphan required relations (configurable) + +--- + +## Panel Controls & Interactions + +### Collapse/Expand Mechanisms + +#### Left Panel Collapse Button + +``` +Location: Top-left of panel header +Icon (Expanded): [<] ChevronLeft +Icon (Collapsed): [>] ChevronRight +Tooltip (Expanded): "Collapse Tools Panel (Ctrl+B)" +Tooltip (Collapsed): "Expand Tools Panel (Ctrl+B)" +Size: 32x32px +``` + +**Behavior:** +```typescript +function toggleLeftPanel() { + if (leftPanelExpanded) { + // Collapse to icon bar + animateWidth(leftPanelWidth, 40, 200, 'ease-out'); + setLeftPanelExpanded(false); + localStorage.setItem('leftPanelExpanded', 'false'); + } else { + // Expand to saved width or default + const width = localStorage.getItem('leftPanelWidth') || 280; + animateWidth(40, width, 200, 'ease-out'); + setLeftPanelExpanded(true); + localStorage.setItem('leftPanelExpanded', 'true'); + } +} +``` + +#### Right Panel Toggle Button + +When right panel is closed, a floating toggle button appears on the right edge of the canvas: + +``` +┌─────────────────────────────────────┐ +│ ┃ +│ CANVAS ┃← +│ ┃ +│ ┃ +│ [>]┃ ← Floating button +│ ┃ +│ ┃ +└─────────────────────────────────────┘ +``` + +**Specifications:** +```typescript +interface RightPanelToggle { + position: { + right: 0; + top: '50%'; + transform: 'translateY(-50%)'; + }; + size: { + width: '32px'; + height: '120px'; + }; + style: { + background: 'rgba(255, 255, 255, 0.9)'; + border: '1px solid #e5e7eb'; + borderRight: 'none'; + borderRadius: '8px 0 0 8px'; + boxShadow: '-2px 0 8px rgba(0,0,0,0.1)'; + }; + icon: ChevronLeftIcon; // Points left to indicate "expand" + tooltip: "Show Properties Panel (Ctrl+I)"; +} +``` + +**Interaction:** +- Hover: Slight width expansion (32px → 36px) +- Click: Panel slides in from right +- Auto-hide: Fades to 50% opacity when mouse not nearby +- Persists: Always visible when panel closed + +### Resize Handles + +Both panels can be resized by dragging the edge: + +``` +LEFT PANEL RESIZE: +┌──────────────────┃← Resize handle (4px width) +│ ┃ +│ TOOLS PANEL ┃ +│ ┃ +└──────────────────┃ + +RIGHT PANEL RESIZE: + ┃──────────────────┐ + ┃→ Resize handle │ + ┃ PROPERTIES │ + ┃ │ + ┃──────────────────┘ +``` + +**Specifications:** +```typescript +interface ResizeHandle { + width: 4px; + cursor: 'col-resize'; + + visual: { + default: 'transparent'; + hover: 'rgba(59, 130, 246, 0.5)'; // Blue highlight + dragging: 'rgba(59, 130, 246, 0.8)'; + }; + + constraints: { + leftPanel: { + min: 220, + max: 400, + }; + rightPanel: { + min: 280, + max: 600, + }; + }; + + behavior: { + onDragStart: () => document.body.style.cursor = 'col-resize'; + onDrag: (delta) => updatePanelWidth(currentWidth + delta); + onDragEnd: () => { + document.body.style.cursor = 'default'; + localStorage.setItem('panelWidth', newWidth); + }; + }; +} +``` + +**Visual Feedback During Resize:** +- Handle highlights on hover (blue glow) +- Cursor changes to col-resize +- Width value tooltip appears during drag: "280px" +- Smooth animation when snapping to min/max + +### Keyboard Shortcuts + +```typescript +const panelShortcuts = { + 'Ctrl+B': 'Toggle Left Panel (Tools)', + 'Ctrl+I': 'Toggle Right Panel (Properties/Inspector)', + 'Ctrl+Shift+B': 'Toggle Both Panels', + 'Escape': 'Close Right Panel & Clear Selection', +}; +``` + +**Implementation:** +```typescript +useEffect(() => { + const handleKeyPress = (e: KeyboardEvent) => { + if (e.ctrlKey && e.key === 'b') { + e.preventDefault(); + toggleLeftPanel(); + } + if (e.ctrlKey && e.key === 'i') { + e.preventDefault(); + toggleRightPanel(); + } + if (e.ctrlKey && e.shiftKey && e.key === 'B') { + e.preventDefault(); + toggleBothPanels(); + } + if (e.key === 'Escape') { + closeRightPanel(); + clearSelection(); + } + }; + + window.addEventListener('keydown', handleKeyPress); + return () => window.removeEventListener('keydown', handleKeyPress); +}, []); +``` + +### View Menu Integration + +Add panel visibility controls to View menu: + +``` +View + ├─ Focus Mode (F11) + ├─ ─────────────── + ├─ ☑ Show Tools Panel (Ctrl+B) + ├─ ☑ Show Properties Panel (Ctrl+I) + ├─ ─────────────── + ├─ Show Grid + ├─ Show MiniMap + └─ Fit View (F) +``` + +--- + +## Responsive Behavior + +### Screen Size Adaptations + +#### Large Desktop (≥1920px) +``` +Default State: +├─ Left Panel: Open (280px) +├─ Right Panel: Closed initially, opens on selection +├─ Canvas: ~1600px +└─ Behavior: Full features, all panels comfortable +``` + +#### Desktop (1440px - 1919px) +``` +Default State: +├─ Left Panel: Open (280px) +├─ Right Panel: Closed, opens on demand +├─ Canvas: ~1120px +└─ Behavior: Recommend closing left panel for large graphs +``` + +#### Small Desktop/Laptop (1024px - 1439px) +``` +Default State: +├─ Left Panel: Closed (icon bar only, 40px) +├─ Right Panel: Closed +├─ Canvas: ~980px +└─ Behavior: + • Show notification: "Panels collapsed for space. Press Ctrl+B to expand tools." + • Panels overlay canvas when opened (position: absolute with z-index) +``` + +#### Tablet (768px - 1023px) +``` +Default State: +├─ Left Panel: Collapsed +├─ Right Panel: Collapsed +├─ Canvas: Full width +└─ Behavior: + • Panels overlay canvas when opened + • Semi-transparent backdrop behind panels + • Click outside panel to close + • Touch-friendly panel toggle buttons (larger) +``` + +#### Mobile (<768px) +``` +Warning State: +├─ Show fullscreen message: +│ "For the best experience, please use a desktop browser." +│ [Continue Anyway] [Learn More] +│ +└─ If user continues: + • Panels completely disabled + • Toolbar remains but simplified + • Property editing via modal dialogs (current behavior) + • Limited graph editing capabilities +``` + +### Panel Priority Rules + +When screen space is limited, follow this priority: + +``` +1. Canvas (CRITICAL) - minimum 600px width +2. Left Panel Collapsed (IMPORTANT) - 40px icon bar +3. Right Panel Closed (OPTIONAL) - 0px +4. Left Panel Expanded (NICE-TO-HAVE) - 280px +5. Right Panel Open (NICE-TO-HAVE) - 320px +``` + +**Auto-collapse Logic:** +```typescript +function handleResize(viewportWidth: number) { + const MIN_CANVAS = 600; + + // Calculate required space + const leftWidth = leftPanelExpanded ? 280 : 40; + const rightWidth = rightPanelOpen ? 320 : 0; + const canvasWidth = viewportWidth - leftWidth - rightWidth; + + // Auto-collapse if canvas too small + if (canvasWidth < MIN_CANVAS) { + if (rightPanelOpen) { + closeRightPanel(); + showNotification('Properties panel auto-collapsed for space'); + } else if (leftPanelExpanded) { + collapseLeftPanel(); + showNotification('Tools panel auto-collapsed for space'); + } + } +} +``` + +### Overlay Mode + +On screens <1440px, panels can overlay the canvas instead of pushing it: + +``` +OVERLAY MODE VISUAL: + +┌─────────────────────────────────────┐ +│┌──────────────┐ │ +││ │ │ +││ LEFT PANEL │ CANVAS │ +││ (overlay) │ (dimmed 20%) │ +││ │ │ +││ │ │ +│└──────────────┘ │ +└─────────────────────────────────────┘ + ↑ + Semi-transparent + backdrop +``` + +**Specifications:** +```typescript +interface OverlayMode { + enabled: viewportWidth < 1440; + + leftPanel: { + position: 'absolute'; + zIndex: 100; + boxShadow: '2px 0 16px rgba(0,0,0,0.2)'; + }; + + rightPanel: { + position: 'absolute'; + zIndex: 100; + boxShadow: '-2px 0 16px rgba(0,0,0,0.2)'; + }; + + backdrop: { + position: 'absolute'; + background: 'rgba(0,0,0,0.2)'; + zIndex: 99; + onClick: closePanels; + }; + + behavior: { + clickOutside: 'close'; + escapeKey: 'close'; + animation: 'slide-in'; + }; +} +``` + +--- + +## Visual Design Specifications + +### Color Palette + +Based on existing Tailwind CSS theme in the app: + +```css +:root { + /* Panel Backgrounds */ + --panel-bg: #ffffff; + --panel-bg-hover: #f9fafb; + --panel-border: #e5e7eb; + + /* Headers */ + --panel-header-bg: #f3f4f6; + --panel-header-text: #111827; + + /* Sections */ + --section-title: #6b7280; + --section-border: #e5e7eb; + + /* Inputs */ + --input-border: #d1d5db; + --input-focus: #3b82f6; + --input-bg: #ffffff; + + /* Icon Bar (collapsed left panel) */ + --iconbar-bg: #f9fafb; + --iconbar-icon: #6b7280; + --iconbar-icon-hover: #1f2937; + + /* Resize Handle */ + --resize-hover: rgba(59, 130, 246, 0.5); + --resize-active: rgba(59, 130, 246, 0.8); + + /* Shadows */ + --panel-shadow: 0 1px 3px rgba(0,0,0,0.1); + --panel-shadow-hover: 0 2px 8px rgba(0,0,0,0.15); +} +``` + +### Typography + +```css +/* Panel Headers */ +.panel-header { + font-family: system-ui, -apple-system, sans-serif; + font-size: 14px; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--panel-header-text); +} + +/* Section Titles */ +.section-title { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--section-title); +} + +/* Body Text */ +.panel-body { + font-size: 14px; + line-height: 1.5; + color: #374151; +} + +/* Labels */ +.field-label { + font-size: 12px; + font-weight: 500; + color: #4b5563; + margin-bottom: 4px; +} + +/* Help Text */ +.help-text { + font-size: 11px; + color: #6b7280; + font-style: italic; +} +``` + +### Spacing System + +Consistent spacing using 4px base unit: + +```css +--space-1: 4px; /* Tight */ +--space-2: 8px; /* Close */ +--space-3: 12px; /* Default */ +--space-4: 16px; /* Comfortable */ +--space-5: 20px; /* Spacious */ +--space-6: 24px; /* Section gaps */ +--space-8: 32px; /* Major sections */ +``` + +**Application:** +``` +Panel Padding: var(--space-4) (16px) +Section Gaps: var(--space-6) (24px) +Field Gaps: var(--space-4) (16px) +Label-Input Gap: var(--space-2) (8px) +Button Padding: var(--space-3) var(--space-4) (12px 16px) +``` + +### Component Styles + +#### Panel Container +```css +.panel { + background: var(--panel-bg); + border: 1px solid var(--panel-border); + box-shadow: var(--panel-shadow); + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.panel-left { + border-right: 1px solid var(--panel-border); +} + +.panel-right { + border-left: 1px solid var(--panel-border); +} +``` + +#### Panel Header +```css +.panel-header { + padding: 12px 16px; + background: var(--panel-header-bg); + border-bottom: 1px solid var(--panel-border); + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; +} + +.panel-title { + font-size: 14px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.02em; +} + +.panel-close-btn { + width: 24px; + height: 24px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: background 150ms; +} + +.panel-close-btn:hover { + background: rgba(0,0,0,0.05); +} +``` + +#### Collapsible Section +```css +.section { + border-bottom: 1px solid var(--section-border); +} + +.section-header { + padding: 12px 16px; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + user-select: none; + transition: background 150ms; +} + +.section-header:hover { + background: var(--panel-bg-hover); +} + +.section-title { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--section-title); + display: flex; + align-items: center; + gap: 8px; +} + +.section-badge { + background: #e5e7eb; + color: #6b7280; + font-size: 10px; + padding: 2px 6px; + border-radius: 10px; +} + +.section-content { + padding: 16px; + animation: slideDown 200ms ease-out; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} +``` + +#### Form Fields +```css +.field { + margin-bottom: 16px; +} + +.field-label { + display: block; + font-size: 12px; + font-weight: 500; + color: #4b5563; + margin-bottom: 4px; +} + +.field-input { + width: 100%; + padding: 8px 12px; + font-size: 14px; + border: 1px solid var(--input-border); + border-radius: 6px; + transition: border-color 150ms, box-shadow 150ms; +} + +.field-input:focus { + outline: none; + border-color: var(--input-focus); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.field-textarea { + resize: vertical; + min-height: 80px; + font-family: inherit; +} + +.field-select { + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml..."); + background-position: right 8px center; + background-repeat: no-repeat; + padding-right: 32px; +} +``` + +#### Icon Bar (Collapsed Left Panel) +```css +.icon-bar { + width: 40px; + background: var(--iconbar-bg); + border-right: 1px solid var(--panel-border); + display: flex; + flex-direction: column; + align-items: center; + padding: 8px 0; + gap: 4px; +} + +.icon-bar-button { + width: 32px; + height: 32px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + color: var(--iconbar-icon); + cursor: pointer; + transition: all 150ms; +} + +.icon-bar-button:hover { + background: rgba(59, 130, 246, 0.1); + color: var(--iconbar-icon-hover); +} + +.icon-bar-divider { + width: 24px; + height: 1px; + background: var(--panel-border); + margin: 4px 0; +} +``` + +#### Buttons +```css +.btn-primary { + padding: 8px 16px; + background: #3b82f6; + color: white; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background 150ms; +} + +.btn-primary:hover { + background: #2563eb; +} + +.btn-danger { + background: #fef2f2; + color: #dc2626; + border: 1px solid #fecaca; +} + +.btn-danger:hover { + background: #fee2e2; +} + +.btn-secondary { + background: #f3f4f6; + color: #374151; + border: 1px solid #e5e7eb; +} + +.btn-secondary:hover { + background: #e5e7eb; +} +``` + +### Animation Specifications + +```css +/* Panel Expand/Collapse */ +.panel-transition { + transition: width 200ms ease-out; +} + +/* Content Fade */ +.content-fade-enter { + animation: fadeIn 150ms ease-in; +} + +.content-fade-exit { + animation: fadeOut 100ms ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fadeOut { + from { opacity: 1; } + to { opacity: 0; } +} + +/* Slide In (Right Panel) */ +.panel-slide-in { + animation: slideInRight 200ms ease-out; +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +/* Field Focus Pulse */ +.field-changed { + animation: fieldPulse 400ms ease-out; +} + +@keyframes fieldPulse { + 0% { + border-color: var(--input-focus); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3); + } + 100% { + border-color: var(--input-border); + box-shadow: none; + } +} +``` + +--- + +## User Workflow Improvements + +### Before (Current State) vs After (With Panels) + +#### Workflow 1: Adding Multiple Actors + +**BEFORE:** +``` +1. Look up at toolbar +2. Click "Person" button +3. Node appears in random position +4. Drag node to desired position +5. Scroll up to toolbar again +6. Click "Person" button again +7. Repeat 20 times... +8. Eyes constantly moving between toolbar and canvas +``` + +**AFTER:** +``` +1. Glance left at tools panel +2. Click "Person" button (larger target, always visible) +3. Node appears, position it +4. Click "Person" again (hand never leaves panel area) +5. Repeat efficiently +6. Eyes stay focused on left side and canvas +7. Alternatively: Press 'P' keyboard shortcut (even faster) +``` + +**Time Saved:** ~30% per actor creation +**Ergonomic Benefit:** Less eye movement, more consistent hand position + +#### Workflow 2: Editing Actor Properties + +**BEFORE:** +``` +1. Double-click node +2. Modal dialog appears, blocking canvas view +3. Change type from Person to Organization +4. Click Save +5. Modal closes +6. Look at canvas to see result +7. Not happy with the color +8. Double-click again to reopen +9. Change back +10. Save and close again +``` + +**AFTER:** +``` +1. Click node once (or double-click) +2. Right panel shows properties (canvas still visible) +3. Change type dropdown → see live update immediately +4. See color change on canvas in real-time +5. Adjust label while viewing context +6. See updated description on canvas +7. No Save button needed - all changes live +8. Click away when done +``` + +**Time Saved:** ~50% per property edit +**Context Benefit:** Always see graph while editing + +#### Workflow 3: Creating a Complex Relation Network + +**BEFORE:** +``` +1. Add nodes via toolbar +2. Select relation type in toolbar dropdown +3. Drag from node handle to create edge +4. Realize wrong relation type was selected +5. Double-click edge +6. Modal opens +7. Change type +8. Save +9. Modal closes +10. Repeat for each edge +``` + +**AFTER:** +``` +1. Add nodes from left panel +2. See current relation type highlighted in left panel +3. Change if needed (one click, stays visible) +4. Create edges +5. If wrong type: click edge, change in right panel (no modal) +6. See change immediately +7. Continue creating edges +8. Current type always visible in left panel +``` + +**Time Saved:** ~40% for multi-edge creation +**Error Reduction:** Less mistakes due to visible current type + +#### Workflow 4: Organizing Graph Layout + +**BEFORE:** +``` +1. Manually drag each node +2. Try to align them visually +3. Zoom out to see full graph +4. Still not aligned well +5. Zoom back in +6. Drag more nodes +7. Repeat until frustrated +8. Graph still looks messy +``` + +**AFTER:** +``` +1. Select multiple nodes (Shift+click) +2. Open left panel Layout section +3. Click "Align Left" → instant alignment +4. Click "Distribute Vertically" → even spacing +5. OR: Click "Auto Layout" → entire graph organized +6. Professional appearance in seconds +``` + +**Time Saved:** ~90% for layout tasks +**Quality Improvement:** Professional-looking graphs + +#### Workflow 5: Finding and Editing a Specific Actor + +**BEFORE:** +``` +1. Scroll around canvas looking for actor +2. Zoom in and out +3. Click wrong actor +4. Keep searching +5. Finally find it +6. Double-click to edit +7. Modal blocks view of graph +8. Make changes +9. Close modal +``` + +**AFTER:** +``` +1. Open Search section in left panel +2. Type actor name +3. Graph highlights matching actor +4. Click search result → jumps to actor +5. Right panel shows properties automatically +6. Edit while viewing full context +7. Done +``` + +**Time Saved:** ~70% for finding specific actors +**Frustration Reduction:** Significant + +### Drag-and-Drop Enhancement (Future) + +**Vision:** Drag actor buttons from left panel directly onto canvas + +``` +User Flow: +1. Click and hold "Person" button in left panel +2. Drag cursor onto canvas +3. Visual preview follows cursor (ghost node) +4. Release at desired position +5. Node created at exact drop location +6. Right panel auto-opens with focus on label field +7. User types name immediately +``` + +**Benefits:** +- More intuitive than click-then-drag +- Precise initial positioning +- Seamless creation-to-editing flow +- Familiar pattern from design tools (Figma, Sketch) + +**Implementation:** Phase 2 feature (not MVP) + +### Multi-Selection Property Editing + +**Scenario:** User has 10 Person actors that should be Organization actors + +**BEFORE:** +``` +1. Double-click actor 1 +2. Change type to Organization +3. Save, close modal +4. Repeat 9 more times +5. Total: ~2 minutes +``` + +**AFTER:** +``` +1. Shift+click all 10 actors +2. Right panel shows "BULK EDIT (10 ACTORS)" +3. Change Type dropdown to "Organization" +4. Click "Apply to All" +5. All 10 change instantly +6. Total: ~10 seconds +``` + +**Time Saved:** ~92% for bulk changes + +--- + +## Implementation Guide + +### Phase 1: Foundation (Week 1-2) + +#### Step 1.1: Create Panel Component Structure +``` +/src/components/Panels/ +├── PanelContainer.tsx # Wrapper with resize logic +├── LeftPanel/ +│ ├── LeftPanel.tsx # Main left panel component +│ ├── IconBar.tsx # Collapsed state icon bar +│ ├── sections/ +│ │ ├── HistorySection.tsx +│ │ ├── AddActorsSection.tsx +│ │ ├── RelationsSection.tsx +│ │ ├── LayoutSection.tsx +│ │ ├── ViewSection.tsx +│ │ └── SearchSection.tsx +│ └── styles.css +├── RightPanel/ +│ ├── RightPanel.tsx # Main right panel component +│ ├── ToggleButton.tsx # Floating toggle when closed +│ ├── states/ +│ │ ├── EmptyState.tsx +│ │ ├── NodeProperties.tsx +│ │ ├── EdgeProperties.tsx +│ │ ├── BulkNodeEdit.tsx +│ │ └── BulkEdgeEdit.tsx +│ └── styles.css +└── hooks/ + ├── usePanelState.ts # Manage panel open/closed/width + ├── useResizeHandle.ts # Resize drag logic + └── usePanelShortcuts.ts # Keyboard controls +``` + +#### Step 1.2: Create Panel State Store +```typescript +// /src/stores/panelStore.ts +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +interface PanelState { + // Left panel + leftPanelExpanded: boolean; + leftPanelWidth: number; + leftPanelSections: Record; // section collapsed state + + // Right panel + rightPanelOpen: boolean; + rightPanelWidth: number; + + // Actions + toggleLeftPanel: () => void; + setLeftPanelWidth: (width: number) => void; + toggleLeftSection: (sectionId: string) => void; + + toggleRightPanel: () => void; + setRightPanelWidth: (width: number) => void; + openRightPanel: () => void; + closeRightPanel: () => void; +} + +export const usePanelStore = create()( + persist( + (set) => ({ + // Defaults + leftPanelExpanded: true, + leftPanelWidth: 280, + leftPanelSections: { + history: true, + addActors: true, + relations: true, + layout: false, + view: false, + search: false, + }, + + rightPanelOpen: false, + rightPanelWidth: 320, + + // Actions + toggleLeftPanel: () => set((state) => ({ + leftPanelExpanded: !state.leftPanelExpanded + })), + + setLeftPanelWidth: (width) => set({ leftPanelWidth: width }), + + toggleLeftSection: (sectionId) => set((state) => ({ + leftPanelSections: { + ...state.leftPanelSections, + [sectionId]: !state.leftPanelSections[sectionId], + } + })), + + toggleRightPanel: () => set((state) => ({ + rightPanelOpen: !state.rightPanelOpen + })), + + setRightPanelWidth: (width) => set({ rightPanelWidth: width }), + + openRightPanel: () => set({ rightPanelOpen: true }), + closeRightPanel: () => set({ rightPanelOpen: false }), + }), + { + name: 'constellation-panels', // localStorage key + partialize: (state) => ({ + // Only persist these fields + leftPanelExpanded: state.leftPanelExpanded, + leftPanelWidth: state.leftPanelWidth, + leftPanelSections: state.leftPanelSections, + rightPanelWidth: state.rightPanelWidth, + // Don't persist rightPanelOpen (start closed) + }), + } + ) +); +``` + +#### Step 1.3: Implement Resize Hook +```typescript +// /src/components/Panels/hooks/useResizeHandle.ts +import { useCallback, useEffect, useRef, useState } from 'react'; + +interface UseResizeHandleOptions { + initialWidth: number; + minWidth: number; + maxWidth: number; + onResize: (width: number) => void; + direction: 'left' | 'right'; // Which side to resize from +} + +export function useResizeHandle({ + initialWidth, + minWidth, + maxWidth, + onResize, + direction, +}: UseResizeHandleOptions) { + const [isDragging, setIsDragging] = useState(false); + const [width, setWidth] = useState(initialWidth); + const startXRef = useRef(0); + const startWidthRef = useRef(initialWidth); + + const handleMouseDown = useCallback((e: React.MouseEvent) => { + setIsDragging(true); + startXRef.current = e.clientX; + startWidthRef.current = width; + document.body.style.cursor = 'col-resize'; + e.preventDefault(); + }, [width]); + + useEffect(() => { + if (!isDragging) return; + + const handleMouseMove = (e: MouseEvent) => { + const delta = direction === 'left' + ? e.clientX - startXRef.current + : startXRef.current - e.clientX; + + const newWidth = Math.min( + maxWidth, + Math.max(minWidth, startWidthRef.current + delta) + ); + + setWidth(newWidth); + onResize(newWidth); + }; + + const handleMouseUp = () => { + setIsDragging(false); + document.body.style.cursor = 'default'; + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, [isDragging, direction, minWidth, maxWidth, onResize]); + + return { + width, + isDragging, + handleMouseDown, + }; +} +``` + +### Phase 2: Left Panel Implementation (Week 2-3) + +#### Step 2.1: Build Collapsible Section Component +```typescript +// /src/components/Panels/LeftPanel/CollapsibleSection.tsx +import { ReactNode } from 'react'; +import { ChevronDownIcon, ChevronRightIcon } from '@mui/icons-material'; + +interface CollapsibleSectionProps { + id: string; + title: string; + badge?: string | number; + defaultExpanded?: boolean; + children: ReactNode; +} + +export function CollapsibleSection({ + id, + title, + badge, + children, +}: CollapsibleSectionProps) { + const { leftPanelSections, toggleLeftSection } = usePanelStore(); + const isExpanded = leftPanelSections[id]; + + return ( +
+
toggleLeftSection(id)} + > +
+ {isExpanded ? : } + {title} + {badge && {badge}} +
+
+ + {isExpanded && ( +
+ {children} +
+ )} +
+ ); +} +``` + +#### Step 2.2: Implement History Section +```typescript +// /src/components/Panels/LeftPanel/sections/HistorySection.tsx +import { CollapsibleSection } from '../CollapsibleSection'; +import { useDocumentHistory } from '@/hooks/useDocumentHistory'; +import { IconButton, Tooltip } from '@mui/material'; +import UndoIcon from '@mui/icons-material/Undo'; +import RedoIcon from '@mui/icons-material/Redo'; + +export function HistorySection() { + const { undo, redo, canUndo, canRedo, undoDescription, redoDescription } = + useDocumentHistory(); + + return ( + +
+ + + + + + + + + + + + + + + + + {undoDescription && ( + + {undoDescription} + + )} +
+
+ ); +} +``` + +#### Step 2.3: Implement Add Actors Section +```typescript +// /src/components/Panels/LeftPanel/sections/AddActorsSection.tsx +import { CollapsibleSection } from '../CollapsibleSection'; +import { useGraphWithHistory } from '@/hooks/useGraphWithHistory'; +import { createNode } from '@/utils/nodeUtils'; + +export function AddActorsSection() { + const { nodeTypes, addNode } = useGraphWithHistory(); + + const handleAddNode = (nodeTypeId: string) => { + const position = { + x: Math.random() * 400 + 100, + y: Math.random() * 300 + 100, + }; + + const nodeTypeConfig = nodeTypes.find((nt) => nt.id === nodeTypeId); + const newNode = createNode(nodeTypeId, position, nodeTypeConfig); + addNode(newNode); + }; + + return ( + +
+ {nodeTypes.map((nodeType) => ( + + ))} + + +
+
+ ); +} +``` + +### Phase 3: Right Panel Implementation (Week 3-4) + +#### Step 3.1: Build Right Panel Container with State Management +```typescript +// /src/components/Panels/RightPanel/RightPanel.tsx +import { useEffect } from 'react'; +import { usePanelStore } from '@/stores/panelStore'; +import { useGraphWithHistory } from '@/hooks/useGraphWithHistory'; +import { EmptyState } from './states/EmptyState'; +import { NodeProperties } from './states/NodeProperties'; +import { EdgeProperties } from './states/EdgeProperties'; +import { BulkNodeEdit } from './states/BulkNodeEdit'; +import { BulkEdgeEdit } from './states/BulkEdgeEdit'; +import { CloseIcon } from '@mui/icons-material'; + +export function RightPanel() { + const { rightPanelOpen, closeRightPanel, rightPanelWidth } = usePanelStore(); + const { selectedNodes, selectedEdges } = useGraphWithHistory(); + + // Determine which state to show + const getContent = () => { + if (selectedNodes.length === 1 && selectedEdges.length === 0) { + return ; + } + if (selectedEdges.length === 1 && selectedNodes.length === 0) { + return ; + } + if (selectedNodes.length > 1 && selectedEdges.length === 0) { + return ; + } + if (selectedEdges.length > 1 && selectedNodes.length === 0) { + return ; + } + return ; + }; + + if (!rightPanelOpen) return null; + + return ( +
+
+

PROPERTIES

+ +
+ +
+ {getContent()} +
+
+ ); +} +``` + +#### Step 3.2: Implement Node Properties State +```typescript +// /src/components/Panels/RightPanel/states/NodeProperties.tsx +import { useState, useEffect, useRef } from 'react'; +import { useGraphWithHistory } from '@/hooks/useGraphWithHistory'; +import type { Actor } from '@/types'; + +interface NodePropertiesProps { + node: Actor; +} + +export function NodeProperties({ node }: NodePropertiesProps) { + const { nodeTypes, updateNode, deleteNode } = useGraphWithHistory(); + const [label, setLabel] = useState(node.data?.label || ''); + const [type, setType] = useState(node.data?.type || ''); + const [description, setDescription] = useState(node.data?.description || ''); + const labelInputRef = useRef(null); + + // Auto-focus label input + useEffect(() => { + labelInputRef.current?.focus(); + labelInputRef.current?.select(); + }, [node.id]); + + // Live update on change (debounced for description) + const handleLabelChange = (value: string) => { + setLabel(value); + updateNode(node.id, { data: { ...node.data, label: value } }); + }; + + const handleTypeChange = (typeId: string) => { + setType(typeId); + updateNode(node.id, { data: { ...node.data, type: typeId } }); + }; + + const handleDescriptionChange = (value: string) => { + setDescription(value); + // Debounce this update + const timeoutId = setTimeout(() => { + updateNode(node.id, { data: { ...node.data, description: value } }); + }, 500); + return () => clearTimeout(timeoutId); + }; + + const currentType = nodeTypes.find(nt => nt.id === type); + + return ( +
+ {/* Label */} +
+ + handleLabelChange(e.target.value)} + placeholder="Enter actor name" + /> +
+ + {/* Type */} +
+ + + + {/* Color Preview */} + {currentType && ( +
+ Color Preview +
+ )} +
+ + {/* Description */} +
+ +