Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-resizable-panels": "^3.0.6",
"react-router-dom": "^7.4.1"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "디룩(Delook)",
"author": "@waterbinnn",
"homepage_url": "https://www.delook.co.kr/",
"version": "1.0.9",
"version": "1.0.10",
"description": "개념 학습부터 기술 면접 준비까지, 성장하는 개발자의 새 탭",
"action": {
"default_popup": "popup.html",
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function Layout() {
<div className="relative flex min-h-svh flex-col border-border bg-background lg:mx-4 xl:border-x xl:border-dashed">
<Header />
<main
className={`flex w-full flex-1 flex-col p-4 sm:mx-auto ${!isPageIncludeSidebar ? 'max-w-3xl ' : 'md:p-0'}`}
className={`flex w-full flex-auto flex-col p-4 sm:mx-auto ${!isPageIncludeSidebar ? 'max-w-3xl ' : 'md:p-0'}`}
>
<Outlet />
<Toaster />
Expand Down
15 changes: 12 additions & 3 deletions src/components/layout/SidebarLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { useIsMobile } from '@/hooks';

import { Sidebar, SidebarInset, SidebarMenu, SidebarProvider, SidebarTrigger } from '../ui';
import {
Sidebar,
SidebarInset,
SidebarMenu,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from '../ui';

export const SidebarLayout = ({
sidebarMenu,
Expand All @@ -13,14 +20,16 @@ export const SidebarLayout = ({

return (
<SidebarProvider>
<Sidebar className="top-24 h-[calc(100svh-6.5rem)] w-[--sidebar-width] overflow-y-auto border-r border-dashed px-2 group-data-[side=left]:border-none md:sticky">
<Sidebar className="top-24 h-[calc(100svh-6.5rem)] w-[--sidebar-width] border-r border-dashed px-2 group-data-[side=left]:border-none md:sticky">
<SidebarMenu className="h-full pt-4">{sidebarMenu}</SidebarMenu>
<SidebarRail />
</Sidebar>

<div className="flex flex-col">
<div className="flex flex-col overflow-x-auto">
{isMobile && (
<SidebarTrigger className="bg-primary text-gray-100 hover:bg-primary/90 hover:text-gray-100" />
)}

<SidebarInset className="flex-1">{children}</SidebarInset>
</div>
</SidebarProvider>
Expand Down
40 changes: 40 additions & 0 deletions src/components/ui/Resizable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { GripVertical } from 'lucide-react';
import * as ResizablePrimitive from 'react-resizable-panels';

import { cn } from '@/lib/utils';

const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn('flex h-full w-full data-[panel-group-direction=vertical]:flex-col', className)}
{...props}
/>
);

const ResizablePanel = ResizablePrimitive.Panel;

const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean;
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
'relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',
className,
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);

export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
126 changes: 89 additions & 37 deletions src/components/ui/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
TooltipTrigger,
} from '@/components';
import { useIsMobile } from '@/hooks';
import { useSidebarResize } from '@/hooks/useSidebarResize';
import { mergeButtonRefs } from '@/lib/merge-button-refs';
import { cn } from '@/lib/utils';

const SIDEBAR_COOKIE_NAME = 'sidebar_state';
Expand All @@ -25,6 +27,8 @@ const SIDEBAR_WIDTH = '22rem';
const SIDEBAR_WIDTH_MOBILE = '18rem';
const SIDEBAR_WIDTH_ICON = '3rem';
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
const MAX_SIDEBAR_WIDTH = '25rem';
const MIN_SIDEBAR_WIDTH = '18rem';

export type SidebarContextProps = {
state: 'expanded' | 'collapsed';
Expand All @@ -34,6 +38,10 @@ export type SidebarContextProps = {
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
width: string;
setWidth: (width: string) => void;
isDraggingRail: boolean;
setIsDraggingRail: (isDraggingRail: boolean) => void;
};

const SidebarContext = React.createContext<SidebarContextProps | null>(null);
Expand All @@ -53,6 +61,7 @@ const SidebarProvider = React.forwardRef<
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
defaultWidth?: string;
}
>(
(
Expand All @@ -63,12 +72,15 @@ const SidebarProvider = React.forwardRef<
className,
style,
children,
defaultWidth = SIDEBAR_WIDTH,
...props
},
ref,
) => {
const isMobile = useIsMobile();
const [width, setWidth] = React.useState(defaultWidth);
const [openMobile, setOpenMobile] = React.useState(false);
const [isDraggingRail, setIsDraggingRail] = React.useState(false);

// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
Expand Down Expand Up @@ -120,8 +132,22 @@ const SidebarProvider = React.forwardRef<
openMobile,
setOpenMobile,
toggleSidebar,
width,
setWidth,
isDraggingRail,
setIsDraggingRail,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
[
state,
open,
setOpen,
isMobile,
openMobile,
// setOpenMobile,
toggleSidebar,
width,
isDraggingRail,
],
);

return (
Expand All @@ -130,7 +156,7 @@ const SidebarProvider = React.forwardRef<
<div
style={
{
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width': width,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
Expand Down Expand Up @@ -170,7 +196,7 @@ const Sidebar = React.forwardRef<
},
ref,
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
const { isMobile, state, openMobile, setOpenMobile, isDraggingRail } = useSidebar();

if (collapsible === 'none') {
return (
Expand Down Expand Up @@ -234,28 +260,33 @@ const Sidebar = React.forwardRef<
data-collapsible={state === 'collapsed' ? collapsible : ''}
data-variant={variant}
data-side={side}
data-dragging={isDraggingRail}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
'relative w-[--sidebar-width] bg-background transition-[width] duration-200 ease-linear',
'duration-200 relative h-svh w-(--sidebar-width) bg-transparent transition-[width] ease-linear',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]',
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
//* set duration to 0 for all elements when dragging
'group-data-[dragging=true]:duration-0! group-data-[dragging=true]_*:!duration-0',
)}
/>
<div
className={cn(
'fixed hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex border-dashed',
'duration-200 fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] ease-linear md:flex',
side === 'left'
? 'group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
// Adjust the padding for floating and inset variants.
variant === 'floating' || variant === 'inset'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
//* set duration to 0 for all elements when dragging
'group-data-[dragging=true]:duration-0! group-data-[dragging=true]_*:!duration-0',
className,
)}
{...props}
Expand Down Expand Up @@ -299,32 +330,53 @@ const SidebarTrigger = React.forwardRef<
});
SidebarTrigger.displayName = 'SidebarTrigger';

const SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<'button'>>(
({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
const SidebarRail = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<'button'> & {
enableDrag?: boolean;
}
>(({ className, enableDrag = true, ...props }, ref) => {
const { toggleSidebar, setWidth, state, width, setIsDraggingRail } = useSidebar();

const { dragRef, handleMouseDown } = useSidebarResize({
direction: 'right',
enableDrag,
onResize: setWidth,
onToggle: toggleSidebar,
currentWidth: width,
isCollapsed: state === 'collapsed',
minResizeWidth: MIN_SIDEBAR_WIDTH,
maxResizeWidth: MAX_SIDEBAR_WIDTH,
setIsDraggingRail,
widthCookieName: 'sidebar:width',
widthCookieMaxAge: 60 * 60 * 24 * 7, // 1 week
});

//* Merge external ref with our dragRef
const combinedRef = React.useMemo(() => mergeButtonRefs([ref, dragRef]), [ref, dragRef]);

return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar',
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
className,
)}
{...props}
/>
);
},
);
return (
<button
ref={combinedRef}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onMouseDown={handleMouseDown}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
'absolute inset-y-0 z- hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar',
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
className,
)}
{...props}
/>
);
});
SidebarRail.displayName = 'SidebarRail';

const SidebarInset = React.forwardRef<HTMLDivElement, React.ComponentProps<'main'>>(
Expand Down Expand Up @@ -670,7 +722,7 @@ const SidebarMenuSubButton = React.forwardRef<
data-size={size}
data-active={isActive}
className={cn(
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
'flex min-h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === 'sm' && 'text-xs',
size === 'md' && 'text-sm',
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/SidebarCollapsibleMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const SidebarCollapsibleMenu = ({
closeOnClick = true,
}: SidebarCollapsibleMenuItemProps) => {
return (
<Collapsible defaultOpen className="group/collapsible">
<Collapsible defaultOpen={true} className="group/collapsible">
<SidebarMenuItem>
{/* Parent Item */}
<CollapsibleTrigger asChild>
Expand Down
1 change: 1 addition & 0 deletions src/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './ImageContainer';
export * from './Input';
export * from './Label';
export * from './Popover';
export * from './Resizable';
export * from './Sheet';
export * from './Sidebar';
export * from './SidebarCollapsibleMenu';
Expand Down
6 changes: 4 additions & 2 deletions src/features/post/components/PostSidebarLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { CategoryList } from '../types/postTypes';
export const PostSidebarLayout = ({
categoryList,
children,
activePost,
}: {
categoryList: CategoryList[];
children: React.ReactNode;
activePost?: string;
}) => {
const [searchParams, setSearchParams] = useSearchParams();

Expand All @@ -28,15 +30,15 @@ export const PostSidebarLayout = ({
items={posts.map(({ filename, title }) => ({
key: filename,
title: title,
isActive: searchParams.get('filename') === filename,
isActive: searchParams.get('filename') === filename || activePost === filename,
onClick: () => handleSearchParams(category, filename),
}))}
/>
));

return (
<SidebarLayout sidebarMenu={sidebarMenu}>
<div className="mx-auto max-w-3xl md:pl-6 md:pt-4">{children}</div>
<div className="mx-auto max-w-3xl md:px-6 md:pt-4">{children}</div>
</SidebarLayout>
);
};
Loading