@doany-ai/catalog

中文文档
0.1.6发布于 2026/10/29MITTypeScriptES Module近 30 天下载 2069

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

原始使用文档
文档目录13 节
  1. Records
  2. Components
  3. CatalogGrid
  4. CatalogItemCard
  5. CatalogItemDetail
  6. VariantPicker
  7. Price
  8. Hooks
  9. Labels
  10. Customizing
  11. Four runnable usages
  12. Develop
  13. Optional original price

@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), optional compare_at_amount (display-only reference price in the same currency), 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".

PropType
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) => stringeach card's link
renderLink({ href, className, children }) => ReactNodehow a link is rendered; a plain <a> by default. Pass the router's Link to navigate in the site
onSelect(item) => voidmakes each card a button instead
renderItem(item) => ReactNodereplaces the default card
layout"cards" | "list"default cards: capped, centered tiles with automatic columns. list: one full-width column, including loading placeholders; no 384px tile cap or small-count cap
footerReactNode | (item) => ReactNodeunder each card's price
showDescriptionboolean
appearance"default" | "editorial" | "discovery" | "course"default: compact cards. Editorial: landscape media and headings. Discovery: large square covers, borderless layout, full titles/outcomes and a prominent navigation CTA; Course: 16:9 covers, centered titles and descriptions, compact centered CTA, no price or category by default; the package owns all layouts
showPricebooleandefault true; hide displayed amounts without changing API eligibility or selected variants
mediaReactNode | (item) => ReactNodereplace each card's image with configured site media
mediaOverlayReactNode | (item) => ReactNodeindependent preview control over the cover, outside the navigation link/button
formatLabelReactNode | (item) => ReactNodeoffering format such as Online course or 1:1 coaching, separate from the topic; omit in a list already dedicated to one format
accessLabelReactNode | (item) => ReactNodesupplied access/membership note; remains visible with showPrice={false}. Presentation only, never an entitlement calculation
eyebrowReactNode | (item) => ReactNodereplace the category line
actionLabelstringnavigation cue inside each linked/selectable card; discovery uses it as the CTA text (defaults to labels.viewItem). It is not a purchase action
emptyReactNodeshown when nothing is for sale. Default: the empty label

For a service price row or a full-width feature, select layout="list". renderItem alone deliberately keeps the existing card layout. Make the custom row's own columns respond to its available width, not the screen:

<CatalogGrid layout="list" renderItem={(item) => (
  <article className="grid grid-cols-[repeat(auto-fit,minmax(min(100%,16rem),1fr))] gap-6 [&>*]:min-w-0">
    <div><h3>{item.name}</h3><p>{item.description}</p></div>
    <div><Price item={item} /><Button wrap asChild><Link to={`/services/${item.id}`}>Explore this service</Link></Button></div>
  </article>
)} />

Price is from @doany-ai/catalog, Button from @doany-ai/app-core/ui/button, and Link from the site's router. Use the same responsive column definition for a price table's header and rows; on narrow containers stack the cells and show their labels. Do not hide overflow to conceal clipped text or actions.

CatalogItemCard

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

PropType
itemCatalogItemrequired
hrefstringmakes the whole card a link; in discovery, only the explicit CTA is the link
onSelect(item) => voidmakes the whole card a button; in discovery, only the explicit CTA is the button
renderLink, footer, showDescriptionas CatalogGrid
appearance, showPrice, media, mediaOverlay, formatLabel, accessLabel, eyebrow, actionLabelas CatalogGrid

media, eyebrow, formatLabel and accessLabel sit inside the card-wide link or button in default/editorial appearance. Keep them noninteractive (an image or muted preview video without controls). A separate preview control belongs in mediaOverlay; additional play/purchase controls can use footer. Both slots remain outside navigation. Default and editorial overlays align to the package's 4:3 and 16:10 media areas; custom media should keep those proportions. Discovery uses a square cover and a separate CTA, so a preview button can never activate the course link. Its CTA includes the item name for assistive technology. An interactive slot needs its own accessible label. Media config and page copy come from the site; the reusable card layout stays here. These slots do not change product classification, access or purchase rules.

<CatalogGrid
  appearance="editorial"
  showPrice={false}
  hrefFor={(item) => `/collections/${item.id}`}
  actionLabel="Explore collection"
  media={(item) => <img src={posters[item.id]} alt={item.name} />}
  footer={(item) => <button onClick={() => openPreview(item.id)}>Watch preview</button>}
/>

For course discovery, use appearance="course": a 16:9 cover, centered course title and description, and a compact CTA. Show a clear link to the detail page. Hide prices in this collection explicitly; detail offers retain their prices. Use eyebrow="" to omit a redundant topic line, and formatLabel only where the context needs it. Neither this appearance nor hidden prices changes the generic Catalog default or decides whether an item is purchasable.

<CatalogGrid
  appearance="course"
  showPrice={false}
  eyebrow=""
  actionLabel="View course"
  hrefFor={(item) => `/courses/${item.id}`}
  media={(item) => <img src={posters[item.id]} alt="" />}
  mediaOverlay={(item) => <button onClick={() => openPreview(item.id)}>Preview<span className="sr-only"> {item.name}</span></button>}
  accessLabel={(item) => accessNotes[item.id]}
/>

Access notes are supplied by the site from its authorized projection; the component does not infer membership from a product type, price, or browser-local state.

CatalogItemDetail

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

PropType
itemIdstringread by id…
itemCatalogItem…or shown as given
actions(variant | null, item) => ReactNodeunder the options: what to do with the chosen variant, e.g. a BuyButton
variantId, onVariantChangestring | null, (variantId) => voidoptional controlled choice for shared hero/sidebar/mobile purchase placements; omit to keep local selection. Missing or inactive IDs fall back through the package's default-variant rule
notFoundReactNodethe item does not exist or is not published. Default: the notAvailable label
appearance"default" | "editorial"editorial uses landscape media and a prominent title, with the same query/selection/actions
mediaReactNodereplace the item's image; configured by the site
showMediabooleandefault true; false removes the entire media column (including custom media and the loading placeholder), for a compact summary
headingLevel1 | 2 | 3default 1; match the page's heading hierarchy when the item summary sits below an existing page heading
eyebrowReactNodereplace the category line
showPricebooleandefault true; when false, hide the chosen price and picker amounts; options and action arguments remain intact

For a purchase aside below a site's existing hero <h1>, use <CatalogItemDetail itemId={id} headingLevel={2} showMedia={false} actions={...} />. The item query, priced-variant selection and action inputs remain the same; there is no empty image column. The detail props example below runs this composition with a second-level heading.

VariantPicker

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

PropType
variantsCatalogVariant[]required
valuestring | nullthe chosen variant's id
onChange(variantId) => voidrequired
includeUnpricedbooleanalso offer variants without a price (they cannot be ordered). Default false
labelstringfor screen readers; wins over options
showPricebooleandefault true; presentation only, does not enable unpriced choices

Price

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

Hooks

HookReturns
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, { value?, onChange? }){ 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. Optional controlled input shares one choice across multiple placements

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.

KeyDefault
freeFree
fromPrice(price) => "From " + price
duration(minutes) => "45 min", "1 h 30 min"
optionsOptions (the picker, for screen readers)
emptyNothing here yet.
loadErrorThe catalog could not be loaded. (unless set, the platform's own message is shown)
tryAgainTry again
loadMoreLoad more
loadingMoreLoading…
notAvailableThis item is not available.
viewItemView details (discovery CTA; explicit actionLabel wins)

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.

Four runnable usages

The official examples ship in @doany-ai/catalog/examples. Each changed component has the four usages: grid, card, detail, and picker.

import { CatalogExamples } from "@doany-ai/catalog/examples";

<CatalogExamples component="grid" mode="default" />
<CatalogExamples component="grid" mode="css" />
<CatalogExamples component="grid" mode="props" />
<CatalogExamples component="grid" mode="hook" />
  • default: the actual package component and its query/selection behavior.
  • css: identical component props with a scoped set of theme variables. These examples contain no portals; a real site's global theme belongs on :root.
  • props: package-owned editorial appearance, labels/slots and price visibility.
  • hook: explicit custom markup using useCatalogItems, useCatalogItem and useVariantSelection; formatting still uses the package's price helpers.

The same four card/grid usages can demonstrate the discovery presentation:

<CatalogExamples component="card" appearance="course" mode="default" />
<CatalogExamples component="card" appearance="course" mode="css" />
<CatalogExamples component="card" appearance="course" mode="props" />
<CatalogExamples component="card" appearance="course" mode="hook" />

Default and CSS-only have identical component props and records. Props adds an explicit format/access note and custom CTA; hook-only builds the interface from the real catalog query. Every discovery example has separate Preview and View course actions: Preview reports the selected local sample without navigating or claiming to play a trailer. The caller supplies actual media and player behavior.

onSelect(item) can observe selections. Every example has its own local DoanyProvider and query cache. The exported catalogExampleItems and createCatalogExampleClient(items?, pageSize = 2) also let a preview compose package components in one provider. The client answers local catalog list/get, supports the shown filters, name sort and pagination, and rejects every account/upload/mutation capability with EXAMPLE_ONLY. It never calls the network. These illustrative digital records do not establish a Course API, authorization, subscription, download-delivery or checkout contract. Production pages use the site's real SDK client instead of this example adapter.

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.

CatalogItemDetail appearance="summary" uses a compact heading and hides the long description by default for purchase sidebars and closing actions. showDescription can override that presentation; option selection, actual prices, and actions(variant, item) keep the same behavior. The official props example demonstrates this appearance.

Optional original price

Price and CatalogItemDetail display a struck-through compare_at_amount supplied by Catalog beside the current price_amount. Missing/null reference prices render only the current price. An item price range has no reference price unless every priced variant shares both amounts and currency. Selecting another variant updates both amounts. showCompareAt={false} hides the reference; labels.currentPrice and labels.originalPrice customize screen-reader wording. Orders and checkout always use price_amount; the reference amount is never a charge or client discount.

All four usages are runnable with <CatalogExamples component="price" mode={mode} />: default, theme-only css, wording props, and hook using useCatalogItem, useVariantSelection, variantPriceLabel and compareAtPriceLabel with custom markup.

版本与发布记录

@doany-ai/catalog 共发布 11 个版本,首次发布于 2026/09/24;过去 12 个月发布了 11 个版本。

版本发布日期距今
0.1.6latest2026/10/2916 天前
0.1.52026/10/2322 天前
0.1.42026/10/2124 天前
0.1.32026/10/2124 天前
0.1.22026/10/2025 天前
0.1.12026/10/1926 天前

其他发布通道:alpha → 0.1.0-alpha.3。

关于 @doany-ai/catalog 的常见问题

@doany-ai/catalog 是什么?

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

如何安装 @doany-ai/catalog?

在项目目录运行 npm install @doany-ai/catalog,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^18.2.0 || ^19.0.0)、react-dom(^18.2.0 || ^19.0.0)、@doany-ai/app-core(^0.1.8 || ^0.2.0)、@tanstack/react-query(^5.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@doany-ai/catalog 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@doany-ai/catalog 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@doany-ai/catalog 可以商用吗?

@doany-ai/catalog 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@doany-ai/catalog 的下载量有多少?

根据 npm 官方下载统计,@doany-ai/catalog 在 2026/10/13 – 2026/11/11 这 30 天内被下载 2,069 次,日均约 69 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@doany-ai/catalog 还在维护吗?

最新版本 0.1.6 发布于 2026/10/29(16 天前);至今共发布 11 个版本,其中过去 12 个月发布了 11 个;首次发布于 2026/09/24。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。