84 KiB
TASK-008 Changelog
[December 28, 2025] - KNOWN BUG: Drag-Drop Completion Still Broken
Summary
🐛 UNRESOLVED BUG - Drag-drop onto items still leaves drag visual attached to cursor.
After multiple fix attempts, the component/folder drag-drop completion is still broken. When dropping a component onto another component or folder, the drag visual (label following cursor) stays attached to the cursor instead of completing.
What Works
- ✅ Root drops (dropping onto empty space in tree background)
- ✅ Drag visual appears correctly
- ✅ Drop target highlighting works
- ✅ The actual move/rename operation executes successfully
What's Broken
- ❌ After dropping onto a component or folder, drag visual stays attached to cursor
- ❌ User has to click elsewhere to "release" the phantom drag
Attempted Fixes (All Failed)
Attempt 1: State-based flow through useDragDrop
- Used
handleDropfrom useDragDrop that set state → triggered useEffect → calledhandleDropOn - Result: Same bug, drag visual persisted
Attempt 2: Direct drop handler (handleDirectDrop)
- Bypassed useDragDrop state system
- Created
handleDirectDropthat calledhandleDropOndirectly - Result: Same bug, drag visual persisted
Attempt 3: Remove duplicate dragCompleted() calls
- Removed
dragCompleted()from FolderItem and ComponentItemhandleMouseUp - Left only the call in
handleDropOnin useComponentActions - Result: Same bug, drag visual persisted
Technical Context
The drag system uses PopupLayer from @noodl-views/popuplayer:
startDragging()- begins drag with label elementisDragging()- checks if currently draggingindicateDropType()- shows cursor feedbackdragCompleted()- should end drag and hide label
Root drops work because handleTreeMouseUp calls handleDropOnRoot which calls dragCompleted() directly.
Item drops go through more complex flow that somehow doesn't properly complete.
Files Involved
ComponentsPanelReact.tsx- Main panel, hashandleDirectDropandhandleTreeMouseUpFolderItem.tsx- Folder items, has drop detection inhandleMouseUpComponentItem.tsx- Component items, has drop detection inhandleMouseUpuseComponentActions.ts- HashandleDropOnwithdragCompleted()callsuseDragDrop.ts- Original state-based drop handler (now mostly bypassed)
Status
DEFERRED - Will revisit in future session. Core functionality (sheets, menus, rename, delete, move) works. Drag-drop is a nice-to-have but not blocking.
Notes for Future Investigation
- Check if
dragCompleted()is actually being called (add console.log) - Check if multiple
dragCompleted()calls might be interfering - Investigate PopupLayer internals for what resets
dragItem - Compare with working root drop flow step-by-step
- Check if React re-render is somehow re-initializing drag state
- Consider if the module instance pattern (require vs import) matters
[December 28, 2025] - Bug Fix: Drag-Drop Regression on Empty Folders
Summary
🐛 Fixed 2 drag-drop bugs when dropping components onto newly created folders:
- Folder icon incorrectly changed to component icon after drop
- Drag state persisted - user remained in dragging state after dropping
Bug Details
Issue 1: Icon change after drop
When a component was dropped onto an empty folder (one created via placeholder), the folder's icon incorrectly changed from the folder icon to the component-with-children icon.
Root Cause: The isComponentFolder detection logic was wrong:
// WRONG - marked ANY folder with components as a component-folder
const isComponentFolder = matchingComponent !== undefined || childFolder.components.length > 0;
A "component-folder" should ONLY be when a COMPONENT has nested children (e.g., /test1 is both a component AND has /test1/child). Having children inside a folder does NOT make it a component-folder - it's just a regular folder with contents.
Fix: Changed to only check for matching component:
const isComponentFolder = matchingComponent !== undefined;
Issue 2: Stuck dragging after drop
After dropping a component onto a folder, the user remained in dragging state with the drag element following the cursor.
Root Cause: PopupLayer.instance.dragCompleted() was being called AFTER UndoQueue.instance.pushAndDo(). The rename operation triggers ProjectModel events which cause React to schedule a re-render. This timing issue could cause the drag state to persist across the tree rebuild.
Fix: Call dragCompleted() FIRST, before any rename operations:
// End drag operation FIRST - before the rename triggers a re-render
PopupLayer.instance.dragCompleted();
// THEN do the rename
UndoQueue.instance.pushAndDo(
new UndoActionGroup({
label: `Move component to folder`,
do: () => {
ProjectModel.instance?.renameComponent(component, newName);
},
undo: () => {
ProjectModel.instance?.renameComponent(component, oldName);
}
})
);
Files Modified
useComponentsPanel.ts - Fixed isComponentFolder detection:
- Changed from
matchingComponent !== undefined || childFolder.components.length > 0 - To just
matchingComponent !== undefined
useComponentActions.ts - Fixed drag completion timing for ALL drop handlers:
handleDropOn: Component → FolderhandleDropOn: Folder → FolderhandleDropOn: Component → ComponenthandleDropOn: Folder → ComponenthandleDropOnRoot: Component → RoothandleDropOnRoot: Folder → Root
Key Learning: React Re-renders and Drag State
When performing drag-drop operations that trigger React state changes:
- ALWAYS complete the drag state FIRST (
dragCompleted()) - THEN perform the action that triggers re-renders
If you do it in the opposite order, the React re-render may cause issues with PopupLayer's drag state tracking across the component tree rebuild.
Testing Checklist
- Create empty folder via right-click → Create Folder
- Drag component onto empty folder → should move without icon change
- After drop, drag should complete (cursor returns to normal)
- Folder icon should remain folder icon, not component-with-children icon
- Test all drag-drop combinations work correctly with proper completion
[December 28, 2025] - Bug Fix: Folder Creation Regression (COMPLETE FIX)
Summary
🐛 Fixed folder creation regression - Folders were being created but not appearing in the tree.
Bug Details
Problem: User could open the "New folder name" popup, enter a name, click "Add", but no folder appeared in the tree. No console errors.
Root Cause (Two Issues):
-
Missing leading
/: ThehandleAddFolderfunction was creating component names without the required leading/. Fixed inuseComponentActions.ts. -
Placeholders filtered before folder building: The tree builder in
useComponentsPanel.tswas filtering out.placeholdercomponents BEFORE building the folder structure. Since empty folders only exist as.placeholdercomponents (e.g.,/MyFolder/.placeholder), the folder was never created in the tree!
Fix Applied
File 1: useComponentActions.ts - Fixed path normalization to always include leading /
File 2: useComponentsPanel.ts - Fixed buildTreeFromProject() to:
- Process ALL components (including placeholders) for folder structure building
- Use
skipAddComponentflag to create folder structure without adding placeholder tofolder.components - Result: Empty folders appear as folders, without showing the
.placeholdercomponent
Key changes to addComponentToFolderStructure():
// Added 4th parameter to skip adding component (for placeholders)
function addComponentToFolderStructure(
rootFolder: FolderStructure,
component: ComponentModel,
displayPath?: string,
skipAddComponent?: boolean // NEW: for placeholders
) {
// ... create folder structure ...
// Only add component if not a placeholder
if (!skipAddComponent) {
currentFolder.components.push(component);
}
}
Key changes to buildTreeFromProject():
// Before: Filtered out placeholders FIRST (broken - folders never created)
const filteredComponents = components.filter(comp => !comp.name.endsWith('/.placeholder'));
filteredComponents.forEach(comp => addComponentToFolderStructure(...));
// After: Process ALL components, skip adding placeholders to display
components.forEach(comp => {
const isPlaceholder = comp.name.endsWith('/.placeholder');
addComponentToFolderStructure(rootFolder, comp, displayPath, isPlaceholder);
});
Key Learning
Folder visualization requires two things:
- Component path must start with
/ - Placeholders must create folder structure even though they're not displayed as components
The old code filtered out .placeholder before building folders, so empty folders (which ONLY contain a placeholder) never got created in the tree structure.
Testing Checklist
- Right-click empty space → Create Folder → enters name → folder appears
- Right-click component → Create Folder → folder appears nested inside
- Right-click folder → Create Folder → folder appears nested inside
- Undo folder creation → folder disappears
- Empty folders remain visible until deleted
[December 28, 2025] - Context Menu Bug Fixes: Make Home, Duplicate, Component-Folders
Summary
🐛 Fixed 3 context menu bugs discovered during testing:
- "Make Home" menu restriction - Only shows for pages/visual components, not logic components
- Duplicate not working - Fixed undo pattern so duplicate actually creates the copy
- Component-folders missing menu options - Added Open, Make Home, Duplicate to component-folder menus
Bugs Fixed
Bug 1: "Make Home" showing for wrong component types
- Problem: "Make Home" appeared in context menu for ALL components including cloud functions and logic components
- Root Cause: No type check before showing menu item
- Solution: Added conditional check - only show for
isPage || isVisualcomponents - Files:
ComponentItem.tsx,FolderItem.tsx
// Only show "Make Home" for pages or visual components (not logic/cloud functions)
if (component.isPage || component.isVisual) {
items.push({
label: 'Make Home',
disabled: component.isRoot,
onClick: () => onMakeHome?.(node)
});
}
Bug 2: Duplicate component does nothing
- Problem: Clicking "Duplicate" in context menu did nothing - no console log, no duplicate created
- Root Cause: Wrong undo pattern - used
undoGroup.push()+undoGroup.do()butduplicateComponentalready handles its own undo registration internally - Solution: Simplified to just call
duplicateComponentwith undo group, then push the group and switch to new component - File:
useComponentActions.ts
// OLD (broken):
undoGroup.push({ do: () => { duplicateComponent(...)}, undo: () => {...} });
undoGroup.do();
// NEW (working):
ProjectModel.instance?.duplicateComponent(component, newName, { undo: undoGroup, ... });
UndoQueue.instance.push(undoGroup);
Bug 3: Component-folders (top-level of nested tree) get fewer menu options
- Problem: When right-clicking a component that has children (displayed as a folder), the menu only showed Create, Rename, Move to, Delete - missing Open, Make Home, Duplicate
- Root Cause: FolderItem didn't have props or logic for these component-specific actions
- Solution:
- Added
onOpen,onMakeHome,onDuplicateprops to FolderItem - Added component type flags (
isRoot,isPage,isVisual,isCloudFunction) to FolderItemData type - Updated
useComponentsPanel.tsto populate these flags when building folder nodes - Updated FolderItem context menu to include Open, Make Home (conditional), Duplicate for component-folders
- Updated
useComponentActions.tshandlers to support folder nodes with components - Updated ComponentTree to pass the new props to FolderItem
- Added
Files Modified
-
types.ts
- Added
isRoot,isPage,isCloudFunction,isVisualoptional flags toFolderItemData
- Added
-
useComponentsPanel.ts
- Populated component type flags when creating folder nodes with matching components
-
ComponentItem.tsx
- Added conditional check for "Make Home" menu item
-
FolderItem.tsx
- Added
onOpen,onMakeHome,onDuplicateprops - Added Open, Make Home (conditional), Duplicate menu items for component-folders
- Updated useCallback dependencies
- Added
-
ComponentTree.tsx
- Passed
onOpen,onMakeHome,onDuplicateprops to FolderItem
- Passed
-
useComponentActions.ts
- Fixed
handleDuplicateto use correct undo pattern - Updated
handleMakeHome,handleDuplicate,handleOpento support folder nodes (for component-folders)
- Fixed
Technical Notes
Component-Folders: A component-folder is when a component has nested children. For example:
/test1(component)/test1/child(nested component)
In this case, /test1 is displayed as a FolderItem (with expand caret) but IS actually a component. It should have all component menu options.
Handler Updates for Folder Nodes:
The handlers handleMakeHome, handleDuplicate, and handleOpen now check for both:
node.type === 'component'(regular component)node.type === 'folder' && node.data.isComponentFolder && node.data.component(component-folder)
This allows the same handlers to work for both ComponentItem and FolderItem.
Testing Checklist
- Right-click cloud function → "Make Home" should NOT appear
- Right-click page component → "Make Home" should appear
- Right-click visual component → "Make Home" should appear
- Right-click any component → Duplicate → should create copy and switch to it
- Right-click component-folder (component with children) → should have Open, Rename, Duplicate, Make Home (if visual/page), Move to, Delete
[December 28, 2025] - Visual Polish: Action Menu UX Improvements
Summary
✨ Fixed 2 visual/UX issues for the SheetSelector action menu:
- Action menu positioning - Menu now opens upward so it's always visible
- Click-outside dismissal - Action menu now properly closes when clicking outside
Fixes Applied
Fix 1: Action menu opens upward
- Problem: When clicking the three-dot menu on the last sheet item, the rename/delete menu appeared below and required scrolling to see
- Solution: Changed
.ActionMenuCSS fromtop: 100%tobottom: 100%so it opens above the button - File:
SheetSelector.module.scss
Fix 2: Action menu click-outside handling
- Problem: Clicking outside the action menu (rename/delete) didn't close it
- Root Cause: Only the main dropdown had click-outside detection, not the nested action menu
- Solution: Added two improvements:
- Modified main click-outside handler to also clear
activeSheetMenustate - Added separate effect to close action menu when clicking elsewhere in the dropdown
- Modified main click-outside handler to also clear
- File:
SheetSelector.tsx
Files Modified
- SheetSelector.module.scss - Changed
top: 100%tobottom: 100%for.ActionMenu - SheetSelector.tsx - Added click-outside handling for action menu
Task Status: COMPLETE ✅
All sheet system functionality is now fully implemented and polished:
- ✅ Create sheets
- ✅ Rename sheets
- ✅ Delete sheets (moves components to root)
- ✅ Move components between sheets
- ✅ "All" view hides sheet folders
- ✅ Navigation to "All" after deleting current sheet
- ✅ Full undo/redo support
- ✅ Proper visual feedback and UX polish
[December 28, 2025] - Bug Fixes: Sheet System Critical Fixes
Summary
🐛 Fixed 3 critical bugs for sheet operations:
- deleteSheet() stale references - Undo didn't work because component references became stale
- Navigation after delete - Deleting current sheet left user on deleted sheet view
- "All" view showing #folders - Sheet folders appeared as visible folders instead of being hidden organizational tags
Bugs Fixed
Bug 1: deleteSheet() undo broken due to stale component references
-
Problem: Deleting a sheet appeared to work, but undo threw errors or did nothing
-
Root Cause:
renameMapstoredcomponentobject references instead of string names. After thedo()action renamed components, the references pointed to objects with changed names, causing undo to fail. -
Solution: Changed to store only
oldNameandnewNamestrings, then look up components by name during bothdoandundo:// OLD (broken): renameMap.forEach(({ component, newName }) => { ProjectModel.instance?.renameComponent(component, newName); }); // NEW (fixed): renameMap.forEach(({ oldName, newName }) => { const comp = ProjectModel.instance?.getComponentWithName(oldName); if (comp) { ProjectModel.instance?.renameComponent(comp, newName); } }); -
File:
useSheetManagement.ts
Bug 2: No navigation after deleting current sheet
- Problem: After deleting the currently selected sheet, user was left viewing a non-existent sheet
- Solution: Added check in
handleDeleteSheetto navigate to "All" view (selectSheet(null)) if the deleted sheet was currently selected - File:
ComponentsPanelReact.tsx
Bug 3: Sheet folders visible in "All" view
- Problem: When viewing "All", sheet folders like
#Pagesappeared as visible folders in the tree, contradicting the user requirement that sheets should be invisible organizational tags - Root Cause:
buildTreeFromProject()only stripped sheet prefixes when viewing a specific sheet, not when viewing "All" - Solution: Extended the prefix stripping logic to also apply in "All" view (when
currentSheet === null):if (currentSheet === null) { // Strip any #folder prefix to show components without sheet organization const parts = comp.name.split('/').filter((p) => p !== ''); if (parts.length > 0 && parts[0].startsWith('#')) { displayPath = '/' + parts.slice(1).join('/'); } } - File:
useComponentsPanel.ts
Files Modified
- useSheetManagement.ts - Fixed deleteSheet() to use string-based lookup
- ComponentsPanelReact.tsx - Added navigation to "All" after delete
- useComponentsPanel.ts - Strip sheet prefixes in "All" view
Key Learning: String Lookups in Undo Actions
When implementing undo/redo for operations that modify object names/paths:
- Never store object references in the undo data - they become stale
- Always store identifying strings (names, paths, IDs)
- Look up objects fresh during both
doandundoexecution
This pattern is now consistently used in:
renameSheet()✅deleteSheet()✅moveToSheet()✅
Testing Checklist
- Delete sheet → components moved to root, visible in "All"
- Delete current sheet → automatically navigates to "All" view
- Undo delete sheet → sheet and components restored
- Move component to sheet → works correctly
- View "All" → no #folder names visible as folders
- View specific sheet → shows only that sheet's components
[December 27, 2025] - Bug Fixes: Delete, Rename, Move UI
Summary
🐛 Fixed 3 critical bugs discovered during testing:
- Delete sheet error - Used non-existent
PopupLayer.ConfirmDeletePopup - Rename sheet creating duplicates - Component path prefix bug
- Move to submenu UX - Improved to open separate popup
Bugs Fixed
Bug 1: Delete sheet throws TypeError
- Error:
PopupLayer.ConfirmDeletePopup is not a constructor - Root Cause: Used non-existent PopupLayer constructor
- Solution: Changed to
DialogLayerModel.instance.showConfirm()pattern - File:
ComponentsPanelReact.tsx
Bug 2: Rename sheet creates duplicates
- Problem: Renaming a sheet created a new sheet with the new name while leaving the old one
- Root Cause: Component path filter checked for
#SheetName/but component paths start with/, so they're actually/#SheetName/. The filter never matched! - Solution: Fixed prefix checks to include leading
/:const oldPrefix = '/' + oldFolderName + '/'; // "/#Pages/" const newPrefix = '/' + newFolderName + '/'; // "/#NewName/" - File:
useSheetManagement.ts
Bug 3: Move to submenu showed all sheets inline
- Problem: User complained inline sheet list clutters context menu, especially with many sheets
- Solution: Changed "Move to..." to open a separate popup when clicked instead of inline list
- Files:
ComponentItem.tsx,FolderItem.tsx
Files Modified
- ComponentsPanelReact.tsx - Use DialogLayerModel.showConfirm for delete
- useSheetManagement.ts - Fixed path prefix bug in renameSheet
- ComponentItem.tsx - Move to opens separate popup
- FolderItem.tsx - Same change as ComponentItem
Testing Checklist
- Rename sheet → should rename without duplicates
- Delete sheet → confirmation dialog appears, components moved to root
- Move to... → opens separate popup with sheet list
- All undo operations work
[December 27, 2025] - Phase 4: Sheet Management Actions - COMPLETE
Summary
✅ Phase 4 COMPLETE - Implemented full sheet management: rename, delete, and move components between sheets.
What Was Implemented
1. Rename Sheet
- Added rename option to SheetSelector's three-dot menu for each non-default sheet
- Shows StringInputPopup with current name pre-filled
- Validates new name (no empty, no duplicate, no invalid chars)
- Full undo support via
renameSheet()in useSheetManagement
2. Delete Sheet (Non-destructive)
- Added delete option to SheetSelector's three-dot menu
- Critical behavior change: Deleting a sheet now MOVES components to root level instead of deleting them
- Shows confirmation popup explaining components will be moved
- Components become visible in "All" view after sheet deletion
- Full undo support
3. Move Components Between Sheets
- Added "Move to" submenu in component right-click context menu
- Shows all available sheets with current sheet highlighted/disabled
- Works for both ComponentItem and FolderItem (component-folders)
- Inline submenu rendered via MenuDialog's
componentproperty - Full undo support via
moveToSheet()in useSheetManagement
Files Modified
hooks/useSheetManagement.ts
- Completely rewrote
deleteSheet()to move components instead of deleting - Uses rename operations to strip sheet prefix from component paths
- Handles placeholders separately (deleted, not moved)
- Checks for naming conflicts before deletion
components/SheetSelector.tsx
- Added
onRenameSheetandonDeleteSheetprops - Added three-dot action menu for each non-default sheet
- Shows on hover with rename/delete options
- Styled action menu with proper design tokens
components/SheetSelector.module.scss
- Added styles for
.SheetActions,.ActionButton,.ActionMenu,.ActionMenuItem - Hover reveal for action buttons
- Danger styling for delete option
components/ComponentItem.tsx
- Added
sheetsandonMoveToSheetprops - Added "Move to" submenu in handleContextMenu
- Determines current sheet from component path
- Inline submenu shows all sheets with current highlighted
components/FolderItem.tsx
- Same changes as ComponentItem
- Only shows "Move to" for component-folders (folders with associated component)
components/ComponentTree.tsx
- Added
sheetsandonMoveToSheetto props interface - Passes props through to all ComponentItem and FolderItem instances
- Passes through recursive ComponentTree calls
ComponentsPanelReact.tsx
- Imports
renameSheet,deleteSheet,moveToSheetfrom useSheetManagement - Creates
handleRenameSheet,handleDeleteSheet,handleMoveToSheethandlers - Passes handlers to SheetSelector and ComponentTree
Design Decisions
Delete = Move, Not Destroy
- User requested: "deleting a sheet should NOT delete its components"
- Components move to Default sheet (root level)
- Visible in "All" view
- Full undo support for recovery
Move via Context Menu, Not Drag-Drop
- User specifically requested: "I don't want to do drag and drop into sheets"
- Right-click → "Move to" → select sheet
- Current sheet shown but not clickable
- Clear UX without complex drag-drop interactions
Inline Submenu
- MenuDialog doesn't support native nested menus
- Used
componentproperty to render inline sheet list - Styled to visually appear as submenu
dontCloseMenuOnClick: truekeeps menu open for selection
Testing Checklist
- Rename sheet via three-dot menu → popup appears
- Enter new name → sheet renamed, all components updated
- Delete sheet → confirmation shows component count
- Confirm delete → components moved to root, sheet removed
- Undo delete → sheet restored with components
- Right-click component → "Move to" submenu appears
- Current sheet highlighted and disabled
- Click different sheet → component moves
- Undo move → component returns to original sheet
- Move to Default → removes sheet prefix
- Component-folders also have "Move to" option
Next Steps
Phase 5: Integration testing and documentation updates.
[December 27, 2025] - Bug Fixes: Sheet Creation & Reactivity - COMPLETE
Summary
✅ Fixed 4 critical bugs preventing sheet creation from working properly:
- Add Sheet popup timing - setTimeout delay to prevent dropdown/popup conflict
- Placeholder naming convention - Added leading
/to match component path format - Sheet detection for empty sheets - Include placeholders in detection, exclude from count
- React array reference issue - Spread operator to force useMemo recalculation
Bug Details
Bug 1: Add Sheet popup not appearing
- Problem: Clicking "Add Sheet" button closed dropdown but popup never appeared
- Root Cause:
setIsOpen(false)closed dropdown before popup could display; timing conflict - Solution: Added 50ms
setTimeoutdelay to allow dropdown to close before showing popup - File:
components/SheetSelector.tsx
Bug 2: Sheet placeholder naming
- Problem: Created placeholder
#SheetName/.placeholderbut component names start with/ - Root Cause: Inconsistent path format - all component names must start with
/ - Solution: Changed placeholder name to
/#SheetName/.placeholder - File:
hooks/useSheetManagement.ts
Bug 3: New sheets not appearing in dropdown
- Problem: Sheet created successfully (toast shown, project saved) but didn't appear in dropdown
- Root Cause:
allComponentsfilter excluded placeholders, so empty sheets had 0 components → not detected - Solution: Two-pass detection:
- First pass: Detect ALL sheets from
rawComponents(including placeholders) - Second pass: Count only non-placeholder components per sheet
- First pass: Detect ALL sheets from
- File:
hooks/useComponentsPanel.ts
Bug 4: useMemo not recalculating after component added
- Problem: Even after event received and updateCounter incremented, sheets useMemo didn't recalculate
- Root Cause:
ProjectModel.getComponents()returns same array reference (mutated, not replaced). React'sObject.is()comparison didn't detect change. - Solution: Spread operator to create new array reference:
[...ProjectModel.instance.getComponents()] - File:
hooks/useComponentsPanel.ts
Key Learning: Mutable Data Sources + React
This is a critical React pattern when working with EventDispatcher-based models:
// ❌ WRONG - Same array reference, useMemo skips recalculation
const rawComponents = useMemo(() => {
return ProjectModel.instance.getComponents(); // Returns mutated array
}, [updateCounter]);
// ✅ RIGHT - New array reference forces useMemo to recalculate
const rawComponents = useMemo(() => {
return [...ProjectModel.instance.getComponents()]; // New reference
}, [updateCounter]);
Why this happens:
getComponents()returns the internal array (same reference)- When component is added, array is mutated (push)
Object.is(oldArray, newArray)returnstrue(same reference)- useMemo thinks nothing changed, skips recalculation
- Spreading creates new array reference → forces recalculation
Files Modified
-
components/SheetSelector.tsx- Added setTimeout delay in
handleCreateSheet
- Added setTimeout delay in
-
hooks/useSheetManagement.ts- Fixed placeholder name:
/#SheetName/.placeholder
- Fixed placeholder name:
-
hooks/useComponentsPanel.ts- Added
rawComponentsspread to force new reference - Two-pass sheet detection (detect from raw, count from filtered)
- Added
Testing Status
✅ Sheet creation works end-to-end:
- Click Add Sheet → popup appears
- Enter name → click Create
- Toast shows success
- Sheet appears immediately in dropdown
- Sheet persists after project reload
Related Learnings
This bug pattern is now documented:
- LEARNINGS.md: "Mutable Data Sources + useMemo"
- .clinerules: React + EventDispatcher section
[December 27, 2025] - Phase 3: Sheet Selector UI - COMPLETE
Summary
✅ Phase 3 COMPLETE - Implemented the SheetSelector dropdown UI component and integrated it into the ComponentsPanel header.
The SheetSelector allows users to:
- View all available sheets with component counts
- Switch between sheets to filter the component tree
- Select "All" to view all components across sheets
- Create new sheets via the "Add Sheet" button
What Was Implemented
1. SheetSelector Component (components/SheetSelector.tsx)
interface SheetSelectorProps {
sheets: Sheet[]; // All available sheets
currentSheet: Sheet | null; // Currently selected (null = show all)
onSelectSheet: (sheet: Sheet | null) => void;
onCreateSheet?: () => void;
disabled?: boolean; // For locked sheet mode
}
Features:
- Dropdown trigger button with chevron indicator
- "All" option to show all components
- Sheet list with radio-style indicators
- Component counts per sheet
- "Add Sheet" button with divider
- Click-outside to close
- Escape key to close
- Auto-hide when only default sheet exists
2. SheetSelector Styles (components/SheetSelector.module.scss)
All styles use design tokens (no hardcoded colors):
.SheetSelector- Container.TriggerButton- Dropdown trigger with hover/open states.Dropdown- Positioned menu below trigger.SheetList- Scrollable sheet items.SheetItem- Individual sheet with radio indicator.AddSheetButton- Create new sheet action
3. ComponentsPanelReact.tsx Integration
- Added SheetSelector to header JSX (after title)
- Wired up
sheets,currentSheet,selectSheetfrom useComponentsPanel - Wired up
handleCreateSheetcallback using StringInputPopup - Added
disabled={!!options?.lockToSheet}for locked sheet mode
Header Layout
The header now displays:
+--------------------------------+
| Components [SheetSelector▼] |
+--------------------------------+
Using justify-content: space-between for proper spacing.
Files Created
components/SheetSelector.tsx- Dropdown componentcomponents/SheetSelector.module.scss- Styles with design tokens
Files Modified
ComponentsPanelReact.tsx- Added SheetSelector to header
Backwards Compatibility
✅ Fully backwards compatible:
- SheetSelector auto-hides when only default sheet exists
- Works with existing
lockToSheetoption (disables selector) - No changes to existing behavior
Testing Status
✅ TypeScript compilation passes ⏳ Manual testing required:
- Open project with multiple sheets (components in
#folders) - Verify SheetSelector appears in header
- Test switching between sheets
- Test "All" option
- Test creating new sheet
- Verify tree filters correctly
Next Steps
Phase 4: Wire up sheet management actions
- Add rename/delete options to sheet selector
- Wire up move-to-sheet functionality
- Add sheet context menu
[December 27, 2025] - Phase 2: Sheet System Backend - COMPLETE
Summary
✅ Phase 2 COMPLETE - Implemented full sheet detection, filtering, and management backend.
Sheets are a way to organize components into top-level groups. Components in folders starting with # are grouped into sheets (e.g., #Pages/Home belongs to the "Pages" sheet).
What Was Implemented
1. Sheet Interface (types.ts)
interface Sheet {
name: string; // Display name (without # prefix)
folderName: string; // Original folder name with # (e.g., "#Pages")
isDefault: boolean; // Whether this is the default sheet
componentCount: number; // Number of components in this sheet
}
2. Sheet Detection (useComponentsPanel.ts)
- Automatic detection of sheets from component paths
- Sheets are identified as top-level folders starting with
# - Default sheet contains all components NOT in any
#folder - Component counts calculated per sheet
- Hidden sheets support via
hideSheetsoption - Locked sheet support via
lockToSheetoption
3. Sheet Filtering
currentSheetstate tracks selected sheetselectSheet()function to change active sheet- Tree view automatically filters to show only components in selected sheet
- For non-default sheets, the
#SheetName/prefix is stripped from display paths
4. Sheet Management Hook (useSheetManagement.ts)
New hook with full CRUD operations:
createSheet(name)- Create new sheet (creates#SheetName/.placeholder)renameSheet(sheet, newName)- Rename sheet and update all component pathsdeleteSheet(sheet)- Delete sheet and all components (with undo support!)moveToSheet(componentName, targetSheet)- Move component between sheets
All operations include:
- Input validation
- Conflict detection
- Toast notifications
- Full undo/redo support using
UndoQueue.pushAndDo()pattern
Backwards Compatibility
✅ Fully backwards compatible with existing projects:
- Existing
#-prefixed folders automatically appear as sheets - Default sheet behavior unchanged (components not in # folders)
hideSheetsoption continues to work- No migration required
Files Created
hooks/useSheetManagement.ts- Sheet CRUD operations hook
Files Modified
types.ts- AddedSheetinterface,lockToSheetoptionhooks/useComponentsPanel.ts- Added sheet detection, filtering, state management
Return Values from useComponentsPanel
const {
// Existing
treeData,
expandedFolders,
selectedId,
toggleFolder,
handleItemClick,
// NEW: Sheet system
sheets, // Sheet[] - All detected sheets
currentSheet, // Sheet | null - Currently selected sheet
selectSheet // (sheet: Sheet | null) => void
} = useComponentsPanel(options);
Next Steps
Phase 3: Sheet Selector UI
- Create
SheetSelector.tsxdropdown component - Integrate into ComponentsPanel header
- Wire up sheet selection
[December 27, 2025] - TASK-008C: Final Fix - dragCompleted() Method Name
Summary
✅ Fixed final bug preventing drag-drop from completing: wrong method name.
After fixing the onDrop → onMouseUp issue, discovered that PopupLayer.instance.endDrag() was being called, but the correct method name is dragCompleted().
The Error
TypeError: PopupLayer.instance.endDrag is not a function
Root Cause
The useComponentActions.ts file was calling PopupLayer.instance.endDrag(), but this method doesn't exist in PopupLayer. The correct method is dragCompleted().
Changes Made
File: useComponentActions.ts
Replaced all 16 instances of PopupLayer.instance.endDrag() with PopupLayer.instance.dragCompleted():
handleDropOnRoot: Component → Root (3 calls)handleDropOnRoot: Folder → Root (3 calls)handleDropOn: Component → Folder (2 calls)handleDropOn: Folder → Folder (3 calls)handleDropOn: Component → Component (2 calls)handleDropOn: Folder → Component (3 calls)
PopupLayer Drag API
From popuplayer.js:
// Start dragging - initiates drag with label
PopupLayer.prototype.startDragging = function (args) {
// ... sets up drag label that follows cursor
};
// Check if dragging - returns boolean
PopupLayer.prototype.isDragging = function () {
return this.dragItem !== undefined;
};
// Indicate drop type - shows cursor feedback
PopupLayer.prototype.indicateDropType = function (droptype) {
// ... 'move', 'copy', or 'none'
};
// ✅ CORRECT: Complete drag operation
PopupLayer.prototype.dragCompleted = function () {
this.$('.popup-layer-dragger').css({ opacity: '0' });
this.dragItem = undefined;
};
// ❌ WRONG: endDrag() doesn't exist!
Testing Results
✅ All 7 drop combinations now work:
- B1: Component → Component (nest)
- B2: Component → Folder (move into)
- B3: Component → Root (move to top level)
- B4: Folder → Folder (nest folders)
- B5: Folder → Component (nest folder)
- B6: Folder → Root (move to top level)
- B7: Component-Folder → any target
Key Learning
PopupLayer drag completion method is dragCompleted(), not endDrag().
Added to LEARNINGS.md for future reference.
[December 27, 2025] - TASK-008C: Drag-Drop System Root Cause Fix
Summary
🔥 Fixed the fundamental root cause of all drag-drop issues: Wrong event type.
The drag-drop system was using onDrop (HTML5 Drag-and-Drop API event), but the PopupLayer uses a custom mouse-based drag system. The HTML5 onDrop event never fires because we're not using native browser drag-and-drop.
The Root Cause
Previous broken flow:
- ✅ Drag starts via
handleMouseDown→handleMouseMove(5px threshold) →PopupLayer.startDragging() - ✅ Hover detection via
handleMouseEnter→ item becomes drop target, visual feedback works - ❌
onDrop={handleDrop}→ NEVER FIRES because HTML5 DnD events don't fire for mouse-based dragging
Fixed flow:
- ✅ Same drag start
- ✅ Same hover detection
- ✅
onMouseUptriggers drop whenisDropTarget === true
Changes Made
1. ComponentItem.tsx - Enhanced handleMouseUp
// Before (broken):
const handleMouseUp = useCallback(() => {
dragStartPos.current = null; // Only cleared drag start
}, []);
// After (fixed):
const handleMouseUp = useCallback((e: React.MouseEvent) => {
dragStartPos.current = null;
if (isDropTarget && onDrop) {
e.stopPropagation(); // Prevent bubble to Tree
const node: TreeNode = { type: 'component', data: component };
onDrop(node);
setIsDropTarget(false);
}
}, [isDropTarget, component, onDrop]);
2. FolderItem.tsx - Same fix
- Enhanced
handleMouseUpto trigger drop whenisDropTargetis true
3. ComponentsPanelReact.tsx - Simplified background drop
// Before (broken):
// - Used onMouseEnter/Leave/Drop with e.target === e.currentTarget check
// - onDrop never fires because it's HTML5 DnD event
// - e.target === e.currentTarget never true due to child elements
// After (fixed):
const handleTreeMouseUp = useCallback(() => {
const PopupLayer = require('@noodl-views/popuplayer');
if (draggedItem && PopupLayer.instance.isDragging()) {
handleDropOnRoot(draggedItem);
}
}, [draggedItem, handleDropOnRoot]);
// JSX:
<div className={css['Tree']} onContextMenu={handleTreeContextMenu} onMouseUp={handleTreeMouseUp}>
How Event Bubbling Enables Root Drop
- User releases mouse while dragging
- If over a valid tree item → item's
handleMouseUpfires, callse.stopPropagation(), executes drop - If over empty space → no item catches event, bubbles to Tree div, triggers root drop
Files Modified
- ComponentItem.tsx - Enhanced
handleMouseUpto trigger drop - FolderItem.tsx - Same enhancement
- ComponentsPanelReact.tsx - Replaced complex background handlers with simple
onMouseUp
Testing Checklist
All drop combinations should now work:
- B1: Component → Component (nest component inside another)
- B2: Component → Folder (move component into folder)
- B3: Component → Root (drag to empty space)
- B4: Folder → Folder (move folder into another)
- B5: Folder → Component (nest folder inside component)
- B6: Folder → Root (drag folder to empty space)
- B7: Component-Folder → any target
Key Learning: HTML5 DnD vs Mouse-Based Dragging
HTML5 Drag-and-Drop API:
- Uses
draggable="true",ondragstart,ondragenter,ondragover,ondrop - Native browser implementation with built-in ghost image
onDropfires when dropping a dragged element
Mouse-Based Dragging (PopupLayer):
- Uses
onMouseDown,onMouseMove,onMouseUp - Custom implementation that moves a label element with cursor
onDropnever fires - must useonMouseUpto detect drop
Rule: If using PopupLayer's drag system, always use onMouseUp for drop detection, not onDrop.
[December 27, 2025] - BUG FIX: Drag-Drop Regression & Root Drop Zone
Summary
🐛 Fixed drag-drop regression caused by duplicate fix + ✨ Added background drop zone for moving items to root level.
The Regression: After fixing the duplicate rendering bug, drag-drop for component-folders stopped working. Items would drag but return to origin instead of completing the drop.
Root Cause: Component-folders are now rendered as FolderItem (not ComponentItem), so handleDropOn needed to handle the new folder → component and folder → folder (with component data) cases.
New Feature: Users can now drag nested components/folders onto empty space in the panel to move them to root level.
Issues Fixed
Bug: Component-folders can't be dropped
- Problem: After duplicate fix, dragging
/test1(with nested/test1/child) would drag but snap back to origin - Why it broke: Duplicate fix merged component-folders into folder nodes, changing
draggedItem.typefrom'component'to'folder' - Missing cases:
handleDropOndidn't handlefolder → componentorfolder → folderwith attached component data - Solution:
- Updated
folder → folderto include component at folder path:comp.name === sourcePath || comp.name.startsWith(sourcePath + '/') - Added new
folder → componentcase to nest folder AS a component inside target - Added safety check to prevent moving folder into itself
- Updated
- Files:
useComponentActions.ts- EnhancedhandleDropOn()with two new cases
Feature: Move items to root level
- Problem: No way to move nested components back to root (e.g.,
/test1/child→/child) - Solution: Added background drop zone on empty space
- Created
handleDropOnRoot()for both components and folders - Handles path unwrapping and proper rename operations
- Added visual feedback (light blue background on hover)
- Integrates with PopupLayer drag system
- Created
- Files:
useComponentActions.ts- NewhandleDropOnRoot()functionComponentsPanelReact.tsx- Background drop handlers and visual styling
Technical Details
All Drop Combinations Now Supported:
- ✅ Component → Component (nest component inside another)
- ✅ Component → Folder (move component into folder)
- ✅ Component → Root (move nested component to top level) NEW
- ✅ Folder → Folder (move folder into another folder, including component-folder)
- ✅ Folder → Component (nest folder inside component) NEW
- ✅ Folder → Root (move nested folder to top level) NEW
Component-Folder Handling:
When a folder node has an attached component (e.g., /test1 with /test1/child), moving operations now correctly:
- Move the component itself:
/test1 - Move all nested children:
/test1/child,/test1/child/grandchild, etc. - Update all paths atomically with proper undo support
Background Drop Zone:
- Activates only when
draggedItemexists AND mouse enters empty space (not tree items) - Shows visual feedback:
rgba(100, 150, 255, 0.1)background tint - Uses
e.target === e.currentTargetto ensure drops only on background - Calls
PopupLayer.indicateDropType('move')for cursor feedback - Properly calls
PopupLayer.endDrag()to complete operation
Files Modified
-
useComponentActions.ts
- Added
handleDropOnRoot()function (lines ~390-470) - Updated
folder → foldercase to include component at folder path - Added new
folder → componentcase - Added folder-into-self prevention
- Exported
handleDropOnRootin return statement
- Added
-
ComponentsPanelReact.tsx
- Added
handleDropOnRootto useComponentActions destructure - Added
isBackgroundDropTargetstate - Added
handleBackgroundMouseEnter()handler - Added
handleBackgroundMouseLeave()handler - Added
handleBackgroundDrop()handler - Wired handlers to Tree div with visual styling
- Added
Testing Status
✅ Code compiles successfully ✅ No TypeScript errors ✅ All handlers properly wired ⏳ Manual testing required:
Component-Folder Drag-Drop:
- Create
/test1with nested/test1/child - Drag
/test1folder onto another component → should nest properly - Drag
/test1folder onto another folder → should move with all children - Verify
/test1and/test1/childboth move together
Background Drop Zone:
- Create nested component like
/folder/component - Drag it to empty space in panel
- Should show blue tint on empty areas
- Drop → component should move to root as
/component - Test with folders too:
/folder1/folder2→/folder2
All Combinations:
- Test all 6 drop combinations listed above
- Verify undo works for each
- Check that drops complete (no snap-back)
Next Steps
User should:
- Clear all caches:
npm run clean:all - Restart dev server:
npm run dev - Test component-folder drag-drop (the regression)
- Test background drop zone (new feature)
- Verify all combinations work with undo
[December 27, 2025] - BUG FIX: Duplicate Component-Folders
Summary
🐛 Fixed duplicate rendering bug when components become folders:
When a component had nested children (e.g., /test1 with /test1/child), the tree displayed TWO entries:
- A folder for "test1"
- A component for "/test1"
Both would highlight red when clicked (same selectedId), creating confusing UX.
Issue Details
Problem: Component /test1 dropped onto another component to create /test1/child resulted in duplicate tree nodes.
Root Cause: Tree building logic in convertFolderToTreeNodes() created:
- Folder nodes for paths with children (line 205-222)
- Component nodes for ALL components (line 227-245)
It never checked if a component's name matched a folder path, so /test1 got rendered twice.
User Report: "when a dropped component has its first nested component, it duplicates, one with the nested component, the other with no nested components. when i click one of the duplicates, both turn red"
Solution
Modified convertFolderToTreeNodes() to merge component-folders into single nodes:
- Build folder path set (line 202): Create Set of all folder paths for O(1) lookup
- Attach matching components to folders (line 218-219): When creating folder nodes, find component with matching path and attach it to folder's data
- Skip duplicate components (line 234-237): When creating component nodes, skip any that match folder paths
Code changes in useComponentsPanel.ts:
// Build a set of folder paths for quick lookup
const folderPaths = new Set(folder.children.map((child) => child.path));
// When creating folder nodes:
const matchingComponent = folder.components.find((comp) => comp.name === childFolder.path);
const folderNode: TreeNode = {
type: 'folder',
data: {
// ...
component: matchingComponent, // Attach the component if it exists
}
};
// When creating component nodes:
if (folderPaths.has(comp.name)) {
return; // Skip components that are also folders
}
Result
/test1with nested/test1/childnow renders as single folder node- Folder node represents the component and contains children
- No more duplicates, no more confusing selection behavior
- Component data attached to folder, so it's clickable and has proper icon/state
Files Modified
useComponentsPanel.ts - convertFolderToTreeNodes() function (lines 198-260)
- Added folderPaths Set for quick lookup
- Added logic to find and attach matching components to folder nodes
- Added skip condition for components that match folder paths
Testing Status
✅ Code compiles successfully ✅ No TypeScript errors ⏳ Manual testing required:
- Create component
/test1 - Drag another component onto
/test1to create/test1/child - Should see single "test1" folder (not duplicate)
- Clicking "test1" should select only that node
- Expanding should show nested child
Technical Notes
Component-as-Folder Pattern:
In Noodl, components CAN act as folders when they have nested components:
/test1is a component/test1/childmakes "test1" act as a folder containing "child"- The folder node must represent both the component AND the container
Why attach component to folder data:
- Folder needs component reference for Open/Delete/etc actions
- Folder icon should reflect component type (Page, CloudFunction, etc.)
- Selection should work on the folder node
Why skip duplicate in component loop:
- Component already rendered as folder
- Rendering again creates duplicate with same selectedId
- Skipping prevents the duplication bug
[December 26, 2025] - BUG FIXES Round 3: Complete Feature Polish
Summary
🐛 Fixed 4 major bugs discovered during testing:
- ✅ Drop operations now complete - Added
PopupLayer.endDrag()calls - ✅ Placeholder components hidden - Filtered out
.placeholderfrom tree display - ✅ Nested component creation works - Fixed parent path calculation
- ✅ Open button functional - Implemented component switching
Issues Fixed
Bug 1: Drop operations returned elements to original position
- Problem: Red drop indicator appeared, but elements snapped back after drop
- Root Cause: Missing
PopupLayer.endDrag()call to complete the drag operation - Impact: All drag-drop operations appeared broken to users
- Fix: Added
PopupLayer.instance.endDrag()after successful drop in all three scenarios - Files:
useComponentActions.ts- AddedendDrag()to component→folder, folder→folder, and component→component drops - Also fixed: Added
endDrag()on error paths to prevent stuck drag state
Bug 2: Placeholder components visible in tree
- Problem:
.placeholdercomponents showed up in the component tree - Root Cause: No filtering in
buildTreeFromProject- these are implementation details for empty folders - Impact: Confusing UX - users saw internal components they shouldn't interact with
- Fix: Added filter in
useComponentsPanel.tsline 136:// Hide placeholder components (used for empty folder visualization) if (comp.name.endsWith('/.placeholder')) { return false; } - Result: Empty folders display correctly without showing placeholder internals
Bug 3: Creating components from right-click menu went to root
- Problem: Right-click component → "Create Page" created
/NewPageinstead of/test1/NewPage - Root Cause: Parent path calculation extracted the PARENT folder, not the component as folder
- Old logic:
component.path.substring(0, component.path.lastIndexOf('/') + 1)(wrong) - New logic:
component.path + '/'(correct) - Impact: Couldn't create nested component structures from context menu
- Fix:
ComponentItem.tsxline 153 - simplified to just append/ - Example: Right-click
/test1→ Create → now creates/test1/NewComponent✅
Bug 4: Open button only logged to console
- Problem: Right-click → "Open" showed console log but didn't switch to component
- Root Cause:
handleOpenwas a TODO stub that only logged - Fix: Implemented using same pattern as
handleItemClick:EventDispatcher.instance.notifyListeners('ComponentPanel.SwitchToComponent', { component, pushHistory: true }); - Files:
useComponentActions.tsline 255 - Result: Open menu item now switches active component in editor
Files Modified
-
useComponentActions.ts
- Added
PopupLayer.instance.endDrag()to 3 drop scenarios (lines ~432, ~475, ~496) - Added
endDrag()on error paths (lines ~429, ~470) - Implemented
handleOpento dispatch SwitchToComponent event (line 255)
- Added
-
useComponentsPanel.ts
- Added filter for
.placeholdercomponents (line 136-139) - Components ending in
/.placeholdernow excluded from tree
- Added filter for
-
ComponentItem.tsx
- Fixed parent path calculation for nested creation (line 153)
- Changed from substring extraction to simple append:
component.path + '/'
Technical Notes
PopupLayer Drag Lifecycle:
The PopupLayer drag system requires explicit completion:
startDrag()- Begins drag (done by existing code)indicateDropType('move')- Shows visual feedback (done by drop handlers)endDrag()- MUST be called or element returns to origin
Missing step 3 caused all drops to fail visually even though the rename operations succeeded.
Virtual Folder System:
Placeholder components are an implementation detail:
- Created at
{folderPath}/.placeholderto make empty folders exist - Must be hidden from tree display
- Filtered before tree building to avoid complexity
Parent Path for Nesting:
When creating from component context menu:
- Goal: Nest inside the component (make it a folder)
- Solution: Use component's full path +
/as parent - Example:
/test1→ create → parent is/test1/→ result is/test1/NewComponent
Testing Status
✅ All code compiles successfully ✅ No TypeScript errors ⏳ Manual testing required:
Drop Operations:
- Drag component to folder → should move and stay
- Drag folder to folder → should nest properly
- Drag component to component → should nest
- All should complete without returning to origin
Placeholder Filtering:
- Create empty folder
- Should not see
.placeholdercomponent in tree - Folder should display normally
Nested Creation:
- Right-click component
/test1 - Create Page → enter name
- Should create
/test1/NewPage(not/NewPage)
Open Functionality:
- Right-click any component
- Click "Open"
- Component should open in editor (not just log)
React Key Warning
Status: Not critical - keys appear correctly implemented in code
The warning mentions ComponentTree but inspection shows:
- Folders use
key={node.data.path}(unique) - Components use
key={node.data.id}(unique)
This may be a false warning or coming from a different source. Not addressing in this fix as it doesn't break functionality.
Next Steps
User should:
- Test all four scenarios above
- Verify drag-drop completes properly
- Check nested component creation works
- Confirm Open menu item functions
- Verify no placeholder components visible
[December 26, 2025] - BUG FIXES Round 2: Drag-Drop & Folder Creation
Summary
🐛 Fixed remaining critical bugs after context restoration:
- ✅ Component drag-drop now works - Fixed missing props in ComponentTree
- ✅ Folder creation works - Implemented real virtual folder creation
- ✅ No more PopupLayer crashes - Fixed dialog positioning
Issues Fixed
Bug 1: Components couldn't be drop targets
- Problem: Could drag components but couldn't drop onto them (no visual feedback, no drop handler triggered)
- Root Cause: ComponentItem had drop handlers added but ComponentTree wasn't passing
onDropandcanAcceptDropprops - Impact: Component→Component nesting completely non-functional
- Fix: Added missing props to ComponentItem in ComponentTree.tsx line 135
Bug 2: Folder creation showed placeholder toast
- Problem: Right-click folder → "Create Folder" showed "Coming in next phase" toast instead of actually working
- Root Cause:
handleAddFolderwas stub implementation from Phase 1 - Solution: Implemented full virtual folder creation using placeholder component pattern:
const placeholderName = `${folderPath}/.placeholder`; UndoQueue.instance.pushAndDo( new UndoActionGroup({ label: `Create folder ${folderName}`, do: () => { const placeholder = new ComponentModel({ name: placeholderName, graph: new NodeGraphModel(), id: guid() }); ProjectModel.instance?.addComponent(placeholder); }, undo: () => { const placeholder = ProjectModel.instance?.getComponentWithName(placeholderName); if (placeholder) { ProjectModel.instance?.removeComponent(placeholder); } } }) ); - File:
useComponentActions.tsline 180-230 - Features:
- Name validation (no empty names)
- Duplicate detection (prevents overwriting existing folders)
- Proper parent path handling
- Full undo support
- Toast feedback on success/error
Bug 3: PopupLayer crash when creating folders
- Problem: After implementing folder creation, clicking OK crashed with error:
Error: Invalid position bottom for dialog popup - Root Cause: StringInputPopup is a dialog (modal), not a dropdown menu. Used wrong
positionvalue. - Solution: Changed
showPopup()call fromposition: 'bottom'toposition: 'screen-center'withisBackgroundDimmed: true - File:
useComponentActions.tsline 224 - Technical Detail: PopupLayer has two positioning modes:
- Dialogs (modals): Use
position: 'screen-center'+isBackgroundDimmed - Dropdowns (menus): Use
attachTo+position: 'bottom'/'top'/etc
- Dialogs (modals): Use
Files Modified
-
ComponentTree.tsx
- Added
onDrop={onDrop}prop to ComponentItem (line 135) - Added
canAcceptDrop={canAcceptDrop}prop to ComponentItem (line 136) - Now properly passes drop handlers down the tree
- Added
-
useComponentActions.ts
- Implemented full
handleAddFolderfunction (line 180-230) - Added validation, duplicate checking, placeholder creation
- Fixed PopupLayer positioning to use
screen-centerfor dialogs - Added proper error handling with toast messages
- Implemented full
Technical Notes
Virtual Folder System:
Noodl's folders are virtual - they're just path prefixes on component names. To create a folder, you create a hidden placeholder component at {folderPath}/.placeholder. The tree-building logic (buildTree in useComponentsPanel) automatically:
- Detects folder paths from component names
- Groups components by folder
- Filters out
.placeholdercomponents from display - Creates FolderNode structures with children
Component Drop Handlers: ComponentItem now has the same drop-handling pattern as FolderItem:
handleMouseEnter: Check if valid drop target, set visual feedbackhandleMouseLeave: Clear visual feedbackhandleDrop: Execute the move operationisDropTargetstate: Controls visual CSS class
All Nesting Combinations Now Supported:
- ✅ Component → Component (nest component inside another)
- ✅ Component → Folder (move component into folder)
- ✅ Folder → Component (nest folder inside component)
- ✅ Folder → Folder (move folder into another folder)
Testing Status
✅ Code compiles successfully ✅ No TypeScript errors ✅ All imports resolved ⏳ Manual testing required:
Folder Creation:
- Right-click any folder → Create Folder
- Enter name → Click OK
- New folder should appear in tree
- Undo should remove folder
- Try duplicate name → should show error toast
Component Drop Targets:
- Drag any component
- Hover over another component → should show drop indicator
- Drop → component should nest inside target
- Try all four nesting combinations listed above
Next Steps
User should:
- Clear caches and rebuild:
npm run clean:all && npm run dev - Test folder creation end-to-end
- Test all four nesting scenarios
- Verify undo works for all operations
- Check for any visual glitches in drop feedback
[December 26, 2025] - BUG FIXES: Critical Issues Resolved
Summary
🐛 Fixed 4 critical bugs discovered during manual testing:
- ✅ Folder drag-drop now works - Fixed incorrect PopupLayer import path
- ✅ No more phantom drags - Clear drag state when context menu opens
- ✅ Delete actually deletes - Fixed UndoQueue anti-pattern
- ✅ Component nesting works - Fixed parent path normalization
Issues Fixed
Bug 1: FolderItem drag-drop completely broken
- Problem: Dragging folders caused runtime errors, drag operations failed silently
- Root Cause: Import error in
FolderItem.tsxline 13:import PopupLayer from './popuplayer' - Path should be:
../../../popuplayer(not relative to current directory) - Impact: All folder drag operations were non-functional
- Fix: Corrected import path
Bug 2: Phantom drag after closing context menu
- Problem: After right-clicking an item and closing the menu, moving the mouse would start an unwanted drag operation
- Root Cause:
dragStartPos.currentwas set onmouseDownbut never cleared when context menu opened - Impact: Confusing UX, items being dragged unintentionally
- Fix: Added
dragStartPos.current = nullat start ofhandleContextMenuin both ComponentItem and FolderItem
Bug 3: Delete shows confirmation but doesn't delete
-
Problem: Clicking "Delete" showed confirmation dialog and appeared to succeed, but component remained in tree
-
Root Cause: Classic UndoQueue anti-pattern in
handleDelete- usedpush()+do()instead ofpushAndDo() -
Technical Details:
// ❌ BROKEN (silent failure): undoGroup.push({ do: () => {...}, undo: () => {...} }); undoGroup.do(); // Loop never runs because ptr == actions.length // ✅ FIXED: UndoQueue.instance.pushAndDo(new UndoActionGroup({ do: () => {...}, undo: () => {...} })); -
Impact: Users couldn't delete components
-
Fix: Converted to correct
pushAndDopattern as documented in UNDO-QUEUE-PATTERNS.md
Bug 4: "Add Component/Folder" creates at root level
- Problem: Right-clicking a folder and selecting "Create Component" created component at root instead of inside folder
- Root Cause: Parent path "/" was being prepended as literal string instead of being normalized to empty string
- Impact: Folder organization workflow broken
- Fix: Normalize parent path in
handleAddComponent:parentPath === '/' ? '' : parentPath
Files Modified
-
FolderItem.tsx
- Fixed PopupLayer import path (line 13)
- Added
dragStartPos.current = nullinhandleContextMenu
-
ComponentItem.tsx
- Added
dragStartPos.current = nullinhandleContextMenu
- Added
-
useComponentActions.ts
- Fixed
handleDeleteto usepushAndDopattern - Fixed
handleAddComponentparent path normalization
- Fixed
Technical Notes
UndoQueue Pattern Importance:
This bug demonstrates why following the UNDO-QUEUE-PATTERNS.md guide is critical. The anti-pattern:
undoGroup.push(action);
undoGroup.do();
...compiles successfully, appears to work (no errors), but silently fails because the internal pointer makes the loop condition false. Always use pushAndDo().
Import Path Errors:
Import errors like ./popuplayer vs ../../../popuplayer don't always cause build failures if webpack resolves them differently in dev vs prod. Always verify imports relative to file location.
Testing Status
✅ Code compiles successfully ✅ No TypeScript errors ⏳ Manual testing required:
- Drag folder to another folder (should move)
- Right-click component → close menu → move mouse (should NOT drag)
- Right-click component → Delete → Confirm (component should disappear)
- Right-click folder → Create Component (should create inside folder)
Next Steps
User should:
- Clear caches and rebuild:
npm run clean:all && npm run dev - Test all four scenarios above
- Verify no regressions in existing functionality
[December 26, 2025] - FINAL SOLUTION: Right-Click on Empty Space
Summary
✅ TASK COMPLETE - After hours of failed attempts with button-triggered menus, implemented the pragmatic solution: Right-click on empty space shows Create menu.
Why This Works:
- Uses proven
showContextMenuInPopup()pattern that works perfectly for right-click events - Cursor position is naturally correct for right-click menus
- Consistent with native app UX patterns
- Actually more discoverable than hidden plus button
What Changed:
- Removed: Plus (+) button from ComponentsPanel header
- Added:
onContextMenuhandler on Tree div that shows Create menu - Result: Users can right-click anywhere in the panel (components, folders, OR empty space) to access Create menu
The Button Click Nightmare: A Cautionary Tale
Failed Attempts (4+ hours total):
-
showContextMenuInPopup() from button click ❌
- Silent failure - menu appeared in wrong location or not at all
- Root cause:
screen.getCursorScreenPoint()gives cursor position AFTER click, not button location - Duration: 1+ hours
-
PopupLayer.showPopout() with button ref ❌
- Silent failures despite "success" logs
- API confusion between showPopup/showPopout
- Duration: 1+ hours
-
NewPopupLayer.PopupMenu constructor ❌
- "PopupMenu is not a constructor" runtime error
- Export issues in legacy code
- Duration: 30 minutes
-
PopupMenu rendering but clicks not working ❌
- Menu appeared but onClick handlers didn't fire
- Event delegation issues in jQuery/React integration
- Duration: 1+ hours, multiple cache clears, fresh builds
The Breaking Point: "this is the renaming task all over again. we can't keep trying the same damn thing with the same bad result"
The Pragmatic Solution: Remove the button. Use right-click on empty space. It works perfectly.
Implementation
File: ComponentsPanelReact.tsx
// Handle right-click on empty space - Show create menu
const handleTreeContextMenu = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const templates = ComponentTemplates.instance.getTemplates({
forRuntimeType: 'browser'
});
const items: TSFixme[] = templates.map((template) => ({
icon: template.icon,
label: `Create ${template.label}`,
onClick: () => handleAddComponent(template)
}));
items.push({
icon: IconName.FolderClosed,
label: 'Create Folder',
onClick: () => handleAddFolder()
});
showContextMenuInPopup({
items,
width: MenuDialogWidth.Default
});
},
[handleAddComponent, handleAddFolder]
);
// Attach to tree container
<div className={css['Tree']} onContextMenu={handleTreeContextMenu}>
Files Modified
- ComponentsPanelReact.tsx
- Removed
handleAddClickfunction (broken plus button handler) - Removed plus button from header JSX
- Added
handleTreeContextMenuusing working showContextMenuInPopup pattern - Attached
onContextMenuto Tree div - Removed all PopupLayer/PopupMenu imports
- Removed
UX Benefits
Better than a plus button:
- ✅ More discoverable (right-click is universal pattern)
- ✅ Works anywhere in the panel (not just on button)
- ✅ Consistent with component/folder right-click menus
- ✅ Common pattern in native desktop applications
- ✅ No cursor positioning issues
- ✅ Uses proven, reliable code path
Critical Lessons Learned
-
Button clicks + cursor-based positioning = broken UX in Electron
screen.getCursorScreenPoint()doesn't work for button clicks- Cursor moves between click and menu render
- No reliable way to position menu at button location from React
-
Legacy PopupLayer/PopupMenu + React = fragile
- jQuery event delegation breaks in React context
- Constructor export issues
- API confusion (showPopup vs showPopout)
- Multiple silent failure modes
-
When repeatedly failing with same approach, change the approach
- Spent 4+ hours on variations of the same broken pattern
- Should have pivoted to alternative UX sooner
- Pragmatic solutions beat perfect-but-broken solutions
-
Right-click context menus are the reliable choice
- Cursor position is inherently correct
- Works consistently across the application
- Proven pattern with zero positioning issues
Documentation Added
LEARNINGS.md:
- New section: "🔥 CRITICAL: React Button Clicks vs Cursor-Based Menu Positioning"
- Documents all failed attempts with technical details
- Explains why button clicks fail and right-click works
- Provides detection patterns for future debugging
Testing Status
✅ Code compiles with no TypeScript errors
✅ All imports resolved correctly
✅ Right-click on empty space shows Create menu
✅ Menu items functional and properly styled
✅ Consistent UX with component/folder menus
Task Complete
Phase 1 of TASK-008 is now COMPLETE. Users can access the Create menu by:
- Right-clicking on any component
- Right-clicking on any folder
- Right-clicking on empty space in the panel
All three methods show the same comprehensive Create menu with all component templates plus folder creation.
[December 26, 2025] - SOLUTION: Use showContextMenuInPopup Utility
Summary
✅ FINALLY WORKING - Rewrote all menu handlers to use the showContextMenuInPopup() utility function.
After hours of debugging coordinate systems and PopupLayer APIs, discovered that OpenNoodl already has a utility function specifically designed to show React context menus from Electron. This function automatically handles:
- Cursor position detection
- Coordinate conversion (screen → window-relative)
- React root creation and cleanup
- MenuDialog rendering with proper styling
- Popout positioning and lifecycle
The Correct Pattern
File: packages/noodl-editor/src/editor/src/views/ShowContextMenuInPopup.tsx
import { MenuDialogWidth } from '@noodl-core-ui/components/popups/MenuDialog';
import { showContextMenuInPopup } from '../../../ShowContextMenuInPopup';
// In your handler:
showContextMenuInPopup({
items: [
{ icon: IconName.Component, label: 'Create Page', onClick: () => handleCreate() },
'divider',
{ label: 'Delete', onClick: () => handleDelete() }
],
width: MenuDialogWidth.Default
});
That's it. No coordinate math, no PopupMenu creation, no manual positioning.
What We Changed
1. ComponentItem.tsx
- Removed manual PopupMenu creation
- Removed coordinate conversion logic
- Removed PopupLayer.instance.showPopup() call
- Added simple
showContextMenuInPopup()call - Menu appears exactly at cursor position ✅
2. FolderItem.tsx
- Same changes as ComponentItem.tsx
- Right-click menus now work perfectly ✅
3. ComponentsPanelReact.tsx
- Removed
showPopout()approach - Removed button ref (no longer needed)
- Plus button now uses
showContextMenuInPopup()✅ - Menu appears at cursor, not attached to button (consistent UX)
Why Previous Approaches Failed
❌ Direct PopupLayer/PopupMenu usage:
- Designed for jQuery views, not React components
- Coordinate system incompatible (requires manual conversion)
- Requires understanding Electron window positioning
- Menu lifecycle not managed properly
❌ showPopup() with attachToPoint:
- Wrong API for dropdown menus
- Position calculations were incorrect
- Doesn't work reliably with React event coordinates
❌ showPopout() with attachTo:
- Requires jQuery element reference
- Position relative to element, not cursor
- Different UX than other context menus in the app
✅ showContextMenuInPopup():
- Purpose-built for React→Electron context menus
- Handles all complexity internally
- Already proven in NodeGraphEditor
- Consistent with rest of app
Files Modified
-
ComponentItem.tsx
- Added import:
showContextMenuInPopup,MenuDialogWidth - Rewrote
handleContextMenu()to use utility - Removed debug console.logs
- 50 lines of complex code → 10 lines simple code
- Added import:
-
FolderItem.tsx
- Same pattern as ComponentItem.tsx
- Context menus now work reliably
-
ComponentsPanelReact.tsx
- Simplified
handleAddClick() - Removed
addButtonRef - Removed PopupLayer require
- Removed complex popout setup
- Cleaned up debug logs throughout file
- Simplified
Testing Status
✅ Code compiles with no errors ✅ TypeScript types all correct ✅ All imports resolved ⏳ Manual testing needed (all three menu types):
- Right-click on component
- Right-click on folder
- Click plus (+) button
Key Learning
Before debugging low-level APIs, check if a utility function already exists!
The codebase had ShowContextMenuInPopup.tsx all along, successfully used in:
NodeGraphEditor.tsx(node right-click menus)PropertyPanel(property context menus)- Other modern React components
We should have checked existing React components for patterns before trying to use jQuery-era APIs directly.
Documentation Impact
This experience should be added to:
- LEARNINGS.md - "Always use showContextMenuInPopup for React context menus"
- COMMON-ISSUES.md - "Context menus not appearing? Don't use PopupLayer directly from React"
[December 26, 2025] - Debugging Session: Menu Visibility Fixes
Summary
🔧 Fixed multiple menu visibility issues discovered during testing:
- Template popup visibility - Added
isBackgroundDimmed: trueflag - Plus button menu not showing - Changed from
showPopup()toshowPopout()API - Right-click menus now fully functional - All items clickable and visible
Issues Resolved
Issue 1: Template name input dialog transparent/oddly positioned
- Problem: When clicking "Create Page" from context menu, the name input popup appeared transparent in the wrong position
- Root Cause: Missing
isBackgroundDimmedflag inshowPopup()call - Solution: Added
isBackgroundDimmed: trueto template popup configuration - File:
useComponentActions.tsline 313
PopupLayer.instance.showPopup({
content: popup,
position: 'screen-center',
isBackgroundDimmed: true // ← Added this flag
});
Issue 2: Plus button menu not appearing
- Problem: Clicking the "+" button logged success but menu didn't show
- Root Cause: Used wrong PopupLayer API -
showPopup()doesn't supportposition: 'bottom' - Solution: Changed to
showPopout()API which is designed for attached menus - File:
ComponentsPanelReact.tsxline 157
// BEFORE (wrong API):
PopupLayer.instance.showPopup({
content: menu,
attachTo: $(addButtonRef.current),
position: 'bottom'
});
// AFTER (correct API):
PopupLayer.instance.showPopout({
content: { el: menu.el },
attachTo: $(addButtonRef.current),
position: 'bottom'
});
Key Learning: PopupLayer API Confusion
PopupLayer has two distinct methods for showing menus:
showPopup(args)- For centered modals/dialogs- Supports
position: 'screen-center' - Supports
isBackgroundDimmedflag - Does NOT support relative positioning like
'bottom'
- Supports
showPopout(args)- For attached dropdowns/menus- Supports
attachTowithposition: 'bottom'|'top'|'left'|'right' - Content must be
{ el: jQuery element } - Has arrow indicator pointing to anchor element
- Supports
Rule of thumb:
- Use
showPopup()for dialogs (confirmation, input, etc.) - Use
showPopout()for dropdown menus attached to buttons
Files Modified
-
useComponentActions.ts
- Added
isBackgroundDimmed: trueto template popup
- Added
-
ComponentsPanelReact.tsx
- Changed plus button handler from
showPopup()toshowPopout() - Updated content format to
{ el: menu.el }
- Changed plus button handler from
Testing Status
- ⏳ Template popup visibility (needs user testing after restart)
- ⏳ Plus button menu (needs user testing after restart)
- ✅ Right-click menus working correctly
Next Steps
User should:
- Restart dev server or clear caches
- Test plus button menu appears below button
- Test right-click → Create Page shows proper modal dialog
- Verify all creation operations work end-to-end
[December 26, 2025] - Phase 1 Complete: Enhanced Context Menus
Summary
✅ Phase 1 COMPLETE - Added "Create" menu items to component and folder context menus.
Users can now right-click on any component or folder in the ComponentsPanel and see creation options at the top of the menu:
- Create Page Component
- Create Visual Component
- Create Logic Component
- Create Cloud Function Component
- Create Folder
All items are positioned at the top of the context menu with appropriate icons and dividers.
Implementation Details
Files Modified:
-
ComponentItem.tsx
- Added
onAddComponentandonAddFolderprops - Enhanced
handleContextMenuto fetch templates and build menu items - Calculates correct parent path from component location
- All creation menu items appear at top, before existing actions
- Added
-
FolderItem.tsx
- Added same
onAddComponentandonAddFolderprops - Enhanced
handleContextMenuwith template creation items - Uses folder path as parent for new items
- Same menu structure as ComponentItem for consistency
- Added same
-
ComponentTree.tsx
- Added
onAddComponentandonAddFolderto interface - Passed handlers down to both ComponentItem and FolderItem
- Handlers propagate recursively through tree structure
- Added
-
ComponentsPanelReact.tsx
- Passed
handleAddComponentandhandleAddFolderto ComponentTree - These handlers already existed from TASK-004B
- No new logic needed - just wiring
- Passed
Technical Notes
PopupMenu Structure: Since PopupMenu doesn't support nested submenus, we used a flat structure with dividers:
Create Page Component ← Icon + Label
Create Visual Component
Create Logic Component
Create Cloud Function Component
─────────────── ← Divider
Create Folder
─────────────── ← Divider
[Existing menu items...]
Parent Path Calculation:
- Components: Extract parent folder from component path
- Folders: Use folder path directly
- Root-level items get "/" as parent path
Template System:
Uses existing ComponentTemplates.instance.getTemplates({ forRuntimeType: 'browser' }) to fetch available templates dynamically.
Testing
- ✅ Compiled successfully with no errors
- ✅ Typescript types all correct
- ⏳ Manual testing pending (see Testing Notes below)
Testing Notes
To manually test in the Electron app:
- Open a project in Noodl
- Right-click on any component in the ComponentsPanel
- Verify "Create" items appear at the top of the menu
- Right-click on any folder
- Verify same "Create" items appear
- Test creating each type:
- Page Component (opens page template popup)
- Visual Component (opens name input)
- Logic Component (opens name input)
- Cloud Function (opens name input)
- Folder (shows "next phase" toast)
Known Limitations
Folder Creation: Currently shows a toast message indicating it will be available in the next phase. The infrastructure for virtual folder management needs to be completed as part of the sheet system.
Next Steps
Ready to proceed with Phase 2: Sheet System Backend
[December 26, 2025] - Task Created
Summary
Created comprehensive implementation plan for completing the ComponentsPanel feature set. This task builds on TASK-004B (ComponentsPanel React Migration) to add the remaining features from the legacy implementation.
Task Scope
Phase 1: Enhanced Context Menus (2-3 hours)
- Add "Create" submenus to component and folder context menus
- Wire up all component templates + folder creation
- Full undo support
Phase 2: Sheet System Backend (2 hours)
- Sheet detection and filtering logic
- Sheet state management
- Sheet CRUD operations with undo
Phase 3: Sheet Selector UI (2-3 hours)
- Dropdown component for sheet selection
- Sheet list with management actions
- Integration into ComponentsPanel header
Phase 4: Sheet Management Actions (1-2 hours)
- Create sheet with popup
- Rename sheet with validation
- Delete sheet with confirmation
- Optional: drag-drop between sheets
Phase 5: Integration & Testing (1 hour)
- Comprehensive testing
- Documentation updates
- Edge case verification
Research Findings
From analyzing the legacy ComponentsPanel.ts.legacy:
Context Menu Structure:
// Component context menu has:
- Create submenu:
- Page
- Visual Component
- Logic Component
- Cloud Function
- (divider)
- Folder
- (divider)
- Make Home (conditional)
- Rename
- Duplicate
- Delete
Sheet System:
- Sheets are top-level folders starting with
# - Default sheet = components not in any
#folder - Sheet selector shows all non-hidden sheets
- Each sheet (except Default) has rename/delete actions
- Hidden sheets filtered via
hideSheetsoption - Locked sheets via
lockCurrentSheetNameoption
Key Methods from Legacy:
onAddSheetClicked()- Create new sheetselectSheet(sheet)- Switch to sheetonSheetActionsClicked()- Sheet menu (rename/delete)renderSheets()- Render sheet listgetSheetForComponentWithName()- Find component's sheetonComponentActionsClicked()- Has "Create" submenu logiconFolderActionsClicked()- Has "Create" submenu logic
Technical Notes
PopupMenu Enhancement: Need to check if PopupMenu supports nested submenus. If not, may use flat menu with dividers as alternative.
Sheet Filtering:
Must filter tree data by current sheet. Default sheet shows components NOT in any # folder. Named sheets show ONLY components in that sheet's folder.
UndoQueue Pattern:
All operations must use UndoQueue.instance.pushAndDo() - the proven pattern from TASK-004B.
Component Path Updates: Renaming sheets requires updating ALL component paths in that sheet, similar to folder rename logic.
Files to Create
packages/noodl-editor/src/editor/src/views/panels/ComponentsPanelNew/
├── components/
│ ├── SheetSelector.tsx # NEW
│ └── SheetSelector.module.scss # NEW
└── hooks/
└── useSheetManagement.ts # NEW
Files to Modify
packages/noodl-editor/src/editor/src/views/panels/ComponentsPanelNew/
├── ComponentsPanelReact.tsx # Add SheetSelector
├── components/
│ ├── ComponentItem.tsx # Enhance context menu
│ └── FolderItem.tsx # Enhance context menu
├── hooks/
│ ├── useComponentsPanel.ts # Add sheet filtering
│ └── useComponentActions.ts # Add sheet actions
└── types.ts # Add Sheet types
Status
Current Status: NOT STARTED
Completion: 0%
Checklist:
- Phase 1: Enhanced Context Menus
- Phase 2: Sheet System Backend
- Phase 3: Sheet Selector UI
- Phase 4: Sheet Management Actions
- Phase 5: Integration & Testing
Next Steps
When starting work on this task:
- Investigate PopupMenu: Check if nested menus are supported
- Start with Phase 1: Context menu enhancements (lowest risk)
- Build foundation in Phase 2: Sheet detection and filtering
- Create UI in Phase 3: SheetSelector component
- Wire actions in Phase 4: Sheet management operations
- Test thoroughly in Phase 5: All features and edge cases
Related Tasks
- TASK-004B: ComponentsPanel React Migration (COMPLETE ✅) - Foundation
- Future: This completes ComponentsPanel, unblocking potential TASK-004 (migration badges/filters)
Template for Future Entries
## [YYYY-MM-DD] - Session N: [Phase Name]
### Summary
Brief description of what was accomplished
### Files Created/Modified
List of changes with key details
### Testing Notes
What was tested and results
### Challenges & Solutions
Any issues encountered and how they were resolved
### Next Steps
What needs to be done next
Last Updated: December 26, 2025