@clubmed/trident-ui 2.1.0-beta.3 → 2.1.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +1 -0
  2. package/chunks/ChatMessage.js +282 -265
  3. package/chunks/ChatMessage.js.map +1 -1
  4. package/examples/chat-body-demo.js +4 -2
  5. package/examples/chat-body-demo.js.map +1 -1
  6. package/examples/chat-button-demo.js +2 -1
  7. package/examples/chat-button-demo.js.map +1 -1
  8. package/examples/chat-button-offset-demo.js +2 -1
  9. package/examples/chat-button-offset-demo.js.map +1 -1
  10. package/examples/chat-header-demo.js +2 -1
  11. package/examples/chat-header-demo.js.map +1 -1
  12. package/examples/chat-input-demo.js +2 -1
  13. package/examples/chat-input-demo.js.map +1 -1
  14. package/examples/chat-input-streaming-demo.d.ts +1 -0
  15. package/examples/chat-input-streaming-demo.js +30 -0
  16. package/examples/chat-input-streaming-demo.js.map +1 -0
  17. package/examples/chat-input-submit-on-enter-demo.d.ts +1 -0
  18. package/examples/chat-input-submit-on-enter-demo.js +23 -0
  19. package/examples/chat-input-submit-on-enter-demo.js.map +1 -0
  20. package/examples/chat-message-actions-demo.d.ts +1 -0
  21. package/examples/chat-message-actions-demo.js +29 -0
  22. package/examples/chat-message-actions-demo.js.map +1 -0
  23. package/examples/chat-message-demo.js +1 -0
  24. package/examples/chat-message-demo.js.map +1 -1
  25. package/examples/chat-message-editor-demo.d.ts +1 -0
  26. package/examples/chat-message-editor-demo.js +30 -0
  27. package/examples/chat-message-editor-demo.js.map +1 -0
  28. package/examples/chat-message-with-actions-demo.d.ts +1 -0
  29. package/examples/chat-message-with-actions-demo.js +26 -0
  30. package/examples/chat-message-with-actions-demo.js.map +1 -0
  31. package/examples/chat-typing-indicator-demo.js +2 -1
  32. package/examples/chat-typing-indicator-demo.js.map +1 -1
  33. package/examples/chat-window-csat-demo.js +2 -1
  34. package/examples/chat-window-csat-demo.js.map +1 -1
  35. package/examples/chat-window-csat-prompt-demo.js +2 -1
  36. package/examples/chat-window-csat-prompt-demo.js.map +1 -1
  37. package/examples/chat-window-demo.js +2 -1
  38. package/examples/chat-window-demo.js.map +1 -1
  39. package/examples/chat-window-quote-demo.js +2 -1
  40. package/examples/chat-window-quote-demo.js.map +1 -1
  41. package/examples/chat-window-quotes-demo.js +2 -1
  42. package/examples/chat-window-quotes-demo.js.map +1 -1
  43. package/package.json +1 -1
  44. package/ui/ChatBody.d.ts +1 -25
  45. package/ui/ChatBody.js +2 -88
  46. package/ui/ChatButton.d.ts +1 -6
  47. package/ui/ChatButton.js +2 -33
  48. package/ui/ChatHeader.d.ts +1 -6
  49. package/ui/ChatHeader.js +2 -43
  50. package/ui/ChatInput.d.ts +1 -10
  51. package/ui/ChatInput.js +2 -148
  52. package/ui/ChatMessage.d.ts +1 -6
  53. package/ui/ChatMessage.js +1 -0
  54. package/ui/ChatTypingIndicator.d.ts +1 -2
  55. package/ui/ChatTypingIndicator.js +2 -30
  56. package/ui/ChatWindow.d.ts +1 -34
  57. package/ui/ChatWindow.js +2 -57
  58. package/ui/ChatWindowCsatPrompt.d.ts +1 -15
  59. package/ui/ChatWindowCsatPrompt.js +2 -70
  60. package/ui/ChatWindowCsatThankYou.d.ts +1 -9
  61. package/ui/ChatWindowCsatThankYou.js +2 -28
  62. package/ui/ChatWindowQuote.d.ts +1 -22
  63. package/ui/ChatWindowQuote.js +2 -140
  64. package/ui/chat/ChatBody.d.ts +31 -0
  65. package/ui/chat/ChatBody.js +92 -0
  66. package/ui/chat/ChatBody.js.map +1 -0
  67. package/ui/chat/ChatButton.d.ts +6 -0
  68. package/ui/chat/ChatButton.js +33 -0
  69. package/ui/chat/ChatButton.js.map +1 -0
  70. package/ui/chat/ChatHeader.d.ts +6 -0
  71. package/ui/chat/ChatHeader.js +43 -0
  72. package/ui/chat/ChatHeader.js.map +1 -0
  73. package/ui/chat/ChatInput.d.ts +28 -0
  74. package/ui/chat/ChatInput.js +165 -0
  75. package/ui/chat/ChatInput.js.map +1 -0
  76. package/ui/chat/ChatMessage.d.ts +17 -0
  77. package/ui/chat/ChatMessage.js +2 -0
  78. package/ui/chat/ChatMessageActions.d.ts +23 -0
  79. package/ui/chat/ChatMessageActions.js +69 -0
  80. package/ui/chat/ChatMessageActions.js.map +1 -0
  81. package/ui/chat/ChatMessageEditor.d.ts +9 -0
  82. package/ui/chat/ChatMessageEditor.js +57 -0
  83. package/ui/chat/ChatMessageEditor.js.map +1 -0
  84. package/ui/chat/ChatTypingIndicator.d.ts +2 -0
  85. package/ui/chat/ChatTypingIndicator.js +30 -0
  86. package/ui/chat/ChatTypingIndicator.js.map +1 -0
  87. package/ui/chat/ChatWindow.d.ts +37 -0
  88. package/ui/chat/ChatWindow.js +65 -0
  89. package/ui/chat/ChatWindow.js.map +1 -0
  90. package/ui/chat/ChatWindowCsatPrompt.d.ts +15 -0
  91. package/ui/chat/ChatWindowCsatPrompt.js +71 -0
  92. package/ui/chat/ChatWindowCsatPrompt.js.map +1 -0
  93. package/ui/chat/ChatWindowCsatThankYou.d.ts +9 -0
  94. package/ui/chat/ChatWindowCsatThankYou.js +29 -0
  95. package/ui/chat/ChatWindowCsatThankYou.js.map +1 -0
  96. package/ui/chat/ChatWindowQuote.d.ts +22 -0
  97. package/ui/chat/ChatWindowQuote.js +141 -0
  98. package/ui/chat/ChatWindowQuote.js.map +1 -0
  99. package/ui/ChatBody.js.map +0 -1
  100. package/ui/ChatButton.js.map +0 -1
  101. package/ui/ChatHeader.js.map +0 -1
  102. package/ui/ChatInput.js.map +0 -1
  103. package/ui/ChatTypingIndicator.js.map +0 -1
  104. package/ui/ChatWindow.js.map +0 -1
  105. package/ui/ChatWindowCsatPrompt.js.map +0 -1
  106. package/ui/ChatWindowCsatThankYou.js.map +0 -1
  107. package/ui/ChatWindowQuote.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatWindowQuote.js","names":[],"sources":["../../../lib/ui/chat/ChatWindowQuote.tsx"],"sourcesContent":["'use client';\n\nimport { twMerge } from '../helpers/twMerge';\nimport { Button } from '../buttons/Button';\nimport { Scrollbar } from '../Scrollbar';\n\nexport interface ChatWindowQuoteItem {\n id: string;\n resortName: string;\n country: string;\n imageUrl: string;\n /** Date range, e.g. \"15 June 2026 to 24 June 2026\" */\n dateRange: string;\n /** Pax info, e.g. \"for 2 adults 2 children\" */\n pax: string;\n /** CTA button label. Defaults to \"See prices\" */\n ctaLabel?: string;\n /** Bullet-point package items. Shown in single-quote inline mode only */\n packageIncludes?: string[];\n /** Free-text paragraph shown in single-quote inline mode only */\n additionalInfo?: string;\n}\n\nexport interface ChatWindowQuoteProps {\n quotes: ChatWindowQuoteItem[];\n onCtaClick?: (quote: ChatWindowQuoteItem) => void;\n className?: string;\n}\n\ninterface QuoteCardProps {\n quote: ChatWindowQuoteItem;\n onCtaClick?: (quote: ChatWindowQuoteItem) => void;\n}\n\nfunction SingleQuoteCard({ quote, onCtaClick }: QuoteCardProps) {\n return (\n <article\n aria-label={quote.resortName}\n className=\"flex flex-col bg-white border border-lightGrey rounded-32 shadow-[0px_2px_14px_0px_rgba(0,0,0,0.25)] overflow-hidden p-24 gap-16\"\n >\n <div className=\"relative h-210 w-full overflow-hidden rounded-16\">\n <img src={quote.imageUrl} alt=\"\" className=\"absolute inset-0 size-full object-cover\" />\n <div className=\"absolute inset-0 bg-gradient-to-b from-transparent from-50% to-[rgba(36,36,36,0.4)]\" />\n <div className=\"absolute bottom-20 inset-x-0 flex flex-col items-center text-white text-center px-20\">\n <p className=\"text-b2 font-semibold\">{quote.resortName}</p>\n <p className=\"text-b3\">{quote.country}</p>\n </div>\n </div>\n <div className=\"flex flex-col gap-16 items-center\">\n <p className=\"text-b4 font-semibold text-black text-center\">\n {quote.resortName}, {quote.dateRange}, {quote.pax}\n </p>\n <Button color=\"saffron\" onClick={() => onCtaClick?.(quote)}>\n {quote.ctaLabel ?? 'See prices'}\n </Button>\n {quote.packageIncludes && quote.packageIncludes.length > 0 && (\n <div className=\"flex flex-col gap-4 w-full\">\n <p className=\"text-b3 font-semibold text-black\">Your All-Inclusive package includes:</p>\n <ul className=\"flex flex-col gap-2\">\n {quote.packageIncludes.map((item) => (\n <li key={item} className=\"text-b3 text-black\">\n • {item}\n </li>\n ))}\n </ul>\n </div>\n )}\n {quote.additionalInfo && <p className=\"text-b3 text-black\">{quote.additionalInfo}</p>}\n </div>\n </article>\n );\n}\n\nfunction CompactQuoteCard({ quote, onCtaClick }: QuoteCardProps) {\n return (\n <article\n aria-label={quote.resortName}\n className=\"flex flex-col gap-12 items-center shrink-0 w-200\"\n >\n <div className=\"relative size-200 rounded-16 overflow-hidden\">\n <img src={quote.imageUrl} alt=\"\" className=\"absolute inset-0 size-full object-cover\" />\n <div className=\"absolute inset-0 rounded-16 bg-gradient-to-b from-transparent from-50% to-[rgba(36,36,36,0.4)]\" />\n <div className=\"absolute bottom-20 inset-x-0 flex flex-col items-center text-white text-center px-8\">\n <p className=\"text-b4 font-semibold\">{quote.resortName}</p>\n <p className=\"text-b5\">{quote.country}</p>\n </div>\n </div>\n <div className=\"flex flex-col gap-8 items-center\">\n <p className=\"text-b5 font-semibold text-black text-center\">\n {quote.resortName}\n <br />\n {quote.dateRange},\n <br />\n {quote.pax}\n </p>\n <Button color=\"black\" onClick={() => onCtaClick?.(quote)}>\n {quote.ctaLabel ?? 'See prices'}\n </Button>\n </div>\n </article>\n );\n}\n\nexport function ChatWindowQuote({ quotes, onCtaClick, className }: ChatWindowQuoteProps) {\n if (quotes.length === 0) return null;\n\n if (quotes.length === 1) {\n return (\n <div className={twMerge(className)}>\n <SingleQuoteCard quote={quotes[0]} onCtaClick={onCtaClick} />\n </div>\n );\n }\n\n return (\n <div className={twMerge('flex flex-col gap-8', className)}>\n <Scrollbar\n timelineName=\"--chat-quote-scroller\"\n scrollerClassName=\"pb-24\"\n scrollbarWidth=\"60%\"\n aria-label=\"Quote options\"\n >\n <div className=\"flex gap-12\">\n {quotes.map((quote) => (\n <CompactQuoteCard key={quote.id} quote={quote} onCtaClick={onCtaClick} />\n ))}\n </div>\n </Scrollbar>\n </div>\n );\n}\n"],"mappings":";;;;;;AAkCA,SAAS,EAAgB,EAAE,UAAO,iBAA8B;AAC9D,QACE,kBAAC,WAAD;EACE,cAAY,EAAM;EAClB,WAAU;YAFZ,CAIE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,KAAK,EAAM;KAAU,KAAI;KAAG,WAAU;KAA4C,CAAA;IACvF,kBAAC,OAAD,EAAK,WAAU,uFAAwF,CAAA;IACvG,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAyB,EAAM;MAAe,CAAA,EAC3D,kBAAC,KAAD;MAAG,WAAU;gBAAW,EAAM;MAAY,CAAA,CAAA;;;MAG9C,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,KAAD;KAAG,WAAU;eAAb;MACG,EAAM;MAAW;MAAG,EAAM;MAAU;MAAG,EAAM;;;IAEhD,kBAAC,GAAD;KAAQ,OAAM;KAAU,eAAe,IAAa,EAAM;eACvD,EAAM,YAAY;KACZ,CAAA;IACR,EAAM,mBAAmB,EAAM,gBAAgB,SAAS,KACvD,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAmC;MAAwC,CAAA,EACxF,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAM,gBAAgB,KAAK,MAC1B,kBAAC,MAAD;OAAe,WAAU;iBAAzB,CAA8C,MACzC,EAAA;SADI,EAEJ,CAAA;MAEJ,CAAA,CAAA;;IAGR,EAAM,kBAAkB,kBAAC,KAAD;KAAG,WAAU;eAAsB,EAAM;KAAmB,CAAA;;;;;AAM7F,SAAS,EAAiB,EAAE,UAAO,iBAA8B;AAC/D,QACE,kBAAC,WAAD;EACE,cAAY,EAAM;EAClB,WAAU;YAFZ,CAIE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,KAAK,EAAM;KAAU,KAAI;KAAG,WAAU;KAA4C,CAAA;IACvF,kBAAC,OAAD,EAAK,WAAU,kGAAmG,CAAA;IAClH,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAyB,EAAM;MAAe,CAAA,EAC3D,kBAAC,KAAD;MAAG,WAAU;gBAAW,EAAM;MAAY,CAAA,CAAA;;;MAG9C,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,KAAD;IAAG,WAAU;cAAb;KACG,EAAM;KACP,kBAAC,MAAD,EAAM,CAAA;KACL,EAAM;KAAU;KACjB,kBAAC,MAAD,EAAM,CAAA;KACL,EAAM;;OAET,kBAAC,GAAD;IAAQ,OAAM;IAAQ,eAAe,IAAa,EAAM;cACrD,EAAM,YAAY;IACZ,CAAA,CAAA;;;;AAMjB,SAAgB,EAAgB,EAAE,WAAQ,eAAY,gBAAmC;AAWvF,QAVI,EAAO,WAAW,IAAU,OAE5B,EAAO,WAAW,IAElB,kBAAC,OAAD;EAAK,WAAW,EAAQ,EAAU;YAChC,kBAAC,GAAD;GAAiB,OAAO,EAAO;GAAgB;GAAc,CAAA;EACzD,CAAA,GAKR,kBAAC,OAAD;EAAK,WAAW,EAAQ,uBAAuB,EAAU;YACvD,kBAAC,GAAD;GACE,cAAa;GACb,mBAAkB;GAClB,gBAAe;GACf,cAAW;aAEX,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAO,KAAK,MACX,kBAAC,GAAD;KAAwC;KAAmB;KAAc,EAAlD,EAAM,GAA4C,CAAA;IAEvE,CAAA;GACI,CAAA;EACR,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"ChatBody.js","names":[],"sources":["../../lib/ui/ChatBody.tsx"],"sourcesContent":["import { ChatMessage, type ChatMessageSender } from './ChatMessage';\nimport { ChatTypingIndicator } from './ChatTypingIndicator';\nimport type { ChatWindowCsatPromptLabels } from './ChatWindowCsatPrompt';\nimport { ChatWindowCsatPrompt } from './ChatWindowCsatPrompt';\nimport {\n ChatWindowCsatThankYou,\n type ChatWindowCsatThankYouLabels,\n} from './ChatWindowCsatThankYou';\nimport { ChatWindowQuote, type ChatWindowQuoteItem } from './ChatWindowQuote';\nimport {\n forwardRef,\n type HTMLAttributes,\n type ReactNode,\n type Ref,\n useEffect,\n useImperativeHandle,\n useRef,\n useState,\n} from 'react';\nimport { twMerge } from './helpers/twMerge';\n\nexport interface ChatMessage {\n id: string;\n sender: ChatMessageSender;\n content: ReactNode;\n}\n\nexport interface ChatBodyProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {\n greeting?: string;\n messages: ChatMessage[];\n isTyping?: boolean;\n onFeedbackRate?: (rating: number) => void;\n csatLabels?: ChatWindowCsatPromptLabels;\n csatRenderRatingLabel?: (rating: number) => ReactNode;\n csatThankYouLabels?: ChatWindowCsatThankYouLabels;\n onQuoteCtaClick?: (quote: ChatWindowQuoteItem) => void;\n}\n\nexport interface ChatHandle {\n askForFeedback: () => void;\n showQuotes: (quotes: ChatWindowQuoteItem[]) => void;\n}\n\nfunction useChat(\n { messages, isTyping, csatRenderRatingLabel, onFeedbackRate, csatLabels }: ChatBodyProps,\n ref: Ref<ChatHandle>,\n) {\n const logRef = useRef<HTMLDivElement>(null);\n const [showCsat, setShowCsat] = useState(false);\n const [csatRatingMessage, setCsatRatingMessage] = useState<string | null>(null);\n const [quoteGroups, setQuoteGroups] = useState<ChatWindowQuoteItem[][]>([]);\n\n useImperativeHandle(ref, () => ({\n askForFeedback() {\n setShowCsat(true);\n },\n showQuotes(quotes: ChatWindowQuoteItem[]) {\n setQuoteGroups((prev) => [...prev, quotes]);\n },\n }));\n\n useEffect(() => {\n const el = logRef.current;\n if (el) el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });\n }, [messages, isTyping, showCsat, csatRatingMessage, quoteGroups]);\n\n function handleRate(rating: number) {\n const label = csatRenderRatingLabel ? String(csatRenderRatingLabel(rating)) : String(rating);\n setCsatRatingMessage(label);\n onFeedbackRate?.(rating);\n }\n\n const csatReady = showCsat && csatLabels && csatRenderRatingLabel;\n\n return { logRef, csatRatingMessage, quoteGroups, handleRate, csatReady };\n}\n\nexport const ChatBody = forwardRef<ChatHandle, ChatBodyProps>(function Chat(props, ref) {\n const { logRef, csatRatingMessage, quoteGroups, handleRate, csatReady } = useChat(props, ref);\n\n const {\n greeting,\n messages,\n isTyping = false,\n csatLabels,\n csatRenderRatingLabel,\n csatThankYouLabels,\n onQuoteCtaClick,\n className,\n ...extraProps\n } = props;\n\n return (\n <div\n {...extraProps}\n ref={logRef}\n role=\"log\"\n aria-label=\"Chat messages\"\n aria-live=\"polite\"\n className={twMerge(\n 'chat-message-log flex flex-col flex-1 gap-20 overflow-y-auto py-20',\n className,\n )}\n >\n {greeting ? (\n <div className=\"px-20\">\n <p className=\"text-b3 text-black\">{greeting}</p>\n </div>\n ) : null}\n\n {messages.map((msg) => (\n <ChatMessage key={msg.id} sender={msg.sender}>\n {msg.content}\n </ChatMessage>\n ))}\n\n {isTyping && (\n <div className=\"px-12\">\n <ChatTypingIndicator />\n </div>\n )}\n\n {csatReady && (\n <ChatWindowCsatPrompt\n onRate={handleRate}\n labels={csatLabels!}\n renderRatingLabel={csatRenderRatingLabel!}\n />\n )}\n\n {csatRatingMessage && <ChatMessage sender=\"user\">{csatRatingMessage}</ChatMessage>}\n\n {csatRatingMessage && csatThankYouLabels && (\n <ChatWindowCsatThankYou labels={csatThankYouLabels} />\n )}\n\n {quoteGroups.map((quotes, idx) => (\n <ChatWindowQuote key={idx} quotes={quotes} onCtaClick={onQuoteCtaClick} />\n ))}\n </div>\n );\n});\n"],"mappings":";;;;;;;;;AA2CA,SAAS,EACP,EAAE,aAAU,aAAU,0BAAuB,mBAAgB,iBAC7D,GACA;CACA,IAAM,IAAS,EAAuB,KAAK,EACrC,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,CAAC,GAAmB,KAAwB,EAAwB,KAAK,EACzE,CAAC,GAAa,KAAkB,EAAkC,EAAE,CAAC;AAW3E,CATA,EAAoB,UAAY;EAC9B,iBAAiB;AACf,KAAY,GAAK;;EAEnB,WAAW,GAA+B;AACxC,MAAgB,MAAS,CAAC,GAAG,GAAM,EAAO,CAAC;;EAE9C,EAAE,EAEH,QAAgB;EACd,IAAM,IAAK,EAAO;AAClB,EAAI,KAAI,EAAG,SAAS;GAAE,KAAK,EAAG;GAAc,UAAU;GAAU,CAAC;IAChE;EAAC;EAAU;EAAU;EAAU;EAAmB;EAAY,CAAC;CAElE,SAAS,EAAW,GAAgB;AAGlC,EADA,EADsC,OAAxB,IAA+B,EAAsB,EAAO,GAAW,EAAO,CACjE,EAC3B,IAAiB,EAAO;;AAK1B,QAAO;EAAE;EAAQ;EAAmB;EAAa;EAAY,WAF3C,KAAY,KAAc;EAE4B;;AAG1E,IAAa,IAAW,EAAsC,SAAc,GAAO,GAAK;CACtF,IAAM,EAAE,WAAQ,sBAAmB,gBAAa,eAAY,iBAAc,EAAQ,GAAO,EAAI,EAEvF,EACJ,aACA,aACA,cAAW,IACX,eACA,0BACA,uBACA,oBACA,cACA,GAAG,MACD;AAEJ,QACE,kBAAC,OAAD;EACE,GAAI;EACJ,KAAK;EACL,MAAK;EACL,cAAW;EACX,aAAU;EACV,WAAW,EACT,sEACA,EACD;YATH;GAWG,IACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,KAAD;KAAG,WAAU;eAAsB;KAAa,CAAA;IAC5C,CAAA,GACJ;GAEH,EAAS,KAAK,MACb,kBAAC,GAAD;IAA0B,QAAQ,EAAI;cACnC,EAAI;IACO,EAFI,EAAI,GAER,CACd;GAED,KACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD,EAAuB,CAAA;IACnB,CAAA;GAGP,KACC,kBAAC,GAAD;IACE,QAAQ;IACR,QAAQ;IACR,mBAAmB;IACnB,CAAA;GAGH,KAAqB,kBAAC,GAAD;IAAa,QAAO;cAAQ;IAAgC,CAAA;GAEjF,KAAqB,KACpB,kBAAC,GAAD,EAAwB,QAAQ,GAAsB,CAAA;GAGvD,EAAY,KAAK,GAAQ,MACxB,kBAAC,GAAD;IAAmC;IAAQ,YAAY;IAAmB,EAApD,EAAoD,CAC1E;GACE;;EAER"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"ChatButton.js","names":[],"sources":["../../lib/ui/ChatButton.tsx"],"sourcesContent":["import { Icon } from '@clubmed/trident-icons';\nimport type { ButtonHTMLAttributes } from 'react';\nimport { twMerge } from './helpers/twMerge';\n\nconst DEFAULT_TEASER_COPY = 'AI Agent: Have you got a question?';\nconst FALLBACK_ARIA_LABEL = 'Chat with your assistant';\n\nexport type ChatButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {\n offset?: boolean;\n unfoldOn?: 'hover' | 'active';\n};\n\nexport function ChatButton({\n children = DEFAULT_TEASER_COPY,\n className,\n type = 'button',\n 'aria-label': ariaLabel,\n tabIndex,\n offset,\n unfoldOn = 'hover',\n ...rest\n}: ChatButtonProps) {\n const label =\n ariaLabel ??\n (typeof children === 'string' && children.trim().length > 0\n ? children.trim()\n : FALLBACK_ARIA_LABEL);\n\n return (\n <button\n type={type}\n aria-label={label}\n tabIndex={tabIndex ?? 0}\n {...rest}\n className={twMerge(\n 'chat-border grid grid-cols-[24px_0fr] transition-[grid-template-columns] justify-center items-center overflow-hidden cursor-pointer transition-all rounded-pill px-8 md:px-16 outline-none group/Chatbutton focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 pointer-events-auto h-48 min-h-48 md:min-w-80 md:h-80 active:scale-[0.98]',\n offset && 'md:translate-x-[40px] focus-visible:translate-x-0',\n unfoldOn === 'hover' &&\n 'group/hoverChatbutton hover:grid-cols-[24px_1fr] hover:translate-x-0',\n unfoldOn === 'active' &&\n 'group/activeChatbutton active:grid-cols-[24px_1fr] active:translate-x-0',\n 'focus-visible:grid-cols-[24px_1fr]',\n className,\n )}\n >\n <Icon\n name=\"Sparkles\"\n className={twMerge(\n 'chat-button-icon w-24 transition-all',\n offset &&\n 'relative md:start-[calc(-16px)] group-focus-visible/Chatbutton:start-0 group-hover/hoverChatbutton:start-0 group-active/activeChatbutton:start-0',\n )}\n />\n <div className=\"grid grid-cols-[0fr] overflow-hidden opacity-0 transition-opacity group-hover/hoverChatbutton:opacity-100 group-focus-visible/Chatbutton:opacity-100 group-active/activeChatbutton:opacity-100 duration-500\">\n <span\n aria-hidden=\"true\"\n className=\"chat-button-text items-center ps-20 flex gap-10 whitespace-nowrap font-semibold text-b3 text-black\"\n >\n {children}\n <Icon name=\"ArrowTailRight\" width=\"24px\" />\n </span>\n </div>\n </button>\n );\n}\n"],"mappings":";;;;AAIA,IAAM,IAAsB,sCACtB,IAAsB;AAO5B,SAAgB,EAAW,EACzB,cAAW,GACX,cACA,UAAO,UACP,cAAc,GACd,aACA,WACA,cAAW,SACX,GAAG,KACe;CAClB,IAAM,IACJ,MACC,OAAO,KAAa,YAAY,EAAS,MAAM,CAAC,SAAS,IACtD,EAAS,MAAM,GACf;AAEN,QACE,kBAAC,UAAD;EACQ;EACN,cAAY;EACZ,UAAU,KAAY;EACtB,GAAI;EACJ,WAAW,EACT,mWACA,KAAU,qDACV,MAAa,WACX,wEACF,MAAa,YACX,2EACF,sCACA,EACD;YAdH,CAgBE,kBAAC,GAAD;GACE,MAAK;GACL,WAAW,EACT,wCACA,KACE,mJACH;GACD,CAAA,EACF,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,QAAD;IACE,eAAY;IACZ,WAAU;cAFZ,CAIG,GACD,kBAAC,GAAD;KAAM,MAAK;KAAiB,OAAM;KAAS,CAAA,CACtC;;GACH,CAAA,CACC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"ChatHeader.js","names":[],"sources":["../../lib/ui/ChatHeader.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { Icon } from '@clubmed/trident-icons';\n\nexport interface ChatHeaderProps {\n title?: string;\n logo?: ReactNode;\n}\n\nconst headerCurve = (\n <svg\n className=\"absolute top-full inset-x-[50%] -translate-x-1/2\"\n width=\"118\"\n height=\"27\"\n viewBox=\"0 0 118 27\"\n >\n <path\n d=\"M0 0C10.8283 0 21.1946 5.40419 28.7327 13.2062L30.2673 14.7945C45.991 31.0685 72.009 31.0685 87.7327 14.7945L89.2673 13.2062C96.8054 5.40419 107.172 0 118 0C118 0 80.6853 0 0 0Z\"\n fill=\"#F6EFE7\"\n />\n </svg>\n);\n\nexport function ChatHeader({ title = 'Ask me a question', logo }: ChatHeaderProps) {\n return (\n <div className=\"relative flex flex-col items-center shrink-0 pt-4 px-4 h-118 overflow-hidden\">\n <div className=\"absolute inset-x-4 top-4 h-87 bg-lightSand rounded-[24px]\">{headerCurve}</div>\n <div className=\"relative flex flex-col flex-1 items-center justify-end pb-8 w-full z-10\">\n <div className=\"flex items-center justify-center px-12 py-8 w-full\">\n <p className=\"font-serif font-bold italic text-h5 text-center text-black line-clamp-2\">\n {title}\n </p>\n </div>\n {logo ?? (\n <button\n type=\"button\"\n aria-label=\"AI assistant\"\n className=\"chat-border flex items-center justify-center size-48 rounded-full bg-white outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2\"\n >\n <Icon name=\"Sparkles\" width=\"24px\" />\n </button>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;AAQA,IAAM,IACJ,kBAAC,OAAD;CACE,WAAU;CACV,OAAM;CACN,QAAO;CACP,SAAQ;WAER,kBAAC,QAAD;EACE,GAAE;EACF,MAAK;EACL,CAAA;CACE,CAAA;AAGR,SAAgB,EAAW,EAAE,WAAQ,qBAAqB,WAAyB;AACjF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAA6D;GAAkB,CAAA,EAC9F,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,KAAD;KAAG,WAAU;eACV;KACC,CAAA;IACA,CAAA,EACL,KACC,kBAAC,UAAD;IACE,MAAK;IACL,cAAW;IACX,WAAU;cAEV,kBAAC,GAAD;KAAM,MAAK;KAAW,OAAM;KAAS,CAAA;IAC9B,CAAA,CAEP;KACF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"ChatInput.js","names":[],"sources":["../../lib/ui/ChatInput.tsx"],"sourcesContent":["/// <reference types=\"@types/dom-speech-recognition\" />\n'use client';\n\nimport type { FormHTMLAttributes } from 'react';\nimport { useEffect, useRef, useState } from 'react';\nimport { Button } from './buttons/Button';\nimport { twMerge } from './helpers/twMerge';\n\nexport interface ChatInputProps extends FormHTMLAttributes<HTMLFormElement> {\n value: string;\n placeholder?: string;\n onValueChange: (value: string) => void;\n onSubmit: () => void;\n /** Fallback for when the browser does not support the Web Speech API */\n onAudioClick?: () => void;\n}\n\nconst FORM_PAD = 12; // matches p-12\nconst BTN_SIZE = 48; // matches size-48\nconst BTN_GAP = 8; // matches gap-8\n\n// Keep onValueChange stable inside the recognition handler without re-creating the SR instance\nfunction useCallbackRef<T extends (...args: never[]) => unknown>(fn: T) {\n const ref = useRef(fn);\n useEffect(() => {\n ref.current = fn;\n });\n return ref;\n}\n\nconst SILENCE_TIMEOUT_MS = 3_000;\n\nfunction useSpeechRecognition(onValueChange: (v: string) => void) {\n const [isSupported, setIsSupported] = useState(false);\n const [isListening, setIsListening] = useState(false);\n const recognitionRef = useRef<SpeechRecognition | null>(null);\n const silenceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const isListeningRef = useRef(false);\n const onValueChangeRef = useCallbackRef(onValueChange);\n\n function clearSilenceTimer() {\n if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);\n }\n\n function resetSilenceTimer() {\n clearSilenceTimer();\n silenceTimerRef.current = setTimeout(() => {\n isListeningRef.current = false;\n setIsListening(false);\n recognitionRef.current?.stop();\n }, SILENCE_TIMEOUT_MS);\n }\n\n useEffect(() => {\n const SR: typeof SpeechRecognition | undefined =\n (window as Window & { webkitSpeechRecognition?: typeof SpeechRecognition })\n .webkitSpeechRecognition ?? window.SpeechRecognition;\n if (!SR) return;\n\n setIsSupported(true);\n\n const recognition = new SR();\n recognition.continuous = true;\n recognition.interimResults = true;\n recognition.lang = navigator.language;\n\n recognition.onresult = (event: SpeechRecognitionEvent) => {\n const transcript = Array.from(event.results)\n .map((r: SpeechRecognitionResult) => r[0].transcript)\n .join('');\n onValueChangeRef.current(transcript);\n resetSilenceTimer();\n };\n\n recognition.onend = () => {\n clearSilenceTimer();\n // Restart automatically if the user hasn't manually stopped (browser cut us off)\n if (isListeningRef.current) {\n try {\n recognition.start();\n } catch {\n /* already starting */\n }\n } else {\n setIsListening(false);\n }\n };\n\n recognition.onerror = (e: SpeechRecognitionErrorEvent) => {\n if (e.error === 'not-allowed' || e.error === 'aborted') {\n isListeningRef.current = false;\n setIsListening(false);\n clearSilenceTimer();\n }\n };\n\n recognitionRef.current = recognition;\n return () => {\n recognition.abort();\n clearSilenceTimer();\n };\n // SR detection and instance creation only needs to run once on mount\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n function toggle() {\n const recognition = recognitionRef.current;\n if (!recognition) return;\n if (isListeningRef.current) {\n isListeningRef.current = false;\n recognition.stop();\n } else {\n isListeningRef.current = true;\n setIsListening(true);\n recognition.start();\n resetSilenceTimer();\n }\n }\n\n return { isSupported, isListening, toggle };\n}\n\n/**\n * Drives the audio-level indicator by writing directly to the DOM element's\n * style — avoids triggering a React re-render on every animation frame\n * (rule: rerender-use-ref-transient-values).\n *\n * Returns a ref to attach to the indicator <span>.\n */\nfunction useAudioLevel(isListening: boolean) {\n const indicatorRef = useRef<HTMLSpanElement>(null);\n\n useEffect(() => {\n const el = indicatorRef.current;\n\n if (!isListening) {\n if (el) el.style.height = '0px';\n return;\n }\n\n let animId: number;\n let stream: MediaStream | null = null;\n let ctx: AudioContext | null = null;\n\n navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then((s) => {\n stream = s;\n ctx = new AudioContext();\n const source = ctx.createMediaStreamSource(stream);\n const analyser = ctx.createAnalyser();\n analyser.fftSize = 256;\n source.connect(analyser);\n const data = new Uint8Array(analyser.frequencyBinCount);\n\n function tick() {\n analyser.getByteFrequencyData(data);\n const avg = data.reduce((sum, v) => sum + v, 0) / data.length;\n // Square-root curve so quiet sounds register more visibly\n const node = indicatorRef.current;\n if (node) node.style.height = `${Math.sqrt(avg / 255) * 10}px`;\n animId = requestAnimationFrame(tick);\n }\n animId = requestAnimationFrame(tick);\n });\n\n return () => {\n cancelAnimationFrame(animId);\n stream?.getTracks().forEach((t) => t.stop());\n ctx?.close();\n };\n }, [isListening]);\n\n return indicatorRef;\n}\n\nexport function ChatInput({\n value,\n placeholder = 'Ask a question?',\n onValueChange,\n onSubmit,\n onAudioClick,\n className,\n ...props\n}: ChatInputProps) {\n const [isFocused, setIsFocused] = useState(false);\n const trimmed = value.trim();\n const isOpen = isFocused || trimmed.length > 0;\n const { isSupported, isListening, toggle } = useSpeechRecognition(onValueChange);\n const audioIndicatorRef = useAudioLevel(isListening);\n\n const showAudio = isSupported || !!onAudioClick;\n const btnW = showAudio ? BTN_SIZE + BTN_GAP + BTN_SIZE : BTN_SIZE;\n\n function handleAudioClick() {\n if (isSupported) {\n toggle();\n } else {\n onAudioClick?.();\n }\n }\n\n function handleSubmit(e: React.FormEvent) {\n e.preventDefault();\n if (trimmed) onSubmit();\n }\n\n return (\n <form\n onSubmit={handleSubmit}\n onMouseDown={(e) => {\n if ((e.target as HTMLElement).tagName !== 'TEXTAREA') e.preventDefault();\n }}\n {...props}\n style={{\n paddingBottom: isOpen ? FORM_PAD + BTN_GAP + BTN_SIZE : FORM_PAD,\n }}\n className={twMerge(\n 'relative flex items-start overflow-hidden bg-white border border-lightGrey p-12 w-full',\n 'transition-[padding-bottom,border-radius] duration-250 ease-out rounded-b-[24px]',\n isOpen ? 'rounded-32' : 'rounded-[40px]',\n className,\n )}\n >\n <textarea\n rows={isOpen ? 3 : 1}\n value={value}\n onChange={(e) => onValueChange(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && trimmed) {\n e.preventDefault();\n onSubmit();\n }\n }}\n onFocus={() => setIsFocused(true)}\n onBlur={() => setIsFocused(false)}\n placeholder={placeholder}\n aria-label={placeholder}\n style={{\n marginInlineEnd: isOpen ? 0 : btnW + BTN_GAP,\n }}\n className={twMerge(\n 'scrollbar-hidden bg-transparent text-b3 py-12 outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 resize-none w-full px-20 border border-middleGrey',\n 'transition-[margin-inline-end,border-radius] duration-250 ease-out',\n isOpen\n ? 'text-black rounded-[20px]'\n : 'text-darkGrey placeholder:text-darkGrey rounded-[40px]',\n )}\n />\n\n <div\n style={{\n insetInlineStart: isOpen\n ? `calc(50% - ${btnW / 2}px)`\n : `calc(100% - ${FORM_PAD + btnW}px)`,\n bottom: FORM_PAD,\n }}\n className=\"absolute flex gap-8 items-center transition-[inset-inline-start] duration-250 ease-out\"\n >\n <Button\n type=\"submit\"\n aria-label=\"Send message\"\n disabled={!trimmed}\n variant=\"circle\"\n color=\"saffron\"\n icon=\"ArrowTailRight\"\n iconWidth=\"24px\"\n />\n {showAudio && (\n <Button\n type=\"button\"\n aria-label={isListening ? 'Stop recording' : 'Use microphone'}\n aria-pressed={isListening}\n onClick={handleAudioClick}\n variant=\"circle\"\n theme={isListening ? 'solid' : 'outline'}\n color={isListening ? 'sienna' : 'black'}\n icon=\"Audio\"\n iconWidth=\"24px\"\n className={twMerge(\n 'relative',\n isListening && 'animate-pulse',\n !isListening && 'border-lightGrey',\n )}\n >\n <span\n ref={audioIndicatorRef}\n aria-hidden=\"true\"\n className=\"bg-current absolute w-5 bottom-21\"\n style={{ height: '0px' }}\n />\n </Button>\n )}\n </div>\n </form>\n );\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"ChatTypingIndicator.js","names":[],"sources":["../../lib/ui/ChatTypingIndicator.tsx"],"sourcesContent":["import { memo, type HTMLAttributes } from 'react';\nimport { twMerge } from './helpers/twMerge';\n\nexport const ChatTypingIndicator = memo(function ChatTypingIndicator({\n className,\n ...props\n}: HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n role=\"status\"\n aria-label=\"Typing\"\n {...props}\n className={twMerge(\n 'flex items-center gap-4 h-34 w-66 overflow-hidden rounded-pill bg-pearl px-20',\n className,\n )}\n >\n <span\n aria-hidden=\"true\"\n className=\"size-6 rounded-full bg-black animate-bounce [animation-delay:-300ms]\"\n />\n <span\n aria-hidden=\"true\"\n className=\"size-6 rounded-full bg-black animate-bounce [animation-delay:-150ms]\"\n />\n <span aria-hidden=\"true\" className=\"size-6 rounded-full bg-black animate-bounce\" />\n </div>\n );\n});\n"],"mappings":";;;;AAGA,IAAa,IAAsB,EAAK,SAA6B,EACnE,cACA,GAAG,KAC8B;AACjC,QACE,kBAAC,OAAD;EACE,MAAK;EACL,cAAW;EACX,GAAI;EACJ,WAAW,EACT,iFACA,EACD;YAPH;GASE,kBAAC,QAAD;IACE,eAAY;IACZ,WAAU;IACV,CAAA;GACF,kBAAC,QAAD;IACE,eAAY;IACZ,WAAU;IACV,CAAA;GACF,kBAAC,QAAD;IAAM,eAAY;IAAO,WAAU;IAAgD,CAAA;GAC/E;;EAER"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"ChatWindow.js","names":[],"sources":["../../lib/ui/ChatWindow.tsx"],"sourcesContent":["'use client';\n\nimport { Icon } from '@clubmed/trident-icons';\nimport { forwardRef, type ReactNode } from 'react';\nimport { twMerge } from './helpers/twMerge';\nimport type { ChatMessageSender } from './ChatMessage';\nimport type { ChatWindowCsatPromptLabels } from './ChatWindowCsatPrompt';\nimport type { ChatWindowCsatThankYouLabels } from './ChatWindowCsatThankYou';\nimport type { ChatWindowQuoteItem } from './ChatWindowQuote';\nimport { ChatHeader, type ChatHeaderProps } from './ChatHeader';\nimport { ChatBody, type ChatBodyProps } from './ChatBody';\nimport { ChatInput, type ChatInputProps } from '@/ui/ChatInput';\n\nexport type { ChatWindowCsatPromptLabels, ChatWindowCsatThankYouLabels, ChatWindowQuoteItem };\n\nexport interface ChatWindowMessage {\n id: string;\n sender: ChatMessageSender;\n content: ReactNode;\n}\n\ninterface ChatWindowInputProps {\n inputValue: ChatInputProps['value'];\n inputPlaceholder?: ChatInputProps['placeholder'];\n onInputChange: ChatInputProps['onValueChange'];\n onSubmit: ChatInputProps['onSubmit'];\n onAudioClick?: ChatInputProps['onAudioClick'];\n}\n\ninterface ChatWindowBaseProps extends ChatHeaderProps {\n onClose?: () => void;\n}\n\n// Either compose the panel from `ChatHeader`/`ChatBody`/`ChatInput` children,\n// or fall back to the opaque props-only form — in which case the message-log\n// and input props become required.\nexport type ChatWindowProps = ChatWindowBaseProps &\n (\n | ({ children: ReactNode } & Partial<ChatBodyProps> & Partial<ChatWindowInputProps>)\n | (ChatBodyProps & ChatWindowInputProps & { children?: undefined })\n );\n\nexport interface ChatWindowHandle {\n askForFeedback: () => void;\n showQuotes: (quotes: ChatWindowQuoteItem[]) => void;\n}\n\nexport const ChatWindow = forwardRef<ChatWindowHandle, ChatWindowProps>(function ChatWindow(\n {\n title,\n onClose,\n logo,\n className,\n children,\n inputValue,\n inputPlaceholder,\n onInputChange,\n onSubmit,\n onAudioClick,\n greeting,\n messages,\n isTyping,\n onFeedbackRate,\n csatLabels,\n csatRenderRatingLabel,\n csatThankYouLabels,\n onQuoteCtaClick,\n ...props\n },\n ref,\n) {\n return (\n <div {...props} className={twMerge('flex flex-col gap-20 items-center', className)}>\n <div className=\"chat-border bg-white flex flex-col h-full w-full rounded-32 overflow-hidden\">\n {children ?? (\n <>\n <ChatHeader title={title} logo={logo} />\n <ChatBody\n greeting={greeting}\n messages={messages!}\n isTyping={isTyping}\n onFeedbackRate={onFeedbackRate}\n csatLabels={csatLabels}\n csatRenderRatingLabel={csatRenderRatingLabel}\n csatThankYouLabels={csatThankYouLabels}\n onQuoteCtaClick={onQuoteCtaClick}\n ref={ref}\n />\n <div className=\"shrink-0 px-8 pb-8 pt-12\">\n <ChatInput\n value={inputValue!}\n placeholder={inputPlaceholder}\n onValueChange={onInputChange!}\n onSubmit={onSubmit!}\n onAudioClick={onAudioClick}\n />\n </div>\n </>\n )}\n </div>\n\n {onClose && (\n <button\n type=\"button\"\n aria-label=\"Close chat\"\n onClick={onClose}\n className=\"flex items-center justify-center size-48 rounded-full bg-black border border-black text-white shrink-0 hover:opacity-80 transition-opacity outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2\"\n >\n <Icon name=\"CrossDefault\" width=\"24px\" />\n </button>\n )}\n </div>\n );\n});\n"],"mappings":";;;;;;;;;AA+CA,IAAa,IAAa,EAA8C,SACtE,EACE,UACA,YACA,SACA,cACA,aACA,eACA,qBACA,kBACA,aACA,iBACA,aACA,aACA,aACA,mBACA,eACA,0BACA,uBACA,oBACA,GAAG,KAEL,GACA;AACA,QACE,kBAAC,OAAD;EAAK,GAAI;EAAO,WAAW,EAAQ,qCAAqC,EAAU;YAAlF,CACE,kBAAC,OAAD;GAAK,WAAU;aACZ,KACC,kBAAA,GAAA,EAAA,UAAA;IACE,kBAAC,GAAD;KAAmB;KAAa;KAAQ,CAAA;IACxC,kBAAC,GAAD;KACY;KACA;KACA;KACM;KACJ;KACW;KACH;KACH;KACZ;KACL,CAAA;IACF,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MACE,OAAO;MACP,aAAa;MACb,eAAe;MACL;MACI;MACd,CAAA;KACE,CAAA;IACL,EAAA,CAAA;GAED,CAAA,EAEL,KACC,kBAAC,UAAD;GACE,MAAK;GACL,cAAW;GACX,SAAS;GACT,WAAU;aAEV,kBAAC,GAAD;IAAM,MAAK;IAAe,OAAM;IAAS,CAAA;GAClC,CAAA,CAAA;;EAIf"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"ChatWindowCsatPrompt.js","names":[],"sources":["../../lib/ui/ChatWindowCsatPrompt.tsx"],"sourcesContent":["'use client';\n\nimport { Icon } from '@clubmed/trident-icons';\nimport type { ReactNode } from 'react';\nimport { useState } from 'react';\nimport { twMerge } from './helpers/twMerge';\n\nexport interface ChatWindowCsatPromptLabels {\n intro: string;\n question: string;\n questionNote: string;\n groupAriaLabel: string;\n starLabels: [string, string, string, string, string];\n}\n\nexport interface ChatWindowCsatPromptProps {\n onRate: (rating: number) => void;\n labels: ChatWindowCsatPromptLabels;\n renderRatingLabel: (rating: number) => ReactNode;\n className?: string;\n}\n\nexport function ChatWindowCsatPrompt({\n onRate,\n labels,\n renderRatingLabel,\n className,\n}: ChatWindowCsatPromptProps) {\n const [hovered, setHovered] = useState(0);\n const [selected, setSelected] = useState(0);\n\n function handleSelect(rating: number) {\n if (selected > 0) return;\n setSelected(rating);\n onRate(rating);\n }\n\n const activeRating = hovered || selected;\n const isLocked = selected > 0;\n\n return (\n <div className={twMerge('flex flex-col gap-20 px-20', className)}>\n <div className=\"flex flex-col gap-4\">\n <p className=\"text-b3 text-black\">{labels.intro}</p>\n <p className=\"text-b3 text-black\">\n {labels.question} <span className=\"text-b4 text-black\">{labels.questionNote}</span>\n </p>\n </div>\n\n <div role=\"group\" aria-label={labels.groupAriaLabel} className=\"flex items-center gap-0\">\n {labels.starLabels.map((label, i) => {\n const rating = i + 1;\n const isFilled = rating <= activeRating;\n return (\n <button\n key={rating}\n type=\"button\"\n aria-label={`${rating} star${rating > 1 ? 's' : ''} — ${label}`}\n aria-pressed={selected === rating}\n aria-disabled={isLocked}\n onClick={() => handleSelect(rating)}\n onMouseEnter={() => {\n if (!isLocked) setHovered(rating);\n }}\n onMouseLeave={() => {\n if (!isLocked) setHovered(0);\n }}\n className={twMerge(\n 'flex items-center justify-center size-48 rounded-full transition-colors outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2',\n isLocked ? 'cursor-default' : 'hover:bg-lightSand',\n )}\n >\n <Icon\n name={isFilled ? 'StarFilled' : 'StarOutlined'}\n width=\"24px\"\n className=\"text-black\"\n />\n </button>\n );\n })}\n </div>\n\n {activeRating > 0 && (\n <p className=\"text-b4 text-darkGrey\" aria-live=\"polite\">\n {renderRatingLabel(activeRating)}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AAsBA,SAAgB,EAAqB,EACnC,WACA,WACA,sBACA,gBAC4B;CAC5B,IAAM,CAAC,GAAS,KAAc,EAAS,EAAE,EACnC,CAAC,GAAU,KAAe,EAAS,EAAE;CAE3C,SAAS,EAAa,GAAgB;AAChC,MAAW,MACf,EAAY,EAAO,EACnB,EAAO,EAAO;;CAGhB,IAAM,IAAe,KAAW,GAC1B,IAAW,IAAW;AAE5B,QACE,kBAAC,OAAD;EAAK,WAAW,EAAQ,8BAA8B,EAAU;YAAhE;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eAAsB,EAAO;KAAU,CAAA,EACpD,kBAAC,KAAD;KAAG,WAAU;eAAb;MACG,EAAO;MAAS;MAAC,kBAAC,QAAD;OAAM,WAAU;iBAAsB,EAAO;OAAoB,CAAA;;;;GAIvF,kBAAC,OAAD;IAAK,MAAK;IAAQ,cAAY,EAAO;IAAgB,WAAU;cAC5D,EAAO,WAAW,KAAK,GAAO,MAAM;KACnC,IAAM,IAAS,IAAI,GACb,IAAW,KAAU;AAC3B,YACE,kBAAC,UAAD;MAEE,MAAK;MACL,cAAY,GAAG,EAAO,OAAO,IAAS,IAAI,MAAM,GAAG,KAAK;MACxD,gBAAc,MAAa;MAC3B,iBAAe;MACf,eAAe,EAAa,EAAO;MACnC,oBAAoB;AAClB,OAAK,KAAU,EAAW,EAAO;;MAEnC,oBAAoB;AAClB,OAAK,KAAU,EAAW,EAAE;;MAE9B,WAAW,EACT,kKACA,IAAW,mBAAmB,qBAC/B;gBAED,kBAAC,GAAD;OACE,MAAM,IAAW,eAAe;OAChC,OAAM;OACN,WAAU;OACV,CAAA;MACK,EAtBF,EAsBE;;IAGT,CAAA;GAEL,IAAe,KACd,kBAAC,KAAD;IAAG,WAAU;IAAwB,aAAU;cAC5C,EAAkB,EAAA;IACjB,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"ChatWindowCsatThankYou.js","names":[],"sources":["../../lib/ui/ChatWindowCsatThankYou.tsx"],"sourcesContent":["'use client';\n\nimport { Icon } from '@clubmed/trident-icons';\nimport type { HTMLAttributes } from 'react';\nimport { twMerge } from './helpers/twMerge';\n\nexport interface ChatWindowCsatThankYouLabels {\n title: string;\n body: string;\n}\n\nexport interface ChatWindowCsatThankYouProps extends HTMLAttributes<HTMLDivElement> {\n labels: ChatWindowCsatThankYouLabels;\n}\n\nexport function ChatWindowCsatThankYou({\n labels,\n className,\n ...props\n}: ChatWindowCsatThankYouProps) {\n return (\n <div {...props} className={twMerge('flex flex-col gap-20 px-20', className)}>\n <div className=\"flex items-center gap-4 text-green\">\n <Icon name=\"CheckFilled\" width=\"24px\" className=\"shrink-0\" />\n <p className=\"text-b3 font-semibold text-black\">{labels.title}</p>\n </div>\n <p className=\"text-b3 text-black\">{labels.body}</p>\n </div>\n );\n}\n"],"mappings":";;;;;AAeA,SAAgB,EAAuB,EACrC,WACA,cACA,GAAG,KAC2B;AAC9B,QACE,kBAAC,OAAD;EAAK,GAAI;EAAO,WAAW,EAAQ,8BAA8B,EAAU;YAA3E,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IAAM,MAAK;IAAc,OAAM;IAAO,WAAU;IAAa,CAAA,EAC7D,kBAAC,KAAD;IAAG,WAAU;cAAoC,EAAO;IAAU,CAAA,CAAA;MAEpE,kBAAC,KAAD;GAAG,WAAU;aAAsB,EAAO;GAAS,CAAA,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"ChatWindowQuote.js","names":[],"sources":["../../lib/ui/ChatWindowQuote.tsx"],"sourcesContent":["'use client';\n\nimport { twMerge } from './helpers/twMerge';\nimport { Button } from './buttons/Button';\nimport { Scrollbar } from './Scrollbar';\n\nexport interface ChatWindowQuoteItem {\n id: string;\n resortName: string;\n country: string;\n imageUrl: string;\n /** Date range, e.g. \"15 June 2026 to 24 June 2026\" */\n dateRange: string;\n /** Pax info, e.g. \"for 2 adults 2 children\" */\n pax: string;\n /** CTA button label. Defaults to \"See prices\" */\n ctaLabel?: string;\n /** Bullet-point package items. Shown in single-quote inline mode only */\n packageIncludes?: string[];\n /** Free-text paragraph shown in single-quote inline mode only */\n additionalInfo?: string;\n}\n\nexport interface ChatWindowQuoteProps {\n quotes: ChatWindowQuoteItem[];\n onCtaClick?: (quote: ChatWindowQuoteItem) => void;\n className?: string;\n}\n\ninterface QuoteCardProps {\n quote: ChatWindowQuoteItem;\n onCtaClick?: (quote: ChatWindowQuoteItem) => void;\n}\n\nfunction SingleQuoteCard({ quote, onCtaClick }: QuoteCardProps) {\n return (\n <article\n aria-label={quote.resortName}\n className=\"flex flex-col bg-white border border-lightGrey rounded-32 shadow-[0px_2px_14px_0px_rgba(0,0,0,0.25)] overflow-hidden p-24 gap-16\"\n >\n <div className=\"relative h-210 w-full overflow-hidden rounded-16\">\n <img src={quote.imageUrl} alt=\"\" className=\"absolute inset-0 size-full object-cover\" />\n <div className=\"absolute inset-0 bg-gradient-to-b from-transparent from-50% to-[rgba(36,36,36,0.4)]\" />\n <div className=\"absolute bottom-20 inset-x-0 flex flex-col items-center text-white text-center px-20\">\n <p className=\"text-b2 font-semibold\">{quote.resortName}</p>\n <p className=\"text-b3\">{quote.country}</p>\n </div>\n </div>\n <div className=\"flex flex-col gap-16 items-center\">\n <p className=\"text-b4 font-semibold text-black text-center\">\n {quote.resortName}, {quote.dateRange}, {quote.pax}\n </p>\n <Button color=\"saffron\" onClick={() => onCtaClick?.(quote)}>\n {quote.ctaLabel ?? 'See prices'}\n </Button>\n {quote.packageIncludes && quote.packageIncludes.length > 0 && (\n <div className=\"flex flex-col gap-4 w-full\">\n <p className=\"text-b3 font-semibold text-black\">Your All-Inclusive package includes:</p>\n <ul className=\"flex flex-col gap-2\">\n {quote.packageIncludes.map((item) => (\n <li key={item} className=\"text-b3 text-black\">\n • {item}\n </li>\n ))}\n </ul>\n </div>\n )}\n {quote.additionalInfo && <p className=\"text-b3 text-black\">{quote.additionalInfo}</p>}\n </div>\n </article>\n );\n}\n\nfunction CompactQuoteCard({ quote, onCtaClick }: QuoteCardProps) {\n return (\n <article\n aria-label={quote.resortName}\n className=\"flex flex-col gap-12 items-center shrink-0 w-200\"\n >\n <div className=\"relative size-200 rounded-16 overflow-hidden\">\n <img src={quote.imageUrl} alt=\"\" className=\"absolute inset-0 size-full object-cover\" />\n <div className=\"absolute inset-0 rounded-16 bg-gradient-to-b from-transparent from-50% to-[rgba(36,36,36,0.4)]\" />\n <div className=\"absolute bottom-20 inset-x-0 flex flex-col items-center text-white text-center px-8\">\n <p className=\"text-b4 font-semibold\">{quote.resortName}</p>\n <p className=\"text-b5\">{quote.country}</p>\n </div>\n </div>\n <div className=\"flex flex-col gap-8 items-center\">\n <p className=\"text-b5 font-semibold text-black text-center\">\n {quote.resortName}\n <br />\n {quote.dateRange},\n <br />\n {quote.pax}\n </p>\n <Button color=\"black\" onClick={() => onCtaClick?.(quote)}>\n {quote.ctaLabel ?? 'See prices'}\n </Button>\n </div>\n </article>\n );\n}\n\nexport function ChatWindowQuote({ quotes, onCtaClick, className }: ChatWindowQuoteProps) {\n if (quotes.length === 0) return null;\n\n if (quotes.length === 1) {\n return (\n <div className={twMerge(className)}>\n <SingleQuoteCard quote={quotes[0]} onCtaClick={onCtaClick} />\n </div>\n );\n }\n\n return (\n <div className={twMerge('flex flex-col gap-8', className)}>\n <Scrollbar\n timelineName=\"--chat-quote-scroller\"\n scrollerClassName=\"pb-24\"\n scrollbarWidth=\"60%\"\n aria-label=\"Quote options\"\n >\n <div className=\"flex gap-12\">\n {quotes.map((quote) => (\n <CompactQuoteCard key={quote.id} quote={quote} onCtaClick={onCtaClick} />\n ))}\n </div>\n </Scrollbar>\n </div>\n );\n}\n"],"mappings":";;;;;;AAkCA,SAAS,EAAgB,EAAE,UAAO,iBAA8B;AAC9D,QACE,kBAAC,WAAD;EACE,cAAY,EAAM;EAClB,WAAU;YAFZ,CAIE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,KAAK,EAAM;KAAU,KAAI;KAAG,WAAU;KAA4C,CAAA;IACvF,kBAAC,OAAD,EAAK,WAAU,uFAAwF,CAAA;IACvG,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAyB,EAAM;MAAe,CAAA,EAC3D,kBAAC,KAAD;MAAG,WAAU;gBAAW,EAAM;MAAY,CAAA,CAAA;;;MAG9C,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,KAAD;KAAG,WAAU;eAAb;MACG,EAAM;MAAW;MAAG,EAAM;MAAU;MAAG,EAAM;;;IAEhD,kBAAC,GAAD;KAAQ,OAAM;KAAU,eAAe,IAAa,EAAM;eACvD,EAAM,YAAY;KACZ,CAAA;IACR,EAAM,mBAAmB,EAAM,gBAAgB,SAAS,KACvD,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAmC;MAAwC,CAAA,EACxF,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAM,gBAAgB,KAAK,MAC1B,kBAAC,MAAD;OAAe,WAAU;iBAAzB,CAA8C,MACzC,EAAA;SADI,EAEJ,CAAA;MAEJ,CAAA,CAAA;;IAGR,EAAM,kBAAkB,kBAAC,KAAD;KAAG,WAAU;eAAsB,EAAM;KAAmB,CAAA;;;;;AAM7F,SAAS,EAAiB,EAAE,UAAO,iBAA8B;AAC/D,QACE,kBAAC,WAAD;EACE,cAAY,EAAM;EAClB,WAAU;YAFZ,CAIE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,KAAK,EAAM;KAAU,KAAI;KAAG,WAAU;KAA4C,CAAA;IACvF,kBAAC,OAAD,EAAK,WAAU,kGAAmG,CAAA;IAClH,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAyB,EAAM;MAAe,CAAA,EAC3D,kBAAC,KAAD;MAAG,WAAU;gBAAW,EAAM;MAAY,CAAA,CAAA;;;MAG9C,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,KAAD;IAAG,WAAU;cAAb;KACG,EAAM;KACP,kBAAC,MAAD,EAAM,CAAA;KACL,EAAM;KAAU;KACjB,kBAAC,MAAD,EAAM,CAAA;KACL,EAAM;;OAET,kBAAC,GAAD;IAAQ,OAAM;IAAQ,eAAe,IAAa,EAAM;cACrD,EAAM,YAAY;IACZ,CAAA,CAAA;;;;AAMjB,SAAgB,EAAgB,EAAE,WAAQ,eAAY,gBAAmC;AAWvF,QAVI,EAAO,WAAW,IAAU,OAE5B,EAAO,WAAW,IAElB,kBAAC,OAAD;EAAK,WAAW,EAAQ,EAAU;YAChC,kBAAC,GAAD;GAAiB,OAAO,EAAO;GAAgB;GAAc,CAAA;EACzD,CAAA,GAKR,kBAAC,OAAD;EAAK,WAAW,EAAQ,uBAAuB,EAAU;YACvD,kBAAC,GAAD;GACE,cAAa;GACb,mBAAkB;GAClB,gBAAe;GACf,cAAW;aAEX,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAO,KAAK,MACX,kBAAC,GAAD;KAAwC;KAAmB;KAAc,EAAlD,EAAM,GAA4C,CAAA;IAEvE,CAAA;GACI,CAAA;EACR,CAAA"}