diff --git a/.clinerules b/.clinerules
index 4197372..a75fc97 100644
--- a/.clinerules
+++ b/.clinerules
@@ -123,6 +123,7 @@ packages/
- `dev-docs/reference/NODE-PATTERNS.md` - How to create/modify nodes
- `dev-docs/reference/LEARNINGS.md` - Accumulated knowledge
- `dev-docs/reference/UI-STYLING-GUIDE.md` - Styling rules (NO hardcoded colors!)
+- `dev-docs/reference/PANEL-UI-STYLE-GUIDE.md` - **Panels & Modals (READ BEFORE making UI!)**
- `dev-docs/reference/LEARNINGS-NODE-CREATION.md` - Node creation gotchas
---
@@ -418,6 +419,20 @@ Before marking any task complete:
- [ ] **Discoveries added to LEARNINGS.md or COMMON-ISSUES.md**
- [ ] Task CHANGELOG.md updated with progress
+### Visual Components (Panels, Modals, Forms)
+
+**MUST read `dev-docs/reference/PANEL-UI-STYLE-GUIDE.md` before building UI!**
+
+- [ ] NO emojis in buttons, labels, or headers
+- [ ] Using CSS variables for ALL colors (`var(--theme-color-*)`)
+- [ ] Using `Text` component with proper `textType` for typography
+- [ ] Using `PrimaryButton` with correct variant (Cta/Muted/Ghost/Danger)
+- [ ] Panel structure: Header → Toolbar → Content → Footer
+- [ ] Modal structure: Overlay → Modal → Header → Body → Footer
+- [ ] Form inputs styled with proper tokens (bg-1, bg-3 borders)
+- [ ] Empty states, loading states, and error states handled
+- [ ] Dark theme first - ensure contrast with light text
+
### Git
- [ ] Meaningful commit messages (conventional commits format)
diff --git a/dev-docs/reference/LEARNINGS.md b/dev-docs/reference/LEARNINGS.md
index 6161af3..4439550 100644
--- a/dev-docs/reference/LEARNINGS.md
+++ b/dev-docs/reference/LEARNINGS.md
@@ -10,6 +10,95 @@ These fundamental patterns apply across ALL Noodl development. Understanding the
---
+## 📊 Property Expression Runtime Context (Jan 16, 2026)
+
+### The Context Trap: Why evaluateExpression() Needs undefined, Not this.context
+
+**Context**: TASK-006 Expressions Overhaul - Property expressions in the properties panel weren't evaluating. Error: "scope.get is not a function".
+
+**The Problem**: The `evaluateExpression()` function in `expression-evaluator.js` expects either `undefined` (to use global Model) or a Model scope object. Passing `this.context` (the runtime node context with editorConnection, styles, etc.) caused the error because it lacks a `.get()` method.
+
+**The Broken Pattern**:
+
+```javascript
+// ❌ WRONG - this.context is NOT a Model scope
+Node.prototype._evaluateExpressionParameter = function (paramValue, portName) {
+ const compiled = compileExpression(paramValue.expression);
+ const result = evaluateExpression(compiled, this.context); // ☠️ scope.get is not a function
+};
+```
+
+**The Correct Pattern**:
+
+```javascript
+// ✅ RIGHT - Pass undefined to use global Model
+Node.prototype._evaluateExpressionParameter = function (paramValue, portName) {
+ const compiled = compileExpression(paramValue.expression);
+ const result = evaluateExpression(compiled, undefined); // ✅ Uses global Model
+};
+```
+
+**Why This Works**:
+
+The expression-evaluator's `createNoodlContext()` function does:
+
+```javascript
+const scope = modelScope || Model; // Falls back to global Model
+const variablesModel = scope.get('--ndl--global-variables');
+```
+
+When `undefined` is passed, it uses the global `Model` which has the `.get()` method. The runtime's `this.context` is a completely different object containing `editorConnection`, `styles`, etc.
+
+**Reactive Expression Subscriptions**:
+
+To make expressions update when Variables change, the expression-evaluator already has:
+
+- `detectDependencies(expression)` - finds what Variables/Objects/Arrays are referenced
+- `subscribeToChanges(dependencies, callback)` - subscribes to Model changes
+
+Wire these up in `_evaluateExpressionParameter`:
+
+```javascript
+// Set up reactive subscription
+if (!this._expressionSubscriptions[portName]) {
+ const dependencies = detectDependencies(paramValue.expression);
+ if (dependencies.variables.length > 0) {
+ this._expressionSubscriptions[portName] = subscribeToChanges(
+ dependencies,
+ function () {
+ if (this._deleted) return;
+ this.queueInput(portName, paramValue); // Re-evaluate
+ }.bind(this)
+ );
+ }
+}
+```
+
+**Critical Rules**:
+
+1. **NEVER** pass `this.context` to `evaluateExpression()` - it's not a Model scope
+2. **ALWAYS** pass `undefined` to use the global Model for Variables/Objects/Arrays
+3. **Clean up subscriptions** in `_onNodeDeleted()` to prevent memory leaks
+4. **Track subscriptions by port name** to avoid duplicate listeners
+
+**Applies To**:
+
+- Property panel expression fields
+- Any runtime expression evaluation
+- Future expression support in other property types
+
+**Time Saved**: This pattern prevents 1-2 hours debugging "scope.get is not a function" errors.
+
+**Location**:
+
+- Fixed in: `packages/noodl-runtime/src/node.js`
+- Expression evaluator: `packages/noodl-runtime/src/expression-evaluator.js`
+- Task: Phase 3 TASK-006 Expressions Overhaul
+
+**Keywords**: expression, evaluateExpression, this.context, Model scope, scope.get, Variables, reactive, subscribeToChanges, detectDependencies
+
+---
+
## 🔴 Editor/Runtime Window Separation (Jan 2026)
### The Invisible Boundary: Why Editor Methods Don't Exist in Runtime
diff --git a/dev-docs/reference/PANEL-UI-STYLE-GUIDE.md b/dev-docs/reference/PANEL-UI-STYLE-GUIDE.md
new file mode 100644
index 0000000..3f2ba39
--- /dev/null
+++ b/dev-docs/reference/PANEL-UI-STYLE-GUIDE.md
@@ -0,0 +1,511 @@
+# Panel & Modal UI Style Guide
+
+This guide documents the visual patterns used in OpenNoodl's editor panels and modals. **Always follow these patterns when creating new UI components.**
+
+---
+
+## Core Principles
+
+### 1. Professional, Not Playful
+
+- **NO emojis** in UI labels, buttons, or headers
+- **NO decorative icons** unless they serve a functional purpose
+- Clean, minimal aesthetic that respects the user's intelligence
+- Think "developer tool" not "consumer app"
+
+### 2. Consistent Visual Language
+
+- Use design tokens (CSS variables) for ALL colors
+- Consistent spacing using the spacing system (4px base unit)
+- Typography hierarchy using the Text component types
+- All interactive elements must have hover/active states
+
+### 3. Dark Theme First
+
+- Design for dark backgrounds (`--theme-color-bg-2`, `--theme-color-bg-3`)
+- Ensure sufficient contrast with light text
+- Colored elements should be muted, not neon
+
+---
+
+## Panel Structure
+
+### Standard Panel Layout
+
+```tsx
+
+ {/* Header with title and close button */}
+
+
+
+
+ Panel Title
+
+ Subtitle or context
+
+
+
+
+
+
+ {/* Toolbar with actions */}
+
{/* Filters, search, action buttons */}
+
+ {/* Content area (scrollable) */}
+
{/* Main panel content */}
+
+ {/* Footer (optional - pagination, status) */}
+
{/* Page controls, counts */}
+
+```
+
+### Panel CSS Pattern
+
+```scss
+.Root {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background-color: var(--theme-color-bg-2);
+ color: var(--theme-color-fg-default);
+}
+
+.Header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--theme-color-bg-3);
+ flex-shrink: 0;
+}
+
+.Toolbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 16px;
+ background-color: var(--theme-color-bg-3);
+ gap: 8px;
+ flex-shrink: 0;
+}
+
+.Content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 16px;
+}
+
+.Footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 16px;
+ border-top: 1px solid var(--theme-color-bg-3);
+ background-color: var(--theme-color-bg-2);
+ flex-shrink: 0;
+}
+```
+
+---
+
+## Modal Structure
+
+### Standard Modal Layout
+
+```tsx
+
+
+ {/* Header */}
+
+ Modal Title
+
+
+
+ {/* Body */}
+
{/* Form fields, content */}
+
+ {/* Footer with actions */}
+
+
+
+```
+
+### Modal CSS Pattern
+
+```scss
+.Overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.6);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.Modal {
+ background-color: var(--theme-color-bg-2);
+ border-radius: 8px;
+ width: 480px;
+ max-width: 90vw;
+ max-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+}
+
+.Header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--theme-color-bg-3);
+}
+
+.Body {
+ padding: 20px;
+ overflow-y: auto;
+ flex: 1;
+}
+
+.Footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ padding: 16px 20px;
+ border-top: 1px solid var(--theme-color-bg-3);
+}
+```
+
+---
+
+## Form Elements
+
+### Text Inputs
+
+```scss
+.Input {
+ width: 100%;
+ padding: 8px 12px;
+ border: 1px solid var(--theme-color-bg-3);
+ border-radius: 4px;
+ background-color: var(--theme-color-bg-1);
+ color: var(--theme-color-fg-default);
+ font-size: 13px;
+ font-family: inherit;
+
+ &:focus {
+ outline: none;
+ border-color: var(--theme-color-primary);
+ }
+
+ &::placeholder {
+ color: var(--theme-color-fg-default-shy);
+ }
+}
+```
+
+### Select Dropdowns
+
+```scss
+.Select {
+ padding: 8px 12px;
+ border: 1px solid var(--theme-color-bg-3);
+ border-radius: 4px;
+ background-color: var(--theme-color-bg-1);
+ color: var(--theme-color-fg-default);
+ font-size: 13px;
+ min-width: 120px;
+
+ &:focus {
+ outline: none;
+ border-color: var(--theme-color-primary);
+ }
+}
+```
+
+### Form Groups
+
+```scss
+.FormGroup {
+ margin-bottom: 16px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.Label {
+ display: block;
+ margin-bottom: 6px;
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--theme-color-fg-default);
+}
+
+.HelpText {
+ margin-top: 4px;
+ font-size: 11px;
+ color: var(--theme-color-fg-default-shy);
+}
+```
+
+---
+
+## Data Tables & Grids
+
+### Table Pattern
+
+```scss
+.Grid {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 13px;
+
+ th {
+ text-align: left;
+ padding: 10px 12px;
+ font-weight: 500;
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ color: var(--theme-color-fg-default-shy);
+ background-color: var(--theme-color-bg-3);
+ border-bottom: 1px solid var(--theme-color-bg-3);
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ }
+
+ td {
+ padding: 8px 12px;
+ border-bottom: 1px solid var(--theme-color-bg-3);
+ color: var(--theme-color-fg-default);
+ }
+
+ tr:hover td {
+ background-color: var(--theme-color-bg-3);
+ }
+}
+```
+
+### Type Badges
+
+For showing data types, use subtle colored badges:
+
+```scss
+.TypeBadge {
+ display: inline-block;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-size: 10px;
+ font-weight: 500;
+ color: white;
+}
+
+// Use semantic colors, not hardcoded
+.TypeString {
+ background-color: var(--theme-color-primary);
+}
+.TypeNumber {
+ background-color: var(--theme-color-success);
+}
+.TypeBoolean {
+ background-color: var(--theme-color-notice);
+}
+.TypeDate {
+ background-color: #8b5cf6;
+} // Purple - no token available
+.TypePointer {
+ background-color: var(--theme-color-danger);
+}
+```
+
+---
+
+## Expandable Rows
+
+For tree-like or expandable content:
+
+```scss
+.ExpandableRow {
+ border: 1px solid var(--theme-color-bg-3);
+ border-radius: 6px;
+ margin-bottom: 8px;
+ overflow: hidden;
+ background-color: var(--theme-color-bg-2);
+
+ &[data-expanded='true'] {
+ border-color: var(--theme-color-primary);
+ }
+}
+
+.RowHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ cursor: pointer;
+ transition: background-color 0.15s;
+
+ &:hover {
+ background-color: var(--theme-color-bg-3);
+ }
+}
+
+.RowContent {
+ padding: 0 16px 16px;
+ background-color: var(--theme-color-bg-1);
+ border-top: 1px solid var(--theme-color-bg-3);
+}
+
+.ExpandIcon {
+ transition: transform 0.2s;
+ color: var(--theme-color-fg-default-shy);
+}
+```
+
+---
+
+## Empty States
+
+When there's no content to show:
+
+```scss
+.EmptyState {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 48px 24px;
+ text-align: center;
+ color: var(--theme-color-fg-default-shy);
+
+ .EmptyIcon {
+ font-size: 48px;
+ margin-bottom: 16px;
+ opacity: 0.5;
+ }
+
+ .EmptyText {
+ margin-bottom: 8px;
+ }
+}
+```
+
+---
+
+## Loading & Error States
+
+### Loading
+
+```scss
+.Loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 48px;
+ color: var(--theme-color-fg-default-shy);
+}
+```
+
+### Error
+
+```scss
+.Error {
+ padding: 12px 16px;
+ background-color: rgba(239, 68, 68, 0.1);
+ border: 1px solid var(--theme-color-danger);
+ border-radius: 4px;
+ color: var(--theme-color-danger);
+ font-size: 13px;
+}
+```
+
+---
+
+## Button Patterns
+
+### Use PrimaryButton Variants Correctly
+
+| Variant | Use For |
+| -------- | ------------------------------------------ |
+| `Cta` | Primary action (Create, Save, Submit) |
+| `Muted` | Secondary action (Cancel, Close, Refresh) |
+| `Ghost` | Tertiary action (Edit, View, minor action) |
+| `Danger` | Destructive action (Delete) |
+
+### Button Sizing
+
+- `Small` - In toolbars, table rows, compact spaces
+- `Medium` - Modal footers, standalone actions
+- `Large` - Rarely used, hero actions only
+
+---
+
+## Spacing System
+
+Use consistent spacing based on 4px unit:
+
+| Token | Value | Use For |
+| ----- | ----- | ------------------------ |
+| `xs` | 4px | Tight spacing, icon gaps |
+| `sm` | 8px | Related elements |
+| `md` | 12px | Standard padding |
+| `lg` | 16px | Section padding |
+| `xl` | 24px | Large gaps |
+| `xxl` | 32px | Major sections |
+
+---
+
+## Typography
+
+### Use Text Component Types
+
+| Type | Use For |
+| ----------------- | ------------------------------- |
+| `Proud` | Panel titles, modal headers |
+| `DefaultContrast` | Primary content, item names |
+| `Default` | Body text, descriptions |
+| `Shy` | Secondary text, hints, metadata |
+
+### Font Sizes
+
+- Headers: 14-16px
+- Body: 13px
+- Labels: 12px
+- Small text: 11px
+- Badges: 10px
+
+---
+
+## Don'ts
+
+❌ **Don't use emojis** in buttons or labels
+❌ **Don't use hardcoded colors** - always use CSS variables
+❌ **Don't use bright/neon colors** - keep it muted
+❌ **Don't use decorative icons** that don't convey meaning
+❌ **Don't use rounded corners > 8px** - keep it subtle
+❌ **Don't use shadows > 0.4 opacity** - stay subtle
+❌ **Don't use animation duration > 200ms** - keep it snappy
+❌ **Don't mix different styling approaches** - be consistent
+
+---
+
+## Reference Components
+
+For working examples, see:
+
+- `packages/noodl-editor/src/editor/src/views/panels/schemamanager/`
+- `packages/noodl-editor/src/editor/src/views/panels/databrowser/`
+- `packages/noodl-editor/src/editor/src/views/panels/BackendServicesPanel/`
+
+---
+
+_Last Updated: January 2026_
diff --git a/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-006-expressions-overhaul/PHASE-2B-COMPLETE.md b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-006-expressions-overhaul/PHASE-2B-COMPLETE.md
new file mode 100644
index 0000000..e3f9722
--- /dev/null
+++ b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-006-expressions-overhaul/PHASE-2B-COMPLETE.md
@@ -0,0 +1,219 @@
+# Phase 2B: Inline Property Expressions - COMPLETE ✅
+
+**Started:** 2026-01-10
+**Completed:** 2026-01-16
+**Status:** ✅ COMPLETE
+
+---
+
+## Summary
+
+Inline property expressions are now fully functional! Users can set property values using JavaScript expressions that reference Noodl.Variables, Noodl.Objects, and Noodl.Arrays. Expressions update reactively when their dependencies change.
+
+---
+
+## ✅ What Was Implemented
+
+### 1. ExpressionInput Component - COMPLETE ✅
+
+**Files Created:**
+
+- `packages/noodl-core-ui/src/components/property-panel/ExpressionInput/ExpressionInput.tsx`
+- `packages/noodl-core-ui/src/components/property-panel/ExpressionInput/ExpressionInput.module.scss`
+
+**Features:**
+
+- Monospace input field with "fx" badge
+- Expression mode toggle
+- Expand button to open full editor modal
+- Error state display
+
+---
+
+### 2. ExpressionEditorModal - COMPLETE ✅
+
+**Files Created:**
+
+- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionEditorModal/ExpressionEditorModal.tsx`
+- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionEditorModal/ExpressionEditorModal.module.scss`
+- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionEditorModal/index.ts`
+
+**Features:**
+
+- Full-screen modal for editing complex expressions
+- Uses JavaScriptEditor (CodeMirror) for syntax highlighting
+- Help documentation showing available globals (Noodl.Variables, etc.)
+- Apply/Cancel buttons
+
+---
+
+### 3. PropertyPanelInput Integration - COMPLETE ✅
+
+**Files Modified:**
+
+- `packages/noodl-core-ui/src/components/property-panel/PropertyPanelInput/PropertyPanelInput.tsx`
+
+**Features:**
+
+- Added expression-related props (supportsExpression, expressionMode, expression, etc.)
+- Conditional rendering: expression input vs fixed input
+- Mode change handlers
+
+---
+
+### 4. PropertyPanelInputWithExpressionModal Wrapper - COMPLETE ✅
+
+**Files Created:**
+
+- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/components/PropertyPanelInputWithExpressionModal.tsx`
+
+**Features:**
+
+- Combines PropertyPanelInput with ExpressionEditorModal
+- Manages modal open/close state
+- Handles expression expand functionality
+
+---
+
+### 5. BasicType Property Editor Wiring - COMPLETE ✅
+
+**Files Modified:**
+
+- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/DataTypes/BasicType.ts`
+
+**Features:**
+
+- Uses React for rendering via createRoot
+- Supports expression mode toggle
+- Integrates with ExpressionParameter model
+- Uses ParameterValueResolver for safe value display
+
+---
+
+### 6. Runtime Reactive Subscriptions - COMPLETE ✅
+
+**Files Modified:**
+
+- `packages/noodl-runtime/src/node.js`
+
+**Features:**
+
+- `_evaluateExpressionParameter()` now sets up reactive subscriptions
+- Uses `detectDependencies()` to find referenced Variables/Objects/Arrays
+- Uses `subscribeToChanges()` to listen for dependency updates
+- Re-queues input when dependencies change
+- Cleanup in `_onNodeDeleted()`
+
+---
+
+## 🎯 How It Works
+
+### User Experience
+
+1. Select a node with text/number properties
+2. Click the "fx" button to toggle expression mode
+3. Enter an expression like `Noodl.Variables.userName`
+4. The value updates automatically when the variable changes
+5. Click the expand button for a full code editor modal
+
+### Supported Expressions
+
+```javascript
+// Simple variable access
+Noodl.Variables.userName;
+
+// Math operations
+Noodl.Variables.count * 2 + 1;
+
+// Ternary conditionals
+Noodl.Variables.isLoggedIn ? 'Logout' : 'Login';
+
+// String concatenation
+'Hello, ' + Noodl.Variables.userName + '!';
+
+// Math helpers
+min(Noodl.Variables.price, 100);
+max(10, Noodl.Variables.quantity);
+round(Noodl.Variables.total);
+
+// Object/Array access
+Noodl.Objects['user'].name;
+Noodl.Arrays['items'].length;
+```
+
+---
+
+## 📁 Files Changed
+
+### Created
+
+- `packages/noodl-core-ui/src/components/property-panel/ExpressionInput/ExpressionInput.tsx`
+- `packages/noodl-core-ui/src/components/property-panel/ExpressionInput/ExpressionInput.module.scss`
+- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionEditorModal/ExpressionEditorModal.tsx`
+- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionEditorModal/ExpressionEditorModal.module.scss`
+- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/ExpressionEditorModal/index.ts`
+- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/components/PropertyPanelInputWithExpressionModal.tsx`
+
+### Modified
+
+- `packages/noodl-core-ui/src/components/property-panel/PropertyPanelInput/PropertyPanelInput.tsx`
+- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/DataTypes/BasicType.ts`
+- `packages/noodl-runtime/src/node.js` (added reactive subscriptions)
+
+---
+
+## 🐛 Bug Fixes (2026-01-16)
+
+### Modal Showing Stale Expression
+
+**Problem:** When editing an expression in the inline input field and then opening the modal, the modal showed the old expression text instead of the current one.
+
+**Root Cause:** `BasicType.ts` was not re-rendering after `onExpressionChange`, so the modal component never received the updated expression prop.
+
+**Fix:** Added `setTimeout(() => this.renderReact(), 0)` at the end of `onExpressionChange` in `BasicType.ts` to ensure the React tree updates with the new expression value.
+
+### Variable Changes Not Updating Node Values
+
+**Problem:** When using an expression like `Noodl.Variables.label_text`, changing the variable value didn't update the node's property until the expression was manually saved again.
+
+**Root Cause:** The subscription callback in `node.js` captured the old `paramValue` object in a closure. When the subscription fired, it re-queued the old expression, not the current one stored in `_inputValues`.
+
+**Fix:** Updated subscription management in `_evaluateExpressionParameter()`:
+
+- Subscriptions now track both the unsubscribe function AND the expression string
+- When expression changes, old subscription is unsubscribed before creating a new one
+- The callback now reads from `this._inputValues[portName]` (current value) instead of using a closure
+
+**Files Modified:**
+
+- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/DataTypes/BasicType.ts`
+- `packages/noodl-runtime/src/node.js`
+
+---
+
+## 🎓 Key Learnings
+
+### Context Issue
+
+The `evaluateExpression()` function in expression-evaluator.js expects either `undefined` or a Model scope as the second parameter. Passing `this.context` (the runtime context with editorConnection, styles, etc.) caused "scope.get is not a function" errors. **Solution:** Pass `undefined` to use the global Model.
+
+### Reactive Updates
+
+The expression-evaluator module already had `detectDependencies()` and `subscribeToChanges()` functions. We just needed to wire them into `_evaluateExpressionParameter()` in node.js to enable reactive updates.
+
+### Value Display
+
+Expression parameters are objects (`{ mode: 'expression', expression: '...', fallback: ... }`). When displaying the value in the properties panel, we need to extract the fallback value, not display the object. The `ParameterValueResolver.toString()` helper handles this.
+
+---
+
+## 🚀 Future Enhancements
+
+1. **Canvas rendering** - Show expression indicator on node ports (TASK-006B)
+2. **More property types** - Extend to color, enum, and other types
+3. **Expression autocomplete** - IntelliSense for Noodl.Variables names
+4. **Expression validation** - Real-time syntax checking
+
+---
+
+**Last Updated:** 2026-01-16
diff --git a/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007H-COMPLETE.md b/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007H-COMPLETE.md
new file mode 100644
index 0000000..61fd8dd
--- /dev/null
+++ b/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007H-COMPLETE.md
@@ -0,0 +1,107 @@
+# TASK-007H: Schema Manager UI - COMPLETE
+
+## Summary
+
+Implemented a Schema Manager UI panel for viewing and managing database schemas in local SQLite backends.
+
+## Files Created
+
+### Schema Manager Panel Components
+
+- `packages/noodl-editor/src/editor/src/views/panels/schemamanager/SchemaPanel.tsx` - Main panel showing tables list
+- `packages/noodl-editor/src/editor/src/views/panels/schemamanager/SchemaPanel.module.scss` - Panel styles
+- `packages/noodl-editor/src/editor/src/views/panels/schemamanager/TableRow.tsx` - Expandable table row with columns
+- `packages/noodl-editor/src/editor/src/views/panels/schemamanager/TableRow.module.scss` - Table row styles
+- `packages/noodl-editor/src/editor/src/views/panels/schemamanager/index.ts` - Module exports
+
+## Files Modified
+
+### BackendManager.js
+
+Added IPC handlers for schema operations:
+
+- `backend:getSchema` - Get full schema for a backend
+- `backend:getTableSchema` - Get single table schema
+- `backend:getRecordCount` - Get record count for a table
+- `backend:createTable` - Create a new table
+- `backend:addColumn` - Add column to existing table
+
+### LocalBackendCard.tsx
+
+- Added "Schema" button (shows when backend is running)
+- Added schema panel overlay state
+- Integrated SchemaPanel component
+
+### LocalBackendCard.module.scss
+
+- Added `.SchemaPanelOverlay` styles for full-screen modal:
+ - Uses React Portal (`createPortal`) to render at `document.body`
+ - `z-index: 9999` ensures overlay above all UI elements
+ - Dark backdrop (`rgba(0, 0, 0, 0.85)`)
+ - Centered modal with max-width 900px
+ - Proper border-radius, shadow, and background styling
+
+## Features Implemented
+
+### SchemaPanel
+
+- Shows list of all tables in the backend
+- Displays table count in header
+- Refresh button to reload schema
+- Empty state with "Create First Table" prompt (placeholder)
+- Close button to dismiss panel
+
+### TableRow
+
+- Collapsible rows with expand/collapse toggle
+- Table icon with "T" badge
+- Shows field count and record count
+- Expanded view shows:
+ - System columns (objectId, createdAt, updatedAt)
+ - User-defined columns with type badges
+ - Required indicator
+ - Default value display
+- Type badges with color-coded data types:
+ - String (primary)
+ - Number (success/green)
+ - Boolean (notice/yellow)
+ - Date (purple)
+ - Object (pink)
+ - Array (indigo)
+ - Pointer/Relation (red)
+ - GeoPoint (teal)
+ - File (orange)
+
+## Usage
+
+1. Create and start a local backend
+2. Click "Schema" button on the backend card
+3. View tables and expand rows to see columns
+4. Click "Refresh" to reload schema
+5. Click "Close" to dismiss panel
+
+## Future Enhancements (Deferred)
+
+The following were scoped but not implemented in this initial version:
+
+- CreateTableModal - For creating new tables
+- ColumnEditor - For editing column definitions
+- SchemaEditor - Full table editor
+- ExportDialog - Export schema to SQL formats (handlers exist in BackendManager)
+
+These can be added incrementally as needed.
+
+## Verification
+
+The implementation follows project patterns:
+
+- ✅ Uses theme tokens (no hardcoded colors)
+- ✅ Uses IPC pattern from useLocalBackends
+- ✅ Proper TypeScript types
+- ✅ JSDoc documentation
+- ✅ Module SCSS with CSS modules
+- ✅ Follows existing component structure
+
+---
+
+Completed: 15/01/2026
diff --git a/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007H-schema-manager-ui.md b/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007H-schema-manager-ui.md
new file mode 100644
index 0000000..e8a72da
--- /dev/null
+++ b/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007H-schema-manager-ui.md
@@ -0,0 +1,1189 @@
+# TASK-007H: Schema Manager UI
+
+## Overview
+
+Build a native Nodegx UI for viewing and editing database schemas in local backends, providing a visual interface over the existing SchemaManager implementation.
+
+**Parent Task:** TASK-007 (Integrated Local Backend)
+**Phase:** H (Schema Management)
+**Effort:** 16-20 hours (2-3 days)
+**Priority:** HIGH (Unblocks user productivity)
+**Dependencies:** TASK-007A (LocalSQL Adapter)
+
+---
+
+## Objectives
+
+1. Create a schema browser panel showing all tables/collections
+2. Build a visual schema editor for adding/modifying columns
+3. Integrate with existing SchemaManager (no reimplementation)
+4. Support all Nodegx field types (String, Number, Boolean, Date, Object, Array, Pointer, Relation)
+5. Enable table creation from UI
+6. Provide schema export (PostgreSQL, MySQL, Supabase formats)
+7. Reuse existing Nodegx UI components (grid, forms, modals)
+
+---
+
+## Background
+
+### Current State
+
+Users can create local backends through the Backend Services panel, but have no way to:
+- View existing tables/schema
+- Add new tables
+- Modify table structure (add/remove columns)
+- Understand what data structures exist
+
+The `SchemaManager` class in `TASK-007A` already provides all backend functionality:
+```typescript
+class SchemaManager {
+ createTable(schema: TableSchema): void;
+ addColumn(tableName: string, column: ColumnDefinition): void;
+ getTableSchema(tableName: string): TableSchema | null;
+ getSchema(): SchemaDefinition;
+ exportToPostgres(): string;
+ exportToSupabase(): string;
+}
+```
+
+**We need to build the UI layer on top of this existing logic.**
+
+### Design Principles
+
+1. **No Backend Reimplementation** - Only UI, all logic delegates to SchemaManager
+2. **Leverage Existing Components** - Reuse PropertyPanel, DataGrid, Modal patterns
+3. **MVP First** - Ship basic functionality fast, enhance in Phase 3
+4. **Consistent with Nodegx** - Match editor's visual language
+
+---
+
+## User Flows
+
+### Flow 1: View Schema
+
+```
+User clicks "Manage Schema" on Backend Services panel
+ ↓
+Schema Manager panel opens
+ ↓
+Shows list of tables with:
+ - Table name
+ - Column count
+ - Record count (async load)
+ - Last modified
+ ↓
+User clicks table name
+ ↓
+Expands to show columns with types
+```
+
+### Flow 2: Create Table
+
+```
+User clicks "New Table" button
+ ↓
+Modal opens: "Create Table"
+ - Table name input
+ - Optional: Add initial columns
+ ↓
+User enters "Products"
+ ↓
+User clicks "Add Column"
+ ↓
+Column editor appears:
+ - Name: "name"
+ - Type: String (dropdown)
+ - Required: checkbox
+ ↓
+User clicks "Create Table"
+ ↓
+SchemaManager.createTable() called
+ ↓
+Table appears in schema list
+```
+
+### Flow 3: Modify Schema
+
+```
+User clicks "Edit Schema" on table
+ ↓
+Schema editor opens:
+ - List of existing columns (read-only editing)
+ - Add column button
+ - Remove column button (with warning)
+ ↓
+User clicks "Add Column"
+ ↓
+Column form appears
+ ↓
+User fills: name="price", type=Number, required=true
+ ↓
+SchemaManager.addColumn() called
+ ↓
+Column added to table
+```
+
+---
+
+## Implementation Steps
+
+### Step 1: Schema Panel Component (4 hours)
+
+Create the main schema browser UI component.
+
+**File:** `packages/noodl-editor/src/editor/src/views/panels/schemamanager/SchemaPanel.tsx`
+
+```typescript
+import React, { useState, useEffect } from 'react';
+import { ipcRenderer } from 'electron';
+import styles from './SchemaPanel.module.css';
+
+interface TableInfo {
+ name: string;
+ columns: ColumnDefinition[];
+ recordCount?: number;
+ lastModified?: string;
+}
+
+interface ColumnDefinition {
+ name: string;
+ type: string;
+ required: boolean;
+ default?: any;
+}
+
+export function SchemaPanel({ backendId }: { backendId: string }) {
+ const [tables, setTables] = useState([]);
+ const [selectedTable, setSelectedTable] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ loadSchema();
+ }, [backendId]);
+
+ async function loadSchema() {
+ setLoading(true);
+ try {
+ // Call IPC to get schema from backend
+ const schema = await ipcRenderer.invoke('backend:getSchema', backendId);
+
+ // Enrich with record counts (async, non-blocking)
+ const tablesWithCounts = await Promise.all(
+ schema.tables.map(async (table) => {
+ const count = await ipcRenderer.invoke('backend:getRecordCount', backendId, table.name);
+ return { ...table, recordCount: count };
+ })
+ );
+
+ setTables(tablesWithCounts);
+ } catch (error) {
+ console.error('Failed to load schema:', error);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ function handleCreateTable() {
+ // Open create table modal
+ setShowCreateModal(true);
+ }
+
+ function handleEditTable(tableName: string) {
+ setSelectedTable(tableName);
+ }
+
+ if (loading) {
+ return Loading schema...
;
+ }
+
+ return (
+
+
+
Database Schema
+
+ + New Table
+
+
+
+
+ {tables.length === 0 ? (
+
+
No tables yet
+
Create your first table
+
+ ) : (
+ tables.map((table) => (
+
handleEditTable(table.name)}
+ onExpand={() => setSelectedTable(
+ selectedTable === table.name ? null : table.name
+ )}
+ expanded={selectedTable === table.name}
+ />
+ ))
+ )}
+
+
+ {showCreateModal && (
+
setShowCreateModal(false)}
+ onSuccess={loadSchema}
+ />
+ )}
+
+ {selectedTable && (
+ setSelectedTable(null)}
+ onUpdate={loadSchema}
+ />
+ )}
+
+ );
+}
+```
+
+**File:** `packages/noodl-editor/src/editor/src/views/panels/schemamanager/TableRow.tsx`
+
+```typescript
+import React from 'react';
+import styles from './TableRow.module.css';
+
+interface TableRowProps {
+ table: TableInfo;
+ expanded: boolean;
+ onExpand: () => void;
+ onEdit: () => void;
+}
+
+export function TableRow({ table, expanded, onExpand, onEdit }: TableRowProps) {
+ return (
+
+
+
+ {expanded ? '▼' : '▶'}
+
+
{table.name}
+
+
+ {table.columns.length} {table.columns.length === 1 ? 'field' : 'fields'}
+
+ {table.recordCount !== undefined && (
+
+ {table.recordCount.toLocaleString()} {table.recordCount === 1 ? 'record' : 'records'}
+
+ )}
+
+
{
+ e.stopPropagation();
+ onEdit();
+ }}
+ >
+ Edit
+
+
+
+ {expanded && (
+
+
+
+
+ Field Name
+ Type
+ Required
+ Default
+
+
+
+ {table.columns.map((col) => (
+
+ {col.name}
+
+
+
+
+ {col.required ? '✓' : ''}
+
+
+ {col.default || '—'}
+
+
+ ))}
+
+
+
+ )}
+
+ );
+}
+
+function TypeBadge({ type }: { type: string }) {
+ const typeColors = {
+ String: '#3b82f6',
+ Number: '#10b981',
+ Boolean: '#f59e0b',
+ Date: '#8b5cf6',
+ Object: '#ec4899',
+ Array: '#6366f1',
+ Pointer: '#ef4444',
+ Relation: '#ef4444',
+ };
+
+ return (
+
+ {type}
+
+ );
+}
+```
+
+---
+
+### Step 2: Create Table Modal (3 hours)
+
+**File:** `packages/noodl-editor/src/editor/src/views/panels/schemamanager/CreateTableModal.tsx`
+
+```typescript
+import React, { useState } from 'react';
+import { ipcRenderer } from 'electron';
+import { Modal } from '@noodl-core-ui/components/modal';
+import { TextInput } from '@noodl-core-ui/components/inputs/TextInput';
+import { PrimaryButton, SecondaryButton } from '@noodl-core-ui/components/buttons';
+import styles from './CreateTableModal.module.css';
+
+interface CreateTableModalProps {
+ backendId: string;
+ onClose: () => void;
+ onSuccess: () => void;
+}
+
+export function CreateTableModal({ backendId, onClose, onSuccess }: CreateTableModalProps) {
+ const [tableName, setTableName] = useState('');
+ const [columns, setColumns] = useState([
+ { name: 'name', type: 'String', required: true }
+ ]);
+ const [creating, setCreating] = useState(false);
+ const [error, setError] = useState(null);
+
+ async function handleCreate() {
+ // Validation
+ if (!tableName.trim()) {
+ setError('Table name is required');
+ return;
+ }
+
+ if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(tableName)) {
+ setError('Table name must start with a letter and contain only letters, numbers, and underscores');
+ return;
+ }
+
+ if (columns.length === 0) {
+ setError('At least one column is required');
+ return;
+ }
+
+ setCreating(true);
+ setError(null);
+
+ try {
+ await ipcRenderer.invoke('backend:createTable', backendId, {
+ name: tableName,
+ columns: columns
+ });
+
+ onSuccess();
+ onClose();
+ } catch (err: any) {
+ setError(err.message || 'Failed to create table');
+ } finally {
+ setCreating(false);
+ }
+ }
+
+ function handleAddColumn() {
+ setColumns([...columns, { name: '', type: 'String', required: false }]);
+ }
+
+ function handleRemoveColumn(index: number) {
+ setColumns(columns.filter((_, i) => i !== index));
+ }
+
+ function handleColumnChange(index: number, field: string, value: any) {
+ const newColumns = [...columns];
+ newColumns[index] = { ...newColumns[index], [field]: value };
+ setColumns(newColumns);
+ }
+
+ return (
+
+
+
+
Table Name
+
+
+ Use lowercase with underscores (e.g., "blog_posts")
+
+
+
+
+
+
Initial Columns
+
+ + Add Column
+
+
+
+
+ {columns.map((col, index) => (
+ handleColumnChange(index, field, value)}
+ onRemove={() => handleRemoveColumn(index)}
+ canRemove={columns.length > 1}
+ />
+ ))}
+
+
+
+ Note: objectId, createdAt, and updatedAt are added automatically
+
+
+
+ {error && (
+
{error}
+ )}
+
+
+
+
+ );
+}
+```
+
+**File:** `packages/noodl-editor/src/editor/src/views/panels/schemamanager/ColumnEditor.tsx`
+
+```typescript
+import React from 'react';
+import { TextInput } from '@noodl-core-ui/components/inputs/TextInput';
+import { Select } from '@noodl-core-ui/components/inputs/Select';
+import { Checkbox } from '@noodl-core-ui/components/inputs/Checkbox';
+import styles from './ColumnEditor.module.css';
+
+const FIELD_TYPES = [
+ { value: 'String', label: 'String' },
+ { value: 'Number', label: 'Number' },
+ { value: 'Boolean', label: 'Boolean' },
+ { value: 'Date', label: 'Date' },
+ { value: 'Object', label: 'Object' },
+ { value: 'Array', label: 'Array' },
+ { value: 'Pointer', label: 'Pointer' },
+ { value: 'Relation', label: 'Relation' },
+];
+
+interface ColumnEditorProps {
+ column: ColumnDefinition;
+ onChange: (field: string, value: any) => void;
+ onRemove: () => void;
+ canRemove: boolean;
+}
+
+export function ColumnEditor({ column, onChange, onRemove, canRemove }: ColumnEditorProps) {
+ return (
+
+
+
+ onChange('name', value)}
+ placeholder="field_name"
+ />
+
+
+
+ onChange('type', value)}
+ />
+
+
+
+ onChange('required', value)}
+ label="Required"
+ />
+
+
+ {canRemove && (
+
+ ✕
+
+ )}
+
+
+ {(column.type === 'Pointer' || column.type === 'Relation') && (
+
+ Target Table
+ onChange('relationTarget', value)}
+ placeholder="users, products..."
+ />
+
+ )}
+
+ );
+}
+```
+
+---
+
+### Step 3: Schema Editor (4 hours)
+
+**File:** `packages/noodl-editor/src/editor/src/views/panels/schemamanager/SchemaEditor.tsx`
+
+```typescript
+import React, { useState, useEffect } from 'react';
+import { ipcRenderer } from 'electron';
+import { Modal } from '@noodl-core-ui/components/modal';
+import { PrimaryButton, SecondaryButton } from '@noodl-core-ui/components/buttons';
+import { ColumnEditor } from './ColumnEditor';
+import styles from './SchemaEditor.module.css';
+
+interface SchemaEditorProps {
+ backendId: string;
+ tableName: string;
+ onClose: () => void;
+ onUpdate: () => void;
+}
+
+export function SchemaEditor({ backendId, tableName, onClose, onUpdate }: SchemaEditorProps) {
+ const [schema, setSchema] = useState(null);
+ const [newColumns, setNewColumns] = useState([]);
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ loadSchema();
+ }, [backendId, tableName]);
+
+ async function loadSchema() {
+ try {
+ const tableSchema = await ipcRenderer.invoke('backend:getTableSchema', backendId, tableName);
+ setSchema(tableSchema);
+ } catch (err: any) {
+ setError(err.message || 'Failed to load schema');
+ }
+ }
+
+ async function handleSave() {
+ if (newColumns.length === 0) {
+ onClose();
+ return;
+ }
+
+ // Validate new columns
+ for (const col of newColumns) {
+ if (!col.name.trim()) {
+ setError('All columns must have a name');
+ return;
+ }
+
+ if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(col.name)) {
+ setError(`Invalid column name: ${col.name}. Must start with a letter.`);
+ return;
+ }
+
+ // Check for duplicates
+ if (schema?.columns.some(c => c.name === col.name)) {
+ setError(`Column "${col.name}" already exists`);
+ return;
+ }
+ }
+
+ setSaving(true);
+ setError(null);
+
+ try {
+ // Add each new column
+ for (const col of newColumns) {
+ await ipcRenderer.invoke('backend:addColumn', backendId, tableName, col);
+ }
+
+ onUpdate();
+ onClose();
+ } catch (err: any) {
+ setError(err.message || 'Failed to update schema');
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ function handleAddColumn() {
+ setNewColumns([...newColumns, { name: '', type: 'String', required: false }]);
+ }
+
+ function handleRemoveNewColumn(index: number) {
+ setNewColumns(newColumns.filter((_, i) => i !== index));
+ }
+
+ function handleColumnChange(index: number, field: string, value: any) {
+ const updated = [...newColumns];
+ updated[index] = { ...updated[index], [field]: value };
+ setNewColumns(updated);
+ }
+
+ if (!schema) {
+ return Loading...
;
+ }
+
+ return (
+
+
+
+
Existing Columns
+
+
+
+ Name
+ Type
+ Required
+
+
+
+ {schema.columns.map((col) => (
+
+ {col.name}
+ {col.type}
+ {col.required ? '✓' : ''}
+
+ ))}
+
+
+
+
+
+
+
Add Columns
+ + Add Column
+
+
+ {newColumns.length === 0 ? (
+
+ Click "Add Column" to add new fields to this table
+
+ ) : (
+
+ {newColumns.map((col, index) => (
+ handleColumnChange(index, field, value)}
+ onRemove={() => handleRemoveNewColumn(index)}
+ canRemove={true}
+ />
+ ))}
+
+ )}
+
+
+
+ ⚠️ Note: Removing columns is not supported. You can only add new columns.
+
+
+ {error && (
+
{error}
+ )}
+
+
+
+
+ );
+}
+```
+
+---
+
+### Step 4: IPC Handlers (2 hours)
+
+**File:** `packages/noodl-editor/src/main/src/ipc/backend-schema-handlers.ts`
+
+```typescript
+import { ipcMain } from 'electron';
+import { BackendManager } from '../local-backend/BackendManager';
+
+export function registerSchemaHandlers(backendManager: BackendManager) {
+
+ /**
+ * Get full schema for a backend
+ */
+ ipcMain.handle('backend:getSchema', async (event, backendId: string) => {
+ const backend = backendManager.getBackend(backendId);
+ if (!backend) {
+ throw new Error(`Backend not found: ${backendId}`);
+ }
+
+ const adapter = backend.getAdapter();
+ return await adapter.getSchema();
+ });
+
+ /**
+ * Get schema for a specific table
+ */
+ ipcMain.handle('backend:getTableSchema', async (event, backendId: string, tableName: string) => {
+ const backend = backendManager.getBackend(backendId);
+ if (!backend) {
+ throw new Error(`Backend not found: ${backendId}`);
+ }
+
+ const adapter = backend.getAdapter();
+ const schema = await adapter.getSchema();
+ const table = schema.tables.find(t => t.name === tableName);
+
+ if (!table) {
+ throw new Error(`Table not found: ${tableName}`);
+ }
+
+ return table;
+ });
+
+ /**
+ * Get record count for a table
+ */
+ ipcMain.handle('backend:getRecordCount', async (event, backendId: string, tableName: string) => {
+ const backend = backendManager.getBackend(backendId);
+ if (!backend) {
+ throw new Error(`Backend not found: ${backendId}`);
+ }
+
+ const adapter = backend.getAdapter();
+ const result = await adapter.query({
+ collection: tableName,
+ count: true,
+ limit: 0
+ });
+
+ return result.count || 0;
+ });
+
+ /**
+ * Create a new table
+ */
+ ipcMain.handle('backend:createTable', async (event, backendId: string, schema: TableSchema) => {
+ const backend = backendManager.getBackend(backendId);
+ if (!backend) {
+ throw new Error(`Backend not found: ${backendId}`);
+ }
+
+ const adapter = backend.getAdapter();
+ await adapter.createTable(schema);
+
+ return { success: true };
+ });
+
+ /**
+ * Add a column to existing table
+ */
+ ipcMain.handle('backend:addColumn', async (event, backendId: string, tableName: string, column: ColumnDefinition) => {
+ const backend = backendManager.getBackend(backendId);
+ if (!backend) {
+ throw new Error(`Backend not found: ${backendId}`);
+ }
+
+ const adapter = backend.getAdapter();
+ await adapter.addColumn(tableName, column);
+
+ return { success: true };
+ });
+
+ /**
+ * Export schema to SQL
+ */
+ ipcMain.handle('backend:exportSchema', async (event, backendId: string, dialect: 'postgres' | 'mysql' | 'sqlite') => {
+ const backend = backendManager.getBackend(backendId);
+ if (!backend) {
+ throw new Error(`Backend not found: ${backendId}`);
+ }
+
+ const adapter = backend.getAdapter();
+ return await adapter.exportToSQL(dialect);
+ });
+}
+```
+
+---
+
+### Step 5: Integration with Backend Services Panel (2 hours)
+
+**File:** Modifications to `packages/noodl-editor/src/editor/src/views/panels/BackendServicesPanel/BackendCard.tsx`
+
+```typescript
+// Add to BackendCard component
+
+function BackendCard({ backend }: { backend: LocalBackend }) {
+ const [showSchemaManager, setShowSchemaManager] = useState(false);
+
+ return (
+
+ {/* ... existing status, start/stop buttons ... */}
+
+
+ setShowSchemaManager(true)}
+ disabled={backend.status !== 'running'}
+ >
+ 📋 Manage Schema
+
+
+ openDataBrowser(backend.id)}
+ disabled={backend.status !== 'running'}
+ >
+ 📊 Browse Data
+
+
+ {/* ... existing export, delete buttons ... */}
+
+
+ {showSchemaManager && (
+
setShowSchemaManager(false)}
+ />
+ )}
+
+ );
+}
+```
+
+---
+
+### Step 6: Schema Export Dialog (2 hours)
+
+**File:** `packages/noodl-editor/src/editor/src/views/panels/schemamanager/ExportSchemaDialog.tsx`
+
+```typescript
+import React, { useState } from 'react';
+import { ipcRenderer } from 'electron';
+import { Modal } from '@noodl-core-ui/components/modal';
+import { Select } from '@noodl-core-ui/components/inputs/Select';
+import { PrimaryButton, SecondaryButton } from '@noodl-core-ui/components/buttons';
+import styles from './ExportSchemaDialog.module.css';
+
+const EXPORT_FORMATS = [
+ { value: 'postgres', label: 'PostgreSQL' },
+ { value: 'mysql', label: 'MySQL' },
+ { value: 'sqlite', label: 'SQLite' },
+ { value: 'supabase', label: 'Supabase (PostgreSQL + RLS)' },
+];
+
+interface ExportSchemaDialogProps {
+ backendId: string;
+ onClose: () => void;
+}
+
+export function ExportSchemaDialog({ backendId, onClose }: ExportSchemaDialogProps) {
+ const [format, setFormat] = useState('postgres');
+ const [exporting, setExporting] = useState(false);
+ const [result, setResult] = useState(null);
+
+ async function handleExport() {
+ setExporting(true);
+ try {
+ const sql = await ipcRenderer.invoke('backend:exportSchema', backendId, format);
+ setResult(sql);
+ } catch (err: any) {
+ console.error('Export failed:', err);
+ } finally {
+ setExporting(false);
+ }
+ }
+
+ function handleCopy() {
+ if (result) {
+ navigator.clipboard.writeText(result);
+ }
+ }
+
+ function handleDownload() {
+ if (!result) return;
+
+ const blob = new Blob([result], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `schema-${format}.sql`;
+ a.click();
+ URL.revokeObjectURL(url);
+ }
+
+ return (
+
+
+ {!result ? (
+ <>
+
Export your database schema to SQL for use with other platforms.
+
+
+ Export Format
+
+
+
+
+ >
+ ) : (
+ <>
+
+
+
+ >
+ )}
+
+
+ );
+}
+```
+
+---
+
+## Files to Create
+
+```
+packages/noodl-editor/src/editor/src/views/panels/schemamanager/
+├── SchemaPanel.tsx # Main schema browser
+├── SchemaPanel.module.css
+├── TableRow.tsx # Individual table display
+├── TableRow.module.css
+├── CreateTableModal.tsx # New table creation
+├── CreateTableModal.module.css
+├── SchemaEditor.tsx # Edit existing table schema
+├── SchemaEditor.module.css
+├── ColumnEditor.tsx # Column configuration UI
+├── ColumnEditor.module.css
+├── ExportSchemaDialog.tsx # Export to SQL
+├── ExportSchemaDialog.module.css
+└── index.ts # Public exports
+
+packages/noodl-editor/src/main/src/ipc/
+└── backend-schema-handlers.ts # IPC handlers for schema operations
+```
+
+## Files to Modify
+
+```
+packages/noodl-editor/src/editor/src/views/panels/BackendServicesPanel/BackendCard.tsx
+ - Add "Manage Schema" button
+ - Add "Browse Data" button
+
+packages/noodl-editor/src/main/src/ipc/index.ts
+ - Register backend-schema-handlers
+
+packages/noodl-editor/src/editor/src/views/panels/index.ts
+ - Export SchemaPanel for use in other panels
+```
+
+---
+
+## Testing Checklist
+
+### Schema Viewing
+- [ ] Schema panel opens for running backend
+- [ ] All tables displayed with accurate counts
+- [ ] Table expand/collapse works
+- [ ] Column details show correct types
+- [ ] Record counts load asynchronously
+- [ ] Empty state shows when no tables exist
+
+### Table Creation
+- [ ] Create table modal opens
+- [ ] Table name validation works
+- [ ] Can add multiple initial columns
+- [ ] Column removal works
+- [ ] Type dropdown shows all types
+- [ ] Required checkbox toggles
+- [ ] Pointer/Relation shows target selector
+- [ ] Table created successfully
+- [ ] Schema refreshes after creation
+
+### Schema Editing
+- [ ] Schema editor opens for existing table
+- [ ] Existing columns displayed (read-only)
+- [ ] Can add new columns
+- [ ] Column validation works
+- [ ] Duplicate column names rejected
+- [ ] Columns saved successfully
+- [ ] Schema refreshes after save
+
+### Schema Export
+- [ ] Export dialog opens
+- [ ] Format selector shows all options
+- [ ] PostgreSQL export generates valid SQL
+- [ ] MySQL export generates valid SQL
+- [ ] SQLite export generates valid SQL
+- [ ] Supabase export includes RLS policies
+- [ ] Copy to clipboard works
+- [ ] Download file works
+
+### Integration
+- [ ] "Manage Schema" button disabled when backend stopped
+- [ ] Schema panel only accessible for running backends
+- [ ] Schema changes reflected in data browser
+- [ ] Schema changes reflected in node property dropdowns
+
+### Edge Cases
+- [ ] Schema panel handles backend with no tables
+- [ ] Create table handles name conflicts
+- [ ] Schema editor handles invalid column types
+- [ ] Export handles large schemas (100+ tables)
+- [ ] UI handles backend disconnect gracefully
+
+---
+
+## Success Criteria
+
+1. Users can view all tables and columns in their local backend
+2. Users can create new tables with initial columns
+3. Users can add columns to existing tables
+4. Users can export schema to PostgreSQL, MySQL, or Supabase
+5. Schema changes are immediately reflected in the UI
+6. All operations properly delegate to existing SchemaManager
+7. UI follows Nodegx design patterns and component standards
+8. Performance: Schema loads in <500ms for 50 tables
+9. Zero reimplementation of backend logic (only UI layer)
+
+---
+
+## Dependencies
+
+**Requires:**
+- TASK-007A (LocalSQL Adapter with SchemaManager)
+- TASK-007B (Backend Server with IPC)
+- TASK-007C (Backend Services Panel)
+
+**Blocked by:** None
+
+**Blocks:**
+- TASK-007I (Data Browser) - needs schema info for table selection
+- Phase 3 AI features - schema used for AI-powered suggestions
+
+---
+
+## Estimated Session Breakdown
+
+| Session | Focus | Hours |
+|---------|-------|-------|
+| 1 | SchemaPanel + TableRow components | 4 |
+| 2 | CreateTableModal + ColumnEditor | 3 |
+| 3 | SchemaEditor component | 4 |
+| 4 | IPC handlers + integration | 2 |
+| 5 | ExportSchemaDialog + polish | 3 |
+| 6 | Testing + bug fixes | 4 |
+| **Total** | | **20** |
+
+---
+
+## Future Enhancements (Phase 3)
+
+These features are **out of scope for MVP** but should be considered for Phase 3:
+
+1. **Visual Relationship Diagram** - Canvas view showing table relationships
+2. **Schema Migrations UI** - Track and apply schema changes over time
+3. **AI Schema Suggestions** - Claude suggests optimal schema based on description
+4. **Schema Versioning** - Integrate with git for schema history
+5. **Column Removal** - Safe column deletion with data migration
+6. **Index Management** - UI for creating/managing database indexes
+7. **Virtual Fields** - Define computed columns using Nodegx expressions
+8. **Schema Templates** - Pre-built schemas for common use cases (users, posts, products)
+9. **Validation Rules UI** - Visual editor for field validation
+10. **Schema Diff** - Compare schemas between dev/staging/prod
+
+---
+
+## Notes
+
+- This task focuses **only** on schema management UI
+- Data browsing/editing is covered in TASK-007I
+- All backend logic already exists in SchemaManager (TASK-007A)
+- Reuse existing Nodegx UI components wherever possible
+- Follow Storybook patterns from noodl-core-ui
+- Schema panel should feel like a natural extension of Backend Services panel
+- Export feature enables migration path to production databases
diff --git a/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007I-COMPLETE.md b/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007I-COMPLETE.md
new file mode 100644
index 0000000..b8bb690
--- /dev/null
+++ b/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007I-COMPLETE.md
@@ -0,0 +1,205 @@
+# TASK-007I: Data Browser Panel - COMPLETE
+
+## Summary
+
+Implemented a full-featured data browser panel for viewing and editing records in local backend SQLite tables, providing a spreadsheet-like interface with inline editing, search, pagination, and CRUD operations.
+
+## Status: ✅ Complete
+
+## Date: 2026-01-15
+
+---
+
+## What Was Implemented
+
+### 1. Backend IPC Handlers (BackendManager.js)
+
+Added four new IPC handlers for data operations:
+
+- **`backend:queryRecords`** - Query records with pagination, sorting, search filters
+- **`backend:createRecord`** - Create a new record in a table
+- **`backend:saveRecord`** - Update an existing record (inline edit)
+- **`backend:deleteRecord`** - Delete a single record
+
+These handlers wrap the existing LocalSQLAdapter CRUD methods (query, create, save, delete) and expose them to the renderer process.
+
+### 2. DataBrowser Component (`panels/databrowser/`)
+
+Created a complete data browser panel with the following components:
+
+#### DataBrowser.tsx (Main Component)
+
+- Table selector dropdown
+- Search input with cross-field text search
+- Pagination with page navigation (50 records per page)
+- Bulk selection and delete operations
+- CSV export functionality
+- Loading states and error handling
+- New record modal trigger
+
+#### DataGrid.tsx (Spreadsheet View)
+
+- Column headers with type badges (String, Number, Boolean, Date, Object, Array)
+- System columns (objectId, createdAt, updatedAt) displayed as read-only
+- Inline editing - click any editable cell to edit
+- Row selection with checkbox
+- Select all/none toggle
+- Delete action per row
+- Scrollable with sticky header
+
+#### CellEditor.tsx (Inline Editor)
+
+- Type-aware input controls:
+ - **String**: Text input
+ - **Number**: Number input with step="any"
+ - **Boolean**: Checkbox with immediate save
+ - **Date**: datetime-local input
+ - **Object/Array**: Multi-line JSON editor with Save/Cancel buttons
+- Auto-focus and select on mount
+- Enter to save, Escape to cancel (for simple types)
+- Validation for numbers and JSON parsing
+- Error display for invalid input
+
+#### NewRecordModal.tsx (Create Record Form)
+
+- Form fields generated from column schema
+- Type-aware input controls for each field type
+- Required field validation
+- Default values based on column defaults or type defaults
+- Success callback to refresh data grid
+
+### 3. Styling
+
+All components use theme CSS variables per UI-STYLING-GUIDE.md:
+
+- `--theme-color-bg-*` for backgrounds
+- `--theme-color-fg-*` for text
+- `--theme-color-border-*` for borders
+- `--theme-color-primary*` for accents
+- `--theme-color-success/danger/notice` for status colors
+
+### 4. Integration
+
+Added "Data" button to LocalBackendCard (visible when backend is running):
+
+- Opens DataBrowser in a full-screen portal overlay
+- Same pattern as Schema panel
+
+---
+
+## Files Created/Modified
+
+### Created Files:
+
+```
+packages/noodl-editor/src/editor/src/views/panels/databrowser/
+├── DataBrowser.tsx - Main data browser component
+├── DataBrowser.module.scss - Styles for DataBrowser
+├── DataGrid.tsx - Spreadsheet grid component
+├── DataGrid.module.scss - Styles for DataGrid
+├── CellEditor.tsx - Inline cell editor
+├── CellEditor.module.scss - Styles for CellEditor
+├── NewRecordModal.tsx - Create record modal
+├── NewRecordModal.module.scss - Styles for modal
+└── index.ts - Exports
+```
+
+### Modified Files:
+
+```
+packages/noodl-editor/src/main/src/local-backend/BackendManager.js
+ - Added queryRecords(), createRecord(), saveRecord(), deleteRecord() methods
+ - Added corresponding IPC handlers
+
+packages/noodl-editor/src/editor/src/views/panels/BackendServicesPanel/LocalBackendCard/LocalBackendCard.tsx
+ - Added DataBrowser import
+ - Added showDataBrowser state
+ - Added "Data" button
+ - Added DataBrowser portal rendering
+```
+
+---
+
+## How To Use
+
+1. Start a local backend from the Backend Services panel
+2. Click the "Data" button on the running backend card
+3. Select a table from the dropdown (auto-selects first table if available)
+4. Browse records with pagination
+5. Click any editable cell to edit inline
+6. Use "+ New Record" to add records
+7. Use checkboxes for bulk selection and delete
+8. Use "Export CSV" to download current view
+
+---
+
+## Technical Notes
+
+### Query API
+
+The query API supports:
+
+```javascript
+{
+ collection: string, // Table name
+ limit: number, // Max records (default 50)
+ skip: number, // Pagination offset
+ where: object, // Filter conditions ($or, contains, etc.)
+ sort: string[], // Sort order (e.g., ['-createdAt'])
+ count: boolean // Return total count for pagination
+}
+```
+
+### Search Implementation
+
+Search uses `$or` with `contains` operator across all String columns and objectId. The LocalSQLAdapter converts this to SQLite LIKE queries.
+
+### Type Handling
+
+- System fields (objectId, createdAt, updatedAt) are always read-only
+- Boolean cells save immediately on checkbox toggle
+- Object/Array cells require explicit Save button due to JSON editing
+- Dates are stored as ISO8601 strings
+
+---
+
+## Future Improvements (Not in scope)
+
+- Column sorting by clicking headers
+- Filter by specific field values
+- Relation/Pointer field expansion
+- Inline record duplication
+- Undo for delete operations
+- Import CSV/JSON
+
+---
+
+## Verification Checklist
+
+- [x] IPC handlers added to BackendManager.js
+- [x] DataBrowser component created
+- [x] DataGrid with inline editing
+- [x] CellEditor with type-aware controls
+- [x] NewRecordModal for creating records
+- [x] CSV export functionality
+- [x] Pagination working
+- [x] Search functionality
+- [x] Bulk delete operations
+- [x] Integration with LocalBackendCard
+- [x] Theme-compliant styling
+- [x] No hardcoded colors
+- [x] JSDoc comments added
+
+---
+
+## Related Tasks
+
+- **TASK-007A**: Core SQLite adapter ✅
+- **TASK-007B**: Workflow integration ✅
+- **TASK-007C**: Schema management ✅
+- **TASK-007D**: Backend lifecycle ✅
+- **TASK-007E**: Express HTTP API ✅
+- **TASK-007F**: Node integration ✅
+- **TASK-007G**: UI components ✅
+- **TASK-007H**: Schema Panel UI ✅
+- **TASK-007I**: Data Browser ✅ (this task)
diff --git a/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007I-data-browser-editor.md b/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007I-data-browser-editor.md
new file mode 100644
index 0000000..a870e58
--- /dev/null
+++ b/dev-docs/tasks/phase-5-multi-target-deployment/01-byob-backend/TASK-007-integrated-backend/TASK-007I-data-browser-editor.md
@@ -0,0 +1,1290 @@
+# TASK-007I: Data Browser & Editor UI
+
+## Overview
+
+Build a native Nodegx UI for browsing, searching, and editing records in local backend tables, providing a visual interface similar to Parse Dashboard's data browser.
+
+**Parent Task:** TASK-007 (Integrated Local Backend)
+**Phase:** I (Data Management)
+**Effort:** 16-20 hours (2-3 days)
+**Priority:** HIGH (Unblocks user productivity)
+**Dependencies:** TASK-007A (LocalSQL Adapter), TASK-007H (Schema Manager)
+
+---
+
+## Objectives
+
+1. Create a data browser panel for viewing records in tables
+2. Build inline editing capabilities for all field types
+3. Implement search and filtering
+4. Support pagination for large datasets
+5. Enable record creation, update, and deletion
+6. Handle all Nodegx field types (String, Number, Boolean, Date, Object, Array, Pointer, Relation)
+7. Provide bulk operations (delete multiple, export)
+8. Integrate with SchemaManager for type-aware editing
+
+---
+
+## Background
+
+### Current State
+
+With TASK-007H, users can now manage database schemas, but they cannot:
+- View the actual data in tables
+- Add/edit/delete records
+- Search or filter data
+- Import/export data
+
+The `LocalSQLAdapter` already provides all backend CRUD operations:
+```typescript
+class LocalSQLAdapter {
+ async query(options: QueryOptions): Promise;
+ async fetch(options: FetchOptions): Promise;
+ async create(options: CreateOptions): Promise;
+ async save(options: SaveOptions): Promise;
+ async delete(options: DeleteOptions): Promise;
+}
+```
+
+**We need to build the UI layer on top of these existing operations.**
+
+### Design Principles
+
+1. **Spreadsheet-like Interface** - Users think of databases as tables
+2. **Inline Editing** - Click to edit, no separate forms for simple edits
+3. **Type-Aware Editors** - Each field type gets appropriate input (date picker, JSON editor, etc.)
+4. **Performance** - Handle tables with 100K+ records via pagination
+5. **Safety** - Confirm destructive operations
+
+---
+
+## User Flows
+
+### Flow 1: Browse Data
+
+```
+User clicks "Browse Data" on Backend Services panel
+ ↓
+Table selector appears (list of all tables)
+ ↓
+User selects "products" table
+ ↓
+Data Browser opens showing:
+ - Table name
+ - Total record count
+ - Paginated grid (50 records per page)
+ - Search bar
+ - Action buttons (New Record, Export, etc.)
+ ↓
+User scrolls through records
+User clicks page 2
+```
+
+### Flow 2: Edit Record
+
+```
+User clicks on a cell in the grid
+ ↓
+Cell becomes editable
+ ↓
+Type-specific editor appears:
+ - String: Text input
+ - Number: Number input
+ - Boolean: Checkbox
+ - Date: Date picker
+ - Object/Array: JSON editor modal
+ - Pointer: Table selector + record picker
+ ↓
+User edits value
+ ↓
+User presses Enter or clicks outside
+ ↓
+Value saves to database
+ ↓
+Cell shows updated value
+```
+
+### Flow 3: Create Record
+
+```
+User clicks "New Record" button
+ ↓
+Modal opens with form fields for all columns
+ ↓
+User fills in:
+ - name: "Widget Pro"
+ - price: 99.99
+ - inStock: true
+ ↓
+User clicks "Create"
+ ↓
+Record saved to database
+ ↓
+Grid refreshes with new record
+```
+
+### Flow 4: Search & Filter
+
+```
+User types "pro" in search bar
+ ↓
+Grid filters to show only records with "pro" in any field
+ ↓
+User clicks "Advanced Filters"
+ ↓
+Filter builder opens:
+ - Field: "price"
+ - Operator: "greater than"
+ - Value: 50
+ ↓
+Grid shows only records where price > 50
+```
+
+### Flow 5: Bulk Operations
+
+```
+User clicks checkboxes on multiple records
+ ↓
+Bulk action bar appears: "3 records selected"
+ ↓
+User clicks "Delete Selected"
+ ↓
+Confirmation dialog: "Delete 3 records?"
+ ↓
+User confirms
+ ↓
+Records deleted, grid refreshes
+```
+
+---
+
+## Implementation Steps
+
+### Step 1: Data Browser Component (5 hours)
+
+**File:** `packages/noodl-editor/src/editor/src/views/panels/databrowser/DataBrowser.tsx`
+
+```typescript
+import React, { useState, useEffect, useCallback } from 'react';
+import { ipcRenderer } from 'electron';
+import { DataGrid } from './DataGrid';
+import { TableSelector } from './TableSelector';
+import { SearchBar } from './SearchBar';
+import { FilterBuilder } from './FilterBuilder';
+import styles from './DataBrowser.module.css';
+
+interface DataBrowserProps {
+ backendId: string;
+ initialTable?: string;
+ onClose?: () => void;
+}
+
+export function DataBrowser({ backendId, initialTable, onClose }: DataBrowserProps) {
+ const [tables, setTables] = useState([]);
+ const [selectedTable, setSelectedTable] = useState(initialTable || null);
+ const [schema, setSchema] = useState(null);
+ const [records, setRecords] = useState([]);
+ const [totalCount, setTotalCount] = useState(0);
+ const [page, setPage] = useState(0);
+ const [pageSize] = useState(50);
+ const [loading, setLoading] = useState(false);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [filters, setFilters] = useState(null);
+ const [selectedRecords, setSelectedRecords] = useState>(new Set());
+ const [showNewRecord, setShowNewRecord] = useState(false);
+
+ // Load table list on mount
+ useEffect(() => {
+ loadTables();
+ }, [backendId]);
+
+ // Load data when table, page, search, or filters change
+ useEffect(() => {
+ if (selectedTable) {
+ loadData();
+ }
+ }, [selectedTable, page, searchQuery, filters]);
+
+ async function loadTables() {
+ try {
+ const schema = await ipcRenderer.invoke('backend:getSchema', backendId);
+ setTables(schema.tables.map((t: any) => t.name));
+
+ if (!selectedTable && schema.tables.length > 0) {
+ setSelectedTable(schema.tables[0].name);
+ }
+ } catch (error) {
+ console.error('Failed to load tables:', error);
+ }
+ }
+
+ async function loadData() {
+ if (!selectedTable) return;
+
+ setLoading(true);
+ try {
+ // Load schema for this table
+ const tableSchema = await ipcRenderer.invoke('backend:getTableSchema', backendId, selectedTable);
+ setSchema(tableSchema);
+
+ // Build query
+ const query: any = {
+ collection: selectedTable,
+ limit: pageSize,
+ skip: page * pageSize,
+ sort: ['-createdAt'] // Newest first
+ };
+
+ // Apply search
+ if (searchQuery) {
+ // Search across all string fields
+ query.where = {
+ $or: tableSchema.columns
+ .filter((col: any) => col.type === 'String')
+ .map((col: any) => ({
+ [col.name]: { contains: searchQuery }
+ }))
+ };
+ }
+
+ // Apply filters
+ if (filters) {
+ query.where = { ...query.where, ...filters };
+ }
+
+ // Load records
+ const result = await ipcRenderer.invoke('backend:query', backendId, query);
+ setRecords(result.results);
+
+ // Load total count
+ const countQuery = { ...query, count: true, limit: 0 };
+ const countResult = await ipcRenderer.invoke('backend:query', backendId, countQuery);
+ setTotalCount(countResult.count || 0);
+
+ } catch (error) {
+ console.error('Failed to load data:', error);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ async function handleSaveCell(recordId: string, field: string, value: any) {
+ try {
+ await ipcRenderer.invoke('backend:saveRecord', backendId, selectedTable, recordId, {
+ [field]: value
+ });
+
+ // Update local state
+ setRecords(records.map(r =>
+ r.objectId === recordId ? { ...r, [field]: value } : r
+ ));
+ } catch (error) {
+ console.error('Failed to save cell:', error);
+ throw error;
+ }
+ }
+
+ async function handleDeleteRecord(recordId: string) {
+ if (!confirm('Delete this record?')) return;
+
+ try {
+ await ipcRenderer.invoke('backend:deleteRecord', backendId, selectedTable, recordId);
+ loadData(); // Refresh
+ } catch (error) {
+ console.error('Failed to delete record:', error);
+ }
+ }
+
+ async function handleBulkDelete() {
+ if (selectedRecords.size === 0) return;
+
+ if (!confirm(`Delete ${selectedRecords.size} records?`)) return;
+
+ try {
+ for (const recordId of selectedRecords) {
+ await ipcRenderer.invoke('backend:deleteRecord', backendId, selectedTable, recordId);
+ }
+ setSelectedRecords(new Set());
+ loadData(); // Refresh
+ } catch (error) {
+ console.error('Failed to bulk delete:', error);
+ }
+ }
+
+ function handleExport() {
+ // Export to CSV
+ const csv = recordsToCSV(records, schema!);
+ downloadCSV(csv, `${selectedTable}.csv`);
+ }
+
+ const totalPages = Math.ceil(totalCount / pageSize);
+
+ return (
+
+
+
+
+
+
+
+ setShowNewRecord(true)}>
+ + New Record
+
+
+ Export CSV
+
+
+
+
+ {selectedRecords.size > 0 && (
+
+ {selectedRecords.size} records selected
+ Delete Selected
+ setSelectedRecords(new Set())}>Clear Selection
+
+ )}
+
+
+ {loading ? (
+
Loading...
+ ) : schema ? (
+
{
+ const newSelected = new Set(selectedRecords);
+ if (selected) {
+ newSelected.add(id);
+ } else {
+ newSelected.delete(id);
+ }
+ setSelectedRecords(newSelected);
+ }}
+ onSaveCell={handleSaveCell}
+ onDeleteRecord={handleDeleteRecord}
+ />
+ ) : (
+
+ Select a table to browse data
+
+ )}
+
+
+
+
+ Showing {page * pageSize + 1}-{Math.min((page + 1) * pageSize, totalCount)} of {totalCount.toLocaleString()} records
+
+
+ setPage(0)}
+ disabled={page === 0}
+ >
+ First
+
+ setPage(page - 1)}
+ disabled={page === 0}
+ >
+ Previous
+
+ Page {page + 1} of {totalPages}
+ setPage(page + 1)}
+ disabled={page >= totalPages - 1}
+ >
+ Next
+
+ setPage(totalPages - 1)}
+ disabled={page >= totalPages - 1}
+ >
+ Last
+
+
+
+
+ {showNewRecord && schema && (
+
setShowNewRecord(false)}
+ onSuccess={() => {
+ setShowNewRecord(false);
+ loadData();
+ }}
+ />
+ )}
+
+ );
+}
+
+function recordsToCSV(records: any[], schema: TableSchema): string {
+ const headers = schema.columns.map(c => c.name).join(',');
+ const rows = records.map(record =>
+ schema.columns.map(col => {
+ const value = record[col.name];
+ if (value === null || value === undefined) return '';
+ if (typeof value === 'object') return JSON.stringify(value);
+ if (typeof value === 'string' && value.includes(',')) return `"${value}"`;
+ return value;
+ }).join(',')
+ );
+
+ return [headers, ...rows].join('\n');
+}
+
+function downloadCSV(csv: string, filename: string) {
+ const blob = new Blob([csv], { type: 'text/csv' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ a.click();
+ URL.revokeObjectURL(url);
+}
+```
+
+---
+
+### Step 2: Data Grid Component (5 hours)
+
+**File:** `packages/noodl-editor/src/editor/src/views/panels/databrowser/DataGrid.tsx`
+
+```typescript
+import React, { useState } from 'react';
+import { CellEditor } from './CellEditor';
+import styles from './DataGrid.module.css';
+
+interface DataGridProps {
+ schema: TableSchema;
+ records: any[];
+ selectedRecords: Set;
+ onSelectRecord: (id: string, selected: boolean) => void;
+ onSaveCell: (recordId: string, field: string, value: any) => Promise;
+ onDeleteRecord: (recordId: string) => void;
+}
+
+export function DataGrid({
+ schema,
+ records,
+ selectedRecords,
+ onSelectRecord,
+ onSaveCell,
+ onDeleteRecord
+}: DataGridProps) {
+ const [editingCell, setEditingCell] = useState<{ recordId: string; field: string } | null>(null);
+
+ // Show system fields + user fields
+ const displayColumns = [
+ { name: 'objectId', type: 'String', readOnly: true },
+ { name: 'createdAt', type: 'Date', readOnly: true },
+ { name: 'updatedAt', type: 'Date', readOnly: true },
+ ...schema.columns
+ ];
+
+ async function handleCellSave(recordId: string, field: string, value: any) {
+ try {
+ await onSaveCell(recordId, field, value);
+ setEditingCell(null);
+ } catch (error) {
+ console.error('Failed to save cell:', error);
+ alert('Failed to save changes');
+ }
+ }
+
+ function handleCellClick(recordId: string, field: string, readOnly: boolean) {
+ if (readOnly) return;
+ setEditingCell({ recordId, field });
+ }
+
+ function formatCellValue(value: any, type: string): string {
+ if (value === null || value === undefined) return '';
+
+ switch (type) {
+ case 'Boolean':
+ return value ? '✓' : '';
+ case 'Date':
+ return new Date(value).toLocaleString();
+ case 'Object':
+ case 'Array':
+ return JSON.stringify(value);
+ default:
+ return String(value);
+ }
+ }
+
+ return (
+
+
+
+ {records.length === 0 && (
+
+ No records found
+
+ )}
+
+ );
+}
+```
+
+---
+
+### Step 3: Cell Editor Component (4 hours)
+
+**File:** `packages/noodl-editor/src/editor/src/views/panels/databrowser/CellEditor.tsx`
+
+```typescript
+import React, { useState, useEffect, useRef } from 'react';
+import { TextInput } from '@noodl-core-ui/components/inputs/TextInput';
+import { Checkbox } from '@noodl-core-ui/components/inputs/Checkbox';
+import { DatePicker } from '@noodl-core-ui/components/inputs/DatePicker';
+import styles from './CellEditor.module.css';
+
+interface CellEditorProps {
+ value: any;
+ type: string;
+ onSave: (value: any) => void;
+ onCancel: () => void;
+}
+
+export function CellEditor({ value, type, onSave, onCancel }: CellEditorProps) {
+ const [editValue, setEditValue] = useState(value);
+ const [showJsonEditor, setShowJsonEditor] = useState(false);
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ inputRef.current.select();
+ }
+ }, []);
+
+ function handleKeyDown(e: React.KeyboardEvent) {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleSave();
+ } else if (e.key === 'Escape') {
+ onCancel();
+ }
+ }
+
+ function handleSave() {
+ let finalValue = editValue;
+
+ // Type conversion
+ switch (type) {
+ case 'Number':
+ finalValue = parseFloat(editValue);
+ if (isNaN(finalValue)) {
+ alert('Invalid number');
+ return;
+ }
+ break;
+ case 'Boolean':
+ finalValue = Boolean(editValue);
+ break;
+ case 'Date':
+ if (typeof editValue === 'string') {
+ finalValue = new Date(editValue).toISOString();
+ }
+ break;
+ case 'Object':
+ case 'Array':
+ try {
+ finalValue = JSON.parse(editValue);
+ } catch (err) {
+ alert('Invalid JSON');
+ return;
+ }
+ break;
+ }
+
+ onSave(finalValue);
+ }
+
+ // Type-specific editors
+ switch (type) {
+ case 'Boolean':
+ return (
+
+ {
+ setEditValue(checked);
+ onSave(checked);
+ }}
+ />
+
+ );
+
+ case 'Date':
+ return (
+
+ setEditValue(e.target.value)}
+ onKeyDown={handleKeyDown}
+ onBlur={handleSave}
+ />
+
+ );
+
+ case 'Object':
+ case 'Array':
+ return (
+
+ {showJsonEditor ? (
+ {
+ setShowJsonEditor(false);
+ onCancel();
+ }}
+ />
+ ) : (
+ setShowJsonEditor(true)}>
+ Edit JSON
+
+ )}
+
+ );
+
+ case 'Number':
+ return (
+
+ setEditValue(e.target.value)}
+ onKeyDown={handleKeyDown}
+ onBlur={handleSave}
+ />
+
+ );
+
+ case 'String':
+ default:
+ return (
+
+ setEditValue(e.target.value)}
+ onKeyDown={handleKeyDown}
+ onBlur={handleSave}
+ />
+
+ );
+ }
+}
+```
+
+**File:** `packages/noodl-editor/src/editor/src/views/panels/databrowser/JsonEditorModal.tsx`
+
+```typescript
+import React, { useState } from 'react';
+import { Modal } from '@noodl-core-ui/components/modal';
+import { PrimaryButton, SecondaryButton } from '@noodl-core-ui/components/buttons';
+import styles from './JsonEditorModal.module.css';
+
+interface JsonEditorModalProps {
+ value: any;
+ onSave: (value: any) => void;
+ onCancel: () => void;
+}
+
+export function JsonEditorModal({ value, onSave, onCancel }: JsonEditorModalProps) {
+ const [text, setText] = useState(JSON.stringify(value, null, 2));
+ const [error, setError] = useState(null);
+
+ function handleSave() {
+ try {
+ const parsed = JSON.parse(text);
+ onSave(parsed);
+ } catch (err: any) {
+ setError(err.message);
+ }
+ }
+
+ return (
+
+
+
+ );
+}
+```
+
+---
+
+### Step 4: New Record Modal (3 hours)
+
+**File:** `packages/noodl-editor/src/editor/src/views/panels/databrowser/NewRecordModal.tsx`
+
+```typescript
+import React, { useState } from 'react';
+import { ipcRenderer } from 'electron';
+import { Modal } from '@noodl-core-ui/components/modal';
+import { TextInput } from '@noodl-core-ui/components/inputs/TextInput';
+import { Checkbox } from '@noodl-core-ui/components/inputs/Checkbox';
+import { PrimaryButton, SecondaryButton } from '@noodl-core-ui/components/buttons';
+import styles from './NewRecordModal.module.css';
+
+interface NewRecordModalProps {
+ backendId: string;
+ table: string;
+ schema: TableSchema;
+ onClose: () => void;
+ onSuccess: () => void;
+}
+
+export function NewRecordModal({ backendId, table, schema, onClose, onSuccess }: NewRecordModalProps) {
+ const [data, setData] = useState>({});
+ const [creating, setCreating] = useState(false);
+ const [error, setError] = useState(null);
+
+ async function handleCreate() {
+ // Validation
+ for (const col of schema.columns) {
+ if (col.required && !data[col.name]) {
+ setError(`${col.name} is required`);
+ return;
+ }
+ }
+
+ setCreating(true);
+ setError(null);
+
+ try {
+ await ipcRenderer.invoke('backend:createRecord', backendId, table, data);
+ onSuccess();
+ } catch (err: any) {
+ setError(err.message || 'Failed to create record');
+ } finally {
+ setCreating(false);
+ }
+ }
+
+ function handleFieldChange(fieldName: string, value: any) {
+ setData({ ...data, [fieldName]: value });
+ }
+
+ return (
+
+
+
+
+ {error && (
+
{error}
+ )}
+
+
+
+
+ );
+}
+
+function FieldInput({ type, value, onChange }: {
+ type: string;
+ value: any;
+ onChange: (value: any) => void;
+}) {
+ switch (type) {
+ case 'Boolean':
+ return (
+
+ );
+
+ case 'Number':
+ return (
+ onChange(parseFloat(e.target.value))}
+ />
+ );
+
+ case 'Date':
+ return (
+ onChange(new Date(e.target.value).toISOString())}
+ />
+ );
+
+ case 'Object':
+ case 'Array':
+ return (
+