From 9355e784eac689ecd10dad9081f9630b3d58b60b Mon Sep 17 00:00:00 2001 From: Axel Wretman Date: Tue, 9 Sep 2025 17:25:00 +0200 Subject: [PATCH 1/3] Added basic structure for new code section of documentation. --- codebase/architecture.md | 1 + codebase/folders.md | 1 + codebase/modules.md | 1 + codebase/overview.md | 11 +++++++++++ docusaurus.config.js | 17 +++++++++++++++++ sidebarsCodebase.js | 10 ++++++++++ 6 files changed, 41 insertions(+) create mode 100644 codebase/architecture.md create mode 100644 codebase/folders.md create mode 100644 codebase/modules.md create mode 100644 codebase/overview.md create mode 100644 sidebarsCodebase.js 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/folders.md b/codebase/folders.md new file mode 100644 index 0000000..a317e95 --- /dev/null +++ b/codebase/folders.md @@ -0,0 +1 @@ +lorem ipsum dolor sit amet diff --git a/codebase/modules.md b/codebase/modules.md new file mode 100644 index 0000000..a317e95 --- /dev/null +++ b/codebase/modules.md @@ -0,0 +1 @@ +lorem ipsum dolor sit amet diff --git a/codebase/overview.md b/codebase/overview.md new file mode 100644 index 0000000..1c3e1cc --- /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. + +- [Architecture](./architecture.md) (add this file) +- [Folder structure](./folders.md) (add this file) +- [Key modules](./modules.md) (add this file) + +Start by expanding this overview and adding more files as needed. 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..2a84afe --- /dev/null +++ b/sidebarsCodebase.js @@ -0,0 +1,10 @@ +module.exports = { + codebaseSidebar: [ + { + type: 'doc', + id: 'overview', + label: 'Overview', + }, + // Add more docs here + ], +}; From 7f7a33e9a38088215daa189eb7d46a0a83116ae1 Mon Sep 17 00:00:00 2001 From: Axel Wretman Date: Mon, 3 Nov 2025 13:31:06 +0100 Subject: [PATCH 2/3] Added extensive node documentation. --- codebase/architecture/core-concepts.md | 84 +++ codebase/architecture/data-flow.md | 155 +++++ codebase/architecture/overview.md | 142 +++++ codebase/build-test.md | 82 +++ codebase/contributing.md | 89 +++ codebase/development-setup.md | 79 +++ codebase/folders.md | 1 - codebase/guides/adding-nodes.md | 280 +++++++++ codebase/modules.md | 1 - codebase/nodes/context.md | 374 +++++++++++ codebase/nodes/definition.md | 729 ++++++++++++++++++++++ codebase/nodes/dynamic-ports.md | 371 +++++++++++ codebase/nodes/frontend-nodes.md | 832 +++++++++++++++++++++++++ codebase/nodes/instance.md | 297 +++++++++ codebase/nodes/nodes.md | 90 +++ codebase/nodes/overview.md | 45 ++ codebase/nodes/scope.md | 330 ++++++++++ codebase/nodes/variants.md | 429 +++++++++++++ codebase/nodes/visual-states.md | 426 +++++++++++++ codebase/overview.md | 8 +- codebase/structure/folders.md | 186 ++++++ sidebarsCodebase.js | 50 +- 22 files changed, 5071 insertions(+), 9 deletions(-) create mode 100644 codebase/architecture/core-concepts.md create mode 100644 codebase/architecture/data-flow.md create mode 100644 codebase/architecture/overview.md create mode 100644 codebase/build-test.md create mode 100644 codebase/contributing.md create mode 100644 codebase/development-setup.md delete mode 100644 codebase/folders.md create mode 100644 codebase/guides/adding-nodes.md delete mode 100644 codebase/modules.md create mode 100644 codebase/nodes/context.md create mode 100644 codebase/nodes/definition.md create mode 100644 codebase/nodes/dynamic-ports.md create mode 100644 codebase/nodes/frontend-nodes.md create mode 100644 codebase/nodes/instance.md create mode 100644 codebase/nodes/nodes.md create mode 100644 codebase/nodes/overview.md create mode 100644 codebase/nodes/scope.md create mode 100644 codebase/nodes/variants.md create mode 100644 codebase/nodes/visual-states.md create mode 100644 codebase/structure/folders.md 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..6ebf4e4 --- /dev/null +++ b/codebase/build-test.md @@ -0,0 +1,82 @@ +# 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 +``` + +### Production Build + +```bash +npm run build +# or +yarn build +``` + +### Platform-Specific Builds + +```bash +# Desktop app (Electron) +npm run build:desktop + +# Web app +npm run build:web + +# All platforms +npm run build:all +``` + +## Testing + +### Running Tests + +```bash +# Run all tests +npm test + +# Run tests in watch mode +npm run test:watch + +# Run tests with coverage +npm run test:coverage +``` + +### Test Types + +#### Unit Tests + +- Located alongside source files (`.test.js`) +- Test individual functions and components +- Use Jest framework + +#### Integration Tests + +- Located in `/tests/integration` +- Test component interactions +- Use React Testing Library + +#### End-to-End Tests + +- Located in `/tests/e2e` +- Test complete user workflows +- Use Playwright or Cypress + +### Writing Tests + +Follow the testing guidelines in [Testing Guidelines](./guides/testing.md). + +## Continuous Integration + +Our CI pipeline runs on GitHub Actions: + +- Automated testing on pull requests +- Build verification for all platforms +- Code quality checks (ESLint, Prettier) +- Security scanning 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..1c21e25 --- /dev/null +++ b/codebase/development-setup.md @@ -0,0 +1,79 @@ +# 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 +- GitLens +- Auto Rename Tag + +### 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/folders.md b/codebase/folders.md deleted file mode 100644 index a317e95..0000000 --- a/codebase/folders.md +++ /dev/null @@ -1 +0,0 @@ -lorem ipsum dolor sit amet diff --git a/codebase/guides/adding-nodes.md b/codebase/guides/adding-nodes.md new file mode 100644 index 0000000..2d82099 --- /dev/null +++ b/codebase/guides/adding-nodes.md @@ -0,0 +1,280 @@ +# 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 +const { Node } = require("@noodl/runtime"); + +class MyCustomNode extends Node { + 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 +class DynamicNode extends Node { + 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 +class ConfigurableNode extends Node { + static parameters = { + mode: { + type: "enum", + options: ["Add", "Subtract", "Multiply"], + default: "Add", + }, + precision: { + type: "number", + default: 2, + min: 0, + max: 10, + }, + }; +} +``` + +### State Management + +```javascript +class StatefulNode extends Node { + 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 +class MyUINode extends Node { + 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) + } +} +``` + +## Documentation + +Add documentation for your node: + +```javascript +static docs = { + description: 'Converts input text to uppercase', + examples: [ + { + title: 'Basic Usage', + description: 'Connect a string input and trigger to see the uppercase result' + } + ], + inputs: { + value: 'The text to convert to uppercase', + trigger: 'Signal to execute the conversion' + }, + outputs: { + result: 'The uppercase version of the input text', + done: 'Signal sent when conversion is complete' + } +} +``` + +## 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/modules.md b/codebase/modules.md deleted file mode 100644 index a317e95..0000000 --- a/codebase/modules.md +++ /dev/null @@ -1 +0,0 @@ -lorem ipsum dolor sit amet 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..fdac23c --- /dev/null +++ b/codebase/nodes/definition.md @@ -0,0 +1,729 @@ +--- +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(); + } + }, + + // Enum with groups + category: { + type: { + name: 'enum', + enums: [ + { + label: 'Basic', + values: [ + { label: 'Type A', value: 'a' }, + { label: 'Type B', value: 'b' } + ] + }, + { + label: 'Advanced', + values: [ + { label: 'Type X', value: 'x' }, + { label: 'Type Y', value: 'y' } + ] + } + ] + } + } +} +``` + +### 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..2b80cd0 --- /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 +const Node = require("./node"); + +class MyNode extends Node { + 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/nodes.md b/codebase/nodes/nodes.md new file mode 100644 index 0000000..cd4f3bb --- /dev/null +++ b/codebase/nodes/nodes.md @@ -0,0 +1,90 @@ +# Nodes — structure & patterns + +This page describes the typical structure of a Noodl "node" file, what sections you can implement, and common conventions used in the runtime package (see a:\Repos\OpenNoodl\packages\noodl-runtime\src\nodes). + +## Purpose + +A node file exports the definition/implementation of a runtime node. A node defines inputs, outputs, settings, behavior on input changes, and lifecycle hooks. Nodes live under packages/noodl-runtime/src/nodes (and subfolders like std-library, data, user, variables). + +## Typical sections + +- Module export + - The file exports a single object (or a registration call) describing the node. +- Metadata + - id/name, label, category, description — used by editors and docs. +- Inputs / outputs definitions + - Typed ports exposed to other nodes (name, type, default). +- Settings / properties + - Configurable options persisted with the component instance. +- Lifecycle hooks + - init / onAttach / onDetach — run when the node/component is created/connected/disposed. +- Runtime handlers + - onInput or handlers for specific inputs to implement behavior. +- State & storage + - Per-instance state that persists for the life of the component instance. +- Event / task scheduling + - Emit events, schedule async tasks, timers, or call runtime APIs. + +## Conventions and examples + +A minimal JS skeleton (illustrative only): + +```javascript +// minimal node skeleton +module.exports = { + id: "my.example.node", + label: "Example Node", + category: "logic", + inputs: { + inputA: { type: "boolean", default: false }, + value: { type: "number" }, + }, + outputs: { + outValue: { type: "number" }, + }, + settings: { + multiplier: { type: "number", default: 1 }, + }, + init(instance) { + // called once when node instance is created + instance.state = { count: 0 }; + }, + onInput(instance, inputName, value) { + // handle incoming values + if (inputName === "value") { + const result = value * (instance.settings.multiplier || 1); + instance.setOutput("outValue", result); + } + }, + onAttach(instance) { + // optional: when node becomes active/connected in graph + }, + onDetach(instance) { + // cleanup timers/async tasks + }, +}; +``` + +Notes: + +- instance provides helpers: read settings, set outputs, schedule tasks, subscribe/unsubscribe, access persistent component state. +- Use init to allocate resources and onDetach to clean them up to avoid leaks. +- Use descriptive ids and categories to keep the runtime organized (see std-library and data folders for examples). + +## Advanced patterns + +- Async operations: perform fetches or DB actions in handlers, update outputs when promises resolve. +- Composite components: nodes can coordinate child components via runtime APIs. +- Reusable utilities: factor repeated logic into helper modules (see std-library and variables folders). + +## Testing & debugging + +- Add console logs in lifecycle hooks and handlers. +- Unit test node logic by calling exported handlers with a mock instance (mock instance should implement setOutput, settings, state). +- Watch for mismatched input/output names and types — runtime will error if the sidebar/doc references non-existent ids. + +## Common pitfalls + +- Forgetting to clean timers/subscriptions in onDetach. +- Using globals for per-instance state (use instance.state). +- Using a documentation/sidebars id that doesn't match actual doc filenames (see your earlier sidebar error). 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..ed5410e --- /dev/null +++ b/codebase/nodes/scope.md @@ -0,0 +1,330 @@ +--- +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 + +## Creating a NodeScope + +```javascript +const NodeScope = require("./nodescope"); + +const scope = new NodeScope(context, componentOwner); +``` + +### Parameters + +- `context` - NodeContext instance +- `componentOwner` - Parent component instance (if any) + +## 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..6669977 --- /dev/null +++ b/codebase/nodes/variants.md @@ -0,0 +1,429 @@ +--- +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 + +## Variants System + +The variants system is managed by the `Variants` class (`variants.js`): + +```javascript +const Variants = require("./variants"); + +const variants = new Variants(); +``` + +## Defining Variants + +### In Project + +Variants are defined at the project level: + +```javascript +{ + variants: [ + { name: "mobile", condition: "screen.width < 768" }, + { name: "tablet", condition: "screen.width >= 768 && screen.width < 1024" }, + { name: "desktop", condition: "screen.width >= 1024" }, + ]; +} +``` + +### Common Variant Types + +**Responsive** + +- `mobile` +- `tablet` +- `desktop` +- `ultrawide` + +**Platform** + +- `ios` +- `android` +- `web` +- `native` + +**Theme** + +- `light` +- `dark` +- `high-contrast` + +**Localization** + +- `en` +- `es` +- `fr` +- Language-specific variants + +## Enabling Variants + +### In Node Definition + +```javascript +const { defineNode } = require("./nodedefinition"); + +const TextNode = defineNode({ + name: "Visual.Text", + useVariants: true, + + inputs: { + fontSize: { + type: "number", + default: 16, + }, + text: { + type: "string", + default: "Hello", + }, + }, +}); +``` + +Setting `useVariants: true` enables variant support for all parameters. + +## Variant Values + +### Storage + +Variant-specific values are stored with suffix: + +```javascript +{ + type: 'Visual.Text', + id: 'text1', + parameters: { + fontSize: 16, // Default + fontSize_mobile: 14, // Mobile variant + fontSize_tablet: 18, // Tablet variant + fontSize_desktop: 20 // Desktop variant + } +} +``` + +Pattern: `{paramName}_{variantName}` + +### Getting Variant Values + +```javascript +const value = this.getVariantValue("fontSize", "mobile"); +``` + +Falls back to default if variant value not set. + +## Active Variants + +### Setting Active Variants + +```javascript +context.variants.setActiveVariants(["mobile", "dark"]); +``` + +### Checking Active Variants + +```javascript +if (context.variants.isActive("mobile")) { + // Mobile variant is active +} + +const activeVariants = context.variants.getActive(); +// Returns: ['mobile', 'dark'] +``` + +## Variant Resolution + +When multiple variants are active, values are resolved in order: + +1. Most specific variant (last active) +2. Less specific variants +3. Default value + +```javascript +// Active: ['mobile', 'dark'] +// Resolution order: dark → mobile → default + +const value = this.resolveVariantValue("backgroundColor"); +// Checks: backgroundColor_dark, backgroundColor_mobile, backgroundColor +``` + +## Node Integration + +### Applying Variants + +Nodes automatically apply variant values when variants change: + +```javascript +setVariant(variant) { + // Apply variant-specific parameters + Object.keys(this.model.parameters).forEach(key => { + const variantKey = `${key}_${variant}`; + if (this.model.parameters.hasOwnProperty(variantKey)) { + this.setParameter(key, this.model.parameters[variantKey]); + } + }); +} +``` + +### Listening to Variant Changes + +```javascript +initialize() { + this.context.variants.on('changed', () => { + this.updateForVariants(); + }); +} + +methods: { + updateForVariants() { + const activeVariants = this.context.variants.getActive(); + activeVariants.forEach(variant => { + this.setVariant(variant); + }); + } +} +``` + +## Example: Responsive Layout + +```javascript +const ContainerNode = defineNode({ + name: "Visual.Container", + useVariants: true, + + inputs: { + width: { + type: "number", + default: 1200, + }, + padding: { + type: "number", + default: 20, + }, + columns: { + type: "number", + default: 12, + }, + }, + + initialize() { + this._internal = { + element: document.createElement("div"), + }; + + // Listen to variant changes + this.context.variants.on("changed", () => { + this.applyLayout(); + }); + + this.applyLayout(); + }, + + methods: { + applyLayout() { + const variants = this.context.variants.getActive(); + + // Get values for active variants + let width = this.getInputValue("width"); + let padding = this.getInputValue("padding"); + let columns = this.getInputValue("columns"); + + // Override with variant-specific values + variants.forEach((variant) => { + const variantWidth = this.getVariantValue("width", variant); + const variantPadding = this.getVariantValue("padding", variant); + const variantColumns = this.getVariantValue("columns", variant); + + if (variantWidth !== undefined) width = variantWidth; + if (variantPadding !== undefined) padding = variantPadding; + if (variantColumns !== undefined) columns = variantColumns; + }); + + // Apply to element + const el = this._internal.element; + el.style.maxWidth = `${width}px`; + el.style.padding = `${padding}px`; + el.style.gridTemplateColumns = `repeat(${columns}, 1fr)`; + }, + }, +}); +``` + +## Example: Theme Support + +```javascript +const ThemedBoxNode = defineNode({ + name: "Visual.ThemedBox", + useVariants: true, + + inputs: { + backgroundColor: { + type: "color", + default: "#ffffff", + }, + textColor: { + type: "color", + default: "#000000", + }, + borderColor: { + type: "color", + default: "#cccccc", + }, + }, + + initialize() { + // Default theme + this.parameters.backgroundColor = "#ffffff"; + this.parameters.textColor = "#000000"; + + // Dark theme + this.parameters.backgroundColor_dark = "#1a1a1a"; + this.parameters.textColor_dark = "#ffffff"; + + // High contrast theme + this.parameters.backgroundColor_highcontrast = "#000000"; + this.parameters.textColor_highcontrast = "#ffff00"; + + this.applyTheme(); + }, + + methods: { + applyTheme() { + const variants = this.context.variants.getActive(); + const themeVariant = variants.find((v) => + ["dark", "light", "highcontrast"].includes(v) + ); + + if (themeVariant) { + this.setVariant(themeVariant); + } + }, + }, +}); +``` + +## Conditional Variants + +Variants can have conditions that determine when they're active: + +```javascript +{ + name: 'mobile', + condition: 'screen.width < 768' +} +``` + +The system evaluates conditions and activates matching variants automatically. + +## Querying Nodes by Variant + +Find all nodes with a specific variant: + +```javascript +const mobileNodes = nodeScope.getAllNodesWithVariantRecursive("mobile"); +``` + +## Variant Transitions + +Smoothly transition between variants: + +```javascript +methods: { + transitionToVariant(variant) { + const el = this._internal.element; + + // Enable transitions + el.style.transition = 'all 0.3s ease'; + + // Apply variant + this.setVariant(variant); + } +} +``` + +## Editor Integration + +### Variant Selector + +The editor provides a variant selector: + +``` +┌────────────────────┐ +│ Variants: ▼ │ +├────────────────────┤ +│ ☐ Mobile │ +│ ☐ Tablet │ +│ ☑ Desktop │ +│ ☐ Dark Theme │ +└────────────────────┘ +``` + +### Parameter Panel + +Shows variant-specific values: + +``` +┌─────────────────────────┐ +│ Font Size │ +│ ┌─────────────────────┐ │ +│ │ Default: 16 │ │ +│ │ Mobile: 14 │ │ +│ │ Tablet: 18 │ │ +│ │ Desktop: 20 │ │ +│ └─────────────────────┘ │ +└─────────────────────────┘ +``` + +## Performance Considerations + +1. **Limit active variants** - Too many active variants can impact performance +2. **Cache variant values** - Don't recalculate on every frame +3. **Batch variant changes** - Change multiple variants at once +4. **Lazy evaluation** - Only resolve variants when needed + +## Testing Variants + +```javascript +test("node respects variant values", () => { + const node = createNode("Visual.Text", "text1"); + + // Set default and variant values + node.setParameter("fontSize", 16); + node.setVariantValue("fontSize", "mobile", 14); + + // Activate mobile variant + context.variants.setActiveVariants(["mobile"]); + + // Check resolved value + expect(node.getResolvedValue("fontSize")).toBe(14); + + // Deactivate variant + context.variants.setActiveVariants([]); + + // Check default value + expect(node.getResolvedValue("fontSize")).toBe(16); +}); +``` + +## Best Practices + +1. **Use semantic names** - Name variants by purpose, not specifics +2. **Provide defaults** - Always have default values +3. **Limit variant count** - Too many variants complicate management +4. **Test all variants** - Verify behavior in each variant +5. **Document conditions** - Explain when variants are active +6. **Graceful degradation** - Fall back to defaults when variant missing +7. **Consider inheritance** - Child components inherit variants +8. **Optimize performance** - Cache resolved values 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 index 1c3e1cc..73a9104 100644 --- a/codebase/overview.md +++ b/codebase/overview.md @@ -4,8 +4,10 @@ Welcome to the Noodl codebase documentation! This section provides a detailed technical overview of the source code, architecture, and development guidelines for contributors. -- [Architecture](./architecture.md) (add this file) -- [Folder structure](./folders.md) (add this file) -- [Key modules](./modules.md) (add this file) +- [Getting started](./architecture/overview.md) +- [Architecture](./development-setup.md) +- [Codebase structure](./structure/folders.md) +- [Nodes](./nodes/overview.md) +- [Development Guides](./development.md) Start by expanding this overview and adding more files as needed. 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/sidebarsCodebase.js b/sidebarsCodebase.js index 2a84afe..6bfc3f9 100644 --- a/sidebarsCodebase.js +++ b/sidebarsCodebase.js @@ -1,10 +1,52 @@ module.exports = { codebaseSidebar: [ + 'overview', { - type: 'doc', - id: 'overview', - label: '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/visual-states', + 'nodes/variants', + 'nodes/frontend-nodes', + ], + }, + { + type: 'category', + label: 'Development Guides', + items: [ + 'guides/adding-nodes', + ], }, - // Add more docs here ], }; From 2cb1a9fd384600daf24be8bd1a20c52d1a34d921 Mon Sep 17 00:00:00 2001 From: Axel Wretman Date: Fri, 5 Dec 2025 09:19:10 +0100 Subject: [PATCH 3/3] AI hallucination cleanup and added details concerning node definitons. --- codebase/build-test.md | 55 +---- codebase/development-setup.md | 2 - codebase/guides/adding-nodes.md | 46 +--- codebase/nodes/definition.md | 23 -- codebase/nodes/instance.md | 4 +- codebase/nodes/nodes.md | 90 -------- codebase/nodes/scope.md | 13 -- codebase/nodes/variants.md | 397 +++++--------------------------- codebase/overview.md | 6 +- sidebarsCodebase.js | 3 +- src/css/variables.css | 110 ++++----- static/img/logo.png | Bin 15080 -> 58566 bytes 12 files changed, 135 insertions(+), 614 deletions(-) delete mode 100644 codebase/nodes/nodes.md diff --git a/codebase/build-test.md b/codebase/build-test.md index 6ebf4e4..69454f7 100644 --- a/codebase/build-test.md +++ b/codebase/build-test.md @@ -12,25 +12,14 @@ npm run dev yarn dev ``` -### Production Build - -```bash -npm run build -# or -yarn build -``` - -### Platform-Specific Builds +### Build ```bash # Desktop app (Electron) -npm run build:desktop - -# Web app -npm run build:web +npm run build:editor -# All platforms -npm run build:all +# Extract executables into /dist folder +npm run build:editor:pack ``` ## Testing @@ -38,40 +27,13 @@ npm run build:all ### Running Tests ```bash -# Run all tests -npm test +# Run editor tests +npm run test:editor -# Run tests in watch mode -npm run test:watch - -# Run tests with coverage -npm run test:coverage +# Run platform tests +npm run test:platform ``` -### Test Types - -#### Unit Tests - -- Located alongside source files (`.test.js`) -- Test individual functions and components -- Use Jest framework - -#### Integration Tests - -- Located in `/tests/integration` -- Test component interactions -- Use React Testing Library - -#### End-to-End Tests - -- Located in `/tests/e2e` -- Test complete user workflows -- Use Playwright or Cypress - -### Writing Tests - -Follow the testing guidelines in [Testing Guidelines](./guides/testing.md). - ## Continuous Integration Our CI pipeline runs on GitHub Actions: @@ -79,4 +41,3 @@ Our CI pipeline runs on GitHub Actions: - Automated testing on pull requests - Build verification for all platforms - Code quality checks (ESLint, Prettier) -- Security scanning diff --git a/codebase/development-setup.md b/codebase/development-setup.md index 1c21e25..c41a8cb 100644 --- a/codebase/development-setup.md +++ b/codebase/development-setup.md @@ -47,8 +47,6 @@ This guide will help you set up your development environment to contribute to Op - ESLint - Prettier -- GitLens -- Auto Rename Tag ### Environment Variables diff --git a/codebase/guides/adding-nodes.md b/codebase/guides/adding-nodes.md index 2d82099..61d9d35 100644 --- a/codebase/guides/adding-nodes.md +++ b/codebase/guides/adding-nodes.md @@ -19,9 +19,9 @@ Create a new file in `/packages/noodl-core-nodes/src/`: ```javascript // MyCustomNode.js -const { Node } = require("@noodl/runtime"); +import * as Noodl from "@noodl/noodl-sdk"; -class MyCustomNode extends Node { +const MyCustomNode = Noodl.defineNode( { static displayName = "My Custom Node"; static category = "Utilities"; @@ -47,7 +47,7 @@ class MyCustomNode extends Node { this.outputs.result = result; this.outputs.done(); } -} +}) module.exports = MyCustomNode; ``` @@ -91,7 +91,7 @@ module.exports = { ### Dynamic Inputs/Outputs ```javascript -class DynamicNode extends Node { +const DynamicNode = Noodl.defineNode({ static getInputs(nodeModel) { const inputs = { count: { type: "number" } }; @@ -106,13 +106,13 @@ class DynamicNode extends Node { static getOutputs(nodeModel) { // Similar dynamic output generation } -} +}) ``` ### Node Parameters ```javascript -class ConfigurableNode extends Node { +const ConfigurableNode = Noodl.defineNode({ static parameters = { mode: { type: "enum", @@ -126,13 +126,13 @@ class ConfigurableNode extends Node { max: 10, }, }; -} +}) ``` ### State Management ```javascript -class StatefulNode extends Node { +const StatefulNode = Noodl.defineNode({ constructor() { super(); this.state = { @@ -147,7 +147,7 @@ class StatefulNode extends Node { this.outputs.count = this.state.counter; } -} +}) ``` ## UI Component Nodes @@ -155,7 +155,7 @@ class StatefulNode extends Node { ### React Component Integration ```javascript -class MyUINode extends Node { +const MyUINode = Noodl.defineNode({ static displayName = "Custom Button"; static category = "UI"; @@ -173,7 +173,7 @@ class MyUINode extends Node { label: { type: "string", displayName: "Label" }, onClick: { type: "signal", displayName: "Click" }, }; -} +}) ``` ## Testing Nodes @@ -237,30 +237,6 @@ execute() { } ``` -## Documentation - -Add documentation for your node: - -```javascript -static docs = { - description: 'Converts input text to uppercase', - examples: [ - { - title: 'Basic Usage', - description: 'Connect a string input and trigger to see the uppercase result' - } - ], - inputs: { - value: 'The text to convert to uppercase', - trigger: 'Signal to execute the conversion' - }, - outputs: { - result: 'The uppercase version of the input text', - done: 'Signal sent when conversion is complete' - } -} -``` - ## Publishing Custom Nodes For nodes that should be available to the community: diff --git a/codebase/nodes/definition.md b/codebase/nodes/definition.md index fdac23c..9562fcd 100644 --- a/codebase/nodes/definition.md +++ b/codebase/nodes/definition.md @@ -214,29 +214,6 @@ inputs: { this._internal.alignment = value; this.updateAlignment(); } - }, - - // Enum with groups - category: { - type: { - name: 'enum', - enums: [ - { - label: 'Basic', - values: [ - { label: 'Type A', value: 'a' }, - { label: 'Type B', value: 'b' } - ] - }, - { - label: 'Advanced', - values: [ - { label: 'Type X', value: 'x' }, - { label: 'Type Y', value: 'y' } - ] - } - ] - } } } ``` diff --git a/codebase/nodes/instance.md b/codebase/nodes/instance.md index 2b80cd0..300c160 100644 --- a/codebase/nodes/instance.md +++ b/codebase/nodes/instance.md @@ -12,9 +12,9 @@ Node instances are runtime objects created from node definitions. They manage st All nodes inherit from `node.js` which provides core functionality: ```javascript -const Node = require("./node"); +import * as Noodl from "@noodl/noodl-sdk"; -class MyNode extends Node { +const MyNode = Noodl.defineNode( { constructor(context, id) { super(context, id); // Custom initialization diff --git a/codebase/nodes/nodes.md b/codebase/nodes/nodes.md deleted file mode 100644 index cd4f3bb..0000000 --- a/codebase/nodes/nodes.md +++ /dev/null @@ -1,90 +0,0 @@ -# Nodes — structure & patterns - -This page describes the typical structure of a Noodl "node" file, what sections you can implement, and common conventions used in the runtime package (see a:\Repos\OpenNoodl\packages\noodl-runtime\src\nodes). - -## Purpose - -A node file exports the definition/implementation of a runtime node. A node defines inputs, outputs, settings, behavior on input changes, and lifecycle hooks. Nodes live under packages/noodl-runtime/src/nodes (and subfolders like std-library, data, user, variables). - -## Typical sections - -- Module export - - The file exports a single object (or a registration call) describing the node. -- Metadata - - id/name, label, category, description — used by editors and docs. -- Inputs / outputs definitions - - Typed ports exposed to other nodes (name, type, default). -- Settings / properties - - Configurable options persisted with the component instance. -- Lifecycle hooks - - init / onAttach / onDetach — run when the node/component is created/connected/disposed. -- Runtime handlers - - onInput or handlers for specific inputs to implement behavior. -- State & storage - - Per-instance state that persists for the life of the component instance. -- Event / task scheduling - - Emit events, schedule async tasks, timers, or call runtime APIs. - -## Conventions and examples - -A minimal JS skeleton (illustrative only): - -```javascript -// minimal node skeleton -module.exports = { - id: "my.example.node", - label: "Example Node", - category: "logic", - inputs: { - inputA: { type: "boolean", default: false }, - value: { type: "number" }, - }, - outputs: { - outValue: { type: "number" }, - }, - settings: { - multiplier: { type: "number", default: 1 }, - }, - init(instance) { - // called once when node instance is created - instance.state = { count: 0 }; - }, - onInput(instance, inputName, value) { - // handle incoming values - if (inputName === "value") { - const result = value * (instance.settings.multiplier || 1); - instance.setOutput("outValue", result); - } - }, - onAttach(instance) { - // optional: when node becomes active/connected in graph - }, - onDetach(instance) { - // cleanup timers/async tasks - }, -}; -``` - -Notes: - -- instance provides helpers: read settings, set outputs, schedule tasks, subscribe/unsubscribe, access persistent component state. -- Use init to allocate resources and onDetach to clean them up to avoid leaks. -- Use descriptive ids and categories to keep the runtime organized (see std-library and data folders for examples). - -## Advanced patterns - -- Async operations: perform fetches or DB actions in handlers, update outputs when promises resolve. -- Composite components: nodes can coordinate child components via runtime APIs. -- Reusable utilities: factor repeated logic into helper modules (see std-library and variables folders). - -## Testing & debugging - -- Add console logs in lifecycle hooks and handlers. -- Unit test node logic by calling exported handlers with a mock instance (mock instance should implement setOutput, settings, state). -- Watch for mismatched input/output names and types — runtime will error if the sidebar/doc references non-existent ids. - -## Common pitfalls - -- Forgetting to clean timers/subscriptions in onDetach. -- Using globals for per-instance state (use instance.state). -- Using a documentation/sidebars id that doesn't match actual doc filenames (see your earlier sidebar error). diff --git a/codebase/nodes/scope.md b/codebase/nodes/scope.md index ed5410e..97ac48a 100644 --- a/codebase/nodes/scope.md +++ b/codebase/nodes/scope.md @@ -15,19 +15,6 @@ NodeScope (`nodescope.js`) manages all node instances within a component. It han - Handles node model synchronization - Provides node lookup and queries -## Creating a NodeScope - -```javascript -const NodeScope = require("./nodescope"); - -const scope = new NodeScope(context, componentOwner); -``` - -### Parameters - -- `context` - NodeContext instance -- `componentOwner` - Parent component instance (if any) - ## Node Creation ### Create from Model diff --git a/codebase/nodes/variants.md b/codebase/nodes/variants.md index 6669977..61601de 100644 --- a/codebase/nodes/variants.md +++ b/codebase/nodes/variants.md @@ -14,62 +14,7 @@ Variants allow nodes to have different parameter values based on active variants - Manage theme variations - Handle localization -## Variants System - -The variants system is managed by the `Variants` class (`variants.js`): - -```javascript -const Variants = require("./variants"); - -const variants = new Variants(); -``` - -## Defining Variants - -### In Project - -Variants are defined at the project level: - -```javascript -{ - variants: [ - { name: "mobile", condition: "screen.width < 768" }, - { name: "tablet", condition: "screen.width >= 768 && screen.width < 1024" }, - { name: "desktop", condition: "screen.width >= 1024" }, - ]; -} -``` - -### Common Variant Types - -**Responsive** - -- `mobile` -- `tablet` -- `desktop` -- `ultrawide` - -**Platform** - -- `ios` -- `android` -- `web` -- `native` - -**Theme** - -- `light` -- `dark` -- `high-contrast` - -**Localization** - -- `en` -- `es` -- `fr` -- Language-specific variants - -## Enabling Variants +## Enabling Variants in Nodes ### In Node Definition @@ -78,352 +23,122 @@ const { defineNode } = require("./nodedefinition"); const TextNode = defineNode({ name: "Visual.Text", - useVariants: true, + useVariants: true, // Enable variant support inputs: { fontSize: { type: "number", default: 16, }, - text: { - type: "string", - default: "Hello", - }, }, }); ``` -Setting `useVariants: true` enables variant support for all parameters. - -## Variant Values +Setting `useVariants: true` enables variant support for the node's parameters. -### Storage +## Variant Storage -Variant-specific values are stored with suffix: +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 - fontSize_mobile: 14, // Mobile variant - fontSize_tablet: 18, // Tablet variant - fontSize_desktop: 20 // Desktop variant + 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}` -### Getting Variant Values - -```javascript -const value = this.getVariantValue("fontSize", "mobile"); -``` - -Falls back to default if variant value not set. - -## Active Variants - -### Setting Active Variants - -```javascript -context.variants.setActiveVariants(["mobile", "dark"]); -``` - -### Checking Active Variants - -```javascript -if (context.variants.isActive("mobile")) { - // Mobile variant is active -} - -const activeVariants = context.variants.getActive(); -// Returns: ['mobile', 'dark'] -``` - -## Variant Resolution - -When multiple variants are active, values are resolved in order: - -1. Most specific variant (last active) -2. Less specific variants -3. Default value - -```javascript -// Active: ['mobile', 'dark'] -// Resolution order: dark → mobile → default - -const value = this.resolveVariantValue("backgroundColor"); -// Checks: backgroundColor_dark, backgroundColor_mobile, backgroundColor -``` - -## Node Integration - -### Applying Variants - -Nodes automatically apply variant values when variants change: - -```javascript -setVariant(variant) { - // Apply variant-specific parameters - Object.keys(this.model.parameters).forEach(key => { - const variantKey = `${key}_${variant}`; - if (this.model.parameters.hasOwnProperty(variantKey)) { - this.setParameter(key, this.model.parameters[variantKey]); - } - }); -} -``` - -### Listening to Variant Changes - -```javascript -initialize() { - this.context.variants.on('changed', () => { - this.updateForVariants(); - }); -} - -methods: { - updateForVariants() { - const activeVariants = this.context.variants.getActive(); - activeVariants.forEach(variant => { - this.setVariant(variant); - }); - } -} -``` +## Variants System -## Example: Responsive Layout +The variants system is managed by the `Variants` class available in `NodeContext`: ```javascript -const ContainerNode = defineNode({ - name: "Visual.Container", - useVariants: true, - - inputs: { - width: { - type: "number", - default: 1200, - }, - padding: { - type: "number", - default: 20, - }, - columns: { - type: "number", - default: 12, - }, - }, - - initialize() { - this._internal = { - element: document.createElement("div"), - }; - - // Listen to variant changes - this.context.variants.on("changed", () => { - this.applyLayout(); - }); - - this.applyLayout(); - }, - - methods: { - applyLayout() { - const variants = this.context.variants.getActive(); - - // Get values for active variants - let width = this.getInputValue("width"); - let padding = this.getInputValue("padding"); - let columns = this.getInputValue("columns"); - - // Override with variant-specific values - variants.forEach((variant) => { - const variantWidth = this.getVariantValue("width", variant); - const variantPadding = this.getVariantValue("padding", variant); - const variantColumns = this.getVariantValue("columns", variant); - - if (variantWidth !== undefined) width = variantWidth; - if (variantPadding !== undefined) padding = variantPadding; - if (variantColumns !== undefined) columns = variantColumns; - }); - - // Apply to element - const el = this._internal.element; - el.style.maxWidth = `${width}px`; - el.style.padding = `${padding}px`; - el.style.gridTemplateColumns = `repeat(${columns}, 1fr)`; - }, - }, -}); +// Access variants from node context +const variants = this.context.variants; ``` -## Example: Theme Support +## Common Variant Types -```javascript -const ThemedBoxNode = defineNode({ - name: "Visual.ThemedBox", - useVariants: true, +**Responsive** - inputs: { - backgroundColor: { - type: "color", - default: "#ffffff", - }, - textColor: { - type: "color", - default: "#000000", - }, - borderColor: { - type: "color", - default: "#cccccc", - }, - }, +- `mobile` +- `tablet` +- `desktop` - initialize() { - // Default theme - this.parameters.backgroundColor = "#ffffff"; - this.parameters.textColor = "#000000"; +**Platform** - // Dark theme - this.parameters.backgroundColor_dark = "#1a1a1a"; - this.parameters.textColor_dark = "#ffffff"; +- `ios` +- `android` +- `web` - // High contrast theme - this.parameters.backgroundColor_highcontrast = "#000000"; - this.parameters.textColor_highcontrast = "#ffff00"; +**Theme** - this.applyTheme(); - }, +- `light` +- `dark` - methods: { - applyTheme() { - const variants = this.context.variants.getActive(); - const themeVariant = variants.find((v) => - ["dark", "light", "highcontrast"].includes(v) - ); +**Localization** - if (themeVariant) { - this.setVariant(themeVariant); - } - }, - }, -}); -``` +- Language codes (e.g., `en`, `es`, `fr`) -## Conditional Variants +## Node Variant Method -Variants can have conditions that determine when they're active: +Nodes with variants support the `setVariant` method: ```javascript { - name: 'mobile', - condition: 'screen.width < 768' + setVariant(variant) { + // Called when variant changes + // Node should update its parameters based on the variant + } } ``` -The system evaluates conditions and activates matching variants automatically. - ## Querying Nodes by Variant -Find all nodes with a specific variant: +The NodeScope provides a method to find nodes with specific variants: ```javascript -const mobileNodes = nodeScope.getAllNodesWithVariantRecursive("mobile"); -``` - -## Variant Transitions - -Smoothly transition between variants: - -```javascript -methods: { - transitionToVariant(variant) { - const el = this._internal.element; - - // Enable transitions - el.style.transition = 'all 0.3s ease'; - - // Apply variant - this.setVariant(variant); - } -} +const nodesWithVariant = nodeScope.getAllNodesWithVariantRecursive("mobile"); ``` ## Editor Integration -### Variant Selector - -The editor provides a variant selector: +### Variant-Specific Parameters -``` -┌────────────────────┐ -│ Variants: ▼ │ -├────────────────────┤ -│ ☐ Mobile │ -│ ☐ Tablet │ -│ ☑ Desktop │ -│ ☐ Dark Theme │ -└────────────────────┘ -``` +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. -### Parameter Panel - -Shows variant-specific values: - -``` -┌─────────────────────────┐ -│ Font Size │ -│ ┌─────────────────────┐ │ -│ │ Default: 16 │ │ -│ │ Mobile: 14 │ │ -│ │ Tablet: 18 │ │ -│ │ Desktop: 20 │ │ -│ └─────────────────────┘ │ -└─────────────────────────┘ -``` - -## Performance Considerations - -1. **Limit active variants** - Too many active variants can impact performance -2. **Cache variant values** - Don't recalculate on every frame -3. **Batch variant changes** - Change multiple variants at once -4. **Lazy evaluation** - Only resolve variants when needed +### Variant Selector -## Testing Variants +The editor provides UI for selecting and previewing different variants during development. -```javascript -test("node respects variant values", () => { - const node = createNode("Visual.Text", "text1"); +## Best Practices - // Set default and variant values - node.setParameter("fontSize", 16); - node.setVariantValue("fontSize", "mobile", 14); +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 - // Activate mobile variant - context.variants.setActiveVariants(["mobile"]); +## Notes - // Check resolved value - expect(node.getResolvedValue("fontSize")).toBe(14); +The variants system implementation details may vary. This documentation covers the basic variant support available through the `useVariants` flag and variant parameter naming convention. - // Deactivate variant - context.variants.setActiveVariants([]); +For the most accurate and up-to-date information about the variants API, refer to: - // Check default value - expect(node.getResolvedValue("fontSize")).toBe(16); -}); -``` +- `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 -## Best Practices +## See Also -1. **Use semantic names** - Name variants by purpose, not specifics -2. **Provide defaults** - Always have default values -3. **Limit variant count** - Too many variants complicate management -4. **Test all variants** - Verify behavior in each variant -5. **Document conditions** - Explain when variants are active -6. **Graceful degradation** - Fall back to defaults when variant missing -7. **Consider inheritance** - Child components inherit variants -8. **Optimize performance** - Cache resolved values +- [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/overview.md b/codebase/overview.md index 73a9104..be82308 100644 --- a/codebase/overview.md +++ b/codebase/overview.md @@ -4,10 +4,8 @@ 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](./architecture/overview.md) -- [Architecture](./development-setup.md) +- [Getting started](./development-setup.md) +- [Architecture](./architecture/overview.md) - [Codebase structure](./structure/folders.md) - [Nodes](./nodes/overview.md) - [Development Guides](./development.md) - -Start by expanding this overview and adding more files as needed. diff --git a/sidebarsCodebase.js b/sidebarsCodebase.js index 6bfc3f9..87e9934 100644 --- a/sidebarsCodebase.js +++ b/sidebarsCodebase.js @@ -36,9 +36,8 @@ module.exports = { 'nodes/scope', 'nodes/context', 'nodes/dynamic-ports', - 'nodes/visual-states', - 'nodes/variants', 'nodes/frontend-nodes', + 'nodes/visual-states', ], }, { 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 1ac93c07ac2588ec21c442545ce671d185bcf149..3336d8b74ecc66b176a4b9643dd9fb5bd4669c76 100644 GIT binary patch literal 58566 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 DmIj8N