# 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 */}