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

@binary1702/annotate

v0.6.5

Published

Figma-style comments attached to live DOM elements — plus private review links, a Postgres store and a drop-in Next.js integration. Visual website annotation / client feedback for React.

Readme

@binary1702/annotate

Figma-style comments, attached to live DOM elements.

Turn on Review Mode, point at anything on your site, click, leave a comment. A numbered pin stays attached to that element; anyone can open it, reply, and resolve it. Turn Review Mode off and the site behaves exactly as before.

  • Works with any React app — Next.js, Vite, CRA, Remix. No framework imports.
  • You own auth, identity and storage. The library talks to a five-method adapter.
  • Zero DOM mutation: outlines and pins are drawn on a portal layer, never on your elements.
  • Zero overhead while disabled: no listeners, no fetches, no DOM.
npm install @binary1702/annotate

Quick Start (Next.js + Postgres)

npm install @binary1702/annotate @neondatabase/serverless
npx @binary1702/annotate init

The CLI will:

  1. Connect to your Postgres database
  2. Create the annotations table
  3. Generate reviewer tokens
  4. Write everything to .env.local

Then wire up these files:

// src/review.ts
import { neon } from "@neondatabase/serverless";
import { postgresStore } from "@binary1702/annotate/postgres";

const sql = neon(process.env.DATABASE_URL!);
export const store = postgresStore((t, p) => sql(t, p), process.env.ANNOTATE_PROJECT_ID!);
// src/middleware.ts
import { createReviewProxy } from "@binary1702/annotate/next/proxy";
export const middleware = createReviewProxy();
export const config = { matcher: ["/((?!api|_next|.*\\.[a-zA-Z0-9]+$).*)"] };
// src/app/api/review/[...route]/route.ts
import { createReviewHandlers } from "@binary1702/annotate/next/handlers";
import { store } from "@/review";
export const { GET, POST } = createReviewHandlers({ store });
// src/app/layout.tsx
import "@binary1702/annotate/styles.css";
import { ReviewMode } from "@binary1702/annotate/next";
// …
<body><ReviewMode>{children}</ReviewMode></body>
// src/app/review/page.tsx — owner's inbox (optional)
import { createReviewInbox } from "@binary1702/annotate/next/inbox";
import { store } from "@/review";
export const dynamic = "force-dynamic";
export default createReviewInbox({ store });

Send each reviewer their link: https://your-site.com?r=<token>. Done.

How the pieces fit

/?r=<token> ─▶ next/proxy ─▶ httpOnly cookie ─▶ <ReviewMode> asks /api/review/me
                                                     401 → plain site (public visitors)
                                                     200 → overlay mounts as "Carol"
click element → comment ─▶ next/handlers ─▶ server (identity from cookie, never payload)
                                             └─▶ postgres store (project_id scoped)
/review ─▶ next/inbox (admin tokens) ─▶ every thread, by page, filter by reviewer/status

Pages stay statically rendered — identity resolves client-side after load.

Subpath map

| Import | Runs | Contains | | --- | --- | --- | | @binary1702/annotate | client | overlay, provider, hook, memory adapter (framework-free) | | @binary1702/annotate/server | server, edge | createIdentity, createReviewService, createReviewApi (Fetch Request → Response), createMemoryStore | | @binary1702/annotate/postgres | server | postgresStore(query, projectId) — bring any driver; schema.sql alongside | | @binary1702/annotate/next | client | <ReviewMode>, <ReviewBar>, createFetchAdapter | | @binary1702/annotate/next/handlers | server | createReviewHandlers | | @binary1702/annotate/next/proxy | edge | createReviewProxy | | @binary1702/annotate/next/inbox | server | createReviewInbox |

Not on Next? createReviewApi({ store }).handle(request) is a plain Fetch handler — mount it in Remix, Hono, Bun or a Worker, and use createIdentity() + createFetchAdapter() with your own provider.

Smallest possible integration

import { AnnotationProvider, createMemoryAnnotationAdapter } from "@binary1702/annotate";
import "@binary1702/annotate/styles.css";

const adapter = createMemoryAnnotationAdapter(); // swap for your backend adapter (below)

export function Root() {
  const [reviewMode, setReviewMode] = useState(false);

  return (
    <AnnotationProvider
      enabled={reviewMode}
      onEnabledChange={setReviewMode}
      currentUser={{ id: "u1", name: "Gladys" }}
      adapter={adapter}
    >
      <button onClick={() => setReviewMode((v) => !v)} data-annotation-ui="">
        Review Mode
      </button>
      <App />
    </AnnotationProvider>
  );
}

That's the whole thing. The overlay is mounted automatically by the provider.

Then, in your UI, tag the elements clients are most likely to comment on:

<button data-annotation-id="homepage.hero.cta">Start a Project</button>

How it fits together

Your app                             @binary1702/annotate
──────────────────────────────       ─────────────────────────────────────────
auth / current user   ───────────▶   currentUser
review-mode toggle    ───────────▶   enabled (controlled)
router pathname       ───────────▶   pageId
backend / DB          ◀── adapter ─  list / create / reply / resolve / reopen
                                     │
                                     ├─ hover outline, click interception
                                     ├─ target identification + relocation
                                     ├─ pins, composer, thread UI
                                     └─ positioning, scroll/resize tracking

1. Provider

<AnnotationProvider
  adapter={adapter}                 // required — see "Persistence"
  currentUser={user}                // required — { id, name, avatarUrl? }
  enabled={reviewMode}              // controlled; omit for uncontrolled + defaultEnabled
  onEnabledChange={setReviewMode}   // called when the package asks to toggle (useAnnotations().setEnabled)
  pageId={pathname}                 // or getPageId={() => window.location.pathname}
  showResolved={false}              // keep resolved pins visible (muted) instead of hiding them
  renderOverlay                     // default true; set false and render <AnnotationOverlay /> yourself
  ignore={(el) => el.closest(".no-comments") !== null}   // optional extra exclusions
>

enabled is controlled-first. Review Mode is usually tied to a permission or a URL flag your app already owns, so the flag lives in your state and the provider mirrors it. useAnnotations().setEnabled exists for convenience (e.g. a toolbar inside the annotated app) and simply calls onEnabledChange; omit enabled entirely and the provider manages it for you.

Page identity. Annotations belong to a page string. Pass whatever your router gives you:

// Next.js (app router)
"use client";
import { usePathname } from "next/navigation";
const pathname = usePathname();
<AnnotationProvider pageId={pathname} …>

When pageId changes the provider reloads that page's annotations. Nothing from Next.js is imported by the package.

2. Review Mode

While enabled is true:

  • Hovering shows a thin outline around the element under the pointer.
  • Clicking is intercepted in the capture phase — buttons don't fire, links don't navigate, your React handlers never run — and a comment composer opens next to the element. Esc cancels, ⌘/Ctrl+Enter submits.
  • Saved comments appear as numbered pins on the element's top-right corner. Pins follow scroll and resize.
  • Clicking a pin (or the element again) opens the thread: reply, resolve, reopen.
  • Elements marked data-annotation-ui keep working normally — put it on your Review Mode toggle.
  • <html data-annotation-mode> is set so you can style your own chrome while reviewing.

While enabled is false the overlay is unmounted: no listeners, no adapter calls, no DOM.

One thread per element. Clicking an element that already has a thread opens it rather than creating a second pin. Simpler for reviewers, and the pin number becomes a stable reference ("see #3").

3. data-annotation-id — please use it

The hard problem in this library is finding the element again after a re-render, a deploy, or a copy change. Targets are stored with four levels of fallback:

| Priority | Stored as | Survives… | | --- | --- | --- | | 1 | data-annotation-id="homepage.hero.cta" | anything short of removing the attribute | | 2 | generated selector (#id, [data-testid], [aria-label], a[href], [role]…) | structural changes, not attribute changes | | 3 | DOM path (#hero > div:nth-of-type(2) > button:nth-of-type(1)) | only cosmetic changes | | 4 | tagName + textSnippet + ariaLabel + last known rect (heuristic recovery) | path changes, if the text is intact |

Levels 2–4 are best effort. Level 1 is the only one that is yours — it means a comment on the hero CTA is still on the hero CTA after the marketing team rewrites the copy, the designer restructures the section, and the engineer swaps the component library. Tag the things clients comment on: headings, CTAs, cards, nav items, forms.

// plain attribute (recommended — nothing to import)
<button data-annotation-id="homepage.hero.cta">Start a Project</button>

// spread helper
import { annotationTarget } from "@binary1702/annotate";
<button {...annotationTarget("homepage.hero.cta")}>Start a Project</button>

// wrapper (child must forward unknown props to its root element)
import { Annotatable } from "@binary1702/annotate";
<Annotatable id="homepage.hero.cta">
  <Button>Start a Project</Button>
</Annotatable>

Use dotted, page-scoped names (pricing.plans.pro.cta) and never reuse an id on the same page — the resolver requires exactly one match.

4. Persistence — the adapter

The library never touches a database. Implement five methods:

import type { AnnotationAdapter } from "@binary1702/annotate";

export const adapter: AnnotationAdapter = {
  list: (page) => api.get(`/annotations?page=${encodeURIComponent(page)}`),

  create: (input) => api.post("/annotations", input),
  //  input: { page, target, body, author }  →  returns Annotation (with server id)

  reply: (annotationId, input, ctx) => api.post(`/annotations/${annotationId}/comments`, input),
  //  input: { body, author }                 →  returns AnnotationComment

  resolve: (annotationId, ctx) => api.post(`/annotations/${annotationId}/resolve`),
  reopen: (annotationId, ctx) => api.post(`/annotations/${annotationId}/reopen`),
};

ctx is { user, page } for the acting user, in case your API needs it. Errors thrown by any method are shown inline in the UI (and resolve/reopen are rolled back).

Example backend (pseudocode)

Two tables, nothing clever:

create table annotations (
  id          text primary key,
  page        text not null,
  target      jsonb not null,        -- AnnotationTarget, stored verbatim
  status      text not null default 'open',
  created_at  timestamptz not null default now(),
  updated_at  timestamptz,
  created_by  jsonb not null,        -- AnnotationUser
  resolved_at timestamptz,
  resolved_by jsonb,
  metadata    jsonb
);
create index on annotations (page);

create table annotation_comments (
  id            text primary key,
  annotation_id text references annotations(id) on delete cascade,
  body          text not null,
  created_at    timestamptz not null default now(),
  created_by    jsonb not null
);
// GET /annotations?page=/pricing
rows = select * from annotations where page = $page order by created_at
each row.comments = select * from annotation_comments where annotation_id = row.id order by created_at
return rows                                        // shape === Annotation[]

// POST /annotations            body: CreateAnnotationInput
assert session.user.id === body.author.id          // identity is yours to enforce
annotation = insert annotations {...}
comment    = insert annotation_comments { body: body.body, created_by: body.author }
return { ...annotation, comments: [comment] }

// POST /annotations/:id/comments   body: CreateCommentInput
return insert annotation_comments { annotation_id: id, body, created_by: body.author }

// POST /annotations/:id/resolve | /reopen
update annotations set status = 'resolved' | 'open', resolved_at = now() | null, resolved_by = user | null

For Next.js the natural home is a route handler (app/api/annotations/route.ts) or server actions; the adapter is then a thin fetch wrapper living in a client component.

Memory adapter (dev / tests)

const adapter = createMemoryAnnotationAdapter({ latency: 250, initial: [] });
adapter.dump(); adapter.clear();

Data lives in a Map for the lifetime of the page. It is for demos and tests, not persistence.

5. Hook

const {
  enabled, setEnabled,
  page,
  annotations, openAnnotations, resolvedAnnotations,
  selectedAnnotation, select,      // open a thread programmatically: select(id) / select(null)
  refresh, loading, error,
  resolve, reopen,
} = useAnnotations();

Handy for a "3 open comments" badge on your Review Mode toggle.

Styling

The stylesheet is scoped under the portal root and driven by CSS variables. Override on :root (or on [data-annotation-root]):

:root {
  --annotation-accent: #4f46e5;
  --annotation-accent-contrast: #fff;
  --annotation-bg: #fff;
  --annotation-text: #111827;
  --annotation-muted: #6b7280;
  --annotation-border: #e5e7eb;
  --annotation-radius: 10px;
  --annotation-shadow: 0 10px 30px rgba(0,0,0,.14);
  --annotation-font: system-ui, sans-serif;
  --annotation-z-index: 2147483000;
  --annotation-resolved: #10b981;
  --annotation-danger: #dc2626;
}

No Tailwind, no runtime CSS-in-JS. Skip the stylesheet entirely and target the .bna-* classes if you want a bespoke look.

SSR / Next.js

Safe to import anywhere. window/document are only touched inside effects and event handlers; on the server the provider renders its children and nothing else. The provider must live in a client component ("use client") because it holds state, like any context provider.

Data model

interface Annotation {
  id: string;
  page: string;
  target: AnnotationTarget;
  status: "open" | "resolved";
  createdAt: string;               // ISO-8601
  updatedAt?: string;
  createdBy: AnnotationUser;
  resolvedAt?: string;
  resolvedBy?: AnnotationUser;
  comments: AnnotationComment[];   // [0] is the original comment
  metadata?: Record<string, unknown>;   // yours; the library never reads it
}

interface AnnotationTarget {
  annotationId?: string;   // data-annotation-id
  selector?: string;
  domPath?: string;
  tagName: string;
  textSnippet?: string;
  ariaLabel?: string;
  rect?: { x: number; y: number; width: number; height: number };   // document coords
}

interface AnnotationComment { id: string; body: string; createdAt: string; createdBy: AnnotationUser }
interface AnnotationUser    { id: string; name: string; avatarUrl?: string }

Everything is plain JSON.

Accessibility

Pins and card controls are real <button>s with labels; the composer autofocuses; Esc closes; focus returns to where it was. In Review Mode, Enter/Space on a focused page element selects it for commenting instead of activating it.

Development

npm install
npm run dev        # demo at http://localhost:5173
npm test
npm run typecheck
npm run build      # dist/index.js + index.d.ts + styles.css

CLI

npx @binary1702/annotate init      # interactive setup
npx @binary1702/annotate schema    # print schema.sql to stdout

init prompts for DATABASE_URL, connects, creates the table, generates reviewer tokens, and writes .env.local.

Changelog

  • 0.3.0 — CLI (npx @binary1702/annotate init) for interactive setup with automatic schema migration.
  • 0.2.2 — "Leave review mode" link in the bar (?r=off).
  • 0.2.1 — <ReviewMode> remembers the Commenting on/off choice across pages and reloads; memory store warns when used outside tests.
  • 0.2.0 — server, postgres and next/* subpaths: private review links, Postgres store, drop-in Next.js integration, inbox.
  • 0.1.0 — overlay, provider, adapter contract.

Deliberately not in V1

Screenshots, drawing, video, attachments, assignees, priorities, Jira/GitHub/Slack integrations, AI summaries, real-time sync, browser extension, mobile SDK. The data model leaves room (metadata, adapter methods are additive) but none of it is built.

License

MIT © Binary 1702 LLC