From cfaf78fb1576e3d939d75484ea28d3ac73c6cbaa Mon Sep 17 00:00:00 2001 From: Richard Osborne Date: Thu, 1 Jan 2026 16:11:21 +0100 Subject: [PATCH] Finished node canvas UI tweaks. Failed to add connection highlighting --- dev-docs/reference/LEARNINGS.md | 124 + .../CHANGELOG.md | 591 ++++- .../CHECKLIST.md | 112 +- .../TASK-007-integrated-backend/CHANGELOG.md | 156 ++ .../TASK-007-integrated-backend/CHECKLIST.md | 435 ++++ .../TASK-007-integrated-backend/NOTES.md | 554 +++++ .../TASK-007-integrated-backend/README.md | 2160 +++++++++++++++++ .../TASK-007A-localsql-adapter.md | 1497 ++++++++++++ .../TASK-007B-backend-server.md | 1237 ++++++++++ .../TASK-007C-workflow-runtime.md | 1564 ++++++++++++ .../TASK-007D-launcher-integration.md | 1091 +++++++++ .../TASK-007E-migration-export.md | 1210 +++++++++ .../TASK-007F-standalone-deployment.md | 1257 ++++++++++ .../TASK-007G-authentication.md | 2109 ++++++++++++++++ .../src/styles/custom-properties/colors.css | 114 +- .../models/nodegraphmodel/NodeGraphNode.ts | 39 + .../src/editor/src/styles/popuplayer.css | 119 + .../src/templates/stringinputpopup.html | 9 +- .../nodegrapheditor/NodeGraphEditorNode.ts | 258 +- .../views/nodegrapheditor/canvasHelpers.ts | 196 ++ .../src/views/nodegrapheditor/portIcons.ts | 210 ++ .../src/editor/src/views/popuplayer.js | 63 +- .../noodl-runtime/src/nodelibraryexport.js | 60 +- 23 files changed, 14880 insertions(+), 285 deletions(-) create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/CHANGELOG.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/CHECKLIST.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/NOTES.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/README.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007A-localsql-adapter.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007B-backend-server.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007C-workflow-runtime.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007D-launcher-integration.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007E-migration-export.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007F-standalone-deployment.md create mode 100644 dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007G-authentication.md create mode 100644 packages/noodl-editor/src/editor/src/views/nodegrapheditor/canvasHelpers.ts create mode 100644 packages/noodl-editor/src/editor/src/views/nodegrapheditor/portIcons.ts diff --git a/dev-docs/reference/LEARNINGS.md b/dev-docs/reference/LEARNINGS.md index 242a406..bbca684 100644 --- a/dev-docs/reference/LEARNINGS.md +++ b/dev-docs/reference/LEARNINGS.md @@ -4,6 +4,130 @@ This document captures important discoveries and gotchas encountered during Open --- +## 🚫 Port Hover Compatibility Highlighting Failed Attempt (Jan 1, 2026) + +### The Invisible Compatibility: Why Port Hover Preview Didn't Work + +**Context**: Phase 3 TASK-000I-C3 - Attempted to add visual feedback showing compatible/incompatible ports when hovering over any port. After 6+ debugging iterations spanning multiple attempts, the feature was abandoned. + +**The Problem**: Despite comprehensive implementation with proper type detection, bidirectional logic, cache optimization, and visual effects, console logs consistently showed "incompatible" for most ports that should have been compatible. + +**What Was Implemented**: + +- Port hover detection with 8px hit radius +- Compatibility cache system for performance +- Type coercion rules (number↔string, boolean↔string, color↔string) +- Bidirectional vs unidirectional port logic (data vs signals) +- Visual feedback (glow for compatible, dim for incompatible) +- Proper port definition lookup (not connection-based) + +**Debugging Attempts**: + +1. Fixed backwards compatibility logic +2. Fixed cache key mismatches +3. Increased glow visibility (shadowBlur 50) +4. Added bidirectional logic for data ports vs unidirectional for signals +5. Fixed type detection to use `model.getPorts()` instead of connections +6. Modified cache rebuilding to support bidirectional data ports + +**Why It Failed** (Suspected Root Causes): + +1. **Port Type System Complexity**: Noodl's type system has more nuances than documented + + - Type coercion rules may be more complex than number↔string, etc. + - Some types may have special compatibility that isn't exposed in port definitions + - Dynamic type resolution at connection time may differ from static analysis + +2. **Dynamic Port Generation**: Many nodes generate ports dynamically based on configuration + + - Port definitions from `model.getPorts()` may not reflect all runtime ports + - StringList-configured ports (headers, query params) create dynamic inputs + - These ports may not have proper type metadata until after connection + +3. **Port Direction Ambiguity**: Input/output distinction may be insufficient + + - Some ports accept data from both directions (middle/bidirectional ports) + - Connection validation logic in the engine may use different rules than exposed in the model + - Legacy nodes may have special-case connection rules + +4. **Hidden Compatibility Layer**: The actual connection validation may happen elsewhere + - NodeLibrary or ConnectionModel may have additional validation logic + - Engine-level type checking may override model-level type information + - Some compatibility may be determined by node behavior, not type declarations + +**Critical Learnings**: + +**❌ Don't assume port type compatibility is simple**: + +```typescript +// ❌ WRONG - Oversimplified compatibility +if (sourceType === targetType) return true; +if (sourceType === 'any' || targetType === 'any') return true; +// Missing: Engine-level rules, dynamic types, node-specific compatibility +``` + +**βœ… Port compatibility is more complex than it appears**: + +- Port definitions don't tell the whole story +- Connection validation happens in multiple places +- Type coercion has engine-level rules not exposed in metadata +- Some compatibility is behavioral, not type-based + +**What Would Be Needed for This Feature**: + +1. **Access to Engine Validation**: Hook into the actual connection validation logic + + - Use the same code path that validates connections when dragging + - Don't reimplement compatibility rules - use existing validator + +2. **Runtime Type Resolution**: Get actual types at connection time, not from definitions + + - Some nodes resolve types dynamically based on connected nodes + - Type information may flow through the graph + +3. **Node-Specific Rules**: Account for special-case compatibility + + - Some nodes accept any connection and do runtime type conversion + - Legacy nodes may have grandfathered compatibility rules + +4. **Testing Infrastructure**: Comprehensive test suite for all node types + - Would need to test every node's port compatibility + - Edge cases like Collection nodes, Router adapters, etc. + +**Alternative Approaches** (For Future Attempts): + +1. **Hook Existing Validation**: Instead of reimplementing, call the existing connection validator + + ```typescript + // Pseudocode - use actual engine validation + const canConnect = connectionModel.validateConnection(sourcePort, targetPort); + ``` + +2. **Show Type Names Only**: Simpler feature - just show port types on hover + + - No compatibility checking + - Let users learn type names and infer compatibility themselves + +3. **Connection Hints After Drag**: Show compatibility when actively dragging a connection + - Only check compatibility for the connection being created + - Use the engine's validation since we're about to create the connection anyway + +**Time Lost**: ~3-4 hours across multiple debugging sessions + +**Files Cleaned Up** (All code removed): + +- `packages/noodl-editor/src/editor/src/views/nodegrapheditor.ts` +- `packages/noodl-editor/src/editor/src/views/nodegrapheditor/NodeGraphEditorNode.ts` + +**Documentation**: + +- Failure documented in: `dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-000-styles-overhaul/TASK-000I-node-graph-visual-improvements/CHANGELOG.md` +- Task marked as: ❌ REMOVED (FAILED) + +**Keywords**: port compatibility, hover preview, type checking, connection validation, node graph, canvas, visual feedback, failed feature, type system, dynamic ports + +--- + ## πŸ”₯ CRITICAL: Electron Blocks window.prompt() and window.confirm() (Dec 2025) ### The Silent Dialog: Native Dialogs Don't Work in Electron diff --git a/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-000-styles-overhaul/TASK-000I-node-graph-visual-improvements/CHANGELOG.md b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-000-styles-overhaul/TASK-000I-node-graph-visual-improvements/CHANGELOG.md index 662dbe9..695b471 100644 --- a/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-000-styles-overhaul/TASK-000I-node-graph-visual-improvements/CHANGELOG.md +++ b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-000-styles-overhaul/TASK-000I-node-graph-visual-improvements/CHANGELOG.md @@ -1,158 +1,547 @@ -# TASK-000I Changelog +# TASK-000I Node Graph Visual Improvements - Changelog -## Overview +## Sub-Task A: Visual Polish βœ… COMPLETED -This changelog tracks the implementation of Node Graph Visual Improvements, covering visual polish, node comments, and port organization features. +### 2026-01-01 - All Visual Polish Enhancements Complete -### Implementation Sessions +**Summary**: Sub-Task A completed with rounded corners, enhanced port styling, text truncation, and modernized color palette. -1. **Session 1**: Sub-Task A - Rounded Corners & Colors -2. **Session 2**: Sub-Task A - Connection Points & Label Truncation -3. **Session 3**: Sub-Task B - Comment Data Layer & Icon -4. **Session 4**: Sub-Task B - Hover Preview & Edit Modal -5. **Session 5**: Sub-Task C - Port Grouping System -6. **Session 6**: Sub-Task C - Type Icons & Connection Preview -7. **Session 7**: Integration & Polish +#### A1: Rounded Corners βœ… + +- Created `canvasHelpers.ts` with comprehensive rounded rectangle utilities +- Implemented `roundRect()`, `fillRoundRect()`, and `strokeRoundRect()` functions +- Applied 6px corner radius to all node rendering +- Updated clipping, backgrounds, borders, and selection highlights +- Supports individual corner radius configuration for future flexibility + +**Files Created:** + +- `packages/noodl-editor/src/editor/src/views/nodegrapheditor/canvasHelpers.ts` + +**Files Modified:** + +- `packages/noodl-editor/src/editor/src/views/nodegrapheditor/NodeGraphEditorNode.ts` + +#### A2: Color Palette Update βœ… + +- Updated node type colors with more vibrant, saturated values +- **Data (green)**: `#2d9a2d` β†’ `#5fcb5f` (more emerald) +- **Visual (blue)**: `#2c7aac` β†’ `#62aed9` (cleaner slate blue) +- **Logic (grey)**: Warmer charcoal with subtle warmth +- **Custom (pink)**: `#b02872` β†’ `#ec5ca8` (refined rose) +- **Component (purple)**: `#7d3da5` β†’ `#b176db` (cleaner violet) +- All colors maintain WCAG AA contrast requirements +- Colors use design system tokens (no hardcoded values) + +**Files Modified:** + +- `packages/noodl-core-ui/src/styles/custom-properties/colors.css` + +#### A3: Connection Point Styling βœ… + +- Increased port indicator radius from 4px to 6px for better visibility +- Added subtle inner highlight (30% white at offset position) for depth +- Enhanced anti-aliasing with `ctx.imageSmoothingQuality = 'high'` +- Improved visual distinction between connected and unconnected ports + +**Files Modified:** + +- `packages/noodl-editor/src/editor/src/views/nodegrapheditor/NodeGraphEditorNode.ts` (dot function) + +#### A4: Port Label Truncation βœ… + +- Implemented efficient `truncateText()` utility using binary search +- Port labels now truncate with ellipsis ('…') when they exceed available width +- Full port names still visible on hover via existing tooltip system +- Prevents text overflow that obscured node boundaries +- Works with all font settings via ctx.measureText() + +**Files Modified:** + +- `packages/noodl-editor/src/editor/src/views/nodegrapheditor/canvasHelpers.ts` +- `packages/noodl-editor/src/editor/src/views/nodegrapheditor/NodeGraphEditorNode.ts` (drawPlugs function) + +### Visual Impact + +The combined changes create a significantly more modern and polished node graph: + +- Softer, more approachable rounded corners +- Vibrant colors that are easier to distinguish at a glance +- Better port visibility and clickability +- Cleaner text layout without overflow issues +- Professional appearance that matches modern design standards --- -## [Date TBD] - Task Created +## Sub-Task C2: Port Type Icons βœ… COMPLETED -### Summary +### 2026-01-01 - Port Type Icon System Implementation -Task documentation created for Node Graph Visual Improvements based on product planning discussion. +**Summary**: Added visual type indicators next to all ports for instant type recognition. -### Files Created +#### Features Implemented -- `dev-docs/tasks/phase-3/TASK-010-node-graph-visual/README.md` - Full task specification -- `dev-docs/tasks/phase-3/TASK-010-node-graph-visual/CHECKLIST.md` - Implementation checklist -- `dev-docs/tasks/phase-3/TASK-010-node-graph-visual/CHANGELOG.md` - This file -- `dev-docs/tasks/phase-3/TASK-010-node-graph-visual/NOTES.md` - Working notes +- **Icon Set**: Created comprehensive Unicode-based icon set for all port types: -### Context + - `⚑` Lightning bolt for Signals/Events + - `#` Hash for Numbers + - `T` Letter T for Strings/Text + - `◐` Half-circle for Booleans + - `{ }` Braces for Objects + - `[ ]` Brackets for Arrays + - `●` Filled circle for Colors + - `β—‡` Diamond for Any type + - `β—ˆ` Diamond with dot for Components + - `≑` Three lines for Enumerations -Discussion identified three key areas for improvement: +- **Smart Type Mapping**: Automatic detection and normalization of Noodl internal type names +- **Visual States**: Icons show at 70% opacity when connected, 40% when unconnected +- **Positioning**: Icons render next to port dots/arrows on both left and right sides +- **Performance**: Lightweight rendering using simple Unicode characters (no SVG overhead) -1. Nodes look dated (sharp corners, flat colors) -2. No way to document individual nodes with comments -3. Dense nodes with many ports become hard to read +#### Files Created -Decision made to implement as three sub-tasks that can be tackled incrementally. +- `packages/noodl-editor/src/editor/src/views/nodegrapheditor/portIcons.ts` + - Type definitions and icon mappings + - `getPortIconType()` - Maps Noodl types to icon types + - `drawPortIcon()` - Renders icons on canvas + - `getPortIconWidth()` - For layout calculations + +#### Files Modified + +- `packages/noodl-editor/src/editor/src/views/nodegrapheditor/NodeGraphEditorNode.ts` + - Added imports for port icon utilities + - Integrated icon rendering in `drawPlugs()` function + - Icons positioned at x=8 (left side) or width-8 (right side) + - Type detection from connection metadata + +#### Technical Details + +**Icon Rendering**: + +- Font size: 10px +- Positioned 8px from node edge +- Centered vertically with port label +- Uses node's text color with opacity variations + +**Type Detection**: + +- Examines first connection's `fromPort.type` or `toPort.type` +- Handles undefined types gracefully (defaults to 'any') +- Case-insensitive type matching +- Supports type aliases (e.g., 'text' β†’ 'string', 'bool' β†’ 'boolean') + +**Browser Compatibility**: + +- Uses standard Unicode characters supported across all platforms +- No external dependencies or font loading +- Fallback to '?' character for unknown types + +#### User Experience Impact + +- **Instant Recognition**: Port types visible at a glance without needing to connect +- **Better Learning**: New users can understand port types faster +- **Reduced Errors**: Visual confirmation before attempting connections +- **Professional Look**: Adds visual richness to the node graph --- -## Progress Summary +## Sub-Task B: Node Comments βœ… COMPLETED -| Sub-Task | Status | Date Started | Date Completed | -| ---------------------- | ----------- | ------------ | -------------- | -| A1: Rounded Corners | Not Started | - | - | -| A2: Color Palette | Not Started | - | - | -| A3: Connection Points | Not Started | - | - | -| A4: Label Truncation | Not Started | - | - | -| B1: Comment Data Layer | Not Started | - | - | -| B2: Comment Icon | Not Started | - | - | -| B3: Hover Preview | Not Started | - | - | -| B4: Edit Modal | Not Started | - | - | -| B5: Click Integration | Not Started | - | - | -| C1: Port Grouping | Not Started | - | - | -| C2: Type Icons | Not Started | - | - | -| C3: Connection Preview | Not Started | - | - | +# TASK-000I-B Node Comments - Changelog ---- +## 2026-01-01 - Enhanced Comment Popup with Code Editor Style -## Template for Session Entries +### βœ… Completed Enhancements -```markdown -## [YYYY-MM-DD] - Session N: [Sub-Task Name] +**Multi-line Code Editor Popup** -### Summary - -[Brief description of what was accomplished] - -### Files Created - -- `path/to/file.ts` - [Purpose] +- Converted single-line input to multi-line textarea (8 rows default) +- Added monospace font family for code-like appearance +- Added line numbers gutter with dynamic updates +- Implemented scroll synchronization between textarea and line numbers +- Added proper code editor styling (dark theme, borders, focus states) +- Disabled spellcheck for cleaner code comment experience ### Files Modified -- `path/to/file.ts` - [What changed and why] +1. **packages/noodl-editor/src/editor/src/templates/stringinputpopup.html** -### Technical Notes + - Changed `` to ` +
- \ No newline at end of file + diff --git a/packages/noodl-editor/src/editor/src/views/nodegrapheditor/NodeGraphEditorNode.ts b/packages/noodl-editor/src/editor/src/views/nodegrapheditor/NodeGraphEditorNode.ts index 4a39763..336aff1 100644 --- a/packages/noodl-editor/src/editor/src/views/nodegrapheditor/NodeGraphEditorNode.ts +++ b/packages/noodl-editor/src/editor/src/views/nodegrapheditor/NodeGraphEditorNode.ts @@ -9,6 +9,7 @@ import { ProjectModel } from '../../models/projectmodel'; import { ViewerConnection } from '../../ViewerConnection'; import { NodeGraphEditor } from '../nodegrapheditor'; import PopupLayer from '../popuplayer'; +import { fillRoundRect, roundRect, strokeRoundRect, truncateText } from './canvasHelpers'; import { NodeGraphEditorConnection } from './NodeGraphEditorConnection'; function _getColorForAnnotation(annotation) { @@ -288,6 +289,7 @@ export class NodeGraphEditorNode { public static readonly attachedThreshold = 20; public static readonly propertyConnectionHeight = 20; public static readonly verticalSpacing = 8; + public static readonly cornerRadius = 6; model: NodeGraphNode; x: number; @@ -315,6 +317,8 @@ export class NodeGraphEditorNode { iconSize: number; iconRotation: number; + commentIconBounds: { x: number; y: number; width: number; height: number } | undefined; + constructor(model) { this.model = model; this.x = model.x || 0; @@ -519,6 +523,16 @@ export class NodeGraphEditorNode { PopupLayer.instance.hideTooltip(); if (this.owner.highlighted === this) { + // Check if clicking on comment icon + const inCommentIcon = this.commentIconBounds && this.isPointInCommentIcon(pos); + + if (inCommentIcon) { + // Show comment edit prompt + this.showCommentEditPopup(); + evt.stopPropagation && evt.stopPropagation(); + return; + } + if (this.borderHighlighted || this.connectionDragAreaHighlighted) { // User starts dragging from the border or connection area with circle icon this.owner.startDraggingConnection(this); @@ -615,19 +629,17 @@ export class NodeGraphEditorNode { ctx.textBaseline = 'middle'; ctx.save(); - // Clip - ctx.beginPath(); - ctx.rect(x, y, this.nodeSize.width, this.nodeSize.height); + // Clip to rounded rectangle + roundRect(ctx, x, y, this.nodeSize.width, this.nodeSize.height, NodeGraphEditorNode.cornerRadius); ctx.clip(); - // Bg + // Bg - Use rounded rectangle for modern appearance ctx.fillStyle = nc.header; - ctx.fillRect(x, y, this.nodeSize.width, this.nodeSize.height); + fillRoundRect(ctx, x, y, this.nodeSize.width, this.nodeSize.height, NodeGraphEditorNode.cornerRadius); const titlebarHeight = this.titlebarHeight(); - // Darken plate - //ctx.globalAlpha = 0.07; + // Darken plate (body area below title) ctx.fillStyle = nc.base; ctx.fillRect(x, y + titlebarHeight, this.nodeSize.width, this.nodeSize.height - titlebarHeight); @@ -637,7 +649,7 @@ export class NodeGraphEditorNode { ctx.globalCompositeOperation = 'hard-light'; // additive blending looks better ctx.globalAlpha = 0.19; ctx.fillStyle = nc.text; - ctx.fillRect(x, y, this.nodeSize.width, this.nodeSize.height); + fillRoundRect(ctx, x, y, this.nodeSize.width, this.nodeSize.height, NodeGraphEditorNode.cornerRadius); ctx.globalCompositeOperation = prevCompOperation; ctx.globalAlpha = 1; } @@ -694,7 +706,7 @@ export class NodeGraphEditorNode { // Title ctx.fillStyle = nc.text; - ctx.font = '12px Inter-Regular'; + ctx.font = '12px Inter-Medium'; ctx.textBaseline = 'top'; textWordWrap( ctx, @@ -711,7 +723,7 @@ export class NodeGraphEditorNode { ctx.save(); ctx.fillStyle = nc.text; ctx.globalAlpha = 0.65; - ctx.font = '12px Inter-Regular'; + ctx.font = '12px Inter-Medium'; ctx.textBaseline = 'top'; textWordWrap( ctx, @@ -726,6 +738,85 @@ export class NodeGraphEditorNode { ctx.restore(); } + // Draw comment icon (if node has comment OR is highlighted) + // Position on right side, before the node icon area if present + const hasComment = this.model.hasComment(); + if (hasComment || isHighligthed) { + const commentIconSize = 14; + // Adjust offset based on whether node icon is present + // If icon exists, offset more to avoid overlap; if not, position closer to edge + const commentIconRightOffset = this.icon ? 30 : 10; + const commentIconX = + x + this.nodeSize.width - connectionDragAreaWidth - commentIconSize - commentIconRightOffset; + const commentIconY = y + titlebarHeight / 2 - commentIconSize / 2; + + // Store bounds for click detection + this.commentIconBounds = { + x: commentIconX, + y: commentIconY, + width: commentIconSize, + height: commentIconSize + }; + + ctx.save(); + + // Set opacity based on whether comment exists + ctx.globalAlpha = hasComment ? 1.0 : 0.4; + ctx.fillStyle = nc.text; + ctx.strokeStyle = nc.text; + ctx.lineWidth = 1.5; + + // Draw speech bubble (rounded rectangle) + const bubbleWidth = commentIconSize; + const bubbleHeight = commentIconSize * 0.8; + const bubbleRadius = 2; + + // Main bubble body + ctx.beginPath(); + ctx.moveTo(commentIconX + bubbleRadius, commentIconY); + ctx.lineTo(commentIconX + bubbleWidth - bubbleRadius, commentIconY); + ctx.quadraticCurveTo( + commentIconX + bubbleWidth, + commentIconY, + commentIconX + bubbleWidth, + commentIconY + bubbleRadius + ); + ctx.lineTo(commentIconX + bubbleWidth, commentIconY + bubbleHeight - bubbleRadius); + ctx.quadraticCurveTo( + commentIconX + bubbleWidth, + commentIconY + bubbleHeight, + commentIconX + bubbleWidth - bubbleRadius, + commentIconY + bubbleHeight + ); + + // Draw tail (small triangle at bottom) + const tailWidth = 3; + const tailHeight = 3; + const tailX = commentIconX + bubbleWidth * 0.7; + ctx.lineTo(tailX + tailWidth, commentIconY + bubbleHeight); + ctx.lineTo(tailX, commentIconY + bubbleHeight + tailHeight); + ctx.lineTo(tailX - tailWidth / 2, commentIconY + bubbleHeight); + + // Complete the bubble + ctx.lineTo(commentIconX + bubbleRadius, commentIconY + bubbleHeight); + ctx.quadraticCurveTo( + commentIconX, + commentIconY + bubbleHeight, + commentIconX, + commentIconY + bubbleHeight - bubbleRadius + ); + ctx.lineTo(commentIconX, commentIconY + bubbleRadius); + ctx.quadraticCurveTo(commentIconX, commentIconY, commentIconX + bubbleRadius, commentIconY); + ctx.closePath(); + + ctx.stroke(); + + ctx.restore(); + } else { + // Clear bounds when not visible + this.commentIconBounds = undefined; + } + ctx.restore(); // Restore clip so we can draw border if (isHighligthed) { @@ -745,16 +836,21 @@ export class NodeGraphEditorNode { // ); } - // Border + // Border - Use rounded rectangles for modern appearance const health = this.model.getHealth(); if (!health.healthy) { ctx.setLineDash([5]); ctx.lineWidth = 1; ctx.strokeStyle = '#F57569'; ctx.globalAlpha = 0.7; - ctx.beginPath(); - ctx.rect(x - 1, y - 1, this.nodeSize.width + 2, this.nodeSize.height + 2); - ctx.stroke(); + strokeRoundRect( + ctx, + x - 1, + y - 1, + this.nodeSize.width + 2, + this.nodeSize.height + 2, + NodeGraphEditorNode.cornerRadius + 1 + ); ctx.setLineDash([]); // Restore line dash ctx.globalAlpha = 1; } @@ -762,9 +858,7 @@ export class NodeGraphEditorNode { if (this.selected || this.borderHighlighted || this.connectionDragAreaHighlighted) { ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 2; - ctx.beginPath(); - ctx.rect(x, y, this.nodeSize.width, this.nodeSize.height); - ctx.stroke(); + strokeRoundRect(ctx, x, y, this.nodeSize.width, this.nodeSize.height, NodeGraphEditorNode.cornerRadius); } if (this.model.annotation) { @@ -773,9 +867,7 @@ export class NodeGraphEditorNode { else if (this.model.annotation === 'Created') ctx.strokeStyle = '#5BF59E'; ctx.lineWidth = 2; - ctx.beginPath(); - ctx.rect(x, y, this.nodeSize.width, this.nodeSize.height); - ctx.stroke(); + strokeRoundRect(ctx, x, y, this.nodeSize.width, this.nodeSize.height, NodeGraphEditorNode.cornerRadius); } // Paint plugs @@ -793,35 +885,61 @@ export class NodeGraphEditorNode { } function dot(side, color) { + const cx = x + (side === 'left' ? 0 : _this.nodeSize.width); + const radius = 6; // Back to normal size + + // Draw main port indicator ctx.fillStyle = color; ctx.beginPath(); - ctx.arc(x + (side === 'left' ? 0 : _this.nodeSize.width), ty, 4, 0, 2 * Math.PI, false); + ctx.arc(cx, ty, radius, 0, 2 * Math.PI, false); + ctx.fill(); + + // Add subtle inner highlight for depth + ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; + ctx.beginPath(); + ctx.arc(cx - 0.5, ty - 0.5, radius * 0.4, 0, 2 * Math.PI, false); ctx.fill(); } function drawPlugs(plugs, offset) { - ctx.font = '11px Inter-Regular'; + ctx.font = '11px Inter-Medium'; ctx.textBaseline = 'middle'; ctx.globalAlpha = 1; for (const i in plugs) { const p = plugs[i]; - // Label + // Calculate Y position for this port ty = p.index * NodeGraphEditorNode.propertyConnectionHeight + offset; - if (p.loc === 'left' || p.loc === 'middle') tx = x + horizontalSpacing; - else if (p.loc === 'right') tx = x + _this.nodeSize.width - horizontalSpacing; - else tx = x + _this.nodeSize.width / 2; + // Draw labels at normal positions + if (p.loc === 'left' || p.loc === 'middle') { + // Left-aligned labels + tx = x + horizontalSpacing; + } else if (p.loc === 'right') { + // Right-aligned labels + tx = x + _this.nodeSize.width - horizontalSpacing; + } else { + tx = x + _this.nodeSize.width / 2; + } + ctx.fillStyle = nc.text; ctx.textAlign = p.loc === 'right' ? 'right' : 'left'; - ctx.fillText(p.displayName ? p.displayName : p.property, tx, ty); + // Truncate port labels to prevent overflow + const label = p.displayName ? p.displayName : p.property; + const portAreaWidth = + p.loc === 'middle' + ? _this.nodeSize.width - 2 * horizontalSpacing + : _this.nodeSize.width - horizontalSpacing - 8; + const truncatedLabel = truncateText(ctx, label, portAreaWidth); - // Plug - if (p.leftCons.length) { + ctx.fillText(truncatedLabel, tx, ty); + + // Plug - Left side + if (p.leftCons.length || p.leftIcon) { var connectionColors = NodeLibrary.instance.colorSchemeForConnectionType( - NodeLibrary.nameForPortType(p.leftCons[0].fromPort ? p.leftCons[0].fromPort.type : undefined) + NodeLibrary.nameForPortType(p.leftCons[0]?.fromPort ? p.leftCons[0].fromPort.type : undefined) ); var color = _.find(p.leftCons, function (p) { return p.isHighlighted(); @@ -834,7 +952,7 @@ export class NodeGraphEditorNode { return p.isHighlighted(); }) || p.leftCons[p.leftCons.length - 1]; - if (topConnection.model.annotation) { + if (topConnection && topConnection.model.annotation) { color = _getColorForAnnotation(topConnection.model.annotation); } @@ -845,9 +963,10 @@ export class NodeGraphEditorNode { } } - if (p.rightCons.length) { + // Plug - Right side + if (p.rightCons.length || p.rightIcon) { connectionColors = NodeLibrary.instance.colorSchemeForConnectionType( - NodeLibrary.nameForPortType(p.rightCons[0].fromPort ? p.rightCons[0].fromPort.type : undefined) + NodeLibrary.nameForPortType(p.rightCons[0]?.fromPort ? p.rightCons[0].fromPort.type : undefined) ); color = _.find(p.rightCons, function (p) { return p.isHighlighted(); @@ -860,7 +979,7 @@ export class NodeGraphEditorNode { return p.isHighlighted(); }) || p.rightCons[p.rightCons.length - 1]; - if (topConnection.model.annotation) { + if (topConnection && topConnection.model.annotation) { color = _getColorForAnnotation(topConnection.model.annotation); } @@ -1088,4 +1207,75 @@ export class NodeGraphEditorNode { this.parent = undefined; } } + + /** + * Check if a point (in local node coordinates) is within the comment icon bounds + */ + isPointInCommentIcon(pos: { x: number; y: number }): boolean { + if (!this.commentIconBounds) return false; + + // Convert local pos to global for comparison with commentIconBounds (which are in global coords) + const globalX = pos.x + this.global.x; + const globalY = pos.y + this.global.y; + + const bounds = this.commentIconBounds; + const padding = 4; // Extra hit area padding for easier clicking + + return ( + globalX >= bounds.x - padding && + globalX <= bounds.x + bounds.width + padding && + globalY >= bounds.y - padding && + globalY <= bounds.y + bounds.height + padding + ); + } + + /** + * Show a popup for editing the node comment + */ + showCommentEditPopup() { + const currentComment = this.model.getComment() || ''; + const nodeLabel = this.model.label || 'Node'; + const model = this.model; + const owner = this.owner; + + // Use PopupLayer.StringInputPopup for Electron compatibility + const popup = new PopupLayer.StringInputPopup({ + label: `Comment for "${nodeLabel}"`, + okLabel: 'Save', + cancelLabel: 'Cancel', + onOk: (newComment: string) => { + // Set comment with undo support + model.setComment(newComment || undefined, { + undo: true, + label: newComment ? 'Edit node comment' : 'Remove node comment' + }); + + // Repaint to update the icon appearance + owner.repaint(); + PopupLayer.instance.hidePopup(); + }, + onCancel: () => { + PopupLayer.instance.hidePopup(); + } + }); + + // Render popup BEFORE showing it + popup.render(); + + // Set initial value after render + popup.$('.string-input-popup-input').val(currentComment); + + // Use requestAnimationFrame + setTimeout to ensure we're past both the current + // event cycle AND any pending DOM updates. This prevents the PopupLayer's body + // click handler from immediately closing the popup. + requestAnimationFrame(() => { + setTimeout(() => { + PopupLayer.instance.showPopup({ + content: popup, + position: 'screen-center', + isBackgroundDimmed: true + }); + }, 100); // 100ms delay to be extra safe + }); + } } diff --git a/packages/noodl-editor/src/editor/src/views/nodegrapheditor/canvasHelpers.ts b/packages/noodl-editor/src/editor/src/views/nodegrapheditor/canvasHelpers.ts new file mode 100644 index 0000000..efeed94 --- /dev/null +++ b/packages/noodl-editor/src/editor/src/views/nodegrapheditor/canvasHelpers.ts @@ -0,0 +1,196 @@ +/** + * Canvas Helper Utilities for Node Graph Rendering + * + * Provides utility functions for drawing rounded rectangles and text truncation + * on HTML5 Canvas. Used primarily for modernizing node appearance in the graph editor. + * + * @module canvasHelpers + * @since TASK-000I-A + */ + +/** + * Corner radius configuration for rounded rectangles + * Can be a single number for all corners, or an object specifying each corner + */ +export type CornerRadius = + | number + | { + tl: number; // top-left + tr: number; // top-right + br: number; // bottom-right + bl: number; // bottom-left + }; + +/** + * Draw a rounded rectangle path (does not fill or stroke) + * + * Uses arcTo() for drawing rounded corners. This method only creates the path; + * you must call ctx.fill() or ctx.stroke() afterwards. + * + * @param ctx - Canvas rendering context + * @param x - X coordinate of top-left corner + * @param y - Y coordinate of top-left corner + * @param width - Width of rectangle + * @param height - Height of rectangle + * @param radius - Corner radius (number for all corners, or object for individual corners) + * + * @example + * ```typescript + * roundRect(ctx, 10, 10, 100, 50, 6); + * ctx.fill(); + * ``` + */ +export function roundRect( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + width: number, + height: number, + radius: CornerRadius +): void { + // Normalize radius to object format + const r = typeof radius === 'number' ? { tl: radius, tr: radius, br: radius, bl: radius } : radius; + + // Clamp radius to reasonable values (can't be larger than half the smallest dimension) + const maxRadius = Math.min(width, height) / 2; + const tl = Math.min(r.tl, maxRadius); + const tr = Math.min(r.tr, maxRadius); + const br = Math.min(r.br, maxRadius); + const bl = Math.min(r.bl, maxRadius); + + ctx.beginPath(); + ctx.moveTo(x + tl, y); + + // Top edge and top-right corner + ctx.lineTo(x + width - tr, y); + ctx.arcTo(x + width, y, x + width, y + tr, tr); + + // Right edge and bottom-right corner + ctx.lineTo(x + width, y + height - br); + ctx.arcTo(x + width, y + height, x + width - br, y + height, br); + + // Bottom edge and bottom-left corner + ctx.lineTo(x + bl, y + height); + ctx.arcTo(x, y + height, x, y + height - bl, bl); + + // Left edge and top-left corner + ctx.lineTo(x, y + tl); + ctx.arcTo(x, y, x + tl, y, tl); + + ctx.closePath(); +} + +/** + * Fill a rounded rectangle + * + * Convenience wrapper that creates a rounded rectangle path and fills it. + * + * @param ctx - Canvas rendering context + * @param x - X coordinate of top-left corner + * @param y - Y coordinate of top-left corner + * @param width - Width of rectangle + * @param height - Height of rectangle + * @param radius - Corner radius + * + * @example + * ```typescript + * ctx.fillStyle = '#333'; + * fillRoundRect(ctx, 10, 10, 100, 50, 6); + * ``` + */ +export function fillRoundRect( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + width: number, + height: number, + radius: CornerRadius +): void { + roundRect(ctx, x, y, width, height, radius); + ctx.fill(); +} + +/** + * Stroke a rounded rectangle + * + * Convenience wrapper that creates a rounded rectangle path and strokes it. + * + * @param ctx - Canvas rendering context + * @param x - X coordinate of top-left corner + * @param y - Y coordinate of top-left corner + * @param width - Width of rectangle + * @param height - Height of rectangle + * @param radius - Corner radius + * + * @example + * ```typescript + * ctx.strokeStyle = '#fff'; + * ctx.lineWidth = 2; + * strokeRoundRect(ctx, 10, 10, 100, 50, 6); + * ``` + */ +export function strokeRoundRect( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + width: number, + height: number, + radius: CornerRadius +): void { + roundRect(ctx, x, y, width, height, radius); + ctx.stroke(); +} + +/** + * Truncate text to fit within a maximum width, adding ellipsis if needed + * + * Efficiently truncates text by measuring progressively shorter strings + * until one fits within the specified width. Uses the context's current font settings. + * + * @param ctx - Canvas rendering context (with font already set) + * @param text - Text to truncate + * @param maxWidth - Maximum width in pixels + * @returns Truncated text with '…' appended if truncation occurred + * + * @example + * ```typescript + * ctx.font = '12px Inter-Regular'; + * const displayText = truncateText(ctx, 'Very Long Port Name', 80); + * // Returns "Very Long Po…" if it doesn't fit + * ctx.fillText(displayText, x, y); + * ``` + */ +export function truncateText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number): string { + // If the text already fits, return it as-is + if (ctx.measureText(text).width <= maxWidth) { + return text; + } + + const ellipsis = '…'; + const ellipsisWidth = ctx.measureText(ellipsis).width; + + // If even the ellipsis doesn't fit, just return it + if (ellipsisWidth > maxWidth) { + return ellipsis; + } + + // Binary search for the optimal truncation point + let left = 0; + let right = text.length; + let result = ''; + + while (left <= right) { + const mid = Math.floor((left + right) / 2); + const truncated = text.slice(0, mid); + const width = ctx.measureText(truncated + ellipsis).width; + + if (width <= maxWidth) { + result = truncated; + left = mid + 1; + } else { + right = mid - 1; + } + } + + return result + ellipsis; +} diff --git a/packages/noodl-editor/src/editor/src/views/nodegrapheditor/portIcons.ts b/packages/noodl-editor/src/editor/src/views/nodegrapheditor/portIcons.ts new file mode 100644 index 0000000..4582c75 --- /dev/null +++ b/packages/noodl-editor/src/editor/src/views/nodegrapheditor/portIcons.ts @@ -0,0 +1,210 @@ +/** + * Port Type Icons for Node Graph Editor + * + * Provides simple, minimal icon indicators for port data types. + * Uses Unicode characters for reliability and clarity at small sizes. + * + * @module portIcons + * @since TASK-000I-C2 + */ + +/** + * Supported port types in the Noodl system + */ +export type PortType = + | 'signal' + | 'string' + | 'number' + | 'boolean' + | 'object' + | 'array' + | 'color' + | 'any' + | 'component' + | 'enum'; + +/** + * Icon representation for a port type + */ +export interface PortIcon { + /** Unicode character to display */ + char: string; + /** Optional description for debugging */ + description?: string; +} + +/** + * Icon definitions for each port type + * Using simple, clear Unicode characters that render well at small sizes + */ +export const PORT_ICONS: Record = { + signal: { + char: '⚑', + description: 'Signal/Event trigger' + }, + string: { + char: 'T', + description: 'Text/String data' + }, + number: { + char: '#', + description: 'Numeric data' + }, + boolean: { + char: '◐', + description: 'True/False value' + }, + object: { + char: '{ }', + description: 'Object/Record' + }, + array: { + char: '[ ]', + description: 'Array/List' + }, + color: { + char: '●', + description: 'Color value' + }, + any: { + char: 'β—‡', + description: 'Any type' + }, + component: { + char: 'β—ˆ', + description: 'Component reference' + }, + enum: { + char: '≑', + description: 'Enumeration/List' + } +}; + +/** + * Visual constants for port icon rendering + */ +export const PORT_ICON_SIZE = 10; // Font size in pixels +export const PORT_ICON_PADDING = 4; // Space between icon and label + +/** + * Map Noodl internal type names to icon types + * + * Noodl uses various type names internally - this function normalizes them + * to our standard PortType set for consistent icon display. + * + * @param type - The internal Noodl type name (may be undefined) + * @returns The corresponding PortType for icon selection + * + * @example + * ```typescript + * getPortIconType('*') // returns 'signal' + * getPortIconType('string') // returns 'string' + * getPortIconType(undefined) // returns 'any' + * ``` + */ +export function getPortIconType(type: string | undefined): PortType { + // Handle undefined or non-string types (runtime safety) + if (!type || typeof type !== 'string') return 'any'; + + // Normalize to lowercase for case-insensitive matching + const normalizedType = type.toLowerCase(); + + // Direct type mappings + const typeMap: Record = { + // Signal types + signal: 'signal', + '*': 'signal', + + // Primitive types + string: 'string', + number: 'number', + boolean: 'boolean', + + // Complex types + object: 'object', + array: 'array', + color: 'color', + + // Special types + component: 'component', + enum: 'enum', + + // Aliases + text: 'string', + bool: 'boolean', + list: 'array', + json: 'object' + }; + + return typeMap[normalizedType] || 'any'; +} + +/** + * Draw a port type icon on canvas + * + * Renders a small icon character indicating the port's data type. + * The icon is drawn with the specified color and at the given position. + * + * @param ctx - Canvas rendering context + * @param type - The port type to render an icon for + * @param x - X coordinate (center of icon) + * @param y - Y coordinate (center of icon) + * @param color - Color to render the icon (CSS color string) + * @param alpha - Optional opacity override (0-1) + * + * @example + * ```typescript + * drawPortIcon(ctx, 'signal', 100, 50, '#ff0000', 0.8); + * drawPortIcon(ctx, 'number', 150, 50, 'rgba(255, 255, 255, 0.6)'); + * ``` + */ +export function drawPortIcon( + ctx: CanvasRenderingContext2D, + type: PortType, + x: number, + y: number, + color: string, + alpha: number = 1 +): void { + const icon = PORT_ICONS[type]; + if (!icon) { + console.warn(`Unknown port type: ${type}`); + return; + } + + ctx.save(); + + // Set rendering properties + ctx.fillStyle = color; + ctx.globalAlpha = alpha; + ctx.font = `${PORT_ICON_SIZE}px Inter-Regular`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + // Draw the icon character + ctx.fillText(icon.char, x, y); + + ctx.restore(); +} + +/** + * Get the visual width of an icon (for layout calculations) + * + * Measures the actual rendered width of a port icon character. + * Useful for positioning labels correctly after icons. + * + * @param ctx - Canvas rendering context (with font already set) + * @param type - The port type + * @returns Width in pixels + */ +export function getPortIconWidth(ctx: CanvasRenderingContext2D, type: PortType): number { + const icon = PORT_ICONS[type]; + if (!icon) return 0; + + ctx.save(); + ctx.font = `${PORT_ICON_SIZE}px Inter-Regular`; + const width = ctx.measureText(icon.char).width; + ctx.restore(); + + return width; +} diff --git a/packages/noodl-editor/src/editor/src/views/popuplayer.js b/packages/noodl-editor/src/editor/src/views/popuplayer.js index 652ee77..d16c3e1 100644 --- a/packages/noodl-editor/src/editor/src/views/popuplayer.js +++ b/packages/noodl-editor/src/editor/src/views/popuplayer.js @@ -281,8 +281,14 @@ PopupLayer.prototype.showPopup = function (args) { args.content.owner = this; this.$('.popup-layer-popup-content').append(content); - var contentWidth = content.outerWidth(true); - var contentHeight = content.outerHeight(true); + + // Force a reflow to ensure the element is measurable + void this.$('.popup-layer-popup-content')[0].offsetHeight; + + // Query the actual appended element to measure dimensions + var popupContent = this.$('.popup-layer-popup-content'); + var contentWidth = popupContent.children().first().outerWidth(true); + var contentHeight = popupContent.children().first().outerHeight(true); if (args.position === 'screen-center') { if (args.isBackgroundDimmed) { @@ -921,13 +927,17 @@ PopupLayer.StringInputPopup.prototype = Object.create(View.prototype); PopupLayer.StringInputPopup.prototype.render = function () { this.el = this.bindView($(StringInputPopupTemplate), this); - this.$('.string-input-popup-input') - .off('keypress') - .on('keypress', (e) => { + // Only close on Enter for single-line inputs, not textareas + const input = this.$('.string-input-popup-input'); + const isTextarea = input.is('textarea'); + + if (!isTextarea) { + input.off('keypress').on('keypress', (e) => { if (e.which == 13) { this.onOkClicked(); } }); + } return this.el; }; @@ -949,8 +959,49 @@ PopupLayer.StringInputPopup.prototype.onCancelClicked = function () { this.owner.hidePopup(); }; +PopupLayer.StringInputPopup.prototype.updateLineNumbers = function () { + const textarea = this.$('.string-input-popup-input')[0]; + const lineNumbersEl = this.$('.string-input-popup-line-numbers')[0]; + + if (!textarea || !lineNumbersEl) return; + + // Count lines based on textarea value + const text = textarea.value; + const lines = text ? text.split('\n').length : 1; + + // Always show at least 8 lines (matching rows="8") + const displayLines = Math.max(8, lines); + + // Generate line numbers + let lineNumbersHTML = ''; + for (let i = 1; i <= displayLines; i++) { + lineNumbersHTML += i + '\n'; + } + + lineNumbersEl.textContent = lineNumbersHTML; + + // Sync scroll + lineNumbersEl.scrollTop = textarea.scrollTop; +}; + PopupLayer.StringInputPopup.prototype.onOpen = function () { - this.$('.string-input-popup-input').focus(); + const textarea = this.$('.string-input-popup-input'); + + // Initial line numbers + this.updateLineNumbers(); + + // Update line numbers on input + textarea.on('input', () => this.updateLineNumbers()); + + // Sync scroll between textarea and line numbers + textarea.on('scroll', () => { + const lineNumbersEl = this.$('.string-input-popup-line-numbers')[0]; + if (lineNumbersEl) { + lineNumbersEl.scrollTop = textarea[0].scrollTop; + } + }); + + textarea.focus(); }; // --------------------------------------------------------------------- diff --git a/packages/noodl-runtime/src/nodelibraryexport.js b/packages/noodl-runtime/src/nodelibraryexport.js index 04ba1ac..5d1d12a 100644 --- a/packages/noodl-runtime/src/nodelibraryexport.js +++ b/packages/noodl-runtime/src/nodelibraryexport.js @@ -161,49 +161,49 @@ function generateNodeLibrary(nodeRegister) { colors: { nodes: { component: { - base: '#643D8B', - baseHighlighted: '#79559b', - header: '#4E2877', - headerHighlighted: '#643d8b', - outline: '#4E2877', + base: '#8B4DAB', + baseHighlighted: '#A167C5', + header: '#6B2D8B', + headerHighlighted: '#8B4DAB', + outline: '#6B2D8B', outlineHighlighted: '#b58900', - text: '#dbd0e4' + text: '#FFFFFF' }, visual: { - base: '#315272', - baseHighlighted: '#4d6784', - header: '#173E5D', - headerHighlighted: '#315272', - outline: '#173E5D', + base: '#4A7CA8', + baseHighlighted: '#6496C2', + header: '#2A5C88', + headerHighlighted: '#4A7CA8', + outline: '#2A5C88', outlineHighlighted: '#b58900', - text: '#cfd5de' + text: '#FFFFFF' }, data: { - base: '#465524', - baseHighlighted: '#5b6a37', - header: '#314110', - headerHighlighted: '#465524', - outline: '#314110', + base: '#6B8F3C', + baseHighlighted: '#85A956', + header: '#4B6F1C', + headerHighlighted: '#6B8F3C', + outline: '#4B6F1C', outlineHighlighted: '#b58900', - text: '#d2d6c5' + text: '#FFFFFF' }, javascript: { - base: '#7E3660', - baseHighlighted: '#944e74', - header: '#67214B', - headerHighlighted: '#7e3660', - outline: '#67214B', + base: '#B84D7C', + baseHighlighted: '#CE6796', + header: '#982D5C', + headerHighlighted: '#B84D7C', + outline: '#982D5C', outlineHighlighted: '#d57bab', - text: '#e4cfd9' + text: '#FFFFFF' }, default: { - base: '#4C4F59', - baseHighlighted: '#62656e', - header: '#373B45', - headerHighlighted: '#4c4f59', - outline: '#373B45', + base: '#6C6F79', + baseHighlighted: '#868993', + header: '#4C4F59', + headerHighlighted: '#6C6F79', + outline: '#4C4F59', outlineHighlighted: '#b58900', - text: '#d3d4d6' + text: '#FFFFFF' } }, connections: {