mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 15:22:55 +01:00
498 lines
17 KiB
Markdown
498 lines
17 KiB
Markdown
# VIEW-007: Semantic Layers
|
|
|
|
**View Type:** 🎨 Canvas Overlay (filters what's visible on canvas)
|
|
|
|
## Overview
|
|
|
|
Split the chaotic canvas into conceptual layers that can be shown/hidden independently. See just the visual tree, just the data nodes, just the logic, or any combination. Reduce cognitive load by focusing on one concern at a time.
|
|
|
|
**Estimate:** 2-3 days
|
|
**Priority:** MEDIUM
|
|
**Complexity:** Low
|
|
**Dependencies:** VIEW-000 (Foundation), PREREQ-003 (Canvas Overlay Pattern)
|
|
|
|
---
|
|
|
|
## The Problem
|
|
|
|
A typical complex Noodl canvas has:
|
|
- Visual components (Groups, Text, Images) forming a hierarchy
|
|
- Data nodes (Variables, Objects, Arrays) scattered around
|
|
- Logic nodes (Conditions, Expressions, Switches) everywhere
|
|
- API nodes (REST, Functions) mixed in
|
|
- Event nodes (Send/Receive Event) connecting things
|
|
|
|
All of these are shown simultaneously, creating overwhelming visual noise. You can't see the forest for the trees.
|
|
|
|
---
|
|
|
|
## The Solution
|
|
|
|
A layer system that:
|
|
1. Categorizes all nodes into semantic layers
|
|
2. Allows showing/hiding each layer independently
|
|
3. Shows connection counts when layers are hidden
|
|
4. Maintains connection visibility (optionally) across hidden layers
|
|
5. Provides quick presets for common views
|
|
|
|
---
|
|
|
|
## User Stories
|
|
|
|
1. **As a developer understanding layout**, I want to see only visual components so I can understand the DOM structure.
|
|
|
|
2. **As a developer debugging data flow**, I want to see only data nodes to understand the state model.
|
|
|
|
3. **As a developer reviewing logic**, I want to see only logic nodes to verify conditional behavior.
|
|
|
|
4. **As a developer cleaning up**, I want to quickly see how many nodes are in each category.
|
|
|
|
---
|
|
|
|
## UI Design
|
|
|
|
### Layer Control Panel
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ Semantic Layers │
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ PRESETS │
|
|
│ [All] [Visual Only] [Data Only] [Logic Only] [Custom] │
|
|
│ │
|
|
│ ─────────────────────────────────────────────────────────────── │
|
|
│ │
|
|
│ LAYERS Nodes Visible │
|
|
│ │
|
|
│ [👁] 📦 Visual Structure 45 ████████████ │
|
|
│ Groups, Pages, Text, Images... │
|
|
│ │
|
|
│ [👁] 💾 Data / State 22 ████████░░░░ │
|
|
│ Variables, Objects, Arrays... │
|
|
│ │
|
|
│ [ ] ⚡ Logic / Control 38 ██████████████ │
|
|
│ Conditions, Expressions, Switches... (hidden) │
|
|
│ │
|
|
│ [👁] 🌐 API / External 8 ████░░░░░░░░ │
|
|
│ REST, Functions, Cloud... │
|
|
│ │
|
|
│ [ ] 📡 Events / Signals 15 ██████░░░░░░ │
|
|
│ Send Event, Receive Event... (hidden) │
|
|
│ │
|
|
│ [👁] 🔧 Navigation 4 ██░░░░░░░░░░ │
|
|
│ Page Router, Navigate... │
|
|
│ │
|
|
│ ─────────────────────────────────────────────────────────────── │
|
|
│ │
|
|
│ OPTIONS │
|
|
│ [✓] Show connections to hidden layers (dotted) │
|
|
│ [✓] Fade hidden layers instead of removing │
|
|
│ [ ] Auto-hide empty layers │
|
|
│ │
|
|
│ ─────────────────────────────────────────────────────────────── │
|
|
│ │
|
|
│ Currently showing: 79 of 132 nodes (60%) │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Canvas with Hidden Layers
|
|
|
|
When a layer is hidden, nodes can either:
|
|
1. **Disappear completely** - Node gone, connections rerouted or hidden
|
|
2. **Fade to ghost** - 20% opacity, non-interactive
|
|
3. **Collapse to indicator** - Small badge showing "12 hidden logic nodes"
|
|
|
|
Example with "fade" mode:
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ CANVAS │
|
|
│ │
|
|
│ ┌─────────┐ │
|
|
│ │ Page │ ← Full opacity (Visual layer visible) │
|
|
│ └────┬────┘ │
|
|
│ │ │
|
|
│ ┌────┴────┐ ┌╌╌╌╌╌╌╌╌╌╌╌┐ │
|
|
│ │ Group │╌╌╌╌╌│ Condition │ ← 20% opacity (Logic hidden) │
|
|
│ └────┬────┘ └╌╌╌╌╌╌╌╌╌╌╌┘ │
|
|
│ │ ╎ │
|
|
│ ┌────┴────┐ ╎ │
|
|
│ │ Text │◄╌╌╌╌┘ ← Dotted connection to hidden layer │
|
|
│ └─────────┘ │
|
|
│ │
|
|
│ ┌─────────┐ │
|
|
│ │ Variable│ ← Full opacity (Data layer visible) │
|
|
│ └─────────┘ │
|
|
│ │
|
|
│ ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ │
|
|
│ │ ⚡ 12 Logic nodes hidden │ ← Collapse indicator │
|
|
│ └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Quick Toggle (Keyboard Shortcuts)
|
|
|
|
```
|
|
Cmd+1 → Toggle Visual layer
|
|
Cmd+2 → Toggle Data layer
|
|
Cmd+3 → Toggle Logic layer
|
|
Cmd+4 → Toggle API layer
|
|
Cmd+5 → Toggle Events layer
|
|
Cmd+0 → Show all layers
|
|
```
|
|
|
|
---
|
|
|
|
## Technical Design
|
|
|
|
### Layer Definition
|
|
|
|
```typescript
|
|
interface SemanticLayer {
|
|
id: string;
|
|
name: string;
|
|
icon: string;
|
|
description: string;
|
|
color: string; // For connection coloring
|
|
|
|
// Which node types belong to this layer
|
|
nodeTypes: string[];
|
|
|
|
// Match function for complex cases
|
|
matchNode?: (node: NodeGraphNode) => boolean;
|
|
}
|
|
|
|
const SEMANTIC_LAYERS: SemanticLayer[] = [
|
|
{
|
|
id: 'visual',
|
|
name: 'Visual Structure',
|
|
icon: '📦',
|
|
description: 'Groups, Pages, Text, Images...',
|
|
color: '#4A90D9',
|
|
nodeTypes: [
|
|
'Group', 'Page', 'Text', 'Image', 'Video', 'Button',
|
|
'Checkbox', 'Radio', 'Dropdown', 'TextInput', 'Repeater',
|
|
'Columns', 'Circle', 'Rectangle', 'Icon', 'Lottie'
|
|
// ... all visual node types
|
|
]
|
|
},
|
|
{
|
|
id: 'data',
|
|
name: 'Data / State',
|
|
icon: '💾',
|
|
description: 'Variables, Objects, Arrays...',
|
|
color: '#50C878',
|
|
nodeTypes: [
|
|
'Variable', 'Object', 'Array', 'String', 'Number', 'Boolean',
|
|
'Color', 'Set Variable', 'Create Object', 'Array Filter',
|
|
'Array Map', 'Insert Into Array', 'Remove From Array'
|
|
]
|
|
},
|
|
{
|
|
id: 'logic',
|
|
name: 'Logic / Control',
|
|
icon: '⚡',
|
|
description: 'Conditions, Expressions, Switches...',
|
|
color: '#F5A623',
|
|
nodeTypes: [
|
|
'Condition', 'Expression', 'Switch', 'And', 'Or', 'Not',
|
|
'Inverter', 'Delay', 'Debounce', 'Counter', 'States',
|
|
'For Each', 'Run Tasks'
|
|
]
|
|
},
|
|
{
|
|
id: 'api',
|
|
name: 'API / External',
|
|
icon: '🌐',
|
|
description: 'REST, Functions, Cloud...',
|
|
color: '#BD10E0',
|
|
nodeTypes: [
|
|
'REST', 'REST Query', 'GraphQL', 'Function', 'Javascript',
|
|
'Cloud Function', 'Query Records', 'Create Record',
|
|
'Update Record', 'Delete Record'
|
|
]
|
|
},
|
|
{
|
|
id: 'events',
|
|
name: 'Events / Signals',
|
|
icon: '📡',
|
|
description: 'Send Event, Receive Event...',
|
|
color: '#FF6B6B',
|
|
nodeTypes: [
|
|
'Send Event', 'Receive Event', 'Component Inputs',
|
|
'Component Outputs', 'Page Inputs', 'Page Outputs'
|
|
]
|
|
},
|
|
{
|
|
id: 'navigation',
|
|
name: 'Navigation',
|
|
icon: '🔧',
|
|
description: 'Page Router, Navigate...',
|
|
color: '#9B9B9B',
|
|
nodeTypes: [
|
|
'Page Router', 'Navigate', 'Navigate Back', 'External Link',
|
|
'Open Popup', 'Close Popup'
|
|
]
|
|
}
|
|
];
|
|
```
|
|
|
|
### Layer State
|
|
|
|
```typescript
|
|
interface LayerState {
|
|
layers: {
|
|
[layerId: string]: {
|
|
visible: boolean;
|
|
nodeCount: number;
|
|
};
|
|
};
|
|
|
|
options: {
|
|
showHiddenConnections: boolean; // Show dotted lines to hidden nodes
|
|
fadeHiddenLayers: boolean; // Fade instead of hide
|
|
autoHideEmpty: boolean; // Hide layers with 0 nodes
|
|
};
|
|
|
|
preset: 'all' | 'visual' | 'data' | 'logic' | 'custom';
|
|
}
|
|
|
|
// Presets
|
|
const PRESETS = {
|
|
all: ['visual', 'data', 'logic', 'api', 'events', 'navigation'],
|
|
visual: ['visual'],
|
|
data: ['data'],
|
|
logic: ['logic', 'data'], // Logic often needs data context
|
|
custom: null // User-defined
|
|
};
|
|
```
|
|
|
|
### Integration with Canvas
|
|
|
|
```typescript
|
|
// In NodeGraphEditor or similar
|
|
|
|
interface LayerFilterOptions {
|
|
visibleLayers: string[];
|
|
fadeHidden: boolean;
|
|
showHiddenConnections: boolean;
|
|
}
|
|
|
|
function applyLayerFilter(options: LayerFilterOptions): void {
|
|
this.nodes.forEach(node => {
|
|
const layer = getNodeLayer(node.model);
|
|
const isVisible = options.visibleLayers.includes(layer);
|
|
|
|
if (options.fadeHidden) {
|
|
node.setOpacity(isVisible ? 1.0 : 0.2);
|
|
node.setInteractive(isVisible);
|
|
} else {
|
|
node.setVisible(isVisible);
|
|
}
|
|
});
|
|
|
|
this.connections.forEach(conn => {
|
|
const fromVisible = options.visibleLayers.includes(getNodeLayer(conn.from));
|
|
const toVisible = options.visibleLayers.includes(getNodeLayer(conn.to));
|
|
|
|
if (fromVisible && toVisible) {
|
|
conn.setStyle('solid');
|
|
conn.setOpacity(1.0);
|
|
} else if (options.showHiddenConnections) {
|
|
conn.setStyle('dotted');
|
|
conn.setOpacity(0.3);
|
|
} else {
|
|
conn.setVisible(false);
|
|
}
|
|
});
|
|
|
|
this.repaint();
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Phases
|
|
|
|
### Phase 1: Layer Categorization (0.5 day)
|
|
|
|
1. Define all semantic layers with node type mappings
|
|
2. Create `getNodeLayer()` function
|
|
3. Handle edge cases (unknown types → "Other")
|
|
4. Test categorization accuracy
|
|
|
|
**Verification:**
|
|
- [ ] All node types categorized
|
|
- [ ] Categories make sense
|
|
- [ ] No nodes fall through
|
|
|
|
### Phase 2: Layer State Management (0.5 day)
|
|
|
|
1. Create layer state store/context
|
|
2. Implement toggle functions
|
|
3. Add preset switching
|
|
4. Persist state to session/preferences
|
|
|
|
**Verification:**
|
|
- [ ] Toggle works
|
|
- [ ] Presets switch correctly
|
|
- [ ] State persists
|
|
|
|
### Phase 3: Canvas Integration (1 day)
|
|
|
|
1. Hook layer state into NodeGraphEditor
|
|
2. Implement node visibility/opacity changes
|
|
3. Implement connection styling
|
|
4. Handle selection in hidden layers
|
|
5. Performance optimization
|
|
|
|
**Verification:**
|
|
- [ ] Nodes show/hide correctly
|
|
- [ ] Connections styled appropriately
|
|
- [ ] Selection works
|
|
- [ ] Performance acceptable
|
|
|
|
### Phase 4: UI Panel (0.5-1 day)
|
|
|
|
1. Create `SemanticLayersPanel` component
|
|
2. Show layer toggles with counts
|
|
3. Add options checkboxes
|
|
4. Add preset buttons
|
|
5. Integrate into Analysis Panel or as floating control
|
|
|
|
**Verification:**
|
|
- [ ] Panel renders correctly
|
|
- [ ] Toggles work
|
|
- [ ] Counts accurate
|
|
|
|
### Phase 5: Keyboard Shortcuts & Polish (0.5 day)
|
|
|
|
1. Add keyboard shortcuts
|
|
2. Add visual indicator for active filter
|
|
3. Toast notification when layers change
|
|
4. Help tooltip explaining layers
|
|
|
|
**Verification:**
|
|
- [ ] Shortcuts work
|
|
- [ ] User knows filter is active
|
|
- [ ] UX polished
|
|
|
|
---
|
|
|
|
## Files to Create
|
|
|
|
```
|
|
packages/noodl-editor/src/editor/src/views/
|
|
├── SemanticLayersPanel/
|
|
│ ├── index.ts
|
|
│ ├── SemanticLayersPanel.tsx
|
|
│ ├── SemanticLayersPanel.module.scss
|
|
│ ├── LayerToggle.tsx
|
|
│ ├── PresetButtons.tsx
|
|
│ └── LayerOptions.tsx
|
|
└── context/
|
|
└── SemanticLayersContext.tsx
|
|
|
|
packages/noodl-editor/src/editor/src/utils/
|
|
└── semanticLayers.ts
|
|
```
|
|
|
|
---
|
|
|
|
## Success Criteria
|
|
|
|
- [ ] All nodes correctly categorized into layers
|
|
- [ ] Toggling layers shows/hides nodes
|
|
- [ ] Connections styled appropriately when crossing layers
|
|
- [ ] Presets work correctly
|
|
- [ ] Keyboard shortcuts functional
|
|
- [ ] Performance acceptable (< 100ms to toggle)
|
|
- [ ] Clear indication when filter is active
|
|
|
|
---
|
|
|
|
## Future Enhancements
|
|
|
|
- **Custom layers** - Let users define their own categorizations
|
|
- **Layer locking** - Prevent editing nodes in certain layers
|
|
- **Layer-based minimap** - Color-coded minimap by layer
|
|
- **Save filter as preset** - Save custom combinations
|
|
- **Per-component layer memory** - Remember last layer state per component
|
|
|
|
---
|
|
|
|
## Risks & Mitigations
|
|
|
|
| Risk | Mitigation |
|
|
|------|------------|
|
|
| Some nodes hard to categorize | Add "Other" category, let user recategorize |
|
|
| Performance with many nodes | Use efficient DOM updates, consider virtualization |
|
|
| Confusion about hidden nodes | Clear indicator, easy "show all" escape hatch |
|
|
| Selection in hidden layers | Either prevent or show warning |
|
|
|
|
---
|
|
|
|
## Dependencies
|
|
|
|
- VIEW-000 Foundation (for categorization utilities)
|
|
- Canvas editor integration
|
|
|
|
## Blocks
|
|
|
|
- None (independent view)
|
|
|
|
## Runtime Integration (PRESERVE EXISTING)
|
|
|
|
**This view modifies the canvas display, so it MUST preserve the existing runtime debugging features.**
|
|
|
|
### What Must Keep Working
|
|
|
|
When layers are filtered, the following must still function:
|
|
|
|
| Feature | Behavior with hidden layers |
|
|
|---------|----------------------------|
|
|
| **Node highlighting** | Hidden nodes should still flash (at reduced opacity if faded) |
|
|
| **Data flow animation** | Connections to hidden nodes should animate (dotted style) |
|
|
| **Debug inspector** | Should still work on faded/hidden nodes |
|
|
| **Click-to-select in preview** | Should reveal hidden node if clicked |
|
|
|
|
### Implementation Consideration
|
|
|
|
The layer system should be a **visual filter only**, not a functional filter. The runtime doesn't care about layers - it still executes all nodes. We're just changing what the user sees.
|
|
|
|
```typescript
|
|
// WRONG: Don't exclude nodes from runtime
|
|
nodes.filter(n => isLayerVisible(n)).forEach(n => n.execute());
|
|
|
|
// RIGHT: Only change visual presentation
|
|
nodes.forEach(node => {
|
|
const isVisible = isLayerVisible(node);
|
|
node.setOpacity(isVisible ? 1.0 : 0.2);
|
|
// Runtime highlighting still works on faded nodes
|
|
});
|
|
```
|
|
|
|
### Auto-Reveal on Activity
|
|
|
|
Consider: when a hidden node fires (lights up due to runtime activity), should it:
|
|
1. **Stay hidden/faded** - Consistent filtering, but user might miss activity
|
|
2. **Temporarily reveal** - Shows what's happening, but breaks filter
|
|
3. **Pulse indicator** - Small badge shows "activity in hidden layers"
|
|
|
|
Recommend option 3 - shows activity without breaking the filter.
|
|
|
|
---
|
|
|
|
## Alternative: Non-Invasive Implementation
|
|
|
|
If modifying the canvas is too risky, an alternative is a **read-only layer view** that:
|
|
|
|
1. Creates a simplified representation of the canvas
|
|
2. Shows only selected layers in this separate view
|
|
3. Click to jump to actual canvas location
|
|
|
|
This would be safer but less integrated.
|