@alsocoder/apna-panel
v0.1.0
Published
React admin panel framework with permissions, navigation, themes, and layout shell for the Alsocoder ecosystem.
Maintainers
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-domOptional:
npm install @alsocoder/apna-panel-auth # login page + session gate
npm install @alsocoder/apna-crud # CRUD pages inside panelPeer 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-inputimport "@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, customersThemes & 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:playgroundOpens at http://localhost:5180 — dashboard, CRUD (modal/drawer/page), plugins, permissions, mobile nav.
License
MIT
