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

@galaxy-foundry/site-kit

v0.10.0

Published

Reusable reading surfaces for Foundry-pattern instances — shell, note frame, tags, references, and licences. Values, domain furniture, theme, and corpus stay per-instance.

Readme

@galaxy-foundry/site-kit

The reading shell for Foundry-pattern instances: document skeleton, header with derived navigation, and footer. The kit ships the markup, structural component styles, and client behavior; the instance supplies the identity values, global theme contract, and corpus.

pnpm add @galaxy-foundry/site-kit

Two lines of configuration, both of which fail silently

This is the part to get right first, because neither mistake produces an error.

1. Point Tailwind at the kit. Tailwind 4's automatic source detection does not look inside node_modules, so without this every utility the shell writes is missing from your stylesheet. The build stays green and the site renders unstyled.

/* your global.css, after @import "tailwindcss" */
@source "../../node_modules/@galaxy-foundry/site-kit/src";

A typo here is exactly as silent as omitting the line — site-kitt builds as cleanly as site-kit. You cannot verify this line by reading it. Assert instead that a class only the kit writes reaches your emitted CSS; min-h-dvh is a good canary, because the kit's <body> is the only place it appears.

2. Define the tokens. The kit brings no global theme stylesheet. It names six custom properties and wears three instance-owned classes, and you supply all nine — SHELL_TOKENS and SHELL_CLASSES are exported so the list is a value you can check rather than a paragraph you can read carefully. Component-scoped rules still own the overflow menu, header grid, and Pagefind layout; those are package behavior rather than instance theme values.

@theme {
  --color-chrome: #2c3143; /* the dark bar: header, More menu, footer */
  --color-accent: #e8c547;
  --color-surface: #ffffff;
  --color-text-primary: #2c3143;
  --color-text-on-dark: #f8f9fa;
  --font-sans: 'Atkinson Hyperlegible', system-ui, sans-serif;
}

Plus .skip-link, .bg-grid and .nav-link-active, which are ordinary classes rather than utilities — Tailwind never has an opinion about them, so a missing one is markup wearing a class no rule matches.

Miss any of the nine and that piece of the shell renders unstyled, silently. Assert on a built stylesheet:

import { shellStyleGaps } from '@galaxy-foundry/site-kit';

expect(shellStyleGaps(everyEmittedStylesheet)).toEqual([]);

Use the helper rather than writing the loop. A token counts only when its declaration is present, so the search needs the colon: --color-chrome on its own also matches the shell's own var(--color-chrome), and the check passes on exactly the sites it exists to fail.

The names are ROLES, not brands. --color-chrome is the dark bar; what your instance calls that colour is its own business, and one line maps it: --color-chrome: var(--color-my-brand-dark);

Use

One composition point. Every page keeps importing your own layout, which passes the identity through:

---
// src/layouts/Base.astro
import '../styles/global.css';          // FIRST — import order decides where the <link> lands
import SiteShell from '@galaxy-foundry/site-kit/SiteShell.astro';
import { SITE_IDENTITY } from '../lib/site-identity';

interface Props { title: string; description?: string }
const { title, description } = Astro.props;
---
<SiteShell
  title={title}
  description={description}
  base={import.meta.env.BASE_URL}
  pathname={Astro.url.pathname}
  identity={SITE_IDENTITY}
>
  <slot />
</SiteShell>

base and pathname are passed IN rather than read by the kit, so the kit touches no environment. That is not tidiness: under vitest, import.meta.env.BASE_URL is mirrored into process.env as /, and a child astro build prefers it over your astro.config.mjs. A test suite that spawns a build can otherwise spend months asserting against a site deployed at the wrong base.

Your site-identity.ts is the whole of what makes the site itself:

import type { SiteIdentity } from '@galaxy-foundry/site-kit';

export const SITE_IDENTITY: SiteIdentity = {
  name: 'Foundry',                    // wordmark and <title> suffix
  fullName: 'Galaxy Workflow Foundry', // footer
  description: '…',
  repoUrl: 'https://github.com/…',
  navLinks: [{ path: '/story/', label: 'Story' }, …],
  navVisible: 5,                       // the rest go under "More"
  footerLinks: [],
};

What is a value and what is not

navVisible is a count set by what FITS on the bar, not a claim about which sections matter — and what fits differs between instances because the wordmark does. Measure it against a built page.

The reading column's width is deliberately not a prop. The two instances this shell came from disagreed about it once, and the disagreement was never decided: one shell was copied from the other and the width changed in the same edit as the name. They converged before the shell moved, so the kit holds the measure and takes no prop for it. A page wanting a narrower measure narrows its own content. See CONTAINER.

Parameterizing a difference is how an accident becomes a policy.

The note frame and tag chips

NoteHeader.astro owns everything above a note's body: back navigation, an eyebrow naming the kind, the heading, the summary, and a row carrying the status, the badges slot and the tags — plus raw/copy actions where the instance publishes a source. ContentNote.astro composes it and adds what a page needs around a header: the metadata and reference slots and the article boundary. TagChips.astro owns tag markup. An instance passes tagBase only when it has a browse route; without one the same controlled metadata renders without a false link.

---
import ContentNote from '@galaxy-foundry/site-kit/ContentNote.astro';
---
<ContentNote
  title={entry.data.title}
  eyebrow={TYPE_LABELS[entry.data.type]}
  summary={entry.data.summary}
  status={entry.data.status}
  tags={entry.data.tags}
  tagBase={hasTagPages ? `${base}/tags` : undefined}
  back={{ href: `${base}/packages/`, label: 'Packages' }}
  showHeading={false}
  articleClass="prose"
>
  <PackageFacts slot="metadata" entry={entry} />
  <Content />
</ContentNote>

eyebrow is a resolved string, never a kind the frame looks up: the label table belongs where the kinds are declared and typed against them, so adding a kind is a compile error there rather than an eyebrow printing a raw type string here. status renders as data-status for the reason the licence badge uses data-policy — the vocabulary and its colours stay with the instance, and a value it has not styled still reads. showHeading defaults to true; a Markdown body that opens with its own <h1> passes false, and that heading is then what search reads as the title.

Import NoteHeader.astro directly for a header outside a note page. The slots are the boundary: package facts, evidence contracts, source metadata, and domain labels stay with the instance. Neither component inspects frontmatter or knows any domain kind. They name the roles in CONTENT_READER_TOKENS and NOTE_HEADER_TOKENS; consumers can check them with contentReaderStyleGaps(css) and noteHeaderStyleGaps(css).

See the content-reader boundary for how this presentation layer composes with @galaxy-foundry/content-reader and explicit Astro collections.

The kind catalog and reference page

KindCatalog.astro renders one Foundry's compact inventory; KindReference.astro renders the complete generated contract for one kind. Both consume ManifestKind through a resolved KindCatalogItem: the generated data stays authoritative while the instance supplies the hrefs and live-corpus facts only it can know.

---
import KindCatalog from '@galaxy-foundry/site-kit/KindCatalog.astro';
import manifest from '../types/kinds.generated.json';

const items = manifest.kinds.map((definition) => ({
  definition,
  href: `${base}/kinds/${definition.kind}/`,
  // Optional: derive this from the content reader, never put the count in the manifest.
  browse: browseFor(definition.kind),
}));
---
<KindCatalog {items} />

The detail route passes one item to KindReference. Its documentation slot is deliberate: kind.md may contain an instance's wiki-link grammar and must go through that instance's Markdown pipeline. The shared component does not parse it. example.md is displayed as escaped source, because a schema fixture can contain synthetic references and must not claim live corpus routes.

<KindReference item={item} catalogHref={`${base}/kinds/`}>
  <KindDocumentation slot="documentation" />
</KindReference>

The two components read KIND_CATALOG_TOKENS; assert them against the emitted stylesheet with kindCatalogStyleGaps(css). An instance may set --color-kind-accent through [data-kind], with --color-brand as the built-in fallback.

The reference card

ReferenceContract.astro renders a note's typed references: manifest against the contract it was authored under. Unlike the shell it brings its own stylesheet, so there are no classes to define — only the tokens its rules read, listed as REFERENCE_TOKENS and checked with referenceStyleGaps.

---
import ReferenceContract from '@galaxy-foundry/site-kit/ReferenceContract.astro';
import { referenceContract } from '../lib/registries';
---
<ReferenceContract
  references={entry.data.references ?? []}
  contract={referenceContract}
  resolveRef={(ref) => resolveWikiLink(ref, linkMap, base)}
/>

resolveRef is a function rather than a link map because how a ref becomes an href is the instance's question — one spells wiki links, another paths — and returning null leaves the ref on the page as written.

Which reference kinds exist is the one part of the contract an instance declares for itself, so the card ships no per-kind colour. Each card carries data-kind, and an instance tints its own:

[data-kind='mold'] {
  --color-kind-accent: var(--color-accent);
}

A kind nothing tints gets --color-brand. That is a real answer, not a missing one — which is why --color-kind-accent is a fallback and not in REFERENCE_TOKENS.

Evidence chips are styled from the standing each term declares (@galaxy-foundry/reference-contract ships standing: provisional | grounded), not from a list of term names in a selector. A term added to the vocabulary gets a colour without a component release.

The licence badge

LicenseBadge.astro renders what a licence permits, from the id a note declares and the table in @galaxy-foundry/license-policy: the licence name, its redistribution policy, and a copyleft chip where the row calls for one.

---
import LicenseBadge from '@galaxy-foundry/site-kit/LicenseBadge.astro';
import { licensePolicy } from '../lib/registries';
---
<LicenseBadge license={entry.data.license} policy={licensePolicy} />

The table is passed in rather than bundled. An instance validates its corpus against one specific version of the policy, and a component reaching for its own copy could disagree with the schema that admitted the note.

What the badge is not is the box around it. It reads license and the row, and nothing else — no license_file, no attribution line, no link. That boundary is the policy table's own, stated in its header comment: what a licence permits is shared, whether a note is right about its licence is instance-local. The credit line a particular source needs, and whether a vendored copy travels with it, are the second question and stay per-instance.

The chip label is the row's name, not the SPDX id, which stays reachable as the chip's title. name equals the id in 1 of 23 rows — rendering the id is fine for MIT, survives Apache-2.0, and turns LicenseRef-arXiv-nonexclusive-distrib-1.0 into a pill that teaches a reader nothing.

The policy chip is keyed on data-policy, the row's own value, so a row added upstream is styled without a component release. Its three colours are LICENSE_BADGE_TOKENS, checked with licenseBadgeStyleGaps — they were raw hexes in both instances that grew this component, the same three to the byte, which is a duplication nothing could have caught and nothing could change.

:root {
  --color-license-verbatim: #16a34a;
  --color-license-own-words: #d97706;
  --color-license-copyleft: #dc2626;
}

Named for the policy rather than the palette: --color-license-own-words is whatever this site uses to mean "this text may not be redistributed".

The vendored-licence route

A site that redistributes third-party text carries verbatim copies of the upstream licences, and renders them in-app so a note can link terms without bouncing the reader to GitHub. Both instances built that route, and both spelled its path inline — once in the page that builds it and again in every component that links to it.

---
import LicenseFileBody from '@galaxy-foundry/site-kit/LicenseFileBody.astro';
import { licenseFileHref, type LicenseFileUse } from '@galaxy-foundry/site-kit';
import { redistributesUnder } from '@galaxy-foundry/license-policy';

const uses: LicenseFileUse[] = notes
  .filter((note) => redistributesUnder(note.data.license_file, licenseFile.id))
  .map((note) => ({ href: `${base}/${note.id}/`, label: note.id, licenseId: note.data.license }));
---
<h1>{licenseFile.filename}</h1>
<LicenseFileBody licenseFile={licenseFile} policy={licensePolicy} uses={uses} />

licenseFileHref(base, licenseFile) builds the link, and takes either a LicenseFileId or the license_file path a note declares — the two call sites hold different ones. It is built from LICENSE_FILE_ROUTE, which the page that generates the route uses as well, so the two cannot drift into a clean build that 404s.

Finding the notes stays yours. One instance walks a single collection and links /{id}/; the other walks three and links /{collection}/{id}/. That is the one genuinely per-instance step, so uses is a prop. Filter with redistributesUnder rather than comparing ids by hand: a vendored copy is keyed by source, so two books under one licence have two copies, and the comparison is between file ids even though it reads like a licence check.

The page's <h1> and wrapper stay yours too, and that one is not taste. One site marks this route with data-pagefind-body; Pagefind reads the first such mark as "index only pages like this one", so a component shipping the wrapper would decide a whole site's search index from inside a licence page. See "The search index" below.

The body's own sections each carry an <h2>, the licence text included — so supply exactly one <h1> and nothing between it and the component. A page whose subject is a licence cannot leave that licence as the one part with no heading: a reader moving by heading lands on the list of notes and the terms are somewhere past the end of it.

licensesUnderFile(policy, uses) is exported separately for a page that wants the licences a copy covers without the body — deduped and sorted, rather than ordered by whichever note was read first.

Tokens: LICENSE_FILE_TOKENS, checked with licenseFileStyleGaps.

The search index

The header renders a Pagefind search box. What goes IN the index is the other half, and its rule runs backwards from what the annotation looks like:

  • Mark no page with data-pagefind-body → every page is indexed, from its <body>.
  • Mark one page → every unmarked page leaves the index entirely.

So adding the attribute to a single route is strictly worse for the rest of the site than never adding it. Measured on a real instance: one annotated route, and the index held 242 of 374 pages — missing every artifact page, every tag page, the glossary, the dashboard, and 48 generated skill pages. The build log printed Pagefind indexed 374 pages in both states, because it counts pages processed rather than pages indexed. Nothing warns, nothing looks wrong, and the only symptom is a search that answers "no results" for words plainly on the site.

SiteShell therefore puts the attribute on <main> by default. Opt out per page:

<SiteShell title="Tags" base={base} pathname={pathname} identity={SITE_IDENTITY} searchable={false}>

Marking <main> rather than falling back to <body> also keeps the header, nav and footer out of every result's excerpt.

Then assert the whole built site, listing the routes that opted out:

const pages = builtPages(); // [{ path, html }, …] from dist
expect(searchIndexGaps(pages, UNSEARCHABLE)).toEqual([]);

The list is what makes an absence a decision. Without one, "deliberately out of the index" and "nobody thought about this route" are the same observation — which is how 132 of them accumulated.

Specimens, and the gallery each instance builds from them

The kit ships the cases its components are meant to handle, as props — every component in the package, which a test measures against the directory rather than against a list here:

import { REFERENCE_SPECIMENS } from '@galaxy-foundry/site-kit/specimens';

Each entry is { id, name, why, props }, grouped per component with its own id, a summary and a surface. Rendering a group is a page of your own, in your own theme:

---
import ReferenceContract from '@galaxy-foundry/site-kit/ReferenceContract.astro';
import { REFERENCE_SPECIMENS, specimenPath } from '@galaxy-foundry/site-kit/specimens';
---
{REFERENCE_SPECIMENS.specimens.map((specimen) => (
  <section id={specimenPath(REFERENCE_SPECIMENS, specimen)}>
    <h2>{specimen.name}</h2>
    <p>{specimen.why}</p>
    <ReferenceContract {...specimen.props} />
  </section>
))}

The theme is the specialization. Nothing here carries a colour, and that is the point: the same cases rendered under two instances' tokens are two galleries, and what differs between them is exactly what each instance owns. An instance adds groups of its own for its own components in the same shape — and a group's id, not its component, is its address, because an instance's extra group for a kit component is the ordinary case: the parent Foundry adds a second ReferenceContract group for its own seven kinds.

surface is not decoration. A group is inline (many to a page), isolated (one to a page or one per frame — valid inline markup carrying document-unique ids, which the header does), or document (its own <html>, so it needs a route and an <iframe>). Getting it wrong does not fail: two headers on one page is two #nav-more-trigger, the second one's menu binds nothing, and the specimen that exists to prove the overflow menu opens is the one that appears not to. Use sharesPage(group) rather than reading the field.

why is what makes a specimen more than a screenshot. no-references renders nothing — a correct result indistinguishable from a broken gallery until something says which it is.

The reference specimens carry their own SPECIMEN_CONTRACT rather than taking yours. Half of them are about terms your contract does not contain — a kind with no destination, a value outside the vocabulary — and fed a real contract they would collapse into the same happy-path card. Say whose vocabulary is on screen; then add your own kinds' specimens beside them.

The repository's executable component gallery renders the complete SPECIMENS array under both a contract-minimum stylesheet and an opinionated one. Treat that app as the current integration example; an instance owns its gallery route and presentation because those are part of its theme, while the package owns the cases and their surface constraints.

Exports

| Import | What | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | @galaxy-foundry/site-kit | SiteIdentity, ShellLink, ResolvedShellLink, ResolvedNav, resolveNav, shellBase, shellHref, CONTAINER, SHELL_TOKENS, SHELL_CLASSES, shellStyleGaps, styleGaps, CONTENT_READER_TOKENS, contentReaderStyleGaps, REFERENCE_TOKENS, referenceStyleGaps, LICENSE_BADGE_TOKENS, licenseBadgeStyleGaps, LICENSE_FILE_ROUTE, licenseFileHref, LicenseFileUse, licensesUnderFile, LICENSE_FILE_TOKENS, licenseFileStyleGaps, ResolvedReference, PAGEFIND_BODY_ATTR, searchIndexGaps, SiteShellProps, SiteHeaderProps, SiteFooterProps, ContentNoteProps, NoteHeaderProps, NOTE_HEADER_TOKENS, noteHeaderStyleGaps, TagChipsProps, ReferenceContractProps, LicenseBadgeProps, LicenseFileBodyProps | | @galaxy-foundry/site-kit/specimens | Specimen, SpecimenGroup, SpecimenSurface, SPECIMENS, CONTENT_NOTE_SPECIMENS, NOTE_HEADER_SPECIMENS, TAG_CHIPS_SPECIMENS, REFERENCE_SPECIMENS, LICENSE_BADGE_SPECIMENS, LICENSE_FILE_SPECIMENS, HEADER_SPECIMENS, FOOTER_SPECIMENS, SHELL_SPECIMENS, SPECIMEN_CONTRACT, sharesPage, specimenPath | | @galaxy-foundry/site-kit/SiteShell.astro | the shell component | | @galaxy-foundry/site-kit/SiteHeader.astro | the header alone — a gallery cannot show it otherwise | | @galaxy-foundry/site-kit/SiteFooter.astro | the footer alone | | @galaxy-foundry/site-kit/ReferenceContract.astro | the reference card | | @galaxy-foundry/site-kit/LicenseBadge.astro | the licence badge | | @galaxy-foundry/site-kit/LicenseFileBody.astro | the vendored-licence page body | | @galaxy-foundry/site-kit/NoteHeader.astro | the note frame — everything above the body | | @galaxy-foundry/site-kit/ContentNote.astro | that frame plus the metadata and reference slots and the article boundary | | @galaxy-foundry/site-kit/TagChips.astro | controlled tag markup, linked only when the instance supplies a browse route |

resolveNav is exported because it is the only part of the shell with behaviour worth asserting on: a destination is active on its own page and everything beneath it, compared on whole path segments, so /tag/ does not light up on /tags/.

shellStyleGaps is exported for the opposite reason — it asserts on something the kit deliberately does NOT do. See "Define the tokens" above. referenceStyleGaps and licenseBadgeStyleGaps are the same check for the card and the badge, and all three are styleGaps with a different list.

The shell also owns a small client runtime: initial dark-mode selection, persisted theme toggling, Pagefind palette synchronization, and the overflow menu's click, outside-click, and Escape-key behavior. Read Site-kit runtime architecture before replacing or wrapping those controls.

Peer dependencies

astro 6 or later and astro-pagefind 2 or later — the header renders the Pagefind 2 search-box component and styles its pf-searchbox structure.

The .astro components ship as source, not built output. Astro compiles them in your build.