@cimplify/sdk 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--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-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction: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;--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-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}.pointer-events-none{pointer-events:none}.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}.top-1\/2{top:50%}.z-10{z-index:10}.z-50{z-index:50}.container{width:100%}.mx-auto{margin-inline:auto}.-mt-px{margin-top:-1px}.ml-auto{margin-left:auto}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.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-\[3\/4\]{aspect-ratio:3/4}.aspect-\[4\/3\]{aspect-ratio:4/3}.aspect-\[5\/2\]{aspect-ratio:5/2}.aspect-\[16\/9\]{aspect-ratio:16/9}.aspect-square{aspect-ratio:1}.h-\[6px\]{height:6px}.h-\[7px\]{height:7px}.h-\[72px\]{height:72px}.h-fit{height:fit-content}.h-full{height:100%}.max-h-\[85vh\]{max-height:85vh}.w-1\/2{width:50%}.w-2\/5{width:40%}.w-3\/4{width:75%}.w-3\/5{width:60%}.w-4\/5{width:80%}.w-\[6px\]{width:6px}.w-\[7px\]{width:7px}.w-\[72px\]{width:72px}.w-full{width:100%}.flex-1{flex:1}.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.list-none{list-style-type:none}.\[appearance\:textfield\]{appearance:textfield}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-border>:not(:last-child)){border-color:var(--color-border,oklch(90% 0 0))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:var(--radius,.5rem)}.rounded-\[14px\]{border-radius:14px}.rounded-\[inherit\]{border-radius:inherit}.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-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-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))}.border-transparent{border-color:#0000}.border-t-foreground{border-top-color:var(--color-foreground,oklch(15% 0 0))}.bg-background{background-color:var(--color-background,oklch(99% 0 0))}.bg-background\/50{background-color:#fcfcfc80}@supports (color:color-mix(in lab, red, red)){.bg-background\/50{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 50%, transparent)}}.bg-background\/90{background-color:#fcfcfce6}@supports (color:color-mix(in lab, red, red)){.bg-background\/90{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 90%, transparent)}}.bg-background\/92{background-color:#fcfcfceb}@supports (color:color-mix(in lab, red, red)){.bg-background\/92{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 92%, transparent)}}.bg-border{background-color:var(--color-border,oklch(90% 0 0))}.bg-destructive{background-color:var(--color-destructive,oklch(50% .2 25))}.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-foreground{background-color:var(--color-foreground,oklch(15% 0 0))}.bg-muted{background-color:var(--color-muted,oklch(95% 0 0))}.bg-primary{background-color:var(--color-primary,oklch(50% .1 35))}.bg-primary\/5{background-color:#934c3a0d}@supports (color:color-mix(in lab, red, red)){.bg-primary\/5{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 5%, transparent)}}.bg-primary\/10{background-color:#934c3a1a}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 10%, transparent)}}.bg-transparent{background-color:#0000}.bg-gradient-to-t{--tw-gradient-position:to top in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.object-cover{object-fit:cover}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-\[inherit\]{font-family:inherit}.text-\[9px\]{font-size:9px}.text-\[10\.5px\]{font-size:10.5px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[14\.5px\]{font-size:14.5px}.text-\[14px\]{font-size:14px}.text-\[inherit\]{color:inherit}.text-background{color:var(--color-background,oklch(99% 0 0))}.text-border{color:var(--color-border,oklch(90% 0 0))}.text-destructive{color:var(--color-destructive,oklch(50% .2 25))}.text-destructive-foreground{color:var(--color-destructive-foreground,oklch(99% 0 0))}.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\/30{color:#6363634d}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/30{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 30%, transparent)}}.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))}.capitalize{text-transform:capitalize}.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,)}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.accent-foreground{accent-color:var(--color-foreground,oklch(15% 0 0))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-\[0_1px_3px_rgba\(0\,0\,0\,0\.04\)\,0_6px_24px_rgba\(0\,0\,0\,0\.06\)\]{--tw-shadow:0 1px 3px var(--tw-shadow-color,#0000000a), 0 6px 24px var(--tw-shadow-color,#0000000f);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_1px_4px_rgba\(0\,0\,0\,0\.10\)\,0_4px_12px_rgba\(0\,0\,0\,0\.08\)\]{--tw-shadow:0 1px 4px var(--tw-shadow-color,#0000001a), 0 4px 12px var(--tw-shadow-color,#00000014);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.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,)}.backdrop-blur-\[1px\]{--tw-backdrop-blur:blur(1px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.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-all{transition-property:all;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)}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-700{--tw-duration:.7s;transition-duration:.7s}.ease-\[cubic-bezier\(0\.19\,1\,0\.22\,1\)\]{--tw-ease:cubic-bezier(.19,1,.22,1);transition-timing-function:cubic-bezier(.19,1,.22,1)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.\[cimplify\:checkout\]{cimplify:checkout}@media (hover:hover){.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:scale-\[1\.04\]:is(:where(.group):hover *){scale:1.04}.group-hover\:text-foreground:is(:where(.group):hover *){color:var(--color-foreground,oklch(15% 0 0))}.group-hover\:text-primary:is(:where(.group):hover *){color:var(--color-primary,oklch(50% .1 35))}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.placeholder\:text-muted-foreground::placeholder{color:var(--color-muted-foreground,oklch(50% 0 0))}@media (hover:hover){.hover\:-translate-y-\[1px\]:hover{--tw-translate-y:calc(1px * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:\!scale-110:hover{--tw-scale-x:110%!important;--tw-scale-y:110%!important;--tw-scale-z:110%!important;scale:var(--tw-scale-x) var(--tw-scale-y)!important}.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:scale-\[1\.04\]:hover{scale:1.04}.hover\:border-primary\/20:hover{border-color:#934c3a33}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/20:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 20%, transparent)}}.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-foreground:hover{background-color:var(--color-foreground,oklch(15% 0 0))!important}.hover\:bg-background:hover{background-color:var(--color-background,oklch(99% 0 0))}.hover\:bg-destructive\/10:hover{background-color:#bb061e1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-destructive\/10:hover{background-color:color-mix(in oklab, var(--color-destructive,oklch(50% .2 25)) 10%, transparent)}}.hover\:bg-foreground\/90:hover{background-color:#0b0b0be6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-foreground\/90:hover{background-color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 90%, transparent)}}.hover\:bg-muted:hover{background-color:var(--color-muted,oklch(95% 0 0))}.hover\:bg-muted\/80:hover{background-color:#eeec}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/80:hover{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 80%, transparent)}}.hover\:bg-primary\/90:hover{background-color:#934c3ae6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 90%, transparent)}}.hover\:\!text-background:hover{color:var(--color-background,oklch(99% 0 0))!important}.hover\:text-destructive:hover{color:var(--color-destructive,oklch(50% .2 25))}.hover\:text-foreground:hover{color:var(--color-foreground,oklch(15% 0 0))}.hover\:text-primary:hover{color:var(--color-primary,oklch(50% .1 35))}.hover\:text-primary\/80:hover{color:#934c3acc}@supports (color:color-mix(in lab, red, red)){.hover\:text-primary\/80:hover{color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 80%, transparent)}}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-\[0_2px_6px_rgba\(0\,0\,0\,0\.04\)\,0_12px_40px_rgba\(0\,0\,0\,0\.10\)\]:hover{--tw-shadow:0 2px 6px var(--tw-shadow-color,#0000000a), 0 12px 40px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:border-primary:focus{border-color:var(--color-primary,oklch(50% .1 35))}.focus\:border-primary\/30:focus{border-color:#934c3a4d}@supports (color:color-mix(in lab, red, red)){.focus\:border-primary\/30:focus{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 30%, transparent)}}.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-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + 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-primary\/10:focus{--tw-ring-color:#934c3a1a}@supports (color:color-mix(in lab, red, red)){.focus\:ring-primary\/10:focus{--tw-ring-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 10%, transparent)}}.focus\:ring-primary\/20:focus{--tw-ring-color:#934c3a33}@supports (color:color-mix(in lab, red, red)){.focus\:ring-primary\/20:focus{--tw-ring-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 20%, transparent)}}.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}.data-\[checked\]\:border-primary[data-checked]{border-color:var(--color-primary,oklch(50% .1 35))}.data-\[checked\]\:bg-primary\/5[data-checked]{background-color:#934c3a0d}@supports (color:color-mix(in lab, red, red)){.data-\[checked\]\:bg-primary\/5[data-checked]{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 5%, transparent)}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}.\[\&\:\:-webkit-inner-spin-button\]\:appearance-none::-webkit-inner-spin-button{appearance:none}.\[\&\:\:-webkit-outer-spin-button\]\:appearance-none::-webkit-outer-spin-button{appearance:none}[open]>.\[\[open\]\>\&\]\:rotate-180{rotate:180deg}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@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-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@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-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}@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-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}
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-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--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-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction: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;--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-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}.pointer-events-none{pointer-events:none}.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}.top-1\/2{top:50%}.top-full{top:100%}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.container{width:100%}.mx-auto{margin-inline:auto}.-mt-px{margin-top:-1px}.ml-auto{margin-left:auto}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.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-\[3\/4\]{aspect-ratio:3/4}.aspect-\[4\/3\]{aspect-ratio:4/3}.aspect-\[5\/2\]{aspect-ratio:5/2}.aspect-\[16\/9\]{aspect-ratio:16/9}.aspect-square{aspect-ratio:1}.h-\[6px\]{height:6px}.h-\[7px\]{height:7px}.h-\[72px\]{height:72px}.h-fit{height:fit-content}.h-full{height:100%}.max-h-\[85vh\]{max-height:85vh}.w-1\/2{width:50%}.w-2\/5{width:40%}.w-3\/4{width:75%}.w-3\/5{width:60%}.w-4\/5{width:80%}.w-\[6px\]{width:6px}.w-\[7px\]{width:7px}.w-\[72px\]{width:72px}.w-full{width:100%}.flex-1{flex:1}.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-crosshair{cursor:crosshair}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.resize-none{resize:none}.list-none{list-style-type:none}.\[appearance\:textfield\]{appearance:textfield}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-border>:not(:last-child)){border-color:var(--color-border,oklch(90% 0 0))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:var(--radius,.5rem)}.rounded-\[14px\]{border-radius:14px}.rounded-\[inherit\]{border-radius:inherit}.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-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.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))}.border-transparent{border-color:#0000}.border-t-foreground{border-top-color:var(--color-foreground,oklch(15% 0 0))}.bg-background{background-color:var(--color-background,oklch(99% 0 0))}.bg-background\/50{background-color:#fcfcfc80}@supports (color:color-mix(in lab, red, red)){.bg-background\/50{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 50%, transparent)}}.bg-background\/90{background-color:#fcfcfce6}@supports (color:color-mix(in lab, red, red)){.bg-background\/90{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 90%, transparent)}}.bg-background\/92{background-color:#fcfcfceb}@supports (color:color-mix(in lab, red, red)){.bg-background\/92{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 92%, transparent)}}.bg-border{background-color:var(--color-border,oklch(90% 0 0))}.bg-destructive{background-color:var(--color-destructive,oklch(50% .2 25))}.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-foreground{background-color:var(--color-foreground,oklch(15% 0 0))}.bg-muted{background-color:var(--color-muted,oklch(95% 0 0))}.bg-primary{background-color:var(--color-primary,oklch(50% .1 35))}.bg-primary\/5{background-color:#934c3a0d}@supports (color:color-mix(in lab, red, red)){.bg-primary\/5{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 5%, transparent)}}.bg-primary\/10{background-color:#934c3a1a}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 10%, transparent)}}.bg-transparent{background-color:#0000}.bg-gradient-to-t{--tw-gradient-position:to top in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.object-cover{object-fit:cover}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-\[inherit\]{font-family:inherit}.text-\[9px\]{font-size:9px}.text-\[10\.5px\]{font-size:10.5px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[14\.5px\]{font-size:14.5px}.text-\[14px\]{font-size:14px}.text-\[inherit\]{color:inherit}.text-background{color:var(--color-background,oklch(99% 0 0))}.text-border{color:var(--color-border,oklch(90% 0 0))}.text-destructive{color:var(--color-destructive,oklch(50% .2 25))}.text-destructive-foreground{color:var(--color-destructive-foreground,oklch(99% 0 0))}.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\/30{color:#6363634d}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/30{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 30%, transparent)}}.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))}.capitalize{text-transform:capitalize}.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,)}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.accent-foreground{accent-color:var(--color-foreground,oklch(15% 0 0))}.accent-primary{accent-color:var(--color-primary,oklch(50% .1 35))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-\[0_1px_3px_rgba\(0\,0\,0\,0\.04\)\,0_6px_24px_rgba\(0\,0\,0\,0\.06\)\]{--tw-shadow:0 1px 3px var(--tw-shadow-color,#0000000a), 0 6px 24px var(--tw-shadow-color,#0000000f);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_1px_4px_rgba\(0\,0\,0\,0\.10\)\,0_4px_12px_rgba\(0\,0\,0\,0\.08\)\]{--tw-shadow:0 1px 4px var(--tw-shadow-color,#0000001a), 0 4px 12px var(--tw-shadow-color,#00000014);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.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,)}.backdrop-blur-\[1px\]{--tw-backdrop-blur:blur(1px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.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-all{transition-property:all;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)}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-700{--tw-duration:.7s;transition-duration:.7s}.ease-\[cubic-bezier\(0\.19\,1\,0\.22\,1\)\]{--tw-ease:cubic-bezier(.19,1,.22,1);transition-timing-function:cubic-bezier(.19,1,.22,1)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.\[cimplify\:checkout\]{cimplify:checkout}@media (hover:hover){.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:scale-\[1\.04\]:is(:where(.group):hover *){scale:1.04}.group-hover\:text-foreground:is(:where(.group):hover *){color:var(--color-foreground,oklch(15% 0 0))}.group-hover\:text-primary:is(:where(.group):hover *){color:var(--color-primary,oklch(50% .1 35))}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.placeholder\:text-muted-foreground::placeholder{color:var(--color-muted-foreground,oklch(50% 0 0))}@media (hover:hover){.hover\:-translate-y-\[1px\]:hover{--tw-translate-y:calc(1px * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:\!scale-110:hover{--tw-scale-x:110%!important;--tw-scale-y:110%!important;--tw-scale-z:110%!important;scale:var(--tw-scale-x) var(--tw-scale-y)!important}.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:scale-\[1\.04\]:hover{scale:1.04}.hover\:border-primary\/20:hover{border-color:#934c3a33}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/20:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 20%, transparent)}}.hover\:border-primary\/40:hover{border-color:#934c3a66}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/40:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 40%, transparent)}}.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-foreground:hover{background-color:var(--color-foreground,oklch(15% 0 0))!important}.hover\:bg-background:hover{background-color:var(--color-background,oklch(99% 0 0))}.hover\:bg-destructive\/10:hover{background-color:#bb061e1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-destructive\/10:hover{background-color:color-mix(in oklab, var(--color-destructive,oklch(50% .2 25)) 10%, transparent)}}.hover\:bg-foreground\/90:hover{background-color:#0b0b0be6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-foreground\/90:hover{background-color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 90%, transparent)}}.hover\:bg-muted:hover{background-color:var(--color-muted,oklch(95% 0 0))}.hover\:bg-muted\/70:hover{background-color:#eeeeeeb3}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/70:hover{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 70%, transparent)}}.hover\:bg-muted\/80:hover{background-color:#eeec}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/80:hover{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 80%, transparent)}}.hover\:bg-primary\/90:hover{background-color:#934c3ae6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 90%, transparent)}}.hover\:\!text-background:hover{color:var(--color-background,oklch(99% 0 0))!important}.hover\:text-destructive:hover{color:var(--color-destructive,oklch(50% .2 25))}.hover\:text-foreground:hover{color:var(--color-foreground,oklch(15% 0 0))}.hover\:text-primary:hover{color:var(--color-primary,oklch(50% .1 35))}.hover\:text-primary\/80:hover{color:#934c3acc}@supports (color:color-mix(in lab, red, red)){.hover\:text-primary\/80:hover{color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 80%, transparent)}}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-\[0_2px_6px_rgba\(0\,0\,0\,0\.04\)\,0_12px_40px_rgba\(0\,0\,0\,0\.10\)\]:hover{--tw-shadow:0 2px 6px var(--tw-shadow-color,#0000000a), 0 12px 40px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:border-primary:focus{border-color:var(--color-primary,oklch(50% .1 35))}.focus\:border-primary\/30:focus{border-color:#934c3a4d}@supports (color:color-mix(in lab, red, red)){.focus\:border-primary\/30:focus{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 30%, transparent)}}.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-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + 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-primary\/10:focus{--tw-ring-color:#934c3a1a}@supports (color:color-mix(in lab, red, red)){.focus\:ring-primary\/10:focus{--tw-ring-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 10%, transparent)}}.focus\:ring-primary\/20:focus{--tw-ring-color:#934c3a33}@supports (color:color-mix(in lab, red, red)){.focus\:ring-primary\/20:focus{--tw-ring-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 20%, transparent)}}.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}.data-\[checked\]\:border-primary[data-checked]{border-color:var(--color-primary,oklch(50% .1 35))}.data-\[checked\]\:bg-primary\/5[data-checked]{background-color:#934c3a0d}@supports (color:color-mix(in lab, red, red)){.data-\[checked\]\:bg-primary\/5[data-checked]{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 5%, transparent)}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}.\[\&\:\:-webkit-inner-spin-button\]\:appearance-none::-webkit-inner-spin-button{appearance:none}.\[\&\:\:-webkit-outer-spin-button\]\:appearance-none::-webkit-outer-spin-button{appearance:none}[open]>.\[\[open\]\>\&\]\:rotate-180{rotate:180deg}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@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-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@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-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}@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-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}
package/dist/utils.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, v as categorizePaymentError, B as detectMobileMoneyProvider, c as formatMoney, d as formatNumberCompact, f as formatPrice, a as formatPriceAdjustment, b as formatPriceCompact, j as formatPriceWithTax, e as formatProductPrice, m as getBasePrice, k as getCurrencySymbol, o as getDiscountPercentage, l as getDisplayPrice, q as getMarkupPercentage, r as getProductCurrency, g as getTaxAmount, h as hasTaxInfo, n as isOnSale, y as isPaymentStatusFailure, z as isPaymentStatusRequiresAction, A as isPaymentStatusSuccess, i as isTaxInclusive, F as isValidPhoneForContext, w as normalizePaymentResponse, D as normalizePhoneToE164, x as normalizeStatusResponse, p as parsePrice, E as resolvePhoneCountryCode } from './index-2JxW7TuQ.mjs';
2
- import './payment-5a21yzi7.mjs';
3
- import './price-CGPMlQUk.mjs';
1
+ export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, v as categorizePaymentError, B as detectMobileMoneyProvider, c as formatMoney, d as formatNumberCompact, f as formatPrice, a as formatPriceAdjustment, b as formatPriceCompact, j as formatPriceWithTax, e as formatProductPrice, m as getBasePrice, k as getCurrencySymbol, o as getDiscountPercentage, l as getDisplayPrice, q as getMarkupPercentage, r as getProductCurrency, g as getTaxAmount, h as hasTaxInfo, n as isOnSale, y as isPaymentStatusFailure, z as isPaymentStatusRequiresAction, A as isPaymentStatusSuccess, i as isTaxInclusive, F as isValidPhoneForContext, w as normalizePaymentResponse, D as normalizePhoneToE164, x as normalizeStatusResponse, p as parsePrice, E as resolvePhoneCountryCode } from './index-D4-b5OZO.mjs';
2
+ import './payment-BeLHpQMo.mjs';
3
+ import './price-Dvl1p9AM.mjs';
4
4
  import 'libphonenumber-js';
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, v as categorizePaymentError, B as detectMobileMoneyProvider, c as formatMoney, d as formatNumberCompact, f as formatPrice, a as formatPriceAdjustment, b as formatPriceCompact, j as formatPriceWithTax, e as formatProductPrice, m as getBasePrice, k as getCurrencySymbol, o as getDiscountPercentage, l as getDisplayPrice, q as getMarkupPercentage, r as getProductCurrency, g as getTaxAmount, h as hasTaxInfo, n as isOnSale, y as isPaymentStatusFailure, z as isPaymentStatusRequiresAction, A as isPaymentStatusSuccess, i as isTaxInclusive, F as isValidPhoneForContext, w as normalizePaymentResponse, D as normalizePhoneToE164, x as normalizeStatusResponse, p as parsePrice, E as resolvePhoneCountryCode } from './index-DBM89ktY.js';
2
- import './payment-5a21yzi7.js';
3
- import './price-BS0Z5Oi9.js';
1
+ export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, v as categorizePaymentError, B as detectMobileMoneyProvider, c as formatMoney, d as formatNumberCompact, f as formatPrice, a as formatPriceAdjustment, b as formatPriceCompact, j as formatPriceWithTax, e as formatProductPrice, m as getBasePrice, k as getCurrencySymbol, o as getDiscountPercentage, l as getDisplayPrice, q as getMarkupPercentage, r as getProductCurrency, g as getTaxAmount, h as hasTaxInfo, n as isOnSale, y as isPaymentStatusFailure, z as isPaymentStatusRequiresAction, A as isPaymentStatusSuccess, i as isTaxInclusive, F as isValidPhoneForContext, w as normalizePaymentResponse, D as normalizePhoneToE164, x as normalizeStatusResponse, p as parsePrice, E as resolvePhoneCountryCode } from './index-Cjfczg2W.js';
2
+ import './payment-BeLHpQMo.js';
3
+ import './price-C6gfys5B.js';
4
4
  import 'libphonenumber-js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cimplify/sdk",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
4
4
  "description": "Cimplify Commerce SDK for storefronts",
5
5
  "keywords": [
6
6
  "cimplify",
@@ -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 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}&times; {sel.product_name}</span>\n {sel.product_type === \"service\" && sel.scheduling?.scheduled_start && (\n <span> &mdash; {new Date(sel.scheduling.scheduled_start).toLocaleDateString()}</span>\n )}\n {sel.product_type === \"digital\" && <span> &mdash; 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}&times; {sel.component_name}</span>\n {sel.product_type === \"service\" && sel.scheduling?.scheduled_start && (\n <span> &mdash; {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"
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 { INPUT_FIELD_TYPE } 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 customerInputs?: 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 {/* Customer input values */}\n {item.customerInputs && item.customerInputs.length > 0 && (\n <div data-cimplify-cart-customer-inputs className={classNames?.customerInputs}>\n {item.customerInputs.map((input) => (\n <div key={input.field_id} data-cimplify-cart-input>\n <span data-cimplify-cart-input-label>{input.field_name}</span>\n <span data-cimplify-cart-input-value>\n {input.field_type === INPUT_FIELD_TYPE.File || input.field_type === INPUT_FIELD_TYPE.Image\n ? String(input.value).split(\"/\").pop() || \"Uploaded file\"\n : String(input.value)}\n </span>\n </div>\n ))}\n </div>\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}&times; {sel.product_name}</span>\n {sel.product_type === \"service\" && sel.scheduling?.scheduled_start && (\n <span> &mdash; {new Date(sel.scheduling.scheduled_start).toLocaleDateString()}</span>\n )}\n {sel.product_type === \"digital\" && <span> &mdash; 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}&times; {sel.component_name}</span>\n {sel.product_type === \"service\" && sel.scheduling?.scheduled_start && (\n <span> &mdash; {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
  }
@@ -11,7 +11,7 @@
11
11
  "files": [
12
12
  {
13
13
  "path": "product-card.tsx",
14
- "content": "\"use client\";\n\nimport React, { useCallback, useState } from \"react\";\nimport { Dialog } from \"@base-ui/react/dialog\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from \"@cimplify/sdk\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { ProductSheet } from \"@cimplify/sdk/react\";\nimport { CardVariant } from \"@cimplify/sdk/react\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { FoodProductCard } from \"@cimplify/sdk/react\";\nimport { RetailProductCard } from \"@cimplify/sdk/react\";\nimport { WholesaleProductCard } from \"@cimplify/sdk/react\";\nimport { DigitalProductCard } from \"@cimplify/sdk/react\";\nimport { StandardServiceCard } from \"@cimplify/sdk/react\";\nimport { RentalServiceCard } from \"@cimplify/sdk/react\";\nimport { AccommodationCard } from \"@cimplify/sdk/react\";\nimport { LeaseServiceCard } from \"@cimplify/sdk/react\";\nimport { SubscriptionCard } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\nexport interface ProductCardClassNames {\n root?: string;\n imageContainer?: string;\n image?: string;\n body?: string;\n name?: string;\n description?: string;\n price?: string;\n badges?: string;\n badge?: string;\n modal?: string;\n modalOverlay?: string;\n}\n\nexport interface ProductCardProps {\n /** The product to display. */\n product: Product;\n /** Display mode: \"card\" opens a modal, \"page\" renders as a link. Auto-detected from product.display_mode. */\n displayMode?: \"card\" | \"page\";\n /** Explicit card variant. Auto-detected from product data when omitted. */\n variant?: CardVariant;\n /** Per-slug card map. Highest priority — maps a product slug to a custom card component. */\n cards?: Record<string, React.ComponentType<CardLayoutProps>>;\n /** Link href for page mode. Default: `/menu/${product.slug}` */\n href?: string;\n /** Custom modal content renderer. Receives the fully-loaded product and a close callback. */\n renderModal?: (product: ProductWithDetails, onClose: () => void) => React.ReactNode;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n /** Custom link renderer for page mode (e.g. Next.js Link). */\n renderLink?: (props: {\n href: string;\n className?: string;\n children: React.ReactNode;\n }) => React.ReactElement;\n /** Called when quick-add button is clicked (if the resolved card supports it). */\n onQuickAdd?: (product: Product) => void;\n /** Replace the entire default card body. */\n children?: React.ReactNode;\n /** Image aspect ratio. Default: \"4/3\". */\n aspectRatio?: \"square\" | \"4/3\" | \"16/10\" | \"3/4\";\n className?: string;\n classNames?: ProductCardClassNames;\n}\n\nconst BUILT_IN_CARDS: Record<string, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nfunction resolveCardVariant(product: Product): CardVariant {\n if (product.quantity_pricing && product.quantity_pricing.length > 1) {\n return CardVariant.Wholesale;\n }\n if (product.type === PRODUCT_TYPE.Digital) {\n return CardVariant.Digital;\n }\n if (product.type === PRODUCT_TYPE.Service) {\n if (product.duration_unit && RENTAL_UNITS.has(product.duration_unit)) {\n return CardVariant.Rental;\n }\n if (product.duration_unit === DURATION_UNIT.Nights) {\n return CardVariant.Accommodation;\n }\n if (product.duration_unit && LEASE_UNITS.has(product.duration_unit)) {\n return CardVariant.Lease;\n }\n if (product.billing_plans && product.billing_plans.length > 0 && !product.duration_minutes) {\n return CardVariant.Subscription;\n }\n return CardVariant.Standard;\n }\n if (product.render_hint === RENDER_HINT.Food) {\n return CardVariant.Food;\n }\n if (product.render_hint === RENDER_HINT.Physical) {\n return CardVariant.Retail;\n }\n return CardVariant.Retail;\n}\n\n/**\n * ProductCard — a product display card with two modes:\n *\n * - **card** (default): clickable button that opens a Base UI Dialog modal\n * - **page**: a plain `<a>` link for SEO-friendly product pages\n */\nexport function ProductCard({\n product,\n displayMode,\n variant,\n cards,\n href,\n renderModal,\n renderImage,\n renderLink,\n onQuickAdd,\n children,\n aspectRatio = \"4/3\",\n className,\n classNames,\n}: ProductCardProps): React.ReactElement {\n const mode = displayMode ?? product.display_mode ?? \"card\";\n const [isOpen, setIsOpen] = useState(false);\n const [shouldFetch, setShouldFetch] = useState(false);\n\n // Prefetch on pointer enter, always fetch when open\n const { product: productDetails } = useProduct(\n product.slug ?? product.id,\n { enabled: shouldFetch || isOpen },\n );\n\n const handlePrefetch = useCallback(() => {\n setShouldFetch(true);\n }, []);\n\n const handleOpenChange = useCallback((open: boolean) => {\n setIsOpen(open);\n if (open) {\n setShouldFetch(true);\n }\n }, []);\n\n const handleClose = useCallback(() => {\n setIsOpen(false);\n }, []);\n\n const imageUrl = product.image_url || product.images?.[0];\n\n const cardBody = (() => {\n if (children) return children;\n\n // Resolve card variant: slug map → explicit variant → auto-detect\n const SlugCard = cards?.[product.slug];\n if (SlugCard) {\n return <SlugCard product={product} onQuickAdd={onQuickAdd} renderImage={renderImage} renderLink={renderLink} />;\n }\n\n const key = variant ?? resolveCardVariant(product);\n const ResolvedCard = BUILT_IN_CARDS[key];\n if (ResolvedCard) {\n return <ResolvedCard product={product} onQuickAdd={onQuickAdd} renderImage={renderImage} renderLink={renderLink} />;\n }\n\n // Fallback: minimal default\n return (\n <>\n {imageUrl && (\n <div\n data-cimplify-product-card-image-container\n className={cn(\"overflow-hidden rounded-t-xl\", classNames?.imageContainer)}\n style={ASPECT_STYLES[aspectRatio]}\n >\n {renderImage ? (\n renderImage({ src: imageUrl, alt: product.name, className: classNames?.image })\n ) : (\n <img\n src={imageUrl}\n alt={product.name}\n className={cn(\"w-full h-full object-cover transition-transform duration-300 group-hover:scale-105\", classNames?.image)}\n data-cimplify-product-card-image\n />\n )}\n </div>\n )}\n <div data-cimplify-product-card-body className={cn(\"p-4 flex flex-col gap-1\", classNames?.body)}>\n <span data-cimplify-product-card-name className={cn(\"font-semibold text-foreground leading-tight\", classNames?.name)}>\n {product.name}\n </span>\n {product.description && (\n <span data-cimplify-product-card-description className={cn(\"text-sm text-muted-foreground line-clamp-2\", classNames?.description)}>\n {product.description}\n </span>\n )}\n <Price amount={product.default_price} className={cn(\"text-sm font-medium text-foreground mt-1\", classNames?.price)} />\n </div>\n </>\n );\n })();\n\n // Page mode — render as a link\n if (mode === \"page\") {\n const linkHref = href ?? `/menu/${product.slug}`;\n const linkClassName = cn(\"group block no-underline text-[inherit] rounded-xl border border-border bg-background transition-shadow duration-200 hover:shadow-md\", className, classNames?.root);\n\n if (renderLink) {\n return renderLink({ href: linkHref, className: linkClassName, children: cardBody });\n }\n\n return (\n <a\n href={linkHref}\n data-cimplify-product-card\n data-display-mode=\"page\"\n className={linkClassName}\n >\n {cardBody}\n </a>\n );\n }\n\n // Card mode — render as Base UI Dialog\n return (\n <Dialog.Root open={isOpen} onOpenChange={handleOpenChange}>\n <Dialog.Trigger\n onPointerEnter={handlePrefetch}\n data-cimplify-product-card\n data-display-mode=\"card\"\n className={cn(\n \"group block w-full text-left p-0 cursor-pointer font-[inherit] text-[inherit] rounded-xl border border-border bg-background transition-shadow duration-200 hover:shadow-md\",\n className,\n classNames?.root,\n )}\n >\n {cardBody}\n </Dialog.Trigger>\n\n <Dialog.Portal>\n <Dialog.Backdrop\n data-cimplify-product-card-backdrop\n className={cn(\n \"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm transition-opacity\",\n classNames?.modalOverlay,\n )}\n />\n <Dialog.Popup\n data-cimplify-product-card-modal\n className={cn(\n \"fixed z-50 rounded-2xl p-0 max-w-lg w-full h-fit max-h-[85vh] overflow-auto bg-background shadow-2xl outline-none\",\n classNames?.modal,\n )}\n >\n {isOpen && (\n productDetails ? (\n renderModal ? (\n renderModal(productDetails, handleClose)\n ) : (\n <ProductSheet\n product={productDetails}\n onClose={handleClose}\n renderImage={renderImage}\n />\n )\n ) : (\n <div\n data-cimplify-product-card-modal-loading\n aria-busy=\"true\"\n className=\"flex flex-col\"\n >\n <div className=\"aspect-[5/2] bg-muted animate-pulse\" />\n <div className=\"p-6 space-y-3\">\n <div className=\"h-5 w-3/5 bg-muted rounded animate-pulse\" />\n <div className=\"h-7 w-2/5 bg-muted rounded animate-pulse\" />\n <div className=\"h-4 w-4/5 bg-muted rounded animate-pulse\" />\n </div>\n <div className=\"mx-6 border-t border-border pt-4 pb-6\">\n <div className=\"h-14 bg-muted rounded-full animate-pulse\" />\n </div>\n </div>\n )\n )}\n </Dialog.Popup>\n </Dialog.Portal>\n </Dialog.Root>\n );\n}\n"
14
+ "content": "\"use client\";\n\nimport React, { useCallback, useState } from \"react\";\nimport { Dialog } from \"@base-ui/react/dialog\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from \"@cimplify/sdk\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { ProductSheet } from \"@cimplify/sdk/react\";\nimport { CardVariant } from \"@cimplify/sdk/react\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { FoodProductCard } from \"@cimplify/sdk/react\";\nimport { RetailProductCard } from \"@cimplify/sdk/react\";\nimport { WholesaleProductCard } from \"@cimplify/sdk/react\";\nimport { DigitalProductCard } from \"@cimplify/sdk/react\";\nimport { StandardServiceCard } from \"@cimplify/sdk/react\";\nimport { RentalServiceCard } from \"@cimplify/sdk/react\";\nimport { AccommodationCard } from \"@cimplify/sdk/react\";\nimport { LeaseServiceCard } from \"@cimplify/sdk/react\";\nimport { SubscriptionCard } from \"@cimplify/sdk/react\";\nimport { BundleProductCard } from \"./cards/bundle-product-card\";\nimport { CompositeProductCard } from \"./cards/composite-product-card\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\nexport interface ProductCardClassNames {\n root?: string;\n imageContainer?: string;\n image?: string;\n body?: string;\n name?: string;\n description?: string;\n price?: string;\n badges?: string;\n badge?: string;\n modal?: string;\n modalOverlay?: string;\n}\n\nexport interface ProductCardProps {\n /** The product to display. */\n product: Product;\n /** Display mode: \"card\" opens a modal, \"page\" renders as a link. Auto-detected from product.display_mode. */\n displayMode?: \"card\" | \"page\";\n /** Explicit card variant. Auto-detected from product data when omitted. */\n variant?: CardVariant;\n /** Per-slug card map. Highest priority — maps a product slug to a custom card component. */\n cards?: Record<string, React.ComponentType<CardLayoutProps>>;\n /** Link href for page mode. Default: `/menu/${product.slug}` */\n href?: string;\n /** Custom modal content renderer. Receives the fully-loaded product and a close callback. */\n renderModal?: (product: ProductWithDetails, onClose: () => void) => React.ReactNode;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n /** Custom link renderer for page mode (e.g. Next.js Link). */\n renderLink?: (props: {\n href: string;\n className?: string;\n children: React.ReactNode;\n }) => React.ReactElement;\n /** Called when quick-add button is clicked (if the resolved card supports it). */\n onQuickAdd?: (product: Product) => void;\n /** Replace the entire default card body. */\n children?: React.ReactNode;\n /** Image aspect ratio. Default: \"4/3\". */\n aspectRatio?: \"square\" | \"4/3\" | \"16/10\" | \"3/4\";\n className?: string;\n classNames?: ProductCardClassNames;\n}\n\nconst BUILT_IN_CARDS: Record<string, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Bundle]: BundleProductCard,\n [CardVariant.Composite]: CompositeProductCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nfunction resolveCardVariant(product: Product): CardVariant {\n if (product.type === PRODUCT_TYPE.Bundle) {\n return CardVariant.Bundle;\n }\n if (product.type === PRODUCT_TYPE.Composite) {\n return CardVariant.Composite;\n }\n if (product.quantity_pricing && product.quantity_pricing.length > 1) {\n return CardVariant.Wholesale;\n }\n if (product.type === PRODUCT_TYPE.Digital) {\n return CardVariant.Digital;\n }\n if (product.type === PRODUCT_TYPE.Service) {\n if (product.duration_unit && RENTAL_UNITS.has(product.duration_unit)) {\n return CardVariant.Rental;\n }\n if (product.duration_unit === DURATION_UNIT.Nights) {\n return CardVariant.Accommodation;\n }\n if (product.duration_unit && LEASE_UNITS.has(product.duration_unit)) {\n return CardVariant.Lease;\n }\n if (product.billing_plans && product.billing_plans.length > 0 && !product.duration_minutes) {\n return CardVariant.Subscription;\n }\n return CardVariant.Standard;\n }\n if (product.render_hint === RENDER_HINT.Food) {\n return CardVariant.Food;\n }\n if (product.render_hint === RENDER_HINT.Physical) {\n return CardVariant.Retail;\n }\n return CardVariant.Retail;\n}\n\n/**\n * ProductCard — a product display card with two modes:\n *\n * - **card** (default): clickable button that opens a Base UI Dialog modal\n * - **page**: a plain `<a>` link for SEO-friendly product pages\n */\nexport function ProductCard({\n product,\n displayMode,\n variant,\n cards,\n href,\n renderModal,\n renderImage,\n renderLink,\n onQuickAdd,\n children,\n aspectRatio = \"4/3\",\n className,\n classNames,\n}: ProductCardProps): React.ReactElement {\n const mode = displayMode ?? product.display_mode ?? \"card\";\n const [isOpen, setIsOpen] = useState(false);\n const [shouldFetch, setShouldFetch] = useState(false);\n\n // Prefetch on pointer enter, always fetch when open\n const { product: productDetails } = useProduct(\n product.slug ?? product.id,\n { enabled: shouldFetch || isOpen },\n );\n\n const handlePrefetch = useCallback(() => {\n setShouldFetch(true);\n }, []);\n\n const handleOpenChange = useCallback((open: boolean) => {\n setIsOpen(open);\n if (open) {\n setShouldFetch(true);\n }\n }, []);\n\n const handleClose = useCallback(() => {\n setIsOpen(false);\n }, []);\n\n const imageUrl = product.image_url || product.images?.[0];\n\n const cardBody = (() => {\n if (children) return children;\n\n // Resolve card variant: slug map → explicit variant → auto-detect\n const SlugCard = cards?.[product.slug];\n if (SlugCard) {\n return <SlugCard product={product} onQuickAdd={onQuickAdd} renderImage={renderImage} renderLink={renderLink} />;\n }\n\n const key = variant ?? resolveCardVariant(product);\n const ResolvedCard = BUILT_IN_CARDS[key];\n if (ResolvedCard) {\n return <ResolvedCard product={product} onQuickAdd={onQuickAdd} renderImage={renderImage} renderLink={renderLink} />;\n }\n\n // Fallback: minimal default\n return (\n <>\n {imageUrl && (\n <div\n data-cimplify-product-card-image-container\n className={cn(\"overflow-hidden rounded-t-xl\", classNames?.imageContainer)}\n style={ASPECT_STYLES[aspectRatio]}\n >\n {renderImage ? (\n renderImage({ src: imageUrl, alt: product.name, className: classNames?.image })\n ) : (\n <img\n src={imageUrl}\n alt={product.name}\n className={cn(\"w-full h-full object-cover transition-transform duration-300 group-hover:scale-105\", classNames?.image)}\n data-cimplify-product-card-image\n />\n )}\n </div>\n )}\n <div data-cimplify-product-card-body className={cn(\"p-4 flex flex-col gap-1\", classNames?.body)}>\n <span data-cimplify-product-card-name className={cn(\"font-semibold text-foreground leading-tight\", classNames?.name)}>\n {product.name}\n </span>\n {product.description && (\n <span data-cimplify-product-card-description className={cn(\"text-sm text-muted-foreground line-clamp-2\", classNames?.description)}>\n {product.description}\n </span>\n )}\n <Price amount={product.default_price} className={cn(\"text-sm font-medium text-foreground mt-1\", classNames?.price)} />\n </div>\n </>\n );\n })();\n\n // Page mode — render as a link\n if (mode === \"page\") {\n const linkHref = href ?? `/menu/${product.slug}`;\n const linkClassName = cn(\"group block no-underline text-[inherit] rounded-xl border border-border bg-background transition-shadow duration-200 hover:shadow-md\", className, classNames?.root);\n\n if (renderLink) {\n return renderLink({ href: linkHref, className: linkClassName, children: cardBody });\n }\n\n return (\n <a\n href={linkHref}\n data-cimplify-product-card\n data-display-mode=\"page\"\n className={linkClassName}\n >\n {cardBody}\n </a>\n );\n }\n\n // Card mode — render as Base UI Dialog\n return (\n <Dialog.Root open={isOpen} onOpenChange={handleOpenChange}>\n <Dialog.Trigger\n onPointerEnter={handlePrefetch}\n data-cimplify-product-card\n data-display-mode=\"card\"\n className={cn(\n \"group block w-full text-left p-0 cursor-pointer font-[inherit] text-[inherit] rounded-xl border border-border bg-background transition-shadow duration-200 hover:shadow-md\",\n className,\n classNames?.root,\n )}\n >\n {cardBody}\n </Dialog.Trigger>\n\n <Dialog.Portal>\n <Dialog.Backdrop\n data-cimplify-product-card-backdrop\n className={cn(\n \"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm transition-opacity\",\n classNames?.modalOverlay,\n )}\n />\n <Dialog.Popup\n data-cimplify-product-card-modal\n className={cn(\n \"fixed z-50 rounded-2xl p-0 max-w-lg w-full h-fit max-h-[85vh] overflow-auto bg-background shadow-2xl outline-none\",\n classNames?.modal,\n )}\n >\n {isOpen && (\n productDetails ? (\n renderModal ? (\n renderModal(productDetails, handleClose)\n ) : (\n <ProductSheet\n product={productDetails}\n onClose={handleClose}\n renderImage={renderImage}\n />\n )\n ) : (\n <div\n data-cimplify-product-card-modal-loading\n aria-busy=\"true\"\n className=\"flex flex-col\"\n >\n <div className=\"aspect-[5/2] bg-muted animate-pulse\" />\n <div className=\"p-6 space-y-3\">\n <div className=\"h-5 w-3/5 bg-muted rounded animate-pulse\" />\n <div className=\"h-7 w-2/5 bg-muted rounded animate-pulse\" />\n <div className=\"h-4 w-4/5 bg-muted rounded animate-pulse\" />\n </div>\n <div className=\"mx-6 border-t border-border pt-4 pb-6\">\n <div className=\"h-14 bg-muted rounded-full animate-pulse\" />\n </div>\n </div>\n )\n )}\n </Dialog.Popup>\n </Dialog.Portal>\n </Dialog.Root>\n );\n}\n"
15
15
  }
16
16
  ]
17
17
  }
@@ -14,7 +14,7 @@
14
14
  "files": [
15
15
  {
16
16
  "path": "product-customizer.tsx",
17
- "content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect, useRef } from \"react\";\nimport { Button } from \"@base-ui/react/button\";\nimport type {\n ProductWithDetails,\n VariantView,\n AddOnOption,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport type { ProductBillingPlan } from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice, getUnitPriceAtQuantity } from \"@cimplify/sdk\";\nimport { useCart, useQuote } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { QuantitySelector } from \"@cimplify/sdk/react\";\nimport { VariantSelector } from \"@cimplify/sdk/react\";\nimport { AddOnSelector } from \"@cimplify/sdk/react\";\nimport { CompositeSelector } from \"@cimplify/sdk/react\";\nimport { BundleSelector } from \"@cimplify/sdk/react\";\nimport { BillingPlanSelector } from \"@cimplify/sdk/react\";\nimport { VolumePricing } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ProductCustomizerClassNames {\n root?: string;\n actions?: string;\n submitButton?: string;\n submitButtonAdded?: string;\n validation?: string;\n specialInstructions?: string;\n depositInfo?: string;\n allergens?: string;\n duration?: string;\n}\n\nexport interface ProductCustomizerProps {\n product: ProductWithDetails;\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n showSpecialInstructions?: boolean;\n className?: string;\n classNames?: ProductCustomizerClassNames;\n}\n\nexport function ProductCustomizer({\n product,\n onAddToCart,\n showSpecialInstructions = true,\n className,\n classNames,\n}: ProductCustomizerProps): React.ReactElement {\n const [quantity, setQuantity] = useState(product.min_order_quantity ?? 1);\n const [isAdded, setIsAdded] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [selectedVariantId, setSelectedVariantId] = useState<string | undefined>();\n const [selectedVariant, setSelectedVariant] = useState<VariantView | undefined>();\n const [selectedAddOnOptionIds, setSelectedAddOnOptionIds] = useState<string[]>([]);\n\n const [compositeSelections, setCompositeSelections] = useState<ComponentSelectionInput[]>([]);\n const [compositePrice, setCompositePrice] = useState<CompositePriceResult | null>(null);\n const [compositeReady, setCompositeReady] = useState(false);\n\n const [bundleSelections, setBundleSelections] = useState<BundleSelectionInput[]>([]);\n const [bundleTotalPrice, setBundleTotalPrice] = useState<number | null>(null);\n const [bundleReady, setBundleReady] = useState(false);\n const [selectedBillingPlan, setSelectedBillingPlan] = useState<ProductBillingPlan | null>(null);\n const [specialInstructions, setSpecialInstructions] = useState(\"\");\n\n const cart = useCart();\n\n const productType = product.type || \"product\";\n const isComposite = productType === \"composite\";\n const isBundle = productType === \"bundle\";\n const isDigital = productType === \"digital\";\n const isService = productType === \"service\";\n const isStandard = !isComposite && !isBundle;\n\n const hasVariants = isStandard && product.variants && product.variants.length > 0;\n const hasAddOns = isStandard && product.add_ons && product.add_ons.length > 0;\n\n useEffect(() => {\n setQuantity(product.min_order_quantity ?? 1);\n setIsAdded(false);\n setIsSubmitting(false);\n setSelectedVariantId(undefined);\n setSelectedVariant(undefined);\n setSelectedAddOnOptionIds([]);\n setCompositeSelections([]);\n setCompositePrice(null);\n setCompositeReady(false);\n setBundleSelections([]);\n setBundleTotalPrice(null);\n setBundleReady(false);\n setSelectedBillingPlan(null);\n setSpecialInstructions(\"\");\n }, [product.id, product.min_order_quantity]);\n\n const selectedAddOnOptions = useMemo(() => {\n if (!product.add_ons) return [];\n const options: AddOnOption[] = [];\n for (const addOn of product.add_ons) {\n for (const option of addOn.options) {\n if (selectedAddOnOptionIds.includes(option.id)) {\n options.push(option);\n }\n }\n }\n return options;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const normalizedAddOnOptionIds = useMemo(() => {\n if (selectedAddOnOptionIds.length === 0) return [];\n return Array.from(new Set(selectedAddOnOptionIds.map((id) => id.trim()).filter(Boolean))).sort();\n }, [selectedAddOnOptionIds]);\n\n const localTotalPrice = useMemo(() => {\n if (isComposite && compositePrice) {\n return parsePrice(compositePrice.final_price) * quantity;\n }\n\n if (isBundle && bundleTotalPrice != null) {\n return bundleTotalPrice * quantity;\n }\n\n let price = parsePrice(product.default_price);\n\n if (selectedVariant?.price_adjustment) {\n price += parsePrice(selectedVariant.price_adjustment);\n }\n\n for (const option of selectedAddOnOptions) {\n if (option.default_price) {\n price += parsePrice(option.default_price);\n }\n }\n\n return price * quantity;\n }, [product.default_price, selectedVariant, selectedAddOnOptions, quantity, isComposite, compositePrice, isBundle, bundleTotalPrice]);\n\n const requiredAddOnsSatisfied = useMemo(() => {\n if (!product.add_ons) return true;\n\n for (const addOn of product.add_ons) {\n if (addOn.is_required) {\n const selectedInGroup = selectedAddOnOptionIds.filter((id) =>\n addOn.options.some((opt) => opt.id === id),\n ).length;\n\n const minRequired = addOn.min_selections || 1;\n if (selectedInGroup < minRequired) {\n return false;\n }\n }\n }\n return true;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const quoteInput = useMemo(\n () => ({\n productId: product.id,\n quantity,\n variantId: selectedVariantId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n }),\n [product.id, quantity, selectedVariantId, normalizedAddOnOptionIds, isBundle, bundleSelections, isComposite, compositeSelections],\n );\n\n const quoteEnabled = isComposite\n ? compositeReady\n : isBundle\n ? bundleReady\n : requiredAddOnsSatisfied;\n\n const { quote } = useQuote(quoteInput, {\n enabled: quoteEnabled,\n });\n\n const quoteId = quote?.quote_id;\n const quotedTotalPrice = useMemo(() => {\n if (!quote) return undefined;\n\n const priceInfo = quote.quoted_total_price_info ?? quote.final_price_info;\n const perUnit = priceInfo.pre_tax_price;\n if (perUnit === undefined || perUnit === null) return undefined;\n return parsePrice(perUnit) * quantity;\n }, [quote, quantity]);\n\n const displayTotalPrice = quotedTotalPrice ?? localTotalPrice;\n\n const handleVariantChange = useCallback(\n (variantId: string | undefined, variant: VariantView | undefined) => {\n setSelectedVariantId(variantId);\n setSelectedVariant(variant);\n },\n [],\n );\n\n const addedTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n useEffect(() => () => clearTimeout(addedTimerRef.current), []);\n\n const handleAddToCart = useCallback(async () => {\n if (isSubmitting) return;\n\n setIsSubmitting(true);\n\n const options: AddToCartOptions = {\n variantId: selectedVariantId,\n variant: selectedVariant\n ? { id: selectedVariant.id, name: selectedVariant.name || \"\", price_adjustment: selectedVariant.price_adjustment }\n : undefined,\n quoteId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n addOnOptions: selectedAddOnOptions.length > 0\n ? selectedAddOnOptions.map((opt) => ({\n id: opt.id,\n name: opt.name,\n add_on_id: opt.add_on_id,\n default_price: opt.default_price,\n }))\n : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n billingPlanId: selectedBillingPlan?.id,\n specialInstructions: specialInstructions.trim() || undefined,\n };\n\n try {\n if (onAddToCart) {\n await onAddToCart(product, quantity, options);\n } else {\n await cart.addItem(product, quantity, options);\n }\n setIsAdded(true);\n clearTimeout(addedTimerRef.current);\n addedTimerRef.current = setTimeout(() => {\n setIsAdded(false);\n setQuantity(product.min_order_quantity ?? 1);\n }, 2000);\n } catch {\n // Caller handles errors via onAddToCart or cart hook error state\n } finally {\n setIsSubmitting(false);\n }\n }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, specialInstructions, isSubmitting, onAddToCart, cart]);\n\n return (\n <div data-cimplify-customizer className={cn(\"space-y-6\", className, classNames?.root)}>\n {isComposite && product.groups && product.composite_id && (\n <CompositeSelector\n compositeId={product.composite_id}\n groups={product.groups}\n onSelectionsChange={setCompositeSelections}\n onPriceChange={setCompositePrice}\n onReady={setCompositeReady}\n skipPriceFetch\n />\n )}\n\n {isBundle && product.components && (\n <BundleSelector\n components={product.components}\n bundlePrice={product.bundle_price}\n discountValue={product.discount_value}\n pricingType={product.pricing_type}\n onSelectionsChange={setBundleSelections}\n onPriceChange={setBundleTotalPrice}\n onReady={setBundleReady}\n />\n )}\n\n {hasVariants && (\n <VariantSelector\n variants={product.variants!}\n variantAxes={product.variant_axes}\n basePrice={product.default_price}\n selectedVariantId={selectedVariantId}\n onVariantChange={handleVariantChange}\n productName={product.name}\n />\n )}\n\n {hasAddOns && (\n <AddOnSelector\n addOns={product.add_ons!}\n selectedOptions={selectedAddOnOptionIds}\n onOptionsChange={setSelectedAddOnOptionIds}\n />\n )}\n\n {/* Billing plans */}\n {product.billing_plans && product.billing_plans.length > 0 && (\n <BillingPlanSelector\n productId={product.id}\n plans={product.billing_plans}\n onPlanSelect={setSelectedBillingPlan}\n selectedPlanId={selectedBillingPlan?.id ?? null}\n showOneTimePurchase\n />\n )}\n\n {/* Volume pricing tiers */}\n {product.quantity_pricing && product.quantity_pricing.length > 1 && (\n <VolumePricing\n tiers={product.quantity_pricing}\n currentQuantity={quantity}\n />\n )}\n\n {/* Deposit info for service products */}\n {product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount && (\n <div data-cimplify-customizer-deposit className={cn(\"text-sm text-muted-foreground\", classNames?.depositInfo)}>\n {product.deposit_type === \"fixed\" ? (\n <span>Deposit required: <Price amount={product.deposit_amount} /></span>\n ) : (\n <span>Deposit required: {parsePrice(product.deposit_amount)}%</span>\n )}\n </div>\n )}\n\n {/* Allergens */}\n {product.allergies && product.allergies.length > 0 && (\n <div data-cimplify-customizer-allergens className={cn(\"flex flex-wrap gap-1.5\", classNames?.allergens)}>\n {product.allergies.map((allergen) => (\n <span\n key={allergen}\n data-cimplify-allergen-tag\n className=\"inline-block text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground\"\n >\n {allergen}\n </span>\n ))}\n </div>\n )}\n\n {/* Service duration */}\n {isService && product.duration_minutes != null && (\n <div data-cimplify-customizer-duration className={cn(\"text-sm text-muted-foreground\", classNames?.duration)}>\n Duration: {product.duration_minutes} {product.duration_unit ?? \"minutes\"}\n </div>\n )}\n\n {/* Special instructions */}\n {showSpecialInstructions && !isDigital && (\n <div data-cimplify-customizer-special-instructions>\n <textarea\n value={specialInstructions}\n onChange={(e) => setSpecialInstructions(e.target.value)}\n placeholder=\"Special instructions (e.g., no onions, extra sauce)\"\n rows={2}\n data-cimplify-customizer-textarea\n className={cn(\n \"w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring resize-none\",\n classNames?.specialInstructions,\n )}\n />\n </div>\n )}\n\n <div\n data-cimplify-customizer-actions\n className={cn(\"pt-4 border-t border-border\", classNames?.actions)}\n >\n {!quoteEnabled && (\n <p\n id=\"cimplify-customizer-validation\"\n data-cimplify-customizer-validation\n className={cn(\"text-sm text-destructive mb-3\", classNames?.validation)}\n >\n Please select all required options\n </p>\n )}\n <div className=\"flex items-center gap-4\">\n <div className=\"flex flex-col gap-1\">\n <QuantitySelector\n value={quantity}\n onChange={setQuantity}\n min={product.min_order_quantity ?? 1}\n />\n {product.min_order_quantity && product.min_order_quantity > 1 && (\n <span data-cimplify-customizer-min-hint className=\"text-xs text-muted-foreground text-center\">\n Min. {product.min_order_quantity}\n </span>\n )}\n </div>\n\n {product.quantity_pricing && product.quantity_pricing.length > 0 && (\n <div data-cimplify-customizer-unit-price className=\"text-sm text-muted-foreground text-right shrink-0\">\n <Price amount={getUnitPriceAtQuantity(product.quantity_pricing, quantity, parsePrice(product.default_price))} className=\"font-medium text-foreground\" /> ea.\n </div>\n )}\n\n <Button\n onClick={handleAddToCart}\n disabled={isAdded || isSubmitting || !quoteEnabled}\n aria-describedby={!quoteEnabled ? \"cimplify-customizer-validation\" : undefined}\n data-cimplify-customizer-submit\n className={cn(\n \"flex-1 h-14 text-base bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed rounded-full\",\n isAdded && classNames?.submitButtonAdded,\n classNames?.submitButton,\n )}\n >\n {isAdded ? \"Added to Cart\" : (\n <>\n Add to Cart &middot; <Price amount={displayTotalPrice} />\n </>\n )}\n </Button>\n </div>\n </div>\n\n </div>\n );\n}\n"
17
+ "content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect, useRef } from \"react\";\nimport { Button } from \"@base-ui/react/button\";\nimport type {\n ProductWithDetails,\n VariantView,\n AddOnOption,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport type { ProductBillingPlan } from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice, getUnitPriceAtQuantity } from \"@cimplify/sdk\";\nimport { useCart, useQuote } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { QuantitySelector } from \"@cimplify/sdk/react\";\nimport { VariantSelector } from \"@cimplify/sdk/react\";\nimport { AddOnSelector } from \"@cimplify/sdk/react\";\nimport { CompositeSelector } from \"@cimplify/sdk/react\";\nimport { BundleSelector } from \"@cimplify/sdk/react\";\nimport { BillingPlanSelector } from \"@cimplify/sdk/react\";\nimport { VolumePricing } from \"@cimplify/sdk/react\";\nimport { CustomerInputFields } from \"./customer-input-fields\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ProductCustomizerClassNames {\n root?: string;\n actions?: string;\n submitButton?: string;\n submitButtonAdded?: string;\n validation?: string;\n specialInstructions?: string;\n depositInfo?: string;\n allergens?: string;\n duration?: string;\n}\n\nexport interface ProductCustomizerProps {\n product: ProductWithDetails;\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n showSpecialInstructions?: boolean;\n className?: string;\n classNames?: ProductCustomizerClassNames;\n}\n\nexport function ProductCustomizer({\n product,\n onAddToCart,\n showSpecialInstructions = true,\n className,\n classNames,\n}: ProductCustomizerProps): React.ReactElement {\n const [quantity, setQuantity] = useState(product.min_order_quantity ?? 1);\n const [isAdded, setIsAdded] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [selectedVariantId, setSelectedVariantId] = useState<string | undefined>();\n const [selectedVariant, setSelectedVariant] = useState<VariantView | undefined>();\n const [selectedAddOnOptionIds, setSelectedAddOnOptionIds] = useState<string[]>([]);\n\n const [compositeSelections, setCompositeSelections] = useState<ComponentSelectionInput[]>([]);\n const [compositePrice, setCompositePrice] = useState<CompositePriceResult | null>(null);\n const [compositeReady, setCompositeReady] = useState(false);\n\n const [bundleSelections, setBundleSelections] = useState<BundleSelectionInput[]>([]);\n const [bundleTotalPrice, setBundleTotalPrice] = useState<number | null>(null);\n const [bundleReady, setBundleReady] = useState(false);\n const [selectedBillingPlan, setSelectedBillingPlan] = useState<ProductBillingPlan | null>(null);\n const [customerInputValues, setCustomerInputValues] = useState<Record<string, unknown>>({});\n const [specialInstructions, setSpecialInstructions] = useState(\"\");\n\n const cart = useCart();\n\n const productType = product.type || \"product\";\n const isComposite = productType === \"composite\";\n const isBundle = productType === \"bundle\";\n const isDigital = productType === \"digital\";\n const isService = productType === \"service\";\n const isStandard = !isComposite && !isBundle;\n\n const hasVariants = isStandard && product.variants && product.variants.length > 0;\n const hasAddOns = isStandard && product.add_ons && product.add_ons.length > 0;\n\n useEffect(() => {\n setQuantity(product.min_order_quantity ?? 1);\n setIsAdded(false);\n setIsSubmitting(false);\n setSelectedVariantId(undefined);\n setSelectedVariant(undefined);\n setSelectedAddOnOptionIds([]);\n setCompositeSelections([]);\n setCompositePrice(null);\n setCompositeReady(false);\n setBundleSelections([]);\n setBundleTotalPrice(null);\n setBundleReady(false);\n setSelectedBillingPlan(null);\n setCustomerInputValues({});\n setSpecialInstructions(\"\");\n }, [product.id, product.min_order_quantity]);\n\n const selectedAddOnOptions = useMemo(() => {\n if (!product.add_ons) return [];\n const options: AddOnOption[] = [];\n for (const addOn of product.add_ons) {\n for (const option of addOn.options) {\n if (selectedAddOnOptionIds.includes(option.id)) {\n options.push(option);\n }\n }\n }\n return options;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const normalizedAddOnOptionIds = useMemo(() => {\n if (selectedAddOnOptionIds.length === 0) return [];\n return Array.from(new Set(selectedAddOnOptionIds.map((id) => id.trim()).filter(Boolean))).sort();\n }, [selectedAddOnOptionIds]);\n\n const localTotalPrice = useMemo(() => {\n if (isComposite && compositePrice) {\n return parsePrice(compositePrice.final_price) * quantity;\n }\n\n if (isBundle && bundleTotalPrice != null) {\n return bundleTotalPrice * quantity;\n }\n\n let price = parsePrice(product.default_price);\n\n if (selectedVariant?.price_adjustment) {\n price += parsePrice(selectedVariant.price_adjustment);\n }\n\n for (const option of selectedAddOnOptions) {\n if (option.default_price) {\n price += parsePrice(option.default_price);\n }\n }\n\n return price * quantity;\n }, [product.default_price, selectedVariant, selectedAddOnOptions, quantity, isComposite, compositePrice, isBundle, bundleTotalPrice]);\n\n const requiredAddOnsSatisfied = useMemo(() => {\n if (!product.add_ons) return true;\n\n for (const addOn of product.add_ons) {\n if (addOn.is_required) {\n const selectedInGroup = selectedAddOnOptionIds.filter((id) =>\n addOn.options.some((opt) => opt.id === id),\n ).length;\n\n const minRequired = addOn.min_selections || 1;\n if (selectedInGroup < minRequired) {\n return false;\n }\n }\n }\n return true;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const quoteInput = useMemo(\n () => ({\n productId: product.id,\n quantity,\n variantId: selectedVariantId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n }),\n [product.id, quantity, selectedVariantId, normalizedAddOnOptionIds, isBundle, bundleSelections, isComposite, compositeSelections],\n );\n\n const requiredInputsSatisfied = useMemo(() => {\n if (!product.input_fields || product.input_fields.length === 0) return true;\n return product.input_fields.every((field) => {\n if (!field.is_required) return true;\n const val = customerInputValues[field.id];\n return val !== undefined && val !== \"\" && val !== null;\n });\n }, [product.input_fields, customerInputValues]);\n\n const quoteEnabled = isComposite\n ? compositeReady && requiredInputsSatisfied\n : isBundle\n ? bundleReady && requiredInputsSatisfied\n : requiredAddOnsSatisfied && requiredInputsSatisfied;\n\n const { quote } = useQuote(quoteInput, {\n enabled: quoteEnabled,\n });\n\n const quoteId = quote?.quote_id;\n const quotedTotalPrice = useMemo(() => {\n if (!quote) return undefined;\n\n const priceInfo = quote.quoted_total_price_info ?? quote.final_price_info;\n const perUnit = priceInfo.pre_tax_price;\n if (perUnit === undefined || perUnit === null) return undefined;\n return parsePrice(perUnit) * quantity;\n }, [quote, quantity]);\n\n const displayTotalPrice = quotedTotalPrice ?? localTotalPrice;\n\n const handleVariantChange = useCallback(\n (variantId: string | undefined, variant: VariantView | undefined) => {\n setSelectedVariantId(variantId);\n setSelectedVariant(variant);\n },\n [],\n );\n\n const addedTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n useEffect(() => () => clearTimeout(addedTimerRef.current), []);\n\n const handleAddToCart = useCallback(async () => {\n if (isSubmitting) return;\n\n setIsSubmitting(true);\n\n const options: AddToCartOptions = {\n variantId: selectedVariantId,\n variant: selectedVariant\n ? { id: selectedVariant.id, name: selectedVariant.name || \"\", price_adjustment: selectedVariant.price_adjustment }\n : undefined,\n quoteId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n addOnOptions: selectedAddOnOptions.length > 0\n ? selectedAddOnOptions.map((opt) => ({\n id: opt.id,\n name: opt.name,\n add_on_id: opt.add_on_id,\n default_price: opt.default_price,\n }))\n : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n billingPlanId: selectedBillingPlan?.id,\n customerInputs: Object.keys(customerInputValues).length > 0\n ? Object.entries(customerInputValues)\n .filter(([, v]) => v !== undefined && v !== \"\")\n .map(([fieldId, value]) => ({ field_id: fieldId, value }))\n : undefined,\n specialInstructions: specialInstructions.trim() || undefined,\n };\n\n try {\n if (onAddToCart) {\n await onAddToCart(product, quantity, options);\n } else {\n await cart.addItem(product, quantity, options);\n }\n setIsAdded(true);\n clearTimeout(addedTimerRef.current);\n addedTimerRef.current = setTimeout(() => {\n setIsAdded(false);\n setQuantity(product.min_order_quantity ?? 1);\n }, 2000);\n } catch {\n // Caller handles errors via onAddToCart or cart hook error state\n } finally {\n setIsSubmitting(false);\n }\n }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, customerInputValues, specialInstructions, isSubmitting, onAddToCart, cart]);\n\n return (\n <div data-cimplify-customizer className={cn(\"space-y-6\", className, classNames?.root)}>\n {isComposite && product.groups && product.composite_id && (\n <CompositeSelector\n compositeId={product.composite_id}\n groups={product.groups}\n onSelectionsChange={setCompositeSelections}\n onPriceChange={setCompositePrice}\n onReady={setCompositeReady}\n skipPriceFetch\n />\n )}\n\n {isBundle && product.components && (\n <BundleSelector\n components={product.components}\n bundlePrice={product.bundle_price}\n discountValue={product.discount_value}\n pricingType={product.pricing_type}\n onSelectionsChange={setBundleSelections}\n onPriceChange={setBundleTotalPrice}\n onReady={setBundleReady}\n />\n )}\n\n {hasVariants && (\n <VariantSelector\n variants={product.variants!}\n variantAxes={product.variant_axes}\n basePrice={product.default_price}\n selectedVariantId={selectedVariantId}\n onVariantChange={handleVariantChange}\n productName={product.name}\n />\n )}\n\n {hasAddOns && (\n <AddOnSelector\n addOns={product.add_ons!}\n selectedOptions={selectedAddOnOptionIds}\n onOptionsChange={setSelectedAddOnOptionIds}\n />\n )}\n\n {/* Billing plans */}\n {product.billing_plans && product.billing_plans.length > 0 && (\n <BillingPlanSelector\n productId={product.id}\n plans={product.billing_plans}\n onPlanSelect={setSelectedBillingPlan}\n selectedPlanId={selectedBillingPlan?.id ?? null}\n showOneTimePurchase\n />\n )}\n\n {/* Volume pricing tiers */}\n {product.quantity_pricing && product.quantity_pricing.length > 1 && (\n <VolumePricing\n tiers={product.quantity_pricing}\n currentQuantity={quantity}\n />\n )}\n\n {/* Customer input fields */}\n {product.input_fields && product.input_fields.length > 0 && (\n <CustomerInputFields\n fields={product.input_fields}\n values={customerInputValues}\n onChange={setCustomerInputValues}\n />\n )}\n\n {/* Deposit info for service products */}\n {product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount && (\n <div data-cimplify-customizer-deposit className={cn(\"text-sm text-muted-foreground\", classNames?.depositInfo)}>\n {product.deposit_type === \"fixed\" ? (\n <span>Deposit required: <Price amount={product.deposit_amount} /></span>\n ) : (\n <span>Deposit required: {parsePrice(product.deposit_amount)}%</span>\n )}\n </div>\n )}\n\n {/* Allergens */}\n {product.allergies && product.allergies.length > 0 && (\n <div data-cimplify-customizer-allergens className={cn(\"flex flex-wrap gap-1.5\", classNames?.allergens)}>\n {product.allergies.map((allergen) => (\n <span\n key={allergen}\n data-cimplify-allergen-tag\n className=\"inline-block text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground\"\n >\n {allergen}\n </span>\n ))}\n </div>\n )}\n\n {/* Service duration */}\n {isService && product.duration_minutes != null && (\n <div data-cimplify-customizer-duration className={cn(\"text-sm text-muted-foreground\", classNames?.duration)}>\n Duration: {product.duration_minutes} {product.duration_unit ?? \"minutes\"}\n </div>\n )}\n\n {/* Special instructions */}\n {showSpecialInstructions && !isDigital && (\n <div data-cimplify-customizer-special-instructions>\n <textarea\n value={specialInstructions}\n onChange={(e) => setSpecialInstructions(e.target.value)}\n placeholder=\"Special instructions (e.g., no onions, extra sauce)\"\n rows={2}\n data-cimplify-customizer-textarea\n className={cn(\n \"w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring resize-none\",\n classNames?.specialInstructions,\n )}\n />\n </div>\n )}\n\n <div\n data-cimplify-customizer-actions\n className={cn(\"pt-4 border-t border-border\", classNames?.actions)}\n >\n {!quoteEnabled && (\n <p\n id=\"cimplify-customizer-validation\"\n data-cimplify-customizer-validation\n className={cn(\"text-sm text-destructive mb-3\", classNames?.validation)}\n >\n Please select all required options\n </p>\n )}\n <div className=\"flex items-center gap-4\">\n <div className=\"flex flex-col gap-1\">\n <QuantitySelector\n value={quantity}\n onChange={setQuantity}\n min={product.min_order_quantity ?? 1}\n />\n {product.min_order_quantity && product.min_order_quantity > 1 && (\n <span data-cimplify-customizer-min-hint className=\"text-xs text-muted-foreground text-center\">\n Min. {product.min_order_quantity}\n </span>\n )}\n </div>\n\n {product.quantity_pricing && product.quantity_pricing.length > 0 && (\n <div data-cimplify-customizer-unit-price className=\"text-sm text-muted-foreground text-right shrink-0\">\n <Price amount={getUnitPriceAtQuantity(product.quantity_pricing, quantity, parsePrice(product.default_price))} className=\"font-medium text-foreground\" /> ea.\n </div>\n )}\n\n <Button\n onClick={handleAddToCart}\n disabled={isAdded || isSubmitting || !quoteEnabled}\n aria-describedby={!quoteEnabled ? \"cimplify-customizer-validation\" : undefined}\n data-cimplify-customizer-submit\n className={cn(\n \"flex-1 h-14 text-base bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed rounded-full\",\n isAdded && classNames?.submitButtonAdded,\n classNames?.submitButton,\n )}\n >\n {isAdded ? \"Added to Cart\" : (\n <>\n Add to Cart &middot; <Price amount={displayTotalPrice} />\n </>\n )}\n </Button>\n </div>\n </div>\n\n </div>\n );\n}\n"
18
18
  }
19
19
  ]
20
20
  }
@@ -14,7 +14,7 @@
14
14
  "files": [
15
15
  {
16
16
  "path": "product-page.tsx",
17
- "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport { PRODUCT_TYPE, RENDER_HINT } from \"@cimplify/sdk\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport { DefaultProductLayout } from \"@cimplify/sdk/react\";\nimport { FoodProductLayout } from \"@cimplify/sdk/react\";\nimport { WholesaleProductLayout } from \"@cimplify/sdk/react\";\nimport { ServiceProductLayout } from \"@cimplify/sdk/react\";\nimport { DigitalProductLayout } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport type { ProductLayoutProps };\n\nexport enum ProductTemplate {\n Default = \"default\",\n Food = \"food\",\n Wholesale = \"wholesale\",\n Service = \"service\",\n Digital = \"digital\",\n Physical = \"physical\",\n}\n\nexport interface ProductPageClassNames {\n root?: string;\n loading?: string;\n}\n\nexport interface ProductPageProps {\n /** Product slug or ID — used for client-side fetch when `product` is not provided. */\n productId?: string;\n /** Pre-fetched product for SSR. Skips client-side fetch when provided. */\n product?: ProductWithDetails;\n /** Per-slug page map. Highest priority — maps a product slug to a custom component. */\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>;\n /** Per-type template map. Overrides built-in layouts for specific template keys. */\n templates?: Partial<Record<ProductTemplate | string, React.ComponentType<ProductLayoutProps>>>;\n /** Override add-to-cart behavior. */\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: { src: string; alt: string; className?: string }) => React.ReactNode;\n /** Breadcrumb renderer. Receives the product for context. */\n renderBreadcrumb?: (product: ProductWithDetails) => React.ReactNode;\n /** Pre-fetched related products. */\n relatedProducts?: Product[];\n /** Show related products section. Default: true. */\n showRelated?: boolean;\n className?: string;\n classNames?: ProductPageClassNames;\n}\n\nconst BUILT_IN_LAYOUTS: Record<string, React.ComponentType<ProductLayoutProps>> = {\n [ProductTemplate.Food]: FoodProductLayout,\n [ProductTemplate.Wholesale]: WholesaleProductLayout,\n [ProductTemplate.Service]: ServiceProductLayout,\n [ProductTemplate.Digital]: DigitalProductLayout,\n [ProductTemplate.Default]: DefaultProductLayout,\n};\n\nfunction resolveTemplateKey(product: ProductWithDetails): ProductTemplate | string {\n const metaTemplate = product.metadata?.page_template;\n if (typeof metaTemplate === \"string\" && metaTemplate.trim()) {\n return metaTemplate.trim();\n }\n\n if (product.quantity_pricing && product.quantity_pricing.length > 1) {\n return ProductTemplate.Wholesale;\n }\n\n if (product.type === PRODUCT_TYPE.Service) {\n return ProductTemplate.Service;\n }\n\n if (product.type === PRODUCT_TYPE.Digital) {\n return ProductTemplate.Digital;\n }\n\n if (product.render_hint === RENDER_HINT.Food) {\n return ProductTemplate.Food;\n }\n\n if (product.render_hint === RENDER_HINT.Physical) {\n return ProductTemplate.Physical;\n }\n\n return ProductTemplate.Default;\n}\n\nfunction resolveLayout(\n product: ProductWithDetails,\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>,\n templates?: Partial<Record<string, React.ComponentType<ProductLayoutProps>>>,\n): React.ComponentType<ProductLayoutProps> {\n // 1. Per-slug page (AI-generated, highest priority)\n if (pages?.[product.slug]) {\n return pages[product.slug];\n }\n\n const key = resolveTemplateKey(product);\n\n // 2. Consumer-provided template override\n if (templates?.[key]) {\n return templates[key];\n }\n\n // 3. Built-in layout\n if (BUILT_IN_LAYOUTS[key]) {\n return BUILT_IN_LAYOUTS[key];\n }\n\n // 4. Fallback\n return DefaultProductLayout;\n}\n\nexport function ProductPage({\n productId,\n product: productProp,\n pages,\n templates,\n onAddToCart,\n renderImage,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n classNames,\n}: ProductPageProps): React.ReactElement {\n const resolvedId = productId || productProp?.slug || productProp?.id || \"\";\n const { product: fetched, isLoading } = useProduct(resolvedId, {\n enabled: !productProp && resolvedId.length > 0,\n });\n const product = productProp ?? fetched;\n\n if (isLoading && !product) {\n return (\n <div\n data-cimplify-product-page\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n >\n <div className=\"grid lg:grid-cols-2 gap-8 lg:gap-12 animate-pulse\">\n <div className=\"aspect-square bg-muted\" />\n <div className=\"space-y-4 py-4\">\n <div className=\"h-8 w-3/5 bg-muted\" />\n <div className=\"h-6 w-2/5 bg-muted\" />\n <div className=\"h-4 w-4/5 bg-muted\" />\n <div className=\"h-4 w-3/5 bg-muted\" />\n <div className=\"h-14 w-full bg-muted mt-8\" />\n </div>\n </div>\n </div>\n );\n }\n\n if (!product) {\n return (\n <div data-cimplify-product-page className={cn(className, classNames?.root)}>\n <p className=\"text-muted-foreground\">Product not found.</p>\n </div>\n );\n }\n\n const Layout = resolveLayout(product, pages, templates);\n\n return (\n <div data-cimplify-product-page className={cn(className, classNames?.root)}>\n <Layout\n product={product}\n onAddToCart={onAddToCart}\n renderImage={renderImage}\n renderBreadcrumb={renderBreadcrumb}\n relatedProducts={relatedProducts}\n showRelated={showRelated}\n />\n </div>\n );\n}\n"
17
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport { PRODUCT_TYPE, RENDER_HINT } from \"@cimplify/sdk\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport { DefaultProductLayout } from \"@cimplify/sdk/react\";\nimport { FoodProductLayout } from \"@cimplify/sdk/react\";\nimport { WholesaleProductLayout } from \"@cimplify/sdk/react\";\nimport { ServiceProductLayout } from \"@cimplify/sdk/react\";\nimport { DigitalProductLayout } from \"@cimplify/sdk/react\";\nimport { BundleProductLayout } from \"./layouts/bundle-product-layout\";\nimport { CompositeProductLayout } from \"./layouts/composite-product-layout\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport type { ProductLayoutProps };\n\nexport enum ProductTemplate {\n Default = \"default\",\n Food = \"food\",\n Wholesale = \"wholesale\",\n Service = \"service\",\n Digital = \"digital\",\n Bundle = \"bundle\",\n Composite = \"composite\",\n Physical = \"physical\",\n}\n\nexport interface ProductPageClassNames {\n root?: string;\n loading?: string;\n}\n\nexport interface ProductPageProps {\n /** Product slug or ID — used for client-side fetch when `product` is not provided. */\n productId?: string;\n /** Pre-fetched product for SSR. Skips client-side fetch when provided. */\n product?: ProductWithDetails;\n /** Per-slug page map. Highest priority — maps a product slug to a custom component. */\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>;\n /** Per-type template map. Overrides built-in layouts for specific template keys. */\n templates?: Partial<Record<ProductTemplate | string, React.ComponentType<ProductLayoutProps>>>;\n /** Override add-to-cart behavior. */\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: { src: string; alt: string; className?: string }) => React.ReactNode;\n /** Breadcrumb renderer. Receives the product for context. */\n renderBreadcrumb?: (product: ProductWithDetails) => React.ReactNode;\n /** Pre-fetched related products. */\n relatedProducts?: Product[];\n /** Show related products section. Default: true. */\n showRelated?: boolean;\n className?: string;\n classNames?: ProductPageClassNames;\n}\n\nconst BUILT_IN_LAYOUTS: Record<string, React.ComponentType<ProductLayoutProps>> = {\n [ProductTemplate.Food]: FoodProductLayout,\n [ProductTemplate.Wholesale]: WholesaleProductLayout,\n [ProductTemplate.Service]: ServiceProductLayout,\n [ProductTemplate.Digital]: DigitalProductLayout,\n [ProductTemplate.Bundle]: BundleProductLayout,\n [ProductTemplate.Composite]: CompositeProductLayout,\n [ProductTemplate.Default]: DefaultProductLayout,\n};\n\nfunction resolveTemplateKey(product: ProductWithDetails): ProductTemplate | string {\n const metaTemplate = product.metadata?.page_template;\n if (typeof metaTemplate === \"string\" && metaTemplate.trim()) {\n return metaTemplate.trim();\n }\n\n if (product.type === PRODUCT_TYPE.Bundle) {\n return ProductTemplate.Bundle;\n }\n\n if (product.type === PRODUCT_TYPE.Composite) {\n return ProductTemplate.Composite;\n }\n\n if (product.quantity_pricing && product.quantity_pricing.length > 1) {\n return ProductTemplate.Wholesale;\n }\n\n if (product.type === PRODUCT_TYPE.Service) {\n return ProductTemplate.Service;\n }\n\n if (product.type === PRODUCT_TYPE.Digital) {\n return ProductTemplate.Digital;\n }\n\n if (product.render_hint === RENDER_HINT.Food) {\n return ProductTemplate.Food;\n }\n\n if (product.render_hint === RENDER_HINT.Physical) {\n return ProductTemplate.Physical;\n }\n\n return ProductTemplate.Default;\n}\n\nfunction resolveLayout(\n product: ProductWithDetails,\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>,\n templates?: Partial<Record<string, React.ComponentType<ProductLayoutProps>>>,\n): React.ComponentType<ProductLayoutProps> {\n // 1. Per-slug page (AI-generated, highest priority)\n if (pages?.[product.slug]) {\n return pages[product.slug];\n }\n\n const key = resolveTemplateKey(product);\n\n // 2. Consumer-provided template override\n if (templates?.[key]) {\n return templates[key];\n }\n\n // 3. Built-in layout\n if (BUILT_IN_LAYOUTS[key]) {\n return BUILT_IN_LAYOUTS[key];\n }\n\n // 4. Fallback\n return DefaultProductLayout;\n}\n\nexport function ProductPage({\n productId,\n product: productProp,\n pages,\n templates,\n onAddToCart,\n renderImage,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n classNames,\n}: ProductPageProps): React.ReactElement {\n const resolvedId = productId || productProp?.slug || productProp?.id || \"\";\n const { product: fetched, isLoading } = useProduct(resolvedId, {\n enabled: !productProp && resolvedId.length > 0,\n });\n const product = productProp ?? fetched;\n\n if (isLoading && !product) {\n return (\n <div\n data-cimplify-product-page\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n >\n <div className=\"grid lg:grid-cols-2 gap-8 lg:gap-12 animate-pulse\">\n <div className=\"aspect-square bg-muted\" />\n <div className=\"space-y-4 py-4\">\n <div className=\"h-8 w-3/5 bg-muted\" />\n <div className=\"h-6 w-2/5 bg-muted\" />\n <div className=\"h-4 w-4/5 bg-muted\" />\n <div className=\"h-4 w-3/5 bg-muted\" />\n <div className=\"h-14 w-full bg-muted mt-8\" />\n </div>\n </div>\n </div>\n );\n }\n\n if (!product) {\n return (\n <div data-cimplify-product-page className={cn(className, classNames?.root)}>\n <p className=\"text-muted-foreground\">Product not found.</p>\n </div>\n );\n }\n\n const Layout = resolveLayout(product, pages, templates);\n\n return (\n <div data-cimplify-product-page className={cn(className, classNames?.root)}>\n <Layout\n product={product}\n onAddToCart={onAddToCart}\n renderImage={renderImage}\n renderBreadcrumb={renderBreadcrumb}\n relatedProducts={relatedProducts}\n showRelated={showRelated}\n />\n </div>\n );\n}\n"
18
18
  }
19
19
  ]
20
20
  }