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

@groveback/ui

v0.7.0

Published

React primitives for Groveback Studio screens — tables, forms, detail views and relation pickers that read through the Groveback SDK, so every query passes the policy engine.

Readme

@groveback/ui

React primitives for screens built with Groveback Studio — tables, forms, detail views and relation pickers that read through the Groveback SDK.

Beta. These components ship alongside Studio, whose stored screen format is still settling. Expect breaking changes before 1.0.

Every read and write goes through an SDK collection handed in as a prop: the primitives never build a URL or hold a key, so all data access passes your project's policy engine. A generated screen cannot reach data its viewer could not reach by hand.

Install

npm install @groveback/ui
# or
bun add @groveback/ui

React 18 or 19 is a peer dependency. The components are styled with Tailwind utility classes and ship no CSS of their own, so your app's Tailwind (v4) build has to scan them — one line in your CSS does it:

@import "tailwindcss";
@import "@groveback/ui/tailwind.css";

That file carries the @source pointing at the package's own bundle, so it works from any install layout. Dark mode follows the viewer's prefers-color-scheme, which is Tailwind's default.

Usage

Studio generates screens that import these; you can also use them directly.

import { GroveUiProvider, Stack, Heading, DataTable } from '@groveback/ui';
import { createGrove } from './grove'; // generated by `grove gen`

const grove = createGrove();

export default function Products() {
  return (
    <GroveUiProvider value={{ navigate: (href) => router.push(href), currency: 'USD' }}>
      <Stack direction="vertical" gap={4}>
        <Heading level={1}>Products</Heading>
        <DataTable
          collection={grove.collection('products')}
          options={{ limit: 50 }}
          columns={[
            { field: 'name', label: 'Name' },
            { field: 'price', label: 'Price', format: 'currency' },
          ]}
          rowHref="/products/:id"
          rowActions={[{ label: 'Delete', action: { kind: 'delete' }, variant: 'destructive' }]}
        />
      </Stack>
    </GroveUiProvider>
  );
}

Navigation is injected

GroveUiProvider takes a navigate function rather than importing a router, so the same component works under Next.js, React Router, or plain History. Without a provider it falls back to the History API.

Relations are chosen, not typed

A relation field stores a document id. Pass resolveRelation and a display hint, and Form renders a searchable picker over the referenced collection instead of an id input:

<Form
  collection={grove.collection('products')}
  mode="create"
  fields={[
    { field: 'name', label: 'Name' },
    { field: 'categoryId', label: 'Category', display: { field: 'title' } },
  ]}
  resolveRelation={(field) => grove.collection('categories')}
/>

The picker loads a bounded first page and filters in memory, and it tells the user when the list is truncated — the data API has no text search to delegate to, and silently hiding matches would be worse than saying so.

The routing shell lives here, not in your repo

grove gen --ui writes routes.tsx as a table — ROUTES, one entry per Studio screen — and a one-line GroveRoutes that mounts it on this package's shell:

import { GroveRoutes } from './routes'; // generated
import { SignIn } from '@groveback/ui';
import { createGrove } from './grove';

const grove = createGrove();

export default function App() {
  return (
    <SignIn auth={grove.auth}>
      <GroveRoutes />
    </SignIn>
  );
}

GroveRoutes matches the current URL against the table (specific routes ahead of parameterised ones), pushes history on navigation, and provides the context the primitives dispatch through. The generated mount already passes locale (the viewer's language when the document has it — detectLocale) and callEndpoint (custom-endpoint actions run through grove.client.run, so they carry the session). Pass context={{ currency, confirm, … }} for the rest, fallback for the unmatched case, and children for chrome that should render on every screen. With no fallback, the root with no home screen shows an index of the document's pages (RouteIndex) rather than "Not found". Already on a router? Feed ROUTES to it and skip GroveRoutesmatchRoute and resolveRoute are exported for that.

Multilingual documents compile each screen with a MESSAGES catalogue and const t = useMessages(MESSAGES, "en"): the active locale is the shell's, so every screen switches together — pass locale to the generated GroveRoutes to drive it from your own switcher.

Sign-in is a gate around the app

SignIn takes the SDK's auth object and renders its children once a session exists: email + password, an account-creation form (when auth.register exists), and the second-factor step when a login answers mfaRequired. Errors show the API's own message. Persisting the session across reloads is the app's job — hand the SDK an onTokensChanged and restore with setTokens on boot.

Deletes always confirm

The delete action lets you override the confirmation wording, never whether the prompt happens. That guarantee lives in the library, not in the calling screen.

Components

Stack · Grid · Divider · Heading · Text · Button · Header · DataTable · Detail · Form · RelationSelect · MarkdownField · GroveRoutes · RouteIndex · SignIn

Plus GroveUiProvider, useAction, useGroveUi, useMessages, detectLocale, formatValue, resolveHref, matchRoute, resolveRoute.

License

Apache-2.0