@cimplify/sdk 0.24.2 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advanced.d.mts +2 -2
- package/dist/advanced.d.ts +2 -2
- package/dist/{chunk-H34GSEGR.mjs → chunk-JMNMSVDQ.mjs} +24 -1
- package/dist/{chunk-IYQGGFVQ.js → chunk-WMQXSDRC.js} +26 -0
- package/dist/{index-DCoxHrmr.d.mts → index-2JxW7TuQ.d.mts} +2 -2
- package/dist/{index-DIHoZUz_.d.mts → index-7mX_cUpQ.d.mts} +1 -1
- package/dist/{index-DT0Mzy8v.d.ts → index-DBM89ktY.d.ts} +2 -2
- package/dist/{index-CJZMsbDi.d.ts → index-DEx3GwlW.d.ts} +1 -1
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +18 -6
- package/dist/index.mjs +1 -1
- package/dist/{payment-DqiwuLma.d.mts → payment-5a21yzi7.d.mts} +22 -1
- package/dist/{payment-DqiwuLma.d.ts → payment-5a21yzi7.d.ts} +22 -1
- package/dist/{price-BZlddg1A.d.ts → price-BS0Z5Oi9.d.ts} +1 -1
- package/dist/{price-DVQrpE7j.d.mts → price-CGPMlQUk.d.mts} +1 -1
- package/dist/react.d.mts +92 -5
- package/dist/react.d.ts +92 -5
- package/dist/react.js +880 -91
- package/dist/react.mjs +861 -89
- package/dist/styles.css +1 -1
- package/dist/utils.d.mts +3 -3
- package/dist/utils.d.ts +3 -3
- package/package.json +1 -1
- package/registry/accommodation-card.json +17 -0
- package/registry/compact-service-card.json +16 -0
- package/registry/digital-product-card.json +16 -0
- package/registry/food-product-card.json +16 -0
- package/registry/food-product-layout.json +1 -1
- package/registry/index.json +113 -0
- package/registry/lease-service-card.json +17 -0
- package/registry/product-card.json +1 -1
- package/registry/product-page.json +1 -1
- package/registry/rental-service-card.json +16 -0
- package/registry/retail-product-card.json +16 -0
- package/registry/schedule-service-card.json +16 -0
- package/registry/standard-service-card.json +16 -0
- package/registry/subscription-card.json +16 -0
- package/registry/wholesale-product-card.json +17 -0
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-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}.aspect-square{aspect-ratio:1}.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}.list-none{list-style-type:none}.\[appearance\:textfield\]{appearance:textfield}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.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-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-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\/5{background-color:#934c3a0d}@supports (color:color-mix(in lab, red, red)){.bg-primary\/5{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 5%, transparent)}}.bg-primary\/10{background-color:#934c3a1a}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 10%, transparent)}}.bg-transparent{background-color:#0000}.object-cover{object-fit:cover}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-\[inherit\]{font-family:inherit}.text-\[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))}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.accent-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}.select-none{-webkit-user-select:none;user-select:none}.\[cimplify\:checkout\]{cimplify:checkout}@media (hover:hover){.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}}.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-details-marker\]\:hidden::-webkit-details-marker{display:none}.\[\&\:\:-webkit-inner-spin-button\]\:appearance-none::-webkit-inner-spin-button{appearance:none}.\[\&\:\:-webkit-outer-spin-button\]\:appearance-none::-webkit-outer-spin-button{appearance:none}[open]>.\[\[open\]\>\&\]\:rotate-180{rotate:180deg}@property --tw-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}
|
|
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-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.z-10{z-index:10}.z-50{z-index:50}.container{width:100%}.-mt-px{margin-top:-1px}.ml-auto{margin-left:auto}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-\[3\/4\]{aspect-ratio:3/4}.aspect-\[4\/3\]{aspect-ratio:4/3}.aspect-\[5\/2\]{aspect-ratio:5/2}.aspect-\[16\/9\]{aspect-ratio:16/9}.aspect-square{aspect-ratio:1}.h-\[6px\]{height:6px}.h-\[7px\]{height:7px}.h-\[72px\]{height:72px}.h-fit{height:fit-content}.h-full{height:100%}.max-h-\[85vh\]{max-height:85vh}.w-2\/5{width:40%}.w-3\/5{width:60%}.w-4\/5{width:80%}.w-\[6px\]{width:6px}.w-\[7px\]{width:7px}.w-\[72px\]{width:72px}.w-full{width:100%}.flex-1{flex:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.list-none{list-style-type:none}.\[appearance\:textfield\]{appearance:textfield}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-border>:not(:last-child)){border-color:var(--color-border,oklch(90% 0 0))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.rounded{border-radius:var(--radius,.5rem)}.rounded-\[14px\]{border-radius:14px}.rounded-\[inherit\]{border-radius:inherit}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-none{--tw-border-style:none;border-style:none}.border-border{border-color:var(--color-border,oklch(90% 0 0))}.border-input{border-color:var(--color-input,oklch(90% 0 0))}.border-muted-foreground\/30{border-color:#6363634d}@supports (color:color-mix(in lab, red, red)){.border-muted-foreground\/30{border-color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 30%, transparent)}}.border-primary{border-color:var(--color-primary,oklch(50% .1 35))}.border-transparent{border-color:#0000}.bg-background{background-color:var(--color-background,oklch(99% 0 0))}.bg-background\/50{background-color:#fcfcfc80}@supports (color:color-mix(in lab, red, red)){.bg-background\/50{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 50%, transparent)}}.bg-background\/90{background-color:#fcfcfce6}@supports (color:color-mix(in lab, red, red)){.bg-background\/90{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 90%, transparent)}}.bg-background\/92{background-color:#fcfcfceb}@supports (color:color-mix(in lab, red, red)){.bg-background\/92{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 92%, transparent)}}.bg-destructive{background-color:var(--color-destructive,oklch(50% .2 25))}.bg-destructive\/10{background-color:#bb061e1a}@supports (color:color-mix(in lab, red, red)){.bg-destructive\/10{background-color:color-mix(in oklab, var(--color-destructive,oklch(50% .2 25)) 10%, transparent)}}.bg-muted{background-color:var(--color-muted,oklch(95% 0 0))}.bg-primary{background-color:var(--color-primary,oklch(50% .1 35))}.bg-primary\/5{background-color:#934c3a0d}@supports (color:color-mix(in lab, red, red)){.bg-primary\/5{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 5%, transparent)}}.bg-primary\/10{background-color:#934c3a1a}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 10%, transparent)}}.bg-transparent{background-color:#0000}.bg-gradient-to-t{--tw-gradient-position:to top in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.object-cover{object-fit:cover}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-\[inherit\]{font-family:inherit}.text-\[9px\]{font-size:9px}.text-\[10\.5px\]{font-size:10.5px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[14\.5px\]{font-size:14.5px}.text-\[14px\]{font-size:14px}.text-\[inherit\]{color:inherit}.text-border{color:var(--color-border,oklch(90% 0 0))}.text-destructive{color:var(--color-destructive,oklch(50% .2 25))}.text-destructive-foreground{color:var(--color-destructive-foreground,oklch(99% 0 0))}.text-foreground{color:var(--color-foreground,oklch(15% 0 0))}.text-muted-foreground{color:var(--color-muted-foreground,oklch(50% 0 0))}.text-muted-foreground\/60{color:#63636399}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/60{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 60%, transparent)}}.text-primary{color:var(--color-primary,oklch(50% .1 35))}.text-primary-foreground{color:var(--color-primary-foreground,oklch(99% 0 0))}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.accent-primary{accent-color:var(--color-primary,oklch(50% .1 35))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-\[0_1px_3px_rgba\(0\,0\,0\,0\.04\)\,0_6px_24px_rgba\(0\,0\,0\,0\.06\)\]{--tw-shadow:0 1px 3px var(--tw-shadow-color,#0000000a), 0 6px 24px var(--tw-shadow-color,#0000000f);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_1px_4px_rgba\(0\,0\,0\,0\.10\)\,0_4px_12px_rgba\(0\,0\,0\,0\.08\)\]{--tw-shadow:0 1px 4px var(--tw-shadow-color,#0000001a), 0 4px 12px var(--tw-shadow-color,#00000014);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-\[1px\]{--tw-backdrop-blur:blur(1px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-700{--tw-duration:.7s;transition-duration:.7s}.ease-\[cubic-bezier\(0\.19\,1\,0\.22\,1\)\]{--tw-ease:cubic-bezier(.19,1,.22,1);transition-timing-function:cubic-bezier(.19,1,.22,1)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.\[cimplify\:checkout\]{cimplify:checkout}@media (hover:hover){.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:scale-\[1\.04\]:is(:where(.group):hover *){scale:1.04}.group-hover\:text-primary:is(:where(.group):hover *){color:var(--color-primary,oklch(50% .1 35))}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.placeholder\:text-muted-foreground::placeholder{color:var(--color-muted-foreground,oklch(50% 0 0))}@media (hover:hover){.hover\:-translate-y-\[1px\]:hover{--tw-translate-y:calc(1px * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:\!scale-110:hover{--tw-scale-x:110%!important;--tw-scale-y:110%!important;--tw-scale-z:110%!important;scale:var(--tw-scale-x) var(--tw-scale-y)!important}.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:scale-\[1\.04\]:hover{scale:1.04}.hover\:border-primary\/20:hover{border-color:#934c3a33}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/20:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 20%, transparent)}}.hover\:border-primary\/50:hover{border-color:#934c3a80}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/50:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 50%, transparent)}}.hover\:\!bg-foreground:hover{background-color:var(--color-foreground,oklch(15% 0 0))!important}.hover\:bg-background:hover{background-color:var(--color-background,oklch(99% 0 0))}.hover\:bg-destructive\/10:hover{background-color:#bb061e1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-destructive\/10:hover{background-color:color-mix(in oklab, var(--color-destructive,oklch(50% .2 25)) 10%, transparent)}}.hover\:bg-muted:hover{background-color:var(--color-muted,oklch(95% 0 0))}.hover\:bg-muted\/80:hover{background-color:#eeec}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/80:hover{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 80%, transparent)}}.hover\:bg-primary\/90:hover{background-color:#934c3ae6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 90%, transparent)}}.hover\:\!text-background:hover{color:var(--color-background,oklch(99% 0 0))!important}.hover\:text-destructive:hover{color:var(--color-destructive,oklch(50% .2 25))}.hover\:text-primary:hover{color:var(--color-primary,oklch(50% .1 35))}.hover\:text-primary\/80:hover{color:#934c3acc}@supports (color:color-mix(in lab, red, red)){.hover\:text-primary\/80:hover{color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 80%, transparent)}}.hover\:shadow-\[0_2px_6px_rgba\(0\,0\,0\,0\.04\)\,0_12px_40px_rgba\(0\,0\,0\,0\.10\)\]:hover{--tw-shadow:0 2px 6px var(--tw-shadow-color,#0000000a), 0 12px 40px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\: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-details-marker\]\:hidden::-webkit-details-marker{display:none}.\[\&\:\:-webkit-inner-spin-button\]\:appearance-none::-webkit-inner-spin-button{appearance:none}.\[\&\:\:-webkit-outer-spin-button\]\:appearance-none::-webkit-outer-spin-button{appearance:none}[open]>.\[\[open\]\>\&\]\:rotate-180{rotate:180deg}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}
|
package/dist/utils.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, v as categorizePaymentError, B as detectMobileMoneyProvider, c as formatMoney, d as formatNumberCompact, f as formatPrice, a as formatPriceAdjustment, b as formatPriceCompact, j as formatPriceWithTax, e as formatProductPrice, m as getBasePrice, k as getCurrencySymbol, o as getDiscountPercentage, l as getDisplayPrice, q as getMarkupPercentage, r as getProductCurrency, g as getTaxAmount, h as hasTaxInfo, n as isOnSale, y as isPaymentStatusFailure, z as isPaymentStatusRequiresAction, A as isPaymentStatusSuccess, i as isTaxInclusive, F as isValidPhoneForContext, w as normalizePaymentResponse, D as normalizePhoneToE164, x as normalizeStatusResponse, p as parsePrice, E as resolvePhoneCountryCode } from './index-
|
|
2
|
-
import './payment-
|
|
3
|
-
import './price-
|
|
1
|
+
export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, v as categorizePaymentError, B as detectMobileMoneyProvider, c as formatMoney, d as formatNumberCompact, f as formatPrice, a as formatPriceAdjustment, b as formatPriceCompact, j as formatPriceWithTax, e as formatProductPrice, m as getBasePrice, k as getCurrencySymbol, o as getDiscountPercentage, l as getDisplayPrice, q as getMarkupPercentage, r as getProductCurrency, g as getTaxAmount, h as hasTaxInfo, n as isOnSale, y as isPaymentStatusFailure, z as isPaymentStatusRequiresAction, A as isPaymentStatusSuccess, i as isTaxInclusive, F as isValidPhoneForContext, w as normalizePaymentResponse, D as normalizePhoneToE164, x as normalizeStatusResponse, p as parsePrice, E as resolvePhoneCountryCode } from './index-2JxW7TuQ.mjs';
|
|
2
|
+
import './payment-5a21yzi7.mjs';
|
|
3
|
+
import './price-CGPMlQUk.mjs';
|
|
4
4
|
import 'libphonenumber-js';
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, v as categorizePaymentError, B as detectMobileMoneyProvider, c as formatMoney, d as formatNumberCompact, f as formatPrice, a as formatPriceAdjustment, b as formatPriceCompact, j as formatPriceWithTax, e as formatProductPrice, m as getBasePrice, k as getCurrencySymbol, o as getDiscountPercentage, l as getDisplayPrice, q as getMarkupPercentage, r as getProductCurrency, g as getTaxAmount, h as hasTaxInfo, n as isOnSale, y as isPaymentStatusFailure, z as isPaymentStatusRequiresAction, A as isPaymentStatusSuccess, i as isTaxInclusive, F as isValidPhoneForContext, w as normalizePaymentResponse, D as normalizePhoneToE164, x as normalizeStatusResponse, p as parsePrice, E as resolvePhoneCountryCode } from './index-
|
|
2
|
-
import './payment-
|
|
3
|
-
import './price-
|
|
1
|
+
export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, v as categorizePaymentError, B as detectMobileMoneyProvider, c as formatMoney, d as formatNumberCompact, f as formatPrice, a as formatPriceAdjustment, b as formatPriceCompact, j as formatPriceWithTax, e as formatProductPrice, m as getBasePrice, k as getCurrencySymbol, o as getDiscountPercentage, l as getDisplayPrice, q as getMarkupPercentage, r as getProductCurrency, g as getTaxAmount, h as hasTaxInfo, n as isOnSale, y as isPaymentStatusFailure, z as isPaymentStatusRequiresAction, A as isPaymentStatusSuccess, i as isTaxInclusive, F as isValidPhoneForContext, w as normalizePaymentResponse, D as normalizePhoneToE164, x as normalizeStatusResponse, p as parsePrice, E as resolvePhoneCountryCode } from './index-DBM89ktY.js';
|
|
2
|
+
import './payment-5a21yzi7.js';
|
|
3
|
+
import './price-BS0Z5Oi9.js';
|
|
4
4
|
import 'libphonenumber-js';
|
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "accommodation-card",
|
|
3
|
+
"title": "AccommodationCard",
|
|
4
|
+
"description": "Hotel/accommodation card with per-night pricing, amenities, capacity, and cancellation.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"price",
|
|
8
|
+
"volume-pricing",
|
|
9
|
+
"cn"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "cards/accommodation-card.tsx",
|
|
14
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ServiceCardLayoutProps } from \"./standard-service-card\";\nimport { CardShell, CardImage } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { VolumePricing } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport function AccommodationCard({\n product,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const hasTiers = product.quantity_pricing && product.quantity_pricing.length > 1;\n const cancellationHours = product.cancellation_window_minutes\n ? Math.floor(product.cancellation_window_minutes / 60)\n : null;\n const tags = product.tags || [];\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"16/9\" renderImage={renderImage}>\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none\" />\n\n <span className=\"absolute bottom-3 left-3 inline-flex items-center text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm\">\n Per night\n </span>\n\n <span className=\"absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm\">\n <Price amount={product.default_price} />/night\n </span>\n </CardImage>\n\n <div className=\"p-4\">\n <h3 className=\"text-[14.5px] font-bold text-foreground leading-snug tracking-tight\">\n {product.name}\n </h3>\n {product.description && (\n <p className=\"text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2\">\n {product.description}\n </p>\n )}\n\n {/* Amenity / tag pills */}\n {tags.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mt-2.5\">\n {tags.slice(0, 4).map((tag) => (\n <span key={tag} className=\"text-[10px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded\">\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {/* Guest capacity */}\n {product.general_service_capacity != null && (\n <div className=\"flex items-center gap-1.5 mt-2 text-[11px] text-muted-foreground\">\n <svg className=\"w-3.5 h-3.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0\" />\n </svg>\n {product.general_service_capacity} guest{product.general_service_capacity > 1 ? \"s\" : \"\"}\n </div>\n )}\n\n {/* Extended stay pricing */}\n {hasTiers && (\n <div className=\"mt-3\">\n <VolumePricing tiers={product.quantity_pricing!} />\n </div>\n )}\n\n <div className=\"flex items-center justify-between mt-3 pt-3 border-t border-border\">\n <div className=\"flex items-center gap-2 text-[11px] text-muted-foreground\">\n {cancellationHours != null && (\n <span className=\"text-emerald-600 font-medium\">\n Free cancellation {cancellationHours}h\n </span>\n )}\n {product.min_order_quantity != null && product.min_order_quantity > 1 && (\n <span>Min. {product.min_order_quantity} nights</span>\n )}\n </div>\n <button\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className=\"text-[13px] font-semibold text-primary hover:text-primary/80 transition-colors\"\n >\n Book room →\n </button>\n </div>\n </div>\n </CardShell>\n );\n}\n"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "compact-service-card",
|
|
3
|
+
"title": "CompactServiceCard",
|
|
4
|
+
"description": "Horizontal service card with thumbnail for list views.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"price",
|
|
8
|
+
"cn"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "cards/compact-service-card.tsx",
|
|
13
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ServiceCardLayoutProps } from \"./standard-service-card\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nfunction formatDuration(minutes: number, unit?: string): string {\n if (unit && unit !== \"minutes\") return `${minutes} ${unit}`;\n if (minutes >= 60) {\n const h = Math.floor(minutes / 60);\n const m = minutes % 60;\n return m > 0 ? `${h}h ${m}m` : `${h} hr`;\n }\n return `${minutes} min`;\n}\n\nexport function CompactServiceCard({\n product,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const hasDeposit = product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount;\n const hasBillingPlans = product.billing_plans && product.billing_plans.length > 0;\n const href = `/products/${product.slug}`;\n\n const content = (\n <div className=\"flex items-center gap-4 p-3\">\n {/* Thumbnail */}\n <div className=\"w-[72px] h-[72px] rounded-xl overflow-hidden bg-muted shrink-0\">\n {renderImage ? (\n renderImage({ src: image, alt: product.name, className: \"w-full h-full object-cover\" })\n ) : (\n <img src={image} alt={product.name} className=\"w-full h-full object-cover\" />\n )}\n </div>\n\n {/* Info */}\n <div className=\"flex-1 min-w-0\">\n <h3 className=\"text-[14px] font-semibold text-foreground leading-tight truncate\">\n {product.name}\n </h3>\n {product.description && (\n <p className=\"text-[12px] text-muted-foreground mt-0.5 truncate\">\n {product.description}\n </p>\n )}\n <div className=\"flex items-center gap-2 mt-2 flex-wrap\">\n {product.duration_minutes != null && (\n <span className=\"text-[10.5px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded\">\n {formatDuration(product.duration_minutes, product.duration_unit)}\n </span>\n )}\n <span className=\"flex items-center gap-1 text-[10.5px] font-medium text-emerald-600\">\n <span className=\"w-[6px] h-[6px] rounded-full bg-emerald-500\" />\n Available\n </span>\n {hasDeposit && (\n <span className=\"text-[10.5px] font-medium text-amber-600\">\n <Price amount={product.deposit_amount!} /> deposit\n </span>\n )}\n {hasBillingPlans && (\n <span className=\"inline-flex items-center gap-0.5 text-[10.5px] font-semibold text-primary px-1.5 py-0.5 bg-primary/10 rounded\">\n Subscription\n </span>\n )}\n </div>\n </div>\n\n {/* Price + Chevron */}\n <div className=\"flex items-center gap-2 shrink-0\">\n <Price amount={product.default_price} className=\"text-sm font-bold\" />\n <svg className=\"w-4 h-4 text-muted-foreground group-hover:text-primary transition-colors\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </div>\n </div>\n );\n\n const shellClass = cn(\n \"group block text-left cursor-pointer bg-card rounded-[14px] overflow-hidden\",\n \"shadow-[0_1px_3px_rgba(0,0,0,0.04),0_6px_24px_rgba(0,0,0,0.06)]\",\n \"border border-transparent\",\n \"transition-all duration-300 ease-[cubic-bezier(0.19,1,0.22,1)]\",\n \"hover:-translate-y-[1px] hover:shadow-[0_2px_6px_rgba(0,0,0,0.04),0_12px_40px_rgba(0,0,0,0.10)] hover:border-primary/20\",\n className,\n );\n\n if (renderLink) {\n return renderLink({ href, className: shellClass, children: content });\n }\n\n return <a href={href} className={shellClass} data-cimplify-card>{content}</a>;\n}\n"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "digital-product-card",
|
|
3
|
+
"title": "DigitalProductCard",
|
|
4
|
+
"description": "Digital product card with type badge, file info, and event details.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"price",
|
|
8
|
+
"cn"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "cards/digital-product-card.tsx",
|
|
13
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { CardShell, CardImage } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\n\nconst TYPE_BADGES: Record<string, { label: string; color: string }> = {\n download: { label: \"Download\", color: \"bg-sky-50 text-sky-700 border-sky-200/60\" },\n license: { label: \"License\", color: \"bg-blue-50 text-blue-700 border-blue-200/60\" },\n event_ticket: { label: \"Event\", color: \"bg-violet-50 text-violet-700 border-violet-200/60\" },\n access_pass: { label: \"Access\", color: \"bg-emerald-50 text-emerald-700 border-emerald-200/60\" },\n gift_card: { label: \"Gift Card\", color: \"bg-amber-50 text-amber-700 border-amber-200/60\" },\n};\n\nexport function DigitalProductCard({\n product,\n renderImage,\n renderLink,\n className,\n}: CardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const typeBadge = product.digital_type ? TYPE_BADGES[product.digital_type] : null;\n const isTicket = product.digital_type === \"event_ticket\";\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"4/3\" renderImage={renderImage}>\n {typeBadge && (\n <span className={`absolute top-3 left-3 text-[11px] font-semibold tracking-wide border px-2 py-0.5 rounded-md ${typeBadge.color}`}>\n {typeBadge.label}\n </span>\n )}\n </CardImage>\n\n <div className=\"p-3.5\">\n <h3 className=\"text-[14.5px] font-semibold text-foreground leading-snug tracking-tight truncate\">\n {product.name}\n </h3>\n\n {/* Event: date + venue */}\n {isTicket && (product.event_date || product.venue) && (\n <p className=\"text-[12px] text-muted-foreground mt-1 truncate\">\n {product.event_date && new Date(product.event_date).toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" })}\n {product.event_date && product.venue && \" · \"}\n {product.venue}\n </p>\n )}\n\n {/* Non-event: description */}\n {!isTicket && product.description && (\n <p className=\"text-[12px] text-muted-foreground mt-1 line-clamp-1\">\n {product.description}\n </p>\n )}\n\n <div className=\"flex items-center justify-between mt-2.5\">\n <Price amount={product.default_price} className=\"text-sm font-bold\" />\n\n {/* File info pill */}\n {product.file_type && (\n <span className=\"text-[10px] text-muted-foreground bg-muted px-1.5 py-0.5 rounded\">\n {product.file_type.toUpperCase()}\n {product.file_size_mb != null && ` · ${product.file_size_mb >= 1024 ? `${(product.file_size_mb / 1024).toFixed(1)}GB` : `${product.file_size_mb}MB`}`}\n </span>\n )}\n </div>\n </div>\n </CardShell>\n );\n}\n"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "food-product-card",
|
|
3
|
+
"title": "FoodProductCard",
|
|
4
|
+
"description": "Product card for food items with tags, badges, and quick-add.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"price",
|
|
8
|
+
"cn"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "cards/food-product-card.tsx",
|
|
13
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport { PRODUCT_TYPE } from \"@cimplify/sdk\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { CardShell, CardImage, QuickAddButton } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\n\nexport function FoodProductCard({\n product,\n onQuickAdd,\n renderImage,\n renderLink,\n className,\n}: CardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const tags = product.tags || [];\n const isSignature = product.metadata?.is_signature === true;\n const isNew = product.metadata?.is_new === true;\n const isComposite = product.type === PRODUCT_TYPE.Composite;\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"square\" renderImage={renderImage}>\n {/* Badges */}\n {isSignature && (\n <span className=\"absolute top-3 left-3 inline-flex items-center gap-1 text-[11px] font-semibold tracking-wide bg-amber-50 text-amber-700 border border-amber-200/60 px-2 py-0.5 rounded-md\">\n <svg className=\"w-3 h-3 opacity-70\" fill=\"currentColor\" viewBox=\"0 0 16 16\"><path d=\"M8 .75l1.85 4.95L15 6.2l-3.7 3.2 1.1 5.1L8 12.15 3.6 14.5l1.1-5.1L1 6.2l5.15-.5z\" /></svg>\n Signature\n </span>\n )}\n {isNew && !isSignature && (\n <span className=\"absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-emerald-50 text-emerald-700 border border-emerald-200/60 px-2 py-0.5 rounded-md\">\n New\n </span>\n )}\n {isComposite && (\n <span className=\"absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-violet-50 text-violet-700 border border-violet-200/60 px-2 py-0.5 rounded-md\">\n Customizable\n </span>\n )}\n\n {/* Quick add */}\n <QuickAddButton\n onClick={() => onQuickAdd?.(product)}\n className=\"absolute bottom-3 right-3\"\n />\n </CardImage>\n\n <div className=\"p-3.5\">\n <h3 className=\"text-[14.5px] font-semibold text-foreground leading-snug tracking-tight truncate\">\n {product.name}\n </h3>\n {product.description && (\n <p className=\"text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2\">\n {product.description}\n </p>\n )}\n <div className=\"flex items-center justify-between mt-3\">\n {isComposite ? (\n <span className=\"text-sm font-bold\">From <Price amount={product.default_price} /></span>\n ) : (\n <Price amount={product.default_price} className=\"text-sm font-bold\" />\n )}\n {tags.length > 0 && (\n <div className=\"flex gap-1\">\n {tags.slice(0, 2).map((tag) => (\n <span key={tag} className=\"text-[10px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded\">\n {tag}\n </span>\n ))}\n </div>\n )}\n </div>\n </div>\n </CardShell>\n );\n}\n"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
{
|
|
14
14
|
"path": "layouts/food-product-layout.tsx",
|
|
15
|
-
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n MetadataStringList,\n CustomAttributesTable,\n TagPills,\n getMetadataStringList,\n} from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\
|
|
15
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport {\n TwoColumnGrid,\n RelatedProductsSection,\n MetadataStringList,\n CustomAttributesTable,\n TagPills,\n getMetadataStringList,\n} from \"@cimplify/sdk/react\";\nimport { ProductImageGallery } from \"@cimplify/sdk/react\";\nimport { ProductCustomizer } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\n\nexport function FoodProductLayout({\n product,\n onAddToCart,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n}: ProductLayoutProps): React.ReactElement {\n const images = product.images || (product.image_url ? [product.image_url] : []);\n const tags = product.tags || [];\n const ingredients = getMetadataStringList(product.metadata, \"ingredients\");\n const pairings = getMetadataStringList(product.metadata, \"pairings\");\n\n return (\n <div data-cimplify-product-layout=\"food\" className={cn(\"space-y-8\", className)}>\n {renderBreadcrumb?.(product)}\n\n <TwoColumnGrid\n left={\n <ProductImageGallery\n images={images}\n productName={product.name}\n />\n }\n right={\n <div className=\"space-y-6\">\n {/* Tags + Allergens */}\n <div className=\"flex flex-wrap gap-1.5\">\n {tags.map((tag) => (\n <span key={tag} className=\"inline-flex items-center gap-1 px-2.5 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full\">\n {tag}\n </span>\n ))}\n {product.allergies && product.allergies.map((allergy) => (\n <span key={allergy} className=\"inline-flex items-center gap-1 px-2.5 py-1 bg-amber-100 text-amber-800 text-xs font-medium rounded-full\">\n Contains {allergy}\n </span>\n ))}\n </div>\n\n {/* Name + Price */}\n <div>\n <h1 data-cimplify-product-layout-name className=\"text-3xl lg:text-4xl font-bold tracking-tight\">\n {product.name}\n </h1>\n <Price amount={product.default_price} className=\"text-2xl font-semibold mt-2 block\" />\n </div>\n\n {/* Quick stats */}\n {(product.calories != null || product.preparation_time_minutes != null) && (\n <div data-cimplify-product-layout-stats className=\"flex flex-wrap gap-4 py-3 border-y border-border\">\n {product.preparation_time_minutes != null && (\n <span className=\"flex items-center gap-1.5 text-sm text-muted-foreground\">\n <strong className=\"text-foreground\">{product.preparation_time_minutes} min</strong> prep time\n </span>\n )}\n {product.calories != null && (\n <span className=\"flex items-center gap-1.5 text-sm text-muted-foreground\">\n <strong className=\"text-foreground\">{product.calories}</strong> calories\n </span>\n )}\n </div>\n )}\n\n {/* Description */}\n {product.description && (\n <p data-cimplify-product-layout-description className=\"text-muted-foreground leading-relaxed\">\n {product.description}\n </p>\n )}\n\n {/* Ingredients + Pairings */}\n {(ingredients.length > 0 || pairings.length > 0) && (\n <div className=\"grid sm:grid-cols-2 gap-4\">\n <MetadataStringList items={ingredients} label=\"Key Ingredients\" />\n <MetadataStringList items={pairings} label=\"Pairings\" />\n </div>\n )}\n\n {/* Custom Attributes */}\n <CustomAttributesTable attributes={product.custom_attributes || []} />\n\n {/* Customizer (variants, add-ons, composites, bundles, billing, volume) */}\n <ProductCustomizer\n product={product}\n onAddToCart={onAddToCart}\n />\n </div>\n }\n />\n\n {showRelated && relatedProducts && relatedProducts.length > 0 && (\n <RelatedProductsSection\n products={relatedProducts}\n title=\"You may also enjoy\"\n />\n )}\n </div>\n );\n}\n"
|
|
16
16
|
}
|
|
17
17
|
]
|
|
18
18
|
}
|
package/registry/index.json
CHANGED
|
@@ -408,6 +408,119 @@
|
|
|
408
408
|
"cn"
|
|
409
409
|
]
|
|
410
410
|
},
|
|
411
|
+
{
|
|
412
|
+
"name": "food-product-card",
|
|
413
|
+
"title": "FoodProductCard",
|
|
414
|
+
"description": "Product card for food items with tags, badges, and quick-add.",
|
|
415
|
+
"type": "component",
|
|
416
|
+
"registryDependencies": [
|
|
417
|
+
"price",
|
|
418
|
+
"cn"
|
|
419
|
+
]
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
"name": "retail-product-card",
|
|
423
|
+
"title": "RetailProductCard",
|
|
424
|
+
"description": "Product card for retail with color swatches, sale badge, wishlist, and sold-out overlay.",
|
|
425
|
+
"type": "component",
|
|
426
|
+
"registryDependencies": [
|
|
427
|
+
"price",
|
|
428
|
+
"cn"
|
|
429
|
+
]
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
"name": "wholesale-product-card",
|
|
433
|
+
"title": "WholesaleProductCard",
|
|
434
|
+
"description": "B2B product card with price range, MOQ badge, and stock count.",
|
|
435
|
+
"type": "component",
|
|
436
|
+
"registryDependencies": [
|
|
437
|
+
"price",
|
|
438
|
+
"price-range",
|
|
439
|
+
"cn"
|
|
440
|
+
]
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
"name": "digital-product-card",
|
|
444
|
+
"title": "DigitalProductCard",
|
|
445
|
+
"description": "Digital product card with type badge, file info, and event details.",
|
|
446
|
+
"type": "component",
|
|
447
|
+
"registryDependencies": [
|
|
448
|
+
"price",
|
|
449
|
+
"cn"
|
|
450
|
+
]
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
"name": "standard-service-card",
|
|
454
|
+
"title": "StandardServiceCard",
|
|
455
|
+
"description": "Service card with hero image, duration, deposit, and availability.",
|
|
456
|
+
"type": "component",
|
|
457
|
+
"registryDependencies": [
|
|
458
|
+
"price",
|
|
459
|
+
"cn"
|
|
460
|
+
]
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
"name": "compact-service-card",
|
|
464
|
+
"title": "CompactServiceCard",
|
|
465
|
+
"description": "Horizontal service card with thumbnail for list views.",
|
|
466
|
+
"type": "component",
|
|
467
|
+
"registryDependencies": [
|
|
468
|
+
"price",
|
|
469
|
+
"cn"
|
|
470
|
+
]
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
"name": "schedule-service-card",
|
|
474
|
+
"title": "ScheduleServiceCard",
|
|
475
|
+
"description": "Service card with next available time slot pills.",
|
|
476
|
+
"type": "component",
|
|
477
|
+
"registryDependencies": [
|
|
478
|
+
"price",
|
|
479
|
+
"cn"
|
|
480
|
+
]
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
"name": "rental-service-card",
|
|
484
|
+
"title": "RentalServiceCard",
|
|
485
|
+
"description": "Rental card with per-day/hour pricing, deposit, and availability count.",
|
|
486
|
+
"type": "component",
|
|
487
|
+
"registryDependencies": [
|
|
488
|
+
"price",
|
|
489
|
+
"cn"
|
|
490
|
+
]
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
"name": "accommodation-card",
|
|
494
|
+
"title": "AccommodationCard",
|
|
495
|
+
"description": "Hotel/accommodation card with per-night pricing, amenities, capacity, and cancellation.",
|
|
496
|
+
"type": "component",
|
|
497
|
+
"registryDependencies": [
|
|
498
|
+
"price",
|
|
499
|
+
"volume-pricing",
|
|
500
|
+
"cn"
|
|
501
|
+
]
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
"name": "lease-service-card",
|
|
505
|
+
"title": "LeaseServiceCard",
|
|
506
|
+
"description": "Long-term lease card with per-month/year pricing, volume tiers, and billing.",
|
|
507
|
+
"type": "component",
|
|
508
|
+
"registryDependencies": [
|
|
509
|
+
"price",
|
|
510
|
+
"volume-pricing",
|
|
511
|
+
"cn"
|
|
512
|
+
]
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
"name": "subscription-card",
|
|
516
|
+
"title": "SubscriptionCard",
|
|
517
|
+
"description": "Subscription card with billing plan options, trial badge, and setup fee.",
|
|
518
|
+
"type": "component",
|
|
519
|
+
"registryDependencies": [
|
|
520
|
+
"price",
|
|
521
|
+
"cn"
|
|
522
|
+
]
|
|
523
|
+
},
|
|
411
524
|
{
|
|
412
525
|
"name": "volume-pricing",
|
|
413
526
|
"title": "VolumePricing",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lease-service-card",
|
|
3
|
+
"title": "LeaseServiceCard",
|
|
4
|
+
"description": "Long-term lease card with per-month/year pricing, volume tiers, and billing.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"price",
|
|
8
|
+
"volume-pricing",
|
|
9
|
+
"cn"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "cards/lease-service-card.tsx",
|
|
14
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ServiceCardLayoutProps } from \"./standard-service-card\";\nimport { CardShell, CardImage } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { VolumePricing } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst UNIT_LABELS: Record<string, string> = {\n weeks: \"week\",\n months: \"mo\",\n years: \"yr\",\n};\n\nexport function LeaseServiceCard({\n product,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const hasDeposit = product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount;\n const hasTiers = product.quantity_pricing && product.quantity_pricing.length > 1;\n const unit = product.duration_unit ? UNIT_LABELS[product.duration_unit] || product.duration_unit : \"mo\";\n const tags = product.tags || [];\n const cancellationHours = product.cancellation_window_minutes\n ? Math.floor(product.cancellation_window_minutes / 60)\n : null;\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"16/9\" renderImage={renderImage}>\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none\" />\n\n <span className=\"absolute bottom-3 left-3 inline-flex items-center text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm\">\n Per {unit}\n </span>\n\n <span className=\"absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm\">\n <Price amount={product.default_price} />/{unit}\n </span>\n </CardImage>\n\n <div className=\"p-4\">\n <h3 className=\"text-[14.5px] font-bold text-foreground leading-snug tracking-tight\">\n {product.name}\n </h3>\n {product.description && (\n <p className=\"text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2\">\n {product.description}\n </p>\n )}\n\n {/* Tags as amenity pills */}\n {tags.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mt-2.5\">\n {tags.slice(0, 4).map((tag) => (\n <span key={tag} className=\"text-[10px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded\">\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {/* Volume tiers (lease terms) */}\n {hasTiers && (\n <div className=\"mt-3\">\n <VolumePricing tiers={product.quantity_pricing!} />\n </div>\n )}\n\n {/* Info row */}\n <div className=\"flex flex-wrap items-center gap-2 mt-3\">\n {product.min_order_quantity != null && product.min_order_quantity > 1 && (\n <span className=\"text-[10.5px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded\">\n Min. {product.min_order_quantity} {unit}{product.min_order_quantity > 1 ? \"s\" : \"\"}\n </span>\n )}\n {hasDeposit && (\n <span className=\"text-[10.5px] font-medium text-amber-600\">\n <Price amount={product.deposit_amount!} /> deposit\n </span>\n )}\n {cancellationHours != null && (\n <span className=\"text-[10.5px] font-medium text-muted-foreground\">\n {cancellationHours >= 720 ? `${Math.floor(cancellationHours / 720)} month notice` : `${cancellationHours}h notice`}\n </span>\n )}\n </div>\n\n <div className=\"flex items-center justify-between mt-3 pt-3 border-t border-border\">\n {product.billing_plans && product.billing_plans.length > 0 && (\n <span className=\"text-[10.5px] font-semibold text-primary px-1.5 py-0.5 bg-primary/10 rounded\">\n {product.billing_plans[0].frequency} billing\n </span>\n )}\n <button\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className=\"text-[13px] font-semibold text-primary hover:text-primary/80 transition-colors ml-auto\"\n >\n Enquire now →\n </button>\n </div>\n </div>\n </CardShell>\n );\n}\n"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -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={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"
|
|
14
|
+
"content": "\"use client\";\n\nimport React, { useCallback, useState } from \"react\";\nimport { Dialog } from \"@base-ui/react/dialog\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from \"@cimplify/sdk\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { ProductSheet } from \"@cimplify/sdk/react\";\nimport { CardVariant } from \"@cimplify/sdk/react\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { FoodProductCard } from \"@cimplify/sdk/react\";\nimport { RetailProductCard } from \"@cimplify/sdk/react\";\nimport { WholesaleProductCard } from \"@cimplify/sdk/react\";\nimport { DigitalProductCard } from \"@cimplify/sdk/react\";\nimport { StandardServiceCard } from \"@cimplify/sdk/react\";\nimport { RentalServiceCard } from \"@cimplify/sdk/react\";\nimport { AccommodationCard } from \"@cimplify/sdk/react\";\nimport { LeaseServiceCard } from \"@cimplify/sdk/react\";\nimport { SubscriptionCard } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\nexport interface ProductCardClassNames {\n root?: string;\n imageContainer?: string;\n image?: string;\n body?: string;\n name?: string;\n description?: string;\n price?: string;\n badges?: string;\n badge?: string;\n modal?: string;\n modalOverlay?: string;\n}\n\nexport interface ProductCardProps {\n /** The product to display. */\n product: Product;\n /** Display mode: \"card\" opens a modal, \"page\" renders as a link. Auto-detected from product.display_mode. */\n displayMode?: \"card\" | \"page\";\n /** Explicit card variant. Auto-detected from product data when omitted. */\n variant?: CardVariant;\n /** Per-slug card map. Highest priority — maps a product slug to a custom card component. */\n cards?: Record<string, React.ComponentType<CardLayoutProps>>;\n /** Link href for page mode. Default: `/menu/${product.slug}` */\n href?: string;\n /** Custom modal content renderer. Receives the fully-loaded product and a close callback. */\n renderModal?: (product: ProductWithDetails, onClose: () => void) => React.ReactNode;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n /** Custom link renderer for page mode (e.g. Next.js Link). */\n renderLink?: (props: {\n href: string;\n className?: string;\n children: React.ReactNode;\n }) => React.ReactElement;\n /** Called when quick-add button is clicked (if the resolved card supports it). */\n onQuickAdd?: (product: Product) => void;\n /** Replace the entire default card body. */\n children?: React.ReactNode;\n /** Image aspect ratio. Default: \"4/3\". */\n aspectRatio?: \"square\" | \"4/3\" | \"16/10\" | \"3/4\";\n className?: string;\n classNames?: ProductCardClassNames;\n}\n\nconst BUILT_IN_CARDS: Record<string, React.ComponentType<CardLayoutProps>> = {\n [CardVariant.Food]: FoodProductCard,\n [CardVariant.Retail]: RetailProductCard,\n [CardVariant.Wholesale]: WholesaleProductCard,\n [CardVariant.Digital]: DigitalProductCard,\n [CardVariant.Standard]: StandardServiceCard,\n [CardVariant.Rental]: RentalServiceCard,\n [CardVariant.Accommodation]: AccommodationCard,\n [CardVariant.Lease]: LeaseServiceCard,\n [CardVariant.Subscription]: SubscriptionCard,\n};\n\nconst RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);\nconst LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);\n\nfunction resolveCardVariant(product: Product): CardVariant {\n if (product.quantity_pricing && product.quantity_pricing.length > 1) {\n return CardVariant.Wholesale;\n }\n if (product.type === PRODUCT_TYPE.Digital) {\n return CardVariant.Digital;\n }\n if (product.type === PRODUCT_TYPE.Service) {\n if (product.duration_unit && RENTAL_UNITS.has(product.duration_unit)) {\n return CardVariant.Rental;\n }\n if (product.duration_unit === DURATION_UNIT.Nights) {\n return CardVariant.Accommodation;\n }\n if (product.duration_unit && LEASE_UNITS.has(product.duration_unit)) {\n return CardVariant.Lease;\n }\n if (product.billing_plans && product.billing_plans.length > 0 && !product.duration_minutes) {\n return CardVariant.Subscription;\n }\n return CardVariant.Standard;\n }\n if (product.render_hint === RENDER_HINT.Food) {\n return CardVariant.Food;\n }\n if (product.render_hint === RENDER_HINT.Physical) {\n return CardVariant.Retail;\n }\n return CardVariant.Retail;\n}\n\n/**\n * ProductCard — a product display card with two modes:\n *\n * - **card** (default): clickable button that opens a Base UI Dialog modal\n * - **page**: a plain `<a>` link for SEO-friendly product pages\n */\nexport function ProductCard({\n product,\n displayMode,\n variant,\n cards,\n href,\n renderModal,\n renderImage,\n renderLink,\n onQuickAdd,\n children,\n aspectRatio = \"4/3\",\n className,\n classNames,\n}: ProductCardProps): React.ReactElement {\n const mode = displayMode ?? product.display_mode ?? \"card\";\n const [isOpen, setIsOpen] = useState(false);\n const [shouldFetch, setShouldFetch] = useState(false);\n\n // Prefetch on pointer enter, always fetch when open\n const { product: productDetails } = useProduct(\n product.slug ?? product.id,\n { enabled: shouldFetch || isOpen },\n );\n\n const handlePrefetch = useCallback(() => {\n setShouldFetch(true);\n }, []);\n\n const handleOpenChange = useCallback((open: boolean) => {\n setIsOpen(open);\n if (open) {\n setShouldFetch(true);\n }\n }, []);\n\n const handleClose = useCallback(() => {\n setIsOpen(false);\n }, []);\n\n const imageUrl = product.image_url || product.images?.[0];\n\n const cardBody = (() => {\n if (children) return children;\n\n // Resolve card variant: slug map → explicit variant → auto-detect\n const SlugCard = cards?.[product.slug];\n if (SlugCard) {\n return <SlugCard product={product} onQuickAdd={onQuickAdd} renderImage={renderImage} renderLink={renderLink} />;\n }\n\n const key = variant ?? resolveCardVariant(product);\n const ResolvedCard = BUILT_IN_CARDS[key];\n if (ResolvedCard) {\n return <ResolvedCard product={product} onQuickAdd={onQuickAdd} renderImage={renderImage} renderLink={renderLink} />;\n }\n\n // Fallback: minimal default\n return (\n <>\n {imageUrl && (\n <div\n data-cimplify-product-card-image-container\n className={cn(\"overflow-hidden rounded-t-xl\", classNames?.imageContainer)}\n style={ASPECT_STYLES[aspectRatio]}\n >\n {renderImage ? (\n renderImage({ src: imageUrl, alt: product.name, className: classNames?.image })\n ) : (\n <img\n src={imageUrl}\n alt={product.name}\n className={cn(\"w-full h-full object-cover transition-transform duration-300 group-hover:scale-105\", classNames?.image)}\n data-cimplify-product-card-image\n />\n )}\n </div>\n )}\n <div data-cimplify-product-card-body className={cn(\"p-4 flex flex-col gap-1\", classNames?.body)}>\n <span data-cimplify-product-card-name className={cn(\"font-semibold text-foreground leading-tight\", classNames?.name)}>\n {product.name}\n </span>\n {product.description && (\n <span data-cimplify-product-card-description className={cn(\"text-sm text-muted-foreground line-clamp-2\", classNames?.description)}>\n {product.description}\n </span>\n )}\n <Price amount={product.default_price} className={cn(\"text-sm font-medium text-foreground mt-1\", classNames?.price)} />\n </div>\n </>\n );\n })();\n\n // Page mode — render as a link\n if (mode === \"page\") {\n const linkHref = href ?? `/menu/${product.slug}`;\n const linkClassName = cn(\"group block no-underline text-[inherit] rounded-xl border border-border bg-background transition-shadow duration-200 hover:shadow-md\", className, classNames?.root);\n\n if (renderLink) {\n return renderLink({ href: linkHref, className: linkClassName, children: cardBody });\n }\n\n return (\n <a\n href={linkHref}\n data-cimplify-product-card\n data-display-mode=\"page\"\n className={linkClassName}\n >\n {cardBody}\n </a>\n );\n }\n\n // Card mode — render as Base UI Dialog\n return (\n <Dialog.Root open={isOpen} onOpenChange={handleOpenChange}>\n <Dialog.Trigger\n onPointerEnter={handlePrefetch}\n data-cimplify-product-card\n data-display-mode=\"card\"\n className={cn(\n \"group block w-full text-left p-0 cursor-pointer font-[inherit] text-[inherit] rounded-xl border border-border bg-background transition-shadow duration-200 hover:shadow-md\",\n className,\n classNames?.root,\n )}\n >\n {cardBody}\n </Dialog.Trigger>\n\n <Dialog.Portal>\n <Dialog.Backdrop\n data-cimplify-product-card-backdrop\n className={cn(\n \"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm transition-opacity\",\n classNames?.modalOverlay,\n )}\n />\n <Dialog.Popup\n data-cimplify-product-card-modal\n className={cn(\n \"fixed z-50 rounded-2xl p-0 max-w-lg w-full h-fit max-h-[85vh] overflow-auto bg-background shadow-2xl outline-none\",\n classNames?.modal,\n )}\n >\n {isOpen && (\n productDetails ? (\n renderModal ? (\n renderModal(productDetails, handleClose)\n ) : (\n <ProductSheet\n product={productDetails}\n onClose={handleClose}\n renderImage={renderImage}\n />\n )\n ) : (\n <div\n data-cimplify-product-card-modal-loading\n aria-busy=\"true\"\n className=\"flex flex-col\"\n >\n <div className=\"aspect-[5/2] bg-muted animate-pulse\" />\n <div className=\"p-6 space-y-3\">\n <div className=\"h-5 w-3/5 bg-muted rounded animate-pulse\" />\n <div className=\"h-7 w-2/5 bg-muted rounded animate-pulse\" />\n <div className=\"h-4 w-4/5 bg-muted rounded animate-pulse\" />\n </div>\n <div className=\"mx-6 border-t border-border pt-4 pb-6\">\n <div className=\"h-14 bg-muted rounded-full animate-pulse\" />\n </div>\n </div>\n )\n )}\n </Dialog.Popup>\n </Dialog.Portal>\n </Dialog.Root>\n );\n}\n"
|
|
15
15
|
}
|
|
16
16
|
]
|
|
17
17
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
{
|
|
16
16
|
"path": "product-page.tsx",
|
|
17
|
-
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport { DefaultProductLayout } from \"@cimplify/sdk/react\";\nimport { FoodProductLayout } from \"@cimplify/sdk/react\";\nimport { WholesaleProductLayout } from \"@cimplify/sdk/react\";\nimport { ServiceProductLayout } from \"@cimplify/sdk/react\";\nimport { DigitalProductLayout } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport type { ProductLayoutProps };\n\nexport enum ProductTemplate {\n Default = \"default\",\n Food = \"food\",\n Wholesale = \"wholesale\",\n Service = \"service\",\n Digital = \"digital\",\n Physical = \"physical\",\n}\n\nexport interface ProductPageClassNames {\n root?: string;\n loading?: string;\n}\n\nexport interface ProductPageProps {\n /** Product slug or ID — used for client-side fetch when `product` is not provided. */\n productId?: string;\n /** Pre-fetched product for SSR. Skips client-side fetch when provided. */\n product?: ProductWithDetails;\n /** Per-slug page map. Highest priority — maps a product slug to a custom component. */\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>;\n /** Per-type template map. Overrides built-in layouts for specific template keys. */\n templates?: Partial<Record<ProductTemplate | string, React.ComponentType<ProductLayoutProps>>>;\n /** Override add-to-cart behavior. */\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: { src: string; alt: string; className?: string }) => React.ReactNode;\n /** Breadcrumb renderer. Receives the product for context. */\n renderBreadcrumb?: (product: ProductWithDetails) => React.ReactNode;\n /** Pre-fetched related products. */\n relatedProducts?: Product[];\n /** Show related products section. Default: true. */\n showRelated?: boolean;\n className?: string;\n classNames?: ProductPageClassNames;\n}\n\nconst BUILT_IN_LAYOUTS: Record<string, React.ComponentType<ProductLayoutProps>> = {\n [ProductTemplate.Food]: FoodProductLayout,\n [ProductTemplate.Wholesale]: WholesaleProductLayout,\n [ProductTemplate.Service]: ServiceProductLayout,\n [ProductTemplate.Digital]: DigitalProductLayout,\n [ProductTemplate.Default]: DefaultProductLayout,\n};\n\nfunction resolveTemplateKey(product: ProductWithDetails): ProductTemplate | string {\n const metaTemplate = product.metadata?.page_template;\n if (typeof metaTemplate === \"string\" && metaTemplate.trim()) {\n return metaTemplate.trim();\n }\n\n if (product.quantity_pricing && product.quantity_pricing.length > 1) {\n return ProductTemplate.Wholesale;\n }\n\n if (product.type ===
|
|
17
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport { PRODUCT_TYPE, RENDER_HINT } from \"@cimplify/sdk\";\nimport type { ProductLayoutProps } from \"@cimplify/sdk/react\";\nimport { DefaultProductLayout } from \"@cimplify/sdk/react\";\nimport { FoodProductLayout } from \"@cimplify/sdk/react\";\nimport { WholesaleProductLayout } from \"@cimplify/sdk/react\";\nimport { ServiceProductLayout } from \"@cimplify/sdk/react\";\nimport { DigitalProductLayout } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport type { ProductLayoutProps };\n\nexport enum ProductTemplate {\n Default = \"default\",\n Food = \"food\",\n Wholesale = \"wholesale\",\n Service = \"service\",\n Digital = \"digital\",\n Physical = \"physical\",\n}\n\nexport interface ProductPageClassNames {\n root?: string;\n loading?: string;\n}\n\nexport interface ProductPageProps {\n /** Product slug or ID — used for client-side fetch when `product` is not provided. */\n productId?: string;\n /** Pre-fetched product for SSR. Skips client-side fetch when provided. */\n product?: ProductWithDetails;\n /** Per-slug page map. Highest priority — maps a product slug to a custom component. */\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>;\n /** Per-type template map. Overrides built-in layouts for specific template keys. */\n templates?: Partial<Record<ProductTemplate | string, React.ComponentType<ProductLayoutProps>>>;\n /** Override add-to-cart behavior. */\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: { src: string; alt: string; className?: string }) => React.ReactNode;\n /** Breadcrumb renderer. Receives the product for context. */\n renderBreadcrumb?: (product: ProductWithDetails) => React.ReactNode;\n /** Pre-fetched related products. */\n relatedProducts?: Product[];\n /** Show related products section. Default: true. */\n showRelated?: boolean;\n className?: string;\n classNames?: ProductPageClassNames;\n}\n\nconst BUILT_IN_LAYOUTS: Record<string, React.ComponentType<ProductLayoutProps>> = {\n [ProductTemplate.Food]: FoodProductLayout,\n [ProductTemplate.Wholesale]: WholesaleProductLayout,\n [ProductTemplate.Service]: ServiceProductLayout,\n [ProductTemplate.Digital]: DigitalProductLayout,\n [ProductTemplate.Default]: DefaultProductLayout,\n};\n\nfunction resolveTemplateKey(product: ProductWithDetails): ProductTemplate | string {\n const metaTemplate = product.metadata?.page_template;\n if (typeof metaTemplate === \"string\" && metaTemplate.trim()) {\n return metaTemplate.trim();\n }\n\n if (product.quantity_pricing && product.quantity_pricing.length > 1) {\n return ProductTemplate.Wholesale;\n }\n\n if (product.type === PRODUCT_TYPE.Service) {\n return ProductTemplate.Service;\n }\n\n if (product.type === PRODUCT_TYPE.Digital) {\n return ProductTemplate.Digital;\n }\n\n if (product.render_hint === RENDER_HINT.Food) {\n return ProductTemplate.Food;\n }\n\n if (product.render_hint === RENDER_HINT.Physical) {\n return ProductTemplate.Physical;\n }\n\n return ProductTemplate.Default;\n}\n\nfunction resolveLayout(\n product: ProductWithDetails,\n pages?: Record<string, React.ComponentType<ProductLayoutProps>>,\n templates?: Partial<Record<string, React.ComponentType<ProductLayoutProps>>>,\n): React.ComponentType<ProductLayoutProps> {\n // 1. Per-slug page (AI-generated, highest priority)\n if (pages?.[product.slug]) {\n return pages[product.slug];\n }\n\n const key = resolveTemplateKey(product);\n\n // 2. Consumer-provided template override\n if (templates?.[key]) {\n return templates[key];\n }\n\n // 3. Built-in layout\n if (BUILT_IN_LAYOUTS[key]) {\n return BUILT_IN_LAYOUTS[key];\n }\n\n // 4. Fallback\n return DefaultProductLayout;\n}\n\nexport function ProductPage({\n productId,\n product: productProp,\n pages,\n templates,\n onAddToCart,\n renderImage,\n renderBreadcrumb,\n relatedProducts,\n showRelated = true,\n className,\n classNames,\n}: ProductPageProps): React.ReactElement {\n const resolvedId = productId || productProp?.slug || productProp?.id || \"\";\n const { product: fetched, isLoading } = useProduct(resolvedId, {\n enabled: !productProp && resolvedId.length > 0,\n });\n const product = productProp ?? fetched;\n\n if (isLoading && !product) {\n return (\n <div\n data-cimplify-product-page\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n >\n <div className=\"grid lg:grid-cols-2 gap-8 lg:gap-12 animate-pulse\">\n <div className=\"aspect-square bg-muted\" />\n <div className=\"space-y-4 py-4\">\n <div className=\"h-8 w-3/5 bg-muted\" />\n <div className=\"h-6 w-2/5 bg-muted\" />\n <div className=\"h-4 w-4/5 bg-muted\" />\n <div className=\"h-4 w-3/5 bg-muted\" />\n <div className=\"h-14 w-full bg-muted mt-8\" />\n </div>\n </div>\n </div>\n );\n }\n\n if (!product) {\n return (\n <div data-cimplify-product-page className={cn(className, classNames?.root)}>\n <p className=\"text-muted-foreground\">Product not found.</p>\n </div>\n );\n }\n\n const Layout = resolveLayout(product, pages, templates);\n\n return (\n <div data-cimplify-product-page className={cn(className, classNames?.root)}>\n <Layout\n product={product}\n onAddToCart={onAddToCart}\n renderImage={renderImage}\n renderBreadcrumb={renderBreadcrumb}\n relatedProducts={relatedProducts}\n showRelated={showRelated}\n />\n </div>\n );\n}\n"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rental-service-card",
|
|
3
|
+
"title": "RentalServiceCard",
|
|
4
|
+
"description": "Rental card with per-day/hour pricing, deposit, and availability count.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"price",
|
|
8
|
+
"cn"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "cards/rental-service-card.tsx",
|
|
13
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ServiceCardLayoutProps } from \"./standard-service-card\";\nimport { CardShell, CardImage, QuickAddButton } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst UNIT_LABELS: Record<string, string> = {\n minutes: \"min\",\n hours: \"hr\",\n days: \"day\",\n nights: \"night\",\n weeks: \"week\",\n months: \"mo\",\n years: \"yr\",\n};\n\nexport function RentalServiceCard({\n product,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const hasDeposit = product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount;\n const unit = product.duration_unit ? UNIT_LABELS[product.duration_unit] || product.duration_unit : \"day\";\n const status = product.inventory_status;\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"16/9\" renderImage={renderImage}>\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none\" />\n\n {/* Per-unit pill */}\n <span className=\"absolute bottom-3 left-3 inline-flex items-center text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm\">\n Per {unit}\n </span>\n\n {/* Price */}\n <span className=\"absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm\">\n <Price amount={product.default_price} />/{unit}\n </span>\n\n {/* Capacity badge */}\n {product.general_service_capacity != null && product.general_service_capacity > 1 && (\n <span className=\"absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-blue-50/90 text-blue-700 border border-blue-200/50 backdrop-blur px-2 py-0.5 rounded-md\">\n Up to {product.general_service_capacity} guests\n </span>\n )}\n </CardImage>\n\n <div className=\"p-4\">\n <h3 className=\"text-[14.5px] font-bold text-foreground leading-snug tracking-tight\">\n {product.name}\n </h3>\n {product.description && (\n <p className=\"text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2\">\n {product.description}\n </p>\n )}\n\n {/* Info pills */}\n <div className=\"flex flex-wrap items-center gap-2 mt-3\">\n {product.min_order_quantity != null && product.min_order_quantity > 1 && (\n <span className=\"text-[10.5px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded\">\n Min. {product.min_order_quantity} {unit}{product.min_order_quantity > 1 ? \"s\" : \"\"}\n </span>\n )}\n {hasDeposit && (\n <span className=\"text-[10.5px] font-medium text-amber-600\">\n <Price amount={product.deposit_amount!} /> deposit\n </span>\n )}\n </div>\n\n <div className=\"flex items-center justify-between mt-3 pt-3 border-t border-border\">\n {/* Availability */}\n {status && (\n <div className=\"flex items-center gap-1.5 text-[12px]\">\n <span className={cn(\n \"w-[7px] h-[7px] rounded-full shrink-0\",\n status.in_stock ? \"bg-emerald-500\" : \"bg-red-500\",\n )} />\n <span className={status.in_stock ? \"text-muted-foreground\" : \"text-red-600\"}>\n {status.in_stock\n ? status.stock_level != null\n ? `${status.stock_level} available`\n : \"Available\"\n : \"Unavailable\"\n }\n </span>\n </div>\n )}\n <button\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className=\"text-[13px] font-semibold text-primary hover:text-primary/80 transition-colors\"\n >\n Reserve now →\n </button>\n </div>\n </div>\n </CardShell>\n );\n}\n"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "retail-product-card",
|
|
3
|
+
"title": "RetailProductCard",
|
|
4
|
+
"description": "Product card for retail with color swatches, sale badge, wishlist, and sold-out overlay.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"price",
|
|
8
|
+
"cn"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "cards/retail-product-card.tsx",
|
|
13
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { CardShell, CardImage, WishlistButton, SoldOutOverlay, SaleBadge, LowStockBadge } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { parsePrice, isOnSale, getBasePrice, getDiscountPercentage } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nfunction getColorSwatches(product: Product): string[] {\n const variants = (product as ProductWithDetails).variants;\n if (!variants || variants.length === 0) return [];\n\n const seen = new Set<string>();\n for (const v of variants) {\n if (v.display_attributes) {\n for (const attr of v.display_attributes) {\n if (attr.axis_name.toLowerCase() === \"color\" && !seen.has(attr.value_name)) {\n seen.add(attr.value_name);\n }\n }\n }\n }\n\n return Array.from(seen).slice(0, 5);\n}\n\nexport function RetailProductCard({\n product,\n renderImage,\n renderLink,\n className,\n}: CardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const outOfStock = product.inventory_status?.in_stock === false;\n const onSale = isOnSale(product);\n const swatches = getColorSwatches(product);\n\n return (\n <CardShell product={product} renderLink={renderLink} disabled={outOfStock} className={className}>\n {outOfStock && <SoldOutOverlay />}\n\n <CardImage src={image} alt={product.name} aspectRatio=\"3/4\" renderImage={renderImage}>\n {/* Sale badge */}\n {onSale && (\n <div className=\"absolute top-3 left-3\">\n <SaleBadge product={product} />\n </div>\n )}\n\n {/* Wishlist */}\n <WishlistButton className=\"absolute top-3 right-3\" />\n\n {/* Low stock */}\n {product.inventory_status?.low_stock && product.inventory_status?.stock_level && (\n <span className=\"absolute bottom-3 left-3 text-[11px] font-semibold bg-amber-50 text-amber-700 border border-amber-200/60 px-2 py-0.5 rounded-md\">\n Only {product.inventory_status.stock_level} left\n </span>\n )}\n </CardImage>\n\n <div className=\"p-3.5\">\n {/* Brand */}\n {product.vendor && (\n <p className=\"text-[10px] font-semibold text-muted-foreground uppercase tracking-widest\">\n {product.vendor}\n </p>\n )}\n\n {/* Name */}\n <h3 className=\"text-[14.5px] font-semibold text-foreground leading-snug tracking-tight mt-1 truncate\">\n {product.name}\n </h3>\n\n {/* Price */}\n <div className=\"flex items-center gap-2 mt-2\">\n <Price amount={product.default_price} className=\"text-sm font-bold\" />\n {onSale && (\n <Price amount={getBasePrice(product)} className=\"text-xs text-muted-foreground line-through\" />\n )}\n </div>\n\n {/* Color swatches */}\n {swatches.length > 0 && (\n <div className=\"flex gap-1.5 mt-2.5\">\n {swatches.map((name) => (\n <span\n key={name}\n title={name}\n className=\"text-[9px] font-medium text-muted-foreground px-1.5 py-0.5 bg-muted rounded\"\n >\n {name}\n </span>\n ))}\n </div>\n )}\n </div>\n </CardShell>\n );\n}\n"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "schedule-service-card",
|
|
3
|
+
"title": "ScheduleServiceCard",
|
|
4
|
+
"description": "Service card with next available time slot pills.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"price",
|
|
8
|
+
"cn"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "cards/schedule-service-card.tsx",
|
|
13
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { ServiceCardLayoutProps } from \"./standard-service-card\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport function ScheduleServiceCard({\n product,\n slots,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const href = `/products/${product.slug}`;\n const durationLabel = product.duration_minutes != null\n ? `${product.duration_minutes} ${product.duration_unit ?? \"min\"}`\n : null;\n\n const displaySlots = (slots || []).slice(0, 3);\n\n const shellClass = cn(\n \"group block text-left cursor-pointer bg-card rounded-[14px] overflow-hidden\",\n \"shadow-[0_1px_3px_rgba(0,0,0,0.04),0_6px_24px_rgba(0,0,0,0.06)]\",\n \"transition-all duration-300 ease-[cubic-bezier(0.19,1,0.22,1)]\",\n \"hover:-translate-y-1 hover:shadow-[0_2px_6px_rgba(0,0,0,0.04),0_12px_40px_rgba(0,0,0,0.10)]\",\n className,\n );\n\n const inner = (\n <div className=\"p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex-1 min-w-0\">\n <h3 className=\"text-[14.5px] font-bold text-foreground leading-snug tracking-tight\">\n {product.name}\n </h3>\n <p className=\"text-[12.5px] text-muted-foreground mt-0.5\">\n {durationLabel && <>{durationLabel} · </>}\n <Price amount={product.default_price} />\n </p>\n </div>\n {image && (\n <div className=\"w-14 h-14 rounded-xl overflow-hidden bg-muted shrink-0\">\n {renderImage ? (\n renderImage({ src: image, alt: product.name, className: \"w-full h-full object-cover\" })\n ) : (\n <img src={image} alt={product.name} className=\"w-full h-full object-cover\" />\n )}\n </div>\n )}\n </div>\n\n {product.description && (\n <p className=\"text-[12px] text-muted-foreground mt-2 line-clamp-2 leading-relaxed\">\n {product.description}\n </p>\n )}\n\n {displaySlots.length > 0 && (\n <div className=\"mt-4 pt-3 border-t border-border\">\n <p className=\"text-[10px] font-semibold text-muted-foreground uppercase tracking-widest mb-2\">\n {product.general_service_capacity != null && product.general_service_capacity > 1\n ? \"Upcoming classes\"\n : \"Next available\"\n }\n </p>\n <div className=\"flex gap-2 flex-wrap\">\n {displaySlots.map((slot, i) => (\n <button\n key={i}\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className={cn(\n \"text-[12.5px] font-medium px-3 py-1.5 rounded-lg transition-all duration-200 ease-out hover:scale-[1.04]\",\n i === 0\n ? \"bg-primary text-primary-foreground font-semibold shadow-sm\"\n : \"bg-muted text-foreground hover:bg-muted/80\",\n )}\n >\n {new Date(slot.start_time).toLocaleString(undefined, {\n weekday: \"short\",\n hour: \"numeric\",\n minute: \"2-digit\",\n })}\n </button>\n ))}\n </div>\n </div>\n )}\n </div>\n );\n\n if (renderLink) {\n return renderLink({ href, className: shellClass, children: inner });\n }\n\n return <a href={href} className={shellClass} data-cimplify-card>{inner}</a>;\n}\n"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "standard-service-card",
|
|
3
|
+
"title": "StandardServiceCard",
|
|
4
|
+
"description": "Service card with hero image, duration, deposit, and availability.",
|
|
5
|
+
"type": "component",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"price",
|
|
8
|
+
"cn"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "cards/standard-service-card.tsx",
|
|
13
|
+
"content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product } from \"@cimplify/sdk\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\nimport type { CardLayoutProps } from \"@cimplify/sdk/react\";\nimport { CardShell, CardImage } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ServiceCardLayoutProps extends CardLayoutProps {\n slots?: AvailableSlot[];\n onBook?: (product: Product) => void;\n}\n\nfunction formatDuration(minutes: number, unit?: string): string {\n if (unit && unit !== \"minutes\") return `${minutes} ${unit}`;\n if (minutes >= 60) {\n const h = Math.floor(minutes / 60);\n const m = minutes % 60;\n return m > 0 ? `${h}h ${m}m` : `${h} hr${h > 1 ? \"s\" : \"\"}`;\n }\n return `${minutes} min`;\n}\n\nexport function StandardServiceCard({\n product,\n onBook,\n renderImage,\n renderLink,\n className,\n}: ServiceCardLayoutProps): React.ReactElement {\n const image = product.image_url || product.images?.[0] || \"\";\n const hasDeposit = product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount;\n\n return (\n <CardShell product={product} renderLink={renderLink} className={className}>\n <CardImage src={image} alt={product.name} aspectRatio=\"16/9\" renderImage={renderImage}>\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none\" />\n\n {/* Duration pill */}\n {product.duration_minutes != null && (\n <span className=\"absolute bottom-3 left-3 inline-flex items-center gap-1 text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm\">\n {formatDuration(product.duration_minutes, product.duration_unit)}\n </span>\n )}\n\n {/* Price on image */}\n <span className=\"absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm\">\n <Price amount={product.default_price} />\n </span>\n </CardImage>\n\n <div className=\"p-4\">\n <h3 className=\"text-[14.5px] font-bold text-foreground leading-snug tracking-tight\">\n {product.name}\n </h3>\n {product.description && (\n <p className=\"text-[12.5px] text-muted-foreground mt-1 leading-relaxed line-clamp-2\">\n {product.description}\n </p>\n )}\n\n <div className=\"flex items-center justify-between mt-3 pt-3 border-t border-border\">\n <div className=\"flex items-center gap-2 text-[12px] text-muted-foreground\">\n <span className=\"w-[7px] h-[7px] rounded-full bg-emerald-500 animate-pulse\" />\n <span>Available</span>\n {hasDeposit && (\n <>\n <span className=\"text-border\">·</span>\n <span><Price amount={product.deposit_amount!} /> deposit</span>\n </>\n )}\n </div>\n <button\n onClick={(e) => { e.preventDefault(); e.stopPropagation(); onBook?.(product); }}\n className=\"text-[13px] font-semibold text-primary hover:text-primary/80 transition-colors\"\n >\n Book now →\n </button>\n </div>\n </div>\n </CardShell>\n );\n}\n"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|