fix(ui): make app shell server compatible
Some checks failed
CI — Common Platform / Build, Test & Typecheck (push) Failing after 0s
CI — Common Platform / Publish @bytelyst/* to Gitea npm registry (push) Has been skipped
CI — Common Platform / Check design token drift (push) Failing after 0s

This commit is contained in:
Saravana Achu Mac 2026-05-06 18:09:54 -07:00
parent efb0162be0
commit 3398574155

View File

@ -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>