@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,85 @@
1
+ import { tv, type VariantProps } from 'tailwind-variants';
2
+
3
+ /**
4
+ * Tailwind-variants recipe for `<Textarea>` with 7 slots.
5
+ *
6
+ * Sibling of `<TextField>` but renders a multi-line `<textarea>` element
7
+ * instead of a single-line `<input>`. Shares the same slot taxonomy +
8
+ * variant axes so consumers can swap the two without rewriting overrides.
9
+ */
10
+ export const textareaVariants = tv({
11
+ slots: {
12
+ root: 'flex',
13
+ label: 'block text-sm font-medium text-neutral-900 mb-1',
14
+ requiredMark: 'text-danger-500 ml-0.5',
15
+ inputWrapper: [
16
+ 'relative flex',
17
+ // bg-neutral-50 inverts via @dashforge/tw-tokens — dark-mode aware.
18
+ 'rounded-md border bg-neutral-50',
19
+ 'border-neutral-300',
20
+ 'transition-colors',
21
+ 'focus-within:border-primary-500 focus-within:ring-2 focus-within:ring-primary-500/30',
22
+ ],
23
+ input: [
24
+ 'block w-full bg-transparent outline-none resize-y',
25
+ 'text-neutral-900 placeholder:text-neutral-400',
26
+ 'disabled:cursor-not-allowed disabled:opacity-60',
27
+ 'read-only:cursor-default',
28
+ ],
29
+ helperText: 'mt-1 text-sm text-neutral-600',
30
+ errorText: 'mt-1 text-sm text-danger-600',
31
+ },
32
+ variants: {
33
+ size: {
34
+ sm: {
35
+ inputWrapper: 'px-2 py-1.5',
36
+ input: 'text-sm',
37
+ },
38
+ md: {
39
+ inputWrapper: 'px-3 py-2',
40
+ input: 'text-base',
41
+ },
42
+ lg: {
43
+ inputWrapper: 'px-4 py-3',
44
+ input: 'text-lg',
45
+ },
46
+ },
47
+ layout: {
48
+ stacked: { root: 'flex-col' },
49
+ inline: { root: 'flex-row items-start gap-3', label: 'mb-0 pt-2 whitespace-nowrap shrink-0' },
50
+ },
51
+ error: {
52
+ true: {
53
+ inputWrapper:
54
+ 'border-danger-500 focus-within:border-danger-500 focus-within:ring-danger-500/30',
55
+ },
56
+ },
57
+ fullWidth: {
58
+ true: {
59
+ root: 'w-full',
60
+ inputWrapper: 'w-full',
61
+ },
62
+ },
63
+ disabled: {
64
+ true: {
65
+ inputWrapper: 'opacity-60 cursor-not-allowed bg-neutral-200',
66
+ },
67
+ },
68
+ resize: {
69
+ none: { input: 'resize-none' },
70
+ vertical: { input: 'resize-y' },
71
+ horizontal: { input: 'resize-x' },
72
+ both: { input: 'resize' },
73
+ },
74
+ },
75
+ defaultVariants: {
76
+ size: 'md',
77
+ layout: 'stacked',
78
+ error: false,
79
+ fullWidth: false,
80
+ disabled: false,
81
+ resize: 'vertical',
82
+ },
83
+ });
84
+
85
+ export type TextareaVariants = VariantProps<typeof textareaVariants>;
@@ -0,0 +1,129 @@
1
+ // @vitest-environment jsdom
2
+ import * as React from 'react';
3
+ import { describe, it, expect, afterEach } from 'vitest';
4
+ import { render, screen, cleanup } from '@testing-library/react';
5
+ import { TopBar } from './TopBar.js';
6
+
7
+ void React;
8
+ afterEach(() => cleanup());
9
+
10
+ describe('<TopBar>', () => {
11
+ it('renders as a banner landmark by default (<header>)', () => {
12
+ render(<TopBar end={<span>x</span>} />);
13
+ expect(screen.getByRole('banner')).toBeTruthy();
14
+ });
15
+
16
+ it('renders as a plain <div> when asDiv is true', () => {
17
+ const { container } = render(<TopBar end={<span>x</span>} asDiv />);
18
+ expect(container.querySelector('header')).toBeNull();
19
+ expect(container.querySelector('div')).toBeTruthy();
20
+ });
21
+
22
+ it('renders the start slot', () => {
23
+ render(<TopBar start={<button>menu</button>} />);
24
+ expect(screen.getByRole('button', { name: 'menu' })).toBeTruthy();
25
+ });
26
+
27
+ it('renders the center slot', () => {
28
+ render(<TopBar center={<span data-testid="c">page</span>} />);
29
+ expect(screen.getByTestId('c')).toBeTruthy();
30
+ });
31
+
32
+ it('renders the end slot', () => {
33
+ render(<TopBar end={<button>user</button>} />);
34
+ expect(screen.getByRole('button', { name: 'user' })).toBeTruthy();
35
+ });
36
+
37
+ it('renders children as the center slot when `center` is not provided', () => {
38
+ render(
39
+ <TopBar>
40
+ <span data-testid="child">title</span>
41
+ </TopBar>
42
+ );
43
+ expect(screen.getByTestId('child')).toBeTruthy();
44
+ });
45
+
46
+ it('prefers explicit `center` over children', () => {
47
+ render(
48
+ <TopBar center={<span data-testid="explicit-center">c</span>}>
49
+ <span data-testid="child">should-not-render</span>
50
+ </TopBar>
51
+ );
52
+ expect(screen.getByTestId('explicit-center')).toBeTruthy();
53
+ expect(screen.queryByTestId('child')).toBeNull();
54
+ });
55
+
56
+ it('does not render start / end wrappers when those props are absent', () => {
57
+ const { container } = render(<TopBar center={<span>c</span>} />);
58
+ // Inner divs: only the center div is rendered when start/end are
59
+ // both undefined (we render `<div>{center ?? children}</div>` always).
60
+ expect(container.querySelectorAll('header > div')).toHaveLength(1);
61
+ });
62
+
63
+ it('applies the sx className to the root', () => {
64
+ const { container } = render(
65
+ <TopBar center={<span>c</span>} sx="custom-class" />
66
+ );
67
+ expect(container.querySelector('header.custom-class')).toBeTruthy();
68
+ });
69
+
70
+ it('renders height variants without throwing', () => {
71
+ (['sm', 'md', 'lg'] as const).forEach((h) => {
72
+ const { unmount } = render(<TopBar center={<span>c</span>} height={h} />);
73
+ expect(screen.getByRole('banner')).toBeTruthy();
74
+ unmount();
75
+ });
76
+ });
77
+ });
78
+
79
+ describe('<TopBar> performance', () => {
80
+ it('mounts in under 20ms', () => {
81
+ const t0 = performance.now();
82
+ render(
83
+ <TopBar
84
+ start={<button>start</button>}
85
+ center={<span>center</span>}
86
+ end={<button>end</button>}
87
+ />
88
+ );
89
+ const t1 = performance.now();
90
+ expect(t1 - t0).toBeLessThan(20);
91
+ });
92
+
93
+ it('renders at most 2 times on mount', () => {
94
+ let count = 0;
95
+ const Counted = (props: React.ComponentProps<typeof TopBar>) => {
96
+ count++;
97
+ return <TopBar {...props} />;
98
+ };
99
+ render(
100
+ <Counted
101
+ start={<button>s</button>}
102
+ center={<span>c</span>}
103
+ end={<button>e</button>}
104
+ />
105
+ );
106
+ expect(count).toBeLessThanOrEqual(2);
107
+ });
108
+
109
+ it('does NOT re-render an unrelated sibling on prop change', () => {
110
+ const sibCount = { count: 0 };
111
+ const Sibling = () => {
112
+ sibCount.count++;
113
+ return <div data-testid="sib" />;
114
+ };
115
+ const Tree = ({ label }: { label: string }) => (
116
+ <div>
117
+ <TopBar center={<span>{label}</span>} />
118
+ <Sibling />
119
+ </div>
120
+ );
121
+ const { rerender } = render(<Tree label="A" />);
122
+ const before = sibCount.count;
123
+ rerender(<Tree label="B" />);
124
+ rerender(<Tree label="C" />);
125
+ // Each parent rerender is expected to also re-render the sibling.
126
+ // 2 parent rerenders ⇒ ≤ 2 extra sibling renders (+1 tolerance).
127
+ expect(sibCount.count - before).toBeLessThanOrEqual(3);
128
+ });
129
+ });
@@ -0,0 +1,66 @@
1
+ import { cn } from '../../utils/cn.js';
2
+ import { topBarVariants } from './topBar.variants.js';
3
+ import type { TopBarProps } from './topBar.types.js';
4
+
5
+ /**
6
+ * Dashforge TW TopBar — sticky header with 3 named slots.
7
+ *
8
+ * Pure composition. Three regions arranged horizontally:
9
+ *
10
+ * ┌──────────────────────────────────────────────────────────┐
11
+ * │ start │ center │ end │
12
+ * └──────────────────────────────────────────────────────────┘
13
+ *
14
+ * - `start` and `end` shrink to content.
15
+ * - `center` grows to fill remaining space.
16
+ * - If you pass `children` instead of `center`, those children render
17
+ * in the center region — handy for terse JSX:
18
+ *
19
+ * ```tsx
20
+ * <TopBar start={<Logo />} end={<UserMenu />}>
21
+ * <Breadcrumbs items={trail} />
22
+ * </TopBar>
23
+ * ```
24
+ *
25
+ * Default renders as `<header role="banner">` (implicit from
26
+ * `<header>` as a top-level landmark). Pass `asDiv` for cases where
27
+ * you don't want the banner role (e.g., nested inside another header).
28
+ *
29
+ * **A11y**:
30
+ * - `<header>` is a banner landmark — only one per page; if you put
31
+ * multiple TopBars on a page, use `asDiv` on the secondary ones.
32
+ * - Slots are agnostic about ARIA — wrap your slot content with
33
+ * appropriate roles (`<nav>`, `<search>`, etc.) where needed.
34
+ */
35
+ export function TopBar(props: TopBarProps) {
36
+ const {
37
+ start,
38
+ center,
39
+ end,
40
+ children,
41
+ height,
42
+ sticky,
43
+ asDiv,
44
+ sx,
45
+ slotProps,
46
+ } = props;
47
+
48
+ const v = topBarVariants({ height, sticky });
49
+ const Tag = (asDiv ? 'div' : 'header') as 'header';
50
+
51
+ return (
52
+ <Tag className={cn(v.root(), sx, slotProps?.root?.className)}>
53
+ {(start != null) && (
54
+ <div className={cn(v.start(), slotProps?.start?.className)}>
55
+ {start}
56
+ </div>
57
+ )}
58
+ <div className={cn(v.center(), slotProps?.center?.className)}>
59
+ {center ?? children}
60
+ </div>
61
+ {(end != null) && (
62
+ <div className={cn(v.end(), slotProps?.end?.className)}>{end}</div>
63
+ )}
64
+ </Tag>
65
+ );
66
+ }
@@ -0,0 +1,42 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { TopBarVariants } from './topBar.variants.js';
3
+
4
+ export interface TopBarSlotProps {
5
+ root?: { className?: string };
6
+ start?: { className?: string };
7
+ center?: { className?: string };
8
+ end?: { className?: string };
9
+ }
10
+
11
+ /**
12
+ * Props for `<TopBar>`.
13
+ *
14
+ * Pure composition container with 3 named slots — `start`, `center`,
15
+ * `end` — that fill a sticky horizontal bar.
16
+ *
17
+ * Typical patterns:
18
+ *
19
+ * - **start**: mobile menu toggle, brand/logo
20
+ * - **center**: Breadcrumbs, page title, global search
21
+ * - **end**: notifications icon, user menu, theme toggle
22
+ *
23
+ * The component is intentionally minimal: it just lays out the three
24
+ * regions. All semantics (e.g., `<header role="banner">`) come for
25
+ * free from rendering as an HTML `<header>` element.
26
+ */
27
+ export interface TopBarProps extends TopBarVariants {
28
+ /** Left-aligned slot (mobile menu, brand). */
29
+ start?: ReactNode;
30
+ /** Center slot (breadcrumbs, title). Grows to fill remaining space. */
31
+ center?: ReactNode;
32
+ /** Right-aligned slot (actions, user menu). */
33
+ end?: ReactNode;
34
+ /** Convenience: render the bar as a plain `<div>` (no banner role). */
35
+ asDiv?: boolean;
36
+ /** Root className shortcut. */
37
+ sx?: string;
38
+ /** Per-slot className overrides. */
39
+ slotProps?: TopBarSlotProps;
40
+ /** Children render between `start` and `end` when no `center` is set. */
41
+ children?: ReactNode;
42
+ }
@@ -0,0 +1,38 @@
1
+ import { tv, type VariantProps } from 'tailwind-variants';
2
+
3
+ /**
4
+ * Tailwind-variants recipe for `<TopBar>`.
5
+ *
6
+ * Slots:
7
+ * - `root` — outer `<header>` (or `<div>` with `asDiv`)
8
+ * - `start` — left region (shrink-to-content)
9
+ * - `center` — middle region (grows to fill)
10
+ * - `end` — right region (shrink-to-content)
11
+ */
12
+ export const topBarVariants = tv({
13
+ slots: {
14
+ root: [
15
+ 'flex items-center gap-3 w-full px-4 shrink-0',
16
+ 'bg-neutral-50 border-b border-neutral-200',
17
+ ],
18
+ start: 'flex items-center gap-2 min-w-0 shrink-0',
19
+ center: 'flex items-center gap-2 min-w-0 flex-1',
20
+ end: 'flex items-center gap-2 shrink-0',
21
+ },
22
+ variants: {
23
+ height: {
24
+ sm: { root: 'h-12 text-sm' },
25
+ md: { root: 'h-14' },
26
+ lg: { root: 'h-16' },
27
+ },
28
+ sticky: {
29
+ true: { root: 'sticky top-0 z-30' },
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ height: 'md',
34
+ sticky: true,
35
+ },
36
+ });
37
+
38
+ export type TopBarVariants = VariantProps<typeof topBarVariants>;
@@ -0,0 +1,60 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { DashFormBridge } from '@dashforge/ui-core';
3
+
4
+ /**
5
+ * Resolved validation state for a bridge-managed field.
6
+ *
7
+ * Returned by `resolveValidationState`, this is consumed by all form
8
+ * components that surface error + helper text (TextField, Checkbox,
9
+ * Switch, …) to derive their final render state.
10
+ */
11
+ export interface ValidationState {
12
+ error: boolean;
13
+ helperText: ReactNode;
14
+ }
15
+
16
+ /**
17
+ * Compute `error` + `helperText` for a bridge-managed field.
18
+ *
19
+ * **Renderer-agnostic** — copy of the MUI-side
20
+ * `textField.validation.ts`. Promoted to a `_shared/` directory inside
21
+ * `@dashforge/tw` so all form components (TextField + Checkbox +
22
+ * Switch) reuse the same precedence logic without depending on a
23
+ * specific component folder.
24
+ *
25
+ * Precedence rules (matches MUI side byte-for-byte):
26
+ *
27
+ * 1. **Explicit props win.** If the user passed `error` / `helperText`
28
+ * explicitly, those values are used as-is.
29
+ * 2. **Auto values are gated by interaction.** The bridge's error is
30
+ * surfaced only when the field is `touched` OR the form has been
31
+ * submitted at least once (`submitCount > 0`). This prevents the
32
+ * "error spam while typing" anti-pattern.
33
+ * 3. **`error === false` explicitly clears the bridge's error message
34
+ * text.** Useful for forcing a "valid" visual state regardless of
35
+ * bridge state.
36
+ *
37
+ * @param name field name registered with the bridge
38
+ * @param bridge active `DashFormBridge` (guaranteed non-null at
39
+ * call sites by the surrounding `if (bridge)`)
40
+ * @param explicitError `error` prop from the consumer
41
+ * @param explicitHelperText `helperText` prop from the consumer
42
+ */
43
+ export function resolveValidationState(
44
+ name: string,
45
+ bridge: DashFormBridge,
46
+ explicitError: boolean | undefined,
47
+ explicitHelperText: ReactNode | undefined
48
+ ): ValidationState {
49
+ const autoErr = bridge.getError(name) ?? null;
50
+ const autoTouched = bridge.isTouched(name) ?? false;
51
+ const submitCount = bridge.submitCount ?? 0;
52
+
53
+ const allowAutoError = autoTouched || submitCount > 0;
54
+
55
+ const error = explicitError ?? (Boolean(autoErr) && allowAutoError);
56
+ const helperText =
57
+ explicitHelperText ?? (allowAutoError ? autoErr?.message : undefined);
58
+
59
+ return { error, helperText };
60
+ }
@@ -0,0 +1,161 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import { renderHook } from '@testing-library/react';
4
+ import { RbacProvider } from '@dashforge/rbac';
5
+ import type {
6
+ RbacPolicy,
7
+ Subject,
8
+ AccessRequirement,
9
+ } from '@dashforge/rbac';
10
+ import { useAccessState } from './useAccessState.js';
11
+
12
+ /**
13
+ * TW-side copy of the MUI useAccessState unit test, kept independent
14
+ * per the isolation contract. The hook contract is identical so the
15
+ * MUI test cases double as the source of truth for behaviour parity.
16
+ */
17
+
18
+ const FULL_ACCESS_POLICY: RbacPolicy = {
19
+ roles: [
20
+ {
21
+ name: 'admin',
22
+ permissions: [{ resource: '*', action: '*', effect: 'allow' }],
23
+ },
24
+ ],
25
+ };
26
+
27
+ const READ_ONLY_POLICY: RbacPolicy = {
28
+ roles: [
29
+ {
30
+ name: 'viewer',
31
+ permissions: [
32
+ { resource: 'document', action: 'read', effect: 'allow' },
33
+ { resource: 'document', action: 'update', effect: 'deny' },
34
+ ],
35
+ },
36
+ ],
37
+ };
38
+
39
+ const adminSubject: Subject = { id: 'admin-user', roles: ['admin'] };
40
+ const viewerSubject: Subject = { id: 'viewer-user', roles: ['viewer'] };
41
+
42
+ function wrapWithRbac(policy: RbacPolicy, subject: Subject) {
43
+ return function Wrapper({ children }: { children: React.ReactNode }) {
44
+ return (
45
+ <RbacProvider policy={policy} subject={subject}>
46
+ {children}
47
+ </RbacProvider>
48
+ );
49
+ };
50
+ }
51
+
52
+ describe('useAccessState (tw)', () => {
53
+ describe('no access requirement', () => {
54
+ it('returns default full-access state when access is undefined', () => {
55
+ const { result } = renderHook(() => useAccessState(undefined));
56
+ expect(result.current).toEqual({
57
+ visible: true,
58
+ disabled: false,
59
+ readonly: false,
60
+ granted: true,
61
+ });
62
+ });
63
+
64
+ it('returns stable identity across re-renders', () => {
65
+ const { result, rerender } = renderHook(() => useAccessState(undefined));
66
+ const first = result.current;
67
+ rerender();
68
+ expect(result.current).toBe(first);
69
+ });
70
+ });
71
+
72
+ describe('access requirement WITHOUT RbacProvider (fail-safe)', () => {
73
+ let warnSpy: ReturnType<typeof vi.spyOn>;
74
+ beforeEach(() => {
75
+ warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
76
+ });
77
+ afterEach(() => {
78
+ warnSpy.mockRestore();
79
+ });
80
+
81
+ it('returns DEFAULT_ACCESS_STATE (fails open) when no provider mounted', () => {
82
+ const access: AccessRequirement = { resource: 'doc', action: 'read' };
83
+ const { result } = renderHook(() => useAccessState(access));
84
+ expect(result.current.visible).toBe(true);
85
+ expect(result.current.granted).toBe(true);
86
+ });
87
+
88
+ it('logs a dev-mode warning naming the missing requirement', () => {
89
+ const access: AccessRequirement = { resource: 'doc', action: 'update' };
90
+ renderHook(() => useAccessState(access));
91
+ expect(warnSpy).toHaveBeenCalled();
92
+ const msg = String(warnSpy.mock.calls[0]?.[0] ?? '');
93
+ expect(msg).toContain('No RbacProvider');
94
+ expect(msg).toContain('doc');
95
+ expect(msg).toContain('update');
96
+ });
97
+ });
98
+
99
+ describe('access requirement WITH RbacProvider', () => {
100
+ it('grants when policy allows', () => {
101
+ const access: AccessRequirement = { resource: 'document', action: 'read' };
102
+ const { result } = renderHook(() => useAccessState(access), {
103
+ wrapper: wrapWithRbac(FULL_ACCESS_POLICY, adminSubject),
104
+ });
105
+ expect(result.current.granted).toBe(true);
106
+ expect(result.current.visible).toBe(true);
107
+ });
108
+
109
+ it('denies (hide) when policy forbids and onUnauthorized=hide', () => {
110
+ const access: AccessRequirement = {
111
+ resource: 'document',
112
+ action: 'update',
113
+ onUnauthorized: 'hide',
114
+ };
115
+ const { result } = renderHook(() => useAccessState(access), {
116
+ wrapper: wrapWithRbac(READ_ONLY_POLICY, viewerSubject),
117
+ });
118
+ expect(result.current.granted).toBe(false);
119
+ expect(result.current.visible).toBe(false);
120
+ });
121
+
122
+ it('denies (disable) when policy forbids and onUnauthorized=disable', () => {
123
+ const access: AccessRequirement = {
124
+ resource: 'document',
125
+ action: 'update',
126
+ onUnauthorized: 'disable',
127
+ };
128
+ const { result } = renderHook(() => useAccessState(access), {
129
+ wrapper: wrapWithRbac(READ_ONLY_POLICY, viewerSubject),
130
+ });
131
+ expect(result.current.granted).toBe(false);
132
+ expect(result.current.visible).toBe(true);
133
+ expect(result.current.disabled).toBe(true);
134
+ });
135
+
136
+ it('denies (readonly) when policy forbids and onUnauthorized=readonly', () => {
137
+ const access: AccessRequirement = {
138
+ resource: 'document',
139
+ action: 'update',
140
+ onUnauthorized: 'readonly',
141
+ };
142
+ const { result } = renderHook(() => useAccessState(access), {
143
+ wrapper: wrapWithRbac(READ_ONLY_POLICY, viewerSubject),
144
+ });
145
+ expect(result.current.granted).toBe(false);
146
+ expect(result.current.readonly).toBe(true);
147
+ });
148
+ });
149
+
150
+ describe('memoization', () => {
151
+ it('returns identical reference when access reference stays the same', () => {
152
+ const access: AccessRequirement = { resource: 'doc', action: 'read' };
153
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
154
+ const { result, rerender } = renderHook(() => useAccessState(access));
155
+ const first = result.current;
156
+ rerender();
157
+ expect(result.current).toBe(first);
158
+ warnSpy.mockRestore();
159
+ });
160
+ });
161
+ });
@@ -0,0 +1,81 @@
1
+ import { useMemo } from 'react';
2
+ import { useRbacOptional, resolveAccessState } from '@dashforge/rbac';
3
+ import type { AccessRequirement, AccessState } from '@dashforge/rbac';
4
+
5
+ /**
6
+ * Default full-access state. Returned when no access requirement is
7
+ * supplied **or** when no `RbacProvider` is mounted (fail-safe default).
8
+ *
9
+ * @internal
10
+ */
11
+ const DEFAULT_ACCESS_STATE: AccessState = {
12
+ visible: true,
13
+ disabled: false,
14
+ readonly: false,
15
+ granted: true,
16
+ };
17
+
18
+ /**
19
+ * Hook to resolve RBAC access state for a UI component.
20
+ *
21
+ * **TW-side copy of the MUI implementation** in `@dashforge/ui/src/hooks/useAccessState.ts`.
22
+ * Per the architecture plan v2 (2026-05-15) the two ecosystems share
23
+ * only the bridge layer (`@dashforge/forms` + `@dashforge/ui-core` +
24
+ * `@dashforge/rbac`); UI-side hooks are duplicated intentionally so
25
+ * the MUI and TW ecosystems remain fully isolated.
26
+ *
27
+ * The behaviour is byte-equivalent to the MUI side:
28
+ *
29
+ * - If `access` is undefined → returns `DEFAULT_ACCESS_STATE`.
30
+ * - If `access` is supplied but no `RbacProvider` is mounted → returns
31
+ * `DEFAULT_ACCESS_STATE` and emits a dev-only `console.warn` so the
32
+ * developer notices the missing provider. Failing **open** is the
33
+ * safer default (RBAC silently denying everything would be a hidden
34
+ * regression in development).
35
+ * - Otherwise → evaluates the requirement against the active
36
+ * `rbac.engine` + `rbac.subject` via `resolveAccessState`.
37
+ *
38
+ * The memo dependency list is `[access, rbac]` — a stable RBAC
39
+ * context + identical access requirement re-uses the same result
40
+ * object so downstream `useMemo` / `React.memo` comparisons stay
41
+ * cheap.
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * function MyComponent({ access }: { access?: AccessRequirement }) {
46
+ * const accessState = useAccessState(access);
47
+ * if (!accessState.visible) return null;
48
+ * return <input disabled={accessState.disabled} readOnly={accessState.readonly} />;
49
+ * }
50
+ * ```
51
+ */
52
+ export function useAccessState(
53
+ access: AccessRequirement | undefined
54
+ ): AccessState {
55
+ // Hooks are called unconditionally to comply with the rules of hooks.
56
+ // `useRbacOptional` returns `null` when no provider is mounted
57
+ // (it does NOT throw — that's intentional, see its doc).
58
+ const rbac = useRbacOptional();
59
+
60
+ return useMemo(() => {
61
+ if (!access) {
62
+ return DEFAULT_ACCESS_STATE;
63
+ }
64
+
65
+ if (!rbac) {
66
+ if (process.env.NODE_ENV !== 'production') {
67
+
68
+ console.warn(
69
+ '[useAccessState] No RbacProvider found but access requirement was provided. ' +
70
+ 'Defaulting to full access. Wrap your component tree with <RbacProvider> to enable RBAC. ' +
71
+ `Resource: ${access.resource}, Action: ${access.action}`
72
+ );
73
+ }
74
+ return DEFAULT_ACCESS_STATE;
75
+ }
76
+
77
+ return resolveAccessState(access, (request) =>
78
+ rbac.engine.can(rbac.subject, request)
79
+ );
80
+ }, [access, rbac]);
81
+ }