Files
SuperCharged-Claude-Code-Up…/public/claude-ide/components/folder-explorer-modal.js
uroma b830e1187e Fix folder explorer error reporting and add logging
- Show actual server error message when project creation fails
- Add console logging to debug project creation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-21 14:40:14 +00:00

1202 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Folder Explorer Modal
* Visual folder browser for selecting project directories
* Following CodeNomad's UX patterns
*/
(function() {
'use strict';
let currentModal = null;
let selectedPath = null;
let currentPath = '~';
let expandedNodes = new Set();
let loadingNodes = new Set();
/**
* Show folder explorer modal
*/
async function showFolderExplorer() {
closeFolderExplorer();
// Create modal overlay
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.id = 'folder-explorer-overlay';
// Create modal content
const modal = document.createElement('div');
modal.className = 'folder-explorer-modal';
modal.id = 'folder-explorer-modal';
modal.innerHTML = `
<div class="folder-explorer-header">
<h2 class="folder-explorer-title">Select Project Folder</h2>
<button class="folder-explorer-close" onclick="window.FolderExplorer.close()">&times;</button>
</div>
<div class="folder-explorer-content" id="folder-explorer-content">
<!-- Quick access bar -->
<div class="quick-access-bar" id="quick-access-bar">
<div class="quick-access-loading">Loading locations...</div>
</div>
<!-- Breadcrumbs -->
<div class="breadcrumbs-bar" id="breadcrumbs-bar">
<!-- Breadcrumbs rendered dynamically -->
</div>
<!-- Folder tree -->
<div class="folder-tree-container">
<div class="folder-tree" id="folder-tree">
<div class="folder-tree-loading">Loading folders...</div>
</div>
</div>
</div>
<div class="folder-explorer-footer" id="folder-explorer-footer">
<div class="selected-path-display">
<span class="selected-path-icon">📁</span>
<span class="selected-path-text" id="selected-path-text">Home (~)</span>
</div>
<div class="footer-actions">
<button class="btn-create-folder" id="btn-create-folder" onclick="window.FolderExplorer.showCreateFolder()">
<span class="btn-icon">+</span> New Folder
</button>
<button class="btn-secondary" onclick="window.FolderExplorer.close()">Cancel</button>
<button class="btn-primary" id="btn-select-folder" onclick="window.FolderExplorer.selectFolder()" disabled>
Select This Folder
</button>
</div>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Prevent body scroll
document.body.style.overflow = 'hidden';
// Trigger animation
setTimeout(() => {
overlay.classList.add('visible');
modal.classList.add('visible');
}, 10);
// Load content
await loadQuickAccess();
await loadFolderTree(currentPath);
// Event listeners
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
closeFolderExplorer();
}
});
// Escape key to close
const escapeHandler = (e) => {
if (e.key === 'Escape') {
closeFolderExplorer();
document.removeEventListener('keydown', escapeHandler);
}
};
document.addEventListener('keydown', escapeHandler);
currentModal = { overlay, escapeHandler };
}
/**
* Load quick access locations
*/
async function loadQuickAccess() {
const container = document.getElementById('quick-access-bar');
if (!container) return;
try {
const res = await fetch('/api/filesystem/quick-access', {
credentials: 'same-origin'
});
if (!res.ok) {
throw new Error('Failed to load quick access');
}
const data = await res.json();
if (data.success && data.locations) {
renderQuickAccess(data.locations);
}
} catch (error) {
console.error('Error loading quick access:', error);
container.innerHTML = `
<div class="quick-access-error">Failed to load locations</div>
`;
}
}
/**
* Render quick access bar
*/
function renderQuickAccess(locations) {
const container = document.getElementById('quick-access-bar');
if (!container) return;
const html = locations.map(loc => `
<div class="quick-access-item ${!loc.exists ? 'not-exists' : ''}"
data-path="${escapeHtml(loc.displayPath)}"
onclick="window.FolderExplorer.quickAccessClick('${escapeHtml(loc.displayPath)}')">
<span class="quick-access-icon">${loc.icon || '📁'}</span>
<span class="quick-access-label">${escapeHtml(loc.name)}</span>
${loc.exists ? `<span class="quick-access-count">${loc.count} folders</span>` : ''}
</div>
`).join('');
container.innerHTML = html;
}
/**
* Load folder tree for path
*/
async function loadFolderTree(path) {
const container = document.getElementById('folder-tree');
if (!container) return;
currentPath = path;
loadingNodes.add(path);
renderLoadingState();
updateBreadcrumbs(path);
try {
const res = await fetch(`/api/filesystem/list?path=${encodeURIComponent(path)}`, {
credentials: 'same-origin'
});
if (!res.ok) {
throw new Error('Failed to load folder');
}
const data = await res.json();
loadingNodes.delete(path);
if (data.success) {
renderFolderTree(data.items, data.path, data.displayPath);
} else {
container.innerHTML = `
<div class="folder-tree-error">
<span class="error-icon">⚠️</span>
<span class="error-message">${escapeHtml(data.error || 'Failed to load folders')}</span>
</div>
`;
}
} catch (error) {
loadingNodes.delete(path);
console.error('Error loading folder tree:', error);
container.innerHTML = `
<div class="folder-tree-error">
<span class="error-icon">⚠️</span>
<span class="error-message">${escapeHtml(error.message)}</span>
</div>
`;
}
}
/**
* Render folder tree
*/
function renderFolderTree(items, fullPath, displayPath) {
const container = document.getElementById('folder-tree');
if (!container) return;
if (!items || items.length === 0) {
container.innerHTML = `
<div class="folder-empty">
<span class="empty-icon">📭</span>
<span class="empty-message">This folder is empty</span>
</div>
`;
return;
}
const html = items.map(item => `
<div class="folder-node ${item.locked ? 'locked' : ''}"
data-path="${escapeHtml(item.path)}"
data-display-path="${escapeHtml(item.displayPath || item.path)}">
<span class="folder-expand ${item.hasChildren ? '' : 'hidden'}">
${item.hasChildren ? '▶' : '•'}
</span>
<span class="folder-icon">${item.locked ? '🔒' : '📁'}</span>
<span class="folder-name">${escapeHtml(item.name)}</span>
${item.hasChildren ? `<span class="folder-count">${item.children} folders</span>` : ''}
</div>
`).join('');
container.innerHTML = html;
// Add click handlers
container.querySelectorAll('.folder-node').forEach(node => {
node.addEventListener('click', (e) => {
const nodePath = node.dataset.path;
// Check if clicked on expand icon (span or its parent)
const clickedExpandIcon = e.target.classList.contains('folder-expand') ||
e.target.parentElement?.classList.contains('folder-expand');
const expandIcon = node.querySelector('.folder-expand');
const isExpanded = expandIcon && expandIcon.textContent === '▼';
if (clickedExpandIcon && !isExpanded) {
// Expand this node
expandFolder(nodePath, node);
} else if (!clickedExpandIcon) {
// Select this folder
selectFolderForCreation(nodePath, node.dataset.displayPath);
}
});
});
}
/**
* Expand folder to show children
*/
async function expandFolder(path, nodeElement) {
const expandIcon = nodeElement.querySelector('.folder-expand');
if (!expandIcon) return;
expandIcon.textContent = '⏳';
loadingNodes.add(path);
try {
const res = await fetch(`/api/filesystem/list?path=${encodeURIComponent(path)}`, {
credentials: 'same-origin'
});
const data = await res.json();
loadingNodes.delete(path);
if (data.success && data.items && data.items.length > 0) {
// Create children container
let childrenContainer = nodeElement.nextElementSibling;
if (!childrenContainer || !childrenContainer.classList.contains('folder-children')) {
childrenContainer = document.createElement('div');
childrenContainer.className = 'folder-children';
nodeElement.after(childrenContainer);
}
// Render children
const childrenHtml = data.items.map(item => `
<div class="folder-node ${item.locked ? 'locked' : ''}"
data-path="${escapeHtml(item.path)}"
style="padding-left: 24px;">
<span class="folder-expand ${item.hasChildren ? '' : 'hidden'}">
${item.hasChildren ? '▶' : '•'}
</span>
<span class="folder-icon">${item.locked ? '🔒' : '📁'}</span>
<span class="folder-name">${escapeHtml(item.name)}</span>
${item.hasChildren ? `<span class="folder-count">${item.children} folders</span>` : ''}
</div>
`).join('');
childrenContainer.innerHTML = childrenHtml;
childrenContainer.style.display = 'block';
// Add click handlers to children
childrenContainer.querySelectorAll('.folder-node').forEach(childNode => {
childNode.addEventListener('click', (e) => {
const childPath = childNode.dataset.path;
// Check if clicked on expand icon (span or its parent)
const clickedExpandIcon = e.target.classList.contains('folder-expand') ||
e.target.parentElement?.classList.contains('folder-expand');
const expandIcon = childNode.querySelector('.folder-expand');
const isExpanded = expandIcon && expandIcon.textContent === '▼';
if (clickedExpandIcon && !isExpanded) {
expandFolder(childPath, childNode);
} else if (!clickedExpandIcon) {
selectFolderForCreation(childPath, childNode.dataset.displayPath);
}
});
});
expandIcon.textContent = '▼';
expandedNodes.add(path);
} else {
expandIcon.textContent = '•';
}
} catch (error) {
loadingNodes.delete(path);
expandIcon.textContent = '▶';
console.error('Error expanding folder:', error);
}
}
/**
* Update breadcrumbs
*/
function updateBreadcrumbs(path) {
const container = document.getElementById('breadcrumbs-bar');
if (!container) return;
// path is already a display path (starts with ~)
const displayPath = path;
const parts = displayPath.split('/').filter(p => p);
let html = `
<span class="breadcrumb-item" data-path="~">
<span class="breadcrumb-icon">🏠</span>
<span class="breadcrumb-text">Home</span>
</span>
`;
let buildPath = '~';
parts.forEach((part, index) => {
buildPath += '/' + part;
html += `
<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-item" data-path="${escapeHtml(buildPath)}">
<span class="breadcrumb-text">${escapeHtml(part)}</span>
</span>
`;
});
container.innerHTML = html;
// Add click handlers
container.querySelectorAll('.breadcrumb-item').forEach(item => {
item.addEventListener('click', () => {
const itemPath = item.dataset.path;
loadFolderTree(itemPath);
});
});
}
/**
* Quick access click handler
*/
function quickAccessClick(path) {
loadFolderTree(path);
}
/**
* Select a folder (internal - highlights selection)
*/
function selectFolderForCreation(fullPath, displayPath) {
selectedPath = fullPath;
// Update UI
document.querySelectorAll('.folder-node').forEach(node => {
node.classList.remove('selected');
});
const selectedNode = document.querySelector(`.folder-node[data-path="${escapeHtml(fullPath)}"]`);
if (selectedNode) {
selectedNode.classList.add('selected');
}
// Update footer
document.getElementById('selected-path-text').textContent = displayPath || fullPath;
document.getElementById('btn-select-folder').disabled = false;
document.getElementById('btn-create-folder').disabled = false;
}
/**
* Final folder selection - create project
*/
async function selectFolder() {
if (!selectedPath) {
showToast('Please select a folder first', 'warning');
return;
}
try {
showLoadingOverlay('Validating folder...');
// Validate path
const res = await fetch(`/api/filesystem/validate?path=${encodeURIComponent(selectedPath)}`, {
credentials: 'same-origin'
});
if (!res.ok) {
throw new Error('Failed to validate folder');
}
const data = await res.json();
if (!data.valid || !data.exists || !data.isDirectory) {
throw new Error('Invalid folder selection');
}
hideLoadingOverlay();
// Create project
const projectName = selectedPath.split('/').filter(Boolean).pop();
console.log('[FolderExplorer] Creating project:', { name: projectName, path: selectedPath });
const createRes = await fetch('/api/projects', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: projectName,
path: selectedPath,
icon: '📁'
})
});
if (!createRes.ok) {
// Try to get error details from server
let errorMsg = `Failed to create project (HTTP ${createRes.status})`;
try {
const errorData = await createRes.json();
errorMsg = errorData.error || errorMsg;
} catch (e) {
console.error('Failed to parse error response:', e);
}
throw new Error(errorMsg);
}
const createData = await createRes.json();
if (createData.success) {
closeFolderExplorer();
showToast(`Project "${projectName}" created!`, 'success');
// Reload projects list and open session picker
if (typeof refreshProjects === 'function') {
await refreshProjects();
}
// Open session picker for the new project
if (window.SessionPicker && createData.project) {
window.SessionPicker.show(createData.project);
}
} else {
throw new Error(createData.error || 'Failed to create project');
}
} catch (error) {
console.error('Error selecting folder:', error);
hideLoadingOverlay();
showToast(error.message || 'Failed to create project', 'error');
}
}
/**
* Show create folder dialog
*/
function showCreateFolder() {
const basePath = selectedPath || currentPath;
// Simple prompt for now (could be enhanced with inline input)
const folderName = prompt(`Create new folder in:\n${basePath}\n\nFolder name:`);
if (!folderName) return;
const newPath = basePath + '/' + folderName;
createFolder(newPath);
}
/**
* Create new folder
*/
async function createFolder(path) {
try {
showLoadingOverlay('Creating folder...');
const res = await fetch('/api/filesystem/mkdir', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path })
});
if (!res.ok) {
throw new Error('Failed to create folder');
}
const data = await res.json();
if (data.success) {
hideLoadingOverlay();
showToast('Folder created successfully', 'success');
// Refresh folder tree and select the new folder
await loadFolderTree(currentPath);
// Select the newly created folder
if (data.path) {
selectFolderForCreation(data.path, data.displayPath);
}
} else {
throw new Error(data.error || 'Failed to create folder');
}
} catch (error) {
console.error('Error creating folder:', error);
hideLoadingOverlay();
showToast(error.message || 'Failed to create folder', 'error');
}
}
/**
* Render loading state
*/
function renderLoadingState() {
const container = document.getElementById('folder-tree');
if (!container) return;
container.innerHTML = `
<div class="folder-tree-loading">
<div class="loading-spinner-small"></div>
<span>Loading folders...</span>
</div>
`;
}
/**
* Close the modal
*/
function closeFolderExplorer() {
const overlay = document.getElementById('folder-explorer-overlay');
if (!overlay) return;
overlay.classList.remove('visible');
const modal = document.getElementById('folder-explorer-modal');
if (modal) modal.classList.remove('visible');
setTimeout(() => {
if (currentModal && currentModal.escapeHandler) {
document.removeEventListener('keydown', currentModal.escapeHandler);
}
overlay.remove();
document.body.style.overflow = '';
currentModal = null;
selectedPath = null;
}, 300);
}
/**
* Escape HTML
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Show loading overlay
*/
function showLoadingOverlay(message = 'Loading...') {
let overlay = document.getElementById('loading-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'loading-overlay';
overlay.className = 'loading-overlay';
overlay.innerHTML = `
<div class="loading-spinner"></div>
<p class="loading-text">${escapeHtml(message)}</p>
`;
document.body.appendChild(overlay);
} else {
const textElement = overlay.querySelector('.loading-text');
if (textElement) {
textElement.textContent = message;
}
}
overlay.classList.remove('hidden');
setTimeout(() => {
overlay.classList.add('visible');
}, 10);
}
/**
* Hide loading overlay
*/
function hideLoadingOverlay() {
const overlay = document.getElementById('loading-overlay');
if (overlay) {
overlay.classList.remove('visible');
setTimeout(() => {
overlay.classList.add('hidden');
}, 300);
}
}
/**
* Show toast notification
*/
function showToast(message, type = 'info', duration = 3000) {
const existingToasts = document.querySelectorAll('.toast-notification');
existingToasts.forEach(toast => toast.remove());
const toast = document.createElement('div');
toast.className = `toast-notification toast-${type}`;
toast.innerHTML = `
<span class="toast-icon">${getToastIcon(type)}</span>
<span class="toast-message">${escapeHtml(message)}</span>
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('visible');
}, 10);
setTimeout(() => {
toast.classList.remove('visible');
setTimeout(() => {
toast.remove();
}, 300);
}, duration);
}
/**
* Get toast icon based on type
*/
function getToastIcon(type) {
const icons = {
success: '✓',
error: '✕',
info: '',
warning: '⚠'
};
return icons[type] || icons.info;
}
// Export to global scope
window.FolderExplorer = {
show: showFolderExplorer,
close: closeFolderExplorer,
quickAccessClick,
selectFolder,
showCreateFolder
};
// Add CSS styles
const style = document.createElement('style');
style.textContent = `
/* Modal Overlay (reuse existing) */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
opacity: 0;
transition: opacity 0.2s ease;
}
.modal-overlay.visible {
opacity: 1;
}
/* Folder Explorer Modal */
.folder-explorer-modal {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
width: 100%;
max-width: 700px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
transform: scale(0.95);
transition: transform 0.2s ease;
}
.folder-explorer-modal.visible {
transform: scale(1);
}
/* Header */
.folder-explorer-header {
padding: 20px 24px;
border-bottom: 1px solid #333;
display: flex;
justify-content: space-between;
align-items: center;
}
.folder-explorer-title {
font-size: 20px;
font-weight: 600;
color: #e0e0e0;
margin: 0;
}
.folder-explorer-close {
background: none;
border: none;
color: #888;
font-size: 28px;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
transition: all 0.2s ease;
}
.folder-explorer-close:hover {
background: #252525;
color: #e0e0e0;
}
/* Content */
.folder-explorer-content {
flex: 1;
overflow-y: auto;
padding: 16px 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* Quick Access Bar */
.quick-access-bar {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.quick-access-item {
padding: 12px 16px;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
transition: all 0.2s ease;
flex: 1;
min-width: 120px;
}
.quick-access-item:hover {
background: #252525;
border-color: #4a9eff;
}
.quick-access-item.not-exists {
opacity: 0.5;
}
.quick-access-icon {
font-size: 20px;
}
.quick-access-label {
font-size: 14px;
font-weight: 500;
color: #e0e0e0;
flex: 1;
}
.quick-access-count {
font-size: 12px;
color: #888;
background: #252525;
padding: 2px 8px;
border-radius: 6px;
}
/* Breadcrumbs */
.breadcrumbs-bar {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 0;
overflow-x: auto;
}
.breadcrumb-item {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
transition: all 0.2s ease;
}
.breadcrumb-item:hover {
background: #252525;
}
.breadcrumb-icon {
font-size: 14px;
}
.breadcrumb-text {
font-size: 14px;
color: #4a9eff;
}
.breadcrumb-separator {
color: #888;
font-size: 14px;
}
/* Folder Tree Container */
.folder-tree-container {
flex: 1;
background: #0d0d0d;
border: 1px solid #222;
border-radius: 12px;
overflow-y: auto;
padding: 8px;
min-height: 300px;
}
/* Folder Node */
.folder-node {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.folder-node:hover {
background: #252525;
}
.folder-node.selected {
background: rgba(74, 158, 255, 0.15);
border: 1px solid #4a9eff;
}
.folder-node.locked {
opacity: 0.6;
}
.folder-expand {
font-size: 12px;
color: #888;
width: 16px;
text-align: center;
}
.folder-expand.hidden {
visibility: hidden;
}
.folder-icon {
font-size: 16px;
}
.folder-name {
font-size: 14px;
color: #e0e0e0;
flex: 1;
}
.folder-count {
font-size: 11px;
color: #888;
background: #252525;
padding: 2px 6px;
border-radius: 4px;
}
/* Folder Children (nested) */
.folder-children {
display: none;
}
/* Footer */
.folder-explorer-footer {
padding: 16px 24px;
border-top: 1px solid #333;
display: flex;
flex-direction: column;
gap: 16px;
}
.selected-path-display {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background: #0d0d0d;
border: 1px solid #222;
border-radius: 8px;
}
.selected-path-icon {
font-size: 16px;
}
.selected-path-text {
font-size: 13px;
color: #888;
font-family: monospace;
flex: 1;
}
.footer-actions {
display: flex;
align-items: center;
gap: 12px;
justify-content: flex-end;
}
/* Buttons */
.btn-primary {
padding: 12px 24px;
background: linear-gradient(135deg, #4a9eff 0%, #a78bfa 100%);
border: none;
border-radius: 10px;
color: white;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(74, 158, 255, 0.4);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-secondary {
padding: 12px 24px;
background: transparent;
border: 1px solid #333;
border-radius: 10px;
color: #e0e0e0;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-secondary:hover {
background: #252525;
border-color: #4a9eff;
}
.btn-create-folder {
padding: 10px 16px;
background: #252525;
border: 1px solid #333;
border-radius: 8px;
color: #e0e0e0;
font-size: 13px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s ease;
margin-right: auto;
}
.btn-create-folder:hover:not(:disabled) {
background: #1a1a1a;
border-color: #4a9eff;
}
.btn-create-folder:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-icon {
font-size: 16px;
line-height: 1;
}
/* States */
.folder-tree-loading,
.folder-tree-error,
.folder-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: #888;
font-size: 14px;
gap: 12px;
}
.loading-spinner-small {
width: 24px;
height: 24px;
border: 2px solid #333;
border-top-color: #4a9eff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.empty-icon,
.error-icon {
font-size: 32px;
}
/* Responsive */
@media (max-width: 640px) {
.folder-explorer-modal {
max-height: 90vh;
}
.folder-explorer-header,
.folder-explorer-content,
.folder-explorer-footer {
padding: 16px;
}
.quick-access-bar {
flex-wrap: wrap;
}
.quick-access-item {
flex: 1 1 calc(50% - 4px);
}
.footer-actions {
flex-wrap: wrap;
}
.btn-create-folder {
width: 100%;
margin-right: 0;
margin-bottom: 8px;
}
.btn-secondary,
.btn-primary {
flex: 1;
}
}
/* Loading Overlay */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 20000;
opacity: 0;
transition: opacity 0.3s ease;
}
.loading-overlay.visible {
opacity: 1;
}
.loading-overlay.hidden {
display: none;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-top-color: #4a9eff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
color: #fff;
margin-top: 16px;
font-size: 14px;
}
/* Toast Notifications */
.toast-notification {
position: fixed;
bottom: 20px;
right: 20px;
background: #2a2a2a;
border: 1px solid #444;
border-radius: 8px;
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
z-index: 20001;
opacity: 0;
transform: translateY(20px);
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
.toast-notification.visible {
opacity: 1;
transform: translateY(0);
}
.toast-icon {
font-size: 18px;
font-weight: bold;
}
.toast-success .toast-icon {
color: #4ade80;
}
.toast-error .toast-icon {
color: #f87171;
}
.toast-warning .toast-icon {
color: #fbbf24;
}
.toast-info .toast-icon {
color: #60a5fa;
}
.toast-message {
color: #fff;
font-size: 14px;
}
`;
document.head.appendChild(style);
console.log('[FolderExplorer] Module loaded');
})();