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

cincin

v0.2.0

Published

🥂 Framework-agnostic toast library: observable core, thin adapters, polished UX

Readme

The framework-agnostic core of the cincin toast library: an observable entry store, a presenter that shows it (cincin/presenter), and framework-free DOM controllers (cincin/dom).

Looking for the React quick start? See cincin-react.

Install

npm install cincin

The package ships untranspiled modern JS as ESM only.

The store

import { createToaster } from 'cincin';

const toaster = createToaster({ duration: 4000 });

toaster.success('Saved');
toaster.promise(upload(), {
  loading: 'Uploading…',
  success: (ms) => `Uploaded in ${ms}ms`,
  error: () => 'Upload failed',
});

const unsubscribe = toaster.subscribe((event) => {
  // event.type: 'added' | 'updated' | 'removed'
});

The store holds ToastEntry records: create/update/remove plus the type sugar. Content is opaque: createToaster<MyContent>() stores whatever your renderer understands. Duration and dismissibility derive from the type (loading is open-ended and locked) unless set explicitly, and an update re-derives them only when the type changes: a content-only update never rewinds the clock, and a morph that must restart it changes the type. Showing is not the store's business.

The presenter

import { createPresenter } from 'cincin/presenter';

const presenter = createPresenter(toaster, { max: 5, exitDuration: 400 });

presenter.subscribe(render);
presenter.mount(); // entries enter, clocks run; unmount() stops it all

// The snapshot holds Toast values: one per showing.
for (const toast of presenter.getSnapshot()) {
  toast.key; // unique per showing (an entry can show more than once)
  toast.entry; // the stored record
  toast.phase; // 'queued' | 'active' | 'leaving'
}

presenter.dismiss(key); // begins the exit; the renderer finishes it
presenter.finish(key); // exit done: the toast goes, and so does the
// entry, once no showing of it remains
presenter.pause(); // hover: freeze the expiry clocks
presenter.setOptions({ max: 3 }); // live options; a raised max promotes

A removed entry does not vanish from the screen: its toast becomes a leaving ghost and plays the exit. A leaving toast nobody finishes is finished by a safety net, a small grace past the declared exitDuration.

DOM controllers

Framework-free controllers from cincin/dom. They take an element and a presenter, write no styling of their own, and are what the adapters are built on.

Swipe to dismiss

import { attachSwipe } from 'cincin/dom';

const detach = attachSwipe(element, {
  directions: ['right', 'down'],
  onDismiss: () => presenter.dismiss(key),
  onRemove: () => presenter.finish(key),
});

The controller writes translate and --cincin-swipe-x/y on the element and marks data-swiping / data-swipe-direction; skins style off those and declare user-select: none on the region. Reduced motion is respected.

Stack layout

import { createStackLayout, createSlotObserver } from 'cincin/dom';

const layout = createStackLayout({ visible: 3, gap: 12 });

// the data spine: mirror the rendered list
layout.setEntries(
  toasts.map((t) => ({ key: t.key, leaving: t.phase === 'leaving' }))
);

// per card: a lens over that card's slot
const slot = createSlotObserver(layout, { key });
const unobserve = slot.observe(element); // the measured node
const unsubscribe = slot.subscribe((s) => {
  // put the slot onto the card in the CSS protocol's vocabulary:
  // --cincin-toast-index, --cincin-toast-offset, z-index, data-hidden,
  // the tri-state data-front, and the measured heights
});
// later: layout.destroy()

The layout measures each card's body with a ResizeObserver, computes a StackSlot per card (index, offset, zIndex, hidden, front, leaving and the measured heights) and publishes changes through the observers; it writes nothing to the DOM itself. The consumer puts the slots on screen (the CSS protocol for skins, inert for semantics) and turns the numbers into motion with its own transitions; mixed natural heights collapse into one clean edge. Slot references are stable between changes, so subscribe/getSnapshot plug straight into useSyncExternalStore.

Visibility pause

import { attachVisibilityPause } from 'cincin/dom';

const detach = attachVisibilityPause(presenter);

Pauses every toast while the document is hidden and resumes its own on return, composing with other pause sources (hover).

Hotkey

import { attachHotkey } from 'cincin/dom';

const detach = attachHotkey('Alt+T', () => {
  region.focus({ preventScroll: true });
});

Listens on the document (or options.target) and calls back on the match, after preventDefault so the keystroke neither types a character nor opens a browser menu. Detach with the returned function or by aborting options.signal. The string names at least one of Control, Alt or Meta, then Shift if wanted, then the key, in that order: letters, digits, F1 to F12, the arrows, Home, End, PageUp, PageDown, Enter, Escape, Space, Tab, Backspace and Delete. Letters and digits match the physical key, so Alt+T fires on the T key under any layout. The grammar is the one aria-keyshortcuts reads, so the same string announces the hotkey.

Browser support

The package ships untranspiled modern JS. The newest APIs are AbortSignal.any (the swipe controller) and ES2023's Array.prototype.toReversed (the stack layout), with sizes coming from ResizeObserver: Chrome 116+, Safari 17.4+, Firefox 124+, Node 20.3+. Skins may raise the bar further with their CSS: the react skin's stylesheet uses @starting-style and light-dark(), which want 2024-class browsers.

Source and issues: github.com/nbarinov/cincin