@clubmed/trident-ui 2.0.0-beta.63 → 2.0.0-beta.65

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 (86) hide show
  1. package/chunks/Backdrop.js +1 -1
  2. package/chunks/Backdrop.js.map +1 -1
  3. package/chunks/DateField.js +2 -2
  4. package/chunks/DateField.js.map +1 -1
  5. package/examples/journey-card-compact-demo.d.ts +1 -0
  6. package/examples/journey-card-compact-demo.js +71 -0
  7. package/examples/journey-card-compact-demo.js.map +1 -0
  8. package/examples/journey-card-demo.d.ts +1 -0
  9. package/examples/journey-card-demo.js +123 -0
  10. package/examples/journey-card-demo.js.map +1 -0
  11. package/examples/tabs-notification-demo.d.ts +1 -0
  12. package/examples/tabs-notification-demo.js +64 -0
  13. package/examples/tabs-notification-demo.js.map +1 -0
  14. package/package.json +1 -1
  15. package/styles/ui/controls.css +3 -6
  16. package/styles/ui/range.css +5 -0
  17. package/ui/AdvancedToast.js +2 -2
  18. package/ui/AdvancedToast.js.map +1 -1
  19. package/ui/Avatar.js +1 -1
  20. package/ui/Avatar.js.map +1 -1
  21. package/ui/BasicToast.js +2 -2
  22. package/ui/BasicToast.js.map +1 -1
  23. package/ui/Breadcrumb.js +1 -0
  24. package/ui/Breadcrumb.js.map +1 -1
  25. package/ui/ChatButton.js +1 -1
  26. package/ui/ChatButton.js.map +1 -1
  27. package/ui/ChatInput.js +1 -1
  28. package/ui/ChatInput.js.map +1 -1
  29. package/ui/ChatWindow.js +2 -2
  30. package/ui/ChatWindow.js.map +1 -1
  31. package/ui/ChatWindowCsatPrompt.js +1 -1
  32. package/ui/ChatWindowCsatPrompt.js.map +1 -1
  33. package/ui/ChoiceExpander.js +1 -1
  34. package/ui/ChoiceExpander.js.map +1 -1
  35. package/ui/ExpandableText.js +1 -1
  36. package/ui/ExpandableText.js.map +1 -1
  37. package/ui/HamburgerIcon.js +1 -1
  38. package/ui/HamburgerIcon.js.map +1 -1
  39. package/ui/Link.js +1 -1
  40. package/ui/Link.js.map +1 -1
  41. package/ui/PageNavigation.js +1 -1
  42. package/ui/PageNavigation.js.map +1 -1
  43. package/ui/Pagination.js +2 -2
  44. package/ui/Pagination.js.map +1 -1
  45. package/ui/SidebarLayout.js +4 -4
  46. package/ui/SidebarLayout.js.map +1 -1
  47. package/ui/Toggle.js +1 -1
  48. package/ui/Toggle.js.map +1 -1
  49. package/ui/buttons/Button.type.js +1 -1
  50. package/ui/buttons/Button.type.js.map +1 -1
  51. package/ui/forms/CheckboxSelect.js +1 -1
  52. package/ui/forms/CheckboxSelect.js.map +1 -1
  53. package/ui/forms/FloatingLabelTextField.js +2 -2
  54. package/ui/forms/FloatingLabelTextField.js.map +1 -1
  55. package/ui/forms/NumberField.js +1 -1
  56. package/ui/forms/NumberField.js.map +1 -1
  57. package/ui/forms/PhoneFieldFullInput.js +1 -1
  58. package/ui/forms/PhoneFieldFullInput.js.map +1 -1
  59. package/ui/forms/PhoneFieldSplitInput.js +2 -2
  60. package/ui/forms/PhoneFieldSplitInput.js.map +1 -1
  61. package/ui/forms/Select.js +1 -1
  62. package/ui/forms/Select.js.map +1 -1
  63. package/ui/forms/TextField.js +3 -3
  64. package/ui/forms/TextField.js.map +1 -1
  65. package/ui/forms/checkboxes/Checkbox.js +1 -1
  66. package/ui/forms/checkboxes/Checkbox.js.map +1 -1
  67. package/ui/forms/password/Password.js +2 -2
  68. package/ui/forms/password/Password.js.map +1 -1
  69. package/ui/journey-card/FlightSegmentCard.d.ts +15 -0
  70. package/ui/journey-card/FlightSegmentCard.js +73 -0
  71. package/ui/journey-card/FlightSegmentCard.js.map +1 -0
  72. package/ui/journey-card/FlightTimeline.d.ts +5 -0
  73. package/ui/journey-card/FlightTimeline.js +27 -0
  74. package/ui/journey-card/FlightTimeline.js.map +1 -0
  75. package/ui/journey-card/ItineraryDetails.d.ts +9 -0
  76. package/ui/journey-card/ItineraryDetails.js +40 -0
  77. package/ui/journey-card/ItineraryDetails.js.map +1 -0
  78. package/ui/journey-card/JourneyCard.d.ts +17 -0
  79. package/ui/journey-card/JourneyCard.js +34 -0
  80. package/ui/journey-card/JourneyCard.js.map +1 -0
  81. package/ui/journey-card/JourneyDetails.d.ts +47 -0
  82. package/ui/journey-card/JourneyDetails.js +183 -0
  83. package/ui/journey-card/JourneyDetails.js.map +1 -0
  84. package/ui/tabs/Tabs.d.ts +5 -1
  85. package/ui/tabs/Tabs.js +49 -43
  86. package/ui/tabs/Tabs.js.map +1 -1
package/ui/BasicToast.js CHANGED
@@ -18,7 +18,7 @@ function l({ className: l, description: u, href: d, icon: f, incentive: p, isVis
18
18
  width: "30px"
19
19
  }),
20
20
  /* @__PURE__ */ c("a", {
21
- className: "group pe-44",
21
+ className: "group pe-44 outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 rounded-pill",
22
22
  href: d,
23
23
  onClick: g,
24
24
  children: [
@@ -41,7 +41,7 @@ function l({ className: l, description: u, href: d, icon: f, incentive: p, isVis
41
41
  _ && /* @__PURE__ */ s("button", {
42
42
  "aria-describedby": C,
43
43
  "aria-label": h.close,
44
- className: e("absolute end-0 top-0 m-20", n(S)),
44
+ className: e("absolute end-0 top-0 m-20 outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 rounded-pill", n(S)),
45
45
  onClick: _,
46
46
  children: /* @__PURE__ */ s(o, {
47
47
  name: "CrossDefault",
@@ -1 +1 @@
1
- {"version":3,"file":"BasicToast.js","names":[],"sources":["../../lib/ui/BasicToast.tsx"],"sourcesContent":["'use client';\nimport clsx from 'clsx';\nimport { type ComponentPropsWithoutRef, type PropsWithChildren, useId } from 'react';\n\nimport { Icon, type IconicNames } from '@clubmed/trident-icons';\nimport { getBgColor, getComplementaryTextColor, getThemeColor } from './helpers/colors/colors';\nimport { Link } from './Link';\nimport type { Theme } from './types/Theme';\n\nexport interface BasicToastProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * @deprecated use children slot instead\n */\n description?: string;\n href: string;\n icon?: IconicNames;\n incentive: string;\n isVisible: boolean;\n labels: { close: string };\n onClick?: () => void;\n onClose?: () => void;\n theme: Theme;\n title: string;\n}\n\nexport function BasicToast({\n className,\n description,\n href,\n icon,\n incentive,\n isVisible,\n labels,\n onClick,\n onClose,\n theme = 'dark',\n title,\n children,\n ...attrs\n}: PropsWithChildren<BasicToastProps>) {\n const color = getThemeColor(theme);\n const id = useId();\n const content = children ?? description;\n\n return (\n <div\n {...attrs}\n className={clsx(\n isVisible ? 'animate-slide-right-enter' : 'animate-slide-right-exit opacity-0',\n 'relative isolate flex gap-x-20 rounded-16 p-20 transition-opacity duration-300',\n getBgColor(color),\n getComplementaryTextColor(color),\n className,\n )}\n role=\"note\"\n >\n {icon && <Icon name={icon} width=\"30px\" />}\n <a className=\"group pe-44\" href={href} onClick={onClick}>\n <p className=\"mb-4 text-b3 font-bold\" id={id}>\n {title}\n </p>\n {content && <div className=\"mb-12 text-b4\">{content}</div>}\n <Link component=\"span\" className=\"text-b3 font-bold\" label={incentive} />\n </a>\n {onClose && (\n <button\n aria-describedby={id}\n aria-label={labels.close}\n className={clsx('absolute end-0 top-0 m-20', getComplementaryTextColor(color))}\n onClick={onClose}\n >\n <Icon name=\"CrossDefault\" width=\"24px\" />\n </button>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;AAyBA,SAAgB,EAAW,EACzB,cACA,gBACA,SACA,SACA,cACA,cACA,WACA,YACA,YACA,WAAQ,QACR,UACA,aACA,GAAG,KACkC;CACrC,IAAM,IAAQ,EAAc,EAAM,EAC5B,IAAK,GAAO,EACZ,IAAU,KAAY;AAE5B,QACE,kBAAC,OAAD;EACE,GAAI;EACJ,WAAW,EACT,IAAY,8BAA8B,sCAC1C,kFACA,EAAW,EAAM,EACjB,EAA0B,EAAM,EAChC,EACD;EACD,MAAK;YATP;GAWG,KAAQ,kBAAC,GAAD;IAAM,MAAM;IAAM,OAAM;IAAS,CAAA;GAC1C,kBAAC,KAAD;IAAG,WAAU;IAAoB;IAAe;cAAhD;KACE,kBAAC,KAAD;MAAG,WAAU;MAA6B;gBACvC;MACC,CAAA;KACH,KAAW,kBAAC,OAAD;MAAK,WAAU;gBAAiB;MAAc,CAAA;KAC1D,kBAAC,GAAD;MAAM,WAAU;MAAO,WAAU;MAAoB,OAAO;MAAa,CAAA;;;GAE1E,KACC,kBAAC,UAAD;IACE,oBAAkB;IAClB,cAAY,EAAO;IACnB,WAAW,EAAK,6BAA6B,EAA0B,EAAM,CAAC;IAC9E,SAAS;cAET,kBAAC,GAAD;KAAM,MAAK;KAAe,OAAM;KAAS,CAAA;IAClC,CAAA"}
1
+ {"version":3,"file":"BasicToast.js","names":[],"sources":["../../lib/ui/BasicToast.tsx"],"sourcesContent":["'use client';\nimport clsx from 'clsx';\nimport { type ComponentPropsWithoutRef, type PropsWithChildren, useId } from 'react';\n\nimport { Icon, type IconicNames } from '@clubmed/trident-icons';\nimport { getBgColor, getComplementaryTextColor, getThemeColor } from './helpers/colors/colors';\nimport { Link } from './Link';\nimport type { Theme } from './types/Theme';\n\nexport interface BasicToastProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * @deprecated use children slot instead\n */\n description?: string;\n href: string;\n icon?: IconicNames;\n incentive: string;\n isVisible: boolean;\n labels: { close: string };\n onClick?: () => void;\n onClose?: () => void;\n theme: Theme;\n title: string;\n}\n\nexport function BasicToast({\n className,\n description,\n href,\n icon,\n incentive,\n isVisible,\n labels,\n onClick,\n onClose,\n theme = 'dark',\n title,\n children,\n ...attrs\n}: PropsWithChildren<BasicToastProps>) {\n const color = getThemeColor(theme);\n const id = useId();\n const content = children ?? description;\n\n return (\n <div\n {...attrs}\n className={clsx(\n isVisible ? 'animate-slide-right-enter' : 'animate-slide-right-exit opacity-0',\n 'relative isolate flex gap-x-20 rounded-16 p-20 transition-opacity duration-300',\n getBgColor(color),\n getComplementaryTextColor(color),\n className,\n )}\n role=\"note\"\n >\n {icon && <Icon name={icon} width=\"30px\" />}\n <a\n className=\"group pe-44 outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 rounded-pill\"\n href={href}\n onClick={onClick}\n >\n <p className=\"mb-4 text-b3 font-bold\" id={id}>\n {title}\n </p>\n {content && <div className=\"mb-12 text-b4\">{content}</div>}\n <Link component=\"span\" className=\"text-b3 font-bold\" label={incentive} />\n </a>\n {onClose && (\n <button\n aria-describedby={id}\n aria-label={labels.close}\n className={clsx(\n 'absolute end-0 top-0 m-20 outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 rounded-pill',\n getComplementaryTextColor(color),\n )}\n onClick={onClose}\n >\n <Icon name=\"CrossDefault\" width=\"24px\" />\n </button>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;AAyBA,SAAgB,EAAW,EACzB,cACA,gBACA,SACA,SACA,cACA,cACA,WACA,YACA,YACA,WAAQ,QACR,UACA,aACA,GAAG,KACkC;CACrC,IAAM,IAAQ,EAAc,EAAM,EAC5B,IAAK,GAAO,EACZ,IAAU,KAAY;AAE5B,QACE,kBAAC,OAAD;EACE,GAAI;EACJ,WAAW,EACT,IAAY,8BAA8B,sCAC1C,kFACA,EAAW,EAAM,EACjB,EAA0B,EAAM,EAChC,EACD;EACD,MAAK;YATP;GAWG,KAAQ,kBAAC,GAAD;IAAM,MAAM;IAAM,OAAM;IAAS,CAAA;GAC1C,kBAAC,KAAD;IACE,WAAU;IACJ;IACG;cAHX;KAKE,kBAAC,KAAD;MAAG,WAAU;MAA6B;gBACvC;MACC,CAAA;KACH,KAAW,kBAAC,OAAD;MAAK,WAAU;gBAAiB;MAAc,CAAA;KAC1D,kBAAC,GAAD;MAAM,WAAU;MAAO,WAAU;MAAoB,OAAO;MAAa,CAAA;;;GAE1E,KACC,kBAAC,UAAD;IACE,oBAAkB;IAClB,cAAY,EAAO;IACnB,WAAW,EACT,iIACA,EAA0B,EAAM,CACjC;IACD,SAAS;cAET,kBAAC,GAAD;KAAM,MAAK;KAAe,OAAM;KAAS,CAAA;IAClC,CAAA"}
package/ui/Breadcrumb.js CHANGED
@@ -32,6 +32,7 @@ var o = ({ className: o, items: s, theme: c = "light", ...l }) => {
32
32
  key: t
33
33
  }, c ? /* @__PURE__ */ a("a", {
34
34
  href: n,
35
+ className: "rounded-pill outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2",
35
36
  children: t
36
37
  }) : t, !s && /* @__PURE__ */ a(i, {
37
38
  className: "mx-4",
@@ -1 +1 @@
1
- {"version":3,"file":"Breadcrumb.js","names":[],"sources":["../../lib/ui/Breadcrumb.tsx"],"sourcesContent":["import clsx from 'clsx';\nimport type { ComponentPropsWithoutRef, FunctionComponent } from 'react';\n\nimport type { Theme } from './types/Theme';\nimport { Icon } from '@clubmed/trident-icons';\nimport { getComplementaryTextColor, getThemeColor } from './helpers/colors/colors';\n\nexport interface BreadcrumbProps extends ComponentPropsWithoutRef<'nav'> {\n items: Array<{\n href: string;\n label: string;\n }>;\n theme?: Theme;\n}\n\nexport const Breadcrumb: FunctionComponent<BreadcrumbProps> = ({\n className,\n items,\n theme = 'light',\n ...attrs\n}) => {\n if (items.length == 0) {\n return null;\n }\n\n const themeColor = getThemeColor(theme);\n const accentColor = getComplementaryTextColor(themeColor);\n const color = accentColor === 'text-white' ? accentColor : 'text-middleGrey';\n\n const [first, second, , ...rest] = items;\n const lastItem = rest.length ? rest[rest.length - 1] : items[items.length - 1];\n const spacer = { label: '…', href: '' };\n const itemsToRender = !rest.length ? items : [first, second, spacer, lastItem];\n\n return (\n <nav aria-label=\"Breadcrumb\" {...attrs} className={clsx(className, 'overflow-hidden')}>\n <ul className=\"scrollbar-hidden text-b4 flex items-center overflow-x-auto font-sans\">\n {itemsToRender.map(({ label, href }, index) => {\n const isLast = index === itemsToRender.length - 1;\n const isLink = href && !isLast;\n\n return (\n <li\n {...(isLast && { 'aria-current': 'page' })}\n className={clsx(\n 'flex shrink-0 items-center',\n { [color]: !isLast },\n {\n 'font-bold': isLast,\n [accentColor]: isLast,\n },\n )}\n key={label}\n >\n {isLink ? <a href={href}>{label}</a> : label}\n {!isLast && <Icon className=\"mx-4\" name=\"Diamond\" width=\"14px\" />}\n </li>\n );\n })}\n </ul>\n </nav>\n );\n};\n"],"mappings":";;;;;;AAeA,IAAa,KAAkD,EAC7D,cACA,UACA,WAAQ,SACR,GAAG,QACC;AACJ,KAAI,EAAM,UAAU,EAClB,QAAO;CAIT,IAAM,IAAc,EADD,EAAc,EAAM,CACkB,EACnD,IAAQ,MAAgB,eAAe,IAAc,mBAErD,CAAC,GAAO,KAAU,GAAG,KAAQ,GAC7B,IAAW,EAAK,SAAS,EAAK,EAAK,SAAS,KAAK,EAAM,EAAM,SAAS,IAEtE,IAAiB,EAAK,SAAiB;EAAC;EAAO;EADtC;GAAE,OAAO;GAAK,MAAM;GAAI;EAC8B;EAAS,GAAzC;AAErC,QACE,kBAAC,OAAD;EAAK,cAAW;EAAa,GAAI;EAAO,WAAW,EAAK,GAAW,kBAAkB;YACnF,kBAAC,MAAD;GAAI,WAAU;aACX,EAAc,KAAK,EAAE,UAAO,WAAQ,MAAU;IAC7C,IAAM,IAAS,MAAU,EAAc,SAAS,GAC1C,IAAS,KAAQ,CAAC;AAExB,WACE,kBAAC,MAAD;KACE,GAAK,KAAU,EAAE,gBAAgB,QAAQ;KACzC,WAAW,EACT,8BACA,GAAG,IAAQ,CAAC,GAAQ,EACpB;MACE,aAAa;OACZ,IAAc;MAChB,CACF;KACD,KAAK;KAIF,EAFF,IAAS,kBAAC,KAAD;KAAS;eAAO;KAAU,CAAA,GAAG,GACtC,CAAC,KAAU,kBAAC,GAAD;KAAM,WAAU;KAAO,MAAK;KAAU,OAAM;KAAS,CAAA,CAC9D;KAEP;GACC,CAAA;EACD,CAAA"}
1
+ {"version":3,"file":"Breadcrumb.js","names":[],"sources":["../../lib/ui/Breadcrumb.tsx"],"sourcesContent":["import clsx from 'clsx';\nimport type { ComponentPropsWithoutRef, FunctionComponent } from 'react';\n\nimport type { Theme } from './types/Theme';\nimport { Icon } from '@clubmed/trident-icons';\nimport { getComplementaryTextColor, getThemeColor } from './helpers/colors/colors';\n\nexport interface BreadcrumbProps extends ComponentPropsWithoutRef<'nav'> {\n items: Array<{\n href: string;\n label: string;\n }>;\n theme?: Theme;\n}\n\nexport const Breadcrumb: FunctionComponent<BreadcrumbProps> = ({\n className,\n items,\n theme = 'light',\n ...attrs\n}) => {\n if (items.length == 0) {\n return null;\n }\n\n const themeColor = getThemeColor(theme);\n const accentColor = getComplementaryTextColor(themeColor);\n const color = accentColor === 'text-white' ? accentColor : 'text-middleGrey';\n\n const [first, second, , ...rest] = items;\n const lastItem = rest.length ? rest[rest.length - 1] : items[items.length - 1];\n const spacer = { label: '…', href: '' };\n const itemsToRender = !rest.length ? items : [first, second, spacer, lastItem];\n\n return (\n <nav aria-label=\"Breadcrumb\" {...attrs} className={clsx(className, 'overflow-hidden')}>\n <ul className=\"scrollbar-hidden text-b4 flex items-center overflow-x-auto font-sans\">\n {itemsToRender.map(({ label, href }, index) => {\n const isLast = index === itemsToRender.length - 1;\n const isLink = href && !isLast;\n\n return (\n <li\n {...(isLast && { 'aria-current': 'page' })}\n className={clsx(\n 'flex shrink-0 items-center',\n { [color]: !isLast },\n {\n 'font-bold': isLast,\n [accentColor]: isLast,\n },\n )}\n key={label}\n >\n {isLink ? (\n <a\n href={href}\n className=\"rounded-pill outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2\"\n >\n {label}\n </a>\n ) : (\n label\n )}\n {!isLast && <Icon className=\"mx-4\" name=\"Diamond\" width=\"14px\" />}\n </li>\n );\n })}\n </ul>\n </nav>\n );\n};\n"],"mappings":";;;;;;AAeA,IAAa,KAAkD,EAC7D,cACA,UACA,WAAQ,SACR,GAAG,QACC;AACJ,KAAI,EAAM,UAAU,EAClB,QAAO;CAIT,IAAM,IAAc,EADD,EAAc,EAAM,CACkB,EACnD,IAAQ,MAAgB,eAAe,IAAc,mBAErD,CAAC,GAAO,KAAU,GAAG,KAAQ,GAC7B,IAAW,EAAK,SAAS,EAAK,EAAK,SAAS,KAAK,EAAM,EAAM,SAAS,IAEtE,IAAiB,EAAK,SAAiB;EAAC;EAAO;EADtC;GAAE,OAAO;GAAK,MAAM;GAAI;EAC8B;EAAS,GAAzC;AAErC,QACE,kBAAC,OAAD;EAAK,cAAW;EAAa,GAAI;EAAO,WAAW,EAAK,GAAW,kBAAkB;YACnF,kBAAC,MAAD;GAAI,WAAU;aACX,EAAc,KAAK,EAAE,UAAO,WAAQ,MAAU;IAC7C,IAAM,IAAS,MAAU,EAAc,SAAS,GAC1C,IAAS,KAAQ,CAAC;AAExB,WACE,kBAAC,MAAD;KACE,GAAK,KAAU,EAAE,gBAAgB,QAAQ;KACzC,WAAW,EACT,8BACA,GAAG,IAAQ,CAAC,GAAQ,EACpB;MACE,aAAa;OACZ,IAAc;MAChB,CACF;KACD,KAAK;KAaF,EAXF,IACC,kBAAC,KAAD;KACQ;KACN,WAAU;eAET;KACC,CAAA,GAEJ,GAED,CAAC,KAAU,kBAAC,GAAD;KAAM,WAAU;KAAO,MAAK;KAAU,OAAM;KAAS,CAAA,CAC9D;KAEP;GACC,CAAA;EACD,CAAA"}
package/ui/ChatButton.js CHANGED
@@ -10,7 +10,7 @@ function o({ children: o = i, className: s, type: c = "button", "aria-label": l,
10
10
  "aria-label": m,
11
11
  tabIndex: u ?? 0,
12
12
  ...p,
13
- className: e("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-8 focus-visible:ring-lavender/20 pointer-events-auto h-48 min-h-48 md:min-w-80 md:h-80 active:scale-[0.98]", d && "md:translate-x-[40px] focus-visible:translate-x-0", f === "hover" && "group/hoverChatbutton hover:grid-cols-[24px_1fr] hover:translate-x-0", f === "active" && "group/activeChatbutton active:grid-cols-[24px_1fr] active:translate-x-0", "focus-visible:grid-cols-[24px_1fr]", s),
13
+ className: e("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]", d && "md:translate-x-[40px] focus-visible:translate-x-0", f === "hover" && "group/hoverChatbutton hover:grid-cols-[24px_1fr] hover:translate-x-0", f === "active" && "group/activeChatbutton active:grid-cols-[24px_1fr] active:translate-x-0", "focus-visible:grid-cols-[24px_1fr]", s),
14
14
  children: [/* @__PURE__ */ n(t, {
15
15
  name: "Sparkles",
16
16
  className: e("chat-button-icon w-24 transition-all", d && "relative md:start-[calc(-16px)] group-focus-visible/Chatbutton:start-0 group-hover/hoverChatbutton:start-0 group-active/activeChatbutton:start-0")
@@ -1 +1 @@
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-8 focus-visible:ring-lavender/20 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,6UACA,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
+ {"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"}
package/ui/ChatInput.js CHANGED
@@ -107,7 +107,7 @@ function m({ value: n, placeholder: r = "Ask a question?", onValueChange: u, onS
107
107
  placeholder: r,
108
108
  "aria-label": r,
109
109
  style: { marginInlineEnd: b ? 0 : E + l },
110
- className: t("scrollbar-hidden bg-transparent text-b3 py-12 outline-none resize-none w-full px-20 border border-middleGrey", "transition-[margin-inline-end,border-radius] duration-250 ease-out", b ? "text-black rounded-[20]" : "text-darkGrey placeholder:text-darkGrey rounded-[40]")
110
+ className: t("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", "transition-[margin-inline-end,border-radius] duration-250 ease-out", b ? "text-black rounded-[20]" : "text-darkGrey placeholder:text-darkGrey rounded-[40]")
111
111
  }), /* @__PURE__ */ o("div", {
112
112
  style: {
113
113
  insetInlineStart: b ? `calc(50% - ${E / 2}px)` : `calc(100% - ${s + E}px)`,
@@ -1 +1 @@
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-light-grey p-12 w-full',\n 'transition-[padding-bottom,border-radius] duration-250 ease-out rounded-b-[24]',\n isOpen ? 'rounded-32' : 'rounded-[40]',\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 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-[20]'\n : 'text-darkGrey placeholder:text-darkGrey rounded-[40]',\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":";;;;;;AAiBA,IAAM,IAAW,IACX,IAAW,IACX,IAAU;AAGhB,SAAS,EAAwD,GAAO;CACtE,IAAM,IAAM,EAAO,EAAG;AAItB,QAHA,QAAgB;AACd,IAAI,UAAU;GACd,EACK;;AAGT,IAAM,IAAqB;AAE3B,SAAS,EAAqB,GAAoC;CAChE,IAAM,CAAC,GAAa,KAAkB,EAAS,GAAM,EAC/C,CAAC,GAAa,KAAkB,EAAS,GAAM,EAC/C,IAAiB,EAAiC,KAAK,EACvD,IAAkB,EAA6C,KAAK,EACpE,IAAiB,EAAO,GAAM,EAC9B,IAAmB,EAAe,EAAc;CAEtD,SAAS,IAAoB;AAC3B,EAAI,EAAgB,WAAS,aAAa,EAAgB,QAAQ;;CAGpE,SAAS,IAAoB;AAE3B,EADA,GAAmB,EACnB,EAAgB,UAAU,iBAAiB;AAGzC,GAFA,EAAe,UAAU,IACzB,EAAe,GAAM,EACrB,EAAe,SAAS,MAAM;KAC7B,EAAmB;;AAGxB,SAAgB;EACd,IAAM,IACH,OACE,2BAA2B,OAAO;AACvC,MAAI,CAAC,EAAI;AAET,IAAe,GAAK;EAEpB,IAAM,IAAc,IAAI,GAAI;AAoC5B,SAnCA,EAAY,aAAa,IACzB,EAAY,iBAAiB,IAC7B,EAAY,OAAO,UAAU,UAE7B,EAAY,YAAY,MAAkC;GACxD,IAAM,IAAa,MAAM,KAAK,EAAM,QAAQ,CACzC,KAAK,MAA+B,EAAE,GAAG,WAAW,CACpD,KAAK,GAAG;AAEX,GADA,EAAiB,QAAQ,EAAW,EACpC,GAAmB;KAGrB,EAAY,cAAc;AAGxB,OAFA,GAAmB,EAEf,EAAe,QACjB,KAAI;AACF,MAAY,OAAO;WACb;OAIR,GAAe,GAAM;KAIzB,EAAY,WAAW,MAAmC;AACxD,IAAI,EAAE,UAAU,iBAAiB,EAAE,UAAU,eAC3C,EAAe,UAAU,IACzB,EAAe,GAAM,EACrB,GAAmB;KAIvB,EAAe,UAAU,SACZ;AAEX,GADA,EAAY,OAAO,EACnB,GAAmB;;IAIpB,EAAE,CAAC;CAEN,SAAS,IAAS;EAChB,IAAM,IAAc,EAAe;AAC9B,QACD,EAAe,WACjB,EAAe,UAAU,IACzB,EAAY,MAAM,KAElB,EAAe,UAAU,IACzB,EAAe,GAAK,EACpB,EAAY,OAAO,EACnB,GAAmB;;AAIvB,QAAO;EAAE;EAAa;EAAa;EAAQ;;AAU7C,SAAS,EAAc,GAAsB;CAC3C,IAAM,IAAe,EAAwB,KAAK;AAyClD,QAvCA,QAAgB;EACd,IAAM,IAAK,EAAa;AAExB,MAAI,CAAC,GAAa;AAChB,GAAI,MAAI,EAAG,MAAM,SAAS;AAC1B;;EAGF,IAAI,GACA,IAA6B,MAC7B,IAA2B;AAsB/B,SApBA,UAAU,aAAa,aAAa;GAAE,OAAO;GAAM,OAAO;GAAO,CAAC,CAAC,MAAM,MAAM;AAE7E,GADA,IAAS,GACT,IAAM,IAAI,cAAc;GACxB,IAAM,IAAS,EAAI,wBAAwB,EAAO,EAC5C,IAAW,EAAI,gBAAgB;AAErC,GADA,EAAS,UAAU,KACnB,EAAO,QAAQ,EAAS;GACxB,IAAM,IAAO,IAAI,WAAW,EAAS,kBAAkB;GAEvD,SAAS,IAAO;AACd,MAAS,qBAAqB,EAAK;IACnC,IAAM,IAAM,EAAK,QAAQ,GAAK,MAAM,IAAM,GAAG,EAAE,GAAG,EAAK,QAEjD,IAAO,EAAa;AAE1B,IADI,MAAM,EAAK,MAAM,SAAS,GAAG,KAAK,KAAK,IAAM,IAAI,GAAG,GAAG,MAC3D,IAAS,sBAAsB,EAAK;;AAEtC,OAAS,sBAAsB,EAAK;IACpC,QAEW;AAGX,GAFA,qBAAqB,EAAO,EAC5B,GAAQ,WAAW,CAAC,SAAS,MAAM,EAAE,MAAM,CAAC,EAC5C,GAAK,OAAO;;IAEb,CAAC,EAAY,CAAC,EAEV;;AAGT,SAAgB,EAAU,EACxB,UACA,iBAAc,mBACd,kBACA,aACA,iBACA,cACA,GAAG,KACc;CACjB,IAAM,CAAC,GAAW,KAAgB,EAAS,GAAM,EAC3C,IAAU,EAAM,MAAM,EACtB,IAAS,KAAa,EAAQ,SAAS,GACvC,EAAE,gBAAa,gBAAa,cAAW,EAAqB,EAAc,EAC1E,IAAoB,EAAc,EAAY,EAE9C,IAAY,KAAe,CAAC,CAAC,GAC7B,IAAO,IAAY,IAAW,IAAU,IAAW;CAEzD,SAAS,IAAmB;AAC1B,EAAI,IACF,GAAQ,GAER,KAAgB;;CAIpB,SAAS,EAAa,GAAoB;AAExC,EADA,EAAE,gBAAgB,EACd,KAAS,GAAU;;AAGzB,QACE,kBAAC,QAAD;EACE,UAAU;EACV,cAAc,MAAM;AAClB,GAAK,EAAE,OAAuB,YAAY,cAAY,EAAE,gBAAgB;;EAE1E,GAAI;EACJ,OAAO,EACL,eAAe,IAAS,IAAW,IAAU,IAAW,GACzD;EACD,WAAW,EACT,2FACA,kFACA,IAAS,eAAe,gBACxB,EACD;YAdH,CAgBE,kBAAC,YAAD;GACE,MAAM,IAAS,IAAI;GACZ;GACP,WAAW,MAAM,EAAc,EAAE,OAAO,MAAM;GAC9C,YAAY,MAAM;AAChB,IAAI,EAAE,QAAQ,YAAY,EAAE,WAAW,EAAE,YAAY,MACnD,EAAE,gBAAgB,EAClB,GAAU;;GAGd,eAAe,EAAa,GAAK;GACjC,cAAc,EAAa,GAAM;GACpB;GACb,cAAY;GACZ,OAAO,EACL,iBAAiB,IAAS,IAAI,IAAO,GACtC;GACD,WAAW,EACT,gHACA,sEACA,IACI,4BACA,uDAAA;GAEN,CAAA,EAEF,kBAAC,OAAD;GACE,OAAO;IACL,kBAAkB,IACd,cAAc,IAAO,EAAE,OACvB,eAAe,IAAW,EAAK;IACnC,QAAQ;IACT;GACD,WAAU;aAPZ,CASE,kBAAC,GAAD;IACE,MAAK;IACL,cAAW;IACX,UAAU,CAAC;IACX,SAAQ;IACR,OAAM;IACN,MAAK;IACL,WAAU;IACV,CAAA,EACD,KACC,kBAAC,GAAD;IACE,MAAK;IACL,cAAY,IAAc,mBAAmB;IAC7C,gBAAc;IACd,SAAS;IACT,SAAQ;IACR,OAAO,IAAc,UAAU;IAC/B,OAAO,IAAc,WAAW;IAChC,MAAK;IACL,WAAU;IACV,WAAW,EACT,YACA,KAAe,iBACf,CAAC,KAAe,mBACjB;cAED,kBAAC,QAAD;KACE,KAAK;KACL,eAAY;KACZ,WAAU;KACV,OAAO,EAAE,QAAQ,OAAA;KACjB,CAAA;IACK,CAAA,CAAA"}
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-light-grey p-12 w-full',\n 'transition-[padding-bottom,border-radius] duration-250 ease-out rounded-b-[24]',\n isOpen ? 'rounded-32' : 'rounded-[40]',\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-[20]'\n : 'text-darkGrey placeholder:text-darkGrey rounded-[40]',\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":";;;;;;AAiBA,IAAM,IAAW,IACX,IAAW,IACX,IAAU;AAGhB,SAAS,EAAwD,GAAO;CACtE,IAAM,IAAM,EAAO,EAAG;AAItB,QAHA,QAAgB;AACd,IAAI,UAAU;GACd,EACK;;AAGT,IAAM,IAAqB;AAE3B,SAAS,EAAqB,GAAoC;CAChE,IAAM,CAAC,GAAa,KAAkB,EAAS,GAAM,EAC/C,CAAC,GAAa,KAAkB,EAAS,GAAM,EAC/C,IAAiB,EAAiC,KAAK,EACvD,IAAkB,EAA6C,KAAK,EACpE,IAAiB,EAAO,GAAM,EAC9B,IAAmB,EAAe,EAAc;CAEtD,SAAS,IAAoB;AAC3B,EAAI,EAAgB,WAAS,aAAa,EAAgB,QAAQ;;CAGpE,SAAS,IAAoB;AAE3B,EADA,GAAmB,EACnB,EAAgB,UAAU,iBAAiB;AAGzC,GAFA,EAAe,UAAU,IACzB,EAAe,GAAM,EACrB,EAAe,SAAS,MAAM;KAC7B,EAAmB;;AAGxB,SAAgB;EACd,IAAM,IACH,OACE,2BAA2B,OAAO;AACvC,MAAI,CAAC,EAAI;AAET,IAAe,GAAK;EAEpB,IAAM,IAAc,IAAI,GAAI;AAoC5B,SAnCA,EAAY,aAAa,IACzB,EAAY,iBAAiB,IAC7B,EAAY,OAAO,UAAU,UAE7B,EAAY,YAAY,MAAkC;GACxD,IAAM,IAAa,MAAM,KAAK,EAAM,QAAQ,CACzC,KAAK,MAA+B,EAAE,GAAG,WAAW,CACpD,KAAK,GAAG;AAEX,GADA,EAAiB,QAAQ,EAAW,EACpC,GAAmB;KAGrB,EAAY,cAAc;AAGxB,OAFA,GAAmB,EAEf,EAAe,QACjB,KAAI;AACF,MAAY,OAAO;WACb;OAIR,GAAe,GAAM;KAIzB,EAAY,WAAW,MAAmC;AACxD,IAAI,EAAE,UAAU,iBAAiB,EAAE,UAAU,eAC3C,EAAe,UAAU,IACzB,EAAe,GAAM,EACrB,GAAmB;KAIvB,EAAe,UAAU,SACZ;AAEX,GADA,EAAY,OAAO,EACnB,GAAmB;;IAIpB,EAAE,CAAC;CAEN,SAAS,IAAS;EAChB,IAAM,IAAc,EAAe;AAC9B,QACD,EAAe,WACjB,EAAe,UAAU,IACzB,EAAY,MAAM,KAElB,EAAe,UAAU,IACzB,EAAe,GAAK,EACpB,EAAY,OAAO,EACnB,GAAmB;;AAIvB,QAAO;EAAE;EAAa;EAAa;EAAQ;;AAU7C,SAAS,EAAc,GAAsB;CAC3C,IAAM,IAAe,EAAwB,KAAK;AAyClD,QAvCA,QAAgB;EACd,IAAM,IAAK,EAAa;AAExB,MAAI,CAAC,GAAa;AAChB,GAAI,MAAI,EAAG,MAAM,SAAS;AAC1B;;EAGF,IAAI,GACA,IAA6B,MAC7B,IAA2B;AAsB/B,SApBA,UAAU,aAAa,aAAa;GAAE,OAAO;GAAM,OAAO;GAAO,CAAC,CAAC,MAAM,MAAM;AAE7E,GADA,IAAS,GACT,IAAM,IAAI,cAAc;GACxB,IAAM,IAAS,EAAI,wBAAwB,EAAO,EAC5C,IAAW,EAAI,gBAAgB;AAErC,GADA,EAAS,UAAU,KACnB,EAAO,QAAQ,EAAS;GACxB,IAAM,IAAO,IAAI,WAAW,EAAS,kBAAkB;GAEvD,SAAS,IAAO;AACd,MAAS,qBAAqB,EAAK;IACnC,IAAM,IAAM,EAAK,QAAQ,GAAK,MAAM,IAAM,GAAG,EAAE,GAAG,EAAK,QAEjD,IAAO,EAAa;AAE1B,IADI,MAAM,EAAK,MAAM,SAAS,GAAG,KAAK,KAAK,IAAM,IAAI,GAAG,GAAG,MAC3D,IAAS,sBAAsB,EAAK;;AAEtC,OAAS,sBAAsB,EAAK;IACpC,QAEW;AAGX,GAFA,qBAAqB,EAAO,EAC5B,GAAQ,WAAW,CAAC,SAAS,MAAM,EAAE,MAAM,CAAC,EAC5C,GAAK,OAAO;;IAEb,CAAC,EAAY,CAAC,EAEV;;AAGT,SAAgB,EAAU,EACxB,UACA,iBAAc,mBACd,kBACA,aACA,iBACA,cACA,GAAG,KACc;CACjB,IAAM,CAAC,GAAW,KAAgB,EAAS,GAAM,EAC3C,IAAU,EAAM,MAAM,EACtB,IAAS,KAAa,EAAQ,SAAS,GACvC,EAAE,gBAAa,gBAAa,cAAW,EAAqB,EAAc,EAC1E,IAAoB,EAAc,EAAY,EAE9C,IAAY,KAAe,CAAC,CAAC,GAC7B,IAAO,IAAY,IAAW,IAAU,IAAW;CAEzD,SAAS,IAAmB;AAC1B,EAAI,IACF,GAAQ,GAER,KAAgB;;CAIpB,SAAS,EAAa,GAAoB;AAExC,EADA,EAAE,gBAAgB,EACd,KAAS,GAAU;;AAGzB,QACE,kBAAC,QAAD;EACE,UAAU;EACV,cAAc,MAAM;AAClB,GAAK,EAAE,OAAuB,YAAY,cAAY,EAAE,gBAAgB;;EAE1E,GAAI;EACJ,OAAO,EACL,eAAe,IAAS,IAAW,IAAU,IAAW,GACzD;EACD,WAAW,EACT,2FACA,kFACA,IAAS,eAAe,gBACxB,EACD;YAdH,CAgBE,kBAAC,YAAD;GACE,MAAM,IAAS,IAAI;GACZ;GACP,WAAW,MAAM,EAAc,EAAE,OAAO,MAAM;GAC9C,YAAY,MAAM;AAChB,IAAI,EAAE,QAAQ,YAAY,EAAE,WAAW,EAAE,YAAY,MACnD,EAAE,gBAAgB,EAClB,GAAU;;GAGd,eAAe,EAAa,GAAK;GACjC,cAAc,EAAa,GAAM;GACpB;GACb,cAAY;GACZ,OAAO,EACL,iBAAiB,IAAS,IAAI,IAAO,GACtC;GACD,WAAW,EACT,0LACA,sEACA,IACI,4BACA,uDAAA;GAEN,CAAA,EAEF,kBAAC,OAAD;GACE,OAAO;IACL,kBAAkB,IACd,cAAc,IAAO,EAAE,OACvB,eAAe,IAAW,EAAK;IACnC,QAAQ;IACT;GACD,WAAU;aAPZ,CASE,kBAAC,GAAD;IACE,MAAK;IACL,cAAW;IACX,UAAU,CAAC;IACX,SAAQ;IACR,OAAM;IACN,MAAK;IACL,WAAU;IACV,CAAA,EACD,KACC,kBAAC,GAAD;IACE,MAAK;IACL,cAAY,IAAc,mBAAmB;IAC7C,gBAAc;IACd,SAAS;IACT,SAAQ;IACR,OAAO,IAAc,UAAU;IAC/B,OAAO,IAAc,WAAW;IAChC,MAAK;IACL,WAAU;IACV,WAAW,EACT,YACA,KAAe,iBACf,CAAC,KAAe,mBACjB;cAED,kBAAC,QAAD;KACE,KAAK;KACL,eAAY;KACZ,WAAU;KACV,OAAO,EAAE,QAAQ,OAAA;KACjB,CAAA;IACK,CAAA,CAAA"}
package/ui/ChatWindow.js CHANGED
@@ -67,7 +67,7 @@ var h = /* @__PURE__ */ p("svg", {
67
67
  }), T ?? /* @__PURE__ */ p("button", {
68
68
  type: "button",
69
69
  "aria-label": "AI assistant",
70
- className: "chat-border flex items-center justify-center size-48 rounded-full bg-white",
70
+ 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",
71
71
  children: /* @__PURE__ */ p(f, {
72
72
  name: "Sparkles",
73
73
  width: "24px"
@@ -128,7 +128,7 @@ var h = /* @__PURE__ */ p("svg", {
128
128
  type: "button",
129
129
  "aria-label": "Close chat",
130
130
  onClick: C,
131
- className: "flex items-center justify-center size-48 rounded-full bg-black border border-black text-white shrink-0 hover:opacity-80 transition-opacity",
131
+ 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",
132
132
  children: /* @__PURE__ */ p(f, {
133
133
  name: "CrossDefault",
134
134
  width: "24px"
@@ -1 +1 @@
1
- {"version":3,"file":"ChatWindow.js","names":[],"sources":["../../lib/ui/ChatWindow.tsx"],"sourcesContent":["'use client';\n\nimport { Icon } from '@clubmed/trident-icons';\nimport type { HTMLAttributes, ReactNode } from 'react';\nimport { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';\nimport { twMerge } from './helpers/twMerge';\nimport { ChatInput } from './ChatInput';\nimport { ChatMessage } from './ChatMessage';\nimport type { ChatMessageSender } from './ChatMessage';\nimport { ChatTypingIndicator } from './ChatTypingIndicator';\nimport { ChatWindowCsatPrompt } from './ChatWindowCsatPrompt';\nimport type { ChatWindowCsatPromptLabels } from './ChatWindowCsatPrompt';\nimport { ChatWindowCsatThankYou } from './ChatWindowCsatThankYou';\nimport type { ChatWindowCsatThankYouLabels } from './ChatWindowCsatThankYou';\nimport { ChatWindowQuote } from './ChatWindowQuote';\nimport type { ChatWindowQuoteItem } from './ChatWindowQuote';\n\nexport type { ChatWindowCsatPromptLabels, ChatWindowCsatThankYouLabels, ChatWindowQuoteItem };\n\nexport interface ChatWindowMessage {\n id: string;\n sender: ChatMessageSender;\n content: string;\n}\n\nexport interface ChatWindowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {\n title?: string;\n greeting?: string;\n messages: ChatWindowMessage[];\n inputValue: string;\n inputPlaceholder?: string;\n isTyping?: boolean;\n onInputChange: (value: string) => void;\n onSubmit: () => void;\n onClose?: () => void;\n onAudioClick?: () => void;\n logo?: ReactNode;\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 ChatWindowHandle {\n askForFeedback: () => void;\n showQuotes: (quotes: ChatWindowQuoteItem[]) => void;\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 const ChatWindow = forwardRef<ChatWindowHandle, ChatWindowProps>(function ChatWindow(\n {\n title = 'Ask me a question',\n greeting,\n messages,\n inputValue,\n inputPlaceholder,\n isTyping = false,\n onInputChange,\n onSubmit,\n onClose,\n onAudioClick,\n logo,\n onFeedbackRate,\n csatLabels,\n csatRenderRatingLabel,\n csatThankYouLabels,\n onQuoteCtaClick,\n className,\n ...props\n },\n ref,\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 (\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 <div className=\"relative flex flex-col items-center shrink-0 pt-4 px-4 h-133 overflow-hidden\">\n <div className=\"absolute inset-x-4 top-4 h-102 bg-lightSand rounded-[24]\">\n {headerCurve}\n </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\"\n >\n <Icon name=\"Sparkles\" width=\"24px\" />\n </button>\n )}\n </div>\n </div>\n\n <div\n ref={logRef}\n role=\"log\"\n aria-label=\"Chat messages\"\n aria-live=\"polite\"\n className=\"chat-message-log flex flex-col flex-1 gap-20 overflow-y-auto py-20\"\n >\n {greeting ? (\n <div className=\"px-20\">\n <p className=\"text-b3 text-black\">{greeting}</p>\n </div>\n ) : null}\n {messages.map((msg) => (\n <ChatMessage key={msg.id} sender={msg.sender}>\n {msg.content}\n </ChatMessage>\n ))}\n {isTyping && (\n <div className=\"px-12\">\n <ChatTypingIndicator />\n </div>\n )}\n {csatReady && (\n <ChatWindowCsatPrompt\n onRate={handleRate}\n labels={csatLabels}\n renderRatingLabel={csatRenderRatingLabel}\n />\n )}\n {csatRatingMessage && <ChatMessage sender=\"user\">{csatRatingMessage}</ChatMessage>}\n {csatRatingMessage && csatThankYouLabels && (\n <ChatWindowCsatThankYou labels={csatThankYouLabels} />\n )}\n {quoteGroups.map((quotes, idx) => (\n <ChatWindowQuote key={idx} quotes={quotes} onCtaClick={onQuoteCtaClick} />\n ))}\n </div>\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 </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\"\n >\n <Icon name=\"CrossDefault\" width=\"24px\" />\n </button>\n )}\n </div>\n );\n});\n"],"mappings":";;;;;;;;;;;;AAiDA,IAAM,IACJ,kBAAC,OAAD;CACE,WAAU;CACV,OAAM;CACN,QAAO;CACP,SAAQ;WAER,kBAAC,QAAD;EACE,GAAE;EACF,MAAK;EACL,CAAA;CACE,CAAA,EAGK,IAAa,EAA8C,SACtE,EACE,WAAQ,qBACR,aACA,aACA,eACA,qBACA,cAAW,IACX,kBACA,aACA,YACA,iBACA,SACA,mBACA,eACA,0BACA,uBACA,oBACA,cACA,GAAG,KAEL,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;;CAG1B,IAAM,IAAY,KAAY,KAAc;AAE5C,QACE,kBAAC,OAAD;EAAK,GAAI;EAAO,WAAW,EAAQ,qCAAqC,EAAU;YAAlF,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACZ;MACG,CAAA,EACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,KAAD;QAAG,WAAU;kBACV;QACC,CAAA;OACA,CAAA,EACL,KACC,kBAAC,UAAD;OACE,MAAK;OACL,cAAW;OACX,WAAU;iBAEV,kBAAC,GAAD;QAAM,MAAK;QAAW,OAAM;QAAS,CAAA;OAC9B,CAAA,CAAA;;;IAKf,kBAAC,OAAD;KACE,KAAK;KACL,MAAK;KACL,cAAW;KACX,aAAU;KACV,WAAU;eALZ;MAOG,IACC,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,KAAD;QAAG,WAAU;kBAAsB;QAAa,CAAA;OAC5C,CAAA,GACJ;MACH,EAAS,KAAK,MACb,kBAAC,GAAD;OAA0B,QAAQ,EAAI;iBACnC,EAAI;OACO,EAFI,EAAI,GAER,CACd;MACD,KACC,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,GAAD,EAAuB,CAAA;OACnB,CAAA;MAEP,KACC,kBAAC,GAAD;OACE,QAAQ;OACR,QAAQ;OACR,mBAAmB;OACnB,CAAA;MAEH,KAAqB,kBAAC,GAAD;OAAa,QAAO;iBAAQ;OAAgC,CAAA;MACjF,KAAqB,KACpB,kBAAC,GAAD,EAAwB,QAAQ,GAAsB,CAAA;MAEvD,EAAY,KAAK,GAAQ,MACxB,kBAAC,GAAD;OAAmC;OAAQ,YAAY;OAAmB,EAApD,EAAoD,CAAA;;;IAI9E,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MACE,OAAO;MACP,aAAa;MACb,eAAe;MACL;MACI;MACd,CAAA;KACE,CAAA;;MAGP,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
+ {"version":3,"file":"ChatWindow.js","names":[],"sources":["../../lib/ui/ChatWindow.tsx"],"sourcesContent":["'use client';\n\nimport { Icon } from '@clubmed/trident-icons';\nimport type { HTMLAttributes, ReactNode } from 'react';\nimport { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';\nimport { twMerge } from './helpers/twMerge';\nimport { ChatInput } from './ChatInput';\nimport { ChatMessage } from './ChatMessage';\nimport type { ChatMessageSender } from './ChatMessage';\nimport { ChatTypingIndicator } from './ChatTypingIndicator';\nimport { ChatWindowCsatPrompt } from './ChatWindowCsatPrompt';\nimport type { ChatWindowCsatPromptLabels } from './ChatWindowCsatPrompt';\nimport { ChatWindowCsatThankYou } from './ChatWindowCsatThankYou';\nimport type { ChatWindowCsatThankYouLabels } from './ChatWindowCsatThankYou';\nimport { ChatWindowQuote } from './ChatWindowQuote';\nimport type { ChatWindowQuoteItem } from './ChatWindowQuote';\n\nexport type { ChatWindowCsatPromptLabels, ChatWindowCsatThankYouLabels, ChatWindowQuoteItem };\n\nexport interface ChatWindowMessage {\n id: string;\n sender: ChatMessageSender;\n content: string;\n}\n\nexport interface ChatWindowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {\n title?: string;\n greeting?: string;\n messages: ChatWindowMessage[];\n inputValue: string;\n inputPlaceholder?: string;\n isTyping?: boolean;\n onInputChange: (value: string) => void;\n onSubmit: () => void;\n onClose?: () => void;\n onAudioClick?: () => void;\n logo?: ReactNode;\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 ChatWindowHandle {\n askForFeedback: () => void;\n showQuotes: (quotes: ChatWindowQuoteItem[]) => void;\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 const ChatWindow = forwardRef<ChatWindowHandle, ChatWindowProps>(function ChatWindow(\n {\n title = 'Ask me a question',\n greeting,\n messages,\n inputValue,\n inputPlaceholder,\n isTyping = false,\n onInputChange,\n onSubmit,\n onClose,\n onAudioClick,\n logo,\n onFeedbackRate,\n csatLabels,\n csatRenderRatingLabel,\n csatThankYouLabels,\n onQuoteCtaClick,\n className,\n ...props\n },\n ref,\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 (\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 <div className=\"relative flex flex-col items-center shrink-0 pt-4 px-4 h-133 overflow-hidden\">\n <div className=\"absolute inset-x-4 top-4 h-102 bg-lightSand rounded-[24]\">\n {headerCurve}\n </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 <div\n ref={logRef}\n role=\"log\"\n aria-label=\"Chat messages\"\n aria-live=\"polite\"\n className=\"chat-message-log flex flex-col flex-1 gap-20 overflow-y-auto py-20\"\n >\n {greeting ? (\n <div className=\"px-20\">\n <p className=\"text-b3 text-black\">{greeting}</p>\n </div>\n ) : null}\n {messages.map((msg) => (\n <ChatMessage key={msg.id} sender={msg.sender}>\n {msg.content}\n </ChatMessage>\n ))}\n {isTyping && (\n <div className=\"px-12\">\n <ChatTypingIndicator />\n </div>\n )}\n {csatReady && (\n <ChatWindowCsatPrompt\n onRate={handleRate}\n labels={csatLabels}\n renderRatingLabel={csatRenderRatingLabel}\n />\n )}\n {csatRatingMessage && <ChatMessage sender=\"user\">{csatRatingMessage}</ChatMessage>}\n {csatRatingMessage && csatThankYouLabels && (\n <ChatWindowCsatThankYou labels={csatThankYouLabels} />\n )}\n {quoteGroups.map((quotes, idx) => (\n <ChatWindowQuote key={idx} quotes={quotes} onCtaClick={onQuoteCtaClick} />\n ))}\n </div>\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 </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":";;;;;;;;;;;;AAiDA,IAAM,IACJ,kBAAC,OAAD;CACE,WAAU;CACV,OAAM;CACN,QAAO;CACP,SAAQ;WAER,kBAAC,QAAD;EACE,GAAE;EACF,MAAK;EACL,CAAA;CACE,CAAA,EAGK,IAAa,EAA8C,SACtE,EACE,WAAQ,qBACR,aACA,aACA,eACA,qBACA,cAAW,IACX,kBACA,aACA,YACA,iBACA,SACA,mBACA,eACA,0BACA,uBACA,oBACA,cACA,GAAG,KAEL,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;;CAG1B,IAAM,IAAY,KAAY,KAAc;AAE5C,QACE,kBAAC,OAAD;EAAK,GAAI;EAAO,WAAW,EAAQ,qCAAqC,EAAU;YAAlF,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACZ;MACG,CAAA,EACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,KAAD;QAAG,WAAU;kBACV;QACC,CAAA;OACA,CAAA,EACL,KACC,kBAAC,UAAD;OACE,MAAK;OACL,cAAW;OACX,WAAU;iBAEV,kBAAC,GAAD;QAAM,MAAK;QAAW,OAAM;QAAS,CAAA;OAC9B,CAAA,CAAA;;;IAKf,kBAAC,OAAD;KACE,KAAK;KACL,MAAK;KACL,cAAW;KACX,aAAU;KACV,WAAU;eALZ;MAOG,IACC,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,KAAD;QAAG,WAAU;kBAAsB;QAAa,CAAA;OAC5C,CAAA,GACJ;MACH,EAAS,KAAK,MACb,kBAAC,GAAD;OAA0B,QAAQ,EAAI;iBACnC,EAAI;OACO,EAFI,EAAI,GAER,CACd;MACD,KACC,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,GAAD,EAAuB,CAAA;OACnB,CAAA;MAEP,KACC,kBAAC,GAAD;OACE,QAAQ;OACR,QAAQ;OACR,mBAAmB;OACnB,CAAA;MAEH,KAAqB,kBAAC,GAAD;OAAa,QAAO;iBAAQ;OAAgC,CAAA;MACjF,KAAqB,KACpB,kBAAC,GAAD,EAAwB,QAAQ,GAAsB,CAAA;MAEvD,EAAY,KAAK,GAAQ,MACxB,kBAAC,GAAD;OAAmC;OAAQ,YAAY;OAAmB,EAApD,EAAoD,CAAA;;;IAI9E,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MACE,OAAO;MACP,aAAa;MACb,eAAe;MACL;MACI;MACd,CAAA;KACE,CAAA;;MAGP,KACC,kBAAC,UAAD;GACE,MAAK;GACL,cAAW;GACX,SAAS;GACT,WAAU;aAEV,kBAAC,GAAD;IAAM,MAAK;IAAe,OAAM;IAAS,CAAA;GAClC,CAAA,CAAA;;EAIf"}
@@ -48,7 +48,7 @@ function a({ onRate: a, labels: o, renderRatingLabel: s, className: c }) {
48
48
  onMouseLeave: () => {
49
49
  h || u(0);
50
50
  },
51
- className: e("flex items-center justify-center size-48 rounded-full transition-colors", h ? "cursor-default" : "hover:bg-lightSand"),
51
+ className: e("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", h ? "cursor-default" : "hover:bg-lightSand"),
52
52
  children: /* @__PURE__ */ r(n, {
53
53
  name: o ? "StarFilled" : "StarOutlined",
54
54
  width: "24px",
@@ -1 +1 @@
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',\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,2EACA,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
+ {"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"}
@@ -60,7 +60,7 @@ var d = ({ label: d, showCheckIcon: f = !1, children: p, expanded: m, defaultExp
60
60
  "aria-label": v || d,
61
61
  disabled: _,
62
62
  onClick: N,
63
- className: e("flex items-center justify-center w-240 mx-auto relative gap-20 transition-opacity duration-200", "rounded-pill focus-visible:ring focus-visible:ring-8 focus-visible:ring-lavender/20 outline-none px-20 py-12 ", _ ? "opacity-50 cursor-not-allowed" : "cursor-pointer", "[@supports(anchor-name:_--label-anchor)]:[--anchorSupport:1]"),
63
+ className: e("flex items-center justify-center w-240 mx-auto relative gap-20 transition-opacity duration-200", "rounded-pill focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 outline-none px-20 py-12 ", _ ? "opacity-50 cursor-not-allowed" : "cursor-pointer", "[@supports(anchor-name:_--label-anchor)]:[--anchorSupport:1]"),
64
64
  children: [
65
65
  /* @__PURE__ */ l("div", {
66
66
  "aria-hidden": "true",
@@ -1 +1 @@
1
- {"version":3,"file":"ChoiceExpander.js","names":[],"sources":["../../lib/ui/ChoiceExpander.tsx"],"sourcesContent":["'use client';\n\nimport clsx from 'clsx';\nimport {\n type ComponentPropsWithoutRef,\n type FunctionComponent,\n type ReactNode,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n} from 'react';\n\nimport { Icon } from '@clubmed/trident-icons';\nimport { getBgColor, getTextColor } from './helpers/colors/colors';\nimport type { Colors } from './types/Colors';\n\nexport interface ChoiceExpanderProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * Text shown when collapsed (e.g., \"From $255\")\n */\n label: string;\n\n /**\n * Show check icon in collapsed state (selected variant)\n */\n showCheckIcon?: boolean;\n\n /**\n * Content shown when expanded\n */\n children: ReactNode;\n\n /**\n * Controlled state - if provided, component is controlled\n */\n expanded?: boolean;\n\n /**\n * Initial state for uncontrolled mode\n */\n defaultExpanded?: boolean;\n\n /**\n * Callback when expanded state changes\n */\n onExpandedChange?: (isExpanded: boolean) => void;\n\n /**\n * Disable interaction\n */\n disabled?: boolean;\n\n /**\n * Custom aria-label for button\n */\n ariaLabel?: string;\n\n /**\n * Background color for unexpanded button state\n */\n buttonBgColor?: Colors;\n\n /**\n * Text color for unexpanded button state\n */\n buttonTextColor?: Colors;\n\n /**\n * collapse label for screen readers\n */\n collapseLabel: string;\n\n /**\n * expand label for screen readers\n */\n expandLabel: string;\n}\n\nexport const ChoiceExpander: FunctionComponent<ChoiceExpanderProps> = ({\n label,\n showCheckIcon = false,\n children,\n expanded,\n defaultExpanded = false,\n onExpandedChange,\n disabled = false,\n ariaLabel,\n buttonBgColor = 'black',\n buttonTextColor = 'white',\n collapseLabel,\n expandLabel,\n className,\n ...props\n}) => {\n const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);\n const contentId = useId();\n const morphContainerRef = useRef<HTMLDivElement>(null);\n const closeButtonRef = useRef<HTMLButtonElement>(null);\n const triggerButtonRef = useRef<HTMLButtonElement>(null);\n const hasInteractedRef = useRef(false);\n\n const isExpanded = expanded !== undefined ? expanded : internalExpanded;\n\n const handleToggle = useCallback(() => {\n if (disabled) return;\n\n hasInteractedRef.current = true;\n const newState = !isExpanded;\n\n if (expanded === undefined) {\n setInternalExpanded(newState);\n }\n\n onExpandedChange?.(newState);\n }, [disabled, isExpanded, expanded, onExpandedChange]);\n\n useEffect(() => {\n if (!isExpanded) return;\n\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n handleToggle();\n }\n };\n\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [isExpanded, handleToggle]);\n\n useEffect(() => {\n if (isExpanded && closeButtonRef.current) {\n closeButtonRef.current.focus();\n } else if (!isExpanded && hasInteractedRef.current && triggerButtonRef.current) {\n triggerButtonRef.current.focus();\n }\n }, [isExpanded]);\n\n useEffect(() => {\n if (!isExpanded) return;\n\n const handleClickOutside = (event: MouseEvent) => {\n if (morphContainerRef.current && !morphContainerRef.current.contains(event.target as Node)) {\n handleToggle();\n }\n };\n\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, [isExpanded, handleToggle]);\n\n return (\n <div\n className={clsx('relative flex items-center justify-center', className)}\n data-name=\"ChoiceExpander\"\n data-testid=\"choice-expander\"\n style={{ width: '240px', height: 48 }}\n {...props}\n >\n <div\n ref={morphContainerRef}\n className={clsx(\n 'absolute top-0 grid transition-all duration-500',\n !isExpanded && [\n 'w-240 rounded-pill-transitionable border-transparent',\n getBgColor(buttonBgColor),\n getTextColor(buttonTextColor),\n ],\n isExpanded && [\n 'w-324 rounded-16 bg-white text-black border border-gray-900',\n 'z-10',\n disabled && 'opacity-50',\n ],\n )}\n >\n <button\n ref={triggerButtonRef}\n type=\"button\"\n data-testid=\"choice-expander-button\"\n aria-expanded={isExpanded}\n aria-controls={contentId}\n aria-label={ariaLabel || label}\n disabled={disabled}\n onClick={handleToggle}\n className={clsx(\n 'flex items-center justify-center w-240 mx-auto relative gap-20 transition-opacity duration-200',\n 'rounded-pill focus-visible:ring focus-visible:ring-8 focus-visible:ring-lavender/20 outline-none px-20 py-12 ',\n disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',\n '[@supports(anchor-name:_--label-anchor)]:[--anchorSupport:1]',\n )}\n >\n {/* invisible top middle anchor position */}\n <div\n aria-hidden=\"true\"\n className=\"absolute h-24 w-24 mx-auto pointer-events-none\"\n style={{ anchorName: '--middle-anchor' } as React.CSSProperties}\n ></div>\n <span className=\"text-b3 flex items-center font-semibold\">\n {/* label anchor */}\n <span\n className={clsx('transition-opacity duration-200 pe-8', isExpanded && 'opacity-0')}\n >\n {label}\n </span>\n <div\n className=\"h-24 w-24\"\n style={{ anchorName: '--label-anchor' } as React.CSSProperties}\n aria-hidden=\"true\"\n >\n <Icon\n name=\"ArrowDefaultDown\"\n width=\"24px\"\n aria-hidden=\"true\"\n className={clsx(\n isExpanded ? 'absolute left-[calc(50%-12px)] rotate-180' : 'rotate-[0deg]',\n )}\n style={\n {\n transition: 'rotate 0.3s, left 0.3s',\n opacity: 'calc((1 - var(--anchorSupport)) * 1)',\n } as React.CSSProperties\n }\n />\n </div>\n <span className=\"sr-only\">{isExpanded ? collapseLabel : expandLabel}</span>\n </span>\n <Icon\n name=\"ArrowDefaultDown\"\n width=\"24px\"\n aria-hidden=\"true\"\n className={clsx('absolute', isExpanded ? 'rotate-180' : 'rotate-[0deg]')}\n style={\n {\n positionAnchor: isExpanded ? '--middle-anchor' : '--label-anchor',\n left: 'anchor(left)',\n top: 'anchor(top)',\n transition: 'left 0.3s, rotate 0.3s',\n } as React.CSSProperties\n }\n />\n {showCheckIcon && (\n <Icon\n name=\"CheckDefault\"\n width=\"24px\"\n aria-hidden=\"true\"\n className={clsx('transition-opacity duration-200', isExpanded && 'opacity-0')}\n />\n )}\n </button>\n\n <div\n className={clsx(\n 'grid overflow-hidden transition-[grid-template-rows,opacity] duration-500',\n isExpanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr] opacity-0',\n )}\n >\n <div className=\"overflow-hidden\">\n <div className=\"px-8 pb-8\" id={contentId}>\n {children}\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"ChoiceExpander.js","names":[],"sources":["../../lib/ui/ChoiceExpander.tsx"],"sourcesContent":["'use client';\n\nimport clsx from 'clsx';\nimport {\n type ComponentPropsWithoutRef,\n type FunctionComponent,\n type ReactNode,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n} from 'react';\n\nimport { Icon } from '@clubmed/trident-icons';\nimport { getBgColor, getTextColor } from './helpers/colors/colors';\nimport type { Colors } from './types/Colors';\n\nexport interface ChoiceExpanderProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * Text shown when collapsed (e.g., \"From $255\")\n */\n label: string;\n\n /**\n * Show check icon in collapsed state (selected variant)\n */\n showCheckIcon?: boolean;\n\n /**\n * Content shown when expanded\n */\n children: ReactNode;\n\n /**\n * Controlled state - if provided, component is controlled\n */\n expanded?: boolean;\n\n /**\n * Initial state for uncontrolled mode\n */\n defaultExpanded?: boolean;\n\n /**\n * Callback when expanded state changes\n */\n onExpandedChange?: (isExpanded: boolean) => void;\n\n /**\n * Disable interaction\n */\n disabled?: boolean;\n\n /**\n * Custom aria-label for button\n */\n ariaLabel?: string;\n\n /**\n * Background color for unexpanded button state\n */\n buttonBgColor?: Colors;\n\n /**\n * Text color for unexpanded button state\n */\n buttonTextColor?: Colors;\n\n /**\n * collapse label for screen readers\n */\n collapseLabel: string;\n\n /**\n * expand label for screen readers\n */\n expandLabel: string;\n}\n\nexport const ChoiceExpander: FunctionComponent<ChoiceExpanderProps> = ({\n label,\n showCheckIcon = false,\n children,\n expanded,\n defaultExpanded = false,\n onExpandedChange,\n disabled = false,\n ariaLabel,\n buttonBgColor = 'black',\n buttonTextColor = 'white',\n collapseLabel,\n expandLabel,\n className,\n ...props\n}) => {\n const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);\n const contentId = useId();\n const morphContainerRef = useRef<HTMLDivElement>(null);\n const closeButtonRef = useRef<HTMLButtonElement>(null);\n const triggerButtonRef = useRef<HTMLButtonElement>(null);\n const hasInteractedRef = useRef(false);\n\n const isExpanded = expanded !== undefined ? expanded : internalExpanded;\n\n const handleToggle = useCallback(() => {\n if (disabled) return;\n\n hasInteractedRef.current = true;\n const newState = !isExpanded;\n\n if (expanded === undefined) {\n setInternalExpanded(newState);\n }\n\n onExpandedChange?.(newState);\n }, [disabled, isExpanded, expanded, onExpandedChange]);\n\n useEffect(() => {\n if (!isExpanded) return;\n\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n handleToggle();\n }\n };\n\n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [isExpanded, handleToggle]);\n\n useEffect(() => {\n if (isExpanded && closeButtonRef.current) {\n closeButtonRef.current.focus();\n } else if (!isExpanded && hasInteractedRef.current && triggerButtonRef.current) {\n triggerButtonRef.current.focus();\n }\n }, [isExpanded]);\n\n useEffect(() => {\n if (!isExpanded) return;\n\n const handleClickOutside = (event: MouseEvent) => {\n if (morphContainerRef.current && !morphContainerRef.current.contains(event.target as Node)) {\n handleToggle();\n }\n };\n\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, [isExpanded, handleToggle]);\n\n return (\n <div\n className={clsx('relative flex items-center justify-center', className)}\n data-name=\"ChoiceExpander\"\n data-testid=\"choice-expander\"\n style={{ width: '240px', height: 48 }}\n {...props}\n >\n <div\n ref={morphContainerRef}\n className={clsx(\n 'absolute top-0 grid transition-all duration-500',\n !isExpanded && [\n 'w-240 rounded-pill-transitionable border-transparent',\n getBgColor(buttonBgColor),\n getTextColor(buttonTextColor),\n ],\n isExpanded && [\n 'w-324 rounded-16 bg-white text-black border border-gray-900',\n 'z-10',\n disabled && 'opacity-50',\n ],\n )}\n >\n <button\n ref={triggerButtonRef}\n type=\"button\"\n data-testid=\"choice-expander-button\"\n aria-expanded={isExpanded}\n aria-controls={contentId}\n aria-label={ariaLabel || label}\n disabled={disabled}\n onClick={handleToggle}\n className={clsx(\n 'flex items-center justify-center w-240 mx-auto relative gap-20 transition-opacity duration-200',\n 'rounded-pill focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 outline-none px-20 py-12 ',\n disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',\n '[@supports(anchor-name:_--label-anchor)]:[--anchorSupport:1]',\n )}\n >\n {/* invisible top middle anchor position */}\n <div\n aria-hidden=\"true\"\n className=\"absolute h-24 w-24 mx-auto pointer-events-none\"\n style={{ anchorName: '--middle-anchor' } as React.CSSProperties}\n ></div>\n <span className=\"text-b3 flex items-center font-semibold\">\n {/* label anchor */}\n <span\n className={clsx('transition-opacity duration-200 pe-8', isExpanded && 'opacity-0')}\n >\n {label}\n </span>\n <div\n className=\"h-24 w-24\"\n style={{ anchorName: '--label-anchor' } as React.CSSProperties}\n aria-hidden=\"true\"\n >\n <Icon\n name=\"ArrowDefaultDown\"\n width=\"24px\"\n aria-hidden=\"true\"\n className={clsx(\n isExpanded ? 'absolute left-[calc(50%-12px)] rotate-180' : 'rotate-[0deg]',\n )}\n style={\n {\n transition: 'rotate 0.3s, left 0.3s',\n opacity: 'calc((1 - var(--anchorSupport)) * 1)',\n } as React.CSSProperties\n }\n />\n </div>\n <span className=\"sr-only\">{isExpanded ? collapseLabel : expandLabel}</span>\n </span>\n <Icon\n name=\"ArrowDefaultDown\"\n width=\"24px\"\n aria-hidden=\"true\"\n className={clsx('absolute', isExpanded ? 'rotate-180' : 'rotate-[0deg]')}\n style={\n {\n positionAnchor: isExpanded ? '--middle-anchor' : '--label-anchor',\n left: 'anchor(left)',\n top: 'anchor(top)',\n transition: 'left 0.3s, rotate 0.3s',\n } as React.CSSProperties\n }\n />\n {showCheckIcon && (\n <Icon\n name=\"CheckDefault\"\n width=\"24px\"\n aria-hidden=\"true\"\n className={clsx('transition-opacity duration-200', isExpanded && 'opacity-0')}\n />\n )}\n </button>\n\n <div\n className={clsx(\n 'grid overflow-hidden transition-[grid-template-rows,opacity] duration-500',\n isExpanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr] opacity-0',\n )}\n >\n <div className=\"overflow-hidden\">\n <div className=\"px-8 pb-8\" id={contentId}>\n {children}\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -28,7 +28,7 @@ var u = ({ buttonClassName: u, children: d, className: f, id: p, labels: m, line
28
28
  }), w && /* @__PURE__ */ c("button", {
29
29
  "aria-controls": p,
30
30
  "aria-expanded": x,
31
- className: e("self-start border-b border-current font-semibold text-inherit transition-colors", u),
31
+ className: e("self-start border-b border-current font-semibold text-inherit transition-colors outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2", u),
32
32
  onClick: () => S(!x),
33
33
  type: "button",
34
34
  children: x ? m.reduce : m.expand
@@ -1 +1 @@
1
- {"version":3,"file":"ExpandableText.js","names":[],"sources":["../../lib/ui/ExpandableText.tsx"],"sourcesContent":["'use client';\nimport clsx from 'clsx';\nimport type { CSSProperties, PropsWithChildren } from 'react';\nimport { useCallback, useLayoutEffect, useRef, useState } from 'react';\n\nimport { useIsClient } from './hooks/useIsClient';\nimport { useResizeObserver } from './hooks/useResizeObserver';\n\nexport interface ExpandableTextLabels {\n expand: string;\n reduce: string;\n}\n\nexport interface ExpandableTextProps {\n buttonClassName?: string;\n className?: string;\n id: string;\n labels: ExpandableTextLabels;\n /** Tailwind line-clamp class to use when collapsed, e.g. \"line-clamp-3\" */\n lineClamps?: string;\n}\n\nexport const ExpandableText = ({\n buttonClassName,\n children,\n className,\n id,\n labels,\n lineClamps = 'line-clamp-3',\n}: PropsWithChildren<ExpandableTextProps>) => {\n const ref = useRef<HTMLDivElement>(null!);\n const [min, setMin] = useState(0);\n const [max, setMax] = useState(0);\n const [isExpanded, setIsExpanded] = useState(false);\n\n const isClient = useIsClient();\n\n const onResize = useCallback(() => {\n setMax(ref.current.scrollHeight);\n setMin((currentMin) => {\n if (currentMin === 0) {\n return ref.current.offsetHeight;\n }\n return currentMin;\n });\n }, []);\n\n useResizeObserver<HTMLDivElement>({\n ref,\n onResize,\n });\n\n useLayoutEffect(() => {\n if (!ref.current) {\n return;\n }\n setMin(ref.current.offsetHeight);\n setMax(ref.current.scrollHeight);\n }, []);\n\n if (!children) {\n return null;\n }\n\n const withToggleButton = max > min || isExpanded;\n const style = { '--height': `${isExpanded ? max : min}px` } as CSSProperties;\n\n return (\n <>\n <div\n className={clsx(className, 'overflow-clip transition-all duration-500', {\n 'h-(--height)': isClient,\n })}\n style={style}\n >\n <div className={clsx({ [lineClamps]: !isExpanded })} id={id} ref={ref}>\n {children}\n </div>\n </div>\n {withToggleButton && (\n <button\n aria-controls={id}\n aria-expanded={isExpanded}\n className={clsx(\n 'self-start border-b border-current font-semibold text-inherit transition-colors',\n buttonClassName,\n )}\n onClick={() => setIsExpanded(!isExpanded)}\n type=\"button\"\n >\n {isExpanded ? labels.reduce : labels.expand}\n </button>\n )}\n </>\n );\n};\n"],"mappings":";;;;;;;AAsBA,IAAa,KAAkB,EAC7B,oBACA,aACA,cACA,OACA,WACA,gBAAa,qBAC+B;CAC5C,IAAM,IAAM,EAAuB,KAAM,EACnC,CAAC,GAAK,KAAU,EAAS,EAAE,EAC3B,CAAC,GAAK,KAAU,EAAS,EAAE,EAC3B,CAAC,GAAY,KAAiB,EAAS,GAAM,EAE7C,IAAW,GAAa;AAyB9B,KAbA,EAAkC;EAChC;EACA,UAZe,QAAkB;AAEjC,GADA,EAAO,EAAI,QAAQ,aAAa,EAChC,GAAQ,MACF,MAAe,IACV,EAAI,QAAQ,eAEd,EACP;KACD,EAAE,CAAA;EAKJ,CAAC,EAEF,QAAsB;AACf,IAAI,YAGT,EAAO,EAAI,QAAQ,aAAa,EAChC,EAAO,EAAI,QAAQ,aAAa;IAC/B,EAAE,CAAC,EAEF,CAAC,EACH,QAAO;CAGT,IAAM,IAAmB,IAAM,KAAO,GAChC,IAAQ,EAAE,YAAY,GAAG,IAAa,IAAM,EAAI,KAAK;AAE3D,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;EACE,WAAW,EAAK,GAAW,6CAA6C,EACtE,gBAAgB,GACjB,CAAC;EACK;YAEP,kBAAC,OAAD;GAAK,WAAW,EAAK,GAAG,IAAa,CAAC,GAAY,CAAC;GAAM;GAAS;GAC/D;GACG,CAAA;EACF,CAAA,EACL,KACC,kBAAC,UAAD;EACE,iBAAe;EACf,iBAAe;EACf,WAAW,EACT,mFACA,EACD;EACD,eAAe,EAAc,CAAC,EAAW;EACzC,MAAK;YAEJ,IAAa,EAAO,SAAS,EAAO;EAC9B,CAAA,CAEV,EAAA,CAAA"}
1
+ {"version":3,"file":"ExpandableText.js","names":[],"sources":["../../lib/ui/ExpandableText.tsx"],"sourcesContent":["'use client';\nimport clsx from 'clsx';\nimport type { CSSProperties, PropsWithChildren } from 'react';\nimport { useCallback, useLayoutEffect, useRef, useState } from 'react';\n\nimport { useIsClient } from './hooks/useIsClient';\nimport { useResizeObserver } from './hooks/useResizeObserver';\n\nexport interface ExpandableTextLabels {\n expand: string;\n reduce: string;\n}\n\nexport interface ExpandableTextProps {\n buttonClassName?: string;\n className?: string;\n id: string;\n labels: ExpandableTextLabels;\n /** Tailwind line-clamp class to use when collapsed, e.g. \"line-clamp-3\" */\n lineClamps?: string;\n}\n\nexport const ExpandableText = ({\n buttonClassName,\n children,\n className,\n id,\n labels,\n lineClamps = 'line-clamp-3',\n}: PropsWithChildren<ExpandableTextProps>) => {\n const ref = useRef<HTMLDivElement>(null!);\n const [min, setMin] = useState(0);\n const [max, setMax] = useState(0);\n const [isExpanded, setIsExpanded] = useState(false);\n\n const isClient = useIsClient();\n\n const onResize = useCallback(() => {\n setMax(ref.current.scrollHeight);\n setMin((currentMin) => {\n if (currentMin === 0) {\n return ref.current.offsetHeight;\n }\n return currentMin;\n });\n }, []);\n\n useResizeObserver<HTMLDivElement>({\n ref,\n onResize,\n });\n\n useLayoutEffect(() => {\n if (!ref.current) {\n return;\n }\n setMin(ref.current.offsetHeight);\n setMax(ref.current.scrollHeight);\n }, []);\n\n if (!children) {\n return null;\n }\n\n const withToggleButton = max > min || isExpanded;\n const style = { '--height': `${isExpanded ? max : min}px` } as CSSProperties;\n\n return (\n <>\n <div\n className={clsx(className, 'overflow-clip transition-all duration-500', {\n 'h-(--height)': isClient,\n })}\n style={style}\n >\n <div className={clsx({ [lineClamps]: !isExpanded })} id={id} ref={ref}>\n {children}\n </div>\n </div>\n {withToggleButton && (\n <button\n aria-controls={id}\n aria-expanded={isExpanded}\n className={clsx(\n 'self-start border-b border-current font-semibold text-inherit transition-colors outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2',\n buttonClassName,\n )}\n onClick={() => setIsExpanded(!isExpanded)}\n type=\"button\"\n >\n {isExpanded ? labels.reduce : labels.expand}\n </button>\n )}\n </>\n );\n};\n"],"mappings":";;;;;;;AAsBA,IAAa,KAAkB,EAC7B,oBACA,aACA,cACA,OACA,WACA,gBAAa,qBAC+B;CAC5C,IAAM,IAAM,EAAuB,KAAM,EACnC,CAAC,GAAK,KAAU,EAAS,EAAE,EAC3B,CAAC,GAAK,KAAU,EAAS,EAAE,EAC3B,CAAC,GAAY,KAAiB,EAAS,GAAM,EAE7C,IAAW,GAAa;AAyB9B,KAbA,EAAkC;EAChC;EACA,UAZe,QAAkB;AAEjC,GADA,EAAO,EAAI,QAAQ,aAAa,EAChC,GAAQ,MACF,MAAe,IACV,EAAI,QAAQ,eAEd,EACP;KACD,EAAE,CAAA;EAKJ,CAAC,EAEF,QAAsB;AACf,IAAI,YAGT,EAAO,EAAI,QAAQ,aAAa,EAChC,EAAO,EAAI,QAAQ,aAAa;IAC/B,EAAE,CAAC,EAEF,CAAC,EACH,QAAO;CAGT,IAAM,IAAmB,IAAM,KAAO,GAChC,IAAQ,EAAE,YAAY,GAAG,IAAa,IAAM,EAAI,KAAK;AAE3D,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;EACE,WAAW,EAAK,GAAW,6CAA6C,EACtE,gBAAgB,GACjB,CAAC;EACK;YAEP,kBAAC,OAAD;GAAK,WAAW,EAAK,GAAG,IAAa,CAAC,GAAY,CAAC;GAAM;GAAS;GAC/D;GACG,CAAA;EACF,CAAA,EACL,KACC,kBAAC,UAAD;EACE,iBAAe;EACf,iBAAe;EACf,WAAW,EACT,0KACA,EACD;EACD,eAAe,EAAc,CAAC,EAAW;EACzC,MAAK;YAEJ,IAAa,EAAO,SAAS,EAAO;EAC9B,CAAA,CAEV,EAAA,CAAA"}
@@ -12,7 +12,7 @@ var n = ({ className: n, isActive: r = !1, ...i }) => {
12
12
  return /* @__PURE__ */ t("span", {
13
13
  "data-name": "HamburgerIcon",
14
14
  ...i,
15
- className: e("menu-icon cursor-pointer", "color-current relative isolate shrink-0 h-24 w-24 block", n),
15
+ className: e("menu-icon cursor-pointer", "color-current relative isolate shrink-0 h-24 w-24 block outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2", n),
16
16
  "data-testid": r ? "HamburgerIcon-active" : "HamburgerIcon",
17
17
  children: /* @__PURE__ */ t("span", { className: e("absolute left-1/2 top-1/2 block h-2 w-24 -translate-x-1/2 -translate-y-1/2 transition-[background] duration-500 cursor-pointer", {
18
18
  "bg-current": !r,
@@ -1 +1 @@
1
- {"version":3,"file":"HamburgerIcon.js","names":[],"sources":["../../lib/ui/HamburgerIcon.tsx"],"sourcesContent":["import clsx from 'clsx';\nimport type { ComponentPropsWithoutRef, FunctionComponent } from 'react';\n\ninterface Props extends ComponentPropsWithoutRef<'span'> {\n isActive?: boolean;\n}\n\nexport const HamburgerIcon: FunctionComponent<Props> = ({\n className,\n isActive = false,\n ...attrs\n}) => {\n const before = clsx(\n 'before:absolute before:left-0 before:top-0 before:block before:h-2 before:w-24 before:bg-current before:opacity-100 before:transition-transform before:duration-500 before:content-[\"\"] before:cursor-pointer',\n { 'before:translate-y-[-6px]': !isActive, 'before:translate-y-0 before:rotate-45': isActive },\n );\n\n const after = clsx(\n 'after:absolute after:left-0 after:top-0 after:block after:h-2 after:w-24 after:bg-current after:opacity-100 after:transition-transform after:duration-500 after:content-[\"\"] after:cursor-pointer',\n { 'after:translate-y-[6px]': !isActive, 'after:translate-y-0 after:-rotate-45': isActive },\n );\n\n return (\n <span\n data-name=\"HamburgerIcon\"\n {...attrs}\n className={clsx(\n 'menu-icon cursor-pointer',\n 'color-current relative isolate shrink-0 h-24 w-24 block',\n className,\n )}\n data-testid={isActive ? 'HamburgerIcon-active' : 'HamburgerIcon'}\n >\n <span\n className={clsx(\n 'absolute left-1/2 top-1/2 block h-2 w-24 -translate-x-1/2 -translate-y-1/2 transition-[background] duration-500 cursor-pointer',\n { 'bg-current': !isActive, 'bg-transparent': isActive },\n before,\n after,\n )}\n />\n </span>\n );\n};\n"],"mappings":";;;AAOA,IAAa,KAA2C,EACtD,cACA,cAAW,IACX,GAAG,QACC;CACJ,IAAM,IAAS,EACb,mNACA;EAAE,6BAA6B,CAAC;EAAU,yCAAyC;EAAU,CAC9F,EAEK,IAAQ,EACZ,uMACA;EAAE,2BAA2B,CAAC;EAAU,wCAAwC;EAAU,CAC3F;AAED,QACE,kBAAC,QAAD;EACE,aAAU;EACV,GAAI;EACJ,WAAW,EACT,4BACA,2DACA,EACD;EACD,eAAa,IAAW,yBAAyB;YAEjD,kBAAC,QAAD,EACE,WAAW,EACT,kIACA;GAAE,cAAc,CAAC;GAAU,kBAAkB;GAAU,EACvD,GACA,EACD,EACD,CAAA;EACG,CAAA"}
1
+ {"version":3,"file":"HamburgerIcon.js","names":[],"sources":["../../lib/ui/HamburgerIcon.tsx"],"sourcesContent":["import clsx from 'clsx';\nimport type { ComponentPropsWithoutRef, FunctionComponent } from 'react';\n\ninterface Props extends ComponentPropsWithoutRef<'span'> {\n isActive?: boolean;\n}\n\nexport const HamburgerIcon: FunctionComponent<Props> = ({\n className,\n isActive = false,\n ...attrs\n}) => {\n const before = clsx(\n 'before:absolute before:left-0 before:top-0 before:block before:h-2 before:w-24 before:bg-current before:opacity-100 before:transition-transform before:duration-500 before:content-[\"\"] before:cursor-pointer',\n { 'before:translate-y-[-6px]': !isActive, 'before:translate-y-0 before:rotate-45': isActive },\n );\n\n const after = clsx(\n 'after:absolute after:left-0 after:top-0 after:block after:h-2 after:w-24 after:bg-current after:opacity-100 after:transition-transform after:duration-500 after:content-[\"\"] after:cursor-pointer',\n { 'after:translate-y-[6px]': !isActive, 'after:translate-y-0 after:-rotate-45': isActive },\n );\n\n return (\n <span\n data-name=\"HamburgerIcon\"\n {...attrs}\n className={clsx(\n 'menu-icon cursor-pointer',\n 'color-current relative isolate shrink-0 h-24 w-24 block outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2',\n className,\n )}\n data-testid={isActive ? 'HamburgerIcon-active' : 'HamburgerIcon'}\n >\n <span\n className={clsx(\n 'absolute left-1/2 top-1/2 block h-2 w-24 -translate-x-1/2 -translate-y-1/2 transition-[background] duration-500 cursor-pointer',\n { 'bg-current': !isActive, 'bg-transparent': isActive },\n before,\n after,\n )}\n />\n </span>\n );\n};\n"],"mappings":";;;AAOA,IAAa,KAA2C,EACtD,cACA,cAAW,IACX,GAAG,QACC;CACJ,IAAM,IAAS,EACb,mNACA;EAAE,6BAA6B,CAAC;EAAU,yCAAyC;EAAU,CAC9F,EAEK,IAAQ,EACZ,uMACA;EAAE,2BAA2B,CAAC;EAAU,wCAAwC;EAAU,CAC3F;AAED,QACE,kBAAC,QAAD;EACE,aAAU;EACV,GAAI;EACJ,WAAW,EACT,4BACA,kJACA,EACD;EACD,eAAa,IAAW,yBAAyB;YAEjD,kBAAC,QAAD,EACE,WAAW,EACT,kIACA;GAAE,cAAc,CAAC;GAAU,kBAAkB;GAAU,EACvD,GACA,EACD,EACD,CAAA;EACG,CAAA"}
package/ui/Link.js CHANGED
@@ -7,7 +7,7 @@ var a = ({ label: a, icon: o, component: s = "a", iconType: c, underlined: l = !
7
7
  if (!a) return null;
8
8
  let m = a.lastIndexOf(" "), h = m === -1 ? a.length : m, g = a.substring(0, 1), _ = a.substring(1, h + 1), v = a.substring(h);
9
9
  return /* @__PURE__ */ i(d ? "span" : s, {
10
- className: t("decoration-none link-container cursor-pointer text-b3 font-semibold", u),
10
+ className: t("decoration-none link-container cursor-pointer text-b3 font-semibold outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2", u),
11
11
  "data-name": "Link",
12
12
  ...p,
13
13
  children: [/* @__PURE__ */ r("span", {
package/ui/Link.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"Link.js","names":[],"sources":["../../lib/ui/Link.tsx"],"sourcesContent":["import clsx from 'clsx';\nimport type { AnchorHTMLAttributes, FunctionComponent, PropsWithChildren } from 'react';\n\nimport { Icon, type IconicNames, type IconicTypes } from '@clubmed/trident-icons';\nimport { twMerge } from './helpers/twMerge';\n\ninterface LinkProps<T extends HTMLAnchorElement = HTMLAnchorElement>\n extends AnchorHTMLAttributes<T> {\n /**\n * Label text\n */\n label: string;\n /**\n * Icon name\n */\n icon?: IconicNames;\n iconType?: IconicTypes;\n /**\n * Icon width\n */\n width?: string;\n /**\n * Underlined\n */\n underlined?: boolean;\n /**\n * Additional class names\n */\n className?: string;\n /**\n * Is the link inert (not itself clickable but part of a clickable element)\n */\n inert?: boolean;\n /**\n * Allow giving a custom component\n */\n component?: FunctionComponent<PropsWithChildren<any>> | string;\n}\n\nexport const Link: FunctionComponent<LinkProps> = ({\n label,\n icon,\n component = 'a',\n iconType,\n underlined = true,\n className,\n inert,\n width = '24px',\n ...anchorAttrs\n}) => {\n if (!label) {\n return null;\n }\n const lastSpace = label.lastIndexOf(' ');\n const lastIndex = lastSpace === -1 ? label.length : lastSpace;\n\n const first = label.substring(0, 1);\n const middle = label.substring(1, lastIndex + 1);\n const last = label.substring(lastIndex);\n const Cmp = component as FunctionComponent<PropsWithChildren>;\n\n const TagName = inert ? 'span' : Cmp;\n\n return (\n <TagName\n className={twMerge(\n 'decoration-none link-container cursor-pointer text-b3 font-semibold',\n className,\n )}\n data-name=\"Link\"\n {...anchorAttrs}\n >\n <span className={clsx({ 'link-underline': underlined })}>{first}</span>\n <span\n className={clsx('hoverable', {\n 'has-icon': icon,\n 'link-underline': underlined,\n })}\n >\n {middle}\n <span className=\"inline-block\">\n {last}\n {icon && (\n <Icon\n name={icon}\n iconType={iconType}\n width={width}\n style={{ marginInlineStart: '8px' }}\n />\n )}\n </span>\n </span>\n </TagName>\n );\n};\n"],"mappings":";;;;;AAuCA,IAAa,KAAsC,EACjD,UACA,SACA,eAAY,KACZ,aACA,gBAAa,IACb,cACA,UACA,WAAQ,QACR,GAAG,QACC;AACJ,KAAI,CAAC,EACH,QAAO;CAET,IAAM,IAAY,EAAM,YAAY,IAAI,EAClC,IAAY,MAAc,KAAK,EAAM,SAAS,GAE9C,IAAQ,EAAM,UAAU,GAAG,EAAE,EAC7B,IAAS,EAAM,UAAU,GAAG,IAAY,EAAE,EAC1C,IAAO,EAAM,UAAU,EAAU;AAKvC,QACE,kBAHc,IAAQ,SAFZ,GAKV;EACE,WAAW,EACT,uEACA,EACD;EACD,aAAU;EACV,GAAI;YANN,CAQE,kBAAC,QAAD;GAAM,WAAW,EAAK,EAAE,kBAAkB,GAAY,CAAC;aAAG;GAAa,CAAA,EACvE,kBAAC,QAAD;GACE,WAAW,EAAK,aAAa;IAC3B,YAAY;IACZ,kBAAkB;IACnB,CAAC;aAJJ,CAMG,GACD,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACG,GACA,KACC,kBAAC,GAAD;KACE,MAAM;KACI;KACH;KACP,OAAO,EAAE,mBAAmB,OAAO;KACnC,CAAA,CAEC;MACF;KACC"}
1
+ {"version":3,"file":"Link.js","names":[],"sources":["../../lib/ui/Link.tsx"],"sourcesContent":["import clsx from 'clsx';\nimport type { AnchorHTMLAttributes, FunctionComponent, PropsWithChildren } from 'react';\n\nimport { Icon, type IconicNames, type IconicTypes } from '@clubmed/trident-icons';\nimport { twMerge } from './helpers/twMerge';\n\ninterface LinkProps<T extends HTMLAnchorElement = HTMLAnchorElement>\n extends AnchorHTMLAttributes<T> {\n /**\n * Label text\n */\n label: string;\n /**\n * Icon name\n */\n icon?: IconicNames;\n iconType?: IconicTypes;\n /**\n * Icon width\n */\n width?: string;\n /**\n * Underlined\n */\n underlined?: boolean;\n /**\n * Additional class names\n */\n className?: string;\n /**\n * Is the link inert (not itself clickable but part of a clickable element)\n */\n inert?: boolean;\n /**\n * Allow giving a custom component\n */\n component?: FunctionComponent<PropsWithChildren<any>> | string;\n}\n\nexport const Link: FunctionComponent<LinkProps> = ({\n label,\n icon,\n component = 'a',\n iconType,\n underlined = true,\n className,\n inert,\n width = '24px',\n ...anchorAttrs\n}) => {\n if (!label) {\n return null;\n }\n const lastSpace = label.lastIndexOf(' ');\n const lastIndex = lastSpace === -1 ? label.length : lastSpace;\n\n const first = label.substring(0, 1);\n const middle = label.substring(1, lastIndex + 1);\n const last = label.substring(lastIndex);\n const Cmp = component as FunctionComponent<PropsWithChildren>;\n\n const TagName = inert ? 'span' : Cmp;\n\n return (\n <TagName\n className={twMerge(\n 'decoration-none link-container cursor-pointer text-b3 font-semibold outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2',\n className,\n )}\n data-name=\"Link\"\n {...anchorAttrs}\n >\n <span className={clsx({ 'link-underline': underlined })}>{first}</span>\n <span\n className={clsx('hoverable', {\n 'has-icon': icon,\n 'link-underline': underlined,\n })}\n >\n {middle}\n <span className=\"inline-block\">\n {last}\n {icon && (\n <Icon\n name={icon}\n iconType={iconType}\n width={width}\n style={{ marginInlineStart: '8px' }}\n />\n )}\n </span>\n </span>\n </TagName>\n );\n};\n"],"mappings":";;;;;AAuCA,IAAa,KAAsC,EACjD,UACA,SACA,eAAY,KACZ,aACA,gBAAa,IACb,cACA,UACA,WAAQ,QACR,GAAG,QACC;AACJ,KAAI,CAAC,EACH,QAAO;CAET,IAAM,IAAY,EAAM,YAAY,IAAI,EAClC,IAAY,MAAc,KAAK,EAAM,SAAS,GAE9C,IAAQ,EAAM,UAAU,GAAG,EAAE,EAC7B,IAAS,EAAM,UAAU,GAAG,IAAY,EAAE,EAC1C,IAAO,EAAM,UAAU,EAAU;AAKvC,QACE,kBAHc,IAAQ,SAFZ,GAKV;EACE,WAAW,EACT,8JACA,EACD;EACD,aAAU;EACV,GAAI;YANN,CAQE,kBAAC,QAAD;GAAM,WAAW,EAAK,EAAE,kBAAkB,GAAY,CAAC;aAAG;GAAa,CAAA,EACvE,kBAAC,QAAD;GACE,WAAW,EAAK,aAAa;IAC3B,YAAY;IACZ,kBAAkB;IACnB,CAAC;aAJJ,CAMG,GACD,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACG,GACA,KACC,kBAAC,GAAD;KACE,MAAM;KACI;KACH;KACP,OAAO,EAAE,mBAAmB,OAAO;KACnC,CAAA,CAEC;MACF;KACC"}
@@ -44,7 +44,7 @@ var o = {
44
44
  href: t.href,
45
45
  "aria-current": n ? "page" : void 0,
46
46
  style: n ? s : void 0,
47
- className: e("relative z-10 grid items-center justify-center gap-2 rounded-pill", "font-sans text-b3 font-semibold focus-visible:ring-8 focus-visible:ring-lavender/20 outline-none", n ? "[@supports(not(anchor-name:--x))]:bg-black" : "focus:bg-pearl", n ? "px-20 py-12" : "md:px-20 md:py-12", n ? "text-white" : "text-black"),
47
+ className: e("relative z-10 grid items-center justify-center gap-2 rounded-pill", "font-sans text-b3 font-semibold focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 outline-none", n ? "[@supports(not(anchor-name:--x))]:bg-black" : "focus:bg-pearl", n ? "px-20 py-12" : "md:px-20 md:py-12", n ? "text-white" : "text-black"),
48
48
  children: [t.icon && /* @__PURE__ */ i("div", {
49
49
  className: e("row-start-1 row-end-1 col-start-1 col-end-1 flex items-center justify-center transition-opacity duration-300 md:hidden", "md:opacity-0 transition-opacity mx-auto", n && "opacity-0"),
50
50
  "aria-hidden": "true",
@@ -1 +1 @@
1
- {"version":3,"file":"PageNavigation.js","names":[],"sources":["../../lib/ui/PageNavigation.tsx"],"sourcesContent":["'use client';\n\nimport type {\n ComponentPropsWithoutRef,\n CSSProperties,\n FunctionComponent,\n KeyboardEvent,\n} from 'react';\nimport { useCallback, useRef } from 'react';\nimport clsx from 'clsx';\nimport { Icon, type IconicNames } from '@clubmed/trident-icons';\n\nexport interface PageNavigationItem {\n id: string;\n label: string;\n href: string;\n icon?: IconicNames;\n}\n\nexport interface PageNavigationProps extends ComponentPropsWithoutRef<'nav'> {\n items: PageNavigationItem[];\n activeItem: string;\n}\n\n// Static — hoisted outside component so it's not recreated on every render\nconst PILL_STYLE: CSSProperties = {\n positionAnchor: '--page-nav-active',\n top: 'anchor(top)',\n bottom: 'anchor(bottom)',\n left: 'anchor(left)',\n right: 'anchor(right)',\n position: 'absolute',\n};\n\nconst ACTIVE_ANCHOR_STYLE: CSSProperties = { anchorName: '--page-nav-active' };\n\nconst PILL = (\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute rounded-pill bg-black transition-all duration-300 [position-anchor:--page-nav-active]\"\n style={PILL_STYLE}\n />\n);\n\nexport const PageNavigation: FunctionComponent<PageNavigationProps> = ({\n items,\n activeItem,\n className,\n ...attrs\n}) => {\n const listRef = useRef<HTMLUListElement>(null);\n\n const handleKeyDown = useCallback((e: KeyboardEvent<HTMLUListElement>) => {\n const links = Array.from(listRef.current?.querySelectorAll<HTMLAnchorElement>('a') ?? []);\n const focused = links.indexOf(document.activeElement as HTMLAnchorElement);\n if (focused === -1) return;\n\n let next = focused;\n if (e.key === 'ArrowRight') next = (focused + 1) % links.length;\n else if (e.key === 'ArrowLeft') next = (focused - 1 + links.length) % links.length;\n else if (e.key === 'Home') next = 0;\n else if (e.key === 'End') next = links.length - 1;\n else return;\n\n e.preventDefault();\n links[next].focus();\n }, []);\n\n return (\n <nav aria-label=\"Page navigation\" {...attrs} className={clsx('flex items-center', className)}>\n <ul\n ref={listRef}\n onKeyDown={handleKeyDown}\n className=\"relative flex items-center gap-4 md:gap-16\"\n role=\"list\"\n >\n {PILL}\n\n {items.map((item) => {\n const isActive = item.id === activeItem;\n\n return (\n <li key={item.id} className=\"relative\">\n <a\n href={item.href}\n aria-current={isActive ? 'page' : undefined}\n style={isActive ? ACTIVE_ANCHOR_STYLE : undefined}\n className={clsx(\n 'relative z-10 grid items-center justify-center gap-2 rounded-pill',\n 'font-sans text-b3 font-semibold focus-visible:ring-8 focus-visible:ring-lavender/20 outline-none',\n isActive ? '[@supports(not(anchor-name:--x))]:bg-black' : 'focus:bg-pearl',\n isActive ? 'px-20 py-12' : 'md:px-20 md:py-12',\n isActive ? 'text-white' : 'text-black',\n )}\n >\n {item.icon && (\n <div\n className={clsx(\n 'row-start-1 row-end-1 col-start-1 col-end-1 flex items-center justify-center transition-opacity duration-300 md:hidden',\n 'md:opacity-0 transition-opacity mx-auto',\n isActive && 'opacity-0',\n )}\n aria-hidden=\"true\"\n >\n <Icon name={item.icon} width=\"30px\" />\n </div>\n )}\n\n <span\n className={clsx(\n 'row-start-1 row-end-1 col-start-1 col-end-1 grid grid-cols-[0fr] md:grid-cols-[1fr] overflow-hidden transition-all opacity-0 duration-300 md:opacity-100 text-center',\n isActive && 'grid-cols-[1fr] opacity-100',\n )}\n >\n <span className=\"overflow-hidden mx-auto\">{item.label}</span>\n </span>\n </a>\n </li>\n );\n })}\n </ul>\n </nav>\n );\n};\n"],"mappings":";;;;;;AAyBA,IAAM,IAA4B;CAChC,gBAAgB;CAChB,KAAK;CACL,QAAQ;CACR,MAAM;CACN,OAAO;CACP,UAAU;CACX,EAEK,IAAqC,EAAE,YAAY,qBAAqB,EAExE,IACJ,kBAAC,QAAD;CACE,eAAY;CACZ,WAAU;CACV,OAAO;CACP,CAAA,EAGS,KAA0D,EACrE,UACA,eACA,cACA,GAAG,QACC;CACJ,IAAM,IAAU,EAAyB,KAAK,EAExC,IAAgB,GAAa,MAAuC;EACxE,IAAM,IAAQ,MAAM,KAAK,EAAQ,SAAS,iBAAoC,IAAI,IAAI,EAAE,CAAC,EACnF,IAAU,EAAM,QAAQ,SAAS,cAAmC;AAC1E,MAAI,MAAY,GAAI;EAEpB,IAAI,IAAO;AACX,MAAI,EAAE,QAAQ,aAAc,MAAQ,IAAU,KAAK,EAAM;WAChD,EAAE,QAAQ,YAAa,MAAQ,IAAU,IAAI,EAAM,UAAU,EAAM;WACnE,EAAE,QAAQ,OAAQ,KAAO;WACzB,EAAE,QAAQ,MAAO,KAAO,EAAM,SAAS;MAC3C;AAGL,EADA,EAAE,gBAAgB,EAClB,EAAM,GAAM,OAAO;IAClB,EAAE,CAAC;AAEN,QACE,kBAAC,OAAD;EAAK,cAAW;EAAkB,GAAI;EAAO,WAAW,EAAK,qBAAqB,EAAU;YAC1F,kBAAC,MAAD;GACE,KAAK;GACL,WAAW;GACX,WAAU;GACV,MAAK;aAJP,CAMG,GAEA,EAAM,KAAK,MAAS;IACnB,IAAM,IAAW,EAAK,OAAO;AAE7B,WACE,kBAAC,MAAD;KAAkB,WAAU;eAC1B,kBAAC,KAAD;MACE,MAAM,EAAK;MACX,gBAAc,IAAW,SAAS,KAAA;MAClC,OAAO,IAAW,IAAsB,KAAA;MACxC,WAAW,EACT,qEACA,oGACA,IAAW,+CAA+C,kBAC1D,IAAW,gBAAgB,qBAC3B,IAAW,eAAe,aAC3B;gBAVH,CAYG,EAAK,QACJ,kBAAC,OAAD;OACE,WAAW,EACT,0HACA,2CACA,KAAY,YACb;OACD,eAAY;iBAEZ,kBAAC,GAAD;QAAM,MAAM,EAAK;QAAM,OAAM;QAAS,CAAA;OAClC,CAAA,EAGR,kBAAC,QAAD;OACE,WAAW,EACT,wKACA,KAAY,8BACb;iBAED,kBAAC,QAAD;QAAM,WAAU;kBAA2B,EAAK;QAAa,CAAA;OACxD,CAAA,CAAA;;KAEN,EAnCI,EAAK,GAmCT;KAEP,CAAA;;EAEA,CAAA"}
1
+ {"version":3,"file":"PageNavigation.js","names":[],"sources":["../../lib/ui/PageNavigation.tsx"],"sourcesContent":["'use client';\n\nimport type {\n ComponentPropsWithoutRef,\n CSSProperties,\n FunctionComponent,\n KeyboardEvent,\n} from 'react';\nimport { useCallback, useRef } from 'react';\nimport clsx from 'clsx';\nimport { Icon, type IconicNames } from '@clubmed/trident-icons';\n\nexport interface PageNavigationItem {\n id: string;\n label: string;\n href: string;\n icon?: IconicNames;\n}\n\nexport interface PageNavigationProps extends ComponentPropsWithoutRef<'nav'> {\n items: PageNavigationItem[];\n activeItem: string;\n}\n\n// Static — hoisted outside component so it's not recreated on every render\nconst PILL_STYLE: CSSProperties = {\n positionAnchor: '--page-nav-active',\n top: 'anchor(top)',\n bottom: 'anchor(bottom)',\n left: 'anchor(left)',\n right: 'anchor(right)',\n position: 'absolute',\n};\n\nconst ACTIVE_ANCHOR_STYLE: CSSProperties = { anchorName: '--page-nav-active' };\n\nconst PILL = (\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute rounded-pill bg-black transition-all duration-300 [position-anchor:--page-nav-active]\"\n style={PILL_STYLE}\n />\n);\n\nexport const PageNavigation: FunctionComponent<PageNavigationProps> = ({\n items,\n activeItem,\n className,\n ...attrs\n}) => {\n const listRef = useRef<HTMLUListElement>(null);\n\n const handleKeyDown = useCallback((e: KeyboardEvent<HTMLUListElement>) => {\n const links = Array.from(listRef.current?.querySelectorAll<HTMLAnchorElement>('a') ?? []);\n const focused = links.indexOf(document.activeElement as HTMLAnchorElement);\n if (focused === -1) return;\n\n let next = focused;\n if (e.key === 'ArrowRight') next = (focused + 1) % links.length;\n else if (e.key === 'ArrowLeft') next = (focused - 1 + links.length) % links.length;\n else if (e.key === 'Home') next = 0;\n else if (e.key === 'End') next = links.length - 1;\n else return;\n\n e.preventDefault();\n links[next].focus();\n }, []);\n\n return (\n <nav aria-label=\"Page navigation\" {...attrs} className={clsx('flex items-center', className)}>\n <ul\n ref={listRef}\n onKeyDown={handleKeyDown}\n className=\"relative flex items-center gap-4 md:gap-16\"\n role=\"list\"\n >\n {PILL}\n\n {items.map((item) => {\n const isActive = item.id === activeItem;\n\n return (\n <li key={item.id} className=\"relative\">\n <a\n href={item.href}\n aria-current={isActive ? 'page' : undefined}\n style={isActive ? ACTIVE_ANCHOR_STYLE : undefined}\n className={clsx(\n 'relative z-10 grid items-center justify-center gap-2 rounded-pill',\n 'font-sans text-b3 font-semibold focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 outline-none',\n isActive ? '[@supports(not(anchor-name:--x))]:bg-black' : 'focus:bg-pearl',\n isActive ? 'px-20 py-12' : 'md:px-20 md:py-12',\n isActive ? 'text-white' : 'text-black',\n )}\n >\n {item.icon && (\n <div\n className={clsx(\n 'row-start-1 row-end-1 col-start-1 col-end-1 flex items-center justify-center transition-opacity duration-300 md:hidden',\n 'md:opacity-0 transition-opacity mx-auto',\n isActive && 'opacity-0',\n )}\n aria-hidden=\"true\"\n >\n <Icon name={item.icon} width=\"30px\" />\n </div>\n )}\n\n <span\n className={clsx(\n 'row-start-1 row-end-1 col-start-1 col-end-1 grid grid-cols-[0fr] md:grid-cols-[1fr] overflow-hidden transition-all opacity-0 duration-300 md:opacity-100 text-center',\n isActive && 'grid-cols-[1fr] opacity-100',\n )}\n >\n <span className=\"overflow-hidden mx-auto\">{item.label}</span>\n </span>\n </a>\n </li>\n );\n })}\n </ul>\n </nav>\n );\n};\n"],"mappings":";;;;;;AAyBA,IAAM,IAA4B;CAChC,gBAAgB;CAChB,KAAK;CACL,QAAQ;CACR,MAAM;CACN,OAAO;CACP,UAAU;CACX,EAEK,IAAqC,EAAE,YAAY,qBAAqB,EAExE,IACJ,kBAAC,QAAD;CACE,eAAY;CACZ,WAAU;CACV,OAAO;CACP,CAAA,EAGS,KAA0D,EACrE,UACA,eACA,cACA,GAAG,QACC;CACJ,IAAM,IAAU,EAAyB,KAAK,EAExC,IAAgB,GAAa,MAAuC;EACxE,IAAM,IAAQ,MAAM,KAAK,EAAQ,SAAS,iBAAoC,IAAI,IAAI,EAAE,CAAC,EACnF,IAAU,EAAM,QAAQ,SAAS,cAAmC;AAC1E,MAAI,MAAY,GAAI;EAEpB,IAAI,IAAO;AACX,MAAI,EAAE,QAAQ,aAAc,MAAQ,IAAU,KAAK,EAAM;WAChD,EAAE,QAAQ,YAAa,MAAQ,IAAU,IAAI,EAAM,UAAU,EAAM;WACnE,EAAE,QAAQ,OAAQ,KAAO;WACzB,EAAE,QAAQ,MAAO,KAAO,EAAM,SAAS;MAC3C;AAGL,EADA,EAAE,gBAAgB,EAClB,EAAM,GAAM,OAAO;IAClB,EAAE,CAAC;AAEN,QACE,kBAAC,OAAD;EAAK,cAAW;EAAkB,GAAI;EAAO,WAAW,EAAK,qBAAqB,EAAU;YAC1F,kBAAC,MAAD;GACE,KAAK;GACL,WAAW;GACX,WAAU;GACV,MAAK;aAJP,CAMG,GAEA,EAAM,KAAK,MAAS;IACnB,IAAM,IAAW,EAAK,OAAO;AAE7B,WACE,kBAAC,MAAD;KAAkB,WAAU;eAC1B,kBAAC,KAAD;MACE,MAAM,EAAK;MACX,gBAAc,IAAW,SAAS,KAAA;MAClC,OAAO,IAAW,IAAsB,KAAA;MACxC,WAAW,EACT,qEACA,0HACA,IAAW,+CAA+C,kBAC1D,IAAW,gBAAgB,qBAC3B,IAAW,eAAe,aAC3B;gBAVH,CAYG,EAAK,QACJ,kBAAC,OAAD;OACE,WAAW,EACT,0HACA,2CACA,KAAY,YACb;OACD,eAAY;iBAEZ,kBAAC,GAAD;QAAM,MAAM,EAAK;QAAM,OAAM;QAAS,CAAA;OAClC,CAAA,EAGR,kBAAC,QAAD;OACE,WAAW,EACT,wKACA,KAAY,8BACb;iBAED,kBAAC,QAAD;QAAM,WAAU;kBAA2B,EAAK;QAAa,CAAA;OACxD,CAAA,CAAA;;KAEN,EAnCI,EAAK,GAmCT;KAEP,CAAA;;EAEA,CAAA"}
package/ui/Pagination.js CHANGED
@@ -53,7 +53,7 @@ var l = () => /* @__PURE__ */ s("li", { children: /* @__PURE__ */ s("div", {
53
53
  "data-action": "previous",
54
54
  "aria-controls": "page-content",
55
55
  "aria-label": `page ${d - 1}`,
56
- className: e("text:black text-base mx-8 flex h-48 shrink-0 items-center justify-center self-start text-b3 font-semibold active:text-black-active disabled:text-grey hocus:text-black-active", { "pointer-events-none disabled:text-grey": n.disabled }),
56
+ className: e("text:black text-base mx-8 flex h-48 shrink-0 items-center justify-center self-start text-b3 font-semibold active:text-black-active disabled:text-grey hocus:text-black-active outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2", { "pointer-events-none disabled:text-grey": n.disabled }),
57
57
  disabled: n.disabled,
58
58
  onClick: () => g(d - 1),
59
59
  type: "button",
@@ -71,7 +71,7 @@ var l = () => /* @__PURE__ */ s("li", { children: /* @__PURE__ */ s("div", {
71
71
  children: /* @__PURE__ */ c("button", {
72
72
  "aria-controls": "page-content",
73
73
  "aria-label": `page ${d + 1}`,
74
- className: e("text:black mb-5 text-base mx-8 flex h-48 shrink-0 items-center justify-center self-start text-b3 font-semibold active:text-black-active disabled:text-grey hocus:text-black-active", { "pointer-events-none disabled:text-grey": n.disabled }),
74
+ className: e("text:black mb-5 text-base mx-8 flex h-48 shrink-0 items-center justify-center self-start text-b3 font-semibold active:text-black-active disabled:text-grey hocus:text-black-active outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2", { "pointer-events-none disabled:text-grey": n.disabled }),
75
75
  "data-action": "next",
76
76
  disabled: n.disabled,
77
77
  onClick: () => g(d + 1),
@@ -1 +1 @@
1
- {"version":3,"file":"Pagination.js","names":[],"sources":["../../lib/ui/Pagination.tsx"],"sourcesContent":["'use client';\n\nimport clsx from 'clsx';\nimport { Fragment, type FunctionComponent, type HTMLAttributes } from 'react';\n\nimport {\n showMobileEllipsis,\n generatePagination,\n hidePageButtonForMobile,\n} from './Pagination.helper';\nimport { Button } from './buttons/Button';\nimport { Icon } from '@clubmed/trident-icons';\nimport type { ScrollerLabels } from './types/ScrollerLabels';\n\nexport interface PaginationProps extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {\n /**\n * The current page number (must be >= 1)\n */\n defaultPage: number;\n /**\n * The total pages number\n */\n count: number;\n /**\n * A callback function to handle page changes.\n */\n onChange: (page: number) => void;\n /**\n * Additional class names for the previous & next buttons\n */\n buttonClassName?: string;\n /**\n * Additional class names for the label of the previous & next buttons\n */\n buttonLabelClassName?: string;\n /**\n * Additional class names\n */\n className?: string;\n /**\n * UI Labels\n */\n labels: ScrollerLabels;\n /**\n * A pagination factory to generate an array of Pagination elements (pageNumber, ellipsis, next button, previous button).\n */\n paginationFactory?: typeof generatePagination;\n}\n\nconst EllipsisDesktop = () => {\n return (\n <li key=\"desktop-ellipsis\">\n <div\n aria-controls=\"page-content\"\n className=\"text-base mx-8 ml-4 hidden size-48 items-center justify-center text-b3 font-semibold text-black md:flex\"\n >\n &#8230;\n </div>\n </li>\n );\n};\n\nconst EllipsisMobile = () => {\n return (\n <li key=\"mobile-ellipsis\">\n <div\n aria-controls=\"page-content\"\n className=\"text-base mx-8 ml-4 flex size-48 items-center justify-center text-b3 font-semibold text-black md:hidden\"\n >\n &#8230;\n </div>\n </li>\n );\n};\n\nexport const Pagination: FunctionComponent<PaginationProps> = ({\n defaultPage,\n buttonClassName = 'md:mr-32',\n buttonLabelClassName = 'md:flex',\n className,\n labels = {},\n onChange,\n paginationFactory = generatePagination,\n count,\n ...props\n}) => {\n const pagination = paginationFactory(defaultPage, count);\n\n return (\n <nav\n {...props}\n data-name=\"Pagination\"\n role=\"navigation\"\n aria-label=\"pagination navigation\"\n className={clsx('flex justify-center border-t-sand', className)}\n >\n <ul className=\"mt-12 flex\">\n {pagination.map((page, index) => {\n switch (page.type) {\n case 'pageNumber':\n return (\n <Fragment key={`pagination-pageNumber-${index}`}>\n {page.value === count && showMobileEllipsis(page.value, defaultPage, count) && (\n <EllipsisMobile />\n )}\n <li\n className={clsx('mr-2', {\n 'hidden md:flex': hidePageButtonForMobile(page.value, defaultPage, count),\n })}\n >\n {page.value === defaultPage ? (\n <Button\n aria-current=\"page\"\n className=\"text-white\"\n color=\"black\"\n variant=\"circle\"\n >\n {page.value.toString()}\n </Button>\n ) : (\n <Button\n className={clsx('size-48 items-center', {\n 'mx-8 flex shrink-0 cursor-pointer justify-center self-start':\n index === pagination.length - 1,\n })}\n onClick={() => onChange(page.value)}\n color=\"white\"\n variant=\"circle\"\n >\n {page.value.toString()}\n </Button>\n )}\n </li>\n {page.value === 1 && showMobileEllipsis(page.value, defaultPage, count) && (\n <EllipsisMobile />\n )}\n </Fragment>\n );\n\n case 'ellipsis':\n return <EllipsisDesktop key={`pagination-ellipsis-${index}`} />;\n\n case 'previousButton':\n return (\n <li key=\"pagination-labels.previous\" className={clsx(buttonClassName)}>\n <button\n data-action=\"previous\"\n aria-controls=\"page-content\"\n aria-label={`page ${defaultPage - 1}`}\n className={clsx(\n 'text:black text-base mx-8 flex h-48 shrink-0 items-center justify-center self-start text-b3 font-semibold active:text-black-active disabled:text-grey hocus:text-black-active',\n {\n 'pointer-events-none disabled:text-grey': page.disabled,\n },\n )}\n disabled={page.disabled}\n onClick={() => onChange(defaultPage - 1)}\n type=\"button\"\n >\n <Icon name=\"ArrowDefaultLeft\" width=\"30px\" />\n <span\n className={clsx(\n 'ml-4 hidden border-b',\n { 'border-black': !page.disabled },\n buttonLabelClassName,\n )}\n >\n {labels.previous}\n </span>\n </button>\n </li>\n );\n\n case 'nextButton':\n return (\n <li className={clsx(buttonClassName)} key=\"pagination-labels.next\">\n <button\n aria-controls=\"page-content\"\n aria-label={`page ${defaultPage + 1}`}\n className={clsx(\n 'text:black mb-5 text-base mx-8 flex h-48 shrink-0 items-center justify-center self-start text-b3 font-semibold active:text-black-active disabled:text-grey hocus:text-black-active',\n {\n 'pointer-events-none disabled:text-grey': page.disabled,\n },\n )}\n data-action=\"next\"\n disabled={page.disabled}\n onClick={() => onChange(defaultPage + 1)}\n type=\"button\"\n >\n <span\n className={clsx(\n 'ml-4 hidden border-b',\n { 'border-black': !page.disabled },\n buttonLabelClassName,\n )}\n >\n {labels.next}\n </span>\n <Icon name=\"ArrowDefaultRight\" width=\"30px\" />\n </button>\n </li>\n );\n\n default:\n return null;\n }\n })}\n </ul>\n </nav>\n );\n};\n"],"mappings":";;;;;;;;AAiDA,IAAM,UAEF,kBAAC,MAAD,EAAA,UACE,kBAAC,OAAD;CACE,iBAAc;CACd,WAAU;WACX;CAEK,CAAA,EACH,EAPG,mBAOH,EAIH,UAEF,kBAAC,MAAD,EAAA,UACE,kBAAC,OAAD;CACE,iBAAc;CACd,WAAU;WACX;CAEK,CAAA,EACH,EAPG,kBAOH,EAII,KAAkD,EAC7D,gBACA,qBAAkB,YAClB,0BAAuB,WACvB,cACA,YAAS,EAAE,EACX,aACA,uBAAoB,GACpB,UACA,GAAG,QACC;CACJ,IAAM,IAAa,EAAkB,GAAa,EAAM;AAExD,QACE,kBAAC,OAAD;EACE,GAAI;EACJ,aAAU;EACV,MAAK;EACL,cAAW;EACX,WAAW,EAAK,qCAAqC,EAAU;YAE/D,kBAAC,MAAD;GAAI,WAAU;aACX,EAAW,KAAK,GAAM,MAAU;AAC/B,YAAQ,EAAK,MAAb;KACE,KAAK,aACH,QACE,kBAAC,GAAD,EAAA,UAAA;MACG,EAAK,UAAU,KAAS,EAAmB,EAAK,OAAO,GAAa,EAAM,IACzE,kBAAC,GAAD,EAAkB,CAAA;MAEpB,kBAAC,MAAD;OACE,WAAW,EAAK,QAAQ,EACtB,kBAAkB,EAAwB,EAAK,OAAO,GAAa,EAAM,EAC1E,CAAC;iBAED,EAAK,UAAU,IACd,kBAAC,GAAD;QACE,gBAAa;QACb,WAAU;QACV,OAAM;QACN,SAAQ;kBAEP,EAAK,MAAM,UAAA;QACL,CAAA,GAET,kBAAC,GAAD;QACE,WAAW,EAAK,wBAAwB,EACtC,+DACE,MAAU,EAAW,SAAS,GACjC,CAAC;QACF,eAAe,EAAS,EAAK,MAAM;QACnC,OAAM;QACN,SAAQ;kBAEP,EAAK,MAAM,UAAA;QACL,CAAA;OAER,CAAA;MACJ,EAAK,UAAU,KAAK,EAAmB,EAAK,OAAO,GAAa,EAAM,IACrE,kBAAC,GAAD,EAAkB,CAAA;MAEX,EAAA,EAnCI,yBAAyB,IAmC7B;KAGf,KAAK,WACH,QAAO,kBAAC,GAAD,EAAwD,EAAlC,uBAAuB,IAAW;KAEjE,KAAK,iBACH,QACE,kBAAC,MAAD;MAAqC,WAAW,EAAK,EAAgB;gBACnE,kBAAC,UAAD;OACE,eAAY;OACZ,iBAAc;OACd,cAAY,QAAQ,IAAc;OAClC,WAAW,EACT,iLACA,EACE,0CAA0C,EAAK,UAChD,CACF;OACD,UAAU,EAAK;OACf,eAAe,EAAS,IAAc,EAAE;OACxC,MAAK;iBAZP,CAcE,kBAAC,GAAD;QAAM,MAAK;QAAmB,OAAM;QAAS,CAAA,EAC7C,kBAAC,QAAD;QACE,WAAW,EACT,wBACA,EAAE,gBAAgB,CAAC,EAAK,UAAU,EAClC,EACD;kBAEA,EAAO;QACH,CAAA,CAAA;;MAEN,EA1BG,6BA0BH;KAGT,KAAK,aACH,QACE,kBAAC,MAAD;MAAI,WAAW,EAAK,EAAgB;gBAClC,kBAAC,UAAD;OACE,iBAAc;OACd,cAAY,QAAQ,IAAc;OAClC,WAAW,EACT,sLACA,EACE,0CAA0C,EAAK,UAChD,CACF;OACD,eAAY;OACZ,UAAU,EAAK;OACf,eAAe,EAAS,IAAc,EAAE;OACxC,MAAK;iBAZP,CAcE,kBAAC,QAAD;QACE,WAAW,EACT,wBACA,EAAE,gBAAgB,CAAC,EAAK,UAAU,EAClC,EACD;kBAEA,EAAO;QACH,CAAA,EACP,kBAAC,GAAD;QAAM,MAAK;QAAoB,OAAM;QAAS,CAAA,CAAA;;MAE7C,EA1BqC,yBA0BrC;KAGT,QACE,QAAO;;;GAGV,CAAA;EACD,CAAA"}
1
+ {"version":3,"file":"Pagination.js","names":[],"sources":["../../lib/ui/Pagination.tsx"],"sourcesContent":["'use client';\n\nimport clsx from 'clsx';\nimport { Fragment, type FunctionComponent, type HTMLAttributes } from 'react';\n\nimport {\n showMobileEllipsis,\n generatePagination,\n hidePageButtonForMobile,\n} from './Pagination.helper';\nimport { Button } from './buttons/Button';\nimport { Icon } from '@clubmed/trident-icons';\nimport type { ScrollerLabels } from './types/ScrollerLabels';\n\nexport interface PaginationProps extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {\n /**\n * The current page number (must be >= 1)\n */\n defaultPage: number;\n /**\n * The total pages number\n */\n count: number;\n /**\n * A callback function to handle page changes.\n */\n onChange: (page: number) => void;\n /**\n * Additional class names for the previous & next buttons\n */\n buttonClassName?: string;\n /**\n * Additional class names for the label of the previous & next buttons\n */\n buttonLabelClassName?: string;\n /**\n * Additional class names\n */\n className?: string;\n /**\n * UI Labels\n */\n labels: ScrollerLabels;\n /**\n * A pagination factory to generate an array of Pagination elements (pageNumber, ellipsis, next button, previous button).\n */\n paginationFactory?: typeof generatePagination;\n}\n\nconst EllipsisDesktop = () => {\n return (\n <li key=\"desktop-ellipsis\">\n <div\n aria-controls=\"page-content\"\n className=\"text-base mx-8 ml-4 hidden size-48 items-center justify-center text-b3 font-semibold text-black md:flex\"\n >\n &#8230;\n </div>\n </li>\n );\n};\n\nconst EllipsisMobile = () => {\n return (\n <li key=\"mobile-ellipsis\">\n <div\n aria-controls=\"page-content\"\n className=\"text-base mx-8 ml-4 flex size-48 items-center justify-center text-b3 font-semibold text-black md:hidden\"\n >\n &#8230;\n </div>\n </li>\n );\n};\n\nexport const Pagination: FunctionComponent<PaginationProps> = ({\n defaultPage,\n buttonClassName = 'md:mr-32',\n buttonLabelClassName = 'md:flex',\n className,\n labels = {},\n onChange,\n paginationFactory = generatePagination,\n count,\n ...props\n}) => {\n const pagination = paginationFactory(defaultPage, count);\n\n return (\n <nav\n {...props}\n data-name=\"Pagination\"\n role=\"navigation\"\n aria-label=\"pagination navigation\"\n className={clsx('flex justify-center border-t-sand', className)}\n >\n <ul className=\"mt-12 flex\">\n {pagination.map((page, index) => {\n switch (page.type) {\n case 'pageNumber':\n return (\n <Fragment key={`pagination-pageNumber-${index}`}>\n {page.value === count && showMobileEllipsis(page.value, defaultPage, count) && (\n <EllipsisMobile />\n )}\n <li\n className={clsx('mr-2', {\n 'hidden md:flex': hidePageButtonForMobile(page.value, defaultPage, count),\n })}\n >\n {page.value === defaultPage ? (\n <Button\n aria-current=\"page\"\n className=\"text-white\"\n color=\"black\"\n variant=\"circle\"\n >\n {page.value.toString()}\n </Button>\n ) : (\n <Button\n className={clsx('size-48 items-center', {\n 'mx-8 flex shrink-0 cursor-pointer justify-center self-start':\n index === pagination.length - 1,\n })}\n onClick={() => onChange(page.value)}\n color=\"white\"\n variant=\"circle\"\n >\n {page.value.toString()}\n </Button>\n )}\n </li>\n {page.value === 1 && showMobileEllipsis(page.value, defaultPage, count) && (\n <EllipsisMobile />\n )}\n </Fragment>\n );\n\n case 'ellipsis':\n return <EllipsisDesktop key={`pagination-ellipsis-${index}`} />;\n\n case 'previousButton':\n return (\n <li key=\"pagination-labels.previous\" className={clsx(buttonClassName)}>\n <button\n data-action=\"previous\"\n aria-controls=\"page-content\"\n aria-label={`page ${defaultPage - 1}`}\n className={clsx(\n 'text:black text-base mx-8 flex h-48 shrink-0 items-center justify-center self-start text-b3 font-semibold active:text-black-active disabled:text-grey hocus:text-black-active outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2',\n {\n 'pointer-events-none disabled:text-grey': page.disabled,\n },\n )}\n disabled={page.disabled}\n onClick={() => onChange(defaultPage - 1)}\n type=\"button\"\n >\n <Icon name=\"ArrowDefaultLeft\" width=\"30px\" />\n <span\n className={clsx(\n 'ml-4 hidden border-b',\n { 'border-black': !page.disabled },\n buttonLabelClassName,\n )}\n >\n {labels.previous}\n </span>\n </button>\n </li>\n );\n\n case 'nextButton':\n return (\n <li className={clsx(buttonClassName)} key=\"pagination-labels.next\">\n <button\n aria-controls=\"page-content\"\n aria-label={`page ${defaultPage + 1}`}\n className={clsx(\n 'text:black mb-5 text-base mx-8 flex h-48 shrink-0 items-center justify-center self-start text-b3 font-semibold active:text-black-active disabled:text-grey hocus:text-black-active outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2',\n {\n 'pointer-events-none disabled:text-grey': page.disabled,\n },\n )}\n data-action=\"next\"\n disabled={page.disabled}\n onClick={() => onChange(defaultPage + 1)}\n type=\"button\"\n >\n <span\n className={clsx(\n 'ml-4 hidden border-b',\n { 'border-black': !page.disabled },\n buttonLabelClassName,\n )}\n >\n {labels.next}\n </span>\n <Icon name=\"ArrowDefaultRight\" width=\"30px\" />\n </button>\n </li>\n );\n\n default:\n return null;\n }\n })}\n </ul>\n </nav>\n );\n};\n"],"mappings":";;;;;;;;AAiDA,IAAM,UAEF,kBAAC,MAAD,EAAA,UACE,kBAAC,OAAD;CACE,iBAAc;CACd,WAAU;WACX;CAEK,CAAA,EACH,EAPG,mBAOH,EAIH,UAEF,kBAAC,MAAD,EAAA,UACE,kBAAC,OAAD;CACE,iBAAc;CACd,WAAU;WACX;CAEK,CAAA,EACH,EAPG,kBAOH,EAII,KAAkD,EAC7D,gBACA,qBAAkB,YAClB,0BAAuB,WACvB,cACA,YAAS,EAAE,EACX,aACA,uBAAoB,GACpB,UACA,GAAG,QACC;CACJ,IAAM,IAAa,EAAkB,GAAa,EAAM;AAExD,QACE,kBAAC,OAAD;EACE,GAAI;EACJ,aAAU;EACV,MAAK;EACL,cAAW;EACX,WAAW,EAAK,qCAAqC,EAAU;YAE/D,kBAAC,MAAD;GAAI,WAAU;aACX,EAAW,KAAK,GAAM,MAAU;AAC/B,YAAQ,EAAK,MAAb;KACE,KAAK,aACH,QACE,kBAAC,GAAD,EAAA,UAAA;MACG,EAAK,UAAU,KAAS,EAAmB,EAAK,OAAO,GAAa,EAAM,IACzE,kBAAC,GAAD,EAAkB,CAAA;MAEpB,kBAAC,MAAD;OACE,WAAW,EAAK,QAAQ,EACtB,kBAAkB,EAAwB,EAAK,OAAO,GAAa,EAAM,EAC1E,CAAC;iBAED,EAAK,UAAU,IACd,kBAAC,GAAD;QACE,gBAAa;QACb,WAAU;QACV,OAAM;QACN,SAAQ;kBAEP,EAAK,MAAM,UAAA;QACL,CAAA,GAET,kBAAC,GAAD;QACE,WAAW,EAAK,wBAAwB,EACtC,+DACE,MAAU,EAAW,SAAS,GACjC,CAAC;QACF,eAAe,EAAS,EAAK,MAAM;QACnC,OAAM;QACN,SAAQ;kBAEP,EAAK,MAAM,UAAA;QACL,CAAA;OAER,CAAA;MACJ,EAAK,UAAU,KAAK,EAAmB,EAAK,OAAO,GAAa,EAAM,IACrE,kBAAC,GAAD,EAAkB,CAAA;MAEX,EAAA,EAnCI,yBAAyB,IAmC7B;KAGf,KAAK,WACH,QAAO,kBAAC,GAAD,EAAwD,EAAlC,uBAAuB,IAAW;KAEjE,KAAK,iBACH,QACE,kBAAC,MAAD;MAAqC,WAAW,EAAK,EAAgB;gBACnE,kBAAC,UAAD;OACE,eAAY;OACZ,iBAAc;OACd,cAAY,QAAQ,IAAc;OAClC,WAAW,EACT,wQACA,EACE,0CAA0C,EAAK,UAChD,CACF;OACD,UAAU,EAAK;OACf,eAAe,EAAS,IAAc,EAAE;OACxC,MAAK;iBAZP,CAcE,kBAAC,GAAD;QAAM,MAAK;QAAmB,OAAM;QAAS,CAAA,EAC7C,kBAAC,QAAD;QACE,WAAW,EACT,wBACA,EAAE,gBAAgB,CAAC,EAAK,UAAU,EAClC,EACD;kBAEA,EAAO;QACH,CAAA,CAAA;;MAEN,EA1BG,6BA0BH;KAGT,KAAK,aACH,QACE,kBAAC,MAAD;MAAI,WAAW,EAAK,EAAgB;gBAClC,kBAAC,UAAD;OACE,iBAAc;OACd,cAAY,QAAQ,IAAc;OAClC,WAAW,EACT,6QACA,EACE,0CAA0C,EAAK,UAChD,CACF;OACD,eAAY;OACZ,UAAU,EAAK;OACf,eAAe,EAAS,IAAc,EAAE;OACxC,MAAK;iBAZP,CAcE,kBAAC,QAAD;QACE,WAAW,EACT,wBACA,EAAE,gBAAgB,CAAC,EAAK,UAAU,EAClC,EACD;kBAEA,EAAO;QACH,CAAA,EACP,kBAAC,GAAD;QAAM,MAAK;QAAoB,OAAM;QAAS,CAAA,CAAA;;MAE7C,EA1BqC,yBA0BrC;KAGT,QACE,QAAO;;;GAGV,CAAA;EACD,CAAA"}