@cimplify/sdk 0.24.2 → 0.26.0

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 (43) hide show
  1. package/dist/advanced.d.mts +2 -2
  2. package/dist/advanced.d.ts +2 -2
  3. package/dist/{chunk-H34GSEGR.mjs → chunk-JMNMSVDQ.mjs} +24 -1
  4. package/dist/{chunk-IYQGGFVQ.js → chunk-WMQXSDRC.js} +26 -0
  5. package/dist/{index-DCoxHrmr.d.mts → index-2JxW7TuQ.d.mts} +2 -2
  6. package/dist/{index-DIHoZUz_.d.mts → index-7mX_cUpQ.d.mts} +1 -1
  7. package/dist/{index-DT0Mzy8v.d.ts → index-DBM89ktY.d.ts} +2 -2
  8. package/dist/{index-CJZMsbDi.d.ts → index-DEx3GwlW.d.ts} +1 -1
  9. package/dist/index.d.mts +5 -5
  10. package/dist/index.d.ts +5 -5
  11. package/dist/index.js +18 -6
  12. package/dist/index.mjs +1 -1
  13. package/dist/{payment-DqiwuLma.d.mts → payment-5a21yzi7.d.mts} +22 -1
  14. package/dist/{payment-DqiwuLma.d.ts → payment-5a21yzi7.d.ts} +22 -1
  15. package/dist/{price-BZlddg1A.d.ts → price-BS0Z5Oi9.d.ts} +1 -1
  16. package/dist/{price-DVQrpE7j.d.mts → price-CGPMlQUk.d.mts} +1 -1
  17. package/dist/react.d.mts +457 -405
  18. package/dist/react.d.ts +457 -405
  19. package/dist/react.js +2193 -1065
  20. package/dist/react.mjs +2161 -1059
  21. package/dist/styles.css +1 -1
  22. package/dist/utils.d.mts +3 -3
  23. package/dist/utils.d.ts +3 -3
  24. package/package.json +1 -1
  25. package/registry/accommodation-card.json +17 -0
  26. package/registry/cart-page.json +1 -1
  27. package/registry/catalogue-page.json +1 -1
  28. package/registry/collection-page.json +1 -1
  29. package/registry/compact-service-card.json +16 -0
  30. package/registry/digital-product-card.json +16 -0
  31. package/registry/food-product-card.json +16 -0
  32. package/registry/food-product-layout.json +1 -1
  33. package/registry/index.json +87 -17
  34. package/registry/lease-service-card.json +17 -0
  35. package/registry/product-card.json +1 -1
  36. package/registry/product-page.json +1 -1
  37. package/registry/rental-service-card.json +16 -0
  38. package/registry/retail-product-card.json +16 -0
  39. package/registry/schedule-service-card.json +16 -0
  40. package/registry/search-page.json +1 -1
  41. package/registry/standard-service-card.json +16 -0
  42. package/registry/subscription-card.json +16 -0
  43. package/registry/wholesale-product-card.json +17 -0
@@ -14,7 +14,7 @@
14
14
  "files": [
15
15
  {
16
16
  "path": "product-page.tsx",
17
- "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport { DefaultProductLayout } from \"@cimplify/sdk/react\";\nimport { FoodProductLayout } from \"@cimplify/sdk/react\";\nimport { WholesaleProductLayout } from \"@cimplify/sdk/react\";\nimport { ServiceProductLayout } from \"@cimplify/sdk/react\";\nimport { DigitalProductLayout } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport type { ProductLayoutProps };\n\nexport enum ProductTemplate {\n Default = \"default\",\n Food = \"food\",\n Wholesale = \"wholesale\",\n Service = \"service\",\n Digital = \"digital\",\n Physical = \"physical\",\n}\n\nexport interface ProductPageClassNames {\n root?: string;\n loading?: string;\n}\n\nexport interface ProductPageProps {\n /** Product slug or ID — used for client-side fetch when `product` is not provided. */\n productId?: string;\n /** Pre-fetched product for SSR. Skips client-side fetch when provided. */\n product?: ProductWithDetails;\n /** Per-slug page map. Highest priority — maps a product slug to a custom component. */\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>;\n /** Per-type template map. Overrides built-in layouts for specific template keys. */\n templates?: Partial<Record<ProductTemplate | string, React.ComponentType<ProductLayoutProps>>>;\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 /** Breadcrumb renderer. Receives the product for context. */\n renderBreadcrumb?: (product: ProductWithDetails) => React.ReactNode;\n /** Pre-fetched related products. */\n relatedProducts?: Product[];\n /** Show related products section. Default: true. */\n showRelated?: boolean;\n className?: string;\n classNames?: ProductPageClassNames;\n}\n\nconst BUILT_IN_LAYOUTS: Record<string, React.ComponentType<ProductLayoutProps>> = {\n [ProductTemplate.Food]: FoodProductLayout,\n [ProductTemplate.Wholesale]: WholesaleProductLayout,\n [ProductTemplate.Service]: ServiceProductLayout,\n [ProductTemplate.Digital]: DigitalProductLayout,\n [ProductTemplate.Default]: DefaultProductLayout,\n};\n\nfunction resolveTemplateKey(product: ProductWithDetails): ProductTemplate | string {\n const metaTemplate = product.metadata?.page_template;\n if (typeof metaTemplate === \"string\" && metaTemplate.trim()) {\n return metaTemplate.trim();\n }\n\n if (product.quantity_pricing && product.quantity_pricing.length > 1) {\n return ProductTemplate.Wholesale;\n }\n\n if (product.type === \"service\") {\n return ProductTemplate.Service;\n }\n\n if (product.type === \"digital\") {\n return ProductTemplate.Digital;\n }\n\n if (product.render_hint === \"food\") {\n return ProductTemplate.Food;\n }\n\n if (product.render_hint === \"physical\") {\n return ProductTemplate.Physical;\n }\n\n return ProductTemplate.Default;\n}\n\nfunction resolveLayout(\n product: ProductWithDetails,\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>,\n templates?: Partial<Record<string, React.ComponentType<ProductLayoutProps>>>,\n): React.ComponentType<ProductLayoutProps> {\n // 1. Per-slug page (AI-generated, highest priority)\n if (pages?.[product.slug]) {\n return pages[product.slug];\n }\n\n const key = resolveTemplateKey(product);\n\n // 2. Consumer-provided template override\n if (templates?.[key]) {\n return templates[key];\n }\n\n // 3. Built-in layout\n if (BUILT_IN_LAYOUTS[key]) {\n return BUILT_IN_LAYOUTS[key];\n }\n\n // 4. Fallback\n return DefaultProductLayout;\n}\n\nexport function ProductPage({\n productId,\n product: productProp,\n pages,\n templates,\n onAddToCart,\n renderImage,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n classNames,\n}: ProductPageProps): React.ReactElement {\n const resolvedId = productId || productProp?.slug || productProp?.id || \"\";\n const { product: fetched, isLoading } = useProduct(resolvedId, {\n enabled: !productProp && resolvedId.length > 0,\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 className=\"grid lg:grid-cols-2 gap-8 lg:gap-12 animate-pulse\">\n <div className=\"aspect-square bg-muted\" />\n <div className=\"space-y-4 py-4\">\n <div className=\"h-8 w-3/5 bg-muted\" />\n <div className=\"h-6 w-2/5 bg-muted\" />\n <div className=\"h-4 w-4/5 bg-muted\" />\n <div className=\"h-4 w-3/5 bg-muted\" />\n <div className=\"h-14 w-full bg-muted mt-8\" />\n </div>\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 className=\"text-muted-foreground\">Product not found.</p>\n </div>\n );\n }\n\n const Layout = resolveLayout(product, pages, templates);\n\n return (\n <div data-cimplify-product-page className={cn(className, classNames?.root)}>\n <Layout\n product={product}\n onAddToCart={onAddToCart}\n renderImage={renderImage}\n renderBreadcrumb={renderBreadcrumb}\n relatedProducts={relatedProducts}\n showRelated={showRelated}\n />\n </div>\n );\n}\n"
17
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport { PRODUCT_TYPE, RENDER_HINT } from \"@cimplify/sdk\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport { DefaultProductLayout } from \"@cimplify/sdk/react\";\nimport { FoodProductLayout } from \"@cimplify/sdk/react\";\nimport { WholesaleProductLayout } from \"@cimplify/sdk/react\";\nimport { ServiceProductLayout } from \"@cimplify/sdk/react\";\nimport { DigitalProductLayout } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport type { ProductLayoutProps };\n\nexport enum ProductTemplate {\n Default = \"default\",\n Food = \"food\",\n Wholesale = \"wholesale\",\n Service = \"service\",\n Digital = \"digital\",\n Physical = \"physical\",\n}\n\nexport interface ProductPageClassNames {\n root?: string;\n loading?: string;\n}\n\nexport interface ProductPageProps {\n /** Product slug or ID — used for client-side fetch when `product` is not provided. */\n productId?: string;\n /** Pre-fetched product for SSR. Skips client-side fetch when provided. */\n product?: ProductWithDetails;\n /** Per-slug page map. Highest priority — maps a product slug to a custom component. */\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>;\n /** Per-type template map. Overrides built-in layouts for specific template keys. */\n templates?: Partial<Record<ProductTemplate | string, React.ComponentType<ProductLayoutProps>>>;\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 /** Breadcrumb renderer. Receives the product for context. */\n renderBreadcrumb?: (product: ProductWithDetails) => React.ReactNode;\n /** Pre-fetched related products. */\n relatedProducts?: Product[];\n /** Show related products section. Default: true. */\n showRelated?: boolean;\n className?: string;\n classNames?: ProductPageClassNames;\n}\n\nconst BUILT_IN_LAYOUTS: Record<string, React.ComponentType<ProductLayoutProps>> = {\n [ProductTemplate.Food]: FoodProductLayout,\n [ProductTemplate.Wholesale]: WholesaleProductLayout,\n [ProductTemplate.Service]: ServiceProductLayout,\n [ProductTemplate.Digital]: DigitalProductLayout,\n [ProductTemplate.Default]: DefaultProductLayout,\n};\n\nfunction resolveTemplateKey(product: ProductWithDetails): ProductTemplate | string {\n const metaTemplate = product.metadata?.page_template;\n if (typeof metaTemplate === \"string\" && metaTemplate.trim()) {\n return metaTemplate.trim();\n }\n\n if (product.quantity_pricing && product.quantity_pricing.length > 1) {\n return ProductTemplate.Wholesale;\n }\n\n if (product.type === PRODUCT_TYPE.Service) {\n return ProductTemplate.Service;\n }\n\n if (product.type === PRODUCT_TYPE.Digital) {\n return ProductTemplate.Digital;\n }\n\n if (product.render_hint === RENDER_HINT.Food) {\n return ProductTemplate.Food;\n }\n\n if (product.render_hint === RENDER_HINT.Physical) {\n return ProductTemplate.Physical;\n }\n\n return ProductTemplate.Default;\n}\n\nfunction resolveLayout(\n product: ProductWithDetails,\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>,\n templates?: Partial<Record<string, React.ComponentType<ProductLayoutProps>>>,\n): React.ComponentType<ProductLayoutProps> {\n // 1. Per-slug page (AI-generated, highest priority)\n if (pages?.[product.slug]) {\n return pages[product.slug];\n }\n\n const key = resolveTemplateKey(product);\n\n // 2. Consumer-provided template override\n if (templates?.[key]) {\n return templates[key];\n }\n\n // 3. Built-in layout\n if (BUILT_IN_LAYOUTS[key]) {\n return BUILT_IN_LAYOUTS[key];\n }\n\n // 4. Fallback\n return DefaultProductLayout;\n}\n\nexport function ProductPage({\n productId,\n product: productProp,\n pages,\n templates,\n onAddToCart,\n renderImage,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n classNames,\n}: ProductPageProps): React.ReactElement {\n const resolvedId = productId || productProp?.slug || productProp?.id || \"\";\n const { product: fetched, isLoading } = useProduct(resolvedId, {\n enabled: !productProp && resolvedId.length > 0,\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 className=\"grid lg:grid-cols-2 gap-8 lg:gap-12 animate-pulse\">\n <div className=\"aspect-square bg-muted\" />\n <div className=\"space-y-4 py-4\">\n <div className=\"h-8 w-3/5 bg-muted\" />\n <div className=\"h-6 w-2/5 bg-muted\" />\n <div className=\"h-4 w-4/5 bg-muted\" />\n <div className=\"h-4 w-3/5 bg-muted\" />\n <div className=\"h-14 w-full bg-muted mt-8\" />\n </div>\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 className=\"text-muted-foreground\">Product not found.</p>\n </div>\n );\n }\n\n const Layout = resolveLayout(product, pages, templates);\n\n return (\n <div data-cimplify-product-page className={cn(className, classNames?.root)}>\n <Layout\n product={product}\n onAddToCart={onAddToCart}\n renderImage={renderImage}\n renderBreadcrumb={renderBreadcrumb}\n relatedProducts={relatedProducts}\n showRelated={showRelated}\n />\n </div>\n );\n}\n"
18
18
  }
19
19
  ]
20
20
  }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "rental-service-card",
3
+ "title": "RentalServiceCard",
4
+ "description": "Rental card with per-day/hour pricing, deposit, and availability count.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "price",
8
+ "cn"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "cards/rental-service-card.tsx",
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { ServiceCardLayoutProps } from \"./standard-service-card\";\nimport { CardShell, CardImage, QuickAddButton } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst UNIT_LABELS: Record<string, string> = {\n minutes: \"min\",\n hours: \"hr\",\n days: \"day\",\n nights: \"night\",\n weeks: \"week\",\n months: \"mo\",\n years: \"yr\",\n};\n\nexport function RentalServiceCard({\n product,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const hasDeposit = product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount;\n const unit = product.duration_unit ? UNIT_LABELS[product.duration_unit] || product.duration_unit : \"day\";\n const status = product.inventory_status;\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"16/9\" renderImage={renderImage}>\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none\" />\n\n {/* Per-unit pill */}\n <span className=\"absolute bottom-3 left-3 inline-flex items-center text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm\">\n Per {unit}\n </span>\n\n {/* Price */}\n <span className=\"absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm\">\n <Price amount={product.default_price} />/{unit}\n </span>\n\n {/* Capacity badge */}\n {product.general_service_capacity != null && product.general_service_capacity > 1 && (\n <span className=\"absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-blue-50/90 text-blue-700 border border-blue-200/50 backdrop-blur px-2 py-0.5 rounded-md\">\n Up to {product.general_service_capacity} guests\n </span>\n )}\n </CardImage>\n\n <div className=\"p-4\">\n <h3 className=\"text-[14.5px] font-bold text-foreground leading-snug tracking-tight\">\n {product.name}\n </h3>\n {product.description && (\n <p className=\"text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2\">\n {product.description}\n </p>\n )}\n\n {/* Info pills */}\n <div className=\"flex flex-wrap items-center gap-2 mt-3\">\n {product.min_order_quantity != null && product.min_order_quantity > 1 && (\n <span className=\"text-[10.5px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded\">\n Min. {product.min_order_quantity} {unit}{product.min_order_quantity > 1 ? \"s\" : \"\"}\n </span>\n )}\n {hasDeposit && (\n <span className=\"text-[10.5px] font-medium text-amber-600\">\n <Price amount={product.deposit_amount!} /> deposit\n </span>\n )}\n </div>\n\n <div className=\"flex items-center justify-between mt-3 pt-3 border-t border-border\">\n {/* Availability */}\n {status && (\n <div className=\"flex items-center gap-1.5 text-[12px]\">\n <span className={cn(\n \"w-[7px] h-[7px] rounded-full shrink-0\",\n status.in_stock ? \"bg-emerald-500\" : \"bg-red-500\",\n )} />\n <span className={status.in_stock ? \"text-muted-foreground\" : \"text-red-600\"}>\n {status.in_stock\n ? status.stock_level != null\n ? `${status.stock_level} available`\n : \"Available\"\n : \"Unavailable\"\n }\n </span>\n </div>\n )}\n <button\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className=\"text-[13px] font-semibold text-primary hover:text-primary/80 transition-colors\"\n >\n Reserve now &rarr;\n </button>\n </div>\n </div>\n </CardShell>\n );\n}\n"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "retail-product-card",
3
+ "title": "RetailProductCard",
4
+ "description": "Product card for retail with color swatches, sale badge, wishlist, and sold-out overlay.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "price",
8
+ "cn"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "cards/retail-product-card.tsx",
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { CardShell, CardImage, WishlistButton, SoldOutOverlay, SaleBadge, LowStockBadge } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice, isOnSale, getBasePrice, getDiscountPercentage } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nfunction getColorSwatches(product: Product): string[] {\n const variants = (product as ProductWithDetails).variants;\n if (!variants || variants.length === 0) return [];\n\n const seen = new Set<string>();\n for (const v of variants) {\n if (v.display_attributes) {\n for (const attr of v.display_attributes) {\n if (attr.axis_name.toLowerCase() === \"color\" && !seen.has(attr.value_name)) {\n seen.add(attr.value_name);\n }\n }\n }\n }\n\n return Array.from(seen).slice(0, 5);\n}\n\nexport function RetailProductCard({\n product,\n renderImage,\n renderLink,\n className,\n}: CardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const outOfStock = product.inventory_status?.in_stock === false;\n const onSale = isOnSale(product);\n const swatches = getColorSwatches(product);\n\n return (\n <CardShell product={product} renderLink={renderLink} disabled={outOfStock} className={className}>\n {outOfStock && <SoldOutOverlay />}\n\n <CardImage src={image} alt={product.name} aspectRatio=\"3/4\" renderImage={renderImage}>\n {/* Sale badge */}\n {onSale && (\n <div className=\"absolute top-3 left-3\">\n <SaleBadge product={product} />\n </div>\n )}\n\n {/* Wishlist */}\n <WishlistButton className=\"absolute top-3 right-3\" />\n\n {/* Low stock */}\n {product.inventory_status?.low_stock && product.inventory_status?.stock_level && (\n <span className=\"absolute bottom-3 left-3 text-[11px] font-semibold bg-amber-50 text-amber-700 border border-amber-200/60 px-2 py-0.5 rounded-md\">\n Only {product.inventory_status.stock_level} left\n </span>\n )}\n </CardImage>\n\n <div className=\"p-3.5\">\n {/* Brand */}\n {product.vendor && (\n <p className=\"text-[10px] font-semibold text-muted-foreground uppercase tracking-widest\">\n {product.vendor}\n </p>\n )}\n\n {/* Name */}\n <h3 className=\"text-[14.5px] font-semibold text-foreground leading-snug tracking-tight mt-1 truncate\">\n {product.name}\n </h3>\n\n {/* Price */}\n <div className=\"flex items-center gap-2 mt-2\">\n <Price amount={product.default_price} className=\"text-sm font-bold\" />\n {onSale && (\n <Price amount={getBasePrice(product)} className=\"text-xs text-muted-foreground line-through\" />\n )}\n </div>\n\n {/* Color swatches */}\n {swatches.length > 0 && (\n <div className=\"flex gap-1.5 mt-2.5\">\n {swatches.map((name) => (\n <span\n key={name}\n title={name}\n className=\"text-[9px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded\"\n >\n {name}\n </span>\n ))}\n </div>\n )}\n </div>\n </CardShell>\n );\n}\n"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "schedule-service-card",
3
+ "title": "ScheduleServiceCard",
4
+ "description": "Service card with next available time slot pills.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "price",
8
+ "cn"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "cards/schedule-service-card.tsx",
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { ServiceCardLayoutProps } from \"./standard-service-card\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport function ScheduleServiceCard({\n product,\n slots,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const href = `/products/${product.slug}`;\n const durationLabel = product.duration_minutes != null\n ? `${product.duration_minutes} ${product.duration_unit ?? \"min\"}`\n : null;\n\n const displaySlots = (slots || []).slice(0, 3);\n\n const shellClass = cn(\n \"group block text-left cursor-pointer bg-card rounded-[14px] overflow-hidden\",\n \"shadow-[0_1px_3px_rgba(0,0,0,0.04),0_6px_24px_rgba(0,0,0,0.06)]\",\n \"transition-all duration-300 ease-[cubic-bezier(0.19,1,0.22,1)]\",\n \"hover:-translate-y-1 hover:shadow-[0_2px_6px_rgba(0,0,0,0.04),0_12px_40px_rgba(0,0,0,0.10)]\",\n className,\n );\n\n const inner = (\n <div className=\"p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex-1 min-w-0\">\n <h3 className=\"text-[14.5px] font-bold text-foreground leading-snug tracking-tight\">\n {product.name}\n </h3>\n <p className=\"text-[12.5px] text-muted-foreground mt-0.5\">\n {durationLabel && <>{durationLabel} · </>}\n <Price amount={product.default_price} />\n </p>\n </div>\n {image && (\n <div className=\"w-14 h-14 rounded-xl overflow-hidden bg-muted shrink-0\">\n {renderImage ? (\n renderImage({ src: image, alt: product.name, className: \"w-full h-full object-cover\" })\n ) : (\n <img src={image} alt={product.name} className=\"w-full h-full object-cover\" />\n )}\n </div>\n )}\n </div>\n\n {product.description && (\n <p className=\"text-[12px] text-muted-foreground mt-2 line-clamp-2 leading-relaxed\">\n {product.description}\n </p>\n )}\n\n {displaySlots.length > 0 && (\n <div className=\"mt-4 pt-3 border-t border-border\">\n <p className=\"text-[10px] font-semibold text-muted-foreground uppercase tracking-widest mb-2\">\n {product.general_service_capacity != null && product.general_service_capacity > 1\n ? \"Upcoming classes\"\n : \"Next available\"\n }\n </p>\n <div className=\"flex gap-2 flex-wrap\">\n {displaySlots.map((slot, i) => (\n <button\n key={i}\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className={cn(\n \"text-[12.5px] font-medium px-3 py-1.5 rounded-lg transition-all duration-200 ease-out hover:scale-[1.04]\",\n i === 0\n ? \"bg-primary text-primary-foreground font-semibold shadow-sm\"\n : \"bg-muted text-foreground hover:bg-muted/80\",\n )}\n >\n {new Date(slot.start_time).toLocaleString(undefined, {\n weekday: \"short\",\n hour: \"numeric\",\n minute: \"2-digit\",\n })}\n </button>\n ))}\n </div>\n </div>\n )}\n </div>\n );\n\n if (renderLink) {\n return renderLink({ href, className: shellClass, children: inner });\n }\n\n return <a href={href} className={shellClass} data-cimplify-card>{inner}</a>;\n}\n"
14
+ }
15
+ ]
16
+ }
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  {
12
12
  "path": "search-page.tsx",
13
- "content": "\"use client\";\n\nimport React from \"react\";\nimport { Input } from \"@base-ui/react/input\";\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 * The search input uses Base UI's `Input` primitive, which provides\n * automatic `data-filled`, `data-focused`, `data-dirty`, and\n * `data-touched` attributes for styling hooks.\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"
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product, Category, PropertyFacet } from \"@cimplify/sdk\";\nimport { useSearch } from \"@cimplify/sdk/react\";\nimport type { UseSearchOptions } from \"./hooks/use-search\";\nimport type { SearchLayoutProps } from \"./search-layouts/shared\";\nimport { DefaultSearchLayout } from \"./search-layouts/default-search-layout\";\nimport { CompactSearchLayout } from \"./search-layouts/compact-search-layout\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport type { SearchLayoutProps };\n\nexport enum SearchTemplate {\n Default = \"default\",\n Compact = \"compact\",\n}\n\nexport interface SearchPageClassNames {\n root?: string;\n}\n\nexport interface SearchPageProps {\n /** Explicit search template. */\n template?: SearchTemplate;\n /** Layout map for overrides. AI or developer can provide custom layouts keyed by template name. */\n layouts?: Partial<Record<SearchTemplate | string, React.ComponentType<SearchLayoutProps>>>;\n /** Pre-fetched categories for filters. */\n categories?: Category[];\n /** Pre-fetched property facets for sidebar. */\n facets?: PropertyFacet[];\n /** Search options passed to useSearch. */\n searchOptions?: UseSearchOptions;\n /** Placeholder text for search input. */\n placeholder?: string;\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 /** Custom link renderer. */\n renderLink?: (props: { href: string; className?: string; children: React.ReactNode }) => React.ReactElement;\n /** Quick-add handler. */\n onQuickAdd?: (product: Product) => void;\n className?: string;\n classNames?: SearchPageClassNames;\n}\n\nconst BUILT_IN_LAYOUTS: Record<string, React.ComponentType<SearchLayoutProps>> = {\n [SearchTemplate.Default]: DefaultSearchLayout,\n [SearchTemplate.Compact]: CompactSearchLayout,\n};\n\nexport function SearchPage({\n template,\n layouts,\n categories,\n facets,\n searchOptions,\n placeholder,\n renderCard,\n renderImage,\n renderLink,\n onQuickAdd,\n className,\n classNames,\n}: SearchPageProps): React.ReactElement {\n const search = useSearch(searchOptions);\n\n const key = template ?? SearchTemplate.Default;\n const Layout = layouts?.[key] ?? BUILT_IN_LAYOUTS[key] ?? DefaultSearchLayout;\n\n return (\n <div data-cimplify-search-page className={cn(className, classNames?.root)}>\n <Layout\n search={search}\n facets={facets}\n categories={categories}\n renderCard={renderCard}\n renderImage={renderImage}\n renderLink={renderLink}\n onQuickAdd={onQuickAdd}\n placeholder={placeholder}\n />\n </div>\n );\n}\n"
14
14
  }
15
15
  ]
16
16
  }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "standard-service-card",
3
+ "title": "StandardServiceCard",
4
+ "description": "Service card with hero image, duration, deposit, and availability.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "price",
8
+ "cn"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "cards/standard-service-card.tsx",
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product } from \"@cimplify/sdk\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { CardShell, CardImage } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ServiceCardLayoutProps extends CardLayoutProps {\n slots?: AvailableSlot[];\n onBook?: (product: Product) => void;\n}\n\nfunction formatDuration(minutes: number, unit?: string): string {\n if (unit && unit !== \"minutes\") return `${minutes} ${unit}`;\n if (minutes >= 60) {\n const h = Math.floor(minutes / 60);\n const m = minutes % 60;\n return m > 0 ? `${h}h ${m}m` : `${h} hr${h > 1 ? \"s\" : \"\"}`;\n }\n return `${minutes} min`;\n}\n\nexport function StandardServiceCard({\n product,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const hasDeposit = product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount;\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"16/9\" renderImage={renderImage}>\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none\" />\n\n {/* Duration pill */}\n {product.duration_minutes != null && (\n <span className=\"absolute bottom-3 left-3 inline-flex items-center gap-1 text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm\">\n {formatDuration(product.duration_minutes, product.duration_unit)}\n </span>\n )}\n\n {/* Price on image */}\n <span className=\"absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm\">\n <Price amount={product.default_price} />\n </span>\n </CardImage>\n\n <div className=\"p-4\">\n <h3 className=\"text-[14.5px] font-bold text-foreground leading-snug tracking-tight\">\n {product.name}\n </h3>\n {product.description && (\n <p className=\"text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2\">\n {product.description}\n </p>\n )}\n\n <div className=\"flex items-center justify-between mt-3 pt-3 border-t border-border\">\n <div className=\"flex items-center gap-2 text-[12px] text-muted-foreground\">\n <span className=\"w-[7px] h-[7px] rounded-full bg-emerald-500 animate-pulse\" />\n <span>Available</span>\n {hasDeposit && (\n <>\n <span className=\"text-border\">·</span>\n <span><Price amount={product.deposit_amount!} /> deposit</span>\n </>\n )}\n </div>\n <button\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className=\"text-[13px] font-semibold text-primary hover:text-primary/80 transition-colors\"\n >\n Book now &rarr;\n </button>\n </div>\n </div>\n </CardShell>\n );\n}\n"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "subscription-card",
3
+ "title": "SubscriptionCard",
4
+ "description": "Subscription card with billing plan options, trial badge, and setup fee.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "price",
8
+ "cn"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "cards/subscription-card.tsx",
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { ServiceCardLayoutProps } from \"./standard-service-card\";\nimport { CardShell, CardImage } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst FREQUENCY_LABELS: Record<string, string> = {\n weekly: \"/week\",\n monthly: \"/mo\",\n quarterly: \"/qtr\",\n annually: \"/yr\",\n};\n\nexport function SubscriptionCard({\n product,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const plans = product.billing_plans || [];\n const primaryPlan = plans[0];\n const frequency = primaryPlan?.frequency\n ? FREQUENCY_LABELS[primaryPlan.frequency] || `/${primaryPlan.frequency}`\n : \"/mo\";\n const hasTrial = primaryPlan?.trial_days != null && primaryPlan.trial_days > 0;\n const tags = product.tags || [];\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"16/9\" renderImage={renderImage}>\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none\" />\n\n {/* Price on image */}\n <span className=\"absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm\">\n <Price amount={product.default_price} />{frequency}\n </span>\n\n {/* Trial badge */}\n {hasTrial && (\n <span className=\"absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-emerald-50/90 text-emerald-700 border border-emerald-200/50 backdrop-blur px-2 py-0.5 rounded-md\">\n {primaryPlan!.trial_days}-day free trial\n </span>\n )}\n </CardImage>\n\n <div className=\"p-4\">\n <h3 className=\"text-[14.5px] font-bold text-foreground leading-snug tracking-tight\">\n {product.name}\n </h3>\n {product.description && (\n <p className=\"text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2\">\n {product.description}\n </p>\n )}\n\n {/* Tags */}\n {tags.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mt-2.5\">\n {tags.slice(0, 3).map((tag) => (\n <span key={tag} className=\"text-[10px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded\">\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {/* Billing plan options */}\n {plans.length > 1 && (\n <div className=\"mt-3 space-y-1\">\n {plans.slice(0, 3).map((plan) => {\n const label = plan.frequency\n ? plan.frequency.charAt(0).toUpperCase() + plan.frequency.slice(1)\n : \"Standard\";\n const hasMarkup = plan.markup_type && plan.markup_amount;\n const isSavings = plan.markup_type === \"percentage\" && plan.markup_amount != null && plan.markup_amount < 0;\n\n return (\n <div key={plan.id} className=\"flex items-center justify-between text-[11px]\">\n <span className=\"text-muted-foreground\">{label}</span>\n <span className=\"font-medium\">\n {hasMarkup && isSavings && (\n <span className=\"text-emerald-600 mr-1\">Save {Math.abs(plan.markup_amount!)}%</span>\n )}\n </span>\n </div>\n );\n })}\n </div>\n )}\n\n <div className=\"flex items-center justify-between mt-3 pt-3 border-t border-border\">\n <div className=\"flex items-center gap-2 text-[11px] text-muted-foreground\">\n {hasTrial && (\n <span className=\"text-emerald-600 font-medium\">Free trial</span>\n )}\n {primaryPlan?.setup_fee != null && primaryPlan.setup_fee > 0 && (\n <span><Price amount={primaryPlan.setup_fee} /> setup</span>\n )}\n </div>\n <button\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className=\"text-[13px] font-semibold text-primary hover:text-primary/80 transition-colors\"\n >\n Subscribe &rarr;\n </button>\n </div>\n </div>\n </CardShell>\n );\n}\n"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "wholesale-product-card",
3
+ "title": "WholesaleProductCard",
4
+ "description": "B2B product card with price range, MOQ badge, and stock count.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "price",
8
+ "price-range",
9
+ "cn"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "cards/wholesale-product-card.tsx",
14
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { CardShell, CardImage, QuickAddButton } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { PriceRange } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport function WholesaleProductCard({\n product,\n onQuickAdd,\n renderImage,\n renderLink,\n className,\n}: CardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const hasTiers = product.quantity_pricing && product.quantity_pricing.length > 1;\n const status = product.inventory_status;\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"square\" renderImage={renderImage}>\n {/* MOQ badge */}\n {product.min_order_quantity != null && product.min_order_quantity > 1 && (\n <span className=\"absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-zinc-800 text-white px-2 py-0.5 rounded-md\">\n MOQ: {product.min_order_quantity}\n </span>\n )}\n\n <QuickAddButton\n onClick={() => onQuickAdd?.(product)}\n className=\"absolute bottom-3 right-3\"\n />\n </CardImage>\n\n <div className=\"p-3.5\">\n {/* SKU */}\n {product.sku && (\n <p className=\"text-[10px] text-muted-foreground font-mono\">\n {product.sku}\n </p>\n )}\n\n {/* Name */}\n <h3 className=\"text-[14.5px] font-semibold text-foreground leading-snug tracking-tight mt-1 truncate\">\n {product.name}\n </h3>\n\n {/* Price range or single */}\n <div className=\"mt-2\">\n {hasTiers ? (\n <PriceRange product={product} className=\"text-sm font-bold\" />\n ) : (\n <Price amount={product.default_price} className=\"text-sm font-bold\" />\n )}\n </div>\n\n {/* Stock */}\n {status && (\n <div className=\"flex items-center gap-1.5 mt-2\">\n <span className={cn(\n \"w-[7px] h-[7px] rounded-full shrink-0\",\n status.in_stock\n ? status.low_stock ? \"bg-amber-500\" : \"bg-emerald-500\"\n : \"bg-red-500\",\n )} />\n <span className={cn(\n \"text-[10.5px] font-medium\",\n status.in_stock\n ? status.low_stock ? \"text-amber-600\" : \"text-emerald-600\"\n : \"text-red-600\",\n )}>\n {!status.in_stock\n ? \"Out of stock\"\n : status.stock_level != null\n ? `${status.stock_level.toLocaleString()} in stock`\n : \"In stock\"\n }\n </span>\n </div>\n )}\n </div>\n </CardShell>\n );\n}\n"
15
+ }
16
+ ]
17
+ }