npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@alsocoder/apna-panel

v0.1.0

Published

React admin panel framework with permissions, navigation, themes, and layout shell for the Alsocoder ecosystem.

Readme

@alsocoder/apna-panel

React admin panel shell framework for the Alsocoder ecosystem — layout, sidebar, header, RBAC, themes, JSON dashboards, plugins, and mobile navigation.

Works with @alsocoder/apna-panel-react-router, @alsocoder/apna-panel-auth, @alsocoder/apna-crud, and other ecosystem packages.


Install

npm install @alsocoder/apna-panel @alsocoder/apna-panel-react-router react-router-dom

Optional:

npm install @alsocoder/apna-panel-auth   # login page + session gate
npm install @alsocoder/apna-crud         # CRUD pages inside panel

Peer dependencies: react, react-dom (>= 18).

import "@alsocoder/apna-panel/styles.css"

Full app example (Panel + CRUD + Dashboard)

import { Routes, Route } from "react-router-dom"
import {
  ApnaPanelShell,
  ApnaPanelHomePage,
  ApnaPanelWidgetRegistryProvider,
  ApnaPanelForbidden,
  ApnaPanelNotFound,
  definePanelConfig,
  defineMenu,
  defineDashboard,
  defineDashboardWidgets,
  defaultIcons,
} from "@alsocoder/apna-panel"
import {
  ApnaPanelRouterProvider,
  ApnaPanelRoute,
  useApnaPanelPluginRouteElements,
} from "@alsocoder/apna-panel-react-router"
import { ApnaCrud } from "@alsocoder/apna-crud"
import "@alsocoder/apna-crud/styles.css"

// ── Auth adapter ──────────────────────────────────────────────
const authAdapter = {
  async getSession() {
    return { user: { id: "1", name: "Admin", roles: ["admin"], permissions: [] } }
  },
  async login(credentials) {
    // call your API, return session
    return { user: { id: "1", name: "Admin", roles: ["admin"], permissions: [] } }
  },
  async logout() {},
}

// ── Dashboard widgets ─────────────────────────────────────────
const dashboard = defineDashboard({
  sections: [
    {
      type: "stats",
      id: "kpis",
      items: [
        { id: "users", title: "Users", value: "1,248", description: "Total", icon: "users", trend: { value: "+12%", positive: true } },
        { id: "orders", title: "Orders", value: "89", description: "This week", icon: "folder" },
      ],
    },
    {
      type: "row",
      id: "charts",
      spans: [8, 4],
      children: [
        { type: "slot", id: "chart", widget: "areaChart", props: { title: "Traffic" } },
        { type: "slot", id: "list", widget: "recentList", props: { title: "Recent" } },
      ],
    },
  ],
})

const widgets = defineDashboardWidgets({
  areaChart: MyAreaChart,
  recentList: MyRecentList,
})

// ── Panel config ──────────────────────────────────────────────
const config = definePanelConfig({
  appName: "My Admin",
  homeHref: "/",
  dashboard,
  layout: { default: "vertical", persistSidebar: true },
  header: { showSearch: true, showNotifications: true, showUserMenu: true },
  main: { showBreadcrumbs: true },
  rolePermissions: {
    admin: ["dashboard.view", "users.view", "users.create", "users.update", "users.delete"],
    editor: ["dashboard.view", "users.view"],
  },
  superAdminRoles: ["superadmin"],
  menu: defineMenu([
    { id: "dashboard", label: "Dashboard", href: "/", icon: defaultIcons.dashboard, permission: "dashboard.view" },
    {
      id: "management",
      type: "group",
      label: "Management",
      children: [
        { id: "users", label: "Users", href: "/users", icon: defaultIcons.users, permission: "users.view" },
      ],
    },
  ]),
  notifications: {
    fetch: async () => [
      { id: "1", title: "Welcome", description: "Panel loaded", read: false },
    ],
    maxItems: 20,
  },
  userMenuItems: [
    { id: "profile", label: "Profile", type: "action", onClick: () => {} },
  ],
  onUnauthorized: () => console.warn("Access denied"),
})

// ── Routes (inside provider) ────────────────────────────────────
function PanelRoutes() {
  const pluginRoutes = useApnaPanelPluginRouteElements()
  return (
    <Routes>
      <Route
        path="/"
        element={
          <ApnaPanelRoute permissions="dashboard.view" redirectTo="/forbidden">
            <ApnaPanelHomePage />
          </ApnaPanelRoute>
        }
      />
      <Route
        path="/users/*"
        element={
          <ApnaPanelRoute permissions="users.view" fallback={<ApnaPanelForbidden />}>
            <ApnaCrud
              formMode="page"
              basePath="/users"
              name="users"
              title="Users"
              endpoint="/api/users"
              baseUrl={import.meta.env.VITE_API_URL}
              formFields={[
                { type: "input", name: "name", label: "Name" },
                { type: "input", name: "email", label: "Email", props: { format: "email" } },
              ]}
              permissions={{ list: "users.view", create: "users.create", update: "users.update", delete: "users.delete" }}
            />
          </ApnaPanelRoute>
        }
      />
      {pluginRoutes}
      <Route path="/forbidden" element={<ApnaPanelForbidden />} />
      <Route path="*" element={<ApnaPanelNotFound />} />
    </Routes>
  )
}

// ── App root ────────────────────────────────────────────────────
export default function App() {
  return (
    <ApnaPanelWidgetRegistryProvider widgets={widgets}>
      <ApnaPanelRouterProvider config={config} authAdapter={authAdapter}>
        <ApnaPanelShell
          slots={{
            headerBeforeProfile: <WalletBadge />,
            sidebarFooter: (ctx) => (ctx.compact ? <SupportIcon /> : <SupportCard />),
          }}
        >
          <PanelRoutes />
        </ApnaPanelShell>
      </ApnaPanelRouterProvider>
    </ApnaPanelWidgetRegistryProvider>
  )
}

definePanelConfig — all options

| Option | Type | Description | |--------|------|-------------| | appName | string | App title (breadcrumbs, brand fallback) | | brand | ApnaPanelBrand | title, subtitle, logoMark, logoFull, href | | homeHref | string | Home link for breadcrumbs (default /) | | layout | ApnaPanelLayoutConfig | See layout section | | dashboard | ApnaPanelDashboardConfig | JSON dashboard used by ApnaPanelHomePage | | mobileNav | ApnaPanelMobileNavConfig | Bottom nav order, moreLabel, maxVisible | | themes | ApnaPanelTheme[] | Custom themes with cssVars | | defaultThemeId | string | Initial theme id | | rtl | boolean | Initial RTL direction | | enableShortcuts | boolean | Keyboard shortcuts | | shortcuts | ApnaPanelShortcut[] | { id, keys, label, action } — built-in: openSearch, toggleSidebar | | menu | ApnaPanelMenuItem[] | Sidebar / nav items | | menuResolver | fn | Async dynamic menu: (ctx) => items \| Promise<items> | | rolePermissions | Record<role, permissions[]> | Role → permission map | | superAdminRoles | string[] | Roles that bypass all permission checks | | resolvePermissions | fn | Custom permission resolver from user + role map | | plugins | ApnaPanelPlugin[] | Plugin modules (menu, routes, slots) | | widgets | ApnaPanelWidget[] | Header/sidebar widget registrations | | headerActions | ApnaPanelHeaderAction[] | Extra header buttons | | slots | ApnaPanelLayoutSlots | Static slot content (overridden by shell slots prop) | | header | ApnaPanelHeaderOptions | showSearch, showUserMenu, showNotifications, searchPlaceholder | | main | ApnaPanelMainOptions | showBreadcrumbs (default true) | | sidebar | ApnaPanelSidebarOptions | showBrand | | userMenuItems | ApnaPanelMenuItem[] | Extra user dropdown items | | notifications | { fetch, maxItems } | Notification fetcher for header bell | | onUnauthorized | fn | Called when route guard denies access | | onSessionChange | fn | Called when auth session changes | | classNames | ApnaPanelClassNames | CSS class overrides | | icons | ApnaPanelIcons | Icon overrides (users, bell, dashboard, etc.) |


Menu items (defineMenu)

defineMenu([
  // Simple link
  { id: "home", label: "Dashboard", href: "/", icon: defaultIcons.dashboard, permission: "dashboard.view" },

  // Group with children
  {
    id: "settings-group",
    type: "group",
    label: "Settings",
    children: [
      { id: "general", label: "General", href: "/settings" },
      { id: "team", label: "Team", href: "/settings/team", permissionAny: ["team.view", "admin"] },
    ],
  },

  // Divider
  { id: "div-1", type: "divider" },

  // External link
  { id: "docs", label: "Docs", href: "https://docs.example.com", external: true },

  // Action (no navigation)
  { id: "support", label: "Get support", type: "action", onClick: () => openChat() },

  // Hidden dynamically
  { id: "beta", label: "Beta", href: "/beta", hidden: (ctx) => !ctx.can("beta.view") },
])

| Menu field | Type | Description | |------------|------|-------------| | id | string | Unique id (used for active state, mobile nav order) | | type | "link" \| "group" \| "divider" \| "external" \| "action" | Item type | | label | string | Display text | | href | string | Route path | | icon | ReactNode | Sidebar icon | | badge | string \| number | Badge on menu item | | permission | string | Required permission | | permissionAny | string[] | Any of these permissions | | permissionAll | string[] | All of these permissions | | roles / rolesAny | string[] | Role-based visibility | | exact | boolean | Exact path match for active state | | defaultOpen | boolean | Open nested group by default | | children | ApnaPanelMenuItem[] | Nested items | | hidden | boolean \| fn | Hide item |


Layout modes

layout: {
  default: "vertical",    // sidebar left (default)
  // "horizontal"  — top nav bar, no sidebar
  // "compact"     — icon-only sidebar with flyout
  persistSidebar: true,   // save collapsed state in localStorage
  defaultCollapsed: false,
}

| Mode | Behavior | |------|----------| | vertical | Standard left sidebar | | horizontal | Sidebar hidden; ApnaPanelHorizontalNav in header | | compact | Icon sidebar + flyout menus | | hybrid | Reserved for future use |

Responsive behavior:

  • ≤768px — sidebar hidden, tablet drawer
  • ≤475px — bottom mobile nav

Dashboard (JSON config)

Section types

defineDashboard({
  sections: [
    // KPI stat cards
    {
      type: "stats",
      id: "kpis",
      span: 12,
      items: [
        {
          id: "users",
          title: "Users",
          value: "1,248",
          description: "Total registered",
          trend: { value: "+12%", positive: true },
          icon: "users",           // key from mergedIcons
          permission: "dashboard.view",
          span: 3,
        },
      ],
    },

    // Custom widget slot
    {
      type: "slot",
      id: "header",
      widget: "pageHeader",
      span: 12,
      props: { title: "Overview", subtitle: "Last 30 days" },
    },

    // Row with column spans (12-col grid)
    {
      type: "row",
      id: "charts",
      spans: [8, 4],
      children: [
        { type: "slot", id: "chart", widget: "areaChart", props: { title: "Traffic" } },
        { type: "slot", id: "bars", widget: "barChart" },
      ],
    },

    // Nested grid
    {
      type: "grid",
      id: "lists",
      columns: 2,
      children: [
        { type: "slot", id: "recent", widget: "recentList" },
        { type: "slot", id: "activity", widget: "activityList" },
      ],
    },
  ],
})

Register widgets + render

const widgets = defineDashboardWidgets({
  pageHeader: ({ title, subtitle }) => <h1>{title}</h1>,
  areaChart: AreaChartWidget,
})

<ApnaPanelWidgetRegistryProvider widgets={widgets}>
  <ApnaPanelDashboardRenderer config={dashboard} />
  {/* or */}
  <ApnaPanelHomePage />  {/* reads config.dashboard + plugin slots */}
</ApnaPanelWidgetRegistryProvider>

ApnaPanelStatCard (manual JSX)

<ApnaPanelStatCard
  title="Revenue"
  value="$12.4k"
  description="This month"
  trend={{ value: "+5%", positive: true }}
  icon={defaultIcons.dashboard}
/>

Plugins

Extend the panel without editing core routes/menu:

import { definePlugin } from "@alsocoder/apna-panel"
import { useApnaPanelPluginRouteElements } from "@alsocoder/apna-panel-react-router"

const billingPlugin = definePlugin({
  id: "billing",
  setup(api) {
    api.registerMenuItem({ id: "billing", label: "Billing", href: "/billing", permission: "billing.view" })
    api.registerRoute({
      id: "billing-route",
      path: "/billing",
      element: <BillingPage />,
      permissions: "billing.view",
      redirectTo: "/forbidden",
    })
    api.registerWidget({
      id: "billing-widget",
      area: "headerBeforeProfile",
      permission: "billing.view",
      order: 10,
      render: () => <BillingBadge />,
    })
    api.registerHeaderAction({
      id: "billing-action",
      order: 5,
      render: () => <button>New invoice</button>,
    })
    api.registerSlot({
      id: "billing-promo",
      slot: "before-dashboard",
      render: () => <PromoBanner />,
    })
  },
})

// config
plugins: [billingPlugin]

// routes — spread inside <Routes>, before catch-all *
function PanelRoutes() {
  const pluginRoutes = useApnaPanelPluginRouteElements()
  return (
    <Routes>
      {/* your routes */}
      {pluginRoutes}
      <Route path="*" element={<ApnaPanelNotFound />} />
    </Routes>
  )
}

Plugin slot names: before-dashboard, after-dashboard (or any custom string with <ApnaPanelPluginSlot name="..." />).


Slots (header / sidebar customization)

Priority: ApnaPanelShell slots prop > config.slots > defaults.

<ApnaPanelShell
  slots={{
    headerStart: <OrgSwitcher />,
    headerBeforeSearch: <QuickActions />,
    headerAfterSearch: <FilterToggle />,
    headerBeforeProfile: <WalletBalance />,
    headerEnd: <ThemeToggle />,
    headerSearch: null,          // hide default search
    headerProfile: <CustomUserMenu />,
    sidebarHeader: null,         // hide default brand
    sidebarFooter: (ctx) =>       // function receives layout context
      ctx.compact ? <SupportIcon /> : <SupportCard />,
  }}
>

| Slot area | Position | |-----------|----------| | headerStart | Left of search | | headerBeforeSearch | Before search input | | headerAfterSearch | After search input | | headerBeforeProfile | Before user menu | | headerEnd | Far right | | headerSearch | Replace search (null = hide) | | headerProfile | Replace user menu (null = hide) | | sidebarHeader | Replace sidebar brand | | sidebarFooter | Sidebar bottom |

Widget areas (via registerWidget or config.widgets): same header areas + sidebarFooter.


Auth & permissions

Auth adapter

const authAdapter: ApnaPanelAuthAdapter = {
  getSession: () => fetch("/api/me").then(r => r.json()),
  login: (credentials) => fetch("/api/login", { method: "POST", body: JSON.stringify(credentials) }).then(r => r.json()),
  logout: () => fetch("/api/logout", { method: "POST" }),
  refresh: () => fetch("/api/refresh").then(r => r.json()),
}

<ApnaPanelRouterProvider config={config} authAdapter={authAdapter}>

Route guards

import { ApnaPanelRoute, ApnaPanelRouteGuard } from "@alsocoder/apna-panel-react-router"

<ApnaPanelRoute
  permissions={["users.view", "users.create"]}  // all required
  permissionAny={["admin", "superadmin"]}
  roles="admin"
  redirectTo="/forbidden"
  fallback={<ApnaPanelForbidden />}
>
  <UsersPage />
</ApnaPanelRoute>

// Guest-only (login page)
<ApnaPanelRoute guestOnly redirectTo="/">
  <LoginPage />
</ApnaPanelRoute>

Conditional UI

import { Can, CanAny, CanAll, CanNot } from "@alsocoder/apna-panel"

<Can permission="users.create">
  <button>Add user</button>
</Can>

<CanAny permissions={["reports.view", "analytics.view"]} fallback={<span>No access</span>}>
  <ReportsLink />
</CanAny>

Hooks

| Hook | Returns | |------|---------| | useApnaPanel() | config, layoutMode, setLayoutMode, sidebarCollapsed, toggleSidebar, registerWidget, etc. | | useApnaPanelAuth() | session, isAuthenticated, isLoading, login, logout | | useApnaPanelPermissions() | can, canAny, canAll, roles, isSuperAdmin | | useApnaPanelNavigation() | menu, activeItem, breadcrumbs, setBreadcrumbs, navigate, pathname | | useApnaPanelTheme() | themeId, setThemeId, mode, setMode, rtl, setRtl, toggleMode | | useApnaPanelNotifications() | items, unreadCount, markRead, refresh | | useApnaPanelBreadcrumbs() | setItems | | useApnaPanelBreakpoint() | isMobileNav, isTablet |


Auth pages (@alsocoder/apna-panel-auth)

npm install @alsocoder/apna-panel-auth @alsocoder/apna-input
import "@alsocoder/apna-panel-auth/styles.css"
import { ApnaPanelLoginPage, ApnaPanelSessionGate } from "@alsocoder/apna-panel-auth"

<Route
  path="/login"
  element={
    <ApnaPanelLoginPage
      title="Sign in"
      subtitle="Access your admin panel"
      emailLabel="Email"
      passwordLabel="Password"
      submitLabel="Sign in"
      redirectTo="/"
      onNavigate={(href) => navigate(href)}
      onSuccess={() => toast.success("Welcome back")}
    />
  }
/>

<ApnaPanelSessionGate
  loginPath="/login"
  onRedirectToLogin={(path) => navigate(path)}
  loadingFallback={<ApnaPanelLoadingScreen />}
>
  <ApnaPanelShell>...</ApnaPanelShell>
</ApnaPanelSessionGate>

Starter templates

Pre-built menu + dashboard configs for common panel types:

import { applyStarterTemplate, starterTemplates } from "@alsocoder/apna-panel"

const config = definePanelConfig(
  applyStarterTemplate("saas", {
    appName: "My SaaS",
    // merge any extra config
  })
)

// Available templates:
starterTemplates.saas       // users, billing, settings
starterTemplates.cms        // posts, pages, media
starterTemplates.ecommerce  // products, orders, customers

Themes & RTL

import { useApnaPanelTheme } from "@alsocoder/apna-panel"

function ThemeToggle() {
  const { mode, setMode, toggleMode, rtl, setRtl } = useApnaPanelTheme()
  return (
    <>
      <button onClick={toggleMode}>Toggle {mode}</button>
      <button onClick={() => setRtl(!rtl)}>RTL</button>
    </>
  )
}

// Custom theme in config
themes: [
  { id: "brand", label: "Brand", mode: "light", cssVars: { "--primary": "#6366f1" } },
]

Mobile bottom nav

At ≤475px, sidebar hides and a fixed bottom nav appears.

mobileNav: {
  order: ["dashboard", "users", "reports", "settings"],
  moreLabel: "More",
  maxVisible: 4,
}
  • ≤5 items: all shown
  • >5 items: first 4 + More overflow sheet

Ecosystem integration

| Package | Use in panel | |---------|--------------| | @alsocoder/apna-crud | CRUD pages inside <ApnaPanelMain> routes | | @alsocoder/apna-form | Dynamic forms (via ApnaCrud) | | @alsocoder/apna-table | Tables (via ApnaCrud) | | @alsocoder/apna-modal | Bundled in panel provider | | @alsocoder/apna-toast | Bundled in panel provider | | @alsocoder/apna-upload | Upload fields in CRUD forms | | @alsocoder/apna-media-library | Media picker fields |


Playground

cd ApnaPanel
npm run dev:playground

Opens at http://localhost:5180 — dashboard, CRUD (modal/drawer/page), plugins, permissions, mobile nav.


License

MIT