# TASK-000I: Node Graph Visual Improvements ## Overview Modernize the visual appearance of the node graph canvas, add a node comments system, and improve port label handling. This is a high-impact visual refresh that maintains backward compatibility while significantly improving the user experience for complex node graphs. **Phase:** 3 (Visual Improvements) **Priority:** High **Estimated Time:** 35-50 hours total **Risk Level:** Low-Medium --- ## Background The node graph is the heart of OpenNoodl's visual programming experience. While functionally solid, the current visual design shows its age: - Nodes have sharp corners and flat colors that feel dated - No way to attach documentation/comments to individual nodes - Port labels overflow on nodes with many connections - Dense nodes (Object, State, Function) become hard to read This task addresses these pain points through three sub-tasks that can be implemented incrementally. ### Current Architecture The node graph uses a **hybrid rendering approach**: 1. **HTML5 Canvas** (`NodeGraphEditorNode.ts`) - Renders: - Node backgrounds via `ctx.fillRect()` - Borders via `ctx.rect()` and `ctx.strokeRect()` - Port indicators (dots/arrows) via `ctx.arc()` and triangle paths - Connection lines via bezier curves - Text labels via `ctx.fillText()` 2. **DOM Layer** (`domElementContainer`) - Renders: - Comment layer (existing, React-based) - Some overlays and tooltips 3. **Color System** - Node colors come from: - `NodeLibrary.instance.colorSchemeForNodeType()` - Maps to CSS variables in `colors.css` - Already abstracted - we can update colors without touching Canvas code ### Key Files ``` packages/noodl-editor/src/editor/src/views/ ├── nodegrapheditor.ts # Main editor, paint loop ├── nodegrapheditor/ │ ├── NodeGraphEditorNode.ts # Node rendering (PRIMARY TARGET) │ ├── NodeGraphEditorConnection.ts # Connection line rendering │ └── ... ├── commentlayer.ts # Existing comment system packages/noodl-core-ui/src/styles/custom-properties/ ├── colors.css # Design tokens (color updates) packages/noodl-editor/src/editor/src/models/ ├── nodegraphmodel/NodeGraphNode.ts # Node data model (metadata storage) ├── nodelibrary/ # Node type definitions, port groups ``` --- ## Sub-Tasks ### Sub-Task A: Visual Polish (8-12 hours) Modernize node appearance without changing functionality. ### Sub-Task B: Node Comments System (12-18 hours) Add ability to attach documentation to individual nodes. ### Sub-Task C: Port Organization & Smart Connections (15-20 hours) Improve port label handling and add connection preview on hover. --- ## Sub-Task A: Visual Polish ### Scope 1. **Rounded corners** on all node rectangles 2. **Updated color palette** following design system 3. **Refined connection points** (port dots/arrows) 4. **Port label truncation** with ellipsis for overflow ### Implementation #### A1: Rounded Corners (2-3 hours) **Current code** in `NodeGraphEditorNode.ts`: ```typescript // Background ctx.fillRect(x, y, this.nodeSize.width, this.nodeSize.height); // Border ctx.rect(x, y, this.nodeSize.width, this.nodeSize.height); ``` **New approach** - Create helper function: ```typescript function roundRect(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number) { ctx.beginPath(); ctx.roundRect(x, y, width, height, radius); // Native Canvas API ctx.closePath(); } ``` **Apply to:** - Node background fill - Node border stroke - Selection highlight - Error/annotation borders - Title bar area (top corners only, or clip) **Radius recommendation:** 6-8px for nodes, 4px for smaller elements #### A2: Color Palette Update (2-3 hours) Update CSS variables in `colors.css` to use more modern, saturated colors while maintaining the existing semantic meanings: | Node Type | Current | Proposed Direction | | ------------------ | ------------ | -------------------------------- | | Data (green) | Olive/muted | Richer emerald green | | Visual (blue) | Muted blue | Cleaner slate blue | | Logic (grey) | Flat grey | Warmer charcoal with subtle tint | | Custom (pink) | Magenta-pink | Refined rose/coral | | Component (purple) | Muted purple | Cleaner violet | **Also update:** - `--theme-color-signal` (connection lines) - `--theme-color-data` (connection lines) - Background contrast between header and body **Constraint:** Keep changes within design system tokens, ensure sufficient contrast. #### A3: Connection Point Styling (2-3 hours) Current port indicators are simple: - **Dots** (`ctx.arc`) for data sources - **Triangles** (manual path) for signals/targets **Improvements:** - Slightly larger hit areas (currently 4px radius) - Subtle inner highlight or ring effect - Smoother anti-aliasing - Consider pill-shaped indicators for "connected" state **Files:** `NodeGraphEditorNode.ts` - `drawPlugs()` function #### A4: Port Label Truncation (2-3 hours) **Problem:** Long port names overflow the node boundary. **Solution:** ```typescript function truncateText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number): string { const ellipsis = '…'; let truncated = text; while (ctx.measureText(truncated + ellipsis).width > maxWidth && truncated.length > 0) { truncated = truncated.slice(0, -1); } return truncated.length < text.length ? truncated + ellipsis : text; } ``` **Apply in** `drawPlugs()` before `ctx.fillText()`. **Tooltip:** Full port name should show on hover (existing tooltip system). ### Success Criteria - Sub-Task A - [ ] All nodes render with rounded corners (radius configurable) - [ ] Color palette updated, passes contrast checks - [ ] Connection points are visually refined - [ ] Long port labels truncate with ellipsis - [ ] Full port name visible on hover - [ ] No visual regressions in existing projects - [ ] Performance unchanged (canvas render time) --- ## Sub-Task B: Node Comments System ### Scope Allow users to attach plain-text comments to any node, with: - Small indicator icon when comment exists - Hover preview (debounced to avoid bombardment) - Click to open edit modal - Comments persist with project ### Design Decisions **Storage:** `node.metadata.comment: string` - Already have `metadata` object on NodeGraphNode - Persists with project JSON - No schema changes needed **UI Pattern:** Icon + Hover Preview + Modal - Comment icon in title bar (only shows if comment exists OR on hover) - Hover over icon shows preview tooltip (300ms delay) - Click opens sticky modal for editing - Modal can be dragged, stays open while working **Why not inline expansion?** - Would affect node measurement/layout calculations - Creates cascade effects on connections - More invasive to existing code ### Implementation #### B1: Data Layer (1-2 hours) **Add to `NodeGraphNode.ts`:** ```typescript // In metadata interface interface NodeMetadata { // ... existing fields comment?: string; } // Helper methods getComment(): string | undefined { return this.metadata?.comment; } setComment(comment: string | undefined, args?: { undo?: boolean }) { if (!this.metadata) this.metadata = {}; const oldComment = this.metadata.comment; this.metadata.comment = comment || undefined; // Remove if empty this.notifyListeners('commentChanged', { comment }); if (args?.undo) { UndoQueue.instance.push({ label: 'Edit comment', do: () => this.setComment(comment), undo: () => this.setComment(oldComment) }); } } hasComment(): boolean { return !!this.metadata?.comment?.trim(); } ``` #### B2: Comment Icon Rendering (2-3 hours) **In `NodeGraphEditorNode.ts` paint function:** ```typescript // After drawing title, before drawing ports if (this.model.hasComment() || this.isHovered) { this.drawCommentIcon(ctx, x, y, titlebarHeight); } private drawCommentIcon( ctx: CanvasRenderingContext2D, x: number, y: number, titlebarHeight: number ) { const iconX = x + this.nodeSize.width - 24; // Right side of title const iconY = y + titlebarHeight / 2; const hasComment = this.model.hasComment(); ctx.save(); ctx.globalAlpha = hasComment ? 1 : 0.4; ctx.fillStyle = hasComment ? '#ffffff' : nc.text; // Draw speech bubble icon (simple path or loaded SVG) // ... icon drawing code ctx.restore(); // Store hit area for click detection this.commentIconBounds = { x: iconX - 8, y: iconY - 8, width: 16, height: 16 }; } ``` #### B3: Hover Preview (3-4 hours) **Requirements:** - 300ms delay before showing (avoid bombardment on pan/scroll) - Cancel if mouse leaves before delay - Position near node but not obscuring it - Max width ~250px, max height ~150px with scroll **Implementation approach:** - Track mouse position in `NodeGraphEditorNode.handleMouseEvent` - Use `setTimeout` with cleanup for debounce - Render preview using existing `PopupLayer.showTooltip()` or custom ```typescript // In handleMouseEvent, on 'move-in' to comment icon area: this.commentPreviewTimer = setTimeout(() => { if (this.model.hasComment()) { PopupLayer.instance.showTooltip({ content: this.model.getComment(), position: { x: iconX, y: iconY + 20 }, maxWidth: 250 }); } }, 300); // On 'move-out': clearTimeout(this.commentPreviewTimer); PopupLayer.instance.hideTooltip(); ``` #### B4: Edit Modal (4-6 hours) **Create new component:** `NodeCommentEditor.tsx` ```typescript interface NodeCommentEditorProps { node: NodeGraphNode; initialPosition: { x: number; y: number }; onClose: () => void; } export function NodeCommentEditor({ node, initialPosition, onClose }: NodeCommentEditorProps) { const [comment, setComment] = useState(node.getComment() || ''); const [position, setPosition] = useState(initialPosition); const handleSave = () => { node.setComment(comment.trim() || undefined, { undo: true }); onClose(); }; return (
Comment: {node.label}