@@ -379,13 +379,143 @@ function noteApp() {
379379 this . folderTree = { ...tree } ;
380380 } ,
381381
382+ // Render folder recursively (helper for deep nesting)
383+ renderFolderRecursive ( folder , level = 0 ) {
384+ if ( ! folder ) return '' ;
385+
386+ let html = '' ;
387+ const baseIndent = level * 12 ;
388+
389+ // First, render child folders (if any)
390+ if ( folder . children && Object . keys ( folder . children ) . length > 0 ) {
391+ const children = Object . entries ( folder . children ) . sort ( ( a , b ) =>
392+ a [ 1 ] . name . toLowerCase ( ) . localeCompare ( b [ 1 ] . name . toLowerCase ( ) )
393+ ) ;
394+
395+ children . forEach ( ( [ childKey , childFolder ] ) => {
396+ const isExpanded = this . expandedFolders . has ( childFolder . path ) ;
397+
398+ // Folder header HTML (no margin - it's inside folder-contents which provides the indent)
399+ html += `
400+ <div>
401+ <div
402+ draggable="true"
403+ x-data="{}"
404+ @dragstart="onFolderDragStart('${ childFolder . path . replace ( / ' / g, "\\'" ) } ' )"
405+ @dragend="onFolderDragEnd()"
406+ @dragover.prevent
407+ @drop.stop="onFolderDrop('${ childFolder . path . replace ( / ' / g, "\\'" ) } ')"
408+ class="folder-item px-2 py-2 mb-1 text-sm rounded transition-all relative"
409+ style="color: var(--text-primary); cursor: pointer;"
410+ :class="draggedNote || draggedFolder ? 'border-2 border-dashed border-accent-primary bg-accent-light' : 'border-2 border-transparent'"
411+ @mouseover="if(!draggedNote && !draggedFolder) $el.style.backgroundColor='var(--bg-hover)'"
412+ @mouseout="if(!draggedNote && !draggedFolder) $el.style.backgroundColor='transparent'"
413+ @click="toggleFolder('${ childFolder . path . replace ( / ' / g, "\\'" ) } ')"
414+ >
415+ <div class="flex items-center gap-1">
416+ <button
417+ class="flex-shrink-0 w-4 h-4 flex items-center justify-center"
418+ style="color: var(--text-tertiary); cursor: pointer; transition: transform 0.2s; pointer-events: none; ${ isExpanded ? 'transform: rotate(90deg);' : '' } "
419+ >
420+ <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
421+ <path d="M6 4l4 4-4 4V4z"/>
422+ </svg>
423+ </button>
424+ <span class="flex items-center gap-1 flex-1" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; pointer-events: none;">
425+ <span>${ childFolder . name } </span>
426+ ${ childFolder . notes . length === 0 && ( ! childFolder . children || Object . keys ( childFolder . children ) . length === 0 ) ? '<span class="text-xs" style="color: var(--text-tertiary); font-weight: 400;">(empty)</span>' : '' }
427+ </span>
428+ </div>
429+ <div class="hover-buttons flex gap-1 transition-opacity absolute right-2 top-1/2 transform -translate-y-1/2" style="opacity: 0; pointer-events: none; background: linear-gradient(to right, transparent, var(--bg-hover) 20%, var(--bg-hover)); padding-left: 20px;" @click.stop>
430+ <button
431+ @click="createNoteInFolder('${ childFolder . path . replace ( / ' / g, "\\'" ) } ')"
432+ class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
433+ style="background-color: var(--bg-tertiary); color: var(--text-secondary);"
434+ title="New note in this folder"
435+ >📄</button>
436+ <button
437+ @click="createNewFolder('${ childFolder . path . replace ( / ' / g, "\\'" ) } ')"
438+ class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
439+ style="background-color: var(--bg-tertiary); color: var(--text-secondary);"
440+ title="New subfolder"
441+ >📁</button>
442+ <button
443+ @click="renameFolder('${ childFolder . path . replace ( / ' / g, "\\'" ) } ', '${ childFolder . name . replace ( / ' / g, "\\'" ) } ')"
444+ class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
445+ style="background-color: var(--bg-tertiary); color: var(--text-secondary);"
446+ title="Rename folder"
447+ >✏️</button>
448+ <button
449+ @click="deleteFolder('${ childFolder . path . replace ( / ' / g, "\\'" ) } ', '${ childFolder . name . replace ( / ' / g, "\\'" ) } ')"
450+ class="px-1 py-0.5 text-xs rounded hover:brightness-110"
451+ style="color: var(--error);"
452+ title="Delete folder and all contents"
453+ >
454+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
455+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
456+ </svg>
457+ </button>
458+ </div>
459+ </div>
460+ ` ;
461+
462+ // If expanded, recursively render this folder's contents (notes + subfolders)
463+ if ( isExpanded ) {
464+ html += `<div class="folder-contents" style="padding-left: 12px;">` ;
465+ html += this . renderFolderRecursive ( childFolder , 0 ) ; // Reset level to 0 for relative positioning
466+ html += `</div>` ;
467+ }
468+
469+ html += `</div>` ;
470+ } ) ;
471+ }
472+
473+ // Then, render notes in this folder (after subfolders)
474+ if ( folder . notes && folder . notes . length > 0 ) {
475+ folder . notes . forEach ( note => {
476+ const isCurrentNote = this . currentNote === note . path ;
477+ html += `
478+ <div
479+ draggable="true"
480+ x-data="{}"
481+ @dragstart="onNoteDragStart('${ note . path . replace ( / ' / g, "\\'" ) } ', $event)"
482+ @dragend="onNoteDragEnd()"
483+ @click="loadNote('${ note . path . replace ( / ' / g, "\\'" ) } ')"
484+ class="note-item px-3 py-2 mb-1 text-sm rounded relative"
485+ style="${ isCurrentNote ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);' } cursor: pointer;"
486+ @mouseover="if('${ note . path } ' !== currentNote) $el.style.backgroundColor='var(--bg-hover)'"
487+ @mouseout="if('${ note . path } ' !== currentNote) $el.style.backgroundColor='transparent'"
488+ >
489+ <span class="truncate">${ note . name } </span>
490+ <button
491+ @click.stop="deleteNote('${ note . path . replace ( / ' / g, "\\'" ) } ', '${ note . name . replace ( / ' / g, "\\'" ) } ')"
492+ class="note-delete-btn absolute right-2 top-1/2 transform -translate-y-1/2 px-1 py-0.5 text-xs rounded hover:brightness-110 transition-opacity"
493+ style="opacity: 0; color: var(--error);"
494+ title="Delete note"
495+ >
496+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
497+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
498+ </svg>
499+ </button>
500+ </div>
501+ ` ;
502+ } ) ;
503+ }
504+
505+ return html ;
506+ } ,
507+
382508 // Toggle folder expansion
383509 toggleFolder ( folderPath ) {
384510 if ( this . expandedFolders . has ( folderPath ) ) {
385511 this . expandedFolders . delete ( folderPath ) ;
386512 } else {
387513 this . expandedFolders . add ( folderPath ) ;
388514 }
515+ // Force Alpine reactivity by creating new Set reference
516+ this . expandedFolders = new Set ( this . expandedFolders ) ;
517+ // Also trigger folderTree reactivity to re-render x-html
518+ this . folderTree = { ...this . folderTree } ;
389519 } ,
390520
391521 // Check if folder is expanded
0 commit comments