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

@withwiz/gallery

v0.1.0

Published

Headless gallery module — Prisma 7 + Next 16, host-agnostic. Drop-in admin + public surface.

Readme

@withwiz/gallery

English | 한국어

Host-independent gallery module for Next.js 16 + Prisma 7. Self-contained admin UI, headless lightbox, drop-in API route factories.

Status

v0.1.0 — Sprints 1–6 complete (@withwiz/gallery scaffold + validators + services + server layer + hooks/primitive UI + admin composite UI + Prisma partial + ballet preset + README). Sprint 7 (ballet migration) not yet executed.

Features

  • Headless — drop into any Next.js host via setGalleryConfig (Prisma client / API wrapper / storage / revalidate / i18n / limits).
  • Admin UI — 5 composite components (GalleryAdminManager, GalleryEditForm, GalleryHomePreview, GalleryManagerLayout, CategoryAdminManager) + 2 primitives (ImageDropZone, ToggleSwitch). Self-contained 3-pane layout.
  • API routescreateGalleryRoutes(config) returns Next.js Route Handlers for 6 endpoint groups (collection / item / publish toggle / bulk / category collection / category item).
  • RSC loadersgetGalleryItems, getFeaturedGalleries, getRecentGalleries, getGalleryCount for server components / dashboards.
  • Public presetPublicGalleryMosaic (presets/ballet) — 1–7 tile adaptive mosaic + lightbox.
  • Headless lightbox hookuseGalleryLightbox with ESC / Arrow key bindings + wrap-around.
  • Image upload primitiveuseImageDropZone + <ImageDropZone> (host-side validate + accept/maxSize).
  • Storage-agnostic — R2 / S3 / local filesystem — host injects via config.storage.
  • Host-model-name-agnostic — the Prisma Gallery model makes no assumption about the host's User/Admin/Account model name. Only an authorId: String column.
  • Typed error hierarchyGalleryNotFoundError, CategoryNotFoundError, CategoryInUseError, PermissionDeniedError for the host to catch and handle.

Install

Currently consumed via file: reference inside the monorepo (not yet published to npm):

# host root
npm install file:../node-packages/withwiz-gallery

After publishing:

npm install @withwiz/gallery

Peer dependencies

| Package | Range | |---|---| | next | >=16 | | react / react-dom | >=19 | | @prisma/client | >=7 | | zod | >=4 | | clsx | >=2 | | tailwind-merge | >=3 | | sonner | >=2 (optional) |

Quick start

1. Merge the Prisma schema

Copy or symlink this package's partial schema into the host's prisma/ directory.

// host package.json
{
  "prisma": { "schema": "./prisma" }
}
# in the host
cp node_modules/@withwiz/gallery/prisma/gallery.schema.prisma prisma/
npx prisma generate
npx prisma migrate dev --create-only

The partial defines two models (GalleryCategory, Gallery) plus indexes and @@map. Because authorId is a plain String column (no @relation), the host's User-model name is irrelevant and the multi-file schema validates cleanly.

Migrating from a ballet-style enum-based schema? See Migration from ballet enum-based schema below.

2. setGalleryConfig (host bootstrap)

Call this at module top-level — client components invoke getGalleryConfig() on mount.

// host: lib/gallery-config.ts
import { setGalleryConfig } from "@withwiz/gallery/server";
import type { GalleryConfig } from "@withwiz/gallery/server";
import { prisma } from "./prisma";
import { withAdminApi } from "@/lib/api-middleware"; // host's auth wrapper
import { isR2Enabled, collectR2Keys, deleteR2Keys } from "@/lib/r2";
import { revalidatePath } from "next/cache";

export const galleryConfig: GalleryConfig = {
  prisma,
  apiWrapper: withAdminApi,
  authorIdFromContext: (ctx) => ctx.user!.id,
  limits: {
    maxFeatured: 7,
    mosaicCount: 7,
    batchMax: 20,
    captionMaxLength: 200,
  },
  revalidate: revalidatePath,
  revalidatePaths: ["/", "/home/v1"],
  storage: {
    isEnabled: isR2Enabled,
    collectKeys: collectR2Keys,
    deleteKeys: deleteR2Keys,
  },
  i18n: {
    "admin.title": "Gallery",
    "admin.newButton": "New image",
    "form.caption": "Caption",
    "form.save": "Save",
    "form.cancel": "Cancel",
    // see @withwiz/gallery/types → GalleryI18nKey for the full key list
  },
};

setGalleryConfig(galleryConfig);

3. Mount route handlers

Thin re-export per endpoint.

// host: app/api/admin/galleries/route.ts
import { createGalleryRoutes } from "@withwiz/gallery/server";
import { galleryConfig } from "@/lib/gallery-config";

const { collection } = createGalleryRoutes(galleryConfig);
export const { GET, POST, DELETE } = collection;
// host: app/api/admin/galleries/[id]/route.ts
import { createGalleryRoutes } from "@withwiz/gallery/server";
import { galleryConfig } from "@/lib/gallery-config";

const { item } = createGalleryRoutes(galleryConfig);
export const { GET, PUT, DELETE } = item;
// host: app/api/admin/galleries/[id]/publish/route.ts
import { createGalleryRoutes } from "@withwiz/gallery/server";
import { galleryConfig } from "@/lib/gallery-config";

const { publishToggle } = createGalleryRoutes(galleryConfig);
export const { PATCH } = publishToggle;
// host: app/api/admin/galleries/bulk/route.ts
import { createGalleryRoutes } from "@withwiz/gallery/server";
import { galleryConfig } from "@/lib/gallery-config";

const { bulk } = createGalleryRoutes(galleryConfig);
export const { POST, PATCH } = bulk;
// host: app/api/admin/gallery-categories/route.ts
import { createGalleryRoutes } from "@withwiz/gallery/server";
import { galleryConfig } from "@/lib/gallery-config";

const { categoryCollection } = createGalleryRoutes(galleryConfig);
export const { GET, POST } = categoryCollection;
// host: app/api/admin/gallery-categories/[id]/route.ts
import { createGalleryRoutes } from "@withwiz/gallery/server";
import { galleryConfig } from "@/lib/gallery-config";

const { categoryItem } = createGalleryRoutes(galleryConfig);
export const { GET, PUT, DELETE } = categoryItem;

4. Mount the admin UI

// host: app/admin/galleries/page.tsx
import { GalleryAdminManager } from "@withwiz/gallery/components";
import "@withwiz/gallery/components/gallery.css";
import "@/lib/gallery-config"; // setGalleryConfig as a module side effect

export default function GalleryAdminPage() {
  return <GalleryAdminManager />;
}
// host: app/admin/galleries/new/page.tsx
import { GalleryAdminManager } from "@withwiz/gallery/components";
import "@/lib/gallery-config";

export default function GalleryNewPage() {
  return <GalleryAdminManager initialMode="new" />;
}
// host: app/admin/galleries/[id]/page.tsx
import { GalleryAdminManager } from "@withwiz/gallery/components";
import "@/lib/gallery-config";

export default async function GalleryEditPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  return <GalleryAdminManager initialSelectedId={id} />;
}
// host: app/admin/gallery-categories/page.tsx
import { CategoryAdminManager } from "@withwiz/gallery/components";
import "@/lib/gallery-config";

export default function CategoryAdminPage() {
  return <CategoryAdminManager />;
}

5. Public gallery (RSC + client)

// host: app/page.tsx (RSC)
import { PublicGalleryMosaic } from "@withwiz/gallery/presets/ballet";
import { getFeaturedGalleries } from "@withwiz/gallery/server";
import { galleryConfig } from "@/lib/gallery-config";
import "@withwiz/gallery/components/gallery.css";

export default async function HomePage() {
  const featured = await getFeaturedGalleries(galleryConfig, 7);
  const images = featured.map((g) => ({
    src: g.imageUrl,
    alt: g.caption ?? "Gallery",
  }));
  return (
    <PublicGalleryMosaic
      images={images}
      count={7}
      i18n={{ sectionLabel: "Gallery", moments: "Moments from the stage" }}
    />
  );
}

API reference

@withwiz/gallery (main entry)

import {
  setGalleryConfig,
  getGalleryConfig,
  cn,
  getVariantUrl,
  GalleryError,
  GalleryNotFoundError,
  CategoryNotFoundError,
  CategoryInUseError,
  PermissionDeniedError,
} from "@withwiz/gallery";

@withwiz/gallery/server

| Export | Signature | |---|---| | setGalleryConfig(config) | DI setter — call once at module top-level | | getGalleryConfig() | Returns the registered config (throws if unset) | | createGalleryService(config) | CRUD object (list / get / create / update / remove / bulk*) | | createCategoryService(config) | Category CRUD (list / getBySlug / create / update / remove / reorder) | | createGalleryRoutes(config) | 6 Next.js Route Handler groups (collection / item / publishToggle / bulk / categoryCollection / categoryItem) | | getGalleryItems(config, opts) | RSC pagination loader | | getFeaturedGalleries(config, limit?) | RSC featured list | | getRecentGalleries(config, limit) | RSC recent items (dashboard) | | getGalleryCount(config, opts?) | RSC count (dashboard) | | buildPaginatedResult(items, page, limit, total) | helper |

Typed errors (also re-exported from the server entry):

  • GalleryError (base)
  • GalleryNotFoundError
  • CategoryNotFoundError
  • CategoryInUseError
  • PermissionDeniedError

@withwiz/gallery/components

| Export | Description | |---|---| | GalleryAdminManager | 3-pane admin mount point (initialMode? / initialSelectedId?) | | GalleryManagerLayout | 3-pane primitive (left = list / center = form / right = preview) | | GalleryEditForm | Single/multi form (value / multipleMode / onSubmit / onSubmitMany) | | GalleryHomePreview | 7-tile mosaic + drag reorder + featured toggle | | CategoryAdminManager | Category CRUD UI | | ImageDropZone | Image dropzone (accept / maxSize / multiple / disabled / validate) | | ToggleSwitch | size sm/md/lg, ARIA switch role |

CSS (host imports explicitly):

import "@withwiz/gallery/components/gallery.css";

@withwiz/gallery/hooks

| Export | Signature | |---|---| | useGalleryLightbox(images) | { isOpen, currentIndex, current, open, close, next, prev } — auto-bound ESC / Arrow, wrap-around | | useImageDropZone(opts) | Headless dropzone — inputProps / containerProps / files / rejectedReasons / clear | | useScrollReveal(opts?) | IntersectionObserver-based fade-in — { ref, isVisible } |

@withwiz/gallery/validators

import { createGallerySchemas } from "@withwiz/gallery/validators";

const schemas = createGallerySchemas({
  batchMax: 20,
  captionMaxLength: 200,
});
// schemas.CreateGallerySchema / UpdateGallerySchema / BatchCreateGallerySchema
// / BulkUpdateSchema / CreateCategorySchema / UpdateCategorySchema / ReorderCategorySchema

@withwiz/gallery/types

import type {
  GalleryConfig,
  GalleryI18nKey,
  GallerySlotName,
  GalleryListItem,
  GalleryDetail,
  GalleryCategoryItem,
  CreateGalleryInput,
  UpdateGalleryInput,
  CreateCategoryInput,
  UpdateCategoryInput,
  PaginatedResult,
  PaginationMeta,
  SortOrder,
  PrismaLike,
  ApiContext,
  RouteHandler,
  ApiWrapper,
} from "@withwiz/gallery/types";

@withwiz/gallery/presets/ballet

import { PublicGalleryMosaic } from "@withwiz/gallery/presets/ballet";
import type { PublicGalleryMosaicProps } from "@withwiz/gallery/presets/ballet";

Signature:

function PublicGalleryMosaic(props: {
  images: { src: string; alt?: string }[];
  count?: number;          // default 7
  i18n?: {
    sectionLabel?: string;
    moments?: string;
    expandAria?: string;
    lbClose?: string;
    lbPrev?: string;
    lbNext?: string;
  };
  className?: string;
  scrollReveal?: boolean;  // default true
  hideHeader?: boolean;    // default false
}): JSX.Element | null;

Host-independence guarantees

  • Zero @withwiz/pms imports — this package does not depend on any domain package.
  • Zero direct process.env reads — environment variables are read by the host and bound into config.storage, etc.
  • No assumption about the host's User model name — the Prisma Gallery model has only an authorId: String column and no @relation.
  • All infrastructure is injected via GalleryConfig — Prisma client / API wrapper / storage / revalidate / i18n / limits.
  • Storage / auth / revalidate are the host's responsibility — this package only deals with keys/paths; actual R2/S3/local calls are delegated to the host via config.storage.deleteKeys, etc.
  • presets/ballet does not import next/image, next/navigation, or useI18n — images use plain <img>, labels come from props.

CSS customization

Override the CSS variables from the host's root CSS.

/* host: app/globals.css */
:root {
  --gallery-accent: 212 175 55;   /* RGB triplet (NOT hex) */
  --gallery-bg: 10 10 10;
  --gallery-fg: 254 254 254;
  --gallery-border: 30 30 30;
}

These four variables are defined as fallbacks inside :where(.gallery-toggle, .gallery-dropzone, .gallery-manager, .gallery-edit-form, .gallery-home-preview, .gallery-category-admin, .gallery-public-mosaic, .gallery-lightbox, ...), so the package works out of the box (dark tone + gold accent) without any host override.

The admin components also support per-slot customization via config.ui.classNames[slot] / config.ui.slots[slot].

Migration from ballet enum-based schema

ballet's existing schema:

enum GalleryCategory { PERFORMANCE | REHEARSAL | ACTIVITY | ARTIST }
model Gallery {
  category GalleryCategory
  // ...
}

This package's new schema replaces the enum with a gallery_categories table + a Gallery.categoryId FK. Migration steps:

  1. Merge this package's gallery.schema.prisma into the host's prisma/ (copy or symlink).
  2. Run npx prisma migrate dev --create-only to auto-generate the migration SQL.
  3. Manually remove DROP COLUMN category / DROP TYPE "GalleryCategory" from the generated SQL (separate them into a follow-up migration).
  4. Insert the contents of prisma/migrations/2026-05-24-enum-to-table.sql (shipped with this package) at that location — seeds the 4 categories, backfills galleries.category_id, and includes a PL/pgSQL verification block.
  5. npx prisma migrate dev to apply.
  6. Once verification passes, run the second migration (separate file) to DROP the enum column and type.

Reference the shipped prisma/migrations/2026-05-24-enum-to-table.sql as-is.

The ballet code (src/components/sections/Gallery.tsxgallery-mosaic / lightbox-* classes) and this package (gallery-public-mosaic / gallery-lightbox__* classes) use different prefixes and are isolated. During ballet migration, remove those classes from the existing main.css and import only this package's gallery.css.

Architecture

@withwiz/toolkit            (lowest)
   ↑
@withwiz/pms                (performance-management domain CMS)
   ↑                        ↑
   │                        │ consumed as a host
host app (ballet, yeroom)   │
   ↓                        │
   └─→ @withwiz/gallery ┘  (this package — depends on no host)

@withwiz/gallery depends on no host project (ballet, yeroom, etc.), no domain package (@withwiz/pms, etc.), and no host domain model (User, Admin, Account, etc.). Whatever the host uses (Next 16 / Prisma 7 assumed), a single setGalleryConfig call integrates it.

Repository layout

node-packages/withwiz-gallery/
├── package.json
├── tsup.config.ts
├── README.md
├── prisma/
│   ├── gallery.schema.prisma            # GalleryCategory + Gallery partial models
│   └── migrations/
│       └── 2026-05-24-enum-to-table.sql # ballet enum → table backfill
├── docs/superpowers/{specs,plans}/      # design docs + sprint contracts/reports
└── src/
    ├── index.ts              # main entry — config, types, errors, utils
    ├── config.ts
    ├── errors.ts
    ├── types/                # GalleryConfig / domain / pagination / api-context / i18n
    ├── validators/           # createGallerySchemas
    ├── services/             # createGalleryService / createCategoryService / helpers
    ├── server/               # createGalleryRoutes + loaders + server re-exports
    ├── components/           # admin UI + ImageDropZone + ToggleSwitch + gallery.css
    ├── hooks/                # useGalleryLightbox / useImageDropZone / useScrollReveal
    ├── utils/                # cn / image-variants / api-helpers
    └── presets/
        └── ballet.tsx        # PublicGalleryMosaic

Scripts

npm run build       # tsup multi-entry CJS/ESM/DTS
npm test            # vitest run (221 tests, 23 files)
npm run test:watch  # vitest watch

License

MIT