- Modified loadChatHistory() to check for active project before fetching all sessions - When active project exists, use project.sessions instead of fetching from API - Added detailed console logging to debug session filtering - This prevents ALL sessions from appearing in every project's sidebar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1187 lines
35 KiB
JavaScript
1187 lines
35 KiB
JavaScript
/**
|
||
* 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()">×</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 session directly
|
||
*/
|
||
async function selectFolder() {
|
||
if (!selectedPath) {
|
||
showToast('Please select a folder first', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showLoadingOverlay('Creating session...');
|
||
|
||
// Create session directly with the selected folder as workingDir
|
||
console.log('[FolderExplorer] Creating session in folder:', selectedPath);
|
||
|
||
const createRes = await fetch('/claude/api/claude/sessions', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
workingDir: selectedPath,
|
||
metadata: {
|
||
type: 'chat',
|
||
source: 'folder-explorer',
|
||
folderName: selectedPath.split('/').filter(Boolean).pop()
|
||
}
|
||
})
|
||
});
|
||
|
||
if (!createRes.ok) {
|
||
let errorMsg = `Failed to create session (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();
|
||
|
||
// Backend returns: { id, status, createdAt, workingDir, metadata }
|
||
if (createData && createData.id) {
|
||
closeFolderExplorer();
|
||
showToast('Session created!', 'success');
|
||
|
||
// Navigate directly to the new session
|
||
await new Promise(resolve => setTimeout(resolve, 300));
|
||
window.location.href = `/claude/ide/session/${createData.id}`;
|
||
} else {
|
||
throw new Error(createData.error || 'Failed to create session');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error selecting folder:', {
|
||
message: error.message,
|
||
stack: error.stack,
|
||
name: error.name,
|
||
selectedPath: selectedPath,
|
||
currentPath: currentPath,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
hideLoadingOverlay();
|
||
showToast(error.message || 'Failed to create session', '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');
|
||
})();
|