@arbor-education/design-system.components 0.15.0 → 0.16.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 (297) hide show
  1. package/.gather/skills/write-stories/SKILL.md +207 -271
  2. package/.storybook/preview.ts +5 -0
  3. package/CHANGELOG.md +17 -0
  4. package/README.md +8 -0
  5. package/component-library.md +144 -13
  6. package/dist/components/articleCard/ArticleCard.stories.d.ts +137 -11
  7. package/dist/components/articleCard/ArticleCard.stories.d.ts.map +1 -1
  8. package/dist/components/articleCard/ArticleCard.stories.js +358 -91
  9. package/dist/components/articleCard/ArticleCard.stories.js.map +1 -1
  10. package/dist/components/avatar/Avatar.stories.d.ts +6 -6
  11. package/dist/components/avatar/Avatar.stories.d.ts.map +1 -1
  12. package/dist/components/avatar/Avatar.stories.js +393 -49
  13. package/dist/components/avatar/Avatar.stories.js.map +1 -1
  14. package/dist/components/avatarGroup/AvatarGroup.stories.d.ts +9 -7
  15. package/dist/components/avatarGroup/AvatarGroup.stories.d.ts.map +1 -1
  16. package/dist/components/avatarGroup/AvatarGroup.stories.js +688 -65
  17. package/dist/components/avatarGroup/AvatarGroup.stories.js.map +1 -1
  18. package/dist/components/banner/Banner.stories.d.ts.map +1 -1
  19. package/dist/components/banner/Banner.stories.js +7 -3
  20. package/dist/components/banner/Banner.stories.js.map +1 -1
  21. package/dist/components/card/Card.stories.d.ts +105 -4
  22. package/dist/components/card/Card.stories.d.ts.map +1 -1
  23. package/dist/components/card/Card.stories.js +336 -18
  24. package/dist/components/card/Card.stories.js.map +1 -1
  25. package/dist/components/combobox/Combobox.stories.d.ts +134 -21
  26. package/dist/components/combobox/Combobox.stories.d.ts.map +1 -1
  27. package/dist/components/combobox/Combobox.stories.js +676 -175
  28. package/dist/components/combobox/Combobox.stories.js.map +1 -1
  29. package/dist/components/datePicker/DatePicker.stories.d.ts +119 -27
  30. package/dist/components/datePicker/DatePicker.stories.d.ts.map +1 -1
  31. package/dist/components/datePicker/DatePicker.stories.js +575 -47
  32. package/dist/components/datePicker/DatePicker.stories.js.map +1 -1
  33. package/dist/components/dateTimePicker/DateTimePicker.stories.d.ts +155 -39
  34. package/dist/components/dateTimePicker/DateTimePicker.stories.d.ts.map +1 -1
  35. package/dist/components/dateTimePicker/DateTimePicker.stories.js +674 -103
  36. package/dist/components/dateTimePicker/DateTimePicker.stories.js.map +1 -1
  37. package/dist/components/editableText/EditableText.stories.d.ts +53 -12
  38. package/dist/components/editableText/EditableText.stories.d.ts.map +1 -1
  39. package/dist/components/editableText/EditableText.stories.js +401 -64
  40. package/dist/components/editableText/EditableText.stories.js.map +1 -1
  41. package/dist/components/formField/FormField.d.ts +4 -0
  42. package/dist/components/formField/FormField.d.ts.map +1 -1
  43. package/dist/components/formField/FormField.js +2 -1
  44. package/dist/components/formField/FormField.js.map +1 -1
  45. package/dist/components/formField/FormField.test.js +5 -0
  46. package/dist/components/formField/FormField.test.js.map +1 -1
  47. package/dist/components/formField/fieldset/Fieldset.stories.d.ts +56 -4
  48. package/dist/components/formField/fieldset/Fieldset.stories.d.ts.map +1 -1
  49. package/dist/components/formField/fieldset/Fieldset.stories.js +534 -28
  50. package/dist/components/formField/fieldset/Fieldset.stories.js.map +1 -1
  51. package/dist/components/formField/inputs/checkbox/CheckboxGroup.d.ts +3 -1
  52. package/dist/components/formField/inputs/checkbox/CheckboxGroup.d.ts.map +1 -1
  53. package/dist/components/formField/inputs/checkbox/CheckboxInput.js +1 -1
  54. package/dist/components/formField/inputs/checkbox/CheckboxInput.js.map +1 -1
  55. package/dist/components/formField/inputs/colourPickerDropdown/ColourPickerDropdown.stories.d.ts +95 -1
  56. package/dist/components/formField/inputs/colourPickerDropdown/ColourPickerDropdown.stories.d.ts.map +1 -1
  57. package/dist/components/formField/inputs/colourPickerDropdown/ColourPickerDropdown.stories.js +386 -9
  58. package/dist/components/formField/inputs/colourPickerDropdown/ColourPickerDropdown.stories.js.map +1 -1
  59. package/dist/components/formField/inputs/radio/RadioButtonGroup.d.ts +6 -2
  60. package/dist/components/formField/inputs/radio/RadioButtonGroup.d.ts.map +1 -1
  61. package/dist/components/formField/inputs/radio/RadioButtonGroup.js.map +1 -1
  62. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts.map +1 -1
  63. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js +61 -49
  64. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js.map +1 -1
  65. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.d.ts +188 -166
  66. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.d.ts.map +1 -1
  67. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.js +821 -160
  68. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.js.map +1 -1
  69. package/dist/components/formField/inputs/time/TimeInput.stories.d.ts +176 -22
  70. package/dist/components/formField/inputs/time/TimeInput.stories.d.ts.map +1 -1
  71. package/dist/components/formField/inputs/time/TimeInput.stories.js +851 -92
  72. package/dist/components/formField/inputs/time/TimeInput.stories.js.map +1 -1
  73. package/dist/components/formField/label/Label.stories.d.ts +54 -5
  74. package/dist/components/formField/label/Label.stories.d.ts.map +1 -1
  75. package/dist/components/formField/label/Label.stories.js +238 -4
  76. package/dist/components/formField/label/Label.stories.js.map +1 -1
  77. package/dist/components/icoText/IcoText.stories.d.ts +32 -6
  78. package/dist/components/icoText/IcoText.stories.d.ts.map +1 -1
  79. package/dist/components/icoText/IcoText.stories.js +309 -14
  80. package/dist/components/icoText/IcoText.stories.js.map +1 -1
  81. package/dist/components/kpiCard/KPICard.stories.d.ts +100 -2
  82. package/dist/components/kpiCard/KPICard.stories.d.ts.map +1 -1
  83. package/dist/components/kpiCard/KPICard.stories.js +354 -10
  84. package/dist/components/kpiCard/KPICard.stories.js.map +1 -1
  85. package/dist/components/kvpList/KVPList.stories.d.ts +57 -4
  86. package/dist/components/kvpList/KVPList.stories.d.ts.map +1 -1
  87. package/dist/components/kvpList/KVPList.stories.js +403 -10
  88. package/dist/components/kvpList/KVPList.stories.js.map +1 -1
  89. package/dist/components/modal/Modal.stories.d.ts +113 -9
  90. package/dist/components/modal/Modal.stories.d.ts.map +1 -1
  91. package/dist/components/modal/Modal.stories.js +633 -13
  92. package/dist/components/modal/Modal.stories.js.map +1 -1
  93. package/dist/components/modal/modalManager/ModalManager.stories.d.ts +34 -10
  94. package/dist/components/modal/modalManager/ModalManager.stories.d.ts.map +1 -1
  95. package/dist/components/modal/modalManager/ModalManager.stories.js +463 -85
  96. package/dist/components/modal/modalManager/ModalManager.stories.js.map +1 -1
  97. package/dist/components/pill/Pill.d.ts.map +1 -1
  98. package/dist/components/pill/Pill.js +1 -1
  99. package/dist/components/pill/Pill.js.map +1 -1
  100. package/dist/components/pill/Pill.stories.d.ts.map +1 -1
  101. package/dist/components/pill/Pill.stories.js +11 -13
  102. package/dist/components/pill/Pill.stories.js.map +1 -1
  103. package/dist/components/row/Row.stories.d.ts +1 -2
  104. package/dist/components/row/Row.stories.d.ts.map +1 -1
  105. package/dist/components/row/Row.stories.js +360 -50
  106. package/dist/components/row/Row.stories.js.map +1 -1
  107. package/dist/components/searchBar/SearchBar.stories.d.ts +52 -4
  108. package/dist/components/searchBar/SearchBar.stories.d.ts.map +1 -1
  109. package/dist/components/searchBar/SearchBar.stories.js +428 -36
  110. package/dist/components/searchBar/SearchBar.stories.js.map +1 -1
  111. package/dist/components/section/Section.stories.d.ts +11 -41
  112. package/dist/components/section/Section.stories.d.ts.map +1 -1
  113. package/dist/components/section/Section.stories.js +494 -56
  114. package/dist/components/section/Section.stories.js.map +1 -1
  115. package/dist/components/singleUser/SingleUser.stories.d.ts +5 -4
  116. package/dist/components/singleUser/SingleUser.stories.d.ts.map +1 -1
  117. package/dist/components/singleUser/SingleUser.stories.js +303 -31
  118. package/dist/components/singleUser/SingleUser.stories.js.map +1 -1
  119. package/dist/components/slideoverManager/SlideoverManager.stories.d.ts +32 -11
  120. package/dist/components/slideoverManager/SlideoverManager.stories.d.ts.map +1 -1
  121. package/dist/components/slideoverManager/SlideoverManager.stories.js +380 -84
  122. package/dist/components/slideoverManager/SlideoverManager.stories.js.map +1 -1
  123. package/dist/components/table/DSDefaultColDef.d.ts.map +1 -1
  124. package/dist/components/table/DSDefaultColDef.js +4 -3
  125. package/dist/components/table/DSDefaultColDef.js.map +1 -1
  126. package/dist/components/table/Table.d.ts +6 -1
  127. package/dist/components/table/Table.d.ts.map +1 -1
  128. package/dist/components/table/Table.js +8 -3
  129. package/dist/components/table/Table.js.map +1 -1
  130. package/dist/components/table/Table.stories.d.ts +2 -0
  131. package/dist/components/table/Table.stories.d.ts.map +1 -1
  132. package/dist/components/table/Table.stories.js +357 -3
  133. package/dist/components/table/Table.stories.js.map +1 -1
  134. package/dist/components/table/TableFooter.stories.d.ts +49 -0
  135. package/dist/components/table/TableFooter.stories.d.ts.map +1 -0
  136. package/dist/components/table/TableFooter.stories.js +137 -0
  137. package/dist/components/table/TableFooter.stories.js.map +1 -0
  138. package/dist/components/table/TableHeader.stories.d.ts +93 -0
  139. package/dist/components/table/TableHeader.stories.d.ts.map +1 -0
  140. package/dist/components/table/TableHeader.stories.js +176 -0
  141. package/dist/components/table/TableHeader.stories.js.map +1 -0
  142. package/dist/components/table/cellEditors/DateCellEditor.stories.d.ts +44 -0
  143. package/dist/components/table/cellEditors/DateCellEditor.stories.d.ts.map +1 -0
  144. package/dist/components/table/cellEditors/DateCellEditor.stories.js +186 -0
  145. package/dist/components/table/cellEditors/DateCellEditor.stories.js.map +1 -0
  146. package/dist/components/table/cellRenderers/BooleanCellRenderer.stories.d.ts +40 -0
  147. package/dist/components/table/cellRenderers/BooleanCellRenderer.stories.d.ts.map +1 -0
  148. package/dist/components/table/cellRenderers/BooleanCellRenderer.stories.js +209 -0
  149. package/dist/components/table/cellRenderers/BooleanCellRenderer.stories.js.map +1 -0
  150. package/dist/components/table/cellRenderers/ButtonCellRenderer.stories.d.ts +48 -0
  151. package/dist/components/table/cellRenderers/ButtonCellRenderer.stories.d.ts.map +1 -0
  152. package/dist/components/table/cellRenderers/ButtonCellRenderer.stories.js +244 -0
  153. package/dist/components/table/cellRenderers/ButtonCellRenderer.stories.js.map +1 -0
  154. package/dist/components/table/cellRenderers/CheckboxCellRenderer.d.ts.map +1 -1
  155. package/dist/components/table/cellRenderers/CheckboxCellRenderer.js +3 -1
  156. package/dist/components/table/cellRenderers/CheckboxCellRenderer.js.map +1 -1
  157. package/dist/components/table/cellRenderers/CheckboxCellRenderer.stories.d.ts +64 -0
  158. package/dist/components/table/cellRenderers/CheckboxCellRenderer.stories.d.ts.map +1 -0
  159. package/dist/components/table/cellRenderers/CheckboxCellRenderer.stories.js +241 -0
  160. package/dist/components/table/cellRenderers/CheckboxCellRenderer.stories.js.map +1 -0
  161. package/dist/components/table/cellRenderers/DefaultCellRenderer.stories.d.ts +55 -0
  162. package/dist/components/table/cellRenderers/DefaultCellRenderer.stories.d.ts.map +1 -0
  163. package/dist/components/table/cellRenderers/DefaultCellRenderer.stories.js +245 -0
  164. package/dist/components/table/cellRenderers/DefaultCellRenderer.stories.js.map +1 -0
  165. package/dist/components/table/cellRenderers/InlineTextCellRenderer.stories.d.ts +67 -0
  166. package/dist/components/table/cellRenderers/InlineTextCellRenderer.stories.d.ts.map +1 -0
  167. package/dist/components/table/cellRenderers/InlineTextCellRenderer.stories.js +221 -0
  168. package/dist/components/table/cellRenderers/InlineTextCellRenderer.stories.js.map +1 -0
  169. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.d.ts +75 -0
  170. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.d.ts.map +1 -0
  171. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js +270 -0
  172. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js.map +1 -0
  173. package/dist/components/table/columnFilters/BooleanFilter.stories.d.ts +57 -0
  174. package/dist/components/table/columnFilters/BooleanFilter.stories.d.ts.map +1 -0
  175. package/dist/components/table/columnFilters/BooleanFilter.stories.js +198 -0
  176. package/dist/components/table/columnFilters/BooleanFilter.stories.js.map +1 -0
  177. package/dist/components/table/columnFilters/TimeFilter.stories.d.ts +58 -0
  178. package/dist/components/table/columnFilters/TimeFilter.stories.d.ts.map +1 -0
  179. package/dist/components/table/columnFilters/TimeFilter.stories.js +207 -0
  180. package/dist/components/table/columnFilters/TimeFilter.stories.js.map +1 -0
  181. package/dist/components/table/pagination/PaginationPanel.stories.d.ts +113 -0
  182. package/dist/components/table/pagination/PaginationPanel.stories.d.ts.map +1 -0
  183. package/dist/components/table/pagination/PaginationPanel.stories.js +272 -0
  184. package/dist/components/table/pagination/PaginationPanel.stories.js.map +1 -0
  185. package/dist/components/table/tableControls/TableControls.stories.d.ts +151 -0
  186. package/dist/components/table/tableControls/TableControls.stories.d.ts.map +1 -0
  187. package/dist/components/table/tableControls/TableControls.stories.js +356 -0
  188. package/dist/components/table/tableControls/TableControls.stories.js.map +1 -0
  189. package/dist/components/table/tableControls/TableSettingsDropdown.d.ts +27 -1
  190. package/dist/components/table/tableControls/TableSettingsDropdown.d.ts.map +1 -1
  191. package/dist/components/table/tableControls/TableSettingsDropdown.js +53 -26
  192. package/dist/components/table/tableControls/TableSettingsDropdown.js.map +1 -1
  193. package/dist/components/table/tableControls/TableSettingsDropdown.test.d.ts +2 -0
  194. package/dist/components/table/tableControls/TableSettingsDropdown.test.d.ts.map +1 -0
  195. package/dist/components/table/tableControls/TableSettingsDropdown.test.js +178 -0
  196. package/dist/components/table/tableControls/TableSettingsDropdown.test.js.map +1 -0
  197. package/dist/components/tabs/Tabs.stories.d.ts +22 -4
  198. package/dist/components/tabs/Tabs.stories.d.ts.map +1 -1
  199. package/dist/components/tabs/Tabs.stories.js +398 -22
  200. package/dist/components/tabs/Tabs.stories.js.map +1 -1
  201. package/dist/components/tabs/TabsItem.stories.d.ts +54 -1
  202. package/dist/components/tabs/TabsItem.stories.d.ts.map +1 -1
  203. package/dist/components/tabs/TabsItem.stories.js +61 -9
  204. package/dist/components/tabs/TabsItem.stories.js.map +1 -1
  205. package/dist/components/toast/Toast.stories.d.ts +103 -10
  206. package/dist/components/toast/Toast.stories.d.ts.map +1 -1
  207. package/dist/components/toast/Toast.stories.js +409 -47
  208. package/dist/components/toast/Toast.stories.js.map +1 -1
  209. package/dist/components/toggle/Toggle.stories.d.ts +61 -46
  210. package/dist/components/toggle/Toggle.stories.d.ts.map +1 -1
  211. package/dist/components/toggle/Toggle.stories.js +311 -122
  212. package/dist/components/toggle/Toggle.stories.js.map +1 -1
  213. package/dist/components/tooltip/Tooltip.stories.d.ts +78 -6
  214. package/dist/components/tooltip/Tooltip.stories.d.ts.map +1 -1
  215. package/dist/components/tooltip/Tooltip.stories.js +413 -7
  216. package/dist/components/tooltip/Tooltip.stories.js.map +1 -1
  217. package/dist/components/tooltip/TooltipWrapper.stories.d.ts +71 -7
  218. package/dist/components/tooltip/TooltipWrapper.stories.d.ts.map +1 -1
  219. package/dist/components/tooltip/TooltipWrapper.stories.js +238 -10
  220. package/dist/components/tooltip/TooltipWrapper.stories.js.map +1 -1
  221. package/dist/index.css +8 -0
  222. package/dist/index.css.map +1 -1
  223. package/dist/utils/PopupParentContext.stories.d.ts +17 -0
  224. package/dist/utils/PopupParentContext.stories.d.ts.map +1 -0
  225. package/dist/utils/PopupParentContext.stories.js +266 -0
  226. package/dist/utils/PopupParentContext.stories.js.map +1 -0
  227. package/dist/utils/getDefaultPopupParent.d.ts.map +1 -1
  228. package/dist/utils/getDefaultPopupParent.js +6 -0
  229. package/dist/utils/getDefaultPopupParent.js.map +1 -1
  230. package/package.json +1 -1
  231. package/src/components/articleCard/ArticleCard.stories.tsx +524 -111
  232. package/src/components/avatar/Avatar.stories.tsx +504 -59
  233. package/src/components/avatarGroup/AvatarGroup.stories.tsx +977 -175
  234. package/src/components/banner/Banner.stories.tsx +7 -3
  235. package/src/components/card/Card.stories.tsx +466 -36
  236. package/src/components/combobox/Combobox.stories.tsx +867 -260
  237. package/src/components/datePicker/DatePicker.stories.tsx +777 -60
  238. package/src/components/dateTimePicker/DateTimePicker.stories.tsx +910 -132
  239. package/src/components/editableText/EditableText.stories.tsx +567 -91
  240. package/src/components/formField/FormField.test.tsx +6 -0
  241. package/src/components/formField/FormField.tsx +5 -0
  242. package/src/components/formField/fieldset/Fieldset.stories.tsx +761 -51
  243. package/src/components/formField/inputs/checkbox/CheckboxGroup.tsx +1 -1
  244. package/src/components/formField/inputs/checkbox/CheckboxInput.tsx +1 -1
  245. package/src/components/formField/inputs/colourPickerDropdown/ColourPickerDropdown.stories.tsx +504 -11
  246. package/src/components/formField/inputs/radio/RadioButtonGroup.tsx +17 -4
  247. package/src/components/formField/inputs/radio/RadioButtonInput.stories.tsx +71 -59
  248. package/src/components/formField/inputs/selectDropdown/SelectDropdown.stories.tsx +1079 -168
  249. package/src/components/formField/inputs/time/TimeInput.stories.tsx +1140 -104
  250. package/src/components/formField/label/Label.stories.tsx +317 -8
  251. package/src/components/icoText/IcoText.stories.tsx +442 -31
  252. package/src/components/kpiCard/KPICard.stories.tsx +475 -30
  253. package/src/components/kvpList/KVPList.stories.tsx +593 -26
  254. package/src/components/modal/Modal.stories.tsx +963 -26
  255. package/src/components/modal/modalManager/ModalManager.stories.tsx +612 -454
  256. package/src/components/pill/Pill.stories.tsx +11 -13
  257. package/src/components/pill/Pill.tsx +1 -0
  258. package/src/components/row/Row.stories.tsx +474 -58
  259. package/src/components/searchBar/SearchBar.stories.tsx +570 -38
  260. package/src/components/section/Section.stories.tsx +723 -70
  261. package/src/components/singleUser/SingleUser.stories.tsx +393 -34
  262. package/src/components/slideoverManager/SlideoverManager.stories.tsx +572 -342
  263. package/src/components/table/DSDefaultColDef.ts +25 -5
  264. package/src/components/table/Table.stories.tsx +411 -3
  265. package/src/components/table/Table.tsx +9 -2
  266. package/src/components/table/TableFooter.stories.tsx +196 -0
  267. package/src/components/table/TableHeader.stories.tsx +251 -0
  268. package/src/components/table/cellEditors/DateCellEditor.stories.tsx +245 -0
  269. package/src/components/table/cellRenderers/BooleanCellRenderer.stories.tsx +278 -0
  270. package/src/components/table/cellRenderers/ButtonCellRenderer.stories.tsx +333 -0
  271. package/src/components/table/cellRenderers/CheckboxCellRenderer.stories.tsx +337 -0
  272. package/src/components/table/cellRenderers/CheckboxCellRenderer.tsx +5 -1
  273. package/src/components/table/cellRenderers/DefaultCellRenderer.stories.tsx +342 -0
  274. package/src/components/table/cellRenderers/InlineTextCellRenderer.stories.tsx +292 -0
  275. package/src/components/table/cellRenderers/SelectDropdownCellRenderer.stories.tsx +369 -0
  276. package/src/components/table/columnFilters/BooleanFilter.stories.tsx +268 -0
  277. package/src/components/table/columnFilters/TimeFilter.stories.tsx +281 -0
  278. package/src/components/table/pagination/PaginationPanel.stories.tsx +327 -0
  279. package/src/components/table/tableControls/TableControls.stories.tsx +415 -0
  280. package/src/components/table/tableControls/TableSettingsDropdown.test.tsx +207 -0
  281. package/src/components/table/tableControls/TableSettingsDropdown.tsx +103 -39
  282. package/src/components/tabs/Tabs.stories.tsx +540 -60
  283. package/src/components/tabs/TabsItem.stories.tsx +82 -8
  284. package/src/components/toast/Toast.stories.tsx +539 -77
  285. package/src/components/toggle/Toggle.stories.tsx +371 -135
  286. package/src/components/tooltip/Tooltip.stories.tsx +606 -15
  287. package/src/components/tooltip/TooltipWrapper.stories.tsx +348 -12
  288. package/src/docs/Contributing.mdx +241 -0
  289. package/src/docs/UsingComponents.mdx +93 -0
  290. package/src/docs/Welcome.mdx +68 -0
  291. package/src/global.scss +7 -0
  292. package/src/utils/PopupParentContext.stories.tsx +367 -0
  293. package/src/utils/getDefaultPopupParent.ts +6 -0
  294. package/.ralph/storybook-upgrade/knowledge.md +0 -308
  295. package/.ralph/storybook-upgrade/prd.json +0 -777
  296. package/.ralph/storybook-upgrade/progress.md +0 -342
  297. package/src/components/table/TableWIP.mdx +0 -3
@@ -1,202 +1,980 @@
1
+ import { useState } from 'react';
1
2
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { formatNativeDateTimeInputValue } from 'Components/datePicker/dateInputUtils';
3
- import { useEffect, useState } from 'react';
4
- import { fn } from 'storybook/test';
5
- import { DateTimePicker, type DateTimePickerProps } from './DateTimePicker';
3
+ import {
4
+ Controls,
5
+ Heading as DocHeading,
6
+ Markdown,
7
+ Primary as DocPrimary,
8
+ Stories,
9
+ Subtitle,
10
+ Title,
11
+ } from '@storybook/addon-docs/blocks';
12
+ import { FormField } from 'Components/formField/FormField';
13
+ import type { TimeValue } from 'Components/formField/inputs/time/TimeInput';
14
+ import { DateTimePicker } from './DateTimePicker';
6
15
 
7
- const timeOptions = ['09:00', '09:30', '10:00', '10:30', '11:00'] as const;
16
+ // ---------------------------------------------------------------------------
17
+ // Docs page content
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const DESCRIPTION_INTRO = [
21
+ 'DateTimePicker combines a date input with an embedded time input, keeping a single unified `Date`',
22
+ 'value. Built on [Radix UI Popover](https://www.radix-ui.com/primitives/docs/components/popover)',
23
+ 'and [react-day-picker](https://daypicker.dev/).',
24
+ '',
25
+ 'The calendar popup contains both the date picker and the time input, keeping date and time',
26
+ 'selection in a single interaction flow. When a date is picked from the calendar, the existing',
27
+ 'time is preserved. When a time is changed, the existing date is preserved.',
28
+ ].join('\n');
29
+
30
+ const USAGE_GUIDANCE = [
31
+ '### When to use',
32
+ '',
33
+ '- **Event scheduling** — lesson start times, parent evening appointments, report deadlines',
34
+ '- **Booking flows** — any scenario where both a date AND a specific time are required',
35
+ '- **Audit timestamps** — log entries, registration opens/closes with precise time',
36
+ '',
37
+ '---',
38
+ '',
39
+ '### When NOT to use',
40
+ '',
41
+ '| Situation | Use instead |',
42
+ '|---|---|',
43
+ '| Date only (no time needed) | `DatePicker` |',
44
+ '| Time only (no date needed) | `TimeInput` |',
45
+ '| Date range selection | Two `DatePicker` fields |',
46
+ '| Fixed time slots from a list | `DateTimePicker` with `timeOptions` |',
47
+ ].join('\n');
48
+
49
+ const DEVELOPER_NOTES = [
50
+ '### Critical usage patterns',
51
+ '',
52
+ '> **Portal rendering.** The calendar panel renders via a Radix portal — do **not** place inside',
53
+ '> a container with `overflow: hidden` or the calendar will be clipped.',
54
+ '',
55
+ '**`onChange` receives a `Date` object — never a string.** Format for display with',
56
+ '`date?.toLocaleDateString(\'en-GB\')` + time as needed. Do not assume ISO string format.',
57
+ '',
58
+ '**`value` and `defaultValue` are `Date` objects — not strings.** Month is zero-indexed:',
59
+ '`new Date(2026, 3, 14, 9, 0)` = 14 April 2026, 09:00. Passing a string is a type error.',
60
+ '',
61
+ '```tsx',
62
+ '// ✅ Correct',
63
+ 'value={new Date(2026, 3, 14, 9, 0)}',
64
+ '',
65
+ '// ❌ Wrong — type error',
66
+ 'value="2026-04-14T09:00"',
67
+ '```',
68
+ '',
69
+ '**Three display formats — choose based on your context:**',
70
+ '',
71
+ '| `displayFormat` | Input type | Placeholder hint | Example value |',
72
+ '|---|---|---|---|',
73
+ '| `"native"` (default) | `datetime-local` | `YYYY-MM-DDTHH:mm` | `2026-04-14T14:27` |',
74
+ '| `"default"` | `text` | `DD/MM/YYYY HH:mm` | `14/04/2026 14:27` |',
75
+ '| `"friendly"` | `text` | `Pick a date, HH:mm` | `April 14th, 2026 14:27` |',
76
+ '',
77
+ '**`timeOptions` switches the embedded time input to combobox mode.** Instead of a free-form',
78
+ 'time input, the user picks from a predefined list. Use for fixed appointment slots.',
79
+ '',
80
+ '**`hasError` is visual-only when used standalone.** It applies the red border but does NOT',
81
+ 'set `aria-invalid`. Always pair both when outside `<FormField>`:',
82
+ '',
83
+ '```tsx',
84
+ '<DateTimePicker hasError aria-invalid={true} aria-describedby="error-id" />',
85
+ '```',
86
+ '',
87
+ '**`granularity="second"` enables seconds** in both the `datetime-local` step attribute',
88
+ '(native mode) and the embedded time input.',
89
+ '',
90
+ '---',
91
+ '',
92
+ '### Accessibility',
93
+ '',
94
+ '- Always label via `<FormField>` or `aria-label` / `aria-labelledby`',
95
+ '- Pair `hasError` with `aria-invalid={true}` when used standalone — `<FormField>` handles both',
96
+ '- The calendar icon button has built-in screen reader text ("Open date and time picker")',
97
+ '- The embedded time input has `aria-label="Select time"`',
98
+ '- The calendar closes and returns focus to the date input on date selection or Escape',
99
+ '',
100
+ '---',
101
+ '',
102
+ '### TypeScript types',
103
+ '',
104
+ '```ts',
105
+ "import { DateTimePicker } from '@arbor-education/design-system.components';",
106
+ '',
107
+ '// Access via namespace:',
108
+ 'function MyField(props: DateTimePicker.Props) { ... }',
109
+ '',
110
+ '// Display format type:',
111
+ 'const format: DateTimePicker.DisplayFormat = "native"; // "native" | "default" | "friendly"',
112
+ '```',
113
+ '',
114
+ '| Type | Description |',
115
+ '|---|---|',
116
+ '| `DateTimePicker.Props` | Full props interface |',
117
+ '| `DateTimePicker.DisplayFormat` | `"native" \\| "default" \\| "friendly"` |',
118
+ ].join('\n');
119
+
120
+ const RELATED_COMPONENTS = [
121
+ '## Related components',
122
+ '',
123
+ '[DatePicker](?path=/docs/components-datepicker--docs) · [TimeInput](?path=/docs/components-formfield-inputs-timeinput--docs) · [FormField](?path=/docs/components-formfield--docs)',
124
+ ].join('\n');
125
+
126
+ const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Docs page component
130
+ // ---------------------------------------------------------------------------
131
+
132
+ function DateTimePickerDocsPage() {
133
+ return (
134
+ <>
135
+ <Title />
136
+ <Subtitle />
137
+ <Markdown>{DESCRIPTION_INTRO}</Markdown>
138
+ <DocHeading>Interactive example</DocHeading>
139
+ <Markdown>{PROPS_INTRO}</Markdown>
140
+ <DocPrimary />
141
+ <Controls />
142
+ <DocHeading>Usage guidance</DocHeading>
143
+ <Markdown>{USAGE_GUIDANCE}</Markdown>
144
+ <DocHeading>Developer notes</DocHeading>
145
+ <Markdown>{DEVELOPER_NOTES}</Markdown>
146
+ <DocHeading>Examples</DocHeading>
147
+ <Stories title="" />
148
+ <Markdown>{RELATED_COMPONENTS}</Markdown>
149
+ </>
150
+ );
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Meta
155
+ // ---------------------------------------------------------------------------
8
156
 
9
157
  const meta = {
10
158
  title: 'Components/DateTimePicker',
11
159
  component: DateTimePicker,
12
- decorators: [
13
- Story => (
14
- <div style={{ maxWidth: '280px', width: '100%' }}>
15
- <Story />
16
- </div>
17
- ),
18
- ],
160
+ tags: ['autodocs'],
19
161
  parameters: {
20
- layout: 'centered',
162
+ layout: 'padded',
21
163
  docs: {
22
- description: {
23
- component:
24
- '`DateTimePicker` combines a date field with the existing `TimeInput`, keeping a single combined `Date` value. `displayFormat="native"` (default) uses a `datetime-local` input with a decorative empty-state hint span (browsers do not reliably show placeholders on native date/time fields). `"default"` and `"friendly"` use a plain text input with locale-style formatting and a real HTML placeholder.',
25
- },
164
+ page: DateTimePickerDocsPage,
26
165
  },
27
166
  },
28
- tags: ['autodocs'],
29
- args: {
30
- onChange: fn(),
31
- },
32
167
  argTypes: {
33
- displayFormat: {
34
- control: 'inline-radio',
168
+ 'displayFormat': {
169
+ control: { type: 'inline-radio' },
35
170
  options: ['native', 'default', 'friendly'],
36
- description:
37
- '`native`: ISO `datetime-local` (default). `default` / `friendly`: plain text field using the same date patterns as `DatePicker` plus time.',
171
+ description: [
172
+ 'Controls input type and value format.',
173
+ '`"native"` (default): `datetime-local` input with `YYYY-MM-DDTHH:mm` hint.',
174
+ '`"default"`: text input with `DD/MM/YYYY HH:mm` format.',
175
+ '`"friendly"`: text input with `April 14th, 2026 14:27` format.',
176
+ ].join(' '),
177
+ table: {
178
+ type: { summary: '"native" | "default" | "friendly"' },
179
+ defaultValue: { summary: '"native"' },
180
+ },
38
181
  },
39
- granularity: {
40
- control: 'inline-radio',
182
+ 'granularity': {
183
+ control: { type: 'inline-radio' },
41
184
  options: ['minute', 'second'],
42
- description: 'Controls whether the native `datetime-local` input and embedded time input use minute or second precision.',
185
+ description: [
186
+ 'Controls time precision.',
187
+ '`"minute"` (default): HH:mm.',
188
+ '`"second"`: HH:mm:ss — sets `step={1}` on the datetime-local input and shows seconds in the embedded time input.',
189
+ ].join(' '),
190
+ table: {
191
+ type: { summary: '"minute" | "second"' },
192
+ defaultValue: { summary: '"minute"' },
193
+ },
194
+ },
195
+ 'placeholder': {
196
+ control: 'text',
197
+ description: [
198
+ 'Custom text for the empty-state hint overlay (native mode only).',
199
+ 'When set and the field is empty, the input becomes read-only — the calendar opens on click only, not Tab focus.',
200
+ ].join(' '),
201
+ table: {
202
+ type: { summary: 'string' },
203
+ defaultValue: { summary: 'undefined' },
204
+ },
205
+ },
206
+ 'onChange': {
207
+ control: false,
208
+ action: 'onChange',
209
+ description: [
210
+ 'Callback fired when the combined date+time changes.',
211
+ 'Receives a `Date` object, or `undefined` when cleared.',
212
+ 'Never receives a string.',
213
+ ].join(' '),
214
+ table: {
215
+ type: { summary: '(newDate?: Date) => void' },
216
+ },
217
+ },
218
+ 'hasError': {
219
+ control: 'boolean',
220
+ description: [
221
+ 'Applies error-state visual styling (red border).',
222
+ 'Does **not** set `aria-invalid` automatically when used standalone.',
223
+ 'Always pair with `aria-invalid={true}`. `<FormField>` handles both automatically.',
224
+ ].join(' '),
225
+ table: {
226
+ type: { summary: 'boolean' },
227
+ defaultValue: { summary: 'false' },
228
+ },
43
229
  },
44
- timeOptions: {
45
- control: 'object',
46
- description: 'When provided, the embedded time input switches to list-backed time selection.',
230
+ 'value': {
231
+ control: false,
232
+ description: [
233
+ 'Controlled combined date+time value.',
234
+ 'Must be a `Date` object — not a string.',
235
+ 'Pair with `onChange` to keep state in sync.',
236
+ ].join(' '),
237
+ table: {
238
+ type: { summary: 'Date' },
239
+ defaultValue: { summary: 'undefined' },
240
+ },
47
241
  },
48
- value: {
242
+ 'defaultValue': {
49
243
  control: false,
50
- description: 'Controlled combined datetime value for app-level state.',
244
+ description: [
245
+ 'Uncontrolled initial date+time.',
246
+ 'Must be a `Date` object (month is zero-indexed: `new Date(2026, 3, 14, 9, 0)` = 14 Apr 2026 09:00).',
247
+ 'Use when you only need the value on form submit.',
248
+ ].join(' '),
249
+ table: {
250
+ type: { summary: 'Date' },
251
+ defaultValue: { summary: 'undefined' },
252
+ },
51
253
  },
52
- defaultValue: {
254
+ 'timeOptions': {
53
255
  control: false,
54
- description: 'Uncontrolled initial combined datetime value.',
256
+ description: [
257
+ 'Array of `TimeValue` strings to use as preset time slots.',
258
+ 'When provided, the embedded time input switches to **combobox mode** — the user picks from the list instead of typing freely.',
259
+ 'Ideal for fixed appointment slots like "09:00", "09:30", "10:00".',
260
+ ].join(' '),
261
+ table: {
262
+ type: { summary: 'TimeValue[]' },
263
+ defaultValue: { summary: 'undefined' },
264
+ },
265
+ },
266
+ 'searchType': {
267
+ control: { type: 'select' },
268
+ options: ['prefix', 'substring'],
269
+ description: [
270
+ '**Only used when `timeOptions` is provided.**',
271
+ 'Controls how the time combobox filters options as the user types.',
272
+ '`"prefix"` (default) matches from the start; `"substring"` matches anywhere.',
273
+ ].join(' '),
274
+ table: {
275
+ type: { summary: '"prefix" | "substring"' },
276
+ defaultValue: { summary: '"prefix"' },
277
+ },
55
278
  },
56
- onChange: {
57
- action: 'changed',
58
- description: 'Called with the next combined `Date` value, or `undefined` when the date becomes invalid/cleared.',
279
+ 'highlightStringMatches': {
280
+ control: 'boolean',
281
+ description: '**Only used when `timeOptions` is provided.** Bolds matched characters in the time dropdown.',
282
+ table: {
283
+ type: { summary: 'boolean' },
284
+ defaultValue: { summary: 'false' },
285
+ },
59
286
  },
60
- placeholder: {
287
+ 'id': {
61
288
  control: 'text',
62
- description: 'Optional override for the field placeholder (defaults from `displayFormat` and `granularity`).',
289
+ description: 'HTML `id` for the date input element. Required when inside `<FormField>` so the label `htmlFor` can be linked.',
290
+ table: {
291
+ type: { summary: 'string' },
292
+ },
293
+ },
294
+ 'className': {
295
+ control: 'text',
296
+ description: 'Additional CSS class names on the root wrapper element.',
297
+ table: {
298
+ type: { summary: 'string' },
299
+ },
300
+ },
301
+ 'aria-invalid': {
302
+ control: 'boolean',
303
+ description: [
304
+ 'Marks the date input as invalid for screen readers.',
305
+ 'Must be set manually when used standalone — `<FormField>` sets this automatically when `errorText` is provided.',
306
+ ].join(' '),
307
+ table: {
308
+ type: { summary: 'boolean | "true" | "false"' },
309
+ },
310
+ },
311
+ 'aria-describedby': {
312
+ control: 'text',
313
+ description: 'ID of the element describing the field (e.g. an error message).',
314
+ table: {
315
+ type: { summary: 'string' },
316
+ },
63
317
  },
64
318
  },
65
319
  } satisfies Meta<typeof DateTimePicker>;
66
320
 
67
321
  export default meta;
322
+ type Story = StoryObj<typeof DateTimePicker>;
323
+
324
+ // ---------------------------------------------------------------------------
325
+ // Helper: attach a per-story description
326
+ // ---------------------------------------------------------------------------
327
+
328
+ const withDescription = (story: Story, description: string): Story => ({
329
+ ...story,
330
+ parameters: {
331
+ ...story.parameters,
332
+ docs: {
333
+ ...story.parameters?.docs,
334
+ description: {
335
+ story: description,
336
+ },
337
+ },
338
+ },
339
+ });
68
340
 
69
- type Story = StoryObj<typeof meta>;
341
+ // ---------------------------------------------------------------------------
342
+ // Named template components for stateful stories
343
+ // ---------------------------------------------------------------------------
70
344
 
71
- const ControlledDateTimePicker = ({
72
- showCurrentValue = false,
73
- ...args
74
- }: DateTimePickerProps & { showCurrentValue?: boolean }) => {
75
- const [value, setValue] = useState<Date | undefined>(args.value);
345
+ const APPOINTMENT_SLOTS: TimeValue[] = [
346
+ '09:00',
347
+ '09:30',
348
+ '10:00',
349
+ '10:30',
350
+ '11:00',
351
+ '11:30',
352
+ '14:00',
353
+ '14:30',
354
+ '15:00',
355
+ '15:30',
356
+ ];
76
357
 
77
- useEffect(() => {
78
- setValue(args.value);
79
- }, [args.value]);
358
+ const DisplayFormatDefaultTemplate = () => {
359
+ const [value, setValue] = useState<Date | undefined>(new Date(2026, 3, 14, 14, 27));
360
+ return (
361
+ <div style={{ maxWidth: '320px' }}>
362
+ <DateTimePicker
363
+ displayFormat="default"
364
+ value={value}
365
+ onChange={setValue}
366
+ id="format-default"
367
+ />
368
+ <p style={{ margin: 'var(--spacing-small) 0 0', color: 'var(--color-grey-600)' }}>
369
+ Value:
370
+ {' '}
371
+ <code>
372
+ {value
373
+ ? `${value.toLocaleDateString('en-GB')} ${value.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}`
374
+ : '(none)'}
375
+ </code>
376
+ </p>
377
+ </div>
378
+ );
379
+ };
380
+
381
+ const DisplayFormatFriendlyTemplate = () => {
382
+ const [value, setValue] = useState<Date | undefined>(new Date(2026, 3, 14, 14, 27));
383
+ return (
384
+ <div style={{ maxWidth: '320px' }}>
385
+ <DateTimePicker
386
+ displayFormat="friendly"
387
+ value={value}
388
+ onChange={setValue}
389
+ id="format-friendly"
390
+ />
391
+ <p style={{ margin: 'var(--spacing-small) 0 0', color: 'var(--color-grey-600)' }}>
392
+ Value:
393
+ {' '}
394
+ <code>
395
+ {value
396
+ ? `${value.toLocaleDateString('en-GB')} ${value.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}`
397
+ : '(none)'}
398
+ </code>
399
+ </p>
400
+ </div>
401
+ );
402
+ };
403
+
404
+ const WithTimeOptionsTemplate = () => {
405
+ const [value, setValue] = useState<Date | undefined>(new Date(2026, 3, 14, 9, 0));
406
+ return (
407
+ <div style={{ maxWidth: '280px' }}>
408
+ <DateTimePicker
409
+ timeOptions={APPOINTMENT_SLOTS}
410
+ value={value}
411
+ onChange={setValue}
412
+ id="time-options"
413
+ />
414
+ <p style={{ margin: 'var(--spacing-small) 0 0', color: 'var(--color-grey-600)' }}>
415
+ Appointment:
416
+ {' '}
417
+ <code>
418
+ {value
419
+ ? `${value.toLocaleDateString('en-GB')} ${value.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}`
420
+ : '(none)'}
421
+ </code>
422
+ </p>
423
+ </div>
424
+ );
425
+ };
80
426
 
427
+ const GranularitySecondTemplate = () => {
428
+ const [value, setValue] = useState<Date | undefined>(undefined);
81
429
  return (
82
- <div style={{ display: 'grid', gap: 12, width: '100%', maxWidth: 320 }}>
430
+ <div style={{ maxWidth: '280px' }}>
83
431
  <DateTimePicker
84
- {...args}
432
+ granularity="second"
85
433
  value={value}
86
- onChange={(nextValue) => {
87
- setValue(nextValue);
88
- args.onChange?.(nextValue);
434
+ onChange={setValue}
435
+ id="granularity-second"
436
+ />
437
+ <p style={{ margin: 'var(--spacing-small) 0 0', color: 'var(--color-grey-600)' }}>
438
+ Value (with seconds):
439
+ {' '}
440
+ <code>
441
+ {value
442
+ ? `${value.toLocaleDateString('en-GB')} ${value.toLocaleTimeString('en-GB')}`
443
+ : '(none)'}
444
+ </code>
445
+ </p>
446
+ </div>
447
+ );
448
+ };
449
+
450
+ const ControlledWithDisplayTemplate = () => {
451
+ const [value, setValue] = useState<Date | undefined>(undefined);
452
+ return (
453
+ <div style={{ maxWidth: '280px' }}>
454
+ <DateTimePicker
455
+ id="controlled-display"
456
+ onChange={setValue}
457
+ />
458
+ <p style={{ margin: 'var(--spacing-small) 0 0', color: 'var(--color-grey-600)' }}>
459
+ Selected:
460
+ {' '}
461
+ <code>
462
+ {value
463
+ ? `${value.toLocaleDateString('en-GB')} ${value.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}`
464
+ : '(none)'}
465
+ </code>
466
+ </p>
467
+ </div>
468
+ );
469
+ };
470
+
471
+ const WithFormFieldTemplate = () => (
472
+ <div style={{ maxWidth: '320px' }}>
473
+ <FormField
474
+ label="Event start date and time"
475
+ id="ff-event-start"
476
+ inputType="dateTimePicker"
477
+ fieldDescription="Select the date and time the event will begin."
478
+ inputProps={{}}
479
+ />
480
+ </div>
481
+ );
482
+
483
+ const WithFormFieldAndErrorTemplate = () => {
484
+ const [submitted, setSubmitted] = useState(false);
485
+ const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
486
+ const hasError = submitted && !selectedDate;
487
+
488
+ return (
489
+ <div style={{ maxWidth: '320px', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
490
+ <FormField
491
+ label="Lesson start date and time"
492
+ id="ff-lesson-start"
493
+ inputType="dateTimePicker"
494
+ errorText={hasError ? 'Please select a date and time for the lesson.' : undefined}
495
+ inputProps={{
496
+ 'onChange': setSelectedDate,
497
+ hasError,
498
+ 'aria-invalid': hasError ? true : undefined,
89
499
  }}
90
500
  />
91
- {showCurrentValue && (
92
- <span>
93
- Current value:
501
+ <div>
502
+ <button
503
+ type="button"
504
+ onClick={() => setSubmitted(true)}
505
+ style={{ padding: 'var(--spacing-small) var(--spacing-medium)' }}
506
+ >
507
+ Submit
508
+ </button>
509
+ </div>
510
+ {submitted && selectedDate && (
511
+ <p style={{ margin: 0, color: 'var(--color-semantic-success-600)', fontSize: '0.875rem' }}>
512
+ Lesson scheduled for
94
513
  {' '}
95
- {value ? formatNativeDateTimeInputValue(value, args.granularity) : 'None'}
96
- </span>
514
+ {selectedDate.toLocaleDateString('en-GB')}
515
+ {' at '}
516
+ {selectedDate.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}
517
+ .
518
+ </p>
97
519
  )}
98
520
  </div>
99
521
  );
100
522
  };
101
523
 
102
- export const NativeTime: Story = {
103
- args: {
104
- value: new Date(2026, 3, 14, 14, 27),
105
- granularity: 'minute',
106
- },
107
- render: args => <ControlledDateTimePicker {...args} />,
108
- };
524
+ // ---------------------------------------------------------------------------
525
+ // Stories
526
+ // ---------------------------------------------------------------------------
109
527
 
110
- export const TimeList: Story = {
111
- args: {
112
- value: new Date(2026, 3, 14, 10, 0),
113
- timeOptions: [...timeOptions],
528
+ export const Default: Story = withDescription(
529
+ {
530
+ args: {
531
+ id: 'default-datetime',
532
+ },
533
+ render: args => (
534
+ <div style={{ maxWidth: '280px' }}>
535
+ <DateTimePicker {...args} />
536
+ </div>
537
+ ),
114
538
  },
115
- render: args => <ControlledDateTimePicker {...args} />,
116
- };
539
+ [
540
+ 'The interactive canvas — every prop is wired to the Controls panel.',
541
+ 'Default `displayFormat="native"` shows the `YYYY-MM-DDTHH:mm` hint overlay (a decorative span,',
542
+ 'because browsers do not reliably show `placeholder` on `datetime-local` inputs).',
543
+ 'Click the calendar icon to open the combined date+time picker.',
544
+ ].join(' '),
545
+ );
117
546
 
118
- export const ControlledValuePreview: Story = {
119
- args: {
120
- value: new Date(2026, 3, 14, 14, 27),
121
- granularity: 'minute',
122
- },
123
- render: args => <ControlledDateTimePicker {...args} showCurrentValue />,
124
- };
547
+ export const WithDefaultValue: Story = withDescription(
548
+ {
549
+ render: () => (
550
+ <div style={{ maxWidth: '280px' }}>
551
+ {/* 14 April 2026, 14:27 — month is zero-indexed (Apr = 3) */}
552
+ <DateTimePicker
553
+ id="default-value-datetime"
554
+ defaultValue={new Date(2026, 3, 14, 14, 27)}
555
+ />
556
+ </div>
557
+ ),
558
+ parameters: {
559
+ controls: { disable: true },
560
+ docs: {
561
+ source: {
562
+ language: 'tsx',
563
+ code: `
564
+ import { DateTimePicker } from '@arbor-education/design-system.components';
565
+
566
+ function EditEventStart() {
567
+ // 14 April 2026, 14:27 — month is zero-indexed (Apr = 3)
568
+ const eventStart = new Date(2026, 3, 14, 14, 27);
125
569
 
126
- export const PlaceholderNativeMinuteHint: Story = {
127
- name: 'Placeholder · native minute hint',
128
- args: {
129
- granularity: 'minute',
570
+ return (
571
+ <DateTimePicker
572
+ id="event-start"
573
+ defaultValue={eventStart}
574
+ onChange={(date) => {
575
+ // date is Date | undefined — not a string
576
+ console.log('Updated to:', date?.toLocaleString('en-GB'));
577
+ }}
578
+ />
579
+ );
580
+ }
581
+ export default EditEventStart;
582
+ `.trim(),
583
+ },
584
+ },
585
+ },
130
586
  },
131
- parameters: {
132
- docs: {
133
- description: {
134
- story: 'Uncontrolled, no initial value — empty-state hint shows `YYYY-MM-DDTHH:mm` (decorative span for native mode).',
587
+ [
588
+ 'Use `defaultValue` to pre-populate an uncontrolled DateTimePicker — for example when editing',
589
+ 'an existing event. The prop must be a `Date` object (month is zero-indexed: April = 3).',
590
+ ].join(' '),
591
+ );
592
+
593
+ export const GranularitySecond: Story = withDescription(
594
+ {
595
+ render: () => <GranularitySecondTemplate />,
596
+ parameters: {
597
+ controls: { disable: true },
598
+ docs: {
599
+ source: {
600
+ language: 'tsx',
601
+ code: `
602
+ import { useState } from 'react';
603
+ import { DateTimePicker } from '@arbor-education/design-system.components';
604
+
605
+ function PreciseDateTimePickerExample() {
606
+ const [value, setValue] = useState<Date | undefined>(undefined);
607
+
608
+ return (
609
+ <DateTimePicker
610
+ granularity="second"
611
+ value={value}
612
+ onChange={setValue}
613
+ id="precise-datetime"
614
+ />
615
+ );
616
+ }
617
+ export default PreciseDateTimePickerExample;
618
+ `.trim(),
619
+ },
135
620
  },
136
621
  },
137
622
  },
138
- };
623
+ [
624
+ '`granularity="second"` enables seconds in both the `datetime-local` native input (sets `step={1}`)',
625
+ 'and the embedded time input inside the calendar popup.',
626
+ 'Use for precise event logging, stopwatch-style entry, or when sub-minute accuracy matters.',
627
+ ].join(' '),
628
+ );
629
+
630
+ export const DisplayFormatDefault: Story = withDescription(
631
+ {
632
+ render: () => <DisplayFormatDefaultTemplate />,
633
+ parameters: {
634
+ controls: { disable: true },
635
+ docs: {
636
+ source: {
637
+ language: 'tsx',
638
+ code: `
639
+ import { useState } from 'react';
640
+ import { DateTimePicker } from '@arbor-education/design-system.components';
139
641
 
140
- export const PlaceholderNativeSecondHint: Story = {
141
- name: 'Placeholder · native second hint',
142
- args: {
143
- granularity: 'second',
642
+ function FormattedDateTimePickerExample() {
643
+ const [value, setValue] = useState<Date | undefined>(new Date(2026, 3, 14, 14, 27));
644
+
645
+ return (
646
+ <DateTimePicker
647
+ displayFormat="default"
648
+ value={value}
649
+ onChange={setValue}
650
+ id="event-datetime"
651
+ />
652
+ );
653
+ }
654
+ export default FormattedDateTimePickerExample;
655
+ `.trim(),
656
+ },
657
+ },
658
+ },
144
659
  },
145
- parameters: {
146
- docs: {
147
- description: {
148
- story: 'Same as the minute story, but with `granularity="second"` so the hint includes seconds.',
660
+ [
661
+ '`displayFormat="default"` uses a plain text input showing the value as `DD/MM/YYYY HH:mm`',
662
+ '(e.g. `14/04/2026 14:27`). The appearance is consistent across all browsers — unlike `"native"`,',
663
+ 'which uses the OS date/time widget and varies by platform.',
664
+ ].join(' '),
665
+ );
666
+
667
+ export const DisplayFormatFriendly: Story = withDescription(
668
+ {
669
+ render: () => <DisplayFormatFriendlyTemplate />,
670
+ parameters: {
671
+ controls: { disable: true },
672
+ docs: {
673
+ source: {
674
+ language: 'tsx',
675
+ code: `
676
+ import { useState } from 'react';
677
+ import { DateTimePicker } from '@arbor-education/design-system.components';
678
+
679
+ function FriendlyDateTimePickerExample() {
680
+ const [value, setValue] = useState<Date | undefined>(new Date(2026, 3, 14, 14, 27));
681
+
682
+ return (
683
+ <DateTimePicker
684
+ displayFormat="friendly"
685
+ value={value}
686
+ onChange={setValue}
687
+ id="event-datetime"
688
+ />
689
+ );
690
+ }
691
+ export default FriendlyDateTimePickerExample;
692
+ `.trim(),
693
+ },
149
694
  },
150
695
  },
151
696
  },
152
- };
697
+ [
698
+ '`displayFormat="friendly"` shows the value as `April 14th, 2026 14:27` — human-readable and',
699
+ 'locale-style. Best for consumer-facing or less technical contexts where ISO-adjacent formats feel unfriendly.',
700
+ ].join(' '),
701
+ );
702
+
703
+ export const WithTimeOptions: Story = withDescription(
704
+ {
705
+ render: () => <WithTimeOptionsTemplate />,
706
+ parameters: {
707
+ controls: { disable: true },
708
+ docs: {
709
+ source: {
710
+ language: 'tsx',
711
+ code: `
712
+ import { useState } from 'react';
713
+ import { DateTimePicker, type TimeValue } from '@arbor-education/design-system.components';
153
714
 
154
- export const DisplayFormatDefault: Story = {
155
- name: 'Display format · default (text)',
156
- args: {
157
- displayFormat: 'default',
158
- value: new Date(2026, 3, 14, 14, 27),
159
- granularity: 'minute',
715
+ const APPOINTMENT_SLOTS: TimeValue[] = [
716
+ '09:00', '09:30', '10:00', '10:30', '11:00', '11:30',
717
+ '14:00', '14:30', '15:00', '15:30',
718
+ ];
719
+
720
+ function AppointmentBookingExample() {
721
+ const [value, setValue] = useState<Date | undefined>(new Date(2026, 3, 14, 9, 0));
722
+
723
+ return (
724
+ <DateTimePicker
725
+ timeOptions={APPOINTMENT_SLOTS}
726
+ value={value}
727
+ onChange={setValue}
728
+ id="appointment-datetime"
729
+ />
730
+ );
731
+ }
732
+ export default AppointmentBookingExample;
733
+ `.trim(),
734
+ },
735
+ },
736
+ },
160
737
  },
161
- render: args => <ControlledDateTimePicker {...args} />,
162
- parameters: {
163
- docs: {
164
- description: {
165
- story: 'Combined value is shown and edited as **15/04/2026 14:27** instead of the native ISO string.',
738
+ [
739
+ '`timeOptions` switches the embedded time input from free-form entry to a combobox list.',
740
+ 'Ideal for appointment booking where only specific slots are valid — the user picks a date',
741
+ 'from the calendar, then picks a slot from the dropdown. Both stay in a single `Date` value.',
742
+ ].join(' '),
743
+ );
744
+
745
+ export const WithPlaceholder: Story = withDescription(
746
+ {
747
+ render: () => (
748
+ <div style={{ maxWidth: '280px' }}>
749
+ <DateTimePicker
750
+ id="placeholder-datetime"
751
+ placeholder="Choose event start"
752
+ />
753
+ </div>
754
+ ),
755
+ parameters: {
756
+ controls: { disable: true },
757
+ docs: {
758
+ source: {
759
+ language: 'tsx',
760
+ code: `
761
+ import { DateTimePicker } from '@arbor-education/design-system.components';
762
+
763
+ function GuidedDateTimePickerExample() {
764
+ return (
765
+ <DateTimePicker
766
+ id="event-start"
767
+ placeholder="Choose event start"
768
+ onChange={(date) => {
769
+ console.log('Event start:', date?.toLocaleString('en-GB'));
770
+ }}
771
+ />
772
+ );
773
+ }
774
+ export default GuidedDateTimePickerExample;
775
+ `.trim(),
776
+ },
166
777
  },
167
778
  },
168
779
  },
169
- };
780
+ [
781
+ '`placeholder` (native mode only) replaces the `YYYY-MM-DDTHH:mm` hint with custom copy.',
782
+ 'When set and the field is empty, the input becomes **read-only** and the calendar opens on',
783
+ '**click only** — Tab focus does not trigger it. Prevents accidental popup during keyboard navigation.',
784
+ ].join(' '),
785
+ );
786
+
787
+ export const ErrorState: Story = withDescription(
788
+ {
789
+ render: () => (
790
+ <div style={{ maxWidth: '280px' }}>
791
+ <DateTimePicker
792
+ id="error-state-datetime"
793
+ hasError
794
+ aria-invalid={true}
795
+ aria-describedby="error-state-datetime-msg"
796
+ />
797
+ <p
798
+ id="error-state-datetime-msg"
799
+ role="alert"
800
+ style={{ margin: 'var(--spacing-xsmall) 0 0', color: 'var(--color-semantic-destructive-600)', fontSize: '0.875rem' }}
801
+ >
802
+ Please select a valid date and time.
803
+ </p>
804
+ </div>
805
+ ),
806
+ parameters: {
807
+ controls: { disable: true },
808
+ docs: {
809
+ source: {
810
+ language: 'tsx',
811
+ code: `
812
+ import { DateTimePicker } from '@arbor-education/design-system.components';
170
813
 
171
- export const DisplayFormatFriendly: Story = {
172
- name: 'Display format · friendly (text)',
173
- args: {
174
- displayFormat: 'friendly',
175
- value: new Date(2026, 3, 14, 14, 27),
176
- granularity: 'minute',
814
+ // Standalone set both hasError (visual) and aria-invalid (screen reader) yourself.
815
+ // Inside <FormField> both are handled automatically when errorText is set.
816
+ function DateTimePickerWithError() {
817
+ return (
818
+ <>
819
+ <DateTimePicker
820
+ id="event-start"
821
+ hasError
822
+ aria-invalid={true}
823
+ aria-describedby="event-start-error"
824
+ />
825
+ <p id="event-start-error" role="alert">
826
+ Please select a valid date and time.
827
+ </p>
828
+ </>
829
+ );
830
+ }
831
+ export default DateTimePickerWithError;
832
+ `.trim(),
833
+ },
834
+ },
835
+ },
177
836
  },
178
- render: args => <ControlledDateTimePicker {...args} />,
179
- parameters: {
180
- docs: {
181
- description: {
182
- story: 'Combined value uses friendly wording, e.g. **April 14th, 2026 14:27**.',
837
+ [
838
+ 'The error state requires **both** `hasError` (visual red border) and `aria-invalid={true}`',
839
+ '(screen-reader signal). Use `aria-describedby` to link the input to the error message.',
840
+ 'When using `<FormField>`, set `errorText` and the component handles all three automatically.',
841
+ ].join(' '),
842
+ );
843
+
844
+ export const ControlledWithDisplay: Story = withDescription(
845
+ {
846
+ render: () => <ControlledWithDisplayTemplate />,
847
+ parameters: {
848
+ controls: { disable: true },
849
+ docs: {
850
+ source: {
851
+ language: 'tsx',
852
+ code: `
853
+ import { useState } from 'react';
854
+ import { DateTimePicker } from '@arbor-education/design-system.components';
855
+
856
+ function ControlledDateTimePickerExample() {
857
+ const [value, setValue] = useState<Date | undefined>(undefined);
858
+
859
+ return (
860
+ <div>
861
+ <DateTimePicker
862
+ id="event-datetime"
863
+ onChange={setValue}
864
+ />
865
+ <p>
866
+ Selected:{' '}
867
+ <code>
868
+ {value
869
+ ? \`\${value.toLocaleDateString('en-GB')} \${value.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}\`
870
+ : '(none)'}
871
+ </code>
872
+ </p>
873
+ </div>
874
+ );
875
+ }
876
+ export default ControlledDateTimePickerExample;
877
+ `.trim(),
878
+ },
183
879
  },
184
880
  },
185
881
  },
186
- };
882
+ [
883
+ '`onChange` receives a `Date` object (or `undefined`) — never a string.',
884
+ 'This controlled example stores the combined date+time in state and renders it below the field.',
885
+ 'Use `toLocaleDateString` + `toLocaleTimeString` (or `date-fns format`) for display.',
886
+ ].join(' '),
887
+ );
187
888
 
188
- export const PlaceholderCustomOverride: Story = {
189
- name: 'Placeholder · custom copy',
190
- args: {
191
- granularity: 'minute',
192
- placeholder: 'Event start (local)',
889
+ export const WithFormField: Story = withDescription(
890
+ {
891
+ render: () => <WithFormFieldTemplate />,
892
+ parameters: {
893
+ controls: { disable: true },
894
+ docs: {
895
+ source: {
896
+ language: 'tsx',
897
+ code: `
898
+ import { FormField } from '@arbor-education/design-system.components';
899
+
900
+ function EventStartField() {
901
+ return (
902
+ <FormField
903
+ label="Event start date and time"
904
+ id="event-start"
905
+ inputType="dateTimePicker"
906
+ fieldDescription="Select the date and time the event will begin."
907
+ inputProps={{
908
+ onChange: (date) => {
909
+ // date is Date | undefined
910
+ console.log('Event start:', date?.toLocaleString('en-GB'));
911
+ },
912
+ }}
913
+ />
914
+ );
915
+ }
916
+ export default EventStartField;
917
+ `.trim(),
918
+ },
919
+ },
920
+ },
193
921
  },
194
- parameters: {
195
- docs: {
196
- description: {
197
- story: 'Pass `placeholder` to replace the default native empty-state hint.',
922
+ [
923
+ 'The recommended form usage: `<FormField inputType="dateTimePicker">` provides the accessible',
924
+ 'label, description, and error layout. DateTimePicker props go in `inputProps`.',
925
+ 'The `label`, `fieldDescription`, and `errorText` props belong on `<FormField>` itself.',
926
+ ].join(' '),
927
+ );
928
+
929
+ export const WithFormFieldAndError: Story = withDescription(
930
+ {
931
+ render: () => <WithFormFieldAndErrorTemplate />,
932
+ parameters: {
933
+ controls: { disable: true },
934
+ docs: {
935
+ source: {
936
+ language: 'tsx',
937
+ code: `
938
+ import { useState } from 'react';
939
+ import { FormField } from '@arbor-education/design-system.components';
940
+
941
+ function LessonSchedulerForm() {
942
+ const [submitted, setSubmitted] = useState(false);
943
+ const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
944
+ const hasError = submitted && !selectedDate;
945
+
946
+ return (
947
+ <div>
948
+ <FormField
949
+ label="Lesson start date and time"
950
+ id="lesson-start"
951
+ inputType="dateTimePicker"
952
+ errorText={
953
+ hasError
954
+ ? 'Please select a date and time for the lesson.'
955
+ : undefined
956
+ }
957
+ inputProps={{
958
+ onChange: setSelectedDate,
959
+ hasError,
960
+ 'aria-invalid': hasError ? true : undefined,
961
+ }}
962
+ />
963
+ <button type="button" onClick={() => setSubmitted(true)}>
964
+ Submit
965
+ </button>
966
+ </div>
967
+ );
968
+ }
969
+ export default LessonSchedulerForm;
970
+ `.trim(),
971
+ },
198
972
  },
199
973
  },
200
974
  },
201
- render: args => <ControlledDateTimePicker {...args} />,
202
- };
975
+ [
976
+ 'A submit-gated form: clicking Submit without a selection triggers the full error pattern.',
977
+ '`errorText` on `<FormField>` renders the message in the correct accessible layout,',
978
+ 'linked via `aria-describedby` automatically. Once a date+time is chosen, the error clears.',
979
+ ].join(' '),
980
+ );