mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 23:32:55 +01:00
370 lines
12 KiB
Markdown
370 lines
12 KiB
Markdown
# Phase 1: Foundation - Data Model
|
|
|
|
## Overview
|
|
|
|
Establish the data structures and model layer support for responsive breakpoints. This phase adds `breakpointParameters` storage to nodes, extends the model proxy, and adds project-level breakpoint configuration.
|
|
|
|
**Estimate:** 2-3 days
|
|
|
|
## Goals
|
|
|
|
1. Add `breakpointParameters` field to NodeGraphNode model
|
|
2. Extend NodeModel (runtime) with breakpoint parameter support
|
|
3. Add breakpoint configuration to project settings
|
|
4. Extend ModelProxy to handle breakpoint context
|
|
5. Add `allowBreakpoints` flag support to node definitions
|
|
|
|
## Technical Architecture
|
|
|
|
### Data Storage Pattern
|
|
|
|
Following the existing visual states pattern (`stateParameters`), we add parallel `breakpointParameters`:
|
|
|
|
```javascript
|
|
// NodeGraphNode / NodeModel
|
|
{
|
|
id: 'group-1',
|
|
type: 'Group',
|
|
parameters: {
|
|
marginTop: '40px', // base/default breakpoint value
|
|
backgroundColor: '#fff' // non-breakpoint property
|
|
},
|
|
stateParameters: { // existing - visual states
|
|
hover: { backgroundColor: '#eee' }
|
|
},
|
|
breakpointParameters: { // NEW - breakpoints
|
|
tablet: { marginTop: '24px' },
|
|
phone: { marginTop: '16px' },
|
|
smallPhone: { marginTop: '12px' }
|
|
}
|
|
}
|
|
```
|
|
|
|
### Project Settings Schema
|
|
|
|
```javascript
|
|
// project.settings.responsiveBreakpoints
|
|
{
|
|
enabled: true,
|
|
cascadeDirection: 'desktop-first', // or 'mobile-first'
|
|
defaultBreakpoint: 'desktop',
|
|
breakpoints: [
|
|
{ id: 'desktop', name: 'Desktop', minWidth: 1024, icon: 'desktop' },
|
|
{ id: 'tablet', name: 'Tablet', minWidth: 768, maxWidth: 1023, icon: 'tablet' },
|
|
{ id: 'phone', name: 'Phone', minWidth: 320, maxWidth: 767, icon: 'phone' },
|
|
{ id: 'smallPhone', name: 'Small Phone', minWidth: 0, maxWidth: 319, icon: 'phone-small' }
|
|
]
|
|
}
|
|
```
|
|
|
|
### Node Definition Flag
|
|
|
|
```javascript
|
|
// In node definition
|
|
{
|
|
inputs: {
|
|
marginTop: {
|
|
type: { name: 'number', units: ['px', '%'], defaultUnit: 'px' },
|
|
allowBreakpoints: true, // NEW flag
|
|
group: 'Margin and Padding'
|
|
},
|
|
backgroundColor: {
|
|
type: 'color',
|
|
allowVisualStates: true,
|
|
allowBreakpoints: false // colors don't support breakpoints
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Implementation Steps
|
|
|
|
### Step 1: Extend NodeGraphNode Model
|
|
|
|
**File:** `packages/noodl-editor/src/editor/src/models/nodegraphmodel/NodeGraphNode.ts`
|
|
|
|
```typescript
|
|
// Add to class properties
|
|
breakpointParameters: Record<string, Record<string, any>>;
|
|
|
|
// Add to constructor/initialization
|
|
this.breakpointParameters = args.breakpointParameters || {};
|
|
|
|
// Add new methods
|
|
hasBreakpointParameter(name: string, breakpoint: string): boolean {
|
|
return this.breakpointParameters?.[breakpoint]?.[name] !== undefined;
|
|
}
|
|
|
|
getBreakpointParameter(name: string, breakpoint: string): any {
|
|
return this.breakpointParameters?.[breakpoint]?.[name];
|
|
}
|
|
|
|
setBreakpointParameter(name: string, value: any, breakpoint: string, args?: any): void {
|
|
// Similar pattern to setParameter but for breakpoint-specific values
|
|
// Include undo support
|
|
}
|
|
|
|
// Extend getParameter to support breakpoint context
|
|
getParameter(name: string, args?: { state?: string, breakpoint?: string }): any {
|
|
// If breakpoint specified, check breakpointParameters first
|
|
// Then cascade to larger breakpoints
|
|
// Finally fall back to base parameters
|
|
}
|
|
|
|
// Extend toJSON to include breakpointParameters
|
|
toJSON(): object {
|
|
return {
|
|
...existingFields,
|
|
breakpointParameters: this.breakpointParameters
|
|
};
|
|
}
|
|
```
|
|
|
|
### Step 2: Extend Runtime NodeModel
|
|
|
|
**File:** `packages/noodl-runtime/src/models/nodemodel.js`
|
|
|
|
```javascript
|
|
// Add breakpointParameters storage
|
|
NodeModel.prototype.setBreakpointParameter = function(name, value, breakpoint) {
|
|
if (!this.breakpointParameters) this.breakpointParameters = {};
|
|
if (!this.breakpointParameters[breakpoint]) this.breakpointParameters[breakpoint] = {};
|
|
|
|
if (value === undefined) {
|
|
delete this.breakpointParameters[breakpoint][name];
|
|
} else {
|
|
this.breakpointParameters[breakpoint][name] = value;
|
|
}
|
|
|
|
this.emit("breakpointParameterUpdated", { name, value, breakpoint });
|
|
};
|
|
|
|
NodeModel.prototype.setBreakpointParameters = function(breakpointParameters) {
|
|
this.breakpointParameters = breakpointParameters;
|
|
};
|
|
```
|
|
|
|
### Step 3: Add Project Settings Schema
|
|
|
|
**File:** `packages/noodl-editor/src/editor/src/models/projectmodel.ts`
|
|
|
|
```typescript
|
|
// Add default breakpoint settings
|
|
const DEFAULT_BREAKPOINT_SETTINGS = {
|
|
enabled: true,
|
|
cascadeDirection: 'desktop-first',
|
|
defaultBreakpoint: 'desktop',
|
|
breakpoints: [
|
|
{ id: 'desktop', name: 'Desktop', minWidth: 1024, icon: 'DeviceDesktop' },
|
|
{ id: 'tablet', name: 'Tablet', minWidth: 768, maxWidth: 1023, icon: 'DeviceTablet' },
|
|
{ id: 'phone', name: 'Phone', minWidth: 320, maxWidth: 767, icon: 'DevicePhone' },
|
|
{ id: 'smallPhone', name: 'Small Phone', minWidth: 0, maxWidth: 319, icon: 'DevicePhone' }
|
|
]
|
|
};
|
|
|
|
// Add helper methods
|
|
getBreakpointSettings(): BreakpointSettings {
|
|
return this.settings.responsiveBreakpoints || DEFAULT_BREAKPOINT_SETTINGS;
|
|
}
|
|
|
|
setBreakpointSettings(settings: BreakpointSettings): void {
|
|
this.setSetting('responsiveBreakpoints', settings);
|
|
}
|
|
|
|
getBreakpointForWidth(width: number): string {
|
|
const settings = this.getBreakpointSettings();
|
|
const breakpoints = settings.breakpoints;
|
|
|
|
// Find matching breakpoint based on width
|
|
for (const bp of breakpoints) {
|
|
const minMatch = bp.minWidth === undefined || width >= bp.minWidth;
|
|
const maxMatch = bp.maxWidth === undefined || width <= bp.maxWidth;
|
|
if (minMatch && maxMatch) return bp.id;
|
|
}
|
|
|
|
return settings.defaultBreakpoint;
|
|
}
|
|
```
|
|
|
|
### Step 4: Extend ModelProxy
|
|
|
|
**File:** `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/models/modelProxy.ts`
|
|
|
|
```typescript
|
|
export class ModelProxy {
|
|
model: NodeGraphNode;
|
|
editMode: string;
|
|
visualState: string;
|
|
breakpoint: string; // NEW
|
|
|
|
constructor(args) {
|
|
this.model = args.model;
|
|
this.visualState = 'neutral';
|
|
this.breakpoint = 'desktop'; // NEW - default breakpoint
|
|
}
|
|
|
|
setBreakpoint(breakpoint: string) {
|
|
this.breakpoint = breakpoint;
|
|
}
|
|
|
|
// Extend getParameter to handle breakpoints
|
|
getParameter(name: string) {
|
|
const source = this.editMode === 'variant' ? this.model.variant : this.model;
|
|
const port = this.model.getPort(name, 'input');
|
|
|
|
// Check if this property supports breakpoints
|
|
if (port?.allowBreakpoints && this.breakpoint && this.breakpoint !== 'desktop') {
|
|
// Check for breakpoint-specific value
|
|
const breakpointValue = source.getBreakpointParameter(name, this.breakpoint);
|
|
if (breakpointValue !== undefined) return breakpointValue;
|
|
|
|
// Cascade to larger breakpoints (desktop-first)
|
|
// TODO: Support mobile-first cascade
|
|
}
|
|
|
|
// Check visual state
|
|
if (this.visualState && this.visualState !== 'neutral') {
|
|
// existing visual state logic
|
|
}
|
|
|
|
// Fall back to base parameters
|
|
return source.getParameter(name, { state: this.visualState });
|
|
}
|
|
|
|
// Extend setParameter to handle breakpoints
|
|
setParameter(name: string, value: any, args: any = {}) {
|
|
const port = this.model.getPort(name, 'input');
|
|
|
|
// If setting a breakpoint-specific value
|
|
if (port?.allowBreakpoints && this.breakpoint && this.breakpoint !== 'desktop') {
|
|
args.breakpoint = this.breakpoint;
|
|
}
|
|
|
|
// existing state handling
|
|
args.state = this.visualState;
|
|
|
|
const target = this.editMode === 'variant' ? this.model.variant : this.model;
|
|
|
|
if (args.breakpoint) {
|
|
target.setBreakpointParameter(name, value, args.breakpoint, args);
|
|
} else {
|
|
target.setParameter(name, value, args);
|
|
}
|
|
}
|
|
|
|
// Check if current value is inherited or explicitly set
|
|
isBreakpointValueInherited(name: string): boolean {
|
|
if (!this.breakpoint || this.breakpoint === 'desktop') return false;
|
|
|
|
const source = this.editMode === 'variant' ? this.model.variant : this.model;
|
|
return !source.hasBreakpointParameter(name, this.breakpoint);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Step 5: Update Node Type Registration
|
|
|
|
**File:** `packages/noodl-editor/src/editor/src/models/nodelibrary/nodelibrary.ts`
|
|
|
|
```typescript
|
|
// When registering node types, process allowBreakpoints flag
|
|
// Similar to how allowVisualStates is handled
|
|
|
|
processNodeType(nodeType) {
|
|
// existing processing...
|
|
|
|
// Process allowBreakpoints for inputs
|
|
if (nodeType.inputs) {
|
|
for (const [name, input] of Object.entries(nodeType.inputs)) {
|
|
if (input.allowBreakpoints) {
|
|
// Mark this port as breakpoint-aware
|
|
// This will be used by property panel to show breakpoint controls
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Step 6: Update GraphModel (Runtime)
|
|
|
|
**File:** `packages/noodl-runtime/src/models/graphmodel.js`
|
|
|
|
```javascript
|
|
// Add method to update breakpoint parameters
|
|
GraphModel.prototype.updateNodeBreakpointParameter = function(
|
|
nodeId,
|
|
parameterName,
|
|
parameterValue,
|
|
breakpoint
|
|
) {
|
|
const node = this.getNodeWithId(nodeId);
|
|
if (!node) return;
|
|
|
|
node.setBreakpointParameter(parameterName, parameterValue, breakpoint);
|
|
};
|
|
|
|
// Extend project settings handling
|
|
GraphModel.prototype.getBreakpointSettings = function() {
|
|
return this.settings?.responsiveBreakpoints || DEFAULT_BREAKPOINT_SETTINGS;
|
|
};
|
|
```
|
|
|
|
## Files to Modify
|
|
|
|
| File | Changes |
|
|
|------|---------|
|
|
| `packages/noodl-editor/src/editor/src/models/nodegraphmodel/NodeGraphNode.ts` | Add breakpointParameters field, getter/setter methods |
|
|
| `packages/noodl-editor/src/editor/src/models/projectmodel.ts` | Add breakpoint settings helpers |
|
|
| `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/models/modelProxy.ts` | Add breakpoint context, extend get/setParameter |
|
|
| `packages/noodl-runtime/src/models/nodemodel.js` | Add breakpoint parameter methods |
|
|
| `packages/noodl-runtime/src/models/graphmodel.js` | Add breakpoint settings handling |
|
|
|
|
## Files to Create
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `packages/noodl-editor/src/editor/src/models/breakpointSettings.ts` | TypeScript interfaces for breakpoint settings |
|
|
|
|
## Testing Checklist
|
|
|
|
- [ ] NodeGraphNode can store and retrieve breakpointParameters
|
|
- [ ] NodeGraphNode serializes breakpointParameters to JSON correctly
|
|
- [ ] NodeGraphNode loads breakpointParameters from JSON correctly
|
|
- [ ] ModelProxy correctly returns breakpoint-specific values
|
|
- [ ] ModelProxy correctly identifies inherited vs explicit values
|
|
- [ ] Project settings store and load breakpoint configuration
|
|
- [ ] Cascade works correctly (tablet falls back to desktop)
|
|
- [ ] Undo/redo works for breakpoint parameter changes
|
|
|
|
## Success Criteria
|
|
|
|
1. ✅ Can programmatically set `node.setBreakpointParameter('marginTop', '24px', 'tablet')`
|
|
2. ✅ Can retrieve with `node.getBreakpointParameter('marginTop', 'tablet')`
|
|
3. ✅ Project JSON includes breakpointParameters when saved
|
|
4. ✅ Project JSON loads breakpointParameters when opened
|
|
5. ✅ ModelProxy returns correct value based on current breakpoint context
|
|
|
|
## Gotchas & Notes
|
|
|
|
1. **Undo Support**: Make sure breakpoint parameter changes are undoable. Follow the same pattern as `setParameter` with undo groups.
|
|
|
|
2. **Cascade Order**: Desktop-first means `tablet` inherits from `desktop`, `phone` inherits from `tablet`, `smallPhone` inherits from `phone`. Mobile-first reverses this.
|
|
|
|
3. **Default Breakpoint**: When `breakpoint === 'desktop'` (or whatever the default is), we should NOT use breakpointParameters - use base parameters instead.
|
|
|
|
4. **Parameter Migration**: Existing projects won't have breakpointParameters. Handle gracefully (undefined → empty object).
|
|
|
|
5. **Port Flag**: The `allowBreakpoints` flag on ports determines which properties show breakpoint controls in the UI. This is read-only metadata, not stored per-node.
|
|
|
|
## Confidence Checkpoints
|
|
|
|
After completing each step, verify:
|
|
|
|
| Step | Checkpoint |
|
|
|------|------------|
|
|
| 1 | Can add/get breakpoint params in editor console |
|
|
| 2 | Runtime node model accepts breakpoint params |
|
|
| 3 | Project settings UI shows breakpoint config |
|
|
| 4 | ModelProxy returns correct value per breakpoint |
|
|
| 5 | Saving/loading project preserves breakpoint data |
|