@cimplify/sdk 0.73.0 → 0.75.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 (34) hide show
  1. package/dist/advanced.d.mts +3 -3
  2. package/dist/advanced.d.ts +3 -3
  3. package/dist/{client-BvqaIlJ0.d.ts → client--w53owD4.d.ts} +2 -2
  4. package/dist/{client-C7EMuGoz.d.mts → client-D7uHF_jh.d.mts} +2 -2
  5. package/dist/{client-D6CZ2Ggp.d.ts → client-xDpke-KS.d.ts} +2 -2
  6. package/dist/{client-DWA_XIh3.d.mts → client-yFghjc1-.d.mts} +2 -2
  7. package/dist/{index-DIIlPUOC.d.ts → index-DTGS0QC1.d.ts} +3 -3
  8. package/dist/{index-D8vnKlOQ.d.mts → index-lt-fe29V.d.mts} +3 -3
  9. package/dist/index.d.mts +6 -6
  10. package/dist/index.d.ts +6 -6
  11. package/dist/{payment-DVS7ZUEp.d.mts → payment-C-2gApyQ.d.mts} +1 -1
  12. package/dist/{payment-sn-yGL7v.d.ts → payment-CfemTORM.d.ts} +1 -1
  13. package/dist/{price-Cm0tU0V5.d.mts → price-CZBQw-Rh.d.mts} +1 -1
  14. package/dist/{price-9T1Y47oS.d.ts → price-DNeyoO7y.d.ts} +1 -1
  15. package/dist/{product-B_kS4Oxa.d.mts → product-_GFoaPHU.d.mts} +2 -0
  16. package/dist/{product-B_kS4Oxa.d.ts → product-_GFoaPHU.d.ts} +2 -0
  17. package/dist/react.d.mts +39 -5
  18. package/dist/react.d.ts +39 -5
  19. package/dist/react.js +187 -66
  20. package/dist/react.mjs +187 -67
  21. package/dist/{server-DMvQp27Z.d.ts → server-1hXIXH2k.d.ts} +1 -1
  22. package/dist/{server-DDQ1O3XR.d.mts → server-G8I4JtqK.d.mts} +1 -1
  23. package/dist/server.d.mts +4 -4
  24. package/dist/server.d.ts +4 -4
  25. package/dist/testing/msw.d.mts +2 -2
  26. package/dist/testing/msw.d.ts +2 -2
  27. package/dist/testing/suite.d.mts +6 -6
  28. package/dist/testing/suite.d.ts +6 -6
  29. package/dist/testing.d.mts +5 -5
  30. package/dist/testing.d.ts +5 -5
  31. package/dist/utils.d.mts +4 -4
  32. package/dist/utils.d.ts +4 -4
  33. package/package.json +1 -1
  34. package/registry/product-customizer.json +1 -1
package/dist/react.mjs CHANGED
@@ -7306,6 +7306,144 @@ function formatDuration2(value, unit) {
7306
7306
  const label = value === 1 ? labels.singular : labels.plural;
7307
7307
  return `${value} ${label}`;
7308
7308
  }
7309
+ var ANY_VALUE = "__any__";
7310
+ function StaffPicker({
7311
+ staff,
7312
+ selectedStaffId,
7313
+ onStaffSelect,
7314
+ showAnyOption = true,
7315
+ anyLabel = "Any available",
7316
+ className,
7317
+ classNames
7318
+ }) {
7319
+ const groupValue = selectedStaffId === null ? ANY_VALUE : selectedStaffId ?? "";
7320
+ return /* @__PURE__ */ jsxs(
7321
+ RadioGroup,
7322
+ {
7323
+ "data-cimplify-staff-picker": true,
7324
+ className: cn(className, classNames?.root),
7325
+ value: groupValue,
7326
+ onValueChange: (value) => {
7327
+ onStaffSelect?.(value === ANY_VALUE ? null : value);
7328
+ },
7329
+ children: [
7330
+ showAnyOption && /* @__PURE__ */ jsx(
7331
+ Radio.Root,
7332
+ {
7333
+ value: ANY_VALUE,
7334
+ "data-cimplify-staff-option": true,
7335
+ "data-selected": selectedStaffId === null || void 0,
7336
+ "data-any": true,
7337
+ className: classNames?.option,
7338
+ children: /* @__PURE__ */ jsx("span", { "data-cimplify-staff-name": true, className: classNames?.name, children: anyLabel })
7339
+ }
7340
+ ),
7341
+ staff.map((member) => /* @__PURE__ */ jsxs(
7342
+ Radio.Root,
7343
+ {
7344
+ value: member.id,
7345
+ "data-cimplify-staff-option": true,
7346
+ "data-selected": selectedStaffId === member.id || void 0,
7347
+ className: classNames?.option,
7348
+ children: [
7349
+ member.avatar_url && /* @__PURE__ */ jsx(
7350
+ "img",
7351
+ {
7352
+ src: member.avatar_url,
7353
+ alt: member.name,
7354
+ "data-cimplify-staff-avatar": true,
7355
+ className: classNames?.avatar
7356
+ }
7357
+ ),
7358
+ /* @__PURE__ */ jsx("span", { "data-cimplify-staff-name": true, className: classNames?.name, children: member.name }),
7359
+ member.bio && /* @__PURE__ */ jsx("span", { "data-cimplify-staff-bio": true, className: classNames?.bio, children: member.bio })
7360
+ ]
7361
+ },
7362
+ member.id
7363
+ ))
7364
+ ]
7365
+ }
7366
+ );
7367
+ }
7368
+ function formatDate(value) {
7369
+ const date = new Date(value);
7370
+ if (Number.isNaN(date.getTime())) return value;
7371
+ return date.toLocaleDateString(void 0, {
7372
+ weekday: "short",
7373
+ month: "short",
7374
+ day: "numeric"
7375
+ });
7376
+ }
7377
+ function formatTime(value) {
7378
+ const date = new Date(value);
7379
+ if (Number.isNaN(date.getTime())) return value;
7380
+ return date.toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" });
7381
+ }
7382
+ function BookingSummary({
7383
+ slot,
7384
+ total,
7385
+ staffName,
7386
+ durationMinutes,
7387
+ depositType,
7388
+ depositAmount,
7389
+ currency,
7390
+ className,
7391
+ classNames
7392
+ }) {
7393
+ const depositValue = depositAmount != null ? parsePrice(depositAmount) : 0;
7394
+ const depositDue = depositType === "percentage" ? total * depositValue / 100 : depositType === "fixed" ? depositValue : 0;
7395
+ const hasDeposit = depositDue > 0;
7396
+ const balance = Math.max(0, total - depositDue);
7397
+ const rowClass = cn("flex items-center justify-between", classNames?.row);
7398
+ const labelClass = cn("text-muted-foreground", classNames?.label);
7399
+ const valueClass = cn("font-medium", classNames?.value);
7400
+ return /* @__PURE__ */ jsxs(
7401
+ "div",
7402
+ {
7403
+ "data-cimplify-booking-summary": true,
7404
+ className: cn("rounded-lg border border-border p-4 space-y-3", className, classNames?.root),
7405
+ children: [
7406
+ /* @__PURE__ */ jsxs("div", { className: cn("flex items-center justify-between", classNames?.header), children: [
7407
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: "Your booking" }),
7408
+ durationMinutes != null && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
7409
+ durationMinutes,
7410
+ " min"
7411
+ ] })
7412
+ ] }),
7413
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5 text-sm", children: [
7414
+ staffName && /* @__PURE__ */ jsxs("div", { className: rowClass, children: [
7415
+ /* @__PURE__ */ jsx("span", { className: labelClass, children: "With" }),
7416
+ /* @__PURE__ */ jsx("span", { className: valueClass, children: staffName })
7417
+ ] }),
7418
+ /* @__PURE__ */ jsxs("div", { className: rowClass, children: [
7419
+ /* @__PURE__ */ jsx("span", { className: labelClass, children: "Date" }),
7420
+ /* @__PURE__ */ jsx("span", { className: valueClass, children: formatDate(slot.start_time) })
7421
+ ] }),
7422
+ /* @__PURE__ */ jsxs("div", { className: rowClass, children: [
7423
+ /* @__PURE__ */ jsx("span", { className: labelClass, children: "Time" }),
7424
+ /* @__PURE__ */ jsx("span", { className: valueClass, children: formatTime(slot.start_time) })
7425
+ ] })
7426
+ ] }),
7427
+ /* @__PURE__ */ jsxs("div", { className: cn("border-t border-border pt-3 space-y-1.5 text-sm", classNames?.totals), children: [
7428
+ /* @__PURE__ */ jsxs("div", { className: rowClass, children: [
7429
+ /* @__PURE__ */ jsx("span", { className: labelClass, children: "Subtotal" }),
7430
+ /* @__PURE__ */ jsx("span", { className: "font-semibold", children: /* @__PURE__ */ jsx(Price, { amount: total, currency }) })
7431
+ ] }),
7432
+ hasDeposit && /* @__PURE__ */ jsxs(Fragment, { children: [
7433
+ /* @__PURE__ */ jsxs("div", { className: cn(rowClass, classNames?.deposit), children: [
7434
+ /* @__PURE__ */ jsx("span", { className: labelClass, children: "Due now (deposit)" }),
7435
+ /* @__PURE__ */ jsx("span", { className: "font-semibold", children: /* @__PURE__ */ jsx(Price, { amount: depositDue, currency }) })
7436
+ ] }),
7437
+ /* @__PURE__ */ jsxs("div", { className: cn(rowClass, "text-xs text-muted-foreground", classNames?.balance), children: [
7438
+ /* @__PURE__ */ jsx("span", { children: "Balance at appointment" }),
7439
+ /* @__PURE__ */ jsx(Price, { amount: balance, currency })
7440
+ ] })
7441
+ ] })
7442
+ ] })
7443
+ ]
7444
+ }
7445
+ );
7446
+ }
7309
7447
  var FREQUENCY_LABELS = {
7310
7448
  weekly: "Weekly",
7311
7449
  biweekly: "Biweekly",
@@ -8670,7 +8808,7 @@ function groupSlots(slots) {
8670
8808
  }
8671
8809
  return ["morning", "afternoon", "evening"].filter((tod) => groups[tod]?.length).map((tod) => ({ label: TIME_OF_DAY_LABELS[tod], slots: groups[tod] }));
8672
8810
  }
8673
- function formatTime(timeStr) {
8811
+ function formatTime2(timeStr) {
8674
8812
  try {
8675
8813
  const date = new Date(timeStr);
8676
8814
  if (!isNaN(date.getTime())) {
@@ -8845,7 +8983,7 @@ function SlotPicker({
8845
8983
  classNames?.slot
8846
8984
  ),
8847
8985
  children: [
8848
- /* @__PURE__ */ jsx("span", { "data-cimplify-slot-time": true, className: classNames?.slotTime, children: isMultiDay ? formatStaySummary(slot, durationUnit, durationValue) : formatTime(slot.start_time) }),
8986
+ /* @__PURE__ */ jsx("span", { "data-cimplify-slot-time": true, className: classNames?.slotTime, children: isMultiDay ? formatStaySummary(slot, durationUnit, durationValue) : formatTime2(slot.start_time) }),
8849
8987
  showPrice && slot.price && /* @__PURE__ */ jsx(
8850
8988
  "span",
8851
8989
  {
@@ -8879,7 +9017,7 @@ function SlotPicker({
8879
9017
  }
8880
9018
  );
8881
9019
  }
8882
- function formatDate(dateStr) {
9020
+ function formatDate2(dateStr) {
8883
9021
  const date = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
8884
9022
  return date.toLocaleDateString(void 0, { weekday: "short", month: "short", day: "numeric" });
8885
9023
  }
@@ -9024,7 +9162,7 @@ function DateSlotPicker({
9024
9162
  "flex flex-col items-center justify-center rounded-md border border-border bg-background px-1 py-2 text-center text-xs font-medium text-foreground transition-colors hover:border-foreground/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[selected]:border-foreground data-[selected]:bg-foreground data-[selected]:text-background data-[fully-booked]:cursor-not-allowed data-[fully-booked]:opacity-40",
9025
9163
  classNames?.dateButton
9026
9164
  ),
9027
- children: formatDate(date)
9165
+ children: formatDate2(date)
9028
9166
  },
9029
9167
  date
9030
9168
  );
@@ -9084,6 +9222,7 @@ function ProductCustomizer({
9084
9222
  const [customerInputValues, setCustomerInputValues] = useState({});
9085
9223
  const [specialInstructions, setSpecialInstructions] = useState("");
9086
9224
  const [selectedSlot, setSelectedSlot] = useState(null);
9225
+ const [selectedStaffId, setSelectedStaffId] = useState(null);
9087
9226
  const cart = useCart();
9088
9227
  const currency = getProductCurrency(product);
9089
9228
  const productType = product.type || "product";
@@ -9111,7 +9250,16 @@ function ProductCustomizer({
9111
9250
  setCustomerInputValues({});
9112
9251
  setSpecialInstructions("");
9113
9252
  setSelectedSlot(null);
9253
+ setSelectedStaffId(null);
9114
9254
  }, [product.id, product.min_order_quantity]);
9255
+ const slotStaff = useMemo(
9256
+ () => (selectedSlot?.available_staff ?? []).map((member) => ({
9257
+ id: member.staff_id,
9258
+ name: member.name
9259
+ })),
9260
+ [selectedSlot]
9261
+ );
9262
+ const selectedStaffName = selectedStaffId ? slotStaff.find((member) => member.id === selectedStaffId)?.name ?? null : null;
9115
9263
  const selectedAddOnOptions = useMemo(() => {
9116
9264
  if (!product.add_ons) return [];
9117
9265
  const options = [];
@@ -9223,7 +9371,7 @@ function ProductCustomizer({
9223
9371
  ...isService && selectedSlot ? {
9224
9372
  scheduledStart: selectedSlot.start_time,
9225
9373
  scheduledEnd: selectedSlot.end_time,
9226
- staffId: selectedSlot.available_staff?.[0]?.staff_id,
9374
+ staffId: selectedStaffId ?? selectedSlot.available_staff?.[0]?.staff_id,
9227
9375
  resourceId: selectedSlot.available_resources?.[0]?.resource_id
9228
9376
  } : {},
9229
9377
  customerInputs: Object.keys(customerInputValues).length > 0 ? Object.entries(customerInputValues).filter(([, v]) => v !== void 0 && v !== "").map(([fieldId, value]) => ({ field_id: fieldId, value })) : void 0,
@@ -9245,7 +9393,7 @@ function ProductCustomizer({
9245
9393
  } finally {
9246
9394
  setIsSubmitting(false);
9247
9395
  }
9248
- }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, customerInputValues, specialInstructions, isService, selectedSlot, isSubmitting, onAddToCart, cart]);
9396
+ }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, customerInputValues, specialInstructions, isService, selectedSlot, selectedStaffId, isSubmitting, onAddToCart, cart]);
9249
9397
  return /* @__PURE__ */ jsxs("div", { "data-cimplify-customizer": true, className: cn("space-y-6", className, classNames?.root), children: [
9250
9398
  isComposite && product.groups && product.composite_id && /* @__PURE__ */ jsx(
9251
9399
  CompositeSelector,
@@ -9277,11 +9425,30 @@ function ProductCustomizer({
9277
9425
  {
9278
9426
  serviceId: product.id,
9279
9427
  selectedSlot,
9280
- onSlotSelect: (slot) => setSelectedSlot(slot),
9428
+ onSlotSelect: (slot) => {
9429
+ setSelectedSlot(slot);
9430
+ setSelectedStaffId(null);
9431
+ },
9281
9432
  participantCount: quantity,
9282
9433
  currency
9283
9434
  }
9284
9435
  ),
9436
+ isService && slotStaff.length > 0 && /* @__PURE__ */ jsxs("div", { "data-cimplify-customizer-staff": true, children: [
9437
+ /* @__PURE__ */ jsx("label", { className: "block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3", children: "Choose your specialist" }),
9438
+ /* @__PURE__ */ jsx(
9439
+ StaffPicker,
9440
+ {
9441
+ staff: slotStaff,
9442
+ selectedStaffId,
9443
+ onStaffSelect: setSelectedStaffId,
9444
+ className: "flex flex-wrap gap-2",
9445
+ classNames: {
9446
+ option: "inline-flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium transition-colors hover:border-foreground/40 cursor-pointer data-[selected]:border-foreground data-[selected]:bg-foreground data-[selected]:text-background",
9447
+ avatar: "w-6 h-6 rounded-full object-cover"
9448
+ }
9449
+ }
9450
+ )
9451
+ ] }),
9285
9452
  hasVariants && /* @__PURE__ */ jsx(
9286
9453
  VariantSelector,
9287
9454
  {
@@ -9366,6 +9533,18 @@ function ProductCustomizer({
9366
9533
  )
9367
9534
  }
9368
9535
  ) }),
9536
+ isService && selectedSlot && /* @__PURE__ */ jsx(
9537
+ BookingSummary,
9538
+ {
9539
+ slot: selectedSlot,
9540
+ total: displayTotalPrice,
9541
+ staffName: selectedStaffName,
9542
+ durationMinutes: product.duration_minutes,
9543
+ depositType: product.deposit_type,
9544
+ depositAmount: product.deposit_amount,
9545
+ currency
9546
+ }
9547
+ ),
9369
9548
  /* @__PURE__ */ jsxs(
9370
9549
  "div",
9371
9550
  {
@@ -12481,65 +12660,6 @@ function StoreNav({
12481
12660
  ] })
12482
12661
  ] });
12483
12662
  }
12484
- var ANY_VALUE = "__any__";
12485
- function StaffPicker({
12486
- staff,
12487
- selectedStaffId,
12488
- onStaffSelect,
12489
- showAnyOption = true,
12490
- anyLabel = "Any available",
12491
- className,
12492
- classNames
12493
- }) {
12494
- const groupValue = selectedStaffId === null ? ANY_VALUE : selectedStaffId ?? "";
12495
- return /* @__PURE__ */ jsxs(
12496
- RadioGroup,
12497
- {
12498
- "data-cimplify-staff-picker": true,
12499
- className: cn(className, classNames?.root),
12500
- value: groupValue,
12501
- onValueChange: (value) => {
12502
- onStaffSelect?.(value === ANY_VALUE ? null : value);
12503
- },
12504
- children: [
12505
- showAnyOption && /* @__PURE__ */ jsx(
12506
- Radio.Root,
12507
- {
12508
- value: ANY_VALUE,
12509
- "data-cimplify-staff-option": true,
12510
- "data-selected": selectedStaffId === null || void 0,
12511
- "data-any": true,
12512
- className: classNames?.option,
12513
- children: /* @__PURE__ */ jsx("span", { "data-cimplify-staff-name": true, className: classNames?.name, children: anyLabel })
12514
- }
12515
- ),
12516
- staff.map((member) => /* @__PURE__ */ jsxs(
12517
- Radio.Root,
12518
- {
12519
- value: member.id,
12520
- "data-cimplify-staff-option": true,
12521
- "data-selected": selectedStaffId === member.id || void 0,
12522
- className: classNames?.option,
12523
- children: [
12524
- member.avatar_url && /* @__PURE__ */ jsx(
12525
- "img",
12526
- {
12527
- src: member.avatar_url,
12528
- alt: member.name,
12529
- "data-cimplify-staff-avatar": true,
12530
- className: classNames?.avatar
12531
- }
12532
- ),
12533
- /* @__PURE__ */ jsx("span", { "data-cimplify-staff-name": true, className: classNames?.name, children: member.name }),
12534
- member.bio && /* @__PURE__ */ jsx("span", { "data-cimplify-staff-bio": true, className: classNames?.bio, children: member.bio })
12535
- ]
12536
- },
12537
- member.id
12538
- ))
12539
- ]
12540
- }
12541
- );
12542
- }
12543
12663
  var STATUS_LABELS3 = {
12544
12664
  pending: "Pending",
12545
12665
  confirmed: "Confirmed",
@@ -16632,4 +16752,4 @@ function SparkleIcon({ className }) {
16632
16752
  );
16633
16753
  }
16634
16754
 
16635
- export { AccommodationCard, AccountContent, AccountDashboardPage, AccountHero, AccountIdentityStrip, AccountLineItem, AccountLineItems, AccountNav, AccountOrderDetailPage, AccountOrderView, AccountOrdersPage, AccountShell, AccountSidebar, AccountSignedOutPrompt, AccountSubscriptionDetailPage, AccountSubscriptionsPage, Ad, AdProvider, AddOnSelector, AddressElement, AuthElement, AvailabilityBadge, BackLink, BillingPlanSelector, BookingCard, BookingList, BookingPage, BookingsPage, BoxIcon24, BundleProductCard, BundleProductLayout, BundleSelector, Button, CardImage, CardShell, CardVariant, CartDrawer, CartDrawerProvider, CartPage, CartSummary, CartTemplate, CatalogueCollectionLayout, CataloguePage, CatalogueTemplate, CategoryFilter, CategoryGrid, ChatWidget, CheckIcon, CheckoutPage, CimplifyCheckout, CimplifyProvider, CimplifySignInButton, CimplifyUserButton, CollectionPage, CollectionTemplate, CompactCartLayout, CompactCatalogueLayout, CompactSearchLayout, CompactServiceCard, CompositeProductCard, CompositeProductLayout, CompositeSelector, ContextCard, ContinuityCard, CurrencySelector, CustomAttributesTable, CustomerInputFields, DashboardIcon, DashboardIcon20, DatePicker, DateSlotPicker, DealBanner, DealsPage, DefaultCartLayout, DefaultCatalogueLayout, DefaultCollectionLayout, DefaultProductLayout, DefaultSearchLayout, DeliveryEstimate, DeliveryTracker, DeliveryTruckIcon, DigitalProductCard, DigitalProductLayout, DiscountInput, ElementsProvider, EmptyState, ErrorState, FeaturedCollectionLayout, FilterTabs, FoodProductCard, FoodProductLayout, HappeningCard, IdentityIcon, IdentityOfframp, InventoryBadge, ItemsCard, LeaseServiceCard, LineItemDetails, LocationPicker, MediaGallery, MetadataStringList, MobileAccountShell, MobileBackButton, MobileMoreButton, MobileTabBar, NextDeliveryCard, OrderDetailHeader, OrderDetailMetaPipe, OrderDetailPage, OrderHistory, OrderHistoryPage, OrderList, OrderRow, OrderSummary, OrderTimeline, OrderTotals, OrdersIcon, OrdersIcon20, OvenIcon, PackageIcon, PaymentElement, Price, PriceRange, ProductCard, ProductCustomizer, ProductGrid, ProductImageGallery, ProductModel3D, ProductPage, ProductSheet, ProductTemplate, PropertiesTable, QuantitySelector, QuickAddButton, RecentlyViewed, RecommendationCarousel, RelatedProductsSection, RentalServiceCard, ResourcePicker, RetailProductCard, RichText, SaleBadge, ScheduleServiceCard, SearchInput, SearchPage, SearchTemplate, Section, SectionMore, ServiceProductLayout, SessionMessageBanner, SignOutIcon, Skeleton, SlotPicker, SoldOutOverlay, StaffPicker, StandardServiceCard, Status, StoreNav, StoreVideo, SubscriptionCard, SubscriptionList, SubscriptionRow, SubscriptionsIcon, SubscriptionsIcon20, TagPills, TimePicker, Toast, TwoColumnGrid, VariantSelector, VolumePricing, WelcomeCard, WholesaleProductCard, WholesaleProductLayout, WishlistButton, cn, roomToResource, sanitizeRichTextHtml, stripHtmlToText, toAccountLineItem, toAccountLineItems, toAccountOrderView, useActivityState, useAds, useAttributeDefinitions, useAvailableSlots, useBillingPlans, useBookings, useBootstrap, useBundles, useCart, useCartDrawer, useCategories, useChat, useCheckout, useCimplify, useCimplifyClient, useCimplifySession, useCollection, useCollections, useDeals, useDeliveryFee, useElements, useElementsReady, useFxRate, useLocations, useOptionalCimplify, useOrder, useOrders, useProduct, useProductAvailability, useProductDeals, useProductPrice, useProductSchedules, useProducts, useProductsOnSale, usePropertyFacets, useQuote, useRecommendations, useSearch, useServiceAvailability, useServices, useSubscription, useSubscriptions, useTaxonomies, useTaxonomy, useTaxonomyPath, useValidateDiscount, useVariantSelector };
16755
+ export { AccommodationCard, AccountContent, AccountDashboardPage, AccountHero, AccountIdentityStrip, AccountLineItem, AccountLineItems, AccountNav, AccountOrderDetailPage, AccountOrderView, AccountOrdersPage, AccountShell, AccountSidebar, AccountSignedOutPrompt, AccountSubscriptionDetailPage, AccountSubscriptionsPage, Ad, AdProvider, AddOnSelector, AddressElement, AuthElement, AvailabilityBadge, BackLink, BillingPlanSelector, BookingCard, BookingList, BookingPage, BookingSummary, BookingsPage, BoxIcon24, BundleProductCard, BundleProductLayout, BundleSelector, Button, CardImage, CardShell, CardVariant, CartDrawer, CartDrawerProvider, CartPage, CartSummary, CartTemplate, CatalogueCollectionLayout, CataloguePage, CatalogueTemplate, CategoryFilter, CategoryGrid, ChatWidget, CheckIcon, CheckoutPage, CimplifyCheckout, CimplifyProvider, CimplifySignInButton, CimplifyUserButton, CollectionPage, CollectionTemplate, CompactCartLayout, CompactCatalogueLayout, CompactSearchLayout, CompactServiceCard, CompositeProductCard, CompositeProductLayout, CompositeSelector, ContextCard, ContinuityCard, CurrencySelector, CustomAttributesTable, CustomerInputFields, DashboardIcon, DashboardIcon20, DatePicker, DateSlotPicker, DealBanner, DealsPage, DefaultCartLayout, DefaultCatalogueLayout, DefaultCollectionLayout, DefaultProductLayout, DefaultSearchLayout, DeliveryEstimate, DeliveryTracker, DeliveryTruckIcon, DigitalProductCard, DigitalProductLayout, DiscountInput, ElementsProvider, EmptyState, ErrorState, FeaturedCollectionLayout, FilterTabs, FoodProductCard, FoodProductLayout, HappeningCard, IdentityIcon, IdentityOfframp, InventoryBadge, ItemsCard, LeaseServiceCard, LineItemDetails, LocationPicker, MediaGallery, MetadataStringList, MobileAccountShell, MobileBackButton, MobileMoreButton, MobileTabBar, NextDeliveryCard, OrderDetailHeader, OrderDetailMetaPipe, OrderDetailPage, OrderHistory, OrderHistoryPage, OrderList, OrderRow, OrderSummary, OrderTimeline, OrderTotals, OrdersIcon, OrdersIcon20, OvenIcon, PackageIcon, PaymentElement, Price, PriceRange, ProductCard, ProductCustomizer, ProductGrid, ProductImageGallery, ProductModel3D, ProductPage, ProductSheet, ProductTemplate, PropertiesTable, QuantitySelector, QuickAddButton, RecentlyViewed, RecommendationCarousel, RelatedProductsSection, RentalServiceCard, ResourcePicker, RetailProductCard, RichText, SaleBadge, ScheduleServiceCard, SearchInput, SearchPage, SearchTemplate, Section, SectionMore, ServiceProductLayout, SessionMessageBanner, SignOutIcon, Skeleton, SlotPicker, SoldOutOverlay, StaffPicker, StandardServiceCard, Status, StoreNav, StoreVideo, SubscriptionCard, SubscriptionList, SubscriptionRow, SubscriptionsIcon, SubscriptionsIcon20, TagPills, TimePicker, Toast, TwoColumnGrid, VariantSelector, VolumePricing, WelcomeCard, WholesaleProductCard, WholesaleProductLayout, WishlistButton, cn, roomToResource, sanitizeRichTextHtml, stripHtmlToText, toAccountLineItem, toAccountLineItems, toAccountOrderView, useActivityState, useAds, useAttributeDefinitions, useAvailableSlots, useBillingPlans, useBookings, useBootstrap, useBundles, useCart, useCartDrawer, useCategories, useChat, useCheckout, useCimplify, useCimplifyClient, useCimplifySession, useCollection, useCollections, useDeals, useDeliveryFee, useElements, useElementsReady, useFxRate, useLocations, useOptionalCimplify, useOrder, useOrders, useProduct, useProductAvailability, useProductDeals, useProductPrice, useProductSchedules, useProducts, useProductsOnSale, usePropertyFacets, useQuote, useRecommendations, useSearch, useServiceAvailability, useServices, useSubscription, useSubscriptions, useTaxonomies, useTaxonomy, useTaxonomyPath, useValidateDiscount, useVariantSelector };
@@ -1,5 +1,5 @@
1
1
  import { Hono } from 'hono';
2
- import { b1 as ProductInputField } from './product-B_kS4Oxa.js';
2
+ import { b1 as ProductInputField } from './product-_GFoaPHU.js';
3
3
 
4
4
  interface Clock {
5
5
  now(): Date;
@@ -1,5 +1,5 @@
1
1
  import { Hono } from 'hono';
2
- import { b1 as ProductInputField } from './product-B_kS4Oxa.mjs';
2
+ import { b1 as ProductInputField } from './product-_GFoaPHU.mjs';
3
3
 
4
4
  interface Clock {
5
5
  now(): Date;
package/dist/server.d.mts CHANGED
@@ -1,8 +1,8 @@
1
- import { ad as CimplifyClient } from './client-C7EMuGoz.mjs';
2
- export { K as CacheOptions, cq as ReadRequestOptions, cM as Result } from './client-C7EMuGoz.mjs';
3
- export { Q as Category, T as CimplifyError, U as Collection, aX as Product, b9 as ProductWithDetails } from './product-B_kS4Oxa.mjs';
1
+ import { ad as CimplifyClient } from './client-D7uHF_jh.mjs';
2
+ export { K as CacheOptions, cq as ReadRequestOptions, cM as Result } from './client-D7uHF_jh.mjs';
3
+ export { Q as Category, T as CimplifyError, U as Collection, aX as Product, b9 as ProductWithDetails } from './product-_GFoaPHU.mjs';
4
4
  import { JWTVerifyResult } from 'jose';
5
- import './payment-DVS7ZUEp.mjs';
5
+ import './payment-C-2gApyQ.mjs';
6
6
 
7
7
  interface ServerClientOptions {
8
8
  /**
package/dist/server.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { ad as CimplifyClient } from './client-D6CZ2Ggp.js';
2
- export { K as CacheOptions, cq as ReadRequestOptions, cM as Result } from './client-D6CZ2Ggp.js';
3
- export { Q as Category, T as CimplifyError, U as Collection, aX as Product, b9 as ProductWithDetails } from './product-B_kS4Oxa.js';
1
+ import { ad as CimplifyClient } from './client-xDpke-KS.js';
2
+ export { K as CacheOptions, cq as ReadRequestOptions, cM as Result } from './client-xDpke-KS.js';
3
+ export { Q as Category, T as CimplifyError, U as Collection, aX as Product, b9 as ProductWithDetails } from './product-_GFoaPHU.js';
4
4
  import { JWTVerifyResult } from 'jose';
5
- import './payment-sn-yGL7v.js';
5
+ import './payment-CfemTORM.js';
6
6
 
7
7
  interface ServerClientOptions {
8
8
  /**
@@ -1,6 +1,6 @@
1
- import { A as AppHandle, C as CreateAppOptions } from '../server-DDQ1O3XR.mjs';
1
+ import { A as AppHandle, C as CreateAppOptions } from '../server-G8I4JtqK.mjs';
2
2
  import 'hono';
3
- import '../product-B_kS4Oxa.mjs';
3
+ import '../product-_GFoaPHU.mjs';
4
4
 
5
5
  interface MswHandlerSetup {
6
6
  handlers: unknown[];
@@ -1,6 +1,6 @@
1
- import { A as AppHandle, C as CreateAppOptions } from '../server-DMvQp27Z.js';
1
+ import { A as AppHandle, C as CreateAppOptions } from '../server-1hXIXH2k.js';
2
2
  import 'hono';
3
- import '../product-B_kS4Oxa.js';
3
+ import '../product-_GFoaPHU.js';
4
4
 
5
5
  interface MswHandlerSetup {
6
6
  handlers: unknown[];
@@ -1,10 +1,10 @@
1
1
  import { TestAPI } from 'vitest';
2
- import { T as TestClientHandle } from '../client-DWA_XIh3.mjs';
3
- import { C as CreateAppOptions } from '../server-DDQ1O3XR.mjs';
4
- export { S as SeedName } from '../server-DDQ1O3XR.mjs';
5
- import '../client-C7EMuGoz.mjs';
6
- import '../product-B_kS4Oxa.mjs';
7
- import '../payment-DVS7ZUEp.mjs';
2
+ import { T as TestClientHandle } from '../client-yFghjc1-.mjs';
3
+ import { C as CreateAppOptions } from '../server-G8I4JtqK.mjs';
4
+ export { S as SeedName } from '../server-G8I4JtqK.mjs';
5
+ import '../client-D7uHF_jh.mjs';
6
+ import '../product-_GFoaPHU.mjs';
7
+ import '../payment-C-2gApyQ.mjs';
8
8
  import 'hono';
9
9
 
10
10
  /**
@@ -1,10 +1,10 @@
1
1
  import { TestAPI } from 'vitest';
2
- import { T as TestClientHandle } from '../client-BvqaIlJ0.js';
3
- import { C as CreateAppOptions } from '../server-DMvQp27Z.js';
4
- export { S as SeedName } from '../server-DMvQp27Z.js';
5
- import '../client-D6CZ2Ggp.js';
6
- import '../product-B_kS4Oxa.js';
7
- import '../payment-sn-yGL7v.js';
2
+ import { T as TestClientHandle } from '../client--w53owD4.js';
3
+ import { C as CreateAppOptions } from '../server-1hXIXH2k.js';
4
+ export { S as SeedName } from '../server-1hXIXH2k.js';
5
+ import '../client-xDpke-KS.js';
6
+ import '../product-_GFoaPHU.js';
7
+ import '../payment-CfemTORM.js';
8
8
  import 'hono';
9
9
 
10
10
  /**
@@ -1,10 +1,10 @@
1
1
  import { z } from 'zod';
2
2
  import { $ZodType } from 'zod/v4/core';
3
- export { A as AddFirstProductOptions, a as AddFirstProductResult, P as ProductSummary, T as TestClientHandle, c as createTestClient, f as fixtures } from './client-DWA_XIh3.mjs';
4
- import './client-C7EMuGoz.mjs';
5
- import './product-B_kS4Oxa.mjs';
6
- import './payment-DVS7ZUEp.mjs';
7
- import './server-DDQ1O3XR.mjs';
3
+ export { A as AddFirstProductOptions, a as AddFirstProductResult, P as ProductSummary, T as TestClientHandle, c as createTestClient, f as fixtures } from './client-yFghjc1-.mjs';
4
+ import './client-D7uHF_jh.mjs';
5
+ import './product-_GFoaPHU.mjs';
6
+ import './payment-C-2gApyQ.mjs';
7
+ import './server-G8I4JtqK.mjs';
8
8
  import 'hono';
9
9
 
10
10
  /**
package/dist/testing.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { z } from 'zod';
2
2
  import { $ZodType } from 'zod/v4/core';
3
- export { A as AddFirstProductOptions, a as AddFirstProductResult, P as ProductSummary, T as TestClientHandle, c as createTestClient, f as fixtures } from './client-BvqaIlJ0.js';
4
- import './client-D6CZ2Ggp.js';
5
- import './product-B_kS4Oxa.js';
6
- import './payment-sn-yGL7v.js';
7
- import './server-DMvQp27Z.js';
3
+ export { A as AddFirstProductOptions, a as AddFirstProductResult, P as ProductSummary, T as TestClientHandle, c as createTestClient, f as fixtures } from './client--w53owD4.js';
4
+ import './client-xDpke-KS.js';
5
+ import './product-_GFoaPHU.js';
6
+ import './payment-CfemTORM.js';
7
+ import './server-1hXIXH2k.js';
8
8
  import 'hono';
9
9
 
10
10
  /**
package/dist/utils.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, c as categorizePaymentError, d as detectMobileMoneyProvider, f as formatMoney, a as formatNumberCompact, b as formatPrice, e as formatPriceAdjustment, g as formatPriceCompact, i as formatPriceWithTax, j as formatProductPrice, k as getBasePrice, l as getCurrencySymbol, m as getDiscountPercentage, n as getDisplayPrice, o as getMarkupPercentage, q as getProductCurrency, r as getTaxAmount, t as hasTaxInfo, u as isOnSale, v as isPaymentStatusFailure, w as isPaymentStatusRequiresAction, x as isPaymentStatusSuccess, y as isTaxInclusive, z as isValidPhoneForContext, A as normalizePaymentResponse, B as normalizePhoneToE164, D as normalizeStatusResponse, E as parsePrice, F as resolvePhoneCountryCode } from './index-D8vnKlOQ.mjs';
2
- import './product-B_kS4Oxa.mjs';
3
- import './price-Cm0tU0V5.mjs';
4
- import './payment-DVS7ZUEp.mjs';
1
+ export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, c as categorizePaymentError, d as detectMobileMoneyProvider, f as formatMoney, a as formatNumberCompact, b as formatPrice, e as formatPriceAdjustment, g as formatPriceCompact, i as formatPriceWithTax, j as formatProductPrice, k as getBasePrice, l as getCurrencySymbol, m as getDiscountPercentage, n as getDisplayPrice, o as getMarkupPercentage, q as getProductCurrency, r as getTaxAmount, t as hasTaxInfo, u as isOnSale, v as isPaymentStatusFailure, w as isPaymentStatusRequiresAction, x as isPaymentStatusSuccess, y as isTaxInclusive, z as isValidPhoneForContext, A as normalizePaymentResponse, B as normalizePhoneToE164, D as normalizeStatusResponse, E as parsePrice, F as resolvePhoneCountryCode } from './index-lt-fe29V.mjs';
2
+ import './product-_GFoaPHU.mjs';
3
+ import './price-CZBQw-Rh.mjs';
4
+ import './payment-C-2gApyQ.mjs';
5
5
  import 'libphonenumber-js';
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, c as categorizePaymentError, d as detectMobileMoneyProvider, f as formatMoney, a as formatNumberCompact, b as formatPrice, e as formatPriceAdjustment, g as formatPriceCompact, i as formatPriceWithTax, j as formatProductPrice, k as getBasePrice, l as getCurrencySymbol, m as getDiscountPercentage, n as getDisplayPrice, o as getMarkupPercentage, q as getProductCurrency, r as getTaxAmount, t as hasTaxInfo, u as isOnSale, v as isPaymentStatusFailure, w as isPaymentStatusRequiresAction, x as isPaymentStatusSuccess, y as isTaxInclusive, z as isValidPhoneForContext, A as normalizePaymentResponse, B as normalizePhoneToE164, D as normalizeStatusResponse, E as parsePrice, F as resolvePhoneCountryCode } from './index-DIIlPUOC.js';
2
- import './product-B_kS4Oxa.js';
3
- import './price-9T1Y47oS.js';
4
- import './payment-sn-yGL7v.js';
1
+ export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, c as categorizePaymentError, d as detectMobileMoneyProvider, f as formatMoney, a as formatNumberCompact, b as formatPrice, e as formatPriceAdjustment, g as formatPriceCompact, i as formatPriceWithTax, j as formatProductPrice, k as getBasePrice, l as getCurrencySymbol, m as getDiscountPercentage, n as getDisplayPrice, o as getMarkupPercentage, q as getProductCurrency, r as getTaxAmount, t as hasTaxInfo, u as isOnSale, v as isPaymentStatusFailure, w as isPaymentStatusRequiresAction, x as isPaymentStatusSuccess, y as isTaxInclusive, z as isValidPhoneForContext, A as normalizePaymentResponse, B as normalizePhoneToE164, D as normalizeStatusResponse, E as parsePrice, F as resolvePhoneCountryCode } from './index-DTGS0QC1.js';
2
+ import './product-_GFoaPHU.js';
3
+ import './price-DNeyoO7y.js';
4
+ import './payment-CfemTORM.js';
5
5
  import 'libphonenumber-js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cimplify/sdk",
3
- "version": "0.73.0",
3
+ "version": "0.75.0",
4
4
  "description": "Cimplify Commerce SDK for storefronts",
5
5
  "keywords": [
6
6
  "cimplify",
@@ -14,7 +14,7 @@
14
14
  "files": [
15
15
  {
16
16
  "path": "product-customizer.tsx",
17
- "content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect, useRef } from \"react\";\nimport { Button } from \"@base-ui/react/button\";\nimport type {\n ProductWithDetails,\n VariantView,\n AddOnOption,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport type { ProductBillingPlan } from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice, getUnitPriceAtQuantity, getProductCurrency } from \"@cimplify/sdk\";\nimport { formatDuration } from \"./utils/format-duration\";\nimport { useCart, useQuote } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { QuantitySelector } from \"@cimplify/sdk/react\";\nimport { VariantSelector } from \"@cimplify/sdk/react\";\nimport { AddOnSelector } from \"@cimplify/sdk/react\";\nimport { CompositeSelector } from \"@cimplify/sdk/react\";\nimport { BundleSelector } from \"@cimplify/sdk/react\";\nimport { BillingPlanSelector } from \"@cimplify/sdk/react\";\nimport { VolumePricing } from \"@cimplify/sdk/react\";\nimport { CustomerInputFields } from \"./customer-input-fields\";\nimport { DateSlotPicker } from \"@cimplify/sdk/react\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ProductCustomizerClassNames {\n root?: string;\n actions?: string;\n submitButton?: string;\n submitButtonAdded?: string;\n validation?: string;\n specialInstructions?: string;\n depositInfo?: string;\n allergens?: string;\n duration?: string;\n}\n\nexport interface ProductCustomizerProps {\n product: ProductWithDetails;\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n /** Lets the parent swap its gallery for `variant.images` on selection. */\n onVariantChange?: (\n variantId: string | undefined,\n variant: VariantView | undefined,\n ) => void;\n showSpecialInstructions?: boolean;\n className?: string;\n classNames?: ProductCustomizerClassNames;\n}\n\nexport function ProductCustomizer({\n product,\n onAddToCart,\n onVariantChange,\n showSpecialInstructions = true,\n className,\n classNames,\n}: ProductCustomizerProps): React.ReactElement {\n const [quantity, setQuantity] = useState(product.min_order_quantity ?? 1);\n const [isAdded, setIsAdded] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [selectedVariantId, setSelectedVariantId] = useState<string | undefined>();\n const [selectedVariant, setSelectedVariant] = useState<VariantView | undefined>();\n const [selectedAddOnOptionIds, setSelectedAddOnOptionIds] = useState<string[]>([]);\n\n const [compositeSelections, setCompositeSelections] = useState<ComponentSelectionInput[]>([]);\n const [compositePrice, setCompositePrice] = useState<CompositePriceResult | null>(null);\n const [compositeReady, setCompositeReady] = useState(false);\n\n const [bundleSelections, setBundleSelections] = useState<BundleSelectionInput[]>([]);\n const [bundleTotalPrice, setBundleTotalPrice] = useState<number | null>(null);\n const [bundleReady, setBundleReady] = useState(false);\n const [selectedBillingPlan, setSelectedBillingPlan] = useState<ProductBillingPlan | null>(null);\n const [customerInputValues, setCustomerInputValues] = useState<Record<string, unknown>>({});\n const [specialInstructions, setSpecialInstructions] = useState(\"\");\n const [selectedSlot, setSelectedSlot] = useState<AvailableSlot | null>(null);\n\n const cart = useCart();\n\n const currency = getProductCurrency(product);\n const productType = product.type || \"product\";\n const isComposite = productType === \"composite\";\n const isBundle = productType === \"bundle\";\n const isDigital = productType === \"digital\";\n const isService = productType === \"service\";\n const isStandard = !isComposite && !isBundle;\n\n const hasVariants = isStandard && product.variants && product.variants.length > 0;\n const hasAddOns = isStandard && product.add_ons && product.add_ons.length > 0;\n\n useEffect(() => {\n setQuantity(product.min_order_quantity ?? 1);\n setIsAdded(false);\n setIsSubmitting(false);\n setSelectedVariantId(undefined);\n setSelectedVariant(undefined);\n setSelectedAddOnOptionIds([]);\n setCompositeSelections([]);\n setCompositePrice(null);\n setCompositeReady(false);\n setBundleSelections([]);\n setBundleTotalPrice(null);\n setBundleReady(false);\n setSelectedBillingPlan(null);\n setCustomerInputValues({});\n setSpecialInstructions(\"\");\n setSelectedSlot(null);\n }, [product.id, product.min_order_quantity]);\n\n const selectedAddOnOptions = useMemo(() => {\n if (!product.add_ons) return [];\n const options: AddOnOption[] = [];\n for (const addOn of product.add_ons) {\n for (const option of addOn.options) {\n if (selectedAddOnOptionIds.includes(option.id)) {\n options.push(option);\n }\n }\n }\n return options;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const normalizedAddOnOptionIds = useMemo(() => {\n if (selectedAddOnOptionIds.length === 0) return [];\n return Array.from(new Set(selectedAddOnOptionIds.map((id) => id.trim()).filter(Boolean))).sort();\n }, [selectedAddOnOptionIds]);\n\n const localTotalPrice = useMemo(() => {\n if (isComposite && compositePrice) {\n return parsePrice(compositePrice.final_price) * quantity;\n }\n\n if (isBundle && bundleTotalPrice != null) {\n return bundleTotalPrice * quantity;\n }\n\n let price = parsePrice(product.default_price);\n\n if (selectedVariant?.price_adjustment) {\n price += parsePrice(selectedVariant.price_adjustment);\n }\n\n for (const option of selectedAddOnOptions) {\n if (option.default_price) {\n price += parsePrice(option.default_price);\n }\n }\n\n return price * quantity;\n }, [product.default_price, selectedVariant, selectedAddOnOptions, quantity, isComposite, compositePrice, isBundle, bundleTotalPrice]);\n\n const requiredAddOnsSatisfied = useMemo(() => {\n if (!product.add_ons) return true;\n\n for (const addOn of product.add_ons) {\n if (addOn.is_required) {\n const selectedInGroup = selectedAddOnOptionIds.filter((id) =>\n addOn.options.some((opt) => opt.id === id),\n ).length;\n\n const minRequired = addOn.min_selections || 1;\n if (selectedInGroup < minRequired) {\n return false;\n }\n }\n }\n return true;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const quoteInput = useMemo(\n () => ({\n productId: product.id,\n quantity,\n variantId: selectedVariantId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n }),\n [product.id, quantity, selectedVariantId, normalizedAddOnOptionIds, isBundle, bundleSelections, isComposite, compositeSelections],\n );\n\n const requiredInputsSatisfied = useMemo(() => {\n if (!product.input_fields || product.input_fields.length === 0) return true;\n return product.input_fields.every((field) => {\n if (!field.is_required) return true;\n const val = customerInputValues[field.id];\n return val !== undefined && val !== \"\" && val !== null;\n });\n }, [product.input_fields, customerInputValues]);\n\n const quoteEnabled = isComposite\n ? compositeReady && requiredInputsSatisfied\n : isBundle\n ? bundleReady && requiredInputsSatisfied\n : requiredAddOnsSatisfied && requiredInputsSatisfied;\n\n const { quote } = useQuote(quoteInput, {\n enabled: quoteEnabled,\n });\n\n const quoteId = quote?.quote_id;\n const quotedTotalPrice = useMemo(() => {\n if (!quote) return undefined;\n\n const priceInfo = quote.quoted_total_price_info ?? quote.final_price_info;\n const perUnit = priceInfo.pre_tax_price;\n if (perUnit === undefined || perUnit === null) return undefined;\n return parsePrice(perUnit) * quantity;\n }, [quote, quantity]);\n\n const displayTotalPrice = quotedTotalPrice ?? localTotalPrice;\n\n const handleVariantChange = useCallback(\n (variantId: string | undefined, variant: VariantView | undefined) => {\n setSelectedVariantId(variantId);\n setSelectedVariant(variant);\n onVariantChange?.(variantId, variant);\n },\n [onVariantChange],\n );\n\n const addedTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n useEffect(() => () => clearTimeout(addedTimerRef.current), []);\n\n const handleAddToCart = useCallback(async () => {\n if (isSubmitting) return;\n\n setIsSubmitting(true);\n\n const options: AddToCartOptions = {\n variantId: selectedVariantId,\n variant: selectedVariant\n ? { id: selectedVariant.id, name: selectedVariant.name || \"\", price_adjustment: selectedVariant.price_adjustment }\n : undefined,\n quoteId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n addOnOptions: selectedAddOnOptions.length > 0\n ? selectedAddOnOptions.map((opt) => ({\n id: opt.id,\n name: opt.name,\n add_on_id: opt.add_on_id,\n default_price: opt.default_price,\n }))\n : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n billingPlanId: selectedBillingPlan?.id,\n ...(isService && selectedSlot\n ? {\n scheduledStart: selectedSlot.start_time,\n scheduledEnd: selectedSlot.end_time,\n staffId: selectedSlot.available_staff?.[0]?.staff_id,\n resourceId: selectedSlot.available_resources?.[0]?.resource_id,\n }\n : {}),\n customerInputs: Object.keys(customerInputValues).length > 0\n ? Object.entries(customerInputValues)\n .filter(([, v]) => v !== undefined && v !== \"\")\n .map(([fieldId, value]) => ({ field_id: fieldId, value }))\n : undefined,\n specialInstructions: specialInstructions.trim() || undefined,\n };\n\n try {\n if (onAddToCart) {\n await onAddToCart(product, quantity, options);\n } else {\n await cart.addItem(product, quantity, options);\n }\n setIsAdded(true);\n clearTimeout(addedTimerRef.current);\n addedTimerRef.current = setTimeout(() => {\n setIsAdded(false);\n setQuantity(product.min_order_quantity ?? 1);\n }, 2000);\n } catch {\n // Caller handles errors via onAddToCart or cart hook error state\n } finally {\n setIsSubmitting(false);\n }\n }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, customerInputValues, specialInstructions, isService, selectedSlot, isSubmitting, onAddToCart, cart]);\n\n return (\n <div data-cimplify-customizer className={cn(\"space-y-6\", className, classNames?.root)}>\n {isComposite && product.groups && product.composite_id && (\n <CompositeSelector\n compositeId={product.composite_id}\n groups={product.groups}\n currency={currency}\n onSelectionsChange={setCompositeSelections}\n onPriceChange={setCompositePrice}\n onReady={setCompositeReady}\n skipPriceFetch\n />\n )}\n\n {isBundle && product.components && (\n <BundleSelector\n components={product.components}\n bundlePrice={product.bundle_price}\n discountValue={product.discount_value}\n pricingType={product.pricing_type}\n currency={currency}\n onSelectionsChange={setBundleSelections}\n onPriceChange={setBundleTotalPrice}\n onReady={setBundleReady}\n />\n )}\n\n {isService && (\n <DateSlotPicker\n serviceId={product.id}\n selectedSlot={selectedSlot}\n onSlotSelect={(slot) => setSelectedSlot(slot)}\n participantCount={quantity}\n currency={currency}\n />\n )}\n\n {hasVariants && (\n <VariantSelector\n variants={product.variants!}\n variantAxes={product.variant_axes}\n basePrice={product.default_price}\n currency={currency}\n selectedVariantId={selectedVariantId}\n onVariantChange={handleVariantChange}\n productName={product.name}\n />\n )}\n\n {hasAddOns && (\n <AddOnSelector\n addOns={product.add_ons!}\n selectedOptions={selectedAddOnOptionIds}\n onOptionsChange={setSelectedAddOnOptionIds}\n currency={currency}\n />\n )}\n\n {/* Billing plans */}\n {product.billing_plans && product.billing_plans.length > 0 && (\n <BillingPlanSelector\n productId={product.id}\n plans={product.billing_plans}\n currency={currency}\n onPlanSelect={setSelectedBillingPlan}\n selectedPlanId={selectedBillingPlan?.id ?? null}\n showOneTimePurchase\n />\n )}\n\n {/* Volume pricing tiers */}\n {product.quantity_pricing && product.quantity_pricing.length > 1 && (\n <VolumePricing\n tiers={product.quantity_pricing}\n currentQuantity={quantity}\n currency={currency}\n />\n )}\n\n {/* Customer input fields */}\n {product.input_fields && product.input_fields.length > 0 && (\n <CustomerInputFields\n fields={product.input_fields}\n values={customerInputValues}\n onChange={setCustomerInputValues}\n currency={currency}\n />\n )}\n\n {/* Deposit info for service products */}\n {product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount && (\n <div data-cimplify-customizer-deposit className={cn(\"text-sm text-muted-foreground\", classNames?.depositInfo)}>\n {product.deposit_type === \"fixed\" ? (\n <span>Deposit required: <Price amount={product.deposit_amount} currency={currency} /></span>\n ) : (\n <span>Deposit required: {parsePrice(product.deposit_amount)}%</span>\n )}\n </div>\n )}\n\n {/* Allergens */}\n {product.allergies && product.allergies.length > 0 && (\n <div data-cimplify-customizer-allergens className={cn(\"flex flex-wrap gap-1.5\", classNames?.allergens)}>\n {product.allergies.map((allergen) => (\n <span\n key={allergen}\n data-cimplify-allergen-tag\n className=\"inline-block text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground\"\n >\n {allergen}\n </span>\n ))}\n </div>\n )}\n\n {/* Service duration */}\n {isService && product.duration_minutes != null && (\n <div data-cimplify-customizer-duration className={cn(\"text-sm text-muted-foreground\", classNames?.duration)}>\n Duration: {formatDuration(product.duration_minutes, product.duration_unit)}\n </div>\n )}\n\n {/* Special instructions */}\n {showSpecialInstructions && !isDigital && (\n <div data-cimplify-customizer-special-instructions>\n <textarea\n value={specialInstructions}\n onChange={(e) => setSpecialInstructions(e.target.value)}\n placeholder=\"Special instructions (e.g., no onions, extra sauce)\"\n rows={2}\n data-cimplify-customizer-textarea\n className={cn(\n \"w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring resize-none\",\n classNames?.specialInstructions,\n )}\n />\n </div>\n )}\n\n <div\n data-cimplify-customizer-actions\n className={cn(\"pt-4 border-t border-border\", classNames?.actions)}\n >\n {!quoteEnabled && (\n <p\n id=\"cimplify-customizer-validation\"\n data-cimplify-customizer-validation\n className={cn(\"text-sm text-destructive mb-3\", classNames?.validation)}\n >\n Please select all required options\n </p>\n )}\n <div className=\"flex items-center gap-4\">\n <div className=\"flex flex-col gap-1\">\n <QuantitySelector\n value={quantity}\n onChange={setQuantity}\n min={product.min_order_quantity ?? 1}\n />\n {product.min_order_quantity && product.min_order_quantity > 1 && (\n <span data-cimplify-customizer-min-hint className=\"text-xs text-muted-foreground text-center\">\n Min. {product.min_order_quantity}\n </span>\n )}\n </div>\n\n {product.quantity_pricing && product.quantity_pricing.length > 0 && (\n <div data-cimplify-customizer-unit-price className=\"text-sm text-muted-foreground text-right shrink-0\">\n <Price amount={getUnitPriceAtQuantity(product.quantity_pricing, quantity, parsePrice(product.default_price))} currency={currency} className=\"font-medium text-foreground\" /> ea.\n </div>\n )}\n\n <Button\n onClick={handleAddToCart}\n disabled={isAdded || isSubmitting || !quoteEnabled}\n aria-describedby={!quoteEnabled ? \"cimplify-customizer-validation\" : undefined}\n data-cimplify-customizer-submit\n className={cn(\n \"flex-1 h-14 text-base bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed rounded-full\",\n isAdded && classNames?.submitButtonAdded,\n classNames?.submitButton,\n )}\n >\n {isAdded ? \"Added to Cart\" : (\n <>\n Add to Cart &middot; <Price amount={displayTotalPrice} currency={currency} />\n </>\n )}\n </Button>\n </div>\n </div>\n\n </div>\n );\n}\n"
17
+ "content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect, useRef } from \"react\";\nimport { Button } from \"@base-ui/react/button\";\nimport type {\n ProductWithDetails,\n VariantView,\n AddOnOption,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport type { ProductBillingPlan } from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice, getUnitPriceAtQuantity, getProductCurrency } from \"@cimplify/sdk\";\nimport { formatDuration } from \"./utils/format-duration\";\nimport { useCart, useQuote } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { QuantitySelector } from \"@cimplify/sdk/react\";\nimport { VariantSelector } from \"@cimplify/sdk/react\";\nimport { AddOnSelector } from \"@cimplify/sdk/react\";\nimport { StaffPicker } from \"@cimplify/sdk/react\";\nimport { BookingSummary } from \"./booking-summary\";\nimport type { Staff } from \"@cimplify/sdk\";\nimport { CompositeSelector } from \"@cimplify/sdk/react\";\nimport { BundleSelector } from \"@cimplify/sdk/react\";\nimport { BillingPlanSelector } from \"@cimplify/sdk/react\";\nimport { VolumePricing } from \"@cimplify/sdk/react\";\nimport { CustomerInputFields } from \"./customer-input-fields\";\nimport { DateSlotPicker } from \"@cimplify/sdk/react\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ProductCustomizerClassNames {\n root?: string;\n actions?: string;\n submitButton?: string;\n submitButtonAdded?: string;\n validation?: string;\n specialInstructions?: string;\n depositInfo?: string;\n allergens?: string;\n duration?: string;\n}\n\nexport interface ProductCustomizerProps {\n product: ProductWithDetails;\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n /** Lets the parent swap its gallery for `variant.images` on selection. */\n onVariantChange?: (\n variantId: string | undefined,\n variant: VariantView | undefined,\n ) => void;\n showSpecialInstructions?: boolean;\n className?: string;\n classNames?: ProductCustomizerClassNames;\n}\n\nexport function ProductCustomizer({\n product,\n onAddToCart,\n onVariantChange,\n showSpecialInstructions = true,\n className,\n classNames,\n}: ProductCustomizerProps): React.ReactElement {\n const [quantity, setQuantity] = useState(product.min_order_quantity ?? 1);\n const [isAdded, setIsAdded] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [selectedVariantId, setSelectedVariantId] = useState<string | undefined>();\n const [selectedVariant, setSelectedVariant] = useState<VariantView | undefined>();\n const [selectedAddOnOptionIds, setSelectedAddOnOptionIds] = useState<string[]>([]);\n\n const [compositeSelections, setCompositeSelections] = useState<ComponentSelectionInput[]>([]);\n const [compositePrice, setCompositePrice] = useState<CompositePriceResult | null>(null);\n const [compositeReady, setCompositeReady] = useState(false);\n\n const [bundleSelections, setBundleSelections] = useState<BundleSelectionInput[]>([]);\n const [bundleTotalPrice, setBundleTotalPrice] = useState<number | null>(null);\n const [bundleReady, setBundleReady] = useState(false);\n const [selectedBillingPlan, setSelectedBillingPlan] = useState<ProductBillingPlan | null>(null);\n const [customerInputValues, setCustomerInputValues] = useState<Record<string, unknown>>({});\n const [specialInstructions, setSpecialInstructions] = useState(\"\");\n const [selectedSlot, setSelectedSlot] = useState<AvailableSlot | null>(null);\n const [selectedStaffId, setSelectedStaffId] = useState<string | null>(null);\n\n const cart = useCart();\n\n const currency = getProductCurrency(product);\n const productType = product.type || \"product\";\n const isComposite = productType === \"composite\";\n const isBundle = productType === \"bundle\";\n const isDigital = productType === \"digital\";\n const isService = productType === \"service\";\n const isStandard = !isComposite && !isBundle;\n\n const hasVariants = isStandard && product.variants && product.variants.length > 0;\n const hasAddOns = isStandard && product.add_ons && product.add_ons.length > 0;\n\n useEffect(() => {\n setQuantity(product.min_order_quantity ?? 1);\n setIsAdded(false);\n setIsSubmitting(false);\n setSelectedVariantId(undefined);\n setSelectedVariant(undefined);\n setSelectedAddOnOptionIds([]);\n setCompositeSelections([]);\n setCompositePrice(null);\n setCompositeReady(false);\n setBundleSelections([]);\n setBundleTotalPrice(null);\n setBundleReady(false);\n setSelectedBillingPlan(null);\n setCustomerInputValues({});\n setSpecialInstructions(\"\");\n setSelectedSlot(null);\n setSelectedStaffId(null);\n }, [product.id, product.min_order_quantity]);\n\n // Staff offered for the chosen slot. SlotStaffInfo carries id + name; richer\n // profiles (avatars/bios) would come from a staff roster fetch.\n const slotStaff = useMemo<Staff[]>(\n () =>\n (selectedSlot?.available_staff ?? []).map((member) => ({\n id: member.staff_id,\n name: member.name,\n })),\n [selectedSlot],\n );\n const selectedStaffName = selectedStaffId\n ? (slotStaff.find((member) => member.id === selectedStaffId)?.name ?? null)\n : null;\n\n const selectedAddOnOptions = useMemo(() => {\n if (!product.add_ons) return [];\n const options: AddOnOption[] = [];\n for (const addOn of product.add_ons) {\n for (const option of addOn.options) {\n if (selectedAddOnOptionIds.includes(option.id)) {\n options.push(option);\n }\n }\n }\n return options;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const normalizedAddOnOptionIds = useMemo(() => {\n if (selectedAddOnOptionIds.length === 0) return [];\n return Array.from(new Set(selectedAddOnOptionIds.map((id) => id.trim()).filter(Boolean))).sort();\n }, [selectedAddOnOptionIds]);\n\n const localTotalPrice = useMemo(() => {\n if (isComposite && compositePrice) {\n return parsePrice(compositePrice.final_price) * quantity;\n }\n\n if (isBundle && bundleTotalPrice != null) {\n return bundleTotalPrice * quantity;\n }\n\n let price = parsePrice(product.default_price);\n\n if (selectedVariant?.price_adjustment) {\n price += parsePrice(selectedVariant.price_adjustment);\n }\n\n for (const option of selectedAddOnOptions) {\n if (option.default_price) {\n price += parsePrice(option.default_price);\n }\n }\n\n return price * quantity;\n }, [product.default_price, selectedVariant, selectedAddOnOptions, quantity, isComposite, compositePrice, isBundle, bundleTotalPrice]);\n\n const requiredAddOnsSatisfied = useMemo(() => {\n if (!product.add_ons) return true;\n\n for (const addOn of product.add_ons) {\n if (addOn.is_required) {\n const selectedInGroup = selectedAddOnOptionIds.filter((id) =>\n addOn.options.some((opt) => opt.id === id),\n ).length;\n\n const minRequired = addOn.min_selections || 1;\n if (selectedInGroup < minRequired) {\n return false;\n }\n }\n }\n return true;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const quoteInput = useMemo(\n () => ({\n productId: product.id,\n quantity,\n variantId: selectedVariantId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n }),\n [product.id, quantity, selectedVariantId, normalizedAddOnOptionIds, isBundle, bundleSelections, isComposite, compositeSelections],\n );\n\n const requiredInputsSatisfied = useMemo(() => {\n if (!product.input_fields || product.input_fields.length === 0) return true;\n return product.input_fields.every((field) => {\n if (!field.is_required) return true;\n const val = customerInputValues[field.id];\n return val !== undefined && val !== \"\" && val !== null;\n });\n }, [product.input_fields, customerInputValues]);\n\n const quoteEnabled = isComposite\n ? compositeReady && requiredInputsSatisfied\n : isBundle\n ? bundleReady && requiredInputsSatisfied\n : requiredAddOnsSatisfied && requiredInputsSatisfied;\n\n const { quote } = useQuote(quoteInput, {\n enabled: quoteEnabled,\n });\n\n const quoteId = quote?.quote_id;\n const quotedTotalPrice = useMemo(() => {\n if (!quote) return undefined;\n\n const priceInfo = quote.quoted_total_price_info ?? quote.final_price_info;\n const perUnit = priceInfo.pre_tax_price;\n if (perUnit === undefined || perUnit === null) return undefined;\n return parsePrice(perUnit) * quantity;\n }, [quote, quantity]);\n\n const displayTotalPrice = quotedTotalPrice ?? localTotalPrice;\n\n const handleVariantChange = useCallback(\n (variantId: string | undefined, variant: VariantView | undefined) => {\n setSelectedVariantId(variantId);\n setSelectedVariant(variant);\n onVariantChange?.(variantId, variant);\n },\n [onVariantChange],\n );\n\n const addedTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n useEffect(() => () => clearTimeout(addedTimerRef.current), []);\n\n const handleAddToCart = useCallback(async () => {\n if (isSubmitting) return;\n\n setIsSubmitting(true);\n\n const options: AddToCartOptions = {\n variantId: selectedVariantId,\n variant: selectedVariant\n ? { id: selectedVariant.id, name: selectedVariant.name || \"\", price_adjustment: selectedVariant.price_adjustment }\n : undefined,\n quoteId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n addOnOptions: selectedAddOnOptions.length > 0\n ? selectedAddOnOptions.map((opt) => ({\n id: opt.id,\n name: opt.name,\n add_on_id: opt.add_on_id,\n default_price: opt.default_price,\n }))\n : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n billingPlanId: selectedBillingPlan?.id,\n ...(isService && selectedSlot\n ? {\n scheduledStart: selectedSlot.start_time,\n scheduledEnd: selectedSlot.end_time,\n staffId: selectedStaffId ?? selectedSlot.available_staff?.[0]?.staff_id,\n resourceId: selectedSlot.available_resources?.[0]?.resource_id,\n }\n : {}),\n customerInputs: Object.keys(customerInputValues).length > 0\n ? Object.entries(customerInputValues)\n .filter(([, v]) => v !== undefined && v !== \"\")\n .map(([fieldId, value]) => ({ field_id: fieldId, value }))\n : undefined,\n specialInstructions: specialInstructions.trim() || undefined,\n };\n\n try {\n if (onAddToCart) {\n await onAddToCart(product, quantity, options);\n } else {\n await cart.addItem(product, quantity, options);\n }\n setIsAdded(true);\n clearTimeout(addedTimerRef.current);\n addedTimerRef.current = setTimeout(() => {\n setIsAdded(false);\n setQuantity(product.min_order_quantity ?? 1);\n }, 2000);\n } catch {\n // Caller handles errors via onAddToCart or cart hook error state\n } finally {\n setIsSubmitting(false);\n }\n }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, customerInputValues, specialInstructions, isService, selectedSlot, selectedStaffId, isSubmitting, onAddToCart, cart]);\n\n return (\n <div data-cimplify-customizer className={cn(\"space-y-6\", className, classNames?.root)}>\n {isComposite && product.groups && product.composite_id && (\n <CompositeSelector\n compositeId={product.composite_id}\n groups={product.groups}\n currency={currency}\n onSelectionsChange={setCompositeSelections}\n onPriceChange={setCompositePrice}\n onReady={setCompositeReady}\n skipPriceFetch\n />\n )}\n\n {isBundle && product.components && (\n <BundleSelector\n components={product.components}\n bundlePrice={product.bundle_price}\n discountValue={product.discount_value}\n pricingType={product.pricing_type}\n currency={currency}\n onSelectionsChange={setBundleSelections}\n onPriceChange={setBundleTotalPrice}\n onReady={setBundleReady}\n />\n )}\n\n {isService && (\n <DateSlotPicker\n serviceId={product.id}\n selectedSlot={selectedSlot}\n onSlotSelect={(slot) => {\n setSelectedSlot(slot);\n setSelectedStaffId(null);\n }}\n participantCount={quantity}\n currency={currency}\n />\n )}\n\n {isService && slotStaff.length > 0 && (\n <div data-cimplify-customizer-staff>\n <label className=\"block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3\">\n Choose your specialist\n </label>\n <StaffPicker\n staff={slotStaff}\n selectedStaffId={selectedStaffId}\n onStaffSelect={setSelectedStaffId}\n className=\"flex flex-wrap gap-2\"\n classNames={{\n option:\n \"inline-flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium transition-colors hover:border-foreground/40 cursor-pointer data-[selected]:border-foreground data-[selected]:bg-foreground data-[selected]:text-background\",\n avatar: \"w-6 h-6 rounded-full object-cover\",\n }}\n />\n </div>\n )}\n\n {hasVariants && (\n <VariantSelector\n variants={product.variants!}\n variantAxes={product.variant_axes}\n basePrice={product.default_price}\n currency={currency}\n selectedVariantId={selectedVariantId}\n onVariantChange={handleVariantChange}\n productName={product.name}\n />\n )}\n\n {hasAddOns && (\n <AddOnSelector\n addOns={product.add_ons!}\n selectedOptions={selectedAddOnOptionIds}\n onOptionsChange={setSelectedAddOnOptionIds}\n currency={currency}\n />\n )}\n\n {/* Billing plans */}\n {product.billing_plans && product.billing_plans.length > 0 && (\n <BillingPlanSelector\n productId={product.id}\n plans={product.billing_plans}\n currency={currency}\n onPlanSelect={setSelectedBillingPlan}\n selectedPlanId={selectedBillingPlan?.id ?? null}\n showOneTimePurchase\n />\n )}\n\n {/* Volume pricing tiers */}\n {product.quantity_pricing && product.quantity_pricing.length > 1 && (\n <VolumePricing\n tiers={product.quantity_pricing}\n currentQuantity={quantity}\n currency={currency}\n />\n )}\n\n {/* Customer input fields */}\n {product.input_fields && product.input_fields.length > 0 && (\n <CustomerInputFields\n fields={product.input_fields}\n values={customerInputValues}\n onChange={setCustomerInputValues}\n currency={currency}\n />\n )}\n\n {/* Deposit info for service products */}\n {product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount && (\n <div data-cimplify-customizer-deposit className={cn(\"text-sm text-muted-foreground\", classNames?.depositInfo)}>\n {product.deposit_type === \"fixed\" ? (\n <span>Deposit required: <Price amount={product.deposit_amount} currency={currency} /></span>\n ) : (\n <span>Deposit required: {parsePrice(product.deposit_amount)}%</span>\n )}\n </div>\n )}\n\n {/* Allergens */}\n {product.allergies && product.allergies.length > 0 && (\n <div data-cimplify-customizer-allergens className={cn(\"flex flex-wrap gap-1.5\", classNames?.allergens)}>\n {product.allergies.map((allergen) => (\n <span\n key={allergen}\n data-cimplify-allergen-tag\n className=\"inline-block text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground\"\n >\n {allergen}\n </span>\n ))}\n </div>\n )}\n\n {/* Service duration */}\n {isService && product.duration_minutes != null && (\n <div data-cimplify-customizer-duration className={cn(\"text-sm text-muted-foreground\", classNames?.duration)}>\n Duration: {formatDuration(product.duration_minutes, product.duration_unit)}\n </div>\n )}\n\n {/* Special instructions */}\n {showSpecialInstructions && !isDigital && (\n <div data-cimplify-customizer-special-instructions>\n <textarea\n value={specialInstructions}\n onChange={(e) => setSpecialInstructions(e.target.value)}\n placeholder=\"Special instructions (e.g., no onions, extra sauce)\"\n rows={2}\n data-cimplify-customizer-textarea\n className={cn(\n \"w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring resize-none\",\n classNames?.specialInstructions,\n )}\n />\n </div>\n )}\n\n {isService && selectedSlot && (\n <BookingSummary\n slot={selectedSlot}\n total={displayTotalPrice}\n staffName={selectedStaffName}\n durationMinutes={product.duration_minutes}\n depositType={product.deposit_type}\n depositAmount={product.deposit_amount}\n currency={currency}\n />\n )}\n\n <div\n data-cimplify-customizer-actions\n className={cn(\"pt-4 border-t border-border\", classNames?.actions)}\n >\n {!quoteEnabled && (\n <p\n id=\"cimplify-customizer-validation\"\n data-cimplify-customizer-validation\n className={cn(\"text-sm text-destructive mb-3\", classNames?.validation)}\n >\n Please select all required options\n </p>\n )}\n <div className=\"flex items-center gap-4\">\n <div className=\"flex flex-col gap-1\">\n <QuantitySelector\n value={quantity}\n onChange={setQuantity}\n min={product.min_order_quantity ?? 1}\n />\n {product.min_order_quantity && product.min_order_quantity > 1 && (\n <span data-cimplify-customizer-min-hint className=\"text-xs text-muted-foreground text-center\">\n Min. {product.min_order_quantity}\n </span>\n )}\n </div>\n\n {product.quantity_pricing && product.quantity_pricing.length > 0 && (\n <div data-cimplify-customizer-unit-price className=\"text-sm text-muted-foreground text-right shrink-0\">\n <Price amount={getUnitPriceAtQuantity(product.quantity_pricing, quantity, parsePrice(product.default_price))} currency={currency} className=\"font-medium text-foreground\" /> ea.\n </div>\n )}\n\n <Button\n onClick={handleAddToCart}\n disabled={isAdded || isSubmitting || !quoteEnabled}\n aria-describedby={!quoteEnabled ? \"cimplify-customizer-validation\" : undefined}\n data-cimplify-customizer-submit\n className={cn(\n \"flex-1 h-14 text-base bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed rounded-full\",\n isAdded && classNames?.submitButtonAdded,\n classNames?.submitButton,\n )}\n >\n {isAdded ? \"Added to Cart\" : (\n <>\n Add to Cart &middot; <Price amount={displayTotalPrice} currency={currency} />\n </>\n )}\n </Button>\n </div>\n </div>\n\n </div>\n );\n}\n"
18
18
  }
19
19
  ]
20
20
  }