@cimplify/sdk 0.14.3 → 0.15.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.
- package/dist/react.d.mts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +85 -96
- package/dist/react.mjs +85 -96
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/registry/product-card.json +1 -1
- package/registry/product-customizer.json +1 -1
package/dist/react.d.mts
CHANGED
|
@@ -901,7 +901,7 @@ interface ProductCardProps {
|
|
|
901
901
|
/**
|
|
902
902
|
* ProductCard — a product display card with two modes:
|
|
903
903
|
*
|
|
904
|
-
* - **card** (default): clickable button that opens a
|
|
904
|
+
* - **card** (default): clickable button that opens a Base UI Dialog modal
|
|
905
905
|
* - **page**: a plain `<a>` link for SEO-friendly product pages
|
|
906
906
|
*/
|
|
907
907
|
declare function ProductCard({ product, displayMode, href, renderModal, renderImage, renderLink, children, aspectRatio, className, classNames, }: ProductCardProps): React.ReactElement;
|
package/dist/react.d.ts
CHANGED
|
@@ -901,7 +901,7 @@ interface ProductCardProps {
|
|
|
901
901
|
/**
|
|
902
902
|
* ProductCard — a product display card with two modes:
|
|
903
903
|
*
|
|
904
|
-
* - **card** (default): clickable button that opens a
|
|
904
|
+
* - **card** (default): clickable button that opens a Base UI Dialog modal
|
|
905
905
|
* - **page**: a plain `<a>` link for SEO-friendly product pages
|
|
906
906
|
*/
|
|
907
907
|
declare function ProductCard({ product, displayMode, href, renderModal, renderImage, renderLink, children, aspectRatio, className, classNames, }: ProductCardProps): React.ReactElement;
|
package/dist/react.js
CHANGED
|
@@ -9,6 +9,7 @@ var tailwindMerge = require('tailwind-merge');
|
|
|
9
9
|
var radioGroup = require('@base-ui/react/radio-group');
|
|
10
10
|
var radio = require('@base-ui/react/radio');
|
|
11
11
|
var checkbox = require('@base-ui/react/checkbox');
|
|
12
|
+
var dialog = require('@base-ui/react/dialog');
|
|
12
13
|
var field = require('@base-ui/react/field');
|
|
13
14
|
var input = require('@base-ui/react/input');
|
|
14
15
|
var tabs = require('@base-ui/react/tabs');
|
|
@@ -9119,46 +9120,48 @@ function ProductCustomizer({
|
|
|
9119
9120
|
"div",
|
|
9120
9121
|
{
|
|
9121
9122
|
"data-cimplify-customizer-actions": true,
|
|
9122
|
-
className: cn("
|
|
9123
|
+
className: cn("pt-4 border-t border-border", classNames?.actions),
|
|
9123
9124
|
children: [
|
|
9124
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9125
|
-
|
|
9125
|
+
!quoteEnabled && /* @__PURE__ */ jsxRuntime.jsx(
|
|
9126
|
+
"p",
|
|
9126
9127
|
{
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9128
|
+
id: "cimplify-customizer-validation",
|
|
9129
|
+
"data-cimplify-customizer-validation": true,
|
|
9130
|
+
className: cn("text-sm text-destructive mb-3", classNames?.validation),
|
|
9131
|
+
children: "Please select all required options"
|
|
9130
9132
|
}
|
|
9131
9133
|
),
|
|
9132
|
-
/* @__PURE__ */ jsxRuntime.
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9134
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4", children: [
|
|
9135
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9136
|
+
QuantitySelector,
|
|
9137
|
+
{
|
|
9138
|
+
value: quantity,
|
|
9139
|
+
onChange: setQuantity,
|
|
9140
|
+
min: 1
|
|
9141
|
+
}
|
|
9142
|
+
),
|
|
9143
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9144
|
+
"button",
|
|
9145
|
+
{
|
|
9146
|
+
type: "button",
|
|
9147
|
+
onClick: handleAddToCart,
|
|
9148
|
+
disabled: isAdded || isSubmitting || !quoteEnabled,
|
|
9149
|
+
"aria-describedby": !quoteEnabled ? "cimplify-customizer-validation" : void 0,
|
|
9150
|
+
"data-cimplify-customizer-submit": true,
|
|
9151
|
+
className: cn(
|
|
9152
|
+
"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",
|
|
9153
|
+
isAdded && classNames?.submitButtonAdded,
|
|
9154
|
+
classNames?.submitButton
|
|
9155
|
+
),
|
|
9156
|
+
children: isAdded ? "Added to Cart" : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
9157
|
+
"Add to Cart \xB7 ",
|
|
9158
|
+
/* @__PURE__ */ jsxRuntime.jsx(Price, { amount: displayTotalPrice })
|
|
9159
|
+
] })
|
|
9160
|
+
}
|
|
9161
|
+
)
|
|
9162
|
+
] })
|
|
9151
9163
|
]
|
|
9152
9164
|
}
|
|
9153
|
-
),
|
|
9154
|
-
!quoteEnabled && /* @__PURE__ */ jsxRuntime.jsx(
|
|
9155
|
-
"p",
|
|
9156
|
-
{
|
|
9157
|
-
id: "cimplify-customizer-validation",
|
|
9158
|
-
"data-cimplify-customizer-validation": true,
|
|
9159
|
-
className: cn("text-sm text-destructive mt-2", classNames?.validation),
|
|
9160
|
-
children: "Please select all required options"
|
|
9161
|
-
}
|
|
9162
9165
|
)
|
|
9163
9166
|
] });
|
|
9164
9167
|
}
|
|
@@ -9606,15 +9609,6 @@ function ProductCard({
|
|
|
9606
9609
|
const mode = displayMode ?? product.display_mode ?? "card";
|
|
9607
9610
|
const [isOpen, setIsOpen] = React3.useState(false);
|
|
9608
9611
|
const [shouldFetch, setShouldFetch] = React3.useState(false);
|
|
9609
|
-
const dialogRef = React3.useRef(null);
|
|
9610
|
-
React3.useEffect(() => {
|
|
9611
|
-
if (!isOpen) return;
|
|
9612
|
-
const original = document.body.style.overflow;
|
|
9613
|
-
document.body.style.overflow = "hidden";
|
|
9614
|
-
return () => {
|
|
9615
|
-
document.body.style.overflow = original;
|
|
9616
|
-
};
|
|
9617
|
-
}, [isOpen]);
|
|
9618
9612
|
const { product: productDetails } = useProduct(
|
|
9619
9613
|
product.slug ?? product.id,
|
|
9620
9614
|
{ enabled: shouldFetch || isOpen }
|
|
@@ -9622,26 +9616,15 @@ function ProductCard({
|
|
|
9622
9616
|
const handlePrefetch = React3.useCallback(() => {
|
|
9623
9617
|
setShouldFetch(true);
|
|
9624
9618
|
}, []);
|
|
9625
|
-
const
|
|
9626
|
-
setIsOpen(
|
|
9627
|
-
|
|
9628
|
-
|
|
9619
|
+
const handleOpenChange = React3.useCallback((open) => {
|
|
9620
|
+
setIsOpen(open);
|
|
9621
|
+
if (open) {
|
|
9622
|
+
setShouldFetch(true);
|
|
9623
|
+
}
|
|
9629
9624
|
}, []);
|
|
9630
9625
|
const handleClose = React3.useCallback(() => {
|
|
9631
|
-
dialogRef.current?.close();
|
|
9632
|
-
setIsOpen(false);
|
|
9633
|
-
}, []);
|
|
9634
|
-
const handleCancel = React3.useCallback(() => {
|
|
9635
9626
|
setIsOpen(false);
|
|
9636
9627
|
}, []);
|
|
9637
|
-
const handleBackdropClick = React3.useCallback(
|
|
9638
|
-
(e) => {
|
|
9639
|
-
if (e.target === dialogRef.current) {
|
|
9640
|
-
handleClose();
|
|
9641
|
-
}
|
|
9642
|
-
},
|
|
9643
|
-
[handleClose]
|
|
9644
|
-
);
|
|
9645
9628
|
const imageUrl = product.image_url || product.images?.[0];
|
|
9646
9629
|
const cardBody = children ?? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
9647
9630
|
imageUrl && /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -9725,14 +9708,11 @@ function ProductCard({
|
|
|
9725
9708
|
}
|
|
9726
9709
|
);
|
|
9727
9710
|
}
|
|
9728
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
9711
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(dialog.Dialog.Root, { open: isOpen, onOpenChange: handleOpenChange, children: [
|
|
9729
9712
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9730
|
-
|
|
9713
|
+
dialog.Dialog.Trigger,
|
|
9731
9714
|
{
|
|
9732
|
-
type: "button",
|
|
9733
|
-
"aria-haspopup": "dialog",
|
|
9734
9715
|
onPointerEnter: handlePrefetch,
|
|
9735
|
-
onClick: handleOpen,
|
|
9736
9716
|
"data-cimplify-product-card": true,
|
|
9737
9717
|
"data-display-mode": "card",
|
|
9738
9718
|
className: cn(
|
|
@@ -9743,38 +9723,47 @@ function ProductCard({
|
|
|
9743
9723
|
children: cardBody
|
|
9744
9724
|
}
|
|
9745
9725
|
),
|
|
9746
|
-
/* @__PURE__ */ jsxRuntime.
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
|
|
9773
|
-
|
|
9774
|
-
|
|
9775
|
-
|
|
9776
|
-
|
|
9777
|
-
|
|
9726
|
+
/* @__PURE__ */ jsxRuntime.jsxs(dialog.Dialog.Portal, { children: [
|
|
9727
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9728
|
+
dialog.Dialog.Backdrop,
|
|
9729
|
+
{
|
|
9730
|
+
"data-cimplify-product-card-backdrop": true,
|
|
9731
|
+
className: cn(
|
|
9732
|
+
"fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity",
|
|
9733
|
+
classNames?.modalOverlay
|
|
9734
|
+
)
|
|
9735
|
+
}
|
|
9736
|
+
),
|
|
9737
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9738
|
+
dialog.Dialog.Popup,
|
|
9739
|
+
{
|
|
9740
|
+
"data-cimplify-product-card-modal": true,
|
|
9741
|
+
className: cn(
|
|
9742
|
+
"fixed rounded-2xl p-0 max-w-lg w-full max-h-[85vh] overflow-auto bg-background shadow-2xl outline-none",
|
|
9743
|
+
classNames?.modal
|
|
9744
|
+
),
|
|
9745
|
+
children: isOpen && (productDetails ? renderModal ? renderModal(productDetails, handleClose) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
9746
|
+
ProductSheet,
|
|
9747
|
+
{
|
|
9748
|
+
product: productDetails,
|
|
9749
|
+
onClose: handleClose,
|
|
9750
|
+
renderImage
|
|
9751
|
+
}
|
|
9752
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
9753
|
+
"div",
|
|
9754
|
+
{
|
|
9755
|
+
"data-cimplify-product-card-modal-loading": true,
|
|
9756
|
+
"aria-busy": "true",
|
|
9757
|
+
className: "flex flex-col gap-4 p-6",
|
|
9758
|
+
children: [
|
|
9759
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "aspect-[4/3] bg-muted rounded-lg animate-pulse" }),
|
|
9760
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-6 w-3/5 bg-muted rounded animate-pulse" })
|
|
9761
|
+
]
|
|
9762
|
+
}
|
|
9763
|
+
))
|
|
9764
|
+
}
|
|
9765
|
+
)
|
|
9766
|
+
] })
|
|
9778
9767
|
] });
|
|
9779
9768
|
}
|
|
9780
9769
|
function ProductGrid({
|
package/dist/react.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { twMerge } from 'tailwind-merge';
|
|
|
7
7
|
import { RadioGroup } from '@base-ui/react/radio-group';
|
|
8
8
|
import { Radio } from '@base-ui/react/radio';
|
|
9
9
|
import { Checkbox } from '@base-ui/react/checkbox';
|
|
10
|
+
import { Dialog } from '@base-ui/react/dialog';
|
|
10
11
|
import { Field } from '@base-ui/react/field';
|
|
11
12
|
import { Input } from '@base-ui/react/input';
|
|
12
13
|
import { Tabs } from '@base-ui/react/tabs';
|
|
@@ -9113,46 +9114,48 @@ function ProductCustomizer({
|
|
|
9113
9114
|
"div",
|
|
9114
9115
|
{
|
|
9115
9116
|
"data-cimplify-customizer-actions": true,
|
|
9116
|
-
className: cn("
|
|
9117
|
+
className: cn("pt-4 border-t border-border", classNames?.actions),
|
|
9117
9118
|
children: [
|
|
9118
|
-
/* @__PURE__ */ jsx(
|
|
9119
|
-
|
|
9119
|
+
!quoteEnabled && /* @__PURE__ */ jsx(
|
|
9120
|
+
"p",
|
|
9120
9121
|
{
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9122
|
+
id: "cimplify-customizer-validation",
|
|
9123
|
+
"data-cimplify-customizer-validation": true,
|
|
9124
|
+
className: cn("text-sm text-destructive mb-3", classNames?.validation),
|
|
9125
|
+
children: "Please select all required options"
|
|
9124
9126
|
}
|
|
9125
9127
|
),
|
|
9126
|
-
/* @__PURE__ */
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9128
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
|
|
9129
|
+
/* @__PURE__ */ jsx(
|
|
9130
|
+
QuantitySelector,
|
|
9131
|
+
{
|
|
9132
|
+
value: quantity,
|
|
9133
|
+
onChange: setQuantity,
|
|
9134
|
+
min: 1
|
|
9135
|
+
}
|
|
9136
|
+
),
|
|
9137
|
+
/* @__PURE__ */ jsx(
|
|
9138
|
+
"button",
|
|
9139
|
+
{
|
|
9140
|
+
type: "button",
|
|
9141
|
+
onClick: handleAddToCart,
|
|
9142
|
+
disabled: isAdded || isSubmitting || !quoteEnabled,
|
|
9143
|
+
"aria-describedby": !quoteEnabled ? "cimplify-customizer-validation" : void 0,
|
|
9144
|
+
"data-cimplify-customizer-submit": true,
|
|
9145
|
+
className: cn(
|
|
9146
|
+
"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",
|
|
9147
|
+
isAdded && classNames?.submitButtonAdded,
|
|
9148
|
+
classNames?.submitButton
|
|
9149
|
+
),
|
|
9150
|
+
children: isAdded ? "Added to Cart" : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
9151
|
+
"Add to Cart \xB7 ",
|
|
9152
|
+
/* @__PURE__ */ jsx(Price, { amount: displayTotalPrice })
|
|
9153
|
+
] })
|
|
9154
|
+
}
|
|
9155
|
+
)
|
|
9156
|
+
] })
|
|
9145
9157
|
]
|
|
9146
9158
|
}
|
|
9147
|
-
),
|
|
9148
|
-
!quoteEnabled && /* @__PURE__ */ jsx(
|
|
9149
|
-
"p",
|
|
9150
|
-
{
|
|
9151
|
-
id: "cimplify-customizer-validation",
|
|
9152
|
-
"data-cimplify-customizer-validation": true,
|
|
9153
|
-
className: cn("text-sm text-destructive mt-2", classNames?.validation),
|
|
9154
|
-
children: "Please select all required options"
|
|
9155
|
-
}
|
|
9156
9159
|
)
|
|
9157
9160
|
] });
|
|
9158
9161
|
}
|
|
@@ -9600,15 +9603,6 @@ function ProductCard({
|
|
|
9600
9603
|
const mode = displayMode ?? product.display_mode ?? "card";
|
|
9601
9604
|
const [isOpen, setIsOpen] = useState(false);
|
|
9602
9605
|
const [shouldFetch, setShouldFetch] = useState(false);
|
|
9603
|
-
const dialogRef = useRef(null);
|
|
9604
|
-
useEffect(() => {
|
|
9605
|
-
if (!isOpen) return;
|
|
9606
|
-
const original = document.body.style.overflow;
|
|
9607
|
-
document.body.style.overflow = "hidden";
|
|
9608
|
-
return () => {
|
|
9609
|
-
document.body.style.overflow = original;
|
|
9610
|
-
};
|
|
9611
|
-
}, [isOpen]);
|
|
9612
9606
|
const { product: productDetails } = useProduct(
|
|
9613
9607
|
product.slug ?? product.id,
|
|
9614
9608
|
{ enabled: shouldFetch || isOpen }
|
|
@@ -9616,26 +9610,15 @@ function ProductCard({
|
|
|
9616
9610
|
const handlePrefetch = useCallback(() => {
|
|
9617
9611
|
setShouldFetch(true);
|
|
9618
9612
|
}, []);
|
|
9619
|
-
const
|
|
9620
|
-
setIsOpen(
|
|
9621
|
-
|
|
9622
|
-
|
|
9613
|
+
const handleOpenChange = useCallback((open) => {
|
|
9614
|
+
setIsOpen(open);
|
|
9615
|
+
if (open) {
|
|
9616
|
+
setShouldFetch(true);
|
|
9617
|
+
}
|
|
9623
9618
|
}, []);
|
|
9624
9619
|
const handleClose = useCallback(() => {
|
|
9625
|
-
dialogRef.current?.close();
|
|
9626
|
-
setIsOpen(false);
|
|
9627
|
-
}, []);
|
|
9628
|
-
const handleCancel = useCallback(() => {
|
|
9629
9620
|
setIsOpen(false);
|
|
9630
9621
|
}, []);
|
|
9631
|
-
const handleBackdropClick = useCallback(
|
|
9632
|
-
(e) => {
|
|
9633
|
-
if (e.target === dialogRef.current) {
|
|
9634
|
-
handleClose();
|
|
9635
|
-
}
|
|
9636
|
-
},
|
|
9637
|
-
[handleClose]
|
|
9638
|
-
);
|
|
9639
9622
|
const imageUrl = product.image_url || product.images?.[0];
|
|
9640
9623
|
const cardBody = children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
9641
9624
|
imageUrl && /* @__PURE__ */ jsx(
|
|
@@ -9719,14 +9702,11 @@ function ProductCard({
|
|
|
9719
9702
|
}
|
|
9720
9703
|
);
|
|
9721
9704
|
}
|
|
9722
|
-
return /* @__PURE__ */ jsxs(
|
|
9705
|
+
return /* @__PURE__ */ jsxs(Dialog.Root, { open: isOpen, onOpenChange: handleOpenChange, children: [
|
|
9723
9706
|
/* @__PURE__ */ jsx(
|
|
9724
|
-
|
|
9707
|
+
Dialog.Trigger,
|
|
9725
9708
|
{
|
|
9726
|
-
type: "button",
|
|
9727
|
-
"aria-haspopup": "dialog",
|
|
9728
9709
|
onPointerEnter: handlePrefetch,
|
|
9729
|
-
onClick: handleOpen,
|
|
9730
9710
|
"data-cimplify-product-card": true,
|
|
9731
9711
|
"data-display-mode": "card",
|
|
9732
9712
|
className: cn(
|
|
@@ -9737,38 +9717,47 @@ function ProductCard({
|
|
|
9737
9717
|
children: cardBody
|
|
9738
9718
|
}
|
|
9739
9719
|
),
|
|
9740
|
-
/* @__PURE__ */
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
9720
|
+
/* @__PURE__ */ jsxs(Dialog.Portal, { children: [
|
|
9721
|
+
/* @__PURE__ */ jsx(
|
|
9722
|
+
Dialog.Backdrop,
|
|
9723
|
+
{
|
|
9724
|
+
"data-cimplify-product-card-backdrop": true,
|
|
9725
|
+
className: cn(
|
|
9726
|
+
"fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity",
|
|
9727
|
+
classNames?.modalOverlay
|
|
9728
|
+
)
|
|
9729
|
+
}
|
|
9730
|
+
),
|
|
9731
|
+
/* @__PURE__ */ jsx(
|
|
9732
|
+
Dialog.Popup,
|
|
9733
|
+
{
|
|
9734
|
+
"data-cimplify-product-card-modal": true,
|
|
9735
|
+
className: cn(
|
|
9736
|
+
"fixed rounded-2xl p-0 max-w-lg w-full max-h-[85vh] overflow-auto bg-background shadow-2xl outline-none",
|
|
9737
|
+
classNames?.modal
|
|
9738
|
+
),
|
|
9739
|
+
children: isOpen && (productDetails ? renderModal ? renderModal(productDetails, handleClose) : /* @__PURE__ */ jsx(
|
|
9740
|
+
ProductSheet,
|
|
9741
|
+
{
|
|
9742
|
+
product: productDetails,
|
|
9743
|
+
onClose: handleClose,
|
|
9744
|
+
renderImage
|
|
9745
|
+
}
|
|
9746
|
+
) : /* @__PURE__ */ jsxs(
|
|
9747
|
+
"div",
|
|
9748
|
+
{
|
|
9749
|
+
"data-cimplify-product-card-modal-loading": true,
|
|
9750
|
+
"aria-busy": "true",
|
|
9751
|
+
className: "flex flex-col gap-4 p-6",
|
|
9752
|
+
children: [
|
|
9753
|
+
/* @__PURE__ */ jsx("div", { className: "aspect-[4/3] bg-muted rounded-lg animate-pulse" }),
|
|
9754
|
+
/* @__PURE__ */ jsx("div", { className: "h-6 w-3/5 bg-muted rounded animate-pulse" })
|
|
9755
|
+
]
|
|
9756
|
+
}
|
|
9757
|
+
))
|
|
9758
|
+
}
|
|
9759
|
+
)
|
|
9760
|
+
] })
|
|
9772
9761
|
] });
|
|
9773
9762
|
}
|
|
9774
9763
|
function ProductGrid({
|
package/dist/styles.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */
|
|
2
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.container{width:100%}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-\[4\/3\]{aspect-ratio:4/3}.max-h-\[85vh\]{max-height:85vh}.w-3\/5{width:60%}.w-full{width:100%}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.\[appearance\:textfield\]{appearance:textfield}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-border>:not(:last-child)){border-color:var(--color-border,oklch(90% 0 0))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.rounded{border-radius:var(--radius,.5rem)}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-none{--tw-border-style:none;border-style:none}.border-border{border-color:var(--color-border,oklch(90% 0 0))}.border-muted-foreground\/30{border-color:#6363634d}@supports (color:color-mix(in lab, red, red)){.border-muted-foreground\/30{border-color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 30%, transparent)}}.border-primary{border-color:var(--color-primary,oklch(50% .1 35))}.bg-background{background-color:var(--color-background,oklch(99% 0 0))}.bg-destructive\/10{background-color:#bb061e1a}@supports (color:color-mix(in lab, red, red)){.bg-destructive\/10{background-color:color-mix(in oklab, var(--color-destructive,oklch(50% .2 25)) 10%, transparent)}}.bg-muted{background-color:var(--color-muted,oklch(95% 0 0))}.bg-primary{background-color:var(--color-primary,oklch(50% .1 35))}.bg-primary\/10{background-color:#934c3a1a}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 10%, transparent)}}.bg-transparent{background-color:#0000}.text-center{text-align:center}.text-left{text-align:left}.font-\[inherit\]{font-family:inherit}.text-\[10px\]{font-size:10px}.text-\[inherit\]{color:inherit}.text-destructive{color:var(--color-destructive,oklch(50% .2 25))}.text-muted-foreground{color:var(--color-muted-foreground,oklch(50% 0 0))}.text-muted-foreground\/60{color:#63636399}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/60{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 60%, transparent)}}.text-primary{color:var(--color-primary,oklch(50% .1 35))}.text-primary-foreground{color:var(--color-primary-foreground,oklch(99% 0 0))}.uppercase{text-transform:uppercase}.no-underline{text-decoration-line:none}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.outline-none{--tw-outline-style:none;outline-style:none}.\[cimplify\:checkout\]{cimplify:checkout}@media (hover:hover){.hover\:border-primary\/50:hover{border-color:#934c3a80}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/50:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 50%, transparent)}}.hover\:bg-muted:hover{background-color:var(--color-muted,oklch(95% 0 0))}.hover\:bg-primary\/90:hover{background-color:#934c3ae6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 90%, transparent)}}.hover\:text-primary:hover{color:var(--color-primary,oklch(50% .1 35))}}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-50:disabled{opacity:.5}.\[\&\:\:-webkit-inner-spin-button\]\:appearance-none::-webkit-inner-spin-button{appearance:none}.\[\&\:\:-webkit-outer-spin-button\]\:appearance-none::-webkit-outer-spin-button{appearance:none}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.container{width:100%}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-\[4\/3\]{aspect-ratio:4/3}.max-h-\[85vh\]{max-height:85vh}.w-3\/5{width:60%}.w-full{width:100%}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.\[appearance\:textfield\]{appearance:textfield}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-border>:not(:last-child)){border-color:var(--color-border,oklch(90% 0 0))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.rounded{border-radius:var(--radius,.5rem)}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-none{--tw-border-style:none;border-style:none}.border-border{border-color:var(--color-border,oklch(90% 0 0))}.border-muted-foreground\/30{border-color:#6363634d}@supports (color:color-mix(in lab, red, red)){.border-muted-foreground\/30{border-color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 30%, transparent)}}.border-primary{border-color:var(--color-primary,oklch(50% .1 35))}.bg-background{background-color:var(--color-background,oklch(99% 0 0))}.bg-destructive\/10{background-color:#bb061e1a}@supports (color:color-mix(in lab, red, red)){.bg-destructive\/10{background-color:color-mix(in oklab, var(--color-destructive,oklch(50% .2 25)) 10%, transparent)}}.bg-muted{background-color:var(--color-muted,oklch(95% 0 0))}.bg-primary{background-color:var(--color-primary,oklch(50% .1 35))}.bg-primary\/10{background-color:#934c3a1a}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 10%, transparent)}}.bg-transparent{background-color:#0000}.text-center{text-align:center}.text-left{text-align:left}.font-\[inherit\]{font-family:inherit}.text-\[10px\]{font-size:10px}.text-\[inherit\]{color:inherit}.text-destructive{color:var(--color-destructive,oklch(50% .2 25))}.text-muted-foreground{color:var(--color-muted-foreground,oklch(50% 0 0))}.text-muted-foreground\/60{color:#63636399}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/60{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 60%, transparent)}}.text-primary{color:var(--color-primary,oklch(50% .1 35))}.text-primary-foreground{color:var(--color-primary-foreground,oklch(99% 0 0))}.uppercase{text-transform:uppercase}.no-underline{text-decoration-line:none}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.outline-none{--tw-outline-style:none;outline-style:none}.\[cimplify\:checkout\]{cimplify:checkout}@media (hover:hover){.hover\:border-primary\/50:hover{border-color:#934c3a80}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/50:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 50%, transparent)}}.hover\:bg-muted:hover{background-color:var(--color-muted,oklch(95% 0 0))}.hover\:bg-primary\/90:hover{background-color:#934c3ae6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 90%, transparent)}}.hover\:text-primary:hover{color:var(--color-primary,oklch(50% .1 35))}}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-50:disabled{opacity:.5}.\[\&\:\:-webkit-inner-spin-button\]\:appearance-none::-webkit-inner-spin-button{appearance:none}.\[\&\:\:-webkit-outer-spin-button\]\:appearance-none::-webkit-outer-spin-button{appearance:none}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
{
|
|
13
13
|
"path": "product-card.tsx",
|
|
14
|
-
"content": "\"use client\";\n\nimport React, { useCallback,
|
|
14
|
+
"content": "\"use client\";\n\nimport React, { useCallback, useState } from \"react\";\nimport { Dialog } from \"@base-ui/react/dialog\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { ProductSheet } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\nexport interface ProductCardClassNames {\n root?: string;\n imageContainer?: string;\n image?: string;\n body?: string;\n name?: string;\n description?: string;\n price?: string;\n badges?: string;\n badge?: string;\n modal?: string;\n modalOverlay?: string;\n}\n\nexport interface ProductCardProps {\n /** The product to display. */\n product: Product;\n /** Display mode: \"card\" opens a modal, \"page\" renders as a link. Auto-detected from product.display_mode. */\n displayMode?: \"card\" | \"page\";\n /** Link href for page mode. Default: `/menu/${product.slug}` */\n href?: string;\n /** Custom modal content renderer. Receives the fully-loaded product and a close callback. */\n renderModal?: (product: ProductWithDetails, onClose: () => void) => React.ReactNode;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n /** Custom link renderer for page mode (e.g. Next.js Link). */\n renderLink?: (props: {\n href: string;\n className?: string;\n children: React.ReactNode;\n }) => React.ReactElement;\n /** Replace the entire default card body. */\n children?: React.ReactNode;\n /** Image aspect ratio. Default: \"4/3\". */\n aspectRatio?: \"square\" | \"4/3\" | \"16/10\" | \"3/4\";\n className?: string;\n classNames?: ProductCardClassNames;\n}\n\n/**\n * ProductCard — a product display card with two modes:\n *\n * - **card** (default): clickable button that opens a Base UI Dialog modal\n * - **page**: a plain `<a>` link for SEO-friendly product pages\n */\nexport function ProductCard({\n product,\n displayMode,\n href,\n renderModal,\n renderImage,\n renderLink,\n children,\n aspectRatio = \"4/3\",\n className,\n classNames,\n}: ProductCardProps): React.ReactElement {\n const mode = displayMode ?? product.display_mode ?? \"card\";\n const [isOpen, setIsOpen] = useState(false);\n const [shouldFetch, setShouldFetch] = useState(false);\n\n // Prefetch on pointer enter, always fetch when open\n const { product: productDetails } = useProduct(\n product.slug ?? product.id,\n { enabled: shouldFetch || isOpen },\n );\n\n const handlePrefetch = useCallback(() => {\n setShouldFetch(true);\n }, []);\n\n const handleOpenChange = useCallback((open: boolean) => {\n setIsOpen(open);\n if (open) {\n setShouldFetch(true);\n }\n }, []);\n\n const handleClose = useCallback(() => {\n setIsOpen(false);\n }, []);\n\n const imageUrl = product.image_url || product.images?.[0];\n\n const cardBody = children ?? (\n <>\n {/* Image */}\n {imageUrl && (\n <div\n data-cimplify-product-card-image-container\n className={classNames?.imageContainer}\n style={{\n overflow: \"hidden\",\n ...ASPECT_STYLES[aspectRatio],\n }}\n >\n {renderImage ? (\n renderImage({\n src: imageUrl,\n alt: product.name,\n className: classNames?.image,\n })\n ) : (\n <img\n src={imageUrl}\n alt={product.name}\n className={classNames?.image}\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n data-cimplify-product-card-image\n />\n )}\n </div>\n )}\n\n {/* Body */}\n <div\n data-cimplify-product-card-body\n className={classNames?.body}\n >\n <span\n data-cimplify-product-card-name\n className={classNames?.name}\n >\n {product.name}\n </span>\n {product.description && (\n <span\n data-cimplify-product-card-description\n className={classNames?.description}\n style={{\n display: \"-webkit-box\",\n WebkitLineClamp: 2,\n WebkitBoxOrient: \"vertical\",\n overflow: \"hidden\",\n }}\n >\n {product.description}\n </span>\n )}\n <Price\n amount={product.default_price}\n className={classNames?.price}\n />\n </div>\n </>\n );\n\n // Page mode — render as a link\n if (mode === \"page\") {\n const linkHref = href ?? `/menu/${product.slug}`;\n const linkClassName = cn(\"block no-underline text-[inherit]\", className, classNames?.root);\n\n if (renderLink) {\n return renderLink({ href: linkHref, className: linkClassName, children: cardBody });\n }\n\n return (\n <a\n href={linkHref}\n data-cimplify-product-card\n data-display-mode=\"page\"\n className={linkClassName}\n >\n {cardBody}\n </a>\n );\n }\n\n // Card mode — render as Base UI Dialog\n return (\n <Dialog.Root open={isOpen} onOpenChange={handleOpenChange}>\n <Dialog.Trigger\n onPointerEnter={handlePrefetch}\n data-cimplify-product-card\n data-display-mode=\"card\"\n className={cn(\n \"block w-full text-left bg-transparent border-none p-0 cursor-pointer font-[inherit] text-[inherit]\",\n className,\n classNames?.root,\n )}\n >\n {cardBody}\n </Dialog.Trigger>\n\n <Dialog.Portal>\n <Dialog.Backdrop\n data-cimplify-product-card-backdrop\n className={cn(\n \"fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity\",\n classNames?.modalOverlay,\n )}\n />\n <Dialog.Popup\n data-cimplify-product-card-modal\n className={cn(\n \"fixed rounded-2xl p-0 max-w-lg w-full max-h-[85vh] overflow-auto bg-background shadow-2xl outline-none\",\n classNames?.modal,\n )}\n >\n {isOpen && (\n productDetails ? (\n renderModal ? (\n renderModal(productDetails, handleClose)\n ) : (\n <ProductSheet\n product={productDetails}\n onClose={handleClose}\n renderImage={renderImage}\n />\n )\n ) : (\n <div\n data-cimplify-product-card-modal-loading\n aria-busy=\"true\"\n className=\"flex flex-col gap-4 p-6\"\n >\n <div className=\"aspect-[4/3] bg-muted rounded-lg animate-pulse\" />\n <div className=\"h-6 w-3/5 bg-muted rounded animate-pulse\" />\n </div>\n )\n )}\n </Dialog.Popup>\n </Dialog.Portal>\n </Dialog.Root>\n );\n}\n"
|
|
15
15
|
}
|
|
16
16
|
]
|
|
17
17
|
}
|
|
@@ -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 } from \"react\";\nimport type {\n ProductWithDetails,\n ProductVariant,\n AddOnOption,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\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 { 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}\n\nexport interface ProductCustomizerProps {\n product: ProductWithDetails;\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n className?: string;\n classNames?: ProductCustomizerClassNames;\n}\n\nexport function ProductCustomizer({\n product,\n onAddToCart,\n className,\n classNames,\n}: ProductCustomizerProps): React.ReactElement {\n const [quantity, setQuantity] = useState(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<ProductVariant | 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\n const cart = useCart();\n\n const productType = product.type || \"product\";\n const isComposite = productType === \"composite\";\n const isBundle = productType === \"bundle\";\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(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 }, [product.id]);\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 quoteEnabled = isComposite\n ? compositeReady\n : isBundle\n ? bundleReady\n : requiredAddOnsSatisfied;\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 // quoted_total_price_info already includes quantity multiplication\n if (quote.quoted_total_price_info?.final_price != null) {\n return parsePrice(quote.quoted_total_price_info.final_price);\n }\n\n // final_price_info is per-unit — multiply by quantity\n const perUnit = quote.final_price_info.final_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: ProductVariant | undefined) => {\n setSelectedVariantId(variantId);\n setSelectedVariant(variant);\n },\n [],\n );\n\n const handleAddToCart = 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 };\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 setTimeout(() => {\n setIsAdded(false);\n setQuantity(1);\n }, 2000);\n } catch {\n // Caller handles errors via onAddToCart or cart hook error state\n } finally {\n setIsSubmitting(false);\n }\n };\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 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 onSelectionsChange={setBundleSelections}\n onPriceChange={setBundleTotalPrice}\n onReady={setBundleReady}\n />\n )}\n\n {hasVariants && (\n <VariantSelector\n variants={product.variants!}\n variantAxes={product.variant_axes}\n basePrice={product.default_price}\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 />\n )}\n\n <div\n data-cimplify-customizer-actions\n className={cn(\"flex items-center gap-4 pt-4 border-t border-border\", classNames?.actions)}\n >\n <QuantitySelector\n value={quantity}\n onChange={setQuantity}\n min={1}\n />\n\n <button\n type=\"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\",\n isAdded && classNames?.submitButtonAdded,\n classNames?.submitButton,\n )}\n >\n {isAdded ? \"Added to Cart\" : (\n <>\n Add to Cart · <Price amount={displayTotalPrice} />\n </>\n )}\n </button>\n </div>\n\n {!quoteEnabled && (\n <p\n id=\"cimplify-customizer-validation\"\n data-cimplify-customizer-validation\n className={cn(\"text-sm text-destructive mt-2\", classNames?.validation)}\n >\n Please select all required options\n </p>\n )}\n </div>\n );\n}\n"
|
|
17
|
+
"content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect } from \"react\";\nimport type {\n ProductWithDetails,\n ProductVariant,\n AddOnOption,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\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 { 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}\n\nexport interface ProductCustomizerProps {\n product: ProductWithDetails;\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n className?: string;\n classNames?: ProductCustomizerClassNames;\n}\n\nexport function ProductCustomizer({\n product,\n onAddToCart,\n className,\n classNames,\n}: ProductCustomizerProps): React.ReactElement {\n const [quantity, setQuantity] = useState(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<ProductVariant | 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\n const cart = useCart();\n\n const productType = product.type || \"product\";\n const isComposite = productType === \"composite\";\n const isBundle = productType === \"bundle\";\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(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 }, [product.id]);\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 quoteEnabled = isComposite\n ? compositeReady\n : isBundle\n ? bundleReady\n : requiredAddOnsSatisfied;\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 // quoted_total_price_info already includes quantity multiplication\n if (quote.quoted_total_price_info?.final_price != null) {\n return parsePrice(quote.quoted_total_price_info.final_price);\n }\n\n // final_price_info is per-unit — multiply by quantity\n const perUnit = quote.final_price_info.final_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: ProductVariant | undefined) => {\n setSelectedVariantId(variantId);\n setSelectedVariant(variant);\n },\n [],\n );\n\n const handleAddToCart = 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 };\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 setTimeout(() => {\n setIsAdded(false);\n setQuantity(1);\n }, 2000);\n } catch {\n // Caller handles errors via onAddToCart or cart hook error state\n } finally {\n setIsSubmitting(false);\n }\n };\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 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 onSelectionsChange={setBundleSelections}\n onPriceChange={setBundleTotalPrice}\n onReady={setBundleReady}\n />\n )}\n\n {hasVariants && (\n <VariantSelector\n variants={product.variants!}\n variantAxes={product.variant_axes}\n basePrice={product.default_price}\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 />\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 <QuantitySelector\n value={quantity}\n onChange={setQuantity}\n min={1}\n />\n\n <button\n type=\"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 · <Price amount={displayTotalPrice} />\n </>\n )}\n </button>\n </div>\n </div>\n\n </div>\n );\n}\n"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|