initial shell redesign
This commit is contained in:
+17
-20
@@ -1,38 +1,35 @@
|
||||
import { VaultTxModal } from "polkahub"
|
||||
import { Navigate, Route, Routes } from "react-router-dom"
|
||||
import { ConnectionState } from "./components/ConnectionState"
|
||||
import { Accounts } from "./pages/Accounts/Accounts"
|
||||
import { AppShell } from "./pages/AppShell"
|
||||
import { Constants } from "./pages/Constants"
|
||||
import { Explorer } from "./pages/Explorer"
|
||||
import { Extrinsics } from "./pages/Extrinsics"
|
||||
import { Header } from "./pages/Header"
|
||||
import { Metadata } from "./pages/Metadata"
|
||||
import { RpcCalls } from "./pages/RpcCalls"
|
||||
import { RuntimeCalls } from "./pages/RuntimeCalls"
|
||||
import { Storage } from "./pages/Storage"
|
||||
import { Transactions } from "./pages/Transactions"
|
||||
import { ViewFns } from "./pages/ViewFns"
|
||||
import { ConnectionState } from "./components/ConnectionState"
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="w-full h-screen bg-background flex flex-col">
|
||||
<Header />
|
||||
<div className="flex-1 overflow-auto relative">
|
||||
<div className="max-w-(--breakpoint-xl) m-auto">
|
||||
<Routes>
|
||||
<Route path="explorer/*" element={<Explorer />} />
|
||||
<Route path="extrinsics/*" element={<Extrinsics />} />
|
||||
<Route path="storage/*" element={<Storage />} />
|
||||
<Route path="constants/*" element={<Constants />} />
|
||||
<Route path="runtimeCalls/*" element={<RuntimeCalls />} />
|
||||
<Route path="rpcCalls/*" element={<RpcCalls />} />
|
||||
<Route path="metadata/*" element={<Metadata />} />
|
||||
<Route path="accounts/*" element={<Accounts />} />
|
||||
<Route path="viewFns/*" element={<ViewFns />} />
|
||||
<Route path="*" element={<Navigate to="/explorer" replace />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-screen bg-background">
|
||||
<AppShell>
|
||||
<Routes>
|
||||
<Route path="explorer/*" element={<Explorer />} />
|
||||
<Route path="extrinsics/*" element={<Extrinsics />} />
|
||||
<Route path="storage/*" element={<Storage />} />
|
||||
<Route path="constants/*" element={<Constants />} />
|
||||
<Route path="runtimeCalls/*" element={<RuntimeCalls />} />
|
||||
<Route path="rpcCalls/*" element={<RpcCalls />} />
|
||||
<Route path="metadata/*" element={<Metadata />} />
|
||||
<Route path="accounts/*" element={<Accounts />} />
|
||||
<Route path="viewFns/*" element={<ViewFns />} />
|
||||
<Route path="*" element={<Navigate to="/explorer" replace />} />
|
||||
</Routes>
|
||||
</AppShell>
|
||||
<Transactions />
|
||||
<VaultTxModal />
|
||||
<ConnectionState />
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
import SliderToggle from "@/components/Toggle"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Sheet, SheetContent } from "@/components/ui/sheet"
|
||||
import { Link, useNavigate } from "@/hashParams"
|
||||
import { changeTheme, useTheme } from "@/ThemeProvider"
|
||||
import {
|
||||
Blocks,
|
||||
Code2,
|
||||
Database,
|
||||
FileCode2,
|
||||
FileText,
|
||||
Menu,
|
||||
RadioTower,
|
||||
Search,
|
||||
Send,
|
||||
Wallet,
|
||||
} from "lucide-react"
|
||||
import { FC, FormEvent, PropsWithChildren, useState } from "react"
|
||||
import { useLocation } from "react-router-dom"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { NetworkSwitcher } from "./Network/Network"
|
||||
|
||||
type NavigationItem = {
|
||||
path: string
|
||||
label: string
|
||||
icon: FC<{ size?: number; className?: string }>
|
||||
}
|
||||
|
||||
const navigationGroups: Array<{
|
||||
label: string
|
||||
items: NavigationItem[]
|
||||
}> = [
|
||||
{
|
||||
label: "Core",
|
||||
items: [
|
||||
{ path: "/explorer", label: "Explorer", icon: Blocks },
|
||||
{ path: "/storage", label: "Storage", icon: Database },
|
||||
{ path: "/extrinsics", label: "Extrinsics", icon: Send },
|
||||
{ path: "/constants", label: "Constants", icon: FileText },
|
||||
{ path: "/runtimeCalls", label: "Runtime Calls", icon: Code2 },
|
||||
{ path: "/accounts", label: "Accounts", icon: Wallet },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Tools",
|
||||
items: [
|
||||
{ path: "/viewFns", label: "View Functions", icon: FileCode2 },
|
||||
{ path: "/metadata", label: "Metadata", icon: Blocks },
|
||||
{ path: "/rpcCalls", label: "RPC Calls", icon: RadioTower },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const navigationItems = navigationGroups.flatMap((group) => group.items)
|
||||
|
||||
export const AppShell: FC<PropsWithChildren> = ({ children }) => {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full overflow-hidden bg-background text-foreground">
|
||||
<aside className="hidden w-64 shrink-0 border-r bg-card/30 lg:flex">
|
||||
<SidebarContent />
|
||||
</aside>
|
||||
<Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>
|
||||
<SheetContent side="left" className="w-72 bg-background p-0">
|
||||
<SidebarContent onNavigate={() => setSidebarOpen(false)} mobile />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<TopBar onOpenSidebar={() => setSidebarOpen(true)} />
|
||||
<div className="relative min-h-0 flex-1 overflow-auto">
|
||||
<div className="mx-auto h-full w-full max-w-(--breakpoint-xl)">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarContent: FC<{ mobile?: boolean; onNavigate?: () => void }> = ({
|
||||
mobile,
|
||||
onNavigate,
|
||||
}) => (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="flex h-16 shrink-0 items-center gap-3 border-b px-4">
|
||||
<img
|
||||
className="h-10 w-10 shrink-0 hidden dark:inline-block"
|
||||
src="/papi_logo-dark.svg"
|
||||
alt="papi logo"
|
||||
/>
|
||||
<img
|
||||
className="h-10 w-10 shrink-0 dark:hidden"
|
||||
src="/papi_logo-light.svg"
|
||||
alt="papi logo"
|
||||
/>
|
||||
<div className="min-w-0 leading-tight">
|
||||
<div className="truncate text-base">
|
||||
<span className="poppins-regular">papi</span>{" "}
|
||||
<span className="poppins-extralight">console</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">beta</div>
|
||||
</div>
|
||||
</div>
|
||||
{mobile ? (
|
||||
<div className="border-b px-3 py-3">
|
||||
<NetworkSwitcher forSmallScreen className="flex w-full md:hidden" />
|
||||
</div>
|
||||
) : null}
|
||||
<nav className="flex-1 overflow-y-auto px-3 py-4">
|
||||
{navigationGroups.map((group) => (
|
||||
<div key={group.label} className="mb-5 last:mb-0">
|
||||
<div className="px-3 pb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{group.label}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{group.items.map((item) => (
|
||||
<SidebarLink key={item.path} item={item} onClick={onNavigate} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
<div className="shrink-0 border-t p-3">
|
||||
<ThemeToggle />
|
||||
<a
|
||||
href="https://github.com/polkadot-api/papi-console"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mt-2 flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<FileCode2 size={16} />
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const TopBar: FC<{ onOpenSidebar: () => void }> = ({ onOpenSidebar }) => {
|
||||
const location = useLocation()
|
||||
const activeItem = getActiveNavigationItem(location.pathname)
|
||||
|
||||
return (
|
||||
<header className="flex h-16 shrink-0 items-center border-b bg-background/95 px-3 lg:px-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="mr-2 shrink-0 text-foreground hover:bg-accent lg:hidden"
|
||||
onClick={onOpenSidebar}
|
||||
aria-label="Open navigation"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
<div className="mr-4 hidden min-w-36 flex-col leading-tight xl:flex">
|
||||
<span className="text-xs text-muted-foreground">Section</span>
|
||||
<span className="font-medium">{activeItem.label}</span>
|
||||
</div>
|
||||
<GlobalJumpSearch />
|
||||
<div className="ml-3 hidden sm:block md:hidden">
|
||||
<NetworkSwitcher forSmallScreen className="w-45" />
|
||||
</div>
|
||||
<div className="ml-3 hidden md:block">
|
||||
<NetworkSwitcher className="w-55" />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
const GlobalJumpSearch = () => {
|
||||
const navigate = useNavigate()
|
||||
const [value, setValue] = useState("")
|
||||
|
||||
const handleSubmit = (evt: FormEvent) => {
|
||||
evt.preventDefault()
|
||||
|
||||
const target = getJumpTarget(value)
|
||||
if (!target) return
|
||||
|
||||
navigate(target)
|
||||
setValue("")
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="min-w-0 flex-1" onSubmit={handleSubmit}>
|
||||
<label className="flex h-9 min-w-0 max-w-2xl items-center gap-2 rounded-md border bg-input px-3 text-sm focus-within:ring-2 focus-within:ring-ring">
|
||||
<Search size={16} className="shrink-0 text-muted-foreground" />
|
||||
<input
|
||||
value={value}
|
||||
onChange={(evt) => setValue(evt.target.value)}
|
||||
className="min-w-0 flex-1 bg-transparent outline-hidden placeholder:text-muted-foreground"
|
||||
placeholder="Jump to block, hash, or section"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</label>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarLink: FC<{ item: NavigationItem; onClick?: () => void }> = ({
|
||||
item,
|
||||
onClick,
|
||||
}) => {
|
||||
const location = useLocation()
|
||||
const active = isNavigationItemActive(location.pathname, item.path)
|
||||
const Icon = item.icon
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={item.path}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={twMerge(
|
||||
"flex h-9 items-center gap-3 rounded-md px-3 text-sm transition-colors",
|
||||
"text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
||||
"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring",
|
||||
active && "bg-accent text-accent-foreground font-medium",
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon size={16} className="shrink-0" />
|
||||
<span className="truncate">{item.label}</span>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const getActiveNavigationItem = (pathname: string) =>
|
||||
navigationItems.find((item) => isNavigationItemActive(pathname, item.path)) ??
|
||||
navigationItems[0]
|
||||
|
||||
const isNavigationItemActive = (pathname: string, path: string) =>
|
||||
pathname === path || pathname.startsWith(`${path}/`)
|
||||
|
||||
const getJumpTarget = (value: string) => {
|
||||
const query = value.trim()
|
||||
if (!query) return null
|
||||
|
||||
if (query.startsWith("/")) return query
|
||||
|
||||
const normalizedQuery = normalizeLabel(query)
|
||||
const exactSection = navigationItems.find(
|
||||
({ label }) => normalizeLabel(label) === normalizedQuery,
|
||||
)
|
||||
if (exactSection) return exactSection.path
|
||||
|
||||
const matchingSection = navigationItems.find(({ label }) =>
|
||||
normalizeLabel(label).startsWith(normalizedQuery),
|
||||
)
|
||||
if (matchingSection) return matchingSection.path
|
||||
|
||||
if (/^(0x[0-9a-f]+|\d+)$/i.test(query)) {
|
||||
return `/explorer/${query}`
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const normalizeLabel = (value: string) =>
|
||||
value.toLowerCase().replace(/[\s_-]/g, "")
|
||||
|
||||
const ThemeToggle = () => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<label className="flex items-center justify-between rounded-md px-3 py-2 text-sm text-muted-foreground">
|
||||
<span>Dark mode</span>
|
||||
<SliderToggle
|
||||
isToggled={theme === "dark"}
|
||||
toggle={() => changeTheme(theme === "dark" ? "light" : "dark")}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
|
||||
import { Link } from "@/hashParams"
|
||||
import { Menu } from "lucide-react"
|
||||
import { FC, PropsWithChildren, useState } from "react"
|
||||
import { useLocation } from "react-router-dom"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { NetworkSwitcher } from "./Network/Network"
|
||||
import SliderToggle from "@/components/Toggle"
|
||||
import { changeTheme, useTheme } from "@/ThemeProvider"
|
||||
|
||||
const navigationItems = [
|
||||
{ path: "/explorer", label: "Explorer", important: true },
|
||||
{ path: "/storage", label: "Storage", important: true },
|
||||
{ path: "/extrinsics", label: "Extrinsics", important: true },
|
||||
{ path: "/constants", label: "Constants", important: false },
|
||||
{ path: "/runtimeCalls", label: "Runtime Calls", important: true },
|
||||
{ path: "/accounts", label: "Accounts", important: false },
|
||||
]
|
||||
|
||||
export const Header = () => {
|
||||
const [open, setIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="shrink-0 border-b">
|
||||
<div className="flex p-4 pb-2 items-center gap-2 max-w-(--breakpoint-xl) m-auto">
|
||||
<div className="flex items-center flex-row gap-2 relative mr-2">
|
||||
<img
|
||||
className="w-14 min-w-14 hidden dark:inline-block"
|
||||
src="/papi_logo-dark.svg"
|
||||
alt="papi-logo"
|
||||
/>
|
||||
<img
|
||||
className="w-14 min-w-14 dark:hidden"
|
||||
src="/papi_logo-light.svg"
|
||||
alt="papi-logo"
|
||||
/>
|
||||
<h1 className="hidden min-[72rem]:block poppins-regular text-lg">
|
||||
papi <span className="poppins-extralight">console</span>
|
||||
</h1>
|
||||
<div className="absolute -bottom-1 left-0 min-[72rem]:bottom-0 min-[72rem]:right-1 text-right text-sm">
|
||||
(beta)
|
||||
</div>
|
||||
</div>
|
||||
<NetworkSwitcher />
|
||||
<div className="flex-1" />
|
||||
<nav className="flex flex-row items-center justify-end px-1 py-1 text-nowrap flex-wrap min-w-56">
|
||||
{navigationItems.map(({ path, label, important }) => (
|
||||
<NavLink
|
||||
to={path}
|
||||
key={path}
|
||||
className={
|
||||
important ? "text-sm sm:text-base" : "hidden lg:inline-block"
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
<Sheet open={open} onOpenChange={setIsOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-foreground hover:bg-accent"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="right" className="bg-background w-64 pt-10">
|
||||
<NetworkSwitcher forSmallScreen />
|
||||
<nav className="flex flex-col gap-4 lg:hidden">
|
||||
{navigationItems
|
||||
.filter(({ important }) => !important)
|
||||
.map(({ path, label }) => (
|
||||
<NavLink
|
||||
to={path}
|
||||
key={path}
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
{label}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
<NavLink to="/viewFns" onClick={() => setIsOpen(false)}>
|
||||
View Functions
|
||||
</NavLink>
|
||||
<NavLink to="/metadata" onClick={() => setIsOpen(false)}>
|
||||
Metadata
|
||||
</NavLink>
|
||||
<NavLink to="/rpcCalls" onClick={() => setIsOpen(false)}>
|
||||
RPC Calls
|
||||
</NavLink>
|
||||
<hr />
|
||||
<ThemeToggle />
|
||||
<div className="grow" />
|
||||
<div className="border-t p-2 text-right">
|
||||
<a
|
||||
href="https://github.com/polkadot-api/papi-console"
|
||||
target="_blank"
|
||||
>
|
||||
github
|
||||
</a>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NavLink: FC<
|
||||
PropsWithChildren<{ to: string; onClick?: () => void; className?: string }>
|
||||
> = ({ to, children, className, onClick }) => {
|
||||
const location = useLocation()
|
||||
const active = location.pathname.startsWith(to)
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
className={twMerge(
|
||||
"transition-colors text-foreground/75 hover:text-foreground cursor-pointer px-3 py-1 rounded",
|
||||
"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring",
|
||||
active && "text-foreground font-bold",
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const ThemeToggle = () => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<label className="flex items-center justify-between px-4">
|
||||
<div>Dark mode</div>
|
||||
<SliderToggle
|
||||
isToggled={theme === "dark"}
|
||||
toggle={() => changeTheme(theme === "dark" ? "light" : "dark")}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
@@ -37,8 +37,10 @@ import { FC, useState } from "react"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function NetworkSwitcher({
|
||||
className,
|
||||
forSmallScreen,
|
||||
}: {
|
||||
className?: string
|
||||
forSmallScreen?: boolean
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
@@ -93,6 +95,7 @@ export function NetworkSwitcher({
|
||||
className={twMerge(
|
||||
"w-[200px] gap-0 justify-between text-base px-3 border border-border bg-input self-center",
|
||||
forSmallScreen ? "flex md:hidden" : "hidden md:flex",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span className="overflow-hidden text-ellipsis">
|
||||
|
||||
Reference in New Issue
Block a user