mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 07:12:54 +01:00
473 lines
11 KiB
Markdown
473 lines
11 KiB
Markdown
# TASK-000I-A: Node Graph Visual Polish
|
|
|
|
**Parent Task:** TASK-009I Node Graph Visual Improvements
|
|
**Estimated Time:** 8-12 hours
|
|
**Risk Level:** Low
|
|
**Dependencies:** None
|
|
|
|
---
|
|
|
|
## Objective
|
|
|
|
Modernize the visual appearance of nodes on the canvas without changing functionality. This is a purely cosmetic update that improves the perceived quality and modernity of the editor.
|
|
|
|
---
|
|
|
|
## 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
|
|
|
|
### Out of Scope
|
|
|
|
- Node sizing changes
|
|
- Layout algorithm changes
|
|
- New functionality
|
|
- Port grouping (Sub-Task C)
|
|
|
|
---
|
|
|
|
## Implementation Phases
|
|
|
|
### Phase A1: Rounded Corners (2-3 hours)
|
|
|
|
#### Current Code
|
|
|
|
In `NodeGraphEditorNode.ts` paint() method:
|
|
|
|
```typescript
|
|
// Background - sharp corners
|
|
ctx.fillStyle = nc.header;
|
|
ctx.fillRect(x, y, this.nodeSize.width, this.nodeSize.height);
|
|
|
|
// Border - sharp corners
|
|
ctx.rect(x, y, this.nodeSize.width, this.nodeSize.height);
|
|
ctx.stroke();
|
|
```
|
|
|
|
#### New Approach
|
|
|
|
**Create helper file** `canvasHelpers.ts`:
|
|
|
|
```typescript
|
|
/**
|
|
* Draw a rounded rectangle path
|
|
* Uses native roundRect if available, falls back to arcTo
|
|
*/
|
|
export function roundRect(
|
|
ctx: CanvasRenderingContext2D,
|
|
x: number,
|
|
y: number,
|
|
width: number,
|
|
height: number,
|
|
radius: number | { tl: number; tr: number; br: number; bl: number }
|
|
): void {
|
|
const r = typeof radius === 'number' ? { tl: radius, tr: radius, br: radius, bl: radius } : radius;
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(x + r.tl, y);
|
|
ctx.lineTo(x + width - r.tr, y);
|
|
ctx.arcTo(x + width, y, x + width, y + r.tr, r.tr);
|
|
ctx.lineTo(x + width, y + height - r.br);
|
|
ctx.arcTo(x + width, y + height, x + width - r.br, y + height, r.br);
|
|
ctx.lineTo(x + r.bl, y + height);
|
|
ctx.arcTo(x, y + height, x, y + height - r.bl, r.bl);
|
|
ctx.lineTo(x, y + r.tl);
|
|
ctx.arcTo(x, y, x + r.tl, y, r.tl);
|
|
ctx.closePath();
|
|
}
|
|
|
|
/**
|
|
* Fill a rounded rectangle
|
|
*/
|
|
export function fillRoundRect(
|
|
ctx: CanvasRenderingContext2D,
|
|
x: number,
|
|
y: number,
|
|
width: number,
|
|
height: number,
|
|
radius: number
|
|
): void {
|
|
roundRect(ctx, x, y, width, height, radius);
|
|
ctx.fill();
|
|
}
|
|
|
|
/**
|
|
* Stroke a rounded rectangle
|
|
*/
|
|
export function strokeRoundRect(
|
|
ctx: CanvasRenderingContext2D,
|
|
x: number,
|
|
y: number,
|
|
width: number,
|
|
height: number,
|
|
radius: number
|
|
): void {
|
|
roundRect(ctx, x, y, width, height, radius);
|
|
ctx.stroke();
|
|
}
|
|
```
|
|
|
|
#### Changes to NodeGraphEditorNode.ts
|
|
|
|
```typescript
|
|
import { fillRoundRect, strokeRoundRect } from './canvasHelpers';
|
|
|
|
// Constants
|
|
const NODE_CORNER_RADIUS = 6;
|
|
|
|
// In paint() method:
|
|
|
|
// Background - replace fillRect
|
|
ctx.fillStyle = nc.header;
|
|
fillRoundRect(ctx, x, y, this.nodeSize.width, this.nodeSize.height, NODE_CORNER_RADIUS);
|
|
|
|
// Body area - need to clip to rounded shape
|
|
ctx.save();
|
|
roundRect(ctx, x, y, this.nodeSize.width, this.nodeSize.height, NODE_CORNER_RADIUS);
|
|
ctx.clip();
|
|
ctx.fillStyle = nc.base;
|
|
ctx.fillRect(x, y + titlebarHeight, this.nodeSize.width, this.nodeSize.height - titlebarHeight);
|
|
ctx.restore();
|
|
|
|
// Selection border
|
|
if (this.selected || this.borderHighlighted) {
|
|
ctx.strokeStyle = '#ffffff';
|
|
ctx.lineWidth = 2;
|
|
strokeRoundRect(ctx, x, y, this.nodeSize.width, this.nodeSize.height, NODE_CORNER_RADIUS);
|
|
}
|
|
|
|
// Error border
|
|
if (!health.healthy) {
|
|
ctx.setLineDash([5]);
|
|
ctx.strokeStyle = '#F57569';
|
|
strokeRoundRect(ctx, x - 1, y - 1, this.nodeSize.width + 2, this.nodeSize.height + 2, NODE_CORNER_RADIUS + 1);
|
|
ctx.setLineDash([]);
|
|
}
|
|
```
|
|
|
|
#### Locations to Update
|
|
|
|
1. **Node background** (~line 220)
|
|
2. **Node body fill** (~line 230)
|
|
3. **Highlight overlay** (~line 240)
|
|
4. **Selection border** (~line 290)
|
|
5. **Error/unhealthy border** (~line 280)
|
|
6. **Annotation borders** (~line 300)
|
|
|
|
#### Testing
|
|
|
|
- [ ] Nodes render with rounded corners at 100% zoom
|
|
- [ ] Corners visible at 50% zoom
|
|
- [ ] Corners not distorted at 150% zoom
|
|
- [ ] Selection highlight follows rounded shape
|
|
- [ ] Error dashed border follows rounded shape
|
|
- [ ] No visual artifacts at corner intersections
|
|
|
|
---
|
|
|
|
### Phase A2: Color Palette Update (2-3 hours)
|
|
|
|
#### File to Modify
|
|
|
|
`packages/noodl-core-ui/src/styles/custom-properties/colors.css`
|
|
|
|
#### Current vs Proposed
|
|
|
|
Document current values first, then update:
|
|
|
|
```css
|
|
/* ===== NODE COLORS ===== */
|
|
|
|
/* Data nodes - Green */
|
|
/* Current: muted olive */
|
|
/* Proposed: richer emerald */
|
|
--base-color-node-green-900: #052e16;
|
|
--base-color-node-green-700: #166534;
|
|
--base-color-node-green-600: #16a34a;
|
|
--base-color-node-green-500: #22c55e;
|
|
|
|
/* Visual nodes - Blue */
|
|
/* Current: muted blue */
|
|
/* Proposed: cleaner slate */
|
|
--base-color-node-blue-900: #0f172a;
|
|
--base-color-node-blue-700: #334155;
|
|
--base-color-node-blue-600: #475569;
|
|
--base-color-node-blue-500: #64748b;
|
|
--base-color-node-blue-400: #94a3b8;
|
|
--base-color-node-blue-300: #cbd5e1;
|
|
--base-color-node-blue-200: #e2e8f0;
|
|
|
|
/* Logic nodes - Grey */
|
|
/* Current: flat grey */
|
|
/* Proposed: warmer zinc */
|
|
--base-color-node-grey-900: #18181b;
|
|
--base-color-node-grey-700: #3f3f46;
|
|
--base-color-node-grey-600: #52525b;
|
|
|
|
/* Custom nodes - Pink */
|
|
/* Current: magenta */
|
|
/* Proposed: refined rose */
|
|
--base-color-node-pink-900: #4c0519;
|
|
--base-color-node-pink-700: #be123c;
|
|
--base-color-node-pink-600: #e11d48;
|
|
|
|
/* Component nodes - Purple */
|
|
/* Current: muted purple */
|
|
/* Proposed: cleaner violet */
|
|
--base-color-node-purple-900: #2e1065;
|
|
--base-color-node-purple-700: #6d28d9;
|
|
--base-color-node-purple-600: #7c3aed;
|
|
```
|
|
|
|
#### Process
|
|
|
|
1. **Document current** - Screenshot and hex values
|
|
2. **Design new palette** - Use design system principles
|
|
3. **Update CSS variables** - One category at a time
|
|
4. **Test contrast** - WCAG AA minimum (4.5:1 for text)
|
|
5. **Visual review** - Check all node types
|
|
|
|
#### Contrast Checking
|
|
|
|
Use browser dev tools or online checker:
|
|
|
|
- Header text on header background
|
|
- Port labels on body background
|
|
- Selection highlight visibility
|
|
|
|
#### Testing
|
|
|
|
- [ ] Data nodes (green) - legible, modern
|
|
- [ ] Visual nodes (blue) - legible, modern
|
|
- [ ] Logic nodes (grey) - legible, modern
|
|
- [ ] Custom nodes (pink) - legible, modern
|
|
- [ ] Component nodes (purple) - legible, modern
|
|
- [ ] All text passes contrast check
|
|
- [ ] Colors distinguish node types clearly
|
|
|
|
---
|
|
|
|
### Phase A3: Connection Point Styling (2-3 hours)
|
|
|
|
#### Current Implementation
|
|
|
|
In `NodeGraphEditorNode.ts` drawPlugs():
|
|
|
|
```typescript
|
|
function dot(side, color) {
|
|
ctx.fillStyle = color;
|
|
ctx.beginPath();
|
|
ctx.arc(x + (side === 'left' ? 0 : _this.nodeSize.width), ty, 4, 0, 2 * Math.PI, false);
|
|
ctx.fill();
|
|
}
|
|
|
|
function arrow(side, color) {
|
|
const dx = side === 'left' ? 4 : -4;
|
|
const cx = x + (side === 'left' ? 0 : _this.nodeSize.width);
|
|
ctx.fillStyle = color;
|
|
ctx.beginPath();
|
|
ctx.moveTo(cx - dx, ty - 4);
|
|
ctx.lineTo(cx + dx, ty);
|
|
ctx.lineTo(cx - dx, ty + 4);
|
|
ctx.fill();
|
|
}
|
|
```
|
|
|
|
#### Improvements
|
|
|
|
```typescript
|
|
const PORT_RADIUS = 5; // Increased from 4
|
|
const PORT_INNER_RADIUS = 2;
|
|
|
|
function drawPort(side: 'left' | 'right', type: 'dot' | 'arrow', color: string, connected: boolean) {
|
|
const cx = x + (side === 'left' ? 0 : _this.nodeSize.width);
|
|
|
|
ctx.save();
|
|
|
|
if (type === 'dot') {
|
|
// Outer circle
|
|
ctx.fillStyle = color;
|
|
ctx.beginPath();
|
|
ctx.arc(cx, ty, PORT_RADIUS, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
|
|
// Inner highlight (connected state)
|
|
if (connected) {
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
|
ctx.beginPath();
|
|
ctx.arc(cx, ty, PORT_INNER_RADIUS, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
}
|
|
} else {
|
|
// Arrow (signal)
|
|
const dx = side === 'left' ? PORT_RADIUS : -PORT_RADIUS;
|
|
ctx.fillStyle = color;
|
|
ctx.beginPath();
|
|
ctx.moveTo(cx - dx, ty - PORT_RADIUS);
|
|
ctx.lineTo(cx + dx, ty);
|
|
ctx.lineTo(cx - dx, ty + PORT_RADIUS);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
```
|
|
|
|
#### Testing
|
|
|
|
- [ ] Port dots larger and easier to click
|
|
- [ ] Connected ports have visual distinction
|
|
- [ ] Arrows properly sized
|
|
- [ ] Hit detection still works
|
|
- [ ] Dragging connections works
|
|
- [ ] Hover states visible
|
|
|
|
---
|
|
|
|
### Phase A4: Port Label Truncation (2-3 hours)
|
|
|
|
#### Problem
|
|
|
|
Long port names overflow the node boundary, appearing outside the node rectangle.
|
|
|
|
#### Solution
|
|
|
|
**Add to canvasHelpers.ts:**
|
|
|
|
```typescript
|
|
/**
|
|
* Truncate text to fit within maxWidth, adding ellipsis if needed
|
|
*/
|
|
export function truncateText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number): string {
|
|
if (ctx.measureText(text).width <= maxWidth) {
|
|
return text;
|
|
}
|
|
|
|
const ellipsis = '…';
|
|
let truncated = text;
|
|
|
|
while (truncated.length > 0) {
|
|
truncated = truncated.slice(0, -1);
|
|
if (ctx.measureText(truncated + ellipsis).width <= maxWidth) {
|
|
return truncated + ellipsis;
|
|
}
|
|
}
|
|
|
|
return ellipsis;
|
|
}
|
|
```
|
|
|
|
#### Integration in drawPlugs()
|
|
|
|
```typescript
|
|
// Calculate available width for label
|
|
const labelMaxWidth =
|
|
side === 'left'
|
|
? _this.nodeSize.width / 2 - horizontalSpacing - PORT_RADIUS
|
|
: _this.nodeSize.width / 2 - horizontalSpacing - PORT_RADIUS;
|
|
|
|
// Truncate if needed
|
|
const displayName = truncateText(ctx, p.displayName || p.property, labelMaxWidth);
|
|
ctx.fillText(displayName, tx, ty);
|
|
|
|
// Store full name for tooltip
|
|
p.fullDisplayName = p.displayName || p.property;
|
|
```
|
|
|
|
#### Tooltip Integration
|
|
|
|
Verify existing tooltip system shows full port name on hover. If not working:
|
|
|
|
```typescript
|
|
// In handleMouseEvent, on port hover:
|
|
if (p.fullDisplayName !== displayName) {
|
|
PopupLayer.instance.showTooltip({
|
|
content: p.fullDisplayName,
|
|
position: { x: mouseX, y: mouseY }
|
|
});
|
|
}
|
|
```
|
|
|
|
#### Testing
|
|
|
|
- [ ] Long labels truncate with ellipsis
|
|
- [ ] Short labels unchanged
|
|
- [ ] Truncation respects node width
|
|
- [ ] Tooltip shows full name on hover
|
|
- [ ] Left and right aligned labels both work
|
|
- [ ] No text overflow outside node bounds
|
|
|
|
---
|
|
|
|
## Files to Create
|
|
|
|
```
|
|
packages/noodl-editor/src/editor/src/views/nodegrapheditor/
|
|
└── canvasHelpers.ts # Utility functions
|
|
```
|
|
|
|
## Files to Modify
|
|
|
|
```
|
|
packages/noodl-editor/src/editor/src/views/nodegrapheditor/
|
|
└── NodeGraphEditorNode.ts # Main rendering changes
|
|
|
|
packages/noodl-core-ui/src/styles/custom-properties/
|
|
└── colors.css # Color palette updates
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Checklist
|
|
|
|
### Visual Verification
|
|
|
|
- [ ] Open existing project with many node types
|
|
- [ ] All nodes render with rounded corners
|
|
- [ ] Colors updated and consistent
|
|
- [ ] Port indicators refined
|
|
- [ ] Labels truncate properly
|
|
|
|
### Functional Verification
|
|
|
|
- [ ] Node selection works
|
|
- [ ] Connection dragging works
|
|
- [ ] Copy/paste works
|
|
- [ ] Undo/redo works
|
|
- [ ] Zoom in/out renders correctly
|
|
|
|
### Performance
|
|
|
|
- [ ] No noticeable slowdown
|
|
- [ ] Smooth panning with 50+ nodes
|
|
- [ ] Profile render time if concerned
|
|
|
|
---
|
|
|
|
## Success Criteria
|
|
|
|
- [ ] All nodes have rounded corners (6px radius)
|
|
- [ ] Color palette modernized
|
|
- [ ] Port indicators larger and cleaner
|
|
- [ ] Long labels truncate with ellipsis
|
|
- [ ] Full port name visible on hover
|
|
- [ ] No visual regressions
|
|
- [ ] No functional regressions
|
|
- [ ] Performance unchanged
|
|
|
|
---
|
|
|
|
## Rollback Plan
|
|
|
|
If issues arise:
|
|
|
|
1. Revert `NodeGraphEditorNode.ts` changes
|
|
2. Revert `colors.css` changes
|
|
3. Delete `canvasHelpers.ts`
|
|
|
|
All changes are isolated to rendering code with no data model changes.
|