From 070d1cf1956577ced5ee9ffbb5ad200dcd78a52e Mon Sep 17 00:00:00 2001 From: Victor Chavarro Date: Tue, 1 Jul 2025 16:25:47 +0100 Subject: [PATCH 1/4] feat(aside): open automatically aside submenu --- app/components/Aside/Aside.tsx | 10 ++++++++++ app/layout.tsx | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/app/components/Aside/Aside.tsx b/app/components/Aside/Aside.tsx index fb8d753..ffbe95d 100644 --- a/app/components/Aside/Aside.tsx +++ b/app/components/Aside/Aside.tsx @@ -13,6 +13,7 @@ import SignOut from "@/components/ui/icons/SignOut"; /* Lib */ import supabase from "@/lib/supabase/client"; import useMenuStore from "@/lib/store/menu.store"; +import useViewPortStore from "@/lib/store/viewPort.store"; /* Utils */ import { useCloseOutsideCodeEditor } from "@/utils/ui.utils"; @@ -57,6 +58,9 @@ const Aside = ({ const toggleMainMenu = useMenuStore((state) => state.toggleMainMenu); const closeMainMenu = useMenuStore((state) => state.closeMainMenu); const closeSnippetList = useMenuStore((state) => state.closeSnippetList); + const openSnippetList = () => + useMenuStore.setState({ snippetListOpen: true }); + const isMobile = useViewPortStore((state) => state.isMobile); const { menuType } = codeEditorStates; const router = useRouter(); @@ -98,6 +102,12 @@ const Aside = ({ default: break; } + + // If on mobile, open the snippet list and close the main menu after selecting a category + if (isMobile) { + closeMainMenu(); + openSnippetList(); + } }; const handlerMobileOpenSnippetList = (): void => { diff --git a/app/layout.tsx b/app/layout.tsx index dac8e48..fd0ca56 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -28,6 +28,12 @@ export default function RootLayout({ }) { return ( + + + {children} From d04e6ecf5137a7a522367da1e41a9d48e53408b9 Mon Sep 17 00:00:00 2001 From: Victor Chavarro Date: Tue, 1 Jul 2025 17:50:17 +0100 Subject: [PATCH 2/4] feat(aside): make resizable view --- app/components/Aside/aside.module.css | 2 +- .../CodeEditor/codeEditor.module.css | 2 +- .../ResizableLayout/ResizableLayout.tsx | 110 ++++++++++++++++++ .../resizableLayout.module.css | 70 +++++++++++ app/components/SnippetItem/SnippetItem.tsx | 12 ++ .../SnippetList/snippetlist.module.css | 16 ++- app/snippets/page.tsx | 69 ++++++----- 7 files changed, 243 insertions(+), 38 deletions(-) create mode 100644 app/components/ResizableLayout/ResizableLayout.tsx create mode 100644 app/components/ResizableLayout/resizableLayout.module.css diff --git a/app/components/Aside/aside.module.css b/app/components/Aside/aside.module.css index 36c965f..e72c3c9 100644 --- a/app/components/Aside/aside.module.css +++ b/app/components/Aside/aside.module.css @@ -1,7 +1,6 @@ .container { width: 100%; background: var(--bg-color-dark); - max-width: 15.625rem; border-right: 1px solid var(--border-color); height: 100vh; position: relative; @@ -146,6 +145,7 @@ top: 0; z-index: 5; height: calc(100vh - 3.2rem); + max-width: 15.625rem; transition: width 0.1s, visibility 0s; diff --git a/app/components/CodeEditor/codeEditor.module.css b/app/components/CodeEditor/codeEditor.module.css index 8fa88b4..d5960d3 100644 --- a/app/components/CodeEditor/codeEditor.module.css +++ b/app/components/CodeEditor/codeEditor.module.css @@ -1,5 +1,5 @@ .codeEditorContainer { - width: calc(100% - 39.375rem); + width: 100%; height: calc(100vh - 1.5625rem); position: relative; margin-bottom: -1.5625rem; diff --git a/app/components/ResizableLayout/ResizableLayout.tsx b/app/components/ResizableLayout/ResizableLayout.tsx new file mode 100644 index 0000000..95e7acd --- /dev/null +++ b/app/components/ResizableLayout/ResizableLayout.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { ReactElement, useRef, useCallback, useEffect, useState } from "react"; +import useViewPortStore from "@/lib/store/viewPort.store"; +import styles from "./resizableLayout.module.css"; + +interface ResizableLayoutProps { + aside: ReactElement; + snippetList: ReactElement; + codeEditor: ReactElement; +} + +const ResizableLayout = ({ + aside, + snippetList, + codeEditor, +}: ResizableLayoutProps): ReactElement => { + const isMobile = useViewPortStore((state) => state.isMobile); + const containerRef = useRef(null); + const [asideWidth, setAsideWidth] = useState(250); + const [snippetListWidth, setSnippetListWidth] = useState(380); + const [isDragging, setIsDragging] = useState<"aside" | "snippetList" | null>( + null + ); + + const handleMouseDown = useCallback((resizer: "aside" | "snippetList") => { + setIsDragging(resizer); + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + }, []); + + const handleMouseMove = useCallback( + (e: MouseEvent) => { + if (!isDragging || !containerRef.current) return; + + const containerRect = containerRef.current.getBoundingClientRect(); + const mouseX = e.clientX - containerRect.left; + + if (isDragging === "aside") { + const newWidth = Math.max(200, Math.min(400, mouseX)); + + setAsideWidth(newWidth); + } else if (isDragging === "snippetList") { + const newWidth = Math.max(380, Math.min(600, mouseX - asideWidth)); + + setSnippetListWidth(newWidth); + } + }, + [isDragging, asideWidth] + ); + + const handleMouseUp = useCallback(() => { + setIsDragging(null); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }, []); + + useEffect(() => { + if (isDragging) { + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + } + + return undefined; + }, [isDragging, handleMouseMove, handleMouseUp]); + + // For mobile, return the original layout without resize functionality + if (isMobile) { + return ( +
+ {aside} + {snippetList} + {codeEditor} +
+ ); + } + + return ( +
+
+ {aside} +
+ +
handleMouseDown("aside")} + /> + +
+ {snippetList} +
+ +
handleMouseDown("snippetList")} + /> + +
+ {codeEditor} +
+
+ ); +}; + +export default ResizableLayout; diff --git a/app/components/ResizableLayout/resizableLayout.module.css b/app/components/ResizableLayout/resizableLayout.module.css new file mode 100644 index 0000000..bbdae07 --- /dev/null +++ b/app/components/ResizableLayout/resizableLayout.module.css @@ -0,0 +1,70 @@ +.container { + display: flex; + flex-direction: row; + height: 100vh; + overflow: hidden; +} + +.mobileContainer { + display: flex; + flex-direction: row; + overflow: hidden; +} + +/* On mobile, CodeEditor should have calculated width to account for fixed-width aside and snippet list */ +@media (width < 1140px) { + .mobileContainer > *:last-child { + width: 100vw; + } +} + +.panel { + width: 100vw; + height: 100vh; + overflow: hidden; + position: relative; + display: flex; + flex-direction: column; +} + +.resizer { + width: 4px; + background: var(--border-color); + cursor: col-resize; + flex-shrink: 0; + position: relative; + transition: background-color 0.2s ease; +} + +.resizer:hover { + background: var(--foreground-color); +} + +.resizer:active { + background: var(--cyan-color); +} + +/* Visual indicator for resize handle */ +.resizer::before { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 2px; + height: 20px; + background: currentColor; + opacity: 0.5; +} + +/* Hide resizers on mobile */ +@media (width < 1140px) { + .resizer { + display: none; + } + + .container .panel { + width: auto !important; + flex: unset !important; + } +} diff --git a/app/components/SnippetItem/SnippetItem.tsx b/app/components/SnippetItem/SnippetItem.tsx index 7298cb7..966eeba 100644 --- a/app/components/SnippetItem/SnippetItem.tsx +++ b/app/components/SnippetItem/SnippetItem.tsx @@ -4,6 +4,10 @@ import { FC, ReactElement, MouseEvent } from "react"; import Trash from "@/components/ui/icons/Trash"; import Restore from "@/components/ui/icons/Restore"; +/* Stores */ +import useMenuStore from "@/lib/store/menu.store"; +import useViewPortStore from "@/lib/store/viewPort.store"; + import styles from "@/components/SnippetList/snippetlist.module.css"; interface SnippetItemPropsComponent extends SnippetItemProps { @@ -23,6 +27,9 @@ const SnippetItem: FC = ({ onDeleteSnippet, onRestoreSnippet, }: SnippetItemPropsComponent): ReactElement => { + const isMobile = useViewPortStore((state) => state.isMobile); + const closeSnippetList = useMenuStore((state) => state.closeSnippetList); + if (snippet) { const snippetClickHandler = ( event: MouseEvent, @@ -30,6 +37,11 @@ const SnippetItem: FC = ({ ): void => { event.preventDefault(); onActiveSnippet(index); + + // Close the snippet list on mobile when a snippet is selected + if (isMobile) { + closeSnippetList(); + } }; const isSnippetActive = activeSnippetIndex === originalIndex; diff --git a/app/components/SnippetList/snippetlist.module.css b/app/components/SnippetList/snippetlist.module.css index 35d846b..edfd039 100644 --- a/app/components/SnippetList/snippetlist.module.css +++ b/app/components/SnippetList/snippetlist.module.css @@ -46,7 +46,6 @@ display: block; width: 100%; background: var(--bg-color-dark); - max-width: 23.75rem; border-right: 1px solid var(--border-color); height: 100vh; transition: @@ -55,6 +54,13 @@ visibility 0.14s; } +/* Add max-width back for mobile compatibility */ +@media (width < 1140px) { + .snippetsListContainer { + max-width: 23.75rem; + } +} + .fields { display: flex; flex-flow: row nowrap; @@ -193,7 +199,9 @@ } @media (width <= 480px) { - max-width: inherit; - height: auto; - width: 100%; + .snippetsListContainer { + max-width: inherit; + height: auto; + width: 100%; + } } diff --git a/app/snippets/page.tsx b/app/snippets/page.tsx index d95e334..4da44b7 100644 --- a/app/snippets/page.tsx +++ b/app/snippets/page.tsx @@ -6,6 +6,7 @@ import { ReactElement, useEffect, useState } from "react"; import Aside from "@/components/Aside/Aside"; import SnippetList from "@/components/SnippetList/SnippetList"; import CodeEditor from "@/components/CodeEditor/CodeEditor"; +import ResizableLayout from "@/components/ResizableLayout/ResizableLayout"; /* Lib and Utils */ import { @@ -341,37 +342,41 @@ export default function Page(): ReactElement { }, []); return ( - <> -