@cimplify/sdk 0.13.0 → 0.14.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 CHANGED
@@ -877,8 +877,8 @@ interface ProductCardProps {
877
877
  displayMode?: "card" | "page";
878
878
  /** Link href for page mode. Default: `/menu/${product.slug}` */
879
879
  href?: string;
880
- /** Custom modal content renderer. Receives the fully-loaded product. */
881
- renderModal?: (product: ProductWithDetails) => React.ReactNode;
880
+ /** Custom modal content renderer. Receives the fully-loaded product and a close callback. */
881
+ renderModal?: (product: ProductWithDetails, onClose: () => void) => React.ReactNode;
882
882
  /** Custom image renderer (e.g. Next.js Image). */
883
883
  renderImage?: (props: {
884
884
  src: string;
package/dist/react.d.ts CHANGED
@@ -877,8 +877,8 @@ interface ProductCardProps {
877
877
  displayMode?: "card" | "page";
878
878
  /** Link href for page mode. Default: `/menu/${product.slug}` */
879
879
  href?: string;
880
- /** Custom modal content renderer. Receives the fully-loaded product. */
881
- renderModal?: (product: ProductWithDetails) => React.ReactNode;
880
+ /** Custom modal content renderer. Receives the fully-loaded product and a close callback. */
881
+ renderModal?: (product: ProductWithDetails, onClose: () => void) => React.ReactNode;
882
882
  /** Custom image renderer (e.g. Next.js Image). */
883
883
  renderImage?: (props: {
884
884
  src: string;
package/dist/react.js CHANGED
@@ -7978,7 +7978,7 @@ function VariantSelector({
7978
7978
  if (!variants || variants.length <= 1) {
7979
7979
  return null;
7980
7980
  }
7981
- const basePriceNum = basePrice != null ? parsePrice(basePrice) : 0;
7981
+ basePrice != null ? parsePrice(basePrice) : 0;
7982
7982
  if (variantAxes && variantAxes.length > 0) {
7983
7983
  return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-cimplify-variant-selector": true, className: cn("space-y-5", className, classNames?.root), children: variantAxes.map((axis) => {
7984
7984
  const labelId = `${idPrefix}-axis-${axis.id}`;
@@ -8029,14 +8029,16 @@ function VariantSelector({
8029
8029
  }) });
8030
8030
  }
8031
8031
  const listLabelId = `${idPrefix}-variant-list`;
8032
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-cimplify-variant-selector": true, className: cn("space-y-5", className, classNames?.root), children: [
8033
- /* @__PURE__ */ jsxRuntime.jsx(
8034
- "label",
8032
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-cimplify-variant-selector": true, className: cn(className, classNames?.root), children: [
8033
+ /* @__PURE__ */ jsxRuntime.jsxs(
8034
+ "div",
8035
8035
  {
8036
- id: listLabelId,
8037
- "data-cimplify-variant-list-label": true,
8038
- className: cn("block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3", classNames?.listLabel),
8039
- children: "Options"
8036
+ "data-cimplify-variant-list-header": true,
8037
+ className: cn("flex items-center justify-between py-3", classNames?.listLabel),
8038
+ children: [
8039
+ /* @__PURE__ */ jsxRuntime.jsx("label", { id: listLabelId, className: "text-base font-bold", children: "Options" }),
8040
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-semibold text-destructive bg-destructive/10 px-2.5 py-1 rounded", children: "Required" })
8041
+ ]
8040
8042
  }
8041
8043
  ),
8042
8044
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -8049,11 +8051,10 @@ function VariantSelector({
8049
8051
  onVariantChange(variant?.id, variant);
8050
8052
  },
8051
8053
  "data-cimplify-variant-list": true,
8052
- className: cn("space-y-2", classNames?.list),
8054
+ className: cn("divide-y divide-border", classNames?.list),
8053
8055
  children: variants.map((variant) => {
8054
8056
  const isSelected = selectedVariantId === variant.id;
8055
8057
  const adjustment = parsePrice(variant.price_adjustment);
8056
- const effectivePrice = basePriceNum + adjustment;
8057
8058
  return /* @__PURE__ */ jsxRuntime.jsxs(
8058
8059
  radio.Radio.Root,
8059
8060
  {
@@ -8061,35 +8062,32 @@ function VariantSelector({
8061
8062
  "data-cimplify-variant-option": true,
8062
8063
  "data-selected": isSelected || void 0,
8063
8064
  className: cn(
8064
- "w-full flex items-center justify-between px-4 py-3 border transition-colors border-border hover:border-primary/50",
8065
- isSelected && "bg-primary/5 border-primary",
8065
+ "w-full flex items-center gap-3 py-4 transition-colors cursor-pointer",
8066
8066
  isSelected ? classNames?.optionSelected : classNames?.option
8067
8067
  ),
8068
8068
  children: [
8069
+ /* @__PURE__ */ jsxRuntime.jsx(
8070
+ "span",
8071
+ {
8072
+ "data-cimplify-variant-radio": true,
8073
+ className: cn(
8074
+ "w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors",
8075
+ isSelected ? "border-primary" : "border-muted-foreground/30"
8076
+ ),
8077
+ children: isSelected && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-2.5 h-2.5 rounded-full bg-primary" })
8078
+ }
8079
+ ),
8069
8080
  /* @__PURE__ */ jsxRuntime.jsx(
8070
8081
  "span",
8071
8082
  {
8072
8083
  "data-cimplify-variant-name": true,
8073
- className: cn("font-medium", isSelected && "text-primary", classNames?.name),
8084
+ className: cn("flex-1 min-w-0 text-sm", classNames?.name),
8074
8085
  children: getVariantDisplayName(variant, productName)
8075
8086
  }
8076
8087
  ),
8077
- /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-cimplify-variant-pricing": true, className: cn("text-sm flex items-center gap-2", classNames?.pricing), children: [
8078
- adjustment !== 0 && /* @__PURE__ */ jsxRuntime.jsxs(
8079
- "span",
8080
- {
8081
- "data-cimplify-variant-adjustment": true,
8082
- className: cn(
8083
- adjustment > 0 ? "text-muted-foreground" : "text-green-600",
8084
- classNames?.adjustment
8085
- ),
8086
- children: [
8087
- adjustment > 0 ? "+" : "",
8088
- /* @__PURE__ */ jsxRuntime.jsx(Price, { amount: variant.price_adjustment })
8089
- ]
8090
- }
8091
- ),
8092
- /* @__PURE__ */ jsxRuntime.jsx(Price, { amount: effectivePrice, className: "text-muted-foreground" })
8088
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-cimplify-variant-pricing": true, className: cn("text-sm text-muted-foreground", classNames?.pricing), children: [
8089
+ adjustment > 0 ? "+" : adjustment < 0 ? "" : "+",
8090
+ /* @__PURE__ */ jsxRuntime.jsx(Price, { amount: variant.price_adjustment })
8093
8091
  ] })
8094
8092
  ]
8095
8093
  },
@@ -8148,54 +8146,46 @@ function AddOnSelector({
8148
8146
  (id) => addOn.options.some((o) => o.id === id)
8149
8147
  ).length;
8150
8148
  const minMet = !addOn.min_selections || currentSelections >= addOn.min_selections;
8149
+ const isSingleSelect = addOn.is_mutually_exclusive || !addOn.is_multiple_allowed;
8151
8150
  return /* @__PURE__ */ jsxRuntime.jsxs(
8152
8151
  "div",
8153
8152
  {
8154
8153
  "data-cimplify-addon-group": true,
8155
- className: cn("border border-border p-5", classNames?.group),
8154
+ className: cn(classNames?.group),
8156
8155
  children: [
8157
8156
  /* @__PURE__ */ jsxRuntime.jsxs(
8158
8157
  "div",
8159
8158
  {
8160
8159
  "data-cimplify-addon-header": true,
8161
- className: cn("flex items-center justify-between mb-4", classNames?.header),
8160
+ className: cn("flex items-center justify-between py-3", classNames?.header),
8162
8161
  children: [
8163
8162
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
8164
- /* @__PURE__ */ jsxRuntime.jsxs(
8163
+ /* @__PURE__ */ jsxRuntime.jsx(
8165
8164
  "span",
8166
8165
  {
8167
8166
  "data-cimplify-addon-name": true,
8168
- className: cn("text-xs font-medium uppercase tracking-wider text-muted-foreground", classNames?.name),
8169
- children: [
8170
- addOn.name,
8171
- addOn.is_required && /* @__PURE__ */ jsxRuntime.jsxs(
8172
- "span",
8173
- {
8174
- "data-cimplify-addon-required": true,
8175
- className: cn("text-destructive ml-1", classNames?.required),
8176
- children: [
8177
- " ",
8178
- "*"
8179
- ]
8180
- }
8181
- )
8182
- ]
8167
+ className: cn("text-base font-bold", classNames?.name),
8168
+ children: addOn.name
8183
8169
  }
8184
8170
  ),
8185
8171
  (addOn.min_selections || addOn.max_selections) && /* @__PURE__ */ jsxRuntime.jsx(
8186
8172
  "span",
8187
8173
  {
8188
8174
  "data-cimplify-addon-constraint": true,
8189
- className: cn("text-xs text-muted-foreground/70 mt-1", classNames?.constraint),
8175
+ className: cn("block text-xs text-muted-foreground mt-0.5", classNames?.constraint),
8190
8176
  children: addOn.min_selections && addOn.max_selections ? `Choose ${addOn.min_selections}\u2013${addOn.max_selections}` : addOn.min_selections ? `Choose at least ${addOn.min_selections}` : `Choose up to ${addOn.max_selections}`
8191
8177
  }
8192
8178
  )
8193
8179
  ] }),
8194
- !minMet && /* @__PURE__ */ jsxRuntime.jsx(
8180
+ (addOn.is_required || !minMet) && /* @__PURE__ */ jsxRuntime.jsx(
8195
8181
  "span",
8196
8182
  {
8197
- "data-cimplify-addon-validation": true,
8198
- className: cn("text-xs text-destructive font-medium", classNames?.validation),
8183
+ "data-cimplify-addon-required": true,
8184
+ className: cn(
8185
+ "text-xs font-semibold px-2.5 py-1 rounded",
8186
+ !minMet ? "text-destructive bg-destructive/10" : "text-destructive bg-destructive/10",
8187
+ classNames?.required
8188
+ ),
8199
8189
  children: "Required"
8200
8190
  }
8201
8191
  )
@@ -8206,7 +8196,7 @@ function AddOnSelector({
8206
8196
  "div",
8207
8197
  {
8208
8198
  "data-cimplify-addon-options": true,
8209
- className: cn("space-y-1", classNames?.options),
8199
+ className: cn("divide-y divide-border", classNames?.options),
8210
8200
  children: addOn.options.map((option) => {
8211
8201
  const isSelected = isOptionSelected(option.id);
8212
8202
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -8218,8 +8208,7 @@ function AddOnSelector({
8218
8208
  "data-cimplify-addon-option": true,
8219
8209
  "data-selected": isSelected || void 0,
8220
8210
  className: cn(
8221
- "w-full flex items-center gap-3 px-4 py-3 border transition-colors text-left border-transparent hover:bg-muted/50",
8222
- isSelected && "bg-primary/5 border-primary",
8211
+ "w-full flex items-center gap-3 py-4 transition-colors text-left cursor-pointer",
8223
8212
  isSelected ? classNames?.optionSelected : classNames?.option
8224
8213
  ),
8225
8214
  children: [
@@ -8230,25 +8219,42 @@ function AddOnSelector({
8230
8219
  keepMounted: false
8231
8220
  }
8232
8221
  ),
8222
+ isSingleSelect ? /* @__PURE__ */ jsxRuntime.jsx(
8223
+ "span",
8224
+ {
8225
+ "data-cimplify-addon-radio": true,
8226
+ className: cn(
8227
+ "w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors",
8228
+ isSelected ? "border-primary" : "border-muted-foreground/30"
8229
+ ),
8230
+ children: isSelected && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-2.5 h-2.5 rounded-full bg-primary" })
8231
+ }
8232
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
8233
+ "span",
8234
+ {
8235
+ "data-cimplify-addon-checkbox": true,
8236
+ className: cn(
8237
+ "w-5 h-5 rounded-sm border-2 flex items-center justify-center shrink-0 transition-colors",
8238
+ isSelected ? "border-primary bg-primary" : "border-muted-foreground/30"
8239
+ ),
8240
+ children: isSelected && /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 12 12", className: "w-3 h-3 text-primary-foreground", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 6l3 3 5-5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
8241
+ }
8242
+ ),
8233
8243
  /* @__PURE__ */ jsxRuntime.jsx(
8234
8244
  "span",
8235
8245
  {
8236
8246
  "data-cimplify-addon-option-name": true,
8237
8247
  className: cn(
8238
- "flex-1 min-w-0 text-sm font-medium",
8239
- isSelected && "text-primary",
8248
+ "flex-1 min-w-0 text-sm",
8240
8249
  classNames?.optionName
8241
8250
  ),
8242
8251
  children: option.name
8243
8252
  }
8244
8253
  ),
8245
- option.default_price != null && parsePrice(option.default_price) !== 0 && /* @__PURE__ */ jsxRuntime.jsx(
8246
- Price,
8247
- {
8248
- amount: option.default_price,
8249
- prefix: "+"
8250
- }
8251
- )
8254
+ option.default_price != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm text-muted-foreground", children: [
8255
+ "+",
8256
+ /* @__PURE__ */ jsxRuntime.jsx(Price, { amount: option.default_price })
8257
+ ] })
8252
8258
  ]
8253
8259
  },
8254
8260
  option.id
@@ -9570,13 +9576,18 @@ function ProductCard({
9570
9576
  }) {
9571
9577
  const mode = displayMode ?? product.display_mode ?? "card";
9572
9578
  const [isOpen, setIsOpen] = React3.useState(false);
9579
+ const [shouldFetch, setShouldFetch] = React3.useState(false);
9573
9580
  const dialogRef = React3.useRef(null);
9574
9581
  const { product: productDetails } = useProduct(
9575
9582
  product.slug ?? product.id,
9576
- { enabled: isOpen }
9583
+ { enabled: shouldFetch || isOpen }
9577
9584
  );
9585
+ const handlePrefetch = React3.useCallback(() => {
9586
+ setShouldFetch(true);
9587
+ }, []);
9578
9588
  const handleOpen = React3.useCallback(() => {
9579
9589
  setIsOpen(true);
9590
+ setShouldFetch(true);
9580
9591
  dialogRef.current?.showModal();
9581
9592
  }, []);
9582
9593
  const handleClose = React3.useCallback(() => {
@@ -9683,6 +9694,7 @@ function ProductCard({
9683
9694
  {
9684
9695
  type: "button",
9685
9696
  "aria-haspopup": "dialog",
9697
+ onPointerEnter: handlePrefetch,
9686
9698
  onClick: handleOpen,
9687
9699
  "data-cimplify-product-card": true,
9688
9700
  "data-display-mode": "card",
@@ -9705,7 +9717,7 @@ function ProductCard({
9705
9717
  "border-none rounded-2xl p-0 max-w-lg w-full max-h-[85vh] overflow-auto bg-background shadow-2xl backdrop:bg-black/50 backdrop:backdrop-blur-sm open:animate-in open:fade-in-0 open:slide-in-from-bottom-4",
9706
9718
  classNames?.modal
9707
9719
  ),
9708
- children: isOpen && (productDetails ? renderModal ? renderModal(productDetails) : /* @__PURE__ */ jsxRuntime.jsx(
9720
+ children: isOpen && (productDetails ? renderModal ? renderModal(productDetails, handleClose) : /* @__PURE__ */ jsxRuntime.jsx(
9709
9721
  ProductSheet,
9710
9722
  {
9711
9723
  product: productDetails,
package/dist/react.mjs CHANGED
@@ -7972,7 +7972,7 @@ function VariantSelector({
7972
7972
  if (!variants || variants.length <= 1) {
7973
7973
  return null;
7974
7974
  }
7975
- const basePriceNum = basePrice != null ? parsePrice(basePrice) : 0;
7975
+ basePrice != null ? parsePrice(basePrice) : 0;
7976
7976
  if (variantAxes && variantAxes.length > 0) {
7977
7977
  return /* @__PURE__ */ jsx("div", { "data-cimplify-variant-selector": true, className: cn("space-y-5", className, classNames?.root), children: variantAxes.map((axis) => {
7978
7978
  const labelId = `${idPrefix}-axis-${axis.id}`;
@@ -8023,14 +8023,16 @@ function VariantSelector({
8023
8023
  }) });
8024
8024
  }
8025
8025
  const listLabelId = `${idPrefix}-variant-list`;
8026
- return /* @__PURE__ */ jsxs("div", { "data-cimplify-variant-selector": true, className: cn("space-y-5", className, classNames?.root), children: [
8027
- /* @__PURE__ */ jsx(
8028
- "label",
8026
+ return /* @__PURE__ */ jsxs("div", { "data-cimplify-variant-selector": true, className: cn(className, classNames?.root), children: [
8027
+ /* @__PURE__ */ jsxs(
8028
+ "div",
8029
8029
  {
8030
- id: listLabelId,
8031
- "data-cimplify-variant-list-label": true,
8032
- className: cn("block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3", classNames?.listLabel),
8033
- children: "Options"
8030
+ "data-cimplify-variant-list-header": true,
8031
+ className: cn("flex items-center justify-between py-3", classNames?.listLabel),
8032
+ children: [
8033
+ /* @__PURE__ */ jsx("label", { id: listLabelId, className: "text-base font-bold", children: "Options" }),
8034
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-semibold text-destructive bg-destructive/10 px-2.5 py-1 rounded", children: "Required" })
8035
+ ]
8034
8036
  }
8035
8037
  ),
8036
8038
  /* @__PURE__ */ jsx(
@@ -8043,11 +8045,10 @@ function VariantSelector({
8043
8045
  onVariantChange(variant?.id, variant);
8044
8046
  },
8045
8047
  "data-cimplify-variant-list": true,
8046
- className: cn("space-y-2", classNames?.list),
8048
+ className: cn("divide-y divide-border", classNames?.list),
8047
8049
  children: variants.map((variant) => {
8048
8050
  const isSelected = selectedVariantId === variant.id;
8049
8051
  const adjustment = parsePrice(variant.price_adjustment);
8050
- const effectivePrice = basePriceNum + adjustment;
8051
8052
  return /* @__PURE__ */ jsxs(
8052
8053
  Radio.Root,
8053
8054
  {
@@ -8055,35 +8056,32 @@ function VariantSelector({
8055
8056
  "data-cimplify-variant-option": true,
8056
8057
  "data-selected": isSelected || void 0,
8057
8058
  className: cn(
8058
- "w-full flex items-center justify-between px-4 py-3 border transition-colors border-border hover:border-primary/50",
8059
- isSelected && "bg-primary/5 border-primary",
8059
+ "w-full flex items-center gap-3 py-4 transition-colors cursor-pointer",
8060
8060
  isSelected ? classNames?.optionSelected : classNames?.option
8061
8061
  ),
8062
8062
  children: [
8063
+ /* @__PURE__ */ jsx(
8064
+ "span",
8065
+ {
8066
+ "data-cimplify-variant-radio": true,
8067
+ className: cn(
8068
+ "w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors",
8069
+ isSelected ? "border-primary" : "border-muted-foreground/30"
8070
+ ),
8071
+ children: isSelected && /* @__PURE__ */ jsx("span", { className: "w-2.5 h-2.5 rounded-full bg-primary" })
8072
+ }
8073
+ ),
8063
8074
  /* @__PURE__ */ jsx(
8064
8075
  "span",
8065
8076
  {
8066
8077
  "data-cimplify-variant-name": true,
8067
- className: cn("font-medium", isSelected && "text-primary", classNames?.name),
8078
+ className: cn("flex-1 min-w-0 text-sm", classNames?.name),
8068
8079
  children: getVariantDisplayName(variant, productName)
8069
8080
  }
8070
8081
  ),
8071
- /* @__PURE__ */ jsxs("span", { "data-cimplify-variant-pricing": true, className: cn("text-sm flex items-center gap-2", classNames?.pricing), children: [
8072
- adjustment !== 0 && /* @__PURE__ */ jsxs(
8073
- "span",
8074
- {
8075
- "data-cimplify-variant-adjustment": true,
8076
- className: cn(
8077
- adjustment > 0 ? "text-muted-foreground" : "text-green-600",
8078
- classNames?.adjustment
8079
- ),
8080
- children: [
8081
- adjustment > 0 ? "+" : "",
8082
- /* @__PURE__ */ jsx(Price, { amount: variant.price_adjustment })
8083
- ]
8084
- }
8085
- ),
8086
- /* @__PURE__ */ jsx(Price, { amount: effectivePrice, className: "text-muted-foreground" })
8082
+ /* @__PURE__ */ jsxs("span", { "data-cimplify-variant-pricing": true, className: cn("text-sm text-muted-foreground", classNames?.pricing), children: [
8083
+ adjustment > 0 ? "+" : adjustment < 0 ? "" : "+",
8084
+ /* @__PURE__ */ jsx(Price, { amount: variant.price_adjustment })
8087
8085
  ] })
8088
8086
  ]
8089
8087
  },
@@ -8142,54 +8140,46 @@ function AddOnSelector({
8142
8140
  (id) => addOn.options.some((o) => o.id === id)
8143
8141
  ).length;
8144
8142
  const minMet = !addOn.min_selections || currentSelections >= addOn.min_selections;
8143
+ const isSingleSelect = addOn.is_mutually_exclusive || !addOn.is_multiple_allowed;
8145
8144
  return /* @__PURE__ */ jsxs(
8146
8145
  "div",
8147
8146
  {
8148
8147
  "data-cimplify-addon-group": true,
8149
- className: cn("border border-border p-5", classNames?.group),
8148
+ className: cn(classNames?.group),
8150
8149
  children: [
8151
8150
  /* @__PURE__ */ jsxs(
8152
8151
  "div",
8153
8152
  {
8154
8153
  "data-cimplify-addon-header": true,
8155
- className: cn("flex items-center justify-between mb-4", classNames?.header),
8154
+ className: cn("flex items-center justify-between py-3", classNames?.header),
8156
8155
  children: [
8157
8156
  /* @__PURE__ */ jsxs("div", { children: [
8158
- /* @__PURE__ */ jsxs(
8157
+ /* @__PURE__ */ jsx(
8159
8158
  "span",
8160
8159
  {
8161
8160
  "data-cimplify-addon-name": true,
8162
- className: cn("text-xs font-medium uppercase tracking-wider text-muted-foreground", classNames?.name),
8163
- children: [
8164
- addOn.name,
8165
- addOn.is_required && /* @__PURE__ */ jsxs(
8166
- "span",
8167
- {
8168
- "data-cimplify-addon-required": true,
8169
- className: cn("text-destructive ml-1", classNames?.required),
8170
- children: [
8171
- " ",
8172
- "*"
8173
- ]
8174
- }
8175
- )
8176
- ]
8161
+ className: cn("text-base font-bold", classNames?.name),
8162
+ children: addOn.name
8177
8163
  }
8178
8164
  ),
8179
8165
  (addOn.min_selections || addOn.max_selections) && /* @__PURE__ */ jsx(
8180
8166
  "span",
8181
8167
  {
8182
8168
  "data-cimplify-addon-constraint": true,
8183
- className: cn("text-xs text-muted-foreground/70 mt-1", classNames?.constraint),
8169
+ className: cn("block text-xs text-muted-foreground mt-0.5", classNames?.constraint),
8184
8170
  children: addOn.min_selections && addOn.max_selections ? `Choose ${addOn.min_selections}\u2013${addOn.max_selections}` : addOn.min_selections ? `Choose at least ${addOn.min_selections}` : `Choose up to ${addOn.max_selections}`
8185
8171
  }
8186
8172
  )
8187
8173
  ] }),
8188
- !minMet && /* @__PURE__ */ jsx(
8174
+ (addOn.is_required || !minMet) && /* @__PURE__ */ jsx(
8189
8175
  "span",
8190
8176
  {
8191
- "data-cimplify-addon-validation": true,
8192
- className: cn("text-xs text-destructive font-medium", classNames?.validation),
8177
+ "data-cimplify-addon-required": true,
8178
+ className: cn(
8179
+ "text-xs font-semibold px-2.5 py-1 rounded",
8180
+ !minMet ? "text-destructive bg-destructive/10" : "text-destructive bg-destructive/10",
8181
+ classNames?.required
8182
+ ),
8193
8183
  children: "Required"
8194
8184
  }
8195
8185
  )
@@ -8200,7 +8190,7 @@ function AddOnSelector({
8200
8190
  "div",
8201
8191
  {
8202
8192
  "data-cimplify-addon-options": true,
8203
- className: cn("space-y-1", classNames?.options),
8193
+ className: cn("divide-y divide-border", classNames?.options),
8204
8194
  children: addOn.options.map((option) => {
8205
8195
  const isSelected = isOptionSelected(option.id);
8206
8196
  return /* @__PURE__ */ jsxs(
@@ -8212,8 +8202,7 @@ function AddOnSelector({
8212
8202
  "data-cimplify-addon-option": true,
8213
8203
  "data-selected": isSelected || void 0,
8214
8204
  className: cn(
8215
- "w-full flex items-center gap-3 px-4 py-3 border transition-colors text-left border-transparent hover:bg-muted/50",
8216
- isSelected && "bg-primary/5 border-primary",
8205
+ "w-full flex items-center gap-3 py-4 transition-colors text-left cursor-pointer",
8217
8206
  isSelected ? classNames?.optionSelected : classNames?.option
8218
8207
  ),
8219
8208
  children: [
@@ -8224,25 +8213,42 @@ function AddOnSelector({
8224
8213
  keepMounted: false
8225
8214
  }
8226
8215
  ),
8216
+ isSingleSelect ? /* @__PURE__ */ jsx(
8217
+ "span",
8218
+ {
8219
+ "data-cimplify-addon-radio": true,
8220
+ className: cn(
8221
+ "w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors",
8222
+ isSelected ? "border-primary" : "border-muted-foreground/30"
8223
+ ),
8224
+ children: isSelected && /* @__PURE__ */ jsx("span", { className: "w-2.5 h-2.5 rounded-full bg-primary" })
8225
+ }
8226
+ ) : /* @__PURE__ */ jsx(
8227
+ "span",
8228
+ {
8229
+ "data-cimplify-addon-checkbox": true,
8230
+ className: cn(
8231
+ "w-5 h-5 rounded-sm border-2 flex items-center justify-center shrink-0 transition-colors",
8232
+ isSelected ? "border-primary bg-primary" : "border-muted-foreground/30"
8233
+ ),
8234
+ children: isSelected && /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 12", className: "w-3 h-3 text-primary-foreground", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M2 6l3 3 5-5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
8235
+ }
8236
+ ),
8227
8237
  /* @__PURE__ */ jsx(
8228
8238
  "span",
8229
8239
  {
8230
8240
  "data-cimplify-addon-option-name": true,
8231
8241
  className: cn(
8232
- "flex-1 min-w-0 text-sm font-medium",
8233
- isSelected && "text-primary",
8242
+ "flex-1 min-w-0 text-sm",
8234
8243
  classNames?.optionName
8235
8244
  ),
8236
8245
  children: option.name
8237
8246
  }
8238
8247
  ),
8239
- option.default_price != null && parsePrice(option.default_price) !== 0 && /* @__PURE__ */ jsx(
8240
- Price,
8241
- {
8242
- amount: option.default_price,
8243
- prefix: "+"
8244
- }
8245
- )
8248
+ option.default_price != null && /* @__PURE__ */ jsxs("span", { className: "text-sm text-muted-foreground", children: [
8249
+ "+",
8250
+ /* @__PURE__ */ jsx(Price, { amount: option.default_price })
8251
+ ] })
8246
8252
  ]
8247
8253
  },
8248
8254
  option.id
@@ -9564,13 +9570,18 @@ function ProductCard({
9564
9570
  }) {
9565
9571
  const mode = displayMode ?? product.display_mode ?? "card";
9566
9572
  const [isOpen, setIsOpen] = useState(false);
9573
+ const [shouldFetch, setShouldFetch] = useState(false);
9567
9574
  const dialogRef = useRef(null);
9568
9575
  const { product: productDetails } = useProduct(
9569
9576
  product.slug ?? product.id,
9570
- { enabled: isOpen }
9577
+ { enabled: shouldFetch || isOpen }
9571
9578
  );
9579
+ const handlePrefetch = useCallback(() => {
9580
+ setShouldFetch(true);
9581
+ }, []);
9572
9582
  const handleOpen = useCallback(() => {
9573
9583
  setIsOpen(true);
9584
+ setShouldFetch(true);
9574
9585
  dialogRef.current?.showModal();
9575
9586
  }, []);
9576
9587
  const handleClose = useCallback(() => {
@@ -9677,6 +9688,7 @@ function ProductCard({
9677
9688
  {
9678
9689
  type: "button",
9679
9690
  "aria-haspopup": "dialog",
9691
+ onPointerEnter: handlePrefetch,
9680
9692
  onClick: handleOpen,
9681
9693
  "data-cimplify-product-card": true,
9682
9694
  "data-display-mode": "card",
@@ -9699,7 +9711,7 @@ function ProductCard({
9699
9711
  "border-none rounded-2xl p-0 max-w-lg w-full max-h-[85vh] overflow-auto bg-background shadow-2xl backdrop:bg-black/50 backdrop:backdrop-blur-sm open:animate-in open:fade-in-0 open:slide-in-from-bottom-4",
9700
9712
  classNames?.modal
9701
9713
  ),
9702
- children: isOpen && (productDetails ? renderModal ? renderModal(productDetails) : /* @__PURE__ */ jsx(
9714
+ children: isOpen && (productDetails ? renderModal ? renderModal(productDetails, handleClose) : /* @__PURE__ */ jsx(
9703
9715
  ProductSheet,
9704
9716
  {
9705
9717
  product: productDetails,
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-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}.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}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.rounded{border-radius:var(--radius,.5rem)}.border{border-style:var(--tw-border-style);border-width:1px}.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-primary{border-color:var(--color-primary,oklch(50% .1 35))}.border-transparent{border-color:#0000}.bg-background{background-color:var(--color-background,oklch(99% 0 0))}.bg-muted{background-color:var(--color-muted,oklch(95% 0 0))}.bg-primary{background-color:var(--color-primary,oklch(50% .1 35))}.bg-primary\/5{background-color:#934c3a0d}@supports (color:color-mix(in lab, red, red)){.bg-primary\/5{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 5%, transparent)}}.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-muted-foreground\/70{color:#636363b3}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/70{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 70%, 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}.opacity-70{opacity:.7}.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-muted\/50:hover{background-color:#eeeeee80}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/50:hover{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 50%, transparent)}}.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-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}.items-start{align-items:flex-start}.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))}.border-transparent{border-color:#0000}.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\/5{background-color:#934c3a0d}@supports (color:color-mix(in lab, red, red)){.bg-primary\/5{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 5%, transparent)}}.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-muted-foreground\/70{color:#636363b3}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/70{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 70%, 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}.opacity-70{opacity:.7}.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-muted\/50:hover{background-color:#eeeeee80}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/50:hover{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 50%, transparent)}}.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cimplify/sdk",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Cimplify Commerce SDK for storefronts",
5
5
  "keywords": [
6
6
  "cimplify",
@@ -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 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"
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 const isSingleSelect = addOn.is_mutually_exclusive || !addOn.is_multiple_allowed;\n\n return (\n <div\n key={addOn.id}\n data-cimplify-addon-group\n className={cn(classNames?.group)}\n >\n <div\n data-cimplify-addon-header\n className={cn(\"flex items-center justify-between py-3\", classNames?.header)}\n >\n <div>\n <span\n data-cimplify-addon-name\n className={cn(\"text-base font-bold\", classNames?.name)}\n >\n {addOn.name}\n </span>\n {(addOn.min_selections || addOn.max_selections) && (\n <span\n data-cimplify-addon-constraint\n className={cn(\"block text-xs text-muted-foreground mt-0.5\", 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 {(addOn.is_required || !minMet) && (\n <span\n data-cimplify-addon-required\n className={cn(\n \"text-xs font-semibold px-2.5 py-1 rounded\",\n !minMet\n ? \"text-destructive bg-destructive/10\"\n : \"text-destructive bg-destructive/10\",\n classNames?.required,\n )}\n >\n Required\n </span>\n )}\n </div>\n\n <div\n data-cimplify-addon-options\n className={cn(\"divide-y divide-border\", 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 py-4 transition-colors text-left cursor-pointer\",\n isSelected ? classNames?.optionSelected : classNames?.option,\n )}\n >\n <Checkbox.Indicator\n className=\"hidden\"\n keepMounted={false}\n />\n\n {/* Visual indicator: radio circle for single-select, checkbox square for multi-select */}\n {isSingleSelect ? (\n <span\n data-cimplify-addon-radio\n className={cn(\n \"w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors\",\n isSelected ? \"border-primary\" : \"border-muted-foreground/30\",\n )}\n >\n {isSelected && <span className=\"w-2.5 h-2.5 rounded-full bg-primary\" />}\n </span>\n ) : (\n <span\n data-cimplify-addon-checkbox\n className={cn(\n \"w-5 h-5 rounded-sm border-2 flex items-center justify-center shrink-0 transition-colors\",\n isSelected ? \"border-primary bg-primary\" : \"border-muted-foreground/30\",\n )}\n >\n {isSelected && (\n <svg viewBox=\"0 0 12 12\" className=\"w-3 h-3 text-primary-foreground\" fill=\"none\">\n <path d=\"M2 6l3 3 5-5\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"/>\n </svg>\n )}\n </span>\n )}\n\n <span\n data-cimplify-addon-option-name\n className={cn(\n \"flex-1 min-w-0 text-sm\",\n classNames?.optionName,\n )}\n >\n {option.name}\n </span>\n\n {option.default_price != null && (\n <span className=\"text-sm text-muted-foreground\">\n +<Price amount={option.default_price} />\n </span>\n )}\n </Checkbox.Root>\n );\n })}\n </div>\n </div>\n );\n })}\n </div>\n );\n}\n"
13
13
  }
14
14
  ]
15
15
  }
@@ -11,7 +11,7 @@
11
11
  "files": [
12
12
  {
13
13
  "path": "product-card.tsx",
14
- "content": "\"use client\";\n\nimport React, { useCallback, useRef, useState } from \"react\";\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. */\n renderModal?: (product: ProductWithDetails) => 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 native `<dialog>` modal with a ProductSheet\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 dialogRef = useRef<HTMLDialogElement>(null);\n\n // Lazy-fetch product details only when modal is open\n const { product: productDetails } = useProduct(\n product.slug ?? product.id,\n { enabled: isOpen },\n );\n\n const handleOpen = useCallback(() => {\n setIsOpen(true);\n dialogRef.current?.showModal();\n }, []);\n\n const handleClose = useCallback(() => {\n dialogRef.current?.close();\n setIsOpen(false);\n }, []);\n\n const handleCancel = useCallback(() => {\n setIsOpen(false);\n }, []);\n\n const handleBackdropClick = useCallback(\n (e: React.MouseEvent<HTMLDialogElement>) => {\n if (e.target === dialogRef.current) {\n handleClose();\n }\n },\n [handleClose],\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 button + native dialog\n return (\n <>\n <button\n type=\"button\"\n aria-haspopup=\"dialog\"\n onClick={handleOpen}\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 </button>\n\n <dialog\n ref={dialogRef}\n onCancel={handleCancel}\n onClick={handleBackdropClick}\n data-cimplify-product-card-modal\n className={cn(\n \"border-none rounded-2xl p-0 max-w-lg w-full max-h-[85vh] overflow-auto bg-background shadow-2xl backdrop:bg-black/50 backdrop:backdrop-blur-sm open:animate-in open:fade-in-0 open:slide-in-from-bottom-4\",\n classNames?.modal,\n )}\n >\n {isOpen && (\n productDetails ? (\n renderModal ? (\n renderModal(productDetails)\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\" />\n <div className=\"h-6 w-3/5 bg-muted rounded\" />\n </div>\n )\n )}\n </dialog>\n </>\n );\n}\n"
14
+ "content": "\"use client\";\n\nimport React, { useCallback, useRef, useState } from \"react\";\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 native `<dialog>` modal with a ProductSheet\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 const dialogRef = useRef<HTMLDialogElement>(null);\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 handleOpen = useCallback(() => {\n setIsOpen(true);\n setShouldFetch(true);\n dialogRef.current?.showModal();\n }, []);\n\n const handleClose = useCallback(() => {\n dialogRef.current?.close();\n setIsOpen(false);\n }, []);\n\n const handleCancel = useCallback(() => {\n setIsOpen(false);\n }, []);\n\n const handleBackdropClick = useCallback(\n (e: React.MouseEvent<HTMLDialogElement>) => {\n if (e.target === dialogRef.current) {\n handleClose();\n }\n },\n [handleClose],\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 button + native dialog\n return (\n <>\n <button\n type=\"button\"\n aria-haspopup=\"dialog\"\n onPointerEnter={handlePrefetch}\n onClick={handleOpen}\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 </button>\n\n <dialog\n ref={dialogRef}\n onCancel={handleCancel}\n onClick={handleBackdropClick}\n data-cimplify-product-card-modal\n className={cn(\n \"border-none rounded-2xl p-0 max-w-lg w-full max-h-[85vh] overflow-auto bg-background shadow-2xl backdrop:bg-black/50 backdrop:backdrop-blur-sm open:animate-in open:fade-in-0 open:slide-in-from-bottom-4\",\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\" />\n <div className=\"h-6 w-3/5 bg-muted rounded\" />\n </div>\n )\n )}\n </dialog>\n </>\n );\n}\n"
15
15
  }
16
16
  ]
17
17
  }
@@ -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 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 flex items-center gap-2\", 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"
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(className, classNames?.root)}>\n <div\n data-cimplify-variant-list-header\n className={cn(\"flex items-center justify-between py-3\", classNames?.listLabel)}\n >\n <label id={listLabelId} className=\"text-base font-bold\">\n Options\n </label>\n <span className=\"text-xs font-semibold text-destructive bg-destructive/10 px-2.5 py-1 rounded\">\n Required\n </span>\n </div>\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(\"divide-y divide-border\", classNames?.list)}\n >\n {variants.map((variant) => {\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-variant-option\n data-selected={isSelected || undefined}\n className={cn(\n \"w-full flex items-center gap-3 py-4 transition-colors cursor-pointer\",\n isSelected ? classNames?.optionSelected : classNames?.option,\n )}\n >\n <span\n data-cimplify-variant-radio\n className={cn(\n \"w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors\",\n isSelected ? \"border-primary\" : \"border-muted-foreground/30\",\n )}\n >\n {isSelected && <span className=\"w-2.5 h-2.5 rounded-full bg-primary\" />}\n </span>\n <span\n data-cimplify-variant-name\n className={cn(\"flex-1 min-w-0 text-sm\", classNames?.name)}\n >\n {getVariantDisplayName(variant, productName)}\n </span>\n <span data-cimplify-variant-pricing className={cn(\"text-sm text-muted-foreground\", classNames?.pricing)}>\n {adjustment > 0 ? \"+\" : adjustment < 0 ? \"\" : \"+\"}\n <Price amount={variant.price_adjustment} />\n </span>\n </Radio.Root>\n );\n })}\n </RadioGroup>\n </div>\n );\n}\n"
13
13
  }
14
14
  ]
15
15
  }