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

@doany-ai/catalog

v0.1.0

Published

What a business sells, on a Doany site: catalog hooks and default components (item cards, grid, variant picker, item detail)

Readme

@doany-ai/catalog

What a business sells, on its site: hooks that read the catalog through the SDK client given to @doany-ai/app-core's <DoanyProvider>, and default components a site can restyle through the theme, adjust through props, or replace while keeping the hooks.

The site only reads the catalog. What is published, purchasable and at what price is the platform's to decide; the components show what the API answers. A visitor only ever sees published items; a site lists its own items and those every site of the business sells. Items are added and put on sale by the agent's business tools, never from the site's code.

Needs @doany-ai/app-core (DoanyProvider) and a client from @doany-ai/sdk 0.3.0-alpha.0 or later. It ships no CSS: the site's Tailwind build scans it (see @doany-ai/app-core).

import { CatalogGrid, CatalogItemDetail } from "@doany-ai/catalog";
import { BuyButton } from "@doany-ai/commerce";

<CatalogGrid
  params={{ purchasable: true }}
  hrefFor={(item) => `/services/${item.id}`}
  renderLink={({ href, className, children }) => <Link to={href} className={className}>{children}</Link>}
/>

<CatalogItemDetail
  itemId={id}
  actions={(variant, item) => variant && item.purchasable && <BuyButton items={[{ catalog_item_variant_id: variant.id }]}>Order now</BuyButton>}
/>

Records

  • CatalogItem: id, kind (service / physical / digital / plan / bundle), status (draft / active / paused / archived), name, description, category, image_url, service_mode (booking / order / inquiry, or null when unclassified), optional payment_requirement (none / full_online / null), direct_purchase_enabled, purchasable (the API decides website eligibility), sort_order, variants (in display order). An item that was a product of a site built before the catalog also carries legacy_id (the id that site knew it by) and attributes (the fields the site kept on it beside its name and price, such as slug or color, as it kept them). Both are read only, and {} / null on an item made in the catalog.
  • CatalogVariant: id, name, price_amount (integer, smallest currency unit; null = no price, 0 = free), currency, duration_minutes, is_default (preselected), status, sku, sort_order.

Booking uses Calendar; Inquiry requires a confirmed quote. Their direct purchase is fixed off. Order services have direct purchase fixed on, but charged website Orders currently require full_online; none is available for merchant-entered orders. Zero is free and null means no price. Follow the API's purchasable result.

Filter explicit Order services with params={{ service_mode: "order", purchasable: true }} or useCatalogItems({ service_mode: "order" }). A service inquiry form itself creates no order or appointment.

Components

Every component takes locale (money; default: the browser's), labels (a part of CatalogLabels, see below) and className.

CatalogGrid

Cards in a grid, with loading, empty and error states and "Load more".

| Prop | Type | | | --- | --- | --- | | params | { purchasable?, category?, kind?, type?, service_mode?, q?, sort?, limit? } | which items. sort: sort_order (default), name, created_at, updated_at, - in front for descending. limit 1–200, default 50 | | hrefFor | (item) => string | each card's link | | renderLink | ({ href, className, children }) => ReactNode | how a link is rendered; a plain <a> by default. Pass the router's Link to navigate in the site | | onSelect | (item) => void | makes each card a button instead | | renderItem | (item) => ReactNode | replaces the default card | | footer | ReactNode \| (item) => ReactNode | under each card's price | | showDescription | boolean | | | empty | ReactNode | shown when nothing is for sale. Default: the empty label |

CatalogItemCard

Image, name, category, price (From … when variants differ) and duration.

| Prop | Type | | | --- | --- | --- | | item | CatalogItem | required | | href | string | makes the whole card a link | | onSelect | (item) => void | makes the whole card a button | | renderLink, footer, showDescription | | as CatalogGrid |

CatalogItemDetail

An item's page: image, name, description, the variant picker and the price of the chosen variant.

| Prop | Type | | | --- | --- | --- | | itemId | string | read by id… | | item | CatalogItem | …or shown as given | | actions | (variant \| null, item) => ReactNode | under the options: what to do with the chosen variant, e.g. a BuyButton | | notFound | ReactNode | the item does not exist or is not published. Default: the notAvailable label |

VariantPicker

The priced active variants as a radio group; nothing for an item sold one way.

| Prop | Type | | | --- | --- | --- | | variants | CatalogVariant[] | required | | value | string \| null | the chosen variant's id | | onChange | (variantId) => void | required | | includeUnpriced | boolean | also offer variants without a price (they cannot be ordered). Default false | | label | string | for screen readers; wins over options |

Price

variant (one price) or item (one price, or From … when its variants differ); fallback when there is no price (default: nothing).

Hooks

| Hook | Returns | | --- | --- | | useCatalogItems(params) | the site's items, a page at a time, sort_order first: items (all pages read so far), isLoading, isError, hasNextPage, fetchNextPage(). params as CatalogGrid | | useCatalogItem(itemId) | a query; data is the item with its active variants | | useVariantSelection(item) | { variant, select(variantId) }: starts on the item's default variant, follows a new item; an id that is not the item's falls back to the default |

Display helpers the components use: itemPriceLabel(item, locale?, words?), variantPriceLabel(variant, locale?, words?) (text, or null when there is no price), formatDuration(minutes) (1 h 30 min), pricedVariants(item), defaultVariant(item). catalogKeys are the React Query keys, for invalidating.

Labels

Every word the components show is in CatalogLabels. A component takes a part of it and keeps the defaults for the rest, and passes it on to the ones inside it (a grid to its cards, an item page to its picker). defaultCatalogLabels holds the defaults; catalogLabels(partial) merges.

| Key | Default | | --- | --- | | free | Free | | fromPrice | (price) => "From " + price | | duration | (minutes) => "45 min", "1 h 30 min" | | options | Options (the picker, for screen readers) | | empty | Nothing here yet. | | loadError | The catalog could not be loaded. (unless set, the platform's own message is shown) | | tryAgain | Try again | | loadMore | Load more | | loadingMore | Loading… | | notAvailable | This item is not available. |

Customizing

Stop at the first level that does what is needed:

  1. CSS: the theme variables on :root (see @doany-ai/app-core).

  2. Props: the slots and props above, and labels for every word:

    <CatalogGrid labels={{ loadMore: "Show more", free: "Complimentary", duration: (m) => `${m} minutes` }} />
  3. Hooks: build the list or the item page on useCatalogItems, useCatalogItem and useVariantSelection, with itemPriceLabel / variantPriceLabel for the prices.

Never edit this package under node_modules or copy its source into the site.

Develop

npm run typecheck includes tests/sdk-contract.types.ts: the real @doany-ai/sdk client (from ../doany-sdk) must satisfy the client type this package calls, so a changed SDK contract fails here, not on a site.