@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,55 +1,341 @@
1
+ import { useState } from 'react';
1
2
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { Button } from 'Components/button/Button';
3
- import { useEffect, useState } from 'react';
4
- import { fn } from 'storybook/test';
5
- import { TimeInput, type TimeInputProps, type TimeValue } from './TimeInput';
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 { TimeInput, type TimeValue } from './TimeInput';
6
14
 
7
- const timeOptions = ['09:00', '09:30', '10:00', '10:30', '11:00'] as const;
15
+ // ---------------------------------------------------------------------------
16
+ // Docs page content
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const DESCRIPTION_INTRO = [
20
+ 'TimeInput is a flexible time-entry field that operates in **two distinct modes** depending on',
21
+ 'whether the `options` prop is supplied.',
22
+ '',
23
+ '**Native mode** (no `options`) renders a browser-native `<input type="time">` decorated with a',
24
+ 'clock icon. The browser handles the picker UI, spin-button keyboard navigation, and locale-aware',
25
+ 'display. This mode supports `min`, `max`, `granularity`, `className`, a forwarded `ref`, and the',
26
+ 'native `onChange` event.',
27
+ '',
28
+ '**Combobox mode** (`options` provided) replaces the native input with a searchable Combobox.',
29
+ 'The user can type to filter or click to select from a predefined list of time values. This mode',
30
+ 'supports `searchType` and `highlightStringMatches`, but ignores `min`, `max`, `granularity`,',
31
+ '`className`, `ref`, and `onChange`.',
32
+ '',
33
+ 'Both modes share: `value`, `defaultValue`, `onValueChange`, `hasError`, `disabled`, `id`, `name`,',
34
+ '`placeholder`, and ARIA attributes.',
35
+ ].join('\n');
36
+
37
+ const USAGE_GUIDANCE = [
38
+ '### When to use',
39
+ '',
40
+ '- **Any valid time** — use native mode (the default). The browser enforces valid time entry and',
41
+ ' supports `min`/`max` bounds for school hours (e.g. `min="08:00" max="17:00"`).',
42
+ '- **Fixed set of allowed times** — use combobox mode with `options`. Ideal for lesson periods,',
43
+ ' appointment slots, or any scenario where only specific times are valid.',
44
+ '- **Inside a form** — always wrap in `<FormField inputType="time">` so label, error text, and',
45
+ ' ARIA attributes are wired up automatically.',
46
+ '',
47
+ '---',
48
+ '',
49
+ '### When NOT to use',
50
+ '',
51
+ '| Situation | Use instead |',
52
+ '|---|---|',
53
+ '| Date + time combined | Separate `DateInput` and `TimeInput` fields |',
54
+ '| Free-form duration entry | `TextInput` with format hint |',
55
+ '| More than ~50 time options | Native mode with `min`/`max` bounds |',
56
+ '| Status or category selection | `SelectDropdown` with meaningful labels |',
57
+ ].join('\n');
58
+
59
+ const DEVELOPER_NOTES = [
60
+ '### Critical usage patterns',
61
+ '',
62
+ '**Prop availability varies by mode.** Props that are native-only are silently ignored in combobox',
63
+ 'mode, and vice versa:',
64
+ '',
65
+ '| Prop | Native mode | Combobox mode |',
66
+ '|---|---|---|',
67
+ '| `granularity` | Sets `step={1}` for second-level precision | **Ignored** |',
68
+ '| `min` / `max` | Restricts picker range | **Ignored** — filter `options` array instead |',
69
+ '| `className` | Applied to the wrapper `<div>` | **Ignored** |',
70
+ '| `ref` (forwarded) | Attached to the `<input>` element | **Ignored** |',
71
+ '| `onChange` | Native `ChangeEvent<HTMLInputElement>` | **Ignored** — use `onValueChange` |',
72
+ '| `searchType` | **Ignored** | Controls `prefix`/`substring` matching |',
73
+ '| `highlightStringMatches` | **Ignored** | Bolds matched characters in dropdown |',
74
+ '| `onValueChange` | Fires with the string value | Fires with the string value |',
75
+ '',
76
+ '**`aria-invalid` is NOT automatic.** When using TimeInput standalone (outside FormField),',
77
+ '`hasError` only applies visual error styling. It does NOT set `aria-invalid`:',
78
+ '',
79
+ '```tsx',
80
+ '// Standalone — set aria-invalid yourself:',
81
+ '<TimeInput hasError aria-invalid={true} aria-label="Start time" />',
82
+ '',
83
+ '// Inside FormField — aria-invalid is set automatically when errorText is present:',
84
+ '<FormField label="Start time" id="start" inputType="time" errorText="Required." inputProps={{}} />',
85
+ '```',
86
+ '',
87
+ '**`onValueChange` is the preferred callback** — it works in both modes and receives the plain',
88
+ 'string value (`HH:MM` or `HH:MM:SS`). The native `onChange` event is only available in native',
89
+ 'mode and is not forwarded in combobox mode.',
90
+ '',
91
+ '**`TimeValue` is format-shaped, not strictly validated.** The TypeScript type',
92
+ '`` `${string}:${string}` `` accepts `"99:99"` without complaint. The browser rejects it at render',
93
+ 'time for `<input type="time">`, but the component itself does no validation.',
94
+ '',
95
+ '---',
96
+ '',
97
+ '### Accessibility',
98
+ '',
99
+ '- Always provide a visible label via `<FormField>` or `aria-label` / `aria-labelledby`.',
100
+ '- In native mode, the browser\'s built-in time picker is fully keyboard-navigable (arrow keys',
101
+ ' cycle through the hour/minute/second segments; AM/PM toggles with A/P).',
102
+ '- In combobox mode, the Combobox component implements the ARIA combobox pattern',
103
+ ' (`role="combobox"`, `aria-expanded`, `aria-activedescendant`).',
104
+ '- The clock icon is decorative and carries `aria-hidden="true"`.',
105
+ '- Pair `hasError` with `aria-invalid={true}` when used standalone — one is visual, the other',
106
+ ' is for screen readers. `<FormField>` handles this automatically when `errorText` is set.',
107
+ '',
108
+ '---',
109
+ '',
110
+ '### TypeScript types',
111
+ '',
112
+ '```ts',
113
+ "import { TimeInput, type TimeValue, type TimeGranularity } from '@arbor-education/design-system.components';",
114
+ '',
115
+ '// TimeValue: HH:MM or HH:MM:SS (format-shaped, not strictly validated)',
116
+ 'type TimeValue = `${string}:${string}` | `${string}:${string}:${string}`;',
117
+ "type TimeGranularity = 'minute' | 'second';",
118
+ '',
119
+ '// Access types via the namespace:',
120
+ 'function MyField(props: TimeInput.Props) { ... }',
121
+ '```',
122
+ ].join('\n');
123
+
124
+ const RELATED_COMPONENTS = [
125
+ '## Related components',
126
+ '',
127
+ '[FormField](?path=/docs/components-formfield--docs) · [DateInput](?path=/docs/components-formfield-inputs-dateinput--docs) · [Combobox](?path=/docs/components-combobox--docs)',
128
+ ].join('\n');
129
+
130
+ const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Docs page component
134
+ // ---------------------------------------------------------------------------
135
+
136
+ function TimeInputDocsPage() {
137
+ return (
138
+ <>
139
+ <Title />
140
+ <Subtitle />
141
+ <Markdown>{DESCRIPTION_INTRO}</Markdown>
142
+ <DocHeading>Interactive example</DocHeading>
143
+ <Markdown>{PROPS_INTRO}</Markdown>
144
+ <DocPrimary />
145
+ <Controls />
146
+ <DocHeading>Usage guidance</DocHeading>
147
+ <Markdown>{USAGE_GUIDANCE}</Markdown>
148
+ <DocHeading>Developer notes</DocHeading>
149
+ <Markdown>{DEVELOPER_NOTES}</Markdown>
150
+ <DocHeading>Examples</DocHeading>
151
+ <Stories title="" />
152
+ <Markdown>{RELATED_COMPONENTS}</Markdown>
153
+ </>
154
+ );
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Meta
159
+ // ---------------------------------------------------------------------------
8
160
 
9
161
  const meta = {
10
162
  title: 'Components/FormField/Inputs/TimeInput',
11
163
  component: TimeInput,
164
+ tags: ['autodocs'],
12
165
  parameters: {
13
- layout: 'centered',
166
+ layout: 'padded',
14
167
  docs: {
15
- description: {
16
- component:
17
- '`TimeInput` supports both native time entry and a combobox-backed time list. When you pass `value`, treat it as a controlled component and update it in `onValueChange`; use `defaultValue` for uncontrolled usage.',
18
- },
168
+ page: TimeInputDocsPage,
19
169
  },
20
170
  },
21
- tags: ['autodocs'],
22
- args: {
23
- onValueChange: fn(),
24
- },
25
171
  argTypes: {
26
- granularity: {
27
- control: 'inline-radio',
28
- options: ['minute', 'second'],
29
- description: 'Controls whether the native time input works in minute or second increments.',
172
+ 'value': {
173
+ description: [
174
+ 'Controlled value in `HH:MM` or `HH:MM:SS` format.',
175
+ 'Pair with `onValueChange` to keep state in sync.',
176
+ 'If `undefined`, the component manages its own state (uncontrolled).',
177
+ ].join(' '),
178
+ control: 'text',
179
+ table: {
180
+ type: { summary: "TimeValue | ''" },
181
+ defaultValue: { summary: 'undefined' },
182
+ },
183
+ },
184
+ 'defaultValue': {
185
+ description: [
186
+ 'Uncontrolled initial value in `HH:MM` or `HH:MM:SS` format.',
187
+ 'Use when you only need the value on form submit, not on every change.',
188
+ ].join(' '),
189
+ control: 'text',
190
+ table: {
191
+ type: { summary: "TimeValue | ''" },
192
+ defaultValue: { summary: "''" },
193
+ },
30
194
  },
31
- options: {
195
+ 'onValueChange': {
196
+ description: [
197
+ 'Callback fired with the plain string value on every change.',
198
+ 'Works in **both** native and combobox modes.',
199
+ 'Prefer this over the native `onChange` event for cross-mode compatibility.',
200
+ ].join(' '),
201
+ action: 'onValueChange',
202
+ control: false,
203
+ table: {
204
+ type: { summary: '(value: string) => void' },
205
+ },
206
+ },
207
+ 'options': {
208
+ description: [
209
+ 'Array of `TimeValue` strings to display as selectable options.',
210
+ 'Providing this prop switches to **combobox mode** — the native `<input type="time">` is',
211
+ 'replaced by a searchable Combobox. Omit to use the browser-native time picker.',
212
+ ].join(' '),
32
213
  control: 'object',
33
- description: 'When provided, `TimeInput` switches from native `input[type="time"]` to the combobox-backed time list mode.',
214
+ table: {
215
+ type: { summary: 'TimeValue[]' },
216
+ defaultValue: { summary: 'undefined' },
217
+ },
218
+ },
219
+ 'granularity': {
220
+ description: [
221
+ '**Native mode only.** Controls the precision of the time input.',
222
+ '`"second"` sets `step={1}` so the seconds segment is visible.',
223
+ 'Silently ignored when `options` is provided.',
224
+ ].join(' '),
225
+ control: { type: 'select' },
226
+ options: ['minute', 'second'],
227
+ table: {
228
+ type: { summary: "'minute' | 'second'" },
229
+ defaultValue: { summary: "'minute'" },
230
+ },
231
+ },
232
+ 'searchType': {
233
+ description: [
234
+ '**Combobox mode only.** Controls how options are filtered as the user types.',
235
+ '`"prefix"` (default) matches from the start of the string;',
236
+ '`"substring"` matches anywhere in the string.',
237
+ 'Silently ignored in native mode.',
238
+ ].join(' '),
239
+ control: { type: 'select' },
240
+ options: ['prefix', 'substring'],
241
+ table: {
242
+ type: { summary: "'prefix' | 'substring'" },
243
+ defaultValue: { summary: "'prefix'" },
244
+ },
245
+ },
246
+ 'highlightStringMatches': {
247
+ description: [
248
+ '**Combobox mode only.** When `true`, matched characters in dropdown options are bolded.',
249
+ 'Silently ignored in native mode.',
250
+ ].join(' '),
251
+ control: 'boolean',
252
+ table: {
253
+ type: { summary: 'boolean' },
254
+ defaultValue: { summary: 'false' },
255
+ },
256
+ },
257
+ 'hasError': {
258
+ description: [
259
+ 'Applies error-state visual styling (red border).',
260
+ 'Does **not** set `aria-invalid` automatically when used standalone outside FormField.',
261
+ 'Always pair with `aria-invalid={true}` for screen-reader coverage.',
262
+ ].join(' '),
263
+ control: 'boolean',
264
+ table: {
265
+ type: { summary: 'boolean' },
266
+ defaultValue: { summary: 'false' },
267
+ },
34
268
  },
35
- value: {
269
+ 'disabled': {
270
+ description: 'Disables the input. The current value remains visible.',
271
+ control: 'boolean',
272
+ table: {
273
+ type: { summary: 'boolean' },
274
+ defaultValue: { summary: 'false' },
275
+ },
276
+ },
277
+ 'id': {
278
+ description: [
279
+ 'HTML `id` for the input element.',
280
+ 'Required when inside FormField so the `<label>` `htmlFor` can be linked.',
281
+ ].join(' '),
36
282
  control: 'text',
37
- description: 'Controlled value. If you provide this, update it from `onValueChange` in your app or story state.',
283
+ table: {
284
+ type: { summary: 'string' },
285
+ },
38
286
  },
39
- defaultValue: {
287
+ 'name': {
288
+ description: 'HTML `name` attribute for native form submission.',
40
289
  control: 'text',
41
- description: 'Uncontrolled initial value. Use this when you want the component to manage its own state.',
290
+ table: {
291
+ type: { summary: 'string' },
292
+ },
42
293
  },
43
- onValueChange: {
44
- action: 'value changed',
45
- description: 'Called with the next string value (`HH:MM` or `HH:MM:SS`).',
294
+ 'placeholder': {
295
+ description: 'Placeholder text shown when no value is selected. Most useful in combobox mode.',
296
+ control: 'text',
297
+ table: {
298
+ type: { summary: 'string' },
299
+ },
300
+ },
301
+ 'aria-label': {
302
+ description: [
303
+ 'Accessible label for the input.',
304
+ 'Required when the component is used standalone without a visible `<label>`.',
305
+ ].join(' '),
306
+ control: 'text',
307
+ table: {
308
+ type: { summary: 'string' },
309
+ },
310
+ },
311
+ 'aria-invalid': {
312
+ description: [
313
+ 'Marks the input as invalid for screen readers.',
314
+ 'Must be set manually when used standalone — FormField sets this automatically',
315
+ 'when `errorText` is provided.',
316
+ ].join(' '),
317
+ control: 'boolean',
318
+ table: {
319
+ type: { summary: 'boolean' },
320
+ defaultValue: { summary: 'false' },
321
+ },
322
+ },
323
+ 'aria-describedby': {
324
+ description: 'ID of the element that describes the field (e.g. an error or hint message).',
325
+ control: 'text',
326
+ table: {
327
+ type: { summary: 'string' },
328
+ },
46
329
  },
47
330
  },
48
331
  } satisfies Meta<typeof TimeInput>;
49
332
 
50
333
  export default meta;
334
+ type Story = StoryObj<typeof TimeInput>;
51
335
 
52
- type Story = StoryObj<typeof meta>;
336
+ // ---------------------------------------------------------------------------
337
+ // Helper: attach a per-story description
338
+ // ---------------------------------------------------------------------------
53
339
 
54
340
  const withDescription = (story: Story, description: string): Story => ({
55
341
  ...story,
@@ -64,107 +350,857 @@ const withDescription = (story: Story, description: string): Story => ({
64
350
  },
65
351
  });
66
352
 
67
- const ControlledTimeInput = (args: TimeInputProps) => {
68
- const [value, setValue] = useState<TimeValue | ''>(args.value ?? args.defaultValue ?? '');
353
+ // ---------------------------------------------------------------------------
354
+ // Lesson period options (shared across combobox stories)
355
+ // ---------------------------------------------------------------------------
356
+
357
+ const LESSON_PERIOD_OPTIONS: TimeValue[] = [
358
+ '09:00',
359
+ '09:45',
360
+ '10:30',
361
+ '11:15',
362
+ '13:00',
363
+ '13:45',
364
+ '14:30',
365
+ '15:15',
366
+ '16:00',
367
+ ];
69
368
 
70
- useEffect(() => {
71
- setValue(args.value ?? args.defaultValue ?? '');
72
- }, [args.defaultValue, args.value]);
369
+ // ---------------------------------------------------------------------------
370
+ // Named template components for stateful stories
371
+ // ---------------------------------------------------------------------------
73
372
 
373
+ const ControlledTemplate = () => {
374
+ const [value, setValue] = useState<TimeValue | ''>('09:30');
74
375
  return (
75
- <TimeInput
76
- {...args}
77
- value={value}
78
- onValueChange={(nextValue) => {
79
- setValue(nextValue as TimeValue | '');
80
- args.onValueChange?.(nextValue);
81
- }}
82
- />
376
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)', maxWidth: '280px' }}>
377
+ <TimeInput
378
+ value={value}
379
+ onValueChange={v => setValue(v as TimeValue | '')}
380
+ id="controlled-time"
381
+ name="start-time"
382
+ />
383
+ <p style={{ margin: 0, color: 'var(--color-grey-600)' }}>
384
+ Selected:
385
+ {' '}
386
+ <code>{value || '(none)'}</code>
387
+ </p>
388
+ </div>
83
389
  );
84
390
  };
85
391
 
86
- const NativeValidationDemo = (args: TimeInputProps) => {
87
- const [value, setValue] = useState<TimeValue | ''>(args.value ?? '');
392
+ const GranularitySecondTemplate = () => {
393
+ const [value, setValue] = useState<TimeValue | ''>('');
394
+ return (
395
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)', maxWidth: '280px' }}>
396
+ <TimeInput
397
+ granularity="second"
398
+ value={value}
399
+ onValueChange={v => setValue(v as TimeValue | '')}
400
+ id="second-granularity"
401
+ name="precise-time"
402
+ />
403
+ <p style={{ margin: 0, color: 'var(--color-grey-600)' }}>
404
+ Selected:
405
+ {' '}
406
+ <code>{value || '(none)'}</code>
407
+ </p>
408
+ </div>
409
+ );
410
+ };
88
411
 
89
- useEffect(() => {
90
- setValue(args.value ?? '');
91
- }, [args.value]);
412
+ const WithMinMaxBoundsTemplate = () => {
413
+ const [value, setValue] = useState<TimeValue | ''>('');
414
+ return (
415
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)', maxWidth: '280px' }}>
416
+ <TimeInput
417
+ min="08:00"
418
+ max="17:00"
419
+ value={value}
420
+ onValueChange={v => setValue(v as TimeValue | '')}
421
+ id="bounded-time"
422
+ name="school-time"
423
+ aria-label="School hours time"
424
+ />
425
+ <p style={{ margin: 0, color: 'var(--color-grey-600)' }}>
426
+ Restricted to school hours: 08:00 – 17:00
427
+ </p>
428
+ </div>
429
+ );
430
+ };
92
431
 
432
+ const WithOptionsTemplate = () => {
433
+ const [value, setValue] = useState<TimeValue | ''>('');
93
434
  return (
94
- <form
95
- style={{ display: 'grid', gap: 12, width: 280 }}
96
- onSubmit={(event) => {
97
- event.preventDefault();
98
- const form = event.currentTarget;
99
- if (!form.reportValidity()) {
100
- return;
101
- }
102
- }}
103
- >
435
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)', maxWidth: '280px' }}>
104
436
  <TimeInput
105
- {...args}
437
+ options={LESSON_PERIOD_OPTIONS}
106
438
  value={value}
107
- onValueChange={(nextValue) => {
108
- setValue(nextValue as TimeValue | '');
109
- args.onValueChange?.(nextValue);
439
+ onValueChange={v => setValue(v as TimeValue | '')}
440
+ id="lesson-period"
441
+ name="lesson-start"
442
+ placeholder="Select lesson period..."
443
+ aria-label="Lesson period"
444
+ />
445
+ <p style={{ margin: 0, color: 'var(--color-grey-600)' }}>
446
+ Selected:
447
+ {' '}
448
+ <code>{value || '(none)'}</code>
449
+ </p>
450
+ </div>
451
+ );
452
+ };
453
+
454
+ const SubstringSearchTemplate = () => {
455
+ const [value, setValue] = useState<TimeValue | ''>('');
456
+ return (
457
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)', maxWidth: '280px' }}>
458
+ <TimeInput
459
+ options={LESSON_PERIOD_OPTIONS}
460
+ searchType="substring"
461
+ value={value}
462
+ onValueChange={v => setValue(v as TimeValue | '')}
463
+ id="substring-time"
464
+ name="lesson-substring"
465
+ placeholder="Type to search..."
466
+ aria-label="Lesson period"
467
+ />
468
+ <p style={{ margin: 0, color: 'var(--color-grey-600)' }}>
469
+ Try typing
470
+ {' '}
471
+ <code>45</code>
472
+ {' '}
473
+ — matches any period containing "45".
474
+ </p>
475
+ </div>
476
+ );
477
+ };
478
+
479
+ const HighlightedMatchesTemplate = () => {
480
+ const [value, setValue] = useState<TimeValue | ''>('');
481
+ return (
482
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)', maxWidth: '280px' }}>
483
+ <TimeInput
484
+ options={LESSON_PERIOD_OPTIONS}
485
+ searchType="substring"
486
+ highlightStringMatches
487
+ value={value}
488
+ onValueChange={v => setValue(v as TimeValue | '')}
489
+ id="highlighted-time"
490
+ name="lesson-highlighted"
491
+ placeholder="Type to filter..."
492
+ aria-label="Lesson period"
493
+ />
494
+ <p style={{ margin: 0, color: 'var(--color-grey-600)' }}>
495
+ Matched characters are bolded in the dropdown list.
496
+ </p>
497
+ </div>
498
+ );
499
+ };
500
+
501
+ const ControlledComboboxTemplate = () => {
502
+ const [value, setValue] = useState<TimeValue | ''>('13:00');
503
+ return (
504
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)', maxWidth: '280px' }}>
505
+ <TimeInput
506
+ options={LESSON_PERIOD_OPTIONS}
507
+ value={value}
508
+ onValueChange={v => setValue(v as TimeValue | '')}
509
+ id="controlled-combobox"
510
+ name="lesson-controlled"
511
+ placeholder="Select lesson period..."
512
+ aria-label="Lesson period"
513
+ />
514
+ <p style={{ margin: 0, color: 'var(--color-grey-600)' }}>
515
+ Selected:
516
+ {' '}
517
+ <code>{value || '(none)'}</code>
518
+ </p>
519
+ </div>
520
+ );
521
+ };
522
+
523
+ // ---------------------------------------------------------------------------
524
+ // FormField template components
525
+ // ---------------------------------------------------------------------------
526
+
527
+ const InFormFieldTemplate = () => (
528
+ <div style={{ maxWidth: '320px' }}>
529
+ <FormField
530
+ label="Lesson start time"
531
+ id="ff-start-time"
532
+ inputType="time"
533
+ inputProps={{ name: 'lesson-start', defaultValue: '09:00' }}
534
+ />
535
+ </div>
536
+ );
537
+
538
+ const InFormFieldWithErrorTemplate = () => (
539
+ <div style={{ maxWidth: '320px' }}>
540
+ <FormField
541
+ label="Registration start time"
542
+ id="ff-error-time"
543
+ inputType="time"
544
+ errorText="Please enter a valid registration time."
545
+ inputProps={{ name: 'registration-time' }}
546
+ />
547
+ </div>
548
+ );
549
+
550
+ const InFormFieldComboboxTemplate = () => {
551
+ const [value, setValue] = useState<TimeValue | ''>('');
552
+ return (
553
+ <div style={{ maxWidth: '320px' }}>
554
+ <FormField
555
+ label="Lesson period"
556
+ id="ff-lesson-period"
557
+ inputType="time"
558
+ inputProps={{
559
+ name: 'lesson-period',
560
+ options: LESSON_PERIOD_OPTIONS,
561
+ value,
562
+ onValueChange: v => setValue(v as TimeValue | ''),
563
+ placeholder: 'Select a lesson period...',
110
564
  }}
111
565
  />
112
- <Button variant="primary" size="M" type="submit">
113
- Submit
114
- </Button>
115
- </form>
566
+ </div>
116
567
  );
117
568
  };
118
569
 
119
- export const NativeMinute: Story = withDescription({
570
+ // ---------------------------------------------------------------------------
571
+ // Stories — Native mode
572
+ // ---------------------------------------------------------------------------
573
+
574
+ export const Default: Story = {
120
575
  args: {
121
- value: '14:30',
122
- granularity: 'minute',
576
+ 'id': 'default-time',
577
+ 'name': 'default-time',
578
+ 'aria-label': 'Start time',
123
579
  },
124
- render: args => <ControlledTimeInput {...args} />,
125
- }, 'Controlled native time input in minute mode. This story keeps local state on purpose to demonstrate the correct `value` + `onValueChange` integration pattern.');
580
+ render: args => (
581
+ <div style={{ maxWidth: '280px' }}>
582
+ <TimeInput {...args} />
583
+ </div>
584
+ ),
585
+ };
126
586
 
127
- export const NativeSecond: Story = withDescription({
128
- args: {
129
- value: '14:30:15',
130
- granularity: 'second',
587
+ export const Controlled: Story = withDescription(
588
+ {
589
+ parameters: {
590
+ controls: { disable: true },
591
+ docs: {
592
+ source: {
593
+ language: 'tsx',
594
+ code: `
595
+ import { useState } from 'react';
596
+ import { TimeInput, type TimeValue } from '@arbor-education/design-system.components';
597
+
598
+ function ControlledTimeInputExample() {
599
+ const [value, setValue] = useState<TimeValue | ''>('09:30');
600
+
601
+ return (
602
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)', maxWidth: '280px' }}>
603
+ <TimeInput
604
+ value={value}
605
+ onValueChange={v => setValue(v as TimeValue | '')}
606
+ id="start-time"
607
+ name="start-time"
608
+ aria-label="Start time"
609
+ />
610
+ <p style={{ margin: 0, color: 'var(--color-grey-600)' }}>
611
+ Selected: <code>{value || '(none)'}</code>
612
+ </p>
613
+ </div>
614
+ );
615
+ }
616
+
617
+ export default ControlledTimeInputExample;
618
+ `.trim(),
619
+ },
620
+ },
621
+ },
622
+ render: () => <ControlledTemplate />,
131
623
  },
132
- render: args => <ControlledTimeInput {...args} />,
133
- }, 'Controlled native time input in second mode, showing the same parent-managed state pattern with `HH:MM:SS` values.');
624
+ 'A controlled TimeInput keeps the value in React state via `onValueChange`. This is the recommended pattern for forms where you need to read, validate, or derive other values from the selected time.',
625
+ );
134
626
 
135
- export const NativeWithBounds: Story = withDescription({
136
- args: {
137
- value: '08:30',
138
- granularity: 'minute',
139
- min: '09:00',
140
- max: '17:30',
627
+ export const Uncontrolled: Story = withDescription(
628
+ {
629
+ parameters: {
630
+ controls: { disable: true },
631
+ docs: {
632
+ source: {
633
+ language: 'tsx',
634
+ code: `
635
+ import { TimeInput } from '@arbor-education/design-system.components';
636
+
637
+ function UncontrolledTimeInputExample() {
638
+ return (
639
+ <TimeInput
640
+ defaultValue="08:45"
641
+ id="uncontrolled-time"
642
+ name="registration-time"
643
+ aria-label="Registration time"
644
+ />
645
+ );
646
+ }
647
+
648
+ export default UncontrolledTimeInputExample;
649
+ `.trim(),
650
+ },
651
+ },
652
+ },
653
+ render: () => (
654
+ <div style={{ maxWidth: '280px' }}>
655
+ <TimeInput
656
+ defaultValue="08:45"
657
+ id="uncontrolled-time"
658
+ name="registration-time"
659
+ aria-label="Registration time"
660
+ />
661
+ </div>
662
+ ),
141
663
  },
142
- render: args => <NativeValidationDemo {...args} />,
143
- }, 'Demonstrates the browser-native min/max validation flow. Enter a time outside the allowed range and submit the form to let the browser show its own native validation message.');
664
+ 'An uncontrolled TimeInput uses `defaultValue` to set the initial time. React does not track subsequent changes — the current value is read via a form `FormData` or a forwarded `ref`. Suitable for simple forms where you only need the value on submit.',
665
+ );
144
666
 
145
- export const TimeList: Story = withDescription({
146
- args: {
147
- options: [...timeOptions],
148
- value: '10:00',
149
- placeholder: 'Select time',
667
+ export const GranularitySecond: Story = withDescription(
668
+ {
669
+ parameters: {
670
+ controls: { disable: true },
671
+ docs: {
672
+ source: {
673
+ language: 'tsx',
674
+ code: `
675
+ import { useState } from 'react';
676
+ import { TimeInput, type TimeValue } from '@arbor-education/design-system.components';
677
+
678
+ function PreciseTimeInputExample() {
679
+ const [value, setValue] = useState<TimeValue | ''>('');
680
+
681
+ return (
682
+ <TimeInput
683
+ granularity="second"
684
+ value={value}
685
+ onValueChange={v => setValue(v as TimeValue | '')}
686
+ id="precise-time"
687
+ name="precise-time"
688
+ aria-label="Precise time"
689
+ />
690
+ );
691
+ }
692
+
693
+ export default PreciseTimeInputExample;
694
+ `.trim(),
695
+ },
696
+ },
697
+ },
698
+ render: () => <GranularitySecondTemplate />,
150
699
  },
151
- render: args => <ControlledTimeInput {...args} />,
152
- }, 'Controlled time-list mode using the combobox-backed variant. Selecting a time updates the local story state the same way an app component would.');
700
+ 'Setting `granularity="second"` adds a seconds segment to the native time input (`step={1}`). Use this for stopwatch-style timing or precise event logging. **Native mode only** — silently ignored when `options` is provided.',
701
+ );
153
702
 
154
- export const TimeListHighlightedMatches: Story = withDescription({
155
- args: {
156
- options: [...timeOptions],
157
- value: '',
158
- placeholder: 'Search times',
159
- searchType: 'substring',
160
- highlightStringMatches: true,
703
+ export const WithMinMaxBounds: Story = withDescription(
704
+ {
705
+ parameters: {
706
+ controls: { disable: true },
707
+ docs: {
708
+ source: {
709
+ language: 'tsx',
710
+ code: `
711
+ import { TimeInput, type TimeValue } from '@arbor-education/design-system.components';
712
+
713
+ function BoundedTimeInputExample() {
714
+ return (
715
+ <TimeInput
716
+ min="08:00"
717
+ max="17:00"
718
+ id="school-time"
719
+ name="school-time"
720
+ aria-label="School hours time"
721
+ />
722
+ );
723
+ }
724
+
725
+ export default BoundedTimeInputExample;
726
+ `.trim(),
727
+ },
728
+ },
729
+ },
730
+ render: () => <WithMinMaxBoundsTemplate />,
161
731
  },
162
- render: args => <ControlledTimeInput {...args} />,
163
- }, 'Combobox-backed time-list mode with substring matching and highlighted text so consumers can review how search behaves when users type partial time fragments such as `30`.');
732
+ 'The `min` and `max` props restrict the selectable range in the native browser picker to school hours. **Native mode only** — in combobox mode, restrict available times by filtering the `options` array instead.',
733
+ );
164
734
 
165
- export const UncontrolledNative: Story = withDescription({
166
- args: {
167
- defaultValue: '09:30',
168
- granularity: 'minute',
735
+ export const WithError: Story = withDescription(
736
+ {
737
+ parameters: {
738
+ controls: { disable: true },
739
+ docs: {
740
+ source: {
741
+ language: 'tsx',
742
+ code: `
743
+ import { TimeInput } from '@arbor-education/design-system.components';
744
+
745
+ // Standalone usage — you MUST set aria-invalid yourself.
746
+ // Inside <FormField> this is handled automatically when errorText is present.
747
+ function ErrorTimeInputExample() {
748
+ return (
749
+ <TimeInput
750
+ hasError
751
+ aria-invalid={true}
752
+ aria-label="End time"
753
+ id="end-time"
754
+ name="end-time"
755
+ />
756
+ );
757
+ }
758
+
759
+ export default ErrorTimeInputExample;
760
+ `.trim(),
761
+ },
762
+ },
763
+ },
764
+ render: () => (
765
+ <div style={{ maxWidth: '280px' }}>
766
+ <TimeInput
767
+ hasError
768
+ aria-invalid={true}
769
+ aria-label="End time"
770
+ id="end-time-error-story"
771
+ name="end-time"
772
+ />
773
+ </div>
774
+ ),
775
+ },
776
+ '`hasError` applies error-state visual styling. **Important:** it does NOT automatically set `aria-invalid` when used standalone — add that attribute yourself for screen-reader coverage. Inside `<FormField>`, both are set automatically when `errorText` is provided.',
777
+ );
778
+
779
+ export const Disabled: Story = withDescription(
780
+ {
781
+ parameters: {
782
+ controls: { disable: true },
783
+ docs: {
784
+ source: {
785
+ language: 'tsx',
786
+ code: `
787
+ import { TimeInput } from '@arbor-education/design-system.components';
788
+
789
+ function DisabledTimeInputExample() {
790
+ return (
791
+ <TimeInput
792
+ disabled
793
+ value="09:00"
794
+ aria-label="Lesson start time"
795
+ id="disabled-time"
796
+ name="disabled-time"
797
+ />
798
+ );
799
+ }
800
+
801
+ export default DisabledTimeInputExample;
802
+ `.trim(),
803
+ },
804
+ },
805
+ },
806
+ render: () => (
807
+ <div style={{ maxWidth: '280px' }}>
808
+ <TimeInput
809
+ disabled
810
+ value="09:00"
811
+ aria-label="Lesson start time"
812
+ id="disabled-time-story"
813
+ name="disabled-time"
814
+ />
815
+ </div>
816
+ ),
817
+ },
818
+ 'A disabled TimeInput prevents user interaction. The current value remains visible, so users can still read the scheduled time. Pass `disabled` via `inputProps` when using inside `<FormField>`.',
819
+ );
820
+
821
+ // ---------------------------------------------------------------------------
822
+ // Stories — Combobox mode
823
+ // ---------------------------------------------------------------------------
824
+
825
+ export const WithOptions: Story = withDescription(
826
+ {
827
+ parameters: {
828
+ controls: { disable: true },
829
+ docs: {
830
+ source: {
831
+ language: 'tsx',
832
+ code: `
833
+ import { useState } from 'react';
834
+ import { TimeInput, type TimeValue } from '@arbor-education/design-system.components';
835
+
836
+ const LESSON_PERIODS: TimeValue[] = [
837
+ '09:00', '09:45', '10:30', '11:15',
838
+ '13:00', '13:45', '14:30', '15:15', '16:00',
839
+ ];
840
+
841
+ function LessonPeriodPickerExample() {
842
+ const [value, setValue] = useState<TimeValue | ''>('');
843
+
844
+ return (
845
+ <TimeInput
846
+ options={LESSON_PERIODS}
847
+ value={value}
848
+ onValueChange={v => setValue(v as TimeValue | '')}
849
+ id="lesson-period"
850
+ name="lesson-start"
851
+ placeholder="Select lesson period..."
852
+ aria-label="Lesson period"
853
+ />
854
+ );
855
+ }
856
+
857
+ export default LessonPeriodPickerExample;
858
+ `.trim(),
859
+ },
860
+ },
861
+ },
862
+ render: () => <WithOptionsTemplate />,
863
+ },
864
+ 'Providing an `options` array switches the component to **combobox mode**. The user can type to filter or click to select from the predefined list. Ideal for scheduling interfaces where only specific time slots are valid.',
865
+ );
866
+
867
+ export const SubstringSearch: Story = withDescription(
868
+ {
869
+ parameters: {
870
+ controls: { disable: true },
871
+ docs: {
872
+ source: {
873
+ language: 'tsx',
874
+ code: `
875
+ import { useState } from 'react';
876
+ import { TimeInput, type TimeValue } from '@arbor-education/design-system.components';
877
+
878
+ const LESSON_PERIODS: TimeValue[] = [
879
+ '09:00', '09:45', '10:30', '11:15',
880
+ '13:00', '13:45', '14:30', '15:15', '16:00',
881
+ ];
882
+
883
+ function SubstringSearchPickerExample() {
884
+ const [value, setValue] = useState<TimeValue | ''>('');
885
+
886
+ return (
887
+ <TimeInput
888
+ options={LESSON_PERIODS}
889
+ searchType="substring"
890
+ value={value}
891
+ onValueChange={v => setValue(v as TimeValue | '')}
892
+ id="lesson-substring"
893
+ name="lesson-substring"
894
+ placeholder="Type to search..."
895
+ aria-label="Lesson period"
896
+ />
897
+ );
898
+ }
899
+
900
+ export default SubstringSearchPickerExample;
901
+ `.trim(),
902
+ },
903
+ },
904
+ },
905
+ render: () => <SubstringSearchTemplate />,
906
+ },
907
+ '`searchType="substring"` matches anywhere in the option string, not just the start. Try typing `"45"` to match `09:45`, `13:45`, and `14:45`. **Combobox mode only** — silently ignored in native mode.',
908
+ );
909
+
910
+ export const HighlightedMatches: Story = withDescription(
911
+ {
912
+ parameters: {
913
+ controls: { disable: true },
914
+ docs: {
915
+ source: {
916
+ language: 'tsx',
917
+ code: `
918
+ import { useState } from 'react';
919
+ import { TimeInput, type TimeValue } from '@arbor-education/design-system.components';
920
+
921
+ const LESSON_PERIODS: TimeValue[] = [
922
+ '09:00', '09:45', '10:30', '11:15',
923
+ '13:00', '13:45', '14:30', '15:15', '16:00',
924
+ ];
925
+
926
+ function HighlightedMatchesPickerExample() {
927
+ const [value, setValue] = useState<TimeValue | ''>('');
928
+
929
+ return (
930
+ <TimeInput
931
+ options={LESSON_PERIODS}
932
+ searchType="substring"
933
+ highlightStringMatches
934
+ value={value}
935
+ onValueChange={v => setValue(v as TimeValue | '')}
936
+ id="lesson-highlighted"
937
+ name="lesson-highlighted"
938
+ placeholder="Type to filter..."
939
+ aria-label="Lesson period"
940
+ />
941
+ );
942
+ }
943
+
944
+ export default HighlightedMatchesPickerExample;
945
+ `.trim(),
946
+ },
947
+ },
948
+ },
949
+ render: () => <HighlightedMatchesTemplate />,
950
+ },
951
+ '`highlightStringMatches` bolds the matched characters in each dropdown option. Combine with `searchType="substring"` for maximum clarity when filtering. **Combobox mode only.**',
952
+ );
953
+
954
+ export const ComboboxWithError: Story = withDescription(
955
+ {
956
+ parameters: {
957
+ controls: { disable: true },
958
+ docs: {
959
+ source: {
960
+ language: 'tsx',
961
+ code: `
962
+ import { TimeInput, type TimeValue } from '@arbor-education/design-system.components';
963
+
964
+ const LESSON_PERIODS: TimeValue[] = ['09:00', '09:45', '10:30', '11:15'];
965
+
966
+ function ErrorComboboxTimeInputExample() {
967
+ return (
968
+ <TimeInput
969
+ options={LESSON_PERIODS}
970
+ hasError
971
+ aria-invalid={true}
972
+ aria-label="Lesson period"
973
+ id="lesson-error"
974
+ name="lesson-error"
975
+ placeholder="Select a lesson period..."
976
+ />
977
+ );
978
+ }
979
+
980
+ export default ErrorComboboxTimeInputExample;
981
+ `.trim(),
982
+ },
983
+ },
984
+ },
985
+ render: () => (
986
+ <div style={{ maxWidth: '280px' }}>
987
+ <TimeInput
988
+ options={LESSON_PERIOD_OPTIONS}
989
+ hasError
990
+ aria-invalid={true}
991
+ aria-label="Lesson period"
992
+ id="combobox-error-story"
993
+ name="lesson-error"
994
+ placeholder="Select a lesson period..."
995
+ />
996
+ </div>
997
+ ),
998
+ },
999
+ 'Combobox mode supports `hasError` for error-state styling. As with native mode, `aria-invalid` must be set manually when used outside `<FormField>`.',
1000
+ );
1001
+
1002
+ export const ComboboxDisabled: Story = withDescription(
1003
+ {
1004
+ parameters: {
1005
+ controls: { disable: true },
1006
+ docs: {
1007
+ source: {
1008
+ language: 'tsx',
1009
+ code: `
1010
+ import { TimeInput, type TimeValue } from '@arbor-education/design-system.components';
1011
+
1012
+ const LESSON_PERIODS: TimeValue[] = ['09:00', '09:45', '10:30', '11:15'];
1013
+
1014
+ function DisabledComboboxTimeInputExample() {
1015
+ return (
1016
+ <TimeInput
1017
+ options={LESSON_PERIODS}
1018
+ disabled
1019
+ value="09:00"
1020
+ aria-label="Lesson period"
1021
+ id="lesson-disabled"
1022
+ name="lesson-disabled"
1023
+ />
1024
+ );
1025
+ }
1026
+
1027
+ export default DisabledComboboxTimeInputExample;
1028
+ `.trim(),
1029
+ },
1030
+ },
1031
+ },
1032
+ render: () => (
1033
+ <div style={{ maxWidth: '280px' }}>
1034
+ <TimeInput
1035
+ options={LESSON_PERIOD_OPTIONS}
1036
+ disabled
1037
+ value="09:00"
1038
+ aria-label="Lesson period"
1039
+ id="combobox-disabled-story"
1040
+ name="lesson-disabled"
1041
+ />
1042
+ </div>
1043
+ ),
1044
+ },
1045
+ 'Combobox mode respects the `disabled` prop. The trigger button is non-interactive and visually muted, but the selected value remains readable.',
1046
+ );
1047
+
1048
+ export const ControlledCombobox: Story = withDescription(
1049
+ {
1050
+ parameters: {
1051
+ controls: { disable: true },
1052
+ docs: {
1053
+ source: {
1054
+ language: 'tsx',
1055
+ code: `
1056
+ import { useState } from 'react';
1057
+ import { TimeInput, type TimeValue } from '@arbor-education/design-system.components';
1058
+
1059
+ const LESSON_PERIODS: TimeValue[] = [
1060
+ '09:00', '09:45', '10:30', '11:15',
1061
+ '13:00', '13:45', '14:30', '15:15', '16:00',
1062
+ ];
1063
+
1064
+ function ControlledComboboxTimeInputExample() {
1065
+ const [value, setValue] = useState<TimeValue | ''>('13:00');
1066
+
1067
+ return (
1068
+ <TimeInput
1069
+ options={LESSON_PERIODS}
1070
+ value={value}
1071
+ onValueChange={v => setValue(v as TimeValue | '')}
1072
+ id="lesson-controlled"
1073
+ name="lesson-controlled"
1074
+ placeholder="Select lesson period..."
1075
+ aria-label="Lesson period"
1076
+ />
1077
+ );
1078
+ }
1079
+
1080
+ export default ControlledComboboxTimeInputExample;
1081
+ `.trim(),
1082
+ },
1083
+ },
1084
+ },
1085
+ render: () => <ControlledComboboxTemplate />,
1086
+ },
1087
+ 'A controlled combobox TimeInput pre-selects the afternoon session ("13:00") via the `value` prop. `onValueChange` keeps React state in sync whenever the user picks a different period.',
1088
+ );
1089
+
1090
+ // ---------------------------------------------------------------------------
1091
+ // Stories — FormField integration
1092
+ // ---------------------------------------------------------------------------
1093
+
1094
+ export const InFormField: Story = withDescription(
1095
+ {
1096
+ parameters: {
1097
+ controls: { disable: true },
1098
+ docs: {
1099
+ source: {
1100
+ language: 'tsx',
1101
+ code: `
1102
+ import { FormField } from '@arbor-education/design-system.components';
1103
+
1104
+ function InFormFieldExample() {
1105
+ return (
1106
+ <div style={{ maxWidth: '320px' }}>
1107
+ <FormField
1108
+ label="Lesson start time"
1109
+ id="lesson-start"
1110
+ inputType="time"
1111
+ inputProps={{ name: 'lesson-start', defaultValue: '09:00' }}
1112
+ />
1113
+ </div>
1114
+ );
1115
+ }
1116
+
1117
+ export default InFormFieldExample;
1118
+ `.trim(),
1119
+ },
1120
+ },
1121
+ },
1122
+ render: () => <InFormFieldTemplate />,
1123
+ },
1124
+ 'The recommended usage pattern — `<FormField inputType="time">` wires the accessible `<label>`, `aria-describedby`, `hasError`, and `aria-invalid` automatically. No manual prop threading required.',
1125
+ );
1126
+
1127
+ export const InFormFieldWithError: Story = withDescription(
1128
+ {
1129
+ parameters: {
1130
+ controls: { disable: true },
1131
+ docs: {
1132
+ source: {
1133
+ language: 'tsx',
1134
+ code: `
1135
+ import { FormField } from '@arbor-education/design-system.components';
1136
+
1137
+ function InFormFieldWithErrorExample() {
1138
+ return (
1139
+ <div style={{ maxWidth: '320px' }}>
1140
+ <FormField
1141
+ label="Registration start time"
1142
+ id="registration-time"
1143
+ inputType="time"
1144
+ errorText="Please enter a valid registration time."
1145
+ inputProps={{ name: 'registration-time' }}
1146
+ />
1147
+ </div>
1148
+ );
1149
+ }
1150
+
1151
+ export default InFormFieldWithErrorExample;
1152
+ `.trim(),
1153
+ },
1154
+ },
1155
+ },
1156
+ render: () => <InFormFieldWithErrorTemplate />,
1157
+ },
1158
+ 'Setting `errorText` on `<FormField>` automatically applies error styling to the input, renders the error message, sets `aria-invalid`, and links `aria-describedby` to the error element. No extra props needed on `TimeInput` itself.',
1159
+ );
1160
+
1161
+ export const InFormFieldCombobox: Story = withDescription(
1162
+ {
1163
+ parameters: {
1164
+ controls: { disable: true },
1165
+ docs: {
1166
+ source: {
1167
+ language: 'tsx',
1168
+ code: `
1169
+ import { useState } from 'react';
1170
+ import { FormField, type TimeValue } from '@arbor-education/design-system.components';
1171
+
1172
+ const LESSON_PERIODS: TimeValue[] = [
1173
+ '09:00', '09:45', '10:30', '11:15',
1174
+ '13:00', '13:45', '14:30', '15:15', '16:00',
1175
+ ];
1176
+
1177
+ function InFormFieldComboboxExample() {
1178
+ const [value, setValue] = useState<TimeValue | ''>('');
1179
+
1180
+ return (
1181
+ <div style={{ maxWidth: '320px' }}>
1182
+ <FormField
1183
+ label="Lesson period"
1184
+ id="lesson-period"
1185
+ inputType="time"
1186
+ inputProps={{
1187
+ name: 'lesson-period',
1188
+ options: LESSON_PERIODS,
1189
+ value,
1190
+ onValueChange: v => setValue(v as TimeValue | ''),
1191
+ placeholder: 'Select a lesson period...',
1192
+ }}
1193
+ />
1194
+ </div>
1195
+ );
1196
+ }
1197
+
1198
+ export default InFormFieldComboboxExample;
1199
+ `.trim(),
1200
+ },
1201
+ },
1202
+ },
1203
+ render: () => <InFormFieldComboboxTemplate />,
169
1204
  },
170
- }, 'Uncontrolled native usage using `defaultValue`. This is useful when the parent does not need to drive the current value after initial render.');
1205
+ 'Combobox mode works seamlessly inside `<FormField>`. Pass `options` (and any combobox-mode props) via `inputProps`. The label, ARIA wiring, and error handling are all handled identically to native mode.',
1206
+ );