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

@snapsite/cms-editor

v0.1.1

Published

Embeddable headless-CMS editor for @snapsite client sites — CMSProvider, EditableText, EditableImage, EditableSection, MediaPicker, Shadow-DOM toolbar.

Readme

@snapsite/cms-editor

Embeddable, React-based in-page editor for SnapSite client sites. Wraps your app in a provider, lets signed-in owners switch on edit mode and modify content inline, and renders all editor chrome inside a Shadow DOM so nothing leaks into (or is affected by) your site's CSS.

Install

pnpm add @snapsite/cms-editor @snapsite/cms-core
# peer deps
pnpm add react react-dom

Usage

// src/main.tsx
import { createRoot } from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import {
  CMSProvider,
  CMSLogin,
  EditableText,
  EditableImage,
  EditableSection,
} from "@snapsite/cms-editor";
import { defineSection } from "@snapsite/cms-editor";
import { z } from "zod";

// Generated by @snapsite/cms-bake at build time.
import { BAKED_CONTENT } from "./generated/content";

const heroSection = defineSection({
  type: "hero",
  label: "Hero",
  schema: z.object({
    heading: z.string(),
    subheading: z.string(),
    cta: z.object({ label: z.string(), href: z.string().url() }).optional(),
  }),
  defaults: { heading: "Welcome", subheading: "We build things people love." },
  render: ({ data }) => (
    <section>
      <h1>{data.heading}</h1>
      <p>{data.subheading}</p>
    </section>
  ),
});

function App() {
  return (
    <CMSProvider
      siteId={import.meta.env.VITE_CMS_SITE_ID}
      supabaseUrl={import.meta.env.VITE_SUPABASE_URL}
      supabaseAnonKey={import.meta.env.VITE_SUPABASE_ANON_KEY}
      initialContent={BAKED_CONTENT}
      sections={[heroSection]}
    >
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/cms-login" element={<CMSLogin redirectTo="/" />} />
        </Routes>
      </BrowserRouter>
    </CMSProvider>
  );
}

function Home() {
  return (
    <main>
      <EditableText path="/home/hero/heading" as="h1">
        Welcome to our site
      </EditableText>
      <EditableImage path="/home/hero/image" alt="Hero" />
      <EditableSection path="/home/sections" />
    </main>
  );
}

createRoot(document.getElementById("root")!).render(<App />);

What the consumer sees

  • Anonymous visitors: the page renders exactly from initialContent. No editor UI. Disable JavaScript and the baked text is still in the HTML.
  • Authenticated owner: a small floating toggle appears bottom-right. Click it to enter edit mode — a toolbar slides in at bottom-center, every <EditableText> picks up a dashed outline on hover, every <EditableImage> reveals a "Click to replace" overlay, and EditableSection exposes per-section move / publish / delete / duplicate / edit actions. Click, type, blur to save.
  • Authenticated non-owner: same as anonymous — you only see editor UI if you have a site_users membership for this siteId.

All chrome renders inside a Shadow DOM attached to document.body, so the host site's global CSS can't affect its layout, and its rules can't leak back out.

Public API

| Export | Type | Description | | --- | --- | --- | | CMSProvider | component | Wrap your app. Props: siteId, supabaseUrl, supabaseAnonKey, initialContent?, sections?, errorReporter?, children. | | EditableText | component | Inline-editable plain-text field. Props: path, as?, placeholder?, children?, className?. | | EditableImage | component | Click-to-replace image. Props: path, alt?, className?, fallbackSrc?, style?. | | EditableSection | component | Renders a sortable list of sections at path from your defineSection registry. | | CMSLogin | component | Login form: magic link + optional password. | | EditModeToggle | component | Standalone toggle — CMSProvider already renders one. | | defineSection | helper | Type-safe section definition (Zod schema, defaults, optional custom form / render). | | useCMS() | hook | { state, dispatch, client, siteId, runSave, lastFailedSave, ... }. | | useContent(path) | hook | Current value for a content path. | | useSections(path) | hook | Ordered list of sections at path. |

How saves work

  • All edits route through runSave(label, fn) on the useCMS() context, which owns the SAVE_STARTED / SAVE_COMPLETED / SAVE_FAILED lifecycle.
  • On success the optimistic update sticks. On failure the user's input is preserved (no revert), the toolbar shows a Retry button that re-fires the original mutation closure, and the unsaved-changes guard prompts on sign-out.
  • All save mutations are gated by Supabase row-level security; non-owners get a clean error and no data moves.

Error reporting

CMSProvider installs a cms_errors reporter automatically (writes failures to public.cms_errors via log_cms_error). Pass an extra errorReporter prop to fan out to Sentry et al:

import { createSentryReporter } from "@snapsite/cms-core";
import * as Sentry from "@sentry/browser";

<CMSProvider
  ...
  errorReporter={createSentryReporter(Sentry.captureException)}
>

Sentry init (DSN, sample rates) is the host's responsibility; this just wires the sink.

Session lifecycle

CMSProvider creates a single Supabase client via createAnonymousClient(..., { persistSession: true }). When CMSLogin calls signInWithPassword or signInWithMagicLink, the session lands in localStorage. On the next page load the same provider picks it up, checks ownership, and mounts the chrome.

Cross-domain handoff (Prompt 6)

For the dashboard to "Edit this site" without a second login, it mints a single-use handoff token. The host site exposes a /cms-handoff route that exchanges the token for a Supabase session and redirects to /?cms-edit=1. See the dashboard's handoff.ts and the exchangeEditorHandoffToken API in @snapsite/cms-core.

License

MIT