@cimplify/sdk 0.95.0 → 0.97.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/react.d.mts +10 -2
- package/dist/react.d.ts +10 -2
- package/dist/react.js +392 -69
- package/dist/react.mjs +393 -70
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/registry/chat-widget.json +1 -1
- package/registry/date-slot-picker.json +1 -1
- package/registry/slot-picker.json +1 -1
package/dist/styles.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/*! tailwindcss v4.3.1 | MIT License | https://tailwindcss.com */
|
|
2
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-tracking:initial;--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-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.top-1\/2{top:50%}.top-\[18px\]{top:18px}.top-\[calc\(100\%\+8px\)\]{top:calc(100% + 8px)}.top-full{top:100%}.top-px{top:1px}.-right-px{right:-1px}.-bottom-px{bottom:-1px}.-left-\[9px\]{left:-9px}.left-\[14px\]{left:14px}.isolate{isolation:isolate}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-50{z-index:50}.z-\[1\]{z-index:1}.z-\[2\]{z-index:2}.z-\[200\]{z-index:200}.z-\[9999\]{z-index:9999}.container{width:100%}.m-auto{margin:auto}.mx-auto{margin-inline:auto}.-mt-px{margin-top:-1px}.mt-auto{margin-top:auto}.mt-px{margin-top:1px}.mr-auto{margin-right:auto}.-mb-px{margin-bottom:-1px}.ml-\[1px\]{margin-left:1px}.ml-\[9px\]{margin-left:9px}.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}.contents{display:contents}.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}.size-\[7px\]{width:7px;height:7px}.size-\[18px\]{width:18px;height:18px}.size-\[22px\]{width:22px;height:22px}.size-\[26px\]{width:26px;height:26px}.size-\[38px\]{width:38px;height:38px}.size-\[46px\]{width:46px;height:46px}.size-full{width:100%;height:100%}.h-\[6px\]{height:6px}.h-\[7px\]{height:7px}.h-\[11px\]{height:11px}.h-\[18px\]{height:18px}.h-\[72px\]{height:72px}.h-\[216px\]{height:216px}.h-\[min\(600px\,calc\(100vh-6rem\)\)\]{height:min(600px,100vh - 6rem)}.h-fit{height:fit-content}.h-full{height:100%}.h-px{height:1px}.max-h-\[85vh\]{max-height:85vh}.max-h-\[100px\]{max-height:100px}.min-h-\[20px\]{min-height:20px}.min-h-\[480px\]{min-height:480px}.min-h-\[720px\]{min-height:720px}.min-h-screen{min-height:100vh}.w-1\/2{width:50%}.w-2\/5{width:40%}.w-3\/4{width:75%}.w-3\/5{width:60%}.w-4\/5{width:80%}.w-\[3px\]{width:3px}.w-\[6px\]{width:6px}.w-\[7px\]{width:7px}.w-\[11px\]{width:11px}.w-\[18px\]{width:18px}.w-\[72px\]{width:72px}.w-\[280px\]{width:280px}.w-\[400px\]{width:400px}.w-fit{width:fit-content}.w-full{width:100%}.max-w-\[85\%\]{max-width:85%}.max-w-\[120px\]{max-width:120px}.max-w-\[260px\]{max-width:260px}.max-w-\[280px\]{max-width:280px}.max-w-\[300px\]{max-width:300px}.max-w-\[380px\]{max-width:380px}.max-w-\[420px\]{max-width:420px}.max-w-\[460px\]{max-width:460px}.max-w-\[calc\(100vw-1\.5rem\)\]{max-width:calc(100vw - 1.5rem)}.max-w-\[calc\(100vw-3rem\)\]{max-width:calc(100vw - 3rem)}.max-w-full{max-width:100%}.flex-1{flex:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.grow{flex-grow:1}.border-collapse{border-collapse:collapse}.origin-bottom-left{transform-origin:0 100%}.origin-bottom-right{transform-origin:100% 100%}.translate-x-full{--tw-translate-x:100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-90{rotate:90deg}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-crosshair{cursor:crosshair}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.resize{resize:both}.resize-none{resize:none}.list-none{list-style-type:none}.\[appearance\:textfield\]{appearance:textfield}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.grid-cols-\[16px_1fr_auto\]{grid-template-columns:16px 1fr auto}.grid-cols-\[22px_1fr_auto_auto\]{grid-template-columns:22px 1fr auto auto}.grid-cols-\[40px_1fr\]{grid-template-columns:40px 1fr}.grid-cols-\[40px_1fr_auto\]{grid-template-columns:40px 1fr auto}.grid-cols-\[44px_1fr_60px\]{grid-template-columns:44px 1fr 60px}.grid-cols-\[46px_minmax\(0\,1fr\)_auto\]{grid-template-columns:46px minmax(0,1fr) auto}.grid-cols-\[48px_1fr_auto_auto\]{grid-template-columns:48px 1fr auto auto}.grid-cols-\[56px_1fr_auto_auto\]{grid-template-columns:56px 1fr auto auto}.grid-cols-\[84px_minmax\(0\,1fr\)\]{grid-template-columns:84px minmax(0,1fr)}.grid-cols-\[130px_1fr_auto\]{grid-template-columns:130px 1fr auto}.grid-cols-\[auto_1fr_auto\]{grid-template-columns:auto 1fr auto}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-\[7px\]{gap:7px}.gap-px{gap:1px}.gap-x-\[13px\]{column-gap:13px}: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))}:where(.divide-border\/60>:not(:last-child)){border-color:#dedede99}@supports (color:color-mix(in lab, red, red)){:where(.divide-border\/60>:not(:last-child)){border-color:color-mix(in oklab, var(--color-border,oklch(90% 0 0)) 60%, transparent)}}.self-stretch{align-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:var(--radius,.5rem)}.rounded-\[8px\]{border-radius:8px}.rounded-\[9px\]{border-radius:9px}.rounded-\[10px\]{border-radius:10px}.rounded-\[12px\]{border-radius:12px}.rounded-\[14px\]{border-radius:14px}.rounded-\[16px\]{border-radius:16px}.rounded-\[inherit\]{border-radius:inherit}.rounded-\[var\(--radius\,12px\)\]{border-radius:var(--radius,12px)}.rounded-full{border-radius:3.40282e38px}.rounded-tl{border-top-left-radius:var(--radius,.5rem)}.rounded-tr{border-top-right-radius:var(--radius,.5rem)}.rounded-br{border-bottom-right-radius:var(--radius,.5rem)}.rounded-bl{border-bottom-left-radius:var(--radius,.5rem)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-\[1\.5px\]{border-style:var(--tw-border-style);border-width:1.5px}.border-\[2\.5px\]{border-style:var(--tw-border-style);border-width:2.5px}.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-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-none{--tw-border-style:none;border-style:none}.border-\[color-mix\(in_oklch\,var\(--color-primary\)_18\%\,transparent\)\]{border-color:#934c3a2e}@supports (color:color-mix(in lab, red, red)){.border-\[color-mix\(in_oklch\,var\(--color-primary\)_18\%\,transparent\)\]{border-color:color-mix(in oklch,var(--color-primary) 18%,transparent)}}.border-border{border-color:var(--color-border,oklch(90% 0 0))}.border-border\/40{border-color:#dedede66}@supports (color:color-mix(in lab, red, red)){.border-border\/40{border-color:color-mix(in oklab, var(--color-border,oklch(90% 0 0)) 40%, transparent)}}.border-border\/60{border-color:#dedede99}@supports (color:color-mix(in lab, red, red)){.border-border\/60{border-color:color-mix(in oklab, var(--color-border,oklch(90% 0 0)) 60%, transparent)}}.border-foreground{border-color:var(--color-foreground,oklch(15% 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-primary\/20{border-color:#934c3a33}@supports (color:color-mix(in lab, red, red)){.border-primary\/20{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 20%, transparent)}}.border-transparent{border-color:#0000}.bg-background{background-color:var(--color-background,oklch(99% 0 0))}.bg-background\/15{background-color:#fcfcfc26}@supports (color:color-mix(in lab, red, red)){.bg-background\/15{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 15%, transparent)}}.bg-background\/40{background-color:#fcfcfc66}@supports (color:color-mix(in lab, red, red)){.bg-background\/40{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 40%, transparent)}}.bg-background\/50{background-color:#fcfcfc80}@supports (color:color-mix(in lab, red, red)){.bg-background\/50{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 50%, transparent)}}.bg-background\/90{background-color:#fcfcfce6}@supports (color:color-mix(in lab, red, red)){.bg-background\/90{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 90%, transparent)}}.bg-background\/92{background-color:#fcfcfceb}@supports (color:color-mix(in lab, red, red)){.bg-background\/92{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 92%, transparent)}}.bg-border{background-color:var(--color-border,oklch(90% 0 0))}.bg-border\/60{background-color:#dedede99}@supports (color:color-mix(in lab, red, red)){.bg-border\/60{background-color:color-mix(in oklab, var(--color-border,oklch(90% 0 0)) 60%, transparent)}}.bg-current{background-color:currentColor}.bg-destructive{background-color:var(--color-destructive,oklch(50% .2 25))}.bg-destructive\/10{background-color:#bb061e1a}@supports (color:color-mix(in lab, red, red)){.bg-destructive\/10{background-color:color-mix(in oklab, var(--color-destructive,oklch(50% .2 25)) 10%, transparent)}}.bg-foreground{background-color:var(--color-foreground,oklch(15% 0 0))}.bg-foreground\/40{background-color:#0b0b0b66}@supports (color:color-mix(in lab, red, red)){.bg-foreground\/40{background-color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 40%, transparent)}}.bg-muted{background-color:var(--color-muted,oklch(95% 0 0))}.bg-muted-foreground\/40{background-color:#63636366}@supports (color:color-mix(in lab, red, red)){.bg-muted-foreground\/40{background-color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 40%, transparent)}}.bg-muted\/30{background-color:#eeeeee4d}@supports (color:color-mix(in lab, red, red)){.bg-muted\/30{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 30%, transparent)}}.bg-muted\/40{background-color:#eee6}@supports (color:color-mix(in lab, red, red)){.bg-muted\/40{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 40%, transparent)}}.bg-muted\/50{background-color:#eeeeee80}@supports (color:color-mix(in lab, red, red)){.bg-muted\/50{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 50%, transparent)}}.bg-muted\/60{background-color:#eee9}@supports (color:color-mix(in lab, red, red)){.bg-muted\/60{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 60%, transparent)}}.bg-muted\/70{background-color:#eeeeeeb3}@supports (color:color-mix(in lab, red, red)){.bg-muted\/70{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 70%, transparent)}}.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-primary\/\[0\.12\]{background-color:#934c3a1f}@supports (color:color-mix(in lab, red, red)){.bg-primary\/\[0\.12\]{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 12%, 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))}.bg-\[linear-gradient\(135deg\,color-mix\(in_oklch\,var\(--color-primary\)_8\%\,transparent\)\,var\(--color-background\)\)\]{background-image:linear-gradient(135deg,#934c3a14,var(--color-background))}@supports (color:color-mix(in lab, red, red)){.bg-\[linear-gradient\(135deg\,color-mix\(in_oklch\,var\(--color-primary\)_8\%\,transparent\)\,var\(--color-background\)\)\]{background-image:linear-gradient(135deg,color-mix(in oklch,var(--color-primary) 8%,transparent),var(--color-background))}}.bg-\[linear-gradient\(135deg\,oklch\(0\.92_0\.05_80\)\,oklch\(0\.84_0\.10_60\)\)\]{background-image:linear-gradient(135deg,oklch(92% .05 80),oklch(84% .1 60))}.bg-\[radial-gradient\(900px_480px_at_50\%_0\%\,color-mix\(in_oklch\,var\(--color-primary\)_8\%\,transparent\)\,transparent_70\%\)\,var\(--color-background\)\]{background-image:radial-gradient(900px 480px at 50% 0%,#934c3a14,transparent 70%),var(--color-background)}@supports (color:color-mix(in lab, red, red)){.bg-\[radial-gradient\(900px_480px_at_50\%_0\%\,color-mix\(in_oklch\,var\(--color-primary\)_8\%\,transparent\)\,transparent_70\%\)\,var\(--color-background\)\]{background-image:radial-gradient(900px 480px at 50% 0%,color-mix(in oklch,var(--color-primary) 8%,transparent),transparent 70%),var(--color-background)}}.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}.px-\[13px\]{padding-inline:13px}.px-\[18px\]{padding-inline:18px}.px-\[22px\]{padding-inline:22px}.py-\[7px\]{padding-block:7px}.py-\[10px\]{padding-block:10px}.py-\[15px\]{padding-block:15px}.py-\[18px\]{padding-block:18px}.pb-\[13px\]{padding-bottom:13px}.pb-\[22px\]{padding-bottom:22px}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.\[font-family\:inherit\],.font-\[inherit\]{font-family:inherit}.text-\[9px\]{font-size:9px}.text-\[10\.5px\]{font-size:10.5px}.text-\[10px\]{font-size:10px}.text-\[11\.5px\]{font-size:11.5px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[13\.5px\]{font-size:13.5px}.text-\[13px\]{font-size:13px}.text-\[14\.5px\]{font-size:14.5px}.text-\[14px\]{font-size:14px}.text-\[15px\]{font-size:15px}.text-\[16px\]{font-size:16px}.text-\[17px\]{font-size:17px}.text-\[18px\]{font-size:18px}.text-\[22px\]{font-size:22px}.text-\[24px\]{font-size:24px}.text-\[28px\]{font-size:28px}.text-\[30px\]{font-size:30px}.text-\[clamp\(26px\,2\.8vw\,34px\)\]{font-size:clamp(26px,2.8vw,34px)}.text-\[clamp\(28px\,3vw\,40px\)\]{font-size:clamp(28px,3vw,40px)}.leading-\[1\.1\]{--tw-leading:1.1;line-height:1.1}.leading-\[1\.2\]{--tw-leading:1.2;line-height:1.2}.leading-\[1\.4\]{--tw-leading:1.4;line-height:1.4}.leading-\[1\.05\]{--tw-leading:1.05;line-height:1.05}.leading-\[1\.5\]{--tw-leading:1.5;line-height:1.5}.leading-\[1\.15\]{--tw-leading:1.15;line-height:1.15}.leading-\[1\.55\]{--tw-leading:1.55;line-height:1.55}.leading-none{--tw-leading:1;line-height:1}.tracking-\[-0\.01em\]{--tw-tracking:-.01em;letter-spacing:-.01em}.tracking-\[-0\.02em\]{--tw-tracking:-.02em;letter-spacing:-.02em}.tracking-\[-0\.015em\]{--tw-tracking:-.015em;letter-spacing:-.015em}.tracking-\[-0\.025em\]{--tw-tracking:-.025em;letter-spacing:-.025em}.tracking-\[0\.1em\]{--tw-tracking:.1em;letter-spacing:.1em}.tracking-\[0\.06em\]{--tw-tracking:.06em;letter-spacing:.06em}.tracking-\[0\.08em\]{--tw-tracking:.08em;letter-spacing:.08em}.tracking-\[0\.12em\]{--tw-tracking:.12em;letter-spacing:.12em}.tracking-\[0\.16em\]{--tw-tracking:.16em;letter-spacing:.16em}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[inherit\]{color:inherit}.text-\[var\(--color-primary-foreground\,\#fff\)\]{color:var(--color-primary-foreground,#fff)}.text-background{color:var(--color-background,oklch(99% 0 0))}.text-background\/60{color:#fcfcfc99}@supports (color:color-mix(in lab, red, red)){.text-background\/60{color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 60%, transparent)}}.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-foreground\/35{color:#0b0b0b59}@supports (color:color-mix(in lab, red, red)){.text-foreground\/35{color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 35%, transparent)}}.text-foreground\/45{color:#0b0b0b73}@supports (color:color-mix(in lab, red, red)){.text-foreground\/45{color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 45%, transparent)}}.text-foreground\/55{color:#0b0b0b8c}@supports (color:color-mix(in lab, red, red)){.text-foreground\/55{color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 55%, transparent)}}.text-foreground\/60{color:#0b0b0b99}@supports (color:color-mix(in lab, red, red)){.text-foreground\/60{color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 60%, transparent)}}.text-foreground\/70{color:#0b0b0bb3}@supports (color:color-mix(in lab, red, red)){.text-foreground\/70{color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 70%, transparent)}}.text-foreground\/80{color:#0b0b0bcc}@supports (color:color-mix(in lab, red, red)){.text-foreground\/80{color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 80%, transparent)}}.text-foreground\/90{color:#0b0b0be6}@supports (color:color-mix(in lab, red, red)){.text-foreground\/90{color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 90%, transparent)}}.text-muted-foreground{color:var(--color-muted-foreground,oklch(50% 0 0))}.text-muted-foreground\/30{color:#6363634d}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/30{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 30%, transparent)}}.text-muted-foreground\/40{color:#63636366}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/40{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 40%, transparent)}}.text-muted-foreground\/50{color:#63636380}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/50{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 50%, transparent)}}.text-muted-foreground\/60{color:#63636399}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/60{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 60%, transparent)}}.text-muted-foreground\/70{color:#636363b3}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/70{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 70%, transparent)}}.text-primary{color:var(--color-primary,oklch(50% .1 35))}.text-primary-foreground{color:var(--color-primary-foreground,oklch(99% 0 0))}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.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}.underline-offset-2{text-underline-offset:2px}.accent-foreground{accent-color:var(--color-foreground,oklch(15% 0 0))}.accent-primary{accent-color:var(--color-primary,oklch(50% .1 35))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.opacity-85{opacity:.85}.opacity-100{opacity:1}.shadow-\[0_0_0_1px_var\(--color-border\)\]{--tw-shadow:0 0 0 1px var(--tw-shadow-color,var(--color-border));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_0_0_2px_var\(--color-card\)\]{--tw-shadow:0 0 0 2px var(--tw-shadow-color,var(--color-card));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_0_0_6px_color-mix\(in_oklch\,var\(--color-primary\)_18\%\,transparent\)\]{--tw-shadow:0 0 0 6px var(--tw-shadow-color,#934c3a2e)}@supports (color:color-mix(in lab, red, red)){.shadow-\[0_0_0_6px_color-mix\(in_oklch\,var\(--color-primary\)_18\%\,transparent\)\]{--tw-shadow:0 0 0 6px var(--tw-shadow-color,color-mix(in oklch,var(--color-primary) 18%,transparent))}}.shadow-\[0_0_0_6px_color-mix\(in_oklch\,var\(--color-primary\)_18\%\,transparent\)\]{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_0_rgba\(0\,0\,0\,0\.04\)\,0_8px_24px_rgba\(0\,0\,0\,0\.12\)\]{--tw-shadow:0 1px 0 var(--tw-shadow-color,#0000000a), 0 8px 24px var(--tw-shadow-color,#0000001f);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_0_rgba\(0\,0\,0\,0\.04\)\,0_16px_48px_rgba\(0\,0\,0\,0\.08\)\]{--tw-shadow:0 1px 0 var(--tw-shadow-color,#0000000a), 0 16px 48px 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)}.shadow-\[0_1px_2px_rgba\(16\,24\,40\,0\.04\)\,0_16px_40px_-16px_rgba\(16\,24\,40\,0\.24\)\]{--tw-shadow:0 1px 2px var(--tw-shadow-color,#1018280a), 0 16px 40px -16px var(--tw-shadow-color,#1018283d);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_2px_rgba\(16\,24\,40\,0\.06\)\]{--tw-shadow:0 1px 2px var(--tw-shadow-color,#1018280f);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_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)}.shadow-\[0_10px_30px_-16px_rgba\(16\,24\,40\,0\.32\)\]{--tw-shadow:0 10px 30px -16px var(--tw-shadow-color,#10182852);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring,.ring-1{--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)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-4{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + 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)}.ring-background{--tw-ring-color:var(--color-background,oklch(99% 0 0))}.ring-foreground{--tw-ring-color:var(--color-foreground,oklch(15% 0 0))}.ring-primary\/15{--tw-ring-color:#934c3a26}@supports (color:color-mix(in lab, red, red)){.ring-primary\/15{--tw-ring-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 15%, transparent)}}.ring-primary\/40{--tw-ring-color:#934c3a66}@supports (color:color-mix(in lab, red, red)){.ring-primary\/40{--tw-ring-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 40%, transparent)}}.ring-offset-2{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.ring-offset-background{--tw-ring-offset-color:var(--color-background,oklch(99% 0 0))}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.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-\[width\]{transition-property:width;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-500{--tw-duration:.5s;transition-duration:.5s}.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)}.\[transition-timing-function\:cubic-bezier\(0\.16\,1\,0\.3\,1\)\]{transition-timing-function:cubic-bezier(.16,1,.3,1)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.\[cimplify\:checkout\]{cimplify:checkout}.ring-inset{--tw-ring-inset:inset}@media (hover:hover){.group-hover\:flex:is(:where(.group):hover *){display:flex}.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:scale-\[1\.04\]:is(:where(.group):hover *){scale:1.04}.group-hover\:text-foreground:is(:where(.group):hover *){color:var(--color-foreground,oklch(15% 0 0))}.group-hover\:text-primary:is(:where(.group):hover *){color:var(--color-primary,oklch(50% .1 35))}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.placeholder\:text-muted-foreground::placeholder{color:var(--color-muted-foreground,oklch(50% 0 0))}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.focus-within\:border-foreground:focus-within{border-color:var(--color-foreground,oklch(15% 0 0))}.focus-within\:bg-background:focus-within{background-color:var(--color-background,oklch(99% 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\:-translate-y-px:hover{--tw-translate-y:-1px;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-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.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\:scale-\[1\.08\]:hover{scale:1.08}.hover\:border-destructive:hover{border-color:var(--color-destructive,oklch(50% .2 25))}.hover\:border-foreground:hover{border-color:var(--color-foreground,oklch(15% 0 0))}.hover\:border-foreground\/30:hover{border-color:#0b0b0b4d}@supports (color:color-mix(in lab, red, red)){.hover\:border-foreground\/30:hover{border-color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 30%, transparent)}}.hover\:border-foreground\/40:hover{border-color:#0b0b0b66}@supports (color:color-mix(in lab, red, red)){.hover\:border-foreground\/40:hover{border-color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 40%, transparent)}}.hover\:border-primary:hover{border-color:var(--color-primary,oklch(50% .1 35))}.hover\:border-primary\/20:hover{border-color:#934c3a33}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/20:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 20%, transparent)}}.hover\:border-primary\/40:hover{border-color:#934c3a66}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/40:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 40%, transparent)}}.hover\:border-primary\/50:hover{border-color:#934c3a80}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/50:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 50%, transparent)}}.hover\:\!bg-foreground:hover{background-color:var(--color-foreground,oklch(15% 0 0))!important}.hover\:bg-background:hover{background-color:var(--color-background,oklch(99% 0 0))}.hover\:bg-destructive\/10:hover{background-color:#bb061e1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-destructive\/10:hover{background-color:color-mix(in oklab, var(--color-destructive,oklch(50% .2 25)) 10%, transparent)}}.hover\:bg-foreground\/90:hover{background-color:#0b0b0be6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-foreground\/90:hover{background-color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 90%, transparent)}}.hover\:bg-muted:hover{background-color:var(--color-muted,oklch(95% 0 0))}.hover\:bg-muted\/40:hover{background-color:#eee6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/40:hover{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 40%, transparent)}}.hover\:bg-muted\/50:hover{background-color:#eeeeee80}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/50:hover{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 50%, transparent)}}.hover\:bg-muted\/70:hover{background-color:#eeeeeeb3}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/70:hover{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 70%, transparent)}}.hover\:bg-muted\/80:hover{background-color:#eeec}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/80:hover{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 80%, transparent)}}.hover\:bg-primary\/90:hover{background-color:#934c3ae6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 90%, transparent)}}.hover\:\!text-background:hover{color:var(--color-background,oklch(99% 0 0))!important}.hover\:text-background:hover{color:var(--color-background,oklch(99% 0 0))}.hover\:text-destructive:hover{color:var(--color-destructive,oklch(50% .2 25))}.hover\:text-foreground:hover{color:var(--color-foreground,oklch(15% 0 0))}.hover\:text-primary:hover{color:var(--color-primary,oklch(50% .1 35))}.hover\:text-primary\/80:hover{color:#934c3acc}@supports (color:color-mix(in lab, red, red)){.hover\:text-primary\/80:hover{color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 80%, transparent)}}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-\[0_2px_6px_rgba\(0\,0\,0\,0\.04\)\,0_12px_40px_rgba\(0\,0\,0\,0\.10\)\]:hover{--tw-shadow:0 2px 6px var(--tw-shadow-color,#0000000a), 0 12px 40px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.hover\:brightness-105:hover{--tw-brightness:brightness(105%);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,)}}.focus\:border-primary:focus{border-color:var(--color-primary,oklch(50% .1 35))}.focus\:border-primary\/30:focus{border-color:#934c3a4d}@supports (color:color-mix(in lab, red, red)){.focus\:border-primary\/30:focus{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 30%, transparent)}}.focus\:bg-foreground:focus{background-color:var(--color-foreground,oklch(15% 0 0))}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-primary\/10:focus{--tw-ring-color:#934c3a1a}@supports (color:color-mix(in lab, red, red)){.focus\:ring-primary\/10:focus{--tw-ring-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 10%, transparent)}}.focus\:ring-primary\/20:focus{--tw-ring-color:#934c3a33}@supports (color:color-mix(in lab, red, red)){.focus\:ring-primary\/20:focus{--tw-ring-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 20%, transparent)}}.focus\:ring-ring:focus{--tw-ring-color:var(--color-ring,oklch(50% .1 35))}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-primary\/40:focus-visible{--tw-ring-color:#934c3a66}@supports (color:color-mix(in lab, red, red)){.focus-visible\:ring-primary\/40:focus-visible{--tw-ring-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 40%, transparent)}}.focus-visible\:ring-ring:focus-visible{--tw-ring-color:var(--color-ring,oklch(50% .1 35))}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.active\:scale-\[0\.99\]:active{scale:.99}.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}.aria-selected\:bg-foreground[aria-selected=true]{background-color:var(--color-foreground,oklch(15% 0 0))}.aria-selected\:text-background[aria-selected=true]{color:var(--color-background,oklch(99% 0 0))}@media (hover:hover){.aria-selected\:hover\:bg-foreground\/90[aria-selected=true]:hover{background-color:#0b0b0be6}@supports (color:color-mix(in lab, red, red)){.aria-selected\:hover\:bg-foreground\/90[aria-selected=true]:hover{background-color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 90%, transparent)}}}.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)}}.data-\[fully-booked\]\:cursor-not-allowed[data-fully-booked]{cursor:not-allowed}.data-\[fully-booked\]\:opacity-40[data-fully-booked]{opacity:.4}.data-\[selected\]\:border-foreground[data-selected]{border-color:var(--color-foreground,oklch(15% 0 0))}.data-\[selected\]\:bg-foreground[data-selected]{background-color:var(--color-foreground,oklch(15% 0 0))}.data-\[selected\]\:text-background[data-selected]{color:var(--color-background,oklch(99% 0 0))}.data-\[unavailable\]\:cursor-not-allowed[data-unavailable]{cursor:not-allowed}.data-\[unavailable\]\:line-through[data-unavailable]{text-decoration-line:line-through}.data-\[unavailable\]\:opacity-40[data-unavailable]{opacity:.4}.\[\&_a\]\:text-primary a{color:var(--color-primary,oklch(50% .1 35))}.\[\&_a\]\:underline a{text-decoration-line:underline}.\[\&_a\]\:underline-offset-2 a{text-underline-offset:2px}.\[\&_b\]\:text-foreground b{color:var(--color-foreground,oklch(15% 0 0))}.\[\&_blockquote\]\:border-l-2 blockquote{border-left-style:var(--tw-border-style);border-left-width:2px}.\[\&_blockquote\]\:border-border blockquote{border-color:var(--color-border,oklch(90% 0 0))}.\[\&_blockquote\]\:italic blockquote{font-style:italic}.\[\&_caption\]\:caption-bottom caption{caption-side:bottom}.\[\&_caption\]\:text-muted-foreground caption{color:var(--color-muted-foreground,oklch(50% 0 0))}.\[\&_code\]\:rounded code{border-radius:var(--radius,.5rem)}.\[\&_code\]\:bg-muted code{background-color:var(--color-muted,oklch(95% 0 0))}.\[\&_code\]\:text-\[0\.85em\] code{font-size:.85em}.\[\&_del\]\:line-through del{text-decoration-line:line-through}.\[\&_details\]\:border details{border-style:var(--tw-border-style);border-width:1px}.\[\&_details\]\:border-border details{border-color:var(--color-border,oklch(90% 0 0))}.\[\&_dt\]\:text-foreground dt{color:var(--color-foreground,oklch(15% 0 0))}.\[\&_em\]\:italic em{font-style:italic}.\[\&_figcaption\]\:text-center figcaption{text-align:center}.\[\&_figcaption\]\:text-muted-foreground figcaption{color:var(--color-muted-foreground,oklch(50% 0 0))}.\[\&_h1\]\:text-foreground h1,.\[\&_h2\]\:text-foreground h2,.\[\&_h3\]\:text-foreground h3,.\[\&_h4\]\:text-foreground h4,.\[\&_h5\]\:text-foreground h5,.\[\&_h6\]\:text-foreground h6{color:var(--color-foreground,oklch(15% 0 0))}.\[\&_hr\]\:border-border hr{border-color:var(--color-border,oklch(90% 0 0))}.\[\&_i\]\:italic i{font-style:italic}.\[\&_iframe\]\:w-full iframe{width:100%}.\[\&_img\]\:h-auto img{height:auto}.\[\&_img\]\:max-w-full img{max-width:100%}.\[\&_ins\]\:underline ins{text-decoration-line:underline}.\[\&_mark\]\:rounded mark{border-radius:var(--radius,.5rem)}.\[\&_ol\]\:list-decimal ol{list-style-type:decimal}.\[\&_pre\]\:overflow-x-auto pre{overflow-x:auto}.\[\&_pre\]\:bg-muted pre{background-color:var(--color-muted,oklch(95% 0 0))}.\[\&_pre\]\:text-\[0\.85em\] pre{font-size:.85em}.\[\&_pre\>code\]\:bg-transparent pre>code{background-color:#0000}.\[\&_s\]\:line-through s{text-decoration-line:line-through}.\[\&_strong\]\:text-foreground strong{color:var(--color-foreground,oklch(15% 0 0))}.\[\&_sub\]\:align-sub sub{vertical-align:sub}.\[\&_sub\]\:text-\[0\.75em\] sub{font-size:.75em}.\[\&_summary\]\:cursor-pointer summary{cursor:pointer}.\[\&_summary\]\:text-foreground summary{color:var(--color-foreground,oklch(15% 0 0))}.\[\&_sup\]\:align-super sup{vertical-align:super}.\[\&_sup\]\:text-\[0\.75em\] sup{font-size:.75em}.\[\&_table\]\:w-full table{width:100%}.\[\&_table\]\:border-collapse table{border-collapse:collapse}.\[\&_td\]\:border td{border-style:var(--tw-border-style);border-width:1px}.\[\&_td\]\:border-border td{border-color:var(--color-border,oklch(90% 0 0))}.\[\&_td\]\:align-top td{vertical-align:top}.\[\&_td\:last-child\]\:text-right td:last-child{text-align:right}.\[\&_td\:last-child\]\:tabular-nums td:last-child{--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,)}.\[\&_th\]\:border th{border-style:var(--tw-border-style);border-width:1px}.\[\&_th\]\:border-border th{border-color:var(--color-border,oklch(90% 0 0))}.\[\&_th\]\:bg-muted th{background-color:var(--color-muted,oklch(95% 0 0))}.\[\&_th\]\:text-left th{text-align:left}.\[\&_th\]\:text-\[10px\] th{font-size:10px}.\[\&_th\]\:text-foreground th{color:var(--color-foreground,oklch(15% 0 0))}.\[\&_th\]\:uppercase th{text-transform:uppercase}.\[\&_u\]\:underline u{text-decoration-line:underline}.\[\&_ul\]\:list-disc ul{list-style-type:disc}.\[\&_ul_ul\]\:list-\[circle\] ul ul{list-style-type:circle}.\[\&\:\:-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}.\[\&\:not\(\:first-child\)\]\:border-t:not(:first-child){border-top-style:var(--tw-border-style);border-top-width:1px}.\[\&\:not\(\:first-child\)\]\:border-border\/50:not(:first-child){border-color:#dedede80}@supports (color:color-mix(in lab, red, red)){.\[\&\:not\(\:first-child\)\]\:border-border\/50:not(:first-child){border-color:color-mix(in oklab, var(--color-border,oklch(90% 0 0)) 50%, transparent)}}[open]>.\[\[open\]\>\&\]\:rotate-180{rotate:180deg}.cimplify-cols-2,.cimplify-cols-3,.cimplify-cols-4,.cimplify-media{gap:1.5rem;display:grid}.cimplify-media{align-items:center}.cimplify-media--reverse>:first-child{order:2}@media (min-width:640px){.cimplify-cols-2,.cimplify-cols-3,.cimplify-cols-4,.cimplify-media{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.cimplify-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.cimplify-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.cimplify-cols-2>*>:first-child,.cimplify-cols-3>*>:first-child,.cimplify-cols-4>*>:first-child,.cimplify-media>*>:first-child,.cimplify-media>img,.cimplify-cols-2>img,.cimplify-cols-3>img,.cimplify-cols-4>img{margin-top:0}.cimplify-card{border:1px solid var(--color-border);border-radius:var(--radius);padding:1rem}.cimplify-banner{border-radius:var(--radius);position:relative;overflow:hidden}.cimplify-banner>img{width:100%;height:auto;margin:0;display:block}.cimplify-banner__content{flex-direction:column;justify-content:center;gap:.5rem;padding:1.5rem 2rem;display:flex;position:absolute;inset:0}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@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-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-tracking:initial;--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-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.top-1\/2{top:50%}.top-\[18px\]{top:18px}.top-\[calc\(100\%\+8px\)\]{top:calc(100% + 8px)}.top-full{top:100%}.top-px{top:1px}.-right-px{right:-1px}.-bottom-px{bottom:-1px}.-left-\[9px\]{left:-9px}.left-\[14px\]{left:14px}.isolate{isolation:isolate}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-50{z-index:50}.z-\[1\]{z-index:1}.z-\[2\]{z-index:2}.z-\[200\]{z-index:200}.z-\[9999\]{z-index:9999}.container{width:100%}.m-auto{margin:auto}.mx-auto{margin-inline:auto}.-mt-px{margin-top:-1px}.mt-auto{margin-top:auto}.mt-px{margin-top:1px}.mr-auto{margin-right:auto}.-mb-px{margin-bottom:-1px}.ml-\[1px\]{margin-left:1px}.ml-\[9px\]{margin-left:9px}.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}.contents{display:contents}.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}.size-\[7px\]{width:7px;height:7px}.size-\[18px\]{width:18px;height:18px}.size-\[22px\]{width:22px;height:22px}.size-\[26px\]{width:26px;height:26px}.size-\[38px\]{width:38px;height:38px}.size-\[46px\]{width:46px;height:46px}.size-full{width:100%;height:100%}.h-\[6px\]{height:6px}.h-\[7px\]{height:7px}.h-\[11px\]{height:11px}.h-\[18px\]{height:18px}.h-\[72px\]{height:72px}.h-\[216px\]{height:216px}.h-\[min\(600px\,calc\(100vh-6rem\)\)\]{height:min(600px,100vh - 6rem)}.h-fit{height:fit-content}.h-full{height:100%}.h-px{height:1px}.max-h-\[85vh\]{max-height:85vh}.max-h-\[100px\]{max-height:100px}.min-h-\[20px\]{min-height:20px}.min-h-\[480px\]{min-height:480px}.min-h-\[720px\]{min-height:720px}.min-h-screen{min-height:100vh}.w-1\/2{width:50%}.w-2\/5{width:40%}.w-3\/4{width:75%}.w-3\/5{width:60%}.w-4\/5{width:80%}.w-\[3px\]{width:3px}.w-\[6px\]{width:6px}.w-\[7px\]{width:7px}.w-\[11px\]{width:11px}.w-\[18px\]{width:18px}.w-\[72px\]{width:72px}.w-\[280px\]{width:280px}.w-\[400px\]{width:400px}.w-fit{width:fit-content}.w-full{width:100%}.max-w-\[85\%\]{max-width:85%}.max-w-\[120px\]{max-width:120px}.max-w-\[260px\]{max-width:260px}.max-w-\[280px\]{max-width:280px}.max-w-\[300px\]{max-width:300px}.max-w-\[380px\]{max-width:380px}.max-w-\[420px\]{max-width:420px}.max-w-\[460px\]{max-width:460px}.max-w-\[calc\(100vw-1\.5rem\)\]{max-width:calc(100vw - 1.5rem)}.max-w-\[calc\(100vw-3rem\)\]{max-width:calc(100vw - 3rem)}.max-w-full{max-width:100%}.flex-1{flex:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.grow{flex-grow:1}.border-collapse{border-collapse:collapse}.origin-bottom-left{transform-origin:0 100%}.origin-bottom-right{transform-origin:100% 100%}.translate-x-full{--tw-translate-x:100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-90{rotate:90deg}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-crosshair{cursor:crosshair}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.resize{resize:both}.resize-none{resize:none}.list-none{list-style-type:none}.\[appearance\:textfield\]{appearance:textfield}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.grid-cols-\[16px_1fr_auto\]{grid-template-columns:16px 1fr auto}.grid-cols-\[22px_1fr_auto_auto\]{grid-template-columns:22px 1fr auto auto}.grid-cols-\[40px_1fr\]{grid-template-columns:40px 1fr}.grid-cols-\[40px_1fr_auto\]{grid-template-columns:40px 1fr auto}.grid-cols-\[44px_1fr_60px\]{grid-template-columns:44px 1fr 60px}.grid-cols-\[46px_minmax\(0\,1fr\)_auto\]{grid-template-columns:46px minmax(0,1fr) auto}.grid-cols-\[48px_1fr_auto_auto\]{grid-template-columns:48px 1fr auto auto}.grid-cols-\[56px_1fr_auto_auto\]{grid-template-columns:56px 1fr auto auto}.grid-cols-\[84px_minmax\(0\,1fr\)\]{grid-template-columns:84px minmax(0,1fr)}.grid-cols-\[130px_1fr_auto\]{grid-template-columns:130px 1fr auto}.grid-cols-\[auto_1fr_auto\]{grid-template-columns:auto 1fr auto}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-\[7px\]{gap:7px}.gap-px{gap:1px}.gap-x-\[13px\]{column-gap:13px}: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))}:where(.divide-border\/60>:not(:last-child)){border-color:#dedede99}@supports (color:color-mix(in lab, red, red)){:where(.divide-border\/60>:not(:last-child)){border-color:color-mix(in oklab, var(--color-border,oklch(90% 0 0)) 60%, transparent)}}.self-stretch{align-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:var(--radius,.5rem)}.rounded-\[8px\]{border-radius:8px}.rounded-\[9px\]{border-radius:9px}.rounded-\[10px\]{border-radius:10px}.rounded-\[12px\]{border-radius:12px}.rounded-\[14px\]{border-radius:14px}.rounded-\[16px\]{border-radius:16px}.rounded-\[inherit\]{border-radius:inherit}.rounded-\[var\(--radius\,12px\)\]{border-radius:var(--radius,12px)}.rounded-full{border-radius:3.40282e38px}.rounded-tl{border-top-left-radius:var(--radius,.5rem)}.rounded-tr{border-top-right-radius:var(--radius,.5rem)}.rounded-br{border-bottom-right-radius:var(--radius,.5rem)}.rounded-bl{border-bottom-left-radius:var(--radius,.5rem)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-\[1\.5px\]{border-style:var(--tw-border-style);border-width:1.5px}.border-\[2\.5px\]{border-style:var(--tw-border-style);border-width:2.5px}.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-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-none{--tw-border-style:none;border-style:none}.border-\[color-mix\(in_oklch\,var\(--color-primary\)_18\%\,transparent\)\]{border-color:#934c3a2e}@supports (color:color-mix(in lab, red, red)){.border-\[color-mix\(in_oklch\,var\(--color-primary\)_18\%\,transparent\)\]{border-color:color-mix(in oklch,var(--color-primary) 18%,transparent)}}.border-border{border-color:var(--color-border,oklch(90% 0 0))}.border-border\/40{border-color:#dedede66}@supports (color:color-mix(in lab, red, red)){.border-border\/40{border-color:color-mix(in oklab, var(--color-border,oklch(90% 0 0)) 40%, transparent)}}.border-border\/60{border-color:#dedede99}@supports (color:color-mix(in lab, red, red)){.border-border\/60{border-color:color-mix(in oklab, var(--color-border,oklch(90% 0 0)) 60%, transparent)}}.border-destructive\/20{border-color:#bb061e33}@supports (color:color-mix(in lab, red, red)){.border-destructive\/20{border-color:color-mix(in oklab, var(--color-destructive,oklch(50% .2 25)) 20%, transparent)}}.border-foreground{border-color:var(--color-foreground,oklch(15% 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-primary\/20{border-color:#934c3a33}@supports (color:color-mix(in lab, red, red)){.border-primary\/20{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 20%, transparent)}}.border-transparent{border-color:#0000}.bg-background{background-color:var(--color-background,oklch(99% 0 0))}.bg-background\/10{background-color:#fcfcfc1a}@supports (color:color-mix(in lab, red, red)){.bg-background\/10{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 10%, transparent)}}.bg-background\/15{background-color:#fcfcfc26}@supports (color:color-mix(in lab, red, red)){.bg-background\/15{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 15%, transparent)}}.bg-background\/40{background-color:#fcfcfc66}@supports (color:color-mix(in lab, red, red)){.bg-background\/40{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 40%, transparent)}}.bg-background\/50{background-color:#fcfcfc80}@supports (color:color-mix(in lab, red, red)){.bg-background\/50{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 50%, transparent)}}.bg-background\/90{background-color:#fcfcfce6}@supports (color:color-mix(in lab, red, red)){.bg-background\/90{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 90%, transparent)}}.bg-background\/92{background-color:#fcfcfceb}@supports (color:color-mix(in lab, red, red)){.bg-background\/92{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 92%, transparent)}}.bg-border{background-color:var(--color-border,oklch(90% 0 0))}.bg-border\/60{background-color:#dedede99}@supports (color:color-mix(in lab, red, red)){.bg-border\/60{background-color:color-mix(in oklab, var(--color-border,oklch(90% 0 0)) 60%, transparent)}}.bg-current{background-color:currentColor}.bg-destructive{background-color:var(--color-destructive,oklch(50% .2 25))}.bg-destructive\/10{background-color:#bb061e1a}@supports (color:color-mix(in lab, red, red)){.bg-destructive\/10{background-color:color-mix(in oklab, var(--color-destructive,oklch(50% .2 25)) 10%, transparent)}}.bg-foreground{background-color:var(--color-foreground,oklch(15% 0 0))}.bg-foreground\/40{background-color:#0b0b0b66}@supports (color:color-mix(in lab, red, red)){.bg-foreground\/40{background-color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 40%, transparent)}}.bg-muted{background-color:var(--color-muted,oklch(95% 0 0))}.bg-muted-foreground\/40{background-color:#63636366}@supports (color:color-mix(in lab, red, red)){.bg-muted-foreground\/40{background-color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 40%, transparent)}}.bg-muted\/30{background-color:#eeeeee4d}@supports (color:color-mix(in lab, red, red)){.bg-muted\/30{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 30%, transparent)}}.bg-muted\/40{background-color:#eee6}@supports (color:color-mix(in lab, red, red)){.bg-muted\/40{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 40%, transparent)}}.bg-muted\/50{background-color:#eeeeee80}@supports (color:color-mix(in lab, red, red)){.bg-muted\/50{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 50%, transparent)}}.bg-muted\/60{background-color:#eee9}@supports (color:color-mix(in lab, red, red)){.bg-muted\/60{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 60%, transparent)}}.bg-muted\/70{background-color:#eeeeeeb3}@supports (color:color-mix(in lab, red, red)){.bg-muted\/70{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 70%, transparent)}}.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-primary\/\[0\.12\]{background-color:#934c3a1f}@supports (color:color-mix(in lab, red, red)){.bg-primary\/\[0\.12\]{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 12%, 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))}.bg-\[linear-gradient\(135deg\,color-mix\(in_oklch\,var\(--color-primary\)_8\%\,transparent\)\,var\(--color-background\)\)\]{background-image:linear-gradient(135deg,#934c3a14,var(--color-background))}@supports (color:color-mix(in lab, red, red)){.bg-\[linear-gradient\(135deg\,color-mix\(in_oklch\,var\(--color-primary\)_8\%\,transparent\)\,var\(--color-background\)\)\]{background-image:linear-gradient(135deg,color-mix(in oklch,var(--color-primary) 8%,transparent),var(--color-background))}}.bg-\[linear-gradient\(135deg\,oklch\(0\.92_0\.05_80\)\,oklch\(0\.84_0\.10_60\)\)\]{background-image:linear-gradient(135deg,oklch(92% .05 80),oklch(84% .1 60))}.bg-\[radial-gradient\(900px_480px_at_50\%_0\%\,color-mix\(in_oklch\,var\(--color-primary\)_8\%\,transparent\)\,transparent_70\%\)\,var\(--color-background\)\]{background-image:radial-gradient(900px 480px at 50% 0%,#934c3a14,transparent 70%),var(--color-background)}@supports (color:color-mix(in lab, red, red)){.bg-\[radial-gradient\(900px_480px_at_50\%_0\%\,color-mix\(in_oklch\,var\(--color-primary\)_8\%\,transparent\)\,transparent_70\%\)\,var\(--color-background\)\]{background-image:radial-gradient(900px 480px at 50% 0%,color-mix(in oklch,var(--color-primary) 8%,transparent),transparent 70%),var(--color-background)}}.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}.px-\[13px\]{padding-inline:13px}.px-\[18px\]{padding-inline:18px}.px-\[22px\]{padding-inline:22px}.py-\[7px\]{padding-block:7px}.py-\[10px\]{padding-block:10px}.py-\[15px\]{padding-block:15px}.py-\[18px\]{padding-block:18px}.pb-\[13px\]{padding-bottom:13px}.pb-\[22px\]{padding-bottom:22px}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.\[font-family\:inherit\],.font-\[inherit\]{font-family:inherit}.text-\[9px\]{font-size:9px}.text-\[10\.5px\]{font-size:10.5px}.text-\[10px\]{font-size:10px}.text-\[11\.5px\]{font-size:11.5px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[13\.5px\]{font-size:13.5px}.text-\[13px\]{font-size:13px}.text-\[14\.5px\]{font-size:14.5px}.text-\[14px\]{font-size:14px}.text-\[15px\]{font-size:15px}.text-\[16px\]{font-size:16px}.text-\[17px\]{font-size:17px}.text-\[18px\]{font-size:18px}.text-\[22px\]{font-size:22px}.text-\[24px\]{font-size:24px}.text-\[28px\]{font-size:28px}.text-\[30px\]{font-size:30px}.text-\[clamp\(26px\,2\.8vw\,34px\)\]{font-size:clamp(26px,2.8vw,34px)}.text-\[clamp\(28px\,3vw\,40px\)\]{font-size:clamp(28px,3vw,40px)}.leading-\[1\.1\]{--tw-leading:1.1;line-height:1.1}.leading-\[1\.2\]{--tw-leading:1.2;line-height:1.2}.leading-\[1\.4\]{--tw-leading:1.4;line-height:1.4}.leading-\[1\.05\]{--tw-leading:1.05;line-height:1.05}.leading-\[1\.5\]{--tw-leading:1.5;line-height:1.5}.leading-\[1\.15\]{--tw-leading:1.15;line-height:1.15}.leading-\[1\.55\]{--tw-leading:1.55;line-height:1.55}.leading-none{--tw-leading:1;line-height:1}.tracking-\[-0\.01em\]{--tw-tracking:-.01em;letter-spacing:-.01em}.tracking-\[-0\.02em\]{--tw-tracking:-.02em;letter-spacing:-.02em}.tracking-\[-0\.015em\]{--tw-tracking:-.015em;letter-spacing:-.015em}.tracking-\[-0\.025em\]{--tw-tracking:-.025em;letter-spacing:-.025em}.tracking-\[0\.1em\]{--tw-tracking:.1em;letter-spacing:.1em}.tracking-\[0\.2em\]{--tw-tracking:.2em;letter-spacing:.2em}.tracking-\[0\.06em\]{--tw-tracking:.06em;letter-spacing:.06em}.tracking-\[0\.08em\]{--tw-tracking:.08em;letter-spacing:.08em}.tracking-\[0\.12em\]{--tw-tracking:.12em;letter-spacing:.12em}.tracking-\[0\.16em\]{--tw-tracking:.16em;letter-spacing:.16em}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[inherit\]{color:inherit}.text-\[var\(--color-primary-foreground\,\#fff\)\]{color:var(--color-primary-foreground,#fff)}.text-background{color:var(--color-background,oklch(99% 0 0))}.text-background\/60{color:#fcfcfc99}@supports (color:color-mix(in lab, red, red)){.text-background\/60{color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 60%, transparent)}}.text-background\/70{color:#fcfcfcb3}@supports (color:color-mix(in lab, red, red)){.text-background\/70{color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 70%, transparent)}}.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-foreground\/35{color:#0b0b0b59}@supports (color:color-mix(in lab, red, red)){.text-foreground\/35{color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 35%, transparent)}}.text-foreground\/45{color:#0b0b0b73}@supports (color:color-mix(in lab, red, red)){.text-foreground\/45{color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 45%, transparent)}}.text-foreground\/55{color:#0b0b0b8c}@supports (color:color-mix(in lab, red, red)){.text-foreground\/55{color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 55%, transparent)}}.text-foreground\/60{color:#0b0b0b99}@supports (color:color-mix(in lab, red, red)){.text-foreground\/60{color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 60%, transparent)}}.text-foreground\/70{color:#0b0b0bb3}@supports (color:color-mix(in lab, red, red)){.text-foreground\/70{color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 70%, transparent)}}.text-foreground\/80{color:#0b0b0bcc}@supports (color:color-mix(in lab, red, red)){.text-foreground\/80{color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 80%, transparent)}}.text-foreground\/90{color:#0b0b0be6}@supports (color:color-mix(in lab, red, red)){.text-foreground\/90{color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 90%, transparent)}}.text-muted-foreground{color:var(--color-muted-foreground,oklch(50% 0 0))}.text-muted-foreground\/30{color:#6363634d}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/30{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 30%, transparent)}}.text-muted-foreground\/40{color:#63636366}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/40{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 40%, transparent)}}.text-muted-foreground\/50{color:#63636380}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/50{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 50%, transparent)}}.text-muted-foreground\/60{color:#63636399}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/60{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 60%, transparent)}}.text-muted-foreground\/70{color:#636363b3}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/70{color:color-mix(in oklab, var(--color-muted-foreground,oklch(50% 0 0)) 70%, transparent)}}.text-primary{color:var(--color-primary,oklch(50% .1 35))}.text-primary-foreground{color:var(--color-primary-foreground,oklch(99% 0 0))}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.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}.underline-offset-2{text-underline-offset:2px}.accent-foreground{accent-color:var(--color-foreground,oklch(15% 0 0))}.accent-primary{accent-color:var(--color-primary,oklch(50% .1 35))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.opacity-85{opacity:.85}.opacity-100{opacity:1}.shadow-\[0_0_0_1px_var\(--color-border\)\]{--tw-shadow:0 0 0 1px var(--tw-shadow-color,var(--color-border));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_0_0_2px_var\(--color-card\)\]{--tw-shadow:0 0 0 2px var(--tw-shadow-color,var(--color-card));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_0_0_6px_color-mix\(in_oklch\,var\(--color-primary\)_18\%\,transparent\)\]{--tw-shadow:0 0 0 6px var(--tw-shadow-color,#934c3a2e)}@supports (color:color-mix(in lab, red, red)){.shadow-\[0_0_0_6px_color-mix\(in_oklch\,var\(--color-primary\)_18\%\,transparent\)\]{--tw-shadow:0 0 0 6px var(--tw-shadow-color,color-mix(in oklch,var(--color-primary) 18%,transparent))}}.shadow-\[0_0_0_6px_color-mix\(in_oklch\,var\(--color-primary\)_18\%\,transparent\)\]{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_0_rgba\(0\,0\,0\,0\.04\)\,0_8px_24px_rgba\(0\,0\,0\,0\.12\)\]{--tw-shadow:0 1px 0 var(--tw-shadow-color,#0000000a), 0 8px 24px var(--tw-shadow-color,#0000001f);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_0_rgba\(0\,0\,0\,0\.04\)\,0_16px_48px_rgba\(0\,0\,0\,0\.08\)\]{--tw-shadow:0 1px 0 var(--tw-shadow-color,#0000000a), 0 16px 48px 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)}.shadow-\[0_1px_2px_rgba\(16\,24\,40\,0\.04\)\,0_16px_40px_-16px_rgba\(16\,24\,40\,0\.24\)\]{--tw-shadow:0 1px 2px var(--tw-shadow-color,#1018280a), 0 16px 40px -16px var(--tw-shadow-color,#1018283d);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_2px_rgba\(16\,24\,40\,0\.06\)\]{--tw-shadow:0 1px 2px var(--tw-shadow-color,#1018280f);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_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)}.shadow-\[0_10px_30px_-16px_rgba\(16\,24\,40\,0\.32\)\]{--tw-shadow:0 10px 30px -16px var(--tw-shadow-color,#10182852);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring,.ring-1{--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)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-4{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + 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)}.ring-background{--tw-ring-color:var(--color-background,oklch(99% 0 0))}.ring-foreground{--tw-ring-color:var(--color-foreground,oklch(15% 0 0))}.ring-primary\/15{--tw-ring-color:#934c3a26}@supports (color:color-mix(in lab, red, red)){.ring-primary\/15{--tw-ring-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 15%, transparent)}}.ring-primary\/40{--tw-ring-color:#934c3a66}@supports (color:color-mix(in lab, red, red)){.ring-primary\/40{--tw-ring-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 40%, transparent)}}.ring-offset-2{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.ring-offset-background{--tw-ring-offset-color:var(--color-background,oklch(99% 0 0))}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.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-\[width\]{transition-property:width;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-500{--tw-duration:.5s;transition-duration:.5s}.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)}.\[transition-timing-function\:cubic-bezier\(0\.16\,1\,0\.3\,1\)\]{transition-timing-function:cubic-bezier(.16,1,.3,1)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.\[cimplify\:checkout\]{cimplify:checkout}.ring-inset{--tw-ring-inset:inset}@media (hover:hover){.group-hover\:flex:is(:where(.group):hover *){display:flex}.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:scale-\[1\.04\]:is(:where(.group):hover *){scale:1.04}.group-hover\:text-foreground:is(:where(.group):hover *){color:var(--color-foreground,oklch(15% 0 0))}.group-hover\:text-primary:is(:where(.group):hover *){color:var(--color-primary,oklch(50% .1 35))}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.placeholder\:text-muted-foreground::placeholder{color:var(--color-muted-foreground,oklch(50% 0 0))}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.focus-within\:border-foreground:focus-within{border-color:var(--color-foreground,oklch(15% 0 0))}.focus-within\:bg-background:focus-within{background-color:var(--color-background,oklch(99% 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\:-translate-y-px:hover{--tw-translate-y:-1px;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-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.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\:scale-\[1\.08\]:hover{scale:1.08}.hover\:border-destructive:hover{border-color:var(--color-destructive,oklch(50% .2 25))}.hover\:border-foreground:hover{border-color:var(--color-foreground,oklch(15% 0 0))}.hover\:border-foreground\/30:hover{border-color:#0b0b0b4d}@supports (color:color-mix(in lab, red, red)){.hover\:border-foreground\/30:hover{border-color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 30%, transparent)}}.hover\:border-foreground\/40:hover{border-color:#0b0b0b66}@supports (color:color-mix(in lab, red, red)){.hover\:border-foreground\/40:hover{border-color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 40%, transparent)}}.hover\:border-primary:hover{border-color:var(--color-primary,oklch(50% .1 35))}.hover\:border-primary\/20:hover{border-color:#934c3a33}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/20:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 20%, transparent)}}.hover\:border-primary\/40:hover{border-color:#934c3a66}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/40:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 40%, transparent)}}.hover\:border-primary\/50:hover{border-color:#934c3a80}@supports (color:color-mix(in lab, red, red)){.hover\:border-primary\/50:hover{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 50%, transparent)}}.hover\:\!bg-foreground:hover{background-color:var(--color-foreground,oklch(15% 0 0))!important}.hover\:bg-background:hover{background-color:var(--color-background,oklch(99% 0 0))}.hover\:bg-background\/20:hover{background-color:#fcfcfc33}@supports (color:color-mix(in lab, red, red)){.hover\:bg-background\/20:hover{background-color:color-mix(in oklab, var(--color-background,oklch(99% 0 0)) 20%, 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-foreground\/90:hover{background-color:#0b0b0be6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-foreground\/90:hover{background-color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 90%, transparent)}}.hover\:bg-muted:hover{background-color:var(--color-muted,oklch(95% 0 0))}.hover\:bg-muted\/40:hover{background-color:#eee6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/40:hover{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 40%, transparent)}}.hover\:bg-muted\/50:hover{background-color:#eeeeee80}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/50:hover{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 50%, transparent)}}.hover\:bg-muted\/70:hover{background-color:#eeeeeeb3}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/70:hover{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 70%, transparent)}}.hover\:bg-muted\/80:hover{background-color:#eeec}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/80:hover{background-color:color-mix(in oklab, var(--color-muted,oklch(95% 0 0)) 80%, transparent)}}.hover\:bg-primary\/90:hover{background-color:#934c3ae6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 90%, transparent)}}.hover\:\!text-background:hover{color:var(--color-background,oklch(99% 0 0))!important}.hover\:text-background:hover{color:var(--color-background,oklch(99% 0 0))}.hover\:text-destructive:hover{color:var(--color-destructive,oklch(50% .2 25))}.hover\:text-foreground:hover{color:var(--color-foreground,oklch(15% 0 0))}.hover\:text-primary:hover{color:var(--color-primary,oklch(50% .1 35))}.hover\:text-primary\/80:hover{color:#934c3acc}@supports (color:color-mix(in lab, red, red)){.hover\:text-primary\/80:hover{color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 80%, transparent)}}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-\[0_2px_6px_rgba\(0\,0\,0\,0\.04\)\,0_12px_40px_rgba\(0\,0\,0\,0\.10\)\]:hover{--tw-shadow:0 2px 6px var(--tw-shadow-color,#0000000a), 0 12px 40px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.hover\:brightness-105:hover{--tw-brightness:brightness(105%);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,)}}.focus\:border-foreground:focus{border-color:var(--color-foreground,oklch(15% 0 0))}.focus\:border-primary:focus{border-color:var(--color-primary,oklch(50% .1 35))}.focus\:border-primary\/30:focus{border-color:#934c3a4d}@supports (color:color-mix(in lab, red, red)){.focus\:border-primary\/30:focus{border-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 30%, transparent)}}.focus\:bg-foreground:focus{background-color:var(--color-foreground,oklch(15% 0 0))}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-primary\/10:focus{--tw-ring-color:#934c3a1a}@supports (color:color-mix(in lab, red, red)){.focus\:ring-primary\/10:focus{--tw-ring-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 10%, transparent)}}.focus\:ring-primary\/20:focus{--tw-ring-color:#934c3a33}@supports (color:color-mix(in lab, red, red)){.focus\:ring-primary\/20:focus{--tw-ring-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 20%, transparent)}}.focus\:ring-ring:focus{--tw-ring-color:var(--color-ring,oklch(50% .1 35))}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-primary\/40:focus-visible{--tw-ring-color:#934c3a66}@supports (color:color-mix(in lab, red, red)){.focus-visible\:ring-primary\/40:focus-visible{--tw-ring-color:color-mix(in oklab, var(--color-primary,oklch(50% .1 35)) 40%, transparent)}}.focus-visible\:ring-ring:focus-visible{--tw-ring-color:var(--color-ring,oklch(50% .1 35))}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.active\:scale-\[0\.99\]:active{scale:.99}.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}.aria-selected\:bg-foreground[aria-selected=true]{background-color:var(--color-foreground,oklch(15% 0 0))}.aria-selected\:text-background[aria-selected=true]{color:var(--color-background,oklch(99% 0 0))}@media (hover:hover){.aria-selected\:hover\:bg-foreground\/90[aria-selected=true]:hover{background-color:#0b0b0be6}@supports (color:color-mix(in lab, red, red)){.aria-selected\:hover\:bg-foreground\/90[aria-selected=true]:hover{background-color:color-mix(in oklab, var(--color-foreground,oklch(15% 0 0)) 90%, transparent)}}}.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)}}.data-\[fully-booked\]\:cursor-not-allowed[data-fully-booked]{cursor:not-allowed}.data-\[fully-booked\]\:opacity-40[data-fully-booked]{opacity:.4}.data-\[selected\]\:border-foreground[data-selected]{border-color:var(--color-foreground,oklch(15% 0 0))}.data-\[selected\]\:bg-foreground[data-selected]{background-color:var(--color-foreground,oklch(15% 0 0))}.data-\[selected\]\:text-background[data-selected]{color:var(--color-background,oklch(99% 0 0))}.data-\[unavailable\]\:cursor-not-allowed[data-unavailable]{cursor:not-allowed}.data-\[unavailable\]\:line-through[data-unavailable]{text-decoration-line:line-through}.data-\[unavailable\]\:opacity-40[data-unavailable]{opacity:.4}.\[\&_a\]\:text-primary a{color:var(--color-primary,oklch(50% .1 35))}.\[\&_a\]\:underline a{text-decoration-line:underline}.\[\&_a\]\:underline-offset-2 a{text-underline-offset:2px}.\[\&_b\]\:text-foreground b{color:var(--color-foreground,oklch(15% 0 0))}.\[\&_blockquote\]\:border-l-2 blockquote{border-left-style:var(--tw-border-style);border-left-width:2px}.\[\&_blockquote\]\:border-border blockquote{border-color:var(--color-border,oklch(90% 0 0))}.\[\&_blockquote\]\:italic blockquote{font-style:italic}.\[\&_caption\]\:caption-bottom caption{caption-side:bottom}.\[\&_caption\]\:text-muted-foreground caption{color:var(--color-muted-foreground,oklch(50% 0 0))}.\[\&_code\]\:rounded code{border-radius:var(--radius,.5rem)}.\[\&_code\]\:bg-muted code{background-color:var(--color-muted,oklch(95% 0 0))}.\[\&_code\]\:text-\[0\.85em\] code{font-size:.85em}.\[\&_del\]\:line-through del{text-decoration-line:line-through}.\[\&_details\]\:border details{border-style:var(--tw-border-style);border-width:1px}.\[\&_details\]\:border-border details{border-color:var(--color-border,oklch(90% 0 0))}.\[\&_dt\]\:text-foreground dt{color:var(--color-foreground,oklch(15% 0 0))}.\[\&_em\]\:italic em{font-style:italic}.\[\&_figcaption\]\:text-center figcaption{text-align:center}.\[\&_figcaption\]\:text-muted-foreground figcaption{color:var(--color-muted-foreground,oklch(50% 0 0))}.\[\&_h1\]\:text-foreground h1,.\[\&_h2\]\:text-foreground h2,.\[\&_h3\]\:text-foreground h3,.\[\&_h4\]\:text-foreground h4,.\[\&_h5\]\:text-foreground h5,.\[\&_h6\]\:text-foreground h6{color:var(--color-foreground,oklch(15% 0 0))}.\[\&_hr\]\:border-border hr{border-color:var(--color-border,oklch(90% 0 0))}.\[\&_i\]\:italic i{font-style:italic}.\[\&_iframe\]\:w-full iframe{width:100%}.\[\&_img\]\:h-auto img{height:auto}.\[\&_img\]\:max-w-full img{max-width:100%}.\[\&_ins\]\:underline ins{text-decoration-line:underline}.\[\&_mark\]\:rounded mark{border-radius:var(--radius,.5rem)}.\[\&_ol\]\:list-decimal ol{list-style-type:decimal}.\[\&_pre\]\:overflow-x-auto pre{overflow-x:auto}.\[\&_pre\]\:bg-muted pre{background-color:var(--color-muted,oklch(95% 0 0))}.\[\&_pre\]\:text-\[0\.85em\] pre{font-size:.85em}.\[\&_pre\>code\]\:bg-transparent pre>code{background-color:#0000}.\[\&_s\]\:line-through s{text-decoration-line:line-through}.\[\&_strong\]\:text-foreground strong{color:var(--color-foreground,oklch(15% 0 0))}.\[\&_sub\]\:align-sub sub{vertical-align:sub}.\[\&_sub\]\:text-\[0\.75em\] sub{font-size:.75em}.\[\&_summary\]\:cursor-pointer summary{cursor:pointer}.\[\&_summary\]\:text-foreground summary{color:var(--color-foreground,oklch(15% 0 0))}.\[\&_sup\]\:align-super sup{vertical-align:super}.\[\&_sup\]\:text-\[0\.75em\] sup{font-size:.75em}.\[\&_table\]\:w-full table{width:100%}.\[\&_table\]\:border-collapse table{border-collapse:collapse}.\[\&_td\]\:border td{border-style:var(--tw-border-style);border-width:1px}.\[\&_td\]\:border-border td{border-color:var(--color-border,oklch(90% 0 0))}.\[\&_td\]\:align-top td{vertical-align:top}.\[\&_td\:last-child\]\:text-right td:last-child{text-align:right}.\[\&_td\:last-child\]\:tabular-nums td:last-child{--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,)}.\[\&_th\]\:border th{border-style:var(--tw-border-style);border-width:1px}.\[\&_th\]\:border-border th{border-color:var(--color-border,oklch(90% 0 0))}.\[\&_th\]\:bg-muted th{background-color:var(--color-muted,oklch(95% 0 0))}.\[\&_th\]\:text-left th{text-align:left}.\[\&_th\]\:text-\[10px\] th{font-size:10px}.\[\&_th\]\:text-foreground th{color:var(--color-foreground,oklch(15% 0 0))}.\[\&_th\]\:uppercase th{text-transform:uppercase}.\[\&_u\]\:underline u{text-decoration-line:underline}.\[\&_ul\]\:list-disc ul{list-style-type:disc}.\[\&_ul_ul\]\:list-\[circle\] ul ul{list-style-type:circle}.\[\&\:\:-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}.\[\&\:not\(\:first-child\)\]\:border-t:not(:first-child){border-top-style:var(--tw-border-style);border-top-width:1px}.\[\&\:not\(\:first-child\)\]\:border-border\/50:not(:first-child){border-color:#dedede80}@supports (color:color-mix(in lab, red, red)){.\[\&\:not\(\:first-child\)\]\:border-border\/50:not(:first-child){border-color:color-mix(in oklab, var(--color-border,oklch(90% 0 0)) 50%, transparent)}}[open]>.\[\[open\]\>\&\]\:rotate-180{rotate:180deg}.cimplify-cols-2,.cimplify-cols-3,.cimplify-cols-4,.cimplify-media{gap:1.5rem;display:grid}.cimplify-media{align-items:center}.cimplify-media--reverse>:first-child{order:2}@media (min-width:640px){.cimplify-cols-2,.cimplify-cols-3,.cimplify-cols-4,.cimplify-media{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.cimplify-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.cimplify-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.cimplify-cols-2>*>:first-child,.cimplify-cols-3>*>:first-child,.cimplify-cols-4>*>:first-child,.cimplify-media>*>:first-child,.cimplify-media>img,.cimplify-cols-2>img,.cimplify-cols-3>img,.cimplify-cols-4>img{margin-top:0}.cimplify-card{border:1px solid var(--color-border);border-radius:var(--radius);padding:1rem}.cimplify-banner{border-radius:var(--radius);position:relative;overflow:hidden}.cimplify-banner>img{width:100%;height:auto;margin:0;display:block}.cimplify-banner__content{flex-direction:column;justify-content:center;gap:.5rem;padding:1.5rem 2rem;display:flex;position:absolute;inset:0}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@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-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "chat-widget.tsx",
|
|
12
|
-
"content": "\"use client\";\n\nimport React, {\n useState,\n useRef,\n useCallback,\n type KeyboardEvent,\n} from \"react\";\nimport type { CimplifyClient } from \"../client\";\nimport { CimplifyError } from \"@cimplify/sdk\";\nimport type { ChatMessage, ChatUploadAttachment, ChatWidgetStarter } from \"../types/support\";\nimport { ChatMarkdown } from \"./chat-markdown\";\nimport { partitionMessages } from \"./chat-model\";\nimport { safeAttachmentUrl } from \"./utils/attachment\";\nimport { useChat } from \"./hooks/use-chat\";\nimport { useOptionalCimplifyClient } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;\n\n/* ─── Public API ──────────────────────────────────────────── */\n\nexport interface ChatWidgetClassNames {\n root?: string;\n bubble?: string;\n panel?: string;\n header?: string;\n messages?: string;\n input?: string;\n welcome?: string;\n}\n\nexport interface ChatWidgetProps {\n client?: CimplifyClient;\n /** Business display name shown in the header */\n businessName?: string;\n /** Greeting text on the welcome screen */\n greeting?: string;\n /** Subtitle on the welcome screen */\n subtitle?: string;\n /** Conversation starters */\n starters?: ChatWidgetStarter[];\n /** Input placeholder */\n placeholder?: string;\n /** Position of the bubble */\n position?: \"bottom-right\" | \"bottom-left\";\n /** Start with the panel open */\n defaultOpen?: boolean;\n /** Poll interval in ms (default: 3000) */\n pollInterval?: number;\n className?: string;\n classNames?: ChatWidgetClassNames;\n}\n\nexport function ChatWidget({\n client: clientProp,\n businessName = \"Support\",\n greeting = \"Hi there!\",\n subtitle = \"Ask us anything — we typically reply in a few seconds.\",\n starters,\n placeholder = \"Type a message\\u2026\",\n position = \"bottom-right\",\n defaultOpen = false,\n pollInterval,\n className,\n classNames,\n}: ChatWidgetProps) {\n const context = useOptionalCimplifyClient();\n const client = clientProp ?? context?.client;\n\n const [open, setOpen] = useState(defaultOpen);\n const [readAt, setReadAt] = useState<string | null>(null);\n const [attachError, setAttachError] = useState<CimplifyError | null>(null);\n\n const {\n messages,\n isLoading,\n isSending,\n awaitingReply,\n seenAt,\n error,\n isActive,\n claimedAs,\n signOut,\n send,\n react,\n startConversation,\n openIfExisting,\n } = useChat({ client, starters, pollInterval });\n const showTyping = isSending || awaitingReply;\n\n const attachPhoto = useCallback(\n async (file: File) => {\n if (!client) return;\n setAttachError(null);\n if (!file.type.startsWith(\"image/\")) {\n setAttachError(new CimplifyError(\"invalid_attachment\", \"Only photos can be attached.\"));\n return;\n }\n if (file.size > MAX_ATTACHMENT_BYTES) {\n setAttachError(new CimplifyError(\"attachment_too_large\", \"Photos must be under 5MB.\"));\n return;\n }\n const uploaded = await client.uploads.upload(file);\n if (!uploaded.ok) {\n setAttachError(uploaded.error);\n return;\n }\n const attachment: ChatUploadAttachment = {\n upload_id: uploaded.value.id,\n url: uploaded.value.url,\n mime_type: uploaded.value.content_type,\n filename: uploaded.value.filename,\n size: uploaded.value.size_bytes,\n };\n await send(\"\", { attachments: [attachment] });\n },\n [client, send],\n );\n\n const handleSend = useCallback(\n (content: string) => {\n setAttachError(null);\n return send(content);\n },\n [send],\n );\n\n // Unread badge: incoming messages newer than the watermark snapshotted\n // when the panel last closed.\n const unread = open\n ? 0\n : messages.filter(\n (m) => m.sender_type !== \"customer\" && (!readAt || m.created_at > readAt),\n ).length;\n\n useState(() => {\n if (defaultOpen) void openIfExisting();\n });\n\n const handleOpen = useCallback(() => {\n setOpen(true);\n void openIfExisting();\n }, [openIfExisting]);\n\n const handleClose = useCallback(() => {\n const newest = messages[messages.length - 1];\n setReadAt(newest?.created_at ?? null);\n setOpen(false);\n }, [messages]);\n\n const isLeft = position === \"bottom-left\";\n\n return (\n <div\n className={cn(\n \"fixed bottom-6 z-[9999]\",\n isLeft ? \"left-6\" : \"right-6\",\n className,\n classNames?.root,\n )}\n >\n {/* Bubble */}\n {!open && (\n <Bubble\n unread={unread}\n onClick={handleOpen}\n className={classNames?.bubble}\n />\n )}\n\n {/* Panel */}\n {open && (\n <Panel\n businessName={businessName}\n greeting={greeting}\n subtitle={subtitle}\n starters={starters}\n placeholder={placeholder}\n messages={messages}\n isLoading={isLoading}\n isSending={isSending}\n isTyping={showTyping}\n seenAt={seenAt}\n error={error ?? attachError}\n isActive={isActive}\n isLeft={isLeft}\n claimedAs={claimedAs}\n onSignOut={() => void signOut()}\n onClose={handleClose}\n onSend={handleSend}\n onReact={react}\n onAttach={attachPhoto}\n onStartConversation={startConversation}\n classNames={classNames}\n />\n )}\n </div>\n );\n}\n\n/* ─── Bubble ──────────────────────────────────────────────── */\n\nfunction Bubble({\n unread,\n onClick,\n className,\n}: {\n unread: number;\n onClick: () => void;\n className?: string;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label=\"Open chat\"\n className={cn(\n \"relative flex h-14 w-14 items-center justify-center rounded-full\",\n \"bg-foreground text-background shadow-lg\",\n \"transition-transform duration-300 ease-out\",\n \"hover:scale-[1.08] active:scale-95\",\n className,\n )}\n >\n <ChatIcon className=\"h-6 w-6\" />\n {unread > 0 && (\n <span\n className={cn(\n \"absolute -right-0.5 -top-0.5 flex h-5 w-5 items-center justify-center\",\n \"rounded-full bg-red-500 text-[10px] font-bold text-white\",\n \"ring-2 ring-background\",\n )}\n >\n {unread > 9 ? \"9+\" : unread}\n </span>\n )}\n </button>\n );\n}\n\n/* ─── Panel ───────────────────────────────────────────────── */\n\nfunction Panel({\n businessName,\n greeting,\n subtitle,\n starters,\n placeholder,\n messages,\n isLoading,\n isSending,\n isTyping,\n seenAt,\n error,\n isActive,\n isLeft,\n claimedAs,\n onSignOut,\n onClose,\n onSend,\n onReact,\n onAttach,\n onStartConversation,\n classNames,\n}: {\n businessName: string;\n greeting: string;\n subtitle: string;\n starters?: ChatWidgetStarter[];\n placeholder: string;\n messages: ChatMessage[];\n isLoading: boolean;\n isSending: boolean;\n isTyping: boolean;\n seenAt: string | null;\n error: CimplifyError | null;\n isActive: boolean;\n isLeft: boolean;\n claimedAs: string | null;\n onSignOut: () => void;\n onClose: () => void;\n onSend: (content: string) => Promise<unknown>;\n onReact: (message: ChatMessage, emoji: string) => Promise<boolean>;\n onAttach: (file: File) => Promise<void>;\n onStartConversation: (text?: string) => Promise<void>;\n classNames?: ChatWidgetClassNames;\n}) {\n return (\n <div\n className={cn(\n \"flex flex-col overflow-hidden rounded-2xl bg-background shadow-2xl\",\n \"w-[400px] max-w-[calc(100vw-3rem)]\",\n \"h-[min(600px,calc(100vh-6rem))]\",\n \"animate-in fade-in slide-in-from-bottom-4 duration-300\",\n isLeft ? \"origin-bottom-left\" : \"origin-bottom-right\",\n classNames?.panel,\n )}\n >\n {/* Header */}\n <Header\n businessName={businessName}\n claimedAs={claimedAs}\n onSignOut={onSignOut}\n onClose={onClose}\n className={classNames?.header}\n />\n\n {/* Body */}\n {!isActive && !isLoading ? (\n <Welcome\n greeting={greeting}\n subtitle={subtitle}\n starters={starters}\n onStarter={onStartConversation}\n className={classNames?.welcome}\n />\n ) : (\n <MessageList\n messages={messages}\n isLoading={isLoading}\n isSending={isTyping}\n seenAt={seenAt}\n onReact={onReact}\n className={classNames?.messages}\n />\n )}\n\n {/* Error */}\n {error != null && (\n <div className=\"border-t border-red-200 bg-red-50 px-4 py-2 text-xs text-red-600\">\n {error.userMessage || \"Something went wrong. Please try again.\"}\n </div>\n )}\n\n {/* Always present: gating on an active conversation left a visitor who\n wants to type their own question with only the starter buttons. */}\n <ChatInput\n placeholder={placeholder}\n isSending={isSending}\n onSend={isActive ? onSend : onStartConversation}\n onAttach={onAttach}\n className={classNames?.input}\n />\n\n\n {/* Footer */}\n <div className=\"flex-shrink-0 border-t border-border bg-background px-4 py-1.5 text-center text-[10px] text-muted-foreground\">\n Powered by{\" \"}\n <span className=\"font-semibold text-foreground\">Cimplify</span>\n </div>\n </div>\n );\n}\n\n/* ─── Header ──────────────────────────────────────────────── */\n\nfunction Header({\n businessName,\n claimedAs,\n onSignOut,\n onClose,\n className,\n}: {\n businessName: string;\n claimedAs: string | null;\n onSignOut: () => void;\n onClose: () => void;\n className?: string;\n}) {\n return (\n <div\n className={cn(\n \"flex flex-shrink-0 items-center gap-3 bg-foreground px-5 py-4 text-background\",\n className,\n )}\n >\n <div className=\"flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px] bg-white/15\">\n <ChatIcon className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-[15px] font-semibold leading-tight\">\n {businessName}\n </div>\n {claimedAs ? (\n <div className=\"flex items-center gap-1 truncate text-xs text-white/60\">\n <span className=\"truncate\">Chatting as {claimedAs}</span>\n <span aria-hidden=\"true\">·</span>\n <button\n type=\"button\"\n onClick={onSignOut}\n className=\"flex-shrink-0 underline underline-offset-2 hover:text-white\"\n >\n Not you?\n </button>\n </div>\n ) : (\n <div className=\"flex items-center gap-1.5 text-xs text-white/60\">\n <span className=\"h-[7px] w-[7px] flex-shrink-0 rounded-full bg-emerald-400\" />\n Online\n </div>\n )}\n </div>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label=\"Close chat\"\n className={cn(\n \"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg\",\n \"bg-white/10 text-white/70 transition-colors hover:bg-white/20 hover:text-white\",\n )}\n >\n <CloseIcon className=\"h-[18px] w-[18px]\" />\n </button>\n </div>\n );\n}\n\n/* ─── Welcome ─────────────────────────────────────────────── */\n\nfunction Welcome({\n greeting,\n subtitle,\n starters,\n onStarter,\n className,\n}: {\n greeting: string;\n subtitle: string;\n starters?: ChatWidgetStarter[];\n onStarter: (text?: string) => Promise<void>;\n className?: string;\n}) {\n const defaultStarters: ChatWidgetStarter[] = [\n { icon: \"\\ud83d\\udd0d\", text: \"Help me find a product\" },\n { icon: \"\\ud83d\\uded2\", text: \"Check my cart\" },\n { icon: \"\\ud83d\\udce6\", text: \"Track my order\" },\n ];\n\n const items = starters ?? defaultStarters;\n\n return (\n <div\n className={cn(\n \"flex flex-1 flex-col items-center justify-center overflow-y-auto bg-muted/50 px-8 py-10 text-center\",\n className,\n )}\n >\n <div className=\"mb-5 flex h-16 w-16 items-center justify-center rounded-2xl bg-muted\">\n <ChatIcon className=\"h-7 w-7 text-muted-foreground\" />\n </div>\n <h3 className=\"mb-2 text-xl font-bold text-foreground\">{greeting}</h3>\n <p className=\"mb-6 max-w-[280px] text-sm leading-relaxed text-muted-foreground\">\n {subtitle}\n </p>\n <div className=\"flex w-full max-w-[300px] flex-col gap-2\">\n {items.map((s) => (\n <button\n key={s.text}\n type=\"button\"\n onClick={() => onStarter(s.text)}\n className={cn(\n \"flex items-center gap-3 rounded-xl border border-border bg-background px-4 py-3\",\n \"text-left text-[13px] font-medium text-foreground\",\n \"transition-all hover:-translate-y-px hover:border-foreground hover:shadow-sm\",\n )}\n >\n {s.icon && (\n <span className=\"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-muted text-[15px]\">\n {s.icon}\n </span>\n )}\n <span>{s.text}</span>\n </button>\n ))}\n </div>\n </div>\n );\n}\n\n/* ─── Messages ────────────────────────────────────────────── */\n\nconst QUICK_REACTIONS = [\"👍\", \"❤️\"] as const;\n\nfunction lastSeenCustomerMessageId(\n bubbles: ChatMessage[],\n seenAt: string | null,\n): string | null {\n if (!seenAt || !bubbles.some((m) => m.sender_type === \"agent\")) return null;\n for (let i = bubbles.length - 1; i >= 0; i--) {\n if (bubbles[i].sender_type !== \"customer\") continue;\n return bubbles[i].created_at <= seenAt ? bubbles[i].id : null;\n }\n return null;\n}\n\nfunction MessageList({\n messages,\n isLoading,\n isSending,\n seenAt,\n onReact,\n className,\n}: {\n messages: ChatMessage[];\n isLoading: boolean;\n isSending: boolean;\n seenAt: string | null;\n onReact: (message: ChatMessage, emoji: string) => Promise<boolean>;\n className?: string;\n}) {\n const { bubbles, reactionsByTarget } = partitionMessages(messages);\n const seenMessageId = lastSeenCustomerMessageId(bubbles, seenAt);\n\n return (\n <div\n className={cn(\n \"flex flex-1 flex-col gap-1 overflow-y-auto bg-muted/50 px-5 py-5\",\n className,\n )}\n >\n {isLoading && messages.length === 0 && (\n <div className=\"flex flex-1 items-center justify-center\">\n <TypingDots />\n </div>\n )}\n\n {bubbles.map((msg, i) => {\n const isCustomer = msg.sender_type === \"customer\";\n const prev = bubbles[i - 1];\n const sameSenderAsPrev = prev?.sender_type === msg.sender_type;\n const isOptimistic = msg.id.startsWith(\"opt_\");\n const reactions = reactionsByTarget.get(msg.id);\n\n return (\n <div key={msg.id}>\n {!sameSenderAsPrev && i > 0 && <div className=\"h-3\" />}\n <div\n className={cn(\n \"group relative flex max-w-[85%]\",\n isCustomer ? \"ml-auto\" : \"mr-auto\",\n )}\n >\n {!isCustomer && !isOptimistic && (\n <div className=\"absolute -top-3 right-0 z-10 hidden gap-0.5 rounded-full border border-border bg-background px-1 py-0.5 shadow-sm group-hover:flex\">\n {QUICK_REACTIONS.map((emoji) => (\n <button\n key={emoji}\n type=\"button\"\n aria-label={`React ${emoji}`}\n onClick={() => void onReact(msg, emoji)}\n className=\"flex h-6 w-6 items-center justify-center rounded-full text-[13px] hover:bg-muted\"\n >\n {emoji}\n </button>\n ))}\n </div>\n )}\n <div\n className={cn(\n \"rounded-2xl px-3.5 py-2.5 text-sm leading-relaxed\",\n isCustomer\n ? \"bg-foreground text-background\"\n : \"border border-border bg-background text-foreground\",\n isCustomer\n ? sameSenderAsPrev\n ? \"rounded-tr\"\n : \"rounded-br\"\n : sameSenderAsPrev\n ? \"rounded-tl\"\n : \"rounded-bl\",\n isOptimistic && \"opacity-70\",\n )}\n >\n {!isCustomer && !sameSenderAsPrev && (\n <span className=\"mb-1 inline-flex items-center gap-1 rounded bg-violet-100 px-1.5 py-0.5 text-[10px] font-semibold text-violet-600\">\n <SparkleIcon className=\"h-[11px] w-[11px]\" />\n AI\n </span>\n )}\n {msg.reply_to && (\n <div\n className={cn(\n \"mb-1.5 flex gap-1.5 rounded-lg px-2 py-1 text-xs\",\n isCustomer ? \"bg-background/15\" : \"bg-muted\",\n )}\n >\n <span\n className={cn(\n \"w-[3px] shrink-0 self-stretch rounded-full\",\n isCustomer ? \"bg-background/40\" : \"bg-muted-foreground/40\",\n )}\n />\n <span className=\"min-w-0 truncate opacity-80\">{msg.reply_to.preview}</span>\n </div>\n )}\n {msg.attachments.length > 0 && (\n <div className={cn(\"flex flex-wrap gap-1.5\", msg.content && \"mb-1.5\")}>\n {msg.attachments.map((attachment, index) => {\n const url = safeAttachmentUrl(attachment.url);\n if (!url) return null;\n return attachment.mime_type?.startsWith(\"image/\") ? (\n <a key={index} href={url} target=\"_blank\" rel=\"noreferrer\">\n <img\n src={url}\n alt={attachment.filename ?? \"Photo\"}\n loading=\"lazy\"\n className=\"max-h-48 max-w-full rounded-lg object-cover\"\n />\n </a>\n ) : (\n <a\n key={index}\n href={url}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"truncate text-xs underline\"\n >\n {attachment.filename ?? \"Attachment\"}\n </a>\n );\n })}\n </div>\n )}\n {isCustomer ? (\n msg.content && <div className=\"whitespace-pre-wrap\">{msg.content}</div>\n ) : (\n <ChatMarkdown\n text={msg.content}\n className=\"[&_p]:my-0 [&_p+p]:mt-1.5 [&_ul]:my-1 [&_ul]:list-disc [&_ul]:pl-4 [&_ol]:my-1 [&_ol]:list-decimal [&_ol]:pl-4 [&_li]:my-0.5 [&_h1]:text-sm [&_h1]:font-bold [&_h2]:text-sm [&_h2]:font-bold [&_h3]:text-sm [&_h3]:font-semibold [&_hr]:my-2 [&_table]:my-1 [&_table]:w-full [&_th]:pb-1 [&_th]:text-left [&_th]:text-[10px] [&_th]:font-semibold [&_th]:uppercase [&_td]:py-1 [&_td:last-child]:text-right [&_td:last-child]:font-semibold [&_td:last-child]:tabular-nums [&_a]:underline\"\n />\n )}\n </div>\n </div>\n {reactions && reactions.length > 0 && (\n <div className={cn(\"mt-0.5 flex gap-1\", isCustomer ? \"justify-end\" : \"justify-start\")}>\n {reactions.map((reaction) => (\n <span\n key={reaction.id}\n className=\"rounded-full border border-border bg-background px-1.5 py-0.5 text-xs shadow-sm\"\n >\n {reaction.content}\n </span>\n ))}\n </div>\n )}\n {msg.id === seenMessageId && (\n <div className=\"mt-0.5 text-right text-[10px] font-medium text-muted-foreground\">\n Seen\n </div>\n )}\n </div>\n );\n })}\n\n {isSending && (\n <div className=\"mr-auto mt-1\">\n <TypingDots />\n </div>\n )}\n\n <div\n key={`${messages.length}:${isSending}`}\n ref={(node) => {\n // Keyed so the callback re-fires per new message/typing change —\n // follows the conversation without an effect.\n node?.scrollIntoView({ behavior: \"smooth\" });\n }}\n />\n </div>\n );\n}\n\n/* ─── Input ───────────────────────────────────────────────── */\n\nfunction ChatInput({\n placeholder,\n isSending,\n onSend,\n onAttach,\n className,\n}: {\n placeholder: string;\n isSending: boolean;\n onSend: (content: string) => Promise<unknown>;\n onAttach: (file: File) => Promise<void>;\n className?: string;\n}) {\n const [value, setValue] = useState(\"\");\n const [uploading, setUploading] = useState(false);\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const fileRef = useRef<HTMLInputElement>(null);\n\n const handleAttach = useCallback(\n async (file: File) => {\n setUploading(true);\n try {\n await onAttach(file);\n } finally {\n setUploading(false);\n }\n },\n [onAttach],\n );\n\n const handleSend = useCallback(() => {\n const trimmed = value.trim();\n if (!trimmed || isSending) return;\n setValue(\"\");\n if (textareaRef.current) {\n textareaRef.current.style.height = \"auto\";\n }\n onSend(trimmed);\n }, [value, isSending, onSend]);\n\n const handleKeyDown = useCallback(\n (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSend();\n }\n },\n [handleSend],\n );\n\n const handleInput = useCallback(() => {\n const el = textareaRef.current;\n if (!el) return;\n el.style.height = \"auto\";\n el.style.height = `${Math.min(el.scrollHeight, 100)}px`;\n }, []);\n\n const hasContent = value.trim().length > 0;\n\n return (\n <div\n className={cn(\n \"flex flex-shrink-0 items-end gap-2 border-t border-border bg-background px-4 py-3\",\n className,\n )}\n >\n <input\n ref={fileRef}\n type=\"file\"\n accept=\"image/*\"\n className=\"hidden\"\n aria-hidden=\"true\"\n tabIndex={-1}\n onChange={(e) => {\n const file = e.target.files?.[0];\n e.target.value = \"\";\n if (file) void handleAttach(file);\n }}\n />\n <button\n type=\"button\"\n aria-label=\"Attach a photo\"\n disabled={uploading}\n onClick={() => fileRef.current?.click()}\n className={cn(\n \"flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-[10px]\",\n \"bg-muted text-muted-foreground transition-colors hover:text-foreground\",\n uploading && \"animate-pulse\",\n )}\n >\n <ImageIcon className=\"h-[18px] w-[18px]\" />\n </button>\n <div\n className={cn(\n \"flex flex-1 items-end rounded-xl border-[1.5px] border-transparent bg-muted px-1\",\n \"transition-colors focus-within:border-foreground focus-within:bg-background\",\n )}\n >\n <textarea\n ref={textareaRef}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={handleKeyDown}\n onInput={handleInput}\n placeholder={placeholder}\n rows={1}\n className={cn(\n \"max-h-[100px] min-h-[20px] flex-1 resize-none bg-transparent px-2.5 py-2\",\n \"text-sm text-foreground outline-none placeholder:text-muted-foreground\",\n )}\n />\n </div>\n\n <button\n type=\"button\"\n onClick={handleSend}\n disabled={!hasContent || isSending}\n aria-label=\"Send message\"\n className={cn(\n \"flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-[10px]\",\n \"transition-all duration-200\",\n hasContent && !isSending\n ? \"bg-foreground text-background hover:scale-105 active:scale-95\"\n : \"bg-muted text-muted-foreground\",\n )}\n >\n <SendIcon className=\"h-[18px] w-[18px]\" />\n </button>\n </div>\n );\n}\n\n/* ─── Typing dots ─────────────────────────────────────────── */\n\nfunction TypingDots() {\n return (\n <div className=\"flex items-center gap-1 px-4 py-3\">\n {[0, 1, 2].map((i) => (\n <span\n key={i}\n className=\"h-[7px] w-[7px] animate-bounce rounded-full bg-muted-foreground/40\"\n style={{ animationDelay: `${i * 160}ms` }}\n />\n ))}\n </div>\n );\n}\n\n/* ─── Icons (inline SVG — no icon library) ────────────────── */\n\nfunction ChatIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M21 11.5a8.38 8.38 0 01-.9 3.8 8.5 8.5 0 01-7.6 4.7 8.38 8.38 0 01-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 01-.9-3.8 8.5 8.5 0 014.7-7.6 8.38 8.38 0 013.8-.9h.5a8.48 8.48 0 018 8v.5z\" />\n </svg>\n );\n}\n\nfunction CloseIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M18 6L6 18M6 6l12 12\" />\n </svg>\n );\n}\n\nfunction SendIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\" />\n <polygon points=\"22 2 15 22 11 13 2 9 22 2\" />\n </svg>\n );\n}\n\nfunction ImageIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\" />\n <circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\" />\n <polyline points=\"21 15 16 10 5 21\" />\n </svg>\n );\n}\n\nfunction SparkleIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2.5}\n >\n <path d=\"M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83\" />\n </svg>\n );\n}\n"
|
|
12
|
+
"content": "\"use client\";\n\nimport React, {\n useState,\n useRef,\n useCallback,\n type KeyboardEvent,\n} from \"react\";\nimport type { CimplifyClient } from \"../client\";\nimport { CimplifyError } from \"@cimplify/sdk\";\nimport type { ChatMessage, ChatUploadAttachment, ChatWidgetStarter } from \"../types/support\";\nimport { splitGenUIContent } from \"../genui/segments\";\nimport { ChatMarkdown } from \"./chat-markdown\";\nimport { isOptimistic, partitionMessages } from \"./chat-model\";\nimport { safeAttachmentUrl } from \"./utils/attachment\";\nimport { useChat } from \"./hooks/use-chat\";\nimport { useOptionalCimplifyClient } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;\nconst KEEP_DISMISSED_KEY = \"cimplify_keep_chat_dismissed\";\n/** How close to the bottom still counts as reading the live edge. */\nconst AT_BOTTOM_SLACK_PX = 48;\n\nfunction proseOf(content: string): string {\n return splitGenUIContent(content)\n .filter((segment) => segment.kind === \"prose\")\n .map((segment) => segment.text)\n .join(\"\\n\\n\")\n .trim();\n}\n\n/* ─── Public API ──────────────────────────────────────────── */\n\nexport interface ChatWidgetClassNames {\n root?: string;\n bubble?: string;\n panel?: string;\n header?: string;\n messages?: string;\n input?: string;\n welcome?: string;\n}\n\nexport interface ChatWidgetProps {\n client?: CimplifyClient;\n /** Business display name shown in the header */\n businessName?: string;\n /** Greeting text on the welcome screen */\n greeting?: string;\n /** Subtitle on the welcome screen */\n subtitle?: string;\n /** Conversation starters */\n starters?: ChatWidgetStarter[];\n /** Input placeholder */\n placeholder?: string;\n /** Position of the bubble */\n position?: \"bottom-right\" | \"bottom-left\";\n /** Start with the panel open */\n defaultOpen?: boolean;\n /** Poll interval in ms (default: 3000) */\n pollInterval?: number;\n className?: string;\n classNames?: ChatWidgetClassNames;\n}\n\nexport function ChatWidget({\n client: clientProp,\n businessName = \"Support\",\n greeting = \"Hi there!\",\n subtitle = \"Ask us anything — we typically reply in a few seconds.\",\n starters,\n placeholder = \"Type a message\\u2026\",\n position = \"bottom-right\",\n defaultOpen = false,\n pollInterval,\n className,\n classNames,\n}: ChatWidgetProps) {\n const context = useOptionalCimplifyClient();\n const client = clientProp ?? context?.client;\n\n const [open, setOpen] = useState(defaultOpen);\n const [readAt, setReadAt] = useState<string | null>(null);\n const [attachError, setAttachError] = useState<CimplifyError | null>(null);\n const [replyTo, setReplyTo] = useState<ChatMessage | null>(null);\n\n const {\n messages,\n isLoading,\n isSending,\n awaitingReply,\n seenAt,\n error,\n isActive,\n humanRequested,\n claimedAs,\n signOut,\n send,\n react,\n requestHuman,\n startConversation,\n loadOlder,\n mayHaveOlder,\n openIfExisting,\n } = useChat({ client, starters, pollInterval });\n const showTyping = isSending || awaitingReply;\n\n const attachPhoto = useCallback(\n async (file: File) => {\n if (!client) return;\n setAttachError(null);\n if (!file.type.startsWith(\"image/\")) {\n setAttachError(new CimplifyError(\"invalid_attachment\", \"Only photos can be attached.\"));\n return;\n }\n if (file.size > MAX_ATTACHMENT_BYTES) {\n setAttachError(new CimplifyError(\"attachment_too_large\", \"Photos must be under 5MB.\"));\n return;\n }\n const uploaded = await client.uploads.upload(file);\n if (!uploaded.ok) {\n setAttachError(uploaded.error);\n return;\n }\n const attachment: ChatUploadAttachment = {\n upload_id: uploaded.value.id,\n url: uploaded.value.url,\n mime_type: uploaded.value.content_type,\n filename: uploaded.value.filename,\n size: uploaded.value.size_bytes,\n };\n await send(\"\", { attachments: [attachment] });\n },\n [client, send],\n );\n\n const handleSend = useCallback(\n (content: string) => {\n setAttachError(null);\n const target = replyTo;\n setReplyTo(null);\n return send(content, target ? { replyTo: target } : undefined);\n },\n [send, replyTo],\n );\n\n // Unread badge: incoming messages newer than the watermark snapshotted\n // when the panel last closed.\n const unread = open\n ? 0\n : messages.filter(\n (m) => m.sender_type !== \"customer\" && (!readAt || m.created_at > readAt),\n ).length;\n\n useState(() => {\n if (defaultOpen) void openIfExisting();\n });\n\n const handleOpen = useCallback(() => {\n setOpen(true);\n void openIfExisting();\n }, [openIfExisting]);\n\n const handleClose = useCallback(() => {\n const newest = messages[messages.length - 1];\n setReadAt(newest?.created_at ?? null);\n setOpen(false);\n }, [messages]);\n\n const isLeft = position === \"bottom-left\";\n\n return (\n <div\n className={cn(\n \"fixed bottom-6 z-[9999]\",\n isLeft ? \"left-6\" : \"right-6\",\n className,\n classNames?.root,\n )}\n >\n {/* Bubble */}\n {!open && (\n <Bubble\n unread={unread}\n onClick={handleOpen}\n className={classNames?.bubble}\n />\n )}\n\n {/* Panel */}\n {open && (\n <Panel\n client={client}\n businessName={businessName}\n greeting={greeting}\n subtitle={subtitle}\n starters={starters}\n placeholder={placeholder}\n messages={messages}\n isLoading={isLoading}\n isSending={isSending}\n isTyping={showTyping}\n seenAt={seenAt}\n error={error ?? attachError}\n isActive={isActive}\n isLeft={isLeft}\n humanRequested={humanRequested}\n mayHaveOlder={mayHaveOlder}\n replyTo={replyTo}\n claimedAs={claimedAs}\n onSignOut={() => void signOut()}\n onClose={handleClose}\n onSend={handleSend}\n onReact={react}\n onReply={setReplyTo}\n onCancelReply={() => setReplyTo(null)}\n onRequestHuman={() => void requestHuman()}\n onLoadOlder={() => void loadOlder()}\n onAttach={attachPhoto}\n onStartConversation={startConversation}\n classNames={classNames}\n />\n )}\n </div>\n );\n}\n\n/* ─── Bubble ──────────────────────────────────────────────── */\n\nfunction Bubble({\n unread,\n onClick,\n className,\n}: {\n unread: number;\n onClick: () => void;\n className?: string;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label=\"Open chat\"\n className={cn(\n \"relative flex h-14 w-14 items-center justify-center rounded-full\",\n \"bg-foreground text-background shadow-lg\",\n \"transition-transform duration-300 ease-out\",\n \"hover:scale-[1.08] active:scale-95\",\n className,\n )}\n >\n <ChatIcon className=\"h-6 w-6\" />\n {unread > 0 && (\n <span\n className={cn(\n \"absolute -right-0.5 -top-0.5 flex h-5 w-5 items-center justify-center\",\n \"rounded-full bg-destructive text-[10px] font-bold text-white\",\n \"ring-2 ring-background\",\n )}\n >\n {unread > 9 ? \"9+\" : unread}\n </span>\n )}\n </button>\n );\n}\n\n/* ─── Panel ───────────────────────────────────────────────── */\n\nfunction Panel({\n client,\n businessName,\n greeting,\n subtitle,\n starters,\n placeholder,\n messages,\n isLoading,\n isSending,\n isTyping,\n seenAt,\n error,\n isActive,\n isLeft,\n humanRequested,\n mayHaveOlder,\n replyTo,\n claimedAs,\n onSignOut,\n onClose,\n onSend,\n onReact,\n onReply,\n onCancelReply,\n onRequestHuman,\n onLoadOlder,\n onAttach,\n onStartConversation,\n classNames,\n}: {\n client: CimplifyClient | undefined;\n businessName: string;\n greeting: string;\n subtitle: string;\n starters?: ChatWidgetStarter[];\n placeholder: string;\n messages: ChatMessage[];\n isLoading: boolean;\n isSending: boolean;\n isTyping: boolean;\n seenAt: string | null;\n error: CimplifyError | null;\n isActive: boolean;\n isLeft: boolean;\n humanRequested: boolean;\n mayHaveOlder: boolean;\n replyTo: ChatMessage | null;\n claimedAs: string | null;\n onSignOut: () => void;\n onClose: () => void;\n onSend: (content: string) => Promise<boolean>;\n onReact: (message: ChatMessage, emoji: string) => Promise<boolean>;\n onReply: (message: ChatMessage) => void;\n onCancelReply: () => void;\n onRequestHuman: () => void;\n onLoadOlder: () => void;\n onAttach: (file: File) => Promise<void>;\n onStartConversation: (text?: string) => Promise<void>;\n classNames?: ChatWidgetClassNames;\n}) {\n return (\n <div\n className={cn(\n \"flex flex-col overflow-hidden rounded-2xl bg-background shadow-2xl\",\n \"w-[400px] max-w-[calc(100vw-3rem)]\",\n \"h-[min(600px,calc(100vh-6rem))]\",\n \"animate-in fade-in slide-in-from-bottom-4 duration-300\",\n isLeft ? \"origin-bottom-left\" : \"origin-bottom-right\",\n classNames?.panel,\n )}\n >\n {/* Header */}\n <Header\n businessName={businessName}\n claimedAs={claimedAs}\n onSignOut={onSignOut}\n onClose={onClose}\n className={classNames?.header}\n />\n\n {/* Body */}\n {!isActive && !isLoading ? (\n <Welcome\n greeting={greeting}\n subtitle={subtitle}\n starters={starters}\n onStarter={onStartConversation}\n className={classNames?.welcome}\n />\n ) : (\n <MessageList\n messages={messages}\n businessName={businessName}\n isLoading={isLoading}\n isSending={isTyping}\n seenAt={seenAt}\n humanRequested={humanRequested}\n mayHaveOlder={mayHaveOlder}\n onReact={onReact}\n onReply={onReply}\n onRequestHuman={onRequestHuman}\n onLoadOlder={onLoadOlder}\n className={classNames?.messages}\n />\n )}\n\n {isActive && !claimedAs && client && <KeepConversation client={client} />}\n\n {humanRequested && isActive && (\n <div className=\"flex items-center gap-2 border-t border-border bg-muted/50 px-4 py-2 text-[11px] text-muted-foreground\">\n <span className=\"h-1.5 w-1.5 flex-shrink-0 animate-pulse rounded-full bg-amber-500\" />\n Waiting for {businessName} — usually replies in minutes\n </div>\n )}\n\n {/* Error */}\n {error != null && (\n <div className=\"border-t border-destructive/20 bg-destructive/10 px-4 py-2 text-xs text-destructive\">\n {error.userMessage || \"Something went wrong. Please try again.\"}\n </div>\n )}\n\n {replyTo && (\n <div className=\"flex items-start gap-2 border-t border-border bg-muted/50 px-4 py-2\">\n <span className=\"w-[3px] shrink-0 self-stretch rounded-full bg-foreground/40\" />\n <div className=\"min-w-0 flex-1\">\n <div className=\"text-[11px] font-semibold text-foreground\">\n Replying to {replyTo.sender_type === \"customer\" ? \"yourself\" : businessName}\n </div>\n <div className=\"truncate text-[11px] text-muted-foreground\">\n {proseOf(replyTo.content) || \"Photo\"}\n </div>\n </div>\n <button\n type=\"button\"\n aria-label=\"Cancel reply\"\n onClick={onCancelReply}\n className=\"flex-shrink-0 p-0.5 text-muted-foreground hover:text-foreground\"\n >\n <CloseIcon className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n )}\n\n {/* Always present: gating on an active conversation left a visitor who\n wants to type their own question with only the starter buttons. */}\n <ChatInput\n placeholder={placeholder}\n isSending={isSending}\n onSend={isActive ? onSend : async (text) => { await onStartConversation(text); return true; }}\n onAttach={onAttach}\n className={classNames?.input}\n />\n\n\n {/* Footer */}\n <div className=\"flex-shrink-0 border-t border-border bg-background px-4 py-1.5 text-center text-[10px] text-muted-foreground\">\n Powered by{\" \"}\n <span className=\"font-semibold text-foreground\">Cimplify</span>\n </div>\n </div>\n );\n}\n\n/* ─── Header ──────────────────────────────────────────────── */\n\nfunction Header({\n businessName,\n claimedAs,\n onSignOut,\n onClose,\n className,\n}: {\n businessName: string;\n claimedAs: string | null;\n onSignOut: () => void;\n onClose: () => void;\n className?: string;\n}) {\n return (\n <div\n className={cn(\n \"flex flex-shrink-0 items-center gap-3 bg-foreground px-5 py-4 text-background\",\n className,\n )}\n >\n <div className=\"flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px] bg-background/15\">\n <ChatIcon className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-[15px] font-semibold leading-tight\">\n {businessName}\n </div>\n {claimedAs ? (\n <div className=\"flex items-center gap-1 truncate text-xs text-background/60\">\n <span className=\"truncate\">Chatting as {claimedAs}</span>\n <span aria-hidden=\"true\">·</span>\n <button\n type=\"button\"\n onClick={onSignOut}\n className=\"flex-shrink-0 underline underline-offset-2 hover:text-background\"\n >\n Not you?\n </button>\n </div>\n ) : (\n <div className=\"flex items-center gap-1.5 text-xs text-background/60\">\n <span className=\"h-[7px] w-[7px] flex-shrink-0 rounded-full bg-emerald-400\" />\n Online\n </div>\n )}\n </div>\n <button\n type=\"button\"\n onClick={onClose}\n aria-label=\"Close chat\"\n className={cn(\n \"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg\",\n \"bg-background/10 text-background/70 transition-colors hover:bg-background/20 hover:text-background\",\n )}\n >\n <CloseIcon className=\"h-[18px] w-[18px]\" />\n </button>\n </div>\n );\n}\n\n/* ─── Welcome ─────────────────────────────────────────────── */\n\nfunction Welcome({\n greeting,\n subtitle,\n starters,\n onStarter,\n className,\n}: {\n greeting: string;\n subtitle: string;\n starters?: ChatWidgetStarter[];\n onStarter: (text?: string) => Promise<void>;\n className?: string;\n}) {\n const defaultStarters: ChatWidgetStarter[] = [\n { icon: \"\\ud83d\\udd0d\", text: \"Help me find a product\" },\n { icon: \"\\ud83d\\uded2\", text: \"Check my cart\" },\n { icon: \"\\ud83d\\udce6\", text: \"Track my order\" },\n ];\n\n const items = starters ?? defaultStarters;\n\n return (\n <div\n className={cn(\n \"flex flex-1 flex-col items-center justify-center overflow-y-auto bg-muted/50 px-8 py-10 text-center\",\n className,\n )}\n >\n <div className=\"mb-5 flex h-16 w-16 items-center justify-center rounded-2xl bg-muted\">\n <ChatIcon className=\"h-7 w-7 text-muted-foreground\" />\n </div>\n <h3 className=\"mb-2 text-xl font-bold text-foreground\">{greeting}</h3>\n <p className=\"mb-6 max-w-[280px] text-sm leading-relaxed text-muted-foreground\">\n {subtitle}\n </p>\n <div className=\"flex w-full max-w-[300px] flex-col gap-2\">\n {items.map((s) => (\n <button\n key={s.text}\n type=\"button\"\n onClick={() => onStarter(s.text)}\n className={cn(\n \"flex items-center gap-3 rounded-xl border border-border bg-background px-4 py-3\",\n \"text-left text-[13px] font-medium text-foreground\",\n \"transition-all hover:-translate-y-px hover:border-foreground hover:shadow-sm\",\n )}\n >\n {s.icon && (\n <span className=\"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-muted text-[15px]\">\n {s.icon}\n </span>\n )}\n <span>{s.text}</span>\n </button>\n ))}\n </div>\n </div>\n );\n}\n\n/* ─── Messages ────────────────────────────────────────────── */\n\nconst QUICK_REACTIONS = [\"👍\", \"❤️\"] as const;\n\nfunction lastSeenCustomerMessageId(\n bubbles: ChatMessage[],\n seenAt: string | null,\n): string | null {\n if (!seenAt || !bubbles.some((m) => m.sender_type === \"agent\")) return null;\n for (let i = bubbles.length - 1; i >= 0; i--) {\n if (bubbles[i].sender_type !== \"customer\") continue;\n return bubbles[i].created_at <= seenAt ? bubbles[i].id : null;\n }\n return null;\n}\n\nfunction dayLabel(at: string): string {\n const date = new Date(at);\n const today = new Date();\n if (date.toDateString() === today.toDateString()) return \"Today\";\n const yesterday = new Date(today);\n yesterday.setDate(today.getDate() - 1);\n if (date.toDateString() === yesterday.toDateString()) return \"Yesterday\";\n return date.toLocaleDateString([], { weekday: \"short\", day: \"numeric\", month: \"short\" });\n}\n\nfunction MessageList({\n messages,\n businessName,\n isLoading,\n isSending,\n seenAt,\n humanRequested,\n mayHaveOlder,\n onReact,\n onReply,\n onRequestHuman,\n onLoadOlder,\n className,\n}: {\n messages: ChatMessage[];\n businessName: string;\n isLoading: boolean;\n isSending: boolean;\n seenAt: string | null;\n humanRequested: boolean;\n mayHaveOlder: boolean;\n onReact: (message: ChatMessage, emoji: string) => Promise<boolean>;\n onReply: (message: ChatMessage) => void;\n onRequestHuman: () => void;\n onLoadOlder: () => void;\n className?: string;\n}) {\n const scrolledUpRef = useRef(false);\n const partitioned = partitionMessages(messages);\n // A genui-only turn has nothing this surface can draw — an empty bubble\n // would still break the sender run and leave a phantom gap.\n const bubbles = partitioned.bubbles.filter(\n (m) =>\n m.sender_type === \"system\" ||\n m.attachments.length > 0 ||\n (m.sender_type === \"customer\" ? m.content.trim() !== \"\" : proseOf(m.content) !== \"\"),\n );\n const reactionsByTarget = partitioned.reactionsByTarget;\n const seenMessageId = lastSeenCustomerMessageId(bubbles, seenAt);\n const lastBubble = bubbles[bubbles.length - 1];\n const offerHuman = !humanRequested && lastBubble?.sender_type === \"bot\" && !isSending;\n\n const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {\n const node = event.currentTarget;\n scrolledUpRef.current =\n node.scrollHeight - node.scrollTop - node.clientHeight >= AT_BOTTOM_SLACK_PX;\n };\n\n return (\n <div\n onScroll={handleScroll}\n className={cn(\n \"flex flex-1 flex-col gap-1 overflow-y-auto bg-muted/50 px-5 py-5\",\n className,\n )}\n >\n {isLoading && messages.length === 0 && (\n <div className=\"flex flex-1 items-center justify-center\">\n <TypingDots />\n </div>\n )}\n\n {mayHaveOlder && (\n <div className=\"flex justify-center pb-2\">\n <button\n type=\"button\"\n onClick={onLoadOlder}\n className=\"rounded-full border border-border bg-background px-3 py-1 text-[11px] font-semibold text-muted-foreground hover:text-foreground\"\n >\n Show earlier messages\n </button>\n </div>\n )}\n\n {bubbles.map((msg, i) => {\n const isCustomer = msg.sender_type === \"customer\";\n const prev = bubbles[i - 1];\n const sameSenderAsPrev = prev?.sender_type === msg.sender_type;\n const optimistic = isOptimistic(msg);\n const reactions = reactionsByTarget.get(msg.id);\n const newDay = !prev || !sameDay(prev.created_at, msg.created_at);\n\n if (msg.sender_type === \"system\") {\n return (\n <div key={msg.id} className=\"my-2 text-center text-[11px] font-medium text-muted-foreground\">\n {msg.content}\n </div>\n );\n }\n\n return (\n <div key={msg.id}>\n {newDay && (\n <div className=\"my-2 text-center text-[10px] font-semibold tracking-wide text-muted-foreground\">\n {dayLabel(msg.created_at)}\n </div>\n )}\n {!sameSenderAsPrev && i > 0 && <div className=\"h-3\" />}\n <div\n className={cn(\n \"group relative flex max-w-[85%]\",\n isCustomer ? \"ml-auto\" : \"mr-auto\",\n )}\n >\n {!isCustomer && !optimistic && (\n <HoverRail message={msg} onReact={onReact} onReply={onReply} />\n )}\n <div\n className={cn(\n \"rounded-2xl px-3.5 py-2.5 text-sm leading-relaxed\",\n isCustomer\n ? \"bg-foreground text-background\"\n : \"border border-border bg-background text-foreground\",\n isCustomer\n ? sameSenderAsPrev\n ? \"rounded-tr\"\n : \"rounded-br\"\n : sameSenderAsPrev\n ? \"rounded-tl\"\n : \"rounded-bl\",\n optimistic && \"opacity-70\",\n )}\n >\n {!sameSenderAsPrev && <SenderChip sender={msg.sender_type} businessName={businessName} />}\n {msg.reply_to && (\n <div\n className={cn(\n \"mb-1.5 flex gap-1.5 rounded-lg px-2 py-1 text-xs\",\n isCustomer ? \"bg-background/15\" : \"bg-muted\",\n )}\n >\n <span\n className={cn(\n \"w-[3px] shrink-0 self-stretch rounded-full\",\n isCustomer ? \"bg-background/40\" : \"bg-muted-foreground/40\",\n )}\n />\n <span className=\"min-w-0 truncate opacity-80\">{msg.reply_to.preview}</span>\n </div>\n )}\n {msg.attachments.length > 0 && (\n <div className={cn(\"flex flex-wrap gap-1.5\", msg.content && \"mb-1.5\")}>\n {msg.attachments.map((attachment, index) => {\n const url = safeAttachmentUrl(attachment.url);\n if (!url) return null;\n return attachment.mime_type?.startsWith(\"image/\") ? (\n <a key={index} href={url} target=\"_blank\" rel=\"noreferrer\">\n <img\n src={url}\n alt={attachment.filename ?? \"Photo\"}\n loading=\"lazy\"\n className=\"max-h-48 max-w-full rounded-lg object-cover\"\n />\n </a>\n ) : (\n <a\n key={index}\n href={url}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"truncate text-xs underline\"\n >\n {attachment.filename ?? \"Attachment\"}\n </a>\n );\n })}\n </div>\n )}\n {isCustomer ? (\n msg.content && <div className=\"whitespace-pre-wrap\">{msg.content}</div>\n ) : (\n <ChatMarkdown\n text={proseOf(msg.content)}\n className=\"[&_p]:my-0 [&_p+p]:mt-1.5 [&_ul]:my-1 [&_ul]:list-disc [&_ul]:pl-4 [&_ol]:my-1 [&_ol]:list-decimal [&_ol]:pl-4 [&_li]:my-0.5 [&_h1]:text-sm [&_h1]:font-bold [&_h2]:text-sm [&_h2]:font-bold [&_h3]:text-sm [&_h3]:font-semibold [&_hr]:my-2 [&_table]:my-1 [&_table]:w-full [&_th]:pb-1 [&_th]:text-left [&_th]:text-[10px] [&_th]:font-semibold [&_th]:uppercase [&_td]:py-1 [&_td:last-child]:text-right [&_td:last-child]:font-semibold [&_td:last-child]:tabular-nums [&_a]:underline\"\n />\n )}\n </div>\n </div>\n {reactions && reactions.length > 0 && (\n <div className={cn(\"mt-0.5 flex gap-1\", isCustomer ? \"justify-end\" : \"justify-start\")}>\n {reactions.map((reaction) => (\n <span\n key={reaction.id}\n className=\"rounded-full border border-border bg-background px-1.5 py-0.5 text-xs shadow-sm\"\n >\n {reaction.content}\n </span>\n ))}\n </div>\n )}\n {msg.id === seenMessageId && (\n <div className=\"mt-0.5 text-right text-[10px] font-medium text-muted-foreground\">\n Seen\n </div>\n )}\n </div>\n );\n })}\n\n {isSending && (\n <div className=\"mr-auto mt-1\">\n <TypingDots />\n </div>\n )}\n\n {offerHuman && (\n <div className=\"mt-2\">\n <button\n type=\"button\"\n onClick={onRequestHuman}\n className=\"rounded-full border border-border bg-background px-3 py-1.5 text-xs font-semibold text-foreground hover:border-foreground\"\n >\n Talk to a person\n </button>\n </div>\n )}\n\n <div\n key={`${messages.length}:${isSending}`}\n ref={(node) => {\n // Keyed so the callback re-fires per new message/typing change —\n // follows the conversation, but never yanks a reader out of history.\n if (node && !scrolledUpRef.current) node.scrollIntoView({ behavior: \"smooth\" });\n }}\n />\n </div>\n );\n}\n\nfunction sameDay(a: string, b: string): boolean {\n return new Date(a).toDateString() === new Date(b).toDateString();\n}\n\nfunction SenderChip({\n sender,\n businessName,\n}: {\n sender: ChatMessage[\"sender_type\"];\n businessName: string;\n}) {\n if (sender === \"bot\") {\n return (\n <span className=\"mb-1 inline-flex items-center gap-1 rounded bg-primary/10 px-1.5 py-0.5 text-[10px] font-semibold text-primary\">\n <SparkleIcon className=\"h-[11px] w-[11px]\" />\n AI\n </span>\n );\n }\n if (sender === \"agent\") {\n return (\n <span className=\"mb-1 inline-flex items-center rounded bg-muted px-1.5 py-0.5 text-[10px] font-semibold text-muted-foreground\">\n {businessName}\n </span>\n );\n }\n return null;\n}\n\nfunction HoverRail({\n message,\n onReact,\n onReply,\n}: {\n message: ChatMessage;\n onReact: (message: ChatMessage, emoji: string) => Promise<boolean>;\n onReply: (message: ChatMessage) => void;\n}) {\n return (\n <div className=\"absolute -top-3 right-0 z-10 hidden gap-0.5 rounded-full border border-border bg-background px-1 py-0.5 shadow-sm group-hover:flex\">\n {QUICK_REACTIONS.map((emoji) => (\n <button\n key={emoji}\n type=\"button\"\n aria-label={`React ${emoji}`}\n onClick={() => void onReact(message, emoji)}\n className=\"flex h-6 w-6 items-center justify-center rounded-full text-[13px] hover:bg-muted\"\n >\n {emoji}\n </button>\n ))}\n <button\n type=\"button\"\n aria-label=\"Reply\"\n onClick={() => onReply(message)}\n className=\"flex h-6 w-6 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground\"\n >\n <ReplyIcon className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n );\n}\n\n/* ─── Keep this conversation ──────────────────────────────── */\n\nfunction readKeepDismissed(): boolean {\n try {\n return typeof window !== \"undefined\" && window.localStorage.getItem(KEEP_DISMISSED_KEY) === \"1\";\n } catch {\n return false;\n }\n}\n\nfunction writeKeepDismissed(): void {\n try {\n window.localStorage.setItem(KEEP_DISMISSED_KEY, \"1\");\n } catch {}\n}\n\nfunction KeepConversation({ client }: { client: CimplifyClient }) {\n const [step, setStep] = useState<\"row\" | \"contact\" | \"code\">(\"row\");\n const [dismissed, setDismissed] = useState(readKeepDismissed);\n const [contact, setContact] = useState(\"\");\n const [code, setCode] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [verifyError, setVerifyError] = useState<string | null>(null);\n\n if (dismissed) return null;\n\n const contactType = contact.includes(\"@\") ? \"email\" : \"phone\";\n\n const requestCode = async () => {\n const trimmed = contact.trim();\n if (!trimmed || busy) return;\n setBusy(true);\n setVerifyError(null);\n const result = await client.link.requestOtp({ contact: trimmed, contact_type: contactType });\n setBusy(false);\n if (result.ok) {\n setCode(\"\");\n setStep(\"code\");\n } else {\n setVerifyError(result.error.userMessage || \"Couldn't send a code — try again.\");\n }\n };\n\n const verify = async (value: string) => {\n if (value.length !== 6 || busy) return;\n setBusy(true);\n setVerifyError(null);\n const result = await client.link.verifyOtp({\n contact: contact.trim(),\n contact_type: contactType,\n otp_code: value,\n });\n setBusy(false);\n if (!result.ok) {\n setCode(\"\");\n setVerifyError(result.error.userMessage || \"Invalid or expired code.\");\n }\n };\n\n return (\n <div className=\"flex-shrink-0 border-t border-border bg-muted/50 px-4 py-2.5\">\n {step === \"row\" && (\n <div className=\"flex items-center gap-2.5\">\n <button\n type=\"button\"\n onClick={() => setStep(\"contact\")}\n className=\"flex min-w-0 flex-1 flex-col text-left\"\n >\n <span className=\"text-xs font-semibold text-foreground\">Keep this conversation</span>\n <span className=\"truncate text-[11px] text-muted-foreground\">\n Verify your number and it follows you — any device\n </span>\n </button>\n <button\n type=\"button\"\n aria-label=\"Dismiss\"\n onClick={() => {\n writeKeepDismissed();\n setDismissed(true);\n }}\n className=\"flex-shrink-0 p-1 text-muted-foreground hover:text-foreground\"\n >\n <CloseIcon className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n )}\n\n {step === \"contact\" && (\n <div className=\"flex items-center gap-2\">\n <input\n type=\"text\"\n inputMode=\"email\"\n value={contact}\n onChange={(e) => setContact(e.target.value)}\n onKeyDown={(e) => e.key === \"Enter\" && void requestCode()}\n placeholder=\"Phone or email\"\n autoFocus\n className=\"h-9 min-w-0 flex-1 rounded-lg border border-border bg-background px-3 text-xs text-foreground outline-none placeholder:text-muted-foreground focus:border-foreground\"\n />\n <button\n type=\"button\"\n onClick={() => void requestCode()}\n disabled={!contact.trim() || busy}\n className=\"h-9 flex-shrink-0 rounded-lg bg-foreground px-3 text-xs font-semibold text-background disabled:opacity-50\"\n >\n {busy ? \"Sending…\" : \"Send code\"}\n </button>\n </div>\n )}\n\n {step === \"code\" && (\n <div className=\"flex items-center gap-2\">\n <input\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n maxLength={6}\n value={code}\n onChange={(e) => {\n const digits = e.target.value.replace(/\\D/g, \"\").slice(0, 6);\n setCode(digits);\n if (digits.length === 6) void verify(digits);\n }}\n placeholder=\"6-digit code\"\n autoFocus\n className=\"h-9 min-w-0 flex-1 rounded-lg border border-border bg-background px-3 text-xs tracking-[0.2em] text-foreground outline-none placeholder:tracking-normal placeholder:text-muted-foreground focus:border-foreground\"\n />\n <button\n type=\"button\"\n onClick={() => void requestCode()}\n disabled={busy}\n className=\"h-9 flex-shrink-0 rounded-lg px-2 text-[11px] font-medium text-muted-foreground hover:text-foreground disabled:opacity-50\"\n >\n Resend\n </button>\n </div>\n )}\n\n {verifyError && <p className=\"mt-1.5 text-[11px] text-destructive\">{verifyError}</p>}\n </div>\n );\n}\n\n/* ─── Input ───────────────────────────────────────────────── */\n\nfunction ChatInput({\n placeholder,\n isSending,\n onSend,\n onAttach,\n className,\n}: {\n placeholder: string;\n isSending: boolean;\n onSend: (content: string) => Promise<boolean>;\n onAttach: (file: File) => Promise<void>;\n className?: string;\n}) {\n const [value, setValue] = useState(\"\");\n const [uploading, setUploading] = useState(false);\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const fileRef = useRef<HTMLInputElement>(null);\n\n const handleAttach = useCallback(\n async (file: File) => {\n setUploading(true);\n try {\n await onAttach(file);\n } finally {\n setUploading(false);\n }\n },\n [onAttach],\n );\n\n const handleSend = useCallback(async () => {\n const trimmed = value.trim();\n if (!trimmed || isSending) return;\n setValue(\"\");\n if (textareaRef.current) {\n textareaRef.current.style.height = \"auto\";\n }\n const sent = await onSend(trimmed);\n // A failed send must not eat the message — put it back to retry.\n if (!sent) setValue((current) => current || trimmed);\n }, [value, isSending, onSend]);\n\n const handleKeyDown = useCallback(\n (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSend();\n }\n },\n [handleSend],\n );\n\n const handleInput = useCallback(() => {\n const el = textareaRef.current;\n if (!el) return;\n el.style.height = \"auto\";\n el.style.height = `${Math.min(el.scrollHeight, 100)}px`;\n }, []);\n\n const hasContent = value.trim().length > 0;\n\n return (\n <div\n className={cn(\n \"flex flex-shrink-0 items-end gap-2 border-t border-border bg-background px-4 py-3\",\n className,\n )}\n >\n <input\n ref={fileRef}\n type=\"file\"\n accept=\"image/*\"\n className=\"hidden\"\n aria-hidden=\"true\"\n tabIndex={-1}\n onChange={(e) => {\n const file = e.target.files?.[0];\n e.target.value = \"\";\n if (file) void handleAttach(file);\n }}\n />\n <button\n type=\"button\"\n aria-label=\"Attach a photo\"\n disabled={uploading}\n onClick={() => fileRef.current?.click()}\n className={cn(\n \"flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-[10px]\",\n \"bg-muted text-muted-foreground transition-colors hover:text-foreground\",\n uploading && \"animate-pulse\",\n )}\n >\n <ImageIcon className=\"h-[18px] w-[18px]\" />\n </button>\n <div\n className={cn(\n \"flex flex-1 items-end rounded-xl border-[1.5px] border-transparent bg-muted px-1\",\n \"transition-colors focus-within:border-foreground focus-within:bg-background\",\n )}\n >\n <textarea\n ref={textareaRef}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={handleKeyDown}\n onInput={handleInput}\n placeholder={placeholder}\n rows={1}\n className={cn(\n \"max-h-[100px] min-h-[20px] flex-1 resize-none bg-transparent px-2.5 py-2\",\n \"text-sm text-foreground outline-none placeholder:text-muted-foreground\",\n )}\n />\n </div>\n\n <button\n type=\"button\"\n onClick={handleSend}\n disabled={!hasContent || isSending}\n aria-label=\"Send message\"\n className={cn(\n \"flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-[10px]\",\n \"transition-all duration-200\",\n hasContent && !isSending\n ? \"bg-foreground text-background hover:scale-105 active:scale-95\"\n : \"bg-muted text-muted-foreground\",\n )}\n >\n <SendIcon className=\"h-[18px] w-[18px]\" />\n </button>\n </div>\n );\n}\n\n/* ─── Typing dots ─────────────────────────────────────────── */\n\nfunction TypingDots() {\n return (\n <div className=\"flex items-center gap-1 px-4 py-3\">\n {[0, 1, 2].map((i) => (\n <span\n key={i}\n className=\"h-[7px] w-[7px] animate-bounce rounded-full bg-muted-foreground/40\"\n style={{ animationDelay: `${i * 160}ms` }}\n />\n ))}\n </div>\n );\n}\n\n/* ─── Icons (inline SVG — no icon library) ────────────────── */\n\nfunction ChatIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M21 11.5a8.38 8.38 0 01-.9 3.8 8.5 8.5 0 01-7.6 4.7 8.38 8.38 0 01-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 01-.9-3.8 8.5 8.5 0 014.7-7.6 8.38 8.38 0 013.8-.9h.5a8.48 8.48 0 018 8v.5z\" />\n </svg>\n );\n}\n\nfunction CloseIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M18 6L6 18M6 6l12 12\" />\n </svg>\n );\n}\n\nfunction SendIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\" />\n <polygon points=\"22 2 15 22 11 13 2 9 22 2\" />\n </svg>\n );\n}\n\nfunction ImageIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\" />\n <circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\" />\n <polyline points=\"21 15 16 10 5 21\" />\n </svg>\n );\n}\n\nfunction ReplyIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <polyline points=\"9 17 4 12 9 7\" />\n <path d=\"M20 18v-2a4 4 0 0 0-4-4H4\" />\n </svg>\n );\n}\n\nfunction SparkleIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2.5}\n >\n <path d=\"M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83\" />\n </svg>\n );\n}\n"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
{
|
|
12
12
|
"path": "date-slot-picker.tsx",
|
|
13
|
-
"content": "\"use client\";\n\nimport React, { useState, useMemo, useCallback } from \"react\";\nimport { Tabs } from \"@base-ui/react/tabs\";\nimport type { AvailableSlot, DayAvailability } from \"@cimplify/sdk\";\nimport type { DurationUnit, SchedulingMode } from \"@cimplify/sdk\";\nimport type { CurrencyCode } from \"@cimplify/sdk\";\nimport { useServiceAvailability } from \"@cimplify/sdk/react\";\nimport { SlotPicker } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface DateSlotPickerClassNames {\n root?: string;\n dateStrip?: string;\n dateButton?: string;\n nav?: string;\n navButton?: string;\n slots?: string;\n loading?: string;\n}\n\nexport interface DateSlotPickerProps {\n /** Service ID to fetch availability and slots for. */\n serviceId: string;\n /** Number of days to show in the date strip. Default: 7. */\n daysToShow?: number;\n /** Number of participants. */\n participantCount?: number;\n /** Whole units needed — forwarded to availability so short days drop out. */\n units?: number;\n /** Currently selected slot. */\n selectedSlot?: AvailableSlot | null;\n /** Called when a slot is selected. */\n onSlotSelect?: (slot: AvailableSlot, date: string) => void;\n /** Pre-fetched availability data (skips fetch). */\n availability?: DayAvailability[];\n /** Show price on slots. Default: true. */\n showPrice?: boolean;\n /** Currency the slot prices are denominated in. */\n currency?: CurrencyCode;\n /** Forwarded to `<SlotPicker>` to render multi-day stay labels. */\n schedulingMode?: SchedulingMode;\n /** Forwarded to `<SlotPicker>` — unit for the stay summary in multi-day mode. */\n durationUnit?: DurationUnit;\n /** Forwarded to `<SlotPicker>` — value for the stay summary in multi-day mode. */\n durationValue?: number;\n /** Forwarded to `<SlotPicker>` — hide slots already in the past. Default: true. */\n hideElapsedSlots?: boolean;\n /** Forwarded to `<SlotPicker>` — minimum lead time in minutes. Default: 0. */\n minLeadMinutes?: number;\n className?: string;\n classNames?: DateSlotPickerClassNames;\n}\n\nfunction formatDate(dateStr: string): string {\n const date = new Date(dateStr + \"T00:00:00\");\n return date.toLocaleDateString(undefined, { weekday: \"short\", month: \"short\", day: \"numeric\" });\n}\n\nfunction toDateString(date: Date): string {\n return date.toISOString().split(\"T\")[0];\n}\n\nfunction addDays(date: Date, days: number): Date {\n const result = new Date(date);\n result.setDate(result.getDate() + days);\n return result;\n}\n\nexport function DateSlotPicker({\n serviceId,\n daysToShow = 7,\n participantCount,\n units,\n selectedSlot,\n onSlotSelect,\n availability: availabilityProp,\n showPrice = true,\n currency,\n schedulingMode,\n durationUnit,\n durationValue,\n hideElapsedSlots = true,\n minLeadMinutes = 0,\n className,\n classNames,\n}: DateSlotPickerProps): React.ReactElement {\n const [offset, setOffset] = useState(0);\n const [selectedDate, setSelectedDate] = useState<string>(toDateString(new Date()));\n\n const dateRange = useMemo(() => {\n const today = new Date();\n const start = addDays(today, offset);\n const dates: string[] = [];\n for (let i = 0; i < daysToShow; i++) {\n dates.push(toDateString(addDays(start, i)));\n }\n return {\n dates,\n startDate: dates[0],\n endDate: dates[dates.length - 1],\n };\n }, [offset, daysToShow]);\n\n const { days: fetchedDays, isLoading: availabilityLoading } = useServiceAvailability(\n serviceId,\n dateRange.startDate,\n dateRange.endDate,\n {\n participantCount,\n units,\n enabled: availabilityProp === undefined,\n },\n );\n\n const days = availabilityProp ?? fetchedDays;\n\n const availabilityMap = useMemo(() => {\n const map = new Map<string, DayAvailability>();\n for (const day of days) {\n map.set(day.date, day);\n }\n return map;\n }, [days]);\n\n const handlePrev = useCallback(() => {\n setOffset((prev) => Math.max(0, prev - daysToShow));\n }, [daysToShow]);\n\n const handleNext = useCallback(() => {\n setOffset((prev) => prev + daysToShow);\n }, [daysToShow]);\n\n const handleDateChange = useCallback((value: string | number | null) => {\n if (typeof value === \"string\") {\n setSelectedDate(value);\n }\n }, []);\n\n const handleSlotSelect = useCallback(\n (slot: AvailableSlot) => {\n onSlotSelect?.(slot, selectedDate);\n },\n [onSlotSelect, selectedDate],\n );\n\n return (\n <Tabs.Root\n value={selectedDate}\n onValueChange={handleDateChange}\n data-cimplify-date-slot-picker\n className={cn(\"flex flex-col gap-4\", className, classNames?.root)}\n >\n <div\n data-cimplify-date-nav\n className={cn(\"flex items-center justify-end gap-2\", classNames?.nav)}\n >\n <button\n type=\"button\"\n onClick={handlePrev}\n disabled={offset === 0}\n aria-label=\"Previous dates\"\n data-cimplify-date-nav-prev\n className={cn(\n \"grid place-items-center w-8 h-8 rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40\",\n classNames?.navButton,\n )}\n >\n ←\n </button>\n <button\n type=\"button\"\n onClick={handleNext}\n aria-label=\"Next dates\"\n data-cimplify-date-nav-next\n className={cn(\n \"grid place-items-center w-8 h-8 rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\",\n classNames?.navButton,\n )}\n >\n →\n </button>\n </div>\n\n <Tabs.List\n data-cimplify-date-strip\n className={cn(\"grid grid-cols-7 gap-1 sm:gap-2\", classNames?.dateStrip)}\n >\n {dateRange.dates.map((date) => {\n const dayInfo = availabilityMap.get(date);\n const hasAvailability = dayInfo?.has_availability !== false;\n const isSelected = selectedDate === date;\n return (\n <Tabs.Tab\n key={date}\n value={date}\n data-cimplify-date-button\n data-selected={isSelected || undefined}\n data-available={hasAvailability || undefined}\n data-fully-booked={(!hasAvailability) || undefined}\n className={cn(\n \"flex flex-col items-center justify-center rounded-md border border-border bg-background px-1 py-2 text-center text-xs font-medium text-foreground transition-colors hover:border-foreground/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[selected]:border-foreground data-[selected]:bg-foreground data-[selected]:text-background data-[fully-booked]:cursor-not-allowed data-[fully-booked]:opacity-40\",\n classNames?.dateButton,\n )}\n >\n {formatDate(date)}\n </Tabs.Tab>\n );\n })}\n </Tabs.List>\n\n {availabilityLoading && (\n <div\n data-cimplify-date-slot-loading\n aria-busy=\"true\"\n className={cn(\"h-32 rounded-md bg-muted/40 animate-pulse\", classNames?.loading)}\n />\n )}\n\n <div data-cimplify-date-slots className={classNames?.slots}>\n <SlotPicker\n serviceId={serviceId}\n date={selectedDate}\n participantCount={participantCount}\n selectedSlot={selectedSlot}\n onSlotSelect={handleSlotSelect}\n showPrice={showPrice}\n currency={currency}\n schedulingMode={schedulingMode}\n durationUnit={durationUnit}\n durationValue={durationValue}\n hideElapsedSlots={hideElapsedSlots}\n minLeadMinutes={minLeadMinutes}\n />\n </div>\n </Tabs.Root>\n );\n}\n"
|
|
13
|
+
"content": "\"use client\";\n\nimport React, { useState, useMemo, useCallback } from \"react\";\nimport { Tabs } from \"@base-ui/react/tabs\";\nimport type { AvailableSlot, DayAvailability } from \"@cimplify/sdk\";\nimport type { DurationUnit, SchedulingMode } from \"@cimplify/sdk\";\nimport type { CurrencyCode } from \"@cimplify/sdk\";\nimport { useServiceAvailability } from \"@cimplify/sdk/react\";\nimport { SlotPicker } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface DateSlotPickerClassNames {\n root?: string;\n dateStrip?: string;\n dateButton?: string;\n nav?: string;\n navButton?: string;\n slots?: string;\n loading?: string;\n}\n\nexport interface DateSlotPickerProps {\n /** Service ID to fetch availability and slots for. */\n serviceId: string;\n /** Number of days to show in the date strip. Default: 7. */\n daysToShow?: number;\n /** Number of participants. */\n participantCount?: number;\n /** Whole units needed — forwarded to availability so short days drop out. */\n units?: number;\n /** Currently selected slot. */\n selectedSlot?: AvailableSlot | null;\n /** Called when a slot is selected. */\n onSlotSelect?: (slot: AvailableSlot, date: string) => void;\n /** Pre-fetched availability data (skips fetch). */\n availability?: DayAvailability[];\n /** Show price on slots. Default: true. */\n showPrice?: boolean;\n /** Currency the slot prices are denominated in. */\n currency?: CurrencyCode;\n /** Forwarded to `<SlotPicker>` to render multi-day stay labels. */\n schedulingMode?: SchedulingMode;\n /** Forwarded to `<SlotPicker>` — unit for the stay summary in multi-day mode. */\n durationUnit?: DurationUnit;\n /** Forwarded to `<SlotPicker>` — value for the stay summary in multi-day mode. */\n durationValue?: number;\n /** Forwarded to `<SlotPicker>` — hide slots already in the past. Default: true. */\n hideElapsedSlots?: boolean;\n /** Forwarded to `<SlotPicker>` — minimum lead time in minutes. Default: 0. */\n minLeadMinutes?: number;\n /** Forwarded to `<SlotPicker>` — offered when a day has no bookable times. */\n contactPhone?: string;\n className?: string;\n classNames?: DateSlotPickerClassNames;\n}\n\nfunction formatDate(dateStr: string): string {\n const date = new Date(dateStr + \"T00:00:00\");\n return date.toLocaleDateString(undefined, { weekday: \"short\", month: \"short\", day: \"numeric\" });\n}\n\nfunction toDateString(date: Date): string {\n return date.toISOString().split(\"T\")[0];\n}\n\nfunction addDays(date: Date, days: number): Date {\n const result = new Date(date);\n result.setDate(result.getDate() + days);\n return result;\n}\n\nexport function DateSlotPicker({\n serviceId,\n daysToShow = 7,\n participantCount,\n units,\n selectedSlot,\n onSlotSelect,\n availability: availabilityProp,\n showPrice = true,\n currency,\n schedulingMode,\n durationUnit,\n durationValue,\n hideElapsedSlots = true,\n minLeadMinutes = 0,\n contactPhone,\n className,\n classNames,\n}: DateSlotPickerProps): React.ReactElement {\n const [offset, setOffset] = useState(0);\n const [selectedDate, setSelectedDate] = useState<string>(toDateString(new Date()));\n\n const dateRange = useMemo(() => {\n const today = new Date();\n const start = addDays(today, offset);\n const dates: string[] = [];\n for (let i = 0; i < daysToShow; i++) {\n dates.push(toDateString(addDays(start, i)));\n }\n return {\n dates,\n startDate: dates[0],\n endDate: dates[dates.length - 1],\n };\n }, [offset, daysToShow]);\n\n const { days: fetchedDays, isLoading: availabilityLoading } = useServiceAvailability(\n serviceId,\n dateRange.startDate,\n dateRange.endDate,\n {\n participantCount,\n units,\n enabled: availabilityProp === undefined,\n },\n );\n\n const days = availabilityProp ?? fetchedDays;\n\n const availabilityMap = useMemo(() => {\n const map = new Map<string, DayAvailability>();\n for (const day of days) {\n map.set(day.date, day);\n }\n return map;\n }, [days]);\n\n const handlePrev = useCallback(() => {\n setOffset((prev) => Math.max(0, prev - daysToShow));\n }, [daysToShow]);\n\n const handleNext = useCallback(() => {\n setOffset((prev) => prev + daysToShow);\n }, [daysToShow]);\n\n const handleDateChange = useCallback((value: string | number | null) => {\n if (typeof value === \"string\") {\n setSelectedDate(value);\n }\n }, []);\n\n const handleSlotSelect = useCallback(\n (slot: AvailableSlot) => {\n onSlotSelect?.(slot, selectedDate);\n },\n [onSlotSelect, selectedDate],\n );\n\n return (\n <Tabs.Root\n value={selectedDate}\n onValueChange={handleDateChange}\n data-cimplify-date-slot-picker\n className={cn(\"flex flex-col gap-4\", className, classNames?.root)}\n >\n <div\n data-cimplify-date-nav\n className={cn(\"flex items-center justify-end gap-2\", classNames?.nav)}\n >\n <button\n type=\"button\"\n onClick={handlePrev}\n disabled={offset === 0}\n aria-label=\"Previous dates\"\n data-cimplify-date-nav-prev\n className={cn(\n \"grid place-items-center w-8 h-8 rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40\",\n classNames?.navButton,\n )}\n >\n ←\n </button>\n <button\n type=\"button\"\n onClick={handleNext}\n aria-label=\"Next dates\"\n data-cimplify-date-nav-next\n className={cn(\n \"grid place-items-center w-8 h-8 rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\",\n classNames?.navButton,\n )}\n >\n →\n </button>\n </div>\n\n <Tabs.List\n data-cimplify-date-strip\n className={cn(\"grid grid-cols-7 gap-1 sm:gap-2\", classNames?.dateStrip)}\n >\n {dateRange.dates.map((date) => {\n const dayInfo = availabilityMap.get(date);\n const hasAvailability = dayInfo?.has_availability !== false;\n const isSelected = selectedDate === date;\n return (\n <Tabs.Tab\n key={date}\n value={date}\n data-cimplify-date-button\n data-selected={isSelected || undefined}\n data-available={hasAvailability || undefined}\n data-fully-booked={(!hasAvailability) || undefined}\n className={cn(\n \"flex flex-col items-center justify-center rounded-md border border-border bg-background px-1 py-2 text-center text-xs font-medium text-foreground transition-colors hover:border-foreground/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[selected]:border-foreground data-[selected]:bg-foreground data-[selected]:text-background data-[fully-booked]:cursor-not-allowed data-[fully-booked]:opacity-40\",\n classNames?.dateButton,\n )}\n >\n {formatDate(date)}\n </Tabs.Tab>\n );\n })}\n </Tabs.List>\n\n {availabilityLoading && (\n <div\n data-cimplify-date-slot-loading\n aria-busy=\"true\"\n className={cn(\"h-32 rounded-md bg-muted/40 animate-pulse\", classNames?.loading)}\n />\n )}\n\n <div data-cimplify-date-slots className={classNames?.slots}>\n <SlotPicker\n serviceId={serviceId}\n date={selectedDate}\n participantCount={participantCount}\n selectedSlot={selectedSlot}\n onSlotSelect={handleSlotSelect}\n showPrice={showPrice}\n currency={currency}\n schedulingMode={schedulingMode}\n durationUnit={durationUnit}\n durationValue={durationValue}\n hideElapsedSlots={hideElapsedSlots}\n minLeadMinutes={minLeadMinutes}\n contactPhone={contactPhone}\n />\n </div>\n </Tabs.Root>\n );\n}\n"
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
{
|
|
12
12
|
"path": "slot-picker.tsx",
|
|
13
|
-
"content": "\"use client\";\n\nimport { Radio } from \"@base-ui/react/radio\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport React, { useMemo } from \"react\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\nimport type { DurationUnit, SchedulingMode } from \"@cimplify/sdk\";\nimport type { CurrencyCode } from \"@cimplify/sdk\";\nimport { useAvailableSlots } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface SlotPickerClassNames {\n root?: string;\n group?: string;\n groupLabel?: string;\n slot?: string;\n slotTime?: string;\n slotPrice?: string;\n slotScarcity?: string;\n loading?: string;\n empty?: string;\n}\n\nexport interface SlotPickerProps {\n /** Pre-fetched slots (skips fetch). */\n slots?: AvailableSlot[];\n /** Service ID — used to fetch slots when `slots` prop is not provided. */\n serviceId?: string;\n /** Date string (YYYY-MM-DD) — used to fetch slots when `slots` prop is not provided. */\n date?: string;\n /** Number of participants for capacity-based availability. */\n participantCount?: number;\n /** Currently selected slot. */\n selectedSlot?: AvailableSlot | null;\n /** Called when a slot is selected. */\n onSlotSelect?: (slot: AvailableSlot) => void;\n /** Whether to group slots by time of day. Default: true. Ignored when `schedulingMode` is `\"multi_day\"`. */\n groupByTimeOfDay?: boolean;\n /** Show price on each slot. Default: true. */\n showPrice?: boolean;\n /**\n * Show a \"N left\" urgency tag when a slot's remaining capacity is at or below\n * this number (and above zero). Slots at zero capacity render as sold out.\n * Default: 3. Set to 0 to disable scarcity tags.\n */\n lowCapacityThreshold?: number;\n /** Currency the slot prices are denominated in. */\n currency?: CurrencyCode;\n /**\n * Hide slots whose `start_time` is already in the past. Default: true.\n * Set to false to show elapsed slots greyed-out (still un-selectable).\n */\n hideElapsedSlots?: boolean;\n /**\n * Minimum lead time (in minutes) before a slot can be booked. Slots whose\n * start is sooner than `now + minLeadMinutes` are filtered out (when\n * `hideElapsedSlots` is true) or marked unavailable. Default: 0.\n */\n minLeadMinutes?: number;\n /**\n * Service scheduling mode. When `\"multi_day\"`, each slot renders as a\n * stay summary (`\"3 nights: Fri Apr 5, 3:00 PM → Mon Apr 8, 11:00 AM\"`)\n * instead of the time-of-day label. Defaults to `\"intraday\"`.\n */\n schedulingMode?: SchedulingMode;\n /** Service duration unit — used for the stay summary in multi-day mode. */\n durationUnit?: DurationUnit;\n /** Service duration value — used for the stay summary in multi-day mode. */\n durationValue?: number;\n /** Text shown when no slots available. */\n emptyMessage?: string;\n className?: string;\n classNames?: SlotPickerClassNames;\n}\n\ninterface SlotGroup {\n label: string;\n slots: AvailableSlot[];\n}\n\nfunction getTimeOfDay(timeStr: string): \"morning\" | \"afternoon\" | \"evening\" {\n const hour = parseInt(timeStr.split(\"T\").pop()?.split(\":\")[0] ?? timeStr.split(\":\")[0], 10);\n if (hour < 12) return \"morning\";\n if (hour < 17) return \"afternoon\";\n return \"evening\";\n}\n\nconst TIME_OF_DAY_LABELS: Record<string, string> = {\n morning: \"Morning\",\n afternoon: \"Afternoon\",\n evening: \"Evening\",\n};\n\nfunction groupSlots(slots: AvailableSlot[]): SlotGroup[] {\n const groups: Record<string, AvailableSlot[]> = {};\n for (const slot of slots) {\n const tod = getTimeOfDay(slot.start_time);\n if (!groups[tod]) groups[tod] = [];\n groups[tod].push(slot);\n }\n return ([\"morning\", \"afternoon\", \"evening\"] as const)\n .filter((tod) => groups[tod]?.length)\n .map((tod) => ({ label: TIME_OF_DAY_LABELS[tod], slots: groups[tod] }));\n}\n\nfunction formatTime(timeStr: string): string {\n try {\n const date = new Date(timeStr);\n if (!isNaN(date.getTime())) {\n return date.toLocaleTimeString(undefined, { hour: \"numeric\", minute: \"2-digit\" });\n }\n } catch {\n // noop\n }\n\n const parts = timeStr.split(\":\");\n if (parts.length >= 2) {\n const hour = parseInt(parts[0], 10);\n const minute = parts[1];\n const ampm = hour >= 12 ? \"PM\" : \"AM\";\n const displayHour = hour % 12 || 12;\n return `${displayHour}:${minute} ${ampm}`;\n }\n return timeStr;\n}\n\nfunction pluralizeUnit(unit: DurationUnit | undefined, value: number | undefined): string {\n if (!unit) return value === 1 ? \"day\" : \"days\";\n const v = value ?? 1;\n if (unit === \"minutes\") return v === 1 ? \"minute\" : \"minutes\";\n if (unit === \"hours\") return v === 1 ? \"hour\" : \"hours\";\n if (unit === \"days\") return v === 1 ? \"day\" : \"days\";\n if (unit === \"weeks\") return v === 1 ? \"week\" : \"weeks\";\n if (unit === \"months\") return v === 1 ? \"month\" : \"months\";\n return unit;\n}\n\nfunction formatStaySummary(\n slot: AvailableSlot,\n durationUnit: DurationUnit | undefined,\n durationValue: number | undefined,\n): string {\n const start = new Date(slot.start_time);\n const end = new Date(slot.end_time);\n const startLabel = start.toLocaleString(undefined, {\n weekday: \"short\",\n month: \"short\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n });\n const endLabel = end.toLocaleString(undefined, {\n weekday: \"short\",\n month: \"short\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n });\n const unitLabel = pluralizeUnit(durationUnit, durationValue);\n if (durationValue !== undefined) {\n return `${durationValue} ${unitLabel}: ${startLabel} → ${endLabel}`;\n }\n return `${startLabel} → ${endLabel}`;\n}\n\nfunction slotToValue(slot: AvailableSlot): string {\n return `${slot.start_time}|${slot.end_time}`;\n}\n\nexport function SlotPicker({\n slots: slotsProp,\n serviceId,\n date,\n participantCount,\n selectedSlot,\n onSlotSelect,\n groupByTimeOfDay = true,\n showPrice = true,\n lowCapacityThreshold = 3,\n currency,\n schedulingMode = \"intraday\",\n durationUnit,\n durationValue,\n hideElapsedSlots = true,\n minLeadMinutes = 0,\n emptyMessage = \"No available slots\",\n className,\n classNames,\n}: SlotPickerProps): React.ReactElement {\n const isMultiDay = schedulingMode === \"multi_day\";\n const { slots: fetched, isLoading } = useAvailableSlots(\n serviceId ?? null,\n date ?? null,\n {\n participantCount,\n enabled: slotsProp === undefined && !!serviceId && !!date,\n },\n );\n\n const rawSlots = slotsProp ?? fetched;\n // Drop slots that have already elapsed (or fall within the lead-time\n // window). Default behaviour because nothing the merchant can do at\n // the backend stops a client clock from being slightly ahead of the\n // last availability response — this is the same defence other\n // booking flows ship by default.\n const slots = useMemo(() => {\n if (!hideElapsedSlots) return rawSlots;\n const cutoff = Date.now() + minLeadMinutes * 60_000;\n return rawSlots.filter((slot) => {\n const start = Date.parse(slot.start_time);\n return Number.isNaN(start) || start >= cutoff;\n });\n }, [rawSlots, hideElapsedSlots, minLeadMinutes]);\n\n if (isLoading && slots.length === 0) {\n return (\n <div\n data-cimplify-slot-picker\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (slots.length === 0) {\n return (\n <div\n data-cimplify-slot-picker\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage}</p>\n </div>\n );\n }\n\n const groups = groupByTimeOfDay && !isMultiDay\n ? groupSlots(slots)\n : [{ label: \"\", slots }];\n\n const slotsByValue = new Map<string, AvailableSlot>();\n for (const slot of slots) {\n slotsByValue.set(slotToValue(slot), slot);\n }\n\n const selectedValue = selectedSlot ? slotToValue(selectedSlot) : \"\";\n\n return (\n <RadioGroup\n data-cimplify-slot-picker\n className={cn(\"flex flex-col gap-4\", className, classNames?.root)}\n value={selectedValue}\n onValueChange={(value: string) => {\n const slot = slotsByValue.get(value);\n // Slots default to available; treat as unavailable only when the\n // backend explicitly returns `is_available: false`.\n if (slot && slot.is_available !== false) {\n onSlotSelect?.(slot);\n }\n }}\n >\n {groups.map((group) => (\n <div\n key={group.label || \"all\"}\n data-cimplify-slot-group\n className={cn(\"flex flex-col gap-2\", classNames?.group)}\n >\n {group.label && (\n <div\n data-cimplify-slot-group-label\n className={cn(\n \"text-xs font-medium uppercase tracking-[0.12em] text-muted-foreground\",\n classNames?.groupLabel,\n )}\n >\n {group.label}\n </div>\n )}\n <div\n className={cn(\n isMultiDay\n ? \"flex flex-col gap-2\"\n : \"grid grid-cols-3 sm:grid-cols-4 gap-2\",\n )}\n >\n {group.slots.map((slot) => {\n const value = slotToValue(slot);\n const isSelected =\n selectedSlot?.start_time === slot.start_time &&\n selectedSlot?.end_time === slot.end_time;\n const capacity = slot.capacity_available ?? slot.remaining_capacity;\n const soldOut = capacity === 0;\n const unavailable = slot.is_available === false || soldOut;\n const lowCapacity =\n lowCapacityThreshold > 0 &&\n capacity != null &&\n capacity > 0 &&\n capacity <= lowCapacityThreshold;\n return (\n <Radio.Root\n key={value}\n value={value}\n disabled={unavailable}\n data-cimplify-slot\n data-selected={isSelected || undefined}\n data-unavailable={unavailable || undefined}\n className={cn(\n \"inline-flex items-center justify-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm font-medium text-foreground transition-colors hover:border-foreground/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[selected]:border-foreground data-[selected]:bg-foreground data-[selected]:text-background data-[unavailable]:cursor-not-allowed data-[unavailable]:opacity-40 data-[unavailable]:line-through\",\n isMultiDay && \"justify-between text-left\",\n classNames?.slot,\n )}\n >\n <span data-cimplify-slot-time className={classNames?.slotTime}>\n {isMultiDay\n ? formatStaySummary(slot, durationUnit, durationValue)\n : formatTime(slot.start_time)}\n </span>\n {showPrice && slot.price && (\n <span\n data-cimplify-slot-price\n className={cn(\"text-xs opacity-70\", classNames?.slotPrice)}\n >\n <Price amount={slot.price} currency={currency} />\n </span>\n )}\n {lowCapacity && (\n <span\n data-cimplify-slot-scarcity\n className={cn(\"text-xs font-medium text-amber-600\", classNames?.slotScarcity)}\n >\n {capacity} left\n </span>\n )}\n </Radio.Root>\n );\n })}\n </div>\n </div>\n ))}\n </RadioGroup>\n );\n}\n"
|
|
13
|
+
"content": "\"use client\";\n\nimport { Radio } from \"@base-ui/react/radio\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport React, { useMemo } from \"react\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\nimport type { DurationUnit, SchedulingMode } from \"@cimplify/sdk\";\nimport type { CurrencyCode } from \"@cimplify/sdk\";\nimport { useAvailableSlots } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface SlotPickerClassNames {\n root?: string;\n group?: string;\n groupLabel?: string;\n slot?: string;\n slotTime?: string;\n slotPrice?: string;\n slotScarcity?: string;\n loading?: string;\n empty?: string;\n}\n\nexport interface SlotPickerProps {\n /** Pre-fetched slots (skips fetch). */\n slots?: AvailableSlot[];\n /** Service ID — used to fetch slots when `slots` prop is not provided. */\n serviceId?: string;\n /** Date string (YYYY-MM-DD) — used to fetch slots when `slots` prop is not provided. */\n date?: string;\n /** Number of participants for capacity-based availability. */\n participantCount?: number;\n /** Currently selected slot. */\n selectedSlot?: AvailableSlot | null;\n /** Called when a slot is selected. */\n onSlotSelect?: (slot: AvailableSlot) => void;\n /** Whether to group slots by time of day. Default: true. Ignored when `schedulingMode` is `\"multi_day\"`. */\n groupByTimeOfDay?: boolean;\n /** Show price on each slot. Default: true. */\n showPrice?: boolean;\n /**\n * Show a \"N left\" urgency tag when a slot's remaining capacity is at or below\n * this number (and above zero). Slots at zero capacity render as sold out.\n * Default: 3. Set to 0 to disable scarcity tags.\n */\n lowCapacityThreshold?: number;\n /** Currency the slot prices are denominated in. */\n currency?: CurrencyCode;\n /**\n * Hide slots whose `start_time` is already in the past. Default: true.\n * Set to false to show elapsed slots greyed-out (still un-selectable).\n */\n hideElapsedSlots?: boolean;\n /**\n * Minimum lead time (in minutes) before a slot can be booked. Slots whose\n * start is sooner than `now + minLeadMinutes` are filtered out (when\n * `hideElapsedSlots` is true) or marked unavailable. Default: 0.\n */\n minLeadMinutes?: number;\n /**\n * Service scheduling mode. When `\"multi_day\"`, each slot renders as a\n * stay summary (`\"3 nights: Fri Apr 5, 3:00 PM → Mon Apr 8, 11:00 AM\"`)\n * instead of the time-of-day label. Defaults to `\"intraday\"`.\n */\n schedulingMode?: SchedulingMode;\n /** Service duration unit — used for the stay summary in multi-day mode. */\n durationUnit?: DurationUnit;\n /** Service duration value — used for the stay summary in multi-day mode. */\n durationValue?: number;\n /** Text shown when no slots available. */\n emptyMessage?: string;\n /**\n * Phone number offered when a day has no bookable times. A service can be\n * unbookable for reasons the storefront cannot see, and a bare empty grid\n * reads as a broken site rather than a shop to call.\n */\n contactPhone?: string;\n className?: string;\n classNames?: SlotPickerClassNames;\n}\n\ninterface SlotGroup {\n label: string;\n slots: AvailableSlot[];\n}\n\nfunction getTimeOfDay(timeStr: string): \"morning\" | \"afternoon\" | \"evening\" {\n const hour = parseInt(timeStr.split(\"T\").pop()?.split(\":\")[0] ?? timeStr.split(\":\")[0], 10);\n if (hour < 12) return \"morning\";\n if (hour < 17) return \"afternoon\";\n return \"evening\";\n}\n\nconst TIME_OF_DAY_LABELS: Record<string, string> = {\n morning: \"Morning\",\n afternoon: \"Afternoon\",\n evening: \"Evening\",\n};\n\nfunction groupSlots(slots: AvailableSlot[]): SlotGroup[] {\n const groups: Record<string, AvailableSlot[]> = {};\n for (const slot of slots) {\n const tod = getTimeOfDay(slot.start_time);\n if (!groups[tod]) groups[tod] = [];\n groups[tod].push(slot);\n }\n return ([\"morning\", \"afternoon\", \"evening\"] as const)\n .filter((tod) => groups[tod]?.length)\n .map((tod) => ({ label: TIME_OF_DAY_LABELS[tod], slots: groups[tod] }));\n}\n\nfunction formatTime(timeStr: string): string {\n try {\n const date = new Date(timeStr);\n if (!isNaN(date.getTime())) {\n return date.toLocaleTimeString(undefined, { hour: \"numeric\", minute: \"2-digit\" });\n }\n } catch {\n // noop\n }\n\n const parts = timeStr.split(\":\");\n if (parts.length >= 2) {\n const hour = parseInt(parts[0], 10);\n const minute = parts[1];\n const ampm = hour >= 12 ? \"PM\" : \"AM\";\n const displayHour = hour % 12 || 12;\n return `${displayHour}:${minute} ${ampm}`;\n }\n return timeStr;\n}\n\nfunction pluralizeUnit(unit: DurationUnit | undefined, value: number | undefined): string {\n if (!unit) return value === 1 ? \"day\" : \"days\";\n const v = value ?? 1;\n if (unit === \"minutes\") return v === 1 ? \"minute\" : \"minutes\";\n if (unit === \"hours\") return v === 1 ? \"hour\" : \"hours\";\n if (unit === \"days\") return v === 1 ? \"day\" : \"days\";\n if (unit === \"weeks\") return v === 1 ? \"week\" : \"weeks\";\n if (unit === \"months\") return v === 1 ? \"month\" : \"months\";\n return unit;\n}\n\nfunction formatStaySummary(\n slot: AvailableSlot,\n durationUnit: DurationUnit | undefined,\n durationValue: number | undefined,\n): string {\n const start = new Date(slot.start_time);\n const end = new Date(slot.end_time);\n const startLabel = start.toLocaleString(undefined, {\n weekday: \"short\",\n month: \"short\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n });\n const endLabel = end.toLocaleString(undefined, {\n weekday: \"short\",\n month: \"short\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n });\n const unitLabel = pluralizeUnit(durationUnit, durationValue);\n if (durationValue !== undefined) {\n return `${durationValue} ${unitLabel}: ${startLabel} → ${endLabel}`;\n }\n return `${startLabel} → ${endLabel}`;\n}\n\nfunction slotToValue(slot: AvailableSlot): string {\n return `${slot.start_time}|${slot.end_time}`;\n}\n\nexport function SlotPicker({\n slots: slotsProp,\n serviceId,\n date,\n participantCount,\n selectedSlot,\n onSlotSelect,\n groupByTimeOfDay = true,\n showPrice = true,\n lowCapacityThreshold = 3,\n currency,\n schedulingMode = \"intraday\",\n durationUnit,\n durationValue,\n hideElapsedSlots = true,\n minLeadMinutes = 0,\n emptyMessage = \"No available slots\",\n contactPhone,\n className,\n classNames,\n}: SlotPickerProps): React.ReactElement {\n const isMultiDay = schedulingMode === \"multi_day\";\n const { slots: fetched, isLoading } = useAvailableSlots(\n serviceId ?? null,\n date ?? null,\n {\n participantCount,\n enabled: slotsProp === undefined && !!serviceId && !!date,\n },\n );\n\n const rawSlots = slotsProp ?? fetched;\n // Drop slots that have already elapsed (or fall within the lead-time\n // window). Default behaviour because nothing the merchant can do at\n // the backend stops a client clock from being slightly ahead of the\n // last availability response — this is the same defence other\n // booking flows ship by default.\n const slots = useMemo(() => {\n if (!hideElapsedSlots) return rawSlots;\n const cutoff = Date.now() + minLeadMinutes * 60_000;\n return rawSlots.filter((slot) => {\n const start = Date.parse(slot.start_time);\n return Number.isNaN(start) || start >= cutoff;\n });\n }, [rawSlots, hideElapsedSlots, minLeadMinutes]);\n\n if (isLoading && slots.length === 0) {\n return (\n <div\n data-cimplify-slot-picker\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (slots.length === 0) {\n return (\n <div\n data-cimplify-slot-picker\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage}</p>\n {contactPhone ? (\n <p>\n <a href={`tel:${contactPhone.replace(/\\s+/g, \"\")}`}>Call {contactPhone} to book</a>\n </p>\n ) : null}\n </div>\n );\n }\n\n const groups = groupByTimeOfDay && !isMultiDay\n ? groupSlots(slots)\n : [{ label: \"\", slots }];\n\n const slotsByValue = new Map<string, AvailableSlot>();\n for (const slot of slots) {\n slotsByValue.set(slotToValue(slot), slot);\n }\n\n const selectedValue = selectedSlot ? slotToValue(selectedSlot) : \"\";\n\n return (\n <RadioGroup\n data-cimplify-slot-picker\n className={cn(\"flex flex-col gap-4\", className, classNames?.root)}\n value={selectedValue}\n onValueChange={(value: string) => {\n const slot = slotsByValue.get(value);\n // Slots default to available; treat as unavailable only when the\n // backend explicitly returns `is_available: false`.\n if (slot && slot.is_available !== false) {\n onSlotSelect?.(slot);\n }\n }}\n >\n {groups.map((group) => (\n <div\n key={group.label || \"all\"}\n data-cimplify-slot-group\n className={cn(\"flex flex-col gap-2\", classNames?.group)}\n >\n {group.label && (\n <div\n data-cimplify-slot-group-label\n className={cn(\n \"text-xs font-medium uppercase tracking-[0.12em] text-muted-foreground\",\n classNames?.groupLabel,\n )}\n >\n {group.label}\n </div>\n )}\n <div\n className={cn(\n isMultiDay\n ? \"flex flex-col gap-2\"\n : \"grid grid-cols-3 sm:grid-cols-4 gap-2\",\n )}\n >\n {group.slots.map((slot) => {\n const value = slotToValue(slot);\n const isSelected =\n selectedSlot?.start_time === slot.start_time &&\n selectedSlot?.end_time === slot.end_time;\n const capacity = slot.capacity_available ?? slot.remaining_capacity;\n const soldOut = capacity === 0;\n const unavailable = slot.is_available === false || soldOut;\n const lowCapacity =\n lowCapacityThreshold > 0 &&\n capacity != null &&\n capacity > 0 &&\n capacity <= lowCapacityThreshold;\n return (\n <Radio.Root\n key={value}\n value={value}\n disabled={unavailable}\n data-cimplify-slot\n data-selected={isSelected || undefined}\n data-unavailable={unavailable || undefined}\n className={cn(\n \"inline-flex items-center justify-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm font-medium text-foreground transition-colors hover:border-foreground/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[selected]:border-foreground data-[selected]:bg-foreground data-[selected]:text-background data-[unavailable]:cursor-not-allowed data-[unavailable]:opacity-40 data-[unavailable]:line-through\",\n isMultiDay && \"justify-between text-left\",\n classNames?.slot,\n )}\n >\n <span data-cimplify-slot-time className={classNames?.slotTime}>\n {isMultiDay\n ? formatStaySummary(slot, durationUnit, durationValue)\n : formatTime(slot.start_time)}\n </span>\n {showPrice && slot.price && (\n <span\n data-cimplify-slot-price\n className={cn(\"text-xs opacity-70\", classNames?.slotPrice)}\n >\n <Price amount={slot.price} currency={currency} />\n </span>\n )}\n {lowCapacity && (\n <span\n data-cimplify-slot-scarcity\n className={cn(\"text-xs font-medium text-amber-600\", classNames?.slotScarcity)}\n >\n {capacity} left\n </span>\n )}\n </Radio.Root>\n );\n })}\n </div>\n </div>\n ))}\n </RadioGroup>\n );\n}\n"
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
}
|