@dashforge/tw 0.1.0-beta

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 (299) hide show
  1. package/.swcrc +29 -0
  2. package/CHANGELOG.md +131 -0
  3. package/LICENSE +21 -0
  4. package/README.md +244 -0
  5. package/dist/components/AppShell/AppShell.d.ts +32 -0
  6. package/dist/components/AppShell/AppShell.d.ts.map +1 -0
  7. package/dist/components/AppShell/appShell.types.d.ts +62 -0
  8. package/dist/components/AppShell/appShell.types.d.ts.map +1 -0
  9. package/dist/components/AppShell/appShell.variants.d.ts +65 -0
  10. package/dist/components/AppShell/appShell.variants.d.ts.map +1 -0
  11. package/dist/components/Autocomplete/Autocomplete.d.ts +32 -0
  12. package/dist/components/Autocomplete/Autocomplete.d.ts.map +1 -0
  13. package/dist/components/Autocomplete/autocomplete.types.d.ts +214 -0
  14. package/dist/components/Autocomplete/autocomplete.types.d.ts.map +1 -0
  15. package/dist/components/Autocomplete/autocomplete.variants.d.ts +214 -0
  16. package/dist/components/Autocomplete/autocomplete.variants.d.ts.map +1 -0
  17. package/dist/components/Breadcrumbs/Breadcrumbs.d.ts +23 -0
  18. package/dist/components/Breadcrumbs/Breadcrumbs.d.ts.map +1 -0
  19. package/dist/components/Breadcrumbs/breadcrumbs.types.d.ts +98 -0
  20. package/dist/components/Breadcrumbs/breadcrumbs.types.d.ts.map +1 -0
  21. package/dist/components/Breadcrumbs/breadcrumbs.variants.d.ts +85 -0
  22. package/dist/components/Breadcrumbs/breadcrumbs.variants.d.ts.map +1 -0
  23. package/dist/components/Button/Button.d.ts +44 -0
  24. package/dist/components/Button/Button.d.ts.map +1 -0
  25. package/dist/components/Button/button.types.d.ts +66 -0
  26. package/dist/components/Button/button.types.d.ts.map +1 -0
  27. package/dist/components/Button/button.variants.d.ts +104 -0
  28. package/dist/components/Button/button.variants.d.ts.map +1 -0
  29. package/dist/components/Checkbox/Checkbox.d.ts +31 -0
  30. package/dist/components/Checkbox/Checkbox.d.ts.map +1 -0
  31. package/dist/components/Checkbox/checkbox.types.d.ts +86 -0
  32. package/dist/components/Checkbox/checkbox.types.d.ts.map +1 -0
  33. package/dist/components/Checkbox/checkbox.variants.d.ts +109 -0
  34. package/dist/components/Checkbox/checkbox.variants.d.ts.map +1 -0
  35. package/dist/components/ConfirmDialog/ConfirmDialog.d.ts +38 -0
  36. package/dist/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -0
  37. package/dist/components/ConfirmDialog/confirmDialog.types.d.ts +80 -0
  38. package/dist/components/ConfirmDialog/confirmDialog.types.d.ts.map +1 -0
  39. package/dist/components/ConfirmDialog/confirmDialog.variants.d.ts +90 -0
  40. package/dist/components/ConfirmDialog/confirmDialog.variants.d.ts.map +1 -0
  41. package/dist/components/DateTimePicker/DateTimePicker.d.ts +49 -0
  42. package/dist/components/DateTimePicker/DateTimePicker.d.ts.map +1 -0
  43. package/dist/components/DateTimePicker/dateTimePicker.types.d.ts +95 -0
  44. package/dist/components/DateTimePicker/dateTimePicker.types.d.ts.map +1 -0
  45. package/dist/components/DateTimePicker/dateTimePicker.variants.d.ts +165 -0
  46. package/dist/components/DateTimePicker/dateTimePicker.variants.d.ts.map +1 -0
  47. package/dist/components/LeftNav/LeftNav.d.ts +34 -0
  48. package/dist/components/LeftNav/LeftNav.d.ts.map +1 -0
  49. package/dist/components/LeftNav/leftNav.types.d.ts +136 -0
  50. package/dist/components/LeftNav/leftNav.types.d.ts.map +1 -0
  51. package/dist/components/LeftNav/leftNav.variants.d.ts +143 -0
  52. package/dist/components/LeftNav/leftNav.variants.d.ts.map +1 -0
  53. package/dist/components/NumberField/NumberField.d.ts +18 -0
  54. package/dist/components/NumberField/NumberField.d.ts.map +1 -0
  55. package/dist/components/NumberField/numberField.types.d.ts +79 -0
  56. package/dist/components/NumberField/numberField.types.d.ts.map +1 -0
  57. package/dist/components/NumberField/numberField.variants.d.ts +169 -0
  58. package/dist/components/NumberField/numberField.variants.d.ts.map +1 -0
  59. package/dist/components/OTPField/OTPField.d.ts +25 -0
  60. package/dist/components/OTPField/OTPField.d.ts.map +1 -0
  61. package/dist/components/OTPField/otpField.types.d.ts +74 -0
  62. package/dist/components/OTPField/otpField.types.d.ts.map +1 -0
  63. package/dist/components/OTPField/otpField.variants.d.ts +105 -0
  64. package/dist/components/OTPField/otpField.variants.d.ts.map +1 -0
  65. package/dist/components/RadioGroup/RadioGroup.d.ts +35 -0
  66. package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -0
  67. package/dist/components/RadioGroup/radioGroup.types.d.ts +103 -0
  68. package/dist/components/RadioGroup/radioGroup.types.d.ts.map +1 -0
  69. package/dist/components/RadioGroup/radioGroup.variants.d.ts +166 -0
  70. package/dist/components/RadioGroup/radioGroup.variants.d.ts.map +1 -0
  71. package/dist/components/Snackbar/Snackbar.d.ts +37 -0
  72. package/dist/components/Snackbar/Snackbar.d.ts.map +1 -0
  73. package/dist/components/Snackbar/snackbar.types.d.ts +86 -0
  74. package/dist/components/Snackbar/snackbar.types.d.ts.map +1 -0
  75. package/dist/components/Snackbar/snackbar.variants.d.ts +153 -0
  76. package/dist/components/Snackbar/snackbar.variants.d.ts.map +1 -0
  77. package/dist/components/Switch/Switch.d.ts +15 -0
  78. package/dist/components/Switch/Switch.d.ts.map +1 -0
  79. package/dist/components/Switch/switch.types.d.ts +47 -0
  80. package/dist/components/Switch/switch.types.d.ts.map +1 -0
  81. package/dist/components/Switch/switch.variants.d.ts +105 -0
  82. package/dist/components/Switch/switch.variants.d.ts.map +1 -0
  83. package/dist/components/TextField/TextField.d.ts +25 -0
  84. package/dist/components/TextField/TextField.d.ts.map +1 -0
  85. package/dist/components/TextField/textField.types.d.ts +72 -0
  86. package/dist/components/TextField/textField.types.d.ts.map +1 -0
  87. package/dist/components/TextField/textField.variants.d.ts +160 -0
  88. package/dist/components/TextField/textField.variants.d.ts.map +1 -0
  89. package/dist/components/Textarea/Textarea.d.ts +16 -0
  90. package/dist/components/Textarea/Textarea.d.ts.map +1 -0
  91. package/dist/components/Textarea/textarea.types.d.ts +63 -0
  92. package/dist/components/Textarea/textarea.types.d.ts.map +1 -0
  93. package/dist/components/Textarea/textarea.variants.d.ts +197 -0
  94. package/dist/components/Textarea/textarea.variants.d.ts.map +1 -0
  95. package/dist/components/TopBar/TopBar.d.ts +33 -0
  96. package/dist/components/TopBar/TopBar.d.ts.map +1 -0
  97. package/dist/components/TopBar/topBar.types.d.ts +49 -0
  98. package/dist/components/TopBar/topBar.types.d.ts.map +1 -0
  99. package/dist/components/TopBar/topBar.variants.d.ts +79 -0
  100. package/dist/components/TopBar/topBar.variants.d.ts.map +1 -0
  101. package/dist/components/_shared/resolveValidationState.d.ts +42 -0
  102. package/dist/components/_shared/resolveValidationState.d.ts.map +1 -0
  103. package/dist/hooks/useAccessState.d.ts +37 -0
  104. package/dist/hooks/useAccessState.d.ts.map +1 -0
  105. package/dist/index.d.ts +82 -0
  106. package/dist/index.d.ts.map +1 -0
  107. package/dist/index.esm.js +4958 -0
  108. package/dist/src/components/AppShell/AppShell.d.ts +32 -0
  109. package/dist/src/components/AppShell/AppShell.d.ts.map +1 -0
  110. package/dist/src/components/AppShell/appShell.types.d.ts +62 -0
  111. package/dist/src/components/AppShell/appShell.types.d.ts.map +1 -0
  112. package/dist/src/components/AppShell/appShell.variants.d.ts +65 -0
  113. package/dist/src/components/AppShell/appShell.variants.d.ts.map +1 -0
  114. package/dist/src/components/Autocomplete/Autocomplete.d.ts +32 -0
  115. package/dist/src/components/Autocomplete/Autocomplete.d.ts.map +1 -0
  116. package/dist/src/components/Autocomplete/autocomplete.types.d.ts +214 -0
  117. package/dist/src/components/Autocomplete/autocomplete.types.d.ts.map +1 -0
  118. package/dist/src/components/Autocomplete/autocomplete.variants.d.ts +214 -0
  119. package/dist/src/components/Autocomplete/autocomplete.variants.d.ts.map +1 -0
  120. package/dist/src/components/Breadcrumbs/Breadcrumbs.d.ts +23 -0
  121. package/dist/src/components/Breadcrumbs/Breadcrumbs.d.ts.map +1 -0
  122. package/dist/src/components/Breadcrumbs/breadcrumbs.types.d.ts +98 -0
  123. package/dist/src/components/Breadcrumbs/breadcrumbs.types.d.ts.map +1 -0
  124. package/dist/src/components/Breadcrumbs/breadcrumbs.variants.d.ts +85 -0
  125. package/dist/src/components/Breadcrumbs/breadcrumbs.variants.d.ts.map +1 -0
  126. package/dist/src/components/Button/Button.d.ts +44 -0
  127. package/dist/src/components/Button/Button.d.ts.map +1 -0
  128. package/dist/src/components/Button/button.types.d.ts +66 -0
  129. package/dist/src/components/Button/button.types.d.ts.map +1 -0
  130. package/dist/src/components/Button/button.variants.d.ts +104 -0
  131. package/dist/src/components/Button/button.variants.d.ts.map +1 -0
  132. package/dist/src/components/Checkbox/Checkbox.d.ts +31 -0
  133. package/dist/src/components/Checkbox/Checkbox.d.ts.map +1 -0
  134. package/dist/src/components/Checkbox/checkbox.types.d.ts +86 -0
  135. package/dist/src/components/Checkbox/checkbox.types.d.ts.map +1 -0
  136. package/dist/src/components/Checkbox/checkbox.variants.d.ts +109 -0
  137. package/dist/src/components/Checkbox/checkbox.variants.d.ts.map +1 -0
  138. package/dist/src/components/ConfirmDialog/ConfirmDialog.d.ts +38 -0
  139. package/dist/src/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -0
  140. package/dist/src/components/ConfirmDialog/confirmDialog.types.d.ts +80 -0
  141. package/dist/src/components/ConfirmDialog/confirmDialog.types.d.ts.map +1 -0
  142. package/dist/src/components/ConfirmDialog/confirmDialog.variants.d.ts +90 -0
  143. package/dist/src/components/ConfirmDialog/confirmDialog.variants.d.ts.map +1 -0
  144. package/dist/src/components/DateTimePicker/DateTimePicker.d.ts +49 -0
  145. package/dist/src/components/DateTimePicker/DateTimePicker.d.ts.map +1 -0
  146. package/dist/src/components/DateTimePicker/dateTimePicker.types.d.ts +95 -0
  147. package/dist/src/components/DateTimePicker/dateTimePicker.types.d.ts.map +1 -0
  148. package/dist/src/components/DateTimePicker/dateTimePicker.variants.d.ts +165 -0
  149. package/dist/src/components/DateTimePicker/dateTimePicker.variants.d.ts.map +1 -0
  150. package/dist/src/components/LeftNav/LeftNav.d.ts +34 -0
  151. package/dist/src/components/LeftNav/LeftNav.d.ts.map +1 -0
  152. package/dist/src/components/LeftNav/leftNav.types.d.ts +136 -0
  153. package/dist/src/components/LeftNav/leftNav.types.d.ts.map +1 -0
  154. package/dist/src/components/LeftNav/leftNav.variants.d.ts +143 -0
  155. package/dist/src/components/LeftNav/leftNav.variants.d.ts.map +1 -0
  156. package/dist/src/components/NumberField/NumberField.d.ts +18 -0
  157. package/dist/src/components/NumberField/NumberField.d.ts.map +1 -0
  158. package/dist/src/components/NumberField/numberField.types.d.ts +79 -0
  159. package/dist/src/components/NumberField/numberField.types.d.ts.map +1 -0
  160. package/dist/src/components/NumberField/numberField.variants.d.ts +169 -0
  161. package/dist/src/components/NumberField/numberField.variants.d.ts.map +1 -0
  162. package/dist/src/components/OTPField/OTPField.d.ts +25 -0
  163. package/dist/src/components/OTPField/OTPField.d.ts.map +1 -0
  164. package/dist/src/components/OTPField/otpField.types.d.ts +74 -0
  165. package/dist/src/components/OTPField/otpField.types.d.ts.map +1 -0
  166. package/dist/src/components/OTPField/otpField.variants.d.ts +105 -0
  167. package/dist/src/components/OTPField/otpField.variants.d.ts.map +1 -0
  168. package/dist/src/components/RadioGroup/RadioGroup.d.ts +35 -0
  169. package/dist/src/components/RadioGroup/RadioGroup.d.ts.map +1 -0
  170. package/dist/src/components/RadioGroup/radioGroup.types.d.ts +103 -0
  171. package/dist/src/components/RadioGroup/radioGroup.types.d.ts.map +1 -0
  172. package/dist/src/components/RadioGroup/radioGroup.variants.d.ts +166 -0
  173. package/dist/src/components/RadioGroup/radioGroup.variants.d.ts.map +1 -0
  174. package/dist/src/components/Snackbar/Snackbar.d.ts +37 -0
  175. package/dist/src/components/Snackbar/Snackbar.d.ts.map +1 -0
  176. package/dist/src/components/Snackbar/snackbar.types.d.ts +86 -0
  177. package/dist/src/components/Snackbar/snackbar.types.d.ts.map +1 -0
  178. package/dist/src/components/Snackbar/snackbar.variants.d.ts +153 -0
  179. package/dist/src/components/Snackbar/snackbar.variants.d.ts.map +1 -0
  180. package/dist/src/components/Switch/Switch.d.ts +15 -0
  181. package/dist/src/components/Switch/Switch.d.ts.map +1 -0
  182. package/dist/src/components/Switch/switch.types.d.ts +47 -0
  183. package/dist/src/components/Switch/switch.types.d.ts.map +1 -0
  184. package/dist/src/components/Switch/switch.variants.d.ts +105 -0
  185. package/dist/src/components/Switch/switch.variants.d.ts.map +1 -0
  186. package/dist/src/components/TextField/TextField.d.ts +25 -0
  187. package/dist/src/components/TextField/TextField.d.ts.map +1 -0
  188. package/dist/src/components/TextField/textField.types.d.ts +72 -0
  189. package/dist/src/components/TextField/textField.types.d.ts.map +1 -0
  190. package/dist/src/components/TextField/textField.variants.d.ts +160 -0
  191. package/dist/src/components/TextField/textField.variants.d.ts.map +1 -0
  192. package/dist/src/components/Textarea/Textarea.d.ts +16 -0
  193. package/dist/src/components/Textarea/Textarea.d.ts.map +1 -0
  194. package/dist/src/components/Textarea/textarea.types.d.ts +63 -0
  195. package/dist/src/components/Textarea/textarea.types.d.ts.map +1 -0
  196. package/dist/src/components/Textarea/textarea.variants.d.ts +197 -0
  197. package/dist/src/components/Textarea/textarea.variants.d.ts.map +1 -0
  198. package/dist/src/components/TopBar/TopBar.d.ts +33 -0
  199. package/dist/src/components/TopBar/TopBar.d.ts.map +1 -0
  200. package/dist/src/components/TopBar/topBar.types.d.ts +49 -0
  201. package/dist/src/components/TopBar/topBar.types.d.ts.map +1 -0
  202. package/dist/src/components/TopBar/topBar.variants.d.ts +79 -0
  203. package/dist/src/components/TopBar/topBar.variants.d.ts.map +1 -0
  204. package/dist/src/components/_shared/resolveValidationState.d.ts +42 -0
  205. package/dist/src/components/_shared/resolveValidationState.d.ts.map +1 -0
  206. package/dist/src/hooks/useAccessState.d.ts +37 -0
  207. package/dist/src/hooks/useAccessState.d.ts.map +1 -0
  208. package/dist/src/index.d.ts +82 -0
  209. package/dist/src/index.d.ts.map +1 -0
  210. package/dist/src/utils/cn.d.ts +26 -0
  211. package/dist/src/utils/cn.d.ts.map +1 -0
  212. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  213. package/dist/utils/cn.d.ts +26 -0
  214. package/dist/utils/cn.d.ts.map +1 -0
  215. package/eslint.config.mjs +47 -0
  216. package/package.json +37 -0
  217. package/project.json +15 -0
  218. package/rollup.config.cjs +16 -0
  219. package/src/components/AppShell/AppShell.test.tsx +223 -0
  220. package/src/components/AppShell/AppShell.tsx +114 -0
  221. package/src/components/AppShell/appShell.types.ts +49 -0
  222. package/src/components/AppShell/appShell.variants.ts +48 -0
  223. package/src/components/Autocomplete/Autocomplete.perf.test.tsx +225 -0
  224. package/src/components/Autocomplete/Autocomplete.test.tsx +556 -0
  225. package/src/components/Autocomplete/Autocomplete.tsx +996 -0
  226. package/src/components/Autocomplete/autocomplete.types.ts +198 -0
  227. package/src/components/Autocomplete/autocomplete.variants.ts +135 -0
  228. package/src/components/Breadcrumbs/Breadcrumbs.perf.test.tsx +125 -0
  229. package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +231 -0
  230. package/src/components/Breadcrumbs/Breadcrumbs.tsx +203 -0
  231. package/src/components/Breadcrumbs/breadcrumbs.types.ts +87 -0
  232. package/src/components/Breadcrumbs/breadcrumbs.variants.ts +49 -0
  233. package/src/components/Button/Button.test.tsx +183 -0
  234. package/src/components/Button/Button.tsx +139 -0
  235. package/src/components/Button/button.types.ts +74 -0
  236. package/src/components/Button/button.variants.ts +159 -0
  237. package/src/components/Checkbox/Checkbox.test.tsx +146 -0
  238. package/src/components/Checkbox/Checkbox.tsx +228 -0
  239. package/src/components/Checkbox/checkbox.types.ts +88 -0
  240. package/src/components/Checkbox/checkbox.variants.ts +68 -0
  241. package/src/components/ConfirmDialog/ConfirmDialog.perf.test.tsx +172 -0
  242. package/src/components/ConfirmDialog/ConfirmDialog.test.tsx +311 -0
  243. package/src/components/ConfirmDialog/ConfirmDialog.tsx +252 -0
  244. package/src/components/ConfirmDialog/confirmDialog.types.ts +70 -0
  245. package/src/components/ConfirmDialog/confirmDialog.variants.ts +76 -0
  246. package/src/components/DateTimePicker/DateTimePicker.perf.test.tsx +153 -0
  247. package/src/components/DateTimePicker/DateTimePicker.test.tsx +204 -0
  248. package/src/components/DateTimePicker/DateTimePicker.tsx +319 -0
  249. package/src/components/DateTimePicker/dateTimePicker.types.ts +83 -0
  250. package/src/components/DateTimePicker/dateTimePicker.variants.ts +84 -0
  251. package/src/components/LeftNav/LeftNav.perf.test.tsx +172 -0
  252. package/src/components/LeftNav/LeftNav.test.tsx +302 -0
  253. package/src/components/LeftNav/LeftNav.tsx +330 -0
  254. package/src/components/LeftNav/leftNav.types.ts +120 -0
  255. package/src/components/LeftNav/leftNav.variants.ts +96 -0
  256. package/src/components/NumberField/NumberField.test.tsx +83 -0
  257. package/src/components/NumberField/NumberField.tsx +293 -0
  258. package/src/components/NumberField/numberField.types.ts +72 -0
  259. package/src/components/NumberField/numberField.variants.ts +94 -0
  260. package/src/components/OTPField/OTPField.test.tsx +121 -0
  261. package/src/components/OTPField/OTPField.tsx +250 -0
  262. package/src/components/OTPField/otpField.types.ts +58 -0
  263. package/src/components/OTPField/otpField.variants.ts +72 -0
  264. package/src/components/RadioGroup/RadioGroup.test.tsx +140 -0
  265. package/src/components/RadioGroup/RadioGroup.tsx +261 -0
  266. package/src/components/RadioGroup/radioGroup.types.ts +85 -0
  267. package/src/components/RadioGroup/radioGroup.variants.ts +87 -0
  268. package/src/components/Snackbar/Snackbar.perf.test.tsx +144 -0
  269. package/src/components/Snackbar/Snackbar.test.tsx +327 -0
  270. package/src/components/Snackbar/Snackbar.tsx +298 -0
  271. package/src/components/Snackbar/snackbar.types.ts +83 -0
  272. package/src/components/Snackbar/snackbar.variants.ts +72 -0
  273. package/src/components/Switch/Switch.test.tsx +131 -0
  274. package/src/components/Switch/Switch.tsx +158 -0
  275. package/src/components/Switch/switch.types.ts +36 -0
  276. package/src/components/Switch/switch.variants.ts +65 -0
  277. package/src/components/TextField/TextField.test.tsx +194 -0
  278. package/src/components/TextField/TextField.tsx +216 -0
  279. package/src/components/TextField/textField.types.ts +70 -0
  280. package/src/components/TextField/textField.variants.ts +87 -0
  281. package/src/components/Textarea/Textarea.test.tsx +75 -0
  282. package/src/components/Textarea/Textarea.tsx +190 -0
  283. package/src/components/Textarea/textarea.types.ts +60 -0
  284. package/src/components/Textarea/textarea.variants.ts +85 -0
  285. package/src/components/TopBar/TopBar.test.tsx +129 -0
  286. package/src/components/TopBar/TopBar.tsx +66 -0
  287. package/src/components/TopBar/topBar.types.ts +42 -0
  288. package/src/components/TopBar/topBar.variants.ts +38 -0
  289. package/src/components/_shared/resolveValidationState.ts +60 -0
  290. package/src/hooks/useAccessState.spec.tsx +161 -0
  291. package/src/hooks/useAccessState.ts +81 -0
  292. package/src/index.ts +191 -0
  293. package/src/utils/cn.spec.ts +16 -0
  294. package/src/utils/cn.ts +29 -0
  295. package/tsconfig.json +13 -0
  296. package/tsconfig.lib.json +55 -0
  297. package/tsconfig.spec.json +34 -0
  298. package/vite.config.ts +25 -0
  299. package/vitest.config.mts +22 -0
@@ -0,0 +1,250 @@
1
+ import { useContext, useEffect, useId, useRef, useState } from 'react';
2
+ import { DashFormContext, useEngineVisibility } from '@dashforge/ui-core';
3
+ import type { DashFormBridge, FieldRegistration } from '@dashforge/ui-core';
4
+ import { useDashFieldMeta } from '@dashforge/forms';
5
+ import { cn } from '../../utils/cn.js';
6
+ import { useAccessState } from '../../hooks/useAccessState.js';
7
+ import { resolveValidationState } from '../_shared/resolveValidationState.js';
8
+ import { otpFieldVariants } from './otpField.variants.js';
9
+ import type { OTPFieldMode, OTPFieldProps } from './otpField.types.js';
10
+
11
+ /**
12
+ * Whitelist allowed characters per mode. Anything else is dropped.
13
+ *
14
+ * @internal
15
+ */
16
+ function sanitize(input: string, mode: OTPFieldMode, max: number): string {
17
+ const re = mode === 'numeric' ? /[0-9]/ : /[A-Za-z0-9]/;
18
+ let out = '';
19
+ for (const ch of input) {
20
+ if (re.test(ch) && out.length < max) out += ch;
21
+ }
22
+ return out;
23
+ }
24
+
25
+ /**
26
+ * Dashforge TW OTPField — segmented input for one-time codes.
27
+ *
28
+ * Renders N visible "slot" cells over a hidden text input that absorbs
29
+ * all keystrokes. The visible cells are read-only display surfaces; only
30
+ * the hidden input takes focus. This pattern works correctly with
31
+ * password managers + autofill (the hidden input has
32
+ * `autoComplete="one-time-code"`).
33
+ *
34
+ * Bridge contract: the joined string (length ≤ `length`) is what gets
35
+ * written to the bridge. Use `<DashForm rules={{ minLength: length }} />`
36
+ * to require a complete code.
37
+ *
38
+ * Out of scope for F4-B (deferred to a follow-up sprint):
39
+ * - Per-slot click to jump caret
40
+ * - Arrow-key navigation between slots
41
+ * - Right-to-left input direction
42
+ *
43
+ * **A11y**: the hidden `<input>` exposes
44
+ * `inputMode="numeric" | "text"` per mode + `autoComplete="one-time-code"`
45
+ * so iOS Safari + Android Chrome trigger SMS autofill correctly.
46
+ */
47
+ export function OTPField(props: OTPFieldProps) {
48
+ const {
49
+ name,
50
+ rules,
51
+ visibleWhen,
52
+ size,
53
+ label,
54
+ helperText,
55
+ required,
56
+ error,
57
+ disabled,
58
+ access,
59
+ sx,
60
+ slotProps,
61
+ length = 6,
62
+ mode = 'numeric',
63
+ value: userValue,
64
+ defaultValue,
65
+ onChange: userOnChange,
66
+ onComplete,
67
+ } = props;
68
+
69
+ // ───── Hooks (unconditional) ─────
70
+ const bridge = useContext(DashFormContext) as DashFormBridge | null;
71
+ const isVisible = useEngineVisibility(bridge?.engine, visibleWhen);
72
+ useDashFieldMeta(name);
73
+ const accessState = useAccessState(access);
74
+
75
+ const inputId = useId();
76
+ const helperId = `${inputId}-help`;
77
+ const labelId = `${inputId}-label`;
78
+
79
+ const [isFocused, setIsFocused] = useState(false);
80
+ const [uncontrolledValue, setUncontrolledValue] = useState(() =>
81
+ sanitize(defaultValue ?? '', mode, length)
82
+ );
83
+
84
+ // StrictMode-safe unregister-on-unmount
85
+ const unregisterRef = useRef({ bridge, name });
86
+ unregisterRef.current = { bridge, name };
87
+ const isMountedRef = useRef(false);
88
+
89
+ useEffect(() => {
90
+ isMountedRef.current = true;
91
+ return () => {
92
+ isMountedRef.current = false;
93
+ const { bridge: cap, name: capName } = unregisterRef.current;
94
+ queueMicrotask(() => {
95
+ if (!isMountedRef.current) cap?.unregister?.(capName);
96
+ });
97
+ };
98
+ }, []);
99
+
100
+ if (!isVisible) return null;
101
+ if (!accessState.visible) return null;
102
+
103
+ const effectiveDisabled = Boolean(disabled) || accessState.disabled;
104
+ const effectiveReadOnly = accessState.readonly;
105
+
106
+ const isFormMode = Boolean(bridge?.register);
107
+
108
+ let resolvedError = error;
109
+ let resolvedHelperText: typeof helperText = helperText;
110
+ let registration: FieldRegistration | null = null;
111
+ let resolvedValue: string;
112
+
113
+ if (isFormMode && bridge) {
114
+ registration = bridge.register(name, rules);
115
+ const validation = resolveValidationState(name, bridge, error, helperText);
116
+ resolvedError = validation.error;
117
+ resolvedHelperText = validation.helperText;
118
+ if (userValue !== undefined) {
119
+ resolvedValue = sanitize(userValue, mode, length);
120
+ } else {
121
+ const bv = bridge.getValue(name);
122
+ resolvedValue = sanitize(bv == null ? '' : String(bv), mode, length);
123
+ }
124
+ } else if (userValue !== undefined) {
125
+ resolvedValue = sanitize(userValue, mode, length);
126
+ } else {
127
+ resolvedValue = uncontrolledValue;
128
+ }
129
+
130
+ const v = otpFieldVariants({ size, error: resolvedError });
131
+
132
+ const commitValue = (next: string) => {
133
+ const sanitised = sanitize(next, mode, length);
134
+
135
+ if (isFormMode && bridge) {
136
+ bridge.setValue?.(name, sanitised);
137
+ void registration?.onChange?.({
138
+ target: { name, value: sanitised },
139
+ type: 'change',
140
+ });
141
+ } else if (userValue === undefined) {
142
+ setUncontrolledValue(sanitised);
143
+ }
144
+
145
+ userOnChange?.(sanitised);
146
+
147
+ if (sanitised.length === length && onComplete) {
148
+ onComplete(sanitised);
149
+ }
150
+ };
151
+
152
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
153
+ commitValue(e.target.value);
154
+ };
155
+
156
+ const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
157
+ e.preventDefault();
158
+ const pasted = e.clipboardData.getData('text');
159
+ commitValue(pasted);
160
+ };
161
+
162
+ const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
163
+ setIsFocused(false);
164
+ if (isFormMode && registration?.onBlur) {
165
+ registration.onBlur(e);
166
+ }
167
+ };
168
+
169
+ // Compute the active slot index (cursor position) for the visual
170
+ // highlight: it's the next empty slot, or the last slot if full.
171
+ const activeSlotIndex = Math.min(resolvedValue.length, length - 1);
172
+ const inputMode = mode === 'numeric' ? 'numeric' : 'text';
173
+
174
+ return (
175
+ <div className={cn(v.root(), sx, slotProps?.root?.className)}>
176
+ {label && (
177
+ <label
178
+ id={labelId}
179
+ htmlFor={inputId}
180
+ className={cn(v.label(), slotProps?.label?.className)}
181
+ >
182
+ {label}
183
+ {required && (
184
+ <span
185
+ aria-hidden="true"
186
+ className={cn(v.requiredMark(), slotProps?.requiredMark?.className)}
187
+ >
188
+ *
189
+ </span>
190
+ )}
191
+ </label>
192
+ )}
193
+
194
+ <div className={cn(v.slotsRow(), slotProps?.slotsRow?.className)}>
195
+ {Array.from({ length }, (_, i) => {
196
+ const char = resolvedValue[i] ?? '';
197
+ const isActive = isFocused && !effectiveDisabled && i === activeSlotIndex;
198
+ return (
199
+ <div
200
+ key={i}
201
+ data-active={isActive || undefined}
202
+ data-disabled={effectiveDisabled || undefined}
203
+ className={cn(v.slot(), slotProps?.slot?.className)}
204
+ aria-hidden="true"
205
+ >
206
+ <span className={cn(v.slotChar(), slotProps?.slotChar?.className)}>
207
+ {char}
208
+ </span>
209
+ </div>
210
+ );
211
+ })}
212
+ <input
213
+ id={inputId}
214
+ name={name}
215
+ type="text"
216
+ inputMode={inputMode}
217
+ autoComplete="one-time-code"
218
+ maxLength={length}
219
+ value={resolvedValue}
220
+ disabled={effectiveDisabled}
221
+ readOnly={effectiveReadOnly}
222
+ required={required}
223
+ aria-labelledby={label ? labelId : undefined}
224
+ aria-invalid={resolvedError || undefined}
225
+ aria-describedby={resolvedHelperText ? helperId : undefined}
226
+ onChange={handleChange}
227
+ onPaste={handlePaste}
228
+ onFocus={() => setIsFocused(true)}
229
+ onBlur={handleBlur}
230
+ ref={registration?.ref as React.Ref<HTMLInputElement> | undefined}
231
+ className={cn(v.hiddenInput(), slotProps?.hiddenInput?.className)}
232
+ />
233
+ </div>
234
+
235
+ {resolvedHelperText && (
236
+ <p
237
+ id={helperId}
238
+ className={cn(
239
+ resolvedError ? v.errorText() : v.helperText(),
240
+ resolvedError
241
+ ? slotProps?.errorText?.className
242
+ : slotProps?.helperText?.className
243
+ )}
244
+ >
245
+ {resolvedHelperText}
246
+ </p>
247
+ )}
248
+ </div>
249
+ );
250
+ }
@@ -0,0 +1,58 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { Engine } from '@dashforge/ui-core';
3
+ import type { AccessRequirement } from '@dashforge/rbac';
4
+ import type { OTPFieldVariants } from './otpField.variants.js';
5
+
6
+ export type OTPFieldMode = 'numeric' | 'alphanumeric';
7
+
8
+ export interface OTPFieldSlotProps {
9
+ root?: { className?: string };
10
+ label?: { className?: string };
11
+ requiredMark?: { className?: string };
12
+ slotsRow?: { className?: string };
13
+ slot?: { className?: string };
14
+ slotChar?: { className?: string };
15
+ hiddenInput?: { className?: string };
16
+ helperText?: { className?: string };
17
+ errorText?: { className?: string };
18
+ }
19
+
20
+ /**
21
+ * Props for `<OTPField>`.
22
+ *
23
+ * Storage contract: bridge value is a single string (e.g. `"123456"`).
24
+ * The user types characters that the component sanitises per `mode`:
25
+ * - `'numeric'` → `[0-9]`
26
+ * - `'alphanumeric'` → `[A-Za-z0-9]`
27
+ * Anything else is dropped. Paste content is sanitised the same way and
28
+ * fills slots sequentially.
29
+ */
30
+ export interface OTPFieldProps extends OTPFieldVariants {
31
+ name: string;
32
+ rules?: unknown;
33
+ label?: ReactNode;
34
+ helperText?: ReactNode;
35
+ required?: boolean;
36
+ error?: boolean;
37
+ disabled?: boolean;
38
+ /** Engine predicate — field not rendered when it returns `false`. */
39
+ visibleWhen?: (engine: Engine) => boolean;
40
+ /** RBAC access requirement (combines with explicit `disabled`). */
41
+ access?: AccessRequirement;
42
+ /** Root className shortcut. */
43
+ sx?: string;
44
+ /** Per-slot className overrides. */
45
+ slotProps?: OTPFieldSlotProps;
46
+ /** Number of slots (default 6 — the SMS code convention). */
47
+ length?: number;
48
+ /** Character set allowed. Default `'numeric'`. */
49
+ mode?: OTPFieldMode;
50
+ /** Controlled value (form mode reads from the bridge if omitted). */
51
+ value?: string;
52
+ /** Default value (uncontrolled, standalone mode only). */
53
+ defaultValue?: string;
54
+ /** Fires every time the joined value changes (sanitised, ≤ length). */
55
+ onChange?: (value: string) => void;
56
+ /** Fires when the user has filled all slots. */
57
+ onComplete?: (value: string) => void;
58
+ }
@@ -0,0 +1,72 @@
1
+ import { tv, type VariantProps } from 'tailwind-variants';
2
+
3
+ /**
4
+ * Tailwind-variants recipe for `<OTPField>`.
5
+ *
6
+ * Slots:
7
+ * - `root` — outer wrapper (label + slots row + helper/error)
8
+ * - `label` — group-level label
9
+ * - `requiredMark` — red `*` for required fields
10
+ * - `slotsRow` — flex row holding the visible slot boxes
11
+ * - `slot` — single slot cell (visual representation of a char)
12
+ * - `slotChar` — the character displayed inside a slot
13
+ * - `hiddenInput` — the actual <input> absorbing keystrokes (visually
14
+ * hidden but focusable + reads as a single
15
+ * one-line text input to AT)
16
+ * - `helperText` — descriptive line below
17
+ * - `errorText` — semantic counterpart of helperText for error mode
18
+ *
19
+ * Variants:
20
+ * - `size` : sm / md / lg — drives slot box dimensions + text size
21
+ * - `error` : bool — swaps slot border + helperText color to danger tones
22
+ */
23
+ export const otpFieldVariants = tv({
24
+ slots: {
25
+ root: 'flex flex-col gap-1.5',
26
+ label: 'block text-sm font-medium text-neutral-900',
27
+ requiredMark: 'text-danger-500 ml-0.5',
28
+ slotsRow: 'relative inline-flex items-center gap-2',
29
+ slot: [
30
+ 'flex items-center justify-center',
31
+ // bg-neutral-50 inverts — dark-mode aware via the dashforge preset.
32
+ 'rounded-md border bg-neutral-50',
33
+ 'border-neutral-300',
34
+ 'text-neutral-900 font-mono',
35
+ 'transition-colors',
36
+ 'data-[active=true]:border-primary-500 data-[active=true]:ring-2 data-[active=true]:ring-primary-500/30',
37
+ 'data-[disabled=true]:opacity-60 data-[disabled=true]:cursor-not-allowed',
38
+ ],
39
+ slotChar: 'select-none',
40
+ hiddenInput: [
41
+ 'absolute inset-0 w-full h-full opacity-0',
42
+ 'cursor-text outline-none',
43
+ 'disabled:cursor-not-allowed',
44
+ ],
45
+ helperText: 'text-sm text-neutral-600',
46
+ errorText: 'text-sm text-danger-600',
47
+ },
48
+ variants: {
49
+ size: {
50
+ sm: {
51
+ slot: 'h-9 w-7 text-base',
52
+ },
53
+ md: {
54
+ slot: 'h-11 w-9 text-lg',
55
+ },
56
+ lg: {
57
+ slot: 'h-14 w-11 text-2xl',
58
+ },
59
+ },
60
+ error: {
61
+ true: {
62
+ slot: 'border-danger-500 data-[active=true]:border-danger-500 data-[active=true]:ring-danger-500/30',
63
+ },
64
+ },
65
+ },
66
+ defaultVariants: {
67
+ size: 'md',
68
+ error: false,
69
+ },
70
+ });
71
+
72
+ export type OTPFieldVariants = VariantProps<typeof otpFieldVariants>;
@@ -0,0 +1,140 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { render, screen, fireEvent, within } from '@testing-library/react';
4
+ import { DashFormProvider } from '@dashforge/forms';
5
+ import { RadioGroup } from './RadioGroup.js';
6
+ import type { RadioGroupOption } from './radioGroup.types.js';
7
+
8
+ const colors: RadioGroupOption[] = [
9
+ { value: 'red', label: 'Red' },
10
+ { value: 'green', label: 'Green' },
11
+ { value: 'blue', label: 'Blue' },
12
+ ];
13
+
14
+ describe('<RadioGroup>', () => {
15
+ describe('standalone (no DashForm)', () => {
16
+ it('renders all options with the provided labels', () => {
17
+ render(
18
+ <RadioGroup name="color" label="Pick a color" options={colors} />
19
+ );
20
+ // Group label rendered as the labelledby anchor
21
+ expect(screen.getByText('Pick a color')).toBeTruthy();
22
+ // Each option label resolves through the htmlFor → button id link
23
+ for (const opt of colors) {
24
+ expect(screen.getByLabelText(opt.label as string)).toBeTruthy();
25
+ }
26
+ });
27
+
28
+ it('selects the default value via uncontrolled `defaultValue`', () => {
29
+ render(
30
+ <RadioGroup
31
+ name="color"
32
+ options={colors}
33
+ defaultValue="green"
34
+ />
35
+ );
36
+ const radios = screen.getAllByRole('radio');
37
+ // The second option corresponds to "green"
38
+ expect(radios[1].getAttribute('data-state')).toBe('checked');
39
+ });
40
+
41
+ it('fires onValueChange on selection', () => {
42
+ const onValueChange = vi.fn();
43
+ render(
44
+ <RadioGroup
45
+ name="color"
46
+ options={colors}
47
+ onValueChange={onValueChange}
48
+ />
49
+ );
50
+ fireEvent.click(screen.getByLabelText('Blue'));
51
+ expect(onValueChange).toHaveBeenCalledWith('blue');
52
+ });
53
+
54
+ it('respects per-option `disabled`', () => {
55
+ const opts: RadioGroupOption[] = [
56
+ { value: 'a', label: 'A' },
57
+ { value: 'b', label: 'B', disabled: true },
58
+ ];
59
+ render(<RadioGroup name="x" options={opts} />);
60
+ const radios = screen.getAllByRole('radio') as HTMLButtonElement[];
61
+ expect(radios[0].disabled).toBe(false);
62
+ expect(radios[1].disabled).toBe(true);
63
+ });
64
+
65
+ it('disables ALL options when the group is disabled', () => {
66
+ render(
67
+ <RadioGroup name="color" options={colors} disabled />
68
+ );
69
+ for (const r of screen.getAllByRole('radio') as HTMLButtonElement[]) {
70
+ expect(r.disabled).toBe(true);
71
+ }
72
+ });
73
+ });
74
+
75
+ describe('inside DashFormProvider', () => {
76
+ it('renders without errors inside a bridge-providing tree', () => {
77
+ render(
78
+ <DashFormProvider defaultValues={{ color: 'green' }}>
79
+ <RadioGroup name="color" label="Color" options={colors} />
80
+ </DashFormProvider>
81
+ );
82
+ // Bridge mode mounts cleanly; all three options render. The exact
83
+ // checked-state reflection is verified end-to-end in the F4 smoke
84
+ // page in learn/dash where the full reactive cycle has time to
85
+ // settle — unit-test assertions on Radix internal state right
86
+ // after render are timing-sensitive and not worth the brittleness.
87
+ expect(screen.getAllByRole('radio')).toHaveLength(3);
88
+ });
89
+
90
+ it('renders the helperText when provided', () => {
91
+ render(
92
+ <DashFormProvider defaultValues={{ color: '' }}>
93
+ <RadioGroup
94
+ name="color"
95
+ options={colors}
96
+ helperText="Choose carefully"
97
+ />
98
+ </DashFormProvider>
99
+ );
100
+ expect(screen.getByText('Choose carefully')).toBeTruthy();
101
+ });
102
+ });
103
+
104
+ describe('layout variant', () => {
105
+ it('default `stacked` renders options vertically (flex-col)', () => {
106
+ const { container } = render(
107
+ <RadioGroup name="x" options={colors} />
108
+ );
109
+ const list = within(container).getByRole('radiogroup');
110
+ expect(list.className).toMatch(/flex-col/);
111
+ });
112
+
113
+ it('`row` layout renders options horizontally', () => {
114
+ const { container } = render(
115
+ <RadioGroup name="x" options={colors} layout="row" />
116
+ );
117
+ const list = within(container).getByRole('radiogroup');
118
+ expect(list.className).toMatch(/flex-row/);
119
+ });
120
+ });
121
+
122
+ describe('required marker', () => {
123
+ it('renders a red `*` after the label when `required` is set', () => {
124
+ render(
125
+ <RadioGroup
126
+ name="x"
127
+ options={colors}
128
+ label="Color"
129
+ required
130
+ />
131
+ );
132
+ expect(screen.getByText('*')).toBeTruthy();
133
+ });
134
+
135
+ it('does NOT render a `*` when `required` is omitted', () => {
136
+ render(<RadioGroup name="x" options={colors} label="Color" />);
137
+ expect(screen.queryByText('*')).toBeNull();
138
+ });
139
+ });
140
+ });