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

@dev_owl/cliveedit

v0.1.11

Published

CliveEdit — A themeable WYSIWYG markdown editor component for Vue 3

Downloads

1,258

Readme

CliveEdit

A themeable WYSIWYG markdown editor component for Vue 3, built with TypeScript.

CliveEdit gives your users a rich editing experience with a familiar toolbar while keeping markdown as the canonical data format. Switch between visual and raw markdown modes, undo/redo with full history, and adapt the look to any design system using CSS custom properties.

🚀 Try the Demo


Table of Contents


Installation

npm install @dev_owl/cliveedit

Peer dependency: Vue 3.3 or higher.

npm install vue@^3.5

Quick Start

<template>
  <CliveEdit v-model="content" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { CliveEdit } from '@dev_owl/cliveedit'
import '@dev_owl/cliveedit/style.css'

const content = ref('# Hello World\n\nStart writing **markdown** here.')
</script>

That's it. The editor renders a toolbar and a WYSIWYG editing area. The content ref always contains the raw markdown string.


MarkdownViewer (Read-only)

Need to display markdown without editing? Use the MarkdownViewer component:

<template>
  <MarkdownViewer v-model="content" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { MarkdownViewer } from '@dev_owl/cliveedit'
import '@dev_owl/cliveedit/style.css'

const content = ref('# Hello World\n\nThis is **read-only** markdown.')
</script>

The viewer renders markdown to styled HTML using the same theming system as the editor. Links are clickable by default.

MarkdownViewer Props

| Prop | Type | Default | Description | |---|---|---|---| | modelValue | string | (required) | Raw markdown string to render. Use with v-model. | | bordered | boolean | true | Show a border around the viewer. Set to false for borderless rendering. |

Borderless Example

<MarkdownViewer v-model="content" :bordered="false" />

Props

| Prop | Type | Default | Description | |---|---|---|---| | modelValue | string | (required) | Raw markdown content. Use with v-model. | | mode | 'wysiwyg' \| 'markdown' | 'wysiwyg' | Active editing mode. Use with v-model:mode. | | placeholder | string | 'Start writing...' | Placeholder text shown when the editor is empty. | | disabled | boolean | false | When true, disables all editing and toolbar actions. | | toolbarItems | ToolbarItem[] | (built-in set) | Override the default toolbar. See Custom Toolbar. | | historyDepth | number | 100 | Maximum number of undo/redo history entries. | | stickyToolbar | boolean | true | Keep the toolbar pinned at the top of the viewport when scrolling. Set to false to let it scroll with the content. | | highlightOptions | { theme?: string; langs?: string[] } | undefined | Enable syntax highlighting in code blocks via Shiki. See Syntax Highlighting. | | emojiPicker | boolean \| EmojiPickerOptions | undefined | Enable the emoji picker toolbar button via emoji-picker-element. Pass true for defaults or an options object. See Emoji Picker. | | onImageUpload | (file: File) => Promise<string> | undefined | Called when an image is pasted or dropped. Return a URL. If not provided, images are embedded as base64. See Image Paste & Drop. | | maxImageSize | number | 2097152 (2 MB) | Maximum image file size in bytes. Images exceeding this are ignored. |


Events

| Event | Payload | Description | |---|---|---| | update:modelValue | string | Emitted when the markdown content changes. Used by v-model. | | update:mode | 'wysiwyg' \| 'markdown' | Emitted when the editing mode switches. Used by v-model:mode. |

Usage

<CliveEdit
  v-model="content"
  @update:modelValue="onContentChange"
  @update:mode="onModeChange"
/>
function onContentChange(markdown: string) {
  console.log('Content changed:', markdown)
}

function onModeChange(mode: 'wysiwyg' | 'markdown') {
  console.log('Mode switched to:', mode)
}

v-model Bindings

CliveEdit supports two v-model bindings:

<CliveEdit
  v-model="markdown"
  v-model:mode="editorMode"
/>

| Binding | Prop | Event | Description | |---|---|---|---| | v-model | modelValue | update:modelValue | Two-way binding for the markdown content. | | v-model:mode | mode | update:mode | Two-way binding for the editing mode ('wysiwyg' or 'markdown'). |


Exposed Methods

Access these via a template ref:

<template>
  <CliveEdit ref="editorRef" v-model="content" />
  <button @click="editorRef?.focus()">Focus Editor</button>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { CliveEdit } from '@dev_owl/cliveedit'

const editorRef = ref<InstanceType<typeof CliveEdit> | null>(null)
const content = ref('')
</script>

| Method | Signature | Description | |---|---|---| | getMarkdown() | () => string | Returns the current markdown content. | | getMode() | () => EditorMode | Returns the current editing mode. | | undo() | () => void | Programmatically trigger undo. | | redo() | () => void | Programmatically trigger redo. | | focus() | () => void | Focus the active editor area (WYSIWYG or textarea). |


Keyboard Shortcuts

These shortcuts work in both WYSIWYG and Markdown modes:

| Shortcut | Action | |---|---| | Ctrl+B / Cmd+B | Bold | | Ctrl+I / Cmd+I | Italic | | Ctrl+K / Cmd+K | Insert link | | Ctrl+Z / Cmd+Z | Undo | | Ctrl+Shift+Z / Cmd+Shift+Z | Redo | | Tab (inside list) | Indent list item (increase nesting) | | Shift+Tab (inside list) | Outdent list item (decrease nesting) | | Ctrl+Click / Cmd+Click | Open link under cursor in a new tab | | Tab (inside table) | Move to next cell | | Shift+Tab (inside table) | Move to previous cell | | Enter (inside table) | Move to same column in next row |


Auto-Formatting Shortcuts

In WYSIWYG mode, CliveEdit detects common markdown patterns as you type and automatically converts them into the corresponding rich-text elements. These shortcuts trigger when you press Space after typing the pattern at the very beginning of an empty line (inside a <p> or <div> block).

| You type | Result | |---|---| | * | Creates a bullet list (<ul>) | | - | Creates a bullet list (<ul>) | | 1. (or any N. ) | Creates an ordered list (<ol>) | | # | Converts the line to a Heading 1 (<h1>) | | ## | Converts the line to a Heading 2 (<h2>) | | ### | Converts the line to a Heading 3 (<h3>) |

Note: Auto-formatting only applies when the shortcut characters are the sole content of the line. If the line already contains other text, the space is inserted normally. These shortcuts do not apply inside existing lists, blockquotes, tables, code blocks, or headings.


Toolbar

Default Toolbar Items

The built-in toolbar includes 19 buttons plus a mode toggle:

| Button | Icon | Action | Active When | |---|---|---|---| | Bold | Bold | Toggle <strong> / ** | Cursor inside bold text | | Italic | Italic | Toggle <em> / * | Cursor inside italic text | | Strikethrough | Strikethrough | Toggle <del> / ~~ | Cursor inside strikethrough text | | — | separator | | | | Heading 1 | Heading1 | Set/toggle <h1> / # | Cursor inside H1 | | Heading 2 | Heading2 | Set/toggle <h2> / ## | Cursor inside H2 | | Heading 3 | Heading3 | Set/toggle <h3> / ### | Cursor inside H3 | | — | separator | | | | Bullet List | List | Toggle <ul> / - | Cursor inside unordered list | | Ordered List | ListOrdered | Toggle <ol> / 1. | Cursor inside ordered list | | Indent List | IndentIncrease | Nest list item one level deeper | — | | Outdent List | IndentDecrease | Un-nest list item one level | — | | — | separator | | | | Blockquote | Quote | Toggle <blockquote> / > | Cursor inside blockquote | | Inline Code | Code | Toggle <code> / ` | Cursor inside code | | Code Block | CodeXml | Toggle <pre> / ``` | Cursor inside code block | | — | separator | | | | Link | Link | Insert <a> / [](url) | Cursor inside link | | Image | Image | Insert <img> / ![alt]() | — | | Horizontal Rule | Minus | Insert <hr> / --- | — | | Emoji | Smile | Open emoji picker | — | | Table | Table | Insert a 3×3 table | Cursor inside a table | | — | separator | | | | Undo | Undo2 | Undo last change | — | | Redo | Redo2 | Redo last undone change | — | | — | spacer | | | | Mode Toggle | FileCode / Eye | Switch between WYSIWYG and Markdown mode | — |

Custom Toolbar

Pass a toolbarItems array to replace the entire toolbar:

<template>
  <CliveEdit v-model="content" :toolbar-items="myToolbar" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { CliveEdit } from '@dev_owl/cliveedit'
import type { ToolbarItem } from '@dev_owl/cliveedit'
import { Bold, Italic, Heading1 } from 'lucide-vue-next'

const content = ref('')

const myToolbar: ToolbarItem[] = [
  {
    id: 'bold',
    label: 'Bold',
    icon: Bold,
    action: 'bold',
    shortcut: 'Ctrl+B',
    active: (ctx) => ctx.isActive('strong'),
  },
  {
    id: 'italic',
    label: 'Italic',
    icon: Italic,
    action: 'italic',
    shortcut: 'Ctrl+I',
    active: (ctx) => ctx.isActive('em'),
  },
  {
    id: 'heading1',
    label: 'Heading 1',
    icon: Heading1,
    action: 'heading1',
    divider: true,  // renders a separator before this item
  },
]
</script>

ToolbarItem Interface

interface ToolbarItem {
  id: string              // Unique identifier
  label: string           // Accessible label / tooltip
  icon: Component         // Vue component (Lucide icon or custom)
  action: string          // Action name dispatched on click
  shortcut?: string       // Keyboard shortcut label (display only)
  active?: (ctx: EditorContext) => boolean   // Active state check
  divider?: boolean       // Show separator before this item
}

Available action names: bold, italic, strikethrough, heading1, heading2, heading3, bulletList, orderedList, blockquote, codeInline, codeBlock, link, image, horizontalRule, emoji, table, undo, redo.


Syntax Highlighting

CliveEdit supports syntax highlighting in fenced code blocks via Shiki — the same engine that powers VS Code's syntax highlighting.

Installation

Shiki is an optional peer dependency. Install it alongside CliveEdit:

npm install shiki

Without Shiki installed, code blocks render as plain preformatted text — no errors.

Basic Usage

<CliveEdit
  v-model="content"
  :highlight-options="{
    theme: 'github-light',
    langs: ['javascript', 'typescript', 'python', 'html', 'css']
  }"
/>

| Option | Type | Default | Description | |---|---|---|---| | theme | string | 'github-light' | Shiki theme name. See Shiki Themes. | | langs | string[] | Common web languages | Languages to pre-load. Only pre-loaded languages can be highlighted. |

Pass {} (empty object) to use defaults with the github-light theme and a set of common languages.

How It Works

  • Markdown mode: Write fenced code blocks with a language identifier as usual:

    ```javascript
    const greeting = 'Hello, world!'
    ```
  • WYSIWYG mode: When you insert a code block, a small language label appears in the top-right corner of the block (initially showing "plain text"). Click the label to type a language name (e.g. javascript, python, css). The code is immediately re-highlighted with the correct syntax colours.

  • MarkdownViewer: Highlighted code blocks render automatically when a parent component provides the highlight function (via CliveEdit or useHighlighter).

Theming the Language Label

The language label badge can be customised with CSS variables:

| Variable | Default | Description | |---|---|---| | --ce-code-lang-bg | rgba(0, 0, 0, 0.06) | Label background | | --ce-code-lang-text | #6b7280 | Label text colour | | --ce-code-lang-font-size | 0.75em | Label font size |


Emoji Picker

CliveEdit supports an emoji picker for inserting native Unicode emoji via emoji-picker-element — a lightweight web component (~12.5 kB min+gz) with built-in search, categories, skin tones, favourites, and accessibility.

Installation

emoji-picker-element is an optional peer dependency. Install it alongside CliveEdit:

npm install emoji-picker-element

Without emoji-picker-element installed, the emoji button simply does not appear in the toolbar — no errors.

Basic Usage

<!-- Defaults -->
<CliveEdit v-model="content" :emoji-picker="true" />

<!-- With options -->
<CliveEdit
  v-model="content"
  :emoji-picker="{ locale: 'en' }"
/>

Options

| Option | Type | Default | Description | |---|---|---|---| | locale | string | 'en' | Emoji data locale. | | dataSource | string | jsdelivr CDN | URL to fetch emoji data from. Useful for self-hosting. |

Pass true instead of an object to use all defaults.

How It Works

  • Click the 😊 (Smile) button in the toolbar to open a floating emoji picker panel.
  • Search for emoji by typing in the search box.
  • Browse by category (Smileys, Animals, Food, etc.).
  • Pick a skin tone via the built-in skin tone selector.
  • Recently used emoji appear in the Favourites row.
  • Select an emoji to insert the native Unicode character at the current cursor position. This works in both WYSIWYG and Markdown modes.
  • The picker closes on selection, pressing Escape, or clicking outside.

Emoji are native Unicode characters — they render using each platform's built-in emoji font (Apple on macOS/iOS, Segoe UI Emoji on Windows, Noto Color Emoji on Linux/Android). This is the expected cross-platform behaviour.

Dark Mode

The emoji picker automatically detects whether the editor is in dark mode by reading the computed --ce-bg CSS variable on the .cliveedit wrapper and calculating its luminance. There is no manual darkMode option — just apply your dark theme via CSS custom properties as usual and the picker follows:

<div :class="{ 'dark-theme': isDark }">
  <!-- The emoji picker automatically adapts its dark/light appearance -->
  <CliveEdit v-model="content" :emoji-picker="true" />
</div>

Image Paste & Drop

CliveEdit supports pasting and dragging images directly into the editor. Screenshots from the clipboard, files from your desktop, or images copied from other apps can be inserted seamlessly.

Accepted formats: PNG, JPEG, GIF, WebP

Default Behaviour (Base64)

With no extra configuration, pasted or dropped images are embedded as base64 data URIs:

<!-- Images just work — no config needed -->
<CliveEdit v-model="content" />

The resulting markdown:

![pasted image](data:image/png;base64,iVBORw0KGgo...)

Note: Base64 encoding increases file size by ~33%. For large images, consider using a custom upload handler instead.

Custom Upload Handler

Provide an onImageUpload callback to upload images to your own server or cloud storage. The function receives a File object and should return a Promise<string> with the final URL:

<template>
  <CliveEdit v-model="content" :on-image-upload="uploadImage" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { CliveEdit } from '@dev_owl/cliveedit'

const content = ref('')

async function uploadImage(file: File): Promise<string> {
  const formData = new FormData()
  formData.append('image', file)

  const res = await fetch('/api/upload', {
    method: 'POST',
    body: formData,
  })
  const { url } = await res.json()
  return url  // e.g. 'https://cdn.example.com/images/photo.png'
}
</script>

If the upload function throws an error, the image is silently discarded.

Size Limit

By default, images larger than 2 MB are rejected. Customise this with the maxImageSize prop (value in bytes):

<!-- Allow images up to 5 MB -->
<CliveEdit
  v-model="content"
  :max-image-size="5 * 1024 * 1024"
/>

Tables

CliveEdit includes full GFM (GitHub Flavoured Markdown) table support. Tables round-trip cleanly between WYSIWYG and Markdown modes.

Inserting a Table

Click the Table button in the toolbar (or call ctx.table() programmatically) to insert a 3×3 starter table with a header row and two body rows. The table is rendered as standard HTML <table> markup and serialised to pipe-delimited GFM markdown:

| Header | Header | Header |
| --- | --- | --- |
|  |  |  |
|  |  |  |

Table Controls

When the cursor is inside a table, a floating control bar appears above it with the following actions:

| Group | Button | Description | |---|---|---| | Row | ↑ + | Add a row above the current row | | Row | ↓ + | Add a row below the current row | | Row | ↓ − | Remove the current row (disabled for header rows and the last body row) | | Col | ← + | Add a column to the left | | Col | → + | Add a column to the right | | Col | → − | Remove the current column (disabled when only one column remains) | | — | 🗑 | Delete the entire table |

The control bar repositions automatically as the selection moves between tables and hides when the cursor leaves all tables.

Table Keyboard Navigation

| Key | Behaviour | |---|---| | Tab | Move to the next cell (left → right, then next row). If at the last cell, a new row is appended. | | Shift+Tab | Move to the previous cell. | | Enter | Move to the same column in the next row. If already on the last row, a new row is appended. |

Formatting commands (bold, italic, etc.) work within individual cells. Block-level commands (headings, lists, blockquotes) are disabled when the cursor is inside a table to prevent structural corruption.


Theming

CliveEdit uses CSS custom properties scoped under the .cliveedit class. Override any variable to match your application's design system — no JavaScript configuration needed.

CSS Custom Properties

Layout

| Variable | Default | Description | |---|---|---| | --ce-bg | #ffffff | Editor background | | --ce-text | #1a1a1a | Text colour | | --ce-border | #d0d5dd | Border colour | | --ce-radius | 8px | Border radius |

Toolbar

| Variable | Default | Description | |---|---|---| | --ce-toolbar-bg | #f9fafb | Toolbar background | | --ce-toolbar-border | #e5e7eb | Toolbar bottom border | | --ce-toolbar-btn-hover | #e5e7eb | Button hover background | | --ce-toolbar-btn-active | #d0d5dd | Button active/pressed background |

Typography

| Variable | Default | Description | |---|---|---| | --ce-font-family | System font stack | Content font family | | --ce-font-size | 16px | Content font size | | --ce-line-height | 1 | Content line height | | --ce-mono-font | Consolas, monospace... | Monospace font for code |

Headings

| Variable | Default | Description | |---|---|---| | --ce-h1-font-size | 2em | H1 font size | | --ce-h1-font-weight | 700 | H1 font weight | | --ce-h1-margin | 0.67em 0 | H1 margin | | --ce-h1-line-height | 1.25 | H1 line height | | --ce-h2-font-size | 1.5em | H2 font size | | --ce-h2-font-weight | 600 | H2 font weight | | --ce-h2-margin | 0.75em 0 | H2 margin | | --ce-h2-line-height | 1.3 | H2 line height | | --ce-h3-font-size | 1.25em | H3 font size | | --ce-h3-font-weight | 600 | H3 font weight | | --ce-h3-margin | 0.75em 0 | H3 margin | | --ce-h3-line-height | 1.35 | H3 line height |

Content Elements

| Variable | Default | Description | |---|---|---| | --ce-content-margin | 0.75em 0 | Shared vertical margin for blocks (pre, blockquote, table) | | --ce-content-padding | 16px 20px | Editor & viewer area padding | | --ce-code-bg | #f3f4f6 | Code background | | --ce-code-text | #e11d48 | Inline code text colour | | --ce-code-font-size | 0.9em | Code font size | | --ce-code-padding | 2px 5px | Inline code padding | | --ce-code-radius | 3px | Inline code border radius | | --ce-pre-padding | 12px 16px | Code block padding | | --ce-pre-radius | 6px | Code block border radius | | --ce-code-lang-bg | rgba(0, 0, 0, 0.06) | Code block language label background | | --ce-code-lang-text | #6b7280 | Code block language label text colour | | --ce-code-lang-font-size | 0.75em | Code block language label font size | | --ce-blockquote-border | #6366f1 | Blockquote left border colour | | --ce-blockquote-border-width | 4px | Blockquote left border width | | --ce-blockquote-padding | 4px 16px | Blockquote padding | | --ce-blockquote-opacity | 0.85 | Blockquote text opacity | | --ce-link-color | #2563eb | Link colour | | --ce-list-padding-left | 1.5em | List indentation | | --ce-hr-color | #d0d5dd | Horizontal rule colour | | --ce-hr-border-width | 2px | Horizontal rule thickness | | --ce-hr-margin | 1em 0 | Horizontal rule margin | | --ce-img-radius | 4px | Image border radius | | --ce-table-cell-padding | 6px 12px | Table cell padding |

Interaction

| Variable | Default | Description | |---|---|---| | --ce-selection-bg | rgba(99, 102, 241, 0.2) | Text selection highlight | | --ce-placeholder-color | #9ca3af | Placeholder text colour | | --ce-focus-ring | rgba(99, 102, 241, 0.4) | Focus outline colour |

Dark Theme Example

.dark-theme .cliveedit {
  --ce-bg: #1e1e1e;
  --ce-text: #d4d4d4;
  --ce-border: #3e3e42;
  --ce-toolbar-bg: #252526;
  --ce-toolbar-border: #3e3e42;
  --ce-toolbar-btn-hover: #3e3e42;
  --ce-toolbar-btn-active: #505054;
  --ce-code-bg: #2d2d30;
  --ce-code-text: #ce9178;
  --ce-blockquote-border: #6366f1;
  --ce-link-color: #4fc1ff;
  --ce-selection-bg: rgba(38, 79, 120, 0.5);
  --ce-placeholder-color: #6a6a6a;
  --ce-focus-ring: rgba(99, 102, 241, 0.5);
  --ce-hr-color: #3e3e42;
  --ce-line-height: 1;
}
<div :class="{ 'dark-theme': isDark }">
  <CliveEdit v-model="content" />
</div>

You can also use @media (prefers-color-scheme: dark) for automatic dark mode:

@media (prefers-color-scheme: dark) {
  .cliveedit {
    --ce-bg: #1e1e1e;
    --ce-text: #d4d4d4;
    /* ... */
  }
}

Vue Plugin (Global Registration)

Instead of importing CliveEdit in every component, you can register it globally:

// main.ts
import { createApp } from 'vue'
import { CliveEditPlugin } from '@dev_owl/cliveedit'
import '@dev_owl/cliveedit/style.css'
import App from './App.vue'

const app = createApp(App)
app.use(CliveEditPlugin)
app.mount('#app')

Then use <CliveEdit> or <MarkdownViewer> anywhere without importing:

<template>
  <CliveEdit v-model="markdown" />
  <MarkdownViewer v-model="markdown" />
</template>

Advanced Usage

EditorContext (Provide / Inject)

CliveEdit provides an EditorContext object via Vue's provide/inject system. Child or descendant components can access the editor's commands and state:

import { inject } from 'vue'
import { EDITOR_CTX_KEY } from '@dev_owl/cliveedit'

const ctx = inject(EDITOR_CTX_KEY)

// Call formatting commands
ctx?.bold()
ctx?.heading(2)
ctx?.link('https://example.com', 'Example')
ctx?.table()  // Insert a 3×3 table

// Check state
ctx?.isActive('strong') // true if cursor is inside bold text
ctx?.isActive('table')  // true if cursor is inside a table
ctx?.canUndo            // true if undo is available
ctx?.mode               // 'wysiwyg' | 'markdown'

// Switch modes
ctx?.toggleMode()

EditorContext Interface

interface EditorContext {
  // State
  mode: EditorMode
  disabled: boolean

  // Formatting commands
  bold(): void
  italic(): void
  strikethrough(): void
  heading(level: 1 | 2 | 3): void
  bulletList(): void
  orderedList(): void
  indentList(): void
  outdentList(): void
  blockquote(): void
  codeInline(): void
  codeBlock(lang?: string): void
  link(url?: string, text?: string): void
  image(src?: string, alt?: string): void
  horizontalRule(): void
  table(): void
  emoji(): void

  // History
  undo(): void
  redo(): void
  canUndo: boolean
  canRedo: boolean

  // Mode
  toggleMode(): void

  // Query
  isActive(tag: string): boolean
}

useHistory Composable

The undo/redo system is available as a standalone composable for headless or custom editor builds:

import { useHistory } from '@dev_owl/cliveedit'

const history = useHistory({ maxDepth: 50, debounceMs: 200 })

history.init('# Initial content')
history.pushState('# Updated content')    // debounced (for keystrokes)
history.pushImmediate('# After toolbar')  // immediate (for actions)

const prev = history.undo()   // returns HistoryEntry | null
const next = history.redo()   // returns HistoryEntry | null

history.canUndo.value  // boolean (computed ref)
history.canRedo.value  // boolean (computed ref)

| Method | Description | |---|---| | init(markdown) | Set the initial state. Clears all history. | | pushState(markdown) | Debounced push — suited for continuous typing. | | pushImmediate(markdown) | Immediate push — suited for toolbar actions, mode switches. | | undo() | Returns the previous HistoryEntry or null. | | redo() | Returns the next HistoryEntry or null. | | canUndo | Computed ref — true when undo is available. | | canRedo | Computed ref — true when redo is available. | | clear() | Clear all history and cancel pending debounce. |

useEditor Composable

The editor command layer is available standalone for building custom UIs around a contenteditable element:

import { ref } from 'vue'
import { useEditor } from '@dev_owl/cliveedit'

const editorEl = ref<HTMLElement | null>(null)
const editor = useEditor(editorEl)

// Formatting
editor.bold()
editor.italic()
editor.heading(2)
editor.link('https://example.com')

// State
editor.isActive('strong')  // boolean
editor.refreshActiveState()

// DOM access
editor.getHtml()
editor.setHtml('<p>Hello</p>')
editor.focus()

useHighlighter Composable

The syntax highlighting system is available as a standalone composable for advanced or custom integrations:

import { useHighlighter } from '@dev_owl/cliveedit'

const { init, highlight, isReady, provideHighlight } = useHighlighter()

// Initialise Shiki (async — call once at startup)
await init({ theme: 'github-dark', langs: ['javascript', 'python'] })

// Check readiness
isReady.value  // true after successful init

// Highlight code (synchronous after init)
const html = highlight('const x = 42', 'javascript')

// Provide the highlight function to child components
provideHighlight()

| Method / Property | Description | |---|---| | init(options?) | Initialise Shiki with the given theme and languages. Returns Promise<boolean>. | | highlight(code, lang) | Highlight code synchronously. Returns '' before init. | | isReady | Ref<boolean>true when Shiki is loaded. | | highlightFn | Ref<HighlightFn \| null> — reactive highlight function reference. | | provideHighlight() | Call in setup() to provide the highlight function to descendants via inject. |

Child components can consume the highlight function:

import { useInjectHighlight } from '@dev_owl/cliveedit'

const highlightFn = useInjectHighlight()
// highlightFn.value is null until a parent calls provideHighlight()

useEmojiPicker Composable

The emoji picker system is available as a standalone composable for advanced or custom integrations:

import { useEmojiPicker } from '@dev_owl/cliveedit'

const { init, isReady, enabled, setEnabled } = useEmojiPicker()

// Initialise (async — attempts to load emoji-picker-element)
await init()

// Check readiness
isReady.value  // true after successful init

// Control the feature
setEnabled(false)   // hide the emoji button

Note: Dark mode is detected automatically from the editor's background — there is no manual setDarkMode() call.

| Method / Property | Description | |---|---| | init(options?) | Load emoji-picker-element and enable the feature. Returns Promise<boolean>. | | isReady | Ref<boolean>true when the module is loaded. | | enabled | Ref<boolean>true when the feature is active. | | setEnabled(value) | Enable / disable without re-loading. |


TypeScript

All types are exported for full IntelliSense support:

import type {
  EditorMode,
  ToolbarItem,
  HistoryEntry,
  CliveEditProps,
  CliveEditEmits,
  EditorContext,
  HighlightOptions,
  EmojiPickerOptions,
  MarkdownViewerProps,
} from '@dev_owl/cliveedit'

Build Scripts

| Script | Command | Description | |---|---|---| | npm run dev | vite --config vite.dev.config.ts | Start the development playground | | npm run build | Type-check → Vite build → Generate .d.ts | Full production build | | npm run build:only | vite build | Build without type-checking | | npm run typecheck | vue-tsc --noEmit | Type-check only |

Build outputs in dist/:

| File | Format | Description | |---|---|---| | cliveedit.es.js | ESM | For modern bundlers (Vite, Webpack 5, Rollup) | | cliveedit.umd.js | UMD | For script tags and legacy bundlers | | editor.css | CSS | All editor styles (import as @dev_owl/cliveedit/style.css) | | *.d.ts | TypeScript | Type declarations for all exports |


Browser Support

CliveEdit uses standard DOM APIs (contenteditable, Selection, Range, DOMParser) and works in all modern browsers:

  • Chrome / Edge 90+
  • Firefox 90+
  • Safari 15+

License

MIT