mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 23:32:55 +01:00
working on problem opening projet
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
# TASK-010B: Preview "No HOME Component" Bug - Status Actuel
|
||||
|
||||
**Date**: 12 janvier 2026, 11:40
|
||||
**Status**: 🔴 EN COURS - CRITIQUE
|
||||
**Priority**: P0 - BLOQUEUR ABSOLU
|
||||
|
||||
## 🚨 Symptômes Actuels
|
||||
|
||||
**Le preview ne fonctionne JAMAIS après création de projet**
|
||||
|
||||
### Ce que l'utilisateur voit:
|
||||
|
||||
```
|
||||
ERROR
|
||||
|
||||
No 🏠 HOME component selected
|
||||
Click Make home as shown below.
|
||||
[Image avec instructions]
|
||||
```
|
||||
|
||||
### Logs Console:
|
||||
|
||||
```
|
||||
✅ Using real ProjectOrganizationService
|
||||
ProjectsPage.tsx:67 🔧 Initializing GitHub OAuth service...
|
||||
GitHubOAuthService.ts:353 🔧 Initializing GitHubOAuthService
|
||||
ProjectsPage.tsx:73 ✅ GitHub OAuth initialized. Authenticated: false
|
||||
ViewerConnection.ts:49 Connected to viewer server at ws://localhost:8574
|
||||
projectmodel.modules.ts:104 noodl_modules folder not found (fresh project), skipping module loading
|
||||
ProjectsPage.tsx:112 🔔 Projects list changed, updating dashboard
|
||||
useProjectOrganization.ts:75 ✅ Using real ProjectOrganizationService
|
||||
LocalProjectsModel.ts:286 Project created successfully: lkh
|
||||
[object%20Module]:1 Failed to load resource: net::ERR_FILE_NOT_FOUND
|
||||
nodegrapheditor.ts:374 Failed to load AI assistant outer icon: Event
|
||||
nodegrapheditor.ts:379 Failed to load warning icon: Event
|
||||
nodegrapheditor.ts:369 Failed to load AI assistant inner icon: Event
|
||||
nodegrapheditor.ts:359 Failed to load home icon: Event
|
||||
nodegrapheditor.ts:364 Failed to load component icon: Event
|
||||
projectmodel.ts:1259 Project saved Mon Jan 12 2026 11:21:48 GMT+0100
|
||||
```
|
||||
|
||||
**Point clé**: Le projet est créé avec succès, sauvegardé, mais le preview affiche quand même l'erreur "No HOME component".
|
||||
|
||||
---
|
||||
|
||||
## 📋 Historique des Tentatives de Fix
|
||||
|
||||
### Tentative #1 (8 janvier): LocalTemplateProvider avec chemins relatifs
|
||||
|
||||
**Status**: ❌ ÉCHOUÉ
|
||||
**Problème**: Résolution de chemin avec `__dirname` ne fonctionne pas dans webpack
|
||||
**Erreur**: `Template not found at: ./project-examples/...`
|
||||
|
||||
### Tentative #2 (8 janvier): LocalTemplateProvider avec process.cwd()
|
||||
|
||||
**Status**: ❌ ÉCHOUÉ
|
||||
**Problème**: `process.cwd()` pointe vers le mauvais répertoire
|
||||
**Erreur**: `Template not found at: /Users/tw/.../packages/noodl-editor/project-examples/...`
|
||||
|
||||
### Tentative #3 (9 janvier): Génération programmatique
|
||||
|
||||
**Status**: ❌ ÉCHOUÉ
|
||||
**Problème**: Structure JSON incomplète
|
||||
**Erreur**: `Cannot read properties of undefined (reading 'comments')`
|
||||
**Résolution**: Ajout du champ `comments: []` dans la structure
|
||||
|
||||
### Tentative #4 (12 janvier - AUJOURD'HUI): Fix rootComponent
|
||||
|
||||
**Status**: 🟡 EN TEST
|
||||
**Changements**:
|
||||
|
||||
1. Ajout de `rootComponent: 'App'` dans `hello-world.template.ts`
|
||||
2. Ajout du type `rootComponent?: string` dans `ProjectTemplate.ts`
|
||||
3. Modification de `ProjectModel.fromJSON()` pour gérer `rootComponent`
|
||||
|
||||
**Fichiers modifiés**:
|
||||
|
||||
- `packages/noodl-editor/src/editor/src/models/template/templates/hello-world.template.ts`
|
||||
- `packages/noodl-editor/src/editor/src/models/template/ProjectTemplate.ts`
|
||||
- `packages/noodl-editor/src/editor/src/models/projectmodel.ts`
|
||||
|
||||
**Hypothèse**: Le runtime attend une propriété `rootComponent` dans le project.json pour savoir quel composant afficher dans le preview.
|
||||
|
||||
**Résultat**: ⏳ ATTENTE DE CONFIRMATION - L'utilisateur rapporte que ça ne fonctionne toujours pas
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Analyse du Problème Actuel
|
||||
|
||||
### Questions Critiques
|
||||
|
||||
1. **Le fix du rootComponent est-il appliqué?**
|
||||
|
||||
- Le projet a-t-il été créé APRÈS le fix?
|
||||
- Faut-il redémarrer le dev server?
|
||||
- Y a-t-il un problème de cache webpack?
|
||||
|
||||
2. **Le project.json contient-il rootComponent?**
|
||||
|
||||
- Emplacement probable: `~/Documents/[nom-projet]/project.json` ou `~/Noodl Projects/[nom-projet]/project.json`
|
||||
- Contenu attendu: `"rootComponent": "App"`
|
||||
|
||||
3. **Le runtime charge-t-il correctement le projet?**
|
||||
- Vérifier dans `noodl-runtime/src/models/graphmodel.js`
|
||||
- Méthode `importEditorData()` ligne ~83: `this.setRootComponentName(exportData.rootComponent)`
|
||||
|
||||
### Points de Contrôle
|
||||
|
||||
```typescript
|
||||
// 1. EmbeddedTemplateProvider.download() - ligne 92
|
||||
await filesystem.writeFile(projectJsonPath, JSON.stringify(projectContent, null, 2));
|
||||
// ✅ Vérifié: Le template content inclut bien rootComponent
|
||||
|
||||
// 2. ProjectModel.fromJSON() - ligne 172
|
||||
if (json.rootComponent && !_this.rootNode) {
|
||||
const rootComponent = _this.getComponentWithName(json.rootComponent);
|
||||
if (rootComponent) {
|
||||
_this.setRootComponent(rootComponent);
|
||||
}
|
||||
}
|
||||
// ✅ Ajouté: Gestion de rootComponent
|
||||
|
||||
// 3. ProjectModel.setRootComponent() - ligne 233
|
||||
setRootComponent(component: ComponentModel) {
|
||||
const root = _.find(component.graph.roots, function (n) {
|
||||
return n.type.allowAsExportRoot;
|
||||
});
|
||||
if (root) this.setRootNode(root);
|
||||
}
|
||||
// ⚠️ ATTENTION: Dépend de n.type.allowAsExportRoot
|
||||
```
|
||||
|
||||
### Hypothèses sur le Problème Persistant
|
||||
|
||||
**Hypothèse A**: Cache webpack non vidé
|
||||
|
||||
- Le nouveau code n'est pas chargé
|
||||
- Solution: `npm run clean:all && npm run dev`
|
||||
|
||||
**Hypothèse B**: Projet créé avec l'ancien template
|
||||
|
||||
- Le projet existe déjà et n'a pas rootComponent
|
||||
- Solution: Supprimer le projet et en créer un nouveau
|
||||
|
||||
**Hypothèse C**: Le runtime ne charge pas rootComponent
|
||||
|
||||
- Le graphmodel.js ne gère peut-être pas rootComponent?
|
||||
- Solution: Vérifier `noodl-runtime/src/models/graphmodel.js`
|
||||
|
||||
**Hypothèse D**: Le node Router ne permet pas allowAsExportRoot
|
||||
|
||||
- `setRootComponent()` cherche un node avec `allowAsExportRoot: true`
|
||||
- Le Router ne l'a peut-être pas?
|
||||
- Solution: Vérifier la définition du node Router
|
||||
|
||||
**Hypothèse E**: Mauvaise synchronisation editor ↔ runtime
|
||||
|
||||
- Le project.json a rootComponent mais le runtime ne le reçoit pas
|
||||
- Solution: Vérifier ViewerConnection et l'envoi du projet
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Plan de Débogage Immédiat
|
||||
|
||||
### Étape 1: Vérifier que le fix est appliqué (5 min)
|
||||
|
||||
```bash
|
||||
# 1. Nettoyer complètement les caches
|
||||
npm run clean:all
|
||||
|
||||
# 2. Redémarrer le dev server
|
||||
npm run dev
|
||||
|
||||
# 3. Attendre que webpack compile (voir "webpack compiled successfully")
|
||||
```
|
||||
|
||||
### Étape 2: Créer un NOUVEAU projet (2 min)
|
||||
|
||||
- Supprimer le projet "lkh" existant depuis le dashboard
|
||||
- Créer un nouveau projet avec un nom différent (ex: "test-preview")
|
||||
- Observer les logs console
|
||||
|
||||
### Étape 3: Vérifier le project.json créé (2 min)
|
||||
|
||||
```bash
|
||||
# Trouver le projet
|
||||
find ~ -name "test-preview" -type d 2>/dev/null | grep -i noodl
|
||||
|
||||
# Afficher son project.json
|
||||
cat [chemin-trouvé]/project.json | grep -A 2 "rootComponent"
|
||||
```
|
||||
|
||||
**Attendu**: On devrait voir `"rootComponent": "App"`
|
||||
|
||||
### Étape 4: Ajouter des logs de débogage (10 min)
|
||||
|
||||
Si ça ne fonctionne toujours pas, ajouter des console.log:
|
||||
|
||||
**Dans `ProjectModel.fromJSON()`** (ligne 172):
|
||||
|
||||
```typescript
|
||||
if (json.rootComponent && !_this.rootNode) {
|
||||
console.log('🔍 Loading rootComponent from template:', json.rootComponent);
|
||||
const rootComponent = _this.getComponentWithName(json.rootComponent);
|
||||
console.log('🔍 Found component?', !!rootComponent);
|
||||
if (rootComponent) {
|
||||
console.log('🔍 Setting root component:', rootComponent.name);
|
||||
_this.setRootComponent(rootComponent);
|
||||
console.log('🔍 Root node after setRootComponent:', _this.rootNode?.id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Dans `ProjectModel.setRootComponent()`** (ligne 233):
|
||||
|
||||
```typescript
|
||||
setRootComponent(component: ComponentModel) {
|
||||
console.log('🔍 setRootComponent called with:', component.name);
|
||||
console.log('🔍 Graph roots:', component.graph.roots.length);
|
||||
const root = _.find(component.graph.roots, function (n) {
|
||||
console.log('🔍 Checking node:', n.type, 'allowAsExportRoot:', n.type.allowAsExportRoot);
|
||||
return n.type.allowAsExportRoot;
|
||||
});
|
||||
console.log('🔍 Found export root?', !!root);
|
||||
if (root) this.setRootNode(root);
|
||||
}
|
||||
```
|
||||
|
||||
### Étape 5: Vérifier le runtime (15 min)
|
||||
|
||||
**Vérifier `noodl-runtime/src/models/graphmodel.js`**:
|
||||
|
||||
```javascript
|
||||
// Ligne ~83 dans importEditorData()
|
||||
this.setRootComponentName(exportData.rootComponent);
|
||||
```
|
||||
|
||||
Ajouter des logs:
|
||||
|
||||
```javascript
|
||||
console.log('🔍 Runtime receiving rootComponent:', exportData.rootComponent);
|
||||
this.setRootComponentName(exportData.rootComponent);
|
||||
console.log('🔍 Runtime rootComponent set to:', this.rootComponent);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Solutions Possibles
|
||||
|
||||
### Solution Rapide: Forcer le rootComponent manuellement
|
||||
|
||||
Si le template ne fonctionne pas, forcer dans `LocalProjectsModel.ts` après création:
|
||||
|
||||
```typescript
|
||||
// Dans newProject(), après projectFromDirectory
|
||||
projectFromDirectory(dirEntry, (project) => {
|
||||
if (!project) {
|
||||
console.error('Failed to create project from template');
|
||||
fn();
|
||||
return;
|
||||
}
|
||||
|
||||
project.name = name;
|
||||
|
||||
// 🔧 FORCE ROOT COMPONENT
|
||||
const appComponent = project.getComponentWithName('App');
|
||||
if (appComponent && !project.getRootNode()) {
|
||||
console.log('🔧 Forcing root component to App');
|
||||
project.setRootComponent(appComponent);
|
||||
}
|
||||
|
||||
this._addProject(project);
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
### Solution Robuste: Vérifier allowAsExportRoot
|
||||
|
||||
Vérifier que le node Router a bien cette propriété. Sinon, utiliser un Group comme root:
|
||||
|
||||
```typescript
|
||||
// Dans hello-world.template.ts
|
||||
graph: {
|
||||
roots: [
|
||||
{
|
||||
id: generateId(),
|
||||
type: 'Group', // Au lieu de 'Router'
|
||||
x: 100,
|
||||
y: 100,
|
||||
parameters: {},
|
||||
ports: [],
|
||||
children: [
|
||||
{
|
||||
id: generateId(),
|
||||
type: 'Router',
|
||||
x: 0,
|
||||
y: 0,
|
||||
parameters: {
|
||||
startPage: '/#__page__/Home'
|
||||
},
|
||||
ports: [],
|
||||
children: []
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Solution Alternative: Utiliser rootNodeId au lieu de rootComponent
|
||||
|
||||
Si `rootComponent` par nom ne fonctionne pas, utiliser `rootNodeId`:
|
||||
|
||||
```typescript
|
||||
// Dans le template, calculer l'ID du premier root
|
||||
const appRootId = generateId();
|
||||
|
||||
content: {
|
||||
rootComponent: 'App', // Garder pour compatibilité
|
||||
rootNodeId: appRootId, // Ajouter ID direct
|
||||
components: [
|
||||
{
|
||||
name: 'App',
|
||||
graph: {
|
||||
roots: [
|
||||
{
|
||||
id: appRootId, // Utiliser le même ID
|
||||
type: 'Router',
|
||||
// ...
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist de Résolution
|
||||
|
||||
### Tests Immédiats
|
||||
|
||||
- [ ] Cache webpack vidé (`npm run clean:all`)
|
||||
- [ ] Dev server redémarré
|
||||
- [ ] Nouveau projet créé (pas le même nom)
|
||||
- [ ] project.json contient `rootComponent: "App"`
|
||||
- [ ] Logs ajoutés dans ProjectModel
|
||||
- [ ] Console montre les logs de rootComponent
|
||||
- [ ] Preview affiche "Hello World!" au lieu de "No HOME component"
|
||||
|
||||
### Si ça ne fonctionne toujours pas
|
||||
|
||||
- [ ] Vérifier graphmodel.js dans noodl-runtime
|
||||
- [ ] Vérifier définition du node Router (allowAsExportRoot)
|
||||
- [ ] Tester avec un Group comme root
|
||||
- [ ] Tester avec rootNodeId au lieu de rootComponent
|
||||
- [ ] Vérifier ViewerConnection et l'envoi du projet
|
||||
|
||||
### Documentation Finale
|
||||
|
||||
- [ ] Documenter la solution qui fonctionne
|
||||
- [ ] Mettre à jour CHANGELOG.md
|
||||
- [ ] Ajouter dans LEARNINGS.md
|
||||
- [ ] Créer tests de régression
|
||||
- [ ] Mettre à jour README de TASK-010
|
||||
|
||||
---
|
||||
|
||||
## 📞 Prochaines Actions pour l'Utilisateur
|
||||
|
||||
### Action Immédiate (2 min)
|
||||
|
||||
1. Arrêter le dev server (Ctrl+C)
|
||||
2. Exécuter: `npm run clean:all`
|
||||
3. Relancer: `npm run dev`
|
||||
4. Attendre "webpack compiled successfully"
|
||||
5. Supprimer le projet "lkh" existant
|
||||
6. Créer un NOUVEAU projet avec un nom différent
|
||||
7. Tester le preview
|
||||
|
||||
### Si ça ne marche pas
|
||||
|
||||
Me dire:
|
||||
|
||||
- Le nom du nouveau projet créé
|
||||
- Le chemin où il se trouve
|
||||
- Le contenu de `project.json` (surtout la présence de `rootComponent`)
|
||||
- Les nouveaux logs console
|
||||
|
||||
### Commande pour trouver le projet.json:
|
||||
|
||||
```bash
|
||||
find ~ -name "project.json" -path "*/Noodl*" -type f -exec grep -l "rootComponent" {} \; 2>/dev/null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Mis à jour**: 12 janvier 2026, 11:40
|
||||
**Prochaine révision**: Après test avec cache vidé
|
||||
@@ -0,0 +1,106 @@
|
||||
# BUG-001: Home Component Shown as "Component" not "Page"
|
||||
|
||||
**Severity**: 🟡 Medium (Cosmetic/UX Issue)
|
||||
**Status**: Identified
|
||||
**Category**: UI Display
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Symptom
|
||||
|
||||
When creating a new project, the Components panel shows:
|
||||
|
||||
- ✅ **App** - displayed as regular component
|
||||
- ❌ **Home** - displayed as regular component (should show as "page")
|
||||
|
||||
**Expected**: Home should have a page icon (router icon) indicating it's a page component.
|
||||
|
||||
**Actual**: Home shows with standard component icon.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Root Cause
|
||||
|
||||
The component name **IS correct** in the template (`'/#__page__/Home'`), but the UI display logic may not be recognizing it properly.
|
||||
|
||||
### Template Structure (CORRECT)
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/models/template/templates/hello-world.template.ts
|
||||
|
||||
components: [
|
||||
{
|
||||
name: 'App' // ✅ Regular component
|
||||
// ...
|
||||
},
|
||||
{
|
||||
name: '/#__page__/Home' // ✅ CORRECT - Has page prefix!
|
||||
// ...
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
The `/#__page__/` prefix is the standard Noodl convention for marking page components.
|
||||
|
||||
---
|
||||
|
||||
## 💡 Analysis
|
||||
|
||||
**Location**: `packages/noodl-editor/src/editor/src/views/panels/ComponentsPanelNew/hooks/useComponentsPanel.ts`
|
||||
|
||||
The issue is likely in how the Components Panel determines if something is a page:
|
||||
|
||||
```typescript
|
||||
// Pseudo-code of likely logic:
|
||||
const isPage = component.name.startsWith('/#__page__/');
|
||||
```
|
||||
|
||||
**Possible causes**:
|
||||
|
||||
1. The component naming is correct, but display logic has a bug
|
||||
2. The icon determination logic doesn't check for page prefix
|
||||
3. UI state not updated after project load
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Proposed Solution
|
||||
|
||||
### Option 1: Verify Icon Logic (Recommended)
|
||||
|
||||
Check `ComponentItem.tsx` line ~85:
|
||||
|
||||
```typescript
|
||||
let icon = IconName.Component;
|
||||
if (component.isRoot) {
|
||||
icon = IconName.Home;
|
||||
} else if (component.isPage) {
|
||||
// ← Verify this is set correctly
|
||||
icon = IconName.PageRouter;
|
||||
}
|
||||
```
|
||||
|
||||
Ensure `component.isPage` is correctly detected from the `/#__page__/` prefix.
|
||||
|
||||
### Option 2: Debug Data Flow
|
||||
|
||||
Add temporary logging:
|
||||
|
||||
```typescript
|
||||
console.log('Component:', component.name);
|
||||
console.log('Is Page?', component.isPage);
|
||||
console.log('Is Root?', component.isRoot);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Verification Steps
|
||||
|
||||
1. Create new project from launcher
|
||||
2. Open Components panel
|
||||
3. Check icon next to "Home" component
|
||||
4. Expected: Should show router/page icon, not component icon
|
||||
|
||||
---
|
||||
|
||||
**Impact**: Low - Cosmetic issue only, doesn't affect functionality
|
||||
**Priority**: P2 - Fix after critical bugs
|
||||
@@ -0,0 +1,118 @@
|
||||
# BUG-002: App Component Not Set as Home
|
||||
|
||||
**Severity**: 🔴 CRITICAL
|
||||
**Status**: Root Cause Identified
|
||||
**Category**: Core Functionality
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Symptom
|
||||
|
||||
After creating a new project:
|
||||
|
||||
- ❌ Preview shows error: **"No 🏠 HOME component selected"**
|
||||
- ❌ App component is not marked as Home in Components panel
|
||||
- ❌ `ProjectModel.instance.rootNode` is `undefined`
|
||||
|
||||
**Expected**: App component should be automatically set as Home, preview should work.
|
||||
|
||||
**Actual**: No home component is set, preview fails.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Root Cause
|
||||
|
||||
**Router node is missing `allowAsExportRoot: true`**
|
||||
|
||||
### The Problem Chain
|
||||
|
||||
1. **Template includes `rootComponent`**:
|
||||
|
||||
```typescript
|
||||
// hello-world.template.ts
|
||||
content: {
|
||||
rootComponent: 'App', // ✅ This is correct
|
||||
components: [...]
|
||||
}
|
||||
```
|
||||
|
||||
2. **ProjectModel.fromJSON() tries to set it**:
|
||||
|
||||
```typescript
|
||||
// projectmodel.ts:172
|
||||
if (json.rootComponent && !_this.rootNode) {
|
||||
const rootComponent = _this.getComponentWithName(json.rootComponent);
|
||||
if (rootComponent) {
|
||||
_this.setRootComponent(rootComponent); // ← Calls the broken method
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **setRootComponent() SILENTLY FAILS**:
|
||||
|
||||
```typescript
|
||||
// projectmodel.ts:233
|
||||
setRootComponent(component: ComponentModel) {
|
||||
const root = _.find(component.graph.roots, function (n) {
|
||||
return n.type.allowAsExportRoot; // ❌ Router returns undefined!
|
||||
});
|
||||
if (root) this.setRootNode(root); // ❌ NEVER EXECUTES!
|
||||
// NO ERROR THROWN - Silent failure!
|
||||
}
|
||||
```
|
||||
|
||||
4. **Router node has NO `allowAsExportRoot`**:
|
||||
|
||||
```typescript
|
||||
// packages/noodl-viewer-react/src/nodes/navigation/router.tsx
|
||||
const RouterNode = {
|
||||
name: 'Router'
|
||||
// ❌ MISSING: allowAsExportRoot: true
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💥 Impact
|
||||
|
||||
This is a **BLOCKER**:
|
||||
|
||||
- New projects cannot be previewed
|
||||
- Users see cryptic error message
|
||||
- "Make Home" button also fails (same root cause)
|
||||
- No console errors to debug
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Solution
|
||||
|
||||
**Add one line to router.tsx**:
|
||||
|
||||
```typescript
|
||||
const RouterNode = {
|
||||
name: 'Router',
|
||||
displayNodeName: 'Page Router',
|
||||
allowAsExportRoot: true, // ✅ ADD THIS
|
||||
category: 'Visuals'
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
**That's it!** This single line fixes both Bug #2 and Bug #3.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Verification
|
||||
|
||||
After fix:
|
||||
|
||||
1. Create new project
|
||||
2. Check Components panel - App should have home icon
|
||||
3. Open preview - should show "Hello World!"
|
||||
4. No error messages
|
||||
|
||||
---
|
||||
|
||||
**Priority**: P0 - MUST FIX IMMEDIATELY
|
||||
**Blocks**: All new project workflows
|
||||
@@ -0,0 +1,99 @@
|
||||
# BUG-003: "Make Home" Context Menu Does Nothing
|
||||
|
||||
**Severity**: 🔴 CRITICAL
|
||||
**Status**: Root Cause Identified (Same as BUG-002)
|
||||
**Category**: Core Functionality
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Symptom
|
||||
|
||||
When right-clicking on App component and selecting "Make Home":
|
||||
|
||||
- ❌ Nothing happens
|
||||
- ❌ No console output
|
||||
- ❌ No error messages
|
||||
- ❌ Component doesn't become Home
|
||||
|
||||
**Expected**: App should be set as Home, preview should work.
|
||||
|
||||
**Actual**: Silent failure, no feedback.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Root Cause
|
||||
|
||||
**Same as BUG-002**: Router node missing `allowAsExportRoot: true`
|
||||
|
||||
### The Code Path
|
||||
|
||||
1. **User clicks "Make Home"** in context menu
|
||||
|
||||
2. **Handler is called correctly**:
|
||||
|
||||
```typescript
|
||||
// useComponentActions.ts:27
|
||||
const handleMakeHome = useCallback((node: TreeNode) => {
|
||||
const component = node.data.component;
|
||||
|
||||
ProjectModel.instance?.setRootComponent(component); // ← This is called!
|
||||
}, []);
|
||||
```
|
||||
|
||||
3. **setRootComponent() FAILS SILENTLY**:
|
||||
|
||||
```typescript
|
||||
// projectmodel.ts:233
|
||||
setRootComponent(component: ComponentModel) {
|
||||
const root = _.find(component.graph.roots, function (n) {
|
||||
return n.type.allowAsExportRoot; // ❌ Returns undefined for Router!
|
||||
});
|
||||
if (root) this.setRootNode(root); // ❌ Never reaches here
|
||||
// ❌ NO ERROR, NO LOG, NO FEEDBACK
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Why It's Silent
|
||||
|
||||
The method doesn't throw errors or log anything. It just:
|
||||
|
||||
1. Searches for a node with `allowAsExportRoot: true`
|
||||
2. Finds nothing (Router doesn't have it)
|
||||
3. Exits quietly
|
||||
|
||||
**No one knows it failed!**
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Solution
|
||||
|
||||
**Same fix as BUG-002**: Add `allowAsExportRoot: true` to Router node.
|
||||
|
||||
```typescript
|
||||
// packages/noodl-viewer-react/src/nodes/navigation/router.tsx
|
||||
const RouterNode = {
|
||||
name: 'Router',
|
||||
displayNodeName: 'Page Router',
|
||||
allowAsExportRoot: true // ✅ ADD THIS LINE
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Verification
|
||||
|
||||
After fix:
|
||||
|
||||
1. Create new project
|
||||
2. Right-click App component
|
||||
3. Click "Make Home"
|
||||
4. App should get home icon
|
||||
5. Preview should work
|
||||
|
||||
---
|
||||
|
||||
**Priority**: P0 - MUST FIX IMMEDIATELY
|
||||
**Fixes With**: BUG-002 (same root cause, same solution)
|
||||
@@ -0,0 +1,98 @@
|
||||
# BUG-004: "Create Page" Modal Misaligned
|
||||
|
||||
**Severity**: 🟡 Medium (UI/UX Issue)
|
||||
**Status**: Identified
|
||||
**Category**: CSS Styling
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Symptom
|
||||
|
||||
When clicking "+ Add new page" in the Page Router property editor:
|
||||
|
||||
- ❌ Modal rectangle appears **below** the pointer triangle
|
||||
- ❌ Triangle "floats" and barely touches the rectangle
|
||||
- ❌ Looks unprofessional and broken
|
||||
|
||||
**Expected**: Triangle should be seamlessly attached to the modal rectangle.
|
||||
|
||||
**Actual**: Triangle and rectangle are visually disconnected.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Root Cause
|
||||
|
||||
**CSS positioning issue in legacy PopupLayer system**
|
||||
|
||||
### The Modal Components
|
||||
|
||||
```typescript
|
||||
// Pages.tsx line ~195
|
||||
PopupLayer.instance.showPopup({
|
||||
content: { el: $(div) },
|
||||
attachTo: $(this.popupAnchor),
|
||||
position: 'right' // ← Position hint
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
The popup uses legacy jQuery + CSS positioning from `pages.css`.
|
||||
|
||||
### Likely CSS Issue
|
||||
|
||||
```css
|
||||
/* packages/noodl-editor/src/editor/src/styles/propertyeditor/pages.css */
|
||||
|
||||
/* Triangle pointer */
|
||||
.popup-layer-arrow {
|
||||
/* Positioned absolutely */
|
||||
}
|
||||
|
||||
/* Modal rectangle */
|
||||
.popup-layer-content {
|
||||
/* Also positioned absolutely */
|
||||
/* ❌ Offset calculations may be incorrect */
|
||||
}
|
||||
```
|
||||
|
||||
The triangle and rectangle are positioned separately, causing misalignment.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Solution
|
||||
|
||||
### Option 1: Fix CSS (Recommended)
|
||||
|
||||
Adjust positioning in `pages.css`:
|
||||
|
||||
```css
|
||||
.popup-layer-content {
|
||||
/* Ensure top aligns with triangle */
|
||||
margin-top: 0;
|
||||
/* Adjust offset if needed */
|
||||
}
|
||||
|
||||
.popup-layer-arrow {
|
||||
/* Ensure connects to content */
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: Migrate to Modern Popup
|
||||
|
||||
Replace legacy PopupLayer with modern PopupMenu (from `@noodl-core-ui`).
|
||||
|
||||
**Complexity**: Higher, but better long-term solution.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Verification
|
||||
|
||||
1. Open project with Page Router
|
||||
2. Click "+ Add new page" button
|
||||
3. Check modal appearance
|
||||
4. Triangle should seamlessly connect to rectangle
|
||||
|
||||
---
|
||||
|
||||
**Priority**: P2 - Fix after critical bugs
|
||||
**Impact**: Cosmetic only, doesn't affect functionality
|
||||
@@ -0,0 +1,103 @@
|
||||
# Investigation: Project Creation Bugs
|
||||
|
||||
**Date**: January 12, 2026
|
||||
**Status**: 🔴 CRITICAL - Multiple Issues Identified
|
||||
**Priority**: P0 - Blocks Basic Functionality
|
||||
|
||||
---
|
||||
|
||||
## 📋 Summary
|
||||
|
||||
Four critical bugs were identified when creating new projects from the launcher:
|
||||
|
||||
1. **Home component shown as "component" not "page"** in Components panel
|
||||
2. **App component not set as Home** (preview fails with "No HOME component")
|
||||
3. **"Make Home" context menu does nothing** (no console output, no error)
|
||||
4. **"Create new page" modal misaligned** (triangle pointer detached from rectangle)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Root Cause Analysis
|
||||
|
||||
### CRITICAL: Router Node Missing `allowAsExportRoot`
|
||||
|
||||
**Location**: `packages/noodl-viewer-react/src/nodes/navigation/router.tsx`
|
||||
|
||||
The Router node definition is **missing the `allowAsExportRoot` property**:
|
||||
|
||||
```typescript
|
||||
const RouterNode = {
|
||||
name: 'Router',
|
||||
displayNodeName: 'Page Router'
|
||||
// ❌ Missing: allowAsExportRoot: true
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
This causes `ProjectModel.setRootComponent()` to fail silently:
|
||||
|
||||
```typescript
|
||||
// packages/noodl-editor/src/editor/src/models/projectmodel.ts:233
|
||||
setRootComponent(component: ComponentModel) {
|
||||
const root = _.find(component.graph.roots, function (n) {
|
||||
return n.type.allowAsExportRoot; // ❌ Returns undefined for Router!
|
||||
});
|
||||
if (root) this.setRootNode(root); // ❌ Never executes!
|
||||
}
|
||||
```
|
||||
|
||||
**Impact**: This single missing property causes bugs #2 and #3.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Detailed Analysis
|
||||
|
||||
See individual bug files for complete analysis:
|
||||
|
||||
- **[BUG-001-home-component-type.md](./BUG-001-home-component-type.md)** - Home shown as component not page
|
||||
- **[BUG-002-app-not-home.md](./BUG-002-app-not-home.md)** - App component not set as Home
|
||||
- **[BUG-003-make-home-silent-fail.md](./BUG-003-make-home-silent-fail.md)** - "Make Home" does nothing
|
||||
- **[BUG-004-create-page-modal-styling.md](./BUG-004-create-page-modal-styling.md)** - Modal alignment issue
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Proposed Solutions
|
||||
|
||||
See **[SOLUTIONS.md](./SOLUTIONS.md)** for detailed fixes.
|
||||
|
||||
### Quick Summary
|
||||
|
||||
| Bug | Solution | Complexity | Files Affected |
|
||||
| ------- | --------------------------------------- | ------------ | -------------- |
|
||||
| #1 | Improve UI display logic | Low | 1 file |
|
||||
| #2 & #3 | Add `allowAsExportRoot: true` to Router | **Critical** | 1 file |
|
||||
| #4 | Fix CSS positioning | Low | 1 file |
|
||||
|
||||
---
|
||||
|
||||
## 📂 Related Files
|
||||
|
||||
### Core Files
|
||||
|
||||
- `packages/noodl-viewer-react/src/nodes/navigation/router.tsx` - Router node (NEEDS FIX)
|
||||
- `packages/noodl-editor/src/editor/src/models/projectmodel.ts` - Root component logic
|
||||
- `packages/noodl-editor/src/editor/src/models/template/templates/hello-world.template.ts` - Template definition
|
||||
|
||||
### UI Files
|
||||
|
||||
- `packages/noodl-editor/src/editor/src/views/panels/ComponentsPanelNew/hooks/useComponentActions.ts` - "Make Home" handler
|
||||
- `packages/noodl-editor/src/editor/src/views/panels/propertyeditor/Pages/Pages.tsx` - Create page modal
|
||||
- `packages/noodl-editor/src/editor/src/styles/propertyeditor/pages.css` - Modal styling
|
||||
|
||||
---
|
||||
|
||||
## ✅ Next Steps
|
||||
|
||||
1. **CRITICAL**: Add `allowAsExportRoot: true` to Router node
|
||||
2. Test project creation flow end-to-end
|
||||
3. Fix remaining UI issues (bugs #1 and #4)
|
||||
4. Add regression tests
|
||||
|
||||
---
|
||||
|
||||
_Created: January 12, 2026_
|
||||
@@ -0,0 +1,191 @@
|
||||
# Solutions: Project Creation Bugs
|
||||
|
||||
**Date**: January 12, 2026
|
||||
**Status**: Ready for Implementation
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Priority Order
|
||||
|
||||
1. **CRITICAL** - BUG-002 & BUG-003 (Same fix)
|
||||
2. **Medium** - BUG-001 (UI improvement)
|
||||
3. **Low** - BUG-004 (CSS fix)
|
||||
|
||||
---
|
||||
|
||||
## 🔴 CRITICAL FIX: Add `allowAsExportRoot` to Router
|
||||
|
||||
**Fixes**: BUG-002 (App not Home) + BUG-003 ("Make Home" fails)
|
||||
|
||||
### File to Edit
|
||||
|
||||
`packages/noodl-viewer-react/src/nodes/navigation/router.tsx`
|
||||
|
||||
### The Fix (ONE LINE!)
|
||||
|
||||
```typescript
|
||||
const RouterNode = {
|
||||
name: 'Router',
|
||||
displayNodeName: 'Page Router',
|
||||
allowAsExportRoot: true, // ✅ ADD THIS LINE
|
||||
category: 'Visuals',
|
||||
docs: 'https://docs.noodl.net/nodes/navigation/page-router',
|
||||
useVariants: false
|
||||
// ... rest of the definition
|
||||
};
|
||||
```
|
||||
|
||||
### Why This Works
|
||||
|
||||
- `ProjectModel.setRootComponent()` searches for nodes with `allowAsExportRoot: true`
|
||||
- Router node currently doesn't have this property
|
||||
- Adding it allows Router to be set as the root of a component
|
||||
- This fixes both project creation AND "Make Home" functionality
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# 1. Apply the fix
|
||||
# 2. Restart dev server: npm run dev
|
||||
# 3. Create new project
|
||||
# 4. Preview should show "Hello World!"
|
||||
# 5. "Make Home" should work on any component
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🟡 BUG-001: Fix Home Component Display
|
||||
|
||||
**Severity**: Medium (Cosmetic)
|
||||
|
||||
### Investigation Needed
|
||||
|
||||
The template correctly creates `'/#__page__/Home'` with the page prefix.
|
||||
|
||||
**Check**: `useComponentsPanel.ts` line where it builds tree data.
|
||||
|
||||
### Potential Fix
|
||||
|
||||
Ensure `isPage` flag is properly set:
|
||||
|
||||
```typescript
|
||||
// In tree data building logic
|
||||
const isPage = component.name.startsWith('/#__page__/');
|
||||
|
||||
return {
|
||||
// ...
|
||||
isPage: isPage, // ✅ Ensure this is set
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
### Alternative
|
||||
|
||||
Check `ComponentItem.tsx` icon logic:
|
||||
|
||||
```typescript
|
||||
let icon = IconName.Component;
|
||||
if (component.isRoot) {
|
||||
icon = IconName.Home;
|
||||
} else if (component.isPage) {
|
||||
// ← Must be true for pages
|
||||
icon = IconName.PageRouter;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🟡 BUG-004: Fix Modal Styling
|
||||
|
||||
**Severity**: Low (Cosmetic)
|
||||
|
||||
### File to Edit
|
||||
|
||||
`packages/noodl-editor/src/editor/src/styles/propertyeditor/pages.css`
|
||||
|
||||
### Investigation Steps
|
||||
|
||||
1. Inspect the popup when it appears
|
||||
2. Check CSS classes on triangle and rectangle
|
||||
3. Look for positioning offsets
|
||||
|
||||
### Likely Fix
|
||||
|
||||
Adjust vertical alignment:
|
||||
|
||||
```css
|
||||
.popup-layer-content {
|
||||
margin-top: 0 !important;
|
||||
/* or adjust to match triangle position */
|
||||
}
|
||||
|
||||
.popup-layer-arrow {
|
||||
/* Ensure positioned correctly relative to content */
|
||||
}
|
||||
```
|
||||
|
||||
### Long-term Solution
|
||||
|
||||
Migrate from legacy PopupLayer to modern `PopupMenu` from `@noodl-core-ui`.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Implementation Checklist
|
||||
|
||||
### Phase 1: Critical Fix (30 minutes)
|
||||
|
||||
- [ ] Add `allowAsExportRoot: true` to Router node
|
||||
- [ ] Test new project creation
|
||||
- [ ] Test "Make Home" functionality
|
||||
- [ ] Verify preview works
|
||||
|
||||
### Phase 2: UI Improvements (1-2 hours)
|
||||
|
||||
- [ ] Debug BUG-001 (page icon not showing)
|
||||
- [ ] Fix if needed
|
||||
- [ ] Debug BUG-004 (modal alignment)
|
||||
- [ ] Fix CSS positioning
|
||||
|
||||
### Phase 3: Documentation (30 minutes)
|
||||
|
||||
- [ ] Update LEARNINGS.md with findings
|
||||
- [ ] Document `allowAsExportRoot` requirement
|
||||
- [ ] Add regression test notes
|
||||
|
||||
---
|
||||
|
||||
## 📝 Regression Test Plan
|
||||
|
||||
After fixes, test:
|
||||
|
||||
1. **New Project Flow**
|
||||
|
||||
- Create project from launcher
|
||||
- App should be Home automatically
|
||||
- Preview shows "Hello World!"
|
||||
|
||||
2. **Make Home Feature**
|
||||
|
||||
- Create second component
|
||||
- Right-click → "Make Home"
|
||||
- Should work without errors
|
||||
|
||||
3. **Page Router**
|
||||
- App has Router as root
|
||||
- Can add pages to Router
|
||||
- Modal styling looks correct
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Expected Results
|
||||
|
||||
| Bug | Before | After |
|
||||
| --- | ------------------------- | ------------------------- |
|
||||
| #1 | Home shows component icon | Home shows page icon |
|
||||
| #2 | Preview error | Preview works immediately |
|
||||
| #3 | "Make Home" does nothing | "Make Home" works |
|
||||
| #4 | Modal misaligned | Modal looks professional |
|
||||
|
||||
---
|
||||
|
||||
_Ready for implementation!_
|
||||
@@ -0,0 +1,214 @@
|
||||
# STYLE-001 MVP Implementation - CHANGELOG
|
||||
|
||||
**Date**: 2026-01-12
|
||||
**Phase**: STYLE-001-MVP (Minimal Viable Product)
|
||||
**Status**: ✅ Complete - Ready for Testing
|
||||
|
||||
---
|
||||
|
||||
## 📦 What Was Implemented
|
||||
|
||||
This MVP provides the **foundation** for the Style Tokens system. It includes:
|
||||
|
||||
1. **Default Tokens** - 10 essential design tokens
|
||||
2. **Storage System** - Tokens saved in project metadata
|
||||
3. **CSS Injection** - Tokens automatically injected into preview
|
||||
4. **Real-time Updates** - Changes reflected immediately
|
||||
|
||||
### What's NOT in this MVP
|
||||
|
||||
- ❌ UI Panel to edit tokens
|
||||
- ❌ Token Picker component
|
||||
- ❌ Import/Export functionality
|
||||
- ❌ All token categories (only essentials)
|
||||
|
||||
These will come in future phases.
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Files Created
|
||||
|
||||
### Editor Side (Token Management)
|
||||
|
||||
```
|
||||
packages/noodl-editor/src/editor/src/models/StyleTokens/
|
||||
├── DefaultTokens.ts ✅ NEW - 10 default tokens
|
||||
├── StyleTokensModel.ts ✅ NEW - Token management model
|
||||
└── index.ts ✅ NEW - Exports
|
||||
```
|
||||
|
||||
**Purpose**: Manage tokens in the editor, save to project metadata.
|
||||
|
||||
### Viewer Side (CSS Injection)
|
||||
|
||||
```
|
||||
packages/noodl-viewer-react/src/
|
||||
├── style-tokens-injector.ts ✅ NEW - Injects CSS into preview
|
||||
└── viewer.jsx ✅ MODIFIED - Initialize injector
|
||||
```
|
||||
|
||||
**Purpose**: Load tokens from project and inject as CSS custom properties.
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Default Tokens Included
|
||||
|
||||
| Token | Value | Category | Usage |
|
||||
| -------------- | ---------------------- | -------- | ---------------------- |
|
||||
| `--primary` | `#3b82f6` (Blue) | color | Primary buttons, links |
|
||||
| `--background` | `#ffffff` (White) | color | Page background |
|
||||
| `--foreground` | `#0f172a` (Near black) | color | Text color |
|
||||
| `--border` | `#e2e8f0` (Light gray) | color | Borders |
|
||||
| `--space-sm` | `8px` | spacing | Small padding/margins |
|
||||
| `--space-md` | `16px` | spacing | Medium padding/margins |
|
||||
| `--space-lg` | `24px` | spacing | Large padding/margins |
|
||||
| `--radius-md` | `8px` | border | Border radius |
|
||||
| `--shadow-sm` | `0 1px 2px ...` | shadow | Small shadow |
|
||||
| `--shadow-md` | `0 4px 6px ...` | shadow | Medium shadow |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Technical Implementation
|
||||
|
||||
### 1. Data Flow
|
||||
|
||||
```
|
||||
ProjectModel
|
||||
↓ (stores metadata)
|
||||
StyleTokensModel.ts
|
||||
↓ (loads/saves tokens)
|
||||
StyleTokensInjector.ts
|
||||
↓ (generates CSS)
|
||||
<style> in DOM
|
||||
↓ (CSS variables available)
|
||||
Visual Elements
|
||||
```
|
||||
|
||||
### 2. Storage Format
|
||||
|
||||
Tokens are stored in project metadata:
|
||||
|
||||
```json
|
||||
{
|
||||
"styleTokens": {
|
||||
"--primary": "#3b82f6",
|
||||
"--background": "#ffffff",
|
||||
"--foreground": "#0f172a"
|
||||
// ... more tokens
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. CSS Injection
|
||||
|
||||
Tokens are injected as CSS custom properties:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--primary: #3b82f6;
|
||||
--background: #ffffff;
|
||||
--foreground: #0f172a;
|
||||
--border: #e2e8f0;
|
||||
--space-sm: 8px;
|
||||
--space-md: 16px;
|
||||
--space-lg: 24px;
|
||||
--radius-md: 8px;
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ How to Test
|
||||
|
||||
See [TESTING-GUIDE.md](./TESTING-GUIDE.md) for detailed testing instructions.
|
||||
|
||||
### Quick Test
|
||||
|
||||
1. **Start the editor**: `npm run dev`
|
||||
2. **Create a new project**
|
||||
3. **Add a visual node** (e.g., Group, Text)
|
||||
4. **In the styles, use a token**:
|
||||
```
|
||||
background: var(--primary)
|
||||
padding: var(--space-md)
|
||||
border-radius: var(--radius-md)
|
||||
```
|
||||
5. **Preview should show**: Blue background, 16px padding, 8px rounded corners
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Integration Points
|
||||
|
||||
### For Future Development
|
||||
|
||||
**STYLE-002 (Element Configs)** will need:
|
||||
|
||||
- Access to `StyleTokensModel` to read available tokens
|
||||
- Token references in variant definitions
|
||||
|
||||
**STYLE-003 (Presets)** will need:
|
||||
|
||||
- `StyleTokensModel.setToken()` to apply preset values
|
||||
- Bulk token updates
|
||||
|
||||
**STYLE-004 (Property Panel)** will need:
|
||||
|
||||
- Token picker UI component
|
||||
- Visual preview of token values
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Known Limitations
|
||||
|
||||
1. **No UI to edit tokens** - Must be done via browser DevTools console for now:
|
||||
|
||||
```javascript
|
||||
// In browser console:
|
||||
window.ProjectModel.instance.setMetaData('styleTokens', {
|
||||
'--primary': '#ff0000', // Red instead of blue
|
||||
'--space-md': '20px' // 20px instead of 16px
|
||||
});
|
||||
```
|
||||
|
||||
2. **Limited token set** - Only 10 tokens for MVP. More categories coming in STYLE-001 full version.
|
||||
|
||||
3. **No validation** - Token values are not validated. Invalid CSS will fail silently.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Metrics
|
||||
|
||||
- **Files Created**: 4
|
||||
- **Files Modified**: 1
|
||||
- **Lines of Code**: ~400
|
||||
- **Default Tokens**: 10
|
||||
- **Time to Implement**: ~4-6 hours
|
||||
- **Test Coverage**: Manual testing required
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
- [x] Tokens are defined with sensible defaults
|
||||
- [x] Tokens are stored in project metadata
|
||||
- [x] Tokens are injected as CSS variables
|
||||
- [x] Tokens can be used in element styles
|
||||
- [x] Changes persist across editor restarts
|
||||
- [x] TypeScript types are properly defined
|
||||
- [x] No eslint errors in new code
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Steps
|
||||
|
||||
1. **Test the MVP** - Follow TESTING-GUIDE.md
|
||||
2. **Gather feedback** - Does it work as expected?
|
||||
3. **Plan STYLE-001 Full** - UI panel, more tokens, token picker
|
||||
4. **Continue to STYLE-002** - Element configs and variants
|
||||
|
||||
---
|
||||
|
||||
_Created: 2026-01-12_
|
||||
_Last Updated: 2026-01-12_
|
||||
@@ -0,0 +1,299 @@
|
||||
# STYLE-001 MVP - Testing Guide
|
||||
|
||||
**Version**: MVP
|
||||
**Date**: 2026-01-12
|
||||
**Estimated Time**: 15-20 minutes
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Testing Objectives
|
||||
|
||||
Verify that:
|
||||
|
||||
1. ✅ Default tokens are injected into the preview
|
||||
2. ✅ Tokens can be used in element styles
|
||||
3. ✅ Custom token values can be set and persist
|
||||
4. ✅ Tokens update in real-time when changed
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Prerequisites
|
||||
|
||||
- OpenNoodl editor running (`npm run dev` from project root)
|
||||
- Browser with DevTools (Chrome/Firefox/Edge recommended)
|
||||
- Basic knowledge of CSS custom properties
|
||||
|
||||
---
|
||||
|
||||
## 📋 Test Cases
|
||||
|
||||
### Test 1: Verify Default Tokens Are Injected
|
||||
|
||||
**Goal**: Confirm that default tokens are available in the preview.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Start the editor: `npm run dev`
|
||||
2. Create a new project (or open an existing one)
|
||||
3. Wait for the preview to load
|
||||
4. Open browser DevTools (F12 or Right-click → Inspect)
|
||||
5. Go to the **Elements** tab
|
||||
6. Look for a `<style>` tag with `id="noodl-style-tokens"` in the `<head>`
|
||||
|
||||
**Expected Result:**
|
||||
|
||||
```html
|
||||
<style id="noodl-style-tokens">
|
||||
:root {
|
||||
--primary: #3b82f6;
|
||||
--background: #ffffff;
|
||||
--foreground: #0f172a;
|
||||
--border: #e2e8f0;
|
||||
--space-sm: 8px;
|
||||
--space-md: 16px;
|
||||
--space-lg: 24px;
|
||||
--radius-md: 8px;
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
**✅ Pass Criteria**: The style tag exists with all 10 default tokens.
|
||||
|
||||
---
|
||||
|
||||
### Test 2: Use Tokens in Element Styles
|
||||
|
||||
**Goal**: Verify tokens can be used in visual elements.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. In the node graph, add a **Group** node
|
||||
2. In the property panel, go to **Style** section
|
||||
3. Add custom styles:
|
||||
```css
|
||||
background: var(--primary);
|
||||
padding: var(--space-md);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-md);
|
||||
```
|
||||
4. Make the Group visible by setting width/height (e.g., 200px x 200px)
|
||||
|
||||
**Expected Result:**
|
||||
|
||||
- Background color: Blue (#3b82f6)
|
||||
- Padding: 16px on all sides
|
||||
- Border radius: 8px rounded corners
|
||||
- Box shadow: Medium shadow visible
|
||||
|
||||
**✅ Pass Criteria**: All token values are applied correctly.
|
||||
|
||||
---
|
||||
|
||||
### Test 3: Verify Token Persistence
|
||||
|
||||
**Goal**: Confirm tokens persist across editor restarts.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. With a project open, open DevTools Console
|
||||
2. Set a custom token value:
|
||||
```javascript
|
||||
ProjectModel.instance.setMetaData('styleTokens', {
|
||||
'--primary': '#ff0000'
|
||||
});
|
||||
```
|
||||
3. Reload the preview (Cmd+R / Ctrl+R)
|
||||
4. Check if the element from Test 2 now has a red background
|
||||
|
||||
**Expected Result:**
|
||||
|
||||
- Background changes from blue to red
|
||||
- Other tokens remain unchanged (still using defaults)
|
||||
|
||||
**✅ Pass Criteria**: Custom token value persists after reload.
|
||||
|
||||
---
|
||||
|
||||
### Test 4: Real-Time Token Updates
|
||||
|
||||
**Goal**: Verify tokens update without page reload.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. With the preview showing an element styled with tokens
|
||||
2. Open DevTools Console
|
||||
3. Change a token value:
|
||||
```javascript
|
||||
ProjectModel.instance.setMetaData('styleTokens', {
|
||||
'--primary': '#00ff00',
|
||||
'--space-md': '32px'
|
||||
});
|
||||
```
|
||||
4. Observe the preview **without reloading**
|
||||
|
||||
**Expected Result:**
|
||||
|
||||
- Background changes from previous color to green
|
||||
- Padding increases from 16px to 32px
|
||||
- Changes happen immediately
|
||||
|
||||
**✅ Pass Criteria**: Changes reflected in real-time.
|
||||
|
||||
---
|
||||
|
||||
### Test 5: Multiple Element Usage
|
||||
|
||||
**Goal**: Confirm tokens work across multiple elements.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Add multiple visual elements:
|
||||
- Group 1: `background: var(--primary)`
|
||||
- Group 2: `background: var(--primary)`
|
||||
- Text: `color: var(--foreground)`
|
||||
2. Set a custom `--primary` value:
|
||||
```javascript
|
||||
ProjectModel.instance.setMetaData('styleTokens', {
|
||||
'--primary': '#9333ea' // Purple
|
||||
});
|
||||
```
|
||||
|
||||
**Expected Result:**
|
||||
|
||||
- Both Group 1 and Group 2 change to purple simultaneously
|
||||
- Text color remains the foreground color
|
||||
|
||||
**✅ Pass Criteria**: Token change applies to all elements using it.
|
||||
|
||||
---
|
||||
|
||||
### Test 6: Invalid Token Handling
|
||||
|
||||
**Goal**: Verify system handles invalid token values gracefully.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Add an element with `background: var(--nonexistent-token)`
|
||||
2. Observe what happens
|
||||
|
||||
**Expected Result:**
|
||||
|
||||
- No error in console
|
||||
- Element uses browser default (likely transparent/white)
|
||||
- Preview doesn't crash
|
||||
|
||||
**✅ Pass Criteria**: System degrades gracefully.
|
||||
|
||||
---
|
||||
|
||||
### Test 7: Token in Different CSS Properties
|
||||
|
||||
**Goal**: Confirm tokens work in various CSS properties.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Create an element with multiple token usages:
|
||||
```css
|
||||
background: var(--primary);
|
||||
color: var(--foreground);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
margin: var(--space-lg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
```
|
||||
2. Inspect the computed styles in DevTools
|
||||
|
||||
**Expected Result:**
|
||||
|
||||
All properties resolve to their token values:
|
||||
|
||||
- `background: rgb(59, 130, 246)` (--primary)
|
||||
- `padding: 8px 16px` (--space-sm --space-md)
|
||||
- etc.
|
||||
|
||||
**✅ Pass Criteria**: All tokens resolve correctly.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Issue: Tokens not showing in preview
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Check browser console for errors
|
||||
2. Verify the style element exists: `document.getElementById('noodl-style-tokens')`
|
||||
3. Check if StyleTokensInjector was initialized:
|
||||
```javascript
|
||||
// In DevTools console
|
||||
console.log('Injector exists:', !!window._styleTokensInjector);
|
||||
```
|
||||
|
||||
### Issue: Changes don't persist
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Ensure you're using `ProjectModel.instance.setMetaData()`
|
||||
2. Check if metadata is saved:
|
||||
```javascript
|
||||
console.log(ProjectModel.instance.getMetaData('styleTokens'));
|
||||
```
|
||||
3. Save the project explicitly if needed
|
||||
|
||||
### Issue: Tokens not updating in real-time
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Check if the 'metadataChanged' event is firing
|
||||
2. Verify StyleTokensInjector is listening to events
|
||||
3. Try a hard refresh (Cmd+Shift+R / Ctrl+Shift+F5)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Test Results Template
|
||||
|
||||
```
|
||||
STYLE-001 MVP Testing - Results
|
||||
================================
|
||||
Date: ___________
|
||||
Tester: ___________
|
||||
|
||||
Test 1: Default Tokens Injected [ ] PASS [ ] FAIL
|
||||
Test 2: Use Tokens in Styles [ ] PASS [ ] FAIL
|
||||
Test 3: Token Persistence [ ] PASS [ ] FAIL
|
||||
Test 4: Real-Time Updates [ ] PASS [ ] FAIL
|
||||
Test 5: Multiple Element Usage [ ] PASS [ ] FAIL
|
||||
Test 6: Invalid Token Handling [ ] PASS [ ] FAIL
|
||||
Test 7: Various CSS Properties [ ] PASS [ ] FAIL
|
||||
|
||||
Notes:
|
||||
_____________________________________________________
|
||||
_____________________________________________________
|
||||
_____________________________________________________
|
||||
|
||||
Overall Status: [ ] PASS [ ] FAIL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Steps After Testing
|
||||
|
||||
**If all tests pass:**
|
||||
|
||||
- ✅ MVP is ready for use
|
||||
- Document any findings
|
||||
- Plan STYLE-001 full version (UI panel)
|
||||
|
||||
**If tests fail:**
|
||||
|
||||
- Document which tests failed
|
||||
- Note error messages
|
||||
- Create bug reports
|
||||
- Fix issues before continuing
|
||||
|
||||
---
|
||||
|
||||
_Last Updated: 2026-01-12_
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Default Style Tokens (Minimal Set for MVP)
|
||||
*
|
||||
* This file defines the minimal set of CSS custom properties (design tokens)
|
||||
* that will be available in every Noodl project.
|
||||
*
|
||||
* These tokens can be used in any CSS property that accepts the relevant value type.
|
||||
* Example: style="background: var(--primary); padding: var(--space-md);"
|
||||
*
|
||||
* @module StyleTokens
|
||||
*/
|
||||
|
||||
export interface StyleToken {
|
||||
name: string;
|
||||
value: string;
|
||||
category: TokenCategory;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export type TokenCategory = 'color' | 'spacing' | 'border' | 'shadow';
|
||||
|
||||
/**
|
||||
* Minimal set of design tokens for MVP
|
||||
* Following modern design system conventions (similar to Tailwind/shadcn)
|
||||
*/
|
||||
export const DEFAULT_TOKENS: Record<string, StyleToken> = {
|
||||
// ===== COLORS =====
|
||||
'--primary': {
|
||||
name: '--primary',
|
||||
value: '#3b82f6', // Blue
|
||||
category: 'color',
|
||||
description: 'Primary brand color for main actions and highlights'
|
||||
},
|
||||
|
||||
'--background': {
|
||||
name: '--background',
|
||||
value: '#ffffff',
|
||||
category: 'color',
|
||||
description: 'Main background color'
|
||||
},
|
||||
|
||||
'--foreground': {
|
||||
name: '--foreground',
|
||||
value: '#0f172a', // Near black
|
||||
category: 'color',
|
||||
description: 'Main text color'
|
||||
},
|
||||
|
||||
'--border': {
|
||||
name: '--border',
|
||||
value: '#e2e8f0', // Light gray
|
||||
category: 'color',
|
||||
description: 'Default border color'
|
||||
},
|
||||
|
||||
// ===== SPACING =====
|
||||
'--space-sm': {
|
||||
name: '--space-sm',
|
||||
value: '8px',
|
||||
category: 'spacing',
|
||||
description: 'Small spacing (padding, margin, gap)'
|
||||
},
|
||||
|
||||
'--space-md': {
|
||||
name: '--space-md',
|
||||
value: '16px',
|
||||
category: 'spacing',
|
||||
description: 'Medium spacing (padding, margin, gap)'
|
||||
},
|
||||
|
||||
'--space-lg': {
|
||||
name: '--space-lg',
|
||||
value: '24px',
|
||||
category: 'spacing',
|
||||
description: 'Large spacing (padding, margin, gap)'
|
||||
},
|
||||
|
||||
// ===== BORDERS =====
|
||||
'--radius-md': {
|
||||
name: '--radius-md',
|
||||
value: '8px',
|
||||
category: 'border',
|
||||
description: 'Medium border radius for rounded corners'
|
||||
},
|
||||
|
||||
// ===== SHADOWS =====
|
||||
'--shadow-sm': {
|
||||
name: '--shadow-sm',
|
||||
value: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
|
||||
category: 'shadow',
|
||||
description: 'Small shadow for subtle elevation'
|
||||
},
|
||||
|
||||
'--shadow-md': {
|
||||
name: '--shadow-md',
|
||||
value: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
||||
category: 'shadow',
|
||||
description: 'Medium shadow for moderate elevation'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all default tokens as a simple key-value map
|
||||
* Useful for CSS injection
|
||||
*/
|
||||
export function getDefaultTokenValues(): Record<string, string> {
|
||||
const values: Record<string, string> = {};
|
||||
for (const [key, token] of Object.entries(DEFAULT_TOKENS)) {
|
||||
values[key] = token.value;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tokens by category
|
||||
*/
|
||||
export function getTokensByCategory(category: TokenCategory): StyleToken[] {
|
||||
return Object.values(DEFAULT_TOKENS).filter((token) => token.category === category);
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Style Tokens Model
|
||||
*
|
||||
* Manages CSS custom properties (design tokens) for a Noodl project.
|
||||
* Tokens are stored in project metadata and can be customized per project.
|
||||
*
|
||||
* @module StyleTokens
|
||||
*/
|
||||
|
||||
import Model from '../../../../shared/model';
|
||||
import { EventDispatcher } from '../../../../shared/utils/EventDispatcher';
|
||||
import { ProjectModel } from '../projectmodel';
|
||||
import { getDefaultTokenValues, DEFAULT_TOKENS, StyleToken, TokenCategory } from './DefaultTokens';
|
||||
|
||||
export class StyleTokensModel extends Model {
|
||||
/** Custom token values (overrides defaults) */
|
||||
private customTokens: Record<string, string>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.customTokens = {};
|
||||
this.loadFromProject();
|
||||
this.bindListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to project events to stay in sync
|
||||
*/
|
||||
private bindListeners() {
|
||||
const onProjectChanged = () => {
|
||||
this.loadFromProject();
|
||||
this.notifyListeners('tokensChanged');
|
||||
};
|
||||
|
||||
EventDispatcher.instance.on(
|
||||
['ProjectModel.importComplete', 'ProjectModel.instanceHasChanged'],
|
||||
() => {
|
||||
if (ProjectModel.instance) {
|
||||
onProjectChanged();
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
EventDispatcher.instance.on(
|
||||
'ProjectModel.metadataChanged',
|
||||
({ key }) => {
|
||||
if (key === 'styleTokens') {
|
||||
onProjectChanged();
|
||||
}
|
||||
},
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbind listeners
|
||||
*/
|
||||
private unbindListeners() {
|
||||
EventDispatcher.instance.off(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load tokens from current project
|
||||
*/
|
||||
private loadFromProject() {
|
||||
if (ProjectModel.instance) {
|
||||
this.customTokens = ProjectModel.instance.getMetaData('styleTokens') || {};
|
||||
} else {
|
||||
this.customTokens = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save tokens to current project
|
||||
*/
|
||||
private saveToProject() {
|
||||
if (ProjectModel.instance) {
|
||||
this.unbindListeners();
|
||||
ProjectModel.instance.setMetaData('styleTokens', this.customTokens);
|
||||
this.bindListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tokens (defaults + custom overrides)
|
||||
*/
|
||||
getAllTokens(): Record<string, string> {
|
||||
const defaults = getDefaultTokenValues();
|
||||
return {
|
||||
...defaults,
|
||||
...this.customTokens
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific token value
|
||||
* @param name Token name (e.g., '--primary')
|
||||
* @returns Token value or undefined if not found
|
||||
*/
|
||||
getToken(name: string): string | undefined {
|
||||
// Check custom tokens first
|
||||
if (this.customTokens[name] !== undefined) {
|
||||
return this.customTokens[name];
|
||||
}
|
||||
|
||||
// Fall back to default
|
||||
const defaultToken = DEFAULT_TOKENS[name];
|
||||
return defaultToken?.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a custom token value
|
||||
* @param name Token name (e.g., '--primary')
|
||||
* @param value Token value (e.g., '#ff0000')
|
||||
*/
|
||||
setToken(name: string, value: string) {
|
||||
// Validate token name starts with --
|
||||
if (!name.startsWith('--')) {
|
||||
console.warn(`Token name must start with -- : ${name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.customTokens[name] = value;
|
||||
this.saveToProject();
|
||||
this.notifyListeners('tokensChanged');
|
||||
this.notifyListeners('tokenChanged', { name, value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset a token to its default value
|
||||
* @param name Token name (e.g., '--primary')
|
||||
*/
|
||||
resetToken(name: string) {
|
||||
if (this.customTokens[name] !== undefined) {
|
||||
delete this.customTokens[name];
|
||||
this.saveToProject();
|
||||
this.notifyListeners('tokensChanged');
|
||||
this.notifyListeners('tokenReset', { name });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all tokens to defaults
|
||||
*/
|
||||
resetAllTokens() {
|
||||
this.customTokens = {};
|
||||
this.saveToProject();
|
||||
this.notifyListeners('tokensChanged');
|
||||
this.notifyListeners('allTokensReset');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a token has been customized
|
||||
*/
|
||||
isTokenCustomized(name: string): boolean {
|
||||
return this.customTokens[name] !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token metadata
|
||||
*/
|
||||
getTokenInfo(name: string): StyleToken | undefined {
|
||||
return DEFAULT_TOKENS[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tokens by category
|
||||
*/
|
||||
getTokensByCategory(category: TokenCategory): Record<string, string> {
|
||||
const tokens: Record<string, string> = {};
|
||||
|
||||
for (const [name, tokenInfo] of Object.entries(DEFAULT_TOKENS)) {
|
||||
if (tokenInfo.category === category) {
|
||||
tokens[name] = this.getToken(name) || tokenInfo.value;
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CSS string for injection
|
||||
* @returns CSS custom properties as a string
|
||||
*/
|
||||
generateCSS(): string {
|
||||
const allTokens = this.getAllTokens();
|
||||
const entries = Object.entries(allTokens);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const declarations = entries.map(([name, value]) => ` ${name}: ${value};`).join('\n');
|
||||
|
||||
return `:root {\n${declarations}\n}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
dispose() {
|
||||
this.unbindListeners();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton instance
|
||||
*/
|
||||
export const StyleTokens = new StyleTokensModel();
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Style Tokens System
|
||||
*
|
||||
* Exports for the Style Tokens system
|
||||
*
|
||||
* @module StyleTokens
|
||||
*/
|
||||
|
||||
export { StyleTokensModel, StyleTokens } from './StyleTokensModel';
|
||||
export { DEFAULT_TOKENS, getDefaultTokenValues, getTokensByCategory } from './DefaultTokens';
|
||||
export type { StyleToken, TokenCategory } from './DefaultTokens';
|
||||
@@ -169,6 +169,14 @@ export class ProjectModel extends Model {
|
||||
|
||||
if (json.rootNodeId) _this.rootNode = _this.findNodeWithId(json.rootNodeId);
|
||||
|
||||
// Handle rootComponent from templates (name of component instead of node ID)
|
||||
if (json.rootComponent && !_this.rootNode) {
|
||||
const rootComponent = _this.getComponentWithName(json.rootComponent);
|
||||
if (rootComponent) {
|
||||
_this.setRootComponent(rootComponent);
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade project if necessary
|
||||
ProjectModel.upgrade(_this);
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ export interface ProjectContent {
|
||||
/** Project name (will be overridden by user input) */
|
||||
name: string;
|
||||
|
||||
/** Name of the root component that serves as the entry point */
|
||||
rootComponent?: string;
|
||||
|
||||
/** Array of component definitions */
|
||||
components: ComponentDefinition[];
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ export const helloWorldTemplate: ProjectTemplate = {
|
||||
|
||||
content: {
|
||||
name: 'Hello World Project',
|
||||
rootComponent: 'App',
|
||||
components: [
|
||||
// App component (root)
|
||||
{
|
||||
|
||||
@@ -59,6 +59,7 @@ const RouterNode = {
|
||||
displayNodeName: 'Page Router',
|
||||
category: 'Visuals',
|
||||
docs: 'https://docs.noodl.net/nodes/navigation/page-router',
|
||||
allowAsExportRoot: true,
|
||||
useVariants: false,
|
||||
connectionPanel: {
|
||||
groupPriority: ['General', 'Actions', 'Events', 'Mounted']
|
||||
|
||||
163
packages/noodl-viewer-react/src/style-tokens-injector.ts
Normal file
163
packages/noodl-viewer-react/src/style-tokens-injector.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Style Tokens Injector
|
||||
*
|
||||
* Injects CSS custom properties (design tokens) into the DOM
|
||||
* for use in Noodl projects.
|
||||
*
|
||||
* This class is responsible for:
|
||||
* - Injecting default tokens into the page
|
||||
* - Updating tokens when project settings change
|
||||
* - Cleaning up on unmount
|
||||
*/
|
||||
|
||||
interface GraphModel {
|
||||
getMetaData(): Record<string, unknown> | undefined;
|
||||
on(event: string, handler: (data: unknown) => void): void;
|
||||
off(event: string): void;
|
||||
}
|
||||
|
||||
interface StyleTokensInjectorOptions {
|
||||
graphModel: GraphModel;
|
||||
}
|
||||
|
||||
export class StyleTokensInjector {
|
||||
private styleElement: HTMLStyleElement | null = null;
|
||||
private graphModel: GraphModel;
|
||||
private tokens: Record<string, string> = {};
|
||||
|
||||
constructor(options: StyleTokensInjectorOptions) {
|
||||
this.graphModel = options.graphModel;
|
||||
|
||||
// Load tokens from project metadata
|
||||
this.loadTokens();
|
||||
|
||||
// Inject tokens into DOM
|
||||
this.injectTokens();
|
||||
|
||||
// Listen for project changes
|
||||
this.bindListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load tokens from project metadata
|
||||
*/
|
||||
private loadTokens() {
|
||||
try {
|
||||
const metadata = this.graphModel.getMetaData();
|
||||
const styleTokens = metadata?.styleTokens;
|
||||
|
||||
// Validate that styleTokens is a proper object
|
||||
if (styleTokens && typeof styleTokens === 'object' && !Array.isArray(styleTokens)) {
|
||||
this.tokens = styleTokens as Record<string, string>;
|
||||
} else {
|
||||
this.tokens = this.getDefaultTokens();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load style tokens, using defaults:', error);
|
||||
this.tokens = this.getDefaultTokens();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default tokens (fallback if project doesn't have custom tokens)
|
||||
*/
|
||||
private getDefaultTokens(): Record<string, string> {
|
||||
return {
|
||||
// Colors
|
||||
'--primary': '#3b82f6',
|
||||
'--background': '#ffffff',
|
||||
'--foreground': '#0f172a',
|
||||
'--border': '#e2e8f0',
|
||||
// Spacing
|
||||
'--space-sm': '8px',
|
||||
'--space-md': '16px',
|
||||
'--space-lg': '24px',
|
||||
// Borders
|
||||
'--radius-md': '8px',
|
||||
// Shadows
|
||||
'--shadow-sm': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
|
||||
'--shadow-md': '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject tokens as CSS custom properties
|
||||
*/
|
||||
private injectTokens() {
|
||||
// Support SSR
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
// Remove existing style element if any
|
||||
this.removeStyleElement();
|
||||
|
||||
// Create new style element
|
||||
this.styleElement = document.createElement('style');
|
||||
this.styleElement.id = 'noodl-style-tokens';
|
||||
this.styleElement.textContent = this.generateCSS();
|
||||
|
||||
// Inject into head
|
||||
document.head.appendChild(this.styleElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CSS string from tokens
|
||||
*/
|
||||
private generateCSS(): string {
|
||||
const entries = Object.entries(this.tokens);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const declarations = entries.map(([name, value]) => ` ${name}: ${value};`).join('\n');
|
||||
|
||||
return `:root {\n${declarations}\n}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tokens and re-inject
|
||||
*/
|
||||
updateTokens(newTokens: Record<string, string>) {
|
||||
this.tokens = { ...this.getDefaultTokens(), ...newTokens };
|
||||
this.injectTokens();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to graph model events
|
||||
*/
|
||||
private bindListeners() {
|
||||
if (!this.graphModel) return;
|
||||
|
||||
// Listen for metadata changes
|
||||
this.graphModel.on('metadataChanged', (metadata: unknown) => {
|
||||
if (metadata && typeof metadata === 'object' && 'styleTokens' in metadata) {
|
||||
const data = metadata as Record<string, unknown>;
|
||||
if (data.styleTokens && typeof data.styleTokens === 'object') {
|
||||
this.updateTokens(data.styleTokens as Record<string, string>);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove style element from DOM
|
||||
*/
|
||||
private removeStyleElement() {
|
||||
if (this.styleElement && this.styleElement.parentNode) {
|
||||
this.styleElement.parentNode.removeChild(this.styleElement);
|
||||
this.styleElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
dispose() {
|
||||
this.removeStyleElement();
|
||||
if (this.graphModel) {
|
||||
this.graphModel.off('metadataChanged');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StyleTokensInjector;
|
||||
@@ -8,6 +8,7 @@ import NoodlJSAPI from './noodl-js-api';
|
||||
import projectSettings from './project-settings';
|
||||
import { createNodeFromReactComponent } from './react-component-node';
|
||||
import registerNodes from './register-nodes';
|
||||
import { StyleTokensInjector } from './style-tokens-injector';
|
||||
import Styles from './styles';
|
||||
|
||||
if (typeof window !== 'undefined' && window.NoodlEditor) {
|
||||
@@ -189,6 +190,11 @@ export default class Viewer extends React.Component {
|
||||
//make the styles available to all nodes via `this.context.styles`
|
||||
noodlRuntime.context.styles = this.styles;
|
||||
|
||||
// Initialize style tokens injector
|
||||
this.styleTokensInjector = new StyleTokensInjector({
|
||||
graphModel: noodlRuntime.graphModel
|
||||
});
|
||||
|
||||
this.state.waitingForExport = !this.runningDeployed;
|
||||
|
||||
if (this.runningDeployed) {
|
||||
|
||||
Reference in New Issue
Block a user