Nasaq
Components

Store Products Admin

The merchant side of the catalogue: a product list with bulk edit, a product editor with a generated variant matrix, media manager and search preview, and a collections manager with manual and rule-based collections.

PreviewOpen ↗

Code

<ProductAdminList  products={products}  currency="USD"  onOpen={(p) => router.push(`/products/${p.id}`)}  onCreate={() => router.push("/products/new")}  onBulkEdit={async (ids, edit) => api.bulkEdit(ids, edit)}/><ProductEditor  initial={productToDraft(product, { cost: 4000 })}  currency="USD"  siteUrl="https://shop.example"  onSave={async (draft) => api.saveProduct(draft)}/>

Store Admin · beta

Live examples and controls: Store Products Admin in the lab.

Install

npx shadcn@latest add https://docs.nasaqui.com/r/store-products-admin.json

Everything a merchant needs to manage the catalogue: a product list with search, filters and bulk edit of price, stock and status; a product editor with pictures, price and margin, inventory, options that build a variant matrix, and a search listing preview; and a collections manager where a collection is picked by hand or filled by rules with a live match preview. Money is integer minor units. The components hold no data and call no API: you pass the catalogue and save in on* callbacks.

When to use

  • A store back office: products, variants, stock, collections.

When not to use

  • Showing products to shoppers: use product-card, product-detail and the listing components.
  • Stock movements and warehouses: use stock-ledger.

Import

import { ProductAdminList, ProductEditor, CollectionsManager, productToDraft } from "@fadymondy/nasaq/web";

Quick start

<ProductAdminList
  products={products}
  currency="USD"
  onOpen={(p) => router.push(`/products/${p.id}`)}
  onCreate={() => router.push("/products/new")}
  onBulkEdit={async (ids, edit) => api.bulkEdit(ids, edit)}
/>

<ProductEditor
  initial={productToDraft(product, { cost: 4000 })}
  currency="USD"
  siteUrl="https://shop.example"
  onSave={async (draft) => api.saveProduct(draft)}
/>

Anatomy

ProductAdminList   [data-slot=product-list]   DataTable + bulk edit dialog + delete confirm
ProductEditor      [data-slot=product-editor]       form of cards, sticky save bar
  MediaManager     [data-slot=media-manager]        sortable picture tiles [data-slot=media-tile]
  OptionsEditor    [data-slot=options-editor]
  VariantMatrix    [data-slot=variant-matrix]
CollectionsManager [data-slot=collections-manager]  cards + editor dialog with RuleBuilder and match preview

API

ProductAdminList

PropTypeDefaultDescription
productsreadonly CommerceProduct[]requiredThe catalogue.
currencystring"USD" ("SAR" in Arabic)ISO 4217 code.
onOpen(product) => voidnoneRow click, Enter, or Edit in the row menu.
onCreate() => voidnoneShows "New product".
onBulkEdit(ids, edit: ProductBulkEdit) => Promise<ProductAdminResult>noneWithout it the bulk button is hidden.
onStatusChange(product, status) => Promise<ProductAdminResult>noneActivate, draft, archive.
onDelete(product) => Promise<ProductAdminResult>noneDelete after confirmation.
lowStockAtnumber5Stock at or below is low.
loading, error, onRetryStates. error is true or a message.
labelsStoreProductsAdminLabelsen / arOverride any string.

ProductEditor

PropTypeDefaultDescription
initialProductDraftempty draftBuild with productToDraft(product, { cost, visibility, seoTitle, seoDescription }).
currencystring"USD" ("SAR" in Arabic)ISO 4217 code.
siteUrlstringhttps://store.exampleOrigin for the search preview URL.
onSave(draft) => Promise<ProductAdminResult>requiredResolve { error } to keep the edits.
onCancel() => voidnoneCalled when Discard is pressed with nothing changed.
suggestions{ brands?, categories?, tags? }noneAutocomplete lists.
loadingbooleanfalseSkeleton.
labelsStoreProductsAdminLabelsen / arStrings.

Changing options regenerates the variants with generateVariants: a variant whose option values still exist keeps its price, compare-at, SKU, stock and image. Removed combinations are named in a notice; the matrix is capped at 100.

MediaManager, OptionsEditor, VariantMatrix

MediaManager({ images, onImagesChange, maxImages = 12, disabled, labels }), OptionsEditor({ options, onOptionsChange, maxOptions = 3, disabled, labels }), VariantMatrix({ options, variants, onVariantsChange, currency, images, disabled, labels }). All are controlled.

CollectionsManager

PropTypeDescription
collectionsreadonly CollectionDef[]{ id, title, kind: "manual" | "rules", productIds?, conditions? }.
productsreadonly CommerceProduct[]For the picker and live preview.
currencystringRule prices are typed in major units of it.
onSave(collection) => Promise<ProductAdminResult>New collections arrive with a fresh id.
onDelete(collection) => Promise<ProductAdminResult>Adds Delete.
loading, error, onRetry, labels

Logic (pure, also exported)

generateVariants, bulkFillVariants, bulkEditProducts, bulkPrice, marginFromCost, priceForMargin, stockSummary, matchCollection, collectionsOfProduct, validateProductDraft, productToDraft, draftToProduct, draftChanged, emptyProductDraft, decimalToMinor, optionCombinations, variantLabel, duplicateSkus. They use integer maths only.

Examples

// A rule collection: products tagged "summer" under 500
const collection: CollectionDef = {
  id: "c1",
  title: "Summer under 500",
  kind: "rules",
  conditions: { kind: "group", id: "g", join: "and", children: [
    { kind: "condition", id: "a", field: "tag", op: "is", value: "summer" },
    { kind: "condition", id: "b", field: "price", op: "lt", value: "500" },
  ] },
};
matchCollection(products, collection, { minorPerMajor: 100 });

Accessibility

KeyAction
Space, arrows, SpacePick up, move and drop a picture (also the move earlier / later buttons).
Enter on a list rowOpen the product.
Shift+F10, Menu key, context-clickRow actions.

Moves are announced in a live region. Missing alt text is counted and flagged. Every icon-only button has a label.

RTL & i18n

English and Arabic built in through useOptionalNasaq(). SKUs, prices and numbers stay LTR inside dir="ltr" or <bdi>. Chevrons flip in RTL.

Styling & tokens

Uses --nq-* tokens only. Target data-slot names above; data-dragging marks a picture being dragged.

Do / Don't

  • Do pass prices in minor units. Don't pass floats.
  • Do give every picture alt text.
  • Don't rely on client validation alone: check again on the server.

Lab

https://docs.nasaqui.com/?path=/docs/components-store-admin-store-products-admin--docs

On this page