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

@braedonsaunders/appkit-email-designer

v0.2.0

Published

Drag-and-drop email and signature designer: GrapesJS authoring surface, merge-field blocks, and an email-safe compile pipeline.

Readme

@braedonsaunders/appkit-email-designer

A drag-and-drop designer for email — whole messages and signatures — plus the compile pipeline that turns what an author built into markup Outlook will not mangle.

The doctrine is what you see is what ships. Blocks are authored as real, inline-styled, table-based HTML rather than a private document model, so the canvas holds delivery-ready markup from the first drag. There is no MJML step and no second renderer to drift out of sync.

Entry points

| Import | Environment | Contents | | --- | --- | --- | | @braedonsaunders/appkit-email-designer | anywhere | Types, themes, block catalogs, compileEmailDesign, renderEmailDesign, sampleMergeValues, copy. No React, no Node built-ins. | | @braedonsaunders/appkit-email-designer/react | client | EmailDesigner (the authoring surface), EmailBlockPalette, EmailTableToolbar. | | @braedonsaunders/appkit-email-designer/inline | server | inlineEmailCss — the juice CSS inliner. Node only. | | @braedonsaunders/appkit-email-designer/styles.css | client | Designer chrome. Also @imports GrapesJS's stylesheet. |

GrapesJS, @grapesjs/react, juice, and React are optional peers. An app that only compiles or renders saved designs installs none of them.

The two halves of a design

compileEmailDesign returns the pair you persist:

const { sourceHtml, compiledHtml, errors } = compileEmailDesign(raw, {
  inlineCss,              // from @braedonsaunders/appkit-email-designer/inline
  fragment: true,         // signatures splice into an existing body
})
  • sourceHtml — inlined and sanitized, still carrying its data-each markers. This is what the designer reopens, so a round-trip through the editor is lossless.
  • compiledHtml — the same markup with markers expanded to {{#each}} / {{#if}} blocks. Tokens are still embedded; this is what gets rendered per recipient.

Both halves derive from the same inlined, sanitized markup and differ only in marker expansion. Inlining first is what makes the round-trip lossless: sanitizing a fragment drops a top-level <style> block, so a signature saved without inlining would reopen stripped of every rule the editor had written to its stylesheet. Inline style attributes survive sanitizing — and they are what email needs anyway, since Gmail and Outlook drop <style>.

The order is fixed: inline, sanitize, expand. The inliner cannot run last, because juice parses with cheerio and would mangle handlebars block syntax.

Authored markup is sanitized once, here — never per send. Merge values are escaped at render time by @braedonsaunders/appkit-email-render, so a compiled design is safe to store and re-render without re-sanitizing.

Rendering

const { html, text } = renderEmailDesign(compiledHtml, {
  agent: { name: 'Dana Reid', title: 'Operations' },
  hazards: [{ name: 'Silica', level: 'High' }],
})

Dotted tokens resolve through nested objects. Substituted values are HTML-escaped, and the plain-text part is derived from the rendered HTML so the two always agree.

Merge fields and collections

A merge field is a scalar the author drags in as {{key}}. A collection becomes an editable table whose body row carries data-each="key" — a real, invisible attribute that survives the editor and compiles to a loop. This is the trick that makes repeating rows authorable at all: a <tr> cannot hold the text node a bare {{#each}} would need.

Keys are held to ^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$. A block with an unsafe key is dropped rather than emitted, and one bad column voids its whole table.

Presets

| Preset | Starter | Palette | | --- | --- | --- | | email | Centered content column on a page background | Content, Layout, Signature | | signature | Accent-ruled identity block, no page chrome | Signature |

Signature blocks are deliberately table-based and narrow — Outlook renders floats and flex unpredictably inside a quoted reply chain.

Signature blocks and the signature starter are written against the conventional agent.* / company.* keys. If your catalog uses different ones, pass your own initialHtml and extraBlocks. The email starter is deliberately token-free: the package cannot know which keys a host defines, and an unresolved token would render as a blank in a real send.

Theming

EmailDesignerTheme colors are baked into blocks as they are inserted; email clients cannot resolve CSS variables. Changing the theme restyles newly dragged blocks, not what is already on the canvas. Every color that reaches a style attribute must be a plain hex literal — anything else falls back to the default rather than being concatenated into markup.

The designer's own chrome is plain CSS with no Tailwind or UI-kit dependency. Override the --ak-ed-* custom properties to restyle it.

Mounting

GrapesJS touches window, so mount behind a dynamic import with SSR disabled:

const EmailDesigner = dynamic(
  () => import('@braedonsaunders/appkit-email-designer/react').then((m) => m.EmailDesigner),
  { ssr: false },
)

<EmailDesigner
  preset="signature"
  initialHtml={saved?.sourceHtml}
  mergeFields={fields}
  theme={{ accent: '#F5A623' }}
  onChange={setDraftHtml}
  onReady={(editor) => (editorRef.current = editor)}
/>

onChange hands you the serialized design (<style> + markup) on every edit — feed it straight to compileEmailDesign for a live preview. onReady gives you the editor to snapshot with serializeEmailEditor on save.