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

@firstlovecenter/milestone-grid

v0.8.2

Published

Reusable Subject × Milestone red/green grid: completion gate, polymorphic requirement types (files, yes/no, text, number, date, select, URL), optional nested substages, admin CRUD, and ports for auth + storage. Host injects auth, storage, and subject list

Downloads

4,047

Readme

@firstlovecenter/milestone-grid

Reusable Subject × Milestone red/green grid:

  • Generic Subject × Milestone × Submilestone × Requirement × Submission × Status data model — host-injected subjectId is opaque (project, employee, missionary, anything).
  • Polymorphic requirements (0.6+): MEDIA / DOCUMENT file uploads, plus YES_NO, SHORT_TEXT, LONG_TEXT, NUMBER, DATE, SELECT, MULTI_SELECT, URL. Type-specific config + validation are built in.
  • Completion gate that enforces "all required slots filled" across a milestone AND its optional one-level substage.
  • Host-side adapters for auth, storage, and subject listing (no R2/S3 / NextAuth / Prisma coupling in the package).
  • React UI primitives — <MilestoneGrid />, <MilestoneCell />, <MilestoneSlots />, <RequirementSlot />, <SubmilestonePage />, <MilestoneAdminPanel />.
  • Route factories the host re-exports as Next.js API handlers.

Mirrors the patterns established by @firstlovecenter/ai-chat.

Requirement types

| Type | Storage | Notes | | -------------- | ----------------- | ---------------------------------------------------------------- | | MEDIA | object storage | presign → PUT → submission; images / video | | DOCUMENT | object storage | presign → PUT → submission; PDFs | | YES_NO | JSON value | true satisfies the gate, false does not | | SHORT_TEXT | JSON value | trimmed string; config.maxLength optional (default 280) | | LONG_TEXT | JSON value | trimmed string; config.maxLength optional (default 5000) | | NUMBER | JSON value | finite number; config.{min,max,step} optional | | DATE | JSON value | ISO YYYY-MM-DD string; config.{min,max} optional | | SELECT | JSON value | one of config.options[].value | | MULTI_SELECT | JSON value | non-empty subset of config.options[].value | | URL | JSON value | RFC 3986 URL |

Install

pnpm add @firstlovecenter/milestone-grid

1. Pick an ORM adapter

The package ships two persistence adapters; pick whichever matches your host:

Prisma — paste prisma/milestone-models.prisma into your host's schema.prisma, then pnpm prisma migrate dev.

import { createPrismaPersistence } from '@firstlovecenter/milestone-grid/server/prisma';
const persistence = createPrismaPersistence(prisma);

Drizzle (MySQL) — import the canonical tables and re-export from your host's schema, then run a drizzle migration:

// src/db/schema.ts
export {
  milestones,
  milestoneRequirements,
  subjectMilestoneStatus,
  milestoneSubmissions
} from '@firstlovecenter/milestone-grid/server/drizzle';
import { createDrizzlePersistence } from '@firstlovecenter/milestone-grid/server/drizzle';
const persistence = createDrizzlePersistence(db);

Both adapters speak the same PersistencePort, so you can swap between them without changing routes or UI. subjectId is a free String in both — no FK is declared by the package. Your host owns whatever entity it points at.

2. Configure once at boot

// src/lib/milestones/configure.ts
import { configureMilestoneGrid } from '@firstlovecenter/milestone-grid/server';
import { createPrismaPersistence } from '@firstlovecenter/milestone-grid/server/prisma';
import { prisma } from '@/src/lib/db';
import { authPort } from '@/src/lib/milestones/auth';
import { storagePort } from '@/src/lib/milestones/storage';

export const milestoneGrid = configureMilestoneGrid({
  persistence: createPrismaPersistence(prisma as unknown as PrismaLike),
  auth: authPort,
  storage: storagePort,
  subjects: {
    listSubjects: async () => {
      const rows = await prisma.project.findMany({
        where: { isActive: true },
        select: { id: true, name: true }
      });
      return rows.map((p) => ({ id: String(p.id), label: p.name }));
    }
  }
});

The three ports:

  • AuthPort<S>requireAuth(req), isAdmin(scope), userId(scope). The package never calls next-auth directly.
  • StoragePortpresignPut, streamGet, delete, stampKey, verifyKey. Wrap your R2/S3 helpers.
  • SubjectsPort<S>listSubjects(scope). Drives the grid's leftmost column.

3. Mount routes

Each Next route file is a thin re-export.

// app/api/milestone-grid/milestones/route.ts
import { milestoneGrid } from '@/src/lib/milestones/configure';
export const { GET, POST } = milestoneGrid.routes.milestones.list;

Repeat for milestones/[id], milestones/reorder, milestones/[id]/requirements, requirements/[id], subjects/[subjectId]/milestones/[milestoneId], subjects/[subjectId]/submissions, submissions/[id], uploads/presign, uploads/[id], grid.

4. Render the grid

'use client';
import { useEffect, useState } from 'react';
import {
  MilestoneGrid,
  createMilestoneClient,
  type GridSnapshot
} from '@firstlovecenter/milestone-grid/ui';
import '@firstlovecenter/milestone-grid/styles.css';
import '@firstlovecenter/milestone-grid/theme.css';

const client = createMilestoneClient();

export function GridPage() {
  const [data, setData] = useState<GridSnapshot | null>(null);
  useEffect(() => { void client.getGrid().then(setData); }, []);
  if (!data) return null;
  return (
    <MilestoneGrid
      subjects={data.subjects}
      milestones={data.milestones}
      requirements={data.requirements}
      statuses={data.statuses}
      filledCounts={data.filledCounts}
      submissions={data.submissions}
      onCellActivate={({ subjectId, milestoneId }) => {
        // open your modal here
      }}
    />
  );
}

5. Substages

A milestone may have at most one substage (Milestone.parentId points at the parent). Substages don't appear as their own columns in the grid; instead, the parent column's cell shows a "has substage" indicator, and clicking it opens the modal where the host renders the parent's slots plus an "Open substage" button that navigates to a dedicated page powered by <SubmilestonePage />.

The completion gate evaluates the parent's own slots and the substage's slots before allowing status=true.

Tailwind preset (optional)

The shipped dist/styles.css is precompiled and ready to import. Hosts that want to tree-shake utilities can extend the included Tailwind preset:

// tailwind.config.js
const milestoneGridPreset = require('@firstlovecenter/milestone-grid/tailwind-preset');
module.exports = { presets: [milestoneGridPreset], content: ['./app/**/*.tsx'] };

Override CSS variables in your own stylesheet to retheme:

:root {
  --mg-brand: oklch(0.58 0.2 145);
  --mg-success-bg: oklch(0.95 0.04 145);
}

Robustness note: inline styles on critical visuals

A few elements (the dropzone's dashed border, the modal/grid border, the frosted-glass background on sticky table cells) are styled via inline React style props rather than Tailwind classes. This is deliberate.

Many Tailwind v4 hosts ship a universal preflight override like:

@layer base {
  *,
  *::before,
  *::after {
    border-color: var(--border-default);
  }
}

A package that styled those visuals with classes like border-mg-fg-secondary would land in @layer utilities (correct), but the universal selector in @layer base still surfaces ahead in some cascade scenarios — the border-color would silently fall back to the host's --border-default and the dashed line would disappear. Inline styles win unconditionally, so the package renders the same in every host regardless of preflight setup. You can still re-theme via the --mg-* CSS variables (the inline styles reference them).