refactor: restructure component architecture

This commit is contained in:
Thomas Bishop 2025-07-18 13:49:46 +01:00
parent 12132bc660
commit a2775060c7
10 changed files with 754 additions and 812 deletions

View file

@ -1,11 +1,10 @@
import Home from "@/pages/home"
import "./App.css" import "./App.css"
import Page from "./containers/page"
import Layout from "./layouts/layout"
export default function App() { export default function App() {
return ( return (
<> <>
<Page /> <Home />
</> </>
) )
} }

View file

@ -1,141 +0,0 @@
import {
FileText,
Waypoints,
SquareLibrary,
Settings,
Tags,
Info,
ChevronRight,
} from "lucide-react"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarHeader,
SidebarFooter,
SidebarMenuSub,
} from "@/components/ui/sidebar"
import { Collapsible, CollapsibleTrigger } from "@radix-ui/react-collapsible"
import { CollapsibleContent } from "./ui/collapsible"
import { mockEntries } from "@/mock-data/mock-entries"
import { mockTags } from "@/mock-data/mock-tags"
const footerMenu = [
{
title: "About",
url: "#",
icon: Info,
},
{
title: "Settings",
url: "#",
icon: Settings,
},
]
export function AppSidebar() {
return (
<Sidebar>
<SidebarHeader className="border-b h-12">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild className="data-[slot=sidebar-menu-button]:!p-1.5">
<a href="#">
<SquareLibrary className="h-5 w-5" />
<span className="text-base font-semibold">Eólas</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem key="graph">
<SidebarMenuButton asChild>
<a href="#">
<Waypoints />
<span>Graph</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
<Collapsible className="group/collapsible">
<SidebarMenuItem key="entries">
<CollapsibleTrigger asChild>
<SidebarMenuButton asChild>
<a href="#">
<FileText />
<span>Entries</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{mockEntries.map((item) => (
<SidebarMenuItem>
<a href={item.url}>
<span className="text-xs">{item.title}</span>
</a>
</SidebarMenuItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
<Collapsible className="group/collapsible">
<SidebarMenuItem key="entries">
<CollapsibleTrigger asChild>
<SidebarMenuButton asChild>
<a href="#">
<Tags />
<span>Tags</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{mockTags.map((item) => (
<SidebarMenuItem>
<a href={item.url}>
<span className="text-xs">{item.title}</span>
</a>
</SidebarMenuItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{footerMenu.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarFooter>
</Sidebar>
)
}

View file

@ -9,19 +9,14 @@ import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { import {
Sheet, Sheet,
SheetContent, SheetContent,
SheetDescription, SheetDescription,
SheetHeader, SheetHeader,
SheetTitle, SheetTitle,
} from "@/components/ui/sheet" } from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton"
import { import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar_state" const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
@ -31,694 +26,667 @@ const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b" const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContextProps = { type SidebarContextProps = {
state: "expanded" | "collapsed" state: "expanded" | "collapsed"
open: boolean open: boolean
setOpen: (open: boolean) => void setOpen: (open: boolean) => void
openMobile: boolean openMobile: boolean
setOpenMobile: (open: boolean) => void setOpenMobile: (open: boolean) => void
isMobile: boolean isMobile: boolean
toggleSidebar: () => void toggleSidebar: () => void
} }
const SidebarContext = React.createContext<SidebarContextProps | null>(null) const SidebarContext = React.createContext<SidebarContextProps | null>(null)
function useSidebar() { function useSidebar() {
const context = React.useContext(SidebarContext) const context = React.useContext(SidebarContext)
if (!context) { 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
} }
function SidebarProvider({ function SidebarProvider({
defaultOpen = true, defaultOpen = true,
open: openProp, open: openProp,
onOpenChange: setOpenProp, onOpenChange: setOpenProp,
className, className,
style, style,
children, children,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
defaultOpen?: boolean defaultOpen?: boolean
open?: boolean open?: boolean
onOpenChange?: (open: boolean) => void onOpenChange?: (open: boolean) => void
}) { }) {
const isMobile = useIsMobile() const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false) const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar. // This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component. // We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen) const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open const open = openProp ?? _open
const setOpen = React.useCallback( const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => { (value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) { if (setOpenProp) {
setOpenProp(openState) setOpenProp(openState)
} else { } else {
_setOpen(openState) _setOpen(openState)
} }
// This sets the cookie to keep the sidebar state. // This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
}, },
[setOpenProp, open] [setOpenProp, open],
) )
// Helper to toggle the sidebar. // Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => { const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile]) }, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar. // Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => { React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if ( if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.key === SIDEBAR_KEYBOARD_SHORTCUT && event.preventDefault()
(event.metaKey || event.ctrlKey) toggleSidebar()
) { }
event.preventDefault() }
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown) window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown) return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar]) }, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed". // 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. // This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed" const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo<SidebarContextProps>( const contextValue = React.useMemo<SidebarContextProps>(
() => ({ () => ({
state, state,
open, open,
setOpen, setOpen,
isMobile, isMobile,
openMobile, openMobile,
setOpenMobile, setOpenMobile,
toggleSidebar, toggleSidebar,
}), }),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
) )
return ( return (
<SidebarContext.Provider value={contextValue}> <SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<div <div
data-slot="sidebar-wrapper" data-slot="sidebar-wrapper"
style={ style={
{ {
"--sidebar-width": SIDEBAR_WIDTH, "--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON, "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style, ...style,
} as React.CSSProperties } as React.CSSProperties
} }
className={cn( className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full", "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
</div> </div>
</TooltipProvider> </TooltipProvider>
</SidebarContext.Provider> </SidebarContext.Provider>
) )
} }
function Sidebar({ function Sidebar({
side = "left", side = "left",
variant = "sidebar", variant = "sidebar",
collapsible = "offcanvas", collapsible = "offcanvas",
className, className,
children, children,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
side?: "left" | "right" side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset" variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none" collapsible?: "offcanvas" | "icon" | "none"
}) { }) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar() const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") { if (collapsible === "none") {
return ( return (
<div <div
data-slot="sidebar" data-slot="sidebar"
className={cn( className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col", "bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
</div> </div>
) )
} }
if (isMobile) { if (isMobile) {
return ( return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent <SheetContent
data-sidebar="sidebar" data-sidebar="sidebar"
data-slot="sidebar" data-slot="sidebar"
data-mobile="true" data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden" className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={ style={
{ {
"--sidebar-width": SIDEBAR_WIDTH_MOBILE, "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties } as React.CSSProperties
} }
side={side} side={side}
> >
<SheetHeader className="sr-only"> <SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle> <SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription> <SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader> </SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div> <div className="flex h-full w-full flex-col">{children}</div>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
) )
} }
return ( return (
<div <div
className="group peer text-sidebar-foreground hidden md:block" className="group peer text-sidebar-foreground hidden md:block"
data-state={state} data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""} data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant} data-variant={variant}
data-side={side} data-side={side}
data-slot="sidebar" data-slot="sidebar"
> >
{/* This is what handles the sidebar gap on desktop */} {/* This is what handles the sidebar gap on desktop */}
<div <div
data-slot="sidebar-gap" data-slot="sidebar-gap"
className={cn( className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear", "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0", "group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180", "group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset" variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]" ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)" : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
)} )}
/> />
<div <div
data-slot="sidebar-container" data-slot="sidebar-container"
className={cn( className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex", "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left" side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[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)]", : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants. // Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset" variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" ? "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", : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className className,
)} )}
{...props} {...props}
> >
<div <div
data-sidebar="sidebar" data-sidebar="sidebar"
data-slot="sidebar-inner" data-slot="sidebar-inner"
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm" className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
> >
{children} {children}
</div> </div>
</div> </div>
</div> </div>
) )
} }
function SidebarTrigger({ function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) {
className, const { toggleSidebar } = useSidebar()
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar()
return ( return (
<Button <Button
data-sidebar="trigger" data-sidebar="trigger"
data-slot="sidebar-trigger" data-slot="sidebar-trigger"
variant="ghost" variant="ghost"
size="icon" size="icon"
className={cn("size-7", className)} className={cn("size-7", className)}
onClick={(event) => { onClick={(event) => {
onClick?.(event) onClick?.(event)
toggleSidebar() toggleSidebar()
}} }}
{...props} {...props}
> >
<PanelLeftIcon /> <PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span> <span className="sr-only">Toggle Sidebar</span>
</Button> </Button>
) )
} }
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar() const { toggleSidebar } = useSidebar()
return ( return (
<button <button
data-sidebar="rail" data-sidebar="rail"
data-slot="sidebar-rail" data-slot="sidebar-rail"
aria-label="Toggle Sidebar" aria-label="Toggle Sidebar"
tabIndex={-1} tabIndex={-1}
onClick={toggleSidebar} onClick={toggleSidebar}
title="Toggle Sidebar" title="Toggle Sidebar"
className={cn( className={cn(
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex", "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full", "hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className className,
)} )}
{...props} {...props}
/> />
) )
} }
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) { function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return ( return (
<main <main
data-slot="sidebar-inset" data-slot="sidebar-inset"
className={cn( className={cn(
"bg-background relative flex w-full flex-1 flex-col", "bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2", "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className className,
)} )}
{...props} {...props}
/> />
) )
} }
function SidebarInput({ function SidebarInput({ className, ...props }: React.ComponentProps<typeof Input>) {
className, return (
...props <Input
}: React.ComponentProps<typeof Input>) { data-slot="sidebar-input"
return ( data-sidebar="input"
<Input className={cn("bg-background h-8 w-full shadow-none", className)}
data-slot="sidebar-input" {...props}
data-sidebar="input" />
className={cn("bg-background h-8 w-full shadow-none", className)} )
{...props}
/>
)
} }
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) { function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="sidebar-header" data-slot="sidebar-header"
data-sidebar="header" data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)} className={cn("flex flex-col gap-2 p-2", className)}
{...props} {...props}
/> />
) )
} }
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) { function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="sidebar-footer" data-slot="sidebar-footer"
data-sidebar="footer" data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)} className={cn("flex flex-col gap-2 p-2", className)}
{...props} {...props}
/> />
) )
} }
function SidebarSeparator({ function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
className, return (
...props <Separator
}: React.ComponentProps<typeof Separator>) { data-slot="sidebar-separator"
return ( data-sidebar="separator"
<Separator className={cn("bg-sidebar-border mx-2 w-auto", className)}
data-slot="sidebar-separator" {...props}
data-sidebar="separator" />
className={cn("bg-sidebar-border mx-2 w-auto", className)} )
{...props}
/>
)
} }
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="sidebar-content" data-slot="sidebar-content"
data-sidebar="content" data-sidebar="content"
className={cn( className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className className,
)} )}
{...props} {...props}
/> />
) )
} }
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="sidebar-group" data-slot="sidebar-group"
data-sidebar="group" data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)} className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props} {...props}
/> />
) )
} }
function SidebarGroupLabel({ function SidebarGroupLabel({
className, className,
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) { }: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div" const Comp = asChild ? Slot : "div"
return ( return (
<Comp <Comp
data-slot="sidebar-group-label" data-slot="sidebar-group-label"
data-sidebar="group-label" data-sidebar="group-label"
className={cn( className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", "text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className className,
)} )}
{...props} {...props}
/> />
) )
} }
function SidebarGroupAction({ function SidebarGroupAction({
className, className,
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) { }: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button"
return ( return (
<Comp <Comp
data-slot="sidebar-group-action" data-slot="sidebar-group-action"
data-sidebar="group-action" data-sidebar="group-action"
className={cn( className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile. // Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden", "after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
className className,
)} )}
{...props} {...props}
/> />
) )
} }
function SidebarGroupContent({ function SidebarGroupContent({ className, ...props }: React.ComponentProps<"div">) {
className, return (
...props <div
}: React.ComponentProps<"div">) { data-slot="sidebar-group-content"
return ( data-sidebar="group-content"
<div className={cn("w-full text-sm", className)}
data-slot="sidebar-group-content" {...props}
data-sidebar="group-content" />
className={cn("w-full text-sm", className)} )
{...props}
/>
)
} }
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) { function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return ( return (
<ul <ul
data-slot="sidebar-menu" data-slot="sidebar-menu"
data-sidebar="menu" data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)} className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props} {...props}
/> />
) )
} }
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return ( return (
<li <li
data-slot="sidebar-menu-item" data-slot="sidebar-menu-item"
data-sidebar="menu-item" data-sidebar="menu-item"
className={cn("group/menu-item relative", className)} className={cn("group/menu-item relative", className)}
{...props} {...props}
/> />
) )
} }
const sidebarMenuButtonVariants = cva( const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] 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 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] 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 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{ {
variants: { variants: {
variant: { variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline: outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
}, },
size: { size: {
default: "h-8 text-sm", default: "h-8 text-sm",
sm: "h-7 text-xs", sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} },
) )
function SidebarMenuButton({ function SidebarMenuButton({
asChild = false, asChild = false,
isActive = false, isActive = false,
variant = "default", variant = "default",
size = "default", size = "default",
tooltip, tooltip,
className, className,
...props ...props
}: React.ComponentProps<"button"> & { }: React.ComponentProps<"button"> & {
asChild?: boolean asChild?: boolean
isActive?: boolean isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent> tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>) { } & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar() const { isMobile, state } = useSidebar()
const button = ( const button = (
<Comp <Comp
data-slot="sidebar-menu-button" data-slot="sidebar-menu-button"
data-sidebar="menu-button" data-sidebar="menu-button"
data-size={size} data-size={size}
data-active={isActive} data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)} className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props} {...props}
/> />
) )
if (!tooltip) { if (!tooltip) {
return button return button
} }
if (typeof tooltip === "string") { if (typeof tooltip === "string") {
tooltip = { tooltip = {
children: tooltip, children: tooltip,
} }
} }
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger> <TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent <TooltipContent
side="right" side="right"
align="center" align="center"
hidden={state !== "collapsed" || isMobile} hidden={state !== "collapsed" || isMobile}
{...tooltip} {...tooltip}
/> />
</Tooltip> </Tooltip>
) )
} }
function SidebarMenuAction({ function SidebarMenuAction({
className, className,
asChild = false, asChild = false,
showOnHover = false, showOnHover = false,
...props ...props
}: React.ComponentProps<"button"> & { }: React.ComponentProps<"button"> & {
asChild?: boolean asChild?: boolean
showOnHover?: boolean showOnHover?: boolean
}) { }) {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button"
return ( return (
<Comp <Comp
data-slot="sidebar-menu-action" data-slot="sidebar-menu-action"
data-sidebar="menu-action" data-sidebar="menu-action"
className={cn( className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile. // Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden", "after:absolute after:-inset-2 md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1", "peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5", "peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5", "peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
showOnHover && showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className className,
)} )}
{...props} {...props}
/> />
) )
} }
function SidebarMenuBadge({ function SidebarMenuBadge({ className, ...props }: React.ComponentProps<"div">) {
className, return (
...props <div
}: React.ComponentProps<"div">) { data-slot="sidebar-menu-badge"
return ( data-sidebar="menu-badge"
<div className={cn(
data-slot="sidebar-menu-badge" "text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
data-sidebar="menu-badge" "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
className={cn( "peer-data-[size=sm]/menu-button:top-1",
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none", "peer-data-[size=default]/menu-button:top-1.5",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground", "peer-data-[size=lg]/menu-button:top-2.5",
"peer-data-[size=sm]/menu-button:top-1", "group-data-[collapsible=icon]:hidden",
"peer-data-[size=default]/menu-button:top-1.5", className,
"peer-data-[size=lg]/menu-button:top-2.5", )}
"group-data-[collapsible=icon]:hidden", {...props}
className />
)} )
{...props}
/>
)
} }
function SidebarMenuSkeleton({ function SidebarMenuSkeleton({
className, className,
showIcon = false, showIcon = false,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
showIcon?: boolean showIcon?: boolean
}) { }) {
// Random width between 50 to 90%. // Random width between 50 to 90%.
const width = React.useMemo(() => { const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%` return `${Math.floor(Math.random() * 40) + 50}%`
}, []) }, [])
return ( return (
<div <div
data-slot="sidebar-menu-skeleton" data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton" data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)} className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props} {...props}
> >
{showIcon && ( {showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
<Skeleton <Skeleton
className="size-4 rounded-md" className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-icon" data-sidebar="menu-skeleton-text"
/> style={
)} {
<Skeleton "--skeleton-width": width,
className="h-4 max-w-(--skeleton-width) flex-1" } as React.CSSProperties
data-sidebar="menu-skeleton-text" }
style={ />
{ </div>
"--skeleton-width": width, )
} as React.CSSProperties
}
/>
</div>
)
} }
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) { function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return ( return (
<ul <ul
data-slot="sidebar-menu-sub" data-slot="sidebar-menu-sub"
data-sidebar="menu-sub" data-sidebar="menu-sub"
className={cn( className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5", "border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
className className,
)} )}
{...props} {...props}
/> />
) )
} }
function SidebarMenuSubItem({ function SidebarMenuSubItem({ className, ...props }: React.ComponentProps<"li">) {
className, return (
...props <li
}: React.ComponentProps<"li">) { data-slot="sidebar-menu-sub-item"
return ( data-sidebar="menu-sub-item"
<li className={cn("group/menu-sub-item relative", className)}
data-slot="sidebar-menu-sub-item" {...props}
data-sidebar="menu-sub-item" />
className={cn("group/menu-sub-item relative", className)} )
{...props}
/>
)
} }
function SidebarMenuSubButton({ function SidebarMenuSubButton({
asChild = false, asChild = false,
size = "md", size = "md",
isActive = false, isActive = false,
className, className,
...props ...props
}: React.ComponentProps<"a"> & { }: React.ComponentProps<"a"> & {
asChild?: boolean asChild?: boolean
size?: "sm" | "md" size?: "sm" | "md"
isActive?: boolean isActive?: boolean
}) { }) {
const Comp = asChild ? Slot : "a" const Comp = asChild ? Slot : "a"
return ( return (
<Comp <Comp
data-slot="sidebar-menu-sub-button" data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button" data-sidebar="menu-sub-button"
data-size={size} data-size={size}
data-active={isActive} data-active={isActive}
className={cn( className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 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", "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 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",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs", size === "sm" && "text-xs",
size === "md" && "text-sm", size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
className className,
)} )}
{...props} {...props}
/> />
) )
} }
export { export {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
SidebarFooter, SidebarFooter,
SidebarGroup, SidebarGroup,
SidebarGroupAction, SidebarGroupAction,
SidebarGroupContent, SidebarGroupContent,
SidebarGroupLabel, SidebarGroupLabel,
SidebarHeader, SidebarHeader,
SidebarInput, SidebarInput,
SidebarInset, SidebarInset,
SidebarMenu, SidebarMenu,
SidebarMenuAction, SidebarMenuAction,
SidebarMenuBadge, SidebarMenuBadge,
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
SidebarMenuSkeleton, SidebarMenuSkeleton,
SidebarMenuSub, SidebarMenuSub,
SidebarMenuSubButton, SidebarMenuSubButton,
SidebarMenuSubItem, SidebarMenuSubItem,
SidebarProvider, SidebarProvider,
SidebarRail, SidebarRail,
SidebarSeparator, SidebarSeparator,
SidebarTrigger, SidebarTrigger,
useSidebar, useSidebar,
} }

View file

@ -0,0 +1,141 @@
import {
FileText,
Waypoints,
SquareLibrary,
Settings,
Tags,
Info,
ChevronRight,
} from "lucide-react"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarHeader,
SidebarFooter,
SidebarMenuSub,
} from "@/components/ui/sidebar"
import { Collapsible, CollapsibleTrigger } from "@radix-ui/react-collapsible"
import { CollapsibleContent } from "../components/ui/collapsible"
import { mockEntries } from "@/mock-data/mock-entries"
import { mockTags } from "@/mock-data/mock-tags"
const footerMenu = [
{
title: "About",
url: "#",
icon: Info,
},
{
title: "Settings",
url: "#",
icon: Settings,
},
]
export function AppSidebar() {
return (
<Sidebar>
<SidebarHeader className="border-b h-12">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild className="data-[slot=sidebar-menu-button]:!p-1.5">
<a href="#">
<SquareLibrary className="h-5 w-5" />
<span className="text-base font-semibold">Eólas</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem key="graph">
<SidebarMenuButton asChild>
<a href="#">
<Waypoints />
<span>Graph</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
<Collapsible className="group/collapsible">
<SidebarMenuItem key="entries">
<CollapsibleTrigger asChild>
<SidebarMenuButton asChild>
<a href="#">
<FileText />
<span>Entries</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{mockEntries.map((item) => (
<SidebarMenuItem>
<a href={item.url}>
<span className="text-xs">{item.title}</span>
</a>
</SidebarMenuItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
<Collapsible className="group/collapsible">
<SidebarMenuItem key="entries">
<CollapsibleTrigger asChild>
<SidebarMenuButton asChild>
<a href="#">
<Tags />
<span>Tags</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{mockTags.map((item) => (
<SidebarMenuItem>
<a href={item.url}>
<span className="text-xs">{item.title}</span>
</a>
</SidebarMenuItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{footerMenu.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarFooter>
</Sidebar>
)
}

View file

@ -1,53 +0,0 @@
import AppHeader from "@/components/app-header"
import { AppSidebar } from "@/components/app-sidebar"
import { Card, CardContent } from "@/components/ui/card"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
export default function Page() {
return (
<SidebarProvider>
<AppSidebar variant="inset" />
<SidebarInset>
<AppHeader pageTitle="Home" />
<div className="flex-1 flex flex-col overflow-auto">
<div className="@container/main flex flex-col">
<div className="p-4 lg:p-6 flex-1 flex">
<Card className="border rounded-sm shadow-none w-auto">
<CardContent className="p-4 overflow-auto">
<p className="leading-7 [&:not(:first-child)]:mt-6 font-normal">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas eget
justo non ipsum venenatis suscipit. In eu viverra nisl. Pellentesque
vel tincidunt arcu. Sed congue consequat dapibus. Suspendisse
imperdiet faucibus velit, in porta massa. Vivamus ornare, justo
pulvinar hendrerit accumsan, nisl massa tempus orci, id iaculis metus
mi pellentesque nibh. Quisque mollis id massa malesuada sagittis. Nunc
massa est, maximus sit amet sollicitudin in, ultrices at tellus.
Curabitur vel odio varius, faucibus quam sed, sollicitudin nunc. Ut
pretium, risus et malesuada mattis, urna neque maximus est, in
pellentesque eros neque nec mi. Aenean aliquet auctor dolor, quis
condimentum urna vehicula quis.
</p>
<p className="leading-7 [&:not(:first-child)]:mt-6">
Vestibulum bibendum dui sit amet quam molestie gravida. Integer turpis
est, ultricies sed imperdiet vitae, ultricies sed purus. Nulla
facilisi. Nullam ultricies imperdiet urna, id convallis massa commodo
in. Pellentesque dapibus malesuada turpis nec sollicitudin. Duis ante
nunc, faucibus et ornare vitae, posuere quis mi. Sed vitae varius
turpis. Sed fringilla, libero id varius volutpat, eros nibh auctor
purus, sit amet tincidunt enim quam ac libero. Cras congue posuere
dolor, eget rutrum lacus accumsan sed. Etiam sit amet lectus neque.
Morbi dignissim elit tortor, ut porta dolor aliquet at. Ut commodo
interdum malesuada. Nulla velit sem, maximus et ultricies et,
vulputate ac tortor. Maecenas lobortis tellus in mi tristique
fringilla. Morbi ullamcorper et diam et condim
</p>
</CardContent>
</Card>
</div>
</div>
</div>
</SidebarInset>
</SidebarProvider>
)
}

View file

@ -3,17 +3,17 @@ import * as React from "react"
const MOBILE_BREAKPOINT = 768 const MOBILE_BREAKPOINT = 768
export function useIsMobile() { export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined) const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => { React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => { const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
} }
mql.addEventListener("change", onChange) mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange) return () => mql.removeEventListener("change", onChange)
}, []) }, [])
return !!isMobile return !!isMobile
} }

View file

@ -1,14 +0,0 @@
import AppHeader from "@/components/app-header"
import { AppSidebar } from "@/components/app-sidebar"
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<SidebarProvider>
<AppSidebar />
<main>{children}</main>
</SidebarProvider>
</>
)
}

25
src/pages/home.tsx Normal file
View file

@ -0,0 +1,25 @@
import { Card, CardContent } from "@/components/ui/card"
import Main from "@/templates/Main"
export default function Home() {
return (
<>
<Main pageTitle="Home">
<div className="flex-1 flex flex-col overflow-auto">
<div className="@container/main flex flex-col">
<div className="p-4 lg:p-6 flex-1 flex">
<Card className="border rounded-sm shadow-none w-full">
<CardContent className="p-4 overflow-auto">
<p className="leading-7 [&:not(:first-child)]:mt-6 font-normal">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas eget
justo non ipsum venenatis suscipit.
</p>
</CardContent>
</Card>
</div>
</div>
</div>
</Main>
</>
)
}

17
src/templates/Main.tsx Normal file
View file

@ -0,0 +1,17 @@
import AppHeader from "@/components/AppHeader"
import { AppSidebar } from "@/containers/AppSidebar"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
export default function Main({ children, pageTitle }) {
return (
<>
<SidebarProvider variant="inset">
<AppSidebar />
<SidebarInset>
<AppHeader pageTitle={pageTitle} />
<main>{children}</main>
</SidebarInset>
</SidebarProvider>
</>
)
}