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

@smartiv.tv/html-editor

v1.1.4

Published

Custom HTML editor plugin for Vue 3, tuned for Android TV signage output

Downloads

639

Readme

@smartiv.tv/html-editor

By Smartiv · © 2026 Smartiv · MIT license

npm install @smartiv.tv/html-editor

A self-contained HTML editor plugin for Vue 3, built for Android TV signage output. The core, the plugins and the sanitizer are all written here — there is no editor runtime dependency, and the shipped bundle is ~16 KB gzipped.

Rendering on Android TV? Prefer the JitPack AAR — see android/JITPACK.md. Pushing main does not update the library; publish a new Git tag, then bump the version in the player app. Details & legacy copy-in guide: android/README.md.


Built for signage

A room display is not a web page. Each of these is a first-class feature rather than something an operator has to work around:

| Signage requirement | How it is solved | |---|---| | Event: / Host: / Time: with colons on one line | Field List block — the colon is placed by CSS grid, never typed | | Text colour must survive a background change | colour is inline and independent of the theme | | Output rendered by an Android TV WebView | one file, inline CSS, zero network requests | | TV overscan crops 5% of every edge | safe area plus a 1920×1080 preview | | Readable from three metres | rem scale driven by a single root font-size | | Operators add their own brand fonts | upload once, available in every editor in the CMS |


Install

npm install @smartiv.tv/html-editor

Then use it in Vue 3 (below). To work on this repo instead, see Local development.

Use in Vue 3

import { createApp } from 'vue';
import SmartivEditorPlugin from '@smartiv.tv/html-editor';
import '@smartiv.tv/html-editor/style.css';

createApp(App).use(SmartivEditorPlugin).mount('#app');
<template>
  <SmartivEditor
    v-model="html"
    min-height="420px"
    @export="saveToCms"
  />
</template>

<script setup>
import { ref } from 'vue';
const html = ref('');
function saveToCms({ html }) { /* POST to the API */ }
</script>

Without app.use, import the component directly:

import { SmartivEditor } from '@smartiv.tv/html-editor';

Props

| Prop | Type | Default | Notes | |---|---|---|---| | modelValue | String | '' | content HTML (v-model) | | plugins | Array | defaultPlugins | plugin factories | | toolbar | Array | defaultToolbar | groups of registered button names; compactToolbar also ships | | options | Object | {} | editor options plus options.tv for export | | tvSurface | Boolean | true | apply the TV content styles to the editing surface | | readonly | Boolean | false | | | minHeight / maxHeight | String | 320px / 60vh | | | dark | Boolean | false | editor chrome theme (not the document theme) |

Events

update:modelValue, update:fonts, change, init (hands over the Editor instance), export.

Methods (via ref)

getEditor(), getContent({ format: 'html' | 'text' }), setContent(html), exportTv().


The colon problem — and the fix

In a plain rich-text editor Event: … is one run of text. The colon sits at the end of the label, so its x position follows the label's length, and every row lands somewhere different. Padding with spaces does not help: the font is proportional.

The Field List splits label and value into two grid columns and renders the colon as generated content pushed to the label column's edge:

<dl class="sv-fields" data-sv-colon="align">
  <dt>Event</dt><dd>Coordination &amp; Technical Briefing</dd>
  <dt>Host</dt><dd>Media Team</dd>
  <dt>Time</dt><dd>14.00 – 16.00</dd>
</dl>
.sv-fields   { display: grid; grid-template-columns: var(--sv-label-width, max-content) 1fr; }
.sv-fields > dt         { display: flex; justify-content: space-between; }
.sv-fields > dt::after  { content: ":"; }

The label column is as wide as the longest label, and space-between pushes the colon to that column's edge — so every colon shares one x coordinate regardless of label length.

data-sv-colon modes:

| Value | Result | |---|---| | align | labels flush left, colons aligned (default) | | right | labels right-aligned, colon trails the text | | tight | colon hugs the label (the un-aligned legacy look) | | none | no colon |

Lock the column with --sv-label-width (toolbar: Label width) when several screens must share an identical label column.

Keyboard inside a Field List

| Key | Action | |---|---| | Tab | label → value → next row (creates one at the end) | | Shift+Tab | backwards | | Enter | new row; on an empty row, leaves the list | | Backspace in an empty label | delete the row |

A colon typed manually at the end of a label is stripped so it never doubles up.


No background theme — text colour only

There is no background theme. The editing surface is a plain neutral white, and the exported document paints no background — the Android TV player composes its own wallpaper behind a transparent WebView.

Colour comes from the operator, per run, via the toolbar colour picker. It is written as an inline style on a <span>:

<dd><span style="color: #ffffff">Media Team</span></dd>
  • The colour travels inside the HTML, so it reaches the Android TV document exactly as authored — no extra column, no second payload.
  • On a dark wallpaper, pick a light text colour; on a light one, a dark colour. Text with no explicit colour falls back to the player's default (the Android fontColor param, or #14181d).
  • Clear (in the colour popup) removes the inline colour. It does not paint a colour on top.

The popup offers preset swatches plus a native colour input for any hex value. Highlight colour (background-color) works the same way.

Dark-mode writing aid

A toolbar toggle (moon icon, Ctrl/Cmd+Shift+L) flips the editing surface to a dark background so light-coloured text stays visible while authoring. It is a per-viewer preview only — remembered in localStorage, and it never changes the content, the stored HTML, the TV output or the export. Opt out with options.rememberDarkMode: false, or set a custom options.darkModeKey.


Panel layout

One panel is the default. The Columns select turns the same block into 2 or 3 columns without retyping the content:

<div class="sv-panels" data-sv-columns="1">
  <div class="sv-panel">
    <h1 class="sv-panel__title">MEETING IN PROGRESS</h1>
    <dl class="sv-fields" data-sv-colon="align">…</dl>
  </div>
</div>

Reducing the column count removes the rightmost panel and its content.


Coexisting with legacy content during the transition

The editor stamps every document it saves:

<div class="sv-doc" data-sv-doc="1"> … </div>

The number is the format version, so a future change is detectable rather than guessed. The wrapper is a plain unstyled div — a player that knows nothing about it renders straight through.

It never enters the editing DOM. getContent() adds it, setContent() removes it, so selection, normalisation and undo keep working against a flat list of blocks. Deleting it by hand in HTML source mode is harmless: it comes back on the next save. Set options.documentMarker: false to store bare fragments.

import { isSmartivHtml, documentVersion } from '@smartiv.tv/html-editor';

isSmartivHtml(row.html)     // false for legacy content
documentVersion(row.html)   // 1 for Smartiv, 0 for legacy

HtmlView.kt reads the same marker and picks the stylesheet from it, falling back to a class-name check for Smartiv content saved before the marker existed. Drop that fallback once no unmarked content is left.

Why not just look for sv-fields: a Smartiv document that happens to be a plain paragraph carries no Smartiv class at all, and the heuristic would misfile it as legacy. The marker is on every document regardless of what is inside it.

The combination to watch

| | Old player | New player | |---|---|---| | Legacy content | works today | handled — body.legacy + the legacy rules | | Smartiv content | breaks — no tv.css, so field lists stack and colons vanish | handled |

The bottom-left cell — new content on an old player — is closed by portable output (below), which is the default.


Portable output (transition-safe)

By default getContent() emits self-contained HTML that renders correctly with no external stylesheet, so a screen looks right on an old player that has no tv.css:

  • a field list becomes a plain <table data-sv-block="fields"> whose colon sits in its own column, so the colons align in any renderer with zero CSS;
  • multi-column panels get inline flex;
  • text formatting is native HTML, and the operator's colours/fonts are already inline — they travel untouched.

The editing DOM is unchanged: setContent() reverses the transform back to the friendly <dl>/panel classes, so keyboard nav and the toolbar keep working, and the round trip is stable.

options: { output: 'portable' }  // default — safe on any player
options: { output: 'class' }     // lean class-based markup; needs tv.css

Switch to 'class' once the whole fleet ships tv.css: the HTML is shorter and one stylesheet can restyle every screen at once. toPortable / fromPortable are exported for use outside the editor.

Images are not supported — by design

There is no image button, and images cannot be introduced through any other route either. The sanitizer drops img, picture, source, svg, canvas, video, audio, figure and figcaption outright, rejects data: URLs, and allows no background-image in inline styles. Pasting or dropping an image file is refused with a message in the status bar rather than silently swallowed.

The reasoning:

  • A signage player is regularly offline or behind a captive portal. A remote image becomes a broken icon and a reflow in the middle of a rotation.
  • An inlined image inflates the document by a third (base64 is 4/3) and pushes the row past SQLite's 2 MB CursorWindow limit, which throws SQLiteBlobTooBigException on read.
  • Base64 cannot be cached separately: changing one character of text re-syncs the whole payload.

Screen artwork belongs in the player's own asset pipeline — served locally via WebViewAssetLoader and composed underneath or beside the WebView — not inside operator-authored HTML.


Bundled fonts (make the defaults render & travel)

The 12 default faces ship inside the package at dist/fonts/. They are not loaded automatically — the editor needs a URL to read them from, so that the preview, the export, and the TV player all use byte-identical files (this is what stops the font looking different in each place). One setup does all three:

  1. Serve the files. Copy the folder into your app's static dir, e.g. cp -r node_modules/@smartiv.tv/html-editor/dist/fonts public/fonts (or, in Vite, import them with ?url — the package exports ./fonts/*).
  2. Point the editor at them with fontBundledBase:
<SmartivEditor
  v-model="html"
  :options="{ fontBundledBase: '/fonts/', fontRemoteBase: '/api/fonts-files/', tv: { /* … */ } }"
/>

With fontBundledBase set, the preview shows the real faces and exportTv() embeds each used face as base64 — so the exported HTML carries its own fonts to the TV. fontRemoteBase does the same for operator-uploaded fonts. bundledFontFiles() lists the file names if you need a manifest.

So yes — the font travels CMS → TV inside the exported HTML. Without a base, the editor falls back to system fonts and the export cannot embed them.


Android TV output

exportTv() produces one complete HTML document: inline CSS, fonts embedded as base64 (when a base is set), no external scripts, <meta viewport width=1920>. It is async — await it.

import { buildDocument } from '@smartiv.tv/html-editor';

const html = buildDocument(contentHtml, {
  title: 'Smartiv Room Display',
  background: '#ffffff',
  color: '#14181d',
  rootFontSize: '16px',   // 1080p; lower it for 720p panels
  safeArea: '5%'
});

Send the exported document, not the stored fragment. modelValue is the editing fragment: its colons come from dt::after and its layout from the stylesheet. Hand that fragment straight to a WebView and the labels stack, the values indent, and every colon disappears. Two supported ways to render:

// A. store the fragment, wrap it at render time
const page = buildDocument(row.html, { ...resolveTheme(row.theme) });

// B. store the finished document, produced by the editor's export button

As a safety net, buildDocument() bakes each colon into the markup as <span class="sv-colon">:</span> and marks the list data-sv-colon-baked, so the generated colon stands down and there is never a double. If the stylesheet goes missing anyway, the screen still reads Event: Coordination & … instead of losing the separator. Load the fragment back into the editor and it un-bakes itself automatically.

Device notes:

  • Safe area — consumer TVs crop up to 5% per edge. Content sits inside .sv-tv__safe with --sv-safe-area padding; the ⛶ button draws the boundary in the preview.
  • Scale — every size is in rem. A 720p panel only needs a different rootFontSize; the stylesheet also steps down below 1366px.
  • Fonts are embedded — exportTv() inlines each used face as a base64 @font-face src, so the file renders the exact fonts offline in a bare, transparent WebView with no asset base and no network. Embedding needs a URL to read the files from: set options.fontBundledBase (and fontRemoteBase for uploaded fonts). Because it fetches the bytes, exportTv() is async — await it (or the export event payload). Without a base it falls back to url() references. buildDocument() stays synchronous for the live preview.
  • Self-describing — alignment (text-align), colour, and font-family are all inline or in the embedded <style>; the output paints no opaque background, so the player's wallpaper shows through. A bare viewer needs to add nothing.
  • No network — with fonts embedded, the exported document issues no requests at all.

Font manager — uploads shared across the whole CMS

A CMS embeds the editor on many pages. The font catalogue therefore lives in one app-level store, not per instance: a font uploaded on the Fonts page becomes selectable in every editor, including ones already mounted on other routes, with no reload.

import SmartivEditorPlugin, { createFontStore, createRestTransport } from '@smartiv.tv/html-editor';

app.use(SmartivEditorPlugin, {
  fonts: createFontStore({
    transport: createRestTransport({ endpoint: '/api/fonts' }),
    remoteBase: '/fonts/'
  })
});
<!-- the separate Fonts page -->
<SmartivFontManager />

<!-- every editor picks the catalogue up on its own -->
<SmartivEditor v-model="html" @update:fonts="fonts = $event" />

@update:fonts reports the families that screen actually uses — store it on the row so the player can warm its cache before the screen is due.

Backend contract. Full DB mapping, endpoints, file storage and the Android player wiring are in BACKEND.md — hand that to your backend team. In short: GET /api/fonts returns the uploaded families; standard fonts are added by the store and never come from the API:

[{ "id": 7, "label": "Brand Sans", "family": "Brand Sans",
   "fallback": "sans-serif", "source": "remote",
   "faces": [{ "file": "a3f9.woff2", "weight": 400 },
             { "file": "b71c.woff2", "weight": 700 }] }]

POST /api/fonts takes multipart file plus family, label, weight, style, fallback, and returns one such row. DELETE /api/fonts/{id} removes a family. file is resolved against remoteBase.

Without a transport the store still works: uploads are registered with the document and stay for the session. Useful for demos and tests.

Client-side validation before anything is sent: size cap (2 MB by default), magic-byte sniffing — a PDF renamed .woff2 is rejected — and an actual FontFace parse, so a file the TV's WebView would refuse is caught while the operator is still at the keyboard. A .ttf/.otf upload is accepted with a warning that converting to woff2 server-side saves roughly 40%.

The family name is whatever @font-face declares; it does not have to match the name inside the file. It does have to be unique, and it is written into saved content, so renaming it later orphans screens already using it.

Bundled fonts

Every font is declared once, in src/fonts.js. That registry feeds three places at build time:

  • the editor's Font dropdown,
  • the @font-face block injected into the editing surface (pass options.fontBaseUrl so the CMS serves the same .ttf files and the operator previews the real face),
  • dist/smartiv-fonts.css, read by the player from assets/smartiv/fonts.css — it carries both the @font-face rules and the .ql-font-* classes legacy content depends on.

Adding a font later:

// src/fonts.js
{ id: 'georgia', label: 'Georgia', family: 'Georgia', file: 'Georgia.ttf', fallback: 'serif' }
npm run build
cp dist/smartiv-fonts.css app/src/main/assets/smartiv/fonts.css
# drop Georgia.ttf into app/src/main/assets/fonts/

No Kotlin change, and the dropdown can only ever offer faces the player has. family must match the @font-face name exactly — it is the string stored inline in existing HTML, so renaming it orphans old content.

Rendering inside an existing Compose player

Recommended: depend on the JitPack AAR (see android/JITPACK.md):

implementation("com.github.SMARTIV-SAAS:HTML-Editor-by-Smartiv:1.0.4")
import com.smartiv.htmleditor.HtmlView

HtmlView(
    htmlContent = screen.html,
    theme = screen.theme,       // "light" | "paper" | "dark" | "midnight" | "brand"
    rootFontSize = 16.sp,       // 1080p; ~13.sp for 720p panels
    safeArea = "5%"             // only if this view is full-bleed
)

CSS/fonts ship inside the AAR. After editor CSS changes: npm run build:android-assets, commit, new tag, bump the app dependency.

Alternatively, vendor android/htmleditor/…/HtmlView.kt and copy stylesheets manually:

npm run build
cp dist/smartiv-tv.css app/src/main/assets/smartiv/tv.css
HtmlView(
    htmlContent = screen.html,
    theme = screen.theme,
    rootFontSize = 16.sp,
    safeArea = "5%"
)

body gets class="sv-tv" and the content is wrapped in .sv-tv__safe when the HTML carries the editor's hooks (sv-fields / sv-panels); otherwise it falls through to body.legacy and the legacy rules. The two stylesheets never collide because the legacy body block is scoped to that class.

Raw WebView

webView.settings.javaScriptEnabled = false
webView.settings.textZoom = 100     // don't let TV font scaling distort the rem scale

// Explicit themes must not be inverted by algorithmic darkening
if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
    WebSettingsCompat.setAlgorithmicDarkeningAllowed(webView.settings, false)
}

webView.setBackgroundColor(themeBackgroundColor(screen.theme))  // avoid a white flash
webView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null)

Browser support of the emitted CSS: Grid and custom properties (Chrome 49–57), column-gap on grid (66), and overflow-wrap: anywhere (80) with word-break: break-word as the fallback. color-mix() is not used.


Architecture

src/
  core/
    Editor.js        instance: command table, UI registry, selection, event bus
    selection.js     caret/selection helpers scoped to the editor root
    history.js       undo stack over HTML snapshots
    sanitize.js      tag/attribute/CSS whitelist, no dependency
    EventBus.js
  plugins/           one file per feature, all optional
    inline, blocks, align, lists, typography,
    fieldList,       ← the label:value block
    theme,           ← light/dark background presets
    link, table, history, sourceView, tv
  ui/                Vue layer: Toolbar, ToolbarItem, EditorDialog, TvPreview
  styles/
    tvCss.js         content stylesheet (editor surface AND exported file)
    editor.css       editor chrome
  presets.js         defaultPlugins, defaultToolbar, compactToolbar

Plugins never touch Vue. They register commands and toolbar items; the Vue components render whatever ended up in the registry.

Writing a plugin

export function watermarkPlugin(editor) {
  editor.addCommand('watermark', () => {
    const el = document.createElement('p');
    el.className = 'sv-watermark';
    el.textContent = 'SMARTIV';
    editor.selection.insert(el);
    return true;
  });

  editor.ui.addButton('watermark', {
    icon: '©',
    label: 'Watermark',
    command: 'watermark',
    active: () => !!editor.selection.closest((n) => n.classList?.contains('sv-watermark'))
  });
}
watermarkPlugin.pluginName = 'watermark';
<SmartivEditor
  :plugins="[...defaultPlugins, watermarkPlugin]"
  :toolbar="[...defaultToolbar, ['watermark']]"
/>

editor API: addCommand, execCommand, native, queryState, queryValue, addShortcut, addNormalizer, commit, ui.addButton/addSelect/addColor/addSeparator, selection.*, events.on/emit, getContent, setContent, history.

  • addNormalizer(fn) — register an idempotent DOM canonicaliser. It runs before every serialisation, so a cleanup is reflected in the value the host receives and in the undo snapshot, never one change behind. Prefer this over an events.on('change', …) handler that mutates the DOM.
  • commit() — record a change + history entry after mutating the DOM directly from a keydown handler (where no input event fires).

Security

Everything passes through sanitizeHtml() on both read and write:

  • tags outside the whitelist are unwrapped (their text survives)
  • media tags are dropped whole, along with script, iframe, object, embed, link and meta
  • on* attributes are stripped; href must match a safe URL pattern; data: URLs are rejected entirely
  • inline styles are limited to a property whitelist; url(), expression() and javascript: are refused
  • target="_blank" always gets rel="noopener noreferrer"

This matters because the result is executed by a WebView on the signage device.


Status (npm 1.1.1)

Published on npm as @smartiv.tv/html-editor (the Android AAR is versioned separately on JitPack — see android/README.md).

New in 1.1:

  • Font dropdown previews each family in its own face — pick a font by seeing it, like a word processor.
  • Numeric font sizes (12–96) instead of named tiers; values stay in rem so the document still scales by the root font-size.
  • Fixes: the Size dropdown now reflects the selected text's size, and a single column no longer boxes centre/right alignment to a narrow measure.

Working: inline formatting (bold/italic/underline/strikethrough, plus code, mark, small, del, ins, sub, sup, abbr), blocks and headings h1–h6, Preformatted (<pre>), alignment, line spacing, lists, typography with independent text and highlight colour, field lists (colon-aligned), 1–3 column panels, tables, links, undo/redo, HTML source mode, 1080p TV preview with safe area, standalone export, a font-upload manager shared across the CMS, and a dark-mode writing aid (remembered in localStorage) that never touches the content.

Output is portable by default — see Portable output. There is no background theme; text colour is set per run and travels inline.

Not included: images (deliberately — see above), find & replace, D-pad navigation inside the editor (the editor runs in the desktop CMS, not on the TV).


Local development

Only needed to work on this repo (not to use the package):

git clone https://github.com/SMARTIV-SAAS/HTML-Editor-by-Smartiv.git
cd HTML-Editor-by-Smartiv/smartiv-editor
npm install
npm run dev      # demo on http://localhost:5177
npm run build    # library bundle into dist/

Credits & ownership

Smartiv HTML Editor — designed, built and maintained by Smartiv.

  • Website: www.smartiv.tv
  • Repository: SMARTIV-SAAS/HTML-Editor-by-Smartiv

© 2026 Smartiv. Released under the MIT license — free to use, copy, modify and redistribute, provided the copyright and licence notice are kept. Published on npm as @smartiv.tv/html-editor.

The editor stamps each document it saves with data-sv-doc="1". That is a format marker, not a watermark — the player reads it to choose the right stylesheet, and it carries no branding. See Coexisting with legacy content.

Third-party note: no third-party editor code is included or redistributed here. The build-time dependencies in package.json remain under their own licenses.