@tpzdsp/next-toolkit 2.5.0 → 3.0.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 (355) hide show
  1. package/package.json +22 -13
  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 +32 -0
  62. package/src/components/ErrorModal/ErrorModal.stories.tsx +101 -0
  63. package/src/components/ErrorModal/ErrorModal.tsx +120 -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 +4 -2
  77. package/src/components/Footer/Footer.stories.tsx +24 -0
  78. package/src/components/Footer/Footer.tsx +41 -0
  79. package/src/components/Footer/Licence.tsx +18 -0
  80. package/src/components/{layout/footer → Footer}/MetaLinks.tsx +8 -3
  81. package/src/components/Footer/Small/SmallFooter.tsx +43 -42
  82. package/src/components/Form/FormGroup.css +18 -0
  83. package/src/components/Form/FormGroup.stories.tsx +87 -0
  84. package/src/components/Form/FormGroup.tsx +16 -0
  85. package/src/components/{layout/header → Header}/Header.stories.tsx +26 -12
  86. package/src/components/Header/Header.test.tsx +87 -0
  87. package/src/components/Header/Header.tsx +303 -0
  88. package/src/components/Header/HeaderAppContent.tsx +120 -0
  89. package/src/components/Heading/Heading.css +40 -0
  90. package/src/components/Heading/Heading.stories.tsx +15 -5
  91. package/src/components/Heading/Heading.tsx +21 -44
  92. package/src/components/Heading/HeadingSkeleton.stories.tsx +30 -0
  93. package/src/components/Heading/HeadingSkeleton.tsx +14 -0
  94. package/src/components/Hero/Hero.css +19 -0
  95. package/src/components/Hero/Hero.stories.tsx +41 -0
  96. package/src/components/Hero/Hero.tsx +14 -0
  97. package/src/components/HintText/HintText.css +5 -0
  98. package/src/components/HintText/HintText.stories.tsx +29 -0
  99. package/src/components/HintText/HintText.tsx +13 -0
  100. package/src/components/InfoBox/InfoBox.css +38 -0
  101. package/src/components/InfoBox/InfoBox.stories.tsx +51 -403
  102. package/src/components/InfoBox/InfoBox.tsx +31 -158
  103. package/src/components/Input/Input.css +77 -0
  104. package/src/components/Input/Input.stories.tsx +123 -0
  105. package/src/components/Input/Input.tsx +74 -0
  106. package/src/components/InputGroup/InputGroup.css +140 -0
  107. package/src/components/InputGroup/InputGroup.stories.tsx +125 -0
  108. package/src/components/InputGroup/InputGroup.tsx +47 -0
  109. package/src/components/InsetText/InsetText.css +5 -0
  110. package/src/components/InsetText/InsetText.stories.tsx +30 -0
  111. package/src/components/InsetText/InsetText.tsx +9 -0
  112. package/src/components/Keyboard/Keyboard.css +9 -0
  113. package/src/components/Keyboard/Keyboard.stories.tsx +63 -0
  114. package/src/components/Keyboard/Keyboard.tsx +30 -0
  115. package/src/components/Label/Label.css +27 -0
  116. package/src/components/Label/Label.stories.tsx +40 -0
  117. package/src/components/Label/Label.tsx +17 -0
  118. package/src/components/LayerSwitcher/LayerSwitcher.css +59 -0
  119. package/src/components/LayerSwitcher/LayerSwitcher.stories.tsx +112 -0
  120. package/src/components/LayerSwitcher/LayerSwitcher.tsx +61 -0
  121. package/src/components/Layout/401/401.stories.tsx +23 -0
  122. package/src/components/Layout/401/401.tsx +1 -0
  123. package/src/components/Layout/403/403.stories.tsx +23 -0
  124. package/src/components/Layout/403/403.tsx +21 -0
  125. package/src/components/Layout/404/404.stories.tsx +23 -0
  126. package/src/components/Layout/404/404.tsx +28 -0
  127. package/src/components/Layout/500/500.stories.tsx +56 -0
  128. package/src/components/Layout/500/500.tsx +63 -0
  129. package/src/components/Layout/ButtonGroup/ButtonGroup.css +52 -0
  130. package/src/components/Layout/ButtonGroup/ButtonGroup.stories.tsx +156 -0
  131. package/src/components/Layout/ButtonGroup/ButtonGroup.tsx +50 -0
  132. package/src/components/Layout/HeadingGroup/HeadingGroup.css +21 -0
  133. package/src/components/Layout/HeadingGroup/HeadingGroup.stories.tsx +84 -0
  134. package/src/components/Layout/HeadingGroup/HeadingGroup.tsx +9 -0
  135. package/src/components/Layout/MainWrapper/MainWrapper.css +12 -0
  136. package/src/components/Layout/MainWrapper/MainWrapper.stories.tsx +36 -0
  137. package/src/components/Layout/MainWrapper/MainWrapper.tsx +14 -0
  138. package/src/components/Layout/PageShell/PageShell.css +42 -0
  139. package/src/components/Layout/PageShell/PageShell.stories.tsx +108 -0
  140. package/src/components/Layout/PageShell/PageShell.tsx +31 -0
  141. package/src/components/Layout/ParagraphGroup/ParagraphGroup.css +5 -0
  142. package/src/components/Layout/ParagraphGroup/ParagraphGroup.stories.tsx +34 -0
  143. package/src/components/Layout/ParagraphGroup/ParagraphGroup.tsx +9 -0
  144. package/src/components/Layout/Stack/Stack.css +29 -0
  145. package/src/components/Layout/Stack/Stack.stories.tsx +83 -0
  146. package/src/components/Layout/Stack/Stack.tsx +19 -0
  147. package/src/components/Layout/WidthContainer/WidthContainer.css +17 -0
  148. package/src/components/Layout/WidthContainer/WidthContainer.stories.tsx +59 -0
  149. package/src/components/Layout/WidthContainer/WidthContainer.tsx +13 -0
  150. package/src/components/Legend/Legend.css +95 -0
  151. package/src/components/Legend/Legend.stories.tsx +81 -0
  152. package/src/components/Legend/Legend.tsx +32 -0
  153. package/src/components/Legend/LegendDot.tsx +15 -0
  154. package/src/components/Legend/LegendPanel.stories.tsx +101 -0
  155. package/src/components/Legend/LegendPanel.tsx +68 -0
  156. package/src/components/Legend/LegendSkeleton.stories.tsx +87 -0
  157. package/src/components/Legend/LegendSkeleton.tsx +73 -0
  158. package/src/components/LegendSelect/LegendSelect.css +8 -0
  159. package/src/components/LegendSelect/LegendSelect.stories.tsx +80 -0
  160. package/src/components/LegendSelect/LegendSelect.tsx +26 -0
  161. package/src/components/Link/Link.css +98 -0
  162. package/src/components/Link/Link.stories.tsx +70 -0
  163. package/src/components/Link/Link.tsx +52 -0
  164. package/src/components/LinkButton/LinkButton.css +67 -0
  165. package/src/components/LinkButton/LinkButton.stories.tsx +90 -42
  166. package/src/components/LinkButton/LinkButton.tsx +50 -63
  167. package/src/components/LinkList/LinkList.css +28 -0
  168. package/src/components/LinkList/LinkList.stories.tsx +46 -0
  169. package/src/components/LinkList/LinkList.tsx +23 -0
  170. package/src/components/List/Li.tsx +9 -0
  171. package/src/components/List/List.css +9 -0
  172. package/src/components/List/OrderedList.stories.tsx +36 -0
  173. package/src/components/List/OrderedList.tsx +9 -0
  174. package/src/components/List/UnorderedList.stories.tsx +36 -0
  175. package/src/components/List/UnorderedList.tsx +9 -0
  176. package/src/components/LoadingBox/LoadingBox.css +27 -0
  177. package/src/components/LoadingBox/LoadingBox.stories.tsx +66 -0
  178. package/src/components/LoadingBox/LoadingBox.tsx +40 -0
  179. package/src/components/Modal/Modal.css +114 -0
  180. package/src/components/Modal/Modal.stories.tsx +41 -160
  181. package/src/components/Modal/Modal.tsx +132 -75
  182. package/src/components/NoScriptBanner/NoScriptBanner.css +17 -0
  183. package/src/components/NoScriptBanner/NoScriptBanner.stories.tsx +46 -0
  184. package/src/components/NoScriptBanner/NoScriptBanner.tsx +26 -0
  185. package/src/components/Paragraph/Paragraph.css +22 -0
  186. package/src/components/Paragraph/Paragraph.stories.tsx +1 -42
  187. package/src/components/Paragraph/Paragraph.tsx +4 -3
  188. package/src/components/Paragraph/ParagraphSkeleton.stories.tsx +44 -0
  189. package/src/components/Paragraph/ParagraphSkeleton.tsx +33 -0
  190. package/src/components/PasswordInput/PasswordInput.css +3 -0
  191. package/src/components/PasswordInput/PasswordInput.stories.tsx +47 -0
  192. package/src/components/PasswordInput/PasswordInput.tsx +45 -0
  193. package/src/components/PhaseBanner/PhaseBanner.css +12 -0
  194. package/src/components/PhaseBanner/PhaseBanner.stories.tsx +61 -0
  195. package/src/components/PhaseBanner/PhaseBanner.tsx +17 -0
  196. package/src/components/Popover/Popover.css +43 -0
  197. package/src/components/Popover/Popover.stories.tsx +102 -0
  198. package/src/components/Popover/Popover.tsx +40 -0
  199. package/src/components/Radio/Radio.css +69 -0
  200. package/src/components/Radio/Radio.stories.tsx +169 -0
  201. package/src/components/Radio/Radio.tsx +56 -0
  202. package/src/components/Resizable/Resizable.css +86 -0
  203. package/src/components/Resizable/Resizable.stories.tsx +123 -0
  204. package/src/components/Resizable/Resizable.tsx +86 -0
  205. package/src/components/ScrollArea/ScrollArea.css +9 -0
  206. package/src/components/ScrollArea/ScrollArea.stories.tsx +42 -0
  207. package/src/components/ScrollArea/ScrollArea.tsx +11 -0
  208. package/src/components/SearchBox/SearchBox.stories.tsx +88 -0
  209. package/src/components/SearchBox/SearchBox.tsx +36 -0
  210. package/src/components/Select/AsyncSelect.stories.tsx +84 -0
  211. package/src/components/Select/AsyncSelect.tsx +41 -0
  212. package/src/components/Select/Select.css +194 -0
  213. package/src/components/Select/Select.stories.tsx +162 -0
  214. package/src/components/Select/Select.test.tsx +36 -0
  215. package/src/components/Select/Select.tsx +291 -0
  216. package/src/components/Select/SelectSkeleton.stories.tsx +51 -0
  217. package/src/components/Select/SelectSkeleton.tsx +23 -0
  218. package/src/components/Separator/Separator.css +41 -0
  219. package/src/components/Separator/Separator.stories.tsx +63 -0
  220. package/src/components/Separator/Separator.tsx +42 -0
  221. package/src/components/Sidebar/ResizableSidebar.tsx +118 -0
  222. package/src/components/Sidebar/Sidebar.css +203 -0
  223. package/src/components/Sidebar/Sidebar.stories.tsx +438 -0
  224. package/src/components/Sidebar/Sidebar.tsx +225 -0
  225. package/src/components/Skeleton/Skeleton.css +16 -0
  226. package/src/components/Skeleton/Skeleton.stories.tsx +39 -0
  227. package/src/components/Skeleton/Skeleton.tsx +12 -0
  228. package/src/components/SkipLink/SkipLink.css +16 -0
  229. package/src/components/SkipLink/SkipLink.stories.tsx +40 -0
  230. package/src/components/SkipLink/SkipLink.tsx +15 -0
  231. package/src/components/Spinner/Spinner.css +30 -0
  232. package/src/components/Spinner/Spinner.stories.tsx +36 -0
  233. package/src/components/Spinner/Spinner.tsx +45 -0
  234. package/src/components/StartButton/StartButton.css +13 -0
  235. package/src/components/StartButton/StartButton.stories.tsx +68 -0
  236. package/src/components/StartButton/StartButton.tsx +50 -0
  237. package/src/components/SubmitButton/SubmitButton.stories.tsx +31 -0
  238. package/src/components/SubmitButton/SubmitButton.tsx +14 -0
  239. package/src/components/SummaryList/SummaryList.css +71 -0
  240. package/src/components/SummaryList/SummaryList.stories.tsx +211 -0
  241. package/src/components/SummaryList/SummaryList.tsx +41 -0
  242. package/src/components/SummaryList/SummaryListSkeleton.stories.tsx +67 -0
  243. package/src/components/SummaryList/SummaryListSkeleton.tsx +40 -0
  244. package/src/components/Tabs/Tabs.css +84 -0
  245. package/src/components/Tabs/Tabs.stories.tsx +133 -0
  246. package/src/components/Tabs/Tabs.tsx +66 -0
  247. package/src/components/Tag/Tag.css +8 -0
  248. package/src/components/Tag/Tag.stories.tsx +96 -0
  249. package/src/components/Tag/Tag.tsx +34 -0
  250. package/src/components/Textarea/Textarea.css +38 -0
  251. package/src/components/Textarea/Textarea.stories.tsx +131 -0
  252. package/src/components/Textarea/Textarea.tsx +50 -0
  253. package/src/components/TextareaCount/TextareaCount.css +17 -0
  254. package/src/components/TextareaCount/TextareaCount.stories.tsx +131 -0
  255. package/src/components/TextareaCount/TextareaCount.tsx +108 -0
  256. package/src/components/Tooltip/Tooltip.css +43 -0
  257. package/src/components/Tooltip/Tooltip.stories.tsx +110 -0
  258. package/src/components/Tooltip/Tooltip.tsx +53 -0
  259. package/src/components/WarningText/WarningText.css +20 -0
  260. package/src/components/WarningText/WarningText.stories.tsx +29 -0
  261. package/src/components/WarningText/WarningText.tsx +17 -0
  262. package/src/components/index.ts +342 -61
  263. package/src/errors/errorDisplay.ts +70 -0
  264. package/src/errors/index.ts +1 -0
  265. package/src/http/constants.ts +1 -0
  266. package/src/http/fetch.ts +7 -3
  267. package/src/map/LayerSwitcherPanel.tsx +2 -173
  268. package/src/map/LegendShim.tsx +2 -0
  269. package/src/map/MapComponent.tsx +7 -17
  270. package/src/map/MapControlsOverlay.tsx +68 -175
  271. package/src/map/Popup.tsx +71 -24
  272. package/src/map/geocoder/Geocoder.test.tsx +46 -3
  273. package/src/map/geocoder/Geocoder.tsx +155 -278
  274. package/src/map/index.ts +1 -1
  275. package/src/types/utils.ts +0 -10
  276. package/src/utils/components.ts +96 -0
  277. package/src/utils/index.ts +1 -0
  278. package/src/utils/utils.ts +0 -12
  279. package/src/assets/images/defra-logo.svg +0 -51
  280. package/src/assets/images/ea-logo.svg +0 -58
  281. package/src/assets/images/ogl.svg +0 -1
  282. package/src/components/Button/Button.test.tsx +0 -53
  283. package/src/components/ButtonLink/ButtonLink.test.tsx +0 -154
  284. package/src/components/Card/Card.test.tsx +0 -51
  285. package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +0 -75
  286. package/src/components/ErrorBoundary/ErrorFallback.test.tsx +0 -107
  287. package/src/components/ErrorText/ErrorText.test.tsx +0 -33
  288. package/src/components/Heading/Heading.test.tsx +0 -22
  289. package/src/components/Hint/Hint.stories.tsx +0 -58
  290. package/src/components/Hint/Hint.test.tsx +0 -33
  291. package/src/components/Hint/Hint.tsx +0 -12
  292. package/src/components/InfoBox/InfoBox.test.tsx +0 -330
  293. package/src/components/InfoBox/types.ts +0 -6
  294. package/src/components/LinkButton/LinkButton.test.tsx +0 -177
  295. package/src/components/Modal/Modal.test.tsx +0 -246
  296. package/src/components/NotificationBanner/NotificationBanner.stories.tsx +0 -45
  297. package/src/components/NotificationBanner/NotificationBanner.test.tsx +0 -58
  298. package/src/components/NotificationBanner/NotificationBanner.tsx +0 -45
  299. package/src/components/Paragraph/Paragraph.test.tsx +0 -10
  300. package/src/components/SlidingPanel/SlidingPanel.stories.tsx +0 -129
  301. package/src/components/SlidingPanel/SlidingPanel.test.tsx +0 -83
  302. package/src/components/SlidingPanel/SlidingPanel.tsx +0 -258
  303. package/src/components/accordion/Accordion.stories.tsx +0 -234
  304. package/src/components/accordion/Accordion.test.tsx +0 -192
  305. package/src/components/accordion/Accordion.tsx +0 -73
  306. package/src/components/backToTop/BackToTop.stories.tsx +0 -407
  307. package/src/components/backToTop/BackToTop.test.tsx +0 -57
  308. package/src/components/backToTop/BackToTop.tsx +0 -134
  309. package/src/components/chip/Chip.stories.tsx +0 -61
  310. package/src/components/chip/Chip.test.tsx +0 -31
  311. package/src/components/chip/Chip.tsx +0 -23
  312. package/src/components/container/Container.tsx +0 -42
  313. package/src/components/cookieBanner/CookieBanner.stories.tsx +0 -257
  314. package/src/components/cookieBanner/CookieBanner.test.tsx +0 -68
  315. package/src/components/cookieBanner/CookieBanner.tsx +0 -73
  316. package/src/components/divider/RuleDivider.stories.tsx +0 -254
  317. package/src/components/divider/RuleDivider.test.tsx +0 -164
  318. package/src/components/divider/RuleDivider.tsx +0 -23
  319. package/src/components/dropdown/DropdownMenu.test.tsx +0 -211
  320. package/src/components/dropdown/DropdownMenu.tsx +0 -108
  321. package/src/components/dropdown/useDropdownMenu.ts +0 -249
  322. package/src/components/form/Input.stories.tsx +0 -435
  323. package/src/components/form/Input.test.tsx +0 -206
  324. package/src/components/form/Input.tsx +0 -23
  325. package/src/components/form/TextArea.stories.tsx +0 -464
  326. package/src/components/form/TextArea.test.tsx +0 -232
  327. package/src/components/form/TextArea.tsx +0 -23
  328. package/src/components/layout/footer/Footer.tsx +0 -26
  329. package/src/components/layout/footer/Licence.tsx +0 -19
  330. package/src/components/layout/header/Header.test.tsx +0 -36
  331. package/src/components/layout/header/Header.tsx +0 -90
  332. package/src/components/layout/header/HeaderAuthClient.test.tsx +0 -45
  333. package/src/components/layout/header/HeaderAuthClient.tsx +0 -40
  334. package/src/components/layout/header/HeaderNavClient.test.tsx +0 -44
  335. package/src/components/layout/header/HeaderNavClient.tsx +0 -67
  336. package/src/components/link/ExternalLink.test.tsx +0 -102
  337. package/src/components/link/ExternalLink.tsx +0 -29
  338. package/src/components/link/Link.tsx +0 -26
  339. package/src/components/select/Select.stories.tsx +0 -336
  340. package/src/components/select/Select.test.tsx +0 -474
  341. package/src/components/select/Select.tsx +0 -125
  342. package/src/components/select/SelectSkeleton.stories.tsx +0 -194
  343. package/src/components/select/SelectSkeleton.test.tsx +0 -104
  344. package/src/components/select/SelectSkeleton.tsx +0 -23
  345. package/src/components/select/common.ts +0 -3
  346. package/src/components/select/index.ts +0 -18
  347. package/src/components/skipLink/SkipLink.stories.tsx +0 -346
  348. package/src/components/skipLink/SkipLink.test.tsx +0 -22
  349. package/src/components/skipLink/SkipLink.tsx +0 -49
  350. package/src/map/LegendPanel.tsx +0 -85
  351. /package/src/{components → assets}/images/DefraLogo.tsx +0 -0
  352. /package/src/{components → assets}/images/EaLogo.tsx +0 -0
  353. /package/src/{components → assets}/images/OglLogo.tsx +0 -0
  354. /package/src/components/{googleAnalytics → GoogleAnalytics}/GlobalVars.tsx +0 -0
  355. /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';