"use client"; import { useEffect, useMemo, useState, useRef, type DragEvent } from 'react'; import { createPortal } from 'react-dom'; import { Check, X, ArrowLeft, Plus, Trash2, Edit2, Lock } from 'lucide-react'; import type { Node } from '@/types/database'; import ConfirmDialog from '../common/ConfirmDialog'; import InputDialog from '../common/InputDialog'; import { getNodeIcon } from '@/utils/nodeIcons'; import LucideIconPicker, { DynamicIcon } from '../common/LucideIconPicker'; import { useDimensionIcons } from '@/context/DimensionIconsContext'; import { usePersistentState } from '@/hooks/usePersistentState'; interface DimensionSummary { dimension: string; count: number; isPriority: boolean; description?: string | null; } interface FolderViewOverlayProps { onClose: () => void; onNodeOpen: (nodeId: number) => void; refreshToken: number; onDataChanged?: () => void; onDimensionSelect?: (dimensionName: string | null) => void; toolbarHost?: HTMLDivElement | null; } const PAGE_SIZE = 100; export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, onDataChanged, onDimensionSelect, toolbarHost }: FolderViewOverlayProps) { const [view, setView] = useState<'dimensions' | 'nodes'>('dimensions'); const [dimensions, setDimensions] = useState([]); const [dimensionsLoading, setDimensionsLoading] = useState(true); const [dimensionsError, setDimensionsError] = useState(null); const [selectedDimension, setSelectedDimension] = useState(null); const [nodes, setNodes] = useState([]); const [nodesLoading, setNodesLoading] = useState(false); const [nodesError, setNodesError] = useState(null); const [hasMoreNodes, setHasMoreNodes] = useState(false); const [nodeOffset, setNodeOffset] = useState(0); const [deletingDimension, setDeletingDimension] = useState(null); const [dimensionPendingDelete, setDimensionPendingDelete] = useState(null); const [dragHoverDimension, setDragHoverDimension] = useState(null); const [editingDescription, setEditingDescription] = useState(false); const [editDescriptionText, setEditDescriptionText] = useState(''); const [editingDimensionName, setEditingDimensionName] = useState(false); const [editDimensionNameText, setEditDimensionNameText] = useState(''); const [showAddDimensionDialog, setShowAddDimensionDialog] = useState(false); const draggedNodeRef = useRef<{ id: number; title?: string; dimensions?: string[] } | null>(null); // Kanban columns state (global, persisted) const [kanbanColumns, setKanbanColumns] = usePersistentState<{ dimension: string; order: number }[]>( 'ui.kanbanColumns.global', [] ); // Kanban-specific drag states const [draggedNodeId, setDraggedNodeId] = useState(null); const [draggedFromColumn, setDraggedFromColumn] = useState(null); const [dragOverColumn, setDragOverColumn] = useState(null); const [showKanbanColumnPicker, setShowKanbanColumnPicker] = useState(false); const [kanbanSearchQuery, setKanbanSearchQuery] = useState(''); // Kanban drag-and-drop state const [draggedNode, setDraggedNode] = useState<{ id: number; fromDimension: string } | null>(null); const [dropTargetDimension, setDropTargetDimension] = useState(null); // Kanban column reordering state const [draggedColumn, setDraggedColumn] = useState(null); const [columnDropTarget, setColumnDropTarget] = useState(null); // Node priority ordering within dimensions (persisted) const [dimensionOrders, setDimensionOrders] = usePersistentState>('ui.dimensionOrders', {}); // Dimension icons from shared context const { dimensionIcons, setDimensionIcons } = useDimensionIcons(); // Dimension edit modal state const [editingDimensionModal, setEditingDimensionModal] = useState(null); const [editModalName, setEditModalName] = useState(''); const [editModalDescription, setEditModalDescription] = useState(''); const [editModalIcon, setEditModalIcon] = useState('Folder'); const [savingDimensionEdit, setSavingDimensionEdit] = useState(false); const [editModalNameError, setEditModalNameError] = useState(''); // Within-dimension reorder drag state const [reorderDrag, setReorderDrag] = useState<{ nodeId: number; dimension: string; index: number } | null>(null); const [reorderDropIndex, setReorderDropIndex] = useState(null); useEffect(() => { fetchDimensions(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (view === 'dimensions') { fetchDimensions(); } else if (selectedDimension) { fetchNodes(true); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [refreshToken]); useEffect(() => { if (!selectedDimension) return; fetchNodes(true); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedDimension?.dimension]); // Sort nodes by their priority order within a dimension const sortNodesByDimensionOrder = (nodes: Node[], dimension: string): Node[] => { const order = dimensionOrders[dimension] || []; return [...nodes].sort((a, b) => { const aIndex = order.indexOf(a.id); const bIndex = order.indexOf(b.id); // Nodes in order array come first, sorted by their position // Nodes not in order array go to end, sorted by ID if (aIndex === -1 && bIndex === -1) return a.id - b.id; if (aIndex === -1) return 1; if (bIndex === -1) return -1; return aIndex - bIndex; }); }; // Handle reordering a node within a dimension const handleReorderDrop = (dimension: string, fromIndex: number, toIndex: number, nodes: Node[]) => { if (fromIndex === toIndex) return; // Get current order or create from current node order const currentOrder = dimensionOrders[dimension] || nodes.map(n => n.id); const nodeIds = [...currentOrder]; // Ensure all nodes are in the order array nodes.forEach(n => { if (!nodeIds.includes(n.id)) { nodeIds.push(n.id); } }); // Move the node from fromIndex to toIndex const [movedId] = nodeIds.splice(fromIndex, 1); nodeIds.splice(toIndex, 0, movedId); // Update the dimension orders setDimensionOrders({ ...dimensionOrders, [dimension]: nodeIds }); setReorderDrag(null); setReorderDropIndex(null); }; const sortedDimensions = useMemo(() => { return [...dimensions].sort((a, b) => { if (a.isPriority !== b.isPriority) { return a.isPriority ? -1 : 1; } return a.dimension.localeCompare(b.dimension); }); }, [dimensions]); const fetchDimensions = async () => { setDimensionsLoading(true); setDimensionsError(null); try { const response = await fetch('/api/dimensions/popular'); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to fetch dimensions'); } setDimensions(data.data || []); } catch (error) { console.error('Error fetching dimensions:', error); setDimensionsError('Failed to load dimensions'); } finally { setDimensionsLoading(false); } }; const fetchNodes = async (reset = false) => { if (!selectedDimension) return; setNodesLoading(true); setNodesError(null); try { const offset = reset ? 0 : nodeOffset; const response = await fetch(`/api/nodes?dimensions=${encodeURIComponent(selectedDimension.dimension)}&limit=${PAGE_SIZE}&offset=${offset}`); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to fetch nodes'); } const fetchedNodes: Node[] = data.data || []; setNodes((prev) => reset ? fetchedNodes : [...prev, ...fetchedNodes]); setHasMoreNodes(fetchedNodes.length === PAGE_SIZE); setNodeOffset(offset + fetchedNodes.length); } catch (error) { console.error('Error fetching nodes:', error); setNodesError('Failed to load nodes'); } finally { setNodesLoading(false); } }; const handleSelectDimension = (dimension: DimensionSummary) => { onDimensionSelect?.(dimension.dimension); }; const handleBackToDimensions = () => { setView('dimensions'); setSelectedDimension(null); setNodes([]); setNodeOffset(0); setHasMoreNodes(false); onDimensionSelect?.(null); }; const handleAddDimension = async (name: string) => { if (!name || !name.trim()) return; try { const response = await fetch('/api/dimensions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name.trim() }) }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to create dimension'); } await fetchDimensions(); onDataChanged?.(); setShowAddDimensionDialog(false); } catch (error) { console.error('Error adding dimension:', error); alert('Failed to create dimension. Please try again.'); } }; const handleToggleLock = async (dimension: string) => { try { const response = await fetch('/api/dimensions/popular', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dimension }) }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to toggle dimension'); } await fetchDimensions(); onDataChanged?.(); } catch (error) { console.error('Error toggling lock:', error); alert('Failed to update lock state.'); } }; const handleDeleteDimension = async (dimension: string) => { setDeletingDimension(dimension); try { const response = await fetch(`/api/dimensions?name=${encodeURIComponent(dimension)}`, { method: 'DELETE' }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to delete dimension'); } if (selectedDimension?.dimension === dimension) { handleBackToDimensions(); } await fetchDimensions(); onDataChanged?.(); } catch (error) { console.error('Error deleting dimension:', error); alert('Failed to delete dimension. Please try again.'); } finally { setDeletingDimension((current) => (current === dimension ? null : current)); setDimensionPendingDelete((current) => (current === dimension ? null : current)); } }; const handleNodeTileDragStart = (event: DragEvent, node: Node) => { event.dataTransfer.effectAllowed = 'copyMove'; const nodeData = { id: node.id, title: node.title || 'Untitled', dimensions: node.dimensions || [] }; // Store in ref for webview compatibility (dataTransfer.getData can fail in Electron/Tauri) draggedNodeRef.current = nodeData; // Set multiple MIME types for different drop targets event.dataTransfer.setData('application/node-info', JSON.stringify(nodeData)); // For chat input drops - includes title for [NODE:id:"title"] token event.dataTransfer.setData('application/x-rah-node', JSON.stringify({ id: node.id, title: node.title || 'Untitled' })); // Fallback for browsers/webviews that only support text/plain event.dataTransfer.setData('text/plain', `[NODE:${node.id}:"${node.title || 'Untitled'}"]`); // Provide a compact drag preview so drop targets stay visible const preview = document.createElement('div'); preview.textContent = node.title || `Node #${node.id}`; preview.style.position = 'fixed'; preview.style.top = '-1000px'; preview.style.left = '-1000px'; preview.style.padding = '4px 8px'; preview.style.background = '#0f0f0f'; preview.style.color = '#f8fafc'; preview.style.fontSize = '11px'; preview.style.fontWeight = '600'; preview.style.borderRadius = '6px'; preview.style.border = '1px solid #1f1f1f'; document.body.appendChild(preview); event.dataTransfer.setDragImage(preview, 6, 6); setTimeout(() => { if (preview.parentNode) { preview.parentNode.removeChild(preview); } }, 0); }; const handleNodeTileDragEnd = () => { // Clear ref if drag ends without a drop draggedNodeRef.current = null; }; const handleDimensionDragOver = (event: DragEvent) => { if (event.dataTransfer.types.includes('application/node-info') || event.dataTransfer.types.includes('text/plain')) { event.preventDefault(); event.dataTransfer.dropEffect = 'copy'; } }; const handleDimensionDragEnter = (event: DragEvent, dimension: string) => { if (event.dataTransfer.types.includes('application/node-info') || event.dataTransfer.types.includes('text/plain')) { setDragHoverDimension(dimension); } }; const handleDimensionDragLeave = (event: DragEvent, dimension: string) => { if (dragHoverDimension === dimension) { setDragHoverDimension(null); } }; const handleNodeDropOnDimension = async (event: DragEvent, dimension: string) => { event.preventDefault(); event.stopPropagation(); // Try to get data from ref first (works in Electron/Tauri webviews) let payload: { id: number; dimensions?: string[] } | null = draggedNodeRef.current; // Fallback to dataTransfer for browser compatibility if (!payload) { const raw = event.dataTransfer.getData('application/node-info') || event.dataTransfer.getData('text/plain'); if (raw) { try { payload = JSON.parse(raw); } catch (e) { console.error('Failed to parse drag data:', e); } } } // Clear the ref draggedNodeRef.current = null; if (!payload?.id) { console.warn('No valid node data in drop event'); return; } try { const currentDimensions = payload.dimensions || []; if (currentDimensions.some((dim) => dim.toLowerCase() === dimension.toLowerCase())) { setDragHoverDimension(null); return; } const updatedDimensions = Array.from(new Set([...currentDimensions, dimension])); const response = await fetch(`/api/nodes/${payload.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dimensions: updatedDimensions }) }); if (!response.ok) { throw new Error('Failed to update node dimensions'); } if (selectedDimension?.dimension === dimension) { fetchNodes(true); } fetchDimensions(); onDataChanged?.(); } catch (error) { console.error('Error handling node drop:', error); alert('Failed to add dimension to node. Please try again.'); } finally { setDragHoverDimension(null); } }; const getContentPreview = (value?: string | null): string => { if (!value) return ''; const trimmed = value.trim(); if (trimmed.length <= 160) return trimmed; return `${trimmed.slice(0, 160)}…`; }; const handleEditDescription = () => { if (!selectedDimension) return; setEditingDescription(true); setEditDescriptionText(selectedDimension.description || ''); }; const handleSaveDescription = async () => { if (!selectedDimension) return; try { const response = await fetch('/api/dimensions', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: selectedDimension.dimension, description: editDescriptionText.trim() }) }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to update description'); } // Update the local selectedDimension with new description setSelectedDimension(prev => prev ? { ...prev, description: editDescriptionText.trim() } : null); await fetchDimensions(); onDataChanged?.(); setEditingDescription(false); setEditDescriptionText(''); } catch (error) { console.error('Error updating description:', error); alert('Failed to update description. Please try again.'); } }; const handleCancelDescription = () => { setEditingDescription(false); setEditDescriptionText(''); }; // Dimension edit modal handlers (from folder cards) const openDimensionEditModal = (dimension: DimensionSummary) => { setEditingDimensionModal(dimension); setEditModalName(dimension.dimension); setEditModalDescription(dimension.description || ''); setEditModalIcon(dimensionIcons[dimension.dimension] || 'Folder'); setEditModalNameError(''); }; const closeDimensionEditModal = () => { setEditingDimensionModal(null); setEditModalName(''); setEditModalDescription(''); setEditModalIcon('Folder'); setSavingDimensionEdit(false); setEditModalNameError(''); }; const saveDimensionEdit = async () => { if (!editingDimensionModal) return; const trimmedName = editModalName.trim(); if (!trimmedName) { setEditModalNameError('Name cannot be empty'); return; } const isRenamed = trimmedName !== editingDimensionModal.dimension; if (isRenamed) { const duplicate = dimensions.some(d => d.dimension.toLowerCase() === trimmedName.toLowerCase() && d.dimension !== editingDimensionModal.dimension); if (duplicate) { setEditModalNameError('A dimension with this name already exists'); return; } } setSavingDimensionEdit(true); setEditModalNameError(''); try { // Save description (and optionally rename) via API const body: Record = { description: editModalDescription.trim() }; if (isRenamed) { body.currentName = editingDimensionModal.dimension; body.newName = trimmedName; } else { body.name = editingDimensionModal.dimension; } const response = await fetch('/api/dimensions', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to update dimension'); } // Save icon — use new name if renamed const iconKey = isRenamed ? trimmedName : editingDimensionModal.dimension; setDimensionIcons(prev => { const next = { ...prev, [iconKey]: editModalIcon }; if (isRenamed) delete next[editingDimensionModal.dimension]; return next; }); await fetchDimensions(); onDataChanged?.(); closeDimensionEditModal(); } catch (error) { console.error('Error saving dimension:', error); alert('Failed to save dimension. Please try again.'); setSavingDimensionEdit(false); } }; const handleEditDimensionName = () => { if (!selectedDimension) return; setEditingDimensionName(true); setEditDimensionNameText(selectedDimension.dimension); }; const handleSaveDimensionName = async () => { if (!selectedDimension || !editDimensionNameText.trim()) return; const newName = editDimensionNameText.trim(); if (newName === selectedDimension.dimension) { // No change, just cancel handleCancelDimensionName(); return; } try { const response = await fetch('/api/dimensions', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ currentName: selectedDimension.dimension, newName: newName, description: selectedDimension.description }) }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to update dimension name'); } // Update the local selectedDimension with new name setSelectedDimension(prev => prev ? { ...prev, dimension: newName } : null); await fetchDimensions(); onDataChanged?.(); setEditingDimensionName(false); setEditDimensionNameText(''); } catch (error) { console.error('Error updating dimension name:', error); alert('Failed to update dimension name. Please try again.'); } }; const handleCancelDimensionName = () => { setEditingDimensionName(false); setEditDimensionNameText(''); }; const renderDimensionGrid = () => { if (dimensionsLoading) { return (
Loading dimensions...
); } if (dimensionsError) { return (
{dimensionsError}
); } if (sortedDimensions.length === 0) { return (
No dimensions yet. Create one to get started.
); } return (
{sortedDimensions.map((dimension) => { const isLocked = dimension.isPriority; const isDragTarget = dragHoverDimension === dimension.dimension; return (
handleSelectDimension(dimension)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); handleSelectDimension(dimension); } }} onDragOver={(event) => handleDimensionDragOver(event)} onDragEnter={(event) => handleDimensionDragEnter(event, dimension.dimension)} onDragLeave={(event) => handleDimensionDragLeave(event, dimension.dimension)} onDrop={(event) => handleNodeDropOnDimension(event, dimension.dimension)} style={{ background: isDragTarget ? 'rgba(34, 197, 94, 0.05)' : 'transparent', borderLeft: isLocked ? '2px solid #22c55e' : '2px solid transparent', borderRadius: '6px', padding: '12px 14px', textAlign: 'left', display: 'flex', alignItems: 'center', gap: '12px', cursor: 'pointer', transition: 'all 0.12s ease', position: 'relative' }} onMouseEnter={(e) => { if (!isDragTarget) { e.currentTarget.style.background = 'rgba(255, 255, 255, 0.02)'; } }} onMouseLeave={(e) => { if (!isDragTarget) { e.currentTarget.style.background = 'transparent'; } }} > {/* Dimension icon */}
{/* Name and description */}
{dimension.dimension}
{dimension.description && (
{dimension.description}
)}
{/* Count - minimal */} {dimension.count} {/* Action buttons - subtle */}
{ e.currentTarget.style.opacity = '1'; }} onMouseLeave={(e) => { e.currentTarget.style.opacity = '0.4'; }} >
); })}
); }; // Kanban helper functions const getNodesForKanbanColumn = (dimension: string) => { return nodes.filter(node => node.dimensions?.includes(dimension)); }; const handleAddKanbanColumn = (dimension: string) => { const newColumn = { dimension, order: kanbanColumns.length }; setKanbanColumns([...kanbanColumns, newColumn]); setShowKanbanColumnPicker(false); setKanbanSearchQuery(''); }; const handleRemoveKanbanColumn = (dimension: string) => { setKanbanColumns(kanbanColumns.filter(c => c.dimension !== dimension)); }; const handleKanbanNodeDragStart = (e: DragEvent, nodeId: number, fromColumn: string) => { setDraggedNodeId(nodeId); setDraggedFromColumn(fromColumn); e.dataTransfer.effectAllowed = 'copyMove'; // Find the node to get its title for chat drops const node = nodes.find(n => n.id === nodeId); const title = node?.title || 'Untitled'; // Set MIME types for chat input and folder drops e.dataTransfer.setData('application/x-rah-node', JSON.stringify({ id: nodeId, title })); e.dataTransfer.setData('application/node-info', JSON.stringify({ id: nodeId, title, dimensions: node?.dimensions || [] })); e.dataTransfer.setData('text/plain', `[NODE:${nodeId}:"${title}"]`); // Store in ref for webview compatibility draggedNodeRef.current = { id: nodeId, title, dimensions: node?.dimensions || [] }; }; const handleKanbanNodeDragEnd = () => { setDraggedNodeId(null); setDraggedFromColumn(null); setDragOverColumn(null); draggedNodeRef.current = null; }; const handleKanbanColumnDragOver = (e: DragEvent, columnDimension: string) => { e.preventDefault(); if (draggedNodeId !== null) { setDragOverColumn(columnDimension); } }; const handleKanbanColumnDragLeave = () => { setDragOverColumn(null); }; const handleKanbanNodeDrop = async (e: DragEvent, targetDimension: string) => { e.preventDefault(); if (draggedNodeId === null || draggedFromColumn === targetDimension) { handleKanbanNodeDragEnd(); return; } try { const node = nodes.find(n => n.id === draggedNodeId); if (!node) return; const currentDimensions = node.dimensions || []; let updatedDimensions: string[]; if (draggedFromColumn === '__uncategorized__') { // Adding to a new dimension updatedDimensions = [...currentDimensions, targetDimension]; } else { // Replace old dimension with new one updatedDimensions = currentDimensions.map(d => d === draggedFromColumn ? targetDimension : d ); } const response = await fetch(`/api/nodes/${draggedNodeId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dimensions: updatedDimensions }) }); if (!response.ok) { throw new Error('Failed to update node dimensions'); } fetchNodes(true); onDataChanged?.(); } catch (error) { console.error('Error updating node dimension:', error); alert('Failed to move node. Please try again.'); } finally { handleKanbanNodeDragEnd(); } }; const filteredKanbanDimensions = dimensions.filter(d => d.dimension.toLowerCase().includes(kanbanSearchQuery.toLowerCase()) && !kanbanColumns.some(c => c.dimension === d.dimension) && d.dimension !== selectedDimension?.dimension ); const sortedKanbanColumns = [...kanbanColumns].sort((a, b) => a.order - b.order); // Render functions for each view mode const renderGridContent = () => (
{nodes.map((node) => (
handleNodeTileDragStart(event, node)} onDragEnd={handleNodeTileDragEnd} onClick={() => { onNodeOpen(node.id); onClose(); }} style={{ background: 'var(var(--rah-bg-base))', border: '1px solid #161616', borderRadius: '12px', padding: '14px', display: 'flex', flexDirection: 'column', gap: '6px', cursor: 'pointer', transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)', position: 'relative', minHeight: '120px', boxShadow: '0 1px 4px rgba(0, 0, 0, 0.2)' }} onMouseEnter={(e) => { e.currentTarget.style.background = '#111111'; e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)'; }} onMouseLeave={(e) => { e.currentTarget.style.background = '#0a0a0a'; e.currentTarget.style.transform = 'translateY(0px)'; e.currentTarget.style.boxShadow = '0 1px 4px rgba(0, 0, 0, 0.2)'; }} > {/* Header: ID | Title | Icon */}
#{node.id}
{node.title || 'Untitled'}
{node.link && ( {getNodeIcon(node, dimensionIcons)} )}
{node.notes && (
{getContentPreview(node.notes)}
)} {node.dimensions && node.dimensions.length > 0 && (
{node.dimensions.slice(0, 3).map((dimension, index) => { const isCurrentDimension = dimension === selectedDimension?.dimension; return ( {dimension} ); })} {node.dimensions.length > 3 && ( +{node.dimensions.length - 3} )}
)}
))} {nodesLoading && (
Loading...
)} {!nodesLoading && nodes.length === 0 && (
No nodes in this dimension yet.
)}
); const renderListContent = () => (
{nodes.map((node) => ( ))} {nodesLoading && (
Loading...
)} {!nodesLoading && nodes.length === 0 && (
No nodes in this dimension yet.
)}
); const renderKanbanContent = () => (
{/* Kanban Column Setup Bar */}
Group by: {kanbanColumns.length === 0 && ( Add dimensions to create columns )}
{showKanbanColumnPicker && (
setKanbanSearchQuery(e.target.value)} style={{ width: '100%', padding: '8px 12px', background: 'var(var(--rah-bg-base))', border: 'none', borderBottom: '1px solid #333', color: 'var(var(--rah-text-active))', fontSize: '12px', outline: 'none' }} autoFocus />
{filteredKanbanDimensions.length === 0 ? (
No dimensions available
) : ( filteredKanbanDimensions.map(dim => ( )) )}
)}
{showKanbanColumnPicker && (
setShowKanbanColumnPicker(false)} /> )}
{/* Kanban Board */}
{sortedKanbanColumns.map(column => { const columnNodes = getNodesForKanbanColumn(column.dimension); const isDropTarget = dragOverColumn === column.dimension && draggedFromColumn !== column.dimension; return (
handleKanbanColumnDragOver(e, column.dimension)} onDragLeave={handleKanbanColumnDragLeave} onDrop={(e) => handleKanbanNodeDrop(e, column.dimension)} > {/* Column Header */}
{column.dimension} {columnNodes.length}
{/* Column Content */}
{columnNodes.map(node => (
handleKanbanNodeDragStart(e, node.id, column.dimension)} onDragEnd={handleKanbanNodeDragEnd} onClick={() => { onNodeOpen(node.id); onClose(); }} style={{ padding: '10px', marginBottom: '6px', background: draggedNodeId === node.id ? '#1a1a1a' : '#111', border: '1px solid var(--rah-border-strong)', borderRadius: '8px', cursor: 'pointer', opacity: draggedNodeId === node.id ? 0.5 : 1, transition: 'all 0.2s' }} onMouseEnter={(e) => { if (draggedNodeId !== node.id) { e.currentTarget.style.background = '#1a1a1a'; e.currentTarget.style.borderColor = '#333'; } }} onMouseLeave={(e) => { if (draggedNodeId !== node.id) { e.currentTarget.style.background = '#111'; e.currentTarget.style.borderColor = '#222'; } }} >
{node.title || 'Untitled'}
{node.dimensions && node.dimensions.length > 1 && (
{node.dimensions .filter(d => d !== column.dimension && d !== selectedDimension?.dimension) .slice(0, 2) .map(dim => ( {dim} ))}
)}
))} {columnNodes.length === 0 && (
Drop nodes here
)}
); })} {/* Empty State */} {kanbanColumns.length === 0 && (
Add dimension columns to organize your nodes
)}
); const renderNodeGrid = () => { if (!selectedDimension) return null; return (
Showing {nodes.length} nodes tagged with {selectedDimension.dimension.toUpperCase()}
{renderGridContent()} {nodesError && (
{nodesError}
)} {hasMoreNodes && (
)}
); }; const toolbar = (
{view === 'nodes' && ( )} {view === 'nodes' && (
{selectedDimension?.dimension ?? ''}
)}
{view === 'dimensions' && ( )} {!toolbarHost && ( )}
); return ( <>
{toolbarHost ? createPortal(toolbar, toolbarHost) : (
{toolbar}
)} {/* Content */} {view === 'nodes' ? renderNodeGrid() : renderDimensionGrid()}
{ if (dimensionPendingDelete) { handleDeleteDimension(dimensionPendingDelete); } }} onCancel={() => setDimensionPendingDelete(null)} /> setShowAddDimensionDialog(false)} /> {/* Dimension Edit Modal */} {editingDimensionModal && (
{ if (e.target === e.currentTarget) closeDimensionEditModal(); }} >
{/* Header */}
Edit Dimension
{editingDimensionModal.dimension}
{/* Content */}
{/* Description */}