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

gw-admin

v0.2.3

Published

Composable administration UI for the Next.js App Router and next-headless-crud.

Readme

gw-admin

Composable administration UI for the Next.js App Router and next-headless-crud.

gw-admin supplies reusable UI without owning application routes, authentication, database access, navigation data, brand assets, file storage, or deployment. Every feature is available through a focused package subpath, so an application can use the complete default UI, replace one page, or use only a few primitives.

Design goals

  • Application-owned app/admin/** routes
  • Replaceable desktop, mobile, page-header, and page-content slots
  • Separate imports for CRUD pages, CRUD forms, attachments, rich text, and account actions
  • Service-owned authentication, Server Actions, repositories, and uploaders
  • Optional default CSS with stable gw-admin-* class names and CSS variables
  • Preserved React Server Component and Client Component boundaries

Requirements

  • Node.js 20 or newer
  • React 19 or newer
  • Next.js 15 or newer when using the supplied Next.js components

Feature entry points have additional optional peers:

| Entry point | Additional packages | | --- | --- | | gw-admin/crud/pages | next-headless-crud | | gw-admin/crud/form | next-headless-crud, gw-store, react-store-input | | gw-admin/crud/preset | Complete CRUD peer set below | | gw-admin/crud/preset/client | Complete CRUD peer set below | | gw-admin/attachments | gw-store, gw-react-file-input | | gw-admin/rich-text | gw-store, react-store-input, gw-rich-text-editor |

The current integration baseline is next-headless-crud 0.1.1, gw-store 0.2, react-store-input 0.7, and gw-rich-text-editor 0.2.

Install

Install the core package:

npm install gw-admin

Install only the peers required by the features the application uses. For the complete CRUD, attachment, and editor stack:

npm install next-headless-crud gw-store react-store-input \
  gw-react-file-input gw-rich-text-editor

The package ships compiled JavaScript and declarations. A consuming Next.js application does not need transpilePackages.

Recommended setup

The preset API keeps the normal application integration to three small files. The application supplies only its brand and navigation, a storage uploader and resolver, and its CRUD resources and Server Actions.

1. Layout

// app/admin/layout.tsx
import Image from "next/image";
import { AdminLayout } from "gw-admin";
import "gw-admin/styles.css";
import "gw-admin/rich-text.css";

const navigation = [
  { label: "홈", href: "/admin" },
  { label: "새소식", href: "/admin/news" },
  { label: "계정", href: "/admin/account" },
] as const;

export default async function Layout({ children }: { children: React.ReactNode }) {
  await requireAdmin();

  return (
    <AdminLayout
      brand={<Image src="/logo.png" alt="서비스" width={100} height={100} />}
      navigation={navigation}
    >
      {children}
    </AdminLayout>
  );
}

2. One client-side form preset

// admin/admin-form.tsx
"use client";

import { createAdminCrudForm } from "gw-admin/crud/preset/client";
import { uploadFile } from "@/files/upload-file";

export const AdminForm = createAdminCrudForm({
  upload: uploadFile,
  hiddenFields: ["slug", "summary", "publishedAt"],
  hiddenFieldsByPath: {
    "/admin/inquiries": ["status", "repliedAt"],
  },
  createDefaults: { isPrivate: true },
});

By convention the preset renders attachmentFileIds as an attachment field and content and replyContent as rich-text fields. Every name and behavior can be changed through the preset options.

3. One catch-all CRUD route

// app/admin/[...path]/page.tsx
import {
  createAdminCrudPages,
  createAdminCrudRoutePage,
} from "gw-admin/crud/preset";
import { AdminForm } from "@/admin/admin-form";
import { resolveFiles } from "@/files/resolve-files";

const pages = createAdminCrudPages({
  form: AdminForm,
  resolveFiles,
});

export default createAdminCrudRoutePage({
  pathPrefix: "admin",
  pages,
  routes: [
    {
      resource: newsResource,
      actions: { create: createNews, update: updateNews, delete: deleteNews },
    },
    {
      resource: inquiriesResource,
      actions: {
        create: createInquiry,
        update: updateInquiry,
        delete: deleteInquiry,
      },
    },
  ],
});

uploadFile and resolveFiles use the small storage-neutral types exported by gw-admin/attachments; the package does not require a particular API, CDN, or database.

The preset is optional. Low-level entry points remain public for applications that need only one component or want to own the page composition.

To replace one page for one resource, add only that override to the route:

{
  resource: reportsResource,
  actions: reportActions,
  pages: { detail: ReportsDetailPage },
}

To opt out at the field level, use renderField in createAdminCrudForm or renderDetailField in createAdminCrudPages. Returning undefined delegates back to the preset.

Entry points

| Import | Purpose | | --- | --- | | gw-admin | Server-safe shell, sidebar, navigation, and page header | | gw-admin/layout | Shell and desktop sidebar | | gw-admin/layout/mobile | Optional client-side mobile header and drawer | | gw-admin/header | Breadcrumb page header | | gw-admin/form | Application-owned form field, field-group, and alert primitives | | gw-admin/navigation | Application-owned navigation rendering | | gw-admin/account | Configurable logout button | | gw-admin/pages | Dashboard and account pages | | gw-admin/crud/pages | List, detail, create, update, and delete UI | | gw-admin/crud/form | Store-backed standard CRUD form | | gw-admin/crud/preset | Shared CRUD pages and catch-all route factory | | gw-admin/crud/preset/client | Client-side form preset factory | | gw-admin/attachments | Attachment list and uploader field | | gw-admin/rich-text | Store-backed editor, view, nodes, and upload adapter | | gw-admin/styles.css | Optional admin UI theme | | gw-admin/rich-text.css | Optional rich-editor behavioral baseline |

Application-owned forms

Use the lower-level form primitives when a resource does not fit a flat CRUD model, such as an editor that owns nested rows in a separate table:

import { AdminFormAlert, AdminFormField, AdminFormFields } from "gw-admin/form";

<AdminFormFields>
  {message && <AdminFormAlert tone="error">{message}</AdminFormAlert>}
  <AdminFormField label="제목" htmlFor="title" required>
    <input id="title" className="gw-admin-input" />
  </AdminFormField>
</AdminFormFields>;

AdminCrudForm uses these same primitives internally. CRUD list pages also accept breadcrumbs, headerActions, and renderRowActions for resource-specific page composition.

Import focused subpaths in shared libraries and large applications. This keeps optional feature dependencies outside bundles that do not use them.

Application-owned layout

The application chooses its own brand and navigation.

// app/admin/layout.tsx
import Image from "next/image";
import {
  AdminNavigation,
  AdminShell,
  AdminSidebar,
  type AdminNavigationItem,
} from "gw-admin";
import { AdminMobileHeader } from "gw-admin/layout/mobile";
import "gw-admin/styles.css";

const navigation: readonly AdminNavigationItem[] = [
  { label: "홈", href: "/admin" },
  { label: "새소식", href: "/admin/news" },
  { label: "계정", href: "/admin/account" },
];

const brand = <Image src="/logo.png" alt="서비스 이름" width={100} height={100} />;

export default async function AdminLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await requireAdmin();

  return (
    <AdminShell
      sidebar={
        <AdminSidebar brand={brand}>
          <AdminNavigation items={navigation} />
        </AdminSidebar>
      }
      mobileHeader={
        <AdminMobileHeader brand={brand} navigation={navigation} />
      }
    >
      {children}
    </AdminShell>
  );
}

AdminShell does not create either navigation surface by itself:

  • Pass no sidebar to omit the desktop sidebar.
  • Pass no mobileHeader to omit the mobile header.
  • Pass an application component into either slot to replace the default.
  • Skip AdminShell entirely and import only AdminPageHeader or a CRUD page.

Authentication must remain in the application layout or page. Rendering an admin component is not an authorization check.

Logout

The default HTTP flow is configurable and performs a full navigation after success so cached authenticated UI is discarded:

import { AdminLogoutButton } from "gw-admin/account";

<AdminLogoutButton
  endpoint="/api/auth/logout"
  redirectTo="/login"
/>;

For a non-HTTP client flow, wrap the button in an application Client Component and supply request:

"use client";

import { AdminLogoutButton } from "gw-admin/account";
import { authClient } from "@/auth/client";

export function ServiceLogoutButton() {
  return (
    <AdminLogoutButton
      request={() => authClient.logout()}
      redirectTo="/sign-in"
    />
  );
}

CRUD pages

The package builds on next-headless-crud; resources and Server Actions still belong to the application.

// app/admin/[...path]/page.tsx
import { createCrudRoutePage, defineCrudRoute } from "next-headless-crud/next";
import {
  AdminCrudCreatePage,
  AdminCrudDetailPage,
  AdminCrudListPage,
  AdminCrudUpdatePage,
} from "gw-admin/crud/pages";
import { newsResource } from "@/admin/news/resource";
import {
  createNews,
  deleteNews,
  updateNews,
} from "@/admin/news/actions";

const newsRoute = defineCrudRoute({
  resource: newsResource,
  actions: {
    create: createNews,
    update: updateNews,
    delete: deleteNews,
  },
  pages: {
    list: AdminCrudListPage,
    create: AdminCrudCreatePage,
    detail: AdminCrudDetailPage,
    update: AdminCrudUpdatePage,
  },
});

export default createCrudRoutePage([newsRoute]);

Per-page opt-out

Next.js gives an explicit page precedence over a catch-all route. A custom page can therefore replace one package page without changing the remaining CRUD routes:

app/admin/[...path]/page.tsx  -> shared CRUD pages
app/admin/account/page.tsx    -> application-owned page
app/admin/reports/page.tsx    -> application-owned page

Routes can also mix page implementations in one resource:

pages: {
  list: AdminCrudListPage,
  create: ServiceCreatePage,
  detail: AdminCrudDetailPage,
  update: ServiceUpdatePage,
}

Detail field opt-out

Hide fields or replace rendering one field at a time:

import {
  AdminCrudDetailPage,
  type AdminCrudDetailPageProps,
} from "gw-admin/crud/pages";
import { AdminRichTextView } from "gw-admin/rich-text";

export function NewsDetailPage(props: AdminCrudDetailPageProps) {
  return (
    <AdminCrudDetailPage
      {...props}
      hiddenFields={["description"]}
      longTextFields={["content"]}
      renderField={({ field, value }) => {
        if (field.name === "content" && typeof value === "string") {
          return <AdminRichTextView value={value} />;
        }

        return undefined;
      }}
    />
  );
}

renderActions can replace every header action, while formatValue can replace scalar formatting across the page.

Store-backed form

AdminCrudForm handles standard input, select, checkbox, and textarea fields. It deliberately does not guess that a field name represents an attachment, editor, derived value, or server-managed value.

"use client";

import {
  AdminCrudForm,
  type AdminCrudFormProps,
} from "gw-admin/crud/form";

export function ServiceAdminForm(props: AdminCrudFormProps) {
  return (
    <AdminCrudForm
      {...props}
      fieldModes={{
        description: "textarea",
        slug: "hidden",
        publishedAt: "hidden",
      }}
    />
  );
}

The save action is a type="button" action. Pressing Enter in a large input screen does not implicitly submit the form.

Composing attachment and editor fields

Feature adapters are created in the consuming application, not in the package. This example is a Client Component, so uploader functions never cross the Server Component serialization boundary:

"use client";

import {
  AdminCrudForm,
  type AdminCrudFormProps,
} from "gw-admin/crud/form";
import {
  AdminFileAttachments,
  type AdminFileUploader,
  type AdminUploadedFile,
} from "gw-admin/attachments";
import {
  AdminRichTextEditor,
  adminRichTextNodes,
  createAdminRichTextUploadAdapter,
} from "gw-admin/rich-text";
import "gw-admin/rich-text.css";

const uploadFile: AdminFileUploader = async (file, context) => {
  const uploaded = await serviceFileUploader(file, context);

  return {
    id: uploaded.id,
    name: uploaded.name,
    type: uploaded.type,
    size: uploaded.size,
    key: uploaded.key,
    url: cdn(uploaded.key),
    metadata: uploaded.metadata,
  };
};

const editorUpload = createAdminRichTextUploadAdapter(uploadFile);

export function ServiceAdminForm(
  props: AdminCrudFormProps & {
    attachments: readonly AdminUploadedFile[];
  },
) {
  return (
    <AdminCrudForm
      {...props}
      fieldModes={{ slug: "hidden" }}
      renderField={({ field, store, error, errorId, setUploading }) => {
        if (field.name === "attachmentFileIds") {
          return (
            <AdminFileAttachments
              store={store}
              name={field.name}
              uploader={uploadFile}
              initialFiles={props.attachments}
              error={error}
              onUploadingChange={setUploading}
            />
          );
        }

        if (field.name === "content") {
          return (
            <AdminRichTextEditor
              store={store}
              name={field.name}
              upload={editorUpload}
              nodes={adminRichTextNodes}
              invalid={Boolean(error)}
              describedBy={error ? errorId : undefined}
            />
          );
        }

        return undefined;
      }}
    />
  );
}

The rich-text adapter only requires the cancellation and progress context, so the same AdminFileUploader can be reused for attachment and editor fields.

An application that does not need uploads simply does not install or import the attachment entry point. Omitting upload from AdminRichTextEditor also removes the attachment toolbar action.

Styling

Default theme:

import "gw-admin/styles.css";

Editor behavioral baseline, only when the rich-text entry point is used:

import "gw-admin/rich-text.css";

The package never imports these files automatically. To own every style, omit both imports and target the stable gw-admin-* class names yourself.

The default theme can also be adjusted without replacing selectors:

:root {
  --gw-admin-background: #fafafa;
  --gw-admin-surface: #ffffff;
  --gw-admin-text: #111111;
  --gw-admin-primary: #0b4d3b;
  --gw-admin-primary-text: #ffffff;
  --gw-admin-danger: #a61b1b;
  --gw-admin-border: #d8d8d8;
  --gw-admin-sidebar-width: 17rem;
  --gw-admin-content-width: 90rem;
  --gw-admin-radius: 0;
}

Server and client boundaries

  • Layout shell, sidebar, navigation, header, list page, detail page, and page loaders are server-compatible.
  • Mobile navigation, logout, delete, CRUD form, attachment input, and editor are Client Components.
  • Application upload functions must be declared in an application Client Component before they are passed to attachment or editor components.
  • Authentication, authorization, resource scoping, and Server Actions must validate permissions independently of the rendered UI.

Publishing and updates

The repository includes a manual-compatible npm publishing workflow. Configure NPM_PUBLISH_TOKEN in the GitHub repository, bump the package version, and push the package metadata to main.

npm version patch
git push --follow-tags

Before publishing:

npm run check
npm run pack:check

Consuming services update manually:

npm install [email protected]

No automatic pull request or consumer deployment is performed.