diff --git a/collections/documents/docusaurus.md b/collections/documents/docusaurus.md new file mode 100644 index 0000000..74a9422 --- /dev/null +++ b/collections/documents/docusaurus.md @@ -0,0 +1,44 @@ +## Using Docusaurus + +Once you've set up Hero, you can use it to develop, manage and publish Docusaurus websites. + +## Launch the Hero Website + +To start a Hero Docusaurus website in development mode: + +- Build the book then close the prompt with `Ctrl+C` + + ```bash + hero docs -d + ``` + +- See the book on the local browser + + ``` + bash /root/hero/var/docusaurus/develop.sh + ``` + +You can then view the website in your browser at `https://localhost:3100`. + +## Publish a Website + +- To build and publish a Hero website: + - Development + + ```bash + hero docs -bpd + ``` + + - Production + + ```bash + hero docs -bp + ``` + +If you want to specify a different SSH key, use `-dk`: + +```bash +hero docs -bpd -dk ~/.ssh/id_ed25519 +``` + +> Note: The container handles the SSH agent and key management automatically on startup, so in most cases, you won't need to manually specify keys. \ No newline at end of file diff --git a/collections/documents/getting_started/hero_docker.md b/collections/documents/getting_started/hero_docker.md new file mode 100644 index 0000000..6488df8 --- /dev/null +++ b/collections/documents/getting_started/hero_docker.md @@ -0,0 +1,67 @@ +You can build Hero as a Docker container. + +The code is availabe at this [open-source repository](https://github.com/mik-tf/hero-container). + +## Prerequisites + +- Docker installed on your system (More info [here](https://manual.grid.tf/documentation/system_administrators/computer_it_basics/docker_basics.html#install-docker-desktop-and-docker-engine)) +- SSH keys for deploying Hero websites (if publishing) + +## Build the Image + +- Clone the repository + + ``` + git clone https://github.com/mik-tf/hero-container + cd hero-container + ``` + +- Build the Docker image: + + ```bash + docker build -t heroc . + ``` + +## Pull the Image from Docker Hub + +If you don't want to build the image, you can pull it from Docker Hub. + +``` +docker pull logismosis/heroc +``` + +In this case, use `logismosi/heroc` instead of `heroc` to use the container. + +## Run the Hero Container + +You can run the container with an interactive shell: + +```bash +docker run -it heroc +``` + +You can run the container with an interactive shell, while setting the host as your local network, mounting your current directory as the workspace and adding your SSH keys: + +```bash +docker run --network=host \ + -v $(pwd):/workspace \ + -v ~/.ssh:/root/ssh \ + -it heroc +``` + +By default, the container will: + +- Start Redis server in the background +- Copy your SSH keys to the proper location +- Initialize the SSH agent +- Add your default SSH key (`id_ed25519`) + +To use a different SSH key, specify it with the KEY environment variable (e.g. `KEY=id_ed25519`): + +```bash +docker run --network=host \ + -v $(pwd):/workspace \ + -v ~/.ssh:/root/ssh \ + -e KEY=your_custom_key_name \ + -it heroc +``` \ No newline at end of file diff --git a/collections/documents/getting_started/hero_native.md b/collections/documents/getting_started/hero_native.md new file mode 100644 index 0000000..9555bb6 --- /dev/null +++ b/collections/documents/getting_started/hero_native.md @@ -0,0 +1,22 @@ +## Basic Hero + +You can build Hero natively with the following lines: + +``` +curl https://raw.githubusercontent.com/freeflowuniverse/herolib/refs/heads/development/install_hero.sh > /tmp/install_hero.sh +bash /tmp/install_hero.sh +``` + +## Hero for Developers + +For developers, use the following commands: + +``` +curl 'https://raw.githubusercontent.com/freeflowuniverse/herolib/refs/heads/development/install_v.sh' > /tmp/install_v.sh +bash /tmp/install_v.sh --analyzer --herolib +#DONT FORGET TO START A NEW SHELL (otherwise the paths will not be set) +``` + +## Hero with Docker + +If you have issues running Hero natively, you can use the [Docker version of Hero](hero_docker.md). \ No newline at end of file diff --git a/collections/documents/intro.md b/collections/documents/intro.md new file mode 100644 index 0000000..4f2e13f --- /dev/null +++ b/collections/documents/intro.md @@ -0,0 +1,5 @@ +This ebook contains the basic information to get you started with the Hero tool. + +## What is Hero? + +Hero is an open-source toolset to work with Git, AI, mdBook, Docusaurus, Starlight and more. \ No newline at end of file diff --git a/collections/documents/support.md b/collections/documents/support.md new file mode 100644 index 0000000..1dadeae --- /dev/null +++ b/collections/documents/support.md @@ -0,0 +1 @@ +If you need help with Hero, reach out to the ThreeFold Support team [here](https://threefoldfaq.crisp.help/en/). \ No newline at end of file diff --git a/config.yaml b/config.yaml index 4f2e67b..1c0a6da 100644 --- a/config.yaml +++ b/config.yaml @@ -8,18 +8,6 @@ collections: projects: path: ./collections/projects description: Project documentation - new_collectionss: - path: collections/new_collectionss - description: 'User-created collection: new_collectionss' - test_collection_new: - path: collections/test_collection_new - description: 'User-created collection: test_collection_new' - dynamic_test: - path: collections/dynamic_test - description: 'User-created collection: dynamic_test' - runtime_collection: - path: collections/runtime_collection - description: 'User-created collection: runtime_collection' 7madah: path: collections/7madah description: 'User-created collection: 7madah' diff --git a/server_webdav.py b/server_webdav.py index 8150cb0..4f39c48 100755 --- a/server_webdav.py +++ b/server_webdav.py @@ -104,6 +104,10 @@ class MarkdownEditorApp: if path == '/fs/' and method == 'POST': return self.handle_create_collection(environ, start_response) + # API to delete a collection + if path.startswith('/api/collections/') and method == 'DELETE': + return self.handle_delete_collection(environ, start_response) + # Check if path starts with a collection name (for SPA routing) # This handles URLs like /notes/ttt or /documents/file.md # MUST be checked BEFORE WebDAV routing to prevent WebDAV from intercepting SPA routes @@ -205,7 +209,68 @@ class MarkdownEditorApp: print(f"Error creating collection: {e}") start_response('500 Internal Server Error', [('Content-Type', 'application/json')]) return [json.dumps({'error': str(e)}).encode('utf-8')] - + + def handle_delete_collection(self, environ, start_response): + """Delete a collection""" + try: + # Extract collection name from path: /api/collections/{name} + path = environ.get('PATH_INFO', '') + collection_name = path.split('/')[-1] + + if not collection_name: + start_response('400 Bad Request', [('Content-Type', 'application/json')]) + return [json.dumps({'error': 'Collection name is required'}).encode('utf-8')] + + # Check if collection exists + if collection_name not in self.collections: + start_response('404 Not Found', [('Content-Type', 'application/json')]) + return [json.dumps({'error': f'Collection "{collection_name}" not found'}).encode('utf-8')] + + # Get collection path + collection_config = self.collections[collection_name] + collection_path = Path(collection_config['path']) + + # Delete the collection directory and all its contents + import shutil + if collection_path.exists(): + shutil.rmtree(collection_path) + print(f"Deleted collection directory: {collection_path}") + + # Remove from collections dict + del self.collections[collection_name] + + # Update config file + self.save_config() + + # Remove from WebDAV provider mapping + provider_key = f'/fs/{collection_name}' + if hasattr(self.webdav_app, 'provider_map') and provider_key in self.webdav_app.provider_map: + del self.webdav_app.provider_map[provider_key] + print(f"Removed provider from provider_map: {provider_key}") + + # Remove from sorted_share_list if it exists + if hasattr(self.webdav_app, 'sorted_share_list') and provider_key in self.webdav_app.sorted_share_list: + self.webdav_app.sorted_share_list.remove(provider_key) + print(f"Removed from sorted_share_list: {provider_key}") + + print(f"Deleted collection '{collection_name}'") + + response_body = json.dumps({'success': True, 'name': collection_name}).encode('utf-8') + start_response('200 OK', [ + ('Content-Type', 'application/json'), + ('Content-Length', str(len(response_body))), + ('Access-Control-Allow-Origin', '*') + ]) + + return [response_body] + + except Exception as e: + print(f"Error deleting collection: {e}") + import traceback + traceback.print_exc() + start_response('500 Internal Server Error', [('Content-Type', 'application/json')]) + return [json.dumps({'error': str(e)}).encode('utf-8')] + def handle_static(self, environ, start_response): """Serve static files""" path = environ.get('PATH_INFO', '')[1:] # Remove leading / diff --git a/static/css/components.css b/static/css/components.css index e98d7cf..f524953 100644 --- a/static/css/components.css +++ b/static/css/components.css @@ -388,4 +388,66 @@ body.dark-mode #darkModeBtn i { #darkModeBtn:hover i { color: inherit; /* Inherit hover color from parent */ +} + +/* =================================== + Loading Spinner Component + =================================== */ + +/* Loading overlay - covers the target container */ +.loading-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--bg-primary); + opacity: 0.95; + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + transition: opacity 0.2s ease; +} + +/* Loading spinner */ +.loading-spinner { + width: 48px; + height: 48px; + border: 4px solid var(--border-color); + border-top-color: var(--primary-color); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +/* Spinner animation */ +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Loading text */ +.loading-text { + margin-top: 16px; + color: var(--text-secondary); + font-size: 14px; + text-align: center; +} + +/* Loading container with spinner and text */ +.loading-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} + +/* Hide loading overlay by default */ +.loading-overlay.hidden { + display: none; +} + +.language-bash { + color: var(--text-primary) !important; } \ No newline at end of file diff --git a/static/css/layout.css b/static/css/layout.css index cb53bc0..192d5ed 100644 --- a/static/css/layout.css +++ b/static/css/layout.css @@ -119,7 +119,7 @@ body { #sidebarPane { flex: 0 0 20%; min-width: 150px; - max-width: 40%; + max-width: 20%; padding: 0; height: 100%; overflow: hidden; diff --git a/static/js/app.js b/static/js/app.js index 9750bfb..5660ffe 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -337,6 +337,12 @@ document.addEventListener('DOMContentLoaded', async () => { Logger.info(`Previewing binary file: ${item.path}`); + // Initialize and show loading spinner for binary file preview + editor.initLoadingSpinners(); + if (editor.previewSpinner) { + editor.previewSpinner.show(`Loading ${fileType.toLowerCase()}...`); + } + // Set flag to prevent auto-update of preview editor.isShowingCustomPreview = true; @@ -403,6 +409,14 @@ document.addEventListener('DOMContentLoaded', async () => { // Display in preview pane editor.previewElement.innerHTML = previewHtml; + // Hide loading spinner after content is set + // Add small delay for images to start loading + setTimeout(() => { + if (editor.previewSpinner) { + editor.previewSpinner.hide(); + } + }, fileType === 'Image' ? 300 : 100); + // Highlight the file in the tree fileTree.selectAndExpandPath(item.path); diff --git a/static/js/confirmation.js b/static/js/confirmation.js index 4c50176..1d412ef 100644 --- a/static/js/confirmation.js +++ b/static/js/confirmation.js @@ -70,12 +70,13 @@ class ModalManager { } }, { once: true }); + // Remove aria-hidden before showing to prevent accessibility warning + this.modalElement.removeAttribute('aria-hidden'); + this.modal.show(); // Focus confirm button after modal is shown this.modalElement.addEventListener('shown.bs.modal', () => { - // Ensure aria-hidden is removed (Bootstrap should do this, but be explicit) - this.modalElement.removeAttribute('aria-hidden'); this.confirmButton.focus(); }, { once: true }); }); @@ -130,12 +131,13 @@ class ModalManager { } }, { once: true }); + // Remove aria-hidden before showing to prevent accessibility warning + this.modalElement.removeAttribute('aria-hidden'); + this.modal.show(); // Focus and select input after modal is shown this.modalElement.addEventListener('shown.bs.modal', () => { - // Ensure aria-hidden is removed (Bootstrap should do this, but be explicit) - this.modalElement.removeAttribute('aria-hidden'); this.inputElement.focus(); this.inputElement.select(); }, { once: true }); diff --git a/static/js/editor.js b/static/js/editor.js index 59e2147..d718c48 100644 --- a/static/js/editor.js +++ b/static/js/editor.js @@ -16,6 +16,10 @@ class MarkdownEditor { this.editor = null; // Will be initialized later this.isShowingCustomPreview = false; // Flag to prevent auto-update when showing binary files + // Initialize loading spinners (will be created lazily when needed) + this.editorSpinner = null; + this.previewSpinner = null; + // Only initialize CodeMirror if not in read-only mode (view mode) if (!readOnly) { this.initCodeMirror(); @@ -206,11 +210,34 @@ class MarkdownEditor { } } + /** + * Initialize loading spinners (lazy initialization) + */ + initLoadingSpinners() { + if (!this.editorSpinner && !this.readOnly && this.editorElement) { + this.editorSpinner = new LoadingSpinner(this.editorElement, 'Loading file...'); + } + if (!this.previewSpinner && this.previewElement) { + this.previewSpinner = new LoadingSpinner(this.previewElement, 'Rendering preview...'); + } + } + /** * Load file */ async loadFile(path) { try { + // Initialize loading spinners if not already done + this.initLoadingSpinners(); + + // Show loading spinners + if (this.editorSpinner) { + this.editorSpinner.show('Loading file...'); + } + if (this.previewSpinner) { + this.previewSpinner.show('Loading preview...'); + } + // Reset custom preview flag when loading text files this.isShowingCustomPreview = false; @@ -232,8 +259,25 @@ class MarkdownEditor { // Save as last viewed page this.saveLastViewedPage(path); + + // Hide loading spinners + if (this.editorSpinner) { + this.editorSpinner.hide(); + } + if (this.previewSpinner) { + this.previewSpinner.hide(); + } + // No notification for successful file load - it's not critical } catch (error) { + // Hide loading spinners on error + if (this.editorSpinner) { + this.editorSpinner.hide(); + } + if (this.previewSpinner) { + this.previewSpinner.hide(); + } + console.error('Failed to load file:', error); if (window.showNotification) { window.showNotification('Failed to load file', 'danger'); @@ -409,6 +453,14 @@ class MarkdownEditor { } try { + // Initialize loading spinners if not already done + this.initLoadingSpinners(); + + // Show preview loading spinner (only if not already shown by loadFile) + if (this.previewSpinner && !this.previewSpinner.isVisible()) { + this.previewSpinner.show('Rendering preview...'); + } + // Step 0: Convert JSX-style syntax to HTML let processedContent = this.convertJSXToHTML(markdown); @@ -422,6 +474,9 @@ class MarkdownEditor { if (!this.marked) { console.error("Markdown parser (marked) not initialized."); previewDiv.innerHTML = `