fix: adjust UI for open project pop up
This commit is contained in:
@@ -1,42 +1,79 @@
|
|||||||
/* Overlay — blocks and blurs content below */
|
/* Overlay — centered, blurs content below */
|
||||||
.open-project-overlay {
|
.open-project-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
top: 0;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
left: 0;
|
||||||
backdrop-filter: blur(3px);
|
right: 0;
|
||||||
z-index: 2000;
|
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 {
|
.open-project-panel {
|
||||||
position: fixed;
|
|
||||||
background-color: #2d2d2d;
|
background-color: #2d2d2d;
|
||||||
border: 1px solid #3a3a3a;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
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 {
|
.open-project-header {
|
||||||
height: 30px;
|
|
||||||
background-color: #1e1e1e;
|
|
||||||
border-bottom: 1px solid #3a3a3a;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 10px;
|
padding: 20px;
|
||||||
cursor: move;
|
border-bottom: 1px solid #3a3a3a;
|
||||||
user-select: none;
|
background-color: #252525;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.open-project-title {
|
.open-project-title {
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #e0e0e0;
|
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 */
|
/* Filter bar — sticky at top */
|
||||||
@@ -268,18 +305,3 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 40px;
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { FaTimes, FaSortUp, FaSortDown, FaCopy, FaTrash, FaUndo } from 'react-icons/fa';
|
||||||
import { FaTimes, FaGripLines, FaSortUp, FaSortDown, FaCopy, FaTrash, FaUndo } from 'react-icons/fa';
|
|
||||||
import { KGProjectStorage, type ProjectMeta } from '../../core/io/KGProjectStorage';
|
import { KGProjectStorage, type ProjectMeta } from '../../core/io/KGProjectStorage';
|
||||||
import { isValidProjectName } from '../../util/projectNameUtil';
|
import { isValidProjectName } from '../../util/projectNameUtil';
|
||||||
import './OpenProjectModal.css';
|
import './OpenProjectModal.css';
|
||||||
@@ -18,10 +17,6 @@ type ViewMode = 'projects' | 'trash';
|
|||||||
|
|
||||||
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
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 => {
|
const matchesFilter = (projectName: string, filter: string): boolean => {
|
||||||
if (!filter) return true;
|
if (!filter) return true;
|
||||||
const normalizedName = projectName.toLowerCase().replace(/[^a-z0-9]/g, '');
|
const normalizedName = projectName.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||||
@@ -49,50 +44,20 @@ const formatDate = (timestamp: number): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const OpenProjectModal: React.FC<OpenProjectModalProps> = ({ onClose, onOpenProject, currentProjectName, onCreateNewProject }) => {
|
const OpenProjectModal: React.FC<OpenProjectModalProps> = ({ onClose, onOpenProject, currentProjectName, onCreateNewProject }) => {
|
||||||
const panelRef = useRef<HTMLDivElement>(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<ProjectMeta[]>([]);
|
const [projects, setProjects] = useState<ProjectMeta[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [filter, setFilter] = useState('');
|
const [filter, setFilter] = useState('');
|
||||||
|
|
||||||
// Sort
|
|
||||||
const [sortField, setSortField] = useState<SortField>('updatedAt');
|
const [sortField, setSortField] = useState<SortField>('updatedAt');
|
||||||
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
||||||
|
|
||||||
// View mode toggle
|
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('projects');
|
const [viewMode, setViewMode] = useState<ViewMode>('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) => {
|
const fetchProjects = useCallback(async (mode: ViewMode) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const storage = KGProjectStorage.getInstance();
|
const storage = KGProjectStorage.getInstance();
|
||||||
|
|
||||||
// Auto-purge old trash when switching to trash view
|
|
||||||
if (mode === 'trash') {
|
if (mode === 'trash') {
|
||||||
await storage.purgeDeletedOlderThan(THIRTY_DAYS_MS);
|
await storage.purgeDeletedOlderThan(THIRTY_DAYS_MS);
|
||||||
}
|
}
|
||||||
@@ -110,7 +75,6 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({ onClose, onOpenProj
|
|||||||
fetchProjects(viewMode);
|
fetchProjects(viewMode);
|
||||||
}, [viewMode, fetchProjects]);
|
}, [viewMode, fetchProjects]);
|
||||||
|
|
||||||
// Escape key to close
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') onClose();
|
if (e.key === 'Escape') onClose();
|
||||||
@@ -119,49 +83,6 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({ onClose, onOpenProj
|
|||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [onClose]);
|
}, [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) => {
|
const handleSortClick = useCallback((field: SortField) => {
|
||||||
if (field === sortField) {
|
if (field === sortField) {
|
||||||
setSortDirection(d => (d === 'asc' ? 'desc' : 'asc'));
|
setSortDirection(d => (d === 'asc' ? 'desc' : 'asc'));
|
||||||
@@ -171,7 +92,6 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({ onClose, onOpenProj
|
|||||||
}
|
}
|
||||||
}, [sortField]);
|
}, [sortField]);
|
||||||
|
|
||||||
// Filter + sort projects
|
|
||||||
const filteredProjects = React.useMemo(() => {
|
const filteredProjects = React.useMemo(() => {
|
||||||
const filtered = projects.filter(p => matchesFilter(p.name, filter));
|
const filtered = projects.filter(p => matchesFilter(p.name, filter));
|
||||||
|
|
||||||
@@ -188,8 +108,6 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({ onClose, onOpenProj
|
|||||||
return filtered;
|
return filtered;
|
||||||
}, [projects, filter, sortField, sortDirection]);
|
}, [projects, filter, sortField, sortDirection]);
|
||||||
|
|
||||||
// --- Action handlers ---
|
|
||||||
|
|
||||||
const handleOpen = async (projectName: string) => {
|
const handleOpen = async (projectName: string) => {
|
||||||
await onOpenProject(projectName);
|
await onOpenProject(projectName);
|
||||||
onClose();
|
onClose();
|
||||||
@@ -210,7 +128,6 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({ onClose, onOpenProj
|
|||||||
const storage = KGProjectStorage.getInstance();
|
const storage = KGProjectStorage.getInstance();
|
||||||
const finalName = await storage.resolveUniqueName(trimmed);
|
const finalName = await storage.resolveUniqueName(trimmed);
|
||||||
await storage.duplicate(projectName, finalName);
|
await storage.duplicate(projectName, finalName);
|
||||||
// Open the duplicated project
|
|
||||||
await onOpenProject(finalName);
|
await onOpenProject(finalName);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -224,7 +141,6 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({ onClose, onOpenProj
|
|||||||
try {
|
try {
|
||||||
const storage = KGProjectStorage.getInstance();
|
const storage = KGProjectStorage.getInstance();
|
||||||
await storage.softDelete(projectName);
|
await storage.softDelete(projectName);
|
||||||
// Refresh list
|
|
||||||
await fetchProjects(viewMode);
|
await fetchProjects(viewMode);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting project:', error);
|
console.error('Error deleting project:', error);
|
||||||
@@ -256,7 +172,6 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({ onClose, onOpenProj
|
|||||||
await storage.delete(projectName);
|
await storage.delete(projectName);
|
||||||
await fetchProjects(viewMode);
|
await fetchProjects(viewMode);
|
||||||
|
|
||||||
// If the deleted project is the currently open one, create a new project
|
|
||||||
if (currentProjectName && projectName === currentProjectName) {
|
if (currentProjectName && projectName === currentProjectName) {
|
||||||
onCreateNewProject();
|
onCreateNewProject();
|
||||||
}
|
}
|
||||||
@@ -271,34 +186,27 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({ onClose, onOpenProj
|
|||||||
setFilter('');
|
setFilter('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOverlayClick = useCallback((e: React.MouseEvent) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
const SortIcon: React.FC<{ field: SortField }> = ({ field }) => {
|
const SortIcon: React.FC<{ field: SortField }> = ({ field }) => {
|
||||||
if (sortField !== field) return null;
|
if (sortField !== field) return null;
|
||||||
return sortDirection === 'asc' ? <FaSortUp /> : <FaSortDown />;
|
return sortDirection === 'asc' ? <FaSortUp /> : <FaSortDown />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const content = (
|
return (
|
||||||
<div className="open-project-overlay" onClick={onClose}>
|
<div className="open-project-overlay" onClick={handleOverlayClick}>
|
||||||
<div
|
<div className="open-project-panel">
|
||||||
className="open-project-panel"
|
<div className="open-project-header">
|
||||||
ref={panelRef}
|
<h3 className="open-project-title">Open Project</h3>
|
||||||
onClick={(e) => e.stopPropagation()}
|
<button className="open-project-close-btn" onClick={onClose} aria-label="Close">
|
||||||
style={{
|
|
||||||
left: `${position.x}px`,
|
|
||||||
top: `${position.y}px`,
|
|
||||||
width: `${size.width}px`,
|
|
||||||
height: `${size.height}px`,
|
|
||||||
zIndex: 2001,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="open-project-header" onMouseDown={(e) => handleMouseDown(e, 'drag')}>
|
|
||||||
<button className="close-button" onClick={onClose}>
|
|
||||||
<FaTimes />
|
<FaTimes />
|
||||||
</button>
|
</button>
|
||||||
<div className="open-project-title">Open Project</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter + view toggle */}
|
|
||||||
<div className="open-project-filter">
|
<div className="open-project-filter">
|
||||||
<div className="open-project-filter-row">
|
<div className="open-project-filter-row">
|
||||||
<input
|
<input
|
||||||
@@ -325,14 +233,12 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({ onClose, onOpenProj
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Trash hint */}
|
|
||||||
{viewMode === 'trash' && (
|
{viewMode === 'trash' && (
|
||||||
<div className="open-project-trash-hint">
|
<div className="open-project-trash-hint">
|
||||||
Deleted projects are automatically removed after 30 days.
|
Deleted projects are automatically removed after 30 days.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Sort header */}
|
|
||||||
<div className="open-project-sort-header">
|
<div className="open-project-sort-header">
|
||||||
<span
|
<span
|
||||||
className={`open-project-sort-col open-project-sort-col-name ${sortField === 'name' ? 'active' : ''}`}
|
className={`open-project-sort-col open-project-sort-col-name ${sortField === 'name' ? 'active' : ''}`}
|
||||||
@@ -354,7 +260,6 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({ onClose, onOpenProj
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Project list */}
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="open-project-loading">Loading projects...</div>
|
<div className="open-project-loading">Loading projects...</div>
|
||||||
) : filteredProjects.length === 0 ? (
|
) : filteredProjects.length === 0 ? (
|
||||||
@@ -427,16 +332,9 @@ const OpenProjectModal: React.FC<OpenProjectModalProps> = ({ onClose, onOpenProj
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Resize handle */}
|
|
||||||
<div className="resize-handle" onMouseDown={(e) => handleMouseDown(e, 'resize')}>
|
|
||||||
<FaGripLines />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return createPortal(content, document.body);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OpenProjectModal;
|
export default OpenProjectModal;
|
||||||
|
|||||||
Reference in New Issue
Block a user