diff --git a/static/css/layout.css b/static/css/layout.css index 11fb0e2..cb53bc0 100644 --- a/static/css/layout.css +++ b/static/css/layout.css @@ -124,6 +124,63 @@ body { height: 100%; overflow: hidden; /* Prevent pane scrolling */ + transition: flex 0.3s ease, min-width 0.3s ease, max-width 0.3s ease; +} + +/* Collapsed sidebar state - mini sidebar */ +#sidebarPane.collapsed { + flex: 0 0 50px; + min-width: 50px; + max-width: 50px; + border-right: 1px solid var(--border-color); + position: relative; + cursor: pointer; +} + +/* Hide file tree content when collapsed */ +#sidebarPane.collapsed #fileTree { + display: none; +} + +/* Hide collection selector when collapsed */ +#sidebarPane.collapsed .collection-selector { + display: none; +} + +/* Visual indicator in the mini sidebar */ +#sidebarPane.collapsed::before { + content: ''; + display: block; + width: 100%; + height: 100%; + background: var(--bg-secondary); + transition: background 0.2s ease; +} + +/* Hover effect on mini sidebar */ +#sidebarPane.collapsed:hover::before { + background: var(--hover-bg); +} + +/* Right arrow icon in the center of mini sidebar */ +#sidebarPane.collapsed::after { + content: '\F285'; + /* Bootstrap icon chevron-right */ + font-family: 'bootstrap-icons'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 20px; + color: var(--text-secondary); + pointer-events: none; + opacity: 0.5; + transition: opacity 0.2s ease; + cursor: pointer; +} + +#sidebarPane.collapsed:hover::after { + opacity: 1; } #editorPane { diff --git a/static/js/app.js b/static/js/app.js index 35a0eb4..9750bfb 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -277,6 +277,9 @@ document.addEventListener('DOMContentLoaded', async () => { darkMode.toggle(); }); + // Initialize sidebar toggle + const sidebarToggle = new SidebarToggle('sidebarPane', 'sidebarToggleBtn'); + // Initialize collection selector (always needed) collectionSelector = new CollectionSelector('collectionSelect', webdavClient); await collectionSelector.load(); @@ -321,7 +324,8 @@ document.addEventListener('DOMContentLoaded', async () => { editor.setWebDAVClient(webdavClient); // Initialize file tree (needed in both modes) - fileTree = new FileTree('fileTree', webdavClient); + // Pass isEditMode to control image filtering (hide images only in view mode) + fileTree = new FileTree('fileTree', webdavClient, isEditMode); fileTree.onFileSelect = async (item) => { try { const currentCollection = collectionSelector.getCurrentCollection(); @@ -579,6 +583,22 @@ document.addEventListener('DOMContentLoaded', async () => { await autoLoadPageInViewMode(); } + // Setup clickable navbar brand (logo/title) + const navbarBrand = document.getElementById('navbarBrand'); + if (navbarBrand) { + navbarBrand.addEventListener('click', (e) => { + e.preventDefault(); + const currentCollection = collectionSelector ? collectionSelector.getCurrentCollection() : null; + if (currentCollection) { + // Navigate to collection root + window.location.href = `/${currentCollection}/`; + } else { + // Navigate to home page + window.location.href = '/'; + } + }); + } + // Initialize mermaid (always needed) mermaid.initialize({ startOnLoad: true, theme: darkMode.isDark ? 'dark' : 'default' }); // Listen for file-saved event to reload file tree diff --git a/static/js/config.js b/static/js/config.js index 7a48845..219a6c2 100644 --- a/static/js/config.js +++ b/static/js/config.js @@ -5,86 +5,86 @@ const Config = { // ===== TIMING CONFIGURATION ===== - + /** * Long-press threshold in milliseconds * Used for drag-and-drop detection in file tree */ LONG_PRESS_THRESHOLD: 400, - + /** * Debounce delay in milliseconds * Used for editor preview updates */ DEBOUNCE_DELAY: 300, - + /** * Toast notification duration in milliseconds */ TOAST_DURATION: 3000, - + /** * Mouse move threshold in pixels * Used to detect if user is dragging vs clicking */ MOUSE_MOVE_THRESHOLD: 5, - + // ===== UI CONFIGURATION ===== - + /** * Drag preview width in pixels * Width of the drag ghost image during drag-and-drop */ DRAG_PREVIEW_WIDTH: 200, - + /** * Tree indentation in pixels * Indentation per level in the file tree */ TREE_INDENT_PX: 12, - + /** * Toast container z-index * Ensures toasts appear above other elements */ TOAST_Z_INDEX: 9999, - + /** * Minimum sidebar width in pixels */ MIN_SIDEBAR_WIDTH: 150, - + /** * Maximum sidebar width as percentage of container */ MAX_SIDEBAR_WIDTH_PERCENT: 40, - + /** * Minimum editor width in pixels */ MIN_EDITOR_WIDTH: 250, - + /** * Maximum editor width as percentage of container */ MAX_EDITOR_WIDTH_PERCENT: 70, - + // ===== VALIDATION CONFIGURATION ===== - + /** * Valid filename pattern * Only lowercase letters, numbers, underscores, and dots allowed */ FILENAME_PATTERN: /^[a-z0-9_]+(\.[a-z0-9_]+)*$/, - + /** * Characters to replace in filenames * All invalid characters will be replaced with underscore */ FILENAME_INVALID_CHARS: /[^a-z0-9_.]/g, - + // ===== STORAGE KEYS ===== - + /** * LocalStorage keys used throughout the application */ @@ -93,99 +93,104 @@ const Config = { * Dark mode preference */ DARK_MODE: 'darkMode', - + /** * Currently selected collection */ SELECTED_COLLECTION: 'selectedCollection', - + /** * Last viewed page (per collection) * Actual key will be: lastViewedPage:{collection} */ LAST_VIEWED_PAGE: 'lastViewedPage', - + /** * Column dimensions (sidebar, editor, preview widths) */ - COLUMN_DIMENSIONS: 'columnDimensions' + COLUMN_DIMENSIONS: 'columnDimensions', + + /** + * Sidebar collapsed state + */ + SIDEBAR_COLLAPSED: 'sidebarCollapsed' }, - + // ===== EDITOR CONFIGURATION ===== - + /** * CodeMirror theme for light mode */ EDITOR_THEME_LIGHT: 'default', - + /** * CodeMirror theme for dark mode */ EDITOR_THEME_DARK: 'monokai', - + /** * Mermaid theme for light mode */ MERMAID_THEME_LIGHT: 'default', - + /** * Mermaid theme for dark mode */ MERMAID_THEME_DARK: 'dark', - + // ===== FILE TREE CONFIGURATION ===== - + /** * Default content for new files */ DEFAULT_FILE_CONTENT: '# New File\n\n', - + /** * Default filename for new files */ DEFAULT_NEW_FILENAME: 'new_file.md', - + /** * Default folder name for new folders */ DEFAULT_NEW_FOLDERNAME: 'new_folder', - + // ===== WEBDAV CONFIGURATION ===== - + /** * WebDAV base URL */ WEBDAV_BASE_URL: '/fs/', - + /** * PROPFIND depth for file tree loading */ PROPFIND_DEPTH: 'infinity', - + // ===== DRAG AND DROP CONFIGURATION ===== - + /** * Drag preview opacity */ DRAG_PREVIEW_OPACITY: 0.8, - + /** * Dragging item opacity */ DRAGGING_OPACITY: 0.4, - + /** * Drag preview offset X in pixels */ DRAG_PREVIEW_OFFSET_X: 10, - + /** * Drag preview offset Y in pixels */ DRAG_PREVIEW_OFFSET_Y: 10, - + // ===== NOTIFICATION TYPES ===== - + /** * Bootstrap notification type mappings */ diff --git a/static/js/file-tree.js b/static/js/file-tree.js index 3b33394..43e6c9e 100644 --- a/static/js/file-tree.js +++ b/static/js/file-tree.js @@ -4,13 +4,14 @@ */ class FileTree { - constructor(containerId, webdavClient) { + constructor(containerId, webdavClient, isEditMode = false) { this.container = document.getElementById(containerId); this.webdavClient = webdavClient; this.tree = []; this.selectedPath = null; this.onFileSelect = null; this.onFolderSelect = null; + this.filterImagesInViewMode = !isEditMode; // Track if we should filter images (true in view mode) // Drag and drop state this.draggedNode = null; @@ -426,6 +427,19 @@ class FileTree { renderNodes(nodes, parentElement, level) { nodes.forEach(node => { + // Filter out images and image directories in view mode + if (this.filterImagesInViewMode) { + // Skip image files + if (!node.isDirectory && PathUtils.isBinaryFile(node.path)) { + return; + } + + // Skip image directories + if (node.isDirectory && PathUtils.isImageDirectory(node.path)) { + return; + } + } + const nodeWrapper = document.createElement('div'); nodeWrapper.className = 'tree-node-wrapper'; diff --git a/static/js/sidebar-toggle.js b/static/js/sidebar-toggle.js new file mode 100644 index 0000000..876e753 --- /dev/null +++ b/static/js/sidebar-toggle.js @@ -0,0 +1,114 @@ +/** + * Sidebar Toggle Module + * Manages sidebar collapse/expand functionality with localStorage persistence + */ + +class SidebarToggle { + constructor(sidebarId, toggleButtonId) { + this.sidebar = document.getElementById(sidebarId); + this.toggleButton = document.getElementById(toggleButtonId); + this.storageKey = Config.STORAGE_KEYS.SIDEBAR_COLLAPSED || 'sidebarCollapsed'; + this.isCollapsed = localStorage.getItem(this.storageKey) === 'true'; + + this.init(); + } + + /** + * Initialize the sidebar toggle + */ + init() { + // Apply initial state + this.apply(); + + // Setup toggle button click handler + if (this.toggleButton) { + this.toggleButton.addEventListener('click', () => { + this.toggle(); + }); + } + + // Make mini sidebar clickable to expand + if (this.sidebar) { + this.sidebar.addEventListener('click', (e) => { + // Only expand if sidebar is collapsed and click is on the mini sidebar itself + // (not on the file tree content when expanded) + if (this.isCollapsed) { + this.expand(); + } + }); + + // Add cursor pointer when collapsed + this.sidebar.style.cursor = 'default'; + } + + Logger.debug(`Sidebar initialized: ${this.isCollapsed ? 'collapsed' : 'expanded'}`); + } + + /** + * Toggle sidebar state + */ + toggle() { + this.isCollapsed = !this.isCollapsed; + localStorage.setItem(this.storageKey, this.isCollapsed); + this.apply(); + + Logger.debug(`Sidebar ${this.isCollapsed ? 'collapsed' : 'expanded'}`); + } + + /** + * Apply the current sidebar state + */ + apply() { + if (this.sidebar) { + if (this.isCollapsed) { + this.sidebar.classList.add('collapsed'); + this.sidebar.style.cursor = 'pointer'; // Make mini sidebar clickable + } else { + this.sidebar.classList.remove('collapsed'); + this.sidebar.style.cursor = 'default'; // Normal cursor when expanded + } + } + + // Update toggle button icon + if (this.toggleButton) { + const icon = this.toggleButton.querySelector('i'); + if (icon) { + if (this.isCollapsed) { + icon.className = 'bi bi-layout-sidebar-inset-reverse'; + } else { + icon.className = 'bi bi-layout-sidebar'; + } + } + } + } + + /** + * Collapse the sidebar + */ + collapse() { + if (!this.isCollapsed) { + this.toggle(); + } + } + + /** + * Expand the sidebar + */ + expand() { + if (this.isCollapsed) { + this.toggle(); + } + } + + /** + * Check if sidebar is currently collapsed + * @returns {boolean} True if sidebar is collapsed + */ + isCollapsedState() { + return this.isCollapsed; + } +} + +// Make SidebarToggle globally available +window.SidebarToggle = SidebarToggle; + diff --git a/static/js/utils.js b/static/js/utils.js index 926863a..909ebf1 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -103,6 +103,31 @@ const PathUtils = { return binaryExtensions.includes(extension); }, + /** + * Check if a directory is an image directory based on its name + * @param {string} path - The directory path + * @returns {boolean} True if the directory is for images + * @example PathUtils.isImageDirectory('images') // true + * @example PathUtils.isImageDirectory('assets/images') // true + * @example PathUtils.isImageDirectory('docs') // false + */ + isImageDirectory(path) { + const dirName = PathUtils.getFileName(path).toLowerCase(); + const imageDirectoryNames = [ + 'images', + 'image', + 'img', + 'imgs', + 'pictures', + 'pics', + 'photos', + 'assets', + 'media', + 'static' + ]; + return imageDirectoryNames.includes(dirName); + }, + /** * Get a human-readable file type description * @param {string} path - The file path diff --git a/templates/index.html b/templates/index.html index 28f4aff..b78a9a8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -30,10 +30,18 @@