diff --git a/src/components/common/OpenProjectModal.css b/src/components/common/OpenProjectModal.css index 8a3c6c8..9412b95 100644 --- a/src/components/common/OpenProjectModal.css +++ b/src/components/common/OpenProjectModal.css @@ -1,42 +1,79 @@ -/* Overlay — blocks and blurs content below */ +/* Overlay — centered, blurs content below */ .open-project-overlay { position: fixed; - inset: 0; - background-color: rgba(0, 0, 0, 0.5); - backdrop-filter: blur(3px); - z-index: 2000; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 10001; + backdrop-filter: blur(2px); } -/* Open Project Panel — follows PianoRoll panel pattern */ +/* Open Project Panel — centered dialog */ .open-project-panel { - position: fixed; background-color: #2d2d2d; border: 1px solid #3a3a3a; - box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + width: 90%; + max-width: 700px; + height: 70vh; display: flex; flex-direction: column; overflow: hidden; + animation: openProjectFadeIn 0.2s ease-out; } -/* Header — draggable */ +@keyframes openProjectFadeIn { + from { + opacity: 0; + transform: scale(0.95) translateY(-10px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +/* Header */ .open-project-header { - height: 30px; - background-color: #1e1e1e; - border-bottom: 1px solid #3a3a3a; display: flex; + justify-content: space-between; align-items: center; - padding: 0 10px; - cursor: move; - user-select: none; + padding: 20px; + border-bottom: 1px solid #3a3a3a; + background-color: #252525; flex-shrink: 0; } .open-project-title { - flex: 1; - text-align: center; - font-size: 14px; color: #e0e0e0; - text-transform: uppercase; + font-size: 18px; + font-weight: 600; + margin: 0; +} + +.open-project-close-btn { + background: transparent; + border: none; + color: #b0b0b0; + cursor: pointer; + padding: 8px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + transition: all 0.2s ease; +} + +.open-project-close-btn:hover { + background-color: #3a3a3a; + color: #e0e0e0; } /* Filter bar — sticky at top */ @@ -268,18 +305,3 @@ font-size: 14px; padding: 40px; } - -/* Resize handle — same as PianoRoll */ -.open-project-panel .resize-handle { - position: absolute; - right: 5px; - bottom: 5px; - width: 20px; - height: 20px; - cursor: nwse-resize; - color: #999; - display: flex; - align-items: center; - justify-content: center; - z-index: 100; -} diff --git a/src/components/common/OpenProjectModal.tsx b/src/components/common/OpenProjectModal.tsx index 5f93577..eb1ac4f 100644 --- a/src/components/common/OpenProjectModal.tsx +++ b/src/components/common/OpenProjectModal.tsx @@ -1,6 +1,5 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { createPortal } from 'react-dom'; -import { FaTimes, FaGripLines, FaSortUp, FaSortDown, FaCopy, FaTrash, FaUndo } from 'react-icons/fa'; +import React, { useState, useEffect, useCallback } from 'react'; +import { FaTimes, FaSortUp, FaSortDown, FaCopy, FaTrash, FaUndo } from 'react-icons/fa'; import { KGProjectStorage, type ProjectMeta } from '../../core/io/KGProjectStorage'; import { isValidProjectName } from '../../util/projectNameUtil'; import './OpenProjectModal.css'; @@ -18,10 +17,6 @@ type ViewMode = 'projects' | 'trash'; const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; -/** - * Incremental fuzzy filter: characters in the filter must appear in order - * within the project name (case-insensitive, ignoring non-alphanumeric chars). - */ const matchesFilter = (projectName: string, filter: string): boolean => { if (!filter) return true; const normalizedName = projectName.toLowerCase().replace(/[^a-z0-9]/g, ''); @@ -49,50 +44,20 @@ const formatDate = (timestamp: number): string => { }; const OpenProjectModal: React.FC = ({ onClose, onOpenProject, currentProjectName, onCreateNewProject }) => { - const panelRef = useRef(null); - - // Position & size - const [position, setPosition] = useState({ x: 0, y: 0 }); - const [size, setSize] = useState({ width: 600, height: 400 }); - - // Drag & resize - const [isDragging, setIsDragging] = useState(false); - const [isResizing, setIsResizing] = useState(false); - const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); - - // Data const [projects, setProjects] = useState([]); const [isLoading, setIsLoading] = useState(true); const [filter, setFilter] = useState(''); - // Sort const [sortField, setSortField] = useState('updatedAt'); const [sortDirection, setSortDirection] = useState('desc'); - // View mode toggle const [viewMode, setViewMode] = useState('projects'); - // Calculate initial position/size to fill main-content area - useEffect(() => { - const toolbarEl = document.querySelector('.toolbar'); - const statusBarEl = document.querySelector('.status-bar'); - - const width = Math.max(400, window.innerWidth - 400); - const height = Math.max(300, window.innerHeight - 200); - const x = (window.innerWidth - width) / 2; - const y = (window.innerHeight - height) / 2; - - setPosition({ x, y }); - setSize({ width, height }); - }, []); - - // Fetch projects when view mode changes const fetchProjects = useCallback(async (mode: ViewMode) => { setIsLoading(true); try { const storage = KGProjectStorage.getInstance(); - // Auto-purge old trash when switching to trash view if (mode === 'trash') { await storage.purgeDeletedOlderThan(THIRTY_DAYS_MS); } @@ -110,7 +75,6 @@ const OpenProjectModal: React.FC = ({ onClose, onOpenProj fetchProjects(viewMode); }, [viewMode, fetchProjects]); - // Escape key to close useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); @@ -119,49 +83,6 @@ const OpenProjectModal: React.FC = ({ onClose, onOpenProj return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose]); - // Drag & resize mouse handling - const handleMouseDown = (e: React.MouseEvent, action: 'drag' | 'resize') => { - if (action === 'drag') { - setIsDragging(true); - if (panelRef.current) { - const rect = panelRef.current.getBoundingClientRect(); - setDragOffset({ x: e.clientX - rect.left, y: e.clientY - rect.top }); - } - } else { - setIsResizing(true); - e.preventDefault(); - } - }; - - useEffect(() => { - const handleMouseMove = (e: MouseEvent) => { - if (isDragging) { - setPosition({ x: e.clientX - dragOffset.x, y: e.clientY - dragOffset.y }); - } else if (isResizing) { - setSize({ - width: Math.max(400, e.clientX - position.x), - height: Math.max(300, e.clientY - position.y), - }); - } - }; - - const handleMouseUp = () => { - setIsDragging(false); - setIsResizing(false); - }; - - if (isDragging || isResizing) { - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - } - - return () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }; - }, [isDragging, isResizing, dragOffset, position]); - - // Sort toggle const handleSortClick = useCallback((field: SortField) => { if (field === sortField) { setSortDirection(d => (d === 'asc' ? 'desc' : 'asc')); @@ -171,7 +92,6 @@ const OpenProjectModal: React.FC = ({ onClose, onOpenProj } }, [sortField]); - // Filter + sort projects const filteredProjects = React.useMemo(() => { const filtered = projects.filter(p => matchesFilter(p.name, filter)); @@ -188,8 +108,6 @@ const OpenProjectModal: React.FC = ({ onClose, onOpenProj return filtered; }, [projects, filter, sortField, sortDirection]); - // --- Action handlers --- - const handleOpen = async (projectName: string) => { await onOpenProject(projectName); onClose(); @@ -210,7 +128,6 @@ const OpenProjectModal: React.FC = ({ onClose, onOpenProj const storage = KGProjectStorage.getInstance(); const finalName = await storage.resolveUniqueName(trimmed); await storage.duplicate(projectName, finalName); - // Open the duplicated project await onOpenProject(finalName); onClose(); } catch (error) { @@ -224,7 +141,6 @@ const OpenProjectModal: React.FC = ({ onClose, onOpenProj try { const storage = KGProjectStorage.getInstance(); await storage.softDelete(projectName); - // Refresh list await fetchProjects(viewMode); } catch (error) { console.error('Error deleting project:', error); @@ -256,7 +172,6 @@ const OpenProjectModal: React.FC = ({ onClose, onOpenProj await storage.delete(projectName); await fetchProjects(viewMode); - // If the deleted project is the currently open one, create a new project if (currentProjectName && projectName === currentProjectName) { onCreateNewProject(); } @@ -271,172 +186,155 @@ const OpenProjectModal: React.FC = ({ onClose, onOpenProj setFilter(''); }; + const handleOverlayClick = useCallback((e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }, [onClose]); + const SortIcon: React.FC<{ field: SortField }> = ({ field }) => { if (sortField !== field) return null; return sortDirection === 'asc' ? : ; }; - const content = ( -
-
e.stopPropagation()} - style={{ - left: `${position.x}px`, - top: `${position.y}px`, - width: `${size.width}px`, - height: `${size.height}px`, - zIndex: 2001, - }} - > - {/* Header */} -
handleMouseDown(e, 'drag')}> - -
Open Project
-
+ return ( +
+
+
+

Open Project

+ +
- {/* Filter + view toggle */} -
-
- setFilter(e.target.value)} - autoFocus - /> -
- - +
+
+ setFilter(e.target.value)} + autoFocus + /> +
+ + +
-
- {/* Trash hint */} - {viewMode === 'trash' && ( -
- Deleted projects are automatically removed after 30 days. + {viewMode === 'trash' && ( +
+ Deleted projects are automatically removed after 30 days. +
+ )} + +
+ handleSortClick('name')} + > + Name + + handleSortClick('createdAt')} + > + Created + + handleSortClick('updatedAt')} + > + Updated +
- )} - {/* Sort header */} -
- handleSortClick('name')} - > - Name - - handleSortClick('createdAt')} - > - Created - - handleSortClick('updatedAt')} - > - Updated - -
- - {/* Project list */} - {isLoading ? ( -
Loading projects...
- ) : filteredProjects.length === 0 ? ( -
- {projects.length === 0 - ? (viewMode === 'trash' ? 'Trash is empty' : 'No saved projects') - : 'No projects match the filter'} -
- ) : ( -
- {filteredProjects.map((project) => ( -
handleOpen(project.name) : undefined} - > -
-
{project.name}
-
- Created {formatDate(project.createdAt)} · Updated {formatDate(project.updatedAt)} - {viewMode === 'trash' && project.deletedAt && ( - <> · Deleted {formatDate(project.deletedAt)} + {isLoading ? ( +
Loading projects...
+ ) : filteredProjects.length === 0 ? ( +
+ {projects.length === 0 + ? (viewMode === 'trash' ? 'Trash is empty' : 'No saved projects') + : 'No projects match the filter'} +
+ ) : ( +
+ {filteredProjects.map((project) => ( +
handleOpen(project.name) : undefined} + > +
+
{project.name}
+
+ Created {formatDate(project.createdAt)} · Updated {formatDate(project.updatedAt)} + {viewMode === 'trash' && project.deletedAt && ( + <> · Deleted {formatDate(project.deletedAt)} + )} +
+
+
+ {viewMode === 'projects' ? ( + <> + + + + + ) : ( + <> + + + )}
-
- {viewMode === 'projects' ? ( - <> - - - - - ) : ( - <> - - - - )} -
-
- ))} -
- )} - - {/* Resize handle */} -
handleMouseDown(e, 'resize')}> - + ))} +
+ )}
-
); - - return createPortal(content, document.body); }; export default OpenProjectModal;