@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,114 @@
1
+ import { useEffect } from 'react';
2
+ import { cn } from '../../utils/cn.js';
3
+ import { appShellVariants } from './appShell.variants.js';
4
+ import type { AppShellProps } from './appShell.types.js';
5
+
6
+ /**
7
+ * Dashforge TW AppShell — top-level layout orchestrator.
8
+ *
9
+ * ┌──────────────────────────────────────────┐
10
+ * │ header │ ← `header` slot
11
+ * ├────────┬─────────────────────────────────┤
12
+ * │ │ │
13
+ * │ nav │ main │ ← `nav` + `children`
14
+ * │ │ │
15
+ * ├────────┴─────────────────────────────────┤
16
+ * │ footer │ ← `footer` slot
17
+ * └──────────────────────────────────────────┘
18
+ *
19
+ * **Responsive**:
20
+ * - `≥ md`: nav stays in the inline slot at the left.
21
+ * - `< md`: nav becomes a slide-in drawer toggled by `navOpen`.
22
+ * A backdrop closes the drawer on click (mirrors `<Dialog>` UX).
23
+ *
24
+ * **Body scroll lock**: when the mobile drawer is open we add
25
+ * `overflow-hidden` to `<body>` to prevent dual-scroll wobble. The
26
+ * effect cleans up on close + on unmount.
27
+ *
28
+ * **A11y**:
29
+ * - `<main>` landmark wraps the content area.
30
+ * - `<header>` / `<footer>` come for free from their HTML tags.
31
+ * - The mobile drawer + backdrop participate in the standard
32
+ * "click outside to close" pattern — `Escape` closes the drawer
33
+ * too (added via global keydown).
34
+ */
35
+ export function AppShell(props: AppShellProps) {
36
+ const {
37
+ header,
38
+ nav,
39
+ footer,
40
+ children,
41
+ navOpen = false,
42
+ onNavOpenChange,
43
+ sx,
44
+ slotProps,
45
+ } = props;
46
+
47
+ const v = appShellVariants({ navOpen });
48
+
49
+ // Lock body scroll while the mobile drawer is open.
50
+ useEffect(() => {
51
+ if (!navOpen) return;
52
+ const prev = document.body.style.overflow;
53
+ document.body.style.overflow = 'hidden';
54
+ return () => {
55
+ document.body.style.overflow = prev;
56
+ };
57
+ }, [navOpen]);
58
+
59
+ // Esc closes the drawer.
60
+ useEffect(() => {
61
+ if (!navOpen || !onNavOpenChange) return;
62
+ const handler = (event: KeyboardEvent) => {
63
+ if (event.key === 'Escape') onNavOpenChange(false);
64
+ };
65
+ document.addEventListener('keydown', handler);
66
+ return () => document.removeEventListener('keydown', handler);
67
+ }, [navOpen, onNavOpenChange]);
68
+
69
+ return (
70
+ <div className={cn(v.root(), sx, slotProps?.root?.className)}>
71
+ {header && (
72
+ <div className={cn(v.header(), slotProps?.header?.className)}>
73
+ {header}
74
+ </div>
75
+ )}
76
+
77
+ <div className={v.body()}>
78
+ {nav && (
79
+ <>
80
+ <aside className={cn(v.nav(), slotProps?.nav?.className)}>
81
+ {nav}
82
+ </aside>
83
+ <aside
84
+ className={cn(v.navMobile(), slotProps?.navMobile?.className)}
85
+ aria-hidden={!navOpen}
86
+ // The drawer is the SAME nav content rendered twice — once
87
+ // inline (desktop) and once as a slide-in (mobile). We
88
+ // mark the inactive copy hidden from AT to avoid double
89
+ // announcements.
90
+ >
91
+ {nav}
92
+ </aside>
93
+ <div
94
+ className={cn(v.backdrop(), slotProps?.backdrop?.className)}
95
+ role="presentation"
96
+ onClick={() => onNavOpenChange?.(false)}
97
+ data-testid="appshell-backdrop"
98
+ />
99
+ </>
100
+ )}
101
+
102
+ <main className={cn(v.main(), slotProps?.main?.className)}>
103
+ {children}
104
+ </main>
105
+ </div>
106
+
107
+ {footer && (
108
+ <div className={cn(v.footer(), slotProps?.footer?.className)}>
109
+ {footer}
110
+ </div>
111
+ )}
112
+ </div>
113
+ );
114
+ }
@@ -0,0 +1,49 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { AppShellVariants } from './appShell.variants.js';
3
+
4
+ export interface AppShellSlotProps {
5
+ root?: { className?: string };
6
+ header?: { className?: string };
7
+ nav?: { className?: string };
8
+ navMobile?: { className?: string };
9
+ main?: { className?: string };
10
+ footer?: { className?: string };
11
+ backdrop?: { className?: string };
12
+ }
13
+
14
+ /**
15
+ * Props for `<AppShell>`.
16
+ *
17
+ * Top-level layout orchestrator with 4 named slots:
18
+ *
19
+ * - `header` (top, sticky)
20
+ * - `nav` (left side, fixed-width on desktop, drawer on mobile)
21
+ * - `footer` (bottom, non-sticky)
22
+ * - `children` (main content, scrolls independently)
23
+ *
24
+ * Responsive behavior:
25
+ * - **≥ md (768px)**: nav is rendered inline at the left.
26
+ * - **< md**: nav slides in as an overlay drawer when
27
+ * `navOpen={true}` (controlled).
28
+ *
29
+ * The shell is router-agnostic — pass any nav/header content. Typical
30
+ * pairing is `<LeftNav>` + `<TopBar>` (with a hamburger button in
31
+ * `TopBar.start` that flips `navOpen`).
32
+ */
33
+ export interface AppShellProps extends AppShellVariants {
34
+ header?: ReactNode;
35
+ nav?: ReactNode;
36
+ footer?: ReactNode;
37
+ children?: ReactNode;
38
+ /**
39
+ * Mobile drawer open state. Controlled — provide
40
+ * `onNavOpenChange` to update. When `undefined`, the mobile drawer
41
+ * is always closed (consumer hasn't wired up the toggle yet).
42
+ */
43
+ navOpen?: boolean;
44
+ onNavOpenChange?: (open: boolean) => void;
45
+ /** Root className shortcut. */
46
+ sx?: string;
47
+ /** Per-slot className overrides. */
48
+ slotProps?: AppShellSlotProps;
49
+ }
@@ -0,0 +1,48 @@
1
+ import { tv, type VariantProps } from 'tailwind-variants';
2
+
3
+ /**
4
+ * Tailwind-variants recipe for `<AppShell>`.
5
+ *
6
+ * Slots:
7
+ * - `root` — outer flex column (full viewport)
8
+ * - `header` — top region
9
+ * - `body` — flex row holding nav + main
10
+ * - `nav` — desktop nav rail (md+)
11
+ * - `navMobile` — mobile drawer (slide-in)
12
+ * - `main` — scrollable content area
13
+ * - `footer` — bottom region
14
+ * - `backdrop` — semi-opaque overlay behind the mobile drawer
15
+ */
16
+ export const appShellVariants = tv({
17
+ slots: {
18
+ root: 'flex flex-col min-h-screen bg-neutral-100',
19
+ header: 'shrink-0',
20
+ body: 'flex flex-1 min-h-0',
21
+ nav: 'hidden md:flex shrink-0',
22
+ navMobile: [
23
+ 'flex md:hidden fixed inset-y-0 left-0 z-40',
24
+ 'transition-transform duration-200 ease-out',
25
+ '-translate-x-full',
26
+ ],
27
+ main: 'flex-1 min-w-0 overflow-y-auto',
28
+ footer: 'shrink-0',
29
+ backdrop: [
30
+ 'md:hidden fixed inset-0 z-30 bg-black/40',
31
+ 'transition-opacity duration-200',
32
+ 'opacity-0 pointer-events-none',
33
+ ],
34
+ },
35
+ variants: {
36
+ navOpen: {
37
+ true: {
38
+ navMobile: 'translate-x-0',
39
+ backdrop: 'opacity-100 pointer-events-auto',
40
+ },
41
+ },
42
+ },
43
+ defaultVariants: {
44
+ navOpen: false,
45
+ },
46
+ });
47
+
48
+ export type AppShellVariants = VariantProps<typeof appShellVariants>;
@@ -0,0 +1,225 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Performance + re-render budget tests for `<Autocomplete>`.
4
+ *
5
+ * These are NOT "is X fast" benchmarks — they're guardrails against
6
+ * regressions in the things that matter for a 1000+ option combobox:
7
+ *
8
+ * 1. **Filter memoization**: `filteredOptions` must be reference-stable
9
+ * when its inputs (options + filter text + selection state) don't
10
+ * change. A stable reference means downstream lists don't re-mount.
11
+ *
12
+ * 2. **Render budget**: typing N characters must cause O(N) renders
13
+ * of the component, not O(N²) or worse. With our internals there
14
+ * are 2 renders per keystroke (one for `inputValue`, one for the
15
+ * popover opening on first keystroke), so the budget is `2N + k`
16
+ * for a small constant `k`.
17
+ *
18
+ * 3. **Initial render time** with a large option list must finish
19
+ * within a generous wall-clock budget. This catches a future
20
+ * refactor that, say, accidentally calls `getOptionLabel` 1000²
21
+ * times during mount.
22
+ */
23
+ import { describe, it, expect } from 'vitest';
24
+ import { useEffect, useRef } from 'react';
25
+ import { render, fireEvent, screen } from '@testing-library/react';
26
+ import { Autocomplete } from './Autocomplete.js';
27
+ import type { AutocompleteOption } from './autocomplete.types.js';
28
+
29
+ // Generate `n` deterministic options.
30
+ function makeOptions(n: number): AutocompleteOption[] {
31
+ return Array.from({ length: n }, (_, i) => ({
32
+ value: `o-${i}`,
33
+ label: `Option ${i}`,
34
+ }));
35
+ }
36
+
37
+ /**
38
+ * Wrap a component and expose a render-counter ref so tests can assert
39
+ * how many times React re-rendered the wrapped subtree.
40
+ */
41
+ function withRenderCounter<P>(
42
+ Component: React.ComponentType<P>,
43
+ counterRef: { count: number }
44
+ ): React.FC<P> {
45
+ return function Counted(props: P) {
46
+ counterRef.count++;
47
+ return <Component {...(props as P & object)} />;
48
+ };
49
+ }
50
+
51
+ describe('<Autocomplete> performance', () => {
52
+ it('mounts a 1000-option list well under 500ms', () => {
53
+ const options = makeOptions(1000);
54
+ const t0 = performance.now();
55
+ render(<Autocomplete name="x" options={options} label="X" />);
56
+ const t1 = performance.now();
57
+ expect(t1 - t0).toBeLessThan(500);
58
+ });
59
+
60
+ it('filters a 1000-option list well under 100ms per keystroke', () => {
61
+ const options = makeOptions(1000);
62
+ render(<Autocomplete name="x" options={options} label="X" />);
63
+ const input = screen.getByRole('combobox') as HTMLInputElement;
64
+ const t0 = performance.now();
65
+ fireEvent.change(input, { target: { value: '9' } });
66
+ const t1 = performance.now();
67
+ expect(t1 - t0).toBeLessThan(100);
68
+ });
69
+ });
70
+
71
+ describe('<Autocomplete> re-render budget', () => {
72
+ it('renders at most 2 times on mount (StrictMode-free path)', () => {
73
+ const counter = { count: 0 };
74
+ const Counted = withRenderCounter(Autocomplete, counter);
75
+ render(<Counted name="x" options={makeOptions(20)} label="X" />);
76
+ // 1 initial render — and we tolerate up to 2 to account for the
77
+ // synchronous useState callbacks that schedule a follow-up commit.
78
+ expect(counter.count).toBeLessThanOrEqual(2);
79
+ });
80
+
81
+ it('keeps renders linear in keystrokes (≤ 3 renders per char)', () => {
82
+ const counter = { count: 0 };
83
+ const Counted = withRenderCounter(Autocomplete, counter);
84
+ const { container } = render(
85
+ <Counted name="x" options={makeOptions(50)} label="X" />
86
+ );
87
+ const baseline = counter.count;
88
+ const input = container.querySelector(
89
+ 'input[role="combobox"]'
90
+ ) as HTMLInputElement;
91
+ const N = 10;
92
+ for (let i = 0; i < N; i++) {
93
+ fireEvent.change(input, { target: { value: 'a'.repeat(i + 1) } });
94
+ }
95
+ const perChar = (counter.count - baseline) / N;
96
+ // 1-2 renders per keystroke is the design budget; allow some slack
97
+ // for popover-open transitions on the first keystroke and the
98
+ // highlight reset useState call.
99
+ expect(perChar).toBeLessThanOrEqual(3);
100
+ });
101
+
102
+ it('does NOT re-render an unrelated sibling on each keystroke', () => {
103
+ // Critical perf property: typing into one Autocomplete must not
104
+ // cause sibling components in the same tree to re-render. We check
105
+ // this by mounting a sibling whose render count we count directly.
106
+ const sibCount = { count: 0 };
107
+ const Sibling = () => {
108
+ sibCount.count++;
109
+ return <div data-testid="sib" />;
110
+ };
111
+ render(
112
+ <div>
113
+ <Autocomplete name="x" options={makeOptions(50)} label="X" />
114
+ <Sibling />
115
+ </div>
116
+ );
117
+ const before = sibCount.count;
118
+ const input = screen.getByRole('combobox') as HTMLInputElement;
119
+ for (let i = 0; i < 5; i++) {
120
+ fireEvent.change(input, { target: { value: 'a'.repeat(i + 1) } });
121
+ }
122
+ // No state in the parent changes ⇒ sibling never re-renders.
123
+ expect(sibCount.count).toBe(before);
124
+ });
125
+
126
+ it('filteredOptions reference is stable across unrelated re-renders', () => {
127
+ // Functional contract: if the deps (options, filter text, selection)
128
+ // don't change, `useMemo` should return the same array reference.
129
+ // We verify by triggering a parent re-render with no relevant input
130
+ // change and asserting the `<li>` DOM elements are reused (same id
131
+ // identities ⇒ React reused the keys).
132
+ const options = makeOptions(20);
133
+ const seenIds = new Set<string>();
134
+ const { rerender } = render(
135
+ <Autocomplete name="x" options={options} label="X" />
136
+ );
137
+ const input = screen.getByRole('combobox') as HTMLInputElement;
138
+ fireEvent.focus(input); // open popover so the list renders
139
+ const lis1 = Array.from(
140
+ document.querySelectorAll('[role="option"]')
141
+ ).map((el) => el.id);
142
+ lis1.forEach((id) => seenIds.add(id));
143
+
144
+ // Trigger an unrelated re-render: same options ref, same input
145
+ // value, same selection.
146
+ rerender(<Autocomplete name="x" options={options} label="X" />);
147
+ const lis2 = Array.from(
148
+ document.querySelectorAll('[role="option"]')
149
+ ).map((el) => el.id);
150
+
151
+ // Same ids ⇒ React keyed reuse held; list DOM was not torn down.
152
+ expect(lis2).toEqual(lis1);
153
+ });
154
+
155
+ it('captures number of options accessor calls during one filter cycle', () => {
156
+ // Implementation detail with strong perf relevance: filtering 1000
157
+ // options must call `getOptionLabel` at most O(N) times per filter
158
+ // (NOT O(N²) or anything weirdly memoized that re-computes per
159
+ // child).
160
+ let labelCalls = 0;
161
+ let valueCalls = 0;
162
+ const N = 200;
163
+ const options: AutocompleteOption[] = makeOptions(N);
164
+ render(
165
+ <Autocomplete
166
+ name="x"
167
+ options={options}
168
+ label="X"
169
+ getOptionLabel={(o) => {
170
+ labelCalls++;
171
+ return o.label;
172
+ }}
173
+ getOptionValue={(o) => {
174
+ valueCalls++;
175
+ return o.value;
176
+ }}
177
+ />
178
+ );
179
+ const input = screen.getByRole('combobox') as HTMLInputElement;
180
+ labelCalls = 0;
181
+ valueCalls = 0;
182
+ fireEvent.change(input, { target: { value: '5' } });
183
+ // Budget: O(N) on both. Allow 4x N to cover the filter pass plus
184
+ // the popover-open render's per-option `getOptionValue` for ids
185
+ // and `getOptionLabel` for the rendered <li> body.
186
+ expect(labelCalls).toBeLessThanOrEqual(4 * N);
187
+ expect(valueCalls).toBeLessThanOrEqual(4 * N);
188
+ });
189
+ });
190
+
191
+ describe('<Autocomplete> async + perf interaction', () => {
192
+ it('debounces loadOptions so 5 rapid keystrokes ⇒ 1 fetch', async () => {
193
+ const calls: string[] = [];
194
+ const load = (q: string) => {
195
+ calls.push(q);
196
+ return Promise.resolve<AutocompleteOption[]>([]);
197
+ };
198
+ render(
199
+ <Autocomplete
200
+ name="x"
201
+ options={[]}
202
+ loadOptions={load}
203
+ loadDebounceMs={50}
204
+ />
205
+ );
206
+ const input = screen.getByRole('combobox') as HTMLInputElement;
207
+ // Type 5 chars in quick succession (synchronous).
208
+ for (let i = 0; i < 5; i++) {
209
+ fireEvent.change(input, { target: { value: 'abcde'.slice(0, i + 1) } });
210
+ }
211
+ // Before debounce expires, no call yet.
212
+ expect(calls.length).toBe(0);
213
+ // Wait past debounce window.
214
+ await new Promise((r) => setTimeout(r, 80));
215
+ // Exactly ONE call lands, with the LAST typed value.
216
+ expect(calls).toEqual(['abcde']);
217
+ });
218
+ });
219
+
220
+ /**
221
+ * Module-level useEffect import sanity (suppresses "useRef declared but
222
+ * not used" if a test runner tree-shake removes some perf tests in CI).
223
+ */
224
+ void useRef;
225
+ void useEffect;