updated for mobile view and dark theme

This commit is contained in:
2026-06-10 12:29:53 +05:30
parent 735e022294
commit c8d2e53a0f
10 changed files with 205 additions and 81 deletions

View File

@@ -6,6 +6,18 @@
<link rel="icon" type="image/png" href="/favicon.png" /> <link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NearleXpress Developer Docs</title> <title>NearleXpress Developer Docs</title>
<script>
// Apply persisted theme before paint to avoid a flash of the wrong theme
(function () {
try {
var stored = localStorage.getItem('theme');
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (stored === 'dark' || (!stored && prefersDark)) {
document.documentElement.classList.add('dark');
}
} catch (e) {}
})();
</script>
</head> </head>
<body> <body>

8
package-lock.json generated
View File

@@ -67,7 +67,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@@ -1478,7 +1477,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.10.12", "baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782", "caniuse-lite": "^1.0.30001782",
@@ -2337,7 +2335,6 @@
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
@@ -2694,7 +2691,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -2916,7 +2912,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -3405,7 +3400,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -3531,7 +3525,6 @@
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.4", "fdir": "^6.4.4",
@@ -3625,7 +3618,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },

View File

@@ -1,7 +1,9 @@
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { Menu, Layers } from 'lucide-react'
import Sidebar from './components/Sidebar' import Sidebar from './components/Sidebar'
import Introduction from './components/Introduction' import Introduction from './components/Introduction'
import TopicView from './components/TopicView' import TopicView from './components/TopicView'
import ThemeToggle from './components/ThemeToggle'
import { legacyTopics, restTopics, LEGACY_BASE_URL, REST_BASE_URL } from './data/topics' import { legacyTopics, restTopics, LEGACY_BASE_URL, REST_BASE_URL } from './data/topics'
const processTopics = (topics, baseUrl, type) => const processTopics = (topics, baseUrl, type) =>
@@ -26,31 +28,68 @@ function filterTopics(topics, q) {
export default function App() { export default function App() {
const [activeTopic, setActiveTopic] = useState(null) const [activeTopic, setActiveTopic] = useState(null)
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [sidebarOpen, setSidebarOpen] = useState(false)
const visibleLegacy = useMemo(() => filterTopics(allLegacy, searchQuery.trim().toLowerCase()), [searchQuery]) const visibleLegacy = useMemo(() => filterTopics(allLegacy, searchQuery.trim().toLowerCase()), [searchQuery])
const visibleRest = useMemo(() => filterTopics(allRest, searchQuery.trim().toLowerCase()), [searchQuery]) const visibleRest = useMemo(() => filterTopics(allRest, searchQuery.trim().toLowerCase()), [searchQuery])
// Picking a topic (or Introduction) should also close the drawer on mobile
const selectTopic = (t) => {
setActiveTopic(t)
setSidebarOpen(false)
}
return ( return (
<div className="min-h-screen bg-slate-50 bg-grid-pattern flex relative overflow-hidden"> <div className="min-h-screen bg-slate-50 dark:bg-dark-900 bg-grid-pattern flex relative overflow-hidden transition-colors">
{/* Ambient background glows */} {/* Ambient background glows */}
<div className="absolute top-0 -left-4 w-72 h-72 bg-brand-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob"></div> <div className="absolute top-0 -left-4 w-72 h-72 bg-brand-300 rounded-full mix-blend-multiply dark:mix-blend-screen filter blur-3xl opacity-20 dark:opacity-10 animate-blob"></div>
<div className="absolute top-0 -right-4 w-72 h-72 bg-indigo-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob animation-delay-2000"></div> <div className="absolute top-0 -right-4 w-72 h-72 bg-indigo-300 rounded-full mix-blend-multiply dark:mix-blend-screen filter blur-3xl opacity-20 dark:opacity-10 animate-blob animation-delay-2000"></div>
<div className="absolute -bottom-8 left-20 w-72 h-72 bg-pink-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob animation-delay-4000"></div> <div className="absolute -bottom-8 left-20 w-72 h-72 bg-pink-300 rounded-full mix-blend-multiply dark:mix-blend-screen filter blur-3xl opacity-20 dark:opacity-10 animate-blob animation-delay-4000"></div>
{/* Theme toggle (floating, top-right) */}
<ThemeToggle />
{/* Mobile top bar */}
<header className="md:hidden fixed top-0 left-0 right-0 h-16 z-30 glass flex items-center gap-3 px-4">
<button
onClick={() => setSidebarOpen(true)}
aria-label="Open navigation"
className="w-10 h-10 flex items-center justify-center rounded-lg text-slate-600 dark:text-slate-300 hover:bg-slate-100/60 dark:hover:bg-white/5 transition-colors"
>
<Menu size={20} />
</button>
<button onClick={() => selectTopic(null)} className="flex items-center gap-2.5 text-slate-900 dark:text-slate-100 font-bold text-lg">
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-brand-500 to-indigo-600 flex items-center justify-center shadow-glow">
<Layers className="text-white w-4 h-4" />
</div>
<span className="tracking-tight">NearleXpress</span>
</button>
</header>
{/* Mobile drawer backdrop */}
{sidebarOpen && (
<div
onClick={() => setSidebarOpen(false)}
className="md:hidden fixed inset-0 z-30 bg-slate-900/40 dark:bg-black/60 backdrop-blur-sm animate-fade-in"
/>
)}
<Sidebar <Sidebar
legacyTopics={visibleLegacy} legacyTopics={visibleLegacy}
restTopics={visibleRest} restTopics={visibleRest}
activeTopic={activeTopic} activeTopic={activeTopic}
setActiveTopic={setActiveTopic} setActiveTopic={selectTopic}
searchQuery={searchQuery} searchQuery={searchQuery}
setSearchQuery={setSearchQuery} setSearchQuery={setSearchQuery}
isOpen={sidebarOpen}
onClose={() => setSidebarOpen(false)}
/> />
<main className="ml-[280px] flex-1 min-h-screen relative z-10"> <main className="md:ml-[280px] flex-1 min-h-screen relative z-10 pt-16 md:pt-0">
{activeTopic {activeTopic
? ( ? (
<div className="max-w-[1200px] mx-auto p-12 lg:p-16 opacity-0 animate-fade-in-up"> <div className="max-w-[1200px] mx-auto p-5 sm:p-8 lg:p-16 opacity-0 animate-fade-in-up">
<TopicView topic={activeTopic} searchQuery={searchQuery} /> <TopicView topic={activeTopic} searchQuery={searchQuery} />
</div> </div>
) )
@@ -59,7 +98,7 @@ export default function App() {
<Introduction <Introduction
allLegacy={allLegacy} allLegacy={allLegacy}
allRest={allRest} allRest={allRest}
setActiveTopic={setActiveTopic} setActiveTopic={selectTopic}
/> />
</div> </div>
)} )}

View File

@@ -114,20 +114,20 @@ export default function EndpointCard({ endpoint, baseUrl, isLegacy, onSend, resu
} }
const methodColor = { const methodColor = {
GET: 'bg-emerald-100/50 text-emerald-700 border-emerald-200', GET: 'bg-emerald-100/50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border-emerald-200 dark:border-emerald-500/20',
POST: 'bg-blue-100/50 text-blue-700 border-blue-200', POST: 'bg-blue-100/50 dark:bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-500/20',
PUT: 'bg-amber-100/50 text-amber-700 border-amber-200', PUT: 'bg-amber-100/50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 border-amber-200 dark:border-amber-500/20',
DELETE: 'bg-red-100/50 text-red-700 border-red-200', DELETE: 'bg-red-100/50 dark:bg-red-500/10 text-red-700 dark:text-red-400 border-red-200 dark:border-red-500/20',
PATCH: 'bg-purple-100/50 text-purple-700 border-purple-200', PATCH: 'bg-purple-100/50 dark:bg-purple-500/10 text-purple-700 dark:text-purple-400 border-purple-200 dark:border-purple-500/20',
}[endpoint.method] || 'bg-slate-100/50 text-slate-700 border-slate-200' }[endpoint.method] || 'bg-slate-100/50 dark:bg-slate-500/10 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-500/20'
const isOk = result?.kind === 'response' && result.ok const isOk = result?.kind === 'response' && result.ok
const isErr = result?.kind === 'response' && !result.ok const isErr = result?.kind === 'response' && !result.ok
const isNet = result?.kind === 'network-error' const isNet = result?.kind === 'network-error'
return ( return (
<div className="py-16 border-t border-slate-200/60 group/row" id={endpoint.name}> <div className="py-10 sm:py-16 border-t border-slate-200/60 dark:border-white/10 group/row" id={endpoint.name}>
<div className="grid grid-cols-1 xl:grid-cols-12 gap-12 lg:gap-16"> <div className="grid grid-cols-1 xl:grid-cols-12 gap-6 sm:gap-12 lg:gap-16">
{/* Left Column: Description & Parameters */} {/* Left Column: Description & Parameters */}
<div className="xl:col-span-5 space-y-8"> <div className="xl:col-span-5 space-y-8">
@@ -136,39 +136,39 @@ export default function EndpointCard({ endpoint, baseUrl, isLegacy, onSend, resu
<span className={`px-2.5 py-1 rounded text-xs font-bold tracking-widest border ${methodColor}`}> <span className={`px-2.5 py-1 rounded text-xs font-bold tracking-widest border ${methodColor}`}>
{endpoint.method} {endpoint.method}
</span> </span>
<h3 className="text-2xl font-bold text-slate-900 tracking-tight"> <h3 className="text-xl sm:text-2xl font-bold text-slate-900 dark:text-slate-100 tracking-tight break-words">
{endpoint.name} {endpoint.name}
</h3> </h3>
</div> </div>
{endpoint.description && ( {endpoint.description && (
<p className="text-[15px] text-slate-600 leading-relaxed">{endpoint.description}</p> <p className="text-[15px] text-slate-600 dark:text-slate-400 leading-relaxed">{endpoint.description}</p>
)} )}
</div> </div>
{/* URL bar */} {/* URL bar */}
<div className="bg-white border border-slate-200/80 rounded-xl p-3.5 font-mono text-sm shadow-sm flex items-start gap-2 group-hover/row:border-brand-300 transition-colors min-w-0"> <div className="bg-white dark:bg-dark-800 border border-slate-200/80 dark:border-white/10 rounded-xl p-3.5 font-mono text-sm shadow-sm flex items-start gap-2 group-hover/row:border-brand-300 dark:group-hover/row:border-brand-500/40 transition-colors min-w-0">
<Server size={14} className="text-brand-400 shrink-0 mt-0.5" /> <Server size={14} className="text-brand-400 shrink-0 mt-0.5" />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="text-xs text-slate-400 truncate">{baseUrl}</div> <div className="text-xs text-slate-400 dark:text-slate-500 truncate">{baseUrl}</div>
<div className="font-semibold text-slate-800 break-all">{path}</div> <div className="font-semibold text-slate-800 dark:text-slate-200 break-all">{path}</div>
</div> </div>
</div> </div>
{/* Query Parameters */} {/* Query Parameters */}
{paramDefs.length > 0 && ( {paramDefs.length > 0 && (
<div className="pt-2"> <div className="pt-2">
<h4 className="text-xs font-bold text-slate-400 mb-4 uppercase tracking-widest flex items-center gap-2"> <h4 className="text-xs font-bold text-slate-400 dark:text-slate-500 mb-4 uppercase tracking-widest flex items-center gap-2">
<span className="w-4 h-[1px] bg-slate-300"></span> Query Parameters <span className="w-4 h-[1px] bg-slate-300 dark:bg-white/10"></span> Query Parameters
</h4> </h4>
<div className="space-y-4"> <div className="space-y-4">
{paramDefs.map((p) => ( {paramDefs.map((p) => (
<div key={p.name} className="flex flex-col gap-2"> <div key={p.name} className="flex flex-col gap-2">
<label className="text-sm font-semibold text-slate-800 flex justify-between items-center"> <label className="text-sm font-semibold text-slate-800 dark:text-slate-200 flex justify-between items-center">
{p.name} {p.name}
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-mono">{p.type || 'string'}</span> <span className="text-[10px] text-slate-400 dark:text-slate-500 uppercase tracking-wider font-mono">{p.type || 'string'}</span>
</label> </label>
<input <input
className="w-full px-3.5 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500/20 focus:border-brand-500 transition-all shadow-sm text-slate-700" className="w-full px-3.5 py-2.5 bg-white dark:bg-dark-800 border border-slate-200 dark:border-white/10 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500/20 focus:border-brand-500 transition-all shadow-sm text-slate-700 dark:text-slate-200 placeholder:text-slate-400 dark:placeholder:text-slate-500"
value={values[p.name] ?? ''} value={values[p.name] ?? ''}
placeholder={`Enter ${p.name}...`} placeholder={`Enter ${p.name}...`}
onChange={(e) => { onChange={(e) => {

View File

@@ -18,32 +18,32 @@ export default function Introduction({ allLegacy, allRest, setActiveTopic }) {
const allTopics = [...allLegacy, ...allRest] const allTopics = [...allLegacy, ...allRest]
return ( return (
<div className="max-w-[1000px] mx-auto px-12 py-20 lg:px-24 lg:py-28"> <div className="max-w-[1000px] mx-auto px-5 py-12 sm:px-12 sm:py-20 lg:px-24 lg:py-28">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-brand-50 border border-brand-100 text-brand-600 text-sm font-medium mb-8"> <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-brand-50 dark:bg-brand-500/10 border border-brand-100 dark:border-brand-500/20 text-brand-600 dark:text-brand-400 text-sm font-medium mb-8">
<Zap size={14} className="fill-current" /> <Zap size={14} className="fill-current" />
v1.0 Developer API v1.0 Developer API
</div> </div>
<h1 className="text-4xl lg:text-5xl font-bold text-slate-900 mb-6 tracking-tight"> <h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-slate-900 dark:text-slate-100 mb-6 tracking-tight">
<span className="text-gradient">NearleXpress API</span> <span className="text-gradient">NearleXpress API</span>
</h1> </h1>
<p className="text-xl text-slate-600 mb-12 max-w-2xl leading-relaxed"> <p className="text-lg sm:text-xl text-slate-600 dark:text-slate-400 mb-12 max-w-2xl leading-relaxed">
A comprehensive platform for managing tenants, users, partners, customers, A comprehensive platform for managing tenants, users, partners, customers,
orders, deliveries, products, invoices, and payments across the Express network. orders, deliveries, products, invoices, and payments across the Express network.
</p> </p>
<div className="mb-8"> <div className="mb-8">
<h2 className="text-xs font-bold uppercase tracking-widest text-slate-400 mb-4">Base URLs</h2> <h2 className="text-xs font-bold uppercase tracking-widest text-slate-400 dark:text-slate-500 mb-4">Base URLs</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-10"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-10">
<div className="bg-white border border-slate-200/60 rounded-xl p-4 shadow-sm"> <div className="bg-white dark:bg-dark-800 border border-slate-200/60 dark:border-white/10 rounded-xl p-4 shadow-sm">
<div className="text-[11px] text-brand-600 font-bold uppercase tracking-widest mb-1.5">GraphQL API</div> <div className="text-[11px] text-brand-600 dark:text-brand-400 font-bold uppercase tracking-widest mb-1.5">GraphQL API</div>
<code className="font-mono text-sm text-slate-700">{LEGACY_BASE_URL}</code> <code className="font-mono text-sm text-slate-700 dark:text-slate-300 break-all">{LEGACY_BASE_URL}</code>
</div> </div>
<div className="bg-white border border-slate-200/60 rounded-xl p-4 shadow-sm"> <div className="bg-white dark:bg-dark-800 border border-slate-200/60 dark:border-white/10 rounded-xl p-4 shadow-sm">
<div className="text-[11px] text-indigo-600 font-bold uppercase tracking-widest mb-1.5">REST API</div> <div className="text-[11px] text-indigo-600 dark:text-indigo-400 font-bold uppercase tracking-widest mb-1.5">REST API</div>
<code className="font-mono text-sm text-slate-700">{REST_BASE_URL}</code> <code className="font-mono text-sm text-slate-700 dark:text-slate-300 break-all">{REST_BASE_URL}</code>
</div> </div>
</div> </div>
</div> </div>
@@ -55,14 +55,14 @@ export default function Introduction({ allLegacy, allRest, setActiveTopic }) {
<div <div
key={topic.uniqueId} key={topic.uniqueId}
onClick={() => setActiveTopic(topic)} onClick={() => setActiveTopic(topic)}
className="group relative bg-white p-6 rounded-2xl border border-slate-200/60 shadow-sm hover:shadow-xl hover:shadow-brand-500/5 hover:border-brand-200 transition-all duration-300 cursor-pointer overflow-hidden" className="group relative bg-white dark:bg-dark-800 p-6 rounded-2xl border border-slate-200/60 dark:border-white/10 shadow-sm hover:shadow-xl hover:shadow-brand-500/5 dark:hover:shadow-brand-500/10 hover:border-brand-200 dark:hover:border-brand-500/30 transition-all duration-300 cursor-pointer overflow-hidden"
style={{ animationDelay: `${index * 100}ms` }} style={{ animationDelay: `${index * 100}ms` }}
> >
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-brand-50 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div> <div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-brand-50 dark:from-brand-500/10 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
<div className="relative z-10"> <div className="relative z-10">
<h3 className="text-lg font-bold text-slate-900 mb-2 group-hover:text-brand-600 transition-colors flex items-center gap-2"> <h3 className="text-lg font-bold text-slate-900 dark:text-slate-100 mb-2 group-hover:text-brand-600 dark:group-hover:text-brand-400 transition-colors flex items-center gap-2">
<div className="p-1.5 bg-brand-50 text-brand-600 rounded-md group-hover:bg-brand-100 transition-colors"> <div className="p-1.5 bg-brand-50 dark:bg-brand-500/10 text-brand-600 dark:text-brand-400 rounded-md group-hover:bg-brand-100 dark:group-hover:bg-brand-500/20 transition-colors">
<Icon size={18} /> <Icon size={18} />
</div> </div>
{topic.name} {topic.name}
@@ -71,14 +71,14 @@ export default function Introduction({ allLegacy, allRest, setActiveTopic }) {
<div className="flex items-center gap-2 mt-1 mb-2"> <div className="flex items-center gap-2 mt-1 mb-2">
<span className={`text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 rounded ${ <span className={`text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 rounded ${
topic.type === 'legacy' topic.type === 'legacy'
? 'bg-brand-50 text-brand-600 border border-brand-100' ? 'bg-brand-50 dark:bg-brand-500/10 text-brand-600 dark:text-brand-400 border border-brand-100 dark:border-brand-500/20'
: 'bg-indigo-50 text-indigo-600 border border-indigo-100' : 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border border-indigo-100 dark:border-indigo-500/20'
}`}> }`}>
{topic.type === 'legacy' ? 'GraphQL' : 'REST'} {topic.type === 'legacy' ? 'GraphQL' : 'REST'}
</span> </span>
<span className="text-[11px] text-slate-400">{topic.endpoints.length} endpoint{topic.endpoints.length !== 1 ? 's' : ''}</span> <span className="text-[11px] text-slate-400 dark:text-slate-500">{topic.endpoints.length} endpoint{topic.endpoints.length !== 1 ? 's' : ''}</span>
</div> </div>
<p className="text-sm text-slate-500 leading-relaxed"> <p className="text-sm text-slate-500 dark:text-slate-400 leading-relaxed">
{topic.description} {topic.description}
</p> </p>
</div> </div>
@@ -87,7 +87,7 @@ export default function Introduction({ allLegacy, allRest, setActiveTopic }) {
})} })}
</div> </div>
<div className="mt-16 flex items-center gap-4 p-6 bg-slate-900 text-slate-300 rounded-2xl shadow-code"> <div className="mt-16 flex items-center gap-4 p-6 bg-slate-900 dark:bg-dark-700 dark:border dark:border-white/10 text-slate-300 rounded-2xl shadow-code">
<Code2 className="text-brand-400 shrink-0" size={24} /> <Code2 className="text-brand-400 shrink-0" size={24} />
<p className="text-sm"> <p className="text-sm">
Explore interactive endpoints for both GraphQL and REST APIs. All requests route through the dev proxy which injects authentication headers securely. Explore interactive endpoints for both GraphQL and REST APIs. All requests route through the dev proxy which injects authentication headers securely.

View File

@@ -1,8 +1,8 @@
import { useState } from 'react' import { useState } from 'react'
import { Search, ChevronRight, ChevronDown, Layers, Terminal } from 'lucide-react' import { Search, ChevronRight, ChevronDown, Layers, Terminal, X } from 'lucide-react'
import { getTopicIcon } from '../lib/icons' import { getTopicIcon } from '../lib/icons'
export default function Sidebar({ legacyTopics, restTopics, activeTopic, setActiveTopic, searchQuery, setSearchQuery }) { export default function Sidebar({ legacyTopics, restTopics, activeTopic, setActiveTopic, searchQuery, setSearchQuery, isOpen = false, onClose }) {
const [open, setOpen] = useState({ general: true, legacy: true, rest: true }) const [open, setOpen] = useState({ general: true, legacy: true, rest: true })
const toggle = (k) => setOpen((s) => ({ ...s, [k]: !s[k] })) const toggle = (k) => setOpen((s) => ({ ...s, [k]: !s[k] }))
@@ -10,12 +10,12 @@ export default function Sidebar({ legacyTopics, restTopics, activeTopic, setActi
<div> <div>
<button <button
onClick={() => toggle(key)} onClick={() => toggle(key)}
className="w-full flex items-center justify-between px-2 py-1.5 text-slate-800 font-semibold text-sm hover:text-brand-600 transition-colors group" className="w-full flex items-center justify-between px-2 py-1.5 text-slate-800 dark:text-slate-200 font-semibold text-sm hover:text-brand-600 transition-colors group"
> >
<span className="tracking-wide text-xs uppercase text-slate-400 group-hover:text-brand-500 transition-colors"> <span className="tracking-wide text-xs uppercase text-slate-400 dark:text-slate-500 group-hover:text-brand-500 transition-colors">
{title} {title}
</span> </span>
{open[key] ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />} {open[key] ? <ChevronDown size={14} className="text-slate-400 dark:text-slate-500" /> : <ChevronRight size={14} className="text-slate-400 dark:text-slate-500" />}
</button> </button>
<div className={`mt-2 space-y-1 overflow-hidden transition-all duration-500 ${open[key] ? 'max-h-[800px] opacity-100' : 'max-h-0 opacity-0'}`}> <div className={`mt-2 space-y-1 overflow-hidden transition-all duration-500 ${open[key] ? 'max-h-[800px] opacity-100' : 'max-h-0 opacity-0'}`}>
{topics.map((t) => { {topics.map((t) => {
@@ -27,13 +27,13 @@ export default function Sidebar({ legacyTopics, restTopics, activeTopic, setActi
onClick={() => setActiveTopic(t)} onClick={() => setActiveTopic(t)}
className={`w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm transition-all duration-200 group ${ className={`w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm transition-all duration-200 group ${
isActive isActive
? 'text-brand-700 bg-brand-50 shadow-sm font-medium' ? 'text-brand-700 dark:text-brand-300 bg-brand-50 dark:bg-brand-500/10 shadow-sm font-medium'
: 'text-slate-600 hover:text-slate-900 hover:bg-slate-100/50' : 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-100/50 dark:hover:bg-white/5'
}`} }`}
> >
<Icon <Icon
size={16} size={16}
className={`${isActive ? 'text-brand-500' : 'text-slate-400 group-hover:text-slate-500'} transition-colors`} className={`${isActive ? 'text-brand-500' : 'text-slate-400 dark:text-slate-500 group-hover:text-slate-500 dark:group-hover:text-slate-400'} transition-colors`}
/> />
{t.name} {t.name}
</button> </button>
@@ -44,21 +44,35 @@ export default function Sidebar({ legacyTopics, restTopics, activeTopic, setActi
) )
return ( return (
<div className="w-[280px] glass h-screen fixed top-0 left-0 flex flex-col z-20 transition-all"> <div
className={`w-[280px] glass h-screen fixed top-0 left-0 flex flex-col z-40 transition-transform duration-300 ease-out ${
isOpen ? 'translate-x-0' : '-translate-x-full'
} md:translate-x-0`}
>
{/* Brand Header */} {/* Brand Header */}
<div className="p-6 border-b border-slate-100/50"> <div className="p-6 border-b border-slate-100/50 dark:border-white/5">
<div className="flex items-center gap-3 text-slate-900 font-bold text-xl mb-6"> <div className="flex items-center justify-between gap-3 text-slate-900 dark:text-slate-100 font-bold text-xl mb-6">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-brand-500 to-indigo-600 flex items-center justify-center shadow-glow"> <div className="flex items-center gap-3">
<Layers className="text-white w-4 h-4" /> <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-brand-500 to-indigo-600 flex items-center justify-center shadow-glow">
<Layers className="text-white w-4 h-4" />
</div>
<span className="tracking-tight">NearleXpress</span>
</div> </div>
<span className="tracking-tight">NearleXpress</span> {/* Close button — mobile only */}
<button
onClick={onClose}
aria-label="Close navigation"
className="md:hidden w-8 h-8 -mr-1 flex items-center justify-center rounded-lg text-slate-500 dark:text-slate-400 hover:bg-slate-100/60 dark:hover:bg-white/5 transition-colors"
>
<X size={18} />
</button>
</div> </div>
<div className="relative group"> <div className="relative group">
<Search className="w-4 h-4 absolute left-3 top-3 text-slate-400 group-focus-within:text-brand-500 transition-colors" /> <Search className="w-4 h-4 absolute left-3 top-3 text-slate-400 group-focus-within:text-brand-500 transition-colors" />
<input <input
type="text" type="text"
className="w-full pl-9 pr-3 py-2.5 bg-slate-50/50 border border-slate-200/60 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-brand-500/20 focus:border-brand-500 focus:bg-white transition-all placeholder:text-slate-400" className="w-full pl-9 pr-3 py-2.5 bg-slate-50/50 dark:bg-white/5 border border-slate-200/60 dark:border-white/10 rounded-xl text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-brand-500/20 focus:border-brand-500 focus:bg-white dark:focus:bg-white/5 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-500"
placeholder="Search documentation..." placeholder="Search documentation..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
@@ -73,21 +87,21 @@ export default function Sidebar({ legacyTopics, restTopics, activeTopic, setActi
<div> <div>
<button <button
onClick={() => toggle('general')} onClick={() => toggle('general')}
className="w-full flex items-center justify-between px-2 py-1.5 text-slate-800 font-semibold text-sm hover:text-brand-600 transition-colors group" className="w-full flex items-center justify-between px-2 py-1.5 text-slate-800 dark:text-slate-200 font-semibold text-sm hover:text-brand-600 transition-colors group"
> >
<span className="tracking-wide text-xs uppercase text-slate-400 group-hover:text-brand-500 transition-colors">Getting Started</span> <span className="tracking-wide text-xs uppercase text-slate-400 dark:text-slate-500 group-hover:text-brand-500 transition-colors">Getting Started</span>
{open.general ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />} {open.general ? <ChevronDown size={14} className="text-slate-400 dark:text-slate-500" /> : <ChevronRight size={14} className="text-slate-400 dark:text-slate-500" />}
</button> </button>
<div className={`mt-2 space-y-1 overflow-hidden transition-all duration-300 ${open.general ? 'max-h-40 opacity-100' : 'max-h-0 opacity-0'}`}> <div className={`mt-2 space-y-1 overflow-hidden transition-all duration-300 ${open.general ? 'max-h-40 opacity-100' : 'max-h-0 opacity-0'}`}>
<button <button
onClick={() => setActiveTopic(null)} onClick={() => setActiveTopic(null)}
className={`w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm transition-all duration-200 ${ className={`w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm transition-all duration-200 ${
activeTopic === null activeTopic === null
? 'text-brand-700 bg-brand-50 shadow-sm font-medium' ? 'text-brand-700 dark:text-brand-300 bg-brand-50 dark:bg-brand-500/10 shadow-sm font-medium'
: 'text-slate-600 hover:text-slate-900 hover:bg-slate-100/50' : 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-100/50 dark:hover:bg-white/5'
}`} }`}
> >
<Terminal size={16} className={activeTopic === null ? 'text-brand-500' : 'text-slate-400'} /> <Terminal size={16} className={activeTopic === null ? 'text-brand-500' : 'text-slate-400 dark:text-slate-500'} />
Introduction Introduction
</button> </button>
</div> </div>

View File

@@ -0,0 +1,40 @@
import { useEffect, useState } from 'react'
import { Sun, Moon } from 'lucide-react'
function getInitialTheme() {
if (typeof window === 'undefined') return false
const stored = localStorage.getItem('theme')
if (stored) return stored === 'dark'
return window.matchMedia('(prefers-color-scheme: dark)').matches
}
export default function ThemeToggle() {
const [dark, setDark] = useState(getInitialTheme)
useEffect(() => {
const root = document.documentElement
if (dark) root.classList.add('dark')
else root.classList.remove('dark')
try { localStorage.setItem('theme', dark ? 'dark' : 'light') } catch {}
}, [dark])
return (
<button
onClick={() => setDark((d) => !d)}
aria-label={dark ? 'Switch to light mode' : 'Switch to dark mode'}
title={dark ? 'Switch to light mode' : 'Switch to dark mode'}
className="fixed top-3 right-3 sm:top-4 sm:right-4 z-50 w-10 h-10 sm:w-11 sm:h-11 rounded-full flex items-center justify-center glass text-slate-600 dark:text-slate-200 shadow-lg hover:scale-105 hover:text-brand-500 dark:hover:text-brand-400 active:scale-95 transition-all"
>
<span className="relative w-[18px] h-[18px]">
<Sun
size={18}
className={`absolute inset-0 transition-all duration-300 ${dark ? 'opacity-100 rotate-0 scale-100' : 'opacity-0 -rotate-90 scale-50'}`}
/>
<Moon
size={18}
className={`absolute inset-0 transition-all duration-300 ${dark ? 'opacity-0 rotate-90 scale-50' : 'opacity-100 rotate-0 scale-100'}`}
/>
</span>
</button>
)
}

View File

@@ -108,24 +108,24 @@ export default function TopicView({ topic, searchQuery }) {
return ( return (
<div className="pb-24"> <div className="pb-24">
<div className="mb-12"> <div className="mb-12">
<h1 className="text-4xl lg:text-5xl font-bold text-slate-900 mb-4 tracking-tight">{topic.name}</h1> <h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-slate-900 dark:text-slate-100 mb-4 tracking-tight">{topic.name}</h1>
<p className="text-lg text-slate-600 max-w-3xl leading-relaxed">{topic.description}</p> <p className="text-base sm:text-lg text-slate-600 dark:text-slate-400 max-w-3xl leading-relaxed">{topic.description}</p>
<div className="mt-4 flex items-center gap-3"> <div className="mt-4 flex items-center gap-3">
<span className={`inline-flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-widest px-3 py-1 rounded-full border ${ <span className={`inline-flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-widest px-3 py-1 rounded-full border ${
topic.type === 'legacy' topic.type === 'legacy'
? 'bg-brand-50 text-brand-600 border-brand-100' ? 'bg-brand-50 dark:bg-brand-500/10 text-brand-600 dark:text-brand-400 border-brand-100 dark:border-brand-500/20'
: 'bg-indigo-50 text-indigo-600 border-indigo-100' : 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border-indigo-100 dark:border-indigo-500/20'
}`}> }`}>
<span className="w-1.5 h-1.5 rounded-full bg-current"></span> <span className="w-1.5 h-1.5 rounded-full bg-current"></span>
{topic.type === 'legacy' ? 'GraphQL API' : 'REST API'} {topic.type === 'legacy' ? 'GraphQL API' : 'REST API'}
</span> </span>
<span className="text-sm text-slate-400">{filtered.length} endpoint{filtered.length !== 1 ? 's' : ''}</span> <span className="text-sm text-slate-400 dark:text-slate-500">{filtered.length} endpoint{filtered.length !== 1 ? 's' : ''}</span>
</div> </div>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<div className="text-center py-20 text-slate-400 text-sm bg-white rounded-2xl border border-slate-200/60"> <div className="text-center py-20 text-slate-400 dark:text-slate-500 text-sm bg-white dark:bg-dark-800 rounded-2xl border border-slate-200/60 dark:border-white/10">
No endpoints match "{searchQuery}". No endpoints match "{searchQuery}".
</div> </div>
) : ( ) : (

View File

@@ -8,6 +8,18 @@
body { body {
@apply bg-slate-50 text-slate-800 font-sans antialiased selection:bg-brand-500/30 selection:text-brand-900; @apply bg-slate-50 text-slate-800 font-sans antialiased selection:bg-brand-500/30 selection:text-brand-900;
} }
.dark body {
@apply bg-dark-900 text-slate-200;
}
html {
color-scheme: light;
}
html.dark {
color-scheme: dark;
}
} }
@layer utilities { @layer utilities {
@@ -15,6 +27,10 @@
@apply bg-white/70 backdrop-blur-md border border-white/40 shadow-glass; @apply bg-white/70 backdrop-blur-md border border-white/40 shadow-glass;
} }
.dark .glass {
@apply bg-dark-800/80 border-white/5 shadow-none;
}
.dark-glass { .dark-glass {
@apply bg-dark-900/90 backdrop-blur-xl border border-white/5; @apply bg-dark-900/90 backdrop-blur-xl border border-white/5;
} }
@@ -40,12 +56,22 @@
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
@apply bg-slate-300; @apply bg-slate-300;
} }
.dark ::-webkit-scrollbar-thumb {
@apply bg-dark-600 rounded-full;
}
.dark ::-webkit-scrollbar-thumb:hover {
@apply bg-dark-700;
}
.bg-grid-pattern { .bg-grid-pattern {
background-image: linear-gradient(to right, #f1f5f9 1px, transparent 1px), background-image: linear-gradient(to right, #f1f5f9 1px, transparent 1px),
linear-gradient(to bottom, #f1f5f9 1px, transparent 1px); linear-gradient(to bottom, #f1f5f9 1px, transparent 1px);
background-size: 40px 40px; background-size: 40px 40px;
} }
.dark .bg-grid-pattern {
background-image: linear-gradient(to right, rgba(255, 255, 255, 0.03) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
}
.animation-delay-2000 { animation-delay: 2s; } .animation-delay-2000 { animation-delay: 2s; }
.animation-delay-4000 { animation-delay: 4s; } .animation-delay-4000 { animation-delay: 4s; }

View File

@@ -1,5 +1,6 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
darkMode: 'class',
content: ['./index.html', './src/**/*.{js,jsx}'], content: ['./index.html', './src/**/*.{js,jsx}'],
theme: { theme: {
extend: { extend: {