@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,172 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Performance + re-render budget tests for `<LeftNav>`.
4
+ *
5
+ * Layout sidebar is the most expensive layout primitive — it can
6
+ * easily host 50+ rows, each with RBAC checks. Guardrails:
7
+ *
8
+ * 1. Mount cost bounded for realistic + worst-case row counts.
9
+ * 2. `activeId` change only re-renders the rows that need re-styling
10
+ * (today: all rows re-render because we don't `React.memo` them,
11
+ * but the total time is still bounded).
12
+ * 3. Group expand/collapse only re-renders THAT group.
13
+ * 4. Toggling `collapsed` does not allocate per-item maps.
14
+ */
15
+ import * as React from 'react';
16
+ import { describe, it, expect, afterEach } from 'vitest';
17
+ import {
18
+ render,
19
+ screen,
20
+ fireEvent,
21
+ cleanup,
22
+ } from '@testing-library/react';
23
+ import { LeftNav } from './LeftNav.js';
24
+ import type { LeftNavNode } from './leftNav.types.js';
25
+
26
+ void React;
27
+ afterEach(() => cleanup());
28
+
29
+ function makeFlatNav(n: number): LeftNavNode[] {
30
+ return Array.from({ length: n }, (_, i) => ({
31
+ id: `r${i}`,
32
+ label: `Row ${i}`,
33
+ href: `/r${i}`,
34
+ }));
35
+ }
36
+
37
+ function makeNestedNav(groups: number, perGroup: number): LeftNavNode[] {
38
+ return Array.from({ length: groups }, (_, g) => ({
39
+ kind: 'group' as const,
40
+ id: `g${g}`,
41
+ label: `Group ${g}`,
42
+ children: Array.from({ length: perGroup }, (_, i) => ({
43
+ id: `g${g}-r${i}`,
44
+ label: `Row ${i}`,
45
+ href: `/g${g}/r${i}`,
46
+ })),
47
+ }));
48
+ }
49
+
50
+ function withRenderCounter<P>(
51
+ Component: React.ComponentType<P>,
52
+ counterRef: { count: number }
53
+ ): React.FC<P> {
54
+ return function Counted(props: P) {
55
+ counterRef.count++;
56
+ return <Component {...(props as P & object)} />;
57
+ };
58
+ }
59
+
60
+ describe('<LeftNav> performance', () => {
61
+ it('mounts a realistic 30-row flat nav in under 50ms', () => {
62
+ const items = makeFlatNav(30);
63
+ const t0 = performance.now();
64
+ render(<LeftNav items={items} />);
65
+ const t1 = performance.now();
66
+ expect(t1 - t0).toBeLessThan(50);
67
+ });
68
+
69
+ it('mounts a 300-row flat nav in under 250ms', () => {
70
+ const items = makeFlatNav(300);
71
+ const t0 = performance.now();
72
+ render(<LeftNav items={items} />);
73
+ const t1 = performance.now();
74
+ expect(t1 - t0).toBeLessThan(250);
75
+ });
76
+
77
+ it('mounts a 10×10 nested nav (100 leaf rows) in under 150ms', () => {
78
+ const items = makeNestedNav(10, 10);
79
+ const t0 = performance.now();
80
+ render(<LeftNav items={items} />);
81
+ const t1 = performance.now();
82
+ expect(t1 - t0).toBeLessThan(150);
83
+ });
84
+
85
+ it('toggles `collapsed` in under 100ms (30 rows)', () => {
86
+ const items = makeFlatNav(30);
87
+ const { rerender } = render(<LeftNav items={items} />);
88
+ const t0 = performance.now();
89
+ rerender(<LeftNav items={items} collapsed />);
90
+ const t1 = performance.now();
91
+ expect(t1 - t0).toBeLessThan(100);
92
+ });
93
+ });
94
+
95
+ describe('<LeftNav> re-render budget', () => {
96
+ it('renders at most 2 times on mount (top-level wrapper)', () => {
97
+ const counter = { count: 0 };
98
+ const Counted = withRenderCounter(LeftNav, counter);
99
+ render(<Counted items={makeFlatNav(10)} />);
100
+ expect(counter.count).toBeLessThanOrEqual(2);
101
+ });
102
+
103
+ it('does NOT re-render the wrapper when a group expands locally', () => {
104
+ // Group state is local — flipping it must not bubble a re-render
105
+ // up to the LeftNav wrapper.
106
+ const counter = { count: 0 };
107
+ const Counted = withRenderCounter(LeftNav, counter);
108
+ render(<Counted items={makeNestedNav(2, 3)} />);
109
+ const baseline = counter.count;
110
+ const header = screen.getByRole('button', { name: /Group 0/ });
111
+ fireEvent.click(header);
112
+ fireEvent.click(header);
113
+ fireEvent.click(header);
114
+ expect(counter.count).toBe(baseline);
115
+ });
116
+
117
+ it('does NOT re-render an unrelated sibling when collapsing the nav', () => {
118
+ const sibCount = { count: 0 };
119
+ const Sibling = () => {
120
+ sibCount.count++;
121
+ return <div data-testid="sib" />;
122
+ };
123
+ const Shell = ({ collapsed }: { collapsed: boolean }) => (
124
+ <div>
125
+ <LeftNav items={makeFlatNav(20)} collapsed={collapsed} />
126
+ <Sibling />
127
+ </div>
128
+ );
129
+ const { rerender } = render(<Shell collapsed={false} />);
130
+ const baseline = sibCount.count;
131
+ rerender(<Shell collapsed={true} />);
132
+ // 1 parent re-render → 1 sibling re-render. Allow 1 extra for
133
+ // React strict-mode or commit-phase double-invocation in dev.
134
+ expect(sibCount.count - baseline).toBeLessThanOrEqual(2);
135
+ });
136
+
137
+ it('activeId change updates aria-current without throwing', () => {
138
+ const items = makeFlatNav(5);
139
+ const { rerender } = render(
140
+ <LeftNav items={items} activeId="r0" />
141
+ );
142
+ expect(
143
+ screen.getByRole('link', { name: 'Row 0' }).getAttribute('aria-current')
144
+ ).toBe('page');
145
+ rerender(<LeftNav items={items} activeId="r3" />);
146
+ expect(
147
+ screen.getByRole('link', { name: 'Row 0' }).getAttribute('aria-current')
148
+ ).toBeNull();
149
+ expect(
150
+ screen.getByRole('link', { name: 'Row 3' }).getAttribute('aria-current')
151
+ ).toBe('page');
152
+ });
153
+
154
+ it('group expanded state is preserved across parent re-renders', () => {
155
+ // Local state inside `<NavGroupRow>` must survive a parent
156
+ // re-render. Without this, every `activeId` change would reset
157
+ // every group to its `defaultExpanded`, which would be terrible UX.
158
+ const items = makeNestedNav(2, 3);
159
+ const { container, rerender } = render(
160
+ <LeftNav items={items} activeId="g0-r0" />
161
+ );
162
+ // Collapse Group 0 — Group 0's first child href becomes invisible.
163
+ fireEvent.click(screen.getByRole('button', { name: /Group 0/ }));
164
+ const g0Child = () =>
165
+ container.querySelector('a[href="/g0/r0"]');
166
+ expect(g0Child()).toBeNull();
167
+ // Now change activeId to a row in Group 1.
168
+ rerender(<LeftNav items={items} activeId="g1-r0" />);
169
+ // Group 0 must STAY collapsed (local state survived parent re-render).
170
+ expect(g0Child()).toBeNull();
171
+ });
172
+ });
@@ -0,0 +1,302 @@
1
+ // @vitest-environment jsdom
2
+ import * as React from 'react';
3
+ import { describe, it, expect, vi, afterEach } from 'vitest';
4
+ import {
5
+ render,
6
+ screen,
7
+ fireEvent,
8
+ cleanup,
9
+ } from '@testing-library/react';
10
+ import { LeftNav } from './LeftNav.js';
11
+ import type { LeftNavNode } from './leftNav.types.js';
12
+
13
+ void React;
14
+ afterEach(() => cleanup());
15
+
16
+ const flatNav: LeftNavNode[] = [
17
+ { id: 'home', label: 'Home', href: '/' },
18
+ { id: 'reports', label: 'Reports', href: '/reports' },
19
+ { id: 'settings', label: 'Settings', href: '/settings' },
20
+ ];
21
+
22
+ const nestedNav: LeftNavNode[] = [
23
+ { id: 'home', label: 'Home', href: '/' },
24
+ {
25
+ kind: 'group',
26
+ id: 'sales',
27
+ label: 'Sales',
28
+ children: [
29
+ { id: 'invoices', label: 'Invoices', href: '/invoices' },
30
+ { id: 'customers', label: 'Customers', href: '/customers' },
31
+ ],
32
+ },
33
+ ];
34
+
35
+ describe('<LeftNav>', () => {
36
+ describe('rendering', () => {
37
+ it('renders a nav landmark with the default aria-label', () => {
38
+ render(<LeftNav items={flatNav} />);
39
+ expect(
40
+ screen.getByRole('navigation', { name: 'Main navigation' })
41
+ ).toBeTruthy();
42
+ });
43
+
44
+ it('renders a custom aria-label', () => {
45
+ render(<LeftNav items={flatNav} ariaLabel="App nav" />);
46
+ expect(
47
+ screen.getByRole('navigation', { name: 'App nav' })
48
+ ).toBeTruthy();
49
+ });
50
+
51
+ it('renders one link per flat item', () => {
52
+ render(<LeftNav items={flatNav} />);
53
+ expect(screen.getAllByRole('link')).toHaveLength(3);
54
+ });
55
+
56
+ it('renders a brand slot', () => {
57
+ render(
58
+ <LeftNav
59
+ items={flatNav}
60
+ brand={<span data-testid="brand">Dashforge</span>}
61
+ />
62
+ );
63
+ expect(screen.getByTestId('brand')).toBeTruthy();
64
+ });
65
+
66
+ it('renders a footer slot', () => {
67
+ render(
68
+ <LeftNav
69
+ items={flatNav}
70
+ footer={<span data-testid="footer">user@x.io</span>}
71
+ />
72
+ );
73
+ expect(screen.getByTestId('footer')).toBeTruthy();
74
+ });
75
+ });
76
+
77
+ describe('active item', () => {
78
+ it('marks the active item with aria-current="page"', () => {
79
+ render(<LeftNav items={flatNav} activeId="reports" />);
80
+ const link = screen.getByRole('link', { name: 'Reports' });
81
+ expect(link.getAttribute('aria-current')).toBe('page');
82
+ });
83
+
84
+ it('marks NO item when activeId is missing or unknown', () => {
85
+ render(<LeftNav items={flatNav} activeId="unknown" />);
86
+ const links = screen.getAllByRole('link');
87
+ links.forEach((l) => {
88
+ expect(l.getAttribute('aria-current')).toBeNull();
89
+ });
90
+ });
91
+ });
92
+
93
+ describe('items without href render as button', () => {
94
+ it('renders a button when only onSelect is provided', () => {
95
+ const onSelect = vi.fn();
96
+ render(
97
+ <LeftNav
98
+ items={[{ id: 'logout', label: 'Logout', onSelect }]}
99
+ />
100
+ );
101
+ const btn = screen.getByRole('button', { name: 'Logout' });
102
+ expect(btn.tagName).toBe('BUTTON');
103
+ fireEvent.click(btn);
104
+ expect(onSelect).toHaveBeenCalled();
105
+ });
106
+ });
107
+
108
+ describe('disabled state', () => {
109
+ it('does not call onSelect on a disabled item', () => {
110
+ const onSelect = vi.fn();
111
+ render(
112
+ <LeftNav
113
+ items={[{ id: 'x', label: 'X', onSelect, disabled: true }]}
114
+ />
115
+ );
116
+ fireEvent.click(screen.getByRole('button', { name: 'X' }));
117
+ expect(onSelect).not.toHaveBeenCalled();
118
+ });
119
+
120
+ it('marks the disabled link with aria-disabled', () => {
121
+ render(
122
+ <LeftNav
123
+ items={[
124
+ { id: 'x', label: 'X', href: '/x', disabled: true },
125
+ ]}
126
+ />
127
+ );
128
+ const link = screen.getByRole('link', { name: 'X' });
129
+ expect(link.getAttribute('aria-disabled')).toBe('true');
130
+ });
131
+ });
132
+
133
+ describe('groups (nested)', () => {
134
+ it('renders a group header as a button with aria-expanded', () => {
135
+ render(<LeftNav items={nestedNav} />);
136
+ const header = screen.getByRole('button', { name: /Sales/ });
137
+ expect(header.getAttribute('aria-expanded')).toBe('true');
138
+ });
139
+
140
+ it('shows group children when expanded (default true)', () => {
141
+ render(<LeftNav items={nestedNav} />);
142
+ expect(screen.getByRole('link', { name: 'Invoices' })).toBeTruthy();
143
+ expect(screen.getByRole('link', { name: 'Customers' })).toBeTruthy();
144
+ });
145
+
146
+ it('hides group children after the user clicks the header', () => {
147
+ render(<LeftNav items={nestedNav} />);
148
+ const header = screen.getByRole('button', { name: /Sales/ });
149
+ fireEvent.click(header);
150
+ expect(screen.queryByRole('link', { name: 'Invoices' })).toBeNull();
151
+ });
152
+
153
+ it('starts collapsed when defaultExpanded=false', () => {
154
+ const items: LeftNavNode[] = [
155
+ {
156
+ kind: 'group',
157
+ id: 'g',
158
+ label: 'Group',
159
+ defaultExpanded: false,
160
+ children: [{ id: 'c', label: 'Child', href: '/c' }],
161
+ },
162
+ ];
163
+ render(<LeftNav items={items} />);
164
+ expect(screen.queryByRole('link', { name: 'Child' })).toBeNull();
165
+ });
166
+
167
+ it('honours controlled `expanded` prop', () => {
168
+ const items: LeftNavNode[] = [
169
+ {
170
+ kind: 'group',
171
+ id: 'g',
172
+ label: 'Group',
173
+ expanded: false,
174
+ children: [{ id: 'c', label: 'Child', href: '/c' }],
175
+ },
176
+ ];
177
+ const { rerender } = render(<LeftNav items={items} />);
178
+ expect(screen.queryByRole('link', { name: 'Child' })).toBeNull();
179
+ const expanded: LeftNavNode[] = [
180
+ { ...(items[0] as { expanded: boolean }), expanded: true } as LeftNavNode,
181
+ ];
182
+ rerender(<LeftNav items={expanded} />);
183
+ expect(screen.getByRole('link', { name: 'Child' })).toBeTruthy();
184
+ });
185
+
186
+ it('fires onGroupExpandedChange when toggled', () => {
187
+ const cb = vi.fn();
188
+ render(
189
+ <LeftNav items={nestedNav} onGroupExpandedChange={cb} />
190
+ );
191
+ fireEvent.click(screen.getByRole('button', { name: /Sales/ }));
192
+ expect(cb).toHaveBeenCalledWith('sales', false);
193
+ });
194
+ });
195
+
196
+ describe('collapsed (rail mode)', () => {
197
+ it('hides labels visually via sr-only when collapsed', () => {
198
+ const { container } = render(
199
+ <LeftNav items={flatNav} collapsed />
200
+ );
201
+ // Labels are still in the DOM, just hidden — accessible name
202
+ // resolution still works.
203
+ const link = screen.getByRole('link', { name: 'Home' });
204
+ expect(link).toBeTruthy();
205
+ // The label span carries the sr-only class.
206
+ const labelSpans = container.querySelectorAll('.sr-only');
207
+ expect(labelSpans.length).toBeGreaterThanOrEqual(3);
208
+ });
209
+
210
+ it('shows a tooltip via `title` for string labels in rail mode', () => {
211
+ render(<LeftNav items={flatNav} collapsed />);
212
+ const link = screen.getByRole('link', { name: 'Home' });
213
+ expect(link.getAttribute('title')).toBe('Home');
214
+ });
215
+
216
+ it('fires onCollapseChange when the toggle is clicked', () => {
217
+ const cb = vi.fn();
218
+ render(<LeftNav items={flatNav} onCollapseChange={cb} />);
219
+ fireEvent.click(
220
+ screen.getByRole('button', { name: /Collapse navigation/ })
221
+ );
222
+ expect(cb).toHaveBeenCalledWith(true);
223
+ });
224
+
225
+ it('renders the toggle as Expand-button when already collapsed', () => {
226
+ render(<LeftNav items={flatNav} collapsed onCollapseChange={vi.fn()} />);
227
+ expect(
228
+ screen.getByRole('button', { name: /Expand navigation/ })
229
+ ).toBeTruthy();
230
+ });
231
+
232
+ it('hides the toggle button when showCollapseToggle=false', () => {
233
+ render(
234
+ <LeftNav
235
+ items={flatNav}
236
+ onCollapseChange={vi.fn()}
237
+ showCollapseToggle={false}
238
+ />
239
+ );
240
+ expect(
241
+ screen.queryByRole('button', { name: /(Collapse|Expand) navigation/ })
242
+ ).toBeNull();
243
+ });
244
+ });
245
+
246
+ describe('polymorphic linkComponent', () => {
247
+ it('uses the supplied link component for every linked item', () => {
248
+ const MyLink = vi.fn(
249
+ ({
250
+ children,
251
+ ...rest
252
+ }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
253
+ <a data-testid="my-link" {...rest}>
254
+ {children}
255
+ </a>
256
+ )
257
+ );
258
+ render(<LeftNav items={flatNav} linkComponent={MyLink} />);
259
+ expect(screen.getAllByTestId('my-link')).toHaveLength(3);
260
+ });
261
+ });
262
+
263
+ describe('badges', () => {
264
+ it('renders a badge node next to the label', () => {
265
+ const items: LeftNavNode[] = [
266
+ {
267
+ id: 'inbox',
268
+ label: 'Inbox',
269
+ href: '/inbox',
270
+ badge: <span data-testid="badge">42</span>,
271
+ },
272
+ ];
273
+ render(<LeftNav items={items} />);
274
+ expect(screen.getByTestId('badge')).toBeTruthy();
275
+ });
276
+
277
+ it('hides the badge in collapsed/rail mode', () => {
278
+ const items: LeftNavNode[] = [
279
+ {
280
+ id: 'inbox',
281
+ label: 'Inbox',
282
+ href: '/inbox',
283
+ badge: <span data-testid="badge">42</span>,
284
+ },
285
+ ];
286
+ render(<LeftNav items={items} collapsed />);
287
+ expect(screen.queryByTestId('badge')).toBeNull();
288
+ });
289
+ });
290
+
291
+ describe('width variants', () => {
292
+ it('renders sm / md / lg without throwing', () => {
293
+ (['sm', 'md', 'lg'] as const).forEach((w) => {
294
+ const { unmount } = render(<LeftNav items={flatNav} width={w} />);
295
+ expect(
296
+ screen.getByRole('navigation', { name: 'Main navigation' })
297
+ ).toBeTruthy();
298
+ unmount();
299
+ });
300
+ });
301
+ });
302
+ });