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

@suluk/panel

v0.3.2

Published

Contract-first admin panels, in the spirit of Payload but projected from one OpenAPI v4 document. Infers Payload-style field types from the contract, renders shadcn/theme-aware forms + data tables, and mounts a role-aware superadmin (and per-role dashboar

Readme


CANDIDATE tooling — not official OpenAPI. Suluk is a single-contributor candidate for OpenAPI Specification v4.0 ("Moonwalk"), unaffiliated with the OpenAPI Initiative and unable to ratify anything on the SIG's behalf.

Install

bun add @suluk/panel

Peer-depends on hono (the panel is a Hono app you mount on your router).

What it does

  • Infers Payload-style field types from the contract. Each entity's components.schemas JSON-Schema is mapped to the right widget — text / textarea / richtext / number / boolean / select / date / datetime / email / url / media / json / relationship — including enum options, required/nullable flags, and relationships (a categoryId whose Category is itself an entity). The contract IS the config; there's no separate collection DSL.
  • Renders a Payload-style two-pane admin — a sidebar of collections plus a content pane with searchable, sortable, paginated data tables and create/edit forms. Ships a light + OS-following dark theme out of the box (themed entirely with CSS variables, so it can inherit the host site's vocabulary via headHtml).
  • Role-aware by projection. Pass a per-role projected document and you get a per-role panel for free: the available CRUD ops are derived from the operations present in the doc, so an op a role can't perform simply isn't there. Admin sees everything; a user sees only their own rows.
  • A dashboard framework, not just CRUD. Beyond the auto collections it mounts host-injected stat cards, sidebar groups, a bespoke home overview, and custom non-CRUD sections (profile, billing, danger zone) — each of which may be a per-request function of the logged-in user.
  • Never touches your database. Forms and tables drive the contract's own REST endpoints (fetch GET/POST/PATCH/DELETE) — there is no DB adapter, so there's no config drift between the spec and the panel.

When to reach for it

Reach for @suluk/panel when you want a working admin UI derived from the contract — a Payload-equivalent that you didn't have to configure, mounted in-process on the same Hono app that serves your API.

  • vs @suluk/admin — that's the Suluk cockpit (/superadmin): the exhaustive raw-CRUD console for the spec author. @suluk/panel is the product-facing, role-projected dashboard you ship to end users.
  • vs @suluk/shadcn — that generates owned React/shadcn TSX you commit and edit. @suluk/panel renders a complete server-side HTML panel at runtime; reach for shadcn when you want owned components in your own frontend instead.

Usage

Mount a panel (the common case)

panelApp returns a Hono instance. Mount it on your router and gate it with authorize:

import { Hono } from "hono";
import { panelApp } from "@suluk/panel";
import { document } from "./contract"; // your OpenAPI v4 document

const app = new Hono();

app.route(
  "/",
  panelApp({
    document,                 // the v4 doc (a value, or a per-request function of `c`)
    basePath: "/panel",       // default "/panel"
    title: "Acme",            // brand shown in the sidebar + titles
    authorize: (c) => isSignedIn(c), // default: deny everything
  }),
);

That mounts, under /panel: a card-grid home, and a list + create/edit form for every CRUD-managed entity the document exposes. The forms and tables call your contract's REST endpoints directly.

Per-role dashboards (project the document per request)

document (and stats / groups / sections / home / homeHeading) may each be a function of the request context, so one mount serves every audience off a role-projected document:

panelApp({
  document: (c) => projectDocument(doc, viewerOf(c)), // per-role projection → per-role panel
  basePath: "/panel",
  authorize: (c) => isSignedIn(c),
  homeHeading: (c) => (isAdmin(c) ? "Owner dashboard" : "Your account"),
  hideEntities: ["AuthToken"],                        // omit some entities entirely
  groups:   (c) => (isAdmin(c) ? [...userGroups, ...adminGroups] : userGroups),
  sections: (c) => (isAdmin(c) ? [...userSections, ...adminSections] : userSections),
  stats:    (c) => (isAdmin(c) ? adminStats() : userStats(c)),
  home:     (c) => renderOverview({ admin: isAdmin(c) }), // replaces the auto card-grid
});

A StatCard is { label, value, hint?, href? }. A PanelSection is a custom non-CRUD page mounted at ${basePath}/s/<id>, rendered inside the panel shell:

import type { PanelSection, StatCard } from "@suluk/panel";

const sections: PanelSection[] = [
  {
    id: "billing",
    label: "Billing",
    summary: "Plan & invoices",
    render: (c) => `<div class="pf-section">…your billing HTML…</div>`,
  },
];

Media uploads

Media fields (image / cover / avatar / logo / …) degrade to paste-a-URL unless you point them at an upload endpoint that accepts a multipart/form-data file and returns { url }:

panelApp({ document, authorize, uploadPath: "/upload" }); // e.g. an R2-backed worker route

Render pieces directly (lower-level building blocks)

The inference + render helpers are exported standalone, so you can compose your own surface without panelApp:

import { entityModels, renderList, renderForm, renderShell, fieldsOf } from "@suluk/panel";

const models = entityModels(document);       // EntityModel[] — name, REST path, fields, title, access
const product = models.find((m) => m.name === "Product")!;

const list = renderList(product, { basePath: "/panel" });
const form = renderForm(product, { basePath: "/panel", relPaths: { Category: "/category" }, canDelete: true });

const html = renderShell({
  title: "Acme", brand: "Acme", basePath: "/panel",
  entities: models.map((m) => ({ name: m.name })),
  active: "Product", heading: "Product", body: list,
});

// Or just infer the field set from a raw schema:
const fields = fieldsOf(document.components.schemas.Product, new Set(["Product", "Category"]));

API

| Export | What it does | | --- | --- | | panelApp(opts) | Build a Hono admin/dashboard app. Options: document, basePath, title, authorize, headHtml, hide, hideEntities, uploadPath, stats, groups, sections, home, homeHeading, homeLabel. | | entityModels(doc, opts?) | Read the v4 doc → an EntityModel[] (REST path, inferred fields, title field, CRUD access derived from present ops). | | fieldsOf(schema, entities?, opts?) | Infer the ordered Field[] for one entity schema (widget type, required/nullable/readOnly, enum options, relationships). | | titleField(fields) | The entity's best "title" field — for list columns + relationship labels. | | humanize(name) | "coverImageUrl""Cover Image URL", "categoryId""Category". | | renderList(model, { basePath }) | Data-table HTML (client-side search / sort / paginate, per-row edit·delete) over the entity's REST list endpoint. | | renderForm(model, { basePath, relPaths, canDelete, uploadPath? }) | Create/edit form HTML (loads the record on edit, coerces by type, POSTs/PATCHes to REST). | | renderInput(field, value?) / renderFieldRow(field, value?) | One widget / one labelled field row for a Field. | | renderShell(opts) | The two-pane panel chrome (sidebar nav + content); accepts a flat entities list or grouped nav. | | richtextEditor / richtextScript / RICHTEXT_CSS | The dependency-free markdown rich-text editor (toolbar + Write/Preview) and its client init + styles. | | mediaEditor / mediaScript / MEDIA_CSS | The media/upload widget (URL input + preview + optional file picker) and its client init + styles. | | PANEL_CSS | The panel's theme stylesheet (CSS-variable vocabulary). |

Types: PanelOptions, StatCard, PanelSection, PanelGroup, EntityModel, Field, FieldType, FieldsOptions, ListOptions, FormOptions, ShellOptions, NavGroup, NavItem.

Boundary

@suluk/panel renders; it does not host or persist (the Suluk L3 line — render/generate, never host). It emits HTML + client JS that drive your contract's REST endpoints; the database, the auth/session, and file storage stay app-side:

  • Inject the document. The panel reads a v4 doc you pass in (or project per request). It never owns the spec.
  • Inject the gate. authorize is yours; the panel defaults to deny-all. CRUD availability is read off the (projected) operations — projection is how you scope per role.
  • Inject the bytes. Media uploads POST to your uploadPath (e.g. an R2-backed route) that returns { url }. Without one, media fields are paste-a-URL. Storage is the host's concern.
  • No DB adapter. Lists, forms, and deletes are fetch calls to the contract's own endpoints — so there's no second source of truth to drift.

License

Apache-2.0