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 ? (
|
{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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
<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">
|
<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>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user