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

@nighthq/tokens

v0.3.1

Published

The Nightglass colour system: a whole dark palette derived from one accent colour, with a verified contrast floor.

Readme

@nighthq/tokens

The colour system, plus the base stylesheets that depend only on it.

import { defineTheme, themeToCss, validateTheme } from '@nighthq/tokens';

const theme = defineTheme({ name: 'brand', accent: '#2f9e7a' });
validateTheme(theme); // []
themeToCss(theme); // ':root { --ng-...: ...; }'

The model

One accent colour supplies a hue. From it:

  • the ground ramp — five dark stops at that hue, lightness 0.159 to 0.266, chroma rising with lightness;
  • the light ramp — three stops at hue + moonShift (default -33.2 degrees);
  • the accent — the ground hue at 3x the ground's chroma, and a lighter companion;
  • the semanticsok/bad/warn, whose hues you may supply and whose lightness and chroma come from the measured band.

Every constant lives in src/ramp.ts with its provenance, and test/fidelity.test.ts fails if one is "tidied".

OKLCH in, OKLCH out

Tokens emit oklch(). Derived tints are color-mix(in oklab, ...). The repo's pnpm check:colors fails on hex, rgb(), hsl(), or a named colour in any stylesheet or component — a literal cannot follow a theme and never shows up in a contrast report.

Emitted values are gamut-mapped before formatting. An out-of-gamut oklch() is resolved by each browser's own gamut mapping, so the rendered colour — and every contrast measurement made against it — would vary by engine. Mapped first, the value is inside sRGB and every engine agrees.

formatHex still exists, for the places that cannot take a colour function: a favicon or OG-image generator, a native app tint, <meta name="theme-color">, a terminal escape.

Theme inputs accept a hex string, an oklch() string, or a bare hue in degrees — all three are read for hue alone, so all three give identical output.

Why OKLCH

A hue rotation at fixed HSL lightness swings perceived brightness enormously — HSL yellow and HSL blue at 50% are nowhere near equally bright. Building the ramp in OKLCH is what lets the system freeze lightness and rotate hue while keeping a measured contrast floor. Chroma is the axis that gives way when sRGB cannot hold a value; src/color/gamut.ts bisects it down to the boundary rather than letting channels clip, which would move the colour unpredictably and per-channel.

Token layers

Layer 1 — the generated ramp. Positional names: --ng-ramp-ground, --ng-ramp-0 to --ng-ramp-3, --ng-ramp-text, --ng-ramp-glint, --ng-ramp-accent, and the semantics. Positional because a generated ramp cannot carry names like --silver: rotate the hue and the name is a lie. Do not reference these from components.

Layer 2 — semantic aliases. What you build against:

| token | role | | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | --ng-surface-terminal -0 -1 -2 | the ground and what sits on it | | --ng-text-primary --ng-text-secondary | the two voices | | --ng-text-hidden | camouflage — deliberately unreadable, ~1.2:1, for secrets awaiting a reveal. Exempt from the contrast floor because failing it is the point | | --ng-link --ng-focus | the glint | | --ng-accent --ng-accent-soft | activity, never decoration | | --ng-ok --ng-bad --ng-warn (+ -soft) | signals | | --ng-hairline --ng-hairline-strong | borders | | --ng-glow-accent --ng-glow-link | elevation, which is light | | --ng-focus-line -ring -glow, --ng-select-* | focus and selection | | --ng-{accent,ok,bad,warn}-line -wash | tinted borders and fills |

Derived tints are color-mix() expressions over layer 1, so they track the theme instead of being frozen rgba() literals.

Non-colour tokens (--ng-s-*, --ng-t-*, --ng-ease-*, --ng-r-*, --ng-measure-*, --ng-tap-*, --ng-font-*, --ng-text-*, --ng-track-*) are not themable, apart from the font stacks. Colour is the axis a product wants to move; the two-tier motion split and the 2px/3px corners are the design language itself. --ng-measure-* caps line length and page width — a position rather than a default, and the next section says what it commits you to. --ng-tap-min is a WCAG 2.5.8 floor rather than a house preference, so lowering it is a conformance change and not a matter of taste.

That list is derived-checked: check-docs compares it against staticTokens() in both directions, so a family added to the code cannot quietly go unlisted here. It is written down because it did drift — --ng-measure-* and --ng-tap-* were both missing, and a consumer building a two-pane console had to ask which of the two readings was intended (#341).

Width, and what goes on your outermost element

Three measures, and a utility for each. They ship in base.css (via utilities.css), so importing base.css is all it takes to use one:

| token | utility | caps at | for | | -------------------- | ------------- | --------------- | ---------------------------------------- | | --ng-measure | .ng-measure | 36em | a column of prose, at a readable measure | | --ng-measure-panel | .ng-panel | 56rem | a form, a dialog, a settings pane | | --ng-measure-shell | .ng-shell | 78rem ≈1248px | the page itself — also centres it |

.ng-shell is the answer to "what do I put on my outermost element". Reach for it rather than writing max-width: var(--ng-measure-shell); margin-inline: auto, which is what it is.

The house position is cap and centre — including on wide screens. At 2560px that leaves real dead space, and that is the trade being made: a capped, centred page over a full-bleed one. Nightglass does not ship a full-bleed application shell, and a consumer that wants one is departing from the house layout rather than using an unsupported feature — do it knowingly and own the line lengths.

What would change this position: a consumer demonstrating a measured cost — content that cannot fit 78rem without horizontal scroll or truncation. Measured, not predicted: the first report to raise this (#341) predicted exactly that cost for a terminal pane, went looking, and found scrollWidth 796 against clientWidth 796 at 1280 and 2560. The content fit. A screen that looks empty is not yet evidence; a pane that cannot fit is.

The classes base.css ships

Importing base.css brings these with it. They exist so a product does not grow five private copies of the same four declarations — which it will, and did: two consumers and one of this repository's own apps each hand-wrote .ng-shell before this list existed (#341, #289).

Membership is gate-held. check-docs derives the class list from the stylesheets themselves, so a class shipped and never written down here fails the build. It is a derived list because the hand-maintained version was wrong by 41 of 45.

Layout

| class | what it does | | ---------------- | ---------------------------------------------------------------------- | | .ng-stack | vertical grid, --ng-s-4 gap, packed to the start | | .ng-stack-lg | the same at --ng-s-5, for sections rather than fields | | .ng-row | horizontal flex, centred, wrapping, --ng-s-3 gap | | .ng-spread | a row with space-between — a title against its actions | | .ng-grid-2 | responsive columns, each at least min(100%, 22rem) | | .ng-grid-stats | a denser auto-fit grid at 11rem, for Stat tiles | | .ng-measure | caps at --ng-measure (36em) — a column of prose | | .ng-panel | caps at --ng-measure-panel (56rem) — a form, a dialog | | .ng-shell | caps at --ng-measure-shell (78rem) and centres — the page itself |

⚠ The four grid helpers also set min-width: 0 on their direct children, and that is load-bearing rather than tidy: a grid or flex child defaults to min-width: auto, so one wide child pushes the whole container past the viewport instead of scrolling. The symptom is page-wide horizontal overflow that no single rule explains. Nest your own grid inside one of these and you inherit the fix; roll your own and you will meet the bug.

Text

| class | what it does | | ----------------- | -------------------------------------------------------------- | | .ng-display-xl | the largest display voice, viewport-driven | | .ng-display | display voice for section openers | | .ng-h1 .ng-h2 | the two heading sizes, as classes for non-heading elements | | .ng-label | the tracked uppercase voice — captions, column heads | | .ng-verbatim | cancels the uppercasing, for a label holding a literal (an id) | | .ng-lede | opening paragraph: primary text, capped at the reading measure | | .ng-note | secondary text at --ng-text-small, same cap | | .ng-dim | secondary colour, nothing else | | .ng-mono | the mono stack | | .ng-num | mono, right-aligned, tabular-nums — figures that line up | | .ng-trunc | one line, ellipsised | | .ng-sr-only | visually hidden, still announced |

Elements

These style the element they name, so a plain <input class="ng-input"> looks like the system without pulling in Svelte:

  • Forms.ng-field .ng-input .ng-select .ng-select-wrap .ng-textarea .ng-hint .ng-toggle .ng-toggle-row .ng-checkbox .ng-checkbox-row
  • Tables.ng-table, and .ng-table-wrap for the scroll container a wide table needs
  • Code.ng-code .ng-terminal .ng-terminal-bar .ng-terminal-body .ng-prompt .ng-out, and the token classes a highlighter emits: .ng-tok-key .ng-tok-str .ng-tok-num .ng-tok-flag .ng-tok-com

Inline <code> inside prose is styled without a class. .ng-terminal and .ng-code opt out of that, since a block already has its own treatment.

Delivery

| mode | use | | ----------------------------------- | ----------------------------------------------------------- | | @nighthq/tokens/themes/<name>.css | prebuilt, bound to :root. Zero runtime | | @nighthq/tokens/themes/all.css | all five; nightfall on :root, each on [data-ng-theme] | | themesToCss() / themeToCss() | build-time generation for your own brand | | applyTheme() | runtime, for live switching. Returns an undo function |

@nighthq/tokens/styles/base.css is the element foundation — reset, typography, forms, tables, code, animations, utilities. It depends only on the token layer, so it works on any surface that can link a stylesheet.

The cascade layer, and how you beat it

This is published contract. Every prebuilt theme stylesheet opens with the same two lines, and they mean what they say:

@layer nightglass.theme, nightglass.overrides;

@layer nightglass.theme {
  /* the shipped themes */
}

Unlayered styles beat layered ones, by the cascade's own rule. So your theme wins over ours no matter where your bundler puts it — you never have to out-specify themes/all.css, and you never have to care whether your import landed before or after ours. themesToCss() emits unlayered CSS by default for exactly that reason: what you generate outranks what we ship, without your having to know this section exists.

Three postures work, and they were measured in Chromium rather than reasoned about, with the theme attribute on <html> where ThemeSwitcher puts it:

| your override | before the layer | now | | ------------------------------------ | ---------------- | ---- | | :root, imported after us | wins | wins | | :root, imported before us | lost | wins | | :root, [data-ng-theme], either way | order-dependent | wins | | inside @layer nightglass.overrides | lost | wins |

nightglass.overrides is declared and deliberately left empty. It is a name, ordered after ours, for consumers who cannot write unlayered CSS at all — a framework that layers its whole output leaves you nothing outside a layer to write in.

Two limits, stated because they are real:

  • A :root override does not reach into a [data-ng-theme] subtree — the swatches in ThemeSwitcher are one. That is inheritance proximity, not the cascade: the nearer ancestor carries the value and the two rules never meet. No layer can change it, and none should — a subtree asking for harbour is asking for harbour. Write :root, [data-ng-theme] to reach both.
  • styles/*.css is deliberately NOT layered. forced-colors.css redeclares 43 tokens the themes also emit and must win them; unlayered, it now does so unconditionally instead of only when imported second.

A bundler may rewrite the order statement — the showcase's minified CSS turns six copies of it into one @layer nightglass.theme{…} block followed by a bare @layer nightglass.overrides;. That is different text with the same meaning, since layer order is fixed by first mention. The table above was re-measured against that minified bundle and every row matched.

check-generated pins the emitted bytes — it regenerates and compares — so the layer cannot quietly vanish from the committed stylesheets. test/layering.test.ts pins the rule behind those bytes, including the half no byte comparison can see: that the base stylesheets stay outside the layer.

Validation

validateTheme returns two kinds of finding:

  • contrast (error) — every foreground against every surface, WCAG AA;
  • hue-collision (warning) — an accent within 25 degrees of a semantic hue, which no contrast check can catch and which makes "working" and "failed" indistinguishable by the channel that is supposed to separate them.

scripts/build-css.mjs fails closed: a theme that does not clear the floor gets no stylesheet. Shipping an inaccessible palette because a warning scrolled past in a build log is the failure this prevents.

WCAG 2.x is the criterion because it is what audits cite. It is a known-imperfect model of dark-mode legibility — it tends to overstate light-on-dark contrast — so the measured floor sits above the minimum rather than on it.

Built-in themes

Accents below are the resolved values, which is what the theme actually emits. They differ from the literals in src/themes/index.ts wherever the input's lightness or chroma was normalised to the ramp — verdigris is declared #3aa88f and resolves to #00ac8e. Only the hue survives, by design.

| name | accent | note | | ----------- | --------- | -------------------------------------------- | | nightfall | #9b7fe0 | the reference; reproduces the source palette | | harbour | #6690ea | deep blue | | verdigris | #00ac8e | oxidised brass; moves ok off the accent | | lantern | #c98105 | sodium amber; moves warn off the accent | | rosewood | #d76a90 | rose; moves bad off the accent |

The family

Five packages, published together from one repository:

| package | what it is | | -------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | @nighthq/tokens | the colour system, and the base stylesheets that depend only on it | | @nighthq/components | the Svelte 5 interface primitives, plus an opt-in atmospheric tier | | @nighthq/icons | line-work icons and the brand mark, as path data — no framework | | @nighthq/model | the framework-neutral view kernel: one semantic model, two presentations | | @nighthq/host | the effect boundary — history, titles, announcements, focus |

Start with tokens + components. Add icons when you need glyphs, and model + host when one screen has to render as both a desktop and a compact presentation.

The repository is private, so homepage and repository links on these npm pages will not resolve for you — these READMEs and the tarballs are the whole documented surface, which is why they carry more than a link would.

(No counts in this table on purpose: a number repeated across five files is five places for it to go stale, and only each package's own README has a check-docs claim behind its figures.)