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

@flarecart/admin-ui

v1.1.0

Published

The small vanilla-JS UI kit FlareCart's admin is built from: buttons, page headers, sortable table headers, switches, toasts, modals, flyouts, confirm and prompt dialogs, a searchable combobox, context and action menus, and an inline SVG icon set. Styled

Readme

@flarecart/admin-ui

The small vanilla-JS UI kit FlareCart's admin is built from: buttons, page headers, sortable table headers, switches, toasts, modals, flyouts, confirm and prompt dialogs, a searchable combobox, context and action menus, and an inline SVG icon set. Tailwind v4 classes, light and dark; markup built with DOM calls, never parsed; no dependencies.

Why

An admin page needs the same dozen pieces every time: a button, a dialog that asks "are you sure?", a list of 180 currencies you can type into, a "⋯" menu on each row, a toast that says it saved. Frameworks bring those along with a build, a runtime and an opinion about state. FlareCart's admin is plain modules and document.createElement, and these are the pieces it's made of, lifted out so another admin page can look and behave the same:

  • Nothing a seller types becomes markup. Every element is made with DOM calls and text goes in as text, so a file called <img onerror=…>.zip is just a long name.
  • Native where it counts. Modals and flyouts are <dialog> (focus, Escape and the backdrop for free), the switch is a checkbox, the toast stack is a popover so it shows above an open dialog.
  • Keyboard and screen readers. Arrow keys through the combobox and menus, Escape closes the innermost thing only, aria-sort on sortable headers, role="switch", labelled icon buttons.
  • The same classes as FlareCart. Same Tailwind classes, same theme, so a page built from this sits beside FlareCart's admin without a seam.

Features

  • 🧱 Elements: $ and el, the two helpers everything is built with.
  • 🔘 Controls: button, pageHeader, toggle, tile, emptyState, loading.
  • ↕️ Sortable tables: sortHeader, nextSort, compareValues (natural, case-blind, empties last).
  • 💬 Feedback: toast.
  • 🪟 Dialogs: modal, flyout (stacks, guards unsaved changes), confirmDialog, promptDialog.
  • 🔎 Combobox: a searchable select for long lists.
  • 📋 Menus: contextMenu at the pointer, menu behind a "⋯" button.
  • ✏️ Icons: 45 Lucide-style icons, addIcons() for your own.
  • 🌍 Labels: the five words the kit shows itself, in your language with setLabels().
  • 🚫 Zero dependencies.

Install

npm install @flarecart/admin-ui

Tailwind v4 setup

The components only set class names; Tailwind has to see them to generate the CSS. In your stylesheet:

@import "tailwindcss";
@import "@flarecart/admin-ui/theme.css";
@source "../node_modules/@flarecart/admin-ui/src";
  • @source points Tailwind at the kit's scripts. The path is relative to the CSS file, so adjust the ../ to reach your node_modules. Tailwind v4 skips node_modules when it looks for classes, so without it utilities the kit uses and your page doesn't (max-w-[min(20rem,calc(100vw-2rem))], rotate-180, dark:…) are never generated. theme.css carries the same line relative to itself, so with the import it's belt and braces; keep it if you don't import theme.css.

  • theme.css is the part of FlareCart's admin stylesheet the kit depends on:

    • the look: neutral greys in place of zinc's (--color-zinc-50 to -950), Geist as --font-sans and --font-mono (named, not shipped: load the faces yourself, from the geist package, or the system's fonts stand in), --radius-lg at 6px, and the accent --color-brand-50, -500, -600 (blue; the combobox's check mark and the focus ring use brand-500). Override any of them in your own @theme after the import.
    • the component classes the markup uses: .btn, .btn-sm, .btn-icon, .btn-primary, .btn-secondary, .btn-ghost, .btn-danger, .input, .switch, .label; and the admin's others for your own markup: .card, .badge (-zinc, -green, -amber, -red, -blue), .chip, .menu-item, .shell.
    • dialog::backdrop (dimmed, slightly blurred), a brand :focus-visible outline, and the flyout's slide-in (.flyout[open], off under prefers-reduced-motion).
  • Dark mode is Tailwind's dark: variant, which by default follows prefers-color-scheme. To switch it with a class instead, redefine it: @custom-variant dark (&:where(.dark, .dark *));.

  • Colours are Tailwind's zinc greys (remapped to neutral ones by the theme) with red, emerald, amber and sky for tones; no other custom values.

  • The page itself is yours. FlareCart's admin adds these, which the kit looks right on:

    @layer base {
      html { color-scheme: light dark; scrollbar-gutter: stable; }
      body { @apply bg-zinc-50 text-zinc-900 antialiased dark:bg-zinc-950 dark:text-zinc-100; }
    }

Usage

import { button, confirmDialog, el, pageHeader, toast } from '@flarecart/admin-ui';

const remove = button({
	label: 'Delete',
	icon: 'trash',
	variant: 'danger',
	onClick: async () => {
		if (!(await confirmDialog({ title: 'Delete Night Shift.zip?', message: 'It can’t be undone.', confirm: 'Delete', danger: true }))) return;
		await fetch('/api/files/42', { method: 'DELETE' });
		toast('Deleted', 'ok');
	},
});

document.body.append(el('main', 'shell py-8', pageHeader('Files', '12 files, 1.4 GB', remove)));

A sortable table

import { compareValues, el, nextSort, sortHeader } from '@flarecart/admin-ui';

let sort = { column: 'name', desc: false };
const descFirst = new Set(['size', 'created']); // newest and biggest first

function draw() {
	const rows = files.slice().sort((a, b) => compareValues(a[sort.column], b[sort.column], sort.desc));
	const onSort = (column) => ((sort = nextSort(sort, column, descFirst)), draw());
	table.replaceChildren(
		el('thead', '', el('tr', '', sortHeader({ label: 'Name', column: 'name', sort, onSort }), sortHeader({ label: 'Size', column: 'size', sort, onSort, end: true, className: 'text-right' }))),
		el('tbody', '', ...rows.map((f) => el('tr', '', el('td', '', f.name), el('td', 'text-right', f.size)))),
	);
}

A flyout that asks before losing changes

import { button, confirmDialog, flyout } from '@flarecart/admin-ui';

let dirty = false;
const { done } = flyout({
	title: 'Edit product',
	body: [form],
	beforeClose: () => !dirty || confirmDialog({ title: 'Discard changes?', message: 'Your edits will be lost.', confirm: 'Discard', danger: true }),
	actions: (close) => [button({ label: 'Save', variant: 'primary', onClick: () => close(true) })],
});
if (await done) save();

Another language

import { setLabels } from '@flarecart/admin-ui';

setLabels({ close: 'Fermer', cancel: 'Annuler', loading: 'Chargement…', search: 'Rechercher…', noMatches: 'Aucun résultat' });

Components read the labels each time they draw, so call it whenever your page knows its language, before or after the import. Everything else they show (titles, button labels, messages) you pass in.

Your own icons

import { addIcons, icon } from '@flarecart/admin-ui';

addIcons({
	star: 'M12 2l3 7h7l-5.5 4.5 2 7.5-6.5-4.5L5.5 21l2-7.5L2 9h7z',  // one path's d
	sun: ['M12 3v1', 'M12 20v1', 'M3 12h1', 'M20 12h1'],             // several
	dot: [['circle', { cx: 12, cy: 12, r: 3 }]],                      // any shapes
});

button({ icon: 'star', label: 'Feature' });
icon('star', 'size-5 text-amber-500');

Icons are drawn on a 24×24 grid with a 1.75 stroke in currentColor, like Lucide, so a Lucide icon's paths paste straight in.

API

Elements

| | | |---|---| | $(id) | document.getElementById, typed. | | el(tag, className = '', ...children) | An element; children are nodes or text, and null, undefined and false are skipped, so cond ? node : null works inline. |

Controls

| | | |---|---| | button({ label, icon, variant = 'secondary', size = '', title, onClick, className }) | variant: primary, secondary, ghost, danger; size: '', sm, icon. title is the tooltip and the accessible name; give one to an icon-only button. | | pageHeader(title, subtitle, ...actions) | A page's <h1>, the line under it (text or a node), and its buttons on the right. | | toggle({ checked, label }) | An on/off switch: a checkbox (.checked, change) with role="switch". | | tile(iconName, colours, size = 'md') | An icon in a rounded tile; colours are its classes, e.g. bg-sky-50 text-sky-600. sm, md, lg. | | emptyState(iconName, title, text, action?) | "Nothing here yet", with what to do and a button. | | loading(text?) | A spinner and a line; text defaults to the loading label. |

Sorting

| | | |---|---| | sortHeader({ label, column, sort, onSort, className, end }) | A <th> whose button calls onSort(column); shows an arrow and sets aria-sort. end puts the arrow first, for right-aligned columns. | | nextSort(sort, column, descFirst?) | The same column reversed, or a new one A to Z (biggest first if it's in descFirst). | | compareValues(a, b, desc) | Numbers by size, text naturally (file2 before file10) and case-blind; null, undefined and '' last either way. |

Feedback and dialogs

| | | |---|---| | toast(text, tone = '') | Bottom centre, above any open dialog. tone: '', ok (a check), bad (an alert, kept 8 s instead of 4). | | modal({ title, body, size = 'md', actions }) | { dialog, done, close }. actions(close) returns the footer's buttons; done resolves with what close() got (undefined for Escape, ✕ or the backdrop). Sizes sm to xl. | | flyout({ title, subtitle, body, size = 'lg', headerActions, actions, beforeClose }) | A panel from the right, over the page or another flyout. beforeClose (sync or async) can keep it open; close() from code always closes. Also returns setTitle(text) and setBody(nodes). | | confirmDialog({ title, message, confirm, danger }) | true when confirmed. message keeps its line breaks. | | promptDialog({ title, label, value, placeholder, confirm, suggestions, select }) | The text, trimmed, or null when cancelled. suggestions become a <datalist>; select: [start, end] picks what's selected first (say, a filename without its extension). |

Combobox

combobox({ choices, value, placeholder, empty, label, className, display, onChange }) returns { node, input, value }: put node in the page, read or set value. choices are { value, label, hint?, keywords? } (or a function returning them, called when first needed); typing filters on every word across label, value and keywords. empty: 'None' adds a choice whose value is null. display(choice) is how the chosen one reads when closed. Escape inside it closes the list, not the dialog it's in.

Menus

| | | |---|---| | contextMenu(x, y, items) | A menu at a viewport point (a pointer's clientX, clientY), kept on screen and inside an open dialog. Items: { label, icon, danger, disabled, hint, onClick } or '-' for a divider. Closes on Escape, Tab, a click elsewhere, scroll, resize or blur, and gives focus back. | | menu({ label, items }) | A "⋯" button and its menu; label names the button ("Price actions"). Items: { label, icon, danger, onClick }. |

Labels

setLabels(partial) changes some of the words and returns all of them; getLabels(), resetLabels(), DEFAULT_LABELS.

| Key | Default | Where | |---|---|---| | close | Close | The ✕ on a modal or flyout (its accessible name and tooltip). | | cancel | Cancel | confirmDialog() and promptDialog(). | | loading | Loading… | loading() with no text. | | search | Search… | combobox()'s placeholder. | | noMatches | No matches | combobox() when nothing matches. |

Icons

icon(name, className = 'size-4') (an unknown name draws file), icons (the set, by name), addIcons({ name: d | d[] | shapes }) (adds or replaces; returns the names).

The set: alert, archive, audio, ban, bucket, check, chevron-down, chevron-right, code, copy, document, download, external, file, flame, folder, folder-input, folder-plus, font, grip, image, key, laptop, link, loader, log-in, log-out, minus, more, package, pencil, phone, plus, refresh, search, shield, sliders, software, sort, tag, trash, upload, user, video, x.

Keys

splitKey('prism/v1.3/Prism.zip') → { folder: 'prism/v1.3/', name: 'Prism.zip' }.

Notes

  • Browsers only. The components need a DOM (<dialog>, popovers, :popover-open): every current browser. Importing the package in Node is fine; calling a component there isn't.
  • One toast stack per page, created on first use as #toasts.
  • No formatting. Dates, money, sizes and "3 minutes ago" are your language's business; FlareCart keeps its formatters with its translations.
  • Tests cover what doesn't need a DOM: sorting, labels, the icon set and addIcons, plus icon(), button() and loading() against a few lines of fake document.

License

MIT