diff --git a/App/frontend-app/src/components/resizer/panelResizer.scss b/App/frontend-app/src/components/resizer/panelResizer.scss new file mode 100644 index 00000000..f569b2e9 --- /dev/null +++ b/App/frontend-app/src/components/resizer/panelResizer.scss @@ -0,0 +1,64 @@ +.panel-resizer { + width: 8px; + background: #f0f0f0; + border-left: 1px solid #ddd; + border-right: 1px solid #ddd; + cursor: col-resize; + position: relative; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s ease; + user-select: none; + flex-shrink: 0; + + &:hover { + background: #e0e0e0; + } + + &.dragging { + background: #d0d0d0; + cursor: col-resize; + } +} + +.resizer-handle { + height: 60px; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; +} + +.resizer-dots { + display: flex; + flex-direction: column; + gap: 3px; + align-items: center; +} + +.dot { + width: 3px; + height: 3px; + background-color: #999; + border-radius: 50%; +} + +.panel-resizer:hover .dot { + background-color: #666; +} + +.panel-resizer.dragging .dot { + background-color: #333; +} + +/* Global cursor override when dragging */ +body.resizing { + cursor: col-resize !important; + user-select: none !important; +} + +body.resizing * { + pointer-events: none !important; +} diff --git a/App/frontend-app/src/components/resizer/panelResizer.tsx b/App/frontend-app/src/components/resizer/panelResizer.tsx new file mode 100644 index 00000000..5eb7fee2 --- /dev/null +++ b/App/frontend-app/src/components/resizer/panelResizer.tsx @@ -0,0 +1,125 @@ +import React, { useState, useRef, useEffect } from 'react'; + +interface PanelResizerProps { + onResize?: (leftWidth: number, rightWidth: number) => void; + minLeftWidth?: number; + minRightWidth?: number; + initialLeftWidth?: number; +} + +export const PanelResizer: React.FC = ({ + onResize, + minLeftWidth = 200, + minRightWidth = 200, + initialLeftWidth = 50 +}) => { + const [isDragging, setIsDragging] = useState(false); + const resizerRef = useRef(null); + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging || !resizerRef.current) return; + + const container = resizerRef.current.parentElement; + if (!container) return; + + const containerRect = container.getBoundingClientRect(); + const newLeftWidth = ((e.clientX - containerRect.left) / containerRect.width) * 100; + const newRightWidth = 100 - newLeftWidth; + + // Apply minimum width constraints + const minLeftPercent = (minLeftWidth / containerRect.width) * 100; + const minRightPercent = (minRightWidth / containerRect.width) * 100; + + if (newLeftWidth >= minLeftPercent && newRightWidth >= minRightPercent) { + onResize?.(newLeftWidth, newRightWidth); + } + }; + + const handleMouseUp = () => { + setIsDragging(false); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }; + + if (isDragging) { + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + } + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, [isDragging, onResize, minLeftWidth, minRightWidth]); + + const handleMouseDown = () => { + setIsDragging(true); + }; + + const resizerStyle: React.CSSProperties = { + width: '8px', + background: isDragging ? '#d0d0d0' : '#f0f0f0', + borderLeft: '1px solid #ddd', + borderRight: '1px solid #ddd', + cursor: 'col-resize', + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + transition: 'background-color 0.2s ease', + userSelect: 'none', + flexShrink: 0, + }; + + const handleStyle: React.CSSProperties = { + height: '60px', + width: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + pointerEvents: 'none', + }; + + const dotsStyle: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + gap: '3px', + alignItems: 'center', + }; + + const dotStyle: React.CSSProperties = { + width: '3px', + height: '3px', + backgroundColor: isDragging ? '#333' : '#999', + borderRadius: '50%', + }; + + return ( +
{ + if (!isDragging) { + (e.target as HTMLElement).style.background = '#e0e0e0'; + } + }} + onMouseLeave={(e) => { + if (!isDragging) { + (e.target as HTMLElement).style.background = '#f0f0f0'; + } + }} + > +
+
+
+
+
+
+
+
+ ); +}; diff --git a/App/frontend-app/src/pages/home/home.module.scss b/App/frontend-app/src/pages/home/home.module.scss index 2c9e88ee..aa821848 100644 --- a/App/frontend-app/src/pages/home/home.module.scss +++ b/App/frontend-app/src/pages/home/home.module.scss @@ -37,16 +37,64 @@ .filters-panel { width: 15% !important; min-width: 229px; + transition: width 0.3s ease-in-out; } .documents-panel { width: 34% !important; min-width: 410px; + transition: width 0.3s ease-in-out; } .chat-panel { width: 51% !important; + min-width: 300px; + transition: width 0.3s ease-in-out; } + + .filters-panel-collapsed { + width: 50px !important; + min-width: 50px; + transition: width 0.3s ease-in-out; + } +} + +/* Resizable panels styling */ +.resizable-container { + display: flex; + flex-direction: row; + flex-grow: 1; + overflow: hidden; +} + +.resizable-panel { + display: flex; + flex-direction: column; + min-width: 200px; + overflow: hidden; +} + +/* Filter panel transitions */ +.filter-panel-enter { + opacity: 0; + transform: translateX(-100%); +} + +.filter-panel-enter-active { + opacity: 1; + transform: translateX(0); + transition: opacity 300ms, transform 300ms; +} + +.filter-panel-exit { + opacity: 1; + transform: translateX(0); +} + +.filter-panel-exit-active { + opacity: 0; + transform: translateX(-100%); + transition: opacity 300ms, transform 300ms; } @media (max-width: 1080px) { diff --git a/App/frontend-app/src/pages/home/home.tsx b/App/frontend-app/src/pages/home/home.tsx index 99b2ea42..b0318f30 100644 --- a/App/frontend-app/src/pages/home/home.tsx +++ b/App/frontend-app/src/pages/home/home.tsx @@ -32,6 +32,8 @@ import { SidecarCopilot } from "../../components/sidecarCopilot/sidecar"; import homeStyles from "./home.module.scss"; import { ChatRoom } from "../../components/chat/chatRoom"; import UploadFilesButton from "../../components/uploadButton/uploadButton"; +import { PanelResizer } from "../../components/resizer/panelResizer"; +import { ChevronLeft24Regular, ChevronRight24Regular } from "@fluentui/react-icons"; const useStyles = makeStyles({ dropdown: { @@ -64,6 +66,9 @@ export function Home({ isSearchResultsPage }: HomeProps) { const [isLoading, setIsLoading] = useState(false); const [searchParams, setSearchParams] = useSearchParams(); const [areFiltersVisible, setAreFiltersVisible] = useState(true); + const [isFilterPanelCollapsed, setIsFilterPanelCollapsed] = useState(false); + const [documentsWidth, setDocumentsWidth] = useState(34); // Initial width percentage + const [chatWidth, setChatWidth] = useState(51); // Initial width percentage const [showCopilot, setShowCopilot] = useState(false); const [incomingFilter, setIncomingFilter] = useState( "(document/embedded eq false and document/translated eq false)" @@ -236,6 +241,15 @@ export function Home({ isSearchResultsPage }: HomeProps) { setAreFiltersVisible((prevAreFiltersVisible) => !prevAreFiltersVisible); } + function toggleFilterPanel(): void { + setIsFilterPanelCollapsed(!isFilterPanelCollapsed); + } + + const handlePanelResize = (leftWidth: number, rightWidth: number) => { + setDocumentsWidth(leftWidth); + setChatWidth(rightWidth); + }; + const handleSortSelected = (sort: string) => { setInOrderBy(sort); }; @@ -526,25 +540,50 @@ export function Home({ isSearchResultsPage }: HomeProps) {
- {/* Left Section: Filter */} -
- {/* Keep the FilterButton at the top */} -
- + {/* Left Section: Filter Panel with Toggle */} + {!isFilterPanelCollapsed && ( +
+ {/* Filter Header with Toggle Button */} +
+ +
+ {areFiltersVisible && ( + + )}
- {areFiltersVisible && ( - - )} -
+ )} - {/* Right Section: Search Box and Fluent v2 Dropdown */} -
+ {/* Collapsed Filter Toggle Button */} + {isFilterPanelCollapsed && ( +
+
+ )} + + {/* Documents Section */} +
{/* Adjusted space between elements */} {/* Search Box */}
+ {/* Panel Resizer */} + + {/* Chat Room Section */} -
+