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:
parent
f950654827
commit
32d85223f2
File diff suppressed because one or more lines are too long
@ -157,7 +157,8 @@ export default function ApiKeys() {
|
||||
{data.length === 0 ? (
|
||||
<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">
|
||||
<tr>
|
||||
<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>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
<Pagination total={total} limit={limit} offset={offset} onPageChange={setOffset} onLimitChange={setLimit} />
|
||||
</div>
|
||||
|
||||
@ -44,7 +44,8 @@ export default function EventDefs() {
|
||||
{data.length === 0 ? (
|
||||
<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">
|
||||
<tr>
|
||||
<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>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@ -50,9 +50,9 @@ export default function Events() {
|
||||
|
||||
return (
|
||||
<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>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
|
||||
<input
|
||||
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"
|
||||
@ -72,7 +72,8 @@ export default function Events() {
|
||||
{data.length === 0 ? (
|
||||
<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">
|
||||
<tr>
|
||||
<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>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
<Pagination total={total} limit={limit} offset={offset} onPageChange={setOffset} onLimitChange={setLimit} />
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ReactNode, useState, useEffect } from 'react'
|
||||
|
||||
type Page =
|
||||
| '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) {
|
||||
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 (
|
||||
<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 */}
|
||||
<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 */}
|
||||
<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="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
|
||||
@ -69,6 +121,15 @@ export default function Layout({ page, onPageChange, children }: Props) {
|
||||
<p className="text-[10px] text-gray-500 font-medium">Event Engine</p>
|
||||
</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>
|
||||
|
||||
{/* Nav */}
|
||||
@ -83,7 +144,7 @@ export default function Layout({ page, onPageChange, children }: Props) {
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => onPageChange(item.id)}
|
||||
onClick={() => handleNav(item.id)}
|
||||
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
|
||||
${isActive
|
||||
@ -112,10 +173,31 @@ export default function Layout({ page, onPageChange, children }: Props) {
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main */}
|
||||
{/* Main area */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Mobile top bar */}
|
||||
<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-8 max-w-[1440px] mx-auto animate-fade-in">{children}</div>
|
||||
<div className="p-4 sm:p-6 lg:p-8 max-w-[1440px] mx-auto animate-fade-in">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -24,7 +24,8 @@ export default function ObjectDefs() {
|
||||
{data.length === 0 ? (
|
||||
<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">
|
||||
<tr>
|
||||
<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>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -71,7 +71,7 @@ export default function Objects() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<h2 className="text-lg font-semibold text-white tracking-tight">Objects</h2>
|
||||
{!showCreate ? (
|
||||
@ -160,7 +160,8 @@ export default function Objects() {
|
||||
{data.length === 0 ? (
|
||||
<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">
|
||||
<tr>
|
||||
<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>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
<Pagination total={total} limit={limit} offset={offset} onPageChange={setOffset} onLimitChange={setLimit} />
|
||||
</div>
|
||||
|
||||
@ -108,7 +108,7 @@ export default function ReactionLogs() {
|
||||
<EmptyState message="No reaction logs found" />
|
||||
) : (
|
||||
<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">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
||||
|
||||
@ -46,7 +46,8 @@ export default function Reactions() {
|
||||
{data.length === 0 ? (
|
||||
<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">
|
||||
<tr>
|
||||
<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>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
<Pagination total={total} limit={limit} offset={offset} onPageChange={setOffset} onLimitChange={setLimit} />
|
||||
</div>
|
||||
|
||||
@ -104,7 +104,7 @@ export default function RequestLogs() {
|
||||
<EmptyState message="No request logs found" />
|
||||
) : (
|
||||
<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">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user