@cimplify/sdk 0.19.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advanced.d.mts +2 -2
- package/dist/advanced.d.ts +2 -2
- package/dist/advanced.js +21 -21
- package/dist/advanced.mjs +2 -2
- package/dist/{chunk-RYQ5IPWX.js → chunk-2BSYTQAY.js} +18 -28
- package/dist/{chunk-AFEE43MQ.mjs → chunk-676BJ3EU.mjs} +43 -8
- package/dist/{chunk-7ACYNCNM.mjs → chunk-7KH2XJIW.mjs} +55 -12
- package/dist/{chunk-B2RTVYJR.js → chunk-AVVLN353.js} +76 -40
- package/dist/{chunk-FTL2SXCG.mjs → chunk-HG7GG3TT.mjs} +18 -28
- package/dist/{chunk-6SRY4DPY.js → chunk-KSN3RJFL.js} +74 -30
- package/dist/{client-FNZzXO_v.d.ts → client-D4E9aJy7.d.mts} +139 -7
- package/dist/{client-B3payjUV.d.mts → client-VyO6Bqz2.d.ts} +139 -7
- package/dist/index.d.mts +38 -5
- package/dist/index.d.ts +38 -5
- package/dist/index.js +101 -97
- package/dist/index.mjs +3 -3
- package/dist/{payment-CO2tRThu.d.mts → payment-CKqMU4xT.d.mts} +1051 -943
- package/dist/{payment-CO2tRThu.d.ts → payment-CKqMU4xT.d.ts} +1051 -943
- package/dist/{price-Bp6JlVzW.d.ts → price-BqV_DjjQ.d.mts} +1 -1
- package/dist/{price-Dg7MJEsL.d.mts → price-e4beObLZ.d.ts} +1 -1
- package/dist/react.d.mts +463 -11
- package/dist/react.d.ts +463 -11
- package/dist/react.js +1630 -113
- package/dist/react.mjs +1566 -65
- package/dist/styles.css +1 -1
- package/dist/utils.d.mts +2 -2
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +28 -28
- package/dist/utils.mjs +1 -1
- package/package.json +7 -7
- package/registry/availability-badge.json +1 -1
- package/registry/booking-page.json +1 -1
- package/registry/bundle-selector.json +1 -1
- package/registry/cart-summary.json +1 -1
- package/registry/composite-selector.json +1 -1
- package/registry/order-summary.json +1 -1
- package/registry/product-customizer.json +1 -1
- package/registry/search-input.json +1 -1
- package/registry/service-card.json +1 -1
- package/registry/variant-selector.json +1 -1
package/dist/styles.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/*! tailwindcss v4.2.2 | 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-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--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}.z-50{z-index:50}.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-\[5\/2\]{aspect-ratio:5/2}.h-fit{height:fit-content}.max-h-\[85vh\]{max-height:85vh}.w-2\/5{width:40%}.w-3\/5{width:60%}.w-4\/5{width:80%}.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-input{border-color:var(--color-input,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-foreground{color:var(--color-foreground,oklch(15% 0 0))}.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}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.accent-primary{accent-color:var(--color-primary,oklch(50% .1 35))}.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-40:disabled{opacity:.4}.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-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@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-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--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;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}.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}.z-50{z-index:50}.container{width:100%}.-mt-px{margin-top:-1px}.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-\[5\/2\]{aspect-ratio:5/2}.h-fit{height:fit-content}.max-h-\[85vh\]{max-height:85vh}.w-2\/5{width:40%}.w-3\/5{width:60%}.w-4\/5{width:80%}.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}.resize-none{resize:none}.\[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-input{border-color:var(--color-input,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-foreground{color:var(--color-foreground,oklch(15% 0 0))}.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}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.accent-primary{accent-color:var(--color-primary,oklch(50% .1 35))}.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}.placeholder\:text-muted-foreground::placeholder{color:var(--color-muted-foreground,oklch(50% 0 0))}@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))}}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-ring:focus{--tw-ring-color:var(--color-ring,oklch(50% .1 35))}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.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-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@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}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
|
package/dist/utils.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { M as Money,
|
|
2
|
-
import { P as ProductWithPrice } from './price-
|
|
1
|
+
import { M as Money, a as CurrencyCode, aS as PricePathTaxInfo, bD as PaymentErrorDetails, bB as PaymentResponse, bC as PaymentStatusResponse } from './payment-CKqMU4xT.mjs';
|
|
2
|
+
import { P as ProductWithPrice } from './price-BqV_DjjQ.mjs';
|
|
3
3
|
import { CountryCode } from 'libphonenumber-js';
|
|
4
4
|
|
|
5
5
|
declare const CURRENCY_SYMBOLS: Record<string, string>;
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { M as Money,
|
|
2
|
-
import { P as ProductWithPrice } from './price-
|
|
1
|
+
import { M as Money, a as CurrencyCode, aS as PricePathTaxInfo, bD as PaymentErrorDetails, bB as PaymentResponse, bC as PaymentStatusResponse } from './payment-CKqMU4xT.js';
|
|
2
|
+
import { P as ProductWithPrice } from './price-e4beObLZ.js';
|
|
3
3
|
import { CountryCode } from 'libphonenumber-js';
|
|
4
4
|
|
|
5
5
|
declare const CURRENCY_SYMBOLS: Record<string, string>;
|
package/dist/utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chunkS6TU55KP_js = require('./chunk-S6TU55KP.js');
|
|
4
|
-
var
|
|
4
|
+
var chunk2BSYTQAY_js = require('./chunk-2BSYTQAY.js');
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
@@ -19,109 +19,109 @@ Object.defineProperty(exports, "resolvePhoneCountryCode", {
|
|
|
19
19
|
});
|
|
20
20
|
Object.defineProperty(exports, "CURRENCY_SYMBOLS", {
|
|
21
21
|
enumerable: true,
|
|
22
|
-
get: function () { return
|
|
22
|
+
get: function () { return chunk2BSYTQAY_js.CURRENCY_SYMBOLS; }
|
|
23
23
|
});
|
|
24
24
|
Object.defineProperty(exports, "MOBILE_MONEY_PROVIDERS", {
|
|
25
25
|
enumerable: true,
|
|
26
|
-
get: function () { return
|
|
26
|
+
get: function () { return chunk2BSYTQAY_js.MOBILE_MONEY_PROVIDERS; }
|
|
27
27
|
});
|
|
28
28
|
Object.defineProperty(exports, "categorizePaymentError", {
|
|
29
29
|
enumerable: true,
|
|
30
|
-
get: function () { return
|
|
30
|
+
get: function () { return chunk2BSYTQAY_js.categorizePaymentError; }
|
|
31
31
|
});
|
|
32
32
|
Object.defineProperty(exports, "detectMobileMoneyProvider", {
|
|
33
33
|
enumerable: true,
|
|
34
|
-
get: function () { return
|
|
34
|
+
get: function () { return chunk2BSYTQAY_js.detectMobileMoneyProvider; }
|
|
35
35
|
});
|
|
36
36
|
Object.defineProperty(exports, "formatMoney", {
|
|
37
37
|
enumerable: true,
|
|
38
|
-
get: function () { return
|
|
38
|
+
get: function () { return chunk2BSYTQAY_js.formatMoney; }
|
|
39
39
|
});
|
|
40
40
|
Object.defineProperty(exports, "formatNumberCompact", {
|
|
41
41
|
enumerable: true,
|
|
42
|
-
get: function () { return
|
|
42
|
+
get: function () { return chunk2BSYTQAY_js.formatNumberCompact; }
|
|
43
43
|
});
|
|
44
44
|
Object.defineProperty(exports, "formatPrice", {
|
|
45
45
|
enumerable: true,
|
|
46
|
-
get: function () { return
|
|
46
|
+
get: function () { return chunk2BSYTQAY_js.formatPrice; }
|
|
47
47
|
});
|
|
48
48
|
Object.defineProperty(exports, "formatPriceAdjustment", {
|
|
49
49
|
enumerable: true,
|
|
50
|
-
get: function () { return
|
|
50
|
+
get: function () { return chunk2BSYTQAY_js.formatPriceAdjustment; }
|
|
51
51
|
});
|
|
52
52
|
Object.defineProperty(exports, "formatPriceCompact", {
|
|
53
53
|
enumerable: true,
|
|
54
|
-
get: function () { return
|
|
54
|
+
get: function () { return chunk2BSYTQAY_js.formatPriceCompact; }
|
|
55
55
|
});
|
|
56
56
|
Object.defineProperty(exports, "formatPriceWithTax", {
|
|
57
57
|
enumerable: true,
|
|
58
|
-
get: function () { return
|
|
58
|
+
get: function () { return chunk2BSYTQAY_js.formatPriceWithTax; }
|
|
59
59
|
});
|
|
60
60
|
Object.defineProperty(exports, "formatProductPrice", {
|
|
61
61
|
enumerable: true,
|
|
62
|
-
get: function () { return
|
|
62
|
+
get: function () { return chunk2BSYTQAY_js.formatProductPrice; }
|
|
63
63
|
});
|
|
64
64
|
Object.defineProperty(exports, "getBasePrice", {
|
|
65
65
|
enumerable: true,
|
|
66
|
-
get: function () { return
|
|
66
|
+
get: function () { return chunk2BSYTQAY_js.getBasePrice; }
|
|
67
67
|
});
|
|
68
68
|
Object.defineProperty(exports, "getCurrencySymbol", {
|
|
69
69
|
enumerable: true,
|
|
70
|
-
get: function () { return
|
|
70
|
+
get: function () { return chunk2BSYTQAY_js.getCurrencySymbol; }
|
|
71
71
|
});
|
|
72
72
|
Object.defineProperty(exports, "getDiscountPercentage", {
|
|
73
73
|
enumerable: true,
|
|
74
|
-
get: function () { return
|
|
74
|
+
get: function () { return chunk2BSYTQAY_js.getDiscountPercentage; }
|
|
75
75
|
});
|
|
76
76
|
Object.defineProperty(exports, "getDisplayPrice", {
|
|
77
77
|
enumerable: true,
|
|
78
|
-
get: function () { return
|
|
78
|
+
get: function () { return chunk2BSYTQAY_js.getDisplayPrice; }
|
|
79
79
|
});
|
|
80
80
|
Object.defineProperty(exports, "getMarkupPercentage", {
|
|
81
81
|
enumerable: true,
|
|
82
|
-
get: function () { return
|
|
82
|
+
get: function () { return chunk2BSYTQAY_js.getMarkupPercentage; }
|
|
83
83
|
});
|
|
84
84
|
Object.defineProperty(exports, "getProductCurrency", {
|
|
85
85
|
enumerable: true,
|
|
86
|
-
get: function () { return
|
|
86
|
+
get: function () { return chunk2BSYTQAY_js.getProductCurrency; }
|
|
87
87
|
});
|
|
88
88
|
Object.defineProperty(exports, "getTaxAmount", {
|
|
89
89
|
enumerable: true,
|
|
90
|
-
get: function () { return
|
|
90
|
+
get: function () { return chunk2BSYTQAY_js.getTaxAmount; }
|
|
91
91
|
});
|
|
92
92
|
Object.defineProperty(exports, "hasTaxInfo", {
|
|
93
93
|
enumerable: true,
|
|
94
|
-
get: function () { return
|
|
94
|
+
get: function () { return chunk2BSYTQAY_js.hasTaxInfo; }
|
|
95
95
|
});
|
|
96
96
|
Object.defineProperty(exports, "isOnSale", {
|
|
97
97
|
enumerable: true,
|
|
98
|
-
get: function () { return
|
|
98
|
+
get: function () { return chunk2BSYTQAY_js.isOnSale; }
|
|
99
99
|
});
|
|
100
100
|
Object.defineProperty(exports, "isPaymentStatusFailure", {
|
|
101
101
|
enumerable: true,
|
|
102
|
-
get: function () { return
|
|
102
|
+
get: function () { return chunk2BSYTQAY_js.isPaymentStatusFailure; }
|
|
103
103
|
});
|
|
104
104
|
Object.defineProperty(exports, "isPaymentStatusRequiresAction", {
|
|
105
105
|
enumerable: true,
|
|
106
|
-
get: function () { return
|
|
106
|
+
get: function () { return chunk2BSYTQAY_js.isPaymentStatusRequiresAction; }
|
|
107
107
|
});
|
|
108
108
|
Object.defineProperty(exports, "isPaymentStatusSuccess", {
|
|
109
109
|
enumerable: true,
|
|
110
|
-
get: function () { return
|
|
110
|
+
get: function () { return chunk2BSYTQAY_js.isPaymentStatusSuccess; }
|
|
111
111
|
});
|
|
112
112
|
Object.defineProperty(exports, "isTaxInclusive", {
|
|
113
113
|
enumerable: true,
|
|
114
|
-
get: function () { return
|
|
114
|
+
get: function () { return chunk2BSYTQAY_js.isTaxInclusive; }
|
|
115
115
|
});
|
|
116
116
|
Object.defineProperty(exports, "normalizePaymentResponse", {
|
|
117
117
|
enumerable: true,
|
|
118
|
-
get: function () { return
|
|
118
|
+
get: function () { return chunk2BSYTQAY_js.normalizePaymentResponse; }
|
|
119
119
|
});
|
|
120
120
|
Object.defineProperty(exports, "normalizeStatusResponse", {
|
|
121
121
|
enumerable: true,
|
|
122
|
-
get: function () { return
|
|
122
|
+
get: function () { return chunk2BSYTQAY_js.normalizeStatusResponse; }
|
|
123
123
|
});
|
|
124
124
|
Object.defineProperty(exports, "parsePrice", {
|
|
125
125
|
enumerable: true,
|
|
126
|
-
get: function () { return
|
|
126
|
+
get: function () { return chunk2BSYTQAY_js.parsePrice; }
|
|
127
127
|
});
|
package/dist/utils.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { isValidPhoneForContext, normalizePhoneToE164, resolvePhoneCountryCode } from './chunk-WDIMISRC.mjs';
|
|
2
|
-
export { CURRENCY_SYMBOLS, MOBILE_MONEY_PROVIDERS, categorizePaymentError, detectMobileMoneyProvider, formatMoney, formatNumberCompact, formatPrice, formatPriceAdjustment, formatPriceCompact, formatPriceWithTax, formatProductPrice, getBasePrice, getCurrencySymbol, getDiscountPercentage, getDisplayPrice, getMarkupPercentage, getProductCurrency, getTaxAmount, hasTaxInfo, isOnSale, isPaymentStatusFailure, isPaymentStatusRequiresAction, isPaymentStatusSuccess, isTaxInclusive, normalizePaymentResponse, normalizeStatusResponse, parsePrice } from './chunk-
|
|
2
|
+
export { CURRENCY_SYMBOLS, MOBILE_MONEY_PROVIDERS, categorizePaymentError, detectMobileMoneyProvider, formatMoney, formatNumberCompact, formatPrice, formatPriceAdjustment, formatPriceCompact, formatPriceWithTax, formatProductPrice, getBasePrice, getCurrencySymbol, getDiscountPercentage, getDisplayPrice, getMarkupPercentage, getProductCurrency, getTaxAmount, hasTaxInfo, isOnSale, isPaymentStatusFailure, isPaymentStatusRequiresAction, isPaymentStatusSuccess, isTaxInclusive, normalizePaymentResponse, normalizeStatusResponse, parsePrice } from './chunk-HG7GG3TT.mjs';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cimplify/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "Cimplify Commerce SDK for storefronts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cimplify",
|
|
@@ -72,21 +72,21 @@
|
|
|
72
72
|
"@tailwindcss/cli": "4.2.2",
|
|
73
73
|
"@testing-library/dom": "^10.4.1",
|
|
74
74
|
"@testing-library/react": "^16.3.2",
|
|
75
|
-
"@types/node": "^25.5.
|
|
76
|
-
"@types/react": "^19.2.
|
|
77
|
-
"@typescript/native-preview": "^7.0.0-dev.
|
|
75
|
+
"@types/node": "^25.5.2",
|
|
76
|
+
"@types/react": "^19.2.14",
|
|
77
|
+
"@typescript/native-preview": "^7.0.0-dev.20260406.1",
|
|
78
78
|
"jsdom": "^29.0.1",
|
|
79
79
|
"react": "^19.2.4",
|
|
80
80
|
"react-dom": "^19.2.4",
|
|
81
81
|
"tailwindcss": "4.2.2",
|
|
82
82
|
"tsup": "^8.5.1",
|
|
83
83
|
"typescript": "5.9.3",
|
|
84
|
-
"vitest": "^4.1.
|
|
84
|
+
"vitest": "^4.1.2"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
87
|
"@base-ui/react": "^1.3.0",
|
|
88
88
|
"clsx": "^2.1.1",
|
|
89
|
-
"libphonenumber-js": "1.12.
|
|
89
|
+
"libphonenumber-js": "1.12.41",
|
|
90
90
|
"tailwind-merge": "^3.5.0"
|
|
91
91
|
}
|
|
92
|
-
}
|
|
92
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "availability-badge.tsx",
|
|
12
|
-
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface AvailabilityBadgeClassNames {\n root?: string;\n dot?: string;\n label?: string;\n}\n\nexport interface AvailabilityBadgeProps {\n /** The product to check availability for. */\n product: Product;\n /** Override availability (e.g. from location_availability). */\n isAvailable?: boolean;\n /** Override stock status (e.g. from location_availability). */\n isInStock?: boolean;\n className?: string;\n classNames?: AvailabilityBadgeClassNames;\n}\n\n/**\n * AvailabilityBadge — displays in-stock / out-of-stock status for tracked products.\n *\n * Returns `null` for products that don't have inventory tracking enabled,\n * since there's no meaningful stock state to show.\n */\nexport function AvailabilityBadge({\n product,\n isAvailable,\n isInStock,\n className,\n classNames,\n}: AvailabilityBadgeProps): React.ReactElement | null {\n if (product.is_tracked !== true) {\n return null;\n }\n\n const outOfStock
|
|
12
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface AvailabilityBadgeClassNames {\n root?: string;\n dot?: string;\n label?: string;\n lowStock?: string;\n stockCount?: string;\n}\n\nexport interface AvailabilityBadgeProps {\n /** The product to check availability for. */\n product: Product;\n /** Override availability (e.g. from location_availability). */\n isAvailable?: boolean;\n /** Override stock status (e.g. from location_availability). */\n isInStock?: boolean;\n /** Current stock quantity. When provided, enables \"Only X left\" display. */\n stockQuantity?: number;\n /** Threshold at which stock is considered low. Default: 5. */\n lowStockThreshold?: number;\n /** Location context for availability checks. Passed through as a data attribute. */\n locationId?: string;\n className?: string;\n classNames?: AvailabilityBadgeClassNames;\n}\n\n/**\n * AvailabilityBadge — displays in-stock / out-of-stock status for tracked products.\n *\n * Returns `null` for products that don't have inventory tracking enabled,\n * since there's no meaningful stock state to show.\n */\nexport function AvailabilityBadge({\n product,\n isAvailable,\n isInStock,\n stockQuantity,\n lowStockThreshold = 5,\n locationId,\n className,\n classNames,\n}: AvailabilityBadgeProps): React.ReactElement | null {\n if (product.is_tracked !== true) {\n return null;\n }\n\n const outOfStock =\n isInStock === false ||\n isAvailable === false ||\n (stockQuantity !== undefined && stockQuantity <= 0);\n const isLowStock =\n !outOfStock &&\n stockQuantity !== undefined &&\n stockQuantity > 0 &&\n stockQuantity <= lowStockThreshold;\n\n const stockState = outOfStock\n ? \"out_of_stock\"\n : isLowStock\n ? \"low_stock\"\n : \"in_stock\";\n const label = outOfStock ? \"Out of Stock\" : \"In Stock\";\n\n return (\n <span\n data-cimplify-availability-badge\n data-stock-state={stockState}\n {...(locationId ? { \"data-location-id\": locationId } : undefined)}\n className={cn(className, classNames?.root, isLowStock && classNames?.lowStock)}\n style={{ display: \"inline-flex\", alignItems: \"center\", gap: \"0.375rem\" }}\n >\n <span\n data-cimplify-availability-dot\n className={classNames?.dot}\n style={{\n display: \"inline-block\",\n width: \"0.5rem\",\n height: \"0.5rem\",\n borderRadius: \"9999px\",\n }}\n />\n <span data-cimplify-availability-label className={classNames?.label}>\n {label}\n </span>\n {stockQuantity !== undefined && isLowStock && (\n <span data-cimplify-availability-stock-count className={classNames?.stockCount}>\n Only {stockQuantity} left\n </span>\n )}\n </span>\n );\n}\n"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
{
|
|
14
14
|
"path": "booking-page.tsx",
|
|
15
|
-
"content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport type { Product } from \"@cimplify/sdk\";\nimport type { Service, Staff, AvailableSlot } from \"@cimplify/sdk\";\nimport { useCart } from \"@cimplify/sdk/react\";\nimport { DateSlotPicker } from \"@cimplify/sdk/react\";\nimport { StaffPicker } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface BookingPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n serviceInfo?: string;\n step?: string;\n stepTitle?: string;\n summary?: string;\n summaryRow?: string;\n confirmButton?: string;\n backButton?: string;\n error?: string;\n}\n\nexport interface BookingPageProps {\n /** The service being booked. */\n service: Service;\n /** Optional staff list for staff selection step. */\n staff?: Staff[];\n /** Number of participants. */\n participantCount?: number;\n /** Page title. */\n title?: string;\n /** Called after successfully adding to cart. */\n onBooked?: (slot: AvailableSlot, staffId: string | null) => void;\n /** Called when user wants to go back. */\n onBack?: () => void;\n /** Show price on slots. Default: true. */\n showPrice?: boolean;\n className?: string;\n classNames?: BookingPageClassNames;\n}\n\ntype BookingStep = \"select-slot\" | \"select-staff\" | \"confirm\";\n\nexport function BookingPage({\n service,\n staff,\n participantCount,\n title,\n onBooked,\n onBack,\n showPrice = true,\n className,\n classNames,\n}: BookingPageProps): React.ReactElement {\n const { addItem } = useCart();\n\n const [step, setStep] = useState<BookingStep>(\"select-slot\");\n const [selectedSlot, setSelectedSlot] = useState<AvailableSlot | null>(null);\n const [selectedDate, setSelectedDate] = useState<string | null>(null);\n const [selectedStaffId, setSelectedStaffId] = useState<string | null>(null);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const hasStaffStep = staff && staff.length > 0;\n\n const handleSlotSelect = useCallback(\n (slot: AvailableSlot, date: string) => {\n setSelectedSlot(slot);\n setSelectedDate(date);\n setError(null);\n if (hasStaffStep) {\n setStep(\"select-staff\");\n } else {\n setStep(\"confirm\");\n }\n },\n [hasStaffStep],\n );\n\n const handleStaffSelect = useCallback((staffId: string | null) => {\n setSelectedStaffId(staffId);\n setStep(\"confirm\");\n }, []);\n\n const handleBack = useCallback(() => {\n setError(null);\n if (step === \"confirm\" && hasStaffStep) {\n setStep(\"select-staff\");\n } else if (step === \"confirm\" || step === \"select-staff\") {\n setStep(\"select-slot\");\n } else {\n onBack?.();\n }\n }, [step, hasStaffStep, onBack]);\n\n const handleConfirm = useCallback(async () => {\n if (!selectedSlot || !selectedDate) return;\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n const serviceProduct = {\n id: service.product_id || service.id,\n business_id: service.business_id || \"\",\n category_id: service.category_id || undefined,\n name: service.name,\n slug: service.id,\n description: service.description || undefined,\n image_url: service.image_url || undefined,\n default_price: (service.price || \"0\") as Product[\"default_price\"],\n type: \"service\" as const,\n inventory_type: \"none\" as const,\n variant_strategy: \"fetch_all\" as const,\n is_active: service.is_available,\n created_at: new Date().toISOString(),\n updated_at: new Date().toISOString(),\n };\n\n await addItem(serviceProduct, 1, {\n scheduledStart: selectedSlot.start_time,\n scheduledEnd: selectedSlot.end_time,\n staffId: selectedStaffId || undefined,\n });\n\n onBooked?.(selectedSlot, selectedStaffId);\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to add booking to cart\");\n } finally {\n setIsSubmitting(false);\n }\n }, [selectedSlot, selectedDate, selectedStaffId, service, addItem, onBooked]);\n\n return (\n <div data-cimplify-booking-page className={cn(className, classNames?.root)}>\n <div data-cimplify-booking-page-header className={classNames?.header}>\n {onBack && step === \"select-slot\" && (\n <button\n type=\"button\"\n onClick={onBack}\n data-cimplify-booking-page-back\n className={classNames?.backButton}\n >\n Back\n </button>\n )}\n {step !== \"select-slot\" && (\n <button\n type=\"button\"\n onClick={handleBack}\n data-cimplify-booking-page-back\n className={classNames?.backButton}\n >\n Back\n </button>\n )}\n <h1 data-cimplify-booking-page-title className={classNames?.title}>\n {title || `Book ${service.name}`}\n </h1>\n </div>\n\n <div data-cimplify-booking-service-info className={classNames?.serviceInfo}>\n <span data-cimplify-booking-service-name>{service.name}</span>\n <span data-cimplify-booking-service-duration>{service.duration_minutes} min</span>\n {service.price && (\n <span data-cimplify-booking-service-price>\n <Price amount={service.price} />\n </span>\n )}\n </div>\n\n {error && (\n <div data-cimplify-booking-error className={classNames?.error}>\n {error}\n </div>\n )}\n\n {step === \"select-slot\" && (\n <div data-cimplify-booking-step=\"select-slot\" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Select a date & time\n </h2>\n <DateSlotPicker\n serviceId={service.id}\n participantCount={participantCount}\n selectedSlot={selectedSlot}\n onSlotSelect={handleSlotSelect}\n showPrice={showPrice}\n />\n </div>\n )}\n\n {step === \"select-staff\" && staff && (\n <div data-cimplify-booking-step=\"select-staff\" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Choose a provider\n </h2>\n <StaffPicker\n staff={staff}\n selectedStaffId={selectedStaffId}\n onStaffSelect={handleStaffSelect}\n />\n </div>\n )}\n\n {step === \"confirm\" && selectedSlot && (\n <div data-cimplify-booking-step=\"confirm\" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Confirm your booking\n </h2>\n <div data-cimplify-booking-summary className={classNames?.summary}>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Service</span>\n <span>{service.name}</span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Date</span>\n <span>{selectedDate}</span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Time</span>\n <span>\n {new Date(selectedSlot.start_time).toLocaleTimeString(undefined, {\n hour: \"numeric\",\n minute: \"2-digit\",\n })}\n </span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Duration</span>\n <span>{service.duration_minutes} min</span>\n </div>\n {selectedStaffId && staff && (\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Provider</span>\n <span>{staff.find((s) => s.id === selectedStaffId)?.name ?? \"Selected\"}</span>\n </div>\n )}\n {(selectedSlot.price || service.price) && (\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Price</span>\n <span>\n <Price amount={selectedSlot.price || service.price!} />\n </span>\n </div>\n )}\n </div>\n <button\n type=\"button\"\n onClick={handleConfirm}\n disabled={isSubmitting}\n data-cimplify-booking-confirm\n className={classNames?.confirmButton}\n >\n {isSubmitting ? \"Adding to cart…\" : \"Add to cart\"}\n </button>\n </div>\n )}\n </div>\n );\n}\n"
|
|
15
|
+
"content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport type { Product } from \"@cimplify/sdk\";\nimport type { Service, Staff, AvailableSlot, CustomerBooking } from \"@cimplify/sdk\";\nimport { useCart } from \"@cimplify/sdk/react\";\nimport { useCimplifyClient } from \"@cimplify/sdk/react\";\nimport { DateSlotPicker } from \"@cimplify/sdk/react\";\nimport { StaffPicker } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\nimport { formatDuration } from \"./utils/format-duration\";\nimport { parsePrice } from \"@cimplify/sdk\";\n\nexport interface BookingPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n serviceInfo?: string;\n step?: string;\n stepTitle?: string;\n summary?: string;\n summaryRow?: string;\n confirmButton?: string;\n backButton?: string;\n error?: string;\n rescheduleInfo?: string;\n depositInfo?: string;\n cancellationPolicy?: string;\n}\n\nexport interface BookingPageProps {\n /** The service being booked. */\n service: Service;\n /** The underlying product (for deposit/cancellation fields). Falls back to service fields. */\n product?: Product;\n /** Optional staff list for staff selection step. */\n staff?: Staff[];\n /** Number of participants. */\n participantCount?: number;\n /** Page title. */\n title?: string;\n /** Called after successfully adding to cart. */\n onBooked?: (slot: AvailableSlot, staffId: string | null) => void;\n /** Called when user wants to go back. */\n onBack?: () => void;\n /** Existing booking for reschedule mode. */\n existingBooking?: CustomerBooking;\n /** Called after a successful reschedule. */\n onRescheduled?: (booking: CustomerBooking, newSlot: AvailableSlot) => void;\n /** Show price on slots. Default: true. */\n showPrice?: boolean;\n className?: string;\n classNames?: BookingPageClassNames;\n}\n\ntype BookingStep = \"select-slot\" | \"select-staff\" | \"confirm\";\n\nexport function BookingPage({\n service,\n product,\n staff,\n participantCount,\n title,\n onBooked,\n onBack,\n existingBooking,\n onRescheduled,\n showPrice = true,\n className,\n classNames,\n}: BookingPageProps): React.ReactElement {\n const { addItem } = useCart();\n const { client } = useCimplifyClient();\n\n const isReschedule = existingBooking !== undefined;\n\n const [step, setStep] = useState<BookingStep>(\"select-slot\");\n const [selectedSlot, setSelectedSlot] = useState<AvailableSlot | null>(null);\n const [selectedDate, setSelectedDate] = useState<string | null>(null);\n const [selectedStaffId, setSelectedStaffId] = useState<string | null>(null);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const hasStaffStep = staff && staff.length > 0;\n\n // Deposit info — prefer product fields, then fall back to service metadata\n const depositType = product?.deposit_type;\n const depositAmount = product?.deposit_amount;\n const hasDeposit = depositType !== undefined && depositType !== \"none\" && depositAmount !== undefined && parsePrice(depositAmount) !== 0;\n\n // Cancellation policy\n const cancellationMinutes = product?.cancellation_window_minutes;\n const noShowFee = product?.no_show_fee;\n\n const handleSlotSelect = useCallback(\n (slot: AvailableSlot, date: string) => {\n setSelectedSlot(slot);\n setSelectedDate(date);\n setError(null);\n if (hasStaffStep) {\n setStep(\"select-staff\");\n } else {\n setStep(\"confirm\");\n }\n },\n [hasStaffStep],\n );\n\n const handleStaffSelect = useCallback((staffId: string | null) => {\n setSelectedStaffId(staffId);\n setStep(\"confirm\");\n }, []);\n\n const handleBack = useCallback(() => {\n setError(null);\n if (step === \"confirm\" && hasStaffStep) {\n setStep(\"select-staff\");\n } else if (step === \"confirm\" || step === \"select-staff\") {\n setStep(\"select-slot\");\n } else {\n onBack?.();\n }\n }, [step, hasStaffStep, onBack]);\n\n const handleConfirm = useCallback(async () => {\n if (!selectedSlot || !selectedDate) return;\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n if (isReschedule && existingBooking) {\n // Reschedule mode — call the scheduling API instead of adding to cart\n const serviceItem = existingBooking.service_items[0];\n const result = await client.scheduling.rescheduleBooking({\n order_id: existingBooking.order_id,\n line_item_id: serviceItem?.service_id ?? existingBooking.order_id,\n new_start_time: selectedSlot.start_time,\n new_end_time: selectedSlot.end_time,\n new_staff_id: selectedStaffId || undefined,\n reschedule_type: \"customer\",\n });\n\n if (!result.ok) {\n throw result.error;\n }\n\n onRescheduled?.(existingBooking, selectedSlot);\n } else {\n // Normal booking mode — add to cart\n const serviceProduct = {\n id: service.product_id || service.id,\n business_id: service.business_id || \"\",\n category_id: service.category_id || undefined,\n name: service.name,\n slug: service.id,\n description: service.description || undefined,\n image_url: service.image_url || undefined,\n default_price: (service.price || \"0\") as Product[\"default_price\"],\n type: \"service\" as const,\n inventory_type: \"none\" as const,\n variant_strategy: \"fetch_all\" as const,\n is_active: service.is_available,\n created_at: new Date().toISOString(),\n updated_at: new Date().toISOString(),\n };\n\n await addItem(serviceProduct, 1, {\n scheduledStart: selectedSlot.start_time,\n scheduledEnd: selectedSlot.end_time,\n staffId: selectedStaffId || undefined,\n });\n\n onBooked?.(selectedSlot, selectedStaffId);\n }\n } catch (err) {\n const fallbackMessage = isReschedule ? \"Failed to reschedule booking\" : \"Failed to add booking to cart\";\n setError(err instanceof Error ? err.message : fallbackMessage);\n } finally {\n setIsSubmitting(false);\n }\n }, [selectedSlot, selectedDate, selectedStaffId, service, addItem, onBooked, isReschedule, existingBooking, client, onRescheduled]);\n\n return (\n <div data-cimplify-booking-page className={cn(className, classNames?.root)}>\n <div data-cimplify-booking-page-header className={classNames?.header}>\n {onBack && step === \"select-slot\" && (\n <button\n type=\"button\"\n onClick={onBack}\n data-cimplify-booking-page-back\n className={classNames?.backButton}\n >\n Back\n </button>\n )}\n {step !== \"select-slot\" && (\n <button\n type=\"button\"\n onClick={handleBack}\n data-cimplify-booking-page-back\n className={classNames?.backButton}\n >\n Back\n </button>\n )}\n <h1 data-cimplify-booking-page-title className={classNames?.title}>\n {title || (isReschedule ? \"Reschedule Booking\" : `Book ${service.name}`)}\n </h1>\n </div>\n\n {isReschedule && existingBooking && (\n <div data-cimplify-booking-reschedule-info className={classNames?.rescheduleInfo}>\n <span>Rescheduling booking from {new Date(existingBooking.service_items[0]?.scheduled_start ?? existingBooking.created_at).toLocaleDateString()}</span>\n </div>\n )}\n\n <div data-cimplify-booking-service-info className={classNames?.serviceInfo}>\n <span data-cimplify-booking-service-name>{service.name}</span>\n <span data-cimplify-booking-service-duration>{formatDuration(service.duration_minutes, service.duration_unit)}</span>\n {service.price && (\n <span data-cimplify-booking-service-price>\n <Price amount={service.price} />\n </span>\n )}\n {hasDeposit && (\n <span data-cimplify-booking-deposit-info className={classNames?.depositInfo}>\n Deposit: {depositType === \"percentage\" ? `${parsePrice(depositAmount!)}%` : <Price amount={depositAmount!} />}\n </span>\n )}\n {cancellationMinutes !== undefined && cancellationMinutes > 0 && (\n <span data-cimplify-booking-cancellation-policy className={classNames?.cancellationPolicy}>\n Free cancellation up to {cancellationMinutes >= 60 ? `${Math.floor(cancellationMinutes / 60)} hour${Math.floor(cancellationMinutes / 60) !== 1 ? \"s\" : \"\"}` : `${cancellationMinutes} minute${cancellationMinutes !== 1 ? \"s\" : \"\"}`} before\n </span>\n )}\n {noShowFee !== undefined && parsePrice(noShowFee) !== 0 && (\n <span data-cimplify-booking-no-show-fee className={classNames?.cancellationPolicy}>\n No-show fee: <Price amount={noShowFee} />\n </span>\n )}\n </div>\n\n {error && (\n <div data-cimplify-booking-error className={classNames?.error}>\n {error}\n </div>\n )}\n\n {step === \"select-slot\" && (\n <div data-cimplify-booking-step=\"select-slot\" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Select a date & time\n </h2>\n <DateSlotPicker\n serviceId={service.id}\n participantCount={participantCount}\n selectedSlot={selectedSlot}\n onSlotSelect={handleSlotSelect}\n showPrice={showPrice}\n />\n </div>\n )}\n\n {step === \"select-staff\" && staff && (\n <div data-cimplify-booking-step=\"select-staff\" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Choose a provider\n </h2>\n <StaffPicker\n staff={staff}\n selectedStaffId={selectedStaffId}\n onStaffSelect={handleStaffSelect}\n />\n </div>\n )}\n\n {step === \"confirm\" && selectedSlot && (\n <div data-cimplify-booking-step=\"confirm\" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Confirm your booking\n </h2>\n <div data-cimplify-booking-summary className={classNames?.summary}>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Service</span>\n <span>{service.name}</span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Date</span>\n <span>{selectedDate}</span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Time</span>\n <span>\n {new Date(selectedSlot.start_time).toLocaleTimeString(undefined, {\n hour: \"numeric\",\n minute: \"2-digit\",\n })}\n </span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Duration</span>\n <span>{formatDuration(service.duration_minutes, service.duration_unit)}</span>\n </div>\n {selectedStaffId && staff && (\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Provider</span>\n <span>{staff.find((s) => s.id === selectedStaffId)?.name ?? \"Selected\"}</span>\n </div>\n )}\n {(selectedSlot.price || service.price) && (\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Price</span>\n <span>\n <Price amount={selectedSlot.price || service.price!} />\n </span>\n </div>\n )}\n {hasDeposit && (\n <div data-cimplify-booking-summary-row className={cn(classNames?.summaryRow, classNames?.depositInfo)}>\n <span>Deposit</span>\n <span>\n {depositType === \"percentage\" ? `${parsePrice(depositAmount!)}%` : <Price amount={depositAmount!} />}\n </span>\n </div>\n )}\n </div>\n <button\n type=\"button\"\n onClick={handleConfirm}\n disabled={isSubmitting}\n data-cimplify-booking-confirm\n className={classNames?.confirmButton}\n >\n {isSubmitting\n ? (isReschedule ? \"Rescheduling…\" : \"Adding to cart…\")\n : (isReschedule ? \"Reschedule\" : \"Confirm Booking\")}\n </button>\n </div>\n )}\n </div>\n );\n}\n"
|
|
16
16
|
}
|
|
17
17
|
]
|
|
18
18
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "bundle-selector.tsx",
|
|
12
|
-
"content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect, useRef, useId } from \"react\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport { Radio } from \"@base-ui/react/radio\";\nimport type { BundleComponentView, BundleComponentVariantView, BundlePriceType } from \"@cimplify/sdk\";\nimport type { Money } from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface BundleSelectorClassNames {\n root?: string;\n heading?: string;\n components?: string;\n component?: string;\n componentHeader?: string;\n componentQty?: string;\n componentName?: string;\n variantPicker?: string;\n variantOption?: string;\n variantOptionSelected?: string;\n variantAdjustment?: string;\n summary?: string;\n savings?: string;\n}\n\nexport interface BundleSelectorProps {\n components: BundleComponentView[];\n bundlePrice?: Money;\n discountValue?: Money;\n pricingType?: BundlePriceType;\n onSelectionsChange: (selections: BundleSelectionInput[]) => void;\n onPriceChange?: (price: number) => void;\n onReady?: (ready: boolean) => void;\n className?: string;\n classNames?: BundleSelectorClassNames;\n}\n\nexport function BundleSelector({\n components,\n bundlePrice,\n discountValue,\n pricingType,\n onSelectionsChange,\n onPriceChange,\n onReady,\n className,\n classNames,\n}: BundleSelectorProps): React.ReactElement | null {\n const [variantChoices, setVariantChoices] = useState<Record<string, string>>({});\n const lastComponentIds = useRef(\"\");\n\n useEffect(() => {\n const ids = components.map((c) => c.id).sort().join();\n if (ids === lastComponentIds.current) return;\n lastComponentIds.current = ids;\n\n const defaults: Record<string, string> = {};\n for (const comp of components) {\n if (comp.variant_id) {\n defaults[comp.id] = comp.variant_id;\n } else if (comp.available_variants.length > 0) {\n const defaultVariant =\n comp.available_variants.find((v) => v.is_default) || comp.available_variants[0];\n if (defaultVariant) {\n defaults[comp.id] = defaultVariant.id;\n }\n }\n }\n setVariantChoices(defaults);\n }, [components]);\n\n const selections = useMemo((): BundleSelectionInput[] => {\n return components.map((comp) => ({\n component_id: comp.id,\n variant_id: variantChoices[comp.id],\n quantity: comp.quantity,\n }));\n }, [components, variantChoices]);\n\n useEffect(() => {\n onSelectionsChange(selections);\n }, [selections, onSelectionsChange]);\n\n useEffect(() => {\n onReady?.(components.length > 0 && selections.length > 0);\n }, [components, selections, onReady]);\n\n const totalPrice = useMemo(() => {\n if (pricingType === \"fixed\" && bundlePrice) {\n return parsePrice(bundlePrice);\n }\n const componentsTotal = components.reduce((sum, comp) => {\n return sum + getComponentPrice(comp, variantChoices[comp.id]) * comp.quantity;\n }, 0);\n if (pricingType === \"percentage_discount\" && discountValue) {\n return componentsTotal * (1 - parsePrice(discountValue) / 100);\n }\n if (pricingType === \"fixed_discount\" && discountValue) {\n return componentsTotal - parsePrice(discountValue);\n }\n return componentsTotal;\n }, [components, variantChoices, pricingType, bundlePrice, discountValue]);\n\n useEffect(() => {\n onPriceChange?.(totalPrice);\n }, [totalPrice, onPriceChange]);\n\n const handleVariantChange = useCallback(\n (componentId: string, variantId: string) => {\n setVariantChoices((prev) => ({ ...prev, [componentId]: variantId }));\n },\n [],\n );\n\n if (components.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-bundle-selector className={cn(\"space-y-4\", className, classNames?.root)}>\n <div\n data-cimplify-bundle-heading\n className={cn(\"flex items-center justify-between py-3\", classNames?.heading)}\n >\n <span className=\"text-base font-bold\">Included in this bundle</span>\n </div>\n\n <div data-cimplify-bundle-components className={cn(\"divide-y divide-border\", classNames?.components)}>\n {components.map((comp) => (\n <BundleComponentCard\n key={comp.id}\n component={comp}\n selectedVariantId={variantChoices[comp.id]}\n onVariantChange={(variantId) =>\n handleVariantChange(comp.id, variantId)\n }\n classNames={classNames}\n />\n ))}\n </div>\n\n {bundlePrice && (\n <div\n data-cimplify-bundle-summary\n className={cn(\"border-t border-border pt-4 flex justify-between text-sm\", classNames?.summary)}\n >\n <span className=\"text-muted-foreground\">Bundle price</span>\n <Price amount={bundlePrice} className=\"font-medium text-primary\" />\n </div>\n )}\n {discountValue && (\n <div\n data-cimplify-bundle-savings\n className={cn(\"flex justify-between text-sm\", classNames?.savings)}\n >\n <span className=\"text-muted-foreground\">You save</span>\n <Price amount={discountValue} className=\"text-green-600 font-medium\" />\n </div>\n )}\n </div>\n );\n}\n\nfunction getComponentPrice(\n component: BundleComponentView,\n selectedVariantId: string | undefined,\n): number {\n if (!selectedVariantId || component.available_variants.length === 0) {\n return parsePrice(component.effective_price);\n }\n if (selectedVariantId === component.variant_id) {\n return parsePrice(component.effective_price);\n }\n const bakedAdj = component.variant_id\n ? component.available_variants.find((v) => v.id === component.variant_id)\n : undefined;\n const selectedAdj = component.available_variants.find((v) => v.id === selectedVariantId);\n if (!selectedAdj) return parsePrice(component.effective_price);\n return parsePrice(component.effective_price)\n - parsePrice(bakedAdj?.price_adjustment ?? \"0\")\n + parsePrice(selectedAdj.price_adjustment);\n}\n\ninterface BundleComponentCardProps {\n component: BundleComponentView;\n selectedVariantId?: string;\n onVariantChange: (variantId: string) => void;\n classNames?: BundleSelectorClassNames;\n}\n\nfunction BundleComponentCard({\n component,\n selectedVariantId,\n onVariantChange,\n classNames,\n}: BundleComponentCardProps): React.ReactElement {\n const idPrefix = useId();\n const showVariantPicker =\n component.allow_variant_choice && component.available_variants.length > 1;\n\n const displayPrice = useMemo(\n () => getComponentPrice(component, selectedVariantId),\n [component, selectedVariantId],\n );\n\n const labelId = `${idPrefix}-bundle-component-${component.id}`;\n\n return (\n <div\n data-cimplify-bundle-component\n className={cn(\"py-4\", classNames?.component)}\n >\n <div\n data-cimplify-bundle-component-header\n className={cn(\"flex items-center justify-between gap-3\", classNames?.componentHeader)}\n >\n <div className=\"flex items-center gap-2\">\n {component.quantity > 1 && (\n <span\n data-cimplify-bundle-component-qty\n className={cn(\"text-xs font-medium text-primary bg-primary/10 px-1.5 py-0.5 rounded\", classNames?.componentQty)}\n >\n ×{component.quantity}\n </span>\n )}\n <span\n id={labelId}\n data-cimplify-bundle-component-name\n className={cn(\"text-sm\", classNames?.componentName)}\n >\n {component.product_name}\n </span>\n </div>\n <span className=\"text-sm text-muted-foreground\">\n <Price amount={displayPrice} />\n </span>\n </div>\n\n {showVariantPicker && (\n <RadioGroup\n aria-labelledby={labelId}\n value={selectedVariantId ?? \"\"}\n onValueChange={(value) => {\n onVariantChange(value);\n }}\n data-cimplify-bundle-variant-picker\n className={cn(\"mt-3 divide-y divide-border\", classNames?.variantPicker)}\n >\n {component.available_variants.map((variant: BundleComponentVariantView) => {\n const isSelected = selectedVariantId === variant.id;\n const adjustment = parsePrice(variant.price_adjustment);\n\n return (\n <Radio.Root\n key={variant.id}\n value={variant.id}\n data-cimplify-bundle-variant-option\n data-selected={isSelected || undefined}\n className={cn(\n \"w-full flex items-center gap-3 py-3 transition-colors cursor-pointer\",\n isSelected ? classNames?.variantOptionSelected : classNames?.variantOption,\n )}\n >\n <span\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 className=\"flex-1 text-sm\">\n {variant.display_name}\n </span>\n {adjustment !== 0 && (\n <span\n data-cimplify-bundle-variant-adjustment\n className={cn(\"text-sm text-muted-foreground\", classNames?.variantAdjustment)}\n >\n {adjustment > 0 ? \"+\" : \"\"}\n <Price amount={variant.price_adjustment} />\n </span>\n )}\n </Radio.Root>\n );\n })}\n </RadioGroup>\n )}\n </div>\n );\n}\n"
|
|
12
|
+
"content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect, useRef, useId } from \"react\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport { Radio } from \"@base-ui/react/radio\";\nimport type { BundleComponentView, BundleComponentVariantView, BundlePriceType, DurationUnit } from \"@cimplify/sdk\";\nimport type { Money } from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface BundleSelectorClassNames {\n root?: string;\n heading?: string;\n components?: string;\n component?: string;\n componentHeader?: string;\n componentQty?: string;\n componentName?: string;\n typeBadge?: string;\n serviceBadge?: string;\n digitalBadge?: string;\n variantPicker?: string;\n variantOption?: string;\n variantOptionSelected?: string;\n variantAdjustment?: string;\n summary?: string;\n savings?: string;\n}\n\nexport interface BundleSelectorProps {\n components: BundleComponentView[];\n bundlePrice?: Money;\n discountValue?: Money;\n pricingType?: BundlePriceType;\n onSelectionsChange: (selections: BundleSelectionInput[]) => void;\n onPriceChange?: (price: number) => void;\n onReady?: (ready: boolean) => void;\n className?: string;\n classNames?: BundleSelectorClassNames;\n}\n\nexport function BundleSelector({\n components,\n bundlePrice,\n discountValue,\n pricingType,\n onSelectionsChange,\n onPriceChange,\n onReady,\n className,\n classNames,\n}: BundleSelectorProps): React.ReactElement | null {\n const [variantChoices, setVariantChoices] = useState<Record<string, string>>({});\n const lastComponentIds = useRef(\"\");\n\n useEffect(() => {\n const ids = components.map((c) => c.id).sort().join();\n if (ids === lastComponentIds.current) return;\n lastComponentIds.current = ids;\n\n const defaults: Record<string, string> = {};\n for (const comp of components) {\n if (comp.variant_id) {\n defaults[comp.id] = comp.variant_id;\n } else if (comp.available_variants.length > 0) {\n const defaultVariant =\n comp.available_variants.find((v) => v.is_default) || comp.available_variants[0];\n if (defaultVariant) {\n defaults[comp.id] = defaultVariant.id;\n }\n }\n }\n setVariantChoices(defaults);\n }, [components]);\n\n const selections = useMemo((): BundleSelectionInput[] => {\n return components.map((comp) => ({\n component_id: comp.id,\n variant_id: variantChoices[comp.id],\n quantity: comp.quantity,\n }));\n }, [components, variantChoices]);\n\n useEffect(() => {\n onSelectionsChange(selections);\n }, [selections, onSelectionsChange]);\n\n useEffect(() => {\n onReady?.(components.length > 0 && selections.length > 0);\n }, [components, selections, onReady]);\n\n const totalPrice = useMemo(() => {\n if (pricingType === \"fixed\" && bundlePrice) {\n return parsePrice(bundlePrice);\n }\n const componentsTotal = components.reduce((sum, comp) => {\n return sum + getComponentPrice(comp, variantChoices[comp.id]) * comp.quantity;\n }, 0);\n if (pricingType === \"percentage_discount\" && discountValue) {\n return componentsTotal * (1 - parsePrice(discountValue) / 100);\n }\n if (pricingType === \"fixed_discount\" && discountValue) {\n return componentsTotal - parsePrice(discountValue);\n }\n return componentsTotal;\n }, [components, variantChoices, pricingType, bundlePrice, discountValue]);\n\n useEffect(() => {\n onPriceChange?.(totalPrice);\n }, [totalPrice, onPriceChange]);\n\n const handleVariantChange = useCallback(\n (componentId: string, variantId: string) => {\n setVariantChoices((prev) => ({ ...prev, [componentId]: variantId }));\n },\n [],\n );\n\n if (components.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-bundle-selector className={cn(\"space-y-4\", className, classNames?.root)}>\n <div\n data-cimplify-bundle-heading\n className={cn(\"flex items-center justify-between py-3\", classNames?.heading)}\n >\n <span className=\"text-base font-bold\">Included in this bundle</span>\n </div>\n\n <div data-cimplify-bundle-components className={cn(\"divide-y divide-border\", classNames?.components)}>\n {components.map((comp) => (\n <BundleComponentCard\n key={comp.id}\n component={comp}\n selectedVariantId={variantChoices[comp.id]}\n onVariantChange={(variantId) =>\n handleVariantChange(comp.id, variantId)\n }\n classNames={classNames}\n />\n ))}\n </div>\n\n {bundlePrice && (\n <div\n data-cimplify-bundle-summary\n className={cn(\"border-t border-border pt-4 flex justify-between text-sm\", classNames?.summary)}\n >\n <span className=\"text-muted-foreground\">Bundle price</span>\n <Price amount={bundlePrice} className=\"font-medium text-primary\" />\n </div>\n )}\n {discountValue && (\n <div\n data-cimplify-bundle-savings\n className={cn(\"flex justify-between text-sm\", classNames?.savings)}\n >\n <span className=\"text-muted-foreground\">You save</span>\n <Price amount={discountValue} className=\"text-green-600 font-medium\" />\n </div>\n )}\n </div>\n );\n}\n\nfunction getComponentPrice(\n component: BundleComponentView,\n selectedVariantId: string | undefined,\n): number {\n if (!selectedVariantId || component.available_variants.length === 0) {\n return parsePrice(component.effective_price);\n }\n if (selectedVariantId === component.variant_id) {\n return parsePrice(component.effective_price);\n }\n const bakedAdj = component.variant_id\n ? component.available_variants.find((v) => v.id === component.variant_id)\n : undefined;\n const selectedAdj = component.available_variants.find((v) => v.id === selectedVariantId);\n if (!selectedAdj) return parsePrice(component.effective_price);\n return parsePrice(component.effective_price)\n - parsePrice(bakedAdj?.price_adjustment ?? \"0\")\n + parsePrice(selectedAdj.price_adjustment);\n}\n\nfunction formatDuration(minutes: number, unit?: DurationUnit): string {\n if (unit === \"hours\" || (!unit && minutes >= 60 && minutes % 60 === 0)) {\n const h = Math.round(minutes / 60);\n return `${h}h`;\n }\n if (unit === \"days\" || unit === \"nights\") {\n const d = Math.round(minutes / 1440);\n return `${d}${unit === \"nights\" ? \"n\" : \"d\"}`;\n }\n return `${minutes}min`;\n}\n\ninterface BundleComponentCardProps {\n component: BundleComponentView;\n selectedVariantId?: string;\n onVariantChange: (variantId: string) => void;\n classNames?: BundleSelectorClassNames;\n}\n\nfunction BundleComponentCard({\n component,\n selectedVariantId,\n onVariantChange,\n classNames,\n}: BundleComponentCardProps): React.ReactElement {\n const idPrefix = useId();\n const showVariantPicker =\n component.allow_variant_choice && component.available_variants.length > 1;\n\n const displayPrice = useMemo(\n () => getComponentPrice(component, selectedVariantId),\n [component, selectedVariantId],\n );\n\n const labelId = `${idPrefix}-bundle-component-${component.id}`;\n\n return (\n <div\n data-cimplify-bundle-component\n className={cn(\"py-4\", classNames?.component)}\n >\n <div\n data-cimplify-bundle-component-header\n className={cn(\"flex items-center justify-between gap-3\", classNames?.componentHeader)}\n >\n <div className=\"flex items-center gap-2\">\n {component.quantity > 1 && (\n <span\n data-cimplify-bundle-component-qty\n className={cn(\"text-xs font-medium text-primary bg-primary/10 px-1.5 py-0.5 rounded\", classNames?.componentQty)}\n >\n ×{component.quantity}\n </span>\n )}\n <span\n id={labelId}\n data-cimplify-bundle-component-name\n className={cn(\"text-sm\", classNames?.componentName)}\n >\n {component.product_name}\n </span>\n {component.product_type === \"service\" && (\n <span\n data-cimplify-bundle-type-badge=\"service\"\n className={cn(\n \"text-[10px] uppercase tracking-wider font-medium text-blue-600\",\n classNames?.typeBadge,\n classNames?.serviceBadge,\n )}\n >\n <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"inline-block w-3 h-3 mr-0.5 -mt-px\" aria-hidden=\"true\">\n <circle cx=\"8\" cy=\"8\" r=\"7\" stroke=\"currentColor\" strokeWidth=\"1.5\"/>\n <path d=\"M8 4v4l2.5 1.5\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\"/>\n </svg>\n Service{component.duration_minutes != null && (\n <> · {formatDuration(component.duration_minutes, component.duration_unit)}</>\n )}\n </span>\n )}\n {component.product_type === \"digital\" && (\n <span\n data-cimplify-bundle-type-badge=\"digital\"\n className={cn(\n \"text-[10px] uppercase tracking-wider font-medium text-violet-600\",\n classNames?.typeBadge,\n classNames?.digitalBadge,\n )}\n >\n <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"inline-block w-3 h-3 mr-0.5 -mt-px\" aria-hidden=\"true\">\n <path d=\"M9 2L4 9h4l-1 5 5-7H8l1-5z\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\"/>\n </svg>\n Digital\n </span>\n )}\n </div>\n <span className=\"text-sm text-muted-foreground\">\n <Price amount={displayPrice} />\n </span>\n </div>\n\n {showVariantPicker && (\n <RadioGroup\n aria-labelledby={labelId}\n value={selectedVariantId ?? \"\"}\n onValueChange={(value) => {\n onVariantChange(value);\n }}\n data-cimplify-bundle-variant-picker\n className={cn(\"mt-3 divide-y divide-border\", classNames?.variantPicker)}\n >\n {component.available_variants.map((variant: BundleComponentVariantView) => {\n const isSelected = selectedVariantId === variant.id;\n const adjustment = parsePrice(variant.price_adjustment);\n\n return (\n <Radio.Root\n key={variant.id}\n value={variant.id}\n data-cimplify-bundle-variant-option\n data-selected={isSelected || undefined}\n className={cn(\n \"w-full flex items-center gap-3 py-3 transition-colors cursor-pointer\",\n isSelected ? classNames?.variantOptionSelected : classNames?.variantOption,\n )}\n >\n <span\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 className=\"flex-1 text-sm\">\n {variant.display_name}\n </span>\n {adjustment !== 0 && (\n <span\n data-cimplify-bundle-variant-adjustment\n className={cn(\"text-sm text-muted-foreground\", classNames?.variantAdjustment)}\n >\n {adjustment > 0 ? \"+\" : \"\"}\n <Price amount={variant.price_adjustment} />\n </span>\n )}\n </Radio.Root>\n );\n })}\n </RadioGroup>\n )}\n </div>\n );\n}\n"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
{
|
|
12
12
|
"path": "cart-summary.tsx",
|
|
13
|
-
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { UseCartItem } from \"@cimplify/sdk/react\";\nimport { useCart } from \"@cimplify/sdk/react\";\nimport { getVariantDisplayName } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { QuantitySelector } from \"@cimplify/sdk/react\";\nimport { useProductPrice } from \"@cimplify/sdk/react\";\n\nexport interface CartSummaryProps {\n onCheckout?: () => void;\n onItemRemove?: (itemId: string) => void;\n onQuantityChange?: (itemId: string, quantity: number) => void;\n emptyMessage?: string;\n className?: string;\n}\n\nfunction CartLineItemRow({\n item,\n onRemove,\n onQuantityChange,\n}: {\n item: UseCartItem;\n onRemove: (itemId: string) => void;\n onQuantityChange: (itemId: string, qty: number) => void;\n}): React.ReactElement {\n const { unitPrice } = useProductPrice({\n product: item.product,\n variant: item.variant,\n addOnOptions: item.addOnOptions,\n });\n const hasComposite = item.compositeSelections && item.compositeSelections.length > 0;\n const hasBundle = item.bundleSelections && item.bundleSelections.length > 0;\n\n return (\n <div data-cimplify-cart-item>\n <div data-cimplify-cart-item-info>\n <span data-cimplify-cart-item-name>{item.product.name}</span>\n\n {item.variant && (\n <span data-cimplify-cart-item-variant>\n {getVariantDisplayName(item.variant, item.product.name)}\n </span>\n )}\n\n {item.addOnOptions && item.addOnOptions.length > 0 && (\n <span data-cimplify-cart-item-addons>\n + {item.addOnOptions.map((opt) => opt.name).join(\", \")}\n </span>\n )}\n\n {(hasComposite || hasBundle) && (\n <span data-cimplify-cart-item-badge>\n {hasComposite ? \"Custom\" : \"Bundle\"}\n </span>\n )}\n\n <Price amount={unitPrice} />\n </div>\n\n <div data-cimplify-cart-item-controls>\n <QuantitySelector\n value={item.quantity}\n onChange={(qty) => onQuantityChange(item.id, qty)}\n min={0}\n />\n <button\n type=\"button\"\n onClick={() => onRemove(item.id)}\n data-cimplify-cart-item-remove\n aria-label={`Remove ${item.product.name}`}\n >\n Remove\n </button>\n </div>\n </div>\n );\n}\n\n/**\n * CartSummary — renders cart line items + totals.\n *\n * NOT a drawer or modal — just the cart content. Templates wrap this in\n * their own drawer/modal shell with animations.\n */\nexport function CartSummary({\n onCheckout,\n onItemRemove,\n onQuantityChange,\n emptyMessage = \"Your cart is empty\",\n className,\n}: CartSummaryProps): React.ReactElement {\n const { items, itemCount, subtotal, tax, total, isEmpty, removeItem, updateQuantity } =\n useCart();\n\n const handleRemove = (itemId: string) => {\n if (onItemRemove) {\n onItemRemove(itemId);\n } else {\n void removeItem(itemId);\n }\n };\n\n const handleQuantityChange = (itemId: string, qty: number) => {\n if (onQuantityChange) {\n onQuantityChange(itemId, qty);\n } else {\n void updateQuantity(itemId, qty);\n }\n };\n\n return (\n <div data-cimplify-cart-summary className={className}>\n {isEmpty ? (\n <div data-cimplify-cart-empty>\n <p>{emptyMessage}</p>\n </div>\n ) : (\n <>\n {/* Line items */}\n <div data-cimplify-cart-items>\n {items.map((item) => (\n <CartLineItemRow\n key={item.id}\n item={item}\n onRemove={handleRemove}\n onQuantityChange={handleQuantityChange}\n />\n ))}\n </div>\n\n {/* Totals */}\n <div data-cimplify-cart-totals>\n <div data-cimplify-cart-subtotal>\n <span>Subtotal ({itemCount} {itemCount === 1 ? \"item\" : \"items\"})</span>\n <Price amount={subtotal} />\n </div>\n <div data-cimplify-cart-tax>\n <span>Tax</span>\n <Price amount={tax} />\n </div>\n <div data-cimplify-cart-total>\n <span>Total</span>\n <Price amount={total} />\n </div>\n </div>\n\n {onCheckout && (\n <button\n type=\"button\"\n onClick={onCheckout}\n data-cimplify-cart-checkout\n >\n Proceed to Checkout\n </button>\n )}\n </>\n )}\n </div>\n );\n}\n"
|
|
13
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { UseCartItem } from \"@cimplify/sdk/react\";\nimport type { DisplayCart } from \"@cimplify/sdk\";\nimport { useCart } from \"@cimplify/sdk/react\";\nimport { getVariantDisplayName } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { QuantitySelector } from \"@cimplify/sdk/react\";\nimport { useProductPrice } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface CartSummaryClassNames {\n root?: string;\n item?: string;\n itemName?: string;\n itemPrice?: string;\n quantity?: string;\n removeButton?: string;\n totals?: string;\n subtotal?: string;\n tax?: string;\n deliveryFee?: string;\n serviceCharge?: string;\n discount?: string;\n total?: string;\n checkout?: string;\n empty?: string;\n serviceInfo?: string;\n digitalInfo?: string;\n bundleBreakdown?: string;\n compositeBreakdown?: string;\n}\n\nexport interface CartSummaryProps {\n /** Optional server cart for extended pricing details (delivery fee, service charge, discounts). */\n cart?: DisplayCart;\n onCheckout?: () => void;\n onItemRemove?: (itemId: string) => void;\n onQuantityChange?: (itemId: string, quantity: number) => void;\n emptyMessage?: string;\n className?: string;\n classNames?: CartSummaryClassNames;\n}\n\nfunction CartLineItemRow({\n item,\n onRemove,\n onQuantityChange,\n classNames,\n}: {\n item: UseCartItem;\n onRemove: (itemId: string) => void;\n onQuantityChange: (itemId: string, qty: number) => void;\n classNames?: CartSummaryClassNames;\n}): React.ReactElement {\n const { unitPrice } = useProductPrice({\n product: item.product,\n variant: item.variant,\n addOnOptions: item.addOnOptions,\n });\n const hasComposite = item.compositeSelections && item.compositeSelections.length > 0;\n const hasBundle = item.bundleSelections && item.bundleSelections.length > 0;\n\n return (\n <div data-cimplify-cart-item className={classNames?.item}>\n <div data-cimplify-cart-item-info>\n <span data-cimplify-cart-item-name className={classNames?.itemName}>{item.product.name}</span>\n\n {item.variant && (\n <span data-cimplify-cart-item-variant>\n {getVariantDisplayName(item.variant, item.product.name)}\n </span>\n )}\n\n {item.addOnOptions && item.addOnOptions.length > 0 && (\n <span data-cimplify-cart-item-addons>\n + {item.addOnOptions.map((opt) => opt.name).join(\", \")}\n </span>\n )}\n\n {(hasComposite || hasBundle) && (\n <span data-cimplify-cart-item-badge>\n {hasComposite ? \"Custom\" : \"Bundle\"}\n </span>\n )}\n\n {item.specialInstructions && (\n <span data-cimplify-cart-item-instructions className=\"text-xs text-muted-foreground\">\n {item.specialInstructions}\n </span>\n )}\n\n {/* Service items: show booking details */}\n {(item.lineType === \"service\" || item.product.type === \"service\") && item.scheduledStart && (\n <div data-cimplify-cart-service-info className={classNames?.serviceInfo}>\n <span>{\"\\u{1F4C5}\"} {new Date(item.scheduledStart).toLocaleDateString()} at {new Date(item.scheduledStart).toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" })}</span>\n {item.confirmationCode && <span>Confirmation: {item.confirmationCode}</span>}\n {item.serviceStatus && <span data-status={item.serviceStatus}>{item.serviceStatus}</span>}\n </div>\n )}\n\n {/* Digital items: show instant delivery badge */}\n {(item.lineType === \"digital\" || item.product.type === \"digital\") && (\n <div data-cimplify-cart-digital-info className={classNames?.digitalInfo}>\n <span>{\"\\u26A1\"} Instant digital delivery</span>\n </div>\n )}\n\n {/* Bundle component breakdown */}\n {item.bundleResolved && (\n <div data-cimplify-cart-bundle-breakdown className={classNames?.bundleBreakdown}>\n {item.bundleResolved.selections.map((sel) => (\n <div key={sel.component_id} data-product-type={sel.product_type}>\n <span>{sel.quantity}× {sel.product_name}</span>\n {sel.product_type === \"service\" && sel.scheduling?.scheduled_start && (\n <span> — {new Date(sel.scheduling.scheduled_start).toLocaleDateString()}</span>\n )}\n {sel.product_type === \"digital\" && <span> — Digital</span>}\n </div>\n ))}\n </div>\n )}\n\n {/* Composite component breakdown */}\n {item.compositeResolved && (\n <div data-cimplify-cart-composite-breakdown className={classNames?.compositeBreakdown}>\n {item.compositeResolved.selections.map((sel) => (\n <div key={sel.component_id} data-product-type={sel.product_type}>\n <span>{sel.quantity}× {sel.component_name}</span>\n {sel.product_type === \"service\" && sel.scheduling?.scheduled_start && (\n <span> — {new Date(sel.scheduling.scheduled_start).toLocaleDateString()}</span>\n )}\n </div>\n ))}\n </div>\n )}\n\n <Price amount={unitPrice} className={classNames?.itemPrice} />\n </div>\n\n <div data-cimplify-cart-item-controls className={classNames?.quantity}>\n <QuantitySelector\n value={item.quantity}\n onChange={(qty) => onQuantityChange(item.id, qty)}\n min={0}\n />\n <button\n type=\"button\"\n onClick={() => onRemove(item.id)}\n data-cimplify-cart-item-remove\n className={classNames?.removeButton}\n aria-label={`Remove ${item.product.name}`}\n >\n Remove\n </button>\n </div>\n </div>\n );\n}\n\n/**\n * CartSummary — renders cart line items + totals.\n *\n * NOT a drawer or modal — just the cart content. Templates wrap this in\n * their own drawer/modal shell with animations.\n */\nexport function CartSummary({\n cart: serverCart,\n onCheckout,\n onItemRemove,\n onQuantityChange,\n emptyMessage = \"Your cart is empty\",\n className,\n classNames,\n}: CartSummaryProps): React.ReactElement {\n const { items, itemCount, subtotal, tax, total, isEmpty, removeItem, updateQuantity } =\n useCart();\n\n const deliveryFee = serverCart?.delivery_fee;\n const serviceCharge = serverCart?.service_charge;\n const totalDiscounts = serverCart?.total_discounts;\n\n const handleRemove = (itemId: string) => {\n if (onItemRemove) {\n onItemRemove(itemId);\n } else {\n void removeItem(itemId);\n }\n };\n\n const handleQuantityChange = (itemId: string, qty: number) => {\n if (onQuantityChange) {\n onQuantityChange(itemId, qty);\n } else {\n void updateQuantity(itemId, qty);\n }\n };\n\n return (\n <div data-cimplify-cart-summary className={cn(className, classNames?.root)}>\n {isEmpty ? (\n <div data-cimplify-cart-empty className={classNames?.empty}>\n <p>{emptyMessage}</p>\n </div>\n ) : (\n <>\n {/* Line items */}\n <div data-cimplify-cart-items>\n {items.map((item) => (\n <CartLineItemRow\n key={item.id}\n item={item}\n onRemove={handleRemove}\n onQuantityChange={handleQuantityChange}\n classNames={classNames}\n />\n ))}\n </div>\n\n {/* Totals */}\n <div data-cimplify-cart-totals className={classNames?.totals}>\n <div data-cimplify-cart-subtotal className={classNames?.subtotal}>\n <span>Subtotal ({itemCount} {itemCount === 1 ? \"item\" : \"items\"})</span>\n <Price amount={subtotal} />\n </div>\n\n {totalDiscounts != null && parsePrice(totalDiscounts) > 0 && (\n <div data-cimplify-cart-discount className={classNames?.discount}>\n <span>Discount</span>\n <Price amount={totalDiscounts} prefix=\"-\" />\n </div>\n )}\n\n {deliveryFee != null && (\n <div data-cimplify-cart-delivery-fee className={classNames?.deliveryFee}>\n <span>Delivery fee</span>\n {parsePrice(deliveryFee) > 0 ? (\n <Price amount={deliveryFee} />\n ) : (\n <span>Free delivery</span>\n )}\n </div>\n )}\n\n {serviceCharge != null && parsePrice(serviceCharge) > 0 && (\n <div data-cimplify-cart-service-charge className={classNames?.serviceCharge}>\n <span>Service charge</span>\n <Price amount={serviceCharge} />\n </div>\n )}\n\n <div data-cimplify-cart-tax className={classNames?.tax}>\n <span>Tax</span>\n <Price amount={tax} />\n </div>\n <div data-cimplify-cart-total className={classNames?.total}>\n <span>Total</span>\n <Price amount={total} />\n </div>\n </div>\n\n {onCheckout && (\n <button\n type=\"button\"\n onClick={onCheckout}\n data-cimplify-cart-checkout\n className={classNames?.checkout}\n >\n Proceed to Checkout\n </button>\n )}\n </>\n )}\n </div>\n );\n}\n"
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "composite-selector.tsx",
|
|
12
|
-
"content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect } from \"react\";\nimport { Checkbox } from \"@base-ui/react/checkbox\";\nimport { NumberField } from \"@base-ui/react/number-field\";\nimport type {\n CompositeGroupView,\n CompositeComponentView,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\nimport { useCimplifyClient } from \"@cimplify/sdk/react\";\n\nexport interface CompositeSelectorClassNames {\n root?: string;\n group?: string;\n groupHeader?: string;\n groupName?: string;\n required?: string;\n groupDescription?: string;\n groupConstraint?: string;\n validation?: string;\n components?: string;\n component?: string;\n componentSelected?: string;\n componentInfo?: string;\n componentName?: string;\n badgePopular?: string;\n badgePremium?: string;\n componentDescription?: string;\n componentCalories?: string;\n qty?: string;\n qtyButton?: string;\n qtyValue?: string;\n summary?: string;\n summaryLine?: string;\n summaryTotal?: string;\n calculating?: string;\n priceError?: string;\n}\n\nexport interface CompositeSelectorProps {\n compositeId: string;\n groups: CompositeGroupView[];\n onSelectionsChange: (selections: ComponentSelectionInput[]) => void;\n onPriceChange?: (price: CompositePriceResult | null) => void;\n onReady?: (ready: boolean) => void;\n skipPriceFetch?: boolean;\n className?: string;\n classNames?: CompositeSelectorClassNames;\n}\n\nexport function CompositeSelector({\n compositeId,\n groups,\n onSelectionsChange,\n onPriceChange,\n onReady,\n skipPriceFetch,\n className,\n classNames,\n}: CompositeSelectorProps): React.ReactElement | null {\n const { client } = useCimplifyClient();\n\n const [groupSelections, setGroupSelections] = useState<\n Record<string, Record<string, number>>\n >({});\n const [priceResult, setPriceResult] = useState<CompositePriceResult | null>(null);\n const [isPriceLoading, setIsPriceLoading] = useState(false);\n const [priceError, setPriceError] = useState(false);\n\n const selections = useMemo((): ComponentSelectionInput[] => {\n const result: ComponentSelectionInput[] = [];\n for (const groupSels of Object.values(groupSelections)) {\n for (const [componentId, qty] of Object.entries(groupSels)) {\n if (qty > 0) {\n result.push({ component_id: componentId, quantity: qty });\n }\n }\n }\n return result;\n }, [groupSelections]);\n\n useEffect(() => {\n onSelectionsChange(selections);\n }, [selections, onSelectionsChange]);\n\n useEffect(() => {\n onPriceChange?.(priceResult);\n }, [priceResult, onPriceChange]);\n\n const allGroupsSatisfied = useMemo(() => {\n for (const group of groups) {\n const groupSels = groupSelections[group.id] || {};\n const totalSelected = Object.values(groupSels).reduce((sum, q) => sum + q, 0);\n if (totalSelected < group.min_selections) return false;\n }\n return true;\n }, [groups, groupSelections]);\n\n const sortedGroups = useMemo(\n () =>\n [...groups]\n .sort((a, b) => a.display_order - b.display_order)\n .map((group) => ({\n ...group,\n _sortedComponents: group.components\n .filter((component) => component.is_available && !component.is_archived)\n .sort((a, b) => a.display_order - b.display_order),\n })),\n [groups],\n );\n\n useEffect(() => {\n onReady?.(allGroupsSatisfied);\n }, [allGroupsSatisfied, onReady]);\n\n useEffect(() => {\n if (skipPriceFetch || !allGroupsSatisfied || selections.length === 0) return;\n\n let cancelled = false;\n const timer = setTimeout(() => {\n void (async () => {\n setIsPriceLoading(true);\n setPriceError(false);\n try {\n const result = await client.catalogue.calculateCompositePrice(compositeId, selections);\n if (cancelled) return;\n if (result.ok) {\n setPriceResult(result.value);\n } else {\n setPriceError(true);\n }\n } catch {\n if (!cancelled) setPriceError(true);\n } finally {\n if (!cancelled) setIsPriceLoading(false);\n }\n })();\n }, 300);\n\n return () => {\n cancelled = true;\n clearTimeout(timer);\n };\n }, [selections, allGroupsSatisfied, compositeId, client, skipPriceFetch]);\n\n const toggleComponent = useCallback(\n (group: CompositeGroupView, component: CompositeComponentView) => {\n setGroupSelections((prev) => {\n const groupSels = { ...(prev[group.id] || {}) };\n const currentQty = groupSels[component.id] || 0;\n\n if (currentQty > 0) {\n if (group.min_selections > 0) {\n const totalOthers = Object.entries(groupSels)\n .filter(([id]) => id !== component.id)\n .reduce((sum, [, q]) => sum + q, 0);\n if (totalOthers < group.min_selections) {\n return prev;\n }\n }\n delete groupSels[component.id];\n } else {\n const totalSelected = Object.values(groupSels).reduce((sum, q) => sum + q, 0);\n if (group.max_selections && totalSelected >= group.max_selections) {\n if (group.max_selections === 1) {\n return { ...prev, [group.id]: { [component.id]: 1 } };\n }\n return prev;\n }\n groupSels[component.id] = 1;\n }\n\n return { ...prev, [group.id]: groupSels };\n });\n },\n [],\n );\n\n const updateQuantity = useCallback(\n (group: CompositeGroupView, componentId: string, newValue: number) => {\n setGroupSelections((prev) => {\n const groupSels = { ...(prev[group.id] || {}) };\n const current = groupSels[componentId] || 0;\n const next = Math.max(0, newValue);\n\n if (next === current) return prev;\n\n const delta = next - current;\n\n if (group.max_quantity_per_component && next > group.max_quantity_per_component) {\n return prev;\n }\n\n const totalAfter = Object.entries(groupSels)\n .reduce((sum, [id, q]) => sum + (id === componentId ? next : q), 0);\n\n if (delta > 0 && group.max_selections && totalAfter > group.max_selections) {\n return prev;\n }\n\n if (delta < 0 && totalAfter < group.min_selections) {\n return prev;\n }\n\n if (next === 0) {\n delete groupSels[componentId];\n } else {\n groupSels[componentId] = next;\n }\n\n return { ...prev, [group.id]: groupSels };\n });\n },\n [],\n );\n\n if (groups.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-composite-selector className={cn(\"space-y-6\", className, classNames?.root)}>\n {sortedGroups.map((group) => {\n const groupSels = groupSelections[group.id] || {};\n const totalSelected = Object.values(groupSels).reduce((sum, q) => sum + q, 0);\n const minMet = totalSelected >= group.min_selections;\n const isSingleSelect = group.max_selections === 1;\n\n return (\n <div\n key={group.id}\n data-cimplify-composite-group\n className={cn(classNames?.group)}\n >\n <div\n data-cimplify-composite-group-header\n className={cn(\"flex items-center justify-between py-3\", classNames?.groupHeader)}\n >\n <div>\n <span\n data-cimplify-composite-group-name\n className={cn(\"text-base font-bold\", classNames?.groupName)}\n >\n {group.name}\n </span>\n {group.description && (\n <span\n data-cimplify-composite-group-description\n className={cn(\"block text-xs text-muted-foreground mt-0.5\", classNames?.groupDescription)}\n >\n {group.description}\n </span>\n )}\n <span\n data-cimplify-composite-group-constraint\n className={cn(\"block text-xs text-muted-foreground mt-0.5\", classNames?.groupConstraint)}\n >\n {group.min_selections > 0 && group.max_selections\n ? `Choose ${group.min_selections}\\u2013${group.max_selections}`\n : group.min_selections > 0\n ? `Choose at least ${group.min_selections}`\n : group.max_selections\n ? `Choose up to ${group.max_selections}`\n : \"Choose as many as you like\"}\n </span>\n </div>\n {group.min_selections > 0 && (\n <span\n data-cimplify-composite-required\n className={cn(\n \"text-xs font-semibold px-2.5 py-1 rounded shrink-0\",\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-composite-components\n role={isSingleSelect ? \"radiogroup\" : \"group\"}\n aria-label={group.name}\n className={cn(\"divide-y divide-border\", classNames?.components)}\n >\n {group._sortedComponents.map((component) => {\n const qty = groupSels[component.id] || 0;\n const isSelected = qty > 0;\n const displayName = component.display_name || component.id;\n\n return (\n <Checkbox.Root\n key={component.id}\n checked={isSelected}\n onCheckedChange={() => toggleComponent(group, component)}\n value={component.id}\n data-cimplify-composite-component\n data-selected={isSelected || undefined}\n className={cn(\n \"w-full flex items-center gap-3 py-4 transition-colors text-left cursor-pointer\",\n isSelected ? classNames?.componentSelected : classNames?.component,\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-composite-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-composite-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 <div\n data-cimplify-composite-component-info\n className={cn(\"flex-1 min-w-0\", classNames?.componentInfo)}\n >\n <span\n data-cimplify-composite-component-name\n className={cn(\"text-sm\", classNames?.componentName)}\n >\n {displayName}\n </span>\n {component.is_popular && (\n <span\n data-cimplify-composite-badge=\"popular\"\n className={cn(\"text-[10px] uppercase tracking-wider text-primary font-medium\", classNames?.badgePopular)}\n >\n Popular\n </span>\n )}\n {component.is_premium && (\n <span\n data-cimplify-composite-badge=\"premium\"\n className={cn(\"text-[10px] uppercase tracking-wider text-amber-600 font-medium\", classNames?.badgePremium)}\n >\n Premium\n </span>\n )}\n {component.display_description && (\n <span\n data-cimplify-composite-component-description\n className={cn(\"block text-xs text-muted-foreground truncate\", classNames?.componentDescription)}\n >\n {component.display_description}\n </span>\n )}\n {component.calories != null && (\n <span\n data-cimplify-composite-component-calories\n className={cn(\"block text-xs text-muted-foreground/60\", classNames?.componentCalories)}\n >\n {component.calories} cal\n </span>\n )}\n </div>\n\n {group.allow_quantity && isSelected && (\n <NumberField.Root\n value={qty}\n onValueChange={(val) => {\n if (val != null) {\n updateQuantity(group, component.id, val);\n }\n }}\n min={0}\n max={group.max_quantity_per_component || undefined}\n step={1}\n >\n <NumberField.Group\n data-cimplify-composite-qty\n onClick={(e: React.MouseEvent) => e.stopPropagation()}\n className={cn(\"flex items-center gap-2\", classNames?.qty)}\n >\n <NumberField.Decrement\n aria-label={`Decrease ${displayName} quantity`}\n className={cn(\"w-6 h-6 border border-border flex items-center justify-center text-xs hover:bg-muted disabled:opacity-30\", classNames?.qtyButton)}\n >\n −\n </NumberField.Decrement>\n <NumberField.Input\n readOnly\n className={cn(\"w-4 text-center text-sm font-medium bg-transparent border-none outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none\", classNames?.qtyValue)}\n />\n <NumberField.Increment\n aria-label={`Increase ${displayName} quantity`}\n className={cn(\"w-6 h-6 border border-border flex items-center justify-center text-xs hover:bg-muted disabled:opacity-30\", classNames?.qtyButton)}\n >\n +\n </NumberField.Increment>\n </NumberField.Group>\n </NumberField.Root>\n )}\n\n {component.price != null && (\n <span className=\"text-sm text-muted-foreground shrink-0\">\n +<Price amount={component.price} />\n </span>\n )}\n </Checkbox.Root>\n );\n })}\n </div>\n </div>\n );\n })}\n\n {priceResult && (\n <div\n data-cimplify-composite-summary\n className={cn(\"border-t border-border pt-4 space-y-1 text-sm\", classNames?.summary)}\n >\n {parsePrice(priceResult.base_price) !== 0 && (\n <div\n data-cimplify-composite-summary-line\n className={cn(\"flex justify-between text-muted-foreground\", classNames?.summaryLine)}\n >\n <span>Base</span>\n <Price amount={priceResult.base_price} />\n </div>\n )}\n {parsePrice(priceResult.components_total) !== 0 && (\n <div\n data-cimplify-composite-summary-line\n className={cn(\"flex justify-between text-muted-foreground\", classNames?.summaryLine)}\n >\n <span>Selections</span>\n <Price amount={priceResult.components_total} />\n </div>\n )}\n <div\n data-cimplify-composite-summary-total\n className={cn(\"flex justify-between font-medium pt-1 border-t border-border\", classNames?.summaryTotal)}\n >\n <span>Total</span>\n <Price amount={priceResult.final_price} className=\"text-primary\" />\n </div>\n </div>\n )}\n\n {isPriceLoading && (\n <div\n data-cimplify-composite-calculating\n className={cn(\"flex items-center gap-2 text-sm text-muted-foreground\", classNames?.calculating)}\n >\n Calculating price...\n </div>\n )}\n\n {priceError && !isPriceLoading && (\n <div\n data-cimplify-composite-price-error\n className={cn(\"text-sm text-destructive\", classNames?.priceError)}\n >\n Unable to calculate price\n </div>\n )}\n </div>\n );\n}\n"
|
|
12
|
+
"content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect } from \"react\";\nimport { Checkbox } from \"@base-ui/react/checkbox\";\nimport { NumberField } from \"@base-ui/react/number-field\";\nimport type {\n CompositeGroupView,\n CompositeComponentView,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\nimport { useCimplifyClient } from \"@cimplify/sdk/react\";\n\nexport interface CompositeSelectorClassNames {\n root?: string;\n group?: string;\n groupHeader?: string;\n groupName?: string;\n required?: string;\n groupDescription?: string;\n groupConstraint?: string;\n validation?: string;\n components?: string;\n component?: string;\n componentSelected?: string;\n componentInfo?: string;\n componentName?: string;\n typeBadge?: string;\n serviceBadge?: string;\n digitalBadge?: string;\n badgePopular?: string;\n badgePremium?: string;\n componentDescription?: string;\n componentCalories?: string;\n qty?: string;\n qtyButton?: string;\n qtyValue?: string;\n summary?: string;\n summaryLine?: string;\n summaryTotal?: string;\n calculating?: string;\n priceError?: string;\n}\n\nexport interface CompositeSelectorProps {\n compositeId: string;\n groups: CompositeGroupView[];\n onSelectionsChange: (selections: ComponentSelectionInput[]) => void;\n onPriceChange?: (price: CompositePriceResult | null) => void;\n onReady?: (ready: boolean) => void;\n skipPriceFetch?: boolean;\n className?: string;\n classNames?: CompositeSelectorClassNames;\n}\n\nexport function CompositeSelector({\n compositeId,\n groups,\n onSelectionsChange,\n onPriceChange,\n onReady,\n skipPriceFetch,\n className,\n classNames,\n}: CompositeSelectorProps): React.ReactElement | null {\n const { client } = useCimplifyClient();\n\n const [groupSelections, setGroupSelections] = useState<\n Record<string, Record<string, number>>\n >({});\n const [priceResult, setPriceResult] = useState<CompositePriceResult | null>(null);\n const [isPriceLoading, setIsPriceLoading] = useState(false);\n const [priceError, setPriceError] = useState(false);\n\n const selections = useMemo((): ComponentSelectionInput[] => {\n const result: ComponentSelectionInput[] = [];\n for (const groupSels of Object.values(groupSelections)) {\n for (const [componentId, qty] of Object.entries(groupSels)) {\n if (qty > 0) {\n result.push({ component_id: componentId, quantity: qty });\n }\n }\n }\n return result;\n }, [groupSelections]);\n\n useEffect(() => {\n onSelectionsChange(selections);\n }, [selections, onSelectionsChange]);\n\n useEffect(() => {\n onPriceChange?.(priceResult);\n }, [priceResult, onPriceChange]);\n\n const allGroupsSatisfied = useMemo(() => {\n for (const group of groups) {\n const groupSels = groupSelections[group.id] || {};\n const totalSelected = Object.values(groupSels).reduce((sum, q) => sum + q, 0);\n if (totalSelected < group.min_selections) return false;\n }\n return true;\n }, [groups, groupSelections]);\n\n const sortedGroups = useMemo(\n () =>\n [...groups]\n .sort((a, b) => a.display_order - b.display_order)\n .map((group) => ({\n ...group,\n _sortedComponents: group.components\n .filter((component) => component.is_available && !component.is_archived)\n .sort((a, b) => a.display_order - b.display_order),\n })),\n [groups],\n );\n\n useEffect(() => {\n onReady?.(allGroupsSatisfied);\n }, [allGroupsSatisfied, onReady]);\n\n useEffect(() => {\n if (skipPriceFetch || !allGroupsSatisfied || selections.length === 0) return;\n\n let cancelled = false;\n const timer = setTimeout(() => {\n void (async () => {\n setIsPriceLoading(true);\n setPriceError(false);\n try {\n const result = await client.catalogue.calculateCompositePrice(compositeId, selections);\n if (cancelled) return;\n if (result.ok) {\n setPriceResult(result.value);\n } else {\n setPriceError(true);\n }\n } catch {\n if (!cancelled) setPriceError(true);\n } finally {\n if (!cancelled) setIsPriceLoading(false);\n }\n })();\n }, 300);\n\n return () => {\n cancelled = true;\n clearTimeout(timer);\n };\n }, [selections, allGroupsSatisfied, compositeId, client, skipPriceFetch]);\n\n const toggleComponent = useCallback(\n (group: CompositeGroupView, component: CompositeComponentView) => {\n setGroupSelections((prev) => {\n const groupSels = { ...(prev[group.id] || {}) };\n const currentQty = groupSels[component.id] || 0;\n\n if (currentQty > 0) {\n if (group.min_selections > 0) {\n const totalOthers = Object.entries(groupSels)\n .filter(([id]) => id !== component.id)\n .reduce((sum, [, q]) => sum + q, 0);\n if (totalOthers < group.min_selections) {\n return prev;\n }\n }\n delete groupSels[component.id];\n } else {\n const totalSelected = Object.values(groupSels).reduce((sum, q) => sum + q, 0);\n if (group.max_selections && totalSelected >= group.max_selections) {\n if (group.max_selections === 1) {\n return { ...prev, [group.id]: { [component.id]: 1 } };\n }\n return prev;\n }\n groupSels[component.id] = 1;\n }\n\n return { ...prev, [group.id]: groupSels };\n });\n },\n [],\n );\n\n const updateQuantity = useCallback(\n (group: CompositeGroupView, componentId: string, newValue: number) => {\n setGroupSelections((prev) => {\n const groupSels = { ...(prev[group.id] || {}) };\n const current = groupSels[componentId] || 0;\n const next = Math.max(0, newValue);\n\n if (next === current) return prev;\n\n const delta = next - current;\n\n if (group.max_quantity_per_component && next > group.max_quantity_per_component) {\n return prev;\n }\n\n const totalAfter = Object.entries(groupSels)\n .reduce((sum, [id, q]) => sum + (id === componentId ? next : q), 0);\n\n if (delta > 0 && group.max_selections && totalAfter > group.max_selections) {\n return prev;\n }\n\n if (delta < 0 && totalAfter < group.min_selections) {\n return prev;\n }\n\n if (next === 0) {\n delete groupSels[componentId];\n } else {\n groupSels[componentId] = next;\n }\n\n return { ...prev, [group.id]: groupSels };\n });\n },\n [],\n );\n\n if (groups.length === 0) {\n return null;\n }\n\n return (\n <div data-cimplify-composite-selector className={cn(\"space-y-6\", className, classNames?.root)}>\n {sortedGroups.map((group) => {\n const groupSels = groupSelections[group.id] || {};\n const totalSelected = Object.values(groupSels).reduce((sum, q) => sum + q, 0);\n const minMet = totalSelected >= group.min_selections;\n const isSingleSelect = group.max_selections === 1;\n\n return (\n <div\n key={group.id}\n data-cimplify-composite-group\n className={cn(classNames?.group)}\n >\n <div\n data-cimplify-composite-group-header\n className={cn(\"flex items-center justify-between py-3\", classNames?.groupHeader)}\n >\n <div>\n <span\n data-cimplify-composite-group-name\n className={cn(\"text-base font-bold\", classNames?.groupName)}\n >\n {group.name}\n </span>\n {group.description && (\n <span\n data-cimplify-composite-group-description\n className={cn(\"block text-xs text-muted-foreground mt-0.5\", classNames?.groupDescription)}\n >\n {group.description}\n </span>\n )}\n <span\n data-cimplify-composite-group-constraint\n className={cn(\"block text-xs text-muted-foreground mt-0.5\", classNames?.groupConstraint)}\n >\n {group.min_selections > 0 && group.max_selections\n ? `Choose ${group.min_selections}\\u2013${group.max_selections}`\n : group.min_selections > 0\n ? `Choose at least ${group.min_selections}`\n : group.max_selections\n ? `Choose up to ${group.max_selections}`\n : \"Choose as many as you like\"}\n </span>\n </div>\n {group.min_selections > 0 && (\n <span\n data-cimplify-composite-required\n className={cn(\n \"text-xs font-semibold px-2.5 py-1 rounded shrink-0\",\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-composite-components\n role={isSingleSelect ? \"radiogroup\" : \"group\"}\n aria-label={group.name}\n className={cn(\"divide-y divide-border\", classNames?.components)}\n >\n {group._sortedComponents.map((component) => {\n const qty = groupSels[component.id] || 0;\n const isSelected = qty > 0;\n const displayName = component.display_name || component.id;\n\n return (\n <Checkbox.Root\n key={component.id}\n checked={isSelected}\n onCheckedChange={() => toggleComponent(group, component)}\n value={component.id}\n data-cimplify-composite-component\n data-selected={isSelected || undefined}\n className={cn(\n \"w-full flex items-center gap-3 py-4 transition-colors text-left cursor-pointer\",\n isSelected ? classNames?.componentSelected : classNames?.component,\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-composite-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-composite-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 <div\n data-cimplify-composite-component-info\n className={cn(\"flex-1 min-w-0\", classNames?.componentInfo)}\n >\n <span\n data-cimplify-composite-component-name\n className={cn(\"text-sm\", classNames?.componentName)}\n >\n {displayName}\n </span>\n {component.product_type === \"service\" && (\n <span\n data-cimplify-composite-type-badge=\"service\"\n className={cn(\n \"text-[10px] uppercase tracking-wider font-medium text-blue-600\",\n classNames?.typeBadge,\n classNames?.serviceBadge,\n )}\n >\n <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"inline-block w-3 h-3 mr-0.5 -mt-px\" aria-hidden=\"true\">\n <circle cx=\"8\" cy=\"8\" r=\"7\" stroke=\"currentColor\" strokeWidth=\"1.5\"/>\n <path d=\"M8 4v4l2.5 1.5\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\"/>\n </svg>\n Service\n </span>\n )}\n {component.product_type === \"digital\" && (\n <span\n data-cimplify-composite-type-badge=\"digital\"\n className={cn(\n \"text-[10px] uppercase tracking-wider font-medium text-violet-600\",\n classNames?.typeBadge,\n classNames?.digitalBadge,\n )}\n >\n <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"inline-block w-3 h-3 mr-0.5 -mt-px\" aria-hidden=\"true\">\n <path d=\"M9 2L4 9h4l-1 5 5-7H8l1-5z\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\"/>\n </svg>\n Digital\n </span>\n )}\n {component.is_popular && (\n <span\n data-cimplify-composite-badge=\"popular\"\n className={cn(\"text-[10px] uppercase tracking-wider text-primary font-medium\", classNames?.badgePopular)}\n >\n Popular\n </span>\n )}\n {component.is_premium && (\n <span\n data-cimplify-composite-badge=\"premium\"\n className={cn(\"text-[10px] uppercase tracking-wider text-amber-600 font-medium\", classNames?.badgePremium)}\n >\n Premium\n </span>\n )}\n {component.display_description && (\n <span\n data-cimplify-composite-component-description\n className={cn(\"block text-xs text-muted-foreground truncate\", classNames?.componentDescription)}\n >\n {component.display_description}\n </span>\n )}\n {component.calories != null && (\n <span\n data-cimplify-composite-component-calories\n className={cn(\"block text-xs text-muted-foreground/60\", classNames?.componentCalories)}\n >\n {component.calories} cal\n </span>\n )}\n </div>\n\n {group.allow_quantity && isSelected && (\n <NumberField.Root\n value={qty}\n onValueChange={(val) => {\n if (val != null) {\n updateQuantity(group, component.id, val);\n }\n }}\n min={0}\n max={group.max_quantity_per_component || undefined}\n step={1}\n >\n <NumberField.Group\n data-cimplify-composite-qty\n onClick={(e: React.MouseEvent) => e.stopPropagation()}\n className={cn(\"flex items-center gap-2\", classNames?.qty)}\n >\n <NumberField.Decrement\n aria-label={`Decrease ${displayName} quantity`}\n className={cn(\"w-6 h-6 border border-border flex items-center justify-center text-xs hover:bg-muted disabled:opacity-30\", classNames?.qtyButton)}\n >\n −\n </NumberField.Decrement>\n <NumberField.Input\n readOnly\n className={cn(\"w-4 text-center text-sm font-medium bg-transparent border-none outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none\", classNames?.qtyValue)}\n />\n <NumberField.Increment\n aria-label={`Increase ${displayName} quantity`}\n className={cn(\"w-6 h-6 border border-border flex items-center justify-center text-xs hover:bg-muted disabled:opacity-30\", classNames?.qtyButton)}\n >\n +\n </NumberField.Increment>\n </NumberField.Group>\n </NumberField.Root>\n )}\n\n {component.price != null && (\n <span className=\"text-sm text-muted-foreground shrink-0\">\n +<Price amount={component.price} />\n </span>\n )}\n </Checkbox.Root>\n );\n })}\n </div>\n </div>\n );\n })}\n\n {priceResult && (\n <div\n data-cimplify-composite-summary\n className={cn(\"border-t border-border pt-4 space-y-1 text-sm\", classNames?.summary)}\n >\n {parsePrice(priceResult.base_price) !== 0 && (\n <div\n data-cimplify-composite-summary-line\n className={cn(\"flex justify-between text-muted-foreground\", classNames?.summaryLine)}\n >\n <span>Base</span>\n <Price amount={priceResult.base_price} />\n </div>\n )}\n {parsePrice(priceResult.components_total) !== 0 && (\n <div\n data-cimplify-composite-summary-line\n className={cn(\"flex justify-between text-muted-foreground\", classNames?.summaryLine)}\n >\n <span>Selections</span>\n <Price amount={priceResult.components_total} />\n </div>\n )}\n <div\n data-cimplify-composite-summary-total\n className={cn(\"flex justify-between font-medium pt-1 border-t border-border\", classNames?.summaryTotal)}\n >\n <span>Total</span>\n <Price amount={priceResult.final_price} className=\"text-primary\" />\n </div>\n </div>\n )}\n\n {isPriceLoading && (\n <div\n data-cimplify-composite-calculating\n className={cn(\"flex items-center gap-2 text-sm text-muted-foreground\", classNames?.calculating)}\n >\n Calculating price...\n </div>\n )}\n\n {priceError && !isPriceLoading && (\n <div\n data-cimplify-composite-price-error\n className={cn(\"text-sm text-destructive\", classNames?.priceError)}\n >\n Unable to calculate price\n </div>\n )}\n </div>\n );\n}\n"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|