From 58b7c43be59487768a0ba8de48ee70eb5ff2e881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=98=84=EC=84=B1=ED=95=84?= Date: Wed, 5 Feb 2025 11:03:13 +0900 Subject: [PATCH] =?UTF-8?q?sidebar=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/api-tester/APINavigation.jsx | 2 +- .../components/api-tester/MainContentArea.jsx | 11 +- .../components/api-tester/ScenarioEditor.jsx | 2 +- .../api-tester/ScenarioNavigation.jsx | 2 +- TestMasterUI/src/components/ui/sidebar.jsx | 1020 +++++++++-------- 5 files changed, 563 insertions(+), 474 deletions(-) diff --git a/TestMasterUI/src/components/api-tester/APINavigation.jsx b/TestMasterUI/src/components/api-tester/APINavigation.jsx index 9486348..f6b4116 100644 --- a/TestMasterUI/src/components/api-tester/APINavigation.jsx +++ b/TestMasterUI/src/components/api-tester/APINavigation.jsx @@ -240,7 +240,7 @@ const APINavigation = ({ - Collections (Ctrl + /) + Collections {collections.map(renderCollectionItem)} diff --git a/TestMasterUI/src/components/api-tester/MainContentArea.jsx b/TestMasterUI/src/components/api-tester/MainContentArea.jsx index 6e79dc0..3068abc 100644 --- a/TestMasterUI/src/components/api-tester/MainContentArea.jsx +++ b/TestMasterUI/src/components/api-tester/MainContentArea.jsx @@ -173,7 +173,7 @@ const ScenarioMode = ({ }; return (
- +
+
- -
- +
- -
+
+
); }; diff --git a/TestMasterUI/src/components/api-tester/ScenarioEditor.jsx b/TestMasterUI/src/components/api-tester/ScenarioEditor.jsx index 3773851..1a24696 100644 --- a/TestMasterUI/src/components/api-tester/ScenarioEditor.jsx +++ b/TestMasterUI/src/components/api-tester/ScenarioEditor.jsx @@ -102,7 +102,7 @@ const ScenarioEditor = () => { }, []); if (!currentFlowData) { - return
왼쪽에서 시나리오를 선택해 주세요 (Ctrl + .)
; + return
왼쪽에서 시나리오를 선택해 주세요
; } return ( diff --git a/TestMasterUI/src/components/api-tester/ScenarioNavigation.jsx b/TestMasterUI/src/components/api-tester/ScenarioNavigation.jsx index 6292e6e..38a26df 100644 --- a/TestMasterUI/src/components/api-tester/ScenarioNavigation.jsx +++ b/TestMasterUI/src/components/api-tester/ScenarioNavigation.jsx @@ -140,7 +140,7 @@ const ScenarioNavigation = ({ - Scenarios (Ctrl + .) + Scenarios {scenarios.map(renderScenarioItem)} diff --git a/TestMasterUI/src/components/ui/sidebar.jsx b/TestMasterUI/src/components/ui/sidebar.jsx index fd003d1..301ba21 100644 --- a/TestMasterUI/src/components/ui/sidebar.jsx +++ b/TestMasterUI/src/components/ui/sidebar.jsx @@ -1,597 +1,686 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; import { cva } from "class-variance-authority"; -import { PanelLeft } from "lucide-react" +import { PanelLeft } from "lucide-react"; -import { useIsMobile } from "@/hooks/use-mobile" -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Separator } from "@/components/ui/separator" -import { Sheet, SheetContent } from "@/components/ui/sheet" -import { Skeleton } from "@/components/ui/skeleton" +import { useIsMobile } from "@/hooks/use-mobile"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; +import { Sheet, SheetContent } from "@/components/ui/sheet"; +import { Skeleton } from "@/components/ui/skeleton"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, -} from "@/components/ui/tooltip" +} from "@/components/ui/tooltip"; -const SIDEBAR_COOKIE_NAME = "sidebar:state" -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 -const SIDEBAR_WIDTH = "16rem" -const SIDEBAR_WIDTH_MOBILE = "18rem" -const SIDEBAR_WIDTH_ICON = "3rem" -const SIDEBAR_KEYBOARD_SHORTCUT = "b" +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const SIDEBAR_WIDTH = "16rem"; +const SIDEBAR_WIDTH_MOBILE = "18rem"; +const SIDEBAR_WIDTH_ICON = "3rem"; -const SidebarContext = React.createContext(null) +// Use different keyboard shortcuts for left and right sidebars. +const SIDEBAR_LEFT_KEYBOARD_SHORTCUT = "."; +const SIDEBAR_RIGHT_KEYBOARD_SHORTCUT = "/"; -function useSidebar() { - const context = React.useContext(SidebarContext) +// ─── GLOBAL CONTEXT (for state of both sidebars) ───────────────────────────── + +const SidebarContext = React.createContext(null); + +export function useSidebar() { + const context = React.useContext(SidebarContext); if (!context) { - throw new Error("useSidebar must be used within a SidebarProvider.") + throw new Error("useSidebar must be used within a SidebarProvider."); } - - return context + return context; } -const SidebarProvider = React.forwardRef(( - { - defaultOpen = true, - open: openProp, - onOpenChange: setOpenProp, - className, - style, - children, - cookieName, - shortcut, - ...props - }, - ref -) => { - const isMobile = useIsMobile() - const [openMobile, setOpenMobile] = React.useState(false) +// ─── LOCAL INSTANCE CONTEXT (for the current sidebar instance) ───────────── - // This is the internal state of the sidebar. - // We use openProp and setOpenProp for control from outside the component. - const [_open, _setOpen] = React.useState(defaultOpen) - const open = openProp ?? _open - const setOpen = React.useCallback((value) => { - const openState = typeof value === "function" ? value(open) : value - if (setOpenProp) { - setOpenProp(openState) +const SidebarInstanceContext = React.createContext(null); +export function useSidebarInstance() { + const context = React.useContext(SidebarInstanceContext); + if (!context) { + throw new Error("useSidebarInstance must be used within a Sidebar component."); + } + return context; +} + +// ─── SIDEBAR PROVIDER ───────────────────────────────────────────────────────── + +const SidebarProvider = React.forwardRef(({ + // allow separate default values and control for left/right + defaultOpenLeft = true, + defaultOpenRight = true, + openLeft: openLeftProp, + openRight: openRightProp, + onOpenChangeLeft: setOpenLeftProp, + onOpenChangeRight: setOpenRightProp, + cookieNameLeft = "sidebar:left:state", + cookieNameRight = "sidebar:right:state", + className, + style, + children, + shortcut, // (optional; we use our own constants below) + ...props +}, ref) => { + const isMobile = useIsMobile(); + + // Mobile states for left and right sidebars. + const [openMobileLeft, setOpenMobileLeft] = React.useState(false); + const [openMobileRight, setOpenMobileRight] = React.useState(false); + + // Desktop state for left sidebar. + const [_openLeft, _setOpenLeft] = React.useState(defaultOpenLeft); + const openLeft = openLeftProp ?? _openLeft; + const setOpenLeft = React.useCallback((value) => { + const openState = typeof value === "function" ? value(openLeft) : value; + if (setOpenLeftProp) { + setOpenLeftProp(openState); } else { - _setOpen(openState) + _setOpenLeft(openState); } + document.cookie = `${cookieNameLeft}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + }, [setOpenLeftProp, openLeft, cookieNameLeft]); - // This sets the cookie to keep the sidebar state. - document.cookie = `${cookieName}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` - }, [setOpenProp, open]) + // Desktop state for right sidebar. + const [_openRight, _setOpenRight] = React.useState(defaultOpenRight); + const openRight = openRightProp ?? _openRight; + const setOpenRight = React.useCallback((value) => { + const openState = typeof value === "function" ? value(openRight) : value; + if (setOpenRightProp) { + setOpenRightProp(openState); + } else { + _setOpenRight(openState); + } + document.cookie = `${cookieNameRight}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + }, [setOpenRightProp, openRight, cookieNameRight]); - // Helper to toggle the sidebar. - const toggleSidebar = React.useCallback(() => { + const toggleSidebarLeft = React.useCallback(() => { return isMobile - ? setOpenMobile((open) => !open) - : setOpen((open) => !open); - }, [isMobile, setOpen, setOpenMobile]) + ? setOpenMobileLeft((open) => !open) + : setOpenLeft((open) => !open); + }, [isMobile, setOpenLeft, setOpenMobileLeft]); - // Adds a keyboard shortcut to toggle the sidebar. + const toggleSidebarRight = React.useCallback(() => { + return isMobile + ? setOpenMobileRight((open) => !open) + : setOpenRight((open) => !open); + }, [isMobile, setOpenRight, setOpenMobileRight]); + + const stateLeft = openLeft ? "expanded" : "collapsed"; + const stateRight = openRight ? "expanded" : "collapsed"; + + // Keyboard shortcuts (Ctrl/Cmd + key) to toggle each sidebar. React.useEffect(() => { const handleKeyDown = (event) => { - if ( - event.key === shortcut && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault() - toggleSidebar() + if (event.metaKey || event.ctrlKey) { + if (event.key === SIDEBAR_LEFT_KEYBOARD_SHORTCUT) { + event.preventDefault(); + toggleSidebarLeft(); + } else if (event.key === SIDEBAR_RIGHT_KEYBOARD_SHORTCUT) { + event.preventDefault(); + toggleSidebarRight(); + } } - } - - window.addEventListener("keydown", handleKeyDown) + }; + window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [toggleSidebar]) - - // We add a state so that we can do data-state="expanded" or "collapsed". - // This makes it easier to style the sidebar with Tailwind classes. - const state = open ? "expanded" : "collapsed" + }, [toggleSidebarLeft, toggleSidebarRight]); const contextValue = React.useMemo(() => ({ - state, - open, - setOpen, isMobile, - openMobile, - setOpenMobile, - toggleSidebar, - }), [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]) + openLeft, + setOpenLeft, + openRight, + setOpenRight, + openMobileLeft, + setOpenMobileLeft, + openMobileRight, + setOpenMobileRight, + toggleSidebarLeft, + toggleSidebarRight, + stateLeft, + stateRight, + }), [ + isMobile, + openLeft, + setOpenLeft, + openRight, + setOpenRight, + openMobileLeft, + setOpenMobileLeft, + openMobileRight, + setOpenMobileRight, + toggleSidebarLeft, + toggleSidebarRight, + stateLeft, + stateRight, + ]); return ( - ( - -
- {children} -
-
-
) + + +
+ {children} +
+
+
); -}) -SidebarProvider.displayName = "SidebarProvider" +}); +SidebarProvider.displayName = "SidebarProvider"; -const Sidebar = React.forwardRef(( - { - side = "left", - variant = "sidebar", - collapsible = "offcanvas", - className, - children, - ...props - }, - ref -) => { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar() +// ─── SIDEBAR COMPONENT ─────────────────────────────────────────────────────── +const Sidebar = React.forwardRef(({ + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props +}, ref) => { + const { + isMobile, + stateLeft, + stateRight, + openMobileLeft, + openMobileRight, + setOpenMobileLeft, + setOpenMobileRight, + } = useSidebar(); + + // Choose the appropriate state for this sidebar instance. + const currentState = side === "left" ? stateLeft : stateRight; + + // Wrap the sidebar content in a local context provider so that child components + // can easily determine (for example) if their parent sidebar is collapsed. if (collapsible === "none") { return ( - (
- {children} -
) + +
+ {children} +
+
); } if (isMobile) { + const openMobile = side === "left" ? openMobileLeft : openMobileRight; + const setOpenMobile = side === "left" ? setOpenMobileLeft : setOpenMobileRight; return ( - ( - -
{children}
-
-
) + + + +
{children}
+
+
+
); } return ( - (
- {/* This is what handles the sidebar gap on desktop */} -
- ) + ); -}) -Sidebar.displayName = "Sidebar" +}); +Sidebar.displayName = "Sidebar"; -const SidebarTrigger = React.forwardRef(({ className, onClick, ...props }, ref) => { - const { toggleSidebar } = useSidebar() +// ─── SIDEBAR TRIGGER (and similar components) ─────────────────────────────── +const SidebarTrigger = React.forwardRef(({ side = "left", className, onClick, ...props }, ref) => { + const { toggleSidebarLeft, toggleSidebarRight } = useSidebar(); + const toggleSidebar = side === "left" ? toggleSidebarLeft : toggleSidebarRight; return ( - () + ); -}) -SidebarTrigger.displayName = "SidebarTrigger" - -const SidebarRail = React.forwardRef(({ className, ...props }, ref) => { - const { toggleSidebar } = useSidebar() +}); +SidebarTrigger.displayName = "SidebarTrigger"; +const SidebarRail = React.forwardRef(({ side = "left", className, ...props }, ref) => { + const { toggleSidebarLeft, toggleSidebarRight } = useSidebar(); + const toggleSidebar = side === "left" ? toggleSidebarLeft : toggleSidebarRight; return ( - (