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

@mulmocast/beat-editor

v1.1.1

Published

Vue 3 components for editing MulmoScript beats live in the browser — every beat type renders as a div, and every editor is swappable

Readme

@mulmocast/beat-editor

npm version npm downloads License: MIT CI GitHub stars

Vue 3 components for editing @mulmocast/deck slide decks live in the browser.

A list editor over a MulmoScript's beats. Every beat type renders as a div, and the pane on the right is whichever editor is registered for that beat's image.type — that registry is the extension point: replace an editor, or add a second one for a type.

A controlled component with no backend, no persistence and no AI — just data ↔ preview.

Highlights

  • Swappable beat editors — one registry keyed by image.type, several editors allowed per type, and your own can replace or join the shipped ones.
  • Every beat type previewstextSlide / markdown / chart / mermaid / image / movie / slide / html_tailwind, each rendered into a div. One shared copy of chart.js and mermaid serves the whole list, and the stylesheet ships prebuilt — no per-slide CDN load.
  • Inspector for structure — add / remove / swap layout type / nest content blocks / edit non-text fields. It is the slide beat's editor.

Install

yarn add @mulmocast/beat-editor @mulmocast/deck vue

vue ^3.5 and @mulmocast/deck ^2.0.0 are peer dependencies. 2.x is the only line this package is developed and tested against.

Usage

Edit a MulmoScript's beats

BeatListEditor edits the beat array itself, so add / remove / reorder are array operations — the deck editor extracts slide beats and zips them back, which is why it cannot insert mid-deck. Every beat type beatToHtml renders previews in the list; the pane on the right edits the selected one.

<script setup lang="ts">
import { ref } from "vue";
import { BeatListEditor, makeBeat, type EditableBeat } from "@mulmocast/beat-editor";
import "@mulmocast/beat-editor/style.css"; // required — see The stylesheet is not optional

const beats = ref<EditableBeat[]>([makeBeat("textSlide"), makeBeat("chart")]);
</script>

<template>
  <BeatListEditor v-model:beats="beats" />
</template>

A beat is edited as Record<string, unknown>, not as the cli's MulmoBeat: a half-typed url or a chartData mid-keystroke is routinely invalid, and beatToHtml returns undefined for anything it cannot render, so the preview degrades instead of breaking.

When your host owns a whole MulmoScript

BeatListEditor takes and emits the beat array. If what you hold is a whole script, beatsOf and withBeats are the two lines in between:

<script setup lang="ts">
import { beatsOf, withBeats, BeatListEditor, type EditableBeat } from "@mulmocast/beat-editor";

const props = defineProps<{ script: Record<string, unknown> }>();
const emit = defineEmits<{ "update:script": [script: Record<string, unknown>] }>();
</script>

<template>
  <BeatListEditor :beats="beatsOf(props.script)" @update:beats="(beats: EditableBeat[]) => emit('update:script', withBeats(props.script, beats))" />
</template>

Write withBeats(script, beats) rather than { beats } — the second drops presentationStyle, slideParams, and everything else the script carried, and nothing tells you it happened.

beatsOf answers with an empty array for anything without a usable beats array, so it is safe to call before the script has loaded. It hands through every element, including one the editor cannot render: dropping one would round-trip as deletion the next time the host wrote the array back.

Embedding in a narrow host

BeatListEditor reflows on its own width, not the window's, so a host that puts it in a card or a split pane declares nothing. Below 48rem the editing pane moves under the beat list and takes the full width; above it, the two sit side by side.

Above the breakpoint the pane's width is a token:

.my-host { --beat-editor-pane-width: 20rem; } /* default: 24rem */

Measured in a browser at a 600px-wide host: the beat preview went from 156px to 540px.

The token is a raw CSS escape hatch, so give it a positive length. Measured, in a 1000px host: 0 leaves a 33px strip of padding, 100% starves the list to 32px and overflows, and an invalid value (-5rem, banana) drops the declaration so the pane falls back to width: auto and sizes to its content — not to the stacked w-full.

Register your own beat editor

Which editor opens for a beat is decided by one list. Pass your own and you can replace what ships, or add a second way into a type that already has one — which is how chart gets both a form and a raw-JSON view.

<script setup lang="ts">
import { ref } from "vue";
import { BeatListEditor, defaultBeatEditors, makeBeat, type BeatEditorDefinition, type EditableBeat } from "@mulmocast/beat-editor";
import MyChartEditor from "./MyChartEditor.vue";

const beats = ref<EditableBeat[]>([makeBeat("chart")]);

const editors: BeatEditorDefinition[] = [
  // Replace what ships for one type...
  ...defaultBeatEditors.filter((editor) => editor.beatType !== "chart"),
  // ...or drop the filter to leave the built-ins in place and add yours as a second tab.
  { id: "chart.mine", label: "Mine", beatType: "chart", component: MyChartEditor },
];
</script>

<template>
  <BeatListEditor v-model:beats="beats" :editors="editors" />
</template>

The component's contract is the whole extension point:

props: { beat: EditableBeat }
emits: update(beat: EditableBeat)

It edits beat.image and nothing else — the fields every beat has (the spoken text) are drawn by the pane, so an editor stays about the one shape it knows. editorsFor(beatType, editors) returns every editor registered for a type in registration order, and the first one is what opens by default. A type with no editor registered is not an error: the pane says so and the beat still previews.

The demo registers one (textSlide.outline) on top of the defaults, so the tab switcher is visible in yarn dev under Beat editor.

What ships

| image.type | Editors | Notes | |---|---|---| | textSlide | Form | title / subtitle / bullets | | markdown | Markdown | the string form; the named-slot layout form is read-only | | chart | Form, JSON | the form covers the shape a Chart.js config almost always has; JSON is there for the rest | | mermaid | Diagram | title + diagram source | | image / movie | Form | media source (url / path / base64) | | slide | Form | the full Inspector — every layout and content block | | html_tailwind | HTML | author markup, emitted as written |

Components

| Component | Purpose | |---|---| | <BeatListEditor> | List editor over a MulmoScript's beats. v-model:beats, optional editors. | | <BeatView> / <BeatEditorPane> | One beat's preview and one beat's editing pane — drop into your own layout. | | <Inspector> | Structural editor for one SlideLayout. The slide beat's editor is built on it. | | defaultBeatEditors / editorsFor | The shipped editor registry, and the lookup a host uses. | | sanitizeFragment | The sanitizer BeatView runs a fragment through before inserting it. | | defaultTheme / sampleDeck | Data helpers for quick starts. | | makeBeat / BEAT_TYPES / beatType / beatImage | Beat helpers, for building the array a host edits. | | beatsOf / withBeats | Read and write a script's beats when the host owns a whole MulmoScript. |

The Inspector covers every layout (title / bigQuote / columns / comparison / grid / stats / timeline / split / matrix / table / funnel / waterfall / manifesto) and every content block type (text / bullets / callout / tag / code / metric / divider / image / imageRef / chart / mermaid / section / table), with full CRUD + reorder on every array.

How preview works

BeatView calls beatToHtml(beat) from mulmocast/browser, which returns a body-only fragment with no <script> in it. The fragment is sanitized and inserted into a div. chart.js and mermaid are loaded once per page and shared by every beat, and the stylesheet is prebuilt (dist/lib/style.css) rather than fetched — so a list of thirty beats loads the runtimes once, not thirty times.

Architecture

BeatListEditor.vue       (controlled — props: beats, editors; emits: update:beats)
├── BeatView.vue         -- one per beat: its own fragment and its own chart / mermaid runtime,
│                           so editing one beat leaves the others alone
└── BeatEditorPane.vue   -- right (the fields every beat has, plus the registered editor)
    └── editors/registry.ts -- image.type -> component, replaceable by the host
        └── editors/SlideEditor.vue -> Inspector.vue (the `slide` beat's editor)

State is held by the parent (Vue v-model pattern). All mutations are immutable copies, so a preview re-renders reactively.

Scripts

yarn dev          # Vite dev server with the SPA demo
yarn build        # build the demo SPA → dist/
yarn build:lib    # build the publishable library → dist/lib/ (used by prepublishOnly)
yarn lint
yarn format
yarn typecheck    # vue-tsc over src and test
yarn test         # node:test over the pure helpers
yarn knip         # dead-code scan

License

MIT

Using the beat components

yarn add @mulmocast/beat-editor @mulmocast/deck mulmocast vue
import { BeatListEditor, BeatView, defaultBeatEditors } from "@mulmocast/beat-editor";
import "@mulmocast/beat-editor/style.css";   // required — see below
<BeatListEditor :beats="beats" @update:beats="beats = $event" />

BeatView renders one beat, and with editable it also takes inline text edits, the formatting toolbar, and drag-to-reorder of the slide's list items; BeatListEditor is the list plus the editing pane.

The stylesheet is not optional

beatToHtml and generateSlideFragment emit Tailwind class names as strings inside compiled JavaScript. Tailwind honours .gitignore, so it never scans node_modules and your build never generates text-[60px] — a beat renders unstyled with no error to say so. @mulmocast/beat-editor/style.css carries those utilities, generated at publish time.

It contains no preflight: a component library should not reset your page. The typography a markdown beat expects is restored inside .beat-fragment at runtime, so a beat looks the same whether or not your app runs preflight.

Your page does need box-sizing: border-box, which Tailwind's own preflight provides — and if you are already running Tailwind, you have it. Without it every utility that pairs a width with padding overflows by the padding: measured against the built library in a 600px host with no reset, ten elements ran past the edge and the editing pane came out 632px wide.

Swapping an editor

Every editor is a component with one contract — props: { beat }, emits: update(beat) — registered as a BeatEditorDefinition:

import { defaultBeatEditors, type BeatEditorDefinition } from "@mulmocast/beat-editor";
import MyChartEditor from "./MyChartEditor.vue";

const editors: BeatEditorDefinition[] = [
  ...defaultBeatEditors.filter((e) => e.id !== "chart.form"),
  { id: "chart.mine", label: "Mine", beatType: "chart", component: MyChartEditor },
];
<BeatListEditor :beats="beats" :editors="editors" @update:beats="beats = $event" />

A beat type may have several editors; the pane offers them as tabs. chart ships two — a form for the shape a Chart.js config almost always has, and a raw-JSON editor for everything else. Neither erases the other's work: the form carries options, plugin config and per-dataset styling through untouched.