From 566d59b8d3504760e78ed213ef045ce9a83beaf4 Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Thu, 14 May 2026 15:55:21 +0200 Subject: [PATCH 1/5] initial shell redesign --- src/App.tsx | 37 +++-- src/pages/AppShell.tsx | 272 ++++++++++++++++++++++++++++++++++ src/pages/Header.tsx | 146 ------------------ src/pages/Network/Network.tsx | 3 + 4 files changed, 292 insertions(+), 166 deletions(-) create mode 100644 src/pages/AppShell.tsx delete mode 100644 src/pages/Header.tsx diff --git a/src/App.tsx b/src/App.tsx index bcee967..56ed4bd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( -
-
-
-
- - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - -
-
+
+ + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + diff --git a/src/pages/AppShell.tsx b/src/pages/AppShell.tsx new file mode 100644 index 0000000..c5c3f97 --- /dev/null +++ b/src/pages/AppShell.tsx @@ -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 = ({ children }) => { + const [sidebarOpen, setSidebarOpen] = useState(false) + + return ( +
+ + + + setSidebarOpen(false)} mobile /> + + +
+ setSidebarOpen(true)} /> +
+
+ {children} +
+
+
+
+ ) +} + +const SidebarContent: FC<{ mobile?: boolean; onNavigate?: () => void }> = ({ + mobile, + onNavigate, +}) => ( +
+
+ papi logo + papi logo +
+
+ papi{" "} + console +
+
beta
+
+
+ {mobile ? ( +
+ +
+ ) : null} + + +
+) + +const TopBar: FC<{ onOpenSidebar: () => void }> = ({ onOpenSidebar }) => { + const location = useLocation() + const activeItem = getActiveNavigationItem(location.pathname) + + return ( +
+ +
+ Section + {activeItem.label} +
+ +
+ +
+
+ +
+
+ ) +} + +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 ( +
+ +
+ ) +} + +const SidebarLink: FC<{ item: NavigationItem; onClick?: () => void }> = ({ + item, + onClick, +}) => { + const location = useLocation() + const active = isNavigationItemActive(location.pathname, item.path) + const Icon = item.icon + + return ( + + + {item.label} + + ) +} + +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 ( + + ) +} diff --git a/src/pages/Header.tsx b/src/pages/Header.tsx deleted file mode 100644 index f851163..0000000 --- a/src/pages/Header.tsx +++ /dev/null @@ -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 ( -
-
-
- papi-logo - papi-logo -

- papi console -

-
- (beta) -
-
- -
- - - - - - - - - setIsOpen(false)}> - View Functions - - setIsOpen(false)}> - Metadata - - setIsOpen(false)}> - RPC Calls - -
- -
- - - -
-
- ) -} - -const NavLink: FC< - PropsWithChildren<{ to: string; onClick?: () => void; className?: string }> -> = ({ to, children, className, onClick }) => { - const location = useLocation() - const active = location.pathname.startsWith(to) - - return ( - - {children} - - ) -} - -const ThemeToggle = () => { - const theme = useTheme() - - return ( - - ) -} diff --git a/src/pages/Network/Network.tsx b/src/pages/Network/Network.tsx index 59891a1..15a6b61 100644 --- a/src/pages/Network/Network.tsx +++ b/src/pages/Network/Network.tsx @@ -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, )} > From f626692b9a8882967aeba19e1b76a74bb46639d1 Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Thu, 14 May 2026 16:08:15 +0200 Subject: [PATCH 2/5] Align header height, reorder elements --- src/pages/AppShell.tsx | 44 ++++++++++++----------------------- src/pages/Network/Network.tsx | 15 ++++-------- 2 files changed, 19 insertions(+), 40 deletions(-) diff --git a/src/pages/AppShell.tsx b/src/pages/AppShell.tsx index c5c3f97..999df9f 100644 --- a/src/pages/AppShell.tsx +++ b/src/pages/AppShell.tsx @@ -31,24 +31,22 @@ const navigationGroups: Array<{ items: NavigationItem[] }> = [ { - label: "Core", + label: "Tools", 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: "/constants", label: "Constants", icon: FileText }, { path: "/metadata", label: "Metadata", icon: Blocks }, { path: "/rpcCalls", label: "RPC Calls", icon: RadioTower }, ], }, + { + label: "Utils", + items: [{ path: "/accounts", label: "Accounts", icon: Wallet }], + }, ] const navigationItems = navigationGroups.flatMap((group) => group.items) @@ -104,7 +102,7 @@ const SidebarContent: FC<{ mobile?: boolean; onNavigate?: () => void }> = ({
{mobile ? (
- +
) : null}
@@ -248,7 +296,10 @@ const ThemeToggle = () => { return (
) } -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 ( - - - - ) -} - const SidebarLink: FC<{ item: NavigationItem; onClick?: () => void }> = ({ item, onClick, @@ -264,33 +231,6 @@ const SidebarLink: FC<{ item: NavigationItem; onClick?: () => void }> = ({ 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() diff --git a/src/pages/Explorer/BlockTable.tsx b/src/pages/Explorer/BlockTable.tsx index f3e5d40..2b37698 100644 --- a/src/pages/Explorer/BlockTable.tsx +++ b/src/pages/Explorer/BlockTable.tsx @@ -1,12 +1,9 @@ import { CopyText } from "@/components/Copy" import { Popover } from "@/components/Popover" -import { Button } from "@/components/ui/button" -import { SearchInput } from "@/components/ui/search-input" -import { Link, useNavigate } from "@/hashParams" +import { Link } from "@/hashParams" import { BlockInfo, blocksByHeight$, finalized$ } from "@/state/block.state" import { client$ } from "@/state/chains/chain.state" import { state, useStateObservable } from "@react-rxjs/core" -import { Search } from "lucide-react" import { FC } from "react" import { combineLatest, debounceTime, map, switchMap } from "rxjs" import { twMerge } from "tailwind-merge" @@ -87,36 +84,6 @@ const blockTable$ = state( [], ) -export const BlockInput: FC = () => { - const navigate = useNavigate() - return ( -
{ - const blockTarget = new FormData(e.currentTarget).get( - "blockTarget", - ) as string - const isHex = blockTarget?.match(/^0[xX][0-9a-fA-F]+$/) - if ( - (isHex && blockTarget.length === 66) || - (!isHex && !Number.isNaN(Number(blockTarget))) - ) { - navigate(blockTarget) - } - e.preventDefault() - e.stopPropagation() - }} - > -
- -
- -
- ) -} - export const BlockTable = () => { const rows = useStateObservable(blockTable$) const finalized = useStateObservable(finalized$) @@ -133,7 +100,7 @@ export const BlockTable = () => { return ( - }>Recent Blocks + Recent Blocks {rows.map((row, i) => ( > = ({ - children, - search, -}) => ( +export const Title: FC = ({ children }) => (

{children} - {search}

) From c619c777711e062a8f2a09bd4995991e20e2c567 Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Thu, 14 May 2026 16:59:41 +0200 Subject: [PATCH 5/5] reuse NavigationItem --- src/components/GlobalCommandPalette.tsx | 10 +++++----- src/pages/AppShell.tsx | 6 ++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/GlobalCommandPalette.tsx b/src/components/GlobalCommandPalette.tsx index 1cda989..e7e11cd 100644 --- a/src/components/GlobalCommandPalette.tsx +++ b/src/components/GlobalCommandPalette.tsx @@ -13,7 +13,7 @@ import { twMerge } from "tailwind-merge" type CommandPaletteIcon = ComponentType<{ size?: number; className?: string }> -export type CommandPaletteNavigationItem = { +export type NavigationItem = { path: string label: string icon: CommandPaletteIcon @@ -28,7 +28,7 @@ type CommandAction = { } export const GlobalCommandPalette: FC<{ - navigationItems: CommandPaletteNavigationItem[] + navigationItems: NavigationItem[] }> = ({ navigationItems }) => { const navigate = useNavigate() const [value, setValue] = useState("") @@ -156,7 +156,7 @@ const CommandActionItem: FC<{ const getCommandActions = ( value: string, - navigationItems: CommandPaletteNavigationItem[], + navigationItems: NavigationItem[], ) => { const query = value.trim() const primaryActions: CommandAction[] = [] @@ -195,7 +195,7 @@ const getCommandActions = ( const getSectionActions = ( query: string, - navigationItems: CommandPaletteNavigationItem[], + navigationItems: NavigationItem[], ): CommandAction[] => { const normalizedQuery = normalizeLabel(query) const items = !normalizedQuery @@ -231,5 +231,5 @@ const normalizeLabel = (value: string) => const getCommandShortcutLabel = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform) - ? "⌘K" + ? "⌘ K" : "Ctrl K" diff --git a/src/pages/AppShell.tsx b/src/pages/AppShell.tsx index 1768b7c..09680e7 100644 --- a/src/pages/AppShell.tsx +++ b/src/pages/AppShell.tsx @@ -1,6 +1,6 @@ import { GlobalCommandPalette, - type CommandPaletteNavigationItem, + type NavigationItem, } from "@/components/GlobalCommandPalette" import SliderToggle from "@/components/Toggle" import { GithubIcon } from "@/components/Icons" @@ -26,8 +26,6 @@ import { useLocation } from "react-router-dom" import { twMerge } from "tailwind-merge" import { NetworkSwitcher } from "./Network/Network" -type NavigationItem = CommandPaletteNavigationItem - const navigationGroups: Array<{ label: string items: NavigationItem[] @@ -102,7 +100,7 @@ export const AppShell: FC = ({ children }) => { return (
-