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

@nomudev/blob

v0.3.5

Published

Le poisson de Nomu, anime : un avatar SVG pilote par un etat et une expression.

Readme

@nomudev/blob

Nomu's blob, animated, in any React page.

An <svg> with no background, driven by one prop: what the assistant is doing. No dependency, no stylesheet, no image to load — the drawing is computed, 16 kB gzip once and for all.

import { NomuBlob } from '@nomudev/blob/react'

<NomuBlob mood="thinking" size={40} />

Install

pnpm add @nomudev/blob

That is all of it. No token, no registry to configure, nothing special in CI or on Vercel. React 18 or 19, as a peerDependency — the package installs nothing.

The component

| prop | default | what it is | |---|---|---| | mood | 'idle' | what the assistant is doing, see the table below | | expression | 'neutre' | the face's mood — only visible on idle | | size | 96 | side of the square, in pixels | | follow | false | the gaze follows the pointer | | surprise | true | clicking the bot brings the fish back for a second | | label | 'Nomu' | name for screen readers; empty string = decorative | | frozenAt | — | freezes the render at that date: a thumbnail, no loop | | className, style | — | applied to the container |

There is no colour prop: the blob wears its own skin — the gradient, the warm blot and the white rim of the Nomu drawing — and it is an identity, not a palette.

Changing a prop does not remount the component: the clock keeps running, and the blob crosses from one state to the next through the fade measured off the video. That matters most for mood, which changes on every message.

The moods

Seven are held for as long as you leave them:

| mood | what you see | |---|---| | idle | at rest: breathes, blinks, follows the cursor | | listening | the eyes open wide | | thinking | the body becomes three pulsing dots | | working | the blob turns, the six measured rings orbit it | | searching | the blob scans, no rings | | sleeping | asleep | | warning | the "!", held upright |

Five are gestures: they play once, then the blob returns to the held state. A notification that stays on screen is no longer a notification.

| mood | what you see | |---|---| | notification | the blue dot appears | | alert | the "!" travels across and comes back | | wink | a wink | | sending | the blob tightens, the ribbons orbit it, it comes back | | celebrating | the blob collapses, particles fly out, it recomposes |

To fire a gesture without going through the prop — the normal case, since a gesture is not a state:

const blob = useRef<BlobHandle>(null)
// ...
<NomuBlob ref={blob} mood={busy ? 'thinking' : 'idle'} />
blob.current?.play('notification')

The fourteen catalogue ids (egg, hexagon, play, burst…) are also accepted in mood, for whoever knows what they want.

The expressions

Sixteen, and they only show on idle: everywhere else the face is part of the animation measured off the video, and overwriting it would mean no longer playing it.

neutre, attentif, surpris, excite, heureux, hilare, colere, triste, effraye, mefiant, confus, curieux, fier, timide, blase, somnolent.

<NomuBlob mood="idle" expression="heureux" />

In a chat

The typical wiring, driven by the conversation's state:

'use client'
import { NomuBlob, type BlobHandle } from '@nomudev/blob/react'
import type { BlobMood } from '@nomudev/blob'

function moodOf(status: Status): BlobMood {
  if (status === 'tool') return 'working'
  if (status === 'thinking' || status === 'streaming') return 'thinking'
  if (status === 'error') return 'warning'
  return 'idle'
}

export function NomuAvatar({ status }: { status: Status }) {
  const blob = useRef<BlobHandle>(null)
  useEffect(() => {
    if (status === 'done') blob.current?.play('wink')
  }, [status])
  return <NomuBlob ref={blob} mood={moodOf(status)} size={36} follow />
}

Two habits:

  • one instance, kept mounted, whose mood you change. Mounting one blob per message restarts the clock every time, and the fades never happen;
  • follow on the big one, not on thumbnails: it listens to the pointer across the whole window.

Two entry points, and why

@nomudev/blob/react carries 'use client'. React therefore replaces every one of its exports with a client reference: a server component reading one gets an opaque proxy, not the value. So the catalogues live in @nomudev/blob, which imports from either side.

import { MOODS, EXPRESSIONS, PEAU } from '@nomudev/blob'          // values, anywhere
import { NomuBlob, type BlobHandle } from '@nomudev/blob/react'   // the component

Written the other way round, Object.entries(MOODS) in a page returns an empty list — with no error and no warning.

The component's container is a <span> carrying data-nomu-blob. A descendant selector in your page can hit it without meaning to (.card > span { opacity: .6 } and the blob goes pale): the attribute is there to target it, and above all to exclude it.

Without React

The main entry point depends on nothing:

import { createBlob } from '@nomudev/blob'

const blob = createBlob(document.querySelector('#avatar')!, { mood: 'thinking' })
blob.update({ mood: 'idle' })
blob.play('notification')
blob.destroy()

That is exactly what the React binding drives — and what the Vue studio uses too. There is only one drawing.

Clicking brings the fish back

The body was Nomu's fish before it was a rounded square, and clicking the bot returns it for two and a half seconds — wearing today's gradient and rim, so it arrives as a variation rather than as another application blinking through.

Nothing to wire: it is on by default. The click is never captured, so a page that listens for its own still receives it. surprise={false} turns it off for a host that wants the click to itself.

It is deliberately not exposed as a button — no role, no focus ring, no cursor change. It opens nothing and leads nowhere, and a gag that announces itself isn't one.

The background

There isn't one. The eyes are real holes punched in the body, not light shapes laid on top: that is what makes them clip themselves when they slide towards the edge of the silhouette. So they show the page through, whatever it is.

The paper option puts a plate of the body's exact shape underneath, which fills the eyes with a given colour. It is only for a render with nothing behind it — a PNG export, a rasterised thumbnail.

The body stays Nomu

working, searching, sending and celebrating play the animations measured off the video — orbit's rings, the comet's ribbons, the burst's particles — but it is the blob playing them, not the ball. The originals don't tuck the body away, they erase it: a spinning triangle, a dot, dust. That is right in a montage a few seconds long; in a conversation, the avatar is an identity.

The one thing a chosen body doesn't take is orbit's triangle: that is a shape, not a scale. The rotation is there — applied to the whole avatar, face included, rather than to the profile alone. The white rim follows the body's own size, so a body folded into a dot doesn't wear a ring thicker than itself. The fourteen measured states are untouched and still reachable by their id.

What isn't a setting

The bot's numeric constants are measurements taken frame by frame off a reference video, not settings: gaze angles, eye sizes, timings. The body and its colours are Nomu's blob, read off the original drawing. Rounding them or "simplifying" them breaks the resemblance, which is the only success criterion here.

The repository explains the detail, in docs/measurements.md and docs/architecture.md.