Skip to content

Commit b9d6672

Browse files
fix(sandbox): restore mobile scroll behavior on details page (#347)
1 parent 5b5c042 commit b9d6672

2 files changed

Lines changed: 114 additions & 45 deletions

File tree

src/features/dashboard/sandbox/layout.tsx

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ export default function SandboxLayout({
2525
}: SandboxLayoutProps) {
2626
const { teamSlug, sandboxId } =
2727
useRouteParams<'/dashboard/[teamSlug]/sandboxes/[sandboxId]'>()
28-
2928
const { sandboxInfo, isSandboxInfoLoading, isSandboxNotFound } =
3029
useSandboxContext()
3130

@@ -40,41 +39,47 @@ export default function SandboxLayout({
4039
}
4140

4241
return (
43-
<div className="flex h-full min-h-0 flex-1 flex-col max-md:overflow-y-auto">
44-
{header}
42+
<div className="flex h-full min-h-0 flex-1 flex-col">
43+
<div className="flex h-full w-full min-h-0 flex-col max-md:overflow-y-auto md:flex-1">
44+
<div className="max-md:shrink-0">{header}</div>
45+
46+
<div className="flex flex-col max-md:h-full max-md:shrink-0 md:min-h-0 md:flex-1">
47+
<DashboardTabsList
48+
layoutKey="tabs-indicator-sandbox"
49+
className="bg-bg z-20 max-md:sticky max-md:top-0"
50+
mobileVariant="select"
51+
headerAccessory={tabsHeaderAccessory}
52+
tabs={[
53+
{
54+
id: 'monitoring',
55+
label: 'Monitoring',
56+
href: PROTECTED_URLS.SANDBOX_MONITORING(teamSlug, sandboxId),
57+
icon: <TrendIcon className="size-4" />,
58+
},
59+
{
60+
id: 'events',
61+
label: 'Events',
62+
href: PROTECTED_URLS.SANDBOX_EVENTS(teamSlug, sandboxId),
63+
icon: <HistoryIcon className="size-4" />,
64+
},
65+
{
66+
id: 'logs',
67+
label: 'Logs',
68+
href: PROTECTED_URLS.SANDBOX_LOGS(teamSlug, sandboxId),
69+
icon: <ListIcon className="size-4" />,
70+
},
71+
{
72+
id: 'filesystem',
73+
label: 'Filesystem',
74+
href: PROTECTED_URLS.SANDBOX_FILESYSTEM(teamSlug, sandboxId),
75+
icon: <StorageIcon className="size-4" />,
76+
},
77+
]}
78+
/>
4579

46-
<DashboardTabsList
47-
layoutKey="tabs-indicator-sandbox"
48-
className="max-md:sticky max-md:top-0 max-md:z-20"
49-
headerAccessory={tabsHeaderAccessory}
50-
tabs={[
51-
{
52-
id: 'monitoring',
53-
label: 'Monitoring',
54-
href: PROTECTED_URLS.SANDBOX_MONITORING(teamSlug, sandboxId),
55-
icon: <TrendIcon className="size-4" />,
56-
},
57-
{
58-
id: 'events',
59-
label: 'Events',
60-
href: PROTECTED_URLS.SANDBOX_EVENTS(teamSlug, sandboxId),
61-
icon: <HistoryIcon className="size-4" />,
62-
},
63-
{
64-
id: 'logs',
65-
label: 'Logs',
66-
href: PROTECTED_URLS.SANDBOX_LOGS(teamSlug, sandboxId),
67-
icon: <ListIcon className="size-4" />,
68-
},
69-
{
70-
id: 'filesystem',
71-
label: 'Filesystem',
72-
href: PROTECTED_URLS.SANDBOX_FILESYSTEM(teamSlug, sandboxId),
73-
icon: <StorageIcon className="size-4" />,
74-
},
75-
]}
76-
/>
77-
{children}
80+
<div className="flex min-h-0 flex-1 flex-col">{children}</div>
81+
</div>
82+
</div>
7883
</div>
7984
)
8085
}

src/ui/dashboard-tabs.tsx

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,58 @@
11
'use client'
22

3-
import { usePathname } from 'next/navigation'
3+
import { usePathname, useRouter } from 'next/navigation'
44
import { memo, type ReactNode } from 'react'
55
import { cn } from '@/lib/utils'
66
import { HoverPrefetchLink } from '@/ui/hover-prefetch-link'
7+
import {
8+
Select,
9+
SelectContent,
10+
SelectItem,
11+
SelectTrigger,
12+
} from '@/ui/primitives/select'
713
import { Tabs, TabsList, TabsTrigger } from '@/ui/primitives/tabs'
814

9-
export interface DashboardTabsListProps {
10-
layoutKey: string
11-
tabs: DashboardTabItem[]
12-
className?: string
13-
headerAccessory?: ReactNode
14-
}
15-
1615
export interface DashboardTabItem {
1716
id: string
1817
label: string
1918
href: string
2019
icon?: ReactNode
2120
}
2221

22+
export interface DashboardTabsListProps {
23+
layoutKey: string
24+
tabs: DashboardTabItem[]
25+
className?: string
26+
headerAccessory?: ReactNode
27+
/**
28+
* Controls how the tab bar renders on mobile (`max-md`) viewports.
29+
* - `tabs` (default): horizontal `TabsList`, identical to desktop.
30+
* - `select`: replaces the tab list with a `Select` dropdown and renders
31+
* the optional `headerAccessory` inline on the same row.
32+
*
33+
* Desktop rendering is unchanged regardless of variant.
34+
*/
35+
mobileVariant?: 'tabs' | 'select'
36+
}
37+
2338
function DashboardTabsListComponent({
2439
layoutKey,
2540
tabs,
2641
className,
2742
headerAccessory,
43+
mobileVariant = 'tabs',
2844
}: DashboardTabsListProps) {
2945
const pathname = usePathname()
46+
const router = useRouter()
3047

3148
const firstTab = tabs[0]
3249
if (!firstTab) {
3350
return null
3451
}
3552

36-
const activeTabId =
37-
tabs.find((tab) => isTabActive(pathname, tab.href))?.id ?? firstTab.id
53+
const activeTab =
54+
tabs.find((tab) => isTabActive(pathname, tab.href)) ?? firstTab
55+
const activeTabId = activeTab.id
3856

3957
const tabTriggers = tabs.map((tab) => (
4058
<TabsTrigger
@@ -51,6 +69,52 @@ function DashboardTabsListComponent({
5169
</TabsTrigger>
5270
))
5371

72+
if (mobileVariant === 'select') {
73+
const handleSelectChange = (id: string) => {
74+
const next = tabs.find((tab) => tab.id === id)
75+
if (next) router.push(next.href)
76+
}
77+
78+
return (
79+
<Tabs
80+
value={activeTabId}
81+
className={cn(
82+
'bg-bg flex w-full flex-none flex-row items-end',
83+
className
84+
)}
85+
>
86+
<Select value={activeTabId} onValueChange={handleSelectChange}>
87+
<SelectTrigger className="h-9 w-fit border-x-0 border-t-0 border-b border-solid md:hidden">
88+
<div className="flex items-center gap-2">
89+
{activeTab.icon}
90+
{activeTab.label}
91+
</div>
92+
</SelectTrigger>
93+
<SelectContent>
94+
{tabs.map((tab) => (
95+
<SelectItem key={tab.id} value={tab.id}>
96+
<span className="inline-flex items-center gap-2">
97+
{tab.icon}
98+
{tab.label}
99+
</span>
100+
</SelectItem>
101+
))}
102+
</SelectContent>
103+
</Select>
104+
105+
<TabsList className="bg-bg justify-start max-md:hidden md:flex-1">
106+
{tabTriggers}
107+
</TabsList>
108+
109+
{headerAccessory && (
110+
<div className="flex items-end border-b border-solid max-md:flex-1 max-md:justify-end md:px-6">
111+
{headerAccessory}
112+
</div>
113+
)}
114+
</Tabs>
115+
)
116+
}
117+
54118
return (
55119
<Tabs value={activeTabId} className={cn('w-full flex-none', className)}>
56120
{headerAccessory ? (

0 commit comments

Comments
 (0)