feat(ui): responsive layout — mobile sidebar drawer + scrollable tables

- Sidebar becomes off-canvas drawer on mobile (<lg), slides in/out
- Mobile top bar with hamburger menu and current page title
- Backdrop overlay when drawer is open, ESC to close
- All tables wrapped in overflow-x-auto with min-width for scroll
- Search/filter bars stack vertically on small screens
- Content padding scales: p-4 (mobile) → p-6 (tablet) → p-8 (desktop)
- Body scroll locked when mobile drawer is open
This commit is contained in:
小糯 🐱 2026-04-13 18:00:10 +08:00
parent f950654827
commit 32d85223f2
10 changed files with 136 additions and 37 deletions

File diff suppressed because one or more lines are too long

View File

@ -157,7 +157,8 @@ export default function ApiKeys() {
{data.length === 0 ? ( {data.length === 0 ? (
<EmptyState message="No API keys found" /> <EmptyState message="No API keys found" />
) : ( ) : (
<table className="w-full"> <div className="overflow-x-auto">
<table className="w-full min-w-[600px]">
<thead className="bg-surface-3/80 border-b border-gray-700"> <thead className="bg-surface-3/80 border-b border-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th>
@ -222,6 +223,7 @@ export default function ApiKeys() {
))} ))}
</tbody> </tbody>
</table> </table>
</div>
)} )}
<Pagination total={total} limit={limit} offset={offset} onPageChange={setOffset} onLimitChange={setLimit} /> <Pagination total={total} limit={limit} offset={offset} onPageChange={setOffset} onLimitChange={setLimit} />
</div> </div>

View File

@ -44,7 +44,8 @@ export default function EventDefs() {
{data.length === 0 ? ( {data.length === 0 ? (
<EmptyState message="No event definitions found" /> <EmptyState message="No event definitions found" />
) : ( ) : (
<table className="w-full"> <div className="overflow-x-auto">
<table className="w-full min-w-[600px]">
<thead className="bg-surface-3/80 border-b border-gray-700"> <thead className="bg-surface-3/80 border-b border-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider"> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
@ -121,6 +122,7 @@ export default function EventDefs() {
))} ))}
</tbody> </tbody>
</table> </table>
</div>
)} )}
</div> </div>

View File

@ -50,9 +50,9 @@ export default function Events() {
return ( return (
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<div className="flex items-center justify-between mb-6"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 mb-5">
<h2 className="text-lg font-semibold text-white tracking-tight">Events</h2> <h2 className="text-lg font-semibold text-white tracking-tight">Events</h2>
<div className="flex gap-2"> <div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
<input <input
type="text" type="text"
className="px-4 py-2 bg-surface-3 border border-white/[0.08] rounded-lg focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent/30 transition-all" className="px-4 py-2 bg-surface-3 border border-white/[0.08] rounded-lg focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent/30 transition-all"
@ -72,7 +72,8 @@ export default function Events() {
{data.length === 0 ? ( {data.length === 0 ? (
<EmptyState message="No events yet" /> <EmptyState message="No events yet" />
) : ( ) : (
<table className="w-full"> <div className="overflow-x-auto">
<table className="w-full min-w-[600px]">
<thead className="bg-surface-3/80 border-b border-gray-700"> <thead className="bg-surface-3/80 border-b border-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th>
@ -134,6 +135,7 @@ export default function Events() {
))} ))}
</tbody> </tbody>
</table> </table>
</div>
)} )}
<Pagination total={total} limit={limit} offset={offset} onPageChange={setOffset} onLimitChange={setLimit} /> <Pagination total={total} limit={limit} offset={offset} onPageChange={setOffset} onLimitChange={setLimit} />
</div> </div>

View File

@ -1,4 +1,4 @@
import { ReactNode } from 'react' import { ReactNode, useState, useEffect } from 'react'
type Page = type Page =
| 'health' | 'health'
@ -53,13 +53,65 @@ const navGroups: Array<{ label: string; items: Array<{ id: Page; label: string;
}, },
] ]
/** Find current nav label for mobile header */
function getPageLabel(page: Page): string {
for (const g of navGroups) {
for (const item of g.items) {
if (item.id === page) return item.label
}
}
return 'OGraph'
}
export default function Layout({ page, onPageChange, children }: Props) { export default function Layout({ page, onPageChange, children }: Props) {
const [sidebarOpen, setSidebarOpen] = useState(false)
// Close sidebar on page change (mobile)
const handleNav = (p: Page) => {
onPageChange(p)
setSidebarOpen(false)
}
// Close sidebar on ESC
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setSidebarOpen(false)
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [])
// Lock body scroll when sidebar is open on mobile
useEffect(() => {
if (sidebarOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
return () => { document.body.style.overflow = '' }
}, [sidebarOpen])
return ( return (
<div className="flex h-screen bg-surface-0 text-gray-200 grain"> <div className="flex h-screen bg-surface-0 text-gray-200 grain">
{/* Mobile backdrop */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/60 z-30 lg:hidden animate-fade-in"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar */} {/* Sidebar */}
<aside className="w-56 bg-surface-1 border-r border-white/[0.06] flex flex-col shrink-0"> <aside
className={`
fixed inset-y-0 left-0 z-40 w-56 bg-surface-1 border-r border-white/[0.06] flex flex-col shrink-0
transform transition-transform duration-200 ease-out
lg:relative lg:translate-x-0
${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
`}
>
{/* Logo */} {/* Logo */}
<div className="px-5 py-5 border-b border-white/[0.06]"> <div className="px-5 py-5 border-b border-white/[0.06] flex items-center justify-between">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-accent to-blue-400 flex items-center justify-center text-white text-xs font-bold shadow-lg shadow-accent/20"> <div className="w-7 h-7 rounded-lg bg-gradient-to-br from-accent to-blue-400 flex items-center justify-center text-white text-xs font-bold shadow-lg shadow-accent/20">
G G
@ -69,6 +121,15 @@ export default function Layout({ page, onPageChange, children }: Props) {
<p className="text-[10px] text-gray-500 font-medium">Event Engine</p> <p className="text-[10px] text-gray-500 font-medium">Event Engine</p>
</div> </div>
</div> </div>
{/* Close button (mobile only) */}
<button
onClick={() => setSidebarOpen(false)}
className="lg:hidden p-1 rounded-md text-gray-500 hover:text-gray-300 hover:bg-white/[0.06] transition-colors"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div> </div>
{/* Nav */} {/* Nav */}
@ -83,7 +144,7 @@ export default function Layout({ page, onPageChange, children }: Props) {
return ( return (
<button <button
key={item.id} key={item.id}
onClick={() => onPageChange(item.id)} onClick={() => handleNav(item.id)}
className={` className={`
w-full text-left px-2.5 py-[7px] rounded-md mb-0.5 flex items-center gap-2.5 text-[13px] font-medium transition-all w-full text-left px-2.5 py-[7px] rounded-md mb-0.5 flex items-center gap-2.5 text-[13px] font-medium transition-all
${isActive ${isActive
@ -112,10 +173,31 @@ export default function Layout({ page, onPageChange, children }: Props) {
</div> </div>
</aside> </aside>
{/* Main */} {/* Main area */}
<main className="flex-1 overflow-y-auto bg-surface-0"> <div className="flex-1 flex flex-col min-w-0">
<div className="p-8 max-w-[1440px] mx-auto animate-fade-in">{children}</div> {/* Mobile top bar */}
</main> <header className="lg:hidden flex items-center gap-3 px-4 py-3 bg-surface-1 border-b border-white/[0.06] shrink-0">
<button
onClick={() => setSidebarOpen(true)}
className="p-1.5 rounded-md text-gray-400 hover:text-gray-200 hover:bg-white/[0.06] transition-colors"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<div className="flex items-center gap-2">
<div className="w-5 h-5 rounded bg-gradient-to-br from-accent to-blue-400 flex items-center justify-center text-white text-[9px] font-bold">
G
</div>
<span className="text-sm font-semibold text-white">{getPageLabel(page)}</span>
</div>
</header>
{/* Content */}
<main className="flex-1 overflow-y-auto bg-surface-0">
<div className="p-4 sm:p-6 lg:p-8 max-w-[1440px] mx-auto animate-fade-in">{children}</div>
</main>
</div>
</div> </div>
) )
} }

View File

@ -24,7 +24,8 @@ export default function ObjectDefs() {
{data.length === 0 ? ( {data.length === 0 ? (
<EmptyState message="No object definitions found" /> <EmptyState message="No object definitions found" />
) : ( ) : (
<table className="w-full"> <div className="overflow-x-auto">
<table className="w-full min-w-[600px]">
<thead className="bg-surface-3/80 border-b border-gray-700"> <thead className="bg-surface-3/80 border-b border-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider"> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
@ -45,6 +46,7 @@ export default function ObjectDefs() {
))} ))}
</tbody> </tbody>
</table> </table>
</div>
)} )}
</div> </div>
</div> </div>

View File

@ -71,7 +71,7 @@ export default function Objects() {
return ( return (
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<div className="flex items-center justify-between mb-6"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 mb-5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<h2 className="text-lg font-semibold text-white tracking-tight">Objects</h2> <h2 className="text-lg font-semibold text-white tracking-tight">Objects</h2>
{!showCreate ? ( {!showCreate ? (
@ -160,7 +160,8 @@ export default function Objects() {
{data.length === 0 ? ( {data.length === 0 ? (
<EmptyState message="No objects found" /> <EmptyState message="No objects found" />
) : ( ) : (
<table className="w-full"> <div className="overflow-x-auto">
<table className="w-full min-w-[600px]">
<thead className="bg-surface-3/80 border-b border-gray-700"> <thead className="bg-surface-3/80 border-b border-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th>
@ -191,6 +192,7 @@ export default function Objects() {
))} ))}
</tbody> </tbody>
</table> </table>
</div>
)} )}
<Pagination total={total} limit={limit} offset={offset} onPageChange={setOffset} onLimitChange={setLimit} /> <Pagination total={total} limit={limit} offset={offset} onPageChange={setOffset} onLimitChange={setLimit} />
</div> </div>

View File

@ -108,7 +108,7 @@ export default function ReactionLogs() {
<EmptyState message="No reaction logs found" /> <EmptyState message="No reaction logs found" />
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full min-w-[800px]">
<thead className="bg-surface-3/80 border-b border-gray-700"> <thead className="bg-surface-3/80 border-b border-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider"> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">

View File

@ -46,7 +46,8 @@ export default function Reactions() {
{data.length === 0 ? ( {data.length === 0 ? (
<EmptyState message="No reactions found" /> <EmptyState message="No reactions found" />
) : ( ) : (
<table className="w-full"> <div className="overflow-x-auto">
<table className="w-full min-w-[600px]">
<thead className="bg-surface-3/80 border-b border-gray-700"> <thead className="bg-surface-3/80 border-b border-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th>
@ -121,6 +122,7 @@ export default function Reactions() {
))} ))}
</tbody> </tbody>
</table> </table>
</div>
)} )}
<Pagination total={total} limit={limit} offset={offset} onPageChange={setOffset} onLimitChange={setLimit} /> <Pagination total={total} limit={limit} offset={offset} onPageChange={setOffset} onLimitChange={setLimit} />
</div> </div>

View File

@ -104,7 +104,7 @@ export default function RequestLogs() {
<EmptyState message="No request logs found" /> <EmptyState message="No request logs found" />
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full min-w-[800px]">
<thead className="bg-gray-800/80 border-b border-gray-700"> <thead className="bg-gray-800/80 border-b border-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider"> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">