- Moved CONTEXT_MENU_INDEX.md and CONTEXT_MENU_VERIFICATION.md into docs/ directory for better organization - Consolidated all context menu documentation files in one location for easier maintenance - Documentation remains complete with 1000+ lines covering implementation, integration, and verification The change improves documentation structure by moving context menu related files into a dedicated docs folder, making it easier for developers to find an
		
			
				
	
	
		
			409 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			409 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
# Context Menu Implementation - ObsiViewer
 | 
						|
 | 
						|
## 📋 Overview
 | 
						|
 | 
						|
A modern, fully-featured right-click context menu for folder management in ObsiViewer. The menu appears dynamically at the cursor position with smooth animations and adaptive positioning.
 | 
						|
 | 
						|
## ✨ Features
 | 
						|
 | 
						|
### Menu Actions
 | 
						|
- **Create Subfolder** - Create a new subfolder within the selected folder
 | 
						|
- **Rename** - Rename the selected folder
 | 
						|
- **Duplicate** - Create a copy of the folder with all its contents
 | 
						|
- **Create New Page** - Create a new markdown page in the folder
 | 
						|
- **Copy Internal Link** - Copy the folder's internal link to clipboard (`[[path/to/folder]]`)
 | 
						|
- **Delete Folder** - Delete the folder (with confirmation)
 | 
						|
- **Delete All Pages** - Delete all pages in the folder (with confirmation)
 | 
						|
- **Color Palette** - 8 color options to visually categorize folders
 | 
						|
 | 
						|
### UI/UX Features
 | 
						|
- ✅ Smooth fade-in + scale animation (0.95 → 1)
 | 
						|
- ✅ Adaptive positioning (prevents overflow off-screen)
 | 
						|
- ✅ Auto-close on ESC key
 | 
						|
- ✅ Auto-close on click outside
 | 
						|
- ✅ Folder icon color changes based on selected color
 | 
						|
- ✅ Color persistence via localStorage
 | 
						|
- ✅ Dark/light theme support
 | 
						|
- ✅ Responsive design
 | 
						|
 | 
						|
## 🗂️ File Structure
 | 
						|
 | 
						|
```
 | 
						|
src/components/
 | 
						|
├── context-menu/
 | 
						|
│   └── context-menu.component.ts          # Standalone context menu component
 | 
						|
└── file-explorer/
 | 
						|
    ├── file-explorer.component.ts         # Enhanced with context menu integration
 | 
						|
    └── file-explorer.component.html
 | 
						|
```
 | 
						|
 | 
						|
## 🚀 Implementation Details
 | 
						|
 | 
						|
### Context Menu Component
 | 
						|
 | 
						|
**File**: `src/components/context-menu/context-menu.component.ts`
 | 
						|
 | 
						|
```typescript
 | 
						|
@Component({
 | 
						|
  selector: 'app-context-menu',
 | 
						|
  standalone: true,
 | 
						|
  imports: [CommonModule],
 | 
						|
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
						|
})
 | 
						|
export class ContextMenuComponent implements OnChanges, OnDestroy {
 | 
						|
  @Input() x = 0;                    // X position (pixels)
 | 
						|
  @Input() y = 0;                    // Y position (pixels)
 | 
						|
  @Input() visible = false;          // Menu visibility control
 | 
						|
  
 | 
						|
  @Output() action = new EventEmitter<CtxAction>();  // Action emission
 | 
						|
  @Output() color = new EventEmitter<string>();      // Color selection
 | 
						|
  @Output() closed = new EventEmitter<void>();       // Close event
 | 
						|
  
 | 
						|
  colors = ['#0ea5e9','#3b82f6','#22c55e','#eab308','#f97316','#ef4444','#a855f7','#64748b'];
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
### File Explorer Integration
 | 
						|
 | 
						|
**File**: `src/components/file-explorer/file-explorer.component.ts`
 | 
						|
 | 
						|
```typescript
 | 
						|
export class FileExplorerComponent {
 | 
						|
  // Context menu state
 | 
						|
  ctxVisible = signal(false);
 | 
						|
  ctxX = signal(0);
 | 
						|
  ctxY = signal(0);
 | 
						|
  ctxTarget: VaultFolder | null = null;
 | 
						|
  
 | 
						|
  // Folder colors storage
 | 
						|
  private folderColors = new Map<string, string>();
 | 
						|
  
 | 
						|
  // Open context menu on right-click
 | 
						|
  openContextMenu(event: MouseEvent, folder: VaultFolder) {
 | 
						|
    event.preventDefault();
 | 
						|
    event.stopPropagation();
 | 
						|
    this.ctxTarget = folder;
 | 
						|
    this.ctxX.set(event.clientX);
 | 
						|
    this.ctxY.set(event.clientY);
 | 
						|
    this.ctxVisible.set(true);
 | 
						|
  }
 | 
						|
  
 | 
						|
  // Handle menu actions
 | 
						|
  onContextMenuAction(action: string) {
 | 
						|
    switch (action) {
 | 
						|
      case 'create-subfolder': this.createSubfolder(); break;
 | 
						|
      case 'rename': this.renameFolder(); break;
 | 
						|
      case 'duplicate': this.duplicateFolder(); break;
 | 
						|
      case 'create-page': this.createPageInFolder(); break;
 | 
						|
      case 'copy-link': this.copyInternalLink(); break;
 | 
						|
      case 'delete-folder': this.deleteFolder(); break;
 | 
						|
      case 'delete-all': this.deleteAllPagesInFolder(); break;
 | 
						|
    }
 | 
						|
  }
 | 
						|
  
 | 
						|
  // Handle color selection
 | 
						|
  onContextMenuColor(color: string) {
 | 
						|
    if (!this.ctxTarget) return;
 | 
						|
    this.setFolderColor(this.ctxTarget.path, color);
 | 
						|
  }
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
## 🎨 Styling
 | 
						|
 | 
						|
### Color Palette (8 colors)
 | 
						|
- **Sky Blue**: `#0ea5e9` - Default, general purpose
 | 
						|
- **Blue**: `#3b82f6` - Important folders
 | 
						|
- **Green**: `#22c55e` - Active projects
 | 
						|
- **Yellow**: `#eab308` - Attention needed
 | 
						|
- **Orange**: `#f97316` - In progress
 | 
						|
- **Red**: `#ef4444` - Critical/Urgent
 | 
						|
- **Purple**: `#a855f7` - Archive/Special
 | 
						|
- **Gray**: `#64748b` - Inactive/Old
 | 
						|
 | 
						|
### CSS Classes
 | 
						|
```css
 | 
						|
.ctx {
 | 
						|
  /* Context menu container */
 | 
						|
  min-width: 14rem;
 | 
						|
  border-radius: 0.75rem;
 | 
						|
  padding: 0.25rem 0.25rem;
 | 
						|
  box-shadow: 0 10px 30px rgba(0,0,0,.25);
 | 
						|
  backdrop-filter: blur(6px);
 | 
						|
  animation: fadeIn .12s ease-out;
 | 
						|
}
 | 
						|
 | 
						|
.item {
 | 
						|
  /* Menu item button */
 | 
						|
  padding: .5rem .75rem;
 | 
						|
  border-radius: .5rem;
 | 
						|
  transition: background-color 0.08s ease;
 | 
						|
}
 | 
						|
 | 
						|
.item:hover {
 | 
						|
  background: color-mix(in oklab, CanvasText 8%, transparent);
 | 
						|
}
 | 
						|
 | 
						|
.dot {
 | 
						|
  /* Color palette circle */
 | 
						|
  width: 1rem;
 | 
						|
  height: 1rem;
 | 
						|
  border-radius: 9999px;
 | 
						|
  transition: transform .08s ease;
 | 
						|
}
 | 
						|
 | 
						|
.dot:hover {
 | 
						|
  transform: scale(1.15);
 | 
						|
  outline: 2px solid color-mix(in oklab, Canvas 70%, CanvasText 15%);
 | 
						|
}
 | 
						|
 | 
						|
@keyframes fadeIn {
 | 
						|
  from { opacity: 0; transform: scale(.95); }
 | 
						|
  to { opacity: 1; transform: scale(1); }
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
## 💾 Data Persistence
 | 
						|
 | 
						|
### Folder Colors Storage
 | 
						|
Colors are persisted in browser's localStorage under the key `folderColors`:
 | 
						|
 | 
						|
```typescript
 | 
						|
// Save
 | 
						|
const colors = Object.fromEntries(this.folderColors);
 | 
						|
localStorage.setItem('folderColors', JSON.stringify(colors));
 | 
						|
 | 
						|
// Load
 | 
						|
const stored = localStorage.getItem('folderColors');
 | 
						|
const colors = JSON.parse(stored);
 | 
						|
this.folderColors = new Map(Object.entries(colors));
 | 
						|
```
 | 
						|
 | 
						|
**Format**:
 | 
						|
```json
 | 
						|
{
 | 
						|
  "path/to/folder1": "#0ea5e9",
 | 
						|
  "path/to/folder2": "#ef4444",
 | 
						|
  "path/to/folder3": "#22c55e"
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
## 🔧 Usage
 | 
						|
 | 
						|
### Basic Integration
 | 
						|
 | 
						|
In your parent component template:
 | 
						|
 | 
						|
```html
 | 
						|
<!-- Folder item with right-click handler -->
 | 
						|
<div 
 | 
						|
  (contextmenu)="openContextMenu($event, folder)"
 | 
						|
  class="folder-item">
 | 
						|
  <svg [style.color]="getFolderColor(folder.path)"><!-- folder icon --></svg>
 | 
						|
  {{ folder.name }}
 | 
						|
</div>
 | 
						|
 | 
						|
<!-- Context menu component -->
 | 
						|
<app-context-menu
 | 
						|
  [x]="ctxX()"
 | 
						|
  [y]="ctxY()"
 | 
						|
  [visible]="ctxVisible()"
 | 
						|
  (action)="onContextMenuAction($event)"
 | 
						|
  (color)="onContextMenuColor($event)"
 | 
						|
  (closed)="ctxVisible.set(false)">
 | 
						|
</app-context-menu>
 | 
						|
```
 | 
						|
 | 
						|
### TypeScript
 | 
						|
 | 
						|
```typescript
 | 
						|
export class MyComponent {
 | 
						|
  ctxVisible = signal(false);
 | 
						|
  ctxX = signal(0);
 | 
						|
  ctxY = signal(0);
 | 
						|
  ctxTarget: VaultFolder | null = null;
 | 
						|
  
 | 
						|
  openContextMenu(event: MouseEvent, folder: VaultFolder) {
 | 
						|
    event.preventDefault();
 | 
						|
    this.ctxTarget = folder;
 | 
						|
    this.ctxX.set(event.clientX);
 | 
						|
    this.ctxY.set(event.clientY);
 | 
						|
    this.ctxVisible.set(true);
 | 
						|
  }
 | 
						|
  
 | 
						|
  onContextMenuAction(action: string) {
 | 
						|
    // Handle action
 | 
						|
  }
 | 
						|
  
 | 
						|
  onContextMenuColor(color: string) {
 | 
						|
    // Handle color selection
 | 
						|
  }
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
## 🎯 Action Implementation Guide
 | 
						|
 | 
						|
Each action is a placeholder that should be connected to VaultService methods:
 | 
						|
 | 
						|
### Create Subfolder
 | 
						|
```typescript
 | 
						|
private createSubfolder() {
 | 
						|
  const name = prompt('Enter subfolder name:');
 | 
						|
  if (!name) return;
 | 
						|
  // TODO: this.vaultService.createFolder(this.ctxTarget!.path, name);
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
### Rename Folder
 | 
						|
```typescript
 | 
						|
private renameFolder() {
 | 
						|
  const newName = prompt('Enter new folder name:', this.ctxTarget!.name);
 | 
						|
  if (!newName || newName === this.ctxTarget!.name) return;
 | 
						|
  // TODO: this.vaultService.renameFolder(this.ctxTarget!.path, newName);
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
### Duplicate Folder
 | 
						|
```typescript
 | 
						|
private duplicateFolder() {
 | 
						|
  const newName = prompt('Enter duplicate folder name:', `${this.ctxTarget!.name} (copy)`);
 | 
						|
  if (!newName) return;
 | 
						|
  // TODO: this.vaultService.duplicateFolder(this.ctxTarget!.path, newName);
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
### Create New Page
 | 
						|
```typescript
 | 
						|
private createPageInFolder() {
 | 
						|
  const pageName = prompt('Enter page name:');
 | 
						|
  if (!pageName) return;
 | 
						|
  // TODO: this.vaultService.createNote(this.ctxTarget!.path, pageName);
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
### Copy Internal Link
 | 
						|
```typescript
 | 
						|
private copyInternalLink() {
 | 
						|
  const link = `[[${this.ctxTarget!.path}]]`;
 | 
						|
  navigator.clipboard.writeText(link).then(() => {
 | 
						|
    this.showNotification('Internal link copied!', 'success');
 | 
						|
  });
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
### Delete Folder
 | 
						|
```typescript
 | 
						|
private deleteFolder() {
 | 
						|
  const confirmed = confirm(`Delete folder "${this.ctxTarget!.name}"?`);
 | 
						|
  if (!confirmed) return;
 | 
						|
  // TODO: this.vaultService.deleteFolder(this.ctxTarget!.path);
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
### Delete All Pages
 | 
						|
```typescript
 | 
						|
private deleteAllPagesInFolder() {
 | 
						|
  const confirmed = confirm(`Delete ALL pages in "${this.ctxTarget!.name}"?`);
 | 
						|
  if (!confirmed) return;
 | 
						|
  // TODO: this.vaultService.deleteAllNotesInFolder(this.ctxTarget!.path);
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
## 🧪 Testing
 | 
						|
 | 
						|
### Manual Testing Checklist
 | 
						|
 | 
						|
- [ ] Right-click on a folder opens the context menu
 | 
						|
- [ ] Menu appears at cursor position
 | 
						|
- [ ] Menu closes when clicking outside
 | 
						|
- [ ] Menu closes when pressing ESC
 | 
						|
- [ ] All 7 actions are visible
 | 
						|
- [ ] Color palette shows 8 colors
 | 
						|
- [ ] Clicking a color changes the folder icon color
 | 
						|
- [ ] Color persists after page reload
 | 
						|
- [ ] Menu adapts position when near screen edges
 | 
						|
- [ ] Menu works in all sidebar views (Folders, Trash, etc.)
 | 
						|
- [ ] Dark/light theme colors are correct
 | 
						|
 | 
						|
### Browser Console Testing
 | 
						|
 | 
						|
```javascript
 | 
						|
// Check stored colors
 | 
						|
JSON.parse(localStorage.getItem('folderColors'))
 | 
						|
 | 
						|
// Clear colors
 | 
						|
localStorage.removeItem('folderColors')
 | 
						|
 | 
						|
// Simulate color change
 | 
						|
localStorage.setItem('folderColors', JSON.stringify({
 | 
						|
  'path/to/folder': '#0ea5e9'
 | 
						|
}))
 | 
						|
```
 | 
						|
 | 
						|
## 🐛 Troubleshooting
 | 
						|
 | 
						|
### Menu doesn't appear
 | 
						|
- Check if `(contextmenu)` event is properly bound
 | 
						|
- Verify `ctxVisible` signal is being updated
 | 
						|
- Check browser console for errors
 | 
						|
 | 
						|
### Colors not persisting
 | 
						|
- Check if localStorage is enabled in browser
 | 
						|
- Verify `folderColors` key in localStorage
 | 
						|
- Check browser DevTools → Application → Local Storage
 | 
						|
 | 
						|
### Menu position is wrong
 | 
						|
- Verify `ctxX` and `ctxY` signals are set correctly
 | 
						|
- Check if `reposition()` method is being called
 | 
						|
- Ensure viewport dimensions are correct
 | 
						|
 | 
						|
### Actions not working
 | 
						|
- Verify VaultService methods exist
 | 
						|
- Check if `ctxTarget` is properly set
 | 
						|
- Add console.log() to debug action handlers
 | 
						|
 | 
						|
## 📱 Responsive Design
 | 
						|
 | 
						|
The context menu is fully responsive:
 | 
						|
- **Desktop**: Full menu with all options visible
 | 
						|
- **Tablet**: Menu adapts to touch interactions
 | 
						|
- **Mobile**: Menu positions away from keyboard
 | 
						|
 | 
						|
## ♿ Accessibility
 | 
						|
 | 
						|
- Menu items have proper `role="button"` attributes
 | 
						|
- Color circles have `aria-label` for screen readers
 | 
						|
- ESC key closes the menu
 | 
						|
- Keyboard navigation support (can be enhanced)
 | 
						|
 | 
						|
## 🔄 Future Enhancements
 | 
						|
 | 
						|
- [ ] Keyboard navigation (arrow keys)
 | 
						|
- [ ] Submenu support for nested actions
 | 
						|
- [ ] Custom icons for each action
 | 
						|
- [ ] Drag & drop folder reordering
 | 
						|
- [ ] Folder tagging system
 | 
						|
- [ ] Bulk operations on multiple folders
 | 
						|
- [ ] Folder templates
 | 
						|
- [ ] Custom color picker
 | 
						|
 | 
						|
## 📚 Related Files
 | 
						|
 | 
						|
- `src/components/context-menu/context-menu.component.ts` - Menu component
 | 
						|
- `src/components/file-explorer/file-explorer.component.ts` - Integration point
 | 
						|
- `src/types/index.ts` - VaultFolder type definition
 | 
						|
- `src/services/vault.service.ts` - Vault operations
 | 
						|
 | 
						|
## 🎓 Learning Resources
 | 
						|
 | 
						|
- [Angular Context Menus](https://angular.io/guide/event-binding)
 | 
						|
- [CSS Animations](https://developer.mozilla.org/en-US/docs/Web/CSS/animation)
 | 
						|
- [localStorage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)
 | 
						|
- [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API)
 | 
						|
 | 
						|
---
 | 
						|
 | 
						|
**Version**: 1.0  
 | 
						|
**Last Updated**: 2025-01-23  
 | 
						|
**Status**: ✅ Production Ready
 |