Files

342 lines
8.8 KiB
Markdown

# PREREQ-004: Canvas Highlighting API
## Overview
**Priority:** HIGH
**Estimate:** 1-2 days
**Status:** Not started
**Blocked by:** VIEW-000 (Foundation), PREREQ-003 (Overlay Pattern)
---
## The Goal
Create an API for **persistent, multi-channel highlighting** on the canvas.
This is the "Level 5000" feature that makes Data Lineage and Impact Radar legendary:
- Highlights persist until explicitly dismissed
- Multiple highlight channels can coexist (lineage = blue, impact = orange)
- Highlights persist across component navigation
---
## Current State
The canvas already has some highlighting:
- Nodes flash when they fire (debug visualization)
- Connections animate during data flow
- Selection highlighting exists
But these are:
- Temporary (fade after a few seconds)
- Single-purpose (can't have multiple types at once)
- Component-local (don't persist across navigation)
---
## Required API
```typescript
// canvasHighlight.ts
export interface CanvasHighlightAPI {
// Create highlights (returns handle to control them)
highlightNodes(
nodeIds: string[],
options: HighlightOptions
): HighlightHandle;
highlightConnections(
connections: ConnectionRef[],
options: HighlightOptions
): HighlightHandle;
highlightPath(
path: PathDefinition,
options: HighlightOptions
): HighlightHandle;
// Query
getActiveHighlights(): HighlightInfo[];
getHighlightsForChannel(channel: string): HighlightInfo[];
// Clear
clearChannel(channel: string): void;
clearAll(): void;
}
export interface HighlightOptions {
channel: string; // 'lineage', 'impact', 'selection', etc.
color?: string; // Override default channel color
style?: 'solid' | 'pulse' | 'glow';
persistent?: boolean; // Stay until dismissed (default: true)
label?: string; // Optional label near highlight
}
export interface HighlightHandle {
id: string;
channel: string;
// Control
update(nodeIds: string[]): void; // Change what's highlighted
setLabel(label: string): void;
dismiss(): void;
// Query
isActive(): boolean;
getNodeIds(): string[];
}
export interface PathDefinition {
nodes: string[]; // Ordered node IDs in the path
connections: ConnectionRef[]; // Connections between them
crossesComponents?: boolean;
componentBoundaries?: ComponentBoundary[];
}
export interface ConnectionRef {
fromNodeId: string;
fromPort: string;
toNodeId: string;
toPort: string;
}
export interface ComponentBoundary {
componentName: string;
entryNodeId?: string; // Component Input
exitNodeId?: string; // Component Output
}
```
---
## Channel System
Different highlight channels for different purposes:
```typescript
const HIGHLIGHT_CHANNELS = {
lineage: {
color: '#4A90D9', // Blue
style: 'glow',
description: 'Data lineage traces'
},
impact: {
color: '#F5A623', // Orange
style: 'pulse',
description: 'Impact/dependency highlights'
},
selection: {
color: '#FFFFFF', // White
style: 'solid',
description: 'Current selection'
},
warning: {
color: '#FF6B6B', // Red
style: 'pulse',
description: 'Issues or duplicates'
}
};
```
Channels can coexist - a node can have both lineage AND impact highlights.
---
## Persistence Across Navigation
**The key feature:** Highlights persist when you navigate to different components.
### Implementation Approach
```typescript
// Global highlight state (not per-component)
class HighlightManager {
private highlights: Map<string, HighlightState> = new Map();
// When component changes, update what's visible
onComponentChanged(newComponent: ComponentModel) {
this.highlights.forEach((state, id) => {
// Find which nodes in this highlight are in the new component
state.visibleNodes = state.allNodes.filter(
nodeId => this.nodeExistsInComponent(nodeId, newComponent)
);
// Update the visual highlighting
this.updateVisualHighlight(id);
});
}
}
```
### Cross-Component Indicators
When a highlight path crosses component boundaries:
```
┌─────────────────────────────────────────────────────────────┐
│ 🔗 Lineage: messageText │
│ │
│ ⬆️ Path continues in parent: App Shell [Go ↗] │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ REST /api/user → userData.name → String Format → ★ HERE │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ ⬇️ Path continues in child: TextField [Go ↗] │
└─────────────────────────────────────────────────────────────┘
```
---
## Rendering Highlights
### Option A: Canvas-Based (Recommended)
Modify the canvas paint loop to render highlights:
```typescript
// In NodeGraphEditor paint cycle
paintHighlights() {
const highlights = HighlightManager.getHighlightsForCurrentComponent();
highlights.forEach(highlight => {
highlight.visibleNodes.forEach(nodeId => {
const node = this.getNodeById(nodeId);
this.paintNodeHighlight(node, highlight.options);
});
highlight.visibleConnections.forEach(conn => {
this.paintConnectionHighlight(conn, highlight.options);
});
});
}
```
**Pros:** Native canvas rendering, performant
**Cons:** Requires modifying NodeGraphEditor paint loop
### Option B: Overlay-Based
Use the canvas overlay system (PREREQ-003) to render highlights as a layer:
```typescript
// HighlightOverlay.tsx
function HighlightOverlay({ highlights, viewport }) {
return (
<svg style={{ position: 'absolute', pointerEvents: 'none' }}>
{highlights.map(h => (
<HighlightPath key={h.id} path={h} viewport={viewport} />
))}
</svg>
);
}
```
**Pros:** Uses overlay infrastructure, easier to implement
**Cons:** May have z-order issues with canvas content
### Recommendation
Start with **Option B** (overlay-based) for faster implementation, then optimize to **Option A** if performance requires.
---
## Implementation Steps
### Phase 1: Core API (4-6 hours)
1. Create `canvasHighlight.ts` with TypeScript interfaces
2. Implement `HighlightManager` singleton
3. Implement channel system
4. Add highlight state storage
### Phase 2: Visual Rendering (4-6 hours)
1. Create `HighlightOverlay` component (using overlay pattern from PREREQ-003)
2. Implement node highlighting visuals
3. Implement connection highlighting visuals
4. Implement glow/pulse effects
### Phase 3: Persistence (2-4 hours)
1. Hook into component navigation
2. Update visible nodes on component change
3. Create boundary indicators UI
### Phase 4: Integration (2-4 hours)
1. Expose API to view components
2. Create hooks for easy use: `useHighlight()`
3. Test with sample data
---
## Files to Create
```
packages/noodl-editor/src/editor/src/
├── services/
│ └── HighlightManager/
│ ├── index.ts
│ ├── HighlightManager.ts
│ ├── types.ts
│ └── channels.ts
└── views/
└── CanvasOverlays/
└── HighlightOverlay/
├── index.ts
├── HighlightOverlay.tsx
├── NodeHighlight.tsx
├── ConnectionHighlight.tsx
└── BoundaryIndicator.tsx
```
---
## Usage Example
```typescript
// In Data Lineage view
function traceLineage(nodeId: string) {
const path = graphAnalysis.traceUpstream(nodeId);
const handle = highlightAPI.highlightPath(path, {
channel: 'lineage',
style: 'glow',
persistent: true,
label: `Lineage for ${nodeName}`
});
// Store handle to dismiss later
setActiveLineageHandle(handle);
}
// When user clicks dismiss
function dismissLineage() {
activeLineageHandle?.dismiss();
}
```
---
## Verification Checklist
- [ ] Can highlight individual nodes
- [ ] Can highlight connections
- [ ] Can highlight entire paths
- [ ] Multiple channels work simultaneously
- [ ] Highlights persist across component navigation
- [ ] Boundary indicators show correctly
- [ ] Dismiss button works
- [ ] Performance acceptable with many highlights
---
## Success Criteria
1. Highlighting API fully functional
2. Glow/pulse effects visually appealing
3. Persists across navigation
4. Multiple channels coexist
5. Easy to use from view components
6. Performance acceptable