Skip to content
This repository was archived by the owner on Jul 31, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 91 additions & 20 deletions src/components/AppLayout/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,6 @@ const meta: Meta<typeof AppLayout> = {
parameters: {
layout: 'fullscreen',
},
decorators: [
(Story) => (
<div className="h-svh w-full">
<Story />
</div>
),
],
}

export default meta
Expand Down Expand Up @@ -152,7 +145,8 @@ export const WithNavItemGroups: Story = {
<AppLayout.HeaderDivider />
<AppLayout.Breadcrumb>
<AppLayout.BreadcrumbItem>Home</AppLayout.BreadcrumbItem>
<AppLayout.BreadcrumbItem active>Settings</AppLayout.BreadcrumbItem>
<AppLayout.BreadcrumbItem>Settings</AppLayout.BreadcrumbItem>
<AppLayout.BreadcrumbItem active>Users</AppLayout.BreadcrumbItem>
</AppLayout.Breadcrumb>
</AppLayout.SurfaceHeader>,
<AppLayout.Surface className="p-4" key="surface">
Expand Down Expand Up @@ -211,15 +205,25 @@ export const CustomSurfaceHeader: Story = {
),
}

const HomePage = () => {
const HomePage = ({
handlePageChange,
}: {
handlePageChange: (page: AllPages) => void
}) => {
return (
<div>
<div className="flex flex-col gap-2">
<Heading>Home</Heading>

<Text>
This is the home page. It's the first page you see when you open the
app.
</Text>

<div>
<Button onClick={() => handlePageChange('settings')}>
Go to settings
</Button>
</div>
</div>
)
}
Expand All @@ -237,12 +241,15 @@ const SettingsPage = () => {
)
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
const allPages = ['home', 'settings'] as const
type AllPages = (typeof allPages)[number]
const SurfaceTransition = () => {
const [page, setPage] = useState<'home' | 'settings'>('settings')
const [page, setPage] = useState<AllPages>('settings')
const [isTransitioning, setIsTransitioning] = useState(false)
const [direction, setDirection] = useState<'forward' | 'backward'>('forward')

const handlePageChange = (newPage: 'home' | 'settings') => {
const handlePageChange = (newPage: AllPages) => {
if (newPage === page || isTransitioning) return // Don't transition if already on that page or transitioning

setIsTransitioning(true)
Expand Down Expand Up @@ -285,7 +292,7 @@ const SurfaceTransition = () => {
}

return (
<div className="h-svh w-full">
<div>
<style>{`
::view-transition-group(page-transition) {
overflow: hidden;
Expand Down Expand Up @@ -329,6 +336,8 @@ const SurfaceTransition = () => {
<AppLayout.Sidebar>
<AppLayout.Nav>
<AppLayout.NavItem title="Home" icon="house" />
<AppLayout.NavItem title="Settings" icon="settings" />
<AppLayout.NavItem title="Users" icon="users" />
</AppLayout.Nav>
</AppLayout.Sidebar>
<AppLayout.SurfaceHeader>
Expand All @@ -340,20 +349,26 @@ const SurfaceTransition = () => {
>
Home
</AppLayout.BreadcrumbItem>
<AppLayout.BreadcrumbItem
active={page === 'settings'}
onClick={() => handlePageChange('settings')}
>
Settings
</AppLayout.BreadcrumbItem>
{page === 'settings' && (
<AppLayout.BreadcrumbItem
active={page === 'settings'}
onClick={() => handlePageChange('settings')}
>
Settings
</AppLayout.BreadcrumbItem>
)}
</AppLayout.Breadcrumb>
</AppLayout.SurfaceHeader>
<AppLayout.Surface
style={{ viewTransitionName: 'page-transition' }}
data-direction={direction}
className="p-4"
>
{page === 'home' ? <HomePage /> : <SettingsPage />}
{page === 'home' ? (
<HomePage handlePageChange={handlePageChange} />
) : (
<SettingsPage />
)}
</AppLayout.Surface>
</AppLayout>
</AppLayoutProvider>
Expand All @@ -364,3 +379,59 @@ export const SurfaceTransitionStory: Story = {
name: 'Surface Transition',
render: () => <SurfaceTransition />,
}

export const WithFullScreenSurface: Story = {
name: 'With Full Screen Surface',
args: {
children: [
<AppLayout.Sidebar key="sidebar">
<AppLayout.Nav>
<AppLayout.NavItem
onClick={() => alert('Home')}
title="Home"
icon="house"
/>
<AppLayout.NavItem
onClick={() => alert('Settings')}
title="Settings"
icon="settings"
/>
<AppLayout.NavItem
onClick={() => alert('Users')}
title="Users"
icon="users"
/>
</AppLayout.Nav>
</AppLayout.Sidebar>,
<AppLayout.SurfaceHeader key="surface-header">
<AppLayout.CollapseButton />
<AppLayout.HeaderDivider />
<AppLayout.Breadcrumb>
<AppLayout.BreadcrumbItem onClick={() => alert('Home')}>
Home
</AppLayout.BreadcrumbItem>
<AppLayout.BreadcrumbItem active>Settings</AppLayout.BreadcrumbItem>
</AppLayout.Breadcrumb>
</AppLayout.SurfaceHeader>,
<AppLayout.Surface className="p-0" key="surface">
<div className="bg-surface-secondary flex h-full w-full flex-col">
<div className="flex items-center justify-center py-4">
<Text>This is a full screen surface</Text>
</div>

<div className="border-neutral-default mt-auto flex items-center justify-center border-t py-4">
<Text>
This content is pushed to the bottom but doesn't overflow the
viewport
</Text>
</div>
</div>
</AppLayout.Surface>,
],
},
render: (args) => (
<AppLayoutProvider>
<AppLayout {...args} />
</AppLayoutProvider>
),
}
40 changes: 25 additions & 15 deletions src/components/AppLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import React, {
} from 'react'
import { Icon } from '../Icon'
import { useAppLayout } from '@/hooks/useAppLayout'
import { motion } from 'framer-motion'
import { motion, MotionProps } from 'framer-motion'
import { Logo } from '../Logo'
import {
Tooltip,
Expand All @@ -26,7 +26,7 @@ interface AppLayoutProps extends PropsWithChildren {
className?: string
}

const AppLayoutBase = ({ children }: AppLayoutProps) => {
const AppLayoutBase = ({ children, className }: AppLayoutProps) => {
const sidebar = Children.toArray(children).find((child) => {
if (!isValidElement(child)) return false
const type = child.type as { displayName?: string }
Expand Down Expand Up @@ -56,13 +56,16 @@ const AppLayoutBase = ({ children }: AppLayoutProps) => {

return (
<div
className={cn('bg-surface-secondary flex min-h-svh w-full p-3 pr-0 pb-0')}
className={cn(
'bg-surface-secondary flex h-screen w-full overflow-hidden p-3 pr-0 pb-0',
className
)}
>
{sidebar}

<motion.div
layout
className="flex w-full flex-1 flex-col"
className="flex w-full flex-col"
initial={{ left: collapsed ? '100%' : '0' }}
animate={{ left: collapsed ? '100%' : '0' }}
transition={{ duration: 0.25, type: 'spring', bounce: 0 }}
Expand All @@ -71,14 +74,14 @@ const AppLayoutBase = ({ children }: AppLayoutProps) => {

<div
className={cn(
'bg-surface-primary flex-1 overflow-hidden rounded-tl-xl border border-r-0 shadow will-change-transform'
'bg-surface-primary flex h-full flex-col overflow-hidden rounded-tl-xl border border-r-0 shadow will-change-transform'
)}
>
<div className="flex w-full flex-1 items-center border-b p-2">
<div className="flex w-full flex-shrink-0 items-center border-b p-2">
{surfaceHeader}
</div>

{surface}
<div className="min-h-0 flex-1">{surface}</div>
</div>
</motion.div>
</div>
Expand All @@ -97,7 +100,7 @@ const AppLayoutSurface = ({
...props
}: AppLayoutSurfaceProps) => {
return (
<div className={cn('flex-1', className)} {...props}>
<div className={cn('h-full overflow-auto', className)} {...props}>
{children}
</div>
)
Expand Down Expand Up @@ -163,7 +166,7 @@ const AppLayoutBreadcrumb = ({
return (
<div
className={cn(
'bg-surface-primary flex flex-1 items-center gap-2',
'bg-surface-primary flex min-h-8 items-center gap-1.5',
className
)}
>
Expand All @@ -180,12 +183,17 @@ const AppLayoutBreadcrumb = ({
AppLayoutBreadcrumb.displayName = 'AppLayout.Breadcrumb'

const AppLayoutBreadcrumbDivider = () => {
return <span className="text-muted-foreground typography-body-lg">/</span>
return (
<span className="text-muted-foreground typography-body-lg select-none">
/
</span>
)
}

interface AppLayoutBreadcrumbItemProps extends PropsWithChildren {
interface AppLayoutBreadcrumbItemProps extends MotionProps, PropsWithChildren {
className?: string
active?: boolean
children?: React.ReactNode
onClick?: () => void
}

Expand All @@ -194,19 +202,21 @@ const AppLayoutBreadcrumbItem = ({
className,
active = false,
onClick,
...props
}: AppLayoutBreadcrumbItemProps) => {
return (
<div
<motion.div
className={cn(
'typography-body-md text-muted-foreground cursor-pointer',
'typography-body-md text-muted-foreground cursor-pointer rounded-md px-1.5 select-none',
active && 'text-foreground cursor-default',
!active && 'hover:text-foreground',
!active && 'hover:text-foreground hover:bg-accent',
className
)}
onClick={onClick}
{...props}
>
{children}
</div>
</motion.div>
)
}

Expand Down