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/components

v0.5.0

Published

Nightglass Svelte 5 primitives — the design language as components.

Readme

@nighthq/components

43 Svelte 5 primitives.

Quickstart

From nothing to a themed control:

pnpm create vite my-app --template svelte-ts
cd my-app
pnpm add @nighthq/components @nighthq/tokens

Add the one setting from Using it with Vite to vite.config.ts — dev will not start without it — then:

<script lang="ts">
  import '@nighthq/tokens/themes/all.css';
  import '@nighthq/tokens/styles/base.css';
  import { Badge, Button, type BadgeTone } from '@nighthq/components';

  let tone: BadgeTone = 'ok';
</script>

<Badge {tone}>ready</Badge>
<Button variant="primary">Start</Button>

The theme and the base stylesheet are imported once, at your app root. Import the prop vocabularies (BadgeTone here) rather than writing the strings by hand: they are the words these components accept, and a wrong one is otherwise silent.

tone is the name for severity everywhereBadge, Callout and toasts.push — and status is accepted on all of them as an alias, so code written either way keeps working. Badge shipped with status; the two names for one concept are what put 614 badges on their default across a deployed app, because Svelte drops the name a component does not have without a word.

This package publishes source. Svelte components have to be compiled by the consumer's Svelte version anyway, so a prebuilt bundle would add a version-skew failure mode without removing a build step from anyone.

Using it with Vite

vite build works untouched. vite dev does not, until this package is kept out of dependency pre-bundling. Either line does it, and the first needs no list:

// vite.config.ts — preferred: nothing to keep in step with us
export default defineConfig({
  plugins: [svelte({ prebundleSvelteLibraries: false })]
});
// or name the package, if you want prebundling left on for other Svelte libraries
export default defineConfig({
  plugins: [svelte()],
  optimizeDeps: { exclude: ['@nighthq/components'] }
});

@nighthq/components is the only entry that does anything, and the wider ['@nighthq/components', '@nighthq/icons', '@nighthq/model', '@nighthq/tokens'] list this section used to prescribe was three names too long. It is the only @nighthq package that ships Svelte source at all — 45 .svelte and .svelte.ts files, against none in icons, tokens or model, which ship compiled ESM with declarations and pre-bundle perfectly well.

The error will tempt you to exclude @nighthq/model. Don't bother. It is named on the failing line as the import target, not as the file that failed to parse — the file is always one of ours. Measured against the published packages: excluding only @nighthq/components survives an app that imports model directly, and dies the moment that one name is removed.

Without one of the two settings, dev dies before the first frame:

[plugin vite-plugin-svelte:optimize-module] …/@nighthq/components/src/selection/connect.svelte.ts:1:12
RolldownError: Unexpected token
https://svelte.dev/e/js_parse_error
 1 |  import type { Model } from '@nighthq/model';
                  ^

Vite's dependency optimizer hands .svelte.ts rune modules to the Svelte parser without running the TypeScript preprocessor first, so it stops at the first piece of TypeScript it meets. The import type above is the example, not the cause: all three of our rune modules fail, and two of them on an export type or export interface instead. Production builds never go through the optimizer, which is why this bites only in dev — the mode you spend the day in.

That is a direct consequence of publishing source, so it is part of this package's contract rather than a defect in yours. It is written down because the failure gives no route to the fix: it names a file inside node_modules, points at a line that is valid TypeScript, and the word optimizeDeps appears nowhere in it.

We cannot fix this from the package, and it is not for want of metadata. This package already declares the svelte export condition, which is exactly what vite-plugin-svelte reads to put a Svelte library on its automatic exclude list — measured: with prebundleSvelteLibraries: false the package needs no config at all, and deleting that one condition from its manifest brings the failure straight back. The list is then discarded before it is used, because prebundleSvelteLibraries defaults to true in dev. So the switch that would spare you this is on your side of the line, which is why this section asks you for a line of config instead of shipping one.

It is filed upstream, because the asymmetry looks like an oversight rather than a design choice. sveltejs/vite-plugin-svelte#1396. In setup-optimizer.js, compileSvelte preprocesses before compiling and its sibling compileSvelteModule hands the raw source straight to svelte.compileModule — a JS parser — so TypeScript in a dependency's .svelte.ts cannot survive prebundling. The two functions sit in one file and are called from the same hook; only one of them consults options.preprocess.

A fix there removes this section's requirement for every Svelte library shipping .svelte.ts, not just this one. Until it lands, the line of config above is still the answer — this package deliberately did not take the other road of shipping those three modules as compiled JavaScript, which would have bought you that one line at the price of a build step, load-bearing .d.ts emission, and a partial walk-back of publishing source (nightglass #365).

The error is quoted above in full, vite-plugin-svelte:optimize-module and js_parse_error included, so that searching either string — in this file, in llms.txt, or on the open web — reaches this section. Those two are the only parts of the message that are the same for everyone: the path names your node_modules, and the line and column move with our source.

Requirements

This package ships source, so your tsconfig compiles it — ours never runs at your site. check-consumer-types holds that floor on every run here, which makes it a promise rather than a description:

  • lib: ["ES2022", "DOM"], and no more. DOM.Iterable is deliberately not required: it is common but not universal, and avoiding it costs us one Array.from. A published library does not get to require a lib entry it could have avoided.
  • Svelte 5 — the components are runes-based.
  • A bundler that compiles Svelte and TypeScript out of node_modules, via the svelte export condition. @sveltejs/vite-plugin-svelte does this; see Using it with Vite for the one setting it still needs.

Every source-shipping package here is type-checked against exactly that lib set before release, so if it holds for us it holds for you.

What is here

43 interface components:

ActivityRegion Badge Button Callout Card Checkbox Chip Compare Confirm Empty Expandable Field FilePreview Icon Input List ListRow Mark Menu MenuItem Modal MonoId Pane Panel Pending Popover ScrollRegion Select Spinner Split Stat Table Tabs Terminal Textarea TextLink ThemeSwitcher Timeline TimelineEvent TimelineGroup Toaster Toggle Tooltip, plus the toasts store and the activity-region lease.

9 atmospheric components, behind the opt-in /cinematic subpath:

Constellation Cue Grain Halo Nightfield Readout Reticle Scene Vitrine

They are separately imported because they are a different tier: the interface answers in 180ms and the atmosphere breathes over five seconds, and a control carrying cinematic timing is a bug.

And connect, the bridge from a @nighthq/model model to Svelte reactivity. The adaptive components require you to own the model — Selection creates nothing, so that crossing a breakpoint cannot destroy the person's choice — which means you hold one and need to read it in your own components:

const view = connect(() => model); // a GETTER, never the model itself

Pass the value instead and you capture the instance that existed at setup: a swapped model then leaves the view subscribed to the old one, silently, because the stale model still answers every read.

Rules the components carry

These are enforced in code, not left to review:

  • Button shows accent motion only while working. primary is the action that starts work, not the prettiest one; a screen with two primary buttons has a hierarchy problem, not a styling problem.
  • One pulse per view region, enforced by a lease. Wrap a list or panel in ActivityRegion and the first live indicator inside animates while the rest hold still — not by convention, but because there is one lease and it is already taken. Motion repeated forty times has stopped being a signal and become texture. Outside a region the pulse prop still rules, so a single live badge needs no ceremony. A second claimant goes static rather than throwing: a design system that breaks a consumer's production build over a presentational rule has done more damage than the rule prevents.
  • Toggle and Checkbox use the selection tokens, not the accent. Checking a box is the person acting; the accent is the system acting.
  • Spinner is not accent-coloured. "This interface is waiting" is a different claim from "an agent is acting".
  • A confirmation names its action, and calls its way out Cancel. The control that performs a destructive action says what it does — "Drain node", "Revoke token" — never "Confirm", "OK" or "Yes", and the title asks it as a question using the same verb. The way out is the opposite on purpose: "Cancel", plainly, first in the row. Spend the specificity on the thing that changes something.
  • toasts has no error tone. An error belongs in a Callout that stays on the page; a toast is gone in three seconds and the person who looked away has lost it. Making the wrong thing hard to type outlasts documenting that it is wrong.
  • MonoId never truncates in the DOM — only visually. A shortened id that looks complete is worse than an obviously elided one.
  • Terminal's copy action strips the prompt.
  • A component that owns a bindable prop warns, in dev, about props it does not have. Svelte drops an unknown prop silently, which is harmless until the name you got wrong was the bindable one: the strip renders, the underline moves on click, and your bound variable never changes — the visible state and your model disagree with nothing saying so. It warns rather than throws, and says nothing in a production build. Components with a deliberately open prop set (Input, Select, which accept HTML attributes) are excluded, since for them an unrecognised prop is the feature.
  • One name per concept, with the other accepted as an alias. tone and status both reach Badge and Callout; active and selected both reach Tabs, and both are bindable — bind:selected writes back exactly as bind:active does. The canonical names are tone and active; the aliases exist so that nothing written against the older names breaks. Binding selected on a tab strip was the case that motivated this: it used to render, highlight the clicked tab, and leave the caller's variable untouched.
  • Reduced motion is guarded per component as well as in the shared keyframes, so a consumer who skips the base stylesheet still gets it.

Restyling one: --ng-<component>-* is the seam

Some components read a custom property you may set, with the shipped value as its fallback. That property is the supported surface. The class names are not.ng-chip, .ng-table-cell and the rest are implementation details we rename.

| property | on | what it sets | | ---------------------------- | -------------- | ----------------------------------------- | | --ng-chip-color | Chip | the label colour | | --ng-chip-border-color | Chip | the border colour | | --ng-expandable-surface | Expandable | the ground a nested expandable sits on | | --ng-table-sticky-bg | ScrollRegion | the ground behind a Table's sticky head | | --ng-nightfield-min-height | Nightfield | the band's height, when 100dvh is wrong |

Set one wherever it will inherit — a wrapper, a :root rule, or a class you pass through the class prop every component accepts:

<Chip class="chg-breaking">breaking</Chip>

<style>
  :global(.chg-breaking) {
    --ng-chip-color: var(--ng-bad);
  }
</style>

Why a property and not a documented class recipe. A class recipe would be a cascade fight, and one you would lose: Svelte scopes our rules, so what ships is .ng-chip-neutral.svelte-<hash> at (0,2,0). Your own class is (0,1,0) and loses outright; the two-class form you would reach for next, :global(.ng-chip.chg-breaking), only ties at (0,2,0) and is then decided by your bundler's import graph — which passes locally and changes under a dependency bump.

⇒ A custom property does not win that contest, it removes it. We never declare these properties, only read them, so yours is the only declaration and its specificity stops mattering: (0,0,1) serves as well as (0,2,0). That "never declared" property is pinned by a test, because it is the whole of why this works.

A tone is not a status. ChipTone stays neutral | emphasis deliberately — a chip labels a CATEGORY and a badge asserts a STATE, and a chip with status tones would be a second Badge. The property is there so your domain's categories (breaking, security, whatever they are) can look like themselves without becoming part of our vocabulary. That exclusion is our position and the asking consumer's, not a limitation anyone regrets.

The list above is the whole of it. A --ng-* name you find by reading our source is not automatically a seam: several are internal channels a component writes to itself, and setting those from outside does nothing useful. If you need one that is not here, ask — the gap that produced this section was a seam that existed in the source and in no place a consumer would look.

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.)