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

@emabuild/core

v0.2.0

Published

Drag & drop email editor Web Component — embeddable anywhere

Downloads

1,594

Readme

@emabuild — Drag & Drop Email Editor

A fully embeddable drag-and-drop email editor Web Component. Cross-client email HTML export, and 13 built-in content blocks.

Built with Lit 3 — works in Angular, React, Vue, or plain HTML.

Quick Start

npm install @emabuild/core lit
<mail-editor id="editor" style="height:100vh;"></mail-editor>

<script type="module">
  import '@emabuild/core/mail-editor';

  const editor = document.getElementById('editor');

  editor.addEventListener('editor:ready', () => {
    console.log('Editor is ready');
  });
</script>

Packages

| Package | Description | Size | |---------|-------------|------| | @emabuild/core | <mail-editor> Web Component | ~14.6KB gzip | | @emabuild/email-renderer | Standalone HTML export engine (works server-side) | ~2.7KB gzip | | @emabuild/types | TypeScript type definitions | types only |

Framework Integration

Angular

npm install @emabuild/core lit
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';

@NgModule({
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}
// app.component.ts
import '@emabuild/core/mail-editor';

@Component({
  template: `<mail-editor #editor style="height:100vh;"></mail-editor>
             <button (click)="exportHtml()">Export</button>`,
})
export class AppComponent {
  @ViewChild('editor') editor!: ElementRef;

  exportHtml() {
    this.editor.nativeElement.exportHtml((result) => {
      console.log(result.html);
    });
  }
}

React

import '@emabuild/core/mail-editor';
import { useRef, useEffect } from 'react';

export function EmailEditor() {
  const editorRef = useRef<any>(null);

  useEffect(() => {
    const el = editorRef.current;
    el?.addEventListener('editor:ready', () => {
      el.loadDesign(myDesign);
    });
  }, []);

  const handleExport = () => {
    editorRef.current?.exportHtml((result) => {
      console.log(result.html);
    });
  };

  return (
    <div style={{ height: '100vh' }}>
      <mail-editor ref={editorRef} />
      <button onClick={handleExport}>Export HTML</button>
    </div>
  );
}

Vue

<template>
  <mail-editor ref="editor" style="height: 100vh" />
  <button @click="exportHtml">Export</button>
</template>

<script setup>
import '@emabuild/core/mail-editor';
import { ref, onMounted } from 'vue';

const editor = ref(null);

onMounted(() => {
  editor.value.addEventListener('editor:ready', () => {
    editor.value.loadDesign(myDesign);
  });
});

function exportHtml() {
  editor.value.exportHtml((result) => {
    console.log(result.html);
  });
}
</script>

API Reference

Methods

// Load a design JSON
editor.loadDesign(design: EmailDesign): void;

// Save the current design as JSON
editor.saveDesign(callback: (design: EmailDesign) => void): void;

// Export email-ready HTML
editor.exportHtml(callback: (result: ExportResult) => void, options?: ExportOptions): void;

// Promise-based export
const result = await editor.exportHtmlAsync(options?: ExportOptions);

// Undo / Redo
editor.undo(): void;
editor.redo(): void;

// Register a custom tool
editor.registerTool(definition: ToolDefinition): void;

// Register a callback (e.g. for image upload)
editor.registerCallback(type: string, callback: Function): void;

// Update body-level settings
editor.setBodyValues(values: Partial<BodyValues>): void;

Events

editor.addEventListener('editor:ready', () => {
  // Editor is fully initialized
});

editor.addEventListener('design:loaded', (e) => {
  // A design was loaded via loadDesign()
  console.log(e.detail.design);
});

editor.addEventListener('design:updated', (e) => {
  // User made a change
  console.log(e.detail.type); // 'content_updated' | 'row_added' | ...
});

Export Result

editor.exportHtml((result) => {
  result.design;  // EmailDesign JSON — save this for future editing
  result.html;    // Complete HTML document (<!DOCTYPE> to </html>)
  result.chunks;  // { body, css, fonts[], js }
});

Export Options

editor.exportHtml(callback, {
  minify: true,           // Minify HTML output
  inlineStyles: true,     // Inline CSS into style attributes
  cleanup: true,          // Remove unused CSS classes
  mergeTags: {            // Replace merge tags with values
    first_name: 'John',
    company: 'Acme',
  },
});

Built-in Content Blocks

| Block | Description | Email Safe | |-------|-------------|:----------:| | Text | Rich text with formatting | Yes | | Heading | H1-H4 with size/weight/color | Yes | | Paragraph | Block text with line-height | Yes | | Image | Responsive image with link | Yes | | Button | CTA button (VML Outlook fallback) | Yes | | Divider | Horizontal line | Yes | | HTML | Raw HTML injection | Yes | | Social | Social media icon links | Yes | | Menu | Horizontal navigation | Yes | | Video | YouTube/Vimeo thumbnail + play | Yes | | Timer | Countdown display | Yes | | Table | Data table with headers | Yes | | Form | Input form (web mode only) | Web only |

Layout Presets

Drag or click to add rows with predefined column layouts:

  • 100% — single column
  • 50 / 50 — two equal columns
  • 33 / 33 / 33 — three equal columns
  • 66 / 33 — two-thirds + one-third
  • 33 / 66 — one-third + two-thirds
  • 25 / 25 / 25 / 25 — four equal columns

Custom Tools

Register custom content blocks with their own properties and renderers:

import { html } from 'lit';

editor.registerTool({
  name: 'product_card',
  label: 'Product Card',
  icon: '<svg>...</svg>',
  supportedDisplayModes: ['email'],
  options: {
    product: {
      title: 'Product',
      options: {
        name: { label: 'Name', defaultValue: 'Product', widget: 'text' },
        price: { label: 'Price', defaultValue: '$0.00', widget: 'text' },
        image: { label: 'Image URL', defaultValue: '', widget: 'text' },
        bgColor: { label: 'Background', defaultValue: '#ffffff', widget: 'color_picker' },
      },
    },
  },
  defaultValues: {
    name: 'Product', price: '$0.00', image: '', bgColor: '#ffffff',
  },
  renderer: {
    renderEditor(values) {
      return html`
        <div style="background:${values.bgColor};padding:16px;text-align:center;">
          <img src="${values.image}" style="max-width:100%;border-radius:8px;" />
          <h3 style="margin:12px 0 4px;">${values.name}</h3>
          <p style="color:#3b82f6;font-weight:bold;">${values.price}</p>
        </div>
      `;
    },
    renderHtml(values, ctx) {
      return `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
        <tr><td style="background-color:${values.bgColor};padding:16px;text-align:center;">
          <img src="${values.image}" width="${ctx.columnWidth}" style="max-width:100%;" />
          <h3 style="margin:12px 0 4px;">${values.name}</h3>
          <p style="color:#3b82f6;font-weight:bold;">${values.price}</p>
        </td></tr>
      </table>`;
    },
  },
});

Design JSON Structure

Designs are stored as structured JSON. The format is interoperable with other email editor tools.

{
  "counters": { "u_row": 1, "u_column": 1, "u_content_text": 1 },
  "body": {
    "id": "u_body",
    "rows": [
      {
        "id": "u_row_1",
        "cells": [1],
        "columns": [
          {
            "id": "u_column_1",
            "contents": [
              {
                "id": "u_content_text_1",
                "type": "text",
                "values": {
                  "text": "<p>Hello World</p>",
                  "containerPadding": "10px",
                  "_meta": { "htmlID": "u_content_text_1", "htmlClassNames": "u_content_text" }
                }
              }
            ],
            "values": { "backgroundColor": "", "padding": "0px", "_meta": { "htmlID": "u_column_1", "htmlClassNames": "u_column" } }
          }
        ],
        "values": { "backgroundColor": "", "columnsBackgroundColor": "#ffffff", "padding": "0px", "_meta": { "htmlID": "u_row_1", "htmlClassNames": "u_row" } }
      }
    ],
    "headers": [],
    "footers": [],
    "values": {
      "backgroundColor": "#e7e7e7",
      "contentWidth": "600px",
      "fontFamily": { "label": "Arial", "value": "arial,helvetica,sans-serif" },
      "textColor": "#000000",
      "preheaderText": "",
      "_meta": { "htmlID": "u_body", "htmlClassNames": "u_body" }
    }
  },
  "schemaVersion": 16
}

Server-Side Rendering

Use @emabuild/email-renderer to generate HTML from design JSON on the server (Node.js):

import { renderDesignToHtml } from '@emabuild/email-renderer';

const toolRenderers = new Map();
// Register tool HTML renderers...

const result = renderDesignToHtml(designJson, toolRenderers, {
  minify: true,
  mergeTags: { first_name: 'John' },
});

console.log(result.html); // Complete email HTML

Email Client Support

Exported HTML uses fluid hybrid design with MSO conditional comments for maximum compatibility:

  • Gmail (Web, iOS, Android)
  • Outlook (2016, 2019, 365, Outlook.com)
  • Apple Mail (macOS, iOS)
  • Yahoo Mail
  • Thunderbird
  • Samsung Mail

Key techniques used:

  • Table-based layout with role="presentation"
  • MSO ghost tables (<!--[if mso]>) for Outlook column rendering
  • VML roundrect for Outlook button border-radius
  • Responsive CSS with @media queries
  • Fluid hybrid columns (display:inline-block + max-width)
  • Dark mode support (prefers-color-scheme)
  • Preheader text with invisible padding fill

Keyboard Shortcuts

| Shortcut | Action | |----------|--------| | Ctrl/Cmd + Z | Undo | | Ctrl/Cmd + Y | Redo | | Ctrl/Cmd + Shift + Z | Redo | | Delete / Backspace | Delete selected element | | Escape | Deselect |

Property Widgets

Available widget types for tool property definitions:

| Widget | Type | Description | |--------|------|-------------| | text | string | Single-line text input | | rich_text | string | Multi-line textarea (HTML) | | color_picker | string | Color swatch + hex input | | toggle | boolean | Checkbox toggle | | dropdown | string | Select dropdown (requires widgetParams.options) | | alignment | string | Left/center/right visual picker | | padding | string | 4-side padding editor (CSS shorthand) |

Performance

The editor is optimized for speed, even with large designs (100+ rows):

  • Fine-grained reactivity — Components subscribe only to the store channels they need. A hover event re-renders only the affected block, not the entire tree.
  • Lazy tool loading — 7 secondary tools are loaded on-demand via dynamic imports. The initial bundle contains only the 6 most-used tools. All lazy tools are preloaded during browser idle time via requestIdleCallback.
  • Minified bundle — esbuild minification reduces the core package to ~63KB (14.6KB gzip).

| Metric | Value | |--------|-------| | Initial bundle | 63KB min / 14.6KB gzip | | Lazy tool chunks | 7 × ~3KB each | | Hover re-renders | O(1) — only the affected block | | Time to interactive | < 200ms |

Development

# Install dependencies
pnpm install

# Start the demo app
pnpm --filter @emabuild/demo dev

# Build all packages
pnpm build

# Publish to npm
pnpm -r publish --access public

License

MIT