fix(ui): render the sidebar scrollbar with shadcn ScrollArea (#33124)

* fix(ui): render the sidebar scrollbar with shadcn ScrollArea

The sidebar navigation scrolled through a native overflow-y-auto container, so the browser drew its default scrollbar. It now scrolls through the shadcn ScrollArea primitive so the thumb matches the rest of the dashboard

Switching to ScrollArea surfaced a latent styling gap. The Base UI scroll-area, tabs, and separator primitives rely on data-horizontal and data-vertical Tailwind variants that resolve to [data-orientation="horizontal"] and [data-orientation="vertical"], and those variants ship in shadcn's shared stylesheet. The project never imported it, so the classes matched nothing and the scrollbar collapsed to zero width. This adds shadcn as a devDependency and imports shadcn/tailwind.css, which also repairs the vertical tabs and separator styling. See shadcn-ui/ui#9196 for the upstream tracking issue

* refactor(ui): inline the Base UI data-* variants, drop the shadcn dep

The earlier fix imported shadcn/tailwind.css through the shadcn devDependency, which pulled 219 packages and tied the CSS build to shadcn's package exports (an open Turbopack-breaking bug, shadcn-ui/ui#10931). shadcn's model is that we own the components, so the custom variants those components depend on belong in our own stylesheet rather than a runtime dependency. This inlines the nine data-* custom variants and the no-scrollbar utility that the Base UI primitives reference into globals.css, and removes the shadcn package.
This commit is contained in:
yuneng-jiang 2026-07-13 15:35:08 -07:00 • committed by GitHub
parent 39e0efa11d
commit 07d2a03dbf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 31 additions and 10 deletions

View file

@ -11,6 +11,25 @@
@custom-variant dark (&:where(.dark, .dark *));
/* shadcn Base UI primitives reference these variants; upstream omits them (shadcn-ui/ui#9196) */
@custom-variant data-open (&:where([data-state="open"], [data-open]:not([data-open="false"])));
@custom-variant data-closed (&:where([data-state="closed"], [data-closed]:not([data-closed="false"])));
@custom-variant data-checked (&:where([data-state="checked"], [data-checked]:not([data-checked="false"])));
@custom-variant data-unchecked (&:where([data-state="unchecked"], [data-unchecked]:not([data-unchecked="false"])));
@custom-variant data-selected (&:where([data-selected="true"]));
@custom-variant data-disabled (&:where([data-disabled="true"], [data-disabled]:not([data-disabled="false"])));
@custom-variant data-active (&:where([data-state="active"], [data-active]:not([data-active="false"])));
@custom-variant data-horizontal (&:where([data-orientation="horizontal"]));
@custom-variant data-vertical (&:where([data-orientation="vertical"]));
@utility no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
:root {
--radius: 0.5rem;
--background: oklch(1 0 0);

View file

@ -7,9 +7,9 @@ import { getProxyBaseUrl } from "@/components/networking";
import { useTheme } from "@/contexts/ThemeContext";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
@ -608,15 +608,17 @@ const Sidebar_: React.FC<SidebarProps> = ({
</div>
</SidebarHeader>
<SidebarContent>
{visibleGroups.map((group, gi) => (
<SidebarGroup key={group.groupLabel}>
{gi > 0 && <SidebarSeparator className="hidden group-data-[collapsed=true]/sidebar:block" />}
<SidebarGroupLabel>{group.groupLabel}</SidebarGroupLabel>
<SidebarMenu>{group.items.map((item) => renderItem(item))}</SidebarMenu>
</SidebarGroup>
))}
</SidebarContent>
<ScrollArea className="min-h-0 flex-1">
<nav className="flex flex-col gap-0.5 px-3 pb-3">
{visibleGroups.map((group, gi) => (
<SidebarGroup key={group.groupLabel}>
{gi > 0 && <SidebarSeparator className="hidden group-data-[collapsed=true]/sidebar:block" />}
<SidebarGroupLabel>{group.groupLabel}</SidebarGroupLabel>
<SidebarMenu>{group.items.map((item) => renderItem(item))}</SidebarMenu>
</SidebarGroup>
))}
</nav>
</ScrollArea>
<SidebarFooter>
{isAdminRole(userRole) && (