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

@sarimarcus/content-sites-ui

v0.29.2

Published

Universal Astro UI components, layout shell and design tokens shared by the content sites.

Downloads

6,657

Readme

@sarimarcus/content-sites-ui

Universal Astro UI shared by every content site. Named entry points, one per folder: primitives, composed, editorial, affiliate, seo, layout, navigation, search and types, plus the script entry points analytics, travelpayouts-styler and search-page. For example, import { Container } from '@sarimarcus/content-sites-ui/layout'. src/internal is never exported, and src/styles holds the Tailwind entry.

Depends on @sarimarcus/content-sites-core (exact lockstep peer) and @lucide/astro (the Icon primitive). It carries no reader-visible copy: text arrives through props and slots.

Primitives

import { Icon, Badge, Chip, ArrowLabel, SectionDivider, LinkArrow, Button, Callout, FeatureList, Blockquote, SectionHeader, ResponsiveImage, ResponsivePicture, PILL_BASE, isBadgeLength, PRICE_LABEL_MAX } from '@sarimarcus/content-sites-ui/primitives';
  • Icon: any Lucide icon by kebab-case name.
  • Badge: a content label (<span data-badge>), clamped to lines (default 2). Tones: primary (default), secondary, accent, neutral, onImage, or custom with a site-owned toneClass.
  • Chip: an interactive pill (<a>/<button>/<span>, data-chip first). tone sets the idle surface (outline, outlineMuted, outlineQuiet, muted, tint, onImage, custom), accent the hover, active and focus colour (primary (default), secondary, accent, neutral). active emits data-active="true"|"false" only when set; scripts must write dataset.active = 'true'|'false', never add or remove the attribute. Other attributes pass through last, so markers such as data-section-link reach the element.
  • ArrowLabel: a label followed by an arrow that shifts right on group-hover. It carries no colour: the caller's class sets it; arrowClass sizes the arrow (default h-4 w-4).
  • SectionDivider: a break between sections. variant is dots (default), line, or quote (with quote and an optional source).
  • LinkArrow: a text link with a trailing arrow. color is primary (default), secondary, accent or onImage; size is sm (default) or base. An http(s) href opens in a new tab with rel="noopener noreferrer"; an explicit target (_blank or _self) or rel overrides that. Attributes render as href target rel class.
  • Button: a link styled as a button. variant is primary (default, solid), secondary (an outline in the primary colour), secondarySolid (solid in the secondary colour), hero (frosted glass over a photo) or ghost (for dark themes). Same new-tab rule as LinkArrow: an http(s) href gets target="_blank", and a new tab gets rel="noopener noreferrer", unless target or rel is given. Attributes render as href rel target class, then every other anchor attribute (data-*, aria-*, slot) in the order passed.
  • Callout: a boxed note with an icon. type is info (default), tip, warning or success; title is optional.
  • FeatureList: a checklist or a row of tags from items (rendered with set:html). color is primary (default), secondary or accent; container boxes it, compact tightens it, headingLevel sets the heading (default h2), which renders only for a non-empty title; footer adds a line under a border.
  • Blockquote: a centred pull-quote, quote plus an optional source.
  • SectionHeader: an icon tile (slot icon), an h2 title and an optional action slot. iconBg and iconColor are raw classes the site supplies.
  • ResponsiveImage: Astro's <Image> at every IMAGE_LADDER width the source has variants for. Takes <Image>'s props less widths, densities and format, and requires sizes. A fixed-size image (avatar, thumbnail) stays a plain <Image width={N}>.
  • ResponsivePicture: ResponsiveImage inside a <picture> that takes pictureAttributes, for a styled wrapper. Same HTML as Astro's <Picture> with WebP, less its redundant <source>.
  • PILL_BASE: the shared pill geometry. isBadgeLength, BADGE_MAX_CHARS, PRICE_LABEL_MAX: the label-length limits a call site branches on (pill or prose); a price-range schema .max() must equal PRICE_LABEL_MAX.

data-badge and data-chip are what validate-badge-overflow.mjs selects on, so they are part of the API. Badge and Chip colour only through theme roles, including -tint, -emphasis and the badge-on-image / chip-on-image roles, which each site sets to its own values.

Navigation and search

import { SiteHeader, NavDropdown, MobileNavSection, SiteFooter, type NavLink, type FooterSection } from '@sarimarcus/content-sites-ui/navigation';
import { SearchSuggestions, appendResults } from '@sarimarcus/content-sites-ui/search';

Shells for site chrome. The site composes every link, panel, logo and label; the shells own the landmarks, the ARIA state and one bundled controller (idempotent on astro:page-load, any number of headers per page).

  • SiteHeader: the fixed <header> with a labelled <nav>, slots logo, desktop, actions, mobile-actions and mobile, the mobile-menu toggle, and the mobile panel rendered after the header (with data-pagefind-ignore). searchPath turns on Ctrl/Cmd+K → <searchPath>?focus=1. surfaceClass sets the bar's and the panel's background. Ids default to main-nav, mobile-menu, mobile-menu-btn.
  • NavDropdown: trigger plus panel (default slot). Click, hover with a 150 ms close delay, ArrowDown/ArrowUp/Escape, outside click and focus leaving. tone tints the default trigger; buttonClass replaces it.
  • MobileNavSection: one section of the mobile panel's single-open accordion (its button's aria-controls names its content).
  • NavMenuLink / MobileNavLink: one row of a dropdown panel / of the mobile panel, from a NavLink, tinted by a NavTone. active marks the row aria-current="page"; emphasis paints hub links in the tone.
  • isActivePath(currentPath, href): the page or a page below it, on a path-segment boundary.
  • SiteFooter: brand slot, one column per FooterSection, a bottom slot, the copyright line and a <nav aria-label="Legal"> ending with the cookie-preferences button, which calls window.openCookiePreferences() (the site's consent loader defines it). Defaults paint the inverse roles; class props restyle each part.
  • appendResults renders Pagefind result cards from the site's type taxonomy; SearchSuggestions renders the empty-state chips. SearchPage is the whole page body (hero, type chips, an audience row when audienceFacets is passed, result states); the route keeps Layout (noindex), imports search.css and calls initSearchPage from @sarimarcus/content-sites-ui/search-page in its own <script> with the same taxonomy and copy. The copy, the taxonomy and the index stay in the site.

Page templates

import { StaticProsePage, NotFoundPage, ArticlesListingPage, ArticleFeatureCard } from '@sarimarcus/content-sites-ui/editorial';
import { ListingShell } from '@sarimarcus/content-sites-ui/layout';

The route file keeps getStaticPaths, queries, URLs, <Layout title description> (the SEO length gate reads it) and <StructuredData slot="head"> as a direct Layout child; the template receives normalised props and slots. Hrefs and prose stay in the route so the site's link and outbound-domain scanners still see them.

  • StaticProsePage: privacy, terms, contact, editorial standards. Prose in the default slot, after slot, optional cta box. Carries no Pagefind body.
  • NotFoundPage: the 404 body; the route passes noindex to Layout and imports @sarimarcus/content-sites-ui/not-found.css.
  • ArticlesListingPage / ArticleFeatureCard: article index, topic hubs and their paginated pages.
  • ArticleTopicPage: the body of /articles/topic/<category> and its paginated pages, around the topic nav. Blocks the site renders through its own adapters arrive as slots: start-here, grid, pagination, go-deeper, places, faq (wrapped in a measure column only when filled), plus breadcrumb, header-extra and overview-byline. The overview and the author cards are props. A page above 1 renders only the nav, grid and pagination. rankByCount ranks a topic's authors or collections by article count.
  • ListingShell: listing hubs (breadcrumb, hero, before-main, Container'd <main>, after-main), one outer data-pagefind-ignore.

A component exported from a barrel must not carry a <style> block: Astro ships it to every page that imports the barrel. Put such rules in a stylesheet entry the one route imports.

Editorial presentation

import { ArticleKeyFacts, AuthorCard, AuthorStrip, ArticleCard, ArticleGrid, ArticleCategoryNav, InsiderTips } from '@sarimarcus/content-sites-ui/editorial';
import { RelatedContent, RelatedContentCard } from '@sarimarcus/content-sites-ui/composed';
import type { ArticleSummary, AuthorSummary, KeyFact, SourceItem } from '@sarimarcus/content-sites-ui/editorial';
import type { RelatedItem } from '@sarimarcus/content-sites-ui/composed';

Every visible and aria string is a required prop with no default, so the copy stays in the site. The site keeps a thin adapter of the same name that reads its collections, globs, routes, markdown renderer and author profiles, then passes view models: dates already formatted (the package never picks a time zone), hrefs with the base path applied, HTML that crosses the boundary only through trustedHtml() (key-fact details and tip bodies keep their #fn-N anchors because the site parses them with its source count). Palette props (figureClass, surfaceClass, ringClass, proseClass) let a site keep its own roles.

  • ArticleKeyFacts: figure-first <dl>, rendered only with three or more facts.
  • AuthorCard: whole-card link, polaroid, or plain card with a profile link. AuthorStrip: portraits row.
  • ArticleCard / ArticleGrid: listing cards; pagefindIgnore for a listing that is itself indexed. ArticleFeatureCard is the wide lead card.
  • ArticleCategoryNav: "All" plus one chip per populated topic, with counts. Pass the articles and the site's category map (label, icon, in display order); it links /articles and /articles/topic/<slug>.
  • RelatedContent / RelatedContentCard (in ./composed, beside the CardCarousel they render): grid or carousel of RelatedItem cards, tone primary/accent/secondary. Resolving which items to show (collections, scoring, backfill) stays a site adapter.
  • InsiderTips: tip kinds are site keys mapped to a label and a tone.

These render inside the site's article styles (.article-body link rules, typography plugin presets); outside them they look different.

Article interludes

---
import { Interlude, RelatedCard } from '@sarimarcus/content-sites-ui/editorial';
import { trustedHtml } from '@sarimarcus/content-sites-ui/types';
const renderInline = (s: string) => trustedHtml(parseMarkdownInline(s, basePath, sourcesLength));
---
{interlude.type === 'related-card' && <RelatedCard href={href} eyebrow={`Read next · ${label}`} title={name} />}
<Interlude interlude={interlude} renderInline={renderInline} labels={labels} author={author} />

Interlude renders the eleven content interludes (quote, stat-fact, evidence-context, tldr, answer-box, field-note, caution, checklist, did-you-know, timeline, comparison-table) and nothing for the entity ones (related-card, monument-feature, article-feature): their target, route and image are site data, so the site's adapter renders RelatedCard (inline row or featured card) for them. Every string field passes through the site's renderInline except the stat figure. labels carries all the copy; didYouKnowVariant picks ruled or inline.

The pieces are exported too, for pages that place an interlude by hand: AnswerBox, ComparisonTable, EvidenceContext, FieldNote (authorHref links the signature), Quote, StatFact, Timeline, Tldr, DidYouKnow. The content schema is core's createInterludeSchema (@sarimarcus/content-sites-core/interludes).

Document shell

---
import { DocumentShell } from '@sarimarcus/content-sites-ui/layout';
---
<DocumentShell title={title} description={description} siteName={site.siteName} siteUrl={site.url} themeColor="#FAFAF8"
  gtmId={site.analytics.gtmId} consentPrompt="…" consentTypes={consentTypes} skipLinkLabel="Skip to main content">
  <Fragment slot="fonts">…</Fragment>
  <Navigation slot="nav" />
  <slot />
  <Footer slot="footer" />
</DocumentShell>

DocumentShell renders the whole document: description, robots (noindex, follow when noindex), canonical (from the page path unless canonicalUrl), RSS and favicons, Open Graph / article / Twitter (twitterHandle optional), title, non-critical stylesheets, the Emerald loader (travelpayoutsTrs), the skip link, the navigation and footer wrapped in data-pagefind-ignore, #main-content and ConsentAnalytics. Slots: head-start, fonts, head, body-start, nav, footer, default. It imports no stylesheet and emits no bundled script, so the site's thin Layout keeps both (and with them its bundle names).

ConsentAnalytics is inline on purpose: the Consent Mode default must reach the dataLayer before the gtm.js event in one synchronous step. It defines window.openCookiePreferences for the footer button. ./analytics (setupAnalytics, track, pageTypeOf) and ./travelpayouts-styler (setupTravelpayoutsStyler) are their own entry points so a client <script> imports no Astro component. The hosts the shell loads are declared in layout/resources.ts; scripts/validate-csp-coverage.mjs checks every site's CSP against them.

Search and consent styles

@sarimarcus/content-sites-ui/search.css styles the search page markup (appendResults, SearchSuggestions and the page's form, filters and states); @sarimarcus/content-sites-ui/consent.css styles the Silktide consent banner and modal. Both read the theme roles, and optional --search-* / --consent-* custom properties, listed at the top of each file, tune what differs per site. Load them as non-critical sheets with AsyncStylesheet (ui/layout):

---
import searchCssUrl from '@sarimarcus/content-sites-ui/search.css?url';
import { AsyncStylesheet } from '@sarimarcus/content-sites-ui/layout';
---
<AsyncStylesheet href={searchCssUrl} />

A site's own additions to a sheet load after it.

Tailwind

Tailwind v4 does not scan node_modules, so a site must import the package's source entry next to Tailwind:

@import "tailwindcss";
@import "@sarimarcus/content-sites-ui/tailwind.css";

Without it, utility classes used only inside package components are silently missing from the site's CSS.

Theme

theme.css declares the semantic roles package components use (primary, primary-strong, muted-foreground, overlay, the radius scale, …) as --color-X: var(--X). Import it right after the site's own @theme inline block, then set every --X, plus --radius, in the site's :root:

@theme inline { /* the site's palette and --font-display / --font-body */ }
@import "@sarimarcus/content-sites-ui/theme.css";
:root { --primary: …; --primary-strong: …; /* every role */ }

The values are runtime vars, so a .dark block and plain (non-Tailwind) stylesheets read them too. scripts/platform/validate-theme-contract.mjs checks that every role has a value.