diff --git a/dev-docs/tasks/phase-0-foundation-stabilisation/TASK-010-project-creation-bug-fix/CURRENT-STATUS.md b/dev-docs/tasks/phase-0-foundation-stabilisation/TASK-010-project-creation-bug-fix/CURRENT-STATUS.md new file mode 100644 index 0000000..2f96379 --- /dev/null +++ b/dev-docs/tasks/phase-0-foundation-stabilisation/TASK-010-project-creation-bug-fix/CURRENT-STATUS.md @@ -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é diff --git a/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/BUG-001-home-component-type.md b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/BUG-001-home-component-type.md new file mode 100644 index 0000000..00e1f44 --- /dev/null +++ b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/BUG-001-home-component-type.md @@ -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 diff --git a/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/BUG-002-app-not-home.md b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/BUG-002-app-not-home.md new file mode 100644 index 0000000..41ba522 --- /dev/null +++ b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/BUG-002-app-not-home.md @@ -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 diff --git a/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/BUG-003-make-home-silent-fail.md b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/BUG-003-make-home-silent-fail.md new file mode 100644 index 0000000..d0c1e3a --- /dev/null +++ b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/BUG-003-make-home-silent-fail.md @@ -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) diff --git a/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/BUG-004-create-page-modal-styling.md b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/BUG-004-create-page-modal-styling.md new file mode 100644 index 0000000..f88fbbf --- /dev/null +++ b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/BUG-004-create-page-modal-styling.md @@ -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 diff --git a/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/INVESTIGATION-project-creation-bugs.md b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/INVESTIGATION-project-creation-bugs.md new file mode 100644 index 0000000..175adf9 --- /dev/null +++ b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/INVESTIGATION-project-creation-bugs.md @@ -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_ diff --git a/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/SOLUTIONS.md b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/SOLUTIONS.md new file mode 100644 index 0000000..6a3e169 --- /dev/null +++ b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-001B-launcher-fixes/SOLUTIONS.md @@ -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!_ diff --git a/dev-docs/tasks/phase-9-styles-overhaul/STYLE-001-token-system-enhancement/CHANGELOG-MVP.md b/dev-docs/tasks/phase-9-styles-overhaul/STYLE-001-token-system-enhancement/CHANGELOG-MVP.md new file mode 100644 index 0000000..b5244c3 --- /dev/null +++ b/dev-docs/tasks/phase-9-styles-overhaul/STYLE-001-token-system-enhancement/CHANGELOG-MVP.md @@ -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) + +``` + +**✅ 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_ diff --git a/packages/noodl-editor/src/editor/src/models/StyleTokens/DefaultTokens.ts b/packages/noodl-editor/src/editor/src/models/StyleTokens/DefaultTokens.ts new file mode 100644 index 0000000..c86cfe5 --- /dev/null +++ b/packages/noodl-editor/src/editor/src/models/StyleTokens/DefaultTokens.ts @@ -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 = { + // ===== 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 { + const values: Record = {}; + 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); +} diff --git a/packages/noodl-editor/src/editor/src/models/StyleTokens/StyleTokensModel.ts b/packages/noodl-editor/src/editor/src/models/StyleTokens/StyleTokensModel.ts new file mode 100644 index 0000000..94678ad --- /dev/null +++ b/packages/noodl-editor/src/editor/src/models/StyleTokens/StyleTokensModel.ts @@ -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; + + 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 { + 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 { + const tokens: Record = {}; + + 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(); diff --git a/packages/noodl-editor/src/editor/src/models/StyleTokens/index.ts b/packages/noodl-editor/src/editor/src/models/StyleTokens/index.ts new file mode 100644 index 0000000..f402db0 --- /dev/null +++ b/packages/noodl-editor/src/editor/src/models/StyleTokens/index.ts @@ -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'; diff --git a/packages/noodl-editor/src/editor/src/models/projectmodel.ts b/packages/noodl-editor/src/editor/src/models/projectmodel.ts index 63e06db..72f5698 100644 --- a/packages/noodl-editor/src/editor/src/models/projectmodel.ts +++ b/packages/noodl-editor/src/editor/src/models/projectmodel.ts @@ -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); diff --git a/packages/noodl-editor/src/editor/src/models/template/ProjectTemplate.ts b/packages/noodl-editor/src/editor/src/models/template/ProjectTemplate.ts index f926c90..436fd76 100644 --- a/packages/noodl-editor/src/editor/src/models/template/ProjectTemplate.ts +++ b/packages/noodl-editor/src/editor/src/models/template/ProjectTemplate.ts @@ -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[]; diff --git a/packages/noodl-editor/src/editor/src/models/template/templates/hello-world.template.ts b/packages/noodl-editor/src/editor/src/models/template/templates/hello-world.template.ts index 90b10d3..b2e67bd 100644 --- a/packages/noodl-editor/src/editor/src/models/template/templates/hello-world.template.ts +++ b/packages/noodl-editor/src/editor/src/models/template/templates/hello-world.template.ts @@ -36,6 +36,7 @@ export const helloWorldTemplate: ProjectTemplate = { content: { name: 'Hello World Project', + rootComponent: 'App', components: [ // App component (root) { diff --git a/packages/noodl-viewer-react/src/nodes/navigation/router.tsx b/packages/noodl-viewer-react/src/nodes/navigation/router.tsx index 766e5ab..e5ac4ca 100644 --- a/packages/noodl-viewer-react/src/nodes/navigation/router.tsx +++ b/packages/noodl-viewer-react/src/nodes/navigation/router.tsx @@ -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'] diff --git a/packages/noodl-viewer-react/src/style-tokens-injector.ts b/packages/noodl-viewer-react/src/style-tokens-injector.ts new file mode 100644 index 0000000..892f530 --- /dev/null +++ b/packages/noodl-viewer-react/src/style-tokens-injector.ts @@ -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 | 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 = {}; + + 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; + } 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 { + 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) { + 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; + if (data.styleTokens && typeof data.styleTokens === 'object') { + this.updateTokens(data.styleTokens as Record); + } + } + }); + } + + /** + * 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; diff --git a/packages/noodl-viewer-react/src/viewer.jsx b/packages/noodl-viewer-react/src/viewer.jsx index e269d39..90d2734 100644 --- a/packages/noodl-viewer-react/src/viewer.jsx +++ b/packages/noodl-viewer-react/src/viewer.jsx @@ -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) {