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

vue-mail-editor-v3

v1.1.0

Published

A customizable, extensible drag-and-drop email template editor for Vue 3.

Downloads

83

Readme

vue-mail-editor-v3

A customizable, extensible drag-and-drop email template editor for Vue 3. Theme it, register your own blocks and inspector panels, and delegate all persistence/actions to your backend.

This version is based on csesumonpro/vue-mail-editor and has been updated to use Tiptap V3 dependencies and pnpm.

📖 Documentation: csesumonpro.github.io/vue-mail-editor — guides, API reference, and a live interactive demo. You can also run the docs locally with pnpm run docs:dev (the docs/ directory is a VitePress site).

pnpm install vue-mail-editor-v3
// peer dependency
pnpm install vue

Quick start

<script setup lang="ts">
import { ref } from 'vue'
import { EmailEditor } from 'vue-mail-editor-v3'
import 'vue-mail-editor-v3/style.css'
import type { Design } from 'vue-mail-editor-v3'

const design = ref<Design>()
</script>

<template>
  <!-- give it a sized container; the editor fills 100% height -->
  <div style="height: 100vh">
    <EmailEditor v-model="design" />
  </div>
</template>

The editor is fully self-contained — no Pinia, no global setup, no directive registration. You can mount several independent instances on one page.

Props

| Prop | Type | Default | Description | | ---------------- | ----------------------------------------------- | --------- | ----------- | | v-model | Design | — | Two-way design JSON (controlled mode). | | blocks | BlockDefinition[] | [] | Custom blocks (merged with built-ins). | | disabledBlocks | string[] | [] | Hide built-in block types. | | theme | ThemeTokens | — | Color/typography overrides (light + dark). | | colorMode | 'light' \| 'dark' \| 'auto' | 'light' | Theme mode — two-way via v-model:colorMode. | | preview | boolean | — | Preview mode — two-way via v-model:preview. | | config | EditorConfig | — | Feature flags (devices, actions, templates…). | | storage | 'local' \| 'none' | 'local' | local = localStorage autosave; none = host owns persistence. | | onImageUpload | (file: File) => Promise<string> | base64 | Upload an image, return its URL. | | onSave | (design) => void \| Promise | download | Handle Save Design. | | onSaveTemplate | (payload) => void \| Promise | — | Handle Save as template. | | onExport | (html, design) => void \| Promise | — | Receive exported HTML. | | onLoad | () => Design \| Promise<Design> | — | Initial design (e.g. server fetch). |

Events

update:modelValue, update:colorMode, update:preview, change(design), save(design), save-template(payload), export(html, design), select(selection), ready(api).

Slots

#header (replace top bar), #header-brand, #header-actions (inject buttons), #empty (empty-canvas state).

Imperative API (@ready or template ref)

interface EditorApi {
  getDesign(): Design          // deep-cloned snapshot
  loadDesign(design: Design): void
  newDesign(): void            // clear to a fresh, empty design (no prompt)
  exportHtml(): string         // returns email-safe HTML
  save(): void | Promise       // fire Save flow (emit `save` + onSave)
  export(): void | Promise     // fire Export flow (emit `export` + onExport)
  undo(): void; redo(): void
  registerBlock(def: BlockDefinition): void
  selectBody(): void
}

Commands live on the API; UI state (dark/light, preview) is controlled via v-model:colorMode and v-model:preview — one source of truth you can set and read.

Build your own block

<!-- RatingBlock.vue -->
<script setup lang="ts">
defineProps<{ values: { stars: number; color: string }; editing?: boolean }>()
defineEmits<{ update: [patch: Partial<{ stars: number; color: string }>] }>()
</script>
<template>
  <div :style="{ textAlign: 'center', fontSize: '26px', color: values.color }">
    <span v-for="n in values.stars" :key="n">★</span>
  </div>
</template>
import { defineBlock } from 'vue-mail-editor-v3'
import { Star } from 'lucide-vue-next'
import RatingBlock from './RatingBlock.vue'

export const rating = defineBlock<{ stars: number; color: string }>({
  type: 'rating',
  label: 'Rating',
  icon: Star,
  defaultValues: () => ({ stars: 5, color: '#f59e0b' }),
  render: RatingBlock,                      // canvas component (values/selected/editing + update)
  inspector: [
    { title: 'Rating', controls: [
      { type: 'number', key: 'stars', label: 'Stars', min: 1, max: 5 },
      { type: 'color',  key: 'color', label: 'Color' },
    ] },
  ],
  toHtml: (v) => `<div style="text-align:center;color:${v.color}">${'★'.repeat(v.stars)}</div>`,
})
<EmailEditor :blocks="[rating]" />

Inspector controls (the allowed catalog): text, textarea, number, slider, color, select, align, toggle, spacing, border, font, link, image, background, list.

Theming

Tokens are scoped to each editor instance, so multiple themes coexist.

<EmailEditor
  color-mode="auto"
  :theme="{
    colors: { accent: '#7c3aed', primary: '#111827' },
    dark:   { accent: '#a78bfa' },
    font:   { sans: 'Inter, sans-serif', baseSize: '15px' },
  }"
/>

You can also override --cvee-* CSS variables on a .vue-email-editor wrapper.

Server-side (database) usage

<script setup lang="ts">
import { EmailEditor } from 'vue-mail-editor-v3'

async function load()        { return (await api.get('/designs/1')).data }
async function save(design)  { await api.put('/designs/1', design) }
async function upload(file)  { return (await api.upload(file)).url }
</script>

<template>
  <EmailEditor
    storage="none"
    :on-load="load"
    :on-save="save"
    :on-image-upload="upload"
    @export="(html) => api.post('/render', { html })"
  >
    <template #header-actions>
      <button @click="publish">Publish</button>
    </template>
  </EmailEditor>
</template>

With storage="none" nothing is written to localStorage — your database is the single source of truth.

What to persist

The design is plain JSON — store it in one column (jsonb/json/text). The exported html is derived; only persist it if you want a ready-to-send render cache.

designs:  id | name | design (jsonb) | html (text, optional) | updated_at
  • onLoad() runs on mount → fetch and return the design JSON from your DB.
  • onSave(design) runs when the user clicks Save → write the JSON to your DB.
  • exportHtml() (via @ready) or @export gives you the final email HTML.

Server-side autosave

config.autosaveMs only debounces the built-in localStorage autosave (storage="local"). With storage="none" it does nothing — you own the save cadence.

For continuous save-to-database, debounce the change event yourself:

<script setup lang="ts">
import { debounce } from 'lodash-es'
import type { Design } from 'vue-mail-editor-v3'

async function load() { return (await api.get('/designs/1')).data.design }

// this 1000ms is your real-DB "autosaveMs" — tune it freely
const autosave = debounce((design: Design) => {
  api.put('/designs/1', { design })
}, 1000)
</script>

<template>
  <EmailEditor storage="none" :on-load="load" @change="autosave" />
</template>

change already fires debounced (~300ms) on every edit; the extra debounce batches DB writes. Use onSave for an explicit "Save" button on top of (or instead of) this.

Config (feature flags)

const config: EditorConfig = {
  contentWidth: 640,
  devices: ['desktop', 'mobile'],
  actions: { import: false, saveTemplate: true },
  templates: [ /* your starter templates */ ],
  autosaveMs: 1000, // localStorage debounce only; ignored when storage="none"
}

Customizing the top bar

Everything in the top bar is configurable from the host app — swap the logo, show/hide built-in actions, add your own buttons, or replace the bar entirely.

| Goal | How | | ---- | --- | | Show/hide a built-in action | config.actions.<name> (boolean) | | Rename a built-in label/tooltip | config.labels.<name> (string) | | Replace the logo / brand | #header-brand slot | | Add custom buttons (keep built-ins) | #header-actions slot | | Fully custom toolbar (own labels/layout) | #header slot | | Trigger the editor from your buttons | @ready="api = $event" (or events) |

Show / hide built-in actions

<EmailEditor
  :config="{
    actions: {
      undo: true, preview: true, theme: true,
      templates: false, new: false, import: false,
      save: true, saveTemplate: true, export: true,
    },
  }"
/>

All default to true except saveTemplate (defaults false). Set false to hide.

Rename built-in labels — config.labels

Relabel the built-in actions (button text + tooltips) without replacing the bar:

<EmailEditor
  :config="{
    labels: {
      brand: 'Acme Mailer',
      save: 'Publish',
      export: 'Get HTML',
      saveTemplate: 'Save template',
      undo: 'Undo', redo: 'Redo', preview: 'Preview',
      templates: 'Templates', new: 'New design', import: 'Import JSON',
    },
  }"
/>

Every label is optional — unset keys keep their defaults. brand only applies when you haven't overridden the #header-brand slot.

Replace the logo — #header-brand

<EmailEditor>
  <template #header-brand>
    <img src="/logo.svg" alt="Acme" style="height: 24px" />
  </template>
</EmailEditor>

Add custom actions — #header-actions

Your buttons render next to the built-ins; wire them via the imperative API:

<script setup lang="ts">
import { ref } from 'vue'
import { EmailEditor } from 'vue-mail-editor-v3'
import type { EditorApi } from 'vue-mail-editor-v3'
const api = ref<EditorApi>()
function publish() { myBackend.publish(api.value!.exportHtml()) }
</script>

<template>
  <EmailEditor @ready="api = $event">
    <template #header-actions>
      <button @click="publish">Publish</button>
    </template>
  </EmailEditor>
</template>

Fully custom toolbar — #header

Replace the whole bar (your own labels/layout) and drive it via the API:

<EmailEditor @ready="api = $event">
  <template #header>
    <header class="my-toolbar">
      <img src="/logo.svg" />
      <button @click="api?.undo()">Undo</button>
      <button @click="api?.exportHtml()">Export</button>
    </header>
  </template>
</EmailEditor>

With #header you own the markup, so wire every action through the EditorApi (@ready) and/or the events.

Notes

  • The exported HTML is table-based with inlined styles + Outlook (MSO) conditionals + mobile media queries.
  • Predefined social icons load from the Simple Icons CDN (SVG); for maximum client compatibility (e.g. Outlook) supply a custom PNG icon URL per item.

August 17, 2026 Update

  • Migrated package manager from npm to pnpm.
  • Updated Tiptap dependencies to version 3.
  • Resolved TypeScript typing constraints for the new Tiptap V3 generics.
  • Restored visual layout parity and fixed VitePress Tailwind CSS extraction for the dev documentation server.

License

MIT