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 ( +