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

@raggle-ai/scout-ui

v0.3.0

Published

Reusable React project search components for Raggle Scout

Readme

@raggle-ai/scout-ui

Reusable React project search components for Raggle Scout consumers.

import { ProjectSearchList } from "@raggle-ai/scout-ui";
import "@raggle-ai/scout-ui/styles.css";

<ProjectSearchList
  projects={results}
  query={query}
  onQueryChange={setQuery}
  onSelect={setProject}
/>;

The caller owns the query and fetches ranked results from its search API. The list preserves the supplied order and does not filter results again. Set status="loading" while fetching: stale rows are hidden and cannot be selected. Use status="error", errorMessage, and optional onRetry for failures. emptyMessage is shown only when ready with no results; loadingMessage customizes the loading announcement.

ProjectSearchList renders inside an InputFloatingPanel, which floats above the input that opens it. InputFloatingPanel is exported for custom floating compositions.

Folder filter

useFolderFilter supplies folder choices, selection state, and visible items. FolderFilterBar renders a Folders button that opens a searchable modal using ProjectList checkbox rows. Selection changes apply immediately without closing the modal. Clicking a row or its checkbox toggles that folder; selected folders combine their items. Clearing every selection shows all folders. Escape or the close button closes the modal and returns focus to the Folders button.

Import the shared stylesheet. Neither component requires Kennel or Tailwind. Use mode: "subtree" to include descendants; the default is "exact".

import { FolderFilterBar, useFolderFilter } from "@raggle-ai/scout-ui";
import "@raggle-ai/scout-ui/styles.css";

const filter = useFolderFilter(items, {
  rootFolder: folder,
  getFolder: (item) => item.folder,
});

<FolderFilterBar
  choices={filter.choices}
  selected={filter.effectiveSelected}
  onSelect={filter.setSelected}
  total={filter.total}
/>;
// Render filter.visibleItems in your list.

Pure helpers are also available from @raggle-ai/scout-ui/folder-filter without loading React. The Kennel Tasks plugin uses this filter.

Pass folderHref={(choice) => yourFolderUrl(choice.folder)} to add an external-link icon at the right of every folder row. It opens the folder in a new tab without changing the checkbox selection. The caller supplies the navigation URL; Scout UI does not depend on Kennel routes.

Each choice includes a fixed-size folder glyph. To show folder images, pass renderIcon={(choice) => <FolderIcon src={icons[choice.folder]} />} and import FolderIcon from @raggle-ai/scout-ui. The caller supplies image URLs, for example from useFolderIdentity. Icons retain their dimensions without Scout CSS and show the folder glyph when an image cannot load.

Workspace groups by default

For server-backed views, discover groups with the Node-only entry point:

import { folderFilterGroups } from "@raggle-ai/scout-ui/folder-filter/server";

const folderGroups = await folderFilterGroups(sourceFolders, viewedFolder);
// Send folderGroups with the list data, then pass it to useFolderFilter.

useFolderFilter(items, { rootFolder, getFolder, folderGroups }) combines choices and counts under the highest folder with a raggle.json below the viewed root. For example, baker-street/legal and baker-street/registered-office become one baker-street choice. Selecting it includes both sets of tasks, even in exact mode. Nested workspace markers do not split a higher-level group.

The viewed root itself does not collapse all choices into one. Folders without a workspace marker below that root keep their owning-folder choice. Outside folders stay unchanged. Discovery checks only source-folder ancestors inside the viewed root, caches shared ancestors for the request, and does not scan the whole repository. Browser exports do not import Node filesystem modules.

Direct list

For pages where results should render inline (no floating panel), use ProjectList with your own input. Share useProjectNavigation between the input and list to connect keyboard navigation and screen-reader active-result announcements. Pass the same projects, status, maxResults, resetKey, and onSelect to the hook and list. Standalone lists without a controller are a single keyboard-focusable listbox. The existing handleKeyDown ref remains available for keyboard-only integration.

import { useState } from "react";
import { ProjectList, useProjectNavigation, type ProjectSearchItem } from "@raggle-ai/scout-ui";
import "@raggle-ai/scout-ui/styles.css";

function ProjectSearchPage({ results }: { results: ProjectSearchItem[] }) {
  const [query, setQuery] = useState("");
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const onSelect = (project: ProjectSearchItem) => setSelectedId(project.id);
  const navigation = useProjectNavigation({ projects: results, onSelect, resetKey: query });

  return (
    <>
      <input
        value={query}
        onChange={(event) => setQuery(event.target.value)}
        aria-label="Search projects"
        {...navigation.inputProps}
      />
      <ProjectList
        navigation={navigation}
        ariaLabel="Search results"
        projects={results}
        selectedProjectId={selectedId}
        resetKey={query}
        onSelect={onSelect}
      />
    </>
  );
}

Checkbox selection

Supply checkedProjectIds to ProjectList to render native checkbox rows instead of single-selection options. Handle onSelect(project) by toggling that id in the controlled array. Clicking the row label or pressing Space on its checkbox calls onSelect once. Tab moves between checkboxes; the single-selection arrow/Enter controller is not used in checkbox mode. renderIcon(project) can supply a custom folder icon. itemHref(project) adds a separate navigation link to checkbox rows; it is outside the row label, so navigation does not toggle selection. The row layout and shared styles remain the same in both modes.

Anchored floating search

Pass a trigger ref for automatic above/below placement, viewport collision handling, Escape/outside-click dismissal, and focus restoration. Anchored panels portal to document.body; put theme variables on a shared ancestor such as :root. Without anchorRef, the existing parent-relative positioning is retained.

const trigger = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);

<button ref={trigger} aria-haspopup="dialog" aria-expanded={open}
  onClick={() => setOpen(!open)}>Choose project</button>
<ProjectSearchList
  anchorRef={trigger} open={open} onOpenChange={setOpen}
  projects={results} query={query} onQueryChange={setQuery}
  onSelect={(project) => { setProject(project); setOpen(false); }}
/>

Rows adapt to their container width, not just the viewport. Full worktree paths are available through option accessible names and native hover titles. Existing --scout-project-* CSS variables also theme standalone lists. className applies to the results wrapper; header renders outside listbox semantics.

Local demo

Run a standalone Vite demo that imports the package by its published name (@raggle-ai/scout-ui) and serves representative project data with working search and selection:

bun dev

bun dev rebuilds the package (dist/) as needed, starts the web/ Vite dev server on the stable localhost port 43210, and opens the demo in your default browser. The demo lives in web/ and depends on this package through a file:.. link, so it exercises the real published export map rather than relative ../src imports.

The component-state gallery uses local fixtures independently of the Scout feed. Switch between ready/long-name, loading, error/retry, and empty states; resize the inline example or open the anchored floating search.

Publishing

The package publishes from .github/workflows/publish.yml on every push to main and from manual workflow dispatch.

The workflow validates the package on GitHub-hosted Actions, checks the tarball with npm pack --dry-run, and publishes only when the current package version does not already exist on npm.

Configure npm trusted publishing for package @raggle-ai/scout-ui with GitHub organization raggle-ai, repository scout-ui, and workflow filename publish.yml.

npm trusted publishing supports GitHub-hosted runners. The publish job uses ubuntu-latest instead of Blacksmith so npm can verify the OIDC token.

The repository is private, so the publish command does not request npm provenance.