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

@pxlhut/brand-core

v0.1.1

Published

Generate a complete, gamut-mapped, APCA-validated design token tree from one brand colour. Pure, browser-safe, framework-agnostic.

Readme

@pxlhut/brand-core

One brand colour in, a complete OKLCH/APCA-validated design token tree out. Pure, browser-safe, framework-agnostic (see DECISIONS.md D10).

Part of Brandrail — multi-tenant brand theming with a per-field control-tier model the platform vendor owns. See the main README for the full pitch and the other three packages.

npm install @pxlhut/brand-core
import { generateTheme, toShadcnCss } from "@pxlhut/brand-core";

const { tokens, buttonStyle } = generateTheme({ brandColor: "#7c6cff" });
const css = toShadcnCss(tokens, { buttonStyle });

See the quickstart for adding persistence and a settings UI on top of this. The rest of this file is the serializer reference below.

~7.5 KB minified and gzipped (core only — culori and apca-w3 are peer/external, not bundled), budgeted at 15 KB and enforced in CI (pnpm size, scripts/check-bundle-size.mjs). Every claim in this README — "APCA-validated", "pure and deterministic", "runs in a browser", this size figure — has a proof behind it in proofs/, not just a unit test on a handful of hand-picked inputs; see plan/09-core-proofs.md.

Output serializers

generateTheme() produces a TokenTree. Three functions turn it into CSS, all reading the same published tree — there is no separate generation step per output target (guideline §37).

toShadcnCss

Emits shadcn/ui's exact current custom-property set (--background, --card, --sidebar-ring, … — pinned in features/output/roles.ts against the live docs), plus three roles shadcn does not define at all: --success/--success-foreground, --warning/--warning-foreground and --info/--info-foreground. Guideline §34 makes these first-class in this package's own token model; they're emitted alongside the pinned set under their own names rather than dropped or folded into an existing shadcn role.

Both a light and a dark block are always emitted in one string (D6, §25). darkMode picks how the dark block is scoped:

  • 'attribute' (default) — :root[data-theme="dark"]
  • 'class' — :root.dark
  • 'media' — @media (prefers-color-scheme: dark), no manual toggle

selector (default ':root') lets a caller scope the whole thing — the control panel's live preview needs [data-site-theme] instead (guideline §5), so it doesn't leak onto the dashboard's own chrome.

fontStrategy closes guideline §35's loop (self-host a curated set, but a font that isn't actually loaded fails silently to a system fallback):

  • 'none' (default) — just the --font-heading/--font-body values.
  • 'fontsource' — adds a comment naming the exact @fontsource/* package(s) to install. Not a real @import: fontsource ships static files meant to be pulled in by the consumer's own bundler, not fetched from a URL this package would have to invent.
  • 'inline-face' — adds a real @font-face per curated family, with a local() source. This tells the browser to prefer a copy already installed on the visitor's system; it does not embed font binaries. Actually bundling and serving font files needs a font-asset pipeline this package doesn't have and isn't in scope for it (D10 — two runtime dependencies, no binary assets).

toTailwindTheme

Tailwind v4 (version: 4, the default). The same raw :root/dark value blocks as toCssVars, plus an @theme inline { --color-<role>: var(--<role>) } bridge so Tailwind's own utilities (bg-primary, text-sidebar-foreground, …) resolve against whichever block is active.

Tailwind v3 (version: 3), legacy. A tailwind.config.js fragment mapping every role to a bare var(--role) reference — flat keys ('sidebar-primary-foreground') rather than shadcn's nested { DEFAULT, foreground } groups; both produce the same utility class names. This output does not depend on the tree's actual values, deliberately: a Tailwind v3 config is a build-time file, so the only way one static config can serve every tenant is for it to defer to whichever CSS custom properties are inlined at request time, never to bake a value in.

toCssVars

The escape hatch for anyone not on shadcn or Tailwind: raw custom properties, an optional prefix, no assumption about naming beyond that.

Escaping and hashing

Every value passes through the validator's HTML-escaping (escapeForHtml, < and &) on the way out — including values the generator produced itself and that never touched user input. Each serializer's return value is exactly the byte string that should be hashed for the CSP (DECISIONS.md D7) and inlined into the page; there is no separate re-serialization step.