diff --git a/codebase/architecture.md b/codebase/architecture.md new file mode 100644 index 0000000..a317e95 --- /dev/null +++ b/codebase/architecture.md @@ -0,0 +1 @@ +lorem ipsum dolor sit amet diff --git a/codebase/architecture/core-concepts.md b/codebase/architecture/core-concepts.md new file mode 100644 index 0000000..3cbac32 --- /dev/null +++ b/codebase/architecture/core-concepts.md @@ -0,0 +1,84 @@ +# Core Concepts + +Understanding these fundamental concepts is essential for working with the OpenNoodl codebase. + +## Nodes + +**Nodes** are the basic building blocks of Noodl applications. They represent discrete units of functionality that can be connected together to create complex behaviors. + +### Node Characteristics + +- **Inputs**: Data or signals flowing into the node +- **Outputs**: Data or signals flowing out of the node +- **Parameters**: Configuration settings for the node +- **State**: Internal data maintained by the node + +## Connections + +**Connections** link outputs from one node to inputs of another, creating a flow of data and control through the application. + +### Connection Types + +- **Data Connections**: Transfer values between nodes +- **Signal Connections**: Trigger actions and events +- **Property Connections**: Bind UI properties to data + +## Signals + +**Signals** are events that trigger actions in the node graph. They represent moments in time when something happens. + +### Signal Flow + +``` +User Click → Button Node → Logic Node → UI Update +``` + +## Components + +**Components** are reusable collections of nodes that can be instantiated multiple times with different parameters. + +### Component Benefits + +- Encapsulation of functionality +- Reusability across projects +- Simplified node graphs +- Better organization + +## Data Flow + +Data in Noodl follows a **reactive** pattern where changes automatically propagate through connected nodes. + +### Evaluation Order + +1. Input changes trigger node re-evaluation +2. Node processes inputs and updates outputs +3. Connected nodes receive new values +4. Process continues through the graph + +## State Management + +### Local State + +- Maintained within individual nodes +- Persists during the node's lifecycle +- Not shared between node instances + +### Global State + +- Shared across the entire application +- Accessible from any node +- Managed through special state nodes + +## Runtime Environment + +### Execution Context + +- JavaScript engine for custom code +- Sandboxed environment for security +- Access to browser APIs and Noodl runtime + +### Performance Model + +- Lazy evaluation (only compute when needed) +- Efficient diff algorithms +- Optimized rendering pipeline diff --git a/codebase/architecture/data-flow.md b/codebase/architecture/data-flow.md new file mode 100644 index 0000000..f085c9e --- /dev/null +++ b/codebase/architecture/data-flow.md @@ -0,0 +1,155 @@ +# Data Flow + +This document explains how data flows through the OpenNoodl system, from user interactions to UI updates. + +## Overview + +Noodl uses a **reactive data flow** model where changes automatically propagate through connected nodes, similar to spreadsheet formulas or reactive programming frameworks. + +## Signal Propagation + +### Basic Flow + +``` +Input Change → Node Evaluation → Output Update → Connected Nodes +``` + +### Example Flow + +``` +Text Input → String Node → Text Display + ↓ ↓ ↓ + "hello" toUpperCase "HELLO" +``` + +## Evaluation System + +### Lazy Evaluation + +- Nodes only execute when their inputs change +- Outputs are cached until inputs change +- Prevents unnecessary computations + +### Dependency Tracking + +```javascript +// When NodeA.output connects to NodeB.input +NodeA.addDependent(NodeB); +NodeB.addDependency(NodeA); + +// When NodeA.output changes +NodeA.notifyDependents(); // Triggers NodeB.evaluate() +``` + +### Evaluation Order + +1. **Topological Sort**: Determine execution order +2. **Batch Updates**: Group related changes +3. **Execute Nodes**: Run in dependency order +4. **Update UI**: Render changes to screen + +## Event System + +### Event Types + +- **User Events**: Click, hover, input, etc. +- **System Events**: Load, resize, timer, etc. +- **Custom Events**: Application-specific signals + +### Event Handling + +``` +Event Source → Event Node → Signal Output → Action Nodes +``` + +## Data Transformation Pipeline + +### Input Processing + +1. **Validation**: Check input types and constraints +2. **Transformation**: Convert data formats if needed +3. **Caching**: Store processed values + +### Node Execution + +1. **Gather Inputs**: Collect all input values +2. **Execute Logic**: Run node-specific functionality +3. **Generate Outputs**: Produce result values +4. **Emit Signals**: Trigger connected events + +### Output Distribution + +1. **Update Connections**: Send values to connected inputs +2. **Trigger Dependents**: Notify dependent nodes +3. **Schedule UI Updates**: Queue rendering changes + +## State Synchronization + +### Local State Flow + +``` +Node Internal State ← → Node Outputs → Connected Inputs +``` + +### Global State Flow + +``` +Global State Store ← → State Nodes ← → Application Nodes +``` + +### External Data Flow + +``` +API/Database ← → Data Nodes ← → Application Logic +``` + +## Performance Optimizations + +### Change Detection + +- **Reference Equality**: Fast comparison for objects +- **Deep Comparison**: Thorough check when needed +- **Dirty Flagging**: Mark changed nodes for re-evaluation + +### Batching + +- **Synchronous Updates**: Group immediate changes +- **Asynchronous Updates**: Defer expensive operations +- **Frame Scheduling**: Align with browser rendering + +### Memoization + +```javascript +class OptimizedNode { + evaluate() { + const inputHash = this.getInputHash(); + if (inputHash === this.lastInputHash) { + return this.cachedOutput; // Skip computation + } + + this.cachedOutput = this.compute(); + this.lastInputHash = inputHash; + return this.cachedOutput; + } +} +``` + +## Debugging Data Flow + +### Flow Visualization + +- Visual indicators show active connections +- Animation highlights data propagation +- Debugging panels show current values + +### Performance Monitoring + +- Execution time tracking +- Update frequency analysis +- Memory usage monitoring + +### Common Issues + +- **Circular Dependencies**: Detect and prevent infinite loops +- **Performance Bottlenecks**: Identify slow nodes +- **Memory Leaks**: Track unreleased references diff --git a/codebase/architecture/overview.md b/codebase/architecture/overview.md new file mode 100644 index 0000000..5fe30d2 --- /dev/null +++ b/codebase/architecture/overview.md @@ -0,0 +1,142 @@ +# System Architecture + +OpenNoodl is a visual programming platform consisting of several key components that work together to provide a seamless low-code development experience. + +## High-Level Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Visual Editor │ │ Runtime Engine │ │ Cloud Services│ +│ │ │ │ │ │ +│ • Node Graph │◄──►│ • Node Runtime │◄──►│ • Deploy │ +│ • Property │ │ • Data Flow │ │ • Sync │ +│ Panels │ │ • Event System │ │ • Collaboration │ +│ • Preview │ │ • Asset Mgmt │ │ │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +## Core Components + +### 1. Visual Editor + +The main interface where users create applications by connecting nodes visually. + +**Key Features:** + +- Drag-and-drop node creation +- Visual connection system +- Property editing panels +- Live preview functionality +- Project management + +**Technologies:** + +- React-based UI +- Canvas rendering for node graph +- WebSocket for real-time updates + +### 2. Runtime Engine + +The execution environment that runs the node graphs created in the visual editor. + +**Key Features:** + +- Node execution system +- Data flow management +- Event propagation +- State management +- Asset handling + +**Technologies:** + +- Node.js runtime +- Custom evaluation engine +- WebSocket communication + +### 3. Node System + +A plugin-based architecture where functionality is provided through nodes. + +**Node Categories:** + +- **UI Nodes**: Visual components (buttons, inputs, etc.) +- **Logic Nodes**: Control flow and data processing +- **Data Nodes**: Database and API interactions +- **Utility Nodes**: Helper functions and transformations + +### 4. Project Structure + +Applications are organized as projects containing: + +- Node graphs (visual logic) +- Assets (images, fonts, etc.) +- Styles and themes +- Configuration files +- Custom code modules + +## Data Flow Architecture + +### Signal System + +Nodes communicate through a signal-based system: + +1. **Input Signals**: Data flowing into a node +2. **Output Signals**: Data flowing out of a node +3. **Connection**: Links between output and input signals +4. **Evaluation**: Automatic recalculation when inputs change + +### Event System + +User interactions and system events trigger cascading updates: + +``` +User Interaction → Event Node → Logic Nodes → UI Updates +``` + +## Extensibility + +### Plugin Architecture + +- Custom node development +- Third-party integrations +- Module system for reusable components +- API for external tool integration + +### Custom Code Integration + +- JavaScript modules +- External library imports +- Custom function definitions +- Advanced data processing + +## Performance Considerations + +### Optimization Strategies + +- Lazy evaluation of node graphs +- Efficient diff algorithms for UI updates +- Asset caching and optimization +- WebSocket connection pooling + +### Scalability + +- Modular architecture for easy scaling +- Plugin system for feature extension +- Cloud deployment options +- Performance monitoring and profiling + +## Security + +### Code Execution + +- Sandboxed JavaScript execution +- Input validation and sanitization +- Resource usage limits +- Secure asset handling + +### Data Protection + +- Encrypted data transmission +- Secure authentication +- Privacy-compliant data handling +- Audit trails for sensitive operations diff --git a/codebase/build-test.md b/codebase/build-test.md new file mode 100644 index 0000000..69454f7 --- /dev/null +++ b/codebase/build-test.md @@ -0,0 +1,43 @@ +# Building and Testing + +This guide covers the build process and testing procedures for OpenNoodl. + +## Building the Project + +### Development Build + +```bash +npm run dev +# or +yarn dev +``` + +### Build + +```bash +# Desktop app (Electron) +npm run build:editor + +# Extract executables into /dist folder +npm run build:editor:pack +``` + +## Testing + +### Running Tests + +```bash +# Run editor tests +npm run test:editor + +# Run platform tests +npm run test:platform +``` + +## Continuous Integration + +Our CI pipeline runs on GitHub Actions: + +- Automated testing on pull requests +- Build verification for all platforms +- Code quality checks (ESLint, Prettier) diff --git a/codebase/contributing.md b/codebase/contributing.md new file mode 100644 index 0000000..5f7dffb --- /dev/null +++ b/codebase/contributing.md @@ -0,0 +1,89 @@ +# Contributing Guidelines + +Thank you for your interest in contributing to OpenNoodl! This document outlines our development process and guidelines. + +## Code of Conduct + +Please read and follow our [Code of Conduct](https://github.com/noodlapp/noodl/blob/main/CODE_OF_CONDUCT.md). + +## Getting Started + +1. **Fork the repository** on GitHub +2. **Clone your fork** locally +3. **Create a feature branch** from `main` +4. **Make your changes** following our guidelines +5. **Test your changes** thoroughly +6. **Submit a pull request** + +## Development Workflow + +### Branch Naming + +Use descriptive branch names: + +- `feature/add-new-node-type` +- `bugfix/fix-memory-leak` +- `docs/update-api-reference` + +### Commit Messages + +Follow conventional commit format: + +``` +type(scope): description + +feat(nodes): add new Chart node +fix(editor): resolve connection rendering issue +docs(api): update JavaScript API examples +``` + +### Pull Request Process + +1. **Update documentation** if needed +2. **Add tests** for new features +3. **Ensure all tests pass** +4. **Request review** from maintainers +5. **Address feedback** promptly + +## Coding Standards + +### JavaScript/TypeScript + +- Use ESLint and Prettier configurations +- Follow existing code patterns +- Add JSDoc comments for public APIs +- Use TypeScript for new code when possible + +### Testing + +- Write unit tests for new features +- Update existing tests when modifying functionality +- Ensure test coverage doesn't decrease + +## Issue Guidelines + +### Reporting Bugs + +- Use the bug report template +- Include reproduction steps +- Provide system information +- Add relevant screenshots/logs + +### Feature Requests + +- Use the feature request template +- Explain the use case and motivation +- Consider implementation complexity +- Discuss with maintainers first for major changes + +## Review Process + +- All PRs require at least one review +- Address feedback before merging +- Squash commits when merging + +## Getting Help + +- Join our [Discord](https://discord.com/invite/23xU2hYrSJ) +- Check existing [issues](https://github.com/noodlapp/noodl/issues) +- Read the [development docs](./overview.md) diff --git a/codebase/development-setup.md b/codebase/development-setup.md new file mode 100644 index 0000000..c41a8cb --- /dev/null +++ b/codebase/development-setup.md @@ -0,0 +1,77 @@ +# Development Setup + +This guide will help you set up your development environment to contribute to OpenNoodl. + +## Prerequisites + +- **Node.js** (version 16 or higher) +- **npm** or **yarn** +- **Git** +- **Code editor** (VS Code recommended) + +## Clone and Setup + +1. **Clone the repository** + + ```bash + git clone https://github.com/noodlapp/noodl.git + cd noodl + ``` + +2. **Install dependencies** + + ```bash + npm install + # or + yarn install + ``` + +3. **Build the project** + + ```bash + npm run build + # or + yarn build + ``` + +4. **Start development server** + ```bash + npm run dev + # or + yarn dev + ``` + +## Development Environment + +### Recommended VS Code Extensions + +- ESLint +- Prettier + +### Environment Variables + +Create a `.env` file in the root directory: + +```env +# Add any required environment variables here +NODE_ENV=development +``` + +## Common Issues + +### Build Failures + +- Ensure Node.js version is 16+ +- Clear node_modules and reinstall dependencies +- Check for platform-specific build requirements + +### Hot Reload Issues + +- Restart the development server +- Check for syntax errors in recent changes + +## Next Steps + +- Read the [Contributing Guidelines](./contributing.md) +- Explore the [Project Structure](./structure/folders.md) +- Check out [Building and Testing](./build-test.md) diff --git a/codebase/guides/adding-nodes.md b/codebase/guides/adding-nodes.md new file mode 100644 index 0000000..61d9d35 --- /dev/null +++ b/codebase/guides/adding-nodes.md @@ -0,0 +1,256 @@ +# Adding New Nodes + +This guide explains how to create custom nodes for the OpenNoodl platform. + +## Node Basics + +Nodes are the fundamental building blocks of Noodl applications. Each node has: + +- **Inputs**: Data flowing into the node +- **Outputs**: Data flowing out of the node +- **Parameters**: Configuration options +- **Implementation**: The actual functionality + +## Creating a Simple Node + +### 1. Node Definition + +Create a new file in `/packages/noodl-core-nodes/src/`: + +```javascript +// MyCustomNode.js +import * as Noodl from "@noodl/noodl-sdk"; + +const MyCustomNode = Noodl.defineNode( { + static displayName = "My Custom Node"; + static category = "Utilities"; + + static inputs = { + value: { type: "string", displayName: "Input Value" }, + trigger: { type: "signal", displayName: "Trigger" }, + }; + + static outputs = { + result: { type: "string", displayName: "Result" }, + done: { type: "signal", displayName: "Done" }, + }; + + constructor() { + super(); + this.inputs.trigger = () => this.execute(); + } + + execute() { + const inputValue = this.inputs.value || ""; + const result = inputValue.toUpperCase(); + + this.outputs.result = result; + this.outputs.done(); + } +}) + +module.exports = MyCustomNode; +``` + +### 2. Register the Node + +Add your node to the registry in `/packages/noodl-core-nodes/src/index.js`: + +```javascript +const MyCustomNode = require("./MyCustomNode"); + +module.exports = { + // ...existing nodes + MyCustomNode: MyCustomNode, +}; +``` + +## Node Types and Categories + +### Common Node Categories + +- **UI**: Visual components (Button, Text, Input) +- **Logic**: Control flow (Condition, Loop, Switch) +- **Data**: Data manipulation (Object, Array, String) +- **Events**: User interactions (Click, Hover, Key) +- **Utilities**: Helper functions (Debug, Timer, Math) + +### Input/Output Types + +- `string`: Text data +- `number`: Numeric values +- `boolean`: True/false values +- `object`: Complex data structures +- `signal`: Event triggers +- `array`: Lists of items +- `color`: Color values +- `image`: Image references + +## Advanced Node Features + +### Dynamic Inputs/Outputs + +```javascript +const DynamicNode = Noodl.defineNode({ + static getInputs(nodeModel) { + const inputs = { count: { type: "number" } }; + + const count = nodeModel.parameters.count || 1; + for (let i = 0; i < count; i++) { + inputs[`input${i}`] = { type: "string" }; + } + + return inputs; + } + + static getOutputs(nodeModel) { + // Similar dynamic output generation + } +}) +``` + +### Node Parameters + +```javascript +const ConfigurableNode = Noodl.defineNode({ + static parameters = { + mode: { + type: "enum", + options: ["Add", "Subtract", "Multiply"], + default: "Add", + }, + precision: { + type: "number", + default: 2, + min: 0, + max: 10, + }, + }; +}) +``` + +### State Management + +```javascript +const StatefulNode = Noodl.defineNode({ + constructor() { + super(); + this.state = { + counter: 0, + history: [], + }; + } + + execute() { + this.state.counter++; + this.state.history.push(new Date()); + + this.outputs.count = this.state.counter; + } +}) +``` + +## UI Component Nodes + +### React Component Integration + +```javascript +const MyUINode = Noodl.defineNode({ + static displayName = "Custom Button"; + static category = "UI"; + + static getReactComponent() { + return function CustomButton(props) { + return ( + + ); + }; + } + + static inputs = { + label: { type: "string", displayName: "Label" }, + onClick: { type: "signal", displayName: "Click" }, + }; +}) +``` + +## Testing Nodes + +### Unit Tests + +```javascript +// MyCustomNode.test.js +const MyCustomNode = require("./MyCustomNode"); + +describe("MyCustomNode", () => { + let node; + + beforeEach(() => { + node = new MyCustomNode(); + }); + + test("converts input to uppercase", () => { + node.inputs.value = "hello world"; + node.execute(); + + expect(node.outputs.result).toBe("HELLO WORLD"); + }); + + test("triggers done signal", () => { + const doneSpy = jest.fn(); + node.outputs.done = doneSpy; + + node.execute(); + + expect(doneSpy).toHaveBeenCalled(); + }); +}); +``` + +## Best Practices + +### Node Design + +- Keep nodes focused on a single responsibility +- Use clear, descriptive names for inputs/outputs +- Provide helpful documentation and examples +- Handle edge cases gracefully + +### Performance + +- Avoid heavy computations in the constructor +- Cache expensive operations when possible +- Use lazy evaluation for optional features +- Minimize memory usage + +### Error Handling + +```javascript +execute() { + try { + // Node logic here + } catch (error) { + this.sendError('MyCustomNode', error.message) + } +} +``` + +## Publishing Custom Nodes + +For nodes that should be available to the community: + +1. Create a separate npm package +2. Follow the naming convention: `noodl-node-*` +3. Include proper documentation and examples +4. Submit to the Noodl community registry + +## Debugging Nodes + +Use the built-in debugging tools: + +- Console logging within node execution +- Visual debugging in the editor +- Unit tests for isolated testing +- Integration tests for workflow testing diff --git a/codebase/nodes/context.md b/codebase/nodes/context.md new file mode 100644 index 0000000..c005b41 --- /dev/null +++ b/codebase/nodes/context.md @@ -0,0 +1,374 @@ +--- +id: context +title: Node Context +--- + +# Node Context + +NodeContext (`nodecontext.js`) is the shared runtime environment for all nodes. It provides services, manages the update cycle, and coordinates component execution. + +## Purpose + +- Manages the node update cycle +- Provides shared services (timers, events, etc.) +- Maintains global state +- Coordinates component lifecycle +- Handles module/component loading + +## Creating a Context + +```javascript +const NodeContext = require("./nodecontext"); + +const context = new NodeContext({ + platform: "browser", // or 'nodejs' + // ... other options +}); +``` + +## Core Services + +### Node Register + +Registry for all node types: + +```javascript +context.nodeRegister.register(nodeDefinition); +const node = context.nodeRegister.createNode("My.Node", "id", scope); +const metadata = context.nodeRegister.getNodeMetadata("My.Node"); +const hasNode = context.nodeRegister.hasNode("My.Node"); +``` + +### Timer Scheduler + +Manages timeouts and intervals: + +```javascript +const timerId = context.timerScheduler.setTimeout(() => { + console.log("Timer fired"); +}, 1000); + +context.timerScheduler.clearTimeout(timerId); + +const intervalId = context.timerScheduler.setInterval(() => { + console.log("Interval tick"); +}, 500); + +context.timerScheduler.clearInterval(intervalId); +``` + +### Event Emitter + +Context inherits EventEmitter: + +```javascript +context.on("eventName", (data) => { + console.log("Event received:", data); +}); + +context.emit("eventName", { key: "value" }); +``` + +## Update Cycle + +### Dirty Flagging + +Nodes flag themselves dirty when they need to update: + +```javascript +context.nodeIsDirty(node); +``` + +### Scheduling Updates + +Request an update cycle: + +```javascript +context.scheduleUpdate(); +``` + +### Update Execution + +The update cycle runs dirty nodes: + +```javascript +context.update(); +``` + +This: + +1. Collects all dirty nodes +2. Sorts by dependency order +3. Calls `update()` on each node +4. Handles `_updateDependencies` changes +5. Iterates until no nodes are dirty (max 10 iterations) + +### After Update Callbacks + +Execute code after the current update cycle: + +```javascript +context.scheduleAfterUpdate(() => { + console.log("Update cycle complete"); +}); +``` + +### Next Frame Callbacks + +Execute code on the next frame: + +```javascript +context.scheduleNextFrame(() => { + console.log("Next frame"); +}); +``` + +## Global State + +Share data across all nodes: + +```javascript +// Set global value +context.setGlobalValue("theme", "dark"); + +// Get global value +const theme = context.getGlobalValue("theme"); +``` + +Global values can be: + +- Simple values (strings, numbers, booleans) +- Objects +- Arrays +- Functions + +## Component Management + +### Root Component + +Set the root component: + +```javascript +context.setRootComponent(rootComponentInstance); +``` + +### Component Registry + +Register components for reuse: + +```javascript +context.registerComponentModel(componentModel); +context.deregisterComponentModel(componentModel); +``` + +### Component Loading + +Load external component bundles: + +```javascript +const bundle = await context.fetchComponentBundle("ComponentName"); +``` + +## Time Management + +Get current time: + +```javascript +const now = context.getCurrentTime(); +``` + +This returns: + +- Browser: `performance.now()` +- Node.js: High-resolution time + +## Variants System + +The context maintains variant state: + +```javascript +context.variants = new Variants(); + +// Set active variants +context.variants.setActiveVariants(["mobile", "dark"]); + +// Check if variant is active +if (context.variants.isActive("mobile")) { + // Mobile variant is active +} +``` + +## Debug Support + +### Inspector Updates + +Debug inspectors can monitor node state: + +```javascript +context.onDebugInspectorsUpdated([{ nodeId: "node1", portName: "value" }]); +``` + +### Debug Mode + +Check if in debug mode: + +```javascript +if (context.editorConnection) { + // Running in editor +} +``` + +## Platform Abstraction + +Context provides platform-specific implementations: + +```javascript +if (context.platform === "browser") { + // Browser-specific code +} else if (context.platform === "nodejs") { + // Node.js-specific code +} +``` + +## Reset + +Reset the entire context: + +```javascript +context.reset(); +``` + +This: + +- Clears all timers +- Resets global values +- Clears event listeners +- Resets component registry + +## Advanced Usage + +### Custom Services + +Add custom services to context: + +```javascript +context.myCustomService = { + doSomething() { + // Custom logic + }, +}; + +// Access from nodes +this.context.myCustomService.doSomething(); +``` + +### Event-Driven Updates + +Use events to coordinate updates: + +```javascript +context.on("dataChanged", () => { + context.scheduleUpdate(); +}); + +// From a node +this.context.emit("dataChanged"); +``` + +### Performance Monitoring + +Track update performance: + +```javascript +const startTime = context.getCurrentTime(); +context.update(); +const duration = context.getCurrentTime() - startTime; +console.log(`Update took ${duration}ms`); +``` + +## Example: Context Lifecycle + +```javascript +// Create context +const context = new NodeContext({ platform: "browser" }); + +// Register node types +context.nodeRegister.register(MyNodeDefinition); + +// Create root scope and component +const rootScope = new NodeScope(context, null); +const rootComponent = await rootScope.createNode("App", "root"); +context.setRootComponent(rootComponent); + +// Run update loop +function updateLoop() { + context.update(); + context.scheduleNextFrame(updateLoop); +} +updateLoop(); + +// Clean up +context.reset(); +``` + +## Example: Coordinating Updates + +```javascript +// Node A sets a value +class NodeA { + doWork() { + this.context.setGlobalValue("sharedData", newValue); + this.context.scheduleUpdate(); + } +} + +// Node B reacts to changes +class NodeB { + update() { + const data = this.context.getGlobalValue("sharedData"); + this.processData(data); + } +} +``` + +## Example: Async Operations + +```javascript +// Schedule async work after update +context.scheduleAfterUpdate(async () => { + const result = await fetchData(); + + // Update nodes with result + context.setGlobalValue("fetchResult", result); + context.scheduleUpdate(); +}); +``` + +## Context Properties + +### Core Properties + +- `platform` - 'browser' or 'nodejs' +- `nodeRegister` - NodeRegister instance +- `timerScheduler` - TimerScheduler instance +- `variants` - Variants instance +- `editorConnection` - Editor connection (if running in editor) + +### State Properties + +- `_dirtyNodes` - Set of nodes needing update +- `_afterUpdateCallbacks` - Callbacks after update cycle +- `_nextFrameCallbacks` - Callbacks for next frame +- `_globalValues` - Global state storage +- `_componentModels` - Registered component models + +## Best Practices + +1. **Use scheduleUpdate sparingly** - Don't call on every small change +2. **Batch updates** - Use scheduleAfterUpdate for multiple changes +3. **Clean up timers** - Always clear timers in node cleanup +4. **Avoid infinite loops** - Update cycle has 10 iteration limit +5. **Use global values carefully** - Can create hidden dependencies +6. **Handle async properly** - Use scheduleAfterUpdate for async work +7. **Reset on cleanup** - Call context.reset() when done +8. **Monitor performance** - Watch for slow update cycles diff --git a/codebase/nodes/definition.md b/codebase/nodes/definition.md new file mode 100644 index 0000000..9562fcd --- /dev/null +++ b/codebase/nodes/definition.md @@ -0,0 +1,706 @@ +--- +id: definition +title: Node Definition +--- + +# Node Definition + +Node definitions describe the structure and behavior of node types. They define inputs, outputs, lifecycle hooks, and runtime methods. + +## Defining a Node + +Use `defineNode` from `nodedefinition.js` to create a node definition: + +```javascript +const { defineNode } = require("./nodedefinition"); + +const MyNode = defineNode({ + name: "My Node", + category: "Logic", + + inputs: { + value: { + type: "number", + default: 0, + displayName: "Input Value", + set(value) { + this._internal.value = value; + this.flagOutputDirty("result"); + }, + }, + }, + + outputs: { + result: { + type: "number", + getter() { + return this._internal.value * 2; + }, + }, + }, + + initialize() { + this._internal.value = 0; + }, +}); +``` + +## Metadata Properties + +### Required + +- `name` - Unique identifier for the node type +- `category` - Category for grouping ('Visual', 'Logic', 'Data', etc.) + +### Optional + +- `displayName` - Human-readable name shown in editor +- `docs` - URL to documentation +- `shortDesc` - Brief description +- `color` - Custom color theme +- `deprecated` - Mark as deprecated +- `singleton` - Only one instance allowed per component +- `allowChildren` - Node can have visual children +- `allowAsChild` - Node can be placed as a child +- `module` - Associated module name +- `version` - Node version +- `searchTags` - Additional search keywords + +## Input Definition + +Each input has: + +```javascript +inputs: { + inputName: { + type: 'string' | 'number' | 'boolean' | 'signal' | {...}, + default: /* default value */, + displayName: 'Display Name', + group: 'Group Name', + set(value) { + // Handle input value change + }, + index: 0, // Sort order + tooltip: 'Help text', + tab: 'Tab Name', // Property panel tab + allowVisualStates: true, // Can be set per visual state + exportToEditor: true, // Show in editor (default: true) + inputPriority: 0 // Higher priority inputs are set first + } +} +``` + +## Input Types + +### Simple Type Format + +For basic types, use a string: + +```javascript +inputs: { + text: { type: 'string', default: 'Hello' }, + count: { type: 'number', default: 0 }, + enabled: { type: 'boolean', default: true } +} +``` + +### Object Type Format + +All types can be specified as objects for additional configuration: + +```javascript +inputs: { + name: { + type: { + name: 'string', + allowEditOnly: true // Only allow editing, not connections + }, + default: 'Default Name' + } +} +``` + +Common type object properties: + +- `name` - The type name (required) +- `allowEditOnly` - Only allow manual editing, no connections +- `allowConnectionsOnly` - Only allow connections, no manual editing +- `units` - Array of available units (for numbers) +- `defaultUnit` - Default unit to use +- `enums` - Array of enum values +- `properties` - Array of properties (for proplist type) + +### Primitive Types + +```javascript +inputs: { + // String + text: { type: 'string', default: 'Hello' }, + + // String with restrictions + fixedText: { + type: { + name: 'string', + allowEditOnly: true + }, + default: 'Fixed' + }, + + // Number + count: { type: 'number', default: 0 }, + + // Boolean + enabled: { type: 'boolean', default: true } +} +``` + +### Signal Type + +For edge-triggered behavior: + +```javascript +inputs: { + trigger: { + type: 'signal', + valueChangedToTrue() { + // Called when signal fires + this.performAction(); + } + }, + + // Or with object format + execute: { + type: { + name: 'signal', + allowConnectionsOnly: true + }, + valueChangedToTrue() { + this.run(); + } + } +} +``` + +**Note**: Signal inputs use `valueChangedToTrue()` instead of `set()`. + +### Enum Type + +For dropdown selections: + +```javascript +inputs: { + // Simple enum with strings + size: { + type: { + name: 'enum', + enums: ['small', 'medium', 'large'] + }, + default: 'medium' + }, + + // Enum with labels and values + alignment: { + type: { + name: 'enum', + enums: [ + { label: 'Left', value: 'left' }, + { label: 'Center', value: 'center' }, + { label: 'Right', value: 'right' } + ], + allowEditOnly: true + }, + default: 'left', + set(value) { + this._internal.alignment = value; + this.updateAlignment(); + } + } +} +``` + +### Visual Types + +```javascript +inputs: { + // Color + color: { + type: 'color', + default: '#ffffff', + set(value) { + // Value is resolved from color styles automatically + this._internal.element.style.backgroundColor = value; + } + }, + + // Color with restrictions + fixedColor: { + type: { + name: 'color', + allowEditOnly: true + }, + default: '#000000' + }, + + // Image + image: { + type: 'image', + set(value) { + this._internal.element.src = value; + } + }, + + // Text Style + textStyle: { + type: 'textStyle', + set(value) { + this.applyTextStyle(value); + } + } +} +``` + +### Complex Types + +```javascript +inputs: { + // Array + items: { + type: 'array', + set(value) { + // Arrays can be passed as JSON strings and are auto-parsed + this._internal.items = value; + } + }, + + // Array with restrictions + fixedArray: { + type: { + name: 'array', + allowEditOnly: true + } + }, + + // Object + data: { + type: 'object', + set(value) { + this._internal.data = value; + } + }, + + // Object with restrictions + config: { + type: { + name: 'object', + allowConnectionsOnly: true + } + } +} +``` + +### Dimension Type + +For values with units (px, %, em, etc.): + +```javascript +inputs: { + width: { + type: { + name: 'number', + units: ['px', '%', 'vw', 'vh'], + defaultUnit: 'px' + }, + default: 100, + set(value) { + // value is { value: 100, unit: 'px' } + this._internal.element.style.width = value.value + value.unit; + }, + setUnitType(unit) { + // Called when unit type changes + this._internal.widthUnit = unit; + } + } +} +``` + +### Component Type + +For component references: + +```javascript +inputs: { + component: { + type: 'component', + set(value) { + // value is component name string + this.loadComponent(value); + } + }, + + // Component with restrictions + fixedComponent: { + type: { + name: 'component', + allowEditOnly: true + } + } +} +``` + +### Custom Object Types + +For complex configurations: + +```javascript +inputs: { + style: { + type: { + name: 'proplist', + properties: [ + { name: 'color', type: 'color' }, + { name: 'size', type: 'number' }, + { name: 'enabled', type: 'boolean' } + ] + }, + set(value) { + // value is object with color, size, and enabled properties + this.applyStyle(value); + } + } +} +``` + +### Input Setters + +The `set` function is called when input value changes: + +```javascript +inputs: { + value: { + type: 'number', + set(value) { + // 'this' is the node instance + this._internal.value = value; + + // Flag outputs that depend on this input + this.flagOutputDirty('result'); + + // Or trigger immediate update + this.sendValue('result', this.calculate()); + } + } +} +``` + +### Input Properties + +#### Display Properties + +```javascript +{ + displayName: 'My Input', // Name shown in editor + editorName: 'Custom', // Alternative name for specific contexts + group: 'Configuration', // Property panel group + index: 10, // Sort order (higher = later) + tab: 'Advanced' // Property panel tab +} +``` + +#### Behavior Properties + +```javascript +{ + allowVisualStates: true, // Can have different values per visual state + exportToEditor: false, // Hide from editor + inputPriority: 100 // Higher priority = set earlier (default: 0) +} +``` + +#### Documentation Properties + +```javascript +{ + tooltip: 'Enter a number between 0 and 100', + popout: { + // Custom editor UI + type: 'colorpicker', + options: { showAlpha: true } + } +} +``` + +## Output Definition + +```javascript +outputs: { + outputName: { + type: 'number', + displayName: 'Output Name', + editorName: 'Custom Name', + group: 'Results', + index: 10, + getter() { + return this._internal.result; + }, + onFirstConnectionAdded() { + // Called when first connection is made to this output + this.startMonitoring(); + }, + onLastConnectionRemoved() { + // Called when last connection is removed + this.stopMonitoring(); + } + } +} +``` + +### Signal Outputs + +```javascript +outputs: { + done: { + type: "signal"; + // No getter needed for signals + } +} + +// To send signal: +this.sendSignalOnOutput("done"); +``` + +## Lifecycle Methods + +```javascript +{ + initialize() { + // Called once when node instance is created + this._internal = { + data: {}, + counter: 0 + }; + }, + + methods: { + customMethod() { + // Custom methods available on node instance + return this._internal.counter++; + }, + + anotherMethod(arg) { + // Methods can take arguments + this._internal.data[arg] = true; + } + } +} +``` + +## Dynamic Ports + +For numbered inputs like "Input 0", "Input 1": + +```javascript +{ + numberedInputs: { + 'input': { + type: 'number', + displayPrefix: 'Input', + group: 'Inputs', + defaultCount: 2, // Start with 2 inputs + createSetter(index) { + return function(value) { + this._internal.inputs[index] = value; + this.calculateSum(); + }; + } + } + }, + + numberedOutputs: { + 'output': { + type: 'number', + displayPrefix: 'Output', + createGetter(index) { + return function() { + return this._internal.outputs[index]; + }; + } + } + } +} +``` + +See [Dynamic Ports](dynamic-ports.md) for more details. + +## Advanced Features + +### Visual States Support + +```javascript +{ + visualStates: ['hover', 'pressed', 'disabled'], + inputs: { + backgroundColor: { + type: 'color', + allowVisualStates: true + } + } +} +``` + +### Variants Support + +```javascript +{ + useVariants: true; +} +``` + +### Children Support + +```javascript +{ + allowChildren: true, + allowChildrenWithCategory: ['Visual'] +} +``` + +### Dynamic Ports Metadata + +```javascript +{ + dynamicports: [ + { + condition: "enabled", + inputs: ["optionalInput1", "optionalInput2"], + outputs: ["optionalOutput"], + }, + ]; +} +``` + +### Export Control + +```javascript +inputs: { + internalInput: { + type: 'string', + exportToEditor: false // Hide from editor + } +} +``` + +## Complete Example + +```javascript +const MyComplexNode = defineNode({ + name: "My.Complex.Node", + displayName: "Complex Node", + category: "Logic", + color: "data", + docs: "https://docs.noodl.net/nodes/my-complex-node", + searchTags: ["advanced", "utility"], + + inputs: { + enabled: { + type: "boolean", + default: true, + displayName: "Enabled", + group: "General", + set(value) { + this._internal.enabled = value; + if (value) this.start(); + else this.stop(); + }, + }, + mode: { + type: { + name: "enum", + enums: [ + { label: "Simple", value: "simple" }, + { label: "Advanced", value: "advanced" }, + ], + allowEditOnly: true, + }, + default: "simple", + set(value) { + this._internal.mode = value; + this.updateMode(); + }, + }, + trigger: { + type: "signal", + valueChangedToTrue() { + this.execute(); + }, + }, + }, + + outputs: { + result: { + type: "string", + displayName: "Result", + getter() { + return this._internal.result; + }, + }, + done: { + type: "signal", + }, + }, + + initialize() { + this._internal = { + enabled: true, + mode: "simple", + result: "", + }; + }, + + methods: { + execute() { + if (!this._internal.enabled) return; + + this._internal.result = "executed in " + this._internal.mode + " mode"; + this.flagOutputDirty("result"); + this.sendSignalOnOutput("done"); + }, + + start() { + console.log("Node started"); + }, + + stop() { + console.log("Node stopped"); + }, + + updateMode() { + // Update based on mode + }, + }, +}); +``` + +## Registration + +After defining, register the node: + +```javascript +module.exports = MyNode; + +// In node library initialization: +nodeRegister.register(MyNode); +``` + +## Best Practices + +1. **Use descriptive names** - Make input/output names clear and self-documenting +2. **Provide defaults** - Always specify default values for inputs +3. **Group related inputs** - Use `group` property to organize property panel +4. **Document with tooltips** - Add helpful tooltips for complex inputs +5. **Handle undefined** - Check for undefined values in setters +6. **Use appropriate types** - Choose the right type for each input +7. **Use type objects when needed** - Use object format for `allowEditOnly`, `allowConnectionsOnly`, etc. +8. **Order logically** - Use `index` to order inputs meaningfully +9. **Clean up resources** - Use lifecycle methods to manage resources +10. **Flag outputs correctly** - Call `flagOutputDirty` when outputs change +11. **Test edge cases** - Verify behavior with various input combinations diff --git a/codebase/nodes/dynamic-ports.md b/codebase/nodes/dynamic-ports.md new file mode 100644 index 0000000..0aa4d28 --- /dev/null +++ b/codebase/nodes/dynamic-ports.md @@ -0,0 +1,371 @@ +--- +id: dynamic-ports +title: Dynamic Ports +--- + +# Dynamic Ports + +Dynamic ports allow nodes to have a variable number of inputs or outputs that are generated at runtime or based on configuration. + +## Use Cases + +- Numbered inputs (Input 0, Input 1, Input 2, etc.) +- Variable function arguments +- Dynamic object properties +- Conditional ports based on settings +- Generated ports from external schemas + +## Numbered Inputs + +The most common dynamic port pattern is numbered inputs: + +```javascript +const { defineNode } = require("./nodedefinition"); + +const SwitchNode = defineNode({ + name: "Logic.Switch", + displayName: "Switch", + + numberedInputs: { + value: { + type: "*", + displayPrefix: "Value", + group: "Values", + createSetter(index) { + return function (value) { + this._internal.values[index] = value; + this.updateOutput(); + }; + }, + }, + }, + + inputs: { + index: { + type: "number", + default: 0, + set(value) { + this._internal.currentIndex = value; + this.updateOutput(); + }, + }, + }, + + outputs: { + current: { + type: "*", + getter() { + const idx = this._internal.currentIndex; + return this._internal.values[idx]; + }, + }, + }, + + initialize() { + this._internal = { + values: {}, + currentIndex: 0, + }; + }, + + methods: { + updateOutput() { + this.flagOutputDirty("current"); + }, + }, +}); +``` + +## Numbered Input Properties + +### Configuration + +- `displayPrefix` - Prefix for port names (e.g., "Value" → "Value 0", "Value 1") +- `type` - Port type (can be '\*' for any type) +- `group` - Group name in property panel +- `defaultCount` - Default number of ports to create +- `createSetter(index)` - Function that returns setter for specific index + +### Registration + +Numbered inputs are automatically registered when the node is created. The system: + +1. Creates ports based on model data or defaultCount +2. Calls `createSetter(index)` for each port +3. Registers the port with name pattern: `{prefix}{index}` + +## Runtime Port Management + +### Adding Ports + +Ports can be added dynamically: + +```javascript +methods: { + addInput() { + const index = Object.keys(this._internal.inputs).length; + + // Register new input + this.registerInputIfNeeded(`input${index}`); + + // Initialize state + this._internal.inputs[index] = null; + } +} +``` + +### Removing Ports + +```javascript +methods: { + removeInput(index) { + this.deregisterInput(`input${index}`); + delete this._internal.inputs[index]; + } +} +``` + +## Dynamic Output Ports + +Similar pattern for outputs: + +```javascript +{ + numberedOutputs: { + 'result': { + type: 'number', + displayPrefix: 'Result', + createGetter(index) { + return function() { + return this._internal.results[index]; + }; + } + } + } +} +``` + +## Metadata Registration + +For editor integration, dynamic ports need metadata: + +```javascript +{ + dynamicports: [ + { + name: "value{index}", + type: "number", + plug: "input", + group: "Values", + index: 100, // Start index for sorting + }, + ]; +} +``` + +## Example: Function Node + +A node with variable arguments: + +```javascript +const FunctionNode = defineNode({ + name: "Logic.Function", + + numberedInputs: { + arg: { + type: "*", + displayPrefix: "Argument", + group: "Arguments", + defaultCount: 2, + createSetter(index) { + return function (value) { + this._internal.args[index] = value; + }; + }, + }, + }, + + inputs: { + execute: { + valueChangedToTrue() { + const args = Object.values(this._internal.args); + const result = this.executeFunction(args); + this.setOutput("result", result); + }, + }, + function: { + type: "string", + set(code) { + try { + this._internal.fn = new Function(...this.getArgNames(), code); + } catch (error) { + console.error("Function compilation error:", error); + } + }, + }, + }, + + outputs: { + result: { type: "*" }, + }, + + initialize() { + this._internal = { + args: {}, + fn: null, + }; + }, + + methods: { + getArgNames() { + return Object.keys(this._internal.args).map((i) => `arg${i}`); + }, + + executeFunction(args) { + if (!this._internal.fn) return undefined; + try { + return this._internal.fn(...args); + } catch (error) { + console.error("Function execution error:", error); + return undefined; + } + }, + }, +}); +``` + +## Example: Object Property Ports + +Generate ports from object schema: + +```javascript +const ObjectNode = defineNode({ + name: "Data.Object", + + inputs: { + schema: { + type: "object", + set(schema) { + this.updatePortsFromSchema(schema); + }, + }, + }, + + outputs: { + object: { + type: "object", + getter() { + return this._internal.data; + }, + }, + }, + + initialize() { + this._internal = { + data: {}, + schema: null, + }; + }, + + methods: { + updatePortsFromSchema(schema) { + // Remove old ports + if (this._internal.schema) { + Object.keys(this._internal.schema).forEach((key) => { + this.deregisterInput(key); + }); + } + + // Add new ports + Object.entries(schema).forEach(([key, config]) => { + this.registerInput(key, { + type: config.type || "string", + set: (value) => { + this._internal.data[key] = value; + this.flagOutputDirty("object"); + }, + }); + }); + + this._internal.schema = schema; + }, + }, +}); +``` + +## Port Naming Conventions + +### Numbered Ports + +- Use zero-based indexing: `value0`, `value1`, `value2` +- Consistent prefix: all ports share same prefix +- Sequential: no gaps in numbering + +### Dynamic Ports + +- Descriptive names: `user.name`, `user.email` +- Avoid special characters: use alphanumeric and dots/underscores +- Consistent casing: typically camelCase + +## Editor Integration + +### Port Discovery + +The editor discovers dynamic ports through: + +1. `dynamicports` metadata array +2. Runtime port inspection +3. Model port configuration + +### Port Configuration + +Dynamic ports can be configured in the model: + +```javascript +{ + type: 'My.Node', + id: 'node1', + ports: [ + { name: 'value0', type: 'number', plug: 'input' }, + { name: 'value1', type: 'number', plug: 'input' }, + { name: 'value2', type: 'number', plug: 'input' } + ] +} +``` + +## Performance Considerations + +1. **Limit port count** - Too many ports can slow the editor +2. **Lazy creation** - Only create ports when needed +3. **Batch registration** - Register multiple ports together +4. **Clean up unused** - Remove ports that are no longer needed + +## Testing Dynamic Ports + +```javascript +test("numbered inputs work correctly", () => { + const node = createNode("Logic.Switch", "test1"); + + // Set numbered inputs + node.setInputValue("value0", "A"); + node.setInputValue("value1", "B"); + node.setInputValue("value2", "C"); + + // Select index + node.setInputValue("index", 1); + + // Check output + expect(node.getOutputValue("current")).toBe("B"); +}); +``` + +## Best Practices + +1. **Use numbered inputs for arrays** - When order matters +2. **Provide default count** - Make common cases work out of box +3. **Document port patterns** - Explain how ports are named +4. **Validate port names** - Ensure they don't conflict +5. **Handle missing ports** - Gracefully handle undefined inputs +6. **Clean up on removal** - Deregister ports properly +7. **Update metadata** - Keep dynamicports array in sync +8. **Test edge cases** - Empty arrays, single items, large counts diff --git a/codebase/nodes/frontend-nodes.md b/codebase/nodes/frontend-nodes.md new file mode 100644 index 0000000..17b7468 --- /dev/null +++ b/codebase/nodes/frontend-nodes.md @@ -0,0 +1,832 @@ +--- +id: frontend-nodes +title: Frontend Nodes +--- + +# Frontend Nodes + +Frontend nodes are visual UI components built with React that extend the base node system with rendering capabilities. They are defined in `noodl-viewer-react` and handle DOM elements, styling, and user interactions. + +## Purpose + +- Render visual UI components +- Handle DOM manipulation and styling +- Support React component integration +- Provide visual hierarchy and layout +- Enable user interactions and events + +## Frontend vs Runtime Nodes + +### Runtime Nodes + +- Located in `packages/noodl-runtime/src/nodes` +- Pure logic and data processing +- No visual representation +- Defined with `defineNode()` +- Examples: Counter, Expression, REST + +### Frontend Nodes + +- Located in `packages/noodl-viewer-react/src/nodes` +- Visual UI components +- React-based rendering +- Defined with `createNodeFromReactComponent()` +- Examples: Group, Text, Button, Image + +## Creating a Frontend Node + +Frontend nodes are created using `createNodeFromReactComponent()` which wraps a React component with Noodl node capabilities. + +### Basic Structure + +```javascript +import { createNodeFromReactComponent } from "@noodl/react-component-node"; + +const MyVisualNode = createNodeFromReactComponent({ + name: "My.Visual.Node", + displayName: "My Visual Node", + category: "Visual", + + getReactComponent() { + // Return the React component to render + return "div"; // or a custom React component + }, + + inputProps: { + // Props passed to React component + }, + + inputCss: { + // CSS styles applied to component + }, + + outputProps: { + // Outputs triggered by React props + }, +}); + +export default MyVisualNode; +``` + +## Node Definition Structure + +### Core Properties + +```javascript +{ + name: 'My.Visual.Node', + displayName: 'My Visual Node', + displayNodeName: 'Visual Node', // Alternative display name + category: 'Visual', + docs: 'https://docs.noodl.net/nodes/my-visual-node', + + // Visual frame configuration + frame: { + dimensions: true, // Width/Height inputs + position: true, // Position/Transform inputs + margins: true, // Margin inputs + padding: true, // Padding inputs + align: true // Alignment inputs + }, + + // Visual features + allowChildren: true, // Can have child nodes + allowAsExportRoot: true, // Can be root of exported component + visualStates: ['hover', 'pressed'], // Supported visual states + useVariants: true, // Support variants + + // React integration + noodlNodeAsProp: false, // Pass node instance to React component + mountedInput: true, // Include 'Mounted' input + + getReactComponent() { + return MyReactComponent; // or 'div', 'span', etc. + } +} +``` + +## Input Types + +### Input Props + +Props passed directly to the React component: + +```javascript +inputProps: { + text: { + type: 'string', + displayName: 'Text', + group: 'General', + default: 'Hello', + set(value) { + // Optional: custom setter + this.props.text = value; + this.forceUpdate(); + } + }, + + enabled: { + type: 'boolean', + default: true, + // Prop path for nested props + propPath: 'config' // Sets this.props.config.enabled + }, + + // Node reference + targetNode: { + type: 'node', + set(node) { + this.props.target = node; + this.forceUpdate(); + } + } +} +``` + +### Input CSS + +CSS styles applied to the component: + +```javascript +inputCss: { + backgroundColor: { + type: 'color', + displayName: 'Background Color', + group: 'Style', + default: '#ffffff', + // Maps to CSS property (defaults to input name) + targetStyleProperty: 'backgroundColor' + }, + + fontSize: { + type: { + name: 'number', + units: ['px', 'em', 'rem'], + defaultUnit: 'px' + }, + default: 16 + }, + + borderRadius: { + type: 'number', + default: 0, + // Apply to specific styled element + styleTag: 'container' + } +} +``` + +### Default CSS + +Set default CSS styles: + +```javascript +defaultCss: { + display: 'flex', + flexDirection: 'column', + position: 'relative' +} +``` + +## Output Types + +### Output Props + +Outputs triggered by React component callbacks: + +```javascript +outputProps: { + // Signal output from callback + onClick: { + type: 'signal', + displayName: 'Click', + group: 'Events' + }, + + // Value output from callback + value: { + type: 'string', + displayName: 'Value', + getValue(event) { + // Extract value from event + return event.target.value; + }, + onChange(value) { + // Called when output changes + console.log('Value changed:', value); + } + }, + + // Multiple props with same callback + onMouseEvents: { + type: 'signal', + props: ['onMouseEnter', 'onMouseLeave'], + propPath: 'events' + } +} +``` + +## Visual Frame + +The `frame` property automatically adds standard visual inputs: + +```javascript +frame: { + // Adds Width, Height, Size Mode inputs + dimensions: true, + + // Adds custom dimension defaults + dimensions: { + defaultSizeMode: 'contentSize', + defaultWidth: 100, + defaultHeight: 100 + }, + + // Adds Position, Rotation, Scale, etc. + position: true, + + // Adds Margin inputs + margins: true, + + // Adds Padding inputs + padding: true, + + // Adds Align inputs + align: true +} +``` + +## React Component Integration + +### Simple HTML Element + +```javascript +getReactComponent() { + return 'div'; // Renders
+} +``` + +### Custom React Component + +```javascript +getReactComponent() { + class MyComponent extends React.Component { + componentDidMount() { + // React lifecycle - runs when component mounts + console.log('Component mounted'); + } + + componentWillUnmount() { + // React lifecycle - runs when component unmounts + console.log('Component unmounting'); + } + + render() { + const { text, enabled } = this.props; + return ( +
+ {text} +
+ ); + } + } + return MyComponent; +} +``` + +### Functional Component + +```javascript +getReactComponent() { + return function MyComponent(props) { + // Use React hooks for lifecycle + React.useEffect(() => { + // Runs after mount and updates + console.log('Component mounted or updated'); + + return () => { + // Cleanup - runs before unmount + console.log('Component unmounting'); + }; + }, []); // Empty deps = mount/unmount only + + return ( +
+ {props.children} +
+ ); + }; +} +``` + +## Node Instance Methods + +Frontend nodes have additional methods for DOM and styling: + +### Styling + +```javascript +methods: { + customMethod() { + // Set styles directly on DOM element + this.setStyle({ + backgroundColor: '#ff0000', + color: '#ffffff' + }); + + // Set styles on specific element (by styleTag) + this.setStyle({ + borderColor: '#000000' + }, 'container'); + + // Remove styles + this.removeStyle(['backgroundColor', 'color']); + + // Get current style value + const bgColor = this.getStyle('backgroundColor'); + } +} +``` + +### Force Update + +```javascript +methods: { + updateUI() { + // Force React re-render + this.forceUpdate(); + } +} +``` + +### DOM Access + +```javascript +methods: { + accessDOM() { + // Get React component ref + const ref = this.getRef(); + + // Get DOM element + const element = this.getDOMElement(); + + // Access inner React component + const innerRef = this.innerReactComponentRef; + } +} +``` + +## Children Management + +Frontend nodes can have visual children: + +```javascript +{ + allowChildren: true, + + methods: { + handleChildren() { + // Get all children + const children = this.getChildren(); + + // Add child at specific index + this.addChild(childNode, 0); + + // Remove child + this.removeChild(childNode); + + // Check if contains node + const contains = this.contains(someNode); + + // Get child count + const count = this.childrenCount; + } + } +} +``` + +## Visual States + +Visual states allow different styling based on interaction: + +```javascript +{ + visualStates: ['hover', 'pressed', 'focused', 'disabled'], + + inputCss: { + backgroundColor: { + type: 'color', + default: '#ffffff', + allowVisualStates: true // Can have different values per state + } + }, + + methods: { + handleInteraction() { + // Set current visual states + this.setVisualStates(['hover', 'pressed']); + + // Get current states + const states = this._getVisualStates(); + } + } +} +``` + +## Lifecycle Hooks + +Node-level lifecycle hooks (not React component lifecycle): + +```javascript +{ + initialize() { + // Called when node instance is created + this._internal.customData = {}; + }, + + nodeScopeDidInitialize() { + // Called after all nodes in component are created + this.setupConnections(); + }, + + _onNodeDeleted() { + // Called when node is being deleted + // Clean up resources here + if (this._internal.timerId) { + clearInterval(this._internal.timerId); + } + } +} +``` + +For React lifecycle methods, use them inside your React component: + +```javascript +{ + getReactComponent() { + return function MyComponent(props) { + React.useEffect(() => { + // Mount logic + return () => { + // Unmount logic + }; + }, []); + + return
{props.children}
; + }; + } +} +``` + +## Complete Example: Custom Button + +```javascript +import { createNodeFromReactComponent } from "@noodl/react-component-node"; + +const CustomButton = createNodeFromReactComponent({ + name: "Custom.Button", + displayName: "Custom Button", + category: "Visual", + docs: "https://docs.noodl.net/nodes/custom-button", + + frame: { + dimensions: { + defaultSizeMode: "contentSize", + }, + position: true, + margins: true, + padding: true, + }, + + visualStates: ["hover", "pressed", "disabled"], + useVariants: true, + + getReactComponent() { + return "button"; + }, + + defaultCss: { + display: "flex", + alignItems: "center", + justifyContent: "center", + cursor: "pointer", + border: "none", + outline: "none", + }, + + inputProps: { + label: { + type: "string", + displayName: "Label", + group: "General", + default: "Button", + set(value) { + this.props.children = value; + this.forceUpdate(); + }, + }, + + enabled: { + type: "boolean", + displayName: "Enabled", + group: "General", + default: true, + set(value) { + this.props.disabled = !value; + this.setVisualStates(value ? [] : ["disabled"]); + this.forceUpdate(); + }, + }, + }, + + inputCss: { + backgroundColor: { + type: "color", + displayName: "Background Color", + group: "Style", + default: "#007bff", + allowVisualStates: true, + }, + + textColor: { + type: "color", + displayName: "Text Color", + group: "Style", + default: "#ffffff", + targetStyleProperty: "color", + allowVisualStates: true, + }, + + fontSize: { + type: { + name: "number", + units: ["px", "em", "rem"], + defaultUnit: "px", + }, + displayName: "Font Size", + group: "Style", + default: 16, + }, + + borderRadius: { + type: "number", + displayName: "Border Radius", + group: "Style", + default: 4, + }, + }, + + outputProps: { + onClick: { + type: "signal", + displayName: "Click", + group: "Events", + }, + }, + + initialize() { + this._internal.clickCount = 0; + + // Add mouse event handlers + const addHandlers = () => { + const element = this.getDOMElement(); + if (!element) return; + + element.addEventListener("mouseenter", () => { + if (this.getInputValue("enabled")) { + const states = this._getVisualStates(); + if (!states.includes("hover")) { + this.setVisualStates([...states, "hover"]); + } + } + }); + + element.addEventListener("mouseleave", () => { + const states = this._getVisualStates().filter( + (s) => s !== "hover" && s !== "pressed" + ); + this.setVisualStates(states); + }); + + element.addEventListener("mousedown", () => { + if (this.getInputValue("enabled")) { + const states = this._getVisualStates(); + if (!states.includes("pressed")) { + this.setVisualStates([...states, "pressed"]); + } + } + }); + + element.addEventListener("mouseup", () => { + const states = this._getVisualStates().filter((s) => s !== "pressed"); + this.setVisualStates(states); + }); + }; + + this.scheduleAfterInputsHaveUpdated(addHandlers); + }, + + _onNodeDeleted() { + // Clean up event listeners + const element = this.getDOMElement(); + if (element) { + // Remove all listeners + const newElement = element.cloneNode(true); + element.parentNode.replaceChild(newElement, element); + } + }, + + methods: { + getClickCount() { + return this._internal.clickCount; + }, + }, +}); + +export default CustomButton; +``` + +## Style Tags + +Apply styles to specific nested elements: + +```javascript +{ + getReactComponent() { + return function MyComponent(props) { + return ( +
+
+ Header +
+
+ {props.children} +
+
+ ); + }; + }, + + inputCss: { + headerBackground: { + type: 'color', + styleTag: 'header', + targetStyleProperty: 'backgroundColor' + }, + + contentPadding: { + type: 'number', + styleTag: 'content', + targetStyleProperty: 'padding' + } + } +} +``` + +## Advanced CSS + +Users can write custom CSS: + +```javascript +// Automatically added to all frontend nodes +inputs: { + cssClassName: { + type: 'string', + displayName: 'CSS Class', + group: 'Advanced HTML' + }, + + styleCss: { + type: { + name: 'string', + codeeditor: 'text', + allowEditOnly: true + }, + displayName: 'CSS Style', + group: 'Advanced HTML', + default: '/* background-color: red; */' + } +} +``` + +## Dynamic Ports + +Frontend nodes can have dynamic ports: + +```javascript +{ + dynamicports: [ + { + name: "conditionalports/basic", + condition: "showAdvanced", + inputs: ["advancedOption1", "advancedOption2"], + }, + ]; +} +``` + +## Registration + +Register the node with Noodl runtime: + +```javascript +// In register-nodes.js +import CustomButton from "./nodes/custom-button"; + +export default function registerNodes(noodlRuntime) { + noodlRuntime.registerNode(CustomButton); +} +``` + +## Best Practices + +1. **Use frame for standard visuals** - Enable frame features for layout support +2. **Separate logic and styling** - Use inputProps for behavior, inputCss for styling +3. **Handle visual states** - Add appropriate mouse/focus event handlers +4. **Clean up event listeners** - Remove listeners in `_onNodeDeleted` +5. **Use forceUpdate sparingly** - Only when React needs to re-render +6. **Test visual states** - Verify hover, pressed, disabled states work correctly +7. **Support variants** - Enable useVariants for responsive design +8. **Document styling** - Explain how CSS customization works +9. **Optimize re-renders** - Cache computed values when possible +10. **Handle children properly** - Update child indices when children change +11. **Use React lifecycle inside component** - Don't confuse node and React lifecycles + +## Common Patterns + +### Conditional Rendering + +```javascript +getReactComponent() { + return function MyComponent(props) { + if (!props.visible) return null; + + return
{props.children}
; + }; +} +``` + +### Event Handler Props + +```javascript +outputProps: { + onChange: { + type: 'string', + getValue(event) { + return event.target.value; + } + }, + + onFocus: { + type: 'signal' + }, + + onBlur: { + type: 'signal' + } +} +``` + +### Custom React Hooks + +```javascript +getReactComponent() { + return function MyComponent(props) { + const [state, setState] = React.useState(props.initialValue); + + React.useEffect(() => { + // Side effect on mount or when dependency changes + console.log('Effect running'); + + return () => { + // Cleanup + console.log('Cleanup'); + }; + }, [props.dependency]); + + return
{state}
; + }; +} +``` + +## Debugging + +```javascript +{ + getInspectInfo() { + // Return debug info shown in editor + return { + props: this.props, + style: this.style, + children: this.children.length + }; + } +} +``` + +## See Also + +- [Node Definition](definition.md) - Base node definition +- [Visual States](visual-states.md) - Visual state system +- [Variants](variants.md) - Variant system +- [Dynamic Ports](dynamic-ports.md) - Dynamic port system diff --git a/codebase/nodes/instance.md b/codebase/nodes/instance.md new file mode 100644 index 0000000..300c160 --- /dev/null +++ b/codebase/nodes/instance.md @@ -0,0 +1,297 @@ +--- +id: instance +title: Node Instance +--- + +# Node Instance + +Node instances are runtime objects created from node definitions. They manage state, handle input changes, and produce outputs. + +## Base Class + +All nodes inherit from `node.js` which provides core functionality: + +```javascript +import * as Noodl from "@noodl/noodl-sdk"; + +const MyNode = Noodl.defineNode( { + constructor(context, id) { + super(context, id); + // Custom initialization + } +} +``` + +## Instance Properties + +### Core Properties + +- `id` - Unique instance identifier +- `name` - Node type name +- `context` - Reference to NodeContext +- `nodeScope` - Parent NodeScope +- `model` - Graph model data (if from editor) + +### Internal State + +- `_internal` - Private state storage +- `_inputValues` - Current input values +- `_inputs` - Input definitions +- `_outputs` - Output definitions +- `_dirty` - Needs update flag +- `_deleted` - Marked for deletion + +## Managing Inputs + +### Reading Input Values + +```javascript +const value = this.getInputValue("myInput"); +``` + +### Setting Input Values + +```javascript +this.setInputValue("myInput", 42); +``` + +### Queuing Input Changes + +```javascript +this.queueInput("myInput", newValue); +``` + +### Checking Connections + +```javascript +if (this.isInputConnected("myInput")) { + // Input has a connection +} +``` + +## Managing Outputs + +### Sending Output Values + +```javascript +this.sendValue("myOutput", result); +``` + +### Flagging Output Dirty + +```javascript +this.flagOutputDirty("myOutput"); // Re-sends current value +``` + +### Sending Signals + +```javascript +this.sendSignalOnOutput("done"); +``` + +### Flag All Outputs + +```javascript +this.flagAllOutputsDirty(); +``` + +## Update Cycle + +### Dirty Flagging + +Nodes update when flagged dirty: + +```javascript +this.flagDirty(); // Schedule update +``` + +### Update Method + +Override to implement custom update logic: + +```javascript +update() { + super.update(); + // Custom update logic runs after inputs are set +} +``` + +### Scheduled Callbacks + +Execute code after inputs update: + +```javascript +this.scheduleAfterInputsHaveUpdated(() => { + // Runs after all queued inputs are processed +}); +``` + +## State Management + +### Private State + +```javascript +initialize() { + this._internal = { + counter: 0, + data: {}, + timer: null + }; +} +``` + +### Cleaning Up + +```javascript +_onNodeDeleted() { + // Clean up resources + if (this._internal.timer) { + clearInterval(this._internal.timer); + } + super._onNodeDeleted(); +} +``` + +## Working with Context + +### Scheduling Updates + +```javascript +this.context.scheduleUpdate(); // Request frame update +``` + +### Next Frame Callback + +```javascript +this.context.scheduleNextFrame(() => { + // Runs on next frame +}); +``` + +### After Update Callback + +```javascript +this.context.scheduleAfterUpdate(() => { + // Runs after current update cycle +}); +``` + +### Timers + +```javascript +const timerId = this.context.timerScheduler.setTimeout(() => { + // Timer callback +}, 1000); + +// Clean up +this.context.timerScheduler.clearTimeout(timerId); +``` + +### Global Values + +```javascript +this.context.setGlobalValue("myKey", value); +const value = this.context.getGlobalValue("myKey"); +``` + +## Delete Listeners + +Register cleanup callbacks: + +```javascript +initialize() { + this.addDeleteListener(() => { + // Cleanup when node is deleted + }); +} +``` + +## Node Model Integration + +When connected to editor: + +```javascript +setNodeModel(nodeModel) { + super.setNodeModel(nodeModel); + + // Listen to parameter changes + // Handled automatically by base class +} + +_onNodeModelParameterUpdated(event) { + // Called when parameter changes in editor + super._onNodeModelParameterUpdated(event); + // Custom handling +} +``` + +## Visual States + +For nodes supporting visual states: + +```javascript +_getVisualStates() { + return ['hover', 'pressed']; +} +``` + +## Variants + +Setting variants: + +```javascript +setVariant(variant) { + // Apply variant parameters + // Usually handled by NodeScope +} +``` + +## Example: Counter Node + +```javascript +const CounterNode = defineNode({ + name: "Counter", + category: "Logic", + + inputs: { + increment: { + valueChangedToTrue() { + this._internal.count++; + this.flagOutputDirty("count"); + }, + }, + reset: { + valueChangedToTrue() { + this._internal.count = 0; + this.flagOutputDirty("count"); + }, + }, + }, + + outputs: { + count: { + type: "number", + getter() { + return this._internal.count; + }, + }, + }, + + initialize() { + this._internal = { + count: 0, + }; + }, +}); +``` + +## Best Practices + +1. **Use `_internal` for state** - Keep state in `_internal` object +2. **Clean up resources** - Use `_onNodeDeleted` or delete listeners +3. **Avoid globals** - Use context services instead +4. **Queue inputs during updates** - Use `queueInput` to avoid race conditions +5. **Flag outputs dirty** - Call `flagOutputDirty` when output changes +6. **Check connections** - Use `isInputConnected` before optional logic +7. **Schedule properly** - Use appropriate scheduling methods +8. **Handle undefined** - Check for undefined inputs/outputs diff --git a/codebase/nodes/overview.md b/codebase/nodes/overview.md new file mode 100644 index 0000000..e826773 --- /dev/null +++ b/codebase/nodes/overview.md @@ -0,0 +1,45 @@ +--- +id: overview +title: Nodes System Overview +--- + +# Nodes System Overview + +The Noodl nodes system is the foundation of the runtime. Nodes are the building blocks that users connect together to create applications. This section documents the architecture and APIs for working with nodes. + +## Core Concepts + +- **Node Definition**: The blueprint that describes a node's inputs, outputs, and behavior +- **Node Instance**: A runtime instance of a node created from its definition +- **Node Scope**: The container managing all node instances within a component +- **Node Register**: The registry that stores and creates node definitions +- **Node Context**: The shared runtime environment for all nodes + +## Key Files + +- `node.js` - Base class for all node instances +- `nodedefinition.js` - API for defining new node types +- `noderegister.js` - Registry for node definitions +- `nodescope.js` - Manages node instances within components +- `nodecontext.js` - Shared runtime context and lifecycle + +## Node Lifecycle + +1. **Registration** - Node definitions are registered with the NodeRegister +2. **Creation** - Node instances are created from definitions via NodeScope +3. **Connection** - Inputs and outputs are connected between nodes +4. **Execution** - Nodes update when inputs change or they're flagged dirty +5. **Deletion** - Nodes are cleaned up when removed from the graph + +## Documentation Structure + +- [Node Definition](definition.md) - How to define new node types +- [Node Instance](instance.md) - Runtime node instance behavior +- [Inputs & Outputs](inputoutputs.md) - Port system and data flow +- [Registration](register.md) - Node registry and lifecycle +- [Node Scope](scope.md) - Component-level node management +- [Node Context](context.md) - Runtime environment and services +- [Lifecycle Hooks](hooks.md) - Initialization and cleanup +- [Dynamic Ports](dynamic-ports.md) - Runtime port generation +- [Visual States](visual-states.md) - State-based parameter system +- [Variants](variants.md) - Node variant system diff --git a/codebase/nodes/scope.md b/codebase/nodes/scope.md new file mode 100644 index 0000000..97ac48a --- /dev/null +++ b/codebase/nodes/scope.md @@ -0,0 +1,317 @@ +--- +id: scope +title: Node Scope +--- + +# Node Scope + +NodeScope (`nodescope.js`) manages all node instances within a component. It handles node creation, connections, lifecycle, and the node graph structure. + +## Purpose + +- Creates and destroys node instances +- Manages connections between nodes +- Maintains the node graph hierarchy +- Handles node model synchronization +- Provides node lookup and queries + +## Node Creation + +### Create from Model + +Creates a node from editor model data: + +```javascript +const nodeInstance = await scope.createNodeFromModel(nodeModel); +``` + +The model contains: + +- `id` - Node instance ID +- `type` - Node type name +- `parameters` - Node parameters +- `ports` - Port configurations +- `variant` - Variant settings + +### Create Programmatically + +Create a node by type: + +```javascript +const node = await scope.createNode("My.Node.Type", "uniqueId", { + // Extra properties +}); +``` + +### Create Primitive Node + +For built-in types: + +```javascript +const node = scope.createPrimitiveNode("String", "myStringId", { + value: "Hello", +}); +``` + +## Node Lookup + +### Get by ID + +```javascript +const node = scope.getNodeWithId("nodeId"); + +if (scope.hasNodeWithId("nodeId")) { + // Node exists +} +``` + +### Get by Type + +```javascript +const nodes = scope.getNodesWithType("My.Node.Type"); +``` + +### Recursive Queries + +Search this scope and all child scopes: + +```javascript +// All nodes with ID (including children) +const nodes = scope.getNodesWithIdRecursive("nodeId"); + +// All nodes of type +const nodes = scope.getNodesWithTypeRecursive("My.Node.Type"); + +// All nodes +const allNodes = scope.getAllNodesRecursive(); + +// All nodes with specific variant +const variantNodes = scope.getAllNodesWithVariantRecursive("mobile"); +``` + +## Managing Connections + +### Add Connection + +```javascript +scope.addConnection({ + fromId: "sourceNodeId", + fromProperty: "outputName", + targetId: "targetNodeId", + targetProperty: "inputName", +}); +``` + +Connection data structure: + +- `fromId` - Source node ID +- `fromProperty` - Output port name +- `targetId` - Target node ID +- `targetProperty` - Input port name + +### Remove Connection + +```javascript +scope.removeConnection(connectionModel); +``` + +## Node Parameters + +Apply parameters to a node: + +```javascript +scope.setNodeParameters(nodeInstance, nodeModel); +``` + +This applies: + +- Parameter values +- Port configurations +- Visual states +- Variant settings + +## Node Tree Structure + +### Insert in Tree + +Nodes can form a hierarchy (for visual nodes): + +```javascript +scope.insertNodeInTree(nodeInstance, nodeModel); +``` + +This sets: + +- Parent/child relationships +- Sibling order +- Tree structure + +### Tree Navigation + +Nodes have tree properties: + +- `parent` - Parent node +- `children` - Array of child nodes +- `nodeScope` - The NodeScope managing the node + +## Lifecycle Management + +### Node Removal + +```javascript +scope.onNodeModelRemoved(nodeModel); +``` + +This: + +1. Removes all connections to/from the node +2. Calls node's `_onNodeDeleted` +3. Removes from scope's node registry +4. Cleans up child nodes + +### Scope Cleanup + +When destroying a scope: + +```javascript +// Remove all nodes +scope.nodes.forEach((node) => { + if (node._onNodeDeleted) { + node._onNodeDeleted(); + } +}); +scope.nodes = []; +``` + +## Internal Structure + +### Node Registry + +```javascript +scope.nodes = []; // All node instances in this scope +``` + +### Connection Tracking + +Connections are stored per node: + +```javascript +nodeInstance._connections = { + inputs: { + inputName: [{ sourceNode, sourcePort }], + }, + outputs: { + outputName: [{ targetNode, targetPort }], + }, +}; +``` + +## Component Integration + +### Root Component + +```javascript +scope.rootComponent = componentInstance; +``` + +### Component Owner + +```javascript +scope.componentOwner = parentComponent; +``` + +### Child Components + +Child component instances create their own NodeScopes: + +```javascript +childComponent.nodeScope = new NodeScope(context, this); +``` + +## Update Propagation + +NodeScope doesn't directly trigger updates - that's handled by NodeContext. However, it manages the node graph structure that determines update order. + +## Example: Creating a Graph + +```javascript +async function createSimpleGraph(scope) { + // Create nodes + const stringNode = scope.createPrimitiveNode("String", "str1", { + value: "Hello", + }); + + const logNode = await scope.createNode("Debug.Log", "log1"); + + // Connect them + scope.addConnection({ + fromId: "str1", + fromProperty: "value", + targetId: "log1", + targetProperty: "message", + }); + + // Find nodes + const allNodes = scope.getAllNodesRecursive(); + console.log(`Created ${allNodes.length} nodes`); +} +``` + +## Example: Component Hierarchy + +```javascript +// Root scope +const rootScope = new NodeScope(context, null); + +// Create parent component +const parentNode = await rootScope.createNode("Component", "parent1"); +const parentScope = new NodeScope(context, parentNode); + +// Create child in parent's scope +const childNode = await parentScope.createNode("Visual.Text", "text1"); + +// Query from root +const allNodes = rootScope.getAllNodesRecursive(); +// Returns nodes from root, parent, and all descendants +``` + +## Dynamic Node Creation + +For runtime-generated nodes: + +```javascript +const { defineNode } = require("./nodedefinition"); + +// Define dynamic node type +const DynamicNode = defineNode({ + name: "Dynamic.Node", + // ... definition +}); + +// Register it +context.nodeRegister.register(DynamicNode); + +// Create instance +const instance = await scope.createNode("Dynamic.Node", "dynamic1"); +``` + +## Error Handling + +```javascript +try { + const node = await scope.createNode("Unknown.Type", "test1"); +} catch (error) { + // "Unknown node type with name Unknown.Type" +} +``` + +## Best Practices + +1. **Use async/await** - Node creation is asynchronous +2. **Check node existence** - Use `hasNodeWithId` before operations +3. **Clean up connections** - Remove connections before deleting nodes +4. **Use recursive queries** - When searching across component boundaries +5. **Maintain tree structure** - Call `insertNodeInTree` for visual hierarchies +6. **Handle errors** - Wrap node creation in try/catch +7. **Avoid ID collisions** - Ensure unique IDs when creating nodes +8. **Respect scope boundaries** - Child scopes for child components diff --git a/codebase/nodes/variants.md b/codebase/nodes/variants.md new file mode 100644 index 0000000..61601de --- /dev/null +++ b/codebase/nodes/variants.md @@ -0,0 +1,144 @@ +--- +id: variants +title: Variants +--- + +# Variants + +Variants allow nodes to have different parameter values based on active variants (e.g., mobile, tablet, desktop). This enables responsive design and multi-platform support. + +## Purpose + +- Support responsive layouts +- Enable platform-specific designs +- Manage theme variations +- Handle localization + +## Enabling Variants in Nodes + +### In Node Definition + +```javascript +const { defineNode } = require("./nodedefinition"); + +const TextNode = defineNode({ + name: "Visual.Text", + useVariants: true, // Enable variant support + + inputs: { + fontSize: { + type: "number", + default: 16, + }, + }, +}); +``` + +Setting `useVariants: true` enables variant support for the node's parameters. + +## Variant Storage + +When a node has `useVariants: true`, parameter values can be stored with variant suffixes in the model: + +```javascript +{ + type: 'Visual.Text', + id: 'text1', + parameters: { + fontSize: 16, // Default value + fontSize_mobile: 14, // Mobile variant value + fontSize_tablet: 18, // Tablet variant value + fontSize_desktop: 20 // Desktop variant value + } +} +``` + +Pattern: `{paramName}_{variantName}` + +## Variants System + +The variants system is managed by the `Variants` class available in `NodeContext`: + +```javascript +// Access variants from node context +const variants = this.context.variants; +``` + +## Common Variant Types + +**Responsive** + +- `mobile` +- `tablet` +- `desktop` + +**Platform** + +- `ios` +- `android` +- `web` + +**Theme** + +- `light` +- `dark` + +**Localization** + +- Language codes (e.g., `en`, `es`, `fr`) + +## Node Variant Method + +Nodes with variants support the `setVariant` method: + +```javascript +{ + setVariant(variant) { + // Called when variant changes + // Node should update its parameters based on the variant + } +} +``` + +## Querying Nodes by Variant + +The NodeScope provides a method to find nodes with specific variants: + +```javascript +const nodesWithVariant = nodeScope.getAllNodesWithVariantRecursive("mobile"); +``` + +## Editor Integration + +### Variant-Specific Parameters + +The editor allows setting different values for each variant. When a parameter has variant-specific values, the editor stores them with the `{paramName}_{variantName}` pattern. + +### Variant Selector + +The editor provides UI for selecting and previewing different variants during development. + +## Best Practices + +1. **Enable selectively** - Only use `useVariants: true` for nodes that need responsive behavior +2. **Provide defaults** - Always have default values that work without variants +3. **Test all variants** - Verify behavior works correctly in each variant +4. **Use semantic names** - Name variants by purpose (e.g., 'mobile', 'tablet', not 'small', 'medium') +5. **Document variants** - Explain which variants your node supports +6. **Graceful degradation** - Handle cases where variant-specific values aren't set + +## Notes + +The variants system implementation details may vary. This documentation covers the basic variant support available through the `useVariants` flag and variant parameter naming convention. + +For the most accurate and up-to-date information about the variants API, refer to: + +- `packages/noodl-runtime/src/variants.js` - Variants class implementation +- `packages/noodl-runtime/src/nodecontext.js` - Context integration +- Example nodes in `packages/noodl-viewer-react/src/nodes` - Practical usage + +## See Also + +- [Node Definition](definition.md) - Base node definition +- [Node Context](context.md) - Runtime context +- [Frontend Nodes](frontend-nodes.md) - Visual node implementation diff --git a/codebase/nodes/visual-states.md b/codebase/nodes/visual-states.md new file mode 100644 index 0000000..326e0c7 --- /dev/null +++ b/codebase/nodes/visual-states.md @@ -0,0 +1,426 @@ +--- +id: visual-states +title: Visual States +--- + +# Visual States + +Visual states allow node parameters to have different values based on the current state (e.g., hover, pressed, focused). This enables responsive UI without complex logic. + +## Purpose + +- Define state-specific parameter values +- Enable interactive visual feedback +- Simplify conditional styling +- Support state transitions + +## Defining Visual States + +### In Node Definition + +```javascript +const { defineNode } = require("./nodedefinition"); + +const ButtonNode = defineNode({ + name: "Visual.Button", + + visualStates: ["hover", "pressed", "disabled"], + + inputs: { + backgroundColor: { + type: "color", + default: "#3498db", + allowVisualStates: true, + }, + textColor: { + type: "color", + default: "#ffffff", + allowVisualStates: true, + }, + enabled: { + type: "boolean", + default: true, + set(value) { + this._internal.enabled = value; + this.updateVisualState(); + }, + }, + }, + + initialize() { + this._internal = { + currentState: "default", + enabled: true, + hovering: false, + pressing: false, + }; + }, + + methods: { + updateVisualState() { + let state = "default"; + + if (!this._internal.enabled) { + state = "disabled"; + } else if (this._internal.pressing) { + state = "pressed"; + } else if (this._internal.hovering) { + state = "hover"; + } + + if (state !== this._internal.currentState) { + this._internal.currentState = state; + this.applyVisualState(state); + } + }, + + applyVisualState(state) { + // Get state-specific values + const bgColor = this.getVisualStateValue("backgroundColor", state); + const textColor = this.getVisualStateValue("textColor", state); + + // Apply to element + if (this._internal.element) { + this._internal.element.style.backgroundColor = bgColor; + this._internal.element.style.color = textColor; + } + }, + }, +}); +``` + +## Visual State Properties + +### State List + +Define available states: + +```javascript +{ + visualStates: ["hover", "pressed", "focused", "disabled", "active"]; +} +``` + +Common states: + +- `default` - Base state (always present) +- `hover` - Mouse hovering over element +- `pressed` - Mouse button down +- `focused` - Element has focus +- `disabled` - Element is disabled +- `active` - Element is in active state + +### State-Aware Inputs + +Mark inputs that support visual states: + +```javascript +inputs: { + backgroundColor: { + type: 'color', + allowVisualStates: true + } +} +``` + +## Working with Visual States + +### Getting State Values + +Get the value for a specific state: + +```javascript +const value = this.getVisualStateValue("backgroundColor", "hover"); +``` + +If state doesn't have a specific value, falls back to default. + +### Setting State Values + +Set values programmatically: + +```javascript +this.setVisualStateValue("backgroundColor", "hover", "#e74c3c"); +``` + +### Current State + +Check or set the current state: + +```javascript +// Get current state +const state = this._internal.currentState; + +// Change state +this.setVisualState("hover"); +``` + +## State Storage + +Visual state values are stored in the model: + +```javascript +{ + type: 'Visual.Button', + id: 'button1', + parameters: { + backgroundColor: '#3498db', + backgroundColor_hover: '#2980b9', + backgroundColor_pressed: '#1c6ca1' + } +} +``` + +Pattern: `{paramName}_{stateName}` + +## State Transitions + +### Manual Transitions + +```javascript +methods: { + onMouseEnter() { + this._internal.hovering = true; + this.updateVisualState(); + }, + + onMouseLeave() { + this._internal.hovering = false; + this._internal.pressing = false; + this.updateVisualState(); + }, + + onMouseDown() { + this._internal.pressing = true; + this.updateVisualState(); + }, + + onMouseUp() { + this._internal.pressing = false; + this.updateVisualState(); + } +} +``` + +### State Priority + +When multiple states are active, define priority: + +```javascript +methods: { + updateVisualState() { + // Priority: disabled > pressed > hover > default + if (!this._internal.enabled) { + this.setVisualState('disabled'); + } else if (this._internal.pressing) { + this.setVisualState('pressed'); + } else if (this._internal.hovering) { + this.setVisualState('hover'); + } else { + this.setVisualState('default'); + } + } +} +``` + +## Example: Interactive Card + +```javascript +const CardNode = defineNode({ + name: "Visual.Card", + + visualStates: ["hover", "selected"], + + inputs: { + elevation: { + type: "number", + default: 2, + allowVisualStates: true, + }, + scale: { + type: "number", + default: 1.0, + allowVisualStates: true, + }, + opacity: { + type: "number", + default: 1.0, + allowVisualStates: true, + }, + selected: { + type: "boolean", + default: false, + set(value) { + this._internal.selected = value; + this.updateVisualState(); + }, + }, + }, + + initialize() { + this._internal = { + hovering: false, + selected: false, + element: null, + }; + + // Create element + this._internal.element = document.createElement("div"); + this._internal.element.className = "card"; + + // Add event listeners + this._internal.element.addEventListener("mouseenter", () => { + this._internal.hovering = true; + this.updateVisualState(); + }); + + this._internal.element.addEventListener("mouseleave", () => { + this._internal.hovering = false; + this.updateVisualState(); + }); + }, + + methods: { + updateVisualState() { + let state = "default"; + + if (this._internal.selected) { + state = "selected"; + } else if (this._internal.hovering) { + state = "hover"; + } + + this.applyVisualState(state); + }, + + applyVisualState(state) { + const el = this._internal.element; + + const elevation = this.getVisualStateValue("elevation", state); + const scale = this.getVisualStateValue("scale", state); + const opacity = this.getVisualStateValue("opacity", state); + + el.style.boxShadow = `0 ${elevation}px ${ + elevation * 2 + }px rgba(0,0,0,0.2)`; + el.style.transform = `scale(${scale})`; + el.style.opacity = opacity; + }, + }, +}); +``` + +## Example: Input Field States + +```javascript +const InputNode = defineNode({ + name: "Visual.Input", + + visualStates: ["focused", "error", "disabled"], + + inputs: { + borderColor: { + type: "color", + default: "#bdc3c7", + allowVisualStates: true, + }, + borderWidth: { + type: "number", + default: 1, + allowVisualStates: true, + }, + backgroundColor: { + type: "color", + default: "#ffffff", + allowVisualStates: true, + }, + hasError: { + type: "boolean", + set(value) { + this._internal.hasError = value; + this.updateVisualState(); + }, + }, + enabled: { + type: "boolean", + default: true, + set(value) { + this._internal.enabled = value; + this.updateVisualState(); + }, + }, + }, + + methods: { + updateVisualState() { + let state = "default"; + + // Priority: disabled > error > focused + if (!this._internal.enabled) { + state = "disabled"; + } else if (this._internal.hasError) { + state = "error"; + } else if (this._internal.focused) { + state = "focused"; + } + + this.applyVisualState(state); + }, + }, +}); +``` + +## Animated Transitions + +For smooth state transitions: + +```javascript +methods: { + applyVisualState(state) { + const el = this._internal.element; + + // Enable transitions + el.style.transition = 'all 0.3s ease'; + + // Apply state values + const bgColor = this.getVisualStateValue('backgroundColor', state); + el.style.backgroundColor = bgColor; + } +} +``` + +## Editor Integration + +### Property Panel + +Visual state inputs show state selector in the editor: + +``` +┌─────────────────────────┐ +│ Background Color │ +│ ┌─────────────────────┐ │ +│ │ Default ▼ │ │ +│ └─────────────────────┘ │ +│ [Color Picker] │ +└─────────────────────────┘ +``` + +### State Management + +The editor manages state-specific values and provides UI for: + +- Switching between states +- Setting state-specific values +- Previewing different states +- Copying values between states + +## Best Practices + +1. **Use semantic states** - Name states by meaning, not appearance +2. **Provide defaults** - Always have default state values +3. **Limit state count** - Too many states become confusing +4. **Document states** - Explain what each state represents +5. **Test all states** - Verify each state works correctly +6. **Consider priority** - Define clear state precedence +7. **Smooth transitions** - Use CSS transitions for better UX +8. **Clean up listeners** - Remove event listeners in \_onNodeDeleted diff --git a/codebase/overview.md b/codebase/overview.md new file mode 100644 index 0000000..be82308 --- /dev/null +++ b/codebase/overview.md @@ -0,0 +1,11 @@ +# Codebase Overview + +Welcome to the Noodl codebase documentation! + +This section provides a detailed technical overview of the source code, architecture, and development guidelines for contributors. + +- [Getting started](./development-setup.md) +- [Architecture](./architecture/overview.md) +- [Codebase structure](./structure/folders.md) +- [Nodes](./nodes/overview.md) +- [Development Guides](./development.md) diff --git a/codebase/structure/folders.md b/codebase/structure/folders.md new file mode 100644 index 0000000..7a47f6c --- /dev/null +++ b/codebase/structure/folders.md @@ -0,0 +1,186 @@ +# Project Structure + +This document explains the directory organization and file conventions of the OpenNoodl codebase. + +## Root Directory Structure + +``` +noodl/ +├── packages/ # Monorepo packages +├── apps/ # Application entry points +├── docs/ # Documentation +├── scripts/ # Build and utility scripts +├── tests/ # Integration and e2e tests +├── .github/ # GitHub workflows and templates +├── package.json # Root package configuration +└── README.md # Project overview +``` + +## Core Packages (`/packages`) + +### `/packages/noodl-editor` + +The visual editor application (main UI). + +``` +noodl-editor/ +├── src/ +│ ├── components/ # React components +│ ├── editor/ # Core editor logic +│ ├── models/ # Data models +│ ├── stores/ # State management +│ ├── utils/ # Utility functions +│ └── index.js # Entry point +├── public/ # Static assets +├── build/ # Build output +└── package.json +``` + +### `/packages/noodl-runtime` + +The runtime engine that executes node graphs. + +``` +noodl-runtime/ +├── src/ +│ ├── nodes/ # Node implementations +│ ├── engine/ # Execution engine +│ ├── events/ # Event system +│ ├── data/ # Data management +│ └── index.js # Runtime entry +├── tests/ # Unit tests +└── package.json +``` + +### `/packages/noodl-core-nodes` + +Built-in node implementations. + +``` +noodl-core-nodes/ +├── src/ +│ ├── ui/ # UI component nodes +│ ├── logic/ # Logic and control nodes +│ ├── data/ # Data manipulation nodes +│ ├── events/ # Event handling nodes +│ └── index.js # Node registry +└── package.json +``` + +### `/packages/noodl-platform` + +Platform-specific code and adapters. + +``` +noodl-platform/ +├── src/ +│ ├── electron/ # Electron desktop app +│ ├── web/ # Web platform +│ ├── mobile/ # Mobile platform (future) +│ └── common/ # Shared platform code +└── package.json +``` + +## Application Entry Points (`/apps`) + +### `/apps/noodl-editor-app` + +Main desktop application (Electron wrapper). + +### `/apps/noodl-web-app` + +Web version of the editor. + +### `/apps/noodl-cloud-app` + +Cloud services and deployment tools. + +## File Naming Conventions + +### Component Files + +- React components: `PascalCase.jsx` +- Utility modules: `camelCase.js` +- Constants: `UPPER_SNAKE_CASE.js` +- Tests: `filename.test.js` + +### Directory Structure + +- Components in `/components` with index exports +- Models in `/models` following domain organization +- Utilities in `/utils` by functionality + +## Import/Export Patterns + +### Barrel Exports + +Use index files for clean imports: + +```javascript +// /components/index.js +export { NodeEditor } from "./NodeEditor"; +export { PropertyPanel } from "./PropertyPanel"; +export { Toolbar } from "./Toolbar"; +``` + +### Absolute Imports + +Configure path mapping for cleaner imports: + +```javascript +// Instead of: import { Node } from '../../../models/Node' +import { Node } from "@noodl/models/Node"; +``` + +## Asset Organization + +### Static Assets + +``` +public/ +├── icons/ # SVG icons and graphics +├── fonts/ # Custom fonts +├── images/ # Images and illustrations +├── themes/ # CSS theme files +└── manifest.json # App manifest +``` + +### Generated Assets + +``` +build/ +├── static/ # Webpack output +├── locales/ # Internationalization +└── docs/ # Generated documentation +``` + +## Configuration Files + +### Build Configuration + +- `webpack.config.js` - Webpack build setup +- `babel.config.js` - Babel transformation +- `tsconfig.json` - TypeScript configuration + +### Development Tools + +- `.eslintrc.js` - Code linting rules +- `.prettierrc` - Code formatting +- `jest.config.js` - Test configuration + +## Development Guidelines + +### Adding New Features + +1. Create feature branch +2. Add components to appropriate package +3. Update tests and documentation +4. Follow naming conventions +5. Submit PR with clear description + +### Package Dependencies + +- Keep packages loosely coupled +- Use interfaces for package communication +- Avoid circular dependencies +- Document inter-package APIs diff --git a/docusaurus.config.js b/docusaurus.config.js index 33abee4..afc7001 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -120,6 +120,18 @@ const config = { }, ], + // Codebase docs + [ + '@docusaurus/plugin-content-docs', + { + id: 'codebase', + path: 'codebase', + routeBasePath: 'codebase', + breadcrumbs: false, + sidebarPath: require.resolve('./sidebarsCodebase.js'), + }, + ], + // Copy static md files for editor inline docs [ require('./plugins/copy-node-markdowns'), @@ -184,6 +196,11 @@ const config = { position: 'right', className: 'has-divider', }, + { + label: 'Codebase', + to: '/codebase/overview', + position: 'right', + }, { label: 'Discord', to: 'https://discord.com/invite/23xU2hYrSJ', diff --git a/sidebarsCodebase.js b/sidebarsCodebase.js new file mode 100644 index 0000000..87e9934 --- /dev/null +++ b/sidebarsCodebase.js @@ -0,0 +1,51 @@ +module.exports = { + codebaseSidebar: [ + 'overview', + { + type: 'category', + label: 'Getting Started', + items: [ + 'development-setup', + 'contributing', + 'build-test', + ], + }, + { + type: 'category', + label: 'Architecture', + items: [ + 'architecture/overview', + 'architecture/core-concepts', + 'architecture/data-flow', + ], + }, + { + type: 'category', + label: 'Codebase Structure', + items: [ + 'structure/folders', + ], + }, + { + type: 'category', + label: 'Nodes', + items: [ + 'nodes/overview', + 'nodes/definition', + 'nodes/instance', + 'nodes/scope', + 'nodes/context', + 'nodes/dynamic-ports', + 'nodes/frontend-nodes', + 'nodes/visual-states', + ], + }, + { + type: 'category', + label: 'Development Guides', + items: [ + 'guides/adding-nodes', + ], + }, + ], +}; diff --git a/src/css/variables.css b/src/css/variables.css index 108eea3..381b1eb 100644 --- a/src/css/variables.css +++ b/src/css/variables.css @@ -1,57 +1,57 @@ :root { - /* Colors */ - --doc-color-noodl-black: #1f1f1f; - --doc-color-noodl-black-dark: #141416; - --doc-color-noodl-black-darker: #0e0e0e; - --doc-color-noodl-black-darkest: #000000; - --doc-color-noodl-black-light: #232326; - - --doc-color-noodl-white: #f6f6f6; - --doc-color-noodl-white-85: #dcdcdc; - --doc-color-noodl-white-65: #a5a5a5; - - --doc-color-noodl-orange: #f5bc41; - --doc-color-noodl-orange-80: #f7c967; - --doc-color-noodl-orange-60: #f9d78d; - --doc-color-noodl-orange-40: #fbe4b3; - --doc-color-noodl-orange-20: #fce9c2; - --doc-color-noodl-orange-180: #f3b224; - --doc-color-noodl-orange-160: #f3ac15; - --doc-color-noodl-orange-140: #ce900b; - - --doc-color-noodl-green: #1ca5b8; - --doc-color-noodl-green-80: #49b7c6; - --doc-color-noodl-green-60: #77c9d4; - --doc-color-noodl-green-40: #92d4dd; - --doc-color-noodl-green-20: #a8dde4; - --doc-color-noodl-green-180: #1994a6; - --doc-color-noodl-green-160: #188c9c; - --doc-color-noodl-green-140: #147381; - - --doc-color-noodl-blue: #5836f5; - --doc-color-noodl-blue-80: #795ef7; - --doc-color-noodl-blue-60: #9b86f9; - --doc-color-noodl-blue-40: #bcaffb; - --doc-color-noodl-blue-20: #c9bffc; - --doc-color-noodl-blue-180: #401af4; - --doc-color-noodl-blue-160: #350cf2; - --doc-color-noodl-blue-140: #2c0ac7; - - --doc-color-visual-node: #324e6b; - --doc-color-data-node: #697844; - --doc-color-custom-node: #8c436c; - --doc-color-logic-node: #3b3e48; - --doc-color-connection-node: #5f3789; - - --doc-color-title: var(--doc-color-noodl-white); - --doc-color-text: var(--doc-color-noodl-white-85); - - /* Typography */ - --doc-font-title: 'Poppins', sans-serif; - --doc-font-supertitle: 'PolySans', sans-serif; - --doc-font-text: 'Inter', sans-serif; - --doc-font-semibold: 600; - - /* Sizes */ - --doc-size-page-top-spacing-l: 120px; + /* Colors */ + --doc-color-noodl-black: #1f1f1f; + --doc-color-noodl-black-dark: #141416; + --doc-color-noodl-black-darker: #0e0e0e; + --doc-color-noodl-black-darkest: #000000; + --doc-color-noodl-black-light: #232326; + + --doc-color-noodl-white: #f6f6f6; + --doc-color-noodl-white-85: #dcdcdc; + --doc-color-noodl-white-65: #a5a5a5; + + --doc-color-noodl-orange: #39a1b6; + --doc-color-noodl-orange-80: #329db3; + --doc-color-noodl-orange-60: #4fa3b4; + --doc-color-noodl-orange-40: #679faa; + --doc-color-noodl-orange-20: #86aeb6; + --doc-color-noodl-orange-180: #029ebd; + --doc-color-noodl-orange-160: #0e99b4; + --doc-color-noodl-orange-140: #1e9db6; + + --doc-color-noodl-green: #1ca5b8; + --doc-color-noodl-green-80: #49b7c6; + --doc-color-noodl-green-60: #77c9d4; + --doc-color-noodl-green-40: #92d4dd; + --doc-color-noodl-green-20: #a8dde4; + --doc-color-noodl-green-180: #1994a6; + --doc-color-noodl-green-160: #188c9c; + --doc-color-noodl-green-140: #147381; + + --doc-color-noodl-blue: #5836f5; + --doc-color-noodl-blue-80: #795ef7; + --doc-color-noodl-blue-60: #9b86f9; + --doc-color-noodl-blue-40: #bcaffb; + --doc-color-noodl-blue-20: #c9bffc; + --doc-color-noodl-blue-180: #401af4; + --doc-color-noodl-blue-160: #350cf2; + --doc-color-noodl-blue-140: #2c0ac7; + + --doc-color-visual-node: #324e6b; + --doc-color-data-node: #697844; + --doc-color-custom-node: #8c436c; + --doc-color-logic-node: #3b3e48; + --doc-color-connection-node: #5f3789; + + --doc-color-title: var(--doc-color-noodl-white); + --doc-color-text: var(--doc-color-noodl-white-85); + + /* Typography */ + --doc-font-title: "Poppins", sans-serif; + --doc-font-supertitle: "PolySans", sans-serif; + --doc-font-text: "Inter", sans-serif; + --doc-font-semibold: 600; + + /* Sizes */ + --doc-size-page-top-spacing-l: 120px; } diff --git a/static/img/logo.png b/static/img/logo.png index 1ac93c0..3336d8b 100644 Binary files a/static/img/logo.png and b/static/img/logo.png differ