@cimplify/sdk 0.9.10 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/{ads-MkGm5l1T.d.mts → ads-BxbWrwqp.d.mts} +0 -8
  2. package/dist/{ads-MkGm5l1T.d.ts → ads-BxbWrwqp.d.ts} +0 -8
  3. package/dist/advanced.d.mts +2 -2
  4. package/dist/advanced.d.ts +2 -2
  5. package/dist/advanced.js +93 -80
  6. package/dist/advanced.mjs +93 -80
  7. package/dist/cli.js +184 -0
  8. package/dist/{client-BQ1gIg8t.d.mts → client-BSrq89H1.d.mts} +42 -374
  9. package/dist/{client-C3TQtGuy.d.ts → client-xBhdHLq4.d.ts} +42 -374
  10. package/dist/index.d.mts +6 -10
  11. package/dist/index.d.ts +6 -10
  12. package/dist/index.js +98 -126
  13. package/dist/index.mjs +98 -126
  14. package/dist/{payment-CTalZM5l.d.mts → payment-CrNyrc-D.d.mts} +145 -95
  15. package/dist/{payment-CTalZM5l.d.ts → payment-CrNyrc-D.d.ts} +145 -95
  16. package/dist/price-C9Z-hr49.d.mts +21 -0
  17. package/dist/price-RKKoTz-9.d.ts +21 -0
  18. package/dist/react.d.mts +1285 -35
  19. package/dist/react.d.ts +1285 -35
  20. package/dist/react.js +6596 -2598
  21. package/dist/react.mjs +6550 -2600
  22. package/dist/utils.d.mts +55 -2
  23. package/dist/utils.d.ts +55 -2
  24. package/dist/utils.js +23 -20
  25. package/dist/utils.mjs +23 -20
  26. package/package.json +13 -3
  27. package/registry/add-on-selector.json +15 -0
  28. package/registry/availability-badge.json +15 -0
  29. package/registry/booking-card.json +16 -0
  30. package/registry/booking-list.json +16 -0
  31. package/registry/booking-page.json +18 -0
  32. package/registry/bookings-page.json +17 -0
  33. package/registry/bundle-selector.json +15 -0
  34. package/registry/cart-page.json +17 -0
  35. package/registry/cart-summary.json +16 -0
  36. package/registry/catalogue-page.json +18 -0
  37. package/registry/category-filter.json +15 -0
  38. package/registry/category-grid.json +15 -0
  39. package/registry/checkout-page.json +15 -0
  40. package/registry/cn.json +13 -0
  41. package/registry/collection-page.json +16 -0
  42. package/registry/composite-selector.json +15 -0
  43. package/registry/date-slot-picker.json +16 -0
  44. package/registry/deal-banner.json +16 -0
  45. package/registry/deals-page.json +19 -0
  46. package/registry/discount-input.json +16 -0
  47. package/registry/index.json +411 -0
  48. package/registry/order-detail-page.json +16 -0
  49. package/registry/order-history-page.json +17 -0
  50. package/registry/order-history.json +16 -0
  51. package/registry/order-summary.json +16 -0
  52. package/registry/price.json +13 -0
  53. package/registry/product-card.json +17 -0
  54. package/registry/product-customizer.json +20 -0
  55. package/registry/product-grid.json +16 -0
  56. package/registry/product-image-gallery.json +13 -0
  57. package/registry/product-page.json +19 -0
  58. package/registry/product-sheet.json +18 -0
  59. package/registry/quantity-selector.json +13 -0
  60. package/registry/sale-badge.json +16 -0
  61. package/registry/search-input.json +15 -0
  62. package/registry/search-page.json +16 -0
  63. package/registry/service-card.json +16 -0
  64. package/registry/service-grid.json +16 -0
  65. package/registry/slot-picker.json +16 -0
  66. package/registry/staff-picker.json +15 -0
  67. package/registry/store-nav.json +15 -0
  68. package/registry/variant-selector.json +15 -0
  69. package/dist/index-B_25cFc1.d.ts +0 -320
  70. package/dist/index-Cd0shhZU.d.mts +0 -320
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "product-page",
3
+ "title": "ProductPage",
4
+ "description": "Full product detail page with badges and related products.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "product-sheet",
8
+ "availability-badge",
9
+ "sale-badge",
10
+ "product-grid",
11
+ "cn"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "product-page.tsx",
16
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { ProductSheet } from \"@cimplify/sdk/react\";\nimport { AvailabilityBadge } from \"./availability-badge\";\nimport { SaleBadge } from \"./sale-badge\";\nimport { ProductGrid } from \"./product-grid\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ProductPageClassNames {\n root?: string;\n main?: string;\n badges?: string;\n relatedSection?: string;\n relatedTitle?: string;\n loading?: string;\n}\n\nexport interface ProductPageProps {\n /** Product slug or ID to display. */\n productId: string;\n /** Pre-fetched product for SSR. Skips client-side fetch when provided. */\n product?: ProductWithDetails;\n /** Pre-fetched related products for SSR. */\n relatedProducts?: Product[];\n /** Override add-to-cart behavior. */\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: { src: string; alt: string; className?: string }) => React.ReactNode;\n /** Show availability badge. Default: true. */\n showAvailability?: boolean;\n /** Show sale badge. Default: true. */\n showSaleBadge?: boolean;\n /** Show related products section. Default: true. */\n showRelated?: boolean;\n /** Related section title. */\n relatedTitle?: string;\n className?: string;\n classNames?: ProductPageClassNames;\n}\n\n/**\n * ProductPage — full product detail page with badges and related products.\n *\n * SSR-friendly: pass `product` prop for server rendering.\n * Composes ProductSheet, AvailabilityBadge, SaleBadge, and a related products grid.\n */\nexport function ProductPage({\n productId,\n product: productProp,\n relatedProducts,\n onAddToCart,\n renderImage,\n showAvailability = true,\n showSaleBadge = true,\n showRelated = true,\n relatedTitle = \"You might also like\",\n className,\n classNames,\n}: ProductPageProps): React.ReactElement {\n const { product: fetched, isLoading } = useProduct(productId, {\n enabled: !productProp,\n });\n const product = productProp ?? fetched;\n\n if (isLoading && !product) {\n return (\n <div\n data-cimplify-product-page\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n >\n <div data-cimplify-product-page-skeleton style={{ display: \"flex\", flexDirection: \"column\", gap: \"1rem\" }}>\n <div style={{ aspectRatio: \"4/3\", backgroundColor: \"rgba(0,0,0,0.06)\", borderRadius: \"0.5rem\" }} />\n <div style={{ height: \"2rem\", width: \"60%\", backgroundColor: \"rgba(0,0,0,0.06)\", borderRadius: \"0.25rem\" }} />\n <div style={{ height: \"1rem\", width: \"30%\", backgroundColor: \"rgba(0,0,0,0.06)\", borderRadius: \"0.25rem\" }} />\n </div>\n </div>\n );\n }\n\n if (!product) {\n return (\n <div data-cimplify-product-page className={cn(className, classNames?.root)}>\n <p>Product not found.</p>\n </div>\n );\n }\n\n return (\n <div data-cimplify-product-page className={cn(className, classNames?.root)}>\n {/* Badges */}\n {(showAvailability || showSaleBadge) && (\n <div data-cimplify-product-page-badges className={classNames?.badges}>\n {showAvailability && <AvailabilityBadge product={product} />}\n {showSaleBadge && <SaleBadge product={product} />}\n </div>\n )}\n\n {/* Main product content */}\n <div data-cimplify-product-page-main className={classNames?.main}>\n <ProductSheet\n product={product}\n onAddToCart={onAddToCart}\n renderImage={renderImage}\n />\n </div>\n\n {/* Related products */}\n {showRelated && relatedProducts && relatedProducts.length > 0 && (\n <div data-cimplify-product-page-related className={classNames?.relatedSection}>\n <h2 data-cimplify-product-page-related-title className={classNames?.relatedTitle}>\n {relatedTitle}\n </h2>\n <ProductGrid\n products={relatedProducts}\n renderImage={renderImage}\n />\n </div>\n )}\n </div>\n );\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "product-sheet",
3
+ "title": "ProductSheet",
4
+ "description": "Full product detail view with gallery, header, and customizer.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "price",
8
+ "product-image-gallery",
9
+ "product-customizer",
10
+ "cn"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "product-sheet.tsx",
15
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ProductSheetClassNames {\n root?: string;\n image?: string;\n header?: string;\n name?: string;\n price?: string;\n description?: string;\n customizer?: string;\n loading?: string;\n}\n\nexport interface ProductSheetProps {\n /** A slim Product (triggers lazy-fetch) or a fully-loaded ProductWithDetails. */\n product: Product | ProductWithDetails;\n /** Called when the sheet should close. */\n onClose?: () => void;\n /** Override the default add-to-cart behavior. */\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n className?: string;\n classNames?: ProductSheetClassNames;\n}\n\nfunction isProductWithDetails(\n product: Product | ProductWithDetails,\n): product is ProductWithDetails {\n return \"variants\" in product;\n}\n\n/**\n * ProductSheet — full product detail view composing gallery, header, and customizer.\n *\n * When given a slim `Product`, it lazy-fetches the full details via `useProduct`.\n * When given a `ProductWithDetails`, it skips the fetch entirely.\n */\nexport function ProductSheet({\n product,\n onClose,\n onAddToCart,\n renderImage,\n className,\n classNames,\n}: ProductSheetProps): React.ReactElement {\n const needsFetch = !isProductWithDetails(product);\n const { product: fetched, isLoading } = useProduct(\n product.slug ?? product.id,\n { enabled: needsFetch },\n );\n const fullProduct = needsFetch ? fetched : (product as ProductWithDetails);\n\n // Loading state\n if (isLoading && !fullProduct) {\n return (\n <div\n data-cimplify-product-sheet\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n >\n <div\n data-cimplify-product-sheet-skeleton\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"1rem\",\n }}\n >\n <div\n style={{\n aspectRatio: \"4/3\",\n backgroundColor: \"rgba(0,0,0,0.06)\",\n borderRadius: \"0.5rem\",\n }}\n />\n <div\n style={{\n height: \"1.5rem\",\n width: \"60%\",\n backgroundColor: \"rgba(0,0,0,0.06)\",\n borderRadius: \"0.25rem\",\n }}\n />\n <div\n style={{\n height: \"1rem\",\n width: \"30%\",\n backgroundColor: \"rgba(0,0,0,0.06)\",\n borderRadius: \"0.25rem\",\n }}\n />\n </div>\n </div>\n );\n }\n\n // Error state\n if (!fullProduct) {\n return (\n <div\n data-cimplify-product-sheet\n className={cn(className, classNames?.root)}\n >\n <p>Product not found.</p>\n </div>\n );\n }\n\n // Collect images\n const images: string[] = [];\n if (fullProduct.images && fullProduct.images.length > 0) {\n images.push(...fullProduct.images.filter(Boolean));\n } else if (fullProduct.image_url) {\n images.push(fullProduct.image_url);\n }\n\n const hasMultipleImages = images.length > 1;\n const singleImage = images[0];\n\n return (\n <div\n data-cimplify-product-sheet\n className={cn(className, classNames?.root)}\n style={{ display: \"flex\", flexDirection: \"column\", gap: \"1rem\" }}\n >\n {/* Image area */}\n {hasMultipleImages ? (\n <ProductImageGallery\n images={images}\n productName={fullProduct.name}\n className={classNames?.image}\n />\n ) : singleImage ? (\n <div data-cimplify-product-sheet-image className={classNames?.image}>\n {renderImage ? (\n renderImage({ src: singleImage, alt: fullProduct.name })\n ) : (\n <img\n src={singleImage}\n alt={fullProduct.name}\n style={{ width: \"100%\", height: \"auto\", objectFit: \"cover\" }}\n />\n )}\n </div>\n ) : null}\n\n {/* Header */}\n <div data-cimplify-product-sheet-header className={classNames?.header}>\n <h2\n data-cimplify-product-sheet-name\n className={classNames?.name}\n style={{ margin: 0 }}\n >\n {fullProduct.name}\n </h2>\n <Price\n amount={fullProduct.default_price}\n className={classNames?.price}\n />\n </div>\n\n {/* Description */}\n {fullProduct.description && (\n <p\n data-cimplify-product-sheet-description\n className={classNames?.description}\n style={{ margin: 0 }}\n >\n {fullProduct.description}\n </p>\n )}\n\n {/* Customizer */}\n <ProductCustomizer\n product={fullProduct}\n onAddToCart={onAddToCart}\n className={classNames?.customizer}\n />\n </div>\n );\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "quantity-selector",
3
+ "title": "QuantitySelector",
4
+ "description": "Controlled increment/decrement quantity input.",
5
+ "type": "component",
6
+ "registryDependencies": [],
7
+ "files": [
8
+ {
9
+ "path": "quantity-selector.tsx",
10
+ "content": "\"use client\";\n\nimport React from \"react\";\n\nexport interface QuantitySelectorProps {\n value: number;\n onChange: (value: number) => void;\n min?: number;\n max?: number;\n className?: string;\n}\n\n/**\n * QuantitySelector — controlled increment/decrement input.\n *\n * Pure presentation. Pass `value` and `onChange` to drive it.\n */\nexport function QuantitySelector({\n value,\n onChange,\n min = 1,\n max,\n className,\n}: QuantitySelectorProps): React.ReactElement {\n return (\n <div data-cimplify-quantity className={className} style={{ display: \"inline-flex\", alignItems: \"center\", gap: \"0.5rem\" }}>\n <button\n type=\"button\"\n onClick={() => onChange(Math.max(min, value - 1))}\n disabled={value <= min}\n aria-label=\"Decrease quantity\"\n data-cimplify-quantity-decrement\n >\n &#x2212;\n </button>\n <span data-cimplify-quantity-value aria-live=\"polite\">{value}</span>\n <button\n type=\"button\"\n onClick={() => onChange(max != null ? Math.min(max, value + 1) : value + 1)}\n disabled={max != null && value >= max}\n aria-label=\"Increase quantity\"\n data-cimplify-quantity-increment\n >\n +\n </button>\n </div>\n );\n}\n"
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "sale-badge",
3
+ "title": "SaleBadge",
4
+ "description": "Sale/discount indicator with percentage and original price.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "price",
8
+ "cn"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "sale-badge.tsx",
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product, ProductDealInfo } from \"@cimplify/sdk\";\nimport type { ProductWithPrice } from \"@cimplify/sdk\";\nimport {\n isOnSale,\n getDiscountPercentage,\n getBasePrice,\n parsePrice,\n} from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface SaleBadgeClassNames {\n root?: string;\n percentage?: string;\n label?: string;\n originalPrice?: string;\n}\n\nexport interface SaleBadgeProps {\n /** Product, optionally enriched with price_info for sale detection. */\n product: Product & Partial<ProductWithPrice>;\n /** Deal info from useProductDeals / useProductsOnSale. */\n dealInfo?: ProductDealInfo;\n /** Override badge text entirely. */\n label?: string;\n /** Show the original (pre-discount) price with strikethrough styling. */\n showOriginalPrice?: boolean;\n /** Show the percentage off. Default: true. */\n showPercentage?: boolean;\n className?: string;\n classNames?: SaleBadgeClassNames;\n}\n\n/**\n * SaleBadge — shows a sale/discount indicator for a product.\n *\n * Returns `null` when there's no deal, no sale price difference, and no label override,\n * so it's safe to render unconditionally — it simply won't show for non-sale products.\n */\nexport function SaleBadge({\n product,\n dealInfo,\n label,\n showOriginalPrice = false,\n showPercentage = true,\n className,\n classNames,\n}: SaleBadgeProps): React.ReactElement | null {\n const onSale = isOnSale(product);\n const hasDeal = dealInfo !== undefined;\n\n if (!hasDeal && !onSale && !label) {\n return null;\n }\n\n // Percentage: prefer dealInfo when it's a percentage benefit, else compute from prices\n let percentage: number | null = null;\n if (hasDeal && dealInfo.benefit_type === \"percentage\") {\n percentage = parsePrice(dealInfo.value);\n } else if (onSale) {\n percentage = getDiscountPercentage(product);\n }\n\n // Badge text: explicit label > deal label > computed \"X% off\"\n const badgeText =\n label ??\n dealInfo?.label ??\n (percentage != null && percentage > 0 ? `${percentage}% off` : null);\n\n if (!badgeText) {\n return null;\n }\n\n return (\n <span\n data-cimplify-sale-badge\n className={cn(className, classNames?.root)}\n style={{ display: \"inline-flex\", alignItems: \"center\", gap: \"0.375rem\" }}\n >\n {showPercentage && percentage != null && percentage > 0 && (\n <span data-cimplify-sale-percentage className={classNames?.percentage}>\n -{percentage}%\n </span>\n )}\n <span data-cimplify-sale-label className={classNames?.label}>\n {badgeText}\n </span>\n {showOriginalPrice && onSale && (\n <span data-cimplify-sale-original-price className={classNames?.originalPrice}>\n <Price amount={getBasePrice(product)} />\n </span>\n )}\n </span>\n );\n}\n"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "search-input",
3
+ "title": "SearchInput",
4
+ "description": "Search bar with debounced results dropdown.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "cn"
8
+ ],
9
+ "files": [
10
+ {
11
+ "path": "search-input.tsx",
12
+ "content": "\"use client\";\n\nimport React, { useCallback } from \"react\";\nimport { useSearch } from \"@cimplify/sdk/react\";\nimport type { UseSearchOptions } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface SearchInputClassNames {\n root?: string;\n input?: string;\n clearButton?: string;\n results?: string;\n resultItem?: string;\n empty?: string;\n loading?: string;\n}\n\nexport interface SearchInputProps {\n /** Placeholder text for the input. */\n placeholder?: string;\n /** Search options forwarded to useSearch. */\n searchOptions?: UseSearchOptions;\n /** Called when a product result is clicked. */\n onResultClick?: (product: import(\"../types/product\").Product) => void;\n /** Custom result item renderer. */\n renderResult?: (product: import(\"../types/product\").Product) => React.ReactNode;\n /** Show inline results dropdown. Default: true. */\n showResults?: boolean;\n className?: string;\n classNames?: SearchInputClassNames;\n}\n\n/**\n * SearchInput — search bar with debounced results dropdown.\n *\n * Wraps `useSearch` with a controlled input and optional inline results list.\n */\nexport function SearchInput({\n placeholder = \"Search products...\",\n searchOptions,\n onResultClick,\n renderResult,\n showResults = true,\n className,\n classNames,\n}: SearchInputProps): React.ReactElement {\n const { results, isLoading, query, setQuery, clear } = useSearch(searchOptions);\n\n const handleChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n setQuery(e.target.value);\n },\n [setQuery],\n );\n\n return (\n <div\n data-cimplify-search\n className={cn(className, classNames?.root)}\n style={{ position: \"relative\" }}\n >\n <input\n type=\"search\"\n value={query}\n onChange={handleChange}\n placeholder={placeholder}\n data-cimplify-search-input\n className={classNames?.input}\n aria-label=\"Search products\"\n />\n\n {query.length > 0 && (\n <button\n type=\"button\"\n onClick={clear}\n data-cimplify-search-clear\n className={classNames?.clearButton}\n aria-label=\"Clear search\"\n >\n &times;\n </button>\n )}\n\n {showResults && query.length > 0 && (\n <div data-cimplify-search-results className={classNames?.results}>\n {isLoading && (\n <div data-cimplify-search-loading className={classNames?.loading} aria-busy=\"true\">\n Searching...\n </div>\n )}\n\n {!isLoading && results.length === 0 && query.length >= 2 && (\n <div data-cimplify-search-empty className={classNames?.empty}>\n No results found\n </div>\n )}\n\n {!isLoading &&\n results.map((product) => (\n <button\n key={product.id}\n type=\"button\"\n onClick={() => onResultClick?.(product)}\n data-cimplify-search-result\n className={classNames?.resultItem}\n >\n {renderResult ? (\n renderResult(product)\n ) : (\n <>\n <span data-cimplify-search-result-name>{product.name}</span>\n </>\n )}\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n"
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "search-page",
3
+ "title": "SearchPage",
4
+ "description": "Dedicated search page with input and results grid.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "product-grid",
8
+ "cn"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "search-page.tsx",
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product } from \"@cimplify/sdk\";\nimport { useSearch } from \"@cimplify/sdk/react\";\nimport { ProductGrid } from \"./product-grid\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface SearchPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n inputContainer?: string;\n input?: string;\n clearButton?: string;\n resultCount?: string;\n grid?: string;\n empty?: string;\n loading?: string;\n}\n\nexport interface SearchPageProps {\n /** Page title. */\n title?: string;\n /** Placeholder text. */\n placeholder?: string;\n /** Called when a product is clicked in results. */\n onProductClick?: (product: Product) => void;\n /** Custom card renderer. */\n renderCard?: (product: Product) => React.ReactNode;\n /** Custom image renderer. */\n renderImage?: (props: { src: string; alt: string; className?: string }) => React.ReactNode;\n /** Grid column config. */\n columns?: { sm?: number; md?: number; lg?: number; xl?: number };\n className?: string;\n classNames?: SearchPageClassNames;\n}\n\n/**\n * SearchPage — dedicated search page with input and results grid.\n *\n * Uses `useSearch` for debounced full-text search.\n */\nexport function SearchPage({\n title = \"Search\",\n placeholder = \"What are you looking for?\",\n onProductClick,\n renderCard,\n renderImage,\n columns,\n className,\n classNames,\n}: SearchPageProps): React.ReactElement {\n const { results, isLoading, query, setQuery, clear } = useSearch();\n\n return (\n <div data-cimplify-search-page className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-search-page-header className={classNames?.header}>\n <h1 data-cimplify-search-page-title className={classNames?.title}>\n {title}\n </h1>\n </div>\n\n {/* Search input */}\n <div data-cimplify-search-page-input className={classNames?.inputContainer}>\n <input\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={placeholder}\n data-cimplify-search-page-field\n className={classNames?.input}\n aria-label=\"Search products\"\n autoFocus\n />\n {query.length > 0 && (\n <button\n type=\"button\"\n onClick={clear}\n data-cimplify-search-page-clear\n className={classNames?.clearButton}\n aria-label=\"Clear search\"\n >\n &times;\n </button>\n )}\n </div>\n\n {/* Result count */}\n {query.length >= 2 && !isLoading && (\n <div data-cimplify-search-page-count className={classNames?.resultCount}>\n {results.length} {results.length === 1 ? \"result\" : \"results\"} for &ldquo;{query}&rdquo;\n </div>\n )}\n\n {/* Results */}\n <div data-cimplify-search-page-grid className={classNames?.grid}>\n {isLoading ? (\n <div data-cimplify-search-page-loading aria-busy=\"true\" className={classNames?.loading}>\n Searching...\n </div>\n ) : query.length >= 2 && results.length === 0 ? (\n <div data-cimplify-search-page-empty className={classNames?.empty}>\n <p>No products found for &ldquo;{query}&rdquo;</p>\n </div>\n ) : (\n results.length > 0 && (\n <ProductGrid\n products={results}\n renderCard={renderCard}\n renderImage={renderImage}\n columns={columns}\n />\n )\n )}\n </div>\n </div>\n );\n}\n"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "service-card",
3
+ "title": "ServiceCard",
4
+ "description": "Service display card with image, duration, price, and book action.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "price",
8
+ "cn"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "service-card.tsx",
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Service } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ServiceCardClassNames {\n root?: string;\n imageContainer?: string;\n image?: string;\n body?: string;\n name?: string;\n description?: string;\n meta?: string;\n duration?: string;\n price?: string;\n action?: string;\n}\n\nexport interface ServiceCardProps {\n /** The service to display. */\n service: Service;\n /** Called when the card or book button is clicked. */\n onBook?: (service: Service) => void;\n /** Link href for page mode. Renders as `<a>` instead of `<button>`. */\n href?: string;\n /** Label for the action button. Default: \"Book\". */\n actionLabel?: string;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n /** Replace the entire default card body. */\n children?: React.ReactNode;\n /** Image aspect ratio. Default: \"4/3\". */\n aspectRatio?: \"square\" | \"4/3\" | \"16/10\" | \"3/4\";\n className?: string;\n classNames?: ServiceCardClassNames;\n}\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\nexport function ServiceCard({\n service,\n onBook,\n href,\n actionLabel = \"Book\",\n renderImage,\n children,\n aspectRatio = \"4/3\",\n className,\n classNames,\n}: ServiceCardProps): React.ReactElement {\n const imageUrl = service.image_url;\n\n const cardBody = children ?? (\n <>\n {imageUrl && (\n <div\n data-cimplify-service-card-image-container\n className={classNames?.imageContainer}\n style={{\n overflow: \"hidden\",\n ...ASPECT_STYLES[aspectRatio],\n }}\n >\n {renderImage ? (\n renderImage({\n src: imageUrl,\n alt: service.name,\n className: classNames?.image,\n })\n ) : (\n <img\n src={imageUrl}\n alt={service.name}\n className={classNames?.image}\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n data-cimplify-service-card-image\n />\n )}\n </div>\n )}\n\n <div data-cimplify-service-card-body className={classNames?.body}>\n <span data-cimplify-service-card-name className={classNames?.name}>\n {service.name}\n </span>\n {service.description && (\n <span\n data-cimplify-service-card-description\n className={classNames?.description}\n style={{\n display: \"-webkit-box\",\n WebkitLineClamp: 2,\n WebkitBoxOrient: \"vertical\",\n overflow: \"hidden\",\n }}\n >\n {service.description}\n </span>\n )}\n <div data-cimplify-service-card-meta className={classNames?.meta}>\n <span data-cimplify-service-card-duration className={classNames?.duration}>\n {service.duration_minutes} min\n </span>\n {service.price && (\n <span data-cimplify-service-card-price className={classNames?.price}>\n <Price amount={service.price} />\n </span>\n )}\n </div>\n </div>\n\n {!href && onBook && (\n <span data-cimplify-service-card-action className={classNames?.action}>\n {actionLabel}\n </span>\n )}\n </>\n );\n\n if (href) {\n return (\n <a\n href={href}\n data-cimplify-service-card\n data-available={service.is_available || undefined}\n className={cn(className, classNames?.root)}\n style={{ display: \"block\", textDecoration: \"none\", color: \"inherit\" }}\n >\n {cardBody}\n </a>\n );\n }\n\n return (\n <button\n type=\"button\"\n onClick={() => onBook?.(service)}\n disabled={!service.is_available}\n data-cimplify-service-card\n data-available={service.is_available || undefined}\n className={cn(className, classNames?.root)}\n style={{\n display: \"block\",\n width: \"100%\",\n textAlign: \"inherit\",\n background: \"none\",\n border: \"none\",\n padding: 0,\n cursor: service.is_available ? \"pointer\" : \"default\",\n font: \"inherit\",\n color: \"inherit\",\n }}\n >\n {cardBody}\n </button>\n );\n}\n"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "service-grid",
3
+ "title": "ServiceGrid",
4
+ "description": "Responsive grid of bookable services with self-fetching.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "service-card",
8
+ "cn"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "service-grid.tsx",
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Service } from \"@cimplify/sdk\";\nimport { useServices } from \"@cimplify/sdk/react\";\nimport { ServiceCard } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ServiceGridClassNames {\n root?: string;\n item?: string;\n empty?: string;\n loading?: string;\n}\n\nexport interface ServiceGridProps {\n /** Pre-fetched services (skips fetch). */\n services?: Service[];\n /** Responsive column counts at each breakpoint. */\n columns?: { sm?: number; md?: number; lg?: number; xl?: number };\n /** Called when a service card is clicked. */\n onServiceSelect?: (service: Service) => void;\n /** Generate href for each service (renders cards as links). */\n getServiceHref?: (service: Service) => string;\n /** Custom card renderer per service. */\n renderCard?: (service: Service) => React.ReactNode;\n /** Custom image renderer passed to default ServiceCards. */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n /** Only show available services. Default: true. */\n availableOnly?: boolean;\n /** Label for the book action on each card. */\n actionLabel?: string;\n /** Text shown when empty. */\n emptyMessage?: string;\n className?: string;\n classNames?: ServiceGridClassNames;\n}\n\nexport function ServiceGrid({\n services: servicesProp,\n columns,\n onServiceSelect,\n getServiceHref,\n renderCard,\n renderImage,\n availableOnly = true,\n actionLabel,\n emptyMessage = \"No services available\",\n className,\n classNames,\n}: ServiceGridProps): React.ReactElement {\n const rawId = React.useId();\n const gridId = `cimplify-service-grid-${rawId.replace(/:/g, \"\")}`;\n\n const { services: fetched, isLoading } = useServices({\n enabled: servicesProp === undefined,\n });\n\n const allServices = servicesProp ?? fetched;\n const services = availableOnly\n ? allServices.filter((s) => s.is_available)\n : allServices;\n\n const sm = columns?.sm ?? 1;\n const md = columns?.md ?? 2;\n const lg = columns?.lg ?? 3;\n const xl = columns?.xl ?? 4;\n\n if (isLoading && services.length === 0) {\n return (\n <div\n data-cimplify-service-grid\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (services.length === 0) {\n return (\n <div\n data-cimplify-service-grid\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage}</p>\n </div>\n );\n }\n\n const css = [\n `#${gridId}{display:grid;grid-template-columns:repeat(${sm},1fr);gap:1rem}`,\n `@media(min-width:768px){#${gridId}{grid-template-columns:repeat(${md},1fr)}}`,\n `@media(min-width:1024px){#${gridId}{grid-template-columns:repeat(${lg},1fr)}}`,\n `@media(min-width:1280px){#${gridId}{grid-template-columns:repeat(${xl},1fr)}}`,\n ].join(\"\");\n\n return (\n <>\n <style dangerouslySetInnerHTML={{ __html: css }} />\n <div\n id={gridId}\n data-cimplify-service-grid\n className={cn(className, classNames?.root)}\n >\n {services.map((service) => (\n <div\n key={service.id}\n data-cimplify-service-grid-item\n className={classNames?.item}\n >\n {renderCard ? (\n renderCard(service)\n ) : (\n <ServiceCard\n service={service}\n onBook={onServiceSelect}\n href={getServiceHref?.(service)}\n actionLabel={actionLabel}\n renderImage={renderImage}\n />\n )}\n </div>\n ))}\n </div>\n </>\n );\n}\n"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "slot-picker",
3
+ "title": "SlotPicker",
4
+ "description": "Time slot grid for a single day with morning/afternoon/evening grouping.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "price",
8
+ "cn"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "slot-picker.tsx",
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\nimport { useAvailableSlots } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface SlotPickerClassNames {\n root?: string;\n group?: string;\n groupLabel?: string;\n slot?: string;\n slotTime?: string;\n slotPrice?: string;\n loading?: string;\n empty?: string;\n}\n\nexport interface SlotPickerProps {\n /** Pre-fetched slots (skips fetch). */\n slots?: AvailableSlot[];\n /** Service ID — used to fetch slots when `slots` prop is not provided. */\n serviceId?: string;\n /** Date string (YYYY-MM-DD) — used to fetch slots when `slots` prop is not provided. */\n date?: string;\n /** Number of participants for capacity-based availability. */\n participantCount?: number;\n /** Currently selected slot. */\n selectedSlot?: AvailableSlot | null;\n /** Called when a slot is selected. */\n onSlotSelect?: (slot: AvailableSlot) => void;\n /** Whether to group slots by time of day. Default: true. */\n groupByTimeOfDay?: boolean;\n /** Show price on each slot. Default: true. */\n showPrice?: boolean;\n /** Text shown when no slots available. */\n emptyMessage?: string;\n className?: string;\n classNames?: SlotPickerClassNames;\n}\n\ninterface SlotGroup {\n label: string;\n slots: AvailableSlot[];\n}\n\nfunction getTimeOfDay(timeStr: string): \"morning\" | \"afternoon\" | \"evening\" {\n const hour = parseInt(timeStr.split(\"T\").pop()?.split(\":\")[0] ?? timeStr.split(\":\")[0], 10);\n if (hour < 12) return \"morning\";\n if (hour < 17) return \"afternoon\";\n return \"evening\";\n}\n\nconst TIME_OF_DAY_LABELS: Record<string, string> = {\n morning: \"Morning\",\n afternoon: \"Afternoon\",\n evening: \"Evening\",\n};\n\nfunction groupSlots(slots: AvailableSlot[]): SlotGroup[] {\n const groups: Record<string, AvailableSlot[]> = {};\n for (const slot of slots) {\n const tod = getTimeOfDay(slot.start_time);\n if (!groups[tod]) groups[tod] = [];\n groups[tod].push(slot);\n }\n return ([\"morning\", \"afternoon\", \"evening\"] as const)\n .filter((tod) => groups[tod]?.length)\n .map((tod) => ({ label: TIME_OF_DAY_LABELS[tod], slots: groups[tod] }));\n}\n\nfunction formatTime(timeStr: string): string {\n try {\n const date = new Date(timeStr);\n if (!isNaN(date.getTime())) {\n return date.toLocaleTimeString(undefined, { hour: \"numeric\", minute: \"2-digit\" });\n }\n } catch {\n // noop\n }\n\n const parts = timeStr.split(\":\");\n if (parts.length >= 2) {\n const hour = parseInt(parts[0], 10);\n const minute = parts[1];\n const ampm = hour >= 12 ? \"PM\" : \"AM\";\n const displayHour = hour % 12 || 12;\n return `${displayHour}:${minute} ${ampm}`;\n }\n return timeStr;\n}\n\nexport function SlotPicker({\n slots: slotsProp,\n serviceId,\n date,\n participantCount,\n selectedSlot,\n onSlotSelect,\n groupByTimeOfDay = true,\n showPrice = true,\n emptyMessage = \"No available slots\",\n className,\n classNames,\n}: SlotPickerProps): React.ReactElement {\n const { slots: fetched, isLoading } = useAvailableSlots(\n serviceId ?? null,\n date ?? null,\n {\n participantCount,\n enabled: slotsProp === undefined && !!serviceId && !!date,\n },\n );\n\n const slots = slotsProp ?? fetched;\n\n if (isLoading && slots.length === 0) {\n return (\n <div\n data-cimplify-slot-picker\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (slots.length === 0) {\n return (\n <div\n data-cimplify-slot-picker\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage}</p>\n </div>\n );\n }\n\n const groups = groupByTimeOfDay ? groupSlots(slots) : [{ label: \"\", slots }];\n\n return (\n <div data-cimplify-slot-picker className={cn(className, classNames?.root)}>\n {groups.map((group) => (\n <div key={group.label || \"all\"} data-cimplify-slot-group className={classNames?.group}>\n {group.label && (\n <div data-cimplify-slot-group-label className={classNames?.groupLabel}>\n {group.label}\n </div>\n )}\n {group.slots.map((slot) => {\n const isSelected =\n selectedSlot?.start_time === slot.start_time &&\n selectedSlot?.end_time === slot.end_time;\n return (\n <button\n key={`${slot.start_time}-${slot.end_time}`}\n type=\"button\"\n disabled={!slot.is_available}\n onClick={() => slot.is_available && onSlotSelect?.(slot)}\n data-cimplify-slot\n data-selected={isSelected || undefined}\n data-unavailable={!slot.is_available || undefined}\n className={classNames?.slot}\n >\n <span data-cimplify-slot-time className={classNames?.slotTime}>\n {formatTime(slot.start_time)}\n </span>\n {showPrice && slot.price && (\n <span data-cimplify-slot-price className={classNames?.slotPrice}>\n <Price amount={slot.price} />\n </span>\n )}\n </button>\n );\n })}\n </div>\n ))}\n </div>\n );\n}\n"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "staff-picker",
3
+ "title": "StaffPicker",
4
+ "description": "Staff member selection list with avatar and bio.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "cn"
8
+ ],
9
+ "files": [
10
+ {
11
+ "path": "staff-picker.tsx",
12
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Staff } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface StaffPickerClassNames {\n root?: string;\n option?: string;\n avatar?: string;\n name?: string;\n bio?: string;\n}\n\nexport interface StaffPickerProps {\n /** List of available staff members. */\n staff: Staff[];\n /** Currently selected staff ID, or null for \"Any available\". */\n selectedStaffId?: string | null;\n /** Called when a staff member is selected. Passes null for \"Any available\". */\n onStaffSelect?: (staffId: string | null) => void;\n /** Show \"Any available\" option. Default: true. */\n showAnyOption?: boolean;\n /** Label for the \"Any available\" option. */\n anyLabel?: string;\n className?: string;\n classNames?: StaffPickerClassNames;\n}\n\nexport function StaffPicker({\n staff,\n selectedStaffId,\n onStaffSelect,\n showAnyOption = true,\n anyLabel = \"Any available\",\n className,\n classNames,\n}: StaffPickerProps): React.ReactElement {\n return (\n <div data-cimplify-staff-picker className={cn(className, classNames?.root)}>\n {showAnyOption && (\n <button\n type=\"button\"\n onClick={() => onStaffSelect?.(null)}\n data-cimplify-staff-option\n data-selected={selectedStaffId === null || undefined}\n data-any\n className={classNames?.option}\n >\n <span data-cimplify-staff-name className={classNames?.name}>\n {anyLabel}\n </span>\n </button>\n )}\n {staff.map((member) => (\n <button\n key={member.id}\n type=\"button\"\n onClick={() => onStaffSelect?.(member.id)}\n data-cimplify-staff-option\n data-selected={selectedStaffId === member.id || undefined}\n className={classNames?.option}\n >\n {member.avatar_url && (\n <img\n src={member.avatar_url}\n alt={member.name}\n data-cimplify-staff-avatar\n className={classNames?.avatar}\n />\n )}\n <span data-cimplify-staff-name className={classNames?.name}>\n {member.name}\n </span>\n {member.bio && (\n <span data-cimplify-staff-bio className={classNames?.bio}>\n {member.bio}\n </span>\n )}\n </button>\n ))}\n </div>\n );\n}\n"
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "store-nav",
3
+ "title": "StoreNav",
4
+ "description": "Top navigation bar with brand, categories, cart badge, and search.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "cn"
8
+ ],
9
+ "files": [
10
+ {
11
+ "path": "store-nav.tsx",
12
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport { useCart, useCategories } from \"@cimplify/sdk/react\";\nimport type { Category } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface StoreNavClassNames {\n root?: string;\n brand?: string;\n categories?: string;\n categoryLink?: string;\n actions?: string;\n cartButton?: string;\n cartCount?: string;\n searchButton?: string;\n}\n\nexport interface StoreNavProps {\n /** Store/brand name. */\n storeName?: string;\n /** Custom brand element (logo, etc.). Overrides storeName. */\n renderBrand?: () => React.ReactNode;\n /** Override categories (skips fetch). */\n categories?: Category[];\n /** Called when a category link is clicked. */\n onCategoryClick?: (category: Category) => void;\n /** Called when the cart button is clicked. */\n onCartClick?: () => void;\n /** Called when the search button is clicked. */\n onSearchClick?: () => void;\n /** Hide category navigation. */\n hideCategories?: boolean;\n /** Hide the cart button. */\n hideCart?: boolean;\n /** Hide the search button. */\n hideSearch?: boolean;\n className?: string;\n classNames?: StoreNavClassNames;\n}\n\n/**\n * StoreNav — top navigation bar with brand, category links, cart badge, and search.\n *\n * Fetches categories via `useCategories` and cart count via `useCart`.\n * Renders as a semantic `<nav>` element.\n */\nexport function StoreNav({\n storeName,\n renderBrand,\n categories: categoriesProp,\n onCategoryClick,\n onCartClick,\n onSearchClick,\n hideCategories = false,\n hideCart = false,\n hideSearch = false,\n className,\n classNames,\n}: StoreNavProps): React.ReactElement {\n const { categories: fetched } = useCategories({\n enabled: !hideCategories && categoriesProp === undefined,\n });\n const { itemCount } = useCart();\n\n const categories = categoriesProp ?? fetched;\n\n return (\n <nav data-cimplify-store-nav className={cn(className, classNames?.root)}>\n {/* Brand */}\n <div data-cimplify-store-nav-brand className={classNames?.brand}>\n {renderBrand ? renderBrand() : storeName && <span>{storeName}</span>}\n </div>\n\n {/* Category links */}\n {!hideCategories && categories.length > 0 && (\n <div data-cimplify-store-nav-categories className={classNames?.categories}>\n {categories.map((category: Category) => (\n <button\n key={category.id}\n type=\"button\"\n onClick={() => onCategoryClick?.(category)}\n data-cimplify-store-nav-category\n className={classNames?.categoryLink}\n >\n {category.name}\n </button>\n ))}\n </div>\n )}\n\n {/* Actions */}\n <div data-cimplify-store-nav-actions className={classNames?.actions}>\n {!hideSearch && (\n <button\n type=\"button\"\n onClick={onSearchClick}\n data-cimplify-store-nav-search\n className={classNames?.searchButton}\n aria-label=\"Search\"\n >\n Search\n </button>\n )}\n\n {!hideCart && (\n <button\n type=\"button\"\n onClick={onCartClick}\n data-cimplify-store-nav-cart\n className={classNames?.cartButton}\n aria-label={`Cart (${itemCount} items)`}\n >\n Cart\n {itemCount > 0 && (\n <span data-cimplify-store-nav-cart-count className={classNames?.cartCount}>\n {itemCount}\n </span>\n )}\n </button>\n )}\n </div>\n </nav>\n );\n}\n"
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "variant-selector",
3
+ "title": "VariantSelector",
4
+ "description": "Select product variants via axis chips or direct list.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "price"
8
+ ],
9
+ "files": [
10
+ {
11
+ "path": "variant-selector.tsx",
12
+ "content": "\"use client\";\n\nimport React, { useState, useEffect, useRef } from \"react\";\nimport type { ProductVariant, VariantAxisWithValues } from \"@cimplify/sdk\";\nimport type { Money } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { getVariantDisplayName } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\n\nexport interface VariantSelectorProps {\n variants: ProductVariant[];\n variantAxes?: VariantAxisWithValues[];\n basePrice?: Money;\n selectedVariantId?: string;\n onVariantChange: (variantId: string | undefined, variant: ProductVariant | undefined) => void;\n productName?: string;\n className?: string;\n}\n\n/**\n * VariantSelector — select product variants via axis chips or direct list.\n *\n * Axis mode: one row of chips per axis (e.g. Size, Temperature).\n * Direct mode: a vertical list showing variant name + full effective price.\n */\nexport function VariantSelector({\n variants,\n variantAxes,\n basePrice,\n selectedVariantId,\n onVariantChange,\n productName,\n className,\n}: VariantSelectorProps): React.ReactElement | null {\n const [axisSelections, setAxisSelections] = useState<Record<string, string>>({});\n const initialized = useRef(false);\n\n useEffect(() => {\n initialized.current = false;\n }, [variants]);\n\n useEffect(() => {\n if (initialized.current) return;\n if (!variants || variants.length === 0) return;\n\n const defaultVariant = variants.find((v) => v.is_default) || variants[0];\n if (!defaultVariant) return;\n\n initialized.current = true;\n onVariantChange(defaultVariant.id, defaultVariant);\n\n if (defaultVariant.display_attributes) {\n const initial: Record<string, string> = {};\n for (const attr of defaultVariant.display_attributes) {\n initial[attr.axis_id] = attr.value_id;\n }\n setAxisSelections(initial);\n }\n }, [variants, onVariantChange]);\n\n useEffect(() => {\n if (!initialized.current) return;\n if (!variantAxes || variantAxes.length === 0) return;\n\n const match = variants.find((v) => {\n if (!v.display_attributes) return false;\n return v.display_attributes.every(\n (attr) => axisSelections[attr.axis_id] === attr.value_id,\n );\n });\n\n if (match && match.id !== selectedVariantId) {\n onVariantChange(match.id, match);\n }\n }, [axisSelections, variants, variantAxes, selectedVariantId, onVariantChange]);\n\n if (!variants || variants.length <= 1) {\n return null;\n }\n\n const basePriceNum = basePrice != null ? parsePrice(basePrice) : 0;\n\n // Axis-based selection\n if (variantAxes && variantAxes.length > 0) {\n return (\n <div data-cimplify-variant-selector className={className}>\n {variantAxes.map((axis) => (\n <div key={axis.id} data-cimplify-variant-axis>\n <label data-cimplify-variant-axis-label>{axis.name}</label>\n <div data-cimplify-variant-axis-options>\n {axis.values.map((value) => {\n const isSelected = axisSelections[axis.id] === value.id;\n return (\n <button\n key={value.id}\n type=\"button\"\n aria-selected={isSelected}\n onClick={() => {\n setAxisSelections((prev) => ({\n ...prev,\n [axis.id]: value.id,\n }));\n }}\n data-cimplify-variant-option\n data-selected={isSelected || undefined}\n >\n {value.name}\n </button>\n );\n })}\n </div>\n </div>\n ))}\n </div>\n );\n }\n\n // Direct variant list\n return (\n <div data-cimplify-variant-selector className={className}>\n <label data-cimplify-variant-list-label>Options</label>\n <div data-cimplify-variant-list>\n {variants.map((variant) => {\n const isSelected = selectedVariantId === variant.id;\n const adjustment = parsePrice(variant.price_adjustment);\n const effectivePrice = basePriceNum + adjustment;\n\n return (\n <button\n key={variant.id}\n type=\"button\"\n aria-selected={isSelected}\n onClick={() => onVariantChange(variant.id, variant)}\n data-cimplify-variant-option\n data-selected={isSelected || undefined}\n >\n <span data-cimplify-variant-name>{getVariantDisplayName(variant, productName)}</span>\n <span data-cimplify-variant-pricing>\n {adjustment !== 0 && (\n <span data-cimplify-variant-adjustment>\n {adjustment > 0 ? \"+\" : \"\"}\n <Price amount={variant.price_adjustment} />\n </span>\n )}\n <Price amount={effectivePrice} />\n </span>\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n"
13
+ }
14
+ ]
15
+ }
@@ -1,320 +0,0 @@
1
- import { av as ChosenPrice, C as CurrencyCode, as as TaxPathComponent, at as PricePathTaxInfo, M as Money, bd as PaymentErrorDetails, bb as PaymentResponse, bc as PaymentStatusResponse } from './payment-CTalZM5l.js';
2
-
3
- /**
4
- * Price Types
5
- *
6
- * Types for price parsing, formatting, and display utilities.
7
- */
8
-
9
- /**
10
- * Individual tax component (e.g., VAT, NHIL, GETFund)
11
- * @deprecated Use `TaxPathComponent` from `types/cart` instead.
12
- */
13
- type TaxComponent = TaxPathComponent;
14
- /**
15
- * Complete tax information from a pricing response
16
- * @deprecated Use `PricePathTaxInfo` from `types/cart` instead.
17
- */
18
- type TaxInfo = PricePathTaxInfo;
19
- /**
20
- * Price information in snake_case format (as returned from backend)
21
- * Used by components that work with raw API responses
22
- * @deprecated Use `ChosenPrice` from `types/cart` instead.
23
- */
24
- type PriceInfo = ChosenPrice;
25
- /**
26
- * Minimal product shape for price utilities.
27
- * Uses quote-aware `price_info` and plain numeric fallback fields.
28
- */
29
- interface ProductWithPrice {
30
- /** Pre-parsed price info from backend */
31
- price_info?: ChosenPrice;
32
- /** Final computed price in plain field form (if provided by API) */
33
- final_price?: number | string | null;
34
- /** Base/original price in plain field form */
35
- base_price?: number | string | null;
36
- /** Default/indicative price in plain field form */
37
- default_price?: number | string | null;
38
- /** Currency in plain field form */
39
- currency?: CurrencyCode | null;
40
- }
41
- /**
42
- * Options for price formatting functions
43
- */
44
- interface FormatPriceOptions {
45
- /** Currency code (default: "GHS") */
46
- currency?: CurrencyCode;
47
- /** Locale for Intl.NumberFormat (default: "en-US") */
48
- locale?: string;
49
- /** Minimum fraction digits (default: 2) */
50
- minimumFractionDigits?: number;
51
- /** Maximum fraction digits (default: 2) */
52
- maximumFractionDigits?: number;
53
- }
54
- /**
55
- * Options for compact price formatting
56
- */
57
- interface FormatCompactOptions {
58
- /** Currency code (default: "GHS") */
59
- currency?: CurrencyCode;
60
- /** Number of decimal places for compact notation (default: 1) */
61
- decimals?: number;
62
- }
63
-
64
- /**
65
- * Price Utilities
66
- *
67
- * Comprehensive utilities for parsing, formatting, and displaying prices.
68
- * Handles quote-aware pricing fields, currency formatting, and product price helpers.
69
- *
70
- * @example
71
- * ```typescript
72
- * import {
73
- * formatPrice,
74
- * formatPriceCompact,
75
- * isOnSale,
76
- * getDiscountPercentage
77
- * } from '@cimplify/sdk';
78
- *
79
- * // Format prices
80
- * formatPrice(29.99, 'USD'); // "$29.99"
81
- * formatPrice(29.99, 'GHS'); // "GH₵29.99"
82
- * formatPriceCompact(1500000, 'USD'); // "$1.5M"
83
- *
84
- * // Check for discounts
85
- * if (isOnSale(product)) {
86
- * console.log(`${getDiscountPercentage(product)}% off!`);
87
- * }
88
- * ```
89
- */
90
-
91
- /**
92
- * Currency code to symbol mapping
93
- * Includes major world currencies and African currencies
94
- */
95
- declare const CURRENCY_SYMBOLS: Record<string, string>;
96
- /**
97
- * Get currency symbol for a currency code
98
- * @param currencyCode - ISO 4217 currency code (e.g., "USD", "GHS")
99
- * @returns Currency symbol or the code itself if not found
100
- *
101
- * @example
102
- * getCurrencySymbol('USD') // "$"
103
- * getCurrencySymbol('GHS') // "GH₵"
104
- * getCurrencySymbol('XYZ') // "XYZ"
105
- */
106
- declare function getCurrencySymbol(currencyCode: CurrencyCode): string;
107
- /**
108
- * Format a number compactly with K/M/B suffixes
109
- * @param value - Number to format
110
- * @param decimals - Decimal places (default: 1)
111
- * @returns Compact string representation
112
- *
113
- * @example
114
- * formatNumberCompact(1234) // "1.2K"
115
- * formatNumberCompact(1500000) // "1.5M"
116
- * formatNumberCompact(2500000000) // "2.5B"
117
- */
118
- declare function formatNumberCompact(value: number, decimals?: number): string;
119
- /**
120
- * Format a price with locale-aware currency formatting
121
- * Uses Intl.NumberFormat for proper localization
122
- *
123
- * @param amount - Price amount (number or string)
124
- * @param currency - ISO 4217 currency code (default: "GHS")
125
- * @param locale - BCP 47 locale string (default: "en-US")
126
- * @returns Formatted price string
127
- *
128
- * @example
129
- * formatPrice(29.99, 'USD') // "$29.99"
130
- * formatPrice(29.99, 'GHS') // "GH₵29.99"
131
- * formatPrice('29.99', 'EUR') // "€29.99"
132
- * formatPrice(1234.56, 'USD', 'de-DE') // "1.234,56 $"
133
- */
134
- declare function formatPrice(amount: number | Money, currency?: CurrencyCode, locale?: string): string;
135
- /**
136
- * Format a price with +/- sign for adjustments
137
- * Useful for showing price changes, modifiers, or discounts
138
- *
139
- * @param amount - Adjustment amount (positive or negative)
140
- * @param currency - ISO 4217 currency code (default: "GHS")
141
- * @param locale - BCP 47 locale string (default: "en-US")
142
- * @returns Formatted adjustment string with sign
143
- *
144
- * @example
145
- * formatPriceAdjustment(5.00, 'USD') // "+$5.00"
146
- * formatPriceAdjustment(-3.50, 'GHS') // "-GH₵3.50"
147
- * formatPriceAdjustment(0, 'EUR') // "€0.00"
148
- */
149
- declare function formatPriceAdjustment(amount: number, currency?: CurrencyCode, locale?: string): string;
150
- /**
151
- * Format a price compactly for large numbers
152
- * Uses K/M/B suffixes for thousands, millions, billions
153
- *
154
- * @param amount - Price amount (number or string)
155
- * @param currency - ISO 4217 currency code (default: "GHS")
156
- * @param decimals - Decimal places for compact notation (default: 1)
157
- * @returns Compact formatted price
158
- *
159
- * @example
160
- * formatPriceCompact(999, 'USD') // "$999.00"
161
- * formatPriceCompact(1500, 'GHS') // "GH₵1.5K"
162
- * formatPriceCompact(2500000, 'USD') // "$2.5M"
163
- * formatPriceCompact(1200000000, 'EUR') // "€1.2B"
164
- */
165
- declare function formatPriceCompact(amount: number | Money, currency?: CurrencyCode, decimals?: number): string;
166
- /**
167
- * Simple currency symbol + amount format
168
- * Lighter alternative to formatPrice without Intl
169
- *
170
- * @param amount - Price amount (number or string)
171
- * @param currency - ISO 4217 currency code (default: "GHS")
172
- * @returns Simple formatted price
173
- *
174
- * @example
175
- * formatMoney(29.99, 'USD') // "$29.99"
176
- * formatMoney('15.00', 'GHS') // "GH₵15.00"
177
- */
178
- declare function formatMoney(amount: Money | number, currency?: CurrencyCode): string;
179
- /**
180
- * Parse a price string or number to a numeric value
181
- * Handles various input formats gracefully
182
- *
183
- * @param value - Value to parse (string, number, or undefined)
184
- * @returns Parsed numeric value, or 0 if invalid
185
- *
186
- * @example
187
- * parsePrice('29.99') // 29.99
188
- * parsePrice(29.99) // 29.99
189
- * parsePrice('$29.99') // 29.99 (strips non-numeric prefix)
190
- * parsePrice(undefined) // 0
191
- * parsePrice('invalid') // 0
192
- */
193
- declare function parsePrice(value: Money | string | number | undefined | null): number;
194
- /** Check whether tax info exists on the selected price payload. */
195
- declare function hasTaxInfo(priceInfo: {
196
- tax_info?: PricePathTaxInfo;
197
- }): boolean;
198
- /** Get tax amount from a selected price payload, defaulting to 0 when tax info is absent. */
199
- declare function getTaxAmount(priceInfo: {
200
- tax_info?: PricePathTaxInfo;
201
- }): number;
202
- /** Check whether the selected price is tax-inclusive. */
203
- declare function isTaxInclusive(priceInfo: {
204
- tax_info?: PricePathTaxInfo;
205
- }): boolean;
206
- /**
207
- * Format a final price with tax annotation.
208
- * Examples: "$10.50 (incl. tax)" or "$10.50 + $1.05 tax"
209
- */
210
- declare function formatPriceWithTax(priceInfo: {
211
- final_price: Money;
212
- tax_info?: PricePathTaxInfo;
213
- }, currency?: CurrencyCode): string;
214
- /**
215
- * Get the display price from a product.
216
- * Prefers quote-aware price_info, then plain price fields.
217
- *
218
- * @param product - Product with price data
219
- * @returns The final price to display
220
- *
221
- * @example
222
- * const price = getDisplayPrice(product);
223
- * console.log(formatPrice(price, 'GHS')); // "GH₵29.99"
224
- */
225
- declare function getDisplayPrice(product: ProductWithPrice): number;
226
- /**
227
- * Get the base price from a product (before markup/discount)
228
- *
229
- * @param product - Product with price data
230
- * @returns The base price before adjustments
231
- */
232
- declare function getBasePrice(product: ProductWithPrice): number;
233
- /**
234
- * Check if a product is on sale (discounted)
235
- *
236
- * @param product - Product with price data
237
- * @returns True if the final price is less than the base price
238
- *
239
- * @example
240
- * if (isOnSale(product)) {
241
- * return <Badge>Sale!</Badge>;
242
- * }
243
- */
244
- declare function isOnSale(product: ProductWithPrice): boolean;
245
- /**
246
- * Get the discount percentage for a product on sale
247
- *
248
- * @param product - Product with price data
249
- * @returns Discount percentage (0-100), or 0 if not on sale
250
- *
251
- * @example
252
- * const discount = getDiscountPercentage(product);
253
- * if (discount > 0) {
254
- * return <Badge>{discount}% OFF</Badge>;
255
- * }
256
- */
257
- declare function getDiscountPercentage(product: ProductWithPrice): number;
258
- /**
259
- * Get the markup percentage for a product
260
- *
261
- * @param product - Product with price data
262
- * @returns Markup percentage, or 0 if no markup
263
- */
264
- declare function getMarkupPercentage(product: ProductWithPrice): number;
265
- /**
266
- * Get the currency for a product
267
- *
268
- * @param product - Product with price data
269
- * @returns Currency code (default: "GHS")
270
- */
271
- declare function getProductCurrency(product: ProductWithPrice): CurrencyCode;
272
- /**
273
- * Format a product's display price
274
- * Convenience function combining getDisplayPrice and formatPrice
275
- *
276
- * @param product - Product with price data
277
- * @param locale - BCP 47 locale string (default: "en-US")
278
- * @returns Formatted price string
279
- *
280
- * @example
281
- * <span>{formatProductPrice(product)}</span> // "GH₵29.99"
282
- */
283
- declare function formatProductPrice(product: ProductWithPrice, locale?: string): string;
284
-
285
- /**
286
- * Categorize payment errors into user-friendly messages
287
- */
288
- declare function categorizePaymentError(error: Error, errorCode?: string): PaymentErrorDetails;
289
- /**
290
- * Normalize payment response from different formats into a standard PaymentResponse
291
- */
292
- declare function normalizePaymentResponse(response: unknown): PaymentResponse;
293
- declare function isPaymentStatusSuccess(status: string | undefined): boolean;
294
- declare function isPaymentStatusFailure(status: string | undefined): boolean;
295
- declare function isPaymentStatusRequiresAction(status: string | undefined): boolean;
296
- /**
297
- * Normalize payment status response into a standard format
298
- */
299
- declare function normalizeStatusResponse(response: unknown): PaymentStatusResponse;
300
- /** Mobile money provider display names */
301
- declare const MOBILE_MONEY_PROVIDERS: {
302
- readonly mtn: {
303
- readonly name: "MTN Mobile Money";
304
- readonly prefix: readonly ["024", "054", "055", "059"];
305
- };
306
- readonly vodafone: {
307
- readonly name: "Vodafone Cash";
308
- readonly prefix: readonly ["020", "050"];
309
- };
310
- readonly airtel: {
311
- readonly name: "AirtelTigo Money";
312
- readonly prefix: readonly ["027", "057", "026", "056"];
313
- };
314
- };
315
- /**
316
- * Detect mobile money provider from phone number
317
- */
318
- declare function detectMobileMoneyProvider(phoneNumber: string): "mtn" | "vodafone" | "airtel" | null;
319
-
320
- export { type ProductWithPrice as A, type FormatCompactOptions as B, CURRENCY_SYMBOLS as C, type FormatPriceOptions as F, MOBILE_MONEY_PROVIDERS as M, type PriceInfo as P, type TaxComponent as T, formatPriceAdjustment as a, formatPriceCompact as b, formatMoney as c, formatNumberCompact as d, formatProductPrice as e, formatPrice as f, getTaxAmount as g, hasTaxInfo as h, isTaxInclusive as i, formatPriceWithTax as j, getCurrencySymbol as k, getDisplayPrice as l, getBasePrice as m, isOnSale as n, getDiscountPercentage as o, parsePrice as p, getMarkupPercentage as q, getProductCurrency as r, categorizePaymentError as s, normalizePaymentResponse as t, normalizeStatusResponse as u, isPaymentStatusFailure as v, isPaymentStatusRequiresAction as w, isPaymentStatusSuccess as x, detectMobileMoneyProvider as y, type TaxInfo as z };