fix(ui): harden app shell responsive behavior

This commit is contained in:
Saravana Achu Mac 2026-05-06 13:44:44 -07:00
parent 5e7b349a7c
commit 50617c1813

View File

@ -1,3 +1,5 @@
'use client';
import * as React from 'react';
import { clsx } from 'clsx';
import { Menu, X } from 'lucide-react';
@ -6,6 +8,42 @@ type ShellStyle = React.CSSProperties & {
'--bl-app-sidebar-width'?: string;
};
type ShellMediaQueryList = {
matches: boolean;
addEventListener?: (type: 'change', listener: () => void) => void;
removeEventListener?: (type: 'change', listener: () => void) => void;
addListener?: (listener: () => void) => void;
removeListener?: (listener: () => void) => void;
};
function useDesktopShell() {
const [isDesktop, setIsDesktop] = React.useState(false);
React.useEffect(() => {
const browser = globalThis as typeof globalThis & {
matchMedia?: (query: string) => ShellMediaQueryList;
};
const media = browser.matchMedia?.('(min-width: 1024px)');
if (!media) {
return undefined;
}
const sync = () => setIsDesktop(media.matches);
sync();
if (media.addEventListener && media.removeEventListener) {
media.addEventListener('change', sync);
return () => media.removeEventListener?.('change', sync);
}
media.addListener?.(sync);
return () => media.removeListener?.(sync);
}, []);
return isDesktop;
}
export interface AppShellProps extends React.HTMLAttributes<HTMLDivElement> {
sidebarWidth?: number;
}
@ -78,6 +116,12 @@ export function AppShellMobileToggle({
children,
...props
}: AppShellMobileToggleProps) {
const isDesktop = useDesktopShell();
if (isDesktop) {
return null;
}
return (
<button
type="button"
@ -103,14 +147,18 @@ export interface AppShellOverlayProps extends React.HTMLAttributes<HTMLDivElemen
}
export function AppShellOverlay({ open, className, ...props }: AppShellOverlayProps) {
const isDesktop = useDesktopShell();
if (!open || isDesktop) {
return null;
}
return (
<div
aria-hidden="true"
data-open={open ? 'true' : 'false'}
className={clsx(
'fixed inset-0 z-[39] bg-[var(--bl-overlay-scrim,rgba(0,0,0,0.5))]',
open ? 'block' : 'hidden',
'lg:hidden',
className
)}
{...props}
@ -133,6 +181,9 @@ export function AppShellSidebar({
children,
...props
}: AppShellSidebarProps) {
const isDesktop = useDesktopShell();
const isVisible = open || isDesktop;
return (
<aside
aria-label={label}
@ -141,10 +192,15 @@ export function AppShellSidebar({
'fixed left-0 top-0 z-40 flex h-screen flex-col overflow-y-auto',
'border-r border-[var(--bl-border)] bg-[var(--bl-surface-sidebar,var(--bl-bg-elevated))]',
'transition-transform duration-200 ease-out lg:translate-x-0',
open ? 'translate-x-0' : '-translate-x-full',
className
)}
style={{ width, minWidth: width, ...style }}
style={{
width,
minWidth: width,
transform: isVisible ? 'translateX(0)' : 'translateX(-100%)',
transition: 'transform 200ms ease-out',
...style,
}}
{...props}
>
{children}
@ -160,18 +216,23 @@ export function AppShellMain({
id = 'main-content',
labelledBy,
className,
style,
children,
...props
}: AppShellMainProps) {
const isDesktop = useDesktopShell();
return (
<main
id={id}
tabIndex={-1}
aria-labelledby={labelledBy}
className={clsx(
'min-h-screen min-w-0 p-5 pt-16 lg:pl-[var(--bl-app-sidebar-width)] lg:pt-8',
className
)}
className={clsx('min-h-screen min-w-0', className)}
style={{
padding: isDesktop ? '2rem' : '4rem 1.25rem 1.25rem',
paddingLeft: isDesktop ? 'calc(var(--bl-app-sidebar-width, 280px) + 2rem)' : '1.25rem',
...style,
}}
{...props}
>
<div className="grid gap-6">{children}</div>