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

fb-slides

v0.13.0

Published

Markdown-driven reveal.js decks: live demo embeds, annotation, mermaid, and a zero-config dev server

Downloads

2,521

Readme

fb-slides

Slides that are just Markdown. A reveal.js deck that reads your .md files directly, embeds real pages as live demos, and ships as one self-contained folder.

npx fb-slides create my-talk
cd my-talk && npm install && npm run dev

The engine is the package; a talk is only its own content. Bump the version and every deck you have written gets the new features — CHANGELOG.md says which.

The shape of a talk

my-talk/
├─ decks/          01-intro.md, 02-…  ← the talk, in file-name order
├─ demo/
│  ├─ counter/     a static page a slide embeds and runs
│  └─ angular-hello/  an app with its own dev server, started by `servers:`
├─ assets/         images the Markdown links to
├─ theme.css       colour overrides (optional)
└─ slides.config.js  (optional — every key has a default)

That is what create gives you: two nearly empty decks showing the reveal features, and one demo of each kind. Delete what the talk does not need — the Angular folder, its servers: entry and its slide come out together.

| | | | --- | --- | | npm run dev | serves the deck on :4000 and opens it | | npm run build | dist/ — self-contained, relative URLs, works offline | | npm run preview | build, then serve the result | | npm run kill | stops the deck, the demos and the editor — whatever is left holding their ports |

Writing

A slide is whatever sits between two --- lines. Edit the .md, reload the page — there is no build step and no export. Adding a section to the talk is dropping a file into decks/; there is no list to keep in step anywhere.

Three things happen automatically, so you never mark them up by hand:

  • a slide starting with an # h1 becomes a section divider (centred, gradient)
  • a slide starting with <!-- demo: … --> becomes a live demo (below)
  • bullet lists of 3+ items become fragments, revealed one at a time

Section names

The badge in the corner is the file's own section:, from the front matter:

---
marp: true
title: My talk — imperative tools
section: Imperative
---

Marp ignores the key, so the file still exports on its own. Without a section: the badge falls back to the file name.

Live demos

A slide that opens with a demo: comment is replaced by that page, running in an iframe, full-bleed. You can click around in it without leaving the presentation.

---

<!-- demo: cart -->

## Live demo

[the fallback for GitHub and Marp goes here]

The comment must be the first thing in the slide. The value is a folder name inside demo/, a ./ or ../ path taken as-is, or a full http(s) URL for anything that is not in the project:

<!-- demo: http://localhost:4200/checkout -->

The header then shows the host rather than the whole URL. Everything after the marker is what renderers that don't understand it will show — the deck replaces the whole slide.

A demo iframe is loaded when you arrive on its slide and unloaded when you leave, so the page is fresh every time: the cart is empty again, the counter back to zero. The deck also nudges the iframe's width once it loads — an embedded app that measures its container at mount would otherwise come up blank, because reveal scales slides with a CSS transform and that fires no resize inside the frame.

Moving a demo is moving those lines. Put the block wherever you want it, in any file; delete it and the demo slide is gone.

A demo can also be a whole application with its own dev server — Angular, or anything on Vite — copied into demo/ as it is. It is embedded by URL, not by folder name: docs/framework-demos.md.

The source button

editor: true puts a source ↗ link on every demo slide, opening that demo's folder in real VS Code in a new tab — code serve-web, the web server built into the editor you already have, started alongside the deck. Nothing is installed and nothing is embedded, and a built deck never carries it. Run dev once at your desk first: VS Code downloads its server half on the very first use.

It needs VS Code on the machine running the deck. serve-web is a subcommand of VS Code's own CLI, so the button exists only where that CLI does: code, or code-insiders, or a fork that kept it — editor: { command: 'cursor' }. Without it nothing breaks and nothing is missing from the talk; dev says so under the deck's URL and every demo slide comes up as it always did, minus the button. The same is true when code is installed but not on the PATH of the shell you start the deck from, which an IDE's built-in terminal manages on its own — name the binary outright then: editor: { command: '/usr/local/bin/code' }.

docs/source-code.md.

Stepped code highlighting

The line ranges live in the fence's info string — reveal reveals one group per click:

```js [2|3-4|5-9|10]

Harmless anywhere else: every other Markdown renderer just sees a js block.

Diagrams

A ```mermaid fence becomes a real diagram, rendered locally.

Speaker notes

A Note: block at the end of a slide never shows on screen, only in the speaker view (S):

## The description *is* the prompt

- `name` → how the agent **calls** it

Note: a bad description is the most common reason a tool never gets used.

Disabled slides

A slide whose source carries <!-- disabled --> on a line of its own stays in the file and in the navigator, but steps out of the talk:

---

<!-- disabled -->

## The version of this I cut on the train

---

The arrows walk past it in whichever direction they were going, it holds no slide number, and the count and the progress bar are computed as if it were not there. It is still listed in the navigator (V) and in reveal's overview, marked off — and clicking it there is how you look at it, which is the point of keeping it. Looking at one, the corner reads off where the number would be.

Half a talk is the slides you decided against but are not ready to delete. This is where they live, next to the ones that made it.

<!-- draft --> works the same way — out of the talk, uncounted, walked past — for a slide that is still being written rather than one you cut. It says so louder: a sky-blue DRAFT in the navigator and on the stage, where a disabled slide reads off.

On the dev server you do not need to type either marker. In edit mode — the edit drawer out, see Editing in the browser — every row of the navigator grows a ⋮ in its top right corner on hover, with:

  • Disabled, Draft and To Delete — tick one to set the marker, tick it again to take it off; a slide wears one of the three at most. <!-- to-delete --> is off like the others and flagged with a red TO DELETE: a slide you mean to get rid of but have not yet.
  • Separator above… — a line over the slide's row in the navigator, with an optional label: <!-- separator --> or <!-- separator: Part two --> at the top of the slide. It groups the list and nothing else — no slide, nothing on stage — and it travels with its slide when you move it. Double-click a separator — its label or the line — to rename it in place: Enter saves, Esc leaves it be.
  • Add page after — a new ## New slide right after this one.
  • Duplicate — a copy of the slide, notes and markers included (its separator excepted), right after it.
  • Delete — asks first; there is no undo. A file's only slide stays put.

Each one is written into the .md and the deck reloads with the navigator still open. A file that changed on disk since the page loaded is refused rather than guessed at.

Blocks

Six classes cover the layouts a talk keeps needing, so a slide asks for one by name instead of carrying a paragraph of inline styles:

| | | | --- | --- | | .cols / .col | two columns — with the min-width: 0 a code block falls over without | | .frame | an embed that shares the slide instead of taking it whole | | .caption | a quiet line under a figure | | .box | a bordered block: the point the slide comes back to | | .author-slide | the speaker page — photos down the left edge, the bio on the right | | <img>, <video> | centred, unframed, and never wider than the slide |

<div class="cols">
<div class="col">

**In the widget**

```js
app.openLink({ url: 'https://example.com/' });
```

</div>
<div class="col">

The blank lines matter: Markdown inside a block-level tag is only parsed
when there is one on each side of it.

</div>
</div>

Each of them fixes its numbers in CSS variables — --cols-gap, --frame-w, --frame-h, --author-photo, --author-hold — so a project retunes one from its own theme.css without restating the rule.

The speaker page is markup you write and the theme lays out. Give .author-photo as many <img> as you like: they cross-fade in the order they are written, on a loop that restarts every time the slide comes up.

<!-- .slide: class="author-slide" -->

<div class="author-photo">
  <img src="assets/author/on-stage.jpg" alt="" />
  <img src="assets/author/portrait.jpg" alt="" />
</div>

<div class="author-bio">
  <h1>Your name</h1>
  <ul><li>What you do</li></ul>
  <p class="author-meta">The stack · you · work · with</p>
</div>

Editing in the browser

On the dev server, Shift+E (or the last button on the toolbar) opens the slide you are looking at in a drawer: its markdown in one box, its speaker notes in another. Typing re-renders the real slide in place — same renderer, same theme — and Save writes the text back into the slide's own lines of the .md, leaving the rest of the file untouched. Esc closes without saving; moving to another slide — the header's arrows do it without leaving the editor — keeps the drawer open on it, dropping anything unsaved. Add inserts a fresh slide right after the current one and opens it; Delete removes the slide from its file, after asking. Clicking into the notes box trades the room with the slide box, so both are comfortable to write in.

The drawer is also the navigator's edit mode, and the pencil in the navigator's header turns it on and off from there too. While it is out, the navigator (V) is editable whether it is open or not — closed, it opens that way: pick a row up — a title or a preview, whichever face the list is showing — and drop it where it belongs, under another deck file's header too, or use the ⋮ on the row. A line in the accent shows where it would land. The slide is moved in the .md files, its own lines and nothing else, and the deck reloads on it with the navigator and the drawer still open. Esc drops a drag without moving anything; a click that does not travel still opens the slide. Closing the drawer leaves edit mode, and the navigator goes back to a list you only read. The only slide in a file stays in it — delete the file instead. Both the navigator and edit mode outlive a reload: refresh with either one on and it comes back on — already in place, with no slide-in, and the slide in hand back at the same height in the list.

The + over the slide box — or / typed on an empty line — opens the block picker: every block the theme knows, under group headers — the demo markers, the section divider and the speaker page; columns, callout, caption and framed embed; image and video with the attributes that make them behave; plain, stepped and mermaid fences; fragments, table and quote. The filter also answers to the words you would actually type — img, mermaid, appear — and the pane under the list shows the markup the highlighted row is about to write. The block lands where the cursor was, with its first placeholder selected so it is ready to type over; text already selected takes that placeholder's place, so picking Fragment wraps the paragraph that was highlighted. A block that only works at the top of a slide, like the demo marker, goes there whatever the cursor was doing.

snippets: in slides.config.js adds a project's own blocks to the list:

snippets: [
  { label: 'Pricing table', hint: 'the three tiers', keys: 'plans price', body: '<div class="tiers">${…}</div>' },
],

keys are extra words the filter answers to, and group names the header a block sits under — without one, a project's blocks gather under Project.

Dropping files into assets/

While the drawer is open, dragging files anywhere over the deck — a screenshot, a clip, a PDF — raises the folders assets/ already has. Drop on one and the files are saved there; drop on + new folder… and it asks for a name and makes it. The button next to the + opens the same panel for files you would rather pick than drag.

The list under the folder picker is that folder, as it is on disk: what was just dropped and what has been there since last month. Each row carries the path to write in the slide, a button that copies it, and one that writes it for you at the cursor: ![](…) for an image, a <video> with the attributes that make it behave for a clip, an <audio> for sound, a link for everything else. Picking another folder lists that one; what this session put there is tinted, so it stands out among files that were already around. Names are folded to something a URL can carry — Schermata città (1).PNG becomes Schermata-citta-1.png — and nothing is ever overwritten: a second clip.mp4 lands as clip-2.mp4.

The third button on a row deletes the file — the wrong screenshot, dropped a moment ago. It is the one thing here a reload cannot undo, so it asks: the Delete button stays dead until CONFIRM is typed into the box, in capitals. Esc backs out of the question, then out of the panel, then out of the drawer — one layer per press.

assets: in slides.config.js names the folder. It is published with the deck whatever else static: says, so a path the drawer hands out works in the build too.

The drawer, the drop and the navigator's edit mode exist only under fb-slides dev, and only at localhost (or 127.0.0.1): a built deck is static files and never shows the button, and from any other address — a phone on the LAN, a tunnel, GitHub Pages — the page does not even ask for the edit API.

Presenting

| Key | | | --- | --- | | → / Space | next step (fragments included) | | Q | the pen — draw on the slide, the ink fades on its own | | W | the arrow — click the tail, then the tip; same colours as the pen | | E | the spotlight — drag a rectangle, the rest of the slide dims and blurs | | R | the pointer — replaces the cursor: a glowing halo, a dart aimed at the centre, or the normal mouse | | X | put the toolbar away, or bring it back — it comes down when the mouse goes for the top edge or a tool arms, and stays until this | | Shift+E | edit the slide — markdown and notes, saved back into the .md (dev server only) | | S | speaker view — notes, timer, next slide | | Esc / O | overview — the slides as a grid | | V | the vertical navigator — every slide by title, down the left edge | | F | fullscreen | | B / . | black out the screen |

  • ?nofrag — every bullet at once, for a fast read-through
  • ?print-pdf — open it, then print to PDF (the iframes won't render)

slides.config.js

Every key is optional.

export default {
  title: 'My talk',              // browser tab, and the built page
  lang: 'en',

  decks: 'decks',                // folder of .md
  demos: 'demo',                 // folder behind a bare `<!-- demo: name -->`
  assets: 'assets',              // where a file dropped on the deck lands
  static: ['assets', 'demo'],    // served and published; auto-detected when omitted
  revealTheme: 'dracula',        // one of reveal.js's own themes; omit for this one
  webfonts: false,               // let the reveal themes fetch their Google fonts
  themePack: 'custom-aurora',    // one of this package's own named themes
  theme: 'theme.css',            // loaded last, over both; auto when it exists
  favicon: 'assets/favicon.png',

  signature: {                   // the corner logo. Omit the key, omit the logo
    name: 'fabiobiondi.dev',
    url: 'https://www.fabiobiondi.dev',
    logo: 'assets/logo.png',
  },

  servers: [                     // started by `dev`, alongside the deck
    { name: 'angular demo', cwd: 'demo/app', command: 'npm', args: ['start', '--', '--port', '4200'] },
  ],

  editor: true,                  // a `source ↗` button on every demo slide (needs VS Code)

  port: 4000,
  open: true,
  outDir: 'dist',
  exclude: ['dist'],             // extra path segments the build skips
  fragmentLists: true,           // bullet lists reveal one item at a time
  snippets: [],                  // your own blocks in the edit drawer's picker
  reveal: { transition: 'fade' },// passed to Reveal.initialize()
  mermaid: {},                   // passed to mermaid.initialize()
};

A servers: entry that fails — a missing node_modules, a busy port — prints a warning and nothing more: the slide that embeds it comes up empty, the rest of the deck works.

Copying a real Angular or Vite app into demo/ and wiring it up — including the port flag Vite needs so a busy port fails instead of moving: docs/framework-demos.md.

editor: true is a side process of the same kind, and fails the same way: no VS Code on the machine, or no code on the PATH of the shell that started the deck, and you get a warning under the deck's URL and demo slides without their source ↗ button. It listens on 7100, clear of the ports the demos want, and walks up from there if something holds it: docs/source-code.md.

When something survives

dev starts more than itself, and it takes its side processes down with it — as long as it is asked politely. Killed outright, or with its terminal window closed, it leaves them: a vite or an ng serve under the npm that started it, code serve-web, all still holding the ports the next run wants.

npm run kill

It reads the ports out of this file — the deck's, preview's one above it, the url: of every servers: entry, the editor's 7100–7109 — and stops whatever is listening on them, naming each one before it goes:

  ✗ 4000  deck           node …/bin/fb-slides.mjs dev (39190)
  ✗ 4200  angular demo   ng serve (39212)
  ✗ 7100  editor         …/Visual Studio Code.app/… (32669)

  3 processes stopped.

Going by port rather than by process tree is what makes it work at all after the deck is gone: the port is the thing already written down, and the process holding it is the one in the way. The npm above it exits on its own once its server is stopped.

A servers: entry with no url: is the one thing it cannot reach — that key is how the config says where the demo answers.

dev also looks before it leaps: a port a servers: entry is about to use that is already answering is a leftover from a previous run, and it says so under the deck's URL rather than letting you find out from a demo slide showing yesterday's build.

  kill test
  → http://localhost:4010/
  ⚠  4201 is already in use — angular-hello from an earlier run?
     That server is not this deck's: `npx fb-slides kill` frees the ports it uses.

Theming

theme.css in the project is loaded after the package's base theme, so it overrides rather than replaces. Almost everything hangs off the tokens in :root:

:root {
  --bg: #0e1116;
  --card: #1a1f27;
  --text: #e8ebf0;
  --muted: #97a1b0;
  --accent: #6ea8fe;
  --accent-2: #7ee2b8;
}

reveal.js themes

The fifteen themes from revealjs.com/themes are all in the box — pick one by name and nothing else changes:

export default { revealTheme: 'dracula' };

beige · black · black-contrast · blood · dracula · league · moon · night · serif · simple · sky · solarized · white · white-contrast

Only the chosen one is ever loaded — one <link>, ~7 KB — and the build copies that file and nothing else. Under dev all fifteen are reachable, one URL each, so the picker below can try them without a restart. Two things happen beyond the link:

It is served from the deck, not from a CDN. Six of the themes open with @import url(https://fonts.googleapis.com/…). Those lines are stripped, so a talk still renders with the wifi off; the theme falls back to the next font in its own stack. Pass webfonts: true to let them through and get the typography of the previews exactly. The font folders reveal ships itself — league-gothic, source-sans-pro — are copied into dist/ when the theme asks for them.

The chrome follows the theme. The navigator, the badge and the signature are not .reveal elements, so a borrowed theme would leave a black panel down the side of a white deck. Every reveal 5 theme declares its palette as --r-* custom properties, and the base theme points its own tokens at them, so the whole page moves together. A theme.css in the project still has the last word over both.

The named themes

Two whole looks ship with the package, picked by name:

export default { themePack: 'custom-aurora' };

custom-aurora — atmospheric. Coloured light pooling behind the slide, glass panels floating on it, a display serif carrying the titles.

custom-editoriale — flat and typographic. No panels and almost no colour: one serif, hairlines for dividing, and the accent kept for the two or three words a slide is about.

They are not reveal themes and they are not a second renderer. A pack is written against the same --bg / --accent / --text tokens as the base theme and styles the same slide vocabulary, which is what lets both of them dress the same Markdown: the chips line that is a row of pills under custom-aurora is a // LIKE THIS rule under custom-editoriale, and the .md does not change a character.

The three layers compose, in this order — reveal's theme, then the pack, then your theme.css. Wearing a pack and still overriding two of its colours is a two-line file.

Neither pack fetches a webfont. The serif is whatever the machine already has, closest first; a talk that has to survive the conference wifi cannot open with a request to fonts.googleapis.com.

Switching themes while you work

fb-slides dev puts a theme picker in the toolbar — the last button, next to the pen. It lists everything the deck could wear, under two headings: the packs above, reveal's fifteen below, with the bare base theme at the top. Picking swaps one <link>: no reload, no restart, and the choice survives a refresh.

One choice, not two. The config will let you name a pack and a reveal theme at once, but layering them gives you neither look — only whichever rule happened to come last — so the picker treats them as one question. Your theme.css still lands on top of whatever is picked, as it always does.

It is a preview, not a setting. Nothing is written to disk — slides.config.js is a module with your own comments in it, and a picker that rewrites it is a worse problem than the one it solves — so the panel prints the line to paste when you have decided.

The picker exists only under dev, and by construction rather than by a flag: dev renders the page with the theme list in it and build does not, so a published deck has nothing for the switcher to read and it never builds itself. The fifteen are the same way: dev can rewrite any of them on request, build writes only the one the config names.

The slide vocabulary the packs dress

Four blocks both packs know. All four are in the edit drawer's + picker, so none of this has to be remembered:

<p class="chips"><span>Angular</span><span>Signals</span><span>v22</span></p>

The line above the title. The first chip is the loud one — in a row of equals nothing is being asked.

<div class="rows">
  <div class="row">
    <span class="row-icon">( )</span>
    <div><strong>Signals</strong><p>UI state, derived values, clean templates.</p></div>
  </div>
</div>

Two or three things, each with a glyph and one sentence. class="rows rows--cta" turns the same block into the last slide's ways out: every row grows an arrow, and the first is filled.

<!-- file: cart.component.ts -->
```ts
…
```

A header bar on the code block: the name on the left, the language on the right. The language is never written down — it is the file's own extension. The marker is a comment, in the shape the demo slides already use, so the .md still renders anywhere else.

A two-column Markdown table with an empty header row is a definition list — the term on the left, one sentence on the right — and the empty bar is dropped rather than drawn.

Publishing

npm run build   # → dist/

dist/ carries reveal.js and mermaid with it and uses only relative URLs, so it can be published at a domain root or in any subfolder, and it works with no network at all. It also holds decks.json, the file list a static host cannot produce on its own — generated, never hand-written.

What the build cannot fix: a slide embedding http://localhost:… shows an empty frame once published. Those URLs exist only on the machine running them.

Also usable with Marp

Each .md keeps its own front matter, so the files still work standalone:

npx @marp-team/marp-cli decks/01-intro.md --pdf

Marp does not render the mermaid fences or the demo slides — the deck does.

Overriding the engine

The dev server serves the project before the package, so a file of the same name next to your decks shadows the one that ships here: dropping in your own deck.js or theme.base.css replaces it for that talk. It is the escape hatch, not the workflow — what you shadow stops receiving updates.