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

@codefusion-cc/features-page

v0.2.0

Published

The structure and behaviour of a public features page, with no words and no look of its own: typed groups of features with anchors, heading order and accessible names, a section bar and index, screenshots in the viewer's light or dark theme with a strip a

Downloads

872

Readme

@codefusion-cc/features-page

The structure and behaviour of a public page of what an app does: groups of features with stable anchors, headings in order, accessible names, a section bar that follows the reader, real screenshots in the viewer's light or dark theme with a scrolling strip and a full-screen view, the checks that keep content and screenshots in step, and the Playwright run that takes the screenshots.

It holds no words and no look. Titles, texts and the page's own strings (FeaturesCopy) come from the app, and so does every colour, size and layout: each part carries a stable cf-fp-* class and takes the app's own through classNames. A plain list of titles and texts needs no stylesheet at all.

npm install @codefusion-cc/features-page

| Entry | Runs | What | | --- | --- | --- | | @codefusion-cc/features-page | anywhere | the content's types, featureAnchor, featureAnchors, slugify, sectionLinks, shotNames, shotFile, parseShotFile, isPhoneShot, KEY_POINTS, SHOT_MODES | | @codefusion-cc/features-page/react | React 19 | FeatureList, Features, FeaturesIndex, FeaturesNav, Screens, ShotFrame, ShotsProvider, shotUrls, useScrollToHash, useActiveSection | | @codefusion-cc/features-page/styles.css | the page | only what the strip and the full-screen view need to work | | @codefusion-cc/features-page/testing | the app's tests | featuresProblems, outlineProblems | | @codefusion-cc/features-page/screenshots | Node, with Playwright | captureShots, shot, DEVICES, captureArgs, localBase |

No dependencies; React is a peer.

Content

import type { Feature, FeatureGroup } from '@codefusion-cc/features-page'

export const groups: FeatureGroup<'search', 'planned' | 'ready'>[] = [
  {
    id: 'find', // the group's anchor, and the start of its features'
    title: 'Finding things', // optional: without it the group has no heading
    label: 'Find', // optional: its name in a section bar
    lead: 'One box asks six sources.', // optional
    features: [
      {
        id: 'sources', // optional, stable across languages: the anchor is find-sources in every one
        title: 'Six sources at once',
        text: 'The app asks every enabled source in parallel.',
        gain: 'Results from every site in one list.', // optional
        points: ['Most important first', 'The first three show', 'The rest wait under a disclosure'], // optional
        status: 'ready', // optional; the words for it are FeaturesCopy.status
        shots: [{ src: 'search', alt: 'Results for a Linux release, with release tags' }], // optional
      },
    ],
  },
]
  • A feature's anchor is <group id>-<id>, or the title as a slug without id (slugify: accents dropped, Polish letters too). Two features that end up with one anchor get -2, -3: the page never has a duplicate id (featuresProblems still reports it as the mistake it is). In a flat list the anchor is the feature's own part.
  • points: most important first; the first KEY_POINTS (3) show, the rest are under a <details>.
  • A shot is a file per mode: <src>-light.webp and <src>-dark.webp; its size is in the shots.json the screenshot run writes, and a shot taller than wide is marked data-phone.
  • An app with fields of its own extends Feature and reads them in renderFeature.

Parts

A flat list, as a plain page of titles and texts uses it:

import { FeatureList } from '@codefusion-cc/features-page/react'

<FeatureList
  features={content.features}
  classNames={{ list: 'divide-y', item: 'py-8', title: 'text-2xl font-bold', text: 'mt-3 text-lg' }}
/>

Each feature is a <section id="…" aria-labelledby> (its heading's id is <anchor>--title, which no anchor can be) holding its heading (level, 2 by default), status, gain, text, the key points, More details as a <details>, renderFeature's body, and the shots with its aside. A part a feature does not have is not written.

Groups, with a section bar and an index for a long page:

import { Features, FeaturesIndex, FeaturesNav } from '@codefusion-cc/features-page/react'
import { sectionLinks } from '@codefusion-cc/features-page'

<FeaturesNav label={copy.sections} links={[...sectionLinks(groups), { id: 'plans', label: 'Plans' }]} className="sticky top-0" />
<FeaturesIndex groups={groups} />
<Features groups={groups} copy={copy} shots={{ sizes: SIZES, url, mode }} classNames={{ group: 'mt-16', item: 'mt-10' }} linkTitles />
  • A group becomes a <section id> with a heading of level (2) when it has a title, and its features go one level lower; a group with no title has no heading and its features take its level, so headings never skip one.
  • FeaturesIndex puts each group's title in a heading of level (3 by default), for a page that has its own heading above the index.
  • FeaturesNav marks the section in view with aria-current="location" (measured at most once a frame). Sticking to the top, the phone layout and a "contents" button are the app's.
  • linkTitles makes each title a link to its own anchor, described by copy.featureLink. useScrollToHash lands on the address's anchor once the page has rendered (an app whose router does it itself leaves it out).
  • FeaturesCopy: moreDetails(n) (needed by a feature with more than 3 points), status (words for each status in use), featureLink, and shots (needed by a feature with shots). A missing one that is needed throws when it renders, naming the feature: an app's first test finds it.
  • Everything is text: titles, texts, points, statuses and alt text are never read as markup.

Screenshots, with shots={{ sizes, url, mode }} on Features or FeatureList, or ShotsProvider around Screens and ShotFrame for a layout of one's own:

import { shotUrls } from '@codefusion-cc/features-page/react'
const url = shotUrls(import.meta.glob('./shots/*.webp', { eager: true, query: '?url', import: 'default' }))

One shot stands alone; several share a height in a strip that snaps shot by shot, with arrows when they do not all fit (an arrow at its end is aria-disabled and keeps focus; smooth paging gives way to prefers-reduced-motion). A click opens them full screen in a modal <dialog>: arrow keys, buttons or a swipe move round the ends, Escape, Close or a click beside it close it, and focus goes back to the shot. A shot's alt text names its button (copy.shots.enlarge) and is the full-screen image's alt; captions also writes it under the shot, which is off since the picture says it.

Look

styles.css (imported once, only where there are screenshots) makes the strip scroll and snap, sizes the shots and centres the full-screen view. Its few colours are --cf-fp-surface, --cf-fp-ink, --cf-fp-line and --cf-fp-focus, set on any ancestor; neutral ones that follow color-scheme otherwise. It is in the components layer, so the app's own rules win.

Checks

import { featuresProblems, outlineProblems } from '@codefusion-cc/features-page/testing'

it('has what it shows: anchors, texts, statuses with words, screenshots in both modes', () => {
  expect(featuresProblems({ groups, sizes: SIZES, files: Object.keys(import.meta.glob('./shots/*.webp')), statusCopy: copy.status, require: ['gain'] })).toEqual([])
})
it('shows the same features in every language', () => {
  for (const other of [pl, de]) expect(outlineProblems(en.groups, other.groups)).toEqual([])
})

featuresProblems lists, as sentences: anchors that are not ones or that two features, groups or sections share, empty titles and texts, repeated or empty points, repeated shots, alt text too short to say what a shot shows, statuses without words, shots without a size or a file in a mode, and sizes or files nothing shows. require: ['gain', 'points'] also asks for what the app's page needs of every feature. features checks a flat list. outlineProblems lists where a translation shows other features, shots, statuses or numbers of points.

Screenshots

A script of the app takes them from the app running locally, in Chromium, each in every mode:

// scripts/screenshots.ts
import { chromium, type Page } from 'playwright'
import { captureArgs, captureShots, DEVICES, localBase, shot } from '@codefusion-cc/features-page/screenshots'
import { themeConfig } from '../src/themeConfig.ts' // the app's defineTheme(…)

const base = localBase(process.env.BASE ?? 'http://localhost:5173')
const browser = await chromium.launch()
const result = await captureShots({
  browser,
  outDir: new URL('../src/features/shots/', import.meta.url),
  sizesFile: new URL('../src/features/shots.json', import.meta.url),
  // An app that keeps a theme of its own in the browser starts each context on the run's mode (with
  // @codefusion-cc/theme: its config's storageKey). One that follows the system needs nothing: the context has the mode.
  prepare: (context, { mode }) => context.addInitScript(([key, m]) => localStorage.setItem(key, m), [themeConfig.storageKey, mode]),
  scenes: [
    shot('downloads', async (page: Page) => { await page.goto(`${base}/`) }),
    shot('downloads-phone', async (page: Page) => { await page.goto(`${base}/`) }, { device: DEVICES.phone }),
    {
      // Several shots of one setup: two people, one chat.
      shots: ['chat', 'chat-phone'],
      run: async ({ page, shoot }) => {
        const a = await page()
        const b = await page(DEVICES.phone)
        // …
        await shoot(a, 'chat')
        await shoot(b, 'chat-phone')
      },
    },
  ],
  ...captureArgs(process.argv, process.env), // --only=chat,downloads  --modes=dark  (or SHOTS=…, MODES=…)
})
await browser.close()
if (result.failed.length) process.exitCode = 1
  • Contexts open in the mode's colour scheme with reduced motion; prepare sets what the app keeps in the browser (its stored theme, language, a dismissed consent banner). The run closes every context after its scene.
  • Before each shot it waits for fonts, for [aria-busy="true"] (busy) to go and for the images in view, then settleMs; carets are hidden and animations stopped.
  • WebP by Chromium's own encoder: nothing else to install. Desktop shots are 1920 × 1200 (1280 × 800 at 1.5×), phone shots 780 × 1688 (390 × 844 at 2×).
  • shots.json lists, sorted, every shot with all its modes' files; the files of a shot it listed that no scene takes any more are removed, and any other file in the folder is left alone. --only takes some and keeps the rest; --modes=dark takes one mode (a run per mode on fresh data).
  • A failing scene doesn't stop the run: it is in result.failed with why. A script's own mistakes (a shot two scenes take, a name that is not a file name) throw before anything opens.
  • localBase refuses any address but this machine's: the run writes sample data.