From 088b6cefb659e7626587dc3e0250165eed24c420 Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Sun, 16 Aug 2026 23:27:51 -0400 Subject: [PATCH] fix(ui): correct sidebar inset layout and Tailwind v4 custom-property widths The shadcn component update pulled sidebar.tsx using Tailwind v3-style bare-bracket custom property syntax (w-[--sidebar-width]). Under Tailwind v4 this compiles to invalid CSS (width: --sidebar-width) which the browser ignores, so width fell back to auto and the fixed sidebar blew out to its content's intrinsic width (the 1372px logo header ~ half the page). The empty spacer div was equally broken, reserving zero space and leaving the inset m-2 margin as a stray left gap. Three fixes: - sidebar.tsx: wrap all 7 bare-bracket custom properties in var() so they compile to width: var(--sidebar-width) / var(--sidebar-width-icon) / max-width: var(--skeleton-width). The calc(var(...)) rules were already valid; only the bare forms were broken. - layout.tsx: replace the raw
with . AppSidebar uses variant="inset", whose peer styling (md:peer-data-[variant=inset]:m-2 / :rounded-xl / :shadow) only applies to a SidebarInset sibling, so the inset gap/rounding/shadow were silently dropped. - SiteHeader.tsx: replace the raw
+ border-gray-800 + p-4 with the standard shadcn
using semantic border-b, plus -ml-1 on SidebarTrigger and an ml-auto wrapper for NotificationsBell (which takes no className). Verified via the running dev server: SSR HTML carries data-variant="inset", a single
with the peer-data-[variant=inset] classes,
, and zero border-gray-800 occurrences. tsc --noEmit clean for the changed files. --- src/app/(frontend)/layout.tsx | 8 +- src/components/frontend/SiteHeader.tsx | 12 +- src/components/ui/sidebar.tsx | 649 ++++++++++++------------- 3 files changed, 321 insertions(+), 348 deletions(-) diff --git a/src/app/(frontend)/layout.tsx b/src/app/(frontend)/layout.tsx index 67c23d0..0f841a5 100644 --- a/src/app/(frontend)/layout.tsx +++ b/src/app/(frontend)/layout.tsx @@ -1,13 +1,13 @@ import React from "react"; import "./styles.css"; -import { SidebarProvider } from "@/components/ui/sidebar"; +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import { AppSidebar } from "@/components/frontend/blocks/AppSidebar"; import { getPayload } from "payload"; import config from "@payload-config"; import { headers as nextHeaders } from "next/headers"; import { SiteHeader } from "@/components/frontend/SiteHeader"; import { Metadata } from "next"; -import { resolveLevel, type ResolvedLevel } from "@/utils/xp/resolveLevel"; +import { type ResolvedLevel, resolveLevel } from "@/utils/xp/resolveLevel"; import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification"; import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification"; import { ShipmentToasts } from "@/components/frontend/logistics/ShipmentToasts"; @@ -98,10 +98,10 @@ export default async function RootLayout(props: { children: React.ReactNode }) { showCallsign={user.preferences?.display?.showCallsign ?? false} className="border-r" /> -
+ {children} -
+ ) : ( diff --git a/src/components/frontend/SiteHeader.tsx b/src/components/frontend/SiteHeader.tsx index 2bc5567..5a9555f 100644 --- a/src/components/frontend/SiteHeader.tsx +++ b/src/components/frontend/SiteHeader.tsx @@ -4,12 +4,12 @@ import { NotificationsBell } from "@/components/frontend/notifications/Notificat export const SiteHeader = () => { return ( -
-
- - +
+ + +
+
- -
+
); }; diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index 2031898..0c4f239 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -1,64 +1,59 @@ -"use client" +"use client"; -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" -import { PanelLeft } from "lucide-react" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import { PanelLeft } from "lucide-react"; -import { useIsMobile } from "@/components/hooks/use-mobile" -import { cn } from "@/components/lib/utils" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Separator } from "@/components/ui/separator" +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, SheetDescription, SheetHeader, SheetTitle, -} from "@/components/ui/sheet" -import { Skeleton } from "@/components/ui/skeleton" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" +} from "@/components/ui/sheet"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } 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_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"; type SidebarContextProps = { - state: "expanded" | "collapsed" - open: boolean - setOpen: (open: boolean) => void - openMobile: boolean - setOpenMobile: (open: boolean) => void - isMobile: boolean - toggleSidebar: () => void -} + state: "expanded" | "collapsed"; + open: boolean; + setOpen: (open: boolean) => void; + openMobile: boolean; + setOpenMobile: (open: boolean) => void; + isMobile: boolean; + toggleSidebar: () => void; +}; -const SidebarContext = React.createContext(null) +const SidebarContext = React.createContext(null); function useSidebar() { - const context = React.useContext(SidebarContext) + 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< HTMLDivElement, React.ComponentProps<"div"> & { - defaultOpen?: boolean - open?: boolean - onOpenChange?: (open: boolean) => void + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; } >( ( @@ -71,56 +66,51 @@ const SidebarProvider = React.forwardRef< children, ...props }, - ref + ref, ) => { - const isMobile = useIsMobile() - const [openMobile, setOpenMobile] = React.useState(false) + const isMobile = useIsMobile(); + const [openMobile, setOpenMobile] = React.useState(false); // 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 [_open, _setOpen] = React.useState(defaultOpen); + const open = openProp ?? _open; const setOpen = React.useCallback( (value: boolean | ((value: boolean) => boolean)) => { - const openState = typeof value === "function" ? value(open) : value + const openState = typeof value === "function" ? value(open) : value; if (setOpenProp) { - setOpenProp(openState) + setOpenProp(openState); } else { - _setOpen(openState) + _setOpen(openState); } // 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. const toggleSidebar = React.useCallback(() => { - return isMobile - ? setOpenMobile((open) => !open) - : setOpen((open) => !open) - }, [isMobile, setOpen, setOpenMobile]) + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); + }, [isMobile, setOpen, setOpenMobile]); // Adds a keyboard shortcut to toggle the sidebar. React.useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault() - toggleSidebar() + if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + toggleSidebar(); } - } + }; - window.addEventListener("keydown", handleKeyDown) - return () => window.removeEventListener("keydown", handleKeyDown) - }, [toggleSidebar]) + 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" + const state = open ? "expanded" : "collapsed"; const contextValue = React.useMemo( () => ({ @@ -132,8 +122,8 @@ const SidebarProvider = React.forwardRef< setOpenMobile, toggleSidebar, }), - [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] - ) + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar], + ); return ( @@ -148,7 +138,7 @@ const SidebarProvider = React.forwardRef< } className={cn( "group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar", - className + className, )} ref={ref} {...props} @@ -157,17 +147,17 @@ const SidebarProvider = React.forwardRef<
- ) - } -) -SidebarProvider.displayName = "SidebarProvider" + ); + }, +); +SidebarProvider.displayName = "SidebarProvider"; const Sidebar = React.forwardRef< HTMLDivElement, React.ComponentProps<"div"> & { - side?: "left" | "right" - variant?: "sidebar" | "floating" | "inset" - collapsible?: "offcanvas" | "icon" | "none" + side?: "left" | "right"; + variant?: "sidebar" | "floating" | "inset"; + collapsible?: "offcanvas" | "icon" | "none"; } >( ( @@ -179,23 +169,23 @@ const Sidebar = React.forwardRef< children, ...props }, - ref + ref, ) => { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); if (collapsible === "none") { return (
{children}
- ) + ); } if (isMobile) { @@ -204,7 +194,7 @@ const Sidebar = React.forwardRef< {children} - ) + ); } return ( @@ -234,25 +224,25 @@ const Sidebar = React.forwardRef< {/* This is what handles the sidebar gap on desktop */}
- ) - } -) -Sidebar.displayName = "Sidebar" + ); + }, +); +Sidebar.displayName = "Sidebar"; const SidebarTrigger = React.forwardRef< React.ElementRef, React.ComponentProps >(({ className, onClick, ...props }, ref) => { - const { toggleSidebar } = useSidebar() + const { toggleSidebar } = useSidebar(); return ( - ) -}) -SidebarTrigger.displayName = "SidebarTrigger" + ); +}); +SidebarTrigger.displayName = "SidebarTrigger"; -const SidebarRail = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> ->(({ className, ...props }, ref) => { - const { toggleSidebar } = useSidebar() +const SidebarRail = React.forwardRef>( + ({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); - return ( -