mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-03-08 01:53:30 +01:00
Finished prototype local backends and expression editor
This commit is contained in:
@@ -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
|
||||
|
||||
511
dev-docs/reference/PANEL-UI-STYLE-GUIDE.md
Normal file
511
dev-docs/reference/PANEL-UI-STYLE-GUIDE.md
Normal file
@@ -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
|
||||
<div className={css.Root}>
|
||||
{/* Header with title and close button */}
|
||||
<div className={css.Header}>
|
||||
<HStack hasSpacing>
|
||||
<Icon icon={IconName.Something} size={IconSize.Small} />
|
||||
<VStack>
|
||||
<Text textType={TextType.DefaultContrast}>Panel Title</Text>
|
||||
<Text textType={TextType.Shy} style={{ fontSize: '11px' }}>
|
||||
Subtitle or context
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
<IconButton icon={IconName.Close} onClick={onClose} />
|
||||
</div>
|
||||
|
||||
{/* Toolbar with actions */}
|
||||
<div className={css.Toolbar}>{/* Filters, search, action buttons */}</div>
|
||||
|
||||
{/* Content area (scrollable) */}
|
||||
<div className={css.Content}>{/* Main panel content */}</div>
|
||||
|
||||
{/* Footer (optional - pagination, status) */}
|
||||
<div className={css.Footer}>{/* Page controls, counts */}</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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
|
||||
<div className={css.Overlay}>
|
||||
<div className={css.Modal}>
|
||||
{/* Header */}
|
||||
<div className={css.Header}>
|
||||
<Text textType={TextType.Proud}>Modal Title</Text>
|
||||
<IconButton icon={IconName.Close} onClick={onClose} />
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className={css.Body}>{/* Form fields, content */}</div>
|
||||
|
||||
{/* Footer with actions */}
|
||||
<div className={css.Footer}>
|
||||
<PrimaryButton label="Cancel" variant={PrimaryButtonVariant.Muted} onClick={onClose} />
|
||||
<PrimaryButton label="Create" variant={PrimaryButtonVariant.Cta} onClick={onSubmit} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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_
|
||||
Reference in New Issue
Block a user