fix(ui): make app shell server compatible
This commit is contained in:
parent
efb0162be0
commit
3398574155
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import { Menu, X } from 'lucide-react';
|
import { Menu, X } from 'lucide-react';
|
||||||
@ -8,42 +6,6 @@ type ShellStyle = React.CSSProperties & {
|
|||||||
'--bl-app-sidebar-width'?: string;
|
'--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> {
|
export interface AppShellProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
sidebarWidth?: number;
|
sidebarWidth?: number;
|
||||||
}
|
}
|
||||||
@ -63,12 +25,55 @@ export function AppShell({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'min-h-screen bg-[var(--bl-bg-canvas)] text-[var(--bl-text-primary)]',
|
'bl-app-shell min-h-screen bg-[var(--bl-bg-canvas)] text-[var(--bl-text-primary)]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={shellStyle}
|
style={shellStyle}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
.bl-app-shell-mobile-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bl-app-shell-overlay {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bl-app-shell-overlay[data-open="true"] {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bl-app-shell-sidebar {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 200ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bl-app-shell-sidebar[data-open="true"] {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bl-app-shell-main {
|
||||||
|
padding: 4rem 1.25rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.bl-app-shell-mobile-toggle,
|
||||||
|
.bl-app-shell-overlay {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bl-app-shell-sidebar {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bl-app-shell-main {
|
||||||
|
padding: 2rem 2rem 2rem calc(var(--bl-app-sidebar-width, 280px) + 2rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -116,18 +121,12 @@ export function AppShellMobileToggle({
|
|||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: AppShellMobileToggleProps) {
|
}: AppShellMobileToggleProps) {
|
||||||
const isDesktop = useDesktopShell();
|
|
||||||
|
|
||||||
if (isDesktop) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={open ? closeLabel : openLabel}
|
aria-label={open ? closeLabel : openLabel}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'fixed left-3 top-3 z-[38] inline-flex h-10 w-10 items-center justify-center',
|
'bl-app-shell-mobile-toggle fixed left-3 top-3 z-[38] h-10 w-10 items-center justify-center',
|
||||||
'rounded-md border border-[var(--bl-border)] bg-[var(--bl-bg-elevated)]',
|
'rounded-md border border-[var(--bl-border)] bg-[var(--bl-bg-elevated)]',
|
||||||
'text-[var(--bl-text-primary)] shadow-sm transition-colors',
|
'text-[var(--bl-text-primary)] shadow-sm transition-colors',
|
||||||
'hover:bg-[var(--bl-surface-muted)] focus-visible:outline-none',
|
'hover:bg-[var(--bl-surface-muted)] focus-visible:outline-none',
|
||||||
@ -147,18 +146,12 @@ export interface AppShellOverlayProps extends React.HTMLAttributes<HTMLDivElemen
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AppShellOverlay({ open, className, ...props }: AppShellOverlayProps) {
|
export function AppShellOverlay({ open, className, ...props }: AppShellOverlayProps) {
|
||||||
const isDesktop = useDesktopShell();
|
|
||||||
|
|
||||||
if (!open || isDesktop) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
data-open={open ? 'true' : 'false'}
|
data-open={open ? 'true' : 'false'}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'fixed inset-0 z-[39] bg-[var(--bl-overlay-scrim,rgba(0,0,0,0.5))]',
|
'bl-app-shell-overlay fixed inset-0 z-[39] bg-[var(--bl-overlay-scrim,rgba(0,0,0,0.5))]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@ -181,15 +174,12 @@ export function AppShellSidebar({
|
|||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: AppShellSidebarProps) {
|
}: AppShellSidebarProps) {
|
||||||
const isDesktop = useDesktopShell();
|
|
||||||
const isVisible = open || isDesktop;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
data-open={open ? 'true' : 'false'}
|
data-open={open ? 'true' : 'false'}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'fixed left-0 top-0 z-40 flex h-screen flex-col overflow-y-auto',
|
'bl-app-shell-sidebar 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))]',
|
'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',
|
'transition-transform duration-200 ease-out lg:translate-x-0',
|
||||||
className
|
className
|
||||||
@ -197,8 +187,6 @@ export function AppShellSidebar({
|
|||||||
style={{
|
style={{
|
||||||
width,
|
width,
|
||||||
minWidth: width,
|
minWidth: width,
|
||||||
transform: isVisible ? 'translateX(0)' : 'translateX(-100%)',
|
|
||||||
transition: 'transform 200ms ease-out',
|
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
@ -220,19 +208,13 @@ export function AppShellMain({
|
|||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: AppShellMainProps) {
|
}: AppShellMainProps) {
|
||||||
const isDesktop = useDesktopShell();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
id={id}
|
id={id}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
aria-labelledby={labelledBy}
|
aria-labelledby={labelledBy}
|
||||||
className={clsx('min-h-screen min-w-0', className)}
|
className={clsx('bl-app-shell-main min-h-screen min-w-0', className)}
|
||||||
style={{
|
style={style}
|
||||||
padding: isDesktop ? '2rem' : '4rem 1.25rem 1.25rem',
|
|
||||||
paddingLeft: isDesktop ? 'calc(var(--bl-app-sidebar-width, 280px) + 2rem)' : '1.25rem',
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="grid gap-6">{children}</div>
|
<div className="grid gap-6">{children}</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user