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

@sparkdynamic/email-builder

v0.1.11

Published

Email editor and renderer: block registry, canvas, inspector and email HTML output

Readme

@sparkdynamic/email-builder

Email editor and renderer: a block registry, an editor canvas, a property inspector, and email HTML output.

Not published yet — the release workflow is still being set up. Until then, consume it with npm link or a file: override (see Local development).

Install

npm install @sparkdynamic/email-builder

Peers: react 19, react-dom 19, zod 3.

Entry points

Take only what you need:

| Import | What it gives you | | ---------------------------------------- | --------------------------------------------------------------------------------------- | | @sparkdynamic/email-builder/extensions | the block-definition and registry API — declare a block type, derive everything from it | | @sparkdynamic/email-builder/blocks | the built-in block renderers with their zod schemas, props types and defaults | | @sparkdynamic/email-builder/reader | Reader, createReader, renderToStaticMarkup — a document to email HTML | | @sparkdynamic/email-builder/editor | the provider, canvas, inspector, ui primitives and built-in block definitions | | @sparkdynamic/email-builder | all of the above | | @sparkdynamic/email-builder/styles.css | the editor's compiled stylesheet; ./theme.css is the raw design-token file |

ESM only, with .d.mts types, sourcemaps and src/ in the published tarball, so you can step into real source rather than a bundle.

Rendering a document

import { renderToStaticMarkup } from '@sparkdynamic/email-builder/reader';

const html = renderToStaticMarkup(document, { rootBlockId: 'root' });

document is flat — Record<blockId, { type, data }> — and nesting is by id reference, so containers hold childrenIds. ReaderDocumentSchema validates arbitrary JSON into a typed document.

Mounting the editor

import '@sparkdynamic/email-builder/styles.css';

import { buildBlockRegistry } from '@sparkdynamic/email-builder/extensions';
import {
  BUILT_IN_BLOCK_DEFINITIONS,
  EditorBlock,
  EditorBlockWrapper,
  EmailBuilderProvider,
  InspectorDrawer,
} from '@sparkdynamic/email-builder/editor';

const registry = buildBlockRegistry(BUILT_IN_BLOCK_DEFINITIONS, { EditorBlockWrapper });

<EmailBuilderProvider registry={registry} initialDocument={doc} onChange={save}>
  <EditorBlock id="root" />
  <InspectorDrawer />
</EmailBuilderProvider>;

The provider owns one editor's state, so two of them on a page are two independent editors. Everything around the canvas and the inspector — toolbar, view tabs, loading, saving — is yours; examples/vite-emailbuilder is a worked example of exactly that.

Import styles.css before your own stylesheet. It ships no preflight, and its first line fixes the cascade-layer order so your reset cannot outrank the editor's utilities.

Wiring up your image library

Give the provider an imageLibrary and the Image block's panel grows a picker over your own asset store. Every member is optional and the editor adapts to what it gets.

<EmailBuilderProvider
  registry={registry}
  initialDocument={doc}
  imageLibrary={{
    // One page of the library. `cursor` is null on the first call, and
    // whatever you last returned as `nextCursor` after that.
    list: async ({ query, cursor, signal }) => {
      const res = await fetch(`/api/media?q=${query}&cursor=${cursor ?? ''}`, { signal });
      const { items, next } = await res.json();
      return { items, nextCursor: next };
    },
    // Store a file and say where it now lives. A bare URL string is fine too.
    upload: async (file, { signal }) => {
      const body = new FormData();
      body.append('file', file);
      const res = await fetch('/api/media', { method: 'POST', body, signal });
      if (!res.ok) {
        // The message is what the user sees in the dialog.
        throw new Error('The upload was rejected.');
      }
      return res.json(); // { url, alt?, name?, thumbnailUrl?, id? }
    },
    accept: 'image/png,image/jpeg,image/webp',
    maxFileSizeBytes: 5 * 1024 * 1024,
  }}
>

That gets you the built-in dialog: a dropzone, a search box and a paged grid. If you already have an asset manager, skip both and hand off the whole interaction instead — pick takes precedence, and the editor renders no dialog of its own:

imageLibrary={{ pick: ({ url }) => openMyAssetManager({ selected: url }) }}

Resolve with null when the user cancels. A chosen image writes the block's url, and fills its alt only when it has none; width and height are left alone, because an asset's intrinsic size is rarely the size it should render at in an email.

Keep the object stable — module scope, or useMemo. Pass no imageLibrary and the Image panel is what it always was: a URL field.

Wiring up your template library

Give the provider a templateLibrary and every block on the canvas grows a "Save as template" action in its tune menu, which keeps that block and everything under it as a plain JSON fragment. The saved set you hand back is listed in a Templates tab in the sidebar and offered under the block grid in the add-block menu.

Like the image library, this is callbacks rather than storage: the editor never persists anything and keeps no copy of the list, so nothing has to be invalidated.

const [templates, setTemplates] = useState<TBlockTemplate[]>(() => loadTemplates());

<EmailBuilderProvider
  registry={registry}
  initialDocument={doc}
  templateLibrary={{
    templates,
    // `draft` is { name, blockType, rootBlockId, blocks } — plain JSON.
    // Rejecting shows the error's message in the save dialog and leaves it
    // open, so throw something worth reading.
    save: async (draft) => {
      const saved = await api.createTemplate(draft);
      setTemplates((list) => [saved, ...list]);
    },
    // Leave `remove` out and entries cannot be deleted from the editor.
    remove: async (template) => {
      await api.deleteTemplate(template.id);
      setTemplates((list) => list.filter((t) => t.id !== template.id));
    },
  }}
>

Every member is optional. save alone gets you the action without a list; templates alone gets you a read-only library. The list on screen is always your state — the editor does not add a saved draft to it for you.

Inserting renumbers the whole subtree, so one template can sit next to a copy of itself, and a template that references a block type this registry does not have is listed as unsupported rather than offered.

Registering your own block

A block is declared once, as a BlockDefinition: its schema, how it renders in email, how it renders on the canvas, its inspector panel, and its add-block menu entry. buildBlockRegistry derives the reader dictionary, the canvas dictionary, the document schema, the inspector dispatch and the menu from a dictionary of them.

import { buildBlockDefinitionDictionary, buildBlockRegistry } from '@sparkdynamic/email-builder/extensions';
import { BUILT_IN_BLOCK_DEFINITIONS, EditorBlockWrapper } from '@sparkdynamic/email-builder/editor';
import { createReader } from '@sparkdynamic/email-builder/reader';

const blocks = buildBlockDefinitionDictionary({
  ...BUILT_IN_BLOCK_DEFINITIONS,
  ProductPicker: {
    schema: ProductPickerPropsSchema,
    Reader: ProductPicker,
    Editor: ProductPickerEditor,
    SidebarPanel: ProductPickerSidebarPanel,
    menu: { label: 'Product', icon: <Package />, defaults: () => ({}) },
  },
});

const registry = buildBlockRegistry(blocks, { EditorBlockWrapper });
const { Reader, renderToStaticMarkup } = createReader(registry.readerDictionary);

Because the reader is built from the same definitions as the canvas, a registered block renders on the canvas, in the preview and in the exported HTML alike.

Local development

The editor SPA in examples/vite-emailbuilder consumes this package as a workspace, so it exercises the same public entry points a consumer does.

npm ci                                       # from the repo root
npm run build --workspace=packages/email-builder
cd examples/vite-emailbuilder && npx vite

To develop against this package from another repository, npm link it or point at it with a file: dependency, and rebuild the package after each change — the app resolves dist, not src.

Licence and attribution

MIT. This package is a hard fork of usewaypoint/email-builder-js (MIT, Copyright (c) 2024 Waypoint (Metaccountant, Inc.)), taken at upstream commit ce3e610; the block framework, the renderers and the editor shell originate there. Some changes are lifted from onchainsuite/email-builder-js (MIT), noted in the commits that do so. See the LICENSE and NOTICE files shipped alongside this package.