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

@singleton-sd/post-kit-editor

v0.7.0

Published

React admin editor component for PostKit email templates

Readme

@singleton-sd/post-kit-editor

React admin editor component for PostKit email templates. It edits the three Git-backed source files (template.json, metadata.json, preview.json) in memory, with Save and optional Send-test controls that call consumer-supplied callbacks. The package never writes to disk, Git, or the network, and never accepts API keys or other credentials as props.

Installation

pnpm add @singleton-sd/post-kit-editor

React is a peer dependency — the consumer application owns the React instance:

pnpm add react@^18.3.1 react-dom@^18.3.1

Usage

import {
  EmailTemplateEditor,
  type TemplateSourceFiles,
  type SerializedTemplateSource,
  type ValidationIssue,
} from '@singleton-sd/post-kit-editor';

export function TemplateAdminPage({
  template,
  loading,
  loadError,
}: {
  template: TemplateSourceFiles;
  loading?: boolean;
  loadError?: string;
}) {
  return (
    <EmailTemplateEditor
      template={template}
      availableVariables={[{ name: 'name', description: 'Recipient display name' }]}
      loading={loading}
      loadError={loadError}
      onSave={async (serialized: SerializedTemplateSource, files: TemplateSourceFiles) => {
        // Commit serialized.templateJson / metadataJson / previewJson to the
        // consumer repository (e.g. via the app's own server endpoint).
        const res = await fetch('/api/templates', {
          method: 'PUT',
          body: JSON.stringify({ serialized, key: files.metadata.key }),
        });
        // fetch() resolves for HTTP 4xx/5xx — return failure so the editor
        // keeps dirty state and does not treat the rejection as success.
        if (!res.ok) {
          return { ok: false, message: 'Save failed.' };
        }
      }}
      onSendTest={async (serialized, _files, recipient) => {
        // Browser → your trusted server only. The server uses
        // @singleton-sd/post-kit-client with secrets from Azure Key Vault
        // (production) or local `.env` (development). Never embed a
        // long-lived PostKit API key in browser code.
        const res = await fetch('/api/templates/send-test', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ serialized, recipient }),
        });
        if (!res.ok) {
          return { ok: false, message: 'Test send failed.' };
        }
      }}
      onDirtyChange={(dirty) => {
        // Optional: guard in-app navigation while dirty.
      }}
      onValidationChange={(issues: ValidationIssue[]) => {
        // Optional: mirror validation in the host chrome.
      }}
      onPreviewRendered={(html) => {
        console.log('preview bytes', html.length);
      }}
      className="tenant-theme"
    />
  );
}

A copy-pasteable single-file integration (plus synthetic sample JSON) lives in examples/minimal/.

Props

| Prop | Type | Required | Description | | --- | --- | --- | --- | | template | TemplateSourceFiles | yes | Seeded working triple (templateJson, metadata, previewData). | | onSave | (serialized, files) => SaveResult \| void \| Promise<…> | yes | Persist working files. Receives serialized Git strings and structured files. | | onSendTest | (serialized, files, recipient) => SendTestResult \| void \| Promise<…> | no | When set, shows Send-test chrome. Must route through the consumer’s trusted server. | | availableVariables | TemplateVariable[] | no | Catalogue entries offered to the editing user. | | onDirtyChange | (dirty: boolean) => void | no | Fires when working state diverges from the seeded template (resets after successful save). | | onValidationChange | (issues: ValidationIssue[]) => void | no | Current validation issues whenever they change. | | onPreviewRendered | (html: string) => void | no | Successful preview HTML (e.g. open-in-new-tab without recompiling). | | loading | boolean | no | Non-interactive loading shell while the host fetches files. | | loadError | string | no | Non-interactive error shell with the host’s message. | | className | string | no | Extra class on the root element. |

Also exported: loadTemplateSource, serializeTemplateSource, validateTemplate, hasValidationErrors, EDITOR_CLASS_PREFIX, and related types.

What this package does not do

  • No persistence — it never writes to disk, Git, or object storage. onSave is the only way contents leave the editor.
  • No sending — it never calls PostKit or any mail transport. onSendTest (when provided) is a consumer callback only.
  • No credentials — there are no API-key / token props. Secrets stay on the consumer’s trusted server (Key Vault / env), never in browser bundles.
  • No publish pipeline — compilation and content hashing belong to @singleton-sd/post-kit-compiler in CI or server tooling.

Persistence is consumer-supplied

Save serializes the working state with serializeTemplateSource() and passes both the SerializedTemplateSource strings and the structured TemplateSourceFiles to onSave. The editor shows pending / success / failure feedback and re-enables the control in every outcome. Rejected promises become failure messages (no unhandled rejections).

Send-test chrome appears only when onSendTest is provided. The editor validates a non-empty, plausible recipient address, then invokes the callback with the same serialized payload plus the recipient. Test delivery must go through the consumer's trusted server (typically @singleton-sd/post-kit-client server-side).

Optional onDirtyChange fires when working state diverges from the seeded template prop and resets after a successful save.

Validation and accessibility

validateTemplate(files) (also exported) checks required metadata, key charset, undeclared / unused variables, preview coverage, and optional render-failed from the preview pane (passed through so the compiler is not invoked twice). Error-severity issues disable Save and Send-test; warnings never block.

Optional props:

  • onValidationChange — current ValidationIssue[] whenever it changes
  • loading — non-interactive loading shell while the host fetches files
  • loadError — non-interactive error shell with the host’s message

Inline field errors use aria-describedby. The validation summary is an aria-live region; each entry focuses the responsible control.

Preview data (synthetic only)

preview.json is edited in the preview-data panel and used to render the sandboxed preview pane via @singleton-sd/post-kit-compiler/preview renderPreview (same EmailBuilder + Handlebars path as publish). Those sample values are committed to the consumer repository with the template source.

Never put real personal data, customer addresses, or secrets in preview data. Keep values synthetic (e.g. Jane Doe, [email protected]).

The preview pane shows compiler errors inline when render fails and leaves the canvas / metadata editable. Re-renders are debounced while typing. Optional onPreviewRendered receives the HTML string on each successful render.

Browser note: preview imports @singleton-sd/post-kit-compiler/preview, which has no Node built-ins. Full compile() (content hashing + filesystem) remains on the package root for CI and publish tooling. The preview iframe applies a restrictive CSP (connect-src 'none', img-src data: only) so template HTML cannot trigger arbitrary network fetches from the admin page.

Styling

The editor ships plain CSS classes, no CSS-in-JS runtime and no component library. Every class is prefixed with the exported EDITOR_CLASS_PREFIX (pk-editor-), so class names are stable and safe to target from a consumer stylesheet — for example .pk-editor-root. The root element also accepts a className prop as an escape hatch for theme or layout classes.

Component tests

Tests run on Node's built-in test runner with tsx, consistent with the rest of the repo — no browser-based test stack, no jsdom.

Components are rendered to static markup with react-dom/server and asserted against the resulting HTML string:

import { renderToStaticMarkup } from 'react-dom/server';

const html = renderToStaticMarkup(
  <EmailTemplateEditor template={template} onSave={() => {}} />,
);

Specs live next to the code as src/**/*.spec.tsx. Behaviour that needs interaction is factored into pure functions (preview rows, save/send helpers) that can be tested without a DOM. The end-to-end suite (src/e2e.spec.tsx) drives load → edit → validate → save / send-test through those helpers plus SSR markup.

Development

pnpm test   # type-check + run tests
pnpm build  # emit CommonJS to dist/
pnpm lint   # covered by root eslint

examples/ is documentation-only: it is not listed in the package files array and is outside src/, so it is neither published to npm nor emitted into dist/.