diff --git a/app/public/css/layout.css b/app/public/css/layout.css
index 88dc45a0..21d1c2d7 100644
--- a/app/public/css/layout.css
+++ b/app/public/css/layout.css
@@ -1,11 +1,12 @@
/* Main Layout Components */
#container {
- height: 98%;
- width: 100%;
+ height: calc(100% - 40px);
+ width: calc(100% - 40px);
display: flex;
flex-direction: row;
position: relative;
+ margin-left: 40px;
}
.logo {
diff --git a/app/public/css/menu.css b/app/public/css/menu.css
index b96ade43..dfa5a62c 100644
--- a/app/public/css/menu.css
+++ b/app/public/css/menu.css
@@ -3,6 +3,7 @@
.sidebar-container {
width: 0;
min-width: 0;
+ max-width: 50dvw;
overflow: hidden;
padding: 20px 0;
display: flex;
@@ -17,13 +18,70 @@
}
.sidebar-container.open {
- width: 30dvw;
+ width: 50%;
padding: 20px;
- left: 48px;
visibility: visible;
pointer-events: auto;
}
+.sidebar-container.resizing {
+ transition: none; /* Disable transitions while resizing */
+}
+
+/* Resize handle styles */
+.resize-handle {
+ width: 6px;
+ background: transparent;
+ cursor: col-resize;
+ position: relative;
+ z-index: 10;
+ opacity: 0;
+ transition: opacity 0.2s ease, background-color 0.2s ease;
+ flex-shrink: 0;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.resize-handle::before {
+ content: '';
+ width: 3px;
+ height: 40px;
+ background: var(--falkor-border-primary);
+ border-radius: 2px;
+ transition: background-color 0.2s ease;
+ opacity: 0.6;
+}
+
+.resize-handle:hover::before,
+.resize-handle.resizing::before {
+ background: var(--falkor-primary);
+ opacity: 1;
+}
+
+.resize-handle:hover,
+.resize-handle.resizing {
+ opacity: 1;
+}
+
+/* Show resize handles only when sidebars are open */
+.sidebar-container.open + .resize-handle {
+ opacity: 0.7;
+}
+
+.sidebar-container.open + .resize-handle:hover,
+.sidebar-container.open + .resize-handle.resizing {
+ opacity: 1;
+}
+
+/* Hide resize handles when sidebars are closed */
+.sidebar-container:not(.open) + .resize-handle {
+ opacity: 0;
+ pointer-events: none;
+ width: 0;
+}
+
#menu-content {
flex-grow: 1;
display: flex;
@@ -164,7 +222,7 @@
#schema-controls {
position: absolute;
padding: 10px;
- bottom: 15px;
+ bottom: 0px;
left: 0px;
display: flex;
flex-direction: row;
diff --git a/app/templates/chat.j2 b/app/templates/chat.j2
index 637aa435..d39e76c5 100644
--- a/app/templates/chat.j2
+++ b/app/templates/chat.j2
@@ -10,7 +10,9 @@
{% include 'components/left_toolbar.j2' %}
{% include 'components/sidebar_menu.j2' %}
+
{% include 'components/sidebar_schema.j2' %}
+
diff --git a/app/ts/app.ts b/app/ts/app.ts
index 3f4702c2..e9faf856 100644
--- a/app/ts/app.ts
+++ b/app/ts/app.ts
@@ -17,6 +17,7 @@ import {
setupToolbar,
handleWindowResize,
setupCustomDropdown,
+ setupResizeHandles,
} from "./modules/ui";
import { setupAuthenticationModal, setupDatabaseModal } from "./modules/modals";
import { resizeGraph, showGraph } from "./modules/schema";
@@ -226,6 +227,7 @@ function setupUIComponents() {
initLeftToolbar();
setupCustomDropdown();
setupTextareaAutoResize();
+ setupResizeHandles();
}
function loadInitialData() {
diff --git a/app/ts/modules/ui.ts b/app/ts/modules/ui.ts
index 28fdf4ff..a3bae915 100644
--- a/app/ts/modules/ui.ts
+++ b/app/ts/modules/ui.ts
@@ -12,11 +12,18 @@ export function toggleContainer(container: HTMLElement, onOpen?: () => void) {
allContainers.forEach((c) => {
if (c !== container && c.classList.contains("open")) {
c.classList.remove("open");
+ // Clear the inline width style when closing other panels
+ (c as HTMLElement).style.width = '';
}
});
if (!container.classList.contains("open")) {
container.classList.add("open");
+
+ // Reset to default 50% width when opening
+ if (!isMobile) {
+ container.style.width = '50%';
+ }
if (!isMobile && DOM.chatContainer) {
DOM.chatContainer.style.paddingRight = "10%";
@@ -25,6 +32,9 @@ export function toggleContainer(container: HTMLElement, onOpen?: () => void) {
if (onOpen) onOpen();
} else {
container.classList.remove("open");
+
+ // Clear any inline width style that was set during resizing
+ container.style.width = '';
if (!isMobile && DOM.chatContainer) {
DOM.chatContainer.style.paddingRight = "20%";
@@ -246,3 +256,121 @@ export function setupCustomDropdown() {
}
});
}
+
+export function setupResizeHandles() {
+ const resizeHandles = document.querySelectorAll('.resize-handle');
+
+ resizeHandles.forEach(handle => {
+ let isResizing = false;
+ let startX = 0;
+ let startWidth = 0;
+ let targetContainer: HTMLElement | null = null;
+
+ const handleMouseDown = (e: MouseEvent | { clientX: number; preventDefault: () => void }) => {
+ isResizing = true;
+ startX = e.clientX;
+
+ // Get the target container from data-target attribute
+ const targetId = (handle as HTMLElement).getAttribute('data-target');
+ targetContainer = targetId ? document.getElementById(targetId) : null;
+
+ if (targetContainer) {
+ startWidth = targetContainer.offsetWidth;
+ (handle as HTMLElement).classList.add('resizing');
+ targetContainer.classList.add('resizing');
+ document.body.style.cursor = 'col-resize';
+ document.body.style.userSelect = 'none';
+ }
+
+ e.preventDefault();
+ };
+
+ const handleMouseMove = (e: MouseEvent | { clientX: number }) => {
+ if (!isResizing || !targetContainer) return;
+
+ const deltaX = e.clientX - startX;
+ const newWidth = startWidth + deltaX;
+
+ // Get parent container width for percentage calculations
+ const parentWidth = targetContainer.parentElement?.offsetWidth || window.innerWidth;
+ const newWidthPercent = (newWidth / parentWidth) * 100;
+
+ // Set minimum and maximum widths as percentages of parent
+ const collapseThreshold = 25; // 25% of parent width
+ const maxWidthPercent = 60; // 60% of parent width
+
+ // If width goes below 25%, collapse the panel
+ if (newWidthPercent < collapseThreshold) {
+ // Directly close the container
+ const isMobile = window.innerWidth <= 768;
+ targetContainer.classList.remove('open');
+
+ // Clear the inline width style that was set during resizing
+ targetContainer.style.width = '';
+
+ // Reset chat container padding when closing
+ if (!isMobile && DOM.chatContainer) {
+ DOM.chatContainer.style.paddingRight = "20%";
+ DOM.chatContainer.style.paddingLeft = "20%";
+ }
+
+ // Clean up resize state
+ isResizing = false;
+ (handle as HTMLElement).classList.remove('resizing');
+ targetContainer.classList.remove('resizing');
+ document.body.style.cursor = '';
+ document.body.style.userSelect = '';
+ targetContainer = null;
+ return;
+ }
+
+ const clampedWidthPercent = Math.max(collapseThreshold, Math.min(maxWidthPercent, newWidthPercent));
+ targetContainer.style.width = clampedWidthPercent + '%';
+
+ // Trigger graph resize if schema container is being resized
+ if (targetContainer.id === 'schema-container' && targetContainer.classList.contains('open')) {
+ setTimeout(() => {
+ resizeGraph();
+ }, 50);
+ }
+ };
+
+ const handleMouseUp = () => {
+ if (isResizing) {
+ isResizing = false;
+ (handle as HTMLElement).classList.remove('resizing');
+ if (targetContainer) {
+ targetContainer.classList.remove('resizing');
+ }
+ document.body.style.cursor = '';
+ document.body.style.userSelect = '';
+ targetContainer = null;
+ }
+ };
+
+ handle.addEventListener('mousedown', handleMouseDown as EventListener);
+ document.addEventListener('mousemove', handleMouseMove as EventListener);
+ document.addEventListener('mouseup', handleMouseUp);
+
+ // Handle touch events for mobile
+ handle.addEventListener('touchstart', (e: Event) => {
+ const touchEvent = e as TouchEvent;
+ const touch = touchEvent.touches[0];
+ handleMouseDown({
+ clientX: touch.clientX,
+ preventDefault: () => e.preventDefault()
+ });
+ });
+
+ document.addEventListener('touchmove', (e: Event) => {
+ if (isResizing) {
+ const touchEvent = e as TouchEvent;
+ const touch = touchEvent.touches[0];
+ handleMouseMove({ clientX: touch.clientX });
+ e.preventDefault();
+ }
+ });
+
+ document.addEventListener('touchend', handleMouseUp);
+ });
+}