@cimplify/sdk 0.20.2 → 0.22.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.
Files changed (34) hide show
  1. package/dist/advanced.d.mts +2 -2
  2. package/dist/advanced.d.ts +2 -2
  3. package/dist/advanced.js +20 -20
  4. package/dist/advanced.mjs +1 -1
  5. package/dist/{chunk-WXFEO3LT.js → chunk-A7IQN5EK.js} +17 -17
  6. package/dist/{chunk-VTMRPTQ7.mjs → chunk-R4EYMPSN.mjs} +39 -0
  7. package/dist/{chunk-UVBJDWIV.js → chunk-VYSXEDBV.js} +39 -0
  8. package/dist/{chunk-X23RNZAD.mjs → chunk-W3DMONZR.mjs} +1 -1
  9. package/dist/{client-D4E9aJy7.d.mts → client-uvCOGdBp.d.ts} +24 -2
  10. package/dist/{client-VyO6Bqz2.d.ts → client-v1NFqZFG.d.mts} +24 -2
  11. package/dist/index.d.mts +4 -4
  12. package/dist/index.d.ts +4 -4
  13. package/dist/index.js +58 -58
  14. package/dist/index.mjs +2 -2
  15. package/dist/{payment-CKqMU4xT.d.ts → payment-DazihbCY.d.mts} +1 -0
  16. package/dist/{payment-CKqMU4xT.d.mts → payment-DazihbCY.d.ts} +1 -0
  17. package/dist/{price-e4beObLZ.d.ts → price-5_L8WK6Q.d.ts} +1 -1
  18. package/dist/{price-BqV_DjjQ.d.mts → price-BcHVTqqu.d.mts} +1 -1
  19. package/dist/react.d.mts +73 -9
  20. package/dist/react.d.ts +73 -9
  21. package/dist/react.js +329 -114
  22. package/dist/react.mjs +312 -99
  23. package/dist/styles.css +1 -1
  24. package/dist/utils.d.mts +2 -2
  25. package/dist/utils.d.ts +2 -2
  26. package/package.json +1 -1
  27. package/registry/booking-card.json +1 -1
  28. package/registry/booking-page.json +1 -1
  29. package/registry/order-detail-page.json +1 -1
  30. package/registry/order-history-page.json +1 -1
  31. package/registry/order-history.json +1 -1
  32. package/registry/order-summary.json +1 -1
  33. package/registry/product-card.json +1 -1
  34. package/registry/service-card.json +1 -1
package/dist/styles.css CHANGED
@@ -1,2 +1,2 @@
1
1
  /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */
2
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.z-50{z-index:50}.container{width:100%}.-mt-px{margin-top:-1px}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-\[5\/2\]{aspect-ratio:5/2}.h-fit{height:fit-content}.max-h-\[85vh\]{max-height:85vh}.w-2\/5{width:40%}.w-3\/5{width:60%}.w-4\/5{width:80%}.w-full{width:100%}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.\[appearance\:textfield\]{appearance:textfield}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-border>:not(:last-child)){border-color:var(--color-border,oklch(90% 0 0))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.rounded{border-radius:var(--radius,.5rem)}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-none{--tw-border-style:none;border-style:none}.border-border{border-color:var(--color-border,oklch(90% 0 0))}.border-input{border-color:var(--color-input,oklch(90% 0 0))}.border-muted-foreground\/30{border-color:#6363634d}@supports (color:color-mix(in lab, red, red)){.border-muted-foreground\/30{border-color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 30%, transparent)}}.border-primary{border-color:var(--color-primary,oklch(50% .1 35))}.bg-background{background-color:var(--color-background,oklch(99% 0 0))}.bg-destructive\/10{background-color:#bb061e1a}@supports (color:color-mix(in lab, red, red)){.bg-destructive\/10{background-color:color-mix(in oklab, var(--color-destructive,oklch(50% .2 25)) 10%, transparent)}}.bg-muted{background-color:var(--color-muted,oklch(95% 0 0))}.bg-primary{background-color:var(--color-primary,oklch(50% .1 35))}.bg-primary\/10{background-color:#934c3a1a}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 10%, transparent)}}.bg-transparent{background-color:#0000}.text-center{text-align:center}.text-left{text-align:left}.font-\[inherit\]{font-family:inherit}.text-\[10px\]{font-size:10px}.text-\[inherit\]{color:inherit}.text-destructive{color:var(--color-destructive,oklch(50% .2 25))}.text-foreground{color:var(--color-foreground,oklch(15% 0 0))}.text-muted-foreground{color:var(--color-muted-foreground,oklch(50% 0 0))}.text-muted-foreground\/60{color:#63636399}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/60{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 60%, transparent)}}.text-primary{color:var(--color-primary,oklch(50% .1 35))}.text-primary-foreground{color:var(--color-primary-foreground,oklch(99% 0 0))}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.accent-primary{accent-color:var(--color-primary,oklch(50% .1 35))}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.outline-none{--tw-outline-style:none;outline-style:none}.\[cimplify\:checkout\]{cimplify:checkout}.placeholder\:text-muted-foreground::placeholder{color:var(--color-muted-foreground,oklch(50% 0 0))}@media (hover:hover){.hover\:border-primary\/50:hover{border-color:#934c3a80}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/50:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 50%, transparent)}}.hover\:bg-muted:hover{background-color:var(--color-muted,oklch(95% 0 0))}.hover\:bg-primary\/90:hover{background-color:#934c3ae6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 90%, transparent)}}.hover\:text-primary:hover{color:var(--color-primary,oklch(50% .1 35))}}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-ring:focus{--tw-ring-color:var(--color-ring,oklch(50% .1 35))}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.\[\&\:\:-webkit-inner-spin-button\]\:appearance-none::-webkit-inner-spin-button{appearance:none}.\[\&\:\:-webkit-outer-spin-button\]\:appearance-none::-webkit-outer-spin-button{appearance:none}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.z-50{z-index:50}.container{width:100%}.-mt-px{margin-top:-1px}.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-\[5\/2\]{aspect-ratio:5/2}.h-fit{height:fit-content}.h-full{height:100%}.max-h-\[85vh\]{max-height:85vh}.w-2\/5{width:40%}.w-3\/5{width:60%}.w-4\/5{width:80%}.w-full{width:100%}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.\[appearance\:textfield\]{appearance:textfield}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-border>:not(:last-child)){border-color:var(--color-border,oklch(90% 0 0))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.rounded{border-radius:var(--radius,.5rem)}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-none{--tw-border-style:none;border-style:none}.border-border{border-color:var(--color-border,oklch(90% 0 0))}.border-input{border-color:var(--color-input,oklch(90% 0 0))}.border-muted-foreground\/30{border-color:#6363634d}@supports (color:color-mix(in lab, red, red)){.border-muted-foreground\/30{border-color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 30%, transparent)}}.border-primary{border-color:var(--color-primary,oklch(50% .1 35))}.bg-background{background-color:var(--color-background,oklch(99% 0 0))}.bg-destructive\/10{background-color:#bb061e1a}@supports (color:color-mix(in lab, red, red)){.bg-destructive\/10{background-color:color-mix(in oklab, var(--color-destructive,oklch(50% .2 25)) 10%, transparent)}}.bg-muted{background-color:var(--color-muted,oklch(95% 0 0))}.bg-primary{background-color:var(--color-primary,oklch(50% .1 35))}.bg-primary\/10{background-color:#934c3a1a}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 10%, transparent)}}.bg-transparent{background-color:#0000}.object-cover{object-fit:cover}.text-center{text-align:center}.text-left{text-align:left}.font-\[inherit\]{font-family:inherit}.text-\[10px\]{font-size:10px}.text-\[inherit\]{color:inherit}.text-destructive{color:var(--color-destructive,oklch(50% .2 25))}.text-foreground{color:var(--color-foreground,oklch(15% 0 0))}.text-muted-foreground{color:var(--color-muted-foreground,oklch(50% 0 0))}.text-muted-foreground\/60{color:#63636399}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/60{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 60%, transparent)}}.text-primary{color:var(--color-primary,oklch(50% .1 35))}.text-primary-foreground{color:var(--color-primary-foreground,oklch(99% 0 0))}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.accent-primary{accent-color:var(--color-primary,oklch(50% .1 35))}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.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}.outline-none{--tw-outline-style:none;outline-style: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)}}.placeholder\:text-muted-foreground::placeholder{color:var(--color-muted-foreground,oklch(50% 0 0))}@media (hover:hover){.hover\:border-primary\/50:hover{border-color:#934c3a80}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/50:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 50%, transparent)}}.hover\:bg-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-muted:hover{background-color:var(--color-muted,oklch(95% 0 0))}.hover\:bg-primary\/90:hover{background-color:#934c3ae6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 90%, transparent)}}.hover\:text-primary:hover{color:var(--color-primary,oklch(50% .1 35))}}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-ring:focus{--tw-ring-color:var(--color-ring,oklch(50% .1 35))}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.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-inner-spin-button\]\:appearance-none::-webkit-inner-spin-button{appearance:none}.\[\&\:\:-webkit-outer-spin-button\]\:appearance-none::-webkit-outer-spin-button{appearance:none}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{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}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
package/dist/utils.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { M as Money, a as CurrencyCode, aS as PricePathTaxInfo, bD as PaymentErrorDetails, bB as PaymentResponse, bC as PaymentStatusResponse } from './payment-CKqMU4xT.mjs';
2
- import { P as ProductWithPrice } from './price-BqV_DjjQ.mjs';
1
+ import { M as Money, a as CurrencyCode, aS as PricePathTaxInfo, bD as PaymentErrorDetails, bB as PaymentResponse, bC as PaymentStatusResponse } from './payment-DazihbCY.mjs';
2
+ import { P as ProductWithPrice } from './price-BcHVTqqu.mjs';
3
3
  import { CountryCode } from 'libphonenumber-js';
4
4
 
5
5
  declare const CURRENCY_SYMBOLS: Record<string, string>;
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { M as Money, a as CurrencyCode, aS as PricePathTaxInfo, bD as PaymentErrorDetails, bB as PaymentResponse, bC as PaymentStatusResponse } from './payment-CKqMU4xT.js';
2
- import { P as ProductWithPrice } from './price-e4beObLZ.js';
1
+ import { M as Money, a as CurrencyCode, aS as PricePathTaxInfo, bD as PaymentErrorDetails, bB as PaymentResponse, bC as PaymentStatusResponse } from './payment-DazihbCY.js';
2
+ import { P as ProductWithPrice } from './price-5_L8WK6Q.js';
3
3
  import { CountryCode } from 'libphonenumber-js';
4
4
 
5
5
  declare const CURRENCY_SYMBOLS: Record<string, string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cimplify/sdk",
3
- "version": "0.20.2",
3
+ "version": "0.22.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": "booking-card.tsx",
13
- "content": "\"use client\";\n\nimport React from \"react\";\nimport type { CustomerBooking, BookingStatus } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface BookingCardClassNames {\n root?: string;\n service?: string;\n status?: string;\n time?: string;\n confirmationCode?: string;\n total?: string;\n actions?: string;\n cancelButton?: string;\n rescheduleButton?: string;\n}\n\nexport interface BookingCardProps {\n /** The booking to display. */\n booking: CustomerBooking;\n /** Called when cancel is clicked. */\n onCancel?: (booking: CustomerBooking) => void;\n /** Called when reschedule is clicked. */\n onReschedule?: (booking: CustomerBooking) => void;\n /** Custom renderer for the booking. */\n renderBooking?: (booking: CustomerBooking) => React.ReactNode;\n className?: string;\n classNames?: BookingCardClassNames;\n}\n\nconst STATUS_LABELS: Record<string, string> = {\n pending: \"Pending\",\n confirmed: \"Confirmed\",\n in_progress: \"In Progress\",\n completed: \"Completed\",\n cancelled: \"Cancelled\",\n no_show: \"No Show\",\n};\n\nfunction isActiveStatus(status: BookingStatus): boolean {\n const s = String(status).toLowerCase();\n return s !== \"completed\" && s !== \"cancelled\" && s !== \"no_show\";\n}\n\nfunction getFirstServiceItem(booking: CustomerBooking) {\n return booking.service_items[0] ?? null;\n}\n\nfunction formatScheduledTime(start?: string | null, end?: string | null): string | null {\n if (!start) return null;\n try {\n const startDate = new Date(start);\n const options: Intl.DateTimeFormatOptions = {\n month: \"short\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n };\n let formatted = startDate.toLocaleString(undefined, options);\n if (end) {\n const endDate = new Date(end);\n formatted += ` – ${endDate.toLocaleTimeString(undefined, { hour: \"numeric\", minute: \"2-digit\" })}`;\n }\n return formatted;\n } catch {\n return start;\n }\n}\n\nexport function BookingCard({\n booking,\n onCancel,\n onReschedule,\n renderBooking,\n className,\n classNames,\n}: BookingCardProps): React.ReactElement {\n if (renderBooking) {\n return (\n <div\n data-cimplify-booking-card\n data-status={booking.status}\n className={cn(className, classNames?.root)}\n >\n {renderBooking(booking)}\n </div>\n );\n }\n\n const firstItem = getFirstServiceItem(booking);\n const active = isActiveStatus(booking.status);\n const scheduledTime = firstItem\n ? formatScheduledTime(firstItem.scheduled_start, firstItem.scheduled_end)\n : null;\n\n return (\n <div\n data-cimplify-booking-card\n data-status={booking.status}\n className={cn(className, classNames?.root)}\n >\n <div data-cimplify-booking-main>\n <span data-cimplify-booking-service className={classNames?.service}>\n Booking #{booking.order_id.slice(0, 8)}\n </span>\n <span\n data-cimplify-booking-status\n data-status={booking.status}\n className={classNames?.status}\n >\n {STATUS_LABELS[booking.status] ?? booking.status}\n </span>\n </div>\n\n {firstItem?.confirmation_code && (\n <span data-cimplify-booking-code className={classNames?.confirmationCode}>\n {firstItem.confirmation_code}\n </span>\n )}\n\n {scheduledTime && (\n <time data-cimplify-booking-time className={classNames?.time}>\n {scheduledTime}\n </time>\n )}\n\n <span data-cimplify-booking-total className={classNames?.total}>\n <Price amount={booking.total_price} />\n </span>\n\n {active && (onCancel || onReschedule) && (\n <div data-cimplify-booking-actions className={classNames?.actions}>\n {onReschedule && (\n <button\n type=\"button\"\n onClick={() => onReschedule(booking)}\n data-cimplify-booking-reschedule\n className={classNames?.rescheduleButton}\n >\n Reschedule\n </button>\n )}\n {onCancel && (\n <button\n type=\"button\"\n onClick={() => onCancel(booking)}\n data-cimplify-booking-cancel\n className={classNames?.cancelButton}\n >\n Cancel\n </button>\n )}\n </div>\n )}\n </div>\n );\n}\n"
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { CustomerBooking, BookingStatus } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface BookingCardClassNames {\n root?: string;\n service?: string;\n status?: string;\n time?: string;\n confirmationCode?: string;\n total?: string;\n actions?: string;\n cancelButton?: string;\n rescheduleButton?: string;\n}\n\nexport interface BookingCardProps {\n /** The booking to display. */\n booking: CustomerBooking;\n /** Called when cancel is clicked. */\n onCancel?: (booking: CustomerBooking) => void;\n /** Called when reschedule is clicked. */\n onReschedule?: (booking: CustomerBooking) => void;\n /** Custom renderer for the booking. */\n renderBooking?: (booking: CustomerBooking) => React.ReactNode;\n className?: string;\n classNames?: BookingCardClassNames;\n}\n\nconst STATUS_LABELS: Record<string, string> = {\n pending: \"Pending\",\n confirmed: \"Confirmed\",\n in_progress: \"In Progress\",\n completed: \"Completed\",\n cancelled: \"Cancelled\",\n no_show: \"No Show\",\n};\n\nfunction isActiveStatus(status: BookingStatus): boolean {\n const s = String(status).toLowerCase();\n return s !== \"completed\" && s !== \"cancelled\" && s !== \"no_show\";\n}\n\nfunction getFirstServiceItem(booking: CustomerBooking) {\n return booking.service_items[0] ?? null;\n}\n\nfunction formatScheduledTime(start?: string | null, end?: string | null): string | null {\n if (!start) return null;\n try {\n const startDate = new Date(start);\n const options: Intl.DateTimeFormatOptions = {\n month: \"short\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n };\n let formatted = startDate.toLocaleString(undefined, options);\n if (end) {\n const endDate = new Date(end);\n formatted += ` – ${endDate.toLocaleTimeString(undefined, { hour: \"numeric\", minute: \"2-digit\" })}`;\n }\n return formatted;\n } catch {\n return start;\n }\n}\n\nexport function BookingCard({\n booking,\n onCancel,\n onReschedule,\n renderBooking,\n className,\n classNames,\n}: BookingCardProps): React.ReactElement {\n if (renderBooking) {\n return (\n <div\n data-cimplify-booking-card\n data-status={booking.status}\n className={cn(className, classNames?.root)}\n >\n {renderBooking(booking)}\n </div>\n );\n }\n\n const firstItem = getFirstServiceItem(booking);\n const active = isActiveStatus(booking.status);\n const scheduledTime = firstItem\n ? formatScheduledTime(firstItem.scheduled_start, firstItem.scheduled_end)\n : null;\n\n return (\n <div\n data-cimplify-booking-card\n data-status={booking.status}\n className={cn(\n \"rounded-xl border border-border bg-background p-4 flex flex-col gap-3\",\n className,\n classNames?.root,\n )}\n >\n <div data-cimplify-booking-main className=\"flex items-center justify-between gap-2\">\n <span data-cimplify-booking-service className={cn(\"font-semibold text-foreground\", classNames?.service)}>\n Booking #{booking.order_id.slice(0, 8)}\n </span>\n <span\n data-cimplify-booking-status\n data-status={booking.status}\n className={cn(\n \"text-xs font-medium px-2 py-0.5 rounded-full\",\n active ? \"bg-primary/10 text-primary\" : \"bg-muted text-muted-foreground\",\n classNames?.status,\n )}\n >\n {STATUS_LABELS[booking.status] ?? booking.status}\n </span>\n </div>\n\n {firstItem?.confirmation_code && (\n <span data-cimplify-booking-code className={cn(\"text-xs font-mono text-muted-foreground tracking-wide\", classNames?.confirmationCode)}>\n {firstItem.confirmation_code}\n </span>\n )}\n\n {scheduledTime && (\n <time\n data-cimplify-booking-time\n dateTime={firstItem?.scheduled_start ?? undefined}\n className={cn(\"text-sm text-foreground\", classNames?.time)}\n >\n {scheduledTime}\n </time>\n )}\n\n <span data-cimplify-booking-total className={cn(\"text-sm font-medium text-foreground\", classNames?.total)}>\n <Price amount={booking.total_price} />\n </span>\n\n {active && (onCancel || onReschedule) && (\n <div data-cimplify-booking-actions className={cn(\"flex gap-2 pt-1 border-t border-border mt-1\", classNames?.actions)}>\n {onReschedule && (\n <button\n type=\"button\"\n onClick={() => onReschedule(booking)}\n data-cimplify-booking-reschedule\n className={cn(\n \"flex-1 text-sm font-medium py-2 rounded-lg border border-border bg-background text-foreground transition-colors hover:bg-muted cursor-pointer\",\n classNames?.rescheduleButton,\n )}\n >\n Reschedule\n </button>\n )}\n {onCancel && (\n <button\n type=\"button\"\n onClick={() => onCancel(booking)}\n data-cimplify-booking-cancel\n className={cn(\n \"flex-1 text-sm font-medium py-2 rounded-lg border border-border bg-background text-destructive transition-colors hover:bg-destructive/10 cursor-pointer\",\n classNames?.cancelButton,\n )}\n >\n Cancel\n </button>\n )}\n </div>\n )}\n </div>\n );\n}\n"
14
14
  }
15
15
  ]
16
16
  }
@@ -12,7 +12,7 @@
12
12
  "files": [
13
13
  {
14
14
  "path": "booking-page.tsx",
15
- "content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport type { Product } from \"@cimplify/sdk\";\nimport type { Service, Staff, AvailableSlot, CustomerBooking } from \"@cimplify/sdk\";\nimport { useCart } from \"@cimplify/sdk/react\";\nimport { useCimplifyClient } from \"@cimplify/sdk/react\";\nimport { DateSlotPicker } from \"@cimplify/sdk/react\";\nimport { StaffPicker } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\nimport { formatDuration } from \"./utils/format-duration\";\nimport { parsePrice } from \"@cimplify/sdk\";\n\nexport interface BookingPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n serviceInfo?: string;\n step?: string;\n stepTitle?: string;\n summary?: string;\n summaryRow?: string;\n confirmButton?: string;\n backButton?: string;\n error?: string;\n rescheduleInfo?: string;\n depositInfo?: string;\n cancellationPolicy?: string;\n}\n\nexport interface BookingPageProps {\n /** The service being booked. */\n service: Service;\n /** The underlying product (for deposit/cancellation fields). Falls back to service fields. */\n product?: Product;\n /** Optional staff list for staff selection step. */\n staff?: Staff[];\n /** Number of participants. */\n participantCount?: number;\n /** Page title. */\n title?: string;\n /** Called after successfully adding to cart. */\n onBooked?: (slot: AvailableSlot, staffId: string | null) => void;\n /** Called when user wants to go back. */\n onBack?: () => void;\n /** Existing booking for reschedule mode. */\n existingBooking?: CustomerBooking;\n /** Called after a successful reschedule. */\n onRescheduled?: (booking: CustomerBooking, newSlot: AvailableSlot) => void;\n /** Show price on slots. Default: true. */\n showPrice?: boolean;\n className?: string;\n classNames?: BookingPageClassNames;\n}\n\ntype BookingStep = \"select-slot\" | \"select-staff\" | \"confirm\";\n\nexport function BookingPage({\n service,\n product,\n staff,\n participantCount,\n title,\n onBooked,\n onBack,\n existingBooking,\n onRescheduled,\n showPrice = true,\n className,\n classNames,\n}: BookingPageProps): React.ReactElement {\n const { addItem } = useCart();\n const { client } = useCimplifyClient();\n\n const isReschedule = existingBooking !== undefined;\n\n const [step, setStep] = useState<BookingStep>(\"select-slot\");\n const [selectedSlot, setSelectedSlot] = useState<AvailableSlot | null>(null);\n const [selectedDate, setSelectedDate] = useState<string | null>(null);\n const [selectedStaffId, setSelectedStaffId] = useState<string | null>(null);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const hasStaffStep = staff && staff.length > 0;\n\n // Deposit info — prefer product fields, then fall back to service metadata\n const depositType = product?.deposit_type;\n const depositAmount = product?.deposit_amount;\n const hasDeposit = depositType !== undefined && depositType !== \"none\" && depositAmount !== undefined && parsePrice(depositAmount) !== 0;\n\n // Cancellation policy\n const cancellationMinutes = product?.cancellation_window_minutes;\n const noShowFee = product?.no_show_fee;\n\n const handleSlotSelect = useCallback(\n (slot: AvailableSlot, date: string) => {\n setSelectedSlot(slot);\n setSelectedDate(date);\n setError(null);\n if (hasStaffStep) {\n setStep(\"select-staff\");\n } else {\n setStep(\"confirm\");\n }\n },\n [hasStaffStep],\n );\n\n const handleStaffSelect = useCallback((staffId: string | null) => {\n setSelectedStaffId(staffId);\n setStep(\"confirm\");\n }, []);\n\n const handleBack = useCallback(() => {\n setError(null);\n if (step === \"confirm\" && hasStaffStep) {\n setStep(\"select-staff\");\n } else if (step === \"confirm\" || step === \"select-staff\") {\n setStep(\"select-slot\");\n } else {\n onBack?.();\n }\n }, [step, hasStaffStep, onBack]);\n\n const handleConfirm = useCallback(async () => {\n if (!selectedSlot || !selectedDate) return;\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n if (isReschedule && existingBooking) {\n // Reschedule mode — call the scheduling API instead of adding to cart\n const serviceItem = existingBooking.service_items[0];\n const result = await client.scheduling.rescheduleBooking({\n order_id: existingBooking.order_id,\n line_item_id: serviceItem?.service_id ?? existingBooking.order_id,\n new_start_time: selectedSlot.start_time,\n new_end_time: selectedSlot.end_time,\n new_staff_id: selectedStaffId || undefined,\n reschedule_type: \"customer\",\n });\n\n if (!result.ok) {\n throw result.error;\n }\n\n onRescheduled?.(existingBooking, selectedSlot);\n } else {\n // Normal booking mode — add to cart\n const serviceProduct = {\n id: service.product_id || service.id,\n business_id: service.business_id || \"\",\n category_id: service.category_id || undefined,\n name: service.name,\n slug: service.id,\n description: service.description || undefined,\n image_url: service.image_url || undefined,\n default_price: (service.price || \"0\") as Product[\"default_price\"],\n type: \"service\" as const,\n inventory_type: \"none\" as const,\n variant_strategy: \"fetch_all\" as const,\n is_active: service.is_available,\n created_at: new Date().toISOString(),\n updated_at: new Date().toISOString(),\n };\n\n await addItem(serviceProduct, 1, {\n scheduledStart: selectedSlot.start_time,\n scheduledEnd: selectedSlot.end_time,\n staffId: selectedStaffId || undefined,\n });\n\n onBooked?.(selectedSlot, selectedStaffId);\n }\n } catch (err) {\n const fallbackMessage = isReschedule ? \"Failed to reschedule booking\" : \"Failed to add booking to cart\";\n setError(err instanceof Error ? err.message : fallbackMessage);\n } finally {\n setIsSubmitting(false);\n }\n }, [selectedSlot, selectedDate, selectedStaffId, service, addItem, onBooked, isReschedule, existingBooking, client, onRescheduled]);\n\n return (\n <div data-cimplify-booking-page className={cn(className, classNames?.root)}>\n <div data-cimplify-booking-page-header className={classNames?.header}>\n {onBack && step === \"select-slot\" && (\n <button\n type=\"button\"\n onClick={onBack}\n data-cimplify-booking-page-back\n className={classNames?.backButton}\n >\n Back\n </button>\n )}\n {step !== \"select-slot\" && (\n <button\n type=\"button\"\n onClick={handleBack}\n data-cimplify-booking-page-back\n className={classNames?.backButton}\n >\n Back\n </button>\n )}\n <h1 data-cimplify-booking-page-title className={classNames?.title}>\n {title || (isReschedule ? \"Reschedule Booking\" : `Book ${service.name}`)}\n </h1>\n </div>\n\n {isReschedule && existingBooking && (\n <div data-cimplify-booking-reschedule-info className={classNames?.rescheduleInfo}>\n <span>Rescheduling booking from {new Date(existingBooking.service_items[0]?.scheduled_start ?? existingBooking.created_at).toLocaleDateString()}</span>\n </div>\n )}\n\n <div data-cimplify-booking-service-info className={classNames?.serviceInfo}>\n <span data-cimplify-booking-service-name>{service.name}</span>\n <span data-cimplify-booking-service-duration>{formatDuration(service.duration_minutes, service.duration_unit)}</span>\n {service.price && (\n <span data-cimplify-booking-service-price>\n <Price amount={service.price} />\n </span>\n )}\n {hasDeposit && (\n <span data-cimplify-booking-deposit-info className={classNames?.depositInfo}>\n Deposit: {depositType === \"percentage\" ? `${parsePrice(depositAmount!)}%` : <Price amount={depositAmount!} />}\n </span>\n )}\n {cancellationMinutes !== undefined && cancellationMinutes > 0 && (\n <span data-cimplify-booking-cancellation-policy className={classNames?.cancellationPolicy}>\n Free cancellation up to {cancellationMinutes >= 60 ? `${Math.floor(cancellationMinutes / 60)} hour${Math.floor(cancellationMinutes / 60) !== 1 ? \"s\" : \"\"}` : `${cancellationMinutes} minute${cancellationMinutes !== 1 ? \"s\" : \"\"}`} before\n </span>\n )}\n {noShowFee !== undefined && parsePrice(noShowFee) !== 0 && (\n <span data-cimplify-booking-no-show-fee className={classNames?.cancellationPolicy}>\n No-show fee: <Price amount={noShowFee} />\n </span>\n )}\n </div>\n\n {error && (\n <div data-cimplify-booking-error className={classNames?.error}>\n {error}\n </div>\n )}\n\n {step === \"select-slot\" && (\n <div data-cimplify-booking-step=\"select-slot\" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Select a date &amp; time\n </h2>\n <DateSlotPicker\n serviceId={service.id}\n participantCount={participantCount}\n selectedSlot={selectedSlot}\n onSlotSelect={handleSlotSelect}\n showPrice={showPrice}\n />\n </div>\n )}\n\n {step === \"select-staff\" && staff && (\n <div data-cimplify-booking-step=\"select-staff\" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Choose a provider\n </h2>\n <StaffPicker\n staff={staff}\n selectedStaffId={selectedStaffId}\n onStaffSelect={handleStaffSelect}\n />\n </div>\n )}\n\n {step === \"confirm\" && selectedSlot && (\n <div data-cimplify-booking-step=\"confirm\" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Confirm your booking\n </h2>\n <div data-cimplify-booking-summary className={classNames?.summary}>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Service</span>\n <span>{service.name}</span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Date</span>\n <span>{selectedDate}</span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Time</span>\n <span>\n {new Date(selectedSlot.start_time).toLocaleTimeString(undefined, {\n hour: \"numeric\",\n minute: \"2-digit\",\n })}\n </span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Duration</span>\n <span>{formatDuration(service.duration_minutes, service.duration_unit)}</span>\n </div>\n {selectedStaffId && staff && (\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Provider</span>\n <span>{staff.find((s) => s.id === selectedStaffId)?.name ?? \"Selected\"}</span>\n </div>\n )}\n {(selectedSlot.price || service.price) && (\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Price</span>\n <span>\n <Price amount={selectedSlot.price || service.price!} />\n </span>\n </div>\n )}\n {hasDeposit && (\n <div data-cimplify-booking-summary-row className={cn(classNames?.summaryRow, classNames?.depositInfo)}>\n <span>Deposit</span>\n <span>\n {depositType === \"percentage\" ? `${parsePrice(depositAmount!)}%` : <Price amount={depositAmount!} />}\n </span>\n </div>\n )}\n </div>\n <button\n type=\"button\"\n onClick={handleConfirm}\n disabled={isSubmitting}\n data-cimplify-booking-confirm\n className={classNames?.confirmButton}\n >\n {isSubmitting\n ? (isReschedule ? \"Rescheduling…\" : \"Adding to cart…\")\n : (isReschedule ? \"Reschedule\" : \"Confirm Booking\")}\n </button>\n </div>\n )}\n </div>\n );\n}\n"
15
+ "content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport type { Product } from \"@cimplify/sdk\";\nimport type { Service, Staff, AvailableSlot, CustomerBooking } from \"@cimplify/sdk\";\nimport type { Resource } from \"./resource-picker\";\nimport { useCart } from \"@cimplify/sdk/react\";\nimport { useCimplifyClient } from \"@cimplify/sdk/react\";\nimport { DateSlotPicker } from \"@cimplify/sdk/react\";\nimport { StaffPicker } from \"@cimplify/sdk/react\";\nimport { ResourcePicker } from \"./resource-picker\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\nimport { formatDuration } from \"./utils/format-duration\";\nimport { parsePrice } from \"@cimplify/sdk\";\n\nexport interface BookingPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n serviceInfo?: string;\n step?: string;\n stepTitle?: string;\n summary?: string;\n summaryRow?: string;\n confirmButton?: string;\n backButton?: string;\n error?: string;\n rescheduleInfo?: string;\n depositInfo?: string;\n cancellationPolicy?: string;\n resourceStep?: string;\n}\n\nexport interface BookingPageProps {\n /** The service being booked. */\n service: Service;\n /** The underlying product (for deposit/cancellation fields). Falls back to service fields. */\n product?: Product;\n /** Optional staff list for staff selection step. */\n staff?: Staff[];\n /** Optional resources for resource selection step (rooms, equipment, etc.). */\n resources?: Resource[];\n /** Number of participants. */\n participantCount?: number;\n /** Page title. */\n title?: string;\n /** Called after successfully adding to cart. */\n onBooked?: (slot: AvailableSlot, staffId: string | null) => void;\n /** Called when user wants to go back. */\n onBack?: () => void;\n /** Existing booking for reschedule mode. */\n existingBooking?: CustomerBooking;\n /** Called after a successful reschedule. */\n onRescheduled?: (booking: CustomerBooking, newSlot: AvailableSlot) => void;\n /** Show price on slots. Default: true. */\n showPrice?: boolean;\n className?: string;\n classNames?: BookingPageClassNames;\n}\n\nconst STEP = {\n SELECT_SLOT: \"select-slot\",\n SELECT_RESOURCE: \"select-resource\",\n SELECT_STAFF: \"select-staff\",\n CONFIRM: \"confirm\",\n} as const;\n\ntype BookingStep = (typeof STEP)[keyof typeof STEP];\n\nexport function BookingPage({\n service,\n product,\n staff,\n resources,\n participantCount,\n title,\n onBooked,\n onBack,\n existingBooking,\n onRescheduled,\n showPrice = true,\n className,\n classNames,\n}: BookingPageProps): React.ReactElement {\n const { addItem } = useCart();\n const { client } = useCimplifyClient();\n\n const isReschedule = existingBooking !== undefined;\n\n const [step, setStep] = useState<BookingStep>(STEP.SELECT_SLOT);\n const [selectedSlot, setSelectedSlot] = useState<AvailableSlot | null>(null);\n const [selectedDate, setSelectedDate] = useState<string | null>(null);\n const [selectedStaffId, setSelectedStaffId] = useState<string | null>(null);\n const [selectedResourceId, setSelectedResourceId] = useState<string | null>(null);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const hasResourceStep = resources && resources.length > 0;\n const hasStaffStep = staff && staff.length > 0;\n\n // Deposit info — prefer product fields, then fall back to service metadata\n const depositType = product?.deposit_type;\n const depositAmount = product?.deposit_amount;\n const hasDeposit = depositType !== undefined && depositType !== \"none\" && depositAmount !== undefined && parsePrice(depositAmount) !== 0;\n\n // Cancellation policy\n const cancellationMinutes = product?.cancellation_window_minutes;\n const noShowFee = product?.no_show_fee;\n\n const handleSlotSelect = useCallback(\n (slot: AvailableSlot, date: string) => {\n setSelectedSlot(slot);\n setSelectedDate(date);\n setError(null);\n if (hasResourceStep) {\n setStep(STEP.SELECT_RESOURCE);\n } else if (hasStaffStep) {\n setStep(STEP.SELECT_STAFF);\n } else {\n setStep(STEP.CONFIRM);\n }\n },\n [hasResourceStep, hasStaffStep],\n );\n\n const handleResourceSelect = useCallback(\n (resourceId: string | null) => {\n setSelectedResourceId(resourceId);\n if (hasStaffStep) {\n setStep(STEP.SELECT_STAFF);\n } else {\n setStep(STEP.CONFIRM);\n }\n },\n [hasStaffStep],\n );\n\n const handleStaffSelect = useCallback((staffId: string | null) => {\n setSelectedStaffId(staffId);\n setStep(STEP.CONFIRM);\n }, []);\n\n const handleBack = useCallback(() => {\n setError(null);\n if (step === STEP.CONFIRM && hasStaffStep) {\n setStep(STEP.SELECT_STAFF);\n } else if (step === STEP.CONFIRM && hasResourceStep) {\n setStep(STEP.SELECT_RESOURCE);\n } else if (step === STEP.SELECT_STAFF && hasResourceStep) {\n setStep(STEP.SELECT_RESOURCE);\n } else if (step === STEP.CONFIRM || step === STEP.SELECT_STAFF || step === STEP.SELECT_RESOURCE) {\n setStep(STEP.SELECT_SLOT);\n } else {\n onBack?.();\n }\n }, [step, hasStaffStep, hasResourceStep, onBack]);\n\n const handleConfirm = useCallback(async () => {\n if (!selectedSlot || !selectedDate) return;\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n if (isReschedule && existingBooking) {\n // Reschedule mode — call the scheduling API instead of adding to cart\n const serviceItem = existingBooking.service_items[0];\n const result = await client.scheduling.rescheduleBooking({\n order_id: existingBooking.order_id,\n line_item_id: serviceItem?.service_id ?? existingBooking.order_id,\n new_start_time: selectedSlot.start_time,\n new_end_time: selectedSlot.end_time,\n new_staff_id: selectedStaffId || undefined,\n reschedule_type: \"customer\",\n });\n\n if (!result.ok) {\n throw result.error;\n }\n\n onRescheduled?.(existingBooking, selectedSlot);\n } else {\n // Normal booking mode — add to cart\n const serviceProduct = {\n id: service.product_id || service.id,\n business_id: service.business_id || \"\",\n category_id: service.category_id || undefined,\n name: service.name,\n slug: service.id,\n description: service.description || undefined,\n image_url: service.image_url || undefined,\n default_price: (service.price || \"0\") as Product[\"default_price\"],\n type: \"service\" as const,\n inventory_type: \"none\" as const,\n variant_strategy: \"fetch_all\" as const,\n is_active: service.is_available,\n created_at: new Date().toISOString(),\n updated_at: new Date().toISOString(),\n };\n\n await addItem(serviceProduct, 1, {\n scheduledStart: selectedSlot.start_time,\n scheduledEnd: selectedSlot.end_time,\n staffId: selectedStaffId || undefined,\n resourceId: selectedResourceId || undefined,\n });\n\n onBooked?.(selectedSlot, selectedStaffId);\n }\n } catch (err) {\n const fallbackMessage = isReschedule ? \"Failed to reschedule booking\" : \"Failed to add booking to cart\";\n setError(err instanceof Error ? err.message : fallbackMessage);\n } finally {\n setIsSubmitting(false);\n }\n }, [selectedSlot, selectedDate, selectedStaffId, service, addItem, onBooked, isReschedule, existingBooking, client, onRescheduled]);\n\n return (\n <div data-cimplify-booking-page className={cn(className, classNames?.root)}>\n <div data-cimplify-booking-page-header className={classNames?.header}>\n {onBack && step === STEP.SELECT_SLOT && (\n <button\n type=\"button\"\n onClick={onBack}\n data-cimplify-booking-page-back\n className={classNames?.backButton}\n >\n Back\n </button>\n )}\n {step !== STEP.SELECT_SLOT && (\n <button\n type=\"button\"\n onClick={handleBack}\n data-cimplify-booking-page-back\n className={classNames?.backButton}\n >\n Back\n </button>\n )}\n <h1 data-cimplify-booking-page-title className={classNames?.title}>\n {title || (isReschedule ? \"Reschedule Booking\" : `Book ${service.name}`)}\n </h1>\n </div>\n\n {isReschedule && existingBooking && (\n <div data-cimplify-booking-reschedule-info className={classNames?.rescheduleInfo}>\n <span>Rescheduling booking from {new Date(existingBooking.service_items[0]?.scheduled_start ?? existingBooking.created_at).toLocaleDateString()}</span>\n </div>\n )}\n\n <div data-cimplify-booking-service-info className={classNames?.serviceInfo}>\n <span data-cimplify-booking-service-name>{service.name}</span>\n <span data-cimplify-booking-service-duration>{formatDuration(service.duration_minutes, service.duration_unit)}</span>\n {service.price && (\n <span data-cimplify-booking-service-price>\n <Price amount={service.price} />\n </span>\n )}\n {hasDeposit && (\n <span data-cimplify-booking-deposit-info className={classNames?.depositInfo}>\n Deposit: {depositType === \"percentage\" ? `${parsePrice(depositAmount!)}%` : <Price amount={depositAmount!} />}\n </span>\n )}\n {cancellationMinutes !== undefined && cancellationMinutes > 0 && (\n <span data-cimplify-booking-cancellation-policy className={classNames?.cancellationPolicy}>\n Free cancellation up to {cancellationMinutes >= 60 ? `${Math.floor(cancellationMinutes / 60)} hour${Math.floor(cancellationMinutes / 60) !== 1 ? \"s\" : \"\"}` : `${cancellationMinutes} minute${cancellationMinutes !== 1 ? \"s\" : \"\"}`} before\n </span>\n )}\n {noShowFee !== undefined && parsePrice(noShowFee) !== 0 && (\n <span data-cimplify-booking-no-show-fee className={classNames?.cancellationPolicy}>\n No-show fee: <Price amount={noShowFee} />\n </span>\n )}\n </div>\n\n {error && (\n <div data-cimplify-booking-error className={classNames?.error}>\n {error}\n </div>\n )}\n\n {step === STEP.SELECT_SLOT && (\n <div data-cimplify-booking-step=\"select-slot\" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Select a date &amp; time\n </h2>\n <DateSlotPicker\n serviceId={service.id}\n participantCount={participantCount}\n selectedSlot={selectedSlot}\n onSlotSelect={handleSlotSelect}\n showPrice={showPrice}\n />\n </div>\n )}\n\n {step === STEP.SELECT_RESOURCE && resources && (\n <div data-cimplify-booking-step=\"select-resource\" className={cn(classNames?.step, classNames?.resourceStep)}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Choose a room\n </h2>\n <ResourcePicker\n resources={resources}\n selectedResourceId={selectedResourceId}\n onResourceSelect={handleResourceSelect}\n />\n </div>\n )}\n\n {step === STEP.SELECT_STAFF && staff && (\n <div data-cimplify-booking-step=\"select-staff\" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Choose a provider\n </h2>\n <StaffPicker\n staff={staff}\n selectedStaffId={selectedStaffId}\n onStaffSelect={handleStaffSelect}\n />\n </div>\n )}\n\n {step === STEP.CONFIRM && selectedSlot && (\n <div data-cimplify-booking-step=\"confirm\" className={classNames?.step}>\n <h2 data-cimplify-booking-step-title className={classNames?.stepTitle}>\n Confirm your booking\n </h2>\n <div data-cimplify-booking-summary className={classNames?.summary}>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Service</span>\n <span>{service.name}</span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Date</span>\n <span>{selectedDate}</span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Time</span>\n <span>\n {new Date(selectedSlot.start_time).toLocaleTimeString(undefined, {\n hour: \"numeric\",\n minute: \"2-digit\",\n })}\n </span>\n </div>\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Duration</span>\n <span>{formatDuration(service.duration_minutes, service.duration_unit)}</span>\n </div>\n {selectedResourceId && resources && (\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Room</span>\n <span>{resources.find((r) => r.id === selectedResourceId)?.name ?? \"Selected\"}</span>\n </div>\n )}\n {selectedStaffId && staff && (\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Provider</span>\n <span>{staff.find((s) => s.id === selectedStaffId)?.name ?? \"Selected\"}</span>\n </div>\n )}\n {(selectedSlot.price || service.price) && (\n <div data-cimplify-booking-summary-row className={classNames?.summaryRow}>\n <span>Price</span>\n <span>\n <Price amount={selectedSlot.price || service.price!} />\n </span>\n </div>\n )}\n {hasDeposit && (\n <div data-cimplify-booking-summary-row className={cn(classNames?.summaryRow, classNames?.depositInfo)}>\n <span>Deposit</span>\n <span>\n {depositType === \"percentage\" ? `${parsePrice(depositAmount!)}%` : <Price amount={depositAmount!} />}\n </span>\n </div>\n )}\n </div>\n <button\n type=\"button\"\n onClick={handleConfirm}\n disabled={isSubmitting}\n data-cimplify-booking-confirm\n className={classNames?.confirmButton}\n >\n {isSubmitting\n ? (isReschedule ? \"Rescheduling…\" : \"Adding to cart…\")\n : (isReschedule ? \"Reschedule\" : \"Confirm Booking\")}\n </button>\n </div>\n )}\n </div>\n );\n}\n"
16
16
  }
17
17
  ]
18
18
  }
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  {
12
12
  "path": "order-detail-page.tsx",
13
- "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Order, LineItem } from \"@cimplify/sdk\";\nimport { OrderSummary } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface OrderDetailPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n backButton?: string;\n summary?: string;\n}\n\nexport interface OrderDetailPageProps {\n /** Order ID to display. */\n orderId: string;\n /** Pre-fetched order for SSR. */\n order?: Order;\n /** Poll for status updates. Default: true. */\n poll?: boolean;\n /** Called when back button is clicked. */\n onBack?: () => void;\n /** Custom line item renderer. */\n renderLineItem?: (item: LineItem) => React.ReactNode;\n /** Back button label. */\n backLabel?: string;\n className?: string;\n classNames?: OrderDetailPageClassNames;\n}\n\n/**\n * OrderDetailPage — single order detail view with live status polling.\n *\n * SSR-friendly: pass `order` prop for server rendering.\n */\nexport function OrderDetailPage({\n orderId,\n order,\n poll = true,\n onBack,\n renderLineItem,\n backLabel = \"Back to orders\",\n className,\n classNames,\n}: OrderDetailPageProps): React.ReactElement {\n return (\n <div data-cimplify-order-detail-page className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-order-detail-header className={classNames?.header}>\n {onBack && (\n <button\n type=\"button\"\n onClick={onBack}\n data-cimplify-order-detail-back\n className={classNames?.backButton}\n >\n {backLabel}\n </button>\n )}\n <h1 data-cimplify-order-detail-title className={classNames?.title}>\n Order Details\n </h1>\n </div>\n\n {/* Order summary */}\n <div data-cimplify-order-detail-content className={classNames?.summary}>\n <OrderSummary\n order={order}\n orderId={order ? undefined : orderId}\n poll={poll}\n renderLineItem={renderLineItem}\n />\n </div>\n </div>\n );\n}\n"
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Order, LineItem, OrderStatus } from \"@cimplify/sdk\";\nimport { OrderSummary } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface OrderDetailPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n backButton?: string;\n summary?: string;\n}\n\nexport interface OrderDetailPageProps {\n /** Order ID to display. */\n orderId: string;\n /** Pre-fetched order for SSR. */\n order?: Order;\n /** Poll for status updates. Default: true. */\n poll?: boolean;\n /** Called when back button is clicked. */\n onBack?: () => void;\n /** Custom line item renderer. */\n renderLineItem?: (item: LineItem) => React.ReactNode;\n /** Called when the reorder button is clicked. */\n onReorder?: (order: Order) => void;\n /** Called when the order status changes during polling. */\n onStatusChange?: (previousStatus: OrderStatus, newStatus: OrderStatus, order: Order) => void;\n /** Back button label. */\n backLabel?: string;\n className?: string;\n classNames?: OrderDetailPageClassNames;\n}\n\n/**\n * OrderDetailPage — single order detail view with live status polling.\n *\n * SSR-friendly: pass `order` prop for server rendering.\n */\nexport function OrderDetailPage({\n orderId,\n order,\n poll = true,\n onBack,\n renderLineItem,\n onReorder,\n onStatusChange,\n backLabel = \"Back to orders\",\n className,\n classNames,\n}: OrderDetailPageProps): React.ReactElement {\n return (\n <div data-cimplify-order-detail-page className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-order-detail-header className={classNames?.header}>\n {onBack && (\n <button\n type=\"button\"\n onClick={onBack}\n data-cimplify-order-detail-back\n className={classNames?.backButton}\n >\n {backLabel}\n </button>\n )}\n <h1 data-cimplify-order-detail-title className={classNames?.title}>\n Order Details\n </h1>\n </div>\n\n {/* Order summary */}\n <div data-cimplify-order-detail-content className={classNames?.summary}>\n <OrderSummary\n order={order}\n orderId={order ? undefined : orderId}\n poll={poll}\n renderLineItem={renderLineItem}\n onReorder={onReorder}\n onStatusChange={onStatusChange}\n />\n </div>\n </div>\n );\n}\n"
14
14
  }
15
15
  ]
16
16
  }
@@ -11,7 +11,7 @@
11
11
  "files": [
12
12
  {
13
13
  "path": "order-history-page.tsx",
14
- "content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport { Tabs } from \"@base-ui/react/tabs\";\nimport type { Order, OrderStatus } from \"@cimplify/sdk\";\nimport { OrderHistory } from \"@cimplify/sdk/react\";\nimport { OrderSummary } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface OrderHistoryPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n filters?: string;\n filterButton?: string;\n list?: string;\n detail?: string;\n backButton?: string;\n}\n\nexport interface OrderHistoryPageProps {\n /** Page title. */\n title?: string;\n /** Pre-fetched orders for SSR. */\n orders?: Order[];\n /** Max orders to show per page. */\n limit?: number;\n /** Called when navigating to an order detail (e.g. for routing). */\n onOrderNavigate?: (order: Order) => void;\n /** Show status filter tabs. Default: true. */\n showFilters?: boolean;\n /** Custom order row renderer. */\n renderOrder?: (order: Order) => React.ReactNode;\n className?: string;\n classNames?: OrderHistoryPageClassNames;\n}\n\nconst STATUS_FILTERS: { label: string; value: OrderStatus | undefined; tabValue: string }[] = [\n { label: \"All\", value: undefined, tabValue: \"all\" },\n { label: \"Active\", value: \"confirmed\", tabValue: \"confirmed\" },\n { label: \"Completed\", value: \"completed\", tabValue: \"completed\" },\n { label: \"Cancelled\", value: \"cancelled\", tabValue: \"cancelled\" },\n];\n\n/**\n * OrderHistoryPage — order list with inline detail view and status filtering.\n *\n * SSR-friendly: pass `orders` prop for server rendering.\n * Supports both inline detail view and external navigation via `onOrderNavigate`.\n */\nexport function OrderHistoryPage({\n title = \"Order History\",\n orders: ordersProp,\n limit = 20,\n onOrderNavigate,\n showFilters = true,\n renderOrder,\n className,\n classNames,\n}: OrderHistoryPageProps): React.ReactElement {\n const [statusFilter, setStatusFilter] = useState<OrderStatus | undefined>(undefined);\n const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);\n\n const handleTabChange = useCallback((value: string | number | null) => {\n const filter = STATUS_FILTERS.find((f) => f.tabValue === value);\n setStatusFilter(filter?.value);\n }, []);\n\n const handleOrderClick = useCallback(\n (order: Order) => {\n if (onOrderNavigate) {\n onOrderNavigate(order);\n } else {\n setSelectedOrder(order);\n }\n },\n [onOrderNavigate],\n );\n\n const handleBack = useCallback(() => {\n setSelectedOrder(null);\n }, []);\n\n const activeTabValue = STATUS_FILTERS.find((f) => f.value === statusFilter)?.tabValue ?? \"all\";\n\n // Inline detail view\n if (selectedOrder && !onOrderNavigate) {\n return (\n <div data-cimplify-order-history-page className={cn(className, classNames?.root)}>\n <div data-cimplify-order-history-detail className={classNames?.detail}>\n <button\n type=\"button\"\n onClick={handleBack}\n data-cimplify-order-history-back\n className={classNames?.backButton}\n >\n Back to orders\n </button>\n <OrderSummary order={selectedOrder} />\n </div>\n </div>\n );\n }\n\n return (\n <div data-cimplify-order-history-page className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-order-history-header className={classNames?.header}>\n <h1 data-cimplify-order-history-title className={classNames?.title}>\n {title}\n </h1>\n </div>\n\n {/* Status filter */}\n {showFilters && (\n <Tabs.Root value={activeTabValue} onValueChange={handleTabChange}>\n <Tabs.List data-cimplify-order-history-filters className={classNames?.filters}>\n {STATUS_FILTERS.map((filter) => (\n <Tabs.Tab\n key={filter.tabValue}\n value={filter.tabValue}\n data-cimplify-order-filter\n data-selected={statusFilter === filter.value || undefined}\n className={classNames?.filterButton}\n >\n {filter.label}\n </Tabs.Tab>\n ))}\n </Tabs.List>\n </Tabs.Root>\n )}\n\n {/* Order list */}\n <div data-cimplify-order-history-list className={classNames?.list}>\n <OrderHistory\n orders={ordersProp}\n status={statusFilter}\n limit={limit}\n onOrderClick={handleOrderClick}\n renderOrder={renderOrder}\n />\n </div>\n </div>\n );\n}\n"
14
+ "content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport { Tabs } from \"@base-ui/react/tabs\";\nimport type { Order, OrderStatus } from \"@cimplify/sdk\";\nimport { OrderHistory } from \"@cimplify/sdk/react\";\nimport { OrderSummary } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface OrderHistoryPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n filters?: string;\n filterButton?: string;\n list?: string;\n detail?: string;\n backButton?: string;\n}\n\nexport interface OrderHistoryPageProps {\n /** Page title. */\n title?: string;\n /** Pre-fetched orders for SSR. */\n orders?: Order[];\n /** Max orders to show per page. */\n limit?: number;\n /** Called when navigating to an order detail (e.g. for routing). */\n onOrderNavigate?: (order: Order) => void;\n /** Called when the reorder button is clicked. */\n onReorder?: (order: Order) => void;\n /** Show status filter tabs. Default: true. */\n showFilters?: boolean;\n /** Custom order row renderer. */\n renderOrder?: (order: Order) => React.ReactNode;\n className?: string;\n classNames?: OrderHistoryPageClassNames;\n}\n\nconst STATUS_FILTERS: { label: string; value: OrderStatus | undefined; tabValue: string }[] = [\n { label: \"All\", value: undefined, tabValue: \"all\" },\n { label: \"Active\", value: \"confirmed\", tabValue: \"confirmed\" },\n { label: \"Completed\", value: \"completed\", tabValue: \"completed\" },\n { label: \"Cancelled\", value: \"cancelled\", tabValue: \"cancelled\" },\n];\n\n/**\n * OrderHistoryPage — order list with inline detail view and status filtering.\n *\n * SSR-friendly: pass `orders` prop for server rendering.\n * Supports both inline detail view and external navigation via `onOrderNavigate`.\n */\nexport function OrderHistoryPage({\n title = \"Order History\",\n orders: ordersProp,\n limit = 20,\n onOrderNavigate,\n onReorder,\n showFilters = true,\n renderOrder,\n className,\n classNames,\n}: OrderHistoryPageProps): React.ReactElement {\n const [statusFilter, setStatusFilter] = useState<OrderStatus | undefined>(undefined);\n const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);\n\n const handleTabChange = useCallback((value: string | number | null) => {\n const filter = STATUS_FILTERS.find((f) => f.tabValue === value);\n setStatusFilter(filter?.value);\n }, []);\n\n const handleOrderClick = useCallback(\n (order: Order) => {\n if (onOrderNavigate) {\n onOrderNavigate(order);\n } else {\n setSelectedOrder(order);\n }\n },\n [onOrderNavigate],\n );\n\n const handleBack = useCallback(() => {\n setSelectedOrder(null);\n }, []);\n\n const activeTabValue = STATUS_FILTERS.find((f) => f.value === statusFilter)?.tabValue ?? \"all\";\n\n // Inline detail view\n if (selectedOrder && !onOrderNavigate) {\n return (\n <div data-cimplify-order-history-page className={cn(className, classNames?.root)}>\n <div data-cimplify-order-history-detail className={classNames?.detail}>\n <button\n type=\"button\"\n onClick={handleBack}\n data-cimplify-order-history-back\n className={classNames?.backButton}\n >\n Back to orders\n </button>\n <OrderSummary order={selectedOrder} onReorder={onReorder} />\n </div>\n </div>\n );\n }\n\n return (\n <div data-cimplify-order-history-page className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-order-history-header className={classNames?.header}>\n <h1 data-cimplify-order-history-title className={classNames?.title}>\n {title}\n </h1>\n </div>\n\n {/* Status filter */}\n {showFilters && (\n <Tabs.Root value={activeTabValue} onValueChange={handleTabChange}>\n <Tabs.List data-cimplify-order-history-filters className={classNames?.filters}>\n {STATUS_FILTERS.map((filter) => (\n <Tabs.Tab\n key={filter.tabValue}\n value={filter.tabValue}\n data-cimplify-order-filter\n data-selected={statusFilter === filter.value || undefined}\n className={classNames?.filterButton}\n >\n {filter.label}\n </Tabs.Tab>\n ))}\n </Tabs.List>\n </Tabs.Root>\n )}\n\n {/* Order list */}\n <div data-cimplify-order-history-list className={classNames?.list}>\n <OrderHistory\n orders={ordersProp}\n status={statusFilter}\n limit={limit}\n onOrderClick={handleOrderClick}\n renderOrder={renderOrder}\n onReorder={onReorder}\n />\n </div>\n </div>\n );\n}\n"
15
15
  }
16
16
  ]
17
17
  }
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  {
12
12
  "path": "order-history.tsx",
13
- "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Order, OrderStatus } from \"@cimplify/sdk\";\nimport { useOrders } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface OrderHistoryClassNames {\n root?: string;\n item?: string;\n orderId?: string;\n status?: string;\n date?: string;\n total?: string;\n empty?: string;\n loading?: string;\n}\n\nexport interface OrderHistoryProps {\n /** Override orders (skips fetch). For SSR, pass pre-fetched orders. */\n orders?: Order[];\n /** Filter by status. */\n status?: OrderStatus;\n /** Max orders to display. */\n limit?: number;\n /** Called when an order row is clicked. */\n onOrderClick?: (order: Order) => void;\n /** Custom order row renderer. */\n renderOrder?: (order: Order) => React.ReactNode;\n /** Text shown when empty. */\n emptyMessage?: string;\n className?: string;\n classNames?: OrderHistoryClassNames;\n}\n\nconst STATUS_LABELS: Record<OrderStatus, string> = {\n pending: \"Pending\",\n created: \"Created\",\n confirmed: \"Confirmed\",\n in_preparation: \"In Preparation\",\n ready_to_serve: \"Ready\",\n partially_served: \"Partially Served\",\n served: \"Served\",\n delivered: \"Delivered\",\n picked_up: \"Picked Up\",\n completed: \"Completed\",\n cancelled: \"Cancelled\",\n refunded: \"Refunded\",\n};\n\n/**\n * OrderHistory — list of past orders with status and totals.\n *\n * Fetches via `useOrders` unless pre-loaded orders are passed.\n */\nexport function OrderHistory({\n orders: ordersProp,\n status,\n limit,\n onOrderClick,\n renderOrder,\n emptyMessage = \"No orders yet\",\n className,\n classNames,\n}: OrderHistoryProps): React.ReactElement {\n const { orders: fetched, isLoading } = useOrders({\n status,\n limit,\n enabled: ordersProp === undefined,\n });\n\n const orders = ordersProp ?? fetched;\n\n if (isLoading && orders.length === 0) {\n return (\n <div\n data-cimplify-order-history\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (orders.length === 0) {\n return (\n <div\n data-cimplify-order-history\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage}</p>\n </div>\n );\n }\n\n return (\n <div data-cimplify-order-history className={cn(className, classNames?.root)}>\n {orders.map((order) => (\n <button\n key={order.id}\n type=\"button\"\n onClick={() => onOrderClick?.(order)}\n data-cimplify-order-history-item\n data-status={order.status}\n className={classNames?.item}\n >\n {renderOrder ? (\n renderOrder(order)\n ) : (\n <>\n <div data-cimplify-order-history-main>\n <span data-cimplify-order-history-id className={classNames?.orderId}>\n #{order.user_friendly_id}\n </span>\n <span\n data-cimplify-order-history-status\n data-status={order.status}\n className={classNames?.status}\n >\n {STATUS_LABELS[order.status] ?? order.status}\n </span>\n </div>\n <div data-cimplify-order-history-details>\n <time\n data-cimplify-order-history-date\n dateTime={order.created_at}\n className={classNames?.date}\n >\n {new Date(order.created_at).toLocaleDateString()}\n </time>\n <span data-cimplify-order-history-items>\n {order.total_quantity} {order.total_quantity === 1 ? \"item\" : \"items\"}\n </span>\n <span data-cimplify-order-history-total className={classNames?.total}>\n <Price amount={order.total_price} />\n </span>\n </div>\n </>\n )}\n </button>\n ))}\n </div>\n );\n}\n"
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Order, OrderStatus } from \"@cimplify/sdk\";\nimport { useOrders } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface OrderHistoryClassNames {\n root?: string;\n item?: string;\n orderId?: string;\n status?: string;\n date?: string;\n total?: string;\n empty?: string;\n loading?: string;\n reorderButton?: string;\n}\n\nexport interface OrderHistoryProps {\n /** Override orders (skips fetch). For SSR, pass pre-fetched orders. */\n orders?: Order[];\n /** Filter by status. */\n status?: OrderStatus;\n /** Max orders to display. */\n limit?: number;\n /** Called when an order row is clicked. */\n onOrderClick?: (order: Order) => void;\n /** Custom order row renderer. */\n renderOrder?: (order: Order) => React.ReactNode;\n /** Called when the reorder button is clicked. If provided, a reorder button is rendered. */\n onReorder?: (order: Order) => void;\n /** Text shown when empty. */\n emptyMessage?: string;\n className?: string;\n classNames?: OrderHistoryClassNames;\n}\n\nconst STATUS_LABELS: Record<OrderStatus, string> = {\n pending: \"Pending\",\n created: \"Created\",\n confirmed: \"Confirmed\",\n in_preparation: \"In Preparation\",\n ready_to_serve: \"Ready\",\n partially_served: \"Partially Served\",\n served: \"Served\",\n delivered: \"Delivered\",\n picked_up: \"Picked Up\",\n completed: \"Completed\",\n cancelled: \"Cancelled\",\n refunded: \"Refunded\",\n};\n\n/**\n * OrderHistory — list of past orders with status and totals.\n *\n * Fetches via `useOrders` unless pre-loaded orders are passed.\n */\nexport function OrderHistory({\n orders: ordersProp,\n status,\n limit,\n onOrderClick,\n renderOrder,\n onReorder,\n emptyMessage = \"No orders yet\",\n className,\n classNames,\n}: OrderHistoryProps): React.ReactElement {\n const { orders: fetched, isLoading } = useOrders({\n status,\n limit,\n enabled: ordersProp === undefined,\n });\n\n const orders = ordersProp ?? fetched;\n\n if (isLoading && orders.length === 0) {\n return (\n <div\n data-cimplify-order-history\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (orders.length === 0) {\n return (\n <div\n data-cimplify-order-history\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage}</p>\n </div>\n );\n }\n\n return (\n <div data-cimplify-order-history className={cn(className, classNames?.root)}>\n {orders.map((order) => (\n <button\n key={order.id}\n type=\"button\"\n onClick={() => onOrderClick?.(order)}\n data-cimplify-order-history-item\n data-status={order.status}\n className={classNames?.item}\n >\n {renderOrder ? (\n renderOrder(order)\n ) : (\n <>\n <div data-cimplify-order-history-main>\n <span data-cimplify-order-history-id className={classNames?.orderId}>\n #{order.user_friendly_id}\n </span>\n <span\n data-cimplify-order-history-status\n data-status={order.status}\n className={classNames?.status}\n >\n {STATUS_LABELS[order.status] ?? order.status}\n </span>\n </div>\n <div data-cimplify-order-history-details>\n <time\n data-cimplify-order-history-date\n dateTime={order.created_at}\n className={classNames?.date}\n >\n {new Date(order.created_at).toLocaleDateString()}\n </time>\n <span data-cimplify-order-history-items>\n {order.total_quantity} {order.total_quantity === 1 ? \"item\" : \"items\"}\n </span>\n <span data-cimplify-order-history-total className={classNames?.total}>\n <Price amount={order.total_price} />\n </span>\n </div>\n {onReorder && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onReorder(order);\n }}\n data-cimplify-order-reorder\n className={classNames?.reorderButton}\n >\n Reorder\n </button>\n )}\n </>\n )}\n </button>\n ))}\n </div>\n );\n}\n"
14
14
  }
15
15
  ]
16
16
  }
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  {
12
12
  "path": "order-summary.tsx",
13
- "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Order, LineItem, OrderStatus, PaymentState } from \"@cimplify/sdk\";\nimport { useOrder } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface OrderSummaryClassNames {\n root?: string;\n header?: string;\n orderId?: string;\n status?: string;\n paymentState?: string;\n fulfillmentDetails?: string;\n deliveryAddress?: string;\n items?: string;\n lineItem?: string;\n notes?: string;\n totals?: string;\n customer?: string;\n loading?: string;\n serviceInfo?: string;\n digitalInfo?: string;\n bundleBreakdown?: string;\n compositeBreakdown?: string;\n trackingLink?: string;\n}\n\nexport interface OrderSummaryProps {\n /** Pass an Order object directly (skips fetch). */\n order?: Order;\n /** Or pass an order ID to fetch via useOrder. */\n orderId?: string;\n /** Poll for status updates. */\n poll?: boolean;\n /** Custom line item renderer. */\n renderLineItem?: (item: LineItem) => React.ReactNode;\n className?: string;\n classNames?: OrderSummaryClassNames;\n}\n\nconst STATUS_LABELS: Record<OrderStatus, string> = {\n pending: \"Pending\",\n created: \"Created\",\n confirmed: \"Confirmed\",\n in_preparation: \"In Preparation\",\n ready_to_serve: \"Ready\",\n partially_served: \"Partially Served\",\n served: \"Served\",\n delivered: \"Delivered\",\n picked_up: \"Picked Up\",\n completed: \"Completed\",\n cancelled: \"Cancelled\",\n refunded: \"Refunded\",\n};\n\nconst PAYMENT_STATE_LABELS: Record<PaymentState, string> = {\n not_paid: \"Not Paid\",\n paid: \"Paid\",\n partially_refunded: \"Partially Refunded\",\n refunded: \"Refunded\",\n};\n\n/**\n * OrderSummary — displays a single order's details, line items, and totals.\n *\n * Accepts either a pre-loaded `order` object or an `orderId` to fetch.\n * Supports polling for live status updates.\n */\nexport function OrderSummary({\n order: orderProp,\n orderId,\n poll = false,\n renderLineItem,\n className,\n classNames,\n}: OrderSummaryProps): React.ReactElement {\n const { order: fetched, isLoading } = useOrder(orderProp ? null : orderId, {\n enabled: !orderProp && !!orderId,\n poll,\n });\n\n const order = orderProp ?? fetched;\n\n if (isLoading && !order) {\n return (\n <div\n data-cimplify-order-summary\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n >\n <div data-cimplify-order-summary-skeleton />\n </div>\n );\n }\n\n if (!order) {\n return (\n <div data-cimplify-order-summary className={cn(className, classNames?.root)}>\n <p>Order not found.</p>\n </div>\n );\n }\n\n return (\n <div data-cimplify-order-summary className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-order-header className={classNames?.header}>\n <span data-cimplify-order-id className={classNames?.orderId}>\n Order #{order.user_friendly_id}\n </span>\n <span\n data-cimplify-order-status\n data-status={order.status}\n className={classNames?.status}\n >\n {STATUS_LABELS[order.status] ?? order.status}\n </span>\n {order.payment_state && (\n <span\n data-cimplify-order-payment-state\n data-payment-state={order.payment_state}\n className={classNames?.paymentState}\n >\n {PAYMENT_STATE_LABELS[order.payment_state] ?? order.payment_state}\n </span>\n )}\n </div>\n\n {/* Fulfillment details */}\n {order.order_type === \"pickup\" && order.pickup_time && (\n <div data-cimplify-order-fulfillment className={classNames?.fulfillmentDetails}>\n <span>Pickup time: {new Date(order.pickup_time).toLocaleString()}</span>\n </div>\n )}\n {order.order_type === \"dine-in\" && order.table_number && (\n <div data-cimplify-order-fulfillment className={classNames?.fulfillmentDetails}>\n <span>Table: {order.table_number}</span>\n </div>\n )}\n\n {/* Date */}\n <time data-cimplify-order-date dateTime={order.created_at}>\n {new Date(order.created_at).toLocaleDateString(undefined, {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n })}\n </time>\n\n {/* Customer info */}\n {(order.customer_name || order.customer_email) && (\n <div data-cimplify-order-customer className={classNames?.customer}>\n {order.customer_name && (\n <span data-cimplify-order-customer-name>{order.customer_name}</span>\n )}\n {order.customer_email && (\n <span data-cimplify-order-customer-email>{order.customer_email}</span>\n )}\n </div>\n )}\n\n {/* Delivery address */}\n {order.delivery_address && (\n <div data-cimplify-order-delivery-address className={classNames?.deliveryAddress}>\n <span>Delivery address</span>\n <p>{order.delivery_address}</p>\n </div>\n )}\n\n {/* Line items */}\n <div data-cimplify-order-items className={classNames?.items}>\n {order.items.map((item) =>\n renderLineItem ? (\n <React.Fragment key={item.id}>{renderLineItem(item)}</React.Fragment>\n ) : (\n <div key={item.id} data-cimplify-order-line-item className={classNames?.lineItem}>\n <div data-cimplify-order-line-info>\n <span data-cimplify-order-line-qty>{item.quantity}&times;</span>\n <span data-cimplify-order-line-key>{item.line_key}</span>\n </div>\n <Price amount={item.price} />\n {item.fulfillment_type === \"digital\" && item.fulfillment_id && (\n <a\n href={item.fulfillment_id}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n data-cimplify-order-line-download\n className=\"text-sm text-primary underline\"\n >\n Download\n </a>\n )}\n\n {/* Service items: show scheduling and confirmation details */}\n {item.configuration.type === \"service\" && (\n <div data-cimplify-order-service-info className={classNames?.serviceInfo}>\n {item.configuration.scheduled_start && (\n <span>{\"\\u{1F4C5}\"} {new Date(item.configuration.scheduled_start).toLocaleDateString()} at {new Date(item.configuration.scheduled_start).toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" })}</span>\n )}\n {item.configuration.confirmation_code && (\n <span>Confirmation: {item.configuration.confirmation_code}</span>\n )}\n {item.configuration.service_status && (\n <span data-status={item.configuration.service_status}>{item.configuration.service_status}</span>\n )}\n </div>\n )}\n\n {/* Digital items: instant delivery badge */}\n {item.configuration.type === \"digital\" && (\n <div data-cimplify-order-digital-info className={classNames?.digitalInfo}>\n <span>{\"\\u26A1\"} Instant digital delivery</span>\n </div>\n )}\n\n {/* Bundle items: component breakdown */}\n {item.configuration.type === \"bundle\" && item.configuration.resolved && (\n <div data-cimplify-order-bundle-breakdown className={classNames?.bundleBreakdown}>\n {item.configuration.resolved.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 items: component breakdown with pricing */}\n {item.configuration.type === \"composite\" && item.configuration.resolved && (\n <div data-cimplify-order-composite-breakdown className={classNames?.compositeBreakdown}>\n {item.configuration.resolved.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 <Price amount={sel.unit_price} />\n </div>\n ))}\n </div>\n )}\n </div>\n ),\n )}\n </div>\n\n {/* Customer notes / special instructions */}\n {order.customer_notes && order.customer_notes.length > 0 && (\n <div data-cimplify-order-notes className={classNames?.notes}>\n <span>Notes</span>\n {order.customer_notes.map((note, index) => (\n <p key={index} data-cimplify-order-note>{note}</p>\n ))}\n </div>\n )}\n\n {/* Totals */}\n <div data-cimplify-order-totals className={classNames?.totals}>\n {order.total_discount != null && parsePrice(order.total_discount) !== 0 && (\n <div data-cimplify-order-discount>\n <span>Discount</span>\n <Price amount={order.total_discount} prefix=\"-\" />\n </div>\n )}\n {order.delivery_fee != null && parsePrice(order.delivery_fee) > 0 && (\n <div data-cimplify-order-delivery-fee>\n <span>Delivery fee</span>\n <Price amount={order.delivery_fee} />\n </div>\n )}\n {order.service_charge != null && parsePrice(order.service_charge) !== 0 && (\n <div data-cimplify-order-service-charge>\n <span>Service charge</span>\n <Price amount={order.service_charge} />\n </div>\n )}\n {order.tax != null && parsePrice(order.tax) !== 0 && (\n <div data-cimplify-order-tax>\n <span>Tax</span>\n <Price amount={order.tax} />\n </div>\n )}\n <div data-cimplify-order-total>\n <span>Total</span>\n <Price amount={order.total_price} />\n </div>\n </div>\n\n {/* Tracking */}\n {order.tracking_link && (\n <a\n href={order.tracking_link}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n data-cimplify-order-tracking\n className={classNames?.trackingLink}\n >\n Track your order\n </a>\n )}\n </div>\n );\n}\n"
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Order, LineItem, OrderStatus, PaymentState } from \"@cimplify/sdk\";\nimport { useOrder } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface OrderSummaryClassNames {\n root?: string;\n header?: string;\n orderId?: string;\n status?: string;\n paymentState?: string;\n fulfillmentDetails?: string;\n deliveryAddress?: string;\n items?: string;\n lineItem?: string;\n notes?: string;\n totals?: string;\n customer?: string;\n loading?: string;\n serviceInfo?: string;\n digitalInfo?: string;\n bundleBreakdown?: string;\n compositeBreakdown?: string;\n trackingLink?: string;\n reorderButton?: string;\n}\n\nexport interface OrderSummaryProps {\n /** Pass an Order object directly (skips fetch). */\n order?: Order;\n /** Or pass an order ID to fetch via useOrder. */\n orderId?: string;\n /** Poll for status updates. */\n poll?: boolean;\n /** Custom line item renderer. */\n renderLineItem?: (item: LineItem) => React.ReactNode;\n /** Called when the reorder button is clicked. */\n onReorder?: (order: Order) => void;\n /** Called when the order status changes during polling. */\n onStatusChange?: (previousStatus: OrderStatus, newStatus: OrderStatus, order: Order) => void;\n className?: string;\n classNames?: OrderSummaryClassNames;\n}\n\nconst STATUS_LABELS: Record<OrderStatus, string> = {\n pending: \"Pending\",\n created: \"Created\",\n confirmed: \"Confirmed\",\n in_preparation: \"In Preparation\",\n ready_to_serve: \"Ready\",\n partially_served: \"Partially Served\",\n served: \"Served\",\n delivered: \"Delivered\",\n picked_up: \"Picked Up\",\n completed: \"Completed\",\n cancelled: \"Cancelled\",\n refunded: \"Refunded\",\n};\n\nconst PAYMENT_STATE_LABELS: Record<PaymentState, string> = {\n not_paid: \"Not Paid\",\n paid: \"Paid\",\n partially_refunded: \"Partially Refunded\",\n refunded: \"Refunded\",\n};\n\n/**\n * OrderSummary — displays a single order's details, line items, and totals.\n *\n * Accepts either a pre-loaded `order` object or an `orderId` to fetch.\n * Supports polling for live status updates.\n */\nexport function OrderSummary({\n order: orderProp,\n orderId,\n poll = false,\n renderLineItem,\n onReorder,\n onStatusChange,\n className,\n classNames,\n}: OrderSummaryProps): React.ReactElement {\n const { order: fetched, isLoading } = useOrder(orderProp ? null : orderId, {\n enabled: !orderProp && !!orderId,\n poll,\n onStatusChange,\n });\n\n const order = orderProp ?? fetched;\n\n if (isLoading && !order) {\n return (\n <div\n data-cimplify-order-summary\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n >\n <div data-cimplify-order-summary-skeleton />\n </div>\n );\n }\n\n if (!order) {\n return (\n <div data-cimplify-order-summary className={cn(className, classNames?.root)}>\n <p>Order not found.</p>\n </div>\n );\n }\n\n return (\n <div data-cimplify-order-summary className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-order-header className={classNames?.header}>\n <span data-cimplify-order-id className={classNames?.orderId}>\n Order #{order.user_friendly_id}\n </span>\n <span\n data-cimplify-order-status\n data-status={order.status}\n className={classNames?.status}\n >\n {STATUS_LABELS[order.status] ?? order.status}\n </span>\n {order.payment_state && (\n <span\n data-cimplify-order-payment-state\n data-payment-state={order.payment_state}\n className={classNames?.paymentState}\n >\n {PAYMENT_STATE_LABELS[order.payment_state] ?? order.payment_state}\n </span>\n )}\n </div>\n\n {/* Fulfillment details */}\n {order.order_type === \"pickup\" && order.pickup_time && (\n <div data-cimplify-order-fulfillment className={classNames?.fulfillmentDetails}>\n <span>Pickup time: {new Date(order.pickup_time).toLocaleString()}</span>\n </div>\n )}\n {order.order_type === \"dine-in\" && order.table_number && (\n <div data-cimplify-order-fulfillment className={classNames?.fulfillmentDetails}>\n <span>Table: {order.table_number}</span>\n </div>\n )}\n\n {/* Date */}\n <time data-cimplify-order-date dateTime={order.created_at}>\n {new Date(order.created_at).toLocaleDateString(undefined, {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n })}\n </time>\n\n {/* Customer info */}\n {(order.customer_name || order.customer_email) && (\n <div data-cimplify-order-customer className={classNames?.customer}>\n {order.customer_name && (\n <span data-cimplify-order-customer-name>{order.customer_name}</span>\n )}\n {order.customer_email && (\n <span data-cimplify-order-customer-email>{order.customer_email}</span>\n )}\n </div>\n )}\n\n {/* Delivery address */}\n {order.delivery_address && (\n <div data-cimplify-order-delivery-address className={classNames?.deliveryAddress}>\n <span>Delivery address</span>\n <p>{order.delivery_address}</p>\n </div>\n )}\n\n {/* Line items */}\n <div data-cimplify-order-items className={classNames?.items}>\n {order.items.map((item) =>\n renderLineItem ? (\n <React.Fragment key={item.id}>{renderLineItem(item)}</React.Fragment>\n ) : (\n <div key={item.id} data-cimplify-order-line-item className={classNames?.lineItem}>\n <div data-cimplify-order-line-info>\n <span data-cimplify-order-line-qty>{item.quantity}&times;</span>\n <span data-cimplify-order-line-key>{item.line_key}</span>\n </div>\n <Price amount={item.price} />\n {item.fulfillment_type === \"digital\" && item.fulfillment_id && (\n <a\n href={item.fulfillment_id}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n data-cimplify-order-line-download\n className=\"text-sm text-primary underline\"\n >\n Download\n </a>\n )}\n\n {/* Service items: show scheduling and confirmation details */}\n {item.configuration.type === \"service\" && (\n <div data-cimplify-order-service-info className={classNames?.serviceInfo}>\n {item.configuration.scheduled_start && (\n <span>{\"\\u{1F4C5}\"} {new Date(item.configuration.scheduled_start).toLocaleDateString()} at {new Date(item.configuration.scheduled_start).toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" })}</span>\n )}\n {item.configuration.confirmation_code && (\n <span>Confirmation: {item.configuration.confirmation_code}</span>\n )}\n {item.configuration.service_status && (\n <span data-status={item.configuration.service_status}>{item.configuration.service_status}</span>\n )}\n </div>\n )}\n\n {/* Digital items: instant delivery badge */}\n {item.configuration.type === \"digital\" && (\n <div data-cimplify-order-digital-info className={classNames?.digitalInfo}>\n <span>{\"\\u26A1\"} Instant digital delivery</span>\n </div>\n )}\n\n {/* Bundle items: component breakdown */}\n {item.configuration.type === \"bundle\" && item.configuration.resolved && (\n <div data-cimplify-order-bundle-breakdown className={classNames?.bundleBreakdown}>\n {item.configuration.resolved.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 items: component breakdown with pricing */}\n {item.configuration.type === \"composite\" && item.configuration.resolved && (\n <div data-cimplify-order-composite-breakdown className={classNames?.compositeBreakdown}>\n {item.configuration.resolved.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 <Price amount={sel.unit_price} />\n </div>\n ))}\n </div>\n )}\n </div>\n ),\n )}\n </div>\n\n {/* Customer notes / special instructions */}\n {order.customer_notes && order.customer_notes.length > 0 && (\n <div data-cimplify-order-notes className={classNames?.notes}>\n <span>Notes</span>\n {order.customer_notes.map((note, index) => (\n <p key={index} data-cimplify-order-note>{note}</p>\n ))}\n </div>\n )}\n\n {/* Totals */}\n <div data-cimplify-order-totals className={classNames?.totals}>\n {order.total_discount != null && parsePrice(order.total_discount) !== 0 && (\n <div data-cimplify-order-discount>\n <span>Discount</span>\n <Price amount={order.total_discount} prefix=\"-\" />\n </div>\n )}\n {order.delivery_fee != null && parsePrice(order.delivery_fee) > 0 && (\n <div data-cimplify-order-delivery-fee>\n <span>Delivery fee</span>\n <Price amount={order.delivery_fee} />\n </div>\n )}\n {order.service_charge != null && parsePrice(order.service_charge) !== 0 && (\n <div data-cimplify-order-service-charge>\n <span>Service charge</span>\n <Price amount={order.service_charge} />\n </div>\n )}\n {order.tax != null && parsePrice(order.tax) !== 0 && (\n <div data-cimplify-order-tax>\n <span>Tax</span>\n <Price amount={order.tax} />\n </div>\n )}\n <div data-cimplify-order-total>\n <span>Total</span>\n <Price amount={order.total_price} />\n </div>\n </div>\n\n {/* Tracking */}\n {order.tracking_link && (\n <a\n href={order.tracking_link}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n data-cimplify-order-tracking\n className={classNames?.trackingLink}\n >\n Track your order\n </a>\n )}\n\n {onReorder && (\n <button\n type=\"button\"\n onClick={() => onReorder(order)}\n data-cimplify-order-reorder\n className={classNames?.reorderButton}\n >\n Reorder\n </button>\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 { useProduct } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { ProductSheet } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\nexport interface ProductCardClassNames {\n root?: string;\n imageContainer?: string;\n image?: string;\n body?: string;\n name?: string;\n description?: string;\n price?: string;\n badges?: string;\n badge?: string;\n modal?: string;\n modalOverlay?: string;\n}\n\nexport interface ProductCardProps {\n /** The product to display. */\n product: Product;\n /** Display mode: \"card\" opens a modal, \"page\" renders as a link. Auto-detected from product.display_mode. */\n displayMode?: \"card\" | \"page\";\n /** Link href for page mode. Default: `/menu/${product.slug}` */\n href?: string;\n /** Custom modal content renderer. Receives the fully-loaded product and a close callback. */\n renderModal?: (product: ProductWithDetails, onClose: () => void) => React.ReactNode;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n /** Custom link renderer for page mode (e.g. Next.js Link). */\n renderLink?: (props: {\n href: string;\n className?: string;\n children: React.ReactNode;\n }) => React.ReactElement;\n /** Replace the entire default card body. */\n children?: React.ReactNode;\n /** Image aspect ratio. Default: \"4/3\". */\n aspectRatio?: \"square\" | \"4/3\" | \"16/10\" | \"3/4\";\n className?: string;\n classNames?: ProductCardClassNames;\n}\n\n/**\n * ProductCard — a product display card with two modes:\n *\n * - **card** (default): clickable button that opens a Base UI Dialog modal\n * - **page**: a plain `<a>` link for SEO-friendly product pages\n */\nexport function ProductCard({\n product,\n displayMode,\n href,\n renderModal,\n renderImage,\n renderLink,\n children,\n aspectRatio = \"4/3\",\n className,\n classNames,\n}: ProductCardProps): React.ReactElement {\n const mode = displayMode ?? product.display_mode ?? \"card\";\n const [isOpen, setIsOpen] = useState(false);\n const [shouldFetch, setShouldFetch] = useState(false);\n\n // Prefetch on pointer enter, always fetch when open\n const { product: productDetails } = useProduct(\n product.slug ?? product.id,\n { enabled: shouldFetch || isOpen },\n );\n\n const handlePrefetch = useCallback(() => {\n setShouldFetch(true);\n }, []);\n\n const handleOpenChange = useCallback((open: boolean) => {\n setIsOpen(open);\n if (open) {\n setShouldFetch(true);\n }\n }, []);\n\n const handleClose = useCallback(() => {\n setIsOpen(false);\n }, []);\n\n const imageUrl = product.image_url || product.images?.[0];\n\n const cardBody = children ?? (\n <>\n {/* Image */}\n {imageUrl && (\n <div\n data-cimplify-product-card-image-container\n className={classNames?.imageContainer}\n style={{\n overflow: \"hidden\",\n ...ASPECT_STYLES[aspectRatio],\n }}\n >\n {renderImage ? (\n renderImage({\n src: imageUrl,\n alt: product.name,\n className: classNames?.image,\n })\n ) : (\n <img\n src={imageUrl}\n alt={product.name}\n className={classNames?.image}\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n data-cimplify-product-card-image\n />\n )}\n </div>\n )}\n\n {/* Body */}\n <div\n data-cimplify-product-card-body\n className={classNames?.body}\n >\n <span\n data-cimplify-product-card-name\n className={classNames?.name}\n >\n {product.name}\n </span>\n {product.description && (\n <span\n data-cimplify-product-card-description\n className={classNames?.description}\n style={{\n display: \"-webkit-box\",\n WebkitLineClamp: 2,\n WebkitBoxOrient: \"vertical\",\n overflow: \"hidden\",\n }}\n >\n {product.description}\n </span>\n )}\n <Price\n amount={product.default_price}\n className={classNames?.price}\n />\n </div>\n </>\n );\n\n // Page mode — render as a link\n if (mode === \"page\") {\n const linkHref = href ?? `/menu/${product.slug}`;\n const linkClassName = cn(\"block no-underline text-[inherit]\", className, classNames?.root);\n\n if (renderLink) {\n return renderLink({ href: linkHref, className: linkClassName, children: cardBody });\n }\n\n return (\n <a\n href={linkHref}\n data-cimplify-product-card\n data-display-mode=\"page\"\n className={linkClassName}\n >\n {cardBody}\n </a>\n );\n }\n\n // Card mode — render as Base UI Dialog\n return (\n <Dialog.Root open={isOpen} onOpenChange={handleOpenChange}>\n <Dialog.Trigger\n onPointerEnter={handlePrefetch}\n data-cimplify-product-card\n data-display-mode=\"card\"\n className={cn(\n \"block w-full text-left bg-transparent border-none p-0 cursor-pointer font-[inherit] text-[inherit]\",\n className,\n classNames?.root,\n )}\n >\n {cardBody}\n </Dialog.Trigger>\n\n <Dialog.Portal>\n <Dialog.Backdrop\n data-cimplify-product-card-backdrop\n className={cn(\n \"fixed inset-0 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 { useProduct } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { ProductSheet } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\nexport interface ProductCardClassNames {\n root?: string;\n imageContainer?: string;\n image?: string;\n body?: string;\n name?: string;\n description?: string;\n price?: string;\n badges?: string;\n badge?: string;\n modal?: string;\n modalOverlay?: string;\n}\n\nexport interface ProductCardProps {\n /** The product to display. */\n product: Product;\n /** Display mode: \"card\" opens a modal, \"page\" renders as a link. Auto-detected from product.display_mode. */\n displayMode?: \"card\" | \"page\";\n /** Link href for page mode. Default: `/menu/${product.slug}` */\n href?: string;\n /** Custom modal content renderer. Receives the fully-loaded product and a close callback. */\n renderModal?: (product: ProductWithDetails, onClose: () => void) => React.ReactNode;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n /** Custom link renderer for page mode (e.g. Next.js Link). */\n renderLink?: (props: {\n href: string;\n className?: string;\n children: React.ReactNode;\n }) => React.ReactElement;\n /** Replace the entire default card body. */\n children?: React.ReactNode;\n /** Image aspect ratio. Default: \"4/3\". */\n aspectRatio?: \"square\" | \"4/3\" | \"16/10\" | \"3/4\";\n className?: string;\n classNames?: ProductCardClassNames;\n}\n\n/**\n * ProductCard — a product display card with two modes:\n *\n * - **card** (default): clickable button that opens a Base UI Dialog modal\n * - **page**: a plain `<a>` link for SEO-friendly product pages\n */\nexport function ProductCard({\n product,\n displayMode,\n href,\n renderModal,\n renderImage,\n renderLink,\n children,\n aspectRatio = \"4/3\",\n className,\n classNames,\n}: ProductCardProps): React.ReactElement {\n const mode = displayMode ?? product.display_mode ?? \"card\";\n const [isOpen, setIsOpen] = useState(false);\n const [shouldFetch, setShouldFetch] = useState(false);\n\n // Prefetch on pointer enter, always fetch when open\n const { product: productDetails } = useProduct(\n product.slug ?? product.id,\n { enabled: shouldFetch || isOpen },\n );\n\n const handlePrefetch = useCallback(() => {\n setShouldFetch(true);\n }, []);\n\n const handleOpenChange = useCallback((open: boolean) => {\n setIsOpen(open);\n if (open) {\n setShouldFetch(true);\n }\n }, []);\n\n const handleClose = useCallback(() => {\n setIsOpen(false);\n }, []);\n\n const imageUrl = product.image_url || product.images?.[0];\n\n const cardBody = children ?? (\n <>\n {/* Image */}\n {imageUrl && (\n <div\n data-cimplify-product-card-image-container\n className={cn(\"overflow-hidden rounded-t-xl\", classNames?.imageContainer)}\n style={ASPECT_STYLES[aspectRatio]}\n >\n {renderImage ? (\n renderImage({\n src: imageUrl,\n alt: product.name,\n className: classNames?.image,\n })\n ) : (\n <img\n src={imageUrl}\n alt={product.name}\n className={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\n {/* Body */}\n <div\n data-cimplify-product-card-body\n className={cn(\"p-4 flex flex-col gap-1\", classNames?.body)}\n >\n <span\n data-cimplify-product-card-name\n className={cn(\"font-semibold text-foreground leading-tight\", classNames?.name)}\n >\n {product.name}\n </span>\n {product.description && (\n <span\n data-cimplify-product-card-description\n className={cn(\"text-sm text-muted-foreground line-clamp-2\", classNames?.description)}\n >\n {product.description}\n </span>\n )}\n <Price\n amount={product.default_price}\n className={cn(\"text-sm font-medium text-foreground mt-1\", classNames?.price)}\n />\n </div>\n </>\n );\n\n // Page mode — render as a link\n if (mode === \"page\") {\n const linkHref = href ?? `/menu/${product.slug}`;\n const linkClassName = cn(\"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
  }
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  {
12
12
  "path": "service-card.tsx",
13
- "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Service } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\nimport { formatDuration } from \"./utils/format-duration\";\n\nexport interface ServiceCardClassNames {\n root?: string;\n imageContainer?: string;\n image?: string;\n body?: string;\n name?: string;\n description?: string;\n meta?: string;\n duration?: string;\n price?: string;\n action?: string;\n}\n\nexport interface ServiceCardProps {\n /** The service to display. */\n service: Service;\n /** Called when the card or book button is clicked. */\n onBook?: (service: Service) => void;\n /** Link href for page mode. Renders as `<a>` instead of `<button>`. */\n href?: string;\n /** Label for the action button. Default: \"Book\". */\n actionLabel?: string;\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 /** 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?: ServiceCardClassNames;\n}\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\nexport function ServiceCard({\n service,\n onBook,\n href,\n actionLabel = \"Book\",\n renderImage,\n children,\n aspectRatio = \"4/3\",\n className,\n classNames,\n}: ServiceCardProps): React.ReactElement {\n const imageUrl = service.image_url;\n\n const cardBody = children ?? (\n <>\n {imageUrl && (\n <div\n data-cimplify-service-card-image-container\n className={classNames?.imageContainer}\n style={{\n overflow: \"hidden\",\n ...ASPECT_STYLES[aspectRatio],\n }}\n >\n {renderImage ? (\n renderImage({\n src: imageUrl,\n alt: service.name,\n className: classNames?.image,\n })\n ) : (\n <img\n src={imageUrl}\n alt={service.name}\n className={classNames?.image}\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n data-cimplify-service-card-image\n />\n )}\n </div>\n )}\n\n <div data-cimplify-service-card-body className={classNames?.body}>\n <span data-cimplify-service-card-name className={classNames?.name}>\n {service.name}\n </span>\n {service.description && (\n <span\n data-cimplify-service-card-description\n className={classNames?.description}\n style={{\n display: \"-webkit-box\",\n WebkitLineClamp: 2,\n WebkitBoxOrient: \"vertical\",\n overflow: \"hidden\",\n }}\n >\n {service.description}\n </span>\n )}\n <div data-cimplify-service-card-meta className={classNames?.meta}>\n <span data-cimplify-service-card-duration className={classNames?.duration}>\n {formatDuration(service.duration_minutes, service.duration_unit)}\n </span>\n {service.price && (\n <span data-cimplify-service-card-price className={classNames?.price}>\n <Price amount={service.price} />\n </span>\n )}\n </div>\n </div>\n\n {!href && onBook && (\n <span data-cimplify-service-card-action className={classNames?.action}>\n {actionLabel}\n </span>\n )}\n </>\n );\n\n if (href) {\n return (\n <a\n href={href}\n data-cimplify-service-card\n data-available={service.is_available || undefined}\n className={cn(className, classNames?.root)}\n style={{ display: \"block\", textDecoration: \"none\", color: \"inherit\" }}\n >\n {cardBody}\n </a>\n );\n }\n\n return (\n <button\n type=\"button\"\n onClick={() => onBook?.(service)}\n disabled={!service.is_available}\n data-cimplify-service-card\n data-available={service.is_available || undefined}\n className={cn(className, classNames?.root)}\n style={{\n display: \"block\",\n width: \"100%\",\n textAlign: \"inherit\",\n background: \"none\",\n border: \"none\",\n padding: 0,\n cursor: service.is_available ? \"pointer\" : \"default\",\n font: \"inherit\",\n color: \"inherit\",\n }}\n >\n {cardBody}\n </button>\n );\n}\n"
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Service } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\nimport { formatDuration } from \"./utils/format-duration\";\n\nexport interface ServiceCardClassNames {\n root?: string;\n imageContainer?: string;\n image?: string;\n body?: string;\n name?: string;\n description?: string;\n meta?: string;\n duration?: string;\n price?: string;\n action?: string;\n}\n\nexport interface ServiceCardProps {\n /** The service to display. */\n service: Service;\n /** Called when the card or book button is clicked. */\n onBook?: (service: Service) => void;\n /** Link href for page mode. Renders as `<a>` instead of `<button>`. */\n href?: string;\n /** Label for the action button. Default: \"Book\". */\n actionLabel?: string;\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 /** 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?: ServiceCardClassNames;\n}\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\nexport function ServiceCard({\n service,\n onBook,\n href,\n actionLabel = \"Book\",\n renderImage,\n children,\n aspectRatio = \"4/3\",\n className,\n classNames,\n}: ServiceCardProps): React.ReactElement {\n const imageUrl = service.image_url;\n\n const cardBody = children ?? (\n <>\n {imageUrl && (\n <div\n data-cimplify-service-card-image-container\n className={cn(\"overflow-hidden rounded-t-xl\", classNames?.imageContainer)}\n style={ASPECT_STYLES[aspectRatio]}\n >\n {renderImage ? (\n renderImage({\n src: imageUrl,\n alt: service.name,\n className: classNames?.image,\n })\n ) : (\n <img\n src={imageUrl}\n alt={service.name}\n className={cn(\"w-full h-full object-cover transition-transform duration-300 group-hover:scale-105\", classNames?.image)}\n data-cimplify-service-card-image\n />\n )}\n </div>\n )}\n\n <div data-cimplify-service-card-body className={cn(\"p-4 flex flex-col gap-1.5\", classNames?.body)}>\n <span data-cimplify-service-card-name className={cn(\"font-semibold text-foreground leading-tight\", classNames?.name)}>\n {service.name}\n </span>\n {service.description && (\n <span\n data-cimplify-service-card-description\n className={cn(\"text-sm text-muted-foreground line-clamp-2\", classNames?.description)}\n >\n {service.description}\n </span>\n )}\n <div data-cimplify-service-card-meta className={cn(\"flex items-center gap-2 mt-1 text-sm\", classNames?.meta)}>\n <span data-cimplify-service-card-duration className={cn(\"text-muted-foreground\", classNames?.duration)}>\n {formatDuration(service.duration_minutes, service.duration_unit)}\n </span>\n {service.price && (\n <span data-cimplify-service-card-price className={cn(\"font-medium text-foreground\", classNames?.price)}>\n <Price amount={service.price} />\n </span>\n )}\n </div>\n </div>\n\n {!href && onBook && (\n <span data-cimplify-service-card-action className={cn(\"px-4 pb-4 pt-1 text-sm font-medium text-primary\", classNames?.action)}>\n {actionLabel}\n </span>\n )}\n </>\n );\n\n const cardClasses = cn(\n \"group block w-full rounded-xl border border-border bg-background text-left transition-shadow duration-200 hover:shadow-md\",\n !service.is_available && \"opacity-60\",\n className,\n classNames?.root,\n );\n\n if (href) {\n return (\n <a\n href={href}\n data-cimplify-service-card\n data-available={service.is_available || undefined}\n className={cn(cardClasses, \"no-underline text-[inherit]\")}\n >\n {cardBody}\n </a>\n );\n }\n\n return (\n <button\n type=\"button\"\n onClick={() => onBook?.(service)}\n disabled={!service.is_available}\n data-cimplify-service-card\n data-available={service.is_available || undefined}\n className={cn(cardClasses, \"cursor-pointer font-[inherit] p-0 border-border\")}\n >\n {cardBody}\n </button>\n );\n}\n"
14
14
  }
15
15
  ]
16
16
  }