@cimplify/sdk 0.12.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/react.js +0 -9
- package/dist/react.mjs +0 -9
- package/package.json +1 -1
- package/registry/add-on-selector.json +1 -1
- package/registry/bundle-selector.json +1 -1
- package/registry/composite-selector.json +1 -1
- package/registry/product-image-gallery.json +1 -1
- package/registry/slot-picker.json +1 -1
- package/registry/staff-picker.json +1 -1
- package/registry/variant-selector.json +1 -1
package/dist/react.js
CHANGED
|
@@ -8011,7 +8011,6 @@ function VariantSelector({
|
|
|
8011
8011
|
radio.Radio.Root,
|
|
8012
8012
|
{
|
|
8013
8013
|
value: value.id,
|
|
8014
|
-
render: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button" }),
|
|
8015
8014
|
"data-cimplify-variant-option": true,
|
|
8016
8015
|
"data-selected": isSelected || void 0,
|
|
8017
8016
|
className: cn(
|
|
@@ -8059,7 +8058,6 @@ function VariantSelector({
|
|
|
8059
8058
|
radio.Radio.Root,
|
|
8060
8059
|
{
|
|
8061
8060
|
value: variant.id,
|
|
8062
|
-
render: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button" }),
|
|
8063
8061
|
"data-cimplify-variant-option": true,
|
|
8064
8062
|
"data-selected": isSelected || void 0,
|
|
8065
8063
|
className: cn(
|
|
@@ -8217,7 +8215,6 @@ function AddOnSelector({
|
|
|
8217
8215
|
checked: isSelected,
|
|
8218
8216
|
onCheckedChange: (checked) => handleCheckedChange(addOn, option.id, checked),
|
|
8219
8217
|
value: option.id,
|
|
8220
|
-
render: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button" }),
|
|
8221
8218
|
"data-cimplify-addon-option": true,
|
|
8222
8219
|
"data-selected": isSelected || void 0,
|
|
8223
8220
|
className: cn(
|
|
@@ -8458,7 +8455,6 @@ function BundleComponentCard({
|
|
|
8458
8455
|
radio.Radio.Root,
|
|
8459
8456
|
{
|
|
8460
8457
|
value: variant.id,
|
|
8461
|
-
render: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button" }),
|
|
8462
8458
|
"data-cimplify-bundle-variant-option": true,
|
|
8463
8459
|
"data-selected": isSelected || void 0,
|
|
8464
8460
|
className: cn(
|
|
@@ -8704,7 +8700,6 @@ function CompositeSelector({
|
|
|
8704
8700
|
checked: isSelected,
|
|
8705
8701
|
onCheckedChange: () => toggleComponent(group, component),
|
|
8706
8702
|
value: component.id,
|
|
8707
|
-
render: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button" }),
|
|
8708
8703
|
"data-cimplify-composite-component": true,
|
|
8709
8704
|
"data-selected": isSelected || void 0,
|
|
8710
8705
|
className: cn(
|
|
@@ -9189,7 +9184,6 @@ function ProductImageGallery({
|
|
|
9189
9184
|
radio.Radio.Root,
|
|
9190
9185
|
{
|
|
9191
9186
|
value: String(index),
|
|
9192
|
-
render: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button" }),
|
|
9193
9187
|
"data-cimplify-image-gallery-thumb": true,
|
|
9194
9188
|
"data-selected": isSelected || void 0,
|
|
9195
9189
|
style: {
|
|
@@ -10620,7 +10614,6 @@ function SlotPicker({
|
|
|
10620
10614
|
{
|
|
10621
10615
|
value,
|
|
10622
10616
|
disabled: !slot.is_available,
|
|
10623
|
-
render: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button" }),
|
|
10624
10617
|
"data-cimplify-slot": true,
|
|
10625
10618
|
"data-selected": isSelected || void 0,
|
|
10626
10619
|
"data-unavailable": !slot.is_available || void 0,
|
|
@@ -10806,7 +10799,6 @@ function StaffPicker({
|
|
|
10806
10799
|
radio.Radio.Root,
|
|
10807
10800
|
{
|
|
10808
10801
|
value: ANY_VALUE,
|
|
10809
|
-
render: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button" }),
|
|
10810
10802
|
"data-cimplify-staff-option": true,
|
|
10811
10803
|
"data-selected": selectedStaffId === null || void 0,
|
|
10812
10804
|
"data-any": true,
|
|
@@ -10818,7 +10810,6 @@ function StaffPicker({
|
|
|
10818
10810
|
radio.Radio.Root,
|
|
10819
10811
|
{
|
|
10820
10812
|
value: member.id,
|
|
10821
|
-
render: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button" }),
|
|
10822
10813
|
"data-cimplify-staff-option": true,
|
|
10823
10814
|
"data-selected": selectedStaffId === member.id || void 0,
|
|
10824
10815
|
className: classNames?.option,
|
package/dist/react.mjs
CHANGED
|
@@ -8005,7 +8005,6 @@ function VariantSelector({
|
|
|
8005
8005
|
Radio.Root,
|
|
8006
8006
|
{
|
|
8007
8007
|
value: value.id,
|
|
8008
|
-
render: /* @__PURE__ */ jsx("button", { type: "button" }),
|
|
8009
8008
|
"data-cimplify-variant-option": true,
|
|
8010
8009
|
"data-selected": isSelected || void 0,
|
|
8011
8010
|
className: cn(
|
|
@@ -8053,7 +8052,6 @@ function VariantSelector({
|
|
|
8053
8052
|
Radio.Root,
|
|
8054
8053
|
{
|
|
8055
8054
|
value: variant.id,
|
|
8056
|
-
render: /* @__PURE__ */ jsx("button", { type: "button" }),
|
|
8057
8055
|
"data-cimplify-variant-option": true,
|
|
8058
8056
|
"data-selected": isSelected || void 0,
|
|
8059
8057
|
className: cn(
|
|
@@ -8211,7 +8209,6 @@ function AddOnSelector({
|
|
|
8211
8209
|
checked: isSelected,
|
|
8212
8210
|
onCheckedChange: (checked) => handleCheckedChange(addOn, option.id, checked),
|
|
8213
8211
|
value: option.id,
|
|
8214
|
-
render: /* @__PURE__ */ jsx("button", { type: "button" }),
|
|
8215
8212
|
"data-cimplify-addon-option": true,
|
|
8216
8213
|
"data-selected": isSelected || void 0,
|
|
8217
8214
|
className: cn(
|
|
@@ -8452,7 +8449,6 @@ function BundleComponentCard({
|
|
|
8452
8449
|
Radio.Root,
|
|
8453
8450
|
{
|
|
8454
8451
|
value: variant.id,
|
|
8455
|
-
render: /* @__PURE__ */ jsx("button", { type: "button" }),
|
|
8456
8452
|
"data-cimplify-bundle-variant-option": true,
|
|
8457
8453
|
"data-selected": isSelected || void 0,
|
|
8458
8454
|
className: cn(
|
|
@@ -8698,7 +8694,6 @@ function CompositeSelector({
|
|
|
8698
8694
|
checked: isSelected,
|
|
8699
8695
|
onCheckedChange: () => toggleComponent(group, component),
|
|
8700
8696
|
value: component.id,
|
|
8701
|
-
render: /* @__PURE__ */ jsx("button", { type: "button" }),
|
|
8702
8697
|
"data-cimplify-composite-component": true,
|
|
8703
8698
|
"data-selected": isSelected || void 0,
|
|
8704
8699
|
className: cn(
|
|
@@ -9183,7 +9178,6 @@ function ProductImageGallery({
|
|
|
9183
9178
|
Radio.Root,
|
|
9184
9179
|
{
|
|
9185
9180
|
value: String(index),
|
|
9186
|
-
render: /* @__PURE__ */ jsx("button", { type: "button" }),
|
|
9187
9181
|
"data-cimplify-image-gallery-thumb": true,
|
|
9188
9182
|
"data-selected": isSelected || void 0,
|
|
9189
9183
|
style: {
|
|
@@ -10614,7 +10608,6 @@ function SlotPicker({
|
|
|
10614
10608
|
{
|
|
10615
10609
|
value,
|
|
10616
10610
|
disabled: !slot.is_available,
|
|
10617
|
-
render: /* @__PURE__ */ jsx("button", { type: "button" }),
|
|
10618
10611
|
"data-cimplify-slot": true,
|
|
10619
10612
|
"data-selected": isSelected || void 0,
|
|
10620
10613
|
"data-unavailable": !slot.is_available || void 0,
|
|
@@ -10800,7 +10793,6 @@ function StaffPicker({
|
|
|
10800
10793
|
Radio.Root,
|
|
10801
10794
|
{
|
|
10802
10795
|
value: ANY_VALUE,
|
|
10803
|
-
render: /* @__PURE__ */ jsx("button", { type: "button" }),
|
|
10804
10796
|
"data-cimplify-staff-option": true,
|
|
10805
10797
|
"data-selected": selectedStaffId === null || void 0,
|
|
10806
10798
|
"data-any": true,
|
|
@@ -10812,7 +10804,6 @@ function StaffPicker({
|
|
|
10812
10804
|
Radio.Root,
|
|
10813
10805
|
{
|
|
10814
10806
|
value: member.id,
|
|
10815
|
-
render: /* @__PURE__ */ jsx("button", { type: "button" }),
|
|
10816
10807
|
"data-cimplify-staff-option": true,
|
|
10817
10808
|
"data-selected": selectedStaffId === member.id || void 0,
|
|
10818
10809
|
className: classNames?.option,
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "add-on-selector.tsx",
|
|
12
|
-
"content": "\"use client\";\n\nimport React, { useCallback } from \"react\";\nimport { Checkbox } from \"@base-ui/react/checkbox\";\nimport type { AddOnWithOptions } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface AddOnSelectorClassNames {\n root?: string;\n group?: string;\n header?: string;\n name?: string;\n required?: string;\n constraint?: string;\n validation?: string;\n options?: string;\n option?: string;\n optionSelected?: string;\n optionName?: string;\n optionDescription?: string;\n}\n\nexport interface AddOnSelectorProps {\n addOns: AddOnWithOptions[];\n selectedOptions: string[];\n onOptionsChange: (optionIds: string[]) => void;\n className?: string;\n classNames?: AddOnSelectorClassNames;\n}\n\nexport function AddOnSelector({\n addOns,\n selectedOptions,\n onOptionsChange,\n className,\n classNames,\n}: AddOnSelectorProps): React.ReactElement | null {\n const isOptionSelected = useCallback(\n (optionId: string) => selectedOptions.includes(optionId),\n [selectedOptions],\n );\n\n const handleCheckedChange = useCallback(\n (addOn: AddOnWithOptions, optionId: string, checked: boolean) => {\n const isSelected = selectedOptions.includes(optionId);\n\n if (addOn.is_mutually_exclusive || !addOn.is_multiple_allowed) {\n const groupOptionIds = new Set(addOn.options.map((o) => o.id));\n const withoutGroup = selectedOptions.filter((id) => !groupOptionIds.has(id));\n\n if (isSelected) {\n if (!addOn.is_required) {\n onOptionsChange(withoutGroup);\n }\n } else {\n onOptionsChange([...withoutGroup, optionId]);\n }\n } else {\n if (isSelected) {\n onOptionsChange(selectedOptions.filter((id) => id !== optionId));\n } else {\n const currentCount = selectedOptions.filter((id) =>\n addOn.options.some((o) => o.id === id),\n ).length;\n\n if (addOn.max_selections && currentCount >= addOn.max_selections) {\n return;\n }\n\n onOptionsChange([...selectedOptions, optionId]);\n }\n }\n },\n [selectedOptions, onOptionsChange],\n );\n\n if (!addOns || addOns.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-addon-selector className={cn(\"space-y-6\", className, classNames?.root)}>\n {addOns.map((addOn) => {\n const currentSelections = selectedOptions.filter((id) =>\n addOn.options.some((o) => o.id === id),\n ).length;\n const minMet = !addOn.min_selections || currentSelections >= addOn.min_selections;\n\n return (\n <div\n key={addOn.id}\n data-cimplify-addon-group\n className={cn(\"border border-border p-5\", classNames?.group)}\n >\n <div\n data-cimplify-addon-header\n className={cn(\"flex items-center justify-between mb-4\", classNames?.header)}\n >\n <div>\n <span\n data-cimplify-addon-name\n className={cn(\"text-xs font-medium uppercase tracking-wider text-muted-foreground\", classNames?.name)}\n >\n {addOn.name}\n {addOn.is_required && (\n <span\n data-cimplify-addon-required\n className={cn(\"text-destructive ml-1\", classNames?.required)}\n >\n {\" \"}*\n </span>\n )}\n </span>\n {(addOn.min_selections || addOn.max_selections) && (\n <span\n data-cimplify-addon-constraint\n className={cn(\"text-xs text-muted-foreground/70 mt-1\", classNames?.constraint)}\n >\n {addOn.min_selections && addOn.max_selections\n ? `Choose ${addOn.min_selections}\\u2013${addOn.max_selections}`\n : addOn.min_selections\n ? `Choose at least ${addOn.min_selections}`\n : `Choose up to ${addOn.max_selections}`}\n </span>\n )}\n </div>\n {!minMet && (\n <span\n data-cimplify-addon-validation\n className={cn(\"text-xs text-destructive font-medium\", classNames?.validation)}\n >\n Required\n </span>\n )}\n </div>\n\n <div\n data-cimplify-addon-options\n className={cn(\"space-y-1\", classNames?.options)}\n >\n {addOn.options.map((option) => {\n const isSelected = isOptionSelected(option.id);\n\n return (\n <Checkbox.Root\n key={option.id}\n checked={isSelected}\n onCheckedChange={(checked) =>\n handleCheckedChange(addOn, option.id, checked)\n }\n value={option.id}\n
|
|
12
|
+
"content": "\"use client\";\n\nimport React, { useCallback } from \"react\";\nimport { Checkbox } from \"@base-ui/react/checkbox\";\nimport type { AddOnWithOptions } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface AddOnSelectorClassNames {\n root?: string;\n group?: string;\n header?: string;\n name?: string;\n required?: string;\n constraint?: string;\n validation?: string;\n options?: string;\n option?: string;\n optionSelected?: string;\n optionName?: string;\n optionDescription?: string;\n}\n\nexport interface AddOnSelectorProps {\n addOns: AddOnWithOptions[];\n selectedOptions: string[];\n onOptionsChange: (optionIds: string[]) => void;\n className?: string;\n classNames?: AddOnSelectorClassNames;\n}\n\nexport function AddOnSelector({\n addOns,\n selectedOptions,\n onOptionsChange,\n className,\n classNames,\n}: AddOnSelectorProps): React.ReactElement | null {\n const isOptionSelected = useCallback(\n (optionId: string) => selectedOptions.includes(optionId),\n [selectedOptions],\n );\n\n const handleCheckedChange = useCallback(\n (addOn: AddOnWithOptions, optionId: string, checked: boolean) => {\n const isSelected = selectedOptions.includes(optionId);\n\n if (addOn.is_mutually_exclusive || !addOn.is_multiple_allowed) {\n const groupOptionIds = new Set(addOn.options.map((o) => o.id));\n const withoutGroup = selectedOptions.filter((id) => !groupOptionIds.has(id));\n\n if (isSelected) {\n if (!addOn.is_required) {\n onOptionsChange(withoutGroup);\n }\n } else {\n onOptionsChange([...withoutGroup, optionId]);\n }\n } else {\n if (isSelected) {\n onOptionsChange(selectedOptions.filter((id) => id !== optionId));\n } else {\n const currentCount = selectedOptions.filter((id) =>\n addOn.options.some((o) => o.id === id),\n ).length;\n\n if (addOn.max_selections && currentCount >= addOn.max_selections) {\n return;\n }\n\n onOptionsChange([...selectedOptions, optionId]);\n }\n }\n },\n [selectedOptions, onOptionsChange],\n );\n\n if (!addOns || addOns.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-addon-selector className={cn(\"space-y-6\", className, classNames?.root)}>\n {addOns.map((addOn) => {\n const currentSelections = selectedOptions.filter((id) =>\n addOn.options.some((o) => o.id === id),\n ).length;\n const minMet = !addOn.min_selections || currentSelections >= addOn.min_selections;\n\n return (\n <div\n key={addOn.id}\n data-cimplify-addon-group\n className={cn(\"border border-border p-5\", classNames?.group)}\n >\n <div\n data-cimplify-addon-header\n className={cn(\"flex items-center justify-between mb-4\", classNames?.header)}\n >\n <div>\n <span\n data-cimplify-addon-name\n className={cn(\"text-xs font-medium uppercase tracking-wider text-muted-foreground\", classNames?.name)}\n >\n {addOn.name}\n {addOn.is_required && (\n <span\n data-cimplify-addon-required\n className={cn(\"text-destructive ml-1\", classNames?.required)}\n >\n {\" \"}*\n </span>\n )}\n </span>\n {(addOn.min_selections || addOn.max_selections) && (\n <span\n data-cimplify-addon-constraint\n className={cn(\"text-xs text-muted-foreground/70 mt-1\", classNames?.constraint)}\n >\n {addOn.min_selections && addOn.max_selections\n ? `Choose ${addOn.min_selections}\\u2013${addOn.max_selections}`\n : addOn.min_selections\n ? `Choose at least ${addOn.min_selections}`\n : `Choose up to ${addOn.max_selections}`}\n </span>\n )}\n </div>\n {!minMet && (\n <span\n data-cimplify-addon-validation\n className={cn(\"text-xs text-destructive font-medium\", classNames?.validation)}\n >\n Required\n </span>\n )}\n </div>\n\n <div\n data-cimplify-addon-options\n className={cn(\"space-y-1\", classNames?.options)}\n >\n {addOn.options.map((option) => {\n const isSelected = isOptionSelected(option.id);\n\n return (\n <Checkbox.Root\n key={option.id}\n checked={isSelected}\n onCheckedChange={(checked) =>\n handleCheckedChange(addOn, option.id, checked)\n }\n value={option.id}\n data-cimplify-addon-option\n data-selected={isSelected || undefined}\n className={cn(\n \"w-full flex items-center gap-3 px-4 py-3 border transition-colors text-left border-transparent hover:bg-muted/50\",\n isSelected && \"bg-primary/5 border-primary\",\n isSelected ? classNames?.optionSelected : classNames?.option,\n )}\n >\n <Checkbox.Indicator\n className=\"hidden\"\n keepMounted={false}\n />\n\n <span\n data-cimplify-addon-option-name\n className={cn(\n \"flex-1 min-w-0 text-sm font-medium\",\n isSelected && \"text-primary\",\n classNames?.optionName,\n )}\n >\n {option.name}\n </span>\n\n {option.default_price != null && parsePrice(option.default_price) !== 0 && (\n <Price\n amount={option.default_price}\n prefix=\"+\"\n />\n )}\n </Checkbox.Root>\n );\n })}\n </div>\n </div>\n );\n })}\n </div>\n );\n}\n"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "bundle-selector.tsx",
|
|
12
|
-
"content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect, useRef, useId } from \"react\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport { Radio } from \"@base-ui/react/radio\";\nimport type { BundleComponentView, BundleComponentVariantView, BundlePriceType } from \"@cimplify/sdk\";\nimport type { Money } from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface BundleSelectorClassNames {\n root?: string;\n heading?: string;\n components?: string;\n component?: string;\n componentHeader?: string;\n componentQty?: string;\n componentName?: string;\n variantPicker?: string;\n variantOption?: string;\n variantOptionSelected?: string;\n variantAdjustment?: string;\n summary?: string;\n savings?: string;\n}\n\nexport interface BundleSelectorProps {\n components: BundleComponentView[];\n bundlePrice?: Money;\n discountValue?: Money;\n pricingType?: BundlePriceType;\n onSelectionsChange: (selections: BundleSelectionInput[]) => void;\n onPriceChange?: (price: number) => void;\n onReady?: (ready: boolean) => void;\n className?: string;\n classNames?: BundleSelectorClassNames;\n}\n\nexport function BundleSelector({\n components,\n bundlePrice,\n discountValue,\n pricingType,\n onSelectionsChange,\n onPriceChange,\n onReady,\n className,\n classNames,\n}: BundleSelectorProps): React.ReactElement | null {\n const [variantChoices, setVariantChoices] = useState<Record<string, string>>({});\n const lastComponentIds = useRef(\"\");\n\n useEffect(() => {\n const ids = components.map((c) => c.id).sort().join();\n if (ids === lastComponentIds.current) return;\n lastComponentIds.current = ids;\n\n const defaults: Record<string, string> = {};\n for (const comp of components) {\n if (comp.variant_id) {\n defaults[comp.id] = comp.variant_id;\n } else if (comp.available_variants.length > 0) {\n const defaultVariant =\n comp.available_variants.find((v) => v.is_default) || comp.available_variants[0];\n if (defaultVariant) {\n defaults[comp.id] = defaultVariant.id;\n }\n }\n }\n setVariantChoices(defaults);\n }, [components]);\n\n const selections = useMemo((): BundleSelectionInput[] => {\n return components.map((comp) => ({\n component_id: comp.id,\n variant_id: variantChoices[comp.id],\n quantity: comp.quantity,\n }));\n }, [components, variantChoices]);\n\n useEffect(() => {\n onSelectionsChange(selections);\n }, [selections, onSelectionsChange]);\n\n useEffect(() => {\n onReady?.(components.length > 0 && selections.length > 0);\n }, [components, selections, onReady]);\n\n const totalPrice = useMemo(() => {\n if (pricingType === \"fixed\" && bundlePrice) {\n return parsePrice(bundlePrice);\n }\n const componentsTotal = components.reduce((sum, comp) => {\n return sum + getComponentPrice(comp, variantChoices[comp.id]) * comp.quantity;\n }, 0);\n if (pricingType === \"percentage_discount\" && discountValue) {\n return componentsTotal * (1 - parsePrice(discountValue) / 100);\n }\n if (pricingType === \"fixed_discount\" && discountValue) {\n return componentsTotal - parsePrice(discountValue);\n }\n return componentsTotal;\n }, [components, variantChoices, pricingType, bundlePrice, discountValue]);\n\n useEffect(() => {\n onPriceChange?.(totalPrice);\n }, [totalPrice, onPriceChange]);\n\n const handleVariantChange = useCallback(\n (componentId: string, variantId: string) => {\n setVariantChoices((prev) => ({ ...prev, [componentId]: variantId }));\n },\n [],\n );\n\n if (components.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-bundle-selector className={cn(\"space-y-4\", className, classNames?.root)}>\n <span\n data-cimplify-bundle-heading\n className={cn(\"text-xs font-medium uppercase tracking-wider text-muted-foreground\", classNames?.heading)}\n >\n Included in this bundle\n </span>\n\n <div data-cimplify-bundle-components className={cn(\"space-y-3\", classNames?.components)}>\n {components.map((comp) => (\n <BundleComponentCard\n key={comp.id}\n component={comp}\n selectedVariantId={variantChoices[comp.id]}\n onVariantChange={(variantId) =>\n handleVariantChange(comp.id, variantId)\n }\n classNames={classNames}\n />\n ))}\n </div>\n\n {bundlePrice && (\n <div\n data-cimplify-bundle-summary\n className={cn(\"border-t border-border pt-4 flex justify-between text-sm\", classNames?.summary)}\n >\n <span className=\"text-muted-foreground\">Bundle price</span>\n <Price amount={bundlePrice} className=\"font-medium text-primary\" />\n </div>\n )}\n {discountValue && (\n <div\n data-cimplify-bundle-savings\n className={cn(\"flex justify-between text-sm\", classNames?.savings)}\n >\n <span className=\"text-muted-foreground\">You save</span>\n <Price amount={discountValue} className=\"text-green-600 font-medium\" />\n </div>\n )}\n </div>\n );\n}\n\nfunction getComponentPrice(\n component: BundleComponentView,\n selectedVariantId: string | undefined,\n): number {\n if (!selectedVariantId || component.available_variants.length === 0) {\n return parsePrice(component.effective_price);\n }\n if (selectedVariantId === component.variant_id) {\n return parsePrice(component.effective_price);\n }\n const bakedAdj = component.variant_id\n ? component.available_variants.find((v) => v.id === component.variant_id)\n : undefined;\n const selectedAdj = component.available_variants.find((v) => v.id === selectedVariantId);\n if (!selectedAdj) return parsePrice(component.effective_price);\n return parsePrice(component.effective_price)\n - parsePrice(bakedAdj?.price_adjustment ?? \"0\")\n + parsePrice(selectedAdj.price_adjustment);\n}\n\ninterface BundleComponentCardProps {\n component: BundleComponentView;\n selectedVariantId?: string;\n onVariantChange: (variantId: string) => void;\n classNames?: BundleSelectorClassNames;\n}\n\nfunction BundleComponentCard({\n component,\n selectedVariantId,\n onVariantChange,\n classNames,\n}: BundleComponentCardProps): React.ReactElement {\n const idPrefix = useId();\n const showVariantPicker =\n component.allow_variant_choice && component.available_variants.length > 1;\n\n const displayPrice = useMemo(\n () => getComponentPrice(component, selectedVariantId),\n [component, selectedVariantId],\n );\n\n const labelId = `${idPrefix}-bundle-component-${component.id}`;\n\n return (\n <div\n data-cimplify-bundle-component\n className={cn(\"border border-border p-4\", classNames?.component)}\n >\n <div\n data-cimplify-bundle-component-header\n className={cn(\"flex items-start justify-between gap-3\", classNames?.componentHeader)}\n >\n <div>\n {component.quantity > 1 && (\n <span\n data-cimplify-bundle-component-qty\n className={cn(\"text-xs font-medium text-primary bg-primary/10 px-1.5 py-0.5\", classNames?.componentQty)}\n >\n ×{component.quantity}\n </span>\n )}\n <span\n id={labelId}\n data-cimplify-bundle-component-name\n className={cn(\"font-medium text-sm\", classNames?.componentName)}\n >\n {component.product_name}\n </span>\n </div>\n <Price amount={displayPrice} />\n </div>\n\n {showVariantPicker && (\n <RadioGroup\n aria-labelledby={labelId}\n value={selectedVariantId ?? \"\"}\n onValueChange={(value) => {\n onVariantChange(value);\n }}\n data-cimplify-bundle-variant-picker\n className={cn(\"mt-3 flex flex-wrap gap-2\", classNames?.variantPicker)}\n >\n {component.available_variants.map((variant: BundleComponentVariantView) => {\n const isSelected = selectedVariantId === variant.id;\n const adjustment = parsePrice(variant.price_adjustment);\n\n return (\n <Radio.Root\n key={variant.id}\n value={variant.id}\n
|
|
12
|
+
"content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect, useRef, useId } from \"react\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport { Radio } from \"@base-ui/react/radio\";\nimport type { BundleComponentView, BundleComponentVariantView, BundlePriceType } from \"@cimplify/sdk\";\nimport type { Money } from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface BundleSelectorClassNames {\n root?: string;\n heading?: string;\n components?: string;\n component?: string;\n componentHeader?: string;\n componentQty?: string;\n componentName?: string;\n variantPicker?: string;\n variantOption?: string;\n variantOptionSelected?: string;\n variantAdjustment?: string;\n summary?: string;\n savings?: string;\n}\n\nexport interface BundleSelectorProps {\n components: BundleComponentView[];\n bundlePrice?: Money;\n discountValue?: Money;\n pricingType?: BundlePriceType;\n onSelectionsChange: (selections: BundleSelectionInput[]) => void;\n onPriceChange?: (price: number) => void;\n onReady?: (ready: boolean) => void;\n className?: string;\n classNames?: BundleSelectorClassNames;\n}\n\nexport function BundleSelector({\n components,\n bundlePrice,\n discountValue,\n pricingType,\n onSelectionsChange,\n onPriceChange,\n onReady,\n className,\n classNames,\n}: BundleSelectorProps): React.ReactElement | null {\n const [variantChoices, setVariantChoices] = useState<Record<string, string>>({});\n const lastComponentIds = useRef(\"\");\n\n useEffect(() => {\n const ids = components.map((c) => c.id).sort().join();\n if (ids === lastComponentIds.current) return;\n lastComponentIds.current = ids;\n\n const defaults: Record<string, string> = {};\n for (const comp of components) {\n if (comp.variant_id) {\n defaults[comp.id] = comp.variant_id;\n } else if (comp.available_variants.length > 0) {\n const defaultVariant =\n comp.available_variants.find((v) => v.is_default) || comp.available_variants[0];\n if (defaultVariant) {\n defaults[comp.id] = defaultVariant.id;\n }\n }\n }\n setVariantChoices(defaults);\n }, [components]);\n\n const selections = useMemo((): BundleSelectionInput[] => {\n return components.map((comp) => ({\n component_id: comp.id,\n variant_id: variantChoices[comp.id],\n quantity: comp.quantity,\n }));\n }, [components, variantChoices]);\n\n useEffect(() => {\n onSelectionsChange(selections);\n }, [selections, onSelectionsChange]);\n\n useEffect(() => {\n onReady?.(components.length > 0 && selections.length > 0);\n }, [components, selections, onReady]);\n\n const totalPrice = useMemo(() => {\n if (pricingType === \"fixed\" && bundlePrice) {\n return parsePrice(bundlePrice);\n }\n const componentsTotal = components.reduce((sum, comp) => {\n return sum + getComponentPrice(comp, variantChoices[comp.id]) * comp.quantity;\n }, 0);\n if (pricingType === \"percentage_discount\" && discountValue) {\n return componentsTotal * (1 - parsePrice(discountValue) / 100);\n }\n if (pricingType === \"fixed_discount\" && discountValue) {\n return componentsTotal - parsePrice(discountValue);\n }\n return componentsTotal;\n }, [components, variantChoices, pricingType, bundlePrice, discountValue]);\n\n useEffect(() => {\n onPriceChange?.(totalPrice);\n }, [totalPrice, onPriceChange]);\n\n const handleVariantChange = useCallback(\n (componentId: string, variantId: string) => {\n setVariantChoices((prev) => ({ ...prev, [componentId]: variantId }));\n },\n [],\n );\n\n if (components.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-bundle-selector className={cn(\"space-y-4\", className, classNames?.root)}>\n <span\n data-cimplify-bundle-heading\n className={cn(\"text-xs font-medium uppercase tracking-wider text-muted-foreground\", classNames?.heading)}\n >\n Included in this bundle\n </span>\n\n <div data-cimplify-bundle-components className={cn(\"space-y-3\", classNames?.components)}>\n {components.map((comp) => (\n <BundleComponentCard\n key={comp.id}\n component={comp}\n selectedVariantId={variantChoices[comp.id]}\n onVariantChange={(variantId) =>\n handleVariantChange(comp.id, variantId)\n }\n classNames={classNames}\n />\n ))}\n </div>\n\n {bundlePrice && (\n <div\n data-cimplify-bundle-summary\n className={cn(\"border-t border-border pt-4 flex justify-between text-sm\", classNames?.summary)}\n >\n <span className=\"text-muted-foreground\">Bundle price</span>\n <Price amount={bundlePrice} className=\"font-medium text-primary\" />\n </div>\n )}\n {discountValue && (\n <div\n data-cimplify-bundle-savings\n className={cn(\"flex justify-between text-sm\", classNames?.savings)}\n >\n <span className=\"text-muted-foreground\">You save</span>\n <Price amount={discountValue} className=\"text-green-600 font-medium\" />\n </div>\n )}\n </div>\n );\n}\n\nfunction getComponentPrice(\n component: BundleComponentView,\n selectedVariantId: string | undefined,\n): number {\n if (!selectedVariantId || component.available_variants.length === 0) {\n return parsePrice(component.effective_price);\n }\n if (selectedVariantId === component.variant_id) {\n return parsePrice(component.effective_price);\n }\n const bakedAdj = component.variant_id\n ? component.available_variants.find((v) => v.id === component.variant_id)\n : undefined;\n const selectedAdj = component.available_variants.find((v) => v.id === selectedVariantId);\n if (!selectedAdj) return parsePrice(component.effective_price);\n return parsePrice(component.effective_price)\n - parsePrice(bakedAdj?.price_adjustment ?? \"0\")\n + parsePrice(selectedAdj.price_adjustment);\n}\n\ninterface BundleComponentCardProps {\n component: BundleComponentView;\n selectedVariantId?: string;\n onVariantChange: (variantId: string) => void;\n classNames?: BundleSelectorClassNames;\n}\n\nfunction BundleComponentCard({\n component,\n selectedVariantId,\n onVariantChange,\n classNames,\n}: BundleComponentCardProps): React.ReactElement {\n const idPrefix = useId();\n const showVariantPicker =\n component.allow_variant_choice && component.available_variants.length > 1;\n\n const displayPrice = useMemo(\n () => getComponentPrice(component, selectedVariantId),\n [component, selectedVariantId],\n );\n\n const labelId = `${idPrefix}-bundle-component-${component.id}`;\n\n return (\n <div\n data-cimplify-bundle-component\n className={cn(\"border border-border p-4\", classNames?.component)}\n >\n <div\n data-cimplify-bundle-component-header\n className={cn(\"flex items-start justify-between gap-3\", classNames?.componentHeader)}\n >\n <div>\n {component.quantity > 1 && (\n <span\n data-cimplify-bundle-component-qty\n className={cn(\"text-xs font-medium text-primary bg-primary/10 px-1.5 py-0.5\", classNames?.componentQty)}\n >\n ×{component.quantity}\n </span>\n )}\n <span\n id={labelId}\n data-cimplify-bundle-component-name\n className={cn(\"font-medium text-sm\", classNames?.componentName)}\n >\n {component.product_name}\n </span>\n </div>\n <Price amount={displayPrice} />\n </div>\n\n {showVariantPicker && (\n <RadioGroup\n aria-labelledby={labelId}\n value={selectedVariantId ?? \"\"}\n onValueChange={(value) => {\n onVariantChange(value);\n }}\n data-cimplify-bundle-variant-picker\n className={cn(\"mt-3 flex flex-wrap gap-2\", classNames?.variantPicker)}\n >\n {component.available_variants.map((variant: BundleComponentVariantView) => {\n const isSelected = selectedVariantId === variant.id;\n const adjustment = parsePrice(variant.price_adjustment);\n\n return (\n <Radio.Root\n key={variant.id}\n value={variant.id}\n data-cimplify-bundle-variant-option\n data-selected={isSelected || undefined}\n className={cn(\n \"px-3 py-1.5 border text-xs font-medium transition-colors border-border hover:border-primary/50\",\n isSelected && \"bg-primary text-primary-foreground border-primary\",\n isSelected ? classNames?.variantOptionSelected : classNames?.variantOption,\n )}\n >\n {variant.display_name}\n {adjustment !== 0 && (\n <span\n data-cimplify-bundle-variant-adjustment\n className={cn(\"ml-1 opacity-70\", classNames?.variantAdjustment)}\n >\n {adjustment > 0 ? \"+\" : \"\"}\n <Price amount={variant.price_adjustment} />\n </span>\n )}\n </Radio.Root>\n );\n })}\n </RadioGroup>\n )}\n </div>\n );\n}\n"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "composite-selector.tsx",
|
|
12
|
-
"content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect } from \"react\";\nimport { Checkbox } from \"@base-ui/react/checkbox\";\nimport { NumberField } from \"@base-ui/react/number-field\";\nimport type {\n CompositeGroupView,\n CompositeComponentView,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport { useCimplify } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface CompositeSelectorClassNames {\n root?: string;\n group?: string;\n groupHeader?: string;\n groupName?: string;\n required?: string;\n groupDescription?: string;\n groupConstraint?: string;\n validation?: string;\n components?: string;\n component?: string;\n componentSelected?: string;\n componentInfo?: string;\n componentName?: string;\n badgePopular?: string;\n badgePremium?: string;\n componentDescription?: string;\n componentCalories?: string;\n qty?: string;\n qtyButton?: string;\n qtyValue?: string;\n summary?: string;\n summaryLine?: string;\n summaryTotal?: string;\n calculating?: string;\n priceError?: string;\n}\n\nexport interface CompositeSelectorProps {\n compositeId: string;\n groups: CompositeGroupView[];\n onSelectionsChange: (selections: ComponentSelectionInput[]) => void;\n onPriceChange?: (price: CompositePriceResult | null) => void;\n onReady?: (ready: boolean) => void;\n skipPriceFetch?: boolean;\n className?: string;\n classNames?: CompositeSelectorClassNames;\n}\n\nexport function CompositeSelector({\n compositeId,\n groups,\n onSelectionsChange,\n onPriceChange,\n onReady,\n skipPriceFetch,\n className,\n classNames,\n}: CompositeSelectorProps): React.ReactElement | null {\n const { client } = useCimplify();\n\n const [groupSelections, setGroupSelections] = useState<\n Record<string, Record<string, number>>\n >({});\n const [priceResult, setPriceResult] = useState<CompositePriceResult | null>(null);\n const [isPriceLoading, setIsPriceLoading] = useState(false);\n const [priceError, setPriceError] = useState(false);\n\n const selections = useMemo((): ComponentSelectionInput[] => {\n const result: ComponentSelectionInput[] = [];\n for (const groupSels of Object.values(groupSelections)) {\n for (const [componentId, qty] of Object.entries(groupSels)) {\n if (qty > 0) {\n result.push({ component_id: componentId, quantity: qty });\n }\n }\n }\n return result;\n }, [groupSelections]);\n\n useEffect(() => {\n onSelectionsChange(selections);\n }, [selections, onSelectionsChange]);\n\n useEffect(() => {\n onPriceChange?.(priceResult);\n }, [priceResult, onPriceChange]);\n\n const allGroupsSatisfied = useMemo(() => {\n for (const group of groups) {\n const groupSels = groupSelections[group.id] || {};\n const totalSelected = Object.values(groupSels).reduce((sum, q) => sum + q, 0);\n if (totalSelected < group.min_selections) return false;\n }\n return true;\n }, [groups, groupSelections]);\n\n useEffect(() => {\n onReady?.(allGroupsSatisfied);\n }, [allGroupsSatisfied, onReady]);\n\n useEffect(() => {\n if (skipPriceFetch || !allGroupsSatisfied || selections.length === 0) return;\n\n let cancelled = false;\n const timer = setTimeout(() => {\n void (async () => {\n setIsPriceLoading(true);\n setPriceError(false);\n try {\n const result = await client.catalogue.calculateCompositePrice(compositeId, selections);\n if (cancelled) return;\n if (result.ok) {\n setPriceResult(result.value);\n } else {\n setPriceError(true);\n }\n } catch {\n if (!cancelled) setPriceError(true);\n } finally {\n if (!cancelled) setIsPriceLoading(false);\n }\n })();\n }, 300);\n\n return () => {\n cancelled = true;\n clearTimeout(timer);\n };\n }, [selections, allGroupsSatisfied, compositeId, client, skipPriceFetch]);\n\n const toggleComponent = useCallback(\n (group: CompositeGroupView, component: CompositeComponentView) => {\n setGroupSelections((prev) => {\n const groupSels = { ...(prev[group.id] || {}) };\n const currentQty = groupSels[component.id] || 0;\n\n if (currentQty > 0) {\n if (group.min_selections > 0) {\n const totalOthers = Object.entries(groupSels)\n .filter(([id]) => id !== component.id)\n .reduce((sum, [, q]) => sum + q, 0);\n if (totalOthers < group.min_selections) {\n return prev;\n }\n }\n delete groupSels[component.id];\n } else {\n const totalSelected = Object.values(groupSels).reduce((sum, q) => sum + q, 0);\n if (group.max_selections && totalSelected >= group.max_selections) {\n if (group.max_selections === 1) {\n return { ...prev, [group.id]: { [component.id]: 1 } };\n }\n return prev;\n }\n groupSels[component.id] = 1;\n }\n\n return { ...prev, [group.id]: groupSels };\n });\n },\n [],\n );\n\n const updateQuantity = useCallback(\n (group: CompositeGroupView, componentId: string, newValue: number) => {\n setGroupSelections((prev) => {\n const groupSels = { ...(prev[group.id] || {}) };\n const current = groupSels[componentId] || 0;\n const next = Math.max(0, newValue);\n\n if (next === current) return prev;\n\n const delta = next - current;\n\n if (group.max_quantity_per_component && next > group.max_quantity_per_component) {\n return prev;\n }\n\n const totalAfter = Object.entries(groupSels)\n .reduce((sum, [id, q]) => sum + (id === componentId ? next : q), 0);\n\n if (delta > 0 && group.max_selections && totalAfter > group.max_selections) {\n return prev;\n }\n\n if (delta < 0 && totalAfter < group.min_selections) {\n return prev;\n }\n\n if (next === 0) {\n delete groupSels[componentId];\n } else {\n groupSels[componentId] = next;\n }\n\n return { ...prev, [group.id]: groupSels };\n });\n },\n [],\n );\n\n if (groups.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-composite-selector className={cn(\"space-y-6\", className, classNames?.root)}>\n {[...groups]\n .sort((a, b) => a.display_order - b.display_order)\n .map((group) => {\n const groupSels = groupSelections[group.id] || {};\n const totalSelected = Object.values(groupSels).reduce((sum, q) => sum + q, 0);\n const minMet = totalSelected >= group.min_selections;\n const isSingleSelect = group.max_selections === 1;\n\n return (\n <div\n key={group.id}\n data-cimplify-composite-group\n className={cn(\"border border-border p-5\", classNames?.group)}\n >\n <div\n data-cimplify-composite-group-header\n className={cn(\"flex items-center justify-between mb-4\", classNames?.groupHeader)}\n >\n <div>\n <span\n data-cimplify-composite-group-name\n className={cn(\"text-xs font-medium uppercase tracking-wider text-muted-foreground\", classNames?.groupName)}\n >\n {group.name}\n {group.min_selections > 0 && (\n <span\n data-cimplify-composite-required\n className={cn(\"text-destructive ml-1\", classNames?.required)}\n >\n {\" \"}*\n </span>\n )}\n </span>\n {group.description && (\n <span\n data-cimplify-composite-group-description\n className={cn(\"text-xs text-muted-foreground/70 mt-0.5\", classNames?.groupDescription)}\n >\n {group.description}\n </span>\n )}\n <span\n data-cimplify-composite-group-constraint\n className={cn(\"text-xs text-muted-foreground/70 mt-1\", classNames?.groupConstraint)}\n >\n {group.min_selections > 0 && group.max_selections\n ? `Choose ${group.min_selections}\\u2013${group.max_selections}`\n : group.min_selections > 0\n ? `Choose at least ${group.min_selections}`\n : group.max_selections\n ? `Choose up to ${group.max_selections}`\n : \"Choose as many as you like\"}\n </span>\n </div>\n {!minMet && (\n <span\n data-cimplify-composite-validation\n className={cn(\"text-xs text-destructive font-medium\", classNames?.validation)}\n >\n Required\n </span>\n )}\n </div>\n\n <div\n data-cimplify-composite-components\n role={isSingleSelect ? \"radiogroup\" : \"group\"}\n aria-label={group.name}\n className={cn(\"space-y-1\", classNames?.components)}\n >\n {group.components\n .filter((c) => c.is_available && !c.is_archived)\n .sort((a, b) => a.display_order - b.display_order)\n .map((component) => {\n const qty = groupSels[component.id] || 0;\n const isSelected = qty > 0;\n const displayName = component.display_name || component.id;\n\n return (\n <Checkbox.Root\n key={component.id}\n checked={isSelected}\n onCheckedChange={() => toggleComponent(group, component)}\n value={component.id}\n render={<button type=\"button\" />}\n data-cimplify-composite-component\n data-selected={isSelected || undefined}\n className={cn(\n \"w-full flex items-center gap-3 px-4 py-3 border transition-colors text-left border-transparent hover:bg-muted/50\",\n isSelected && \"bg-primary/5 border-primary\",\n isSelected ? classNames?.componentSelected : classNames?.component,\n )}\n >\n <Checkbox.Indicator\n className=\"hidden\"\n keepMounted={false}\n />\n\n <div\n data-cimplify-composite-component-info\n className={cn(\"flex-1 min-w-0\", classNames?.componentInfo)}\n >\n <span\n data-cimplify-composite-component-name\n className={cn(\"text-sm font-medium\", isSelected && \"text-primary\", classNames?.componentName)}\n >\n {displayName}\n </span>\n {component.is_popular && (\n <span\n data-cimplify-composite-badge=\"popular\"\n className={cn(\"text-[10px] uppercase tracking-wider text-primary font-medium\", classNames?.badgePopular)}\n >\n Popular\n </span>\n )}\n {component.is_premium && (\n <span\n data-cimplify-composite-badge=\"premium\"\n className={cn(\"text-[10px] uppercase tracking-wider text-amber-600 font-medium\", classNames?.badgePremium)}\n >\n Premium\n </span>\n )}\n {component.display_description && (\n <span\n data-cimplify-composite-component-description\n className={cn(\"text-xs text-muted-foreground truncate\", classNames?.componentDescription)}\n >\n {component.display_description}\n </span>\n )}\n {component.calories != null && (\n <span\n data-cimplify-composite-component-calories\n className={cn(\"text-xs text-muted-foreground/60\", classNames?.componentCalories)}\n >\n {component.calories} cal\n </span>\n )}\n </div>\n\n {group.allow_quantity && isSelected && (\n <NumberField.Root\n value={qty}\n onValueChange={(val) => {\n if (val != null) {\n updateQuantity(group, component.id, val);\n }\n }}\n min={0}\n max={group.max_quantity_per_component || undefined}\n step={1}\n >\n <NumberField.Group\n data-cimplify-composite-qty\n onClick={(e: React.MouseEvent) => e.stopPropagation()}\n className={cn(\"flex items-center gap-2\", classNames?.qty)}\n >\n <NumberField.Decrement\n aria-label={`Decrease ${displayName} quantity`}\n className={cn(\"w-6 h-6 border border-border flex items-center justify-center text-xs hover:bg-muted disabled:opacity-30\", classNames?.qtyButton)}\n >\n −\n </NumberField.Decrement>\n <NumberField.Input\n readOnly\n className={cn(\"w-4 text-center text-sm font-medium bg-transparent border-none outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none\", classNames?.qtyValue)}\n />\n <NumberField.Increment\n aria-label={`Increase ${displayName} quantity`}\n className={cn(\"w-6 h-6 border border-border flex items-center justify-center text-xs hover:bg-muted disabled:opacity-30\", classNames?.qtyButton)}\n >\n +\n </NumberField.Increment>\n </NumberField.Group>\n </NumberField.Root>\n )}\n\n {component.price != null && parsePrice(component.price) !== 0 && (\n <Price amount={component.price} prefix=\"+\" />\n )}\n </Checkbox.Root>\n );\n })}\n </div>\n </div>\n );\n })}\n\n {priceResult && (\n <div\n data-cimplify-composite-summary\n className={cn(\"border-t border-border pt-4 space-y-1 text-sm\", classNames?.summary)}\n >\n {parsePrice(priceResult.base_price) !== 0 && (\n <div\n data-cimplify-composite-summary-line\n className={cn(\"flex justify-between text-muted-foreground\", classNames?.summaryLine)}\n >\n <span>Base</span>\n <Price amount={priceResult.base_price} />\n </div>\n )}\n {parsePrice(priceResult.components_total) !== 0 && (\n <div\n data-cimplify-composite-summary-line\n className={cn(\"flex justify-between text-muted-foreground\", classNames?.summaryLine)}\n >\n <span>Selections</span>\n <Price amount={priceResult.components_total} />\n </div>\n )}\n <div\n data-cimplify-composite-summary-total\n className={cn(\"flex justify-between font-medium pt-1 border-t border-border\", classNames?.summaryTotal)}\n >\n <span>Total</span>\n <Price amount={priceResult.final_price} className=\"text-primary\" />\n </div>\n </div>\n )}\n\n {isPriceLoading && (\n <div\n data-cimplify-composite-calculating\n className={cn(\"flex items-center gap-2 text-sm text-muted-foreground\", classNames?.calculating)}\n >\n Calculating price...\n </div>\n )}\n\n {priceError && !isPriceLoading && (\n <div\n data-cimplify-composite-price-error\n className={cn(\"text-sm text-destructive\", classNames?.priceError)}\n >\n Unable to calculate price\n </div>\n )}\n </div>\n );\n}\n"
|
|
12
|
+
"content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect } from \"react\";\nimport { Checkbox } from \"@base-ui/react/checkbox\";\nimport { NumberField } from \"@base-ui/react/number-field\";\nimport type {\n CompositeGroupView,\n CompositeComponentView,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport { useCimplify } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface CompositeSelectorClassNames {\n root?: string;\n group?: string;\n groupHeader?: string;\n groupName?: string;\n required?: string;\n groupDescription?: string;\n groupConstraint?: string;\n validation?: string;\n components?: string;\n component?: string;\n componentSelected?: string;\n componentInfo?: string;\n componentName?: string;\n badgePopular?: string;\n badgePremium?: string;\n componentDescription?: string;\n componentCalories?: string;\n qty?: string;\n qtyButton?: string;\n qtyValue?: string;\n summary?: string;\n summaryLine?: string;\n summaryTotal?: string;\n calculating?: string;\n priceError?: string;\n}\n\nexport interface CompositeSelectorProps {\n compositeId: string;\n groups: CompositeGroupView[];\n onSelectionsChange: (selections: ComponentSelectionInput[]) => void;\n onPriceChange?: (price: CompositePriceResult | null) => void;\n onReady?: (ready: boolean) => void;\n skipPriceFetch?: boolean;\n className?: string;\n classNames?: CompositeSelectorClassNames;\n}\n\nexport function CompositeSelector({\n compositeId,\n groups,\n onSelectionsChange,\n onPriceChange,\n onReady,\n skipPriceFetch,\n className,\n classNames,\n}: CompositeSelectorProps): React.ReactElement | null {\n const { client } = useCimplify();\n\n const [groupSelections, setGroupSelections] = useState<\n Record<string, Record<string, number>>\n >({});\n const [priceResult, setPriceResult] = useState<CompositePriceResult | null>(null);\n const [isPriceLoading, setIsPriceLoading] = useState(false);\n const [priceError, setPriceError] = useState(false);\n\n const selections = useMemo((): ComponentSelectionInput[] => {\n const result: ComponentSelectionInput[] = [];\n for (const groupSels of Object.values(groupSelections)) {\n for (const [componentId, qty] of Object.entries(groupSels)) {\n if (qty > 0) {\n result.push({ component_id: componentId, quantity: qty });\n }\n }\n }\n return result;\n }, [groupSelections]);\n\n useEffect(() => {\n onSelectionsChange(selections);\n }, [selections, onSelectionsChange]);\n\n useEffect(() => {\n onPriceChange?.(priceResult);\n }, [priceResult, onPriceChange]);\n\n const allGroupsSatisfied = useMemo(() => {\n for (const group of groups) {\n const groupSels = groupSelections[group.id] || {};\n const totalSelected = Object.values(groupSels).reduce((sum, q) => sum + q, 0);\n if (totalSelected < group.min_selections) return false;\n }\n return true;\n }, [groups, groupSelections]);\n\n useEffect(() => {\n onReady?.(allGroupsSatisfied);\n }, [allGroupsSatisfied, onReady]);\n\n useEffect(() => {\n if (skipPriceFetch || !allGroupsSatisfied || selections.length === 0) return;\n\n let cancelled = false;\n const timer = setTimeout(() => {\n void (async () => {\n setIsPriceLoading(true);\n setPriceError(false);\n try {\n const result = await client.catalogue.calculateCompositePrice(compositeId, selections);\n if (cancelled) return;\n if (result.ok) {\n setPriceResult(result.value);\n } else {\n setPriceError(true);\n }\n } catch {\n if (!cancelled) setPriceError(true);\n } finally {\n if (!cancelled) setIsPriceLoading(false);\n }\n })();\n }, 300);\n\n return () => {\n cancelled = true;\n clearTimeout(timer);\n };\n }, [selections, allGroupsSatisfied, compositeId, client, skipPriceFetch]);\n\n const toggleComponent = useCallback(\n (group: CompositeGroupView, component: CompositeComponentView) => {\n setGroupSelections((prev) => {\n const groupSels = { ...(prev[group.id] || {}) };\n const currentQty = groupSels[component.id] || 0;\n\n if (currentQty > 0) {\n if (group.min_selections > 0) {\n const totalOthers = Object.entries(groupSels)\n .filter(([id]) => id !== component.id)\n .reduce((sum, [, q]) => sum + q, 0);\n if (totalOthers < group.min_selections) {\n return prev;\n }\n }\n delete groupSels[component.id];\n } else {\n const totalSelected = Object.values(groupSels).reduce((sum, q) => sum + q, 0);\n if (group.max_selections && totalSelected >= group.max_selections) {\n if (group.max_selections === 1) {\n return { ...prev, [group.id]: { [component.id]: 1 } };\n }\n return prev;\n }\n groupSels[component.id] = 1;\n }\n\n return { ...prev, [group.id]: groupSels };\n });\n },\n [],\n );\n\n const updateQuantity = useCallback(\n (group: CompositeGroupView, componentId: string, newValue: number) => {\n setGroupSelections((prev) => {\n const groupSels = { ...(prev[group.id] || {}) };\n const current = groupSels[componentId] || 0;\n const next = Math.max(0, newValue);\n\n if (next === current) return prev;\n\n const delta = next - current;\n\n if (group.max_quantity_per_component && next > group.max_quantity_per_component) {\n return prev;\n }\n\n const totalAfter = Object.entries(groupSels)\n .reduce((sum, [id, q]) => sum + (id === componentId ? next : q), 0);\n\n if (delta > 0 && group.max_selections && totalAfter > group.max_selections) {\n return prev;\n }\n\n if (delta < 0 && totalAfter < group.min_selections) {\n return prev;\n }\n\n if (next === 0) {\n delete groupSels[componentId];\n } else {\n groupSels[componentId] = next;\n }\n\n return { ...prev, [group.id]: groupSels };\n });\n },\n [],\n );\n\n if (groups.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-composite-selector className={cn(\"space-y-6\", className, classNames?.root)}>\n {[...groups]\n .sort((a, b) => a.display_order - b.display_order)\n .map((group) => {\n const groupSels = groupSelections[group.id] || {};\n const totalSelected = Object.values(groupSels).reduce((sum, q) => sum + q, 0);\n const minMet = totalSelected >= group.min_selections;\n const isSingleSelect = group.max_selections === 1;\n\n return (\n <div\n key={group.id}\n data-cimplify-composite-group\n className={cn(\"border border-border p-5\", classNames?.group)}\n >\n <div\n data-cimplify-composite-group-header\n className={cn(\"flex items-center justify-between mb-4\", classNames?.groupHeader)}\n >\n <div>\n <span\n data-cimplify-composite-group-name\n className={cn(\"text-xs font-medium uppercase tracking-wider text-muted-foreground\", classNames?.groupName)}\n >\n {group.name}\n {group.min_selections > 0 && (\n <span\n data-cimplify-composite-required\n className={cn(\"text-destructive ml-1\", classNames?.required)}\n >\n {\" \"}*\n </span>\n )}\n </span>\n {group.description && (\n <span\n data-cimplify-composite-group-description\n className={cn(\"text-xs text-muted-foreground/70 mt-0.5\", classNames?.groupDescription)}\n >\n {group.description}\n </span>\n )}\n <span\n data-cimplify-composite-group-constraint\n className={cn(\"text-xs text-muted-foreground/70 mt-1\", classNames?.groupConstraint)}\n >\n {group.min_selections > 0 && group.max_selections\n ? `Choose ${group.min_selections}\\u2013${group.max_selections}`\n : group.min_selections > 0\n ? `Choose at least ${group.min_selections}`\n : group.max_selections\n ? `Choose up to ${group.max_selections}`\n : \"Choose as many as you like\"}\n </span>\n </div>\n {!minMet && (\n <span\n data-cimplify-composite-validation\n className={cn(\"text-xs text-destructive font-medium\", classNames?.validation)}\n >\n Required\n </span>\n )}\n </div>\n\n <div\n data-cimplify-composite-components\n role={isSingleSelect ? \"radiogroup\" : \"group\"}\n aria-label={group.name}\n className={cn(\"space-y-1\", classNames?.components)}\n >\n {group.components\n .filter((c) => c.is_available && !c.is_archived)\n .sort((a, b) => a.display_order - b.display_order)\n .map((component) => {\n const qty = groupSels[component.id] || 0;\n const isSelected = qty > 0;\n const displayName = component.display_name || component.id;\n\n return (\n <Checkbox.Root\n key={component.id}\n checked={isSelected}\n onCheckedChange={() => toggleComponent(group, component)}\n value={component.id}\n data-cimplify-composite-component\n data-selected={isSelected || undefined}\n className={cn(\n \"w-full flex items-center gap-3 px-4 py-3 border transition-colors text-left border-transparent hover:bg-muted/50\",\n isSelected && \"bg-primary/5 border-primary\",\n isSelected ? classNames?.componentSelected : classNames?.component,\n )}\n >\n <Checkbox.Indicator\n className=\"hidden\"\n keepMounted={false}\n />\n\n <div\n data-cimplify-composite-component-info\n className={cn(\"flex-1 min-w-0\", classNames?.componentInfo)}\n >\n <span\n data-cimplify-composite-component-name\n className={cn(\"text-sm font-medium\", isSelected && \"text-primary\", classNames?.componentName)}\n >\n {displayName}\n </span>\n {component.is_popular && (\n <span\n data-cimplify-composite-badge=\"popular\"\n className={cn(\"text-[10px] uppercase tracking-wider text-primary font-medium\", classNames?.badgePopular)}\n >\n Popular\n </span>\n )}\n {component.is_premium && (\n <span\n data-cimplify-composite-badge=\"premium\"\n className={cn(\"text-[10px] uppercase tracking-wider text-amber-600 font-medium\", classNames?.badgePremium)}\n >\n Premium\n </span>\n )}\n {component.display_description && (\n <span\n data-cimplify-composite-component-description\n className={cn(\"text-xs text-muted-foreground truncate\", classNames?.componentDescription)}\n >\n {component.display_description}\n </span>\n )}\n {component.calories != null && (\n <span\n data-cimplify-composite-component-calories\n className={cn(\"text-xs text-muted-foreground/60\", classNames?.componentCalories)}\n >\n {component.calories} cal\n </span>\n )}\n </div>\n\n {group.allow_quantity && isSelected && (\n <NumberField.Root\n value={qty}\n onValueChange={(val) => {\n if (val != null) {\n updateQuantity(group, component.id, val);\n }\n }}\n min={0}\n max={group.max_quantity_per_component || undefined}\n step={1}\n >\n <NumberField.Group\n data-cimplify-composite-qty\n onClick={(e: React.MouseEvent) => e.stopPropagation()}\n className={cn(\"flex items-center gap-2\", classNames?.qty)}\n >\n <NumberField.Decrement\n aria-label={`Decrease ${displayName} quantity`}\n className={cn(\"w-6 h-6 border border-border flex items-center justify-center text-xs hover:bg-muted disabled:opacity-30\", classNames?.qtyButton)}\n >\n −\n </NumberField.Decrement>\n <NumberField.Input\n readOnly\n className={cn(\"w-4 text-center text-sm font-medium bg-transparent border-none outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none\", classNames?.qtyValue)}\n />\n <NumberField.Increment\n aria-label={`Increase ${displayName} quantity`}\n className={cn(\"w-6 h-6 border border-border flex items-center justify-center text-xs hover:bg-muted disabled:opacity-30\", classNames?.qtyButton)}\n >\n +\n </NumberField.Increment>\n </NumberField.Group>\n </NumberField.Root>\n )}\n\n {component.price != null && parsePrice(component.price) !== 0 && (\n <Price amount={component.price} prefix=\"+\" />\n )}\n </Checkbox.Root>\n );\n })}\n </div>\n </div>\n );\n })}\n\n {priceResult && (\n <div\n data-cimplify-composite-summary\n className={cn(\"border-t border-border pt-4 space-y-1 text-sm\", classNames?.summary)}\n >\n {parsePrice(priceResult.base_price) !== 0 && (\n <div\n data-cimplify-composite-summary-line\n className={cn(\"flex justify-between text-muted-foreground\", classNames?.summaryLine)}\n >\n <span>Base</span>\n <Price amount={priceResult.base_price} />\n </div>\n )}\n {parsePrice(priceResult.components_total) !== 0 && (\n <div\n data-cimplify-composite-summary-line\n className={cn(\"flex justify-between text-muted-foreground\", classNames?.summaryLine)}\n >\n <span>Selections</span>\n <Price amount={priceResult.components_total} />\n </div>\n )}\n <div\n data-cimplify-composite-summary-total\n className={cn(\"flex justify-between font-medium pt-1 border-t border-border\", classNames?.summaryTotal)}\n >\n <span>Total</span>\n <Price amount={priceResult.final_price} className=\"text-primary\" />\n </div>\n </div>\n )}\n\n {isPriceLoading && (\n <div\n data-cimplify-composite-calculating\n className={cn(\"flex items-center gap-2 text-sm text-muted-foreground\", classNames?.calculating)}\n >\n Calculating price...\n </div>\n )}\n\n {priceError && !isPriceLoading && (\n <div\n data-cimplify-composite-price-error\n className={cn(\"text-sm text-destructive\", classNames?.priceError)}\n >\n Unable to calculate price\n </div>\n )}\n </div>\n );\n}\n"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"files": [
|
|
8
8
|
{
|
|
9
9
|
"path": "product-image-gallery.tsx",
|
|
10
|
-
"content": "\"use client\";\n\nimport React, { useEffect, useMemo, useState } from \"react\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport { Radio } from \"@base-ui/react/radio\";\n\nexport interface ProductImageGalleryProps {\n images: string[];\n productName: string;\n aspectRatio?: \"square\" | \"4/3\" | \"16/10\" | \"3/4\";\n className?: string;\n}\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\n/**\n * ProductImageGallery — main image + thumbnail strip.\n *\n * Uses plain `<img>` for framework-agnostic rendering (not Next.js Image).\n */\nexport function ProductImageGallery({\n images,\n productName,\n aspectRatio = \"4/3\",\n className,\n}: ProductImageGalleryProps): React.ReactElement | null {\n const normalizedImages = useMemo(\n () =>\n images.filter(\n (image): image is string =>\n typeof image === \"string\" && image.trim().length > 0,\n ),\n [images],\n );\n\n const [selectedImage, setSelectedImage] = useState(0);\n\n useEffect(() => {\n setSelectedImage(0);\n }, [normalizedImages.length, productName]);\n\n if (normalizedImages.length === 0) {\n return null;\n }\n\n const activeImage = normalizedImages[selectedImage] || normalizedImages[0];\n\n return (\n <div data-cimplify-image-gallery className={className}>\n <div\n data-cimplify-image-gallery-main\n style={{ position: \"relative\", overflow: \"hidden\", ...ASPECT_STYLES[aspectRatio] }}\n >\n <img\n src={activeImage}\n alt={productName}\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n data-cimplify-image-gallery-active\n />\n </div>\n\n {normalizedImages.length > 1 && (\n <RadioGroup\n aria-label={`${productName} image thumbnails`}\n value={String(selectedImage)}\n onValueChange={(value) => setSelectedImage(Number(value))}\n data-cimplify-image-gallery-thumbnails\n style={{ display: \"flex\", gap: \"0.5rem\", marginTop: \"0.75rem\" }}\n >\n {normalizedImages.map((image, index) => {\n const isSelected = selectedImage === index;\n return (\n <Radio.Root\n key={`${image}-${index}`}\n value={String(index)}\n
|
|
10
|
+
"content": "\"use client\";\n\nimport React, { useEffect, useMemo, useState } from \"react\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport { Radio } from \"@base-ui/react/radio\";\n\nexport interface ProductImageGalleryProps {\n images: string[];\n productName: string;\n aspectRatio?: \"square\" | \"4/3\" | \"16/10\" | \"3/4\";\n className?: string;\n}\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\n/**\n * ProductImageGallery — main image + thumbnail strip.\n *\n * Uses plain `<img>` for framework-agnostic rendering (not Next.js Image).\n */\nexport function ProductImageGallery({\n images,\n productName,\n aspectRatio = \"4/3\",\n className,\n}: ProductImageGalleryProps): React.ReactElement | null {\n const normalizedImages = useMemo(\n () =>\n images.filter(\n (image): image is string =>\n typeof image === \"string\" && image.trim().length > 0,\n ),\n [images],\n );\n\n const [selectedImage, setSelectedImage] = useState(0);\n\n useEffect(() => {\n setSelectedImage(0);\n }, [normalizedImages.length, productName]);\n\n if (normalizedImages.length === 0) {\n return null;\n }\n\n const activeImage = normalizedImages[selectedImage] || normalizedImages[0];\n\n return (\n <div data-cimplify-image-gallery className={className}>\n <div\n data-cimplify-image-gallery-main\n style={{ position: \"relative\", overflow: \"hidden\", ...ASPECT_STYLES[aspectRatio] }}\n >\n <img\n src={activeImage}\n alt={productName}\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n data-cimplify-image-gallery-active\n />\n </div>\n\n {normalizedImages.length > 1 && (\n <RadioGroup\n aria-label={`${productName} image thumbnails`}\n value={String(selectedImage)}\n onValueChange={(value) => setSelectedImage(Number(value))}\n data-cimplify-image-gallery-thumbnails\n style={{ display: \"flex\", gap: \"0.5rem\", marginTop: \"0.75rem\" }}\n >\n {normalizedImages.map((image, index) => {\n const isSelected = selectedImage === index;\n return (\n <Radio.Root\n key={`${image}-${index}`}\n value={String(index)}\n data-cimplify-image-gallery-thumb\n data-selected={isSelected || undefined}\n style={{\n width: \"4rem\",\n height: \"4rem\",\n overflow: \"hidden\",\n padding: 0,\n border: \"none\",\n cursor: \"pointer\",\n }}\n >\n <img\n src={image}\n alt=\"\"\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n />\n </Radio.Root>\n );\n })}\n </RadioGroup>\n )}\n </div>\n );\n}\n"
|
|
11
11
|
}
|
|
12
12
|
]
|
|
13
13
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
{
|
|
12
12
|
"path": "slot-picker.tsx",
|
|
13
|
-
"content": "\"use client\";\n\nimport { Radio } from \"@base-ui/react/radio\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport React from \"react\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\nimport { useAvailableSlots } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface SlotPickerClassNames {\n root?: string;\n group?: string;\n groupLabel?: string;\n slot?: string;\n slotTime?: string;\n slotPrice?: string;\n loading?: string;\n empty?: string;\n}\n\nexport interface SlotPickerProps {\n /** Pre-fetched slots (skips fetch). */\n slots?: AvailableSlot[];\n /** Service ID — used to fetch slots when `slots` prop is not provided. */\n serviceId?: string;\n /** Date string (YYYY-MM-DD) — used to fetch slots when `slots` prop is not provided. */\n date?: string;\n /** Number of participants for capacity-based availability. */\n participantCount?: number;\n /** Currently selected slot. */\n selectedSlot?: AvailableSlot | null;\n /** Called when a slot is selected. */\n onSlotSelect?: (slot: AvailableSlot) => void;\n /** Whether to group slots by time of day. Default: true. */\n groupByTimeOfDay?: boolean;\n /** Show price on each slot. Default: true. */\n showPrice?: boolean;\n /** Text shown when no slots available. */\n emptyMessage?: string;\n className?: string;\n classNames?: SlotPickerClassNames;\n}\n\ninterface SlotGroup {\n label: string;\n slots: AvailableSlot[];\n}\n\nfunction getTimeOfDay(timeStr: string): \"morning\" | \"afternoon\" | \"evening\" {\n const hour = parseInt(timeStr.split(\"T\").pop()?.split(\":\")[0] ?? timeStr.split(\":\")[0], 10);\n if (hour < 12) return \"morning\";\n if (hour < 17) return \"afternoon\";\n return \"evening\";\n}\n\nconst TIME_OF_DAY_LABELS: Record<string, string> = {\n morning: \"Morning\",\n afternoon: \"Afternoon\",\n evening: \"Evening\",\n};\n\nfunction groupSlots(slots: AvailableSlot[]): SlotGroup[] {\n const groups: Record<string, AvailableSlot[]> = {};\n for (const slot of slots) {\n const tod = getTimeOfDay(slot.start_time);\n if (!groups[tod]) groups[tod] = [];\n groups[tod].push(slot);\n }\n return ([\"morning\", \"afternoon\", \"evening\"] as const)\n .filter((tod) => groups[tod]?.length)\n .map((tod) => ({ label: TIME_OF_DAY_LABELS[tod], slots: groups[tod] }));\n}\n\nfunction formatTime(timeStr: string): string {\n try {\n const date = new Date(timeStr);\n if (!isNaN(date.getTime())) {\n return date.toLocaleTimeString(undefined, { hour: \"numeric\", minute: \"2-digit\" });\n }\n } catch {\n // noop\n }\n\n const parts = timeStr.split(\":\");\n if (parts.length >= 2) {\n const hour = parseInt(parts[0], 10);\n const minute = parts[1];\n const ampm = hour >= 12 ? \"PM\" : \"AM\";\n const displayHour = hour % 12 || 12;\n return `${displayHour}:${minute} ${ampm}`;\n }\n return timeStr;\n}\n\nfunction slotToValue(slot: AvailableSlot): string {\n return `${slot.start_time}|${slot.end_time}`;\n}\n\nexport function SlotPicker({\n slots: slotsProp,\n serviceId,\n date,\n participantCount,\n selectedSlot,\n onSlotSelect,\n groupByTimeOfDay = true,\n showPrice = true,\n emptyMessage = \"No available slots\",\n className,\n classNames,\n}: SlotPickerProps): React.ReactElement {\n const { slots: fetched, isLoading } = useAvailableSlots(\n serviceId ?? null,\n date ?? null,\n {\n participantCount,\n enabled: slotsProp === undefined && !!serviceId && !!date,\n },\n );\n\n const slots = slotsProp ?? fetched;\n\n if (isLoading && slots.length === 0) {\n return (\n <div\n data-cimplify-slot-picker\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (slots.length === 0) {\n return (\n <div\n data-cimplify-slot-picker\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage}</p>\n </div>\n );\n }\n\n const groups = groupByTimeOfDay ? groupSlots(slots) : [{ label: \"\", slots }];\n\n const slotsByValue = new Map<string, AvailableSlot>();\n for (const slot of slots) {\n slotsByValue.set(slotToValue(slot), slot);\n }\n\n const selectedValue = selectedSlot ? slotToValue(selectedSlot) : \"\";\n\n return (\n <RadioGroup\n data-cimplify-slot-picker\n className={cn(className, classNames?.root)}\n value={selectedValue}\n onValueChange={(value: string) => {\n const slot = slotsByValue.get(value);\n if (slot?.is_available) {\n onSlotSelect?.(slot);\n }\n }}\n >\n {groups.map((group) => (\n <div key={group.label || \"all\"} data-cimplify-slot-group className={classNames?.group}>\n {group.label && (\n <div data-cimplify-slot-group-label className={classNames?.groupLabel}>\n {group.label}\n </div>\n )}\n {group.slots.map((slot) => {\n const value = slotToValue(slot);\n const isSelected =\n selectedSlot?.start_time === slot.start_time &&\n selectedSlot?.end_time === slot.end_time;\n return (\n <Radio.Root\n key={value}\n value={value}\n disabled={!slot.is_available}\n
|
|
13
|
+
"content": "\"use client\";\n\nimport { Radio } from \"@base-ui/react/radio\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport React from \"react\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\nimport { useAvailableSlots } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface SlotPickerClassNames {\n root?: string;\n group?: string;\n groupLabel?: string;\n slot?: string;\n slotTime?: string;\n slotPrice?: string;\n loading?: string;\n empty?: string;\n}\n\nexport interface SlotPickerProps {\n /** Pre-fetched slots (skips fetch). */\n slots?: AvailableSlot[];\n /** Service ID — used to fetch slots when `slots` prop is not provided. */\n serviceId?: string;\n /** Date string (YYYY-MM-DD) — used to fetch slots when `slots` prop is not provided. */\n date?: string;\n /** Number of participants for capacity-based availability. */\n participantCount?: number;\n /** Currently selected slot. */\n selectedSlot?: AvailableSlot | null;\n /** Called when a slot is selected. */\n onSlotSelect?: (slot: AvailableSlot) => void;\n /** Whether to group slots by time of day. Default: true. */\n groupByTimeOfDay?: boolean;\n /** Show price on each slot. Default: true. */\n showPrice?: boolean;\n /** Text shown when no slots available. */\n emptyMessage?: string;\n className?: string;\n classNames?: SlotPickerClassNames;\n}\n\ninterface SlotGroup {\n label: string;\n slots: AvailableSlot[];\n}\n\nfunction getTimeOfDay(timeStr: string): \"morning\" | \"afternoon\" | \"evening\" {\n const hour = parseInt(timeStr.split(\"T\").pop()?.split(\":\")[0] ?? timeStr.split(\":\")[0], 10);\n if (hour < 12) return \"morning\";\n if (hour < 17) return \"afternoon\";\n return \"evening\";\n}\n\nconst TIME_OF_DAY_LABELS: Record<string, string> = {\n morning: \"Morning\",\n afternoon: \"Afternoon\",\n evening: \"Evening\",\n};\n\nfunction groupSlots(slots: AvailableSlot[]): SlotGroup[] {\n const groups: Record<string, AvailableSlot[]> = {};\n for (const slot of slots) {\n const tod = getTimeOfDay(slot.start_time);\n if (!groups[tod]) groups[tod] = [];\n groups[tod].push(slot);\n }\n return ([\"morning\", \"afternoon\", \"evening\"] as const)\n .filter((tod) => groups[tod]?.length)\n .map((tod) => ({ label: TIME_OF_DAY_LABELS[tod], slots: groups[tod] }));\n}\n\nfunction formatTime(timeStr: string): string {\n try {\n const date = new Date(timeStr);\n if (!isNaN(date.getTime())) {\n return date.toLocaleTimeString(undefined, { hour: \"numeric\", minute: \"2-digit\" });\n }\n } catch {\n // noop\n }\n\n const parts = timeStr.split(\":\");\n if (parts.length >= 2) {\n const hour = parseInt(parts[0], 10);\n const minute = parts[1];\n const ampm = hour >= 12 ? \"PM\" : \"AM\";\n const displayHour = hour % 12 || 12;\n return `${displayHour}:${minute} ${ampm}`;\n }\n return timeStr;\n}\n\nfunction slotToValue(slot: AvailableSlot): string {\n return `${slot.start_time}|${slot.end_time}`;\n}\n\nexport function SlotPicker({\n slots: slotsProp,\n serviceId,\n date,\n participantCount,\n selectedSlot,\n onSlotSelect,\n groupByTimeOfDay = true,\n showPrice = true,\n emptyMessage = \"No available slots\",\n className,\n classNames,\n}: SlotPickerProps): React.ReactElement {\n const { slots: fetched, isLoading } = useAvailableSlots(\n serviceId ?? null,\n date ?? null,\n {\n participantCount,\n enabled: slotsProp === undefined && !!serviceId && !!date,\n },\n );\n\n const slots = slotsProp ?? fetched;\n\n if (isLoading && slots.length === 0) {\n return (\n <div\n data-cimplify-slot-picker\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (slots.length === 0) {\n return (\n <div\n data-cimplify-slot-picker\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage}</p>\n </div>\n );\n }\n\n const groups = groupByTimeOfDay ? groupSlots(slots) : [{ label: \"\", slots }];\n\n const slotsByValue = new Map<string, AvailableSlot>();\n for (const slot of slots) {\n slotsByValue.set(slotToValue(slot), slot);\n }\n\n const selectedValue = selectedSlot ? slotToValue(selectedSlot) : \"\";\n\n return (\n <RadioGroup\n data-cimplify-slot-picker\n className={cn(className, classNames?.root)}\n value={selectedValue}\n onValueChange={(value: string) => {\n const slot = slotsByValue.get(value);\n if (slot?.is_available) {\n onSlotSelect?.(slot);\n }\n }}\n >\n {groups.map((group) => (\n <div key={group.label || \"all\"} data-cimplify-slot-group className={classNames?.group}>\n {group.label && (\n <div data-cimplify-slot-group-label className={classNames?.groupLabel}>\n {group.label}\n </div>\n )}\n {group.slots.map((slot) => {\n const value = slotToValue(slot);\n const isSelected =\n selectedSlot?.start_time === slot.start_time &&\n selectedSlot?.end_time === slot.end_time;\n return (\n <Radio.Root\n key={value}\n value={value}\n disabled={!slot.is_available}\n data-cimplify-slot\n data-selected={isSelected || undefined}\n data-unavailable={!slot.is_available || undefined}\n className={classNames?.slot}\n >\n <span data-cimplify-slot-time className={classNames?.slotTime}>\n {formatTime(slot.start_time)}\n </span>\n {showPrice && slot.price && (\n <span data-cimplify-slot-price className={classNames?.slotPrice}>\n <Price amount={slot.price} />\n </span>\n )}\n </Radio.Root>\n );\n })}\n </div>\n ))}\n </RadioGroup>\n );\n}\n"
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "staff-picker.tsx",
|
|
12
|
-
"content": "\"use client\";\n\nimport { Radio } from \"@base-ui/react/radio\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport React from \"react\";\nimport type { Staff } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst ANY_VALUE = \"__any__\";\n\nexport interface StaffPickerClassNames {\n root?: string;\n option?: string;\n avatar?: string;\n name?: string;\n bio?: string;\n}\n\nexport interface StaffPickerProps {\n /** List of available staff members. */\n staff: Staff[];\n /** Currently selected staff ID, or null for \"Any available\". */\n selectedStaffId?: string | null;\n /** Called when a staff member is selected. Passes null for \"Any available\". */\n onStaffSelect?: (staffId: string | null) => void;\n /** Show \"Any available\" option. Default: true. */\n showAnyOption?: boolean;\n /** Label for the \"Any available\" option. */\n anyLabel?: string;\n className?: string;\n classNames?: StaffPickerClassNames;\n}\n\nexport function StaffPicker({\n staff,\n selectedStaffId,\n onStaffSelect,\n showAnyOption = true,\n anyLabel = \"Any available\",\n className,\n classNames,\n}: StaffPickerProps): React.ReactElement {\n const groupValue =\n selectedStaffId === null ? ANY_VALUE : (selectedStaffId ?? \"\");\n\n return (\n <RadioGroup\n data-cimplify-staff-picker\n className={cn(className, classNames?.root)}\n value={groupValue}\n onValueChange={(value) => {\n onStaffSelect?.(value === ANY_VALUE ? null : value);\n }}\n >\n {showAnyOption && (\n <Radio.Root\n value={ANY_VALUE}\n
|
|
12
|
+
"content": "\"use client\";\n\nimport { Radio } from \"@base-ui/react/radio\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport React from \"react\";\nimport type { Staff } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst ANY_VALUE = \"__any__\";\n\nexport interface StaffPickerClassNames {\n root?: string;\n option?: string;\n avatar?: string;\n name?: string;\n bio?: string;\n}\n\nexport interface StaffPickerProps {\n /** List of available staff members. */\n staff: Staff[];\n /** Currently selected staff ID, or null for \"Any available\". */\n selectedStaffId?: string | null;\n /** Called when a staff member is selected. Passes null for \"Any available\". */\n onStaffSelect?: (staffId: string | null) => void;\n /** Show \"Any available\" option. Default: true. */\n showAnyOption?: boolean;\n /** Label for the \"Any available\" option. */\n anyLabel?: string;\n className?: string;\n classNames?: StaffPickerClassNames;\n}\n\nexport function StaffPicker({\n staff,\n selectedStaffId,\n onStaffSelect,\n showAnyOption = true,\n anyLabel = \"Any available\",\n className,\n classNames,\n}: StaffPickerProps): React.ReactElement {\n const groupValue =\n selectedStaffId === null ? ANY_VALUE : (selectedStaffId ?? \"\");\n\n return (\n <RadioGroup\n data-cimplify-staff-picker\n className={cn(className, classNames?.root)}\n value={groupValue}\n onValueChange={(value) => {\n onStaffSelect?.(value === ANY_VALUE ? null : value);\n }}\n >\n {showAnyOption && (\n <Radio.Root\n value={ANY_VALUE}\n data-cimplify-staff-option\n data-selected={selectedStaffId === null || undefined}\n data-any\n className={classNames?.option}\n >\n <span data-cimplify-staff-name className={classNames?.name}>\n {anyLabel}\n </span>\n </Radio.Root>\n )}\n {staff.map((member) => (\n <Radio.Root\n key={member.id}\n value={member.id}\n data-cimplify-staff-option\n data-selected={selectedStaffId === member.id || undefined}\n className={classNames?.option}\n >\n {member.avatar_url && (\n <img\n src={member.avatar_url}\n alt={member.name}\n data-cimplify-staff-avatar\n className={classNames?.avatar}\n />\n )}\n <span data-cimplify-staff-name className={classNames?.name}>\n {member.name}\n </span>\n {member.bio && (\n <span data-cimplify-staff-bio className={classNames?.bio}>\n {member.bio}\n </span>\n )}\n </Radio.Root>\n ))}\n </RadioGroup>\n );\n}\n"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "variant-selector.tsx",
|
|
12
|
-
"content": "\"use client\";\n\nimport React, { useState, useEffect, useRef, useId } from \"react\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport { Radio } from \"@base-ui/react/radio\";\nimport type { ProductVariant, VariantAxisWithValues } from \"@cimplify/sdk\";\nimport type { Money } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { getVariantDisplayName } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface VariantSelectorClassNames {\n root?: string;\n axisLabel?: string;\n axisOptions?: string;\n option?: string;\n optionSelected?: string;\n listLabel?: string;\n list?: string;\n name?: string;\n pricing?: string;\n adjustment?: string;\n}\n\nexport interface VariantSelectorProps {\n variants: ProductVariant[];\n variantAxes?: VariantAxisWithValues[];\n basePrice?: Money;\n selectedVariantId?: string;\n onVariantChange: (variantId: string | undefined, variant: ProductVariant | undefined) => void;\n productName?: string;\n className?: string;\n classNames?: VariantSelectorClassNames;\n}\n\nexport function VariantSelector({\n variants,\n variantAxes,\n basePrice,\n selectedVariantId,\n onVariantChange,\n productName,\n className,\n classNames,\n}: VariantSelectorProps): React.ReactElement | null {\n const [axisSelections, setAxisSelections] = useState<Record<string, string>>({});\n const initialized = useRef(false);\n const idPrefix = useId();\n\n useEffect(() => {\n initialized.current = false;\n }, [variants]);\n\n useEffect(() => {\n if (initialized.current) return;\n if (!variants || variants.length === 0) return;\n\n const defaultVariant = variants.find((v) => v.is_default) || variants[0];\n if (!defaultVariant) return;\n\n initialized.current = true;\n onVariantChange(defaultVariant.id, defaultVariant);\n\n if (defaultVariant.display_attributes) {\n const initial: Record<string, string> = {};\n for (const attr of defaultVariant.display_attributes) {\n initial[attr.axis_id] = attr.value_id;\n }\n setAxisSelections(initial);\n }\n }, [variants, onVariantChange]);\n\n useEffect(() => {\n if (!initialized.current) return;\n if (!variantAxes || variantAxes.length === 0) return;\n\n const match = variants.find((v) => {\n if (!v.display_attributes) return false;\n return v.display_attributes.every(\n (attr) => axisSelections[attr.axis_id] === attr.value_id,\n );\n });\n\n if (match && match.id !== selectedVariantId) {\n onVariantChange(match.id, match);\n }\n }, [axisSelections, variants, variantAxes, selectedVariantId, onVariantChange]);\n\n if (!variants || variants.length <= 1) {\n return null;\n }\n\n const basePriceNum = basePrice != null ? parsePrice(basePrice) : 0;\n\n if (variantAxes && variantAxes.length > 0) {\n return (\n <div data-cimplify-variant-selector className={cn(\"space-y-5\", className, classNames?.root)}>\n {variantAxes.map((axis) => {\n const labelId = `${idPrefix}-axis-${axis.id}`;\n return (\n <div key={axis.id} data-cimplify-variant-axis>\n <label\n id={labelId}\n data-cimplify-variant-axis-label\n className={cn(\"block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3\", classNames?.axisLabel)}\n >\n {axis.name}\n </label>\n <RadioGroup\n aria-labelledby={labelId}\n value={axisSelections[axis.id] ?? \"\"}\n onValueChange={(value) => {\n setAxisSelections((prev) => ({\n ...prev,\n [axis.id]: value,\n }));\n }}\n data-cimplify-variant-axis-options\n className={cn(\"flex flex-wrap gap-2\", classNames?.axisOptions)}\n >\n {axis.values.map((value) => {\n const isSelected = axisSelections[axis.id] === value.id;\n return (\n <Radio.Root\n key={value.id}\n value={value.id}\n
|
|
12
|
+
"content": "\"use client\";\n\nimport React, { useState, useEffect, useRef, useId } from \"react\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport { Radio } from \"@base-ui/react/radio\";\nimport type { ProductVariant, VariantAxisWithValues } from \"@cimplify/sdk\";\nimport type { Money } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { getVariantDisplayName } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface VariantSelectorClassNames {\n root?: string;\n axisLabel?: string;\n axisOptions?: string;\n option?: string;\n optionSelected?: string;\n listLabel?: string;\n list?: string;\n name?: string;\n pricing?: string;\n adjustment?: string;\n}\n\nexport interface VariantSelectorProps {\n variants: ProductVariant[];\n variantAxes?: VariantAxisWithValues[];\n basePrice?: Money;\n selectedVariantId?: string;\n onVariantChange: (variantId: string | undefined, variant: ProductVariant | undefined) => void;\n productName?: string;\n className?: string;\n classNames?: VariantSelectorClassNames;\n}\n\nexport function VariantSelector({\n variants,\n variantAxes,\n basePrice,\n selectedVariantId,\n onVariantChange,\n productName,\n className,\n classNames,\n}: VariantSelectorProps): React.ReactElement | null {\n const [axisSelections, setAxisSelections] = useState<Record<string, string>>({});\n const initialized = useRef(false);\n const idPrefix = useId();\n\n useEffect(() => {\n initialized.current = false;\n }, [variants]);\n\n useEffect(() => {\n if (initialized.current) return;\n if (!variants || variants.length === 0) return;\n\n const defaultVariant = variants.find((v) => v.is_default) || variants[0];\n if (!defaultVariant) return;\n\n initialized.current = true;\n onVariantChange(defaultVariant.id, defaultVariant);\n\n if (defaultVariant.display_attributes) {\n const initial: Record<string, string> = {};\n for (const attr of defaultVariant.display_attributes) {\n initial[attr.axis_id] = attr.value_id;\n }\n setAxisSelections(initial);\n }\n }, [variants, onVariantChange]);\n\n useEffect(() => {\n if (!initialized.current) return;\n if (!variantAxes || variantAxes.length === 0) return;\n\n const match = variants.find((v) => {\n if (!v.display_attributes) return false;\n return v.display_attributes.every(\n (attr) => axisSelections[attr.axis_id] === attr.value_id,\n );\n });\n\n if (match && match.id !== selectedVariantId) {\n onVariantChange(match.id, match);\n }\n }, [axisSelections, variants, variantAxes, selectedVariantId, onVariantChange]);\n\n if (!variants || variants.length <= 1) {\n return null;\n }\n\n const basePriceNum = basePrice != null ? parsePrice(basePrice) : 0;\n\n if (variantAxes && variantAxes.length > 0) {\n return (\n <div data-cimplify-variant-selector className={cn(\"space-y-5\", className, classNames?.root)}>\n {variantAxes.map((axis) => {\n const labelId = `${idPrefix}-axis-${axis.id}`;\n return (\n <div key={axis.id} data-cimplify-variant-axis>\n <label\n id={labelId}\n data-cimplify-variant-axis-label\n className={cn(\"block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3\", classNames?.axisLabel)}\n >\n {axis.name}\n </label>\n <RadioGroup\n aria-labelledby={labelId}\n value={axisSelections[axis.id] ?? \"\"}\n onValueChange={(value) => {\n setAxisSelections((prev) => ({\n ...prev,\n [axis.id]: value,\n }));\n }}\n data-cimplify-variant-axis-options\n className={cn(\"flex flex-wrap gap-2\", classNames?.axisOptions)}\n >\n {axis.values.map((value) => {\n const isSelected = axisSelections[axis.id] === value.id;\n return (\n <Radio.Root\n key={value.id}\n value={value.id}\n data-cimplify-variant-option\n data-selected={isSelected || undefined}\n className={cn(\n \"px-4 py-2 border text-sm font-medium transition-colors border-border hover:border-primary/50\",\n isSelected && \"bg-primary text-primary-foreground border-primary\",\n isSelected ? classNames?.optionSelected : classNames?.option,\n )}\n >\n {value.name}\n </Radio.Root>\n );\n })}\n </RadioGroup>\n </div>\n );\n })}\n </div>\n );\n }\n\n const listLabelId = `${idPrefix}-variant-list`;\n\n return (\n <div data-cimplify-variant-selector className={cn(\"space-y-5\", className, classNames?.root)}>\n <label\n id={listLabelId}\n data-cimplify-variant-list-label\n className={cn(\"block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3\", classNames?.listLabel)}\n >\n Options\n </label>\n <RadioGroup\n aria-labelledby={listLabelId}\n value={selectedVariantId ?? \"\"}\n onValueChange={(value) => {\n const variant = variants.find((v) => v.id === value);\n onVariantChange(variant?.id, variant);\n }}\n data-cimplify-variant-list\n className={cn(\"space-y-2\", classNames?.list)}\n >\n {variants.map((variant) => {\n const isSelected = selectedVariantId === variant.id;\n const adjustment = parsePrice(variant.price_adjustment);\n const effectivePrice = basePriceNum + adjustment;\n\n return (\n <Radio.Root\n key={variant.id}\n value={variant.id}\n data-cimplify-variant-option\n data-selected={isSelected || undefined}\n className={cn(\n \"w-full flex items-center justify-between px-4 py-3 border transition-colors border-border hover:border-primary/50\",\n isSelected && \"bg-primary/5 border-primary\",\n isSelected ? classNames?.optionSelected : classNames?.option,\n )}\n >\n <span\n data-cimplify-variant-name\n className={cn(\"font-medium\", isSelected && \"text-primary\", classNames?.name)}\n >\n {getVariantDisplayName(variant, productName)}\n </span>\n <span data-cimplify-variant-pricing className={cn(\"text-sm\", classNames?.pricing)}>\n {adjustment !== 0 && (\n <span\n data-cimplify-variant-adjustment\n className={cn(\n adjustment > 0 ? \"text-muted-foreground\" : \"text-green-600\",\n classNames?.adjustment,\n )}\n >\n {adjustment > 0 ? \"+\" : \"\"}\n <Price amount={variant.price_adjustment} />\n </span>\n )}\n <Price amount={effectivePrice} className=\"text-muted-foreground\" />\n </span>\n </Radio.Root>\n );\n })}\n </RadioGroup>\n </div>\n );\n}\n"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|