@tpzdsp/next-toolkit 2.5.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (357) hide show
  1. package/package.json +21 -12
  2. package/src/assets/styles/globals.css +75 -95
  3. package/src/assets/styles/ol.css +57 -16
  4. package/src/assets/styles/utils.css +80 -0
  5. package/src/components/Accordion/Accordion.css +85 -0
  6. package/src/components/Accordion/Accordion.stories.tsx +264 -0
  7. package/src/components/Accordion/Accordion.tsx +98 -0
  8. package/src/components/Alert/Alert.css +129 -0
  9. package/src/components/Alert/Alert.stories.tsx +291 -0
  10. package/src/components/Alert/Alert.tsx +76 -0
  11. package/src/components/BackLink/BackLink.css +12 -0
  12. package/src/components/BackLink/BackLink.stories.tsx +52 -0
  13. package/src/components/BackLink/BackLink.tsx +44 -0
  14. package/src/components/BackToTop/BackToTop.css +23 -0
  15. package/src/components/BackToTop/BackToTop.stories.tsx +41 -0
  16. package/src/components/BackToTop/BackToTop.tsx +48 -0
  17. package/src/components/Breadcrumb/Breadcrumb.css +42 -0
  18. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +94 -0
  19. package/src/components/Breadcrumb/Breadcrumb.tsx +135 -0
  20. package/src/components/Button/Button.css +229 -0
  21. package/src/components/Button/Button.stories.tsx +140 -20
  22. package/src/components/Button/Button.tsx +53 -28
  23. package/src/components/Button/ButtonSkeleton.stories.tsx +50 -0
  24. package/src/components/Button/ButtonSkeleton.tsx +14 -0
  25. package/src/components/ButtonGroup/ButtonGroup.css +52 -0
  26. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +156 -0
  27. package/src/components/ButtonGroup/ButtonGroup.tsx +50 -0
  28. package/src/components/ButtonLink/ButtonLink.css +114 -0
  29. package/src/components/ButtonLink/ButtonLink.stories.tsx +38 -51
  30. package/src/components/ButtonLink/ButtonLink.tsx +54 -23
  31. package/src/components/Card/Card.css +13 -0
  32. package/src/components/Card/Card.stories.tsx +98 -57
  33. package/src/components/Card/Card.tsx +4 -13
  34. package/src/components/CardGroup/CardGroup.css +10 -0
  35. package/src/components/CardGroup/CardGroup.stories.tsx +81 -0
  36. package/src/components/CardGroup/CardGroup.tsx +9 -0
  37. package/src/components/Checkbox/Checkbox.css +49 -0
  38. package/src/components/Checkbox/Checkbox.stories.tsx +86 -0
  39. package/src/components/Checkbox/Checkbox.tsx +32 -0
  40. package/src/components/Chip/Chip.css +54 -0
  41. package/src/components/Chip/Chip.stories.tsx +94 -0
  42. package/src/components/Chip/Chip.tsx +34 -0
  43. package/src/components/CookieBanner/CookieBanner.css +19 -0
  44. package/src/components/CookieBanner/CookieBanner.stories.tsx +52 -0
  45. package/src/components/CookieBanner/CookieBanner.tsx +55 -0
  46. package/src/components/CopyButton/CopyButton.stories.tsx +28 -0
  47. package/src/components/CopyButton/CopyButton.tsx +65 -0
  48. package/src/components/DateInput/DateInput.css +13 -0
  49. package/src/components/DateInput/DateInput.stories.tsx +131 -0
  50. package/src/components/DateInput/DateInput.test.tsx +21 -0
  51. package/src/components/DateInput/DateInput.tsx +98 -0
  52. package/src/components/Details/Details.css +28 -0
  53. package/src/components/Details/Details.stories.tsx +48 -0
  54. package/src/components/Details/Details.tsx +41 -0
  55. package/src/components/Dropdown/Dropdown.css +81 -0
  56. package/src/components/Dropdown/Dropdown.stories.tsx +131 -0
  57. package/src/components/Dropdown/Dropdown.tsx +69 -0
  58. package/src/components/ErrorBoundary/ErrorBoundary.stories.tsx +4 -39
  59. package/src/components/ErrorBoundary/ErrorBoundary.tsx +7 -4
  60. package/src/components/ErrorBoundary/ErrorFallback.tsx +32 -66
  61. package/src/components/ErrorModal/ErrorModal.css +34 -0
  62. package/src/components/ErrorModal/ErrorModal.stories.tsx +100 -0
  63. package/src/components/ErrorModal/ErrorModal.tsx +123 -0
  64. package/src/components/ErrorText/ErrorText.css +22 -0
  65. package/src/components/ErrorText/ErrorText.stories.tsx +42 -27
  66. package/src/components/ErrorText/ErrorText.tsx +10 -8
  67. package/src/components/ExternalLink/ExternalLink.css +12 -0
  68. package/src/components/ExternalLink/ExternalLink.stories.tsx +43 -0
  69. package/src/components/ExternalLink/ExternalLink.tsx +43 -0
  70. package/src/components/Fieldset/Fieldset.css +103 -0
  71. package/src/components/Fieldset/Fieldset.stories.tsx +304 -0
  72. package/src/components/Fieldset/Fieldset.tsx +38 -0
  73. package/src/components/FileUpload/FileUpload.css +85 -0
  74. package/src/components/FileUpload/FileUpload.stories.tsx +106 -0
  75. package/src/components/FileUpload/FileUpload.tsx +99 -0
  76. package/src/components/{layout/footer → Footer}/Copyright.tsx +3 -2
  77. package/src/components/Footer/Footer.stories.tsx +27 -0
  78. package/src/components/Footer/Footer.tsx +46 -0
  79. package/src/components/Footer/Licence.tsx +18 -0
  80. package/src/components/Footer/MetaLinks.tsx +47 -0
  81. package/src/components/Footer/Small/SmallFooter.stories.tsx +3 -0
  82. package/src/components/Footer/Small/SmallFooter.tsx +53 -41
  83. package/src/components/Form/FormGroup.css +18 -0
  84. package/src/components/Form/FormGroup.stories.tsx +87 -0
  85. package/src/components/Form/FormGroup.tsx +16 -0
  86. package/src/components/{layout/header → Header}/Header.stories.tsx +26 -12
  87. package/src/components/Header/Header.test.tsx +87 -0
  88. package/src/components/Header/Header.tsx +304 -0
  89. package/src/components/Header/HeaderAppContent.tsx +120 -0
  90. package/src/components/Heading/Heading.css +40 -0
  91. package/src/components/Heading/Heading.stories.tsx +15 -5
  92. package/src/components/Heading/Heading.tsx +21 -44
  93. package/src/components/Heading/HeadingSkeleton.stories.tsx +30 -0
  94. package/src/components/Heading/HeadingSkeleton.tsx +14 -0
  95. package/src/components/Hero/Hero.css +19 -0
  96. package/src/components/Hero/Hero.stories.tsx +41 -0
  97. package/src/components/Hero/Hero.tsx +14 -0
  98. package/src/components/HintText/HintText.css +5 -0
  99. package/src/components/HintText/HintText.stories.tsx +29 -0
  100. package/src/components/HintText/HintText.tsx +13 -0
  101. package/src/components/InfoBox/InfoBox.css +38 -0
  102. package/src/components/InfoBox/InfoBox.stories.tsx +51 -403
  103. package/src/components/InfoBox/InfoBox.tsx +31 -158
  104. package/src/components/Input/Input.css +77 -0
  105. package/src/components/Input/Input.stories.tsx +123 -0
  106. package/src/components/Input/Input.tsx +74 -0
  107. package/src/components/InputGroup/InputGroup.css +140 -0
  108. package/src/components/InputGroup/InputGroup.stories.tsx +125 -0
  109. package/src/components/InputGroup/InputGroup.tsx +47 -0
  110. package/src/components/InsetText/InsetText.css +5 -0
  111. package/src/components/InsetText/InsetText.stories.tsx +30 -0
  112. package/src/components/InsetText/InsetText.tsx +9 -0
  113. package/src/components/Keyboard/Keyboard.css +9 -0
  114. package/src/components/Keyboard/Keyboard.stories.tsx +63 -0
  115. package/src/components/Keyboard/Keyboard.tsx +30 -0
  116. package/src/components/Label/Label.css +27 -0
  117. package/src/components/Label/Label.stories.tsx +40 -0
  118. package/src/components/Label/Label.tsx +17 -0
  119. package/src/components/LayerSwitcher/LayerSwitcher.css +59 -0
  120. package/src/components/LayerSwitcher/LayerSwitcher.stories.tsx +112 -0
  121. package/src/components/LayerSwitcher/LayerSwitcher.tsx +61 -0
  122. package/src/components/Layout/401/401.stories.tsx +23 -0
  123. package/src/components/Layout/401/401.tsx +1 -0
  124. package/src/components/Layout/403/403.stories.tsx +23 -0
  125. package/src/components/Layout/403/403.tsx +21 -0
  126. package/src/components/Layout/404/404.stories.tsx +23 -0
  127. package/src/components/Layout/404/404.tsx +28 -0
  128. package/src/components/Layout/500/500.stories.tsx +56 -0
  129. package/src/components/Layout/500/500.tsx +63 -0
  130. package/src/components/Layout/ButtonGroup/ButtonGroup.css +52 -0
  131. package/src/components/Layout/ButtonGroup/ButtonGroup.stories.tsx +156 -0
  132. package/src/components/Layout/ButtonGroup/ButtonGroup.tsx +50 -0
  133. package/src/components/Layout/HeadingGroup/HeadingGroup.css +21 -0
  134. package/src/components/Layout/HeadingGroup/HeadingGroup.stories.tsx +84 -0
  135. package/src/components/Layout/HeadingGroup/HeadingGroup.tsx +9 -0
  136. package/src/components/Layout/MainWrapper/MainWrapper.css +12 -0
  137. package/src/components/Layout/MainWrapper/MainWrapper.stories.tsx +36 -0
  138. package/src/components/Layout/MainWrapper/MainWrapper.tsx +14 -0
  139. package/src/components/Layout/PageShell/PageShell.css +42 -0
  140. package/src/components/Layout/PageShell/PageShell.stories.tsx +108 -0
  141. package/src/components/Layout/PageShell/PageShell.tsx +31 -0
  142. package/src/components/Layout/ParagraphGroup/ParagraphGroup.css +5 -0
  143. package/src/components/Layout/ParagraphGroup/ParagraphGroup.stories.tsx +34 -0
  144. package/src/components/Layout/ParagraphGroup/ParagraphGroup.tsx +9 -0
  145. package/src/components/Layout/Stack/Stack.css +29 -0
  146. package/src/components/Layout/Stack/Stack.stories.tsx +83 -0
  147. package/src/components/Layout/Stack/Stack.tsx +19 -0
  148. package/src/components/Layout/WidthContainer/WidthContainer.css +17 -0
  149. package/src/components/Layout/WidthContainer/WidthContainer.stories.tsx +59 -0
  150. package/src/components/Layout/WidthContainer/WidthContainer.tsx +13 -0
  151. package/src/components/Legend/Legend.css +95 -0
  152. package/src/components/Legend/Legend.stories.tsx +81 -0
  153. package/src/components/Legend/Legend.tsx +32 -0
  154. package/src/components/Legend/LegendDot.tsx +15 -0
  155. package/src/components/Legend/LegendPanel.stories.tsx +101 -0
  156. package/src/components/Legend/LegendPanel.tsx +68 -0
  157. package/src/components/Legend/LegendSkeleton.stories.tsx +87 -0
  158. package/src/components/Legend/LegendSkeleton.tsx +73 -0
  159. package/src/components/LegendSelect/LegendSelect.css +8 -0
  160. package/src/components/LegendSelect/LegendSelect.stories.tsx +80 -0
  161. package/src/components/LegendSelect/LegendSelect.tsx +26 -0
  162. package/src/components/Link/Link.css +98 -0
  163. package/src/components/Link/Link.stories.tsx +70 -0
  164. package/src/components/Link/Link.tsx +52 -0
  165. package/src/components/LinkButton/LinkButton.css +67 -0
  166. package/src/components/LinkButton/LinkButton.stories.tsx +90 -42
  167. package/src/components/LinkButton/LinkButton.tsx +50 -63
  168. package/src/components/LinkList/LinkList.css +28 -0
  169. package/src/components/LinkList/LinkList.stories.tsx +46 -0
  170. package/src/components/LinkList/LinkList.tsx +23 -0
  171. package/src/components/List/Li.tsx +9 -0
  172. package/src/components/List/List.css +9 -0
  173. package/src/components/List/OrderedList.stories.tsx +36 -0
  174. package/src/components/List/OrderedList.tsx +9 -0
  175. package/src/components/List/UnorderedList.stories.tsx +36 -0
  176. package/src/components/List/UnorderedList.tsx +9 -0
  177. package/src/components/LoadingBox/LoadingBox.css +27 -0
  178. package/src/components/LoadingBox/LoadingBox.stories.tsx +66 -0
  179. package/src/components/LoadingBox/LoadingBox.tsx +40 -0
  180. package/src/components/Modal/Modal.css +111 -0
  181. package/src/components/Modal/Modal.stories.tsx +94 -155
  182. package/src/components/Modal/Modal.tsx +141 -75
  183. package/src/components/NoScriptBanner/NoScriptBanner.css +17 -0
  184. package/src/components/NoScriptBanner/NoScriptBanner.stories.tsx +46 -0
  185. package/src/components/NoScriptBanner/NoScriptBanner.tsx +26 -0
  186. package/src/components/Paragraph/Paragraph.css +22 -0
  187. package/src/components/Paragraph/Paragraph.stories.tsx +1 -42
  188. package/src/components/Paragraph/Paragraph.tsx +4 -3
  189. package/src/components/Paragraph/ParagraphSkeleton.stories.tsx +44 -0
  190. package/src/components/Paragraph/ParagraphSkeleton.tsx +33 -0
  191. package/src/components/PasswordInput/PasswordInput.css +3 -0
  192. package/src/components/PasswordInput/PasswordInput.stories.tsx +47 -0
  193. package/src/components/PasswordInput/PasswordInput.tsx +45 -0
  194. package/src/components/PhaseBanner/PhaseBanner.css +12 -0
  195. package/src/components/PhaseBanner/PhaseBanner.stories.tsx +61 -0
  196. package/src/components/PhaseBanner/PhaseBanner.tsx +17 -0
  197. package/src/components/Popover/Popover.css +43 -0
  198. package/src/components/Popover/Popover.stories.tsx +102 -0
  199. package/src/components/Popover/Popover.tsx +40 -0
  200. package/src/components/Radio/Radio.css +69 -0
  201. package/src/components/Radio/Radio.stories.tsx +169 -0
  202. package/src/components/Radio/Radio.tsx +56 -0
  203. package/src/components/Resizable/Resizable.css +86 -0
  204. package/src/components/Resizable/Resizable.stories.tsx +123 -0
  205. package/src/components/Resizable/Resizable.tsx +86 -0
  206. package/src/components/ScrollArea/ScrollArea.css +9 -0
  207. package/src/components/ScrollArea/ScrollArea.stories.tsx +42 -0
  208. package/src/components/ScrollArea/ScrollArea.tsx +11 -0
  209. package/src/components/SearchBox/SearchBox.stories.tsx +88 -0
  210. package/src/components/SearchBox/SearchBox.tsx +36 -0
  211. package/src/components/Select/AsyncSelect.stories.tsx +84 -0
  212. package/src/components/Select/AsyncSelect.tsx +41 -0
  213. package/src/components/Select/Select.css +202 -0
  214. package/src/components/Select/Select.stories.tsx +162 -0
  215. package/src/components/Select/Select.test.tsx +36 -0
  216. package/src/components/Select/Select.tsx +296 -0
  217. package/src/components/Select/SelectSkeleton.stories.tsx +51 -0
  218. package/src/components/Select/SelectSkeleton.tsx +23 -0
  219. package/src/components/Separator/Separator.css +41 -0
  220. package/src/components/Separator/Separator.stories.tsx +63 -0
  221. package/src/components/Separator/Separator.tsx +42 -0
  222. package/src/components/Sidebar/ResizableSidebar.tsx +118 -0
  223. package/src/components/Sidebar/Sidebar.css +203 -0
  224. package/src/components/Sidebar/Sidebar.stories.tsx +438 -0
  225. package/src/components/Sidebar/Sidebar.tsx +225 -0
  226. package/src/components/Skeleton/Skeleton.css +16 -0
  227. package/src/components/Skeleton/Skeleton.stories.tsx +39 -0
  228. package/src/components/Skeleton/Skeleton.tsx +12 -0
  229. package/src/components/SkipLink/SkipLink.css +16 -0
  230. package/src/components/SkipLink/SkipLink.stories.tsx +40 -0
  231. package/src/components/SkipLink/SkipLink.tsx +15 -0
  232. package/src/components/Spinner/Spinner.css +30 -0
  233. package/src/components/Spinner/Spinner.stories.tsx +36 -0
  234. package/src/components/Spinner/Spinner.tsx +45 -0
  235. package/src/components/StartButton/StartButton.css +13 -0
  236. package/src/components/StartButton/StartButton.stories.tsx +68 -0
  237. package/src/components/StartButton/StartButton.tsx +50 -0
  238. package/src/components/SubmitButton/SubmitButton.stories.tsx +31 -0
  239. package/src/components/SubmitButton/SubmitButton.tsx +14 -0
  240. package/src/components/SummaryList/SummaryList.css +71 -0
  241. package/src/components/SummaryList/SummaryList.stories.tsx +211 -0
  242. package/src/components/SummaryList/SummaryList.tsx +41 -0
  243. package/src/components/SummaryList/SummaryListSkeleton.stories.tsx +67 -0
  244. package/src/components/SummaryList/SummaryListSkeleton.tsx +40 -0
  245. package/src/components/Tabs/Tabs.css +84 -0
  246. package/src/components/Tabs/Tabs.stories.tsx +133 -0
  247. package/src/components/Tabs/Tabs.tsx +66 -0
  248. package/src/components/Tag/Tag.css +8 -0
  249. package/src/components/Tag/Tag.stories.tsx +96 -0
  250. package/src/components/Tag/Tag.tsx +34 -0
  251. package/src/components/Textarea/Textarea.css +38 -0
  252. package/src/components/Textarea/Textarea.stories.tsx +131 -0
  253. package/src/components/Textarea/Textarea.tsx +50 -0
  254. package/src/components/TextareaCount/TextareaCount.css +17 -0
  255. package/src/components/TextareaCount/TextareaCount.stories.tsx +131 -0
  256. package/src/components/TextareaCount/TextareaCount.tsx +108 -0
  257. package/src/components/Tooltip/Tooltip.css +43 -0
  258. package/src/components/Tooltip/Tooltip.stories.tsx +110 -0
  259. package/src/components/Tooltip/Tooltip.tsx +53 -0
  260. package/src/components/WarningText/WarningText.css +20 -0
  261. package/src/components/WarningText/WarningText.stories.tsx +29 -0
  262. package/src/components/WarningText/WarningText.tsx +17 -0
  263. package/src/components/index.ts +343 -61
  264. package/src/errors/errorDisplay.ts +70 -0
  265. package/src/errors/index.ts +1 -0
  266. package/src/http/constants.ts +2 -0
  267. package/src/http/fetch.ts +7 -3
  268. package/src/map/LayerSwitcherPanel.tsx +2 -173
  269. package/src/map/LegendShim.tsx +2 -0
  270. package/src/map/MapComponent.tsx +7 -17
  271. package/src/map/MapControlsOverlay.tsx +68 -175
  272. package/src/map/Popup.tsx +71 -24
  273. package/src/map/geocoder/Geocoder.test.tsx +46 -3
  274. package/src/map/geocoder/Geocoder.tsx +155 -278
  275. package/src/map/index.ts +1 -1
  276. package/src/types/utils.ts +0 -10
  277. package/src/utils/components.ts +96 -0
  278. package/src/utils/index.ts +1 -0
  279. package/src/utils/utils.ts +0 -12
  280. package/src/assets/images/defra-logo.svg +0 -51
  281. package/src/assets/images/ea-logo.svg +0 -58
  282. package/src/assets/images/ogl.svg +0 -1
  283. package/src/components/Button/Button.test.tsx +0 -53
  284. package/src/components/ButtonLink/ButtonLink.test.tsx +0 -154
  285. package/src/components/Card/Card.test.tsx +0 -51
  286. package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +0 -75
  287. package/src/components/ErrorBoundary/ErrorFallback.test.tsx +0 -107
  288. package/src/components/ErrorText/ErrorText.test.tsx +0 -33
  289. package/src/components/Heading/Heading.test.tsx +0 -22
  290. package/src/components/Hint/Hint.stories.tsx +0 -58
  291. package/src/components/Hint/Hint.test.tsx +0 -33
  292. package/src/components/Hint/Hint.tsx +0 -12
  293. package/src/components/InfoBox/InfoBox.test.tsx +0 -330
  294. package/src/components/InfoBox/types.ts +0 -6
  295. package/src/components/LinkButton/LinkButton.test.tsx +0 -177
  296. package/src/components/Modal/Modal.test.tsx +0 -246
  297. package/src/components/NotificationBanner/NotificationBanner.stories.tsx +0 -45
  298. package/src/components/NotificationBanner/NotificationBanner.test.tsx +0 -58
  299. package/src/components/NotificationBanner/NotificationBanner.tsx +0 -45
  300. package/src/components/Paragraph/Paragraph.test.tsx +0 -10
  301. package/src/components/SlidingPanel/SlidingPanel.stories.tsx +0 -129
  302. package/src/components/SlidingPanel/SlidingPanel.test.tsx +0 -83
  303. package/src/components/SlidingPanel/SlidingPanel.tsx +0 -258
  304. package/src/components/accordion/Accordion.stories.tsx +0 -234
  305. package/src/components/accordion/Accordion.test.tsx +0 -192
  306. package/src/components/accordion/Accordion.tsx +0 -73
  307. package/src/components/backToTop/BackToTop.stories.tsx +0 -407
  308. package/src/components/backToTop/BackToTop.test.tsx +0 -57
  309. package/src/components/backToTop/BackToTop.tsx +0 -134
  310. package/src/components/chip/Chip.stories.tsx +0 -61
  311. package/src/components/chip/Chip.test.tsx +0 -31
  312. package/src/components/chip/Chip.tsx +0 -23
  313. package/src/components/container/Container.tsx +0 -42
  314. package/src/components/cookieBanner/CookieBanner.stories.tsx +0 -257
  315. package/src/components/cookieBanner/CookieBanner.test.tsx +0 -68
  316. package/src/components/cookieBanner/CookieBanner.tsx +0 -73
  317. package/src/components/divider/RuleDivider.stories.tsx +0 -254
  318. package/src/components/divider/RuleDivider.test.tsx +0 -164
  319. package/src/components/divider/RuleDivider.tsx +0 -23
  320. package/src/components/dropdown/DropdownMenu.test.tsx +0 -211
  321. package/src/components/dropdown/DropdownMenu.tsx +0 -108
  322. package/src/components/dropdown/useDropdownMenu.ts +0 -249
  323. package/src/components/form/Input.stories.tsx +0 -435
  324. package/src/components/form/Input.test.tsx +0 -206
  325. package/src/components/form/Input.tsx +0 -23
  326. package/src/components/form/TextArea.stories.tsx +0 -464
  327. package/src/components/form/TextArea.test.tsx +0 -232
  328. package/src/components/form/TextArea.tsx +0 -23
  329. package/src/components/layout/footer/Footer.tsx +0 -26
  330. package/src/components/layout/footer/Licence.tsx +0 -19
  331. package/src/components/layout/footer/MetaLinks.tsx +0 -36
  332. package/src/components/layout/header/Header.test.tsx +0 -36
  333. package/src/components/layout/header/Header.tsx +0 -90
  334. package/src/components/layout/header/HeaderAuthClient.test.tsx +0 -45
  335. package/src/components/layout/header/HeaderAuthClient.tsx +0 -40
  336. package/src/components/layout/header/HeaderNavClient.test.tsx +0 -44
  337. package/src/components/layout/header/HeaderNavClient.tsx +0 -67
  338. package/src/components/link/ExternalLink.test.tsx +0 -102
  339. package/src/components/link/ExternalLink.tsx +0 -29
  340. package/src/components/link/Link.tsx +0 -26
  341. package/src/components/select/Select.stories.tsx +0 -336
  342. package/src/components/select/Select.test.tsx +0 -474
  343. package/src/components/select/Select.tsx +0 -125
  344. package/src/components/select/SelectSkeleton.stories.tsx +0 -194
  345. package/src/components/select/SelectSkeleton.test.tsx +0 -104
  346. package/src/components/select/SelectSkeleton.tsx +0 -23
  347. package/src/components/select/common.ts +0 -3
  348. package/src/components/select/index.ts +0 -18
  349. package/src/components/skipLink/SkipLink.stories.tsx +0 -346
  350. package/src/components/skipLink/SkipLink.test.tsx +0 -22
  351. package/src/components/skipLink/SkipLink.tsx +0 -49
  352. package/src/map/LegendPanel.tsx +0 -85
  353. /package/src/{components → assets}/images/DefraLogo.tsx +0 -0
  354. /package/src/{components → assets}/images/EaLogo.tsx +0 -0
  355. /package/src/{components → assets}/images/OglLogo.tsx +0 -0
  356. /package/src/components/{googleAnalytics → GoogleAnalytics}/GlobalVars.tsx +0 -0
  357. /package/src/components/{googleAnalytics → GoogleAnalytics}/GoogleAnalytics.tsx +0 -0
@@ -27,10 +27,9 @@ describe('Geocoder', () => {
27
27
  const input = screen.getByRole('combobox');
28
28
  const user = userEvent.setup();
29
29
 
30
- // Type into the input
30
+ // Typing alone triggers the debounced search, Enter here is just
31
+ // incidental (it's a no-op with no option focused yet)
31
32
  await user.type(input, 'Lon');
32
-
33
- // Press Enter to trigger search
34
33
  await user.keyboard('{Enter}');
35
34
 
36
35
  // Wait for the result to appear
@@ -111,4 +110,48 @@ describe('Geocoder', () => {
111
110
  expect(screen.queryByText('London')).not.toBeInTheDocument();
112
111
  expect(input).toHaveValue('');
113
112
  });
113
+
114
+ it('searches automatically after the debounce delay, without pressing Enter', async () => {
115
+ const search = vi.fn().mockResolvedValue([{ id: '1', label: 'London', center: [0, 0] }]);
116
+
117
+ render(<Geocoder map={mockMap} search={search} typeaheadDelay={10} />);
118
+
119
+ const input = screen.getByRole('combobox');
120
+ const user = userEvent.setup();
121
+
122
+ await user.type(input, 'Lon');
123
+
124
+ const option = await screen.findByText('London');
125
+
126
+ expect(option).toBeInTheDocument();
127
+ expect(search).toHaveBeenCalledWith('Lon');
128
+ });
129
+
130
+ it('selects a result, then clears it with Backspace', async () => {
131
+ const search = vi.fn().mockResolvedValue([{ id: '1', label: 'London', center: [0, 0] }]);
132
+
133
+ render(<Geocoder map={mockMap} search={search} typeaheadDelay={10} />);
134
+
135
+ const input = screen.getByRole('combobox');
136
+ const user = userEvent.setup();
137
+
138
+ await user.type(input, 'Lon');
139
+
140
+ const option = await screen.findByText('London');
141
+
142
+ await user.click(option);
143
+
144
+ // Selecting shows the result's label in the control (react-select's
145
+ // standard single-select behaviour), the same as any other Select in
146
+ // this library, rather than leaving the raw typed query behind.
147
+ expect(screen.getByText('London')).toBeInTheDocument();
148
+
149
+ // The visible clear (x) button itself is aria-hidden, react-select's own
150
+ // default, mouse/touch-only, the accessible equivalent is Backspace on
151
+ // an empty, focused control.
152
+ await user.click(input);
153
+ await user.keyboard('{Backspace}');
154
+
155
+ expect(screen.queryByText('London')).not.toBeInTheDocument();
156
+ });
114
157
  });
@@ -3,12 +3,13 @@
3
3
  import { useCallback, useEffect, useId, useRef, useState } from 'react';
4
4
 
5
5
  import type Map from 'ol/Map';
6
- import { ImSpinner2 } from 'react-icons/im';
7
- import { IoClose, IoSearch } from 'react-icons/io5';
6
+ import { LuLoaderCircle, LuSearch } from 'react-icons/lu';
7
+ import type { GroupBase, OptionsOrGroups } from 'react-select';
8
8
 
9
9
  import { groupResults } from './groupResults';
10
10
  import type { GeocoderResult } from './types';
11
- import { useDebounce } from '../../hooks/useDebounce';
11
+ import { InputGroup, InputPrefix, InputSwatch } from '../../components/InputGroup/InputGroup';
12
+ import { AsyncSelect } from '../../components/Select/AsyncSelect';
12
13
 
13
14
  // Typeahead defaults
14
15
  const DEFAULT_TYPEAHEAD_DELAY_MS = 500;
@@ -21,6 +22,8 @@ const DEFAULT_ZOOM_OUT_DURATION = 800;
21
22
  const DEFAULT_FLY_TO_DURATION = 1500;
22
23
  const DEFAULT_FIT_PADDING = 40;
23
24
 
25
+ type GeocoderGroup = GroupBase<GeocoderResult>;
26
+
24
27
  export type GeocoderProps = {
25
28
  map: Map;
26
29
  search: (query: string) => Promise<GeocoderResult[]>;
@@ -41,9 +44,7 @@ export type GeocoderProps = {
41
44
  fitPadding?: number;
42
45
  /** Debug callback for logging results and selections */
43
46
  onDebug?: (event: { type: 'results' | 'select' | 'clear'; data?: unknown }) => void;
44
- /** Enable typeahead search while typing */
45
- enableTypeahead?: boolean;
46
- /** Debounce delay in ms for typeahead search (default: 500) */
47
+ /** Debounce delay in ms before searching once the user stops typing (default: 500) */
47
48
  typeaheadDelay?: number;
48
49
  };
49
50
 
@@ -60,302 +61,178 @@ export const Geocoder = ({
60
61
  flyToDuration = DEFAULT_FLY_TO_DURATION,
61
62
  fitPadding = DEFAULT_FIT_PADDING,
62
63
  onDebug,
63
- enableTypeahead = false,
64
64
  typeaheadDelay = DEFAULT_TYPEAHEAD_DELAY_MS,
65
65
  }: GeocoderProps) => {
66
66
  const generatedId = useId();
67
- const id = providedId ?? generatedId;
68
- const listboxId = `${id}-listbox`;
69
-
70
- const inputRef = useRef<HTMLInputElement>(null);
71
-
72
- const [query, setQuery] = useState('');
73
- const [results, setResults] = useState<GeocoderResult[]>([]);
74
- const [selectedId, setSelectedId] = useState<string | null>(null);
75
- const [activeIndex, setActiveIndex] = useState(-1);
76
- const [open, setOpen] = useState(false);
77
- const [status, setStatus] = useState('');
67
+ const inputId = providedId ?? generatedId;
68
+
69
+ // Controlled rather than left uncontrolled: shows the selected result's
70
+ // label in the control (standard single-select behaviour, and the only
71
+ // way react-select's own clear button appears at all, it's tied to
72
+ // hasValue, not to whether there's typed input text) and lets clearing
73
+ // reset it back to null.
74
+ const [value, setValue] = useState<GeocoderResult | null>(null);
75
+
76
+ // Swaps the InputPrefix icon between search and spinner instead of using
77
+ // react-select's own indicators area (hidden via hideLoadingIndicator
78
+ // below), which otherwise grows/shrinks as the indicator mounts and
79
+ // unmounts and shifts the typed text.
78
80
  const [isSearching, setIsSearching] = useState(false);
79
81
 
80
- const debouncedQuery = useDebounce(query, typeaheadDelay);
82
+ // react-select's own useAsync doesn't debounce loadOptions itself, it
83
+ // fires on every keystroke, so the delay is implemented here instead.
84
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
81
85
 
82
- const performSearch = useCallback(
83
- async (queryStr: string) => {
84
- if (queryStr.length < minChars) {
85
- setStatus(`Enter at least ${minChars} characters`);
86
-
87
- return;
88
- }
89
-
90
- setIsSearching(true);
91
- setStatus('Searching...');
92
-
93
- try {
94
- const data = await search(queryStr);
95
-
96
- setResults(data);
97
- setSelectedId(null);
98
- setOpen(true);
99
- setActiveIndex(-1);
100
- setStatus(data.length ? `${data.length} results available` : 'No results found');
101
-
102
- // Debug callback for results
103
- onDebug?.({
104
- type: 'results',
105
- data: { query: queryStr, results: data, count: data.length },
106
- });
107
- } catch (error) {
108
- console.error('Search error:', error);
109
- setStatus('There was an error reaching the server');
110
- setResults([]);
111
- } finally {
112
- setIsSearching(false);
86
+ useEffect(
87
+ () => () => {
88
+ if (debounceRef.current) {
89
+ clearTimeout(debounceRef.current);
113
90
  }
114
91
  },
115
- [minChars, search, onDebug],
92
+ [],
116
93
  );
117
94
 
118
- useEffect(() => {
119
- if (enableTypeahead && debouncedQuery.length >= minChars) {
120
- performSearch(debouncedQuery);
121
- }
122
- }, [debouncedQuery, enableTypeahead, minChars, performSearch]);
123
-
124
- // Scroll the active result into view when navigating with arrow keys
125
- useEffect(() => {
126
- if (activeIndex >= 0) {
127
- document
128
- .getElementById(`${listboxId}-option-${activeIndex}`)
129
- ?.scrollIntoView({ block: 'nearest' });
130
- }
131
- }, [activeIndex, listboxId]);
132
-
133
- const selectResult = (result: GeocoderResult) => {
134
- const view = map.getView();
135
- const currentZoom = view.getZoom() ?? 10;
136
- const shouldZoomOut = currentZoom > flyOutThreshold;
137
- const flyZoom = shouldZoomOut ? Math.max(currentZoom - 4, flyOutZoomMin) : currentZoom;
138
- const padding = [fitPadding, fitPadding, fitPadding, fitPadding] as [
139
- number,
140
- number,
141
- number,
142
- number,
143
- ];
144
- // Use result-specific zoom if provided, otherwise fall back to targetZoom prop
145
- const finalZoom = result.zoom ?? targetZoom;
146
-
147
- if (result.extent) {
148
- if (shouldZoomOut) {
149
- // Step 1: Zoom out, then fit to extent (combined pan + zoom)
150
- view.animate({ zoom: flyZoom, duration: zoomOutDuration }, () => {
151
- view.fit(result.extent!, { padding, duration: flyToDuration });
152
- });
153
- } else {
154
- view.fit(result.extent, { padding, duration: flyToDuration });
155
- }
156
- } else if (result.center) {
157
- if (shouldZoomOut) {
158
- // Step 1: Zoom out, then combined pan + zoom in (like ol-geocoder)
159
- view.animate({ zoom: flyZoom, duration: zoomOutDuration }, () => {
95
+ const selectResult = useCallback(
96
+ (result: GeocoderResult) => {
97
+ const view = map.getView();
98
+ const currentZoom = view.getZoom() ?? 10;
99
+ const shouldZoomOut = currentZoom > flyOutThreshold;
100
+ const flyZoom = shouldZoomOut ? Math.max(currentZoom - 4, flyOutZoomMin) : currentZoom;
101
+ const padding = [fitPadding, fitPadding, fitPadding, fitPadding] as [
102
+ number,
103
+ number,
104
+ number,
105
+ number,
106
+ ];
107
+ // Use result-specific zoom if provided, otherwise fall back to targetZoom prop
108
+ const finalZoom = result.zoom ?? targetZoom;
109
+
110
+ if (result.extent) {
111
+ if (shouldZoomOut) {
112
+ // Step 1: Zoom out, then fit to extent (combined pan + zoom)
113
+ view.animate({ zoom: flyZoom, duration: zoomOutDuration }, () => {
114
+ view.fit(result.extent!, { padding, duration: flyToDuration });
115
+ });
116
+ } else {
117
+ view.fit(result.extent, { padding, duration: flyToDuration });
118
+ }
119
+ } else if (result.center) {
120
+ if (shouldZoomOut) {
121
+ // Step 1: Zoom out, then combined pan + zoom in (like ol-geocoder)
122
+ view.animate({ zoom: flyZoom, duration: zoomOutDuration }, () => {
123
+ view.animate({ center: result.center, zoom: finalZoom, duration: flyToDuration });
124
+ });
125
+ } else {
126
+ // Combined pan + zoom in one smooth animation
160
127
  view.animate({ center: result.center, zoom: finalZoom, duration: flyToDuration });
161
- });
162
- } else {
163
- // Combined pan + zoom in one smooth animation
164
- view.animate({ center: result.center, zoom: finalZoom, duration: flyToDuration });
128
+ }
165
129
  }
166
- }
167
130
 
168
- setSelectedId(result.id);
169
- setOpen(false);
170
- setStatus(`Selected ${result.label}`);
131
+ // Debug callback for selection
132
+ onDebug?.({
133
+ type: 'select',
134
+ data: { result, zoom: finalZoom, currentZoom, shouldZoomOut },
135
+ });
136
+ },
137
+ [
138
+ map,
139
+ flyOutThreshold,
140
+ flyOutZoomMin,
141
+ fitPadding,
142
+ targetZoom,
143
+ zoomOutDuration,
144
+ flyToDuration,
145
+ onDebug,
146
+ ],
147
+ );
171
148
 
172
- // Debug callback for selection
173
- onDebug?.({
174
- type: 'select',
175
- data: { result, zoom: finalZoom, currentZoom, shouldZoomOut },
176
- });
177
- };
149
+ const loadOptions = useCallback(
150
+ (
151
+ inputValue: string,
152
+ callback: (options: OptionsOrGroups<GeocoderResult, GeocoderGroup>) => void,
153
+ ) => {
154
+ if (debounceRef.current) {
155
+ clearTimeout(debounceRef.current);
156
+ }
178
157
 
179
- const clearSearch = () => {
180
- setQuery('');
181
- setResults([]);
182
- setOpen(false);
183
- setActiveIndex(-1);
184
- setSelectedId(null);
185
- setStatus('');
186
- inputRef.current?.focus();
158
+ if (inputValue.length < minChars) {
159
+ setIsSearching(false);
160
+ callback([]);
187
161
 
188
- // Debug callback for clear
189
- onDebug?.({ type: 'clear' });
190
- };
162
+ return;
163
+ }
191
164
 
192
- const handleFocus = () => {
193
- // Show previous results when focusing the input
194
- if (results.length > 0) {
195
- setOpen(true);
196
- setActiveIndex(-1);
197
- }
198
- };
165
+ setIsSearching(true);
199
166
 
200
- const grouped = groupResults(results);
201
- let flatIndex = -1;
167
+ debounceRef.current = setTimeout(async () => {
168
+ try {
169
+ const results = await search(inputValue);
170
+ const grouped = groupResults(results);
171
+ const options = Object.entries(grouped).map(([label, groupOptions]) => ({
172
+ label,
173
+ options: groupOptions,
174
+ }));
175
+
176
+ callback(options);
177
+
178
+ // Debug callback for results
179
+ onDebug?.({
180
+ type: 'results',
181
+ data: { query: inputValue, results, count: results.length },
182
+ });
183
+ } catch (error) {
184
+ console.error('Search error:', error);
185
+ callback([]);
186
+ } finally {
187
+ setIsSearching(false);
188
+ }
189
+ }, typeaheadDelay);
190
+ },
191
+ [minChars, search, typeaheadDelay, onDebug],
192
+ );
202
193
 
203
194
  return (
204
- <div className="relative w-full max-w-sm">
205
- <div role="status" aria-live="polite" className="sr-only">
206
- {status}
207
- </div>
208
-
209
- <label htmlFor={id} className="sr-only">
195
+ <InputGroup className="min-w-xs max-w-sm">
196
+ <label htmlFor={inputId} className="sr-only">
210
197
  Search for a place
211
198
  </label>
212
199
 
213
- <div
214
- className="relative bg-white rounded shadow-lg border-2 border-[#0b0c0c] transition-all
215
- duration-200 focus-within:ring-2 focus-within:ring-focus"
216
- >
217
- <IoSearch
218
- size={20}
219
- className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-gray-400"
220
- />
221
-
222
- <input
223
- ref={inputRef}
224
- id={id}
225
- type="text"
226
- role="combobox"
227
- aria-autocomplete="list"
228
- aria-expanded={open}
229
- aria-controls={listboxId}
230
- aria-activedescendant={
231
- activeIndex >= 0 ? `${listboxId}-option-${activeIndex}` : undefined
232
- }
233
- value={query}
234
- placeholder={placeholder}
235
- onChange={(e) => setQuery(e.target.value)}
236
- onFocus={handleFocus}
237
- onKeyDown={(e) => {
238
- if (e.key === 'ArrowDown') {
239
- e.preventDefault();
240
-
241
- if (!open && results.length > 0) {
242
- setOpen(true);
243
- } else {
244
- setActiveIndex((i) => Math.min(i + 1, results.length - 1));
245
- }
246
- }
247
-
248
- if (e.key === 'ArrowUp') {
249
- e.preventDefault();
250
- setActiveIndex((i) => Math.max(i - 1, 0));
251
- }
252
-
253
- if (e.key === 'Enter') {
254
- e.preventDefault();
255
- const activeResult = results[activeIndex];
256
-
257
- if (activeResult && open) {
258
- selectResult(activeResult);
259
- } else {
260
- performSearch(query);
261
- }
262
- }
263
-
264
- if (e.key === 'Escape') {
265
- e.preventDefault();
266
- clearSearch();
267
- }
268
- }}
269
- className="w-full h-12 pl-11 pr-11 border-0 bg-transparent text-base text-gray-800
270
- placeholder:text-gray-500 focus:outline-none focus:ring-0"
271
- />
272
-
273
- <div className="absolute right-2 top-1/2 -translate-y-1/2">
274
- {(() => {
275
- if (isSearching) {
276
- return (
277
- <div className="p-1">
278
- <ImSpinner2 size={22} className="animate-spin text-gray-500" />
279
- </div>
280
- );
281
- }
282
-
283
- if (query) {
284
- return (
285
- <button
286
- type="button"
287
- onClick={clearSearch}
288
- aria-label="Clear search"
289
- className="p-1 hover:bg-gray-100 rounded transition-colors"
290
- >
291
- <IoClose size={20} className="text-gray-500" />
292
- </button>
293
- );
294
- }
295
-
296
- return <></>;
297
- })()}
298
- </div>
299
- </div>
300
-
301
- {/* Results dropdown with Mapbox styling */}
302
- {open ? (
303
- <div className="absolute z-1000 mt-1.5 w-full bg-white rounded shadow-lg overflow-hidden">
304
- {results.length > 0 ? (
305
- <div id={listboxId} role="listbox" tabIndex={-1} className="max-h-60 overflow-auto">
306
- {Object.entries(grouped).map(([group, items]) => (
307
- <div key={group} role="presentation">
308
- <div
309
- className="px-3 py-1.5 text-xs font-semibold text-gray-600 bg-gray-50 sticky
310
- top-0"
311
- >
312
- {group}
313
- </div>
314
-
315
- <div role="group">
316
- {items.map((item) => {
317
- flatIndex += 1;
318
- const itemIndex = flatIndex;
319
- const isActive = itemIndex === activeIndex;
320
- const isSelected = item.id === selectedId;
321
-
322
- return (
323
- <div
324
- id={`${listboxId}-option-${itemIndex}`}
325
- key={item.id}
326
- role="option"
327
- aria-selected={isSelected}
328
- tabIndex={-1}
329
- className={`scroll-mt-8 cursor-pointer px-3 py-2 text-sm text-gray-800
330
- transition-colors ${isSelected ? 'bg-blue-50 font-semibold' : ''}
331
- ${isActive && !isSelected ? 'bg-gray-100' : ''}
332
- ${!isSelected && !isActive ? 'hover:bg-gray-50' : ''}`}
333
- onMouseEnter={() => setActiveIndex(itemIndex)}
334
- onMouseDown={() => selectResult(item)}
335
- >
336
- <div className="truncate">{item.label}</div>
337
- </div>
338
- );
339
- })}
340
- </div>
341
- </div>
342
- ))}
343
- </div>
200
+ <InputPrefix>
201
+ <InputSwatch>
202
+ {isSearching ? (
203
+ <LuLoaderCircle aria-hidden className="animate-spin" />
344
204
  ) : (
345
- (() => {
346
- if (query && !isSearching) {
347
- return (
348
- <div className="px-3 py-6 text-center text-sm text-gray-500">
349
- {status || 'No results found'}
350
- </div>
351
- );
352
- }
353
-
354
- return <></>;
355
- })()
205
+ <LuSearch aria-hidden />
356
206
  )}
357
- </div>
358
- ) : null}
359
- </div>
207
+ </InputSwatch>
208
+ </InputPrefix>
209
+
210
+ <AsyncSelect<GeocoderResult, false, GeocoderGroup>
211
+ inputId={inputId}
212
+ value={value}
213
+ placeholder={placeholder}
214
+ loadOptions={loadOptions}
215
+ getOptionLabel={(option) => option.label}
216
+ getOptionValue={(option) => option.id}
217
+ isClearable
218
+ hideLoadingIndicator
219
+ noOptionsMessage={({ inputValue }) =>
220
+ inputValue.length < minChars
221
+ ? `Enter at least ${minChars} characters`
222
+ : 'No results found'
223
+ }
224
+ screenReaderStatus={({ count }) => `${count} result${count === 1 ? '' : 's'} available`}
225
+ onChange={(option, meta) => {
226
+ setValue(option);
227
+
228
+ if (meta.action === 'select-option' && option) {
229
+ selectResult(option);
230
+ } else if (meta.action === 'clear') {
231
+ // Debug callback for clear
232
+ onDebug?.({ type: 'clear' });
233
+ }
234
+ }}
235
+ />
236
+ </InputGroup>
360
237
  );
361
238
  };
package/src/map/index.ts CHANGED
@@ -2,7 +2,7 @@ export * from './basemaps';
2
2
  export * from './geocoder/index';
3
3
  export * from './geometries';
4
4
  export * from './LayerSwitcherPanel';
5
- export * from './LegendPanel';
5
+ export * from './LegendShim';
6
6
  export * from './MapControlsOverlay';
7
7
  export * from './utils';
8
8
  export * from './MapComponent';
@@ -1,13 +1,3 @@
1
- import type { ComponentProps } from 'react';
2
-
3
1
  // Utility types for better developer experience
4
2
  export type PropsWithRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
5
3
  export type PropsWithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
6
-
7
- export type ExtendProps<
8
- // `ComponentProps` internally constrains `Comp` to be `JSXElementConstructor<any>`,
9
- // and since our type must have the same constraints to avoid errors, `any` is required here
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- Comp extends keyof React.JSX.IntrinsicElements | React.JSXElementConstructor<any>,
12
- Props = object,
13
- > = Props & Omit<ComponentProps<Comp>, keyof Props>;
@@ -0,0 +1,96 @@
1
+ import { cva } from 'class-variance-authority';
2
+ import { extendTailwindMerge } from 'tailwind-merge';
3
+
4
+ // This project's border-width scale (border-width-form/standard/wide, see
5
+ // @tpzdsp/eslint-config-dsp/tailwindcss/layout.css) and semantic colour scale
6
+ // (see .../tailwindcss/colors/defra-light.css) are custom theme tokens
7
+ // tailwind-merge has no way to know about by default. Without registering
8
+ // them, tailwind-merge fails to recognise e.g. `border-l-standard` as a
9
+ // border-width utility, misclassifies it as an unrecognised border-colour
10
+ // utility instead, and silently drops it when merged alongside a real colour
11
+ // utility like `border-error` (confirmed via the installed tailwind-merge:
12
+ // `twMerge('border-l-standard border-error')` -> `'border-error'`, the width
13
+ // class vanishes so the border renders at its default 0 width).
14
+ const BORDER_WIDTHS = ['form', 'standard', 'wide'];
15
+
16
+ const SEMANTIC_COLORS = [
17
+ 'panel',
18
+ 'panel-border',
19
+ 'body',
20
+ 'body-secondary',
21
+ 'accent',
22
+ 'accent-safe',
23
+ 'accent-hover',
24
+ 'accent-shadow',
25
+ 'accent-subtle',
26
+ 'on-accent',
27
+ 'on-accent-subtle',
28
+ 'link',
29
+ 'link-hover',
30
+ 'link-visited',
31
+ 'link-active',
32
+ 'divider',
33
+ 'divider-input',
34
+ 'focus',
35
+ 'on-focus',
36
+ 'error',
37
+ 'error-hover',
38
+ 'error-shadow',
39
+ 'error-subtle',
40
+ 'on-error',
41
+ 'on-error-subtle',
42
+ 'success',
43
+ 'success-subtle',
44
+ 'on-success-subtle',
45
+ 'wash',
46
+ 'neutral-mid',
47
+ 'muted',
48
+ 'neutral-subtle',
49
+ 'on-neutral-subtle',
50
+ 'surface',
51
+ 'on-surface',
52
+ 'surface-divider',
53
+ 'surface-hover',
54
+ 'warning',
55
+ 'warning-subtle',
56
+ 'on-warning',
57
+ 'on-warning-subtle',
58
+ 'disabled',
59
+ 'on-disabled',
60
+ 'info-subtle',
61
+ 'on-info-subtle',
62
+ ];
63
+
64
+ const twMerge = extendTailwindMerge({
65
+ extend: {
66
+ theme: {
67
+ color: SEMANTIC_COLORS,
68
+ },
69
+ classGroups: {
70
+ 'border-w': [{ border: BORDER_WIDTHS }],
71
+ 'border-w-x': [{ 'border-x': BORDER_WIDTHS }],
72
+ 'border-w-y': [{ 'border-y': BORDER_WIDTHS }],
73
+ 'border-w-t': [{ 'border-t': BORDER_WIDTHS }],
74
+ 'border-w-r': [{ 'border-r': BORDER_WIDTHS }],
75
+ 'border-w-b': [{ 'border-b': BORDER_WIDTHS }],
76
+ 'border-w-l': [{ 'border-l': BORDER_WIDTHS }],
77
+ },
78
+ },
79
+ });
80
+
81
+ /**
82
+ * Utility function to merge Tailwind CSS classes with conflict resolution
83
+ * Uses tailwind-merge to handle conflicting utility classes
84
+ */
85
+ export const cn = (...classes: (string | undefined | null | false)[]): string => {
86
+ return twMerge(classes.filter(Boolean).join(' '));
87
+ };
88
+
89
+ /**
90
+ * Mixture of `cva` (class-variance-authority) and `twMerge`.
91
+ */
92
+ export const cvm = <T>(...args: Parameters<typeof cva<T>>): ReturnType<typeof cva<T>> => {
93
+ const variants = cva<T>(...args);
94
+
95
+ return (...props) => cn(variants(...props));
96
+ };
@@ -2,3 +2,4 @@ export * from './utils';
2
2
  export * from './auth';
3
3
  export * from './constants';
4
4
  export * from './date';
5
+ export * from './components';