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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@talltydev/crud-kit

v0.4.18

Published

A powerful React CRUD framework with ZenStack integration and shadcn/ui compatibility

Readme

@talltydev/crud-kit

A powerful, declarative React CRUD framework with ZenStack integration and shadcn/ui compatibility.

npm version License: MIT

Features

  • 🚀 Declarative CRUD UI - Build full-featured CRUD interfaces with minimal code
  • 🔗 ZenStack Integration - Auto-generated TanStack Query hooks from Prisma schema
  • 🎨 shadcn/ui Compatible - Uses copied UI components for full customization
  • 📊 Multiple Views - Table, Grid, List views with declarative <Crud.Column> definitions
  • 🔍 Advanced Search - Smart search with field-specific syntax and filters
  • 🔐 Permission Control - UI-level access control with withPermissions()
  • ⚡ Type-Safe - Full TypeScript support with Prisma model types
  • 🎯 Framework Agnostic - Works with Next.js App Router, Pages Router, and Vite

Quick Start

Installation (Recommended: CLI)

The recommended way to use CRUD Kit is through the @talltydev/cli which automatically:

  • Copies shadcn/ui components to your project
  • Configures TypeScript path aliases
  • Sets up ZenStack integration
  • Creates example CRUD pages
# Create a new project with CRUD Kit
npx @talltydev/cli create my-app

# Or add to existing project
npx @talltydev/cli add crud

Manual Installation (Advanced)

If you prefer to install manually:

npm install @talltydev/crud-kit
# or
pnpm add @talltydev/crud-kit

⚠️ Important: Manual installation requires:

  1. shadcn/ui components copied to src/components/ui/
  2. TypeScript path alias @/components/ui/* configured in tsconfig.json
  3. ZenStack with TanStack Query integration

See Manual Setup for detailed instructions.

Architecture

Component Strategy: Copy Components

CRUD Kit follows the shadcn/ui philosophy of copying components directly into your project:

your-project/
├── src/
│   ├── components/
│   │   └── ui/           # shadcn/ui components (copied)
│   │       ├── button.tsx
│   │       ├── table.tsx
│   │       └── ...
│   └── ...
└── node_modules/
    └── @talltydev/crud-kit/  # CRUD logic (imported)

Why this approach?

Full Customization - Modify UI components directly in your project ✅ No Version Lock - Not tied to specific shadcn/ui versions ✅ Flexibility - Mix copied components with NPM packages ✅ Type Safety - TypeScript resolves components at compile time

How It Works

CRUD Kit uses path aliases instead of direct NPM imports:

// CRUD Kit source code
import { Button } from '@/components/ui/button'  // ✅ Path alias
// NOT: import { Button } from '@talltydev/ui'   // ❌ Direct import

During Build (tsup with esbuild plugin):

// dist/index.js - path aliases preserved
import { Button } from '@/components/ui/button'  // External reference

In Your Project (TypeScript resolves):

// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

// Resolves to: src/components/ui/button.tsx

This means:

  • 🎯 CRUD Kit components use your project's UI components
  • 📦 No UI components bundled in @talltydev/crud-kit NPM package
  • 🎨 100% customizable without forking CRUD Kit

Usage

1. Define Resource (Data Layer)

Create a resource configuration that connects your Prisma model with ZenStack hooks:

💡 导入路径说明:tallty-kit 基于 monorepo 架构,数据库相关代码统一从共享的 @repo/database package 导入。

// apps/web/src/resources/user-resource.ts
import { defineResource } from '@talltydev/crud-kit'
import type { User } from '@repo/database/types'
import * as userHooks from '@repo/database/hooks/user'
import metadata from '@repo/database/hooks/__model_meta'

export const userResource = defineResource<User>('User')
  // Connect ZenStack hooks
  .withHooks(userHooks)

  // Schema metadata for auto-generation
  .withMetadata(metadata.models.user)

  // Search configuration
  .withSearchableFields(['name', 'email'])

  // Default query options
  .withBase({
    list: {
      orderBy: [{ createdAt: 'desc' }]
    }
  })

  // Permission control
  .withPermissions({
    canCreate: (session) => !!session?.user,
    canEdit: (session) => session?.user?.role === 'admin',
    canDelete: (session) => session?.user?.role === 'admin'
  })

  // Computed fields
  .withComputed({
    displayName: {
      compute: (user) => user.name || user.email
    }
  })

2. Build CRUD UI (Presentation Layer)

Use the <Crud> component with declarative <Crud.Column> definitions:

// src/app/(admin)/users/page.tsx
'use client'

import { Crud } from '@talltydev/crud-kit'
import { userResource } from '@/resources/user-resource'

export default function UsersPage() {
  return (
    <Crud
      resource={userResource}
      actions={{
        create: { as: 'modal', size: 'lg' },
        edit: { as: 'modal', size: 'lg' },
        view: { as: 'drawer', size: 'lg' }
      }}
    >
      {/* Header */}
      <Crud.Header>
        <Crud.Title>Users</Crud.Title>
        <Crud.Actions>
          <Crud.CreateButton>Add User</Crud.CreateButton>
        </Crud.Actions>
      </Crud.Header>

      {/* Toolbar */}
      <Crud.Toolbar>
        <Crud.ToolbarLeft>
          <Crud.Search
            fields={['name', 'email']}
            placeholder="Search users..."
          />
          <Crud.Filters>
            <Crud.Filter field="role" label="Role" type="select" />
            <Crud.DateRange field="createdAt" label="Created" />
          </Crud.Filters>
        </Crud.ToolbarLeft>
        <Crud.ToolbarRight>
          <Crud.ViewSwitcher views={['table', 'grid']} default="table" />
          <Crud.ExportButton />
        </Crud.ToolbarRight>
      </Crud.Toolbar>

      {/* Views System */}
      <Crud.Views>
        {/* Table View */}
        <Crud.View name="table">
          <Crud.Table>
            <Crud.Column field="name" label="Name" sortable />
            <Crud.Column field="email" label="Email" sortable />
            <Crud.Column field="role" label="Role" />
            <Crud.Column field="createdAt" label="Created" format="date" sortable />
            <Crud.RowActions>
              <Crud.ViewAction as="drawer" />
              <Crud.EditAction as="modal" />
              <Crud.DeleteAction
                confirm
                confirmTitle="Delete User"
                confirmDescription="This action cannot be undone."
              />
            </Crud.RowActions>
          </Crud.Table>
        </Crud.View>

        {/* Grid View */}
        <Crud.View name="grid">
          <Crud.Grid
            columns={{ default: 1, sm: 2, md: 3, lg: 4 }}
            renderItem={(user) => (
              <div className="rounded-lg border p-4">
                <h3 className="font-semibold">{user.name}</h3>
                <p className="text-sm text-muted-foreground">{user.email}</p>
              </div>
            )}
          />
        </Crud.View>
      </Crud.Views>

      {/* Pagination */}
      <Crud.Pagination />
    </Crud>
  )
}

3. Key Concepts

Resource vs UI Separation

Resource (Model Layer) - defineResource():

  • ZenStack hooks binding
  • Default query parameters
  • Permissions
  • Computed fields
  • Data transformations

CRUD Component (Presentation Layer) - <Crud>:

  • UI layout and styling
  • Column definitions with <Crud.Column>
  • View switching (table/grid/list)
  • Search and filters
  • User interactions

Declarative Column Definitions

Instead of programmatic column config, use JSX:

{/* ✅ Declarative - Recommended */}
<Crud.Table>
  <Crud.Column field="name" label="Name" sortable />
  <Crud.Column field="email" label="Email" sortable />
  <Crud.Column field="status" label="Status" />
</Crud.Table>

{/* ❌ Programmatic - Old API */}
<Crud.Table
  columns={[
    { field: 'name', label: 'Name', sortable: true },
    { field: 'email', label: 'Email', sortable: true }
  ]}
/>

Benefits:

  • More readable and maintainable
  • Better TypeScript inference
  • Easier to customize individual columns
  • Follows React patterns

API Reference

defineResource<T>(modelName: string)

Create a type-safe resource configuration.

Methods

.withHooks(hooks: ZenStackHooks)

Bind ZenStack-generated TanStack Query hooks.

import * as userHooks from '@repo/database/hooks/user'

defineResource<User>('User')
  .withHooks(userHooks)

Required hooks:

  • findMany - List records
  • findUnique - Get single record
  • create - Create record
  • update - Update record
  • delete - Delete record
.withMetadata(metadata: ModelMetadata)

Provide schema metadata for auto-generation features.

import metadata from '@repo/database/hooks/__model_meta'

defineResource<User>('User')
  .withMetadata(metadata.models.user)

Enables:

  • Automatic field type detection
  • Enum options extraction
  • Validation rules
  • Default values
.withSearchableFields(fields: string[])

Configure which fields are searchable.

defineResource<User>('User')
  .withSearchableFields(['name', 'email', 'username'])

Enables smart search syntax:

  • email:john@ - Search specific field
  • name:"John Doe" - Exact phrase
  • role:admin status:active - Multiple fields
.withBase(config: BaseQueryConfig)

Set default query parameters.

defineResource<User>('User')
  .withBase({
    list: {
      orderBy: [{ createdAt: 'desc' }],
      where: { deletedAt: null }
    }
  })
.withPermissions(permissions: PermissionConfig)

Control UI-level access.

defineResource<User>('User')
  .withPermissions({
    canCreate: (session) => !!session?.user,
    canEdit: (session) => session?.user?.role === 'admin',
    canDelete: (session) => session?.user?.role === 'admin',
    canView: (session, record) => {
      // Row-level permission
      return session?.user?.id === record.id || session?.user?.role === 'admin'
    }
  })

Note: These are UI permissions. Database-level security is handled by ZenStack access policies.

.withFieldMeta(meta: FieldMetaConfig)

Configure field options and value mappings.

defineResource<User>('User')
  .withFieldMeta({
    role: {
      options: [
        { label: 'Admin', value: 'admin' },
        { label: 'User', value: 'user' },
        { label: 'Guest', value: 'guest' }
      ],
      valueMap: {
        'admin': 'Administrator',
        'user': 'Standard User',
        'guest': 'Guest User'
      }
    }
  })
.withComputed(computed: ComputedFieldsConfig)

Add derived fields.

defineResource<User>('User')
  .withComputed({
    displayName: {
      compute: (user) => user.name || user.email
    },
    isAdmin: {
      compute: (user) => user.role === 'admin'
    }
  })

Computed fields are:

  • Cached automatically
  • Type-safe
  • Accessible in UI like regular fields
.withCallbacks(callbacks: CallbacksConfig)

Add data transformation hooks.

defineResource<User>('User')
  .withCallbacks({
    beforeCreate: async (data) => {
      // Set defaults
      data.role = data.role || 'user'
      return data
    },
    afterList: async (users) => {
      console.log(`Loaded ${users.length} users`)
      return users
    },
    beforeUpdate: async (data) => {
      // Validate or transform
      return data
    }
  })

Available callbacks:

  • beforeCreate, afterCreate
  • beforeUpdate, afterUpdate
  • beforeDelete, afterDelete
  • beforeList, afterList

<Crud> Component

Main CRUD interface component.

<Crud
  resource={userResource}
  actions={{
    create: { as: 'modal' | 'drawer' | 'page', size?: 'sm' | 'md' | 'lg' | 'xl' },
    edit: { as: 'modal' | 'drawer' | 'page', size?: 'sm' | 'md' | 'lg' | 'xl' },
    view: { as: 'modal' | 'drawer' | 'page', size?: 'sm' | 'md' | 'lg' | 'xl' }
  }}
>
  {/* Children: Header, Toolbar, Views, Pagination */}
</Crud>

Sub-Components

<Crud.Column>

Define table columns declaratively:

<Crud.Column
  field="name"              // Field name from model
  label="Name"              // Display label
  sortable                  // Enable sorting
  format="date"             // Format type: 'text' | 'date' | 'datetime' | 'time' | 'number' | 'currency' | 'boolean' | 'percent' | 'email' | 'url' | 'json' | 'relation' | 'count' | 'richtext'
  render={(value, row) => ( // Custom render function
    <span>{value}</span>
  )}
/>

Field Formats:

The format prop automatically transforms field values for display:

| Format | Description | Example | |--------|-------------|---------| | text | Plain text (default) | Any string value | | date | Date only (year-month-day) | 2024-01-15 | | datetime | Date and time | 2024-01-15 14:30:00 | | time | Time only | 14:30:00 | | number | Formatted number with localization | 1,234.56 | | currency | Currency format | ¥1,234.56 (CNY) | | boolean | Badge with Yes/No | Yes / No | | percent | Percentage | 45.2% | | email | Clickable mailto link | [email protected] | | url | Clickable external link | https://example.com | | json | Formatted JSON in code block | { "key": "value" } | | relation | Display related object name | Shows name, title, or label field | | count | Count of array items | 5 items | | richtext | HTML content with styling | Rich text with formatted images |

Rich Text Format (richtext):

The richtext format renders HTML content with proper typography and image styling:

<Crud.Column field="content" label="Content" format="richtext" />

Features:

  • Tailwind Typography: Elegant prose styling
  • Responsive Images: Auto-sized images with borders and rounded corners
  • Dark Mode: Automatic dark theme support
  • Auto-detection: Fields named content, description, or body with HTML are auto-detected

For custom styling:

import { renderRichText } from '@talltydev/crud-kit'

<Crud.Column
  field="content"
  render={({ value }) => renderRichText(value, {
    className: 'prose-lg [&_img]:shadow-lg'
  })}
/>

Security Note: Rich text uses dangerouslySetInnerHTML. Ensure content is from trusted sources or properly sanitized.

<Crud.Table>

Table view with declarative columns:

<Crud.Table>
  <Crud.Column field="id" label="ID" />
  <Crud.Column field="name" label="Name" sortable />
  <Crud.RowActions>
    <Crud.ViewAction />
    <Crud.EditAction />
    <Crud.DeleteAction confirm />
  </Crud.RowActions>
</Crud.Table>

<Crud.Grid> and <Crud.List>

Alternative view layouts:

<Crud.Grid
  columns={{ default: 1, sm: 2, md: 3, lg: 4 }}
  gap={4}
  renderItem={(item) => <CustomCard data={item} />}
/>

<Crud.List
  renderItem={(item) => <CustomListItem data={item} />}
/>

Manual Setup

If you're not using the CLI, follow these steps:

1. Install shadcn/ui Components

# Initialize shadcn/ui
npx shadcn@latest init

# Install required components
npx shadcn@latest add button table card dialog drawer form input label select badge

2. Configure TypeScript Path Aliases

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@/components/ui/*": ["./src/components/ui/*"],
      "@/lib/*": ["./src/lib/*"]
    }
  }
}

3. Install CRUD Kit and Dependencies

npm install @talltydev/crud-kit @tanstack/react-query @zenstackhq/runtime

4. Set Up ZenStack

# Install ZenStack
npm install -D zenstack @zenstackhq/tanstack-query

# Initialize ZenStack
npx zenstack init
// schema.zmodel
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

plugin hooks {
  provider = '@zenstackhq/tanstack-query'
  output = 'src/hooks/model'
  target = 'react'
  version = 'v5'
}

model User {
  id        String   @id @default(cuid())
  name      String?
  email     String   @unique
  role      String   @default("user")
  createdAt DateTime @default(now())

  @@allow('all', true)  // Configure your access policies
}
# Generate Prisma client and ZenStack hooks
npx zenstack generate

5. Wrap App with Providers

// Next.js App Router: app/providers.tsx
'use client'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { CrudAdaptersProvider, createNextAdapter } from '@talltydev/crud-kit'
import { useRouter } from 'next/navigation'
import { getSession } from '@/lib/auth'

const queryClient = new QueryClient()

export function Providers({ children }: { children: React.ReactNode }) {
  const router = useRouter()

  const adapter = createNextAdapter({
    router: {
      push: router.push,
      replace: router.replace,
      back: router.back,
      refresh: router.refresh,
      getPath: () => window.location.pathname
    },
    getSession
  })

  return (
    <QueryClientProvider client={queryClient}>
      <CrudAdaptersProvider value={adapter}>
        {children}
      </CrudAdaptersProvider>
    </QueryClientProvider>
  )
}
// app/layout.tsx
import { Providers } from './providers'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

Framework Adapters

Next.js App Router

import { createNextAdapter } from '@talltydev/crud-kit/adapters/next'
import { useRouter } from 'next/navigation'

const adapter = createNextAdapter({
  router: {
    push: router.push,
    replace: router.replace,
    back: router.back,
    refresh: router.refresh,
    getPath: () => window.location.pathname
  },
  getSession: async () => {
    const session = await getServerSession()
    return session
  }
})

Vite / React Router

import { createViteAdapter } from '@talltydev/crud-kit/adapters/vite'
import { useNavigate, useLocation } from 'react-router-dom'

const navigate = useNavigate()
const location = useLocation()

const adapter = createViteAdapter({
  router: {
    push: navigate,
    replace: (path) => navigate(path, { replace: true }),
    back: () => navigate(-1),
    getPath: () => location.pathname
  },
  getSession: async () => {
    // Your auth logic
    return session
  }
})

FAQ

Why path aliases instead of NPM imports?

Short answer: To follow shadcn/ui's copy-components philosophy.

CRUD Kit uses components from your project (src/components/ui/), not from an NPM package. This gives you:

  1. Full customization - Modify any UI component directly
  2. No version conflicts - Not locked to specific shadcn/ui versions
  3. Type safety - TypeScript resolves to your actual components
  4. Zero runtime overhead - No component proxies or injection

Can I use @talltydev/ui NPM package instead?

No. As of v0.2.0, @talltydev/ui is deprecated. CRUD Kit only supports the copy-components approach.

Migration: If you're upgrading from an older version:

# Remove old UI package
npm uninstall @talltydev/ui

# Install shadcn/ui components
npx shadcn@latest add button table card dialog drawer form input label select badge

# Update imports in your code
# Before: import { Button } from '@talltydev/ui/button'
# After:  Already using @/components/ui/button (no changes needed in CRUD Kit)

Do I need to configure tsconfig paths manually?

No, if using CLI - The @talltydev/cli automatically configures paths.

Yes, if manual install - You must add to tsconfig.json:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

How does CRUD Kit find my UI components?

During development (TypeScript):

@/components/ui/button → src/components/ui/button.tsx (via tsconfig paths)

At runtime (JavaScript):

Your bundler (Next.js/Vite) resolves path aliases using tsconfig.json

In CRUD Kit package (dist files):

// dist/index.js - path alias preserved as external reference
import { Button } from '@/components/ui/button'

What if I get "Cannot find module '@/components/ui/button'"?

Cause: TypeScript path alias not configured or shadcn/ui component not installed.

Fix:

# 1. Verify tsconfig.json has paths
cat tsconfig.json  # Should show "@/*": ["./src/*"]

# 2. Install missing component
npx shadcn@latest add button

# 3. Verify component exists
ls src/components/ui/button.tsx

How do permissions work with ZenStack?

Two layers of security:

  1. Database-level (ZenStack access policies in .zmodel):

    model User {
      // ...
      @@allow('read', true)
      @@allow('create', auth() != null)
      @@allow('update', auth() == this)
      @@allow('delete', auth().role == 'admin')
    }
  2. UI-level (Resource permissions in defineResource):

    .withPermissions({
      canCreate: (session) => !!session?.user,
      canDelete: (session) => session?.user?.role === 'admin'
    })

Best practice: Rely on ZenStack for security, use Resource permissions for UX (hiding disabled actions).

Can I use CRUD Kit without ZenStack?

Not currently. CRUD Kit is designed to work with ZenStack-generated hooks.

However, you could potentially adapt it by:

  1. Creating hook adapters that match ZenStack's API
  2. Wrapping TanStack Query hooks manually

This is not officially supported. Consider using TanStack Table directly if you don't want ZenStack.

How do I customize the generated forms?

Forms are generated from Prisma schema metadata. To customize:

// Option 1: Use field metadata
defineResource<User>('User')
  .withFieldMeta({
    bio: {
      type: 'textarea',  // Override auto-detected type
      placeholder: 'Tell us about yourself...'
    }
  })

// Option 2: Use custom form components
<Crud.Form>
  <Crud.Field name="name" />
  <Crud.Field name="bio" as="textarea" rows={4} />
  <Crud.Field name="avatar" as={CustomFileUpload} />
</Crud.Form>

How do I add custom actions?

Use the actions system:

<Crud resource={userResource}>
  <Crud.Toolbar>
    <Crud.ToolbarRight>
      <Button onClick={customAction}>Custom Action</Button>
      <Crud.ExportButton />
    </Crud.ToolbarRight>
  </Crud.Toolbar>

  <Crud.Table>
    <Crud.Column field="name" label="Name" />
    <Crud.RowActions>
      <Crud.EditAction />
      <Crud.DeleteAction />
      <Button
        variant="ghost"
        size="sm"
        onClick={(e) => {
          e.stopPropagation()
          customRowAction(row.original)
        }}
      >
        Custom
      </Button>
    </Crud.RowActions>
  </Crud.Table>
</Crud>

Can I use CRUD Kit with Prisma directly (without ZenStack)?

CRUD Kit requires ZenStack because it relies on:

  • Auto-generated TanStack Query hooks (useFindManyUser, useCreateUser, etc.)
  • Model metadata for form generation
  • Type-safe model definitions

ZenStack is a thin layer over Prisma that adds:

  • Access control policies
  • React Query hooks generation
  • No runtime overhead for basic operations

Development

See DEV_GUIDE.md for local development setup with YALC.

Contributing

Contributions are welcome! Please read our contributing guidelines first.

License

MIT © Tallty

Links