torch-glare 2.5.6 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/README.md +4 -3
  2. package/dist/bin/index.js +9 -24
  3. package/dist/bin/index.js.map +1 -1
  4. package/dist/src/commands/add.d.ts +2 -2
  5. package/dist/src/commands/add.d.ts.map +1 -1
  6. package/dist/src/commands/add.js +4 -70
  7. package/dist/src/commands/add.js.map +1 -1
  8. package/dist/src/commands/hook.d.ts +5 -4
  9. package/dist/src/commands/hook.d.ts.map +1 -1
  10. package/dist/src/commands/hook.js +7 -75
  11. package/dist/src/commands/hook.js.map +1 -1
  12. package/dist/src/commands/init.js +1 -1
  13. package/dist/src/commands/init.js.map +1 -1
  14. package/dist/src/commands/layout.d.ts +5 -4
  15. package/dist/src/commands/layout.d.ts.map +1 -1
  16. package/dist/src/commands/layout.js +7 -75
  17. package/dist/src/commands/layout.js.map +1 -1
  18. package/dist/src/commands/provider.d.ts +5 -4
  19. package/dist/src/commands/provider.d.ts.map +1 -1
  20. package/dist/src/commands/provider.js +7 -75
  21. package/dist/src/commands/provider.js.map +1 -1
  22. package/dist/src/commands/update.d.ts.map +1 -1
  23. package/dist/src/commands/update.js +78 -43
  24. package/dist/src/commands/update.js.map +1 -1
  25. package/dist/src/commands/utils.d.ts +5 -4
  26. package/dist/src/commands/utils.d.ts.map +1 -1
  27. package/dist/src/commands/utils.js +7 -80
  28. package/dist/src/commands/utils.js.map +1 -1
  29. package/dist/src/shared/addFromRegistry.d.ts +16 -0
  30. package/dist/src/shared/addFromRegistry.d.ts.map +1 -0
  31. package/dist/src/shared/addFromRegistry.js +88 -0
  32. package/dist/src/shared/addFromRegistry.js.map +1 -0
  33. package/dist/src/shared/getInstallCommand.d.ts.map +1 -1
  34. package/dist/src/shared/getInstallCommand.js +5 -0
  35. package/dist/src/shared/getInstallCommand.js.map +1 -1
  36. package/dist/src/shared/installDependencies.d.ts +0 -10
  37. package/dist/src/shared/installDependencies.d.ts.map +1 -1
  38. package/dist/src/shared/installDependencies.js +0 -17
  39. package/dist/src/shared/installDependencies.js.map +1 -1
  40. package/dist/src/shared/installFromPlan.d.ts +7 -5
  41. package/dist/src/shared/installFromPlan.d.ts.map +1 -1
  42. package/dist/src/shared/installFromPlan.js +103 -36
  43. package/dist/src/shared/installFromPlan.js.map +1 -1
  44. package/dist/src/shared/loadRegistry.d.ts +12 -3
  45. package/dist/src/shared/loadRegistry.d.ts.map +1 -1
  46. package/dist/src/shared/loadRegistry.js +19 -20
  47. package/dist/src/shared/loadRegistry.js.map +1 -1
  48. package/dist/src/shared/registryClient.d.ts +19 -0
  49. package/dist/src/shared/registryClient.d.ts.map +1 -0
  50. package/dist/src/shared/registryClient.js +128 -0
  51. package/dist/src/shared/registryClient.js.map +1 -0
  52. package/dist/src/shared/resolveEntry.d.ts +10 -7
  53. package/dist/src/shared/resolveEntry.d.ts.map +1 -1
  54. package/dist/src/shared/resolveEntry.js +18 -13
  55. package/dist/src/shared/resolveEntry.js.map +1 -1
  56. package/dist/src/shared/suggestOtherCommand.d.ts +7 -1
  57. package/dist/src/shared/suggestOtherCommand.d.ts.map +1 -1
  58. package/dist/src/shared/suggestOtherCommand.js +29 -23
  59. package/dist/src/shared/suggestOtherCommand.js.map +1 -1
  60. package/dist/src/types/main.d.ts +31 -2
  61. package/dist/src/types/main.d.ts.map +1 -1
  62. package/package.json +6 -11
  63. package/apps/lib/components/ActionButton.tsx +0 -93
  64. package/apps/lib/components/ActionsGroup.tsx +0 -27
  65. package/apps/lib/components/AlertDialog.tsx +0 -204
  66. package/apps/lib/components/Avatar.tsx +0 -46
  67. package/apps/lib/components/Badge.tsx +0 -250
  68. package/apps/lib/components/BadgeField.tsx +0 -403
  69. package/apps/lib/components/Breadcrumb.tsx +0 -280
  70. package/apps/lib/components/Button.tsx +0 -325
  71. package/apps/lib/components/ButtonGroup.tsx +0 -190
  72. package/apps/lib/components/Calendar.tsx +0 -115
  73. package/apps/lib/components/Card.tsx +0 -95
  74. package/apps/lib/components/Checkbox.tsx +0 -42
  75. package/apps/lib/components/ColorPicker.tsx +0 -441
  76. package/apps/lib/components/ConclusionHeader.tsx +0 -148
  77. package/apps/lib/components/ContextMenu.tsx +0 -571
  78. package/apps/lib/components/CountBadge.tsx +0 -52
  79. package/apps/lib/components/DataTable.tsx +0 -213
  80. package/apps/lib/components/DataViews/badge.ts +0 -49
  81. package/apps/lib/components/DataViews/cell.tsx +0 -324
  82. package/apps/lib/components/DataViews/context.ts +0 -144
  83. package/apps/lib/components/DataViews/data-views.tsx +0 -395
  84. package/apps/lib/components/DataViews/filters/children.tsx +0 -98
  85. package/apps/lib/components/DataViews/filters/custom.tsx +0 -34
  86. package/apps/lib/components/DataViews/filters/filters.tsx +0 -157
  87. package/apps/lib/components/DataViews/filters/index.ts +0 -4
  88. package/apps/lib/components/DataViews/filters/labelled.tsx +0 -20
  89. package/apps/lib/components/DataViews/filters/presets.tsx +0 -65
  90. package/apps/lib/components/DataViews/filters/sync.tsx +0 -35
  91. package/apps/lib/components/DataViews/filters/values.ts +0 -173
  92. package/apps/lib/components/DataViews/header.tsx +0 -220
  93. package/apps/lib/components/DataViews/hooks/index.ts +0 -5
  94. package/apps/lib/components/DataViews/hooks/useActiveRow.ts +0 -22
  95. package/apps/lib/components/DataViews/hooks/useControllable.ts +0 -52
  96. package/apps/lib/components/DataViews/index.ts +0 -88
  97. package/apps/lib/components/DataViews/panel/columns.tsx +0 -153
  98. package/apps/lib/components/DataViews/panel/controls.tsx +0 -106
  99. package/apps/lib/components/DataViews/panel/index.ts +0 -3
  100. package/apps/lib/components/DataViews/panel/panel.tsx +0 -164
  101. package/apps/lib/components/DataViews/panel/saved-views.tsx +0 -67
  102. package/apps/lib/components/DataViews/panel/section.tsx +0 -81
  103. package/apps/lib/components/DataViews/panel/sort.tsx +0 -42
  104. package/apps/lib/components/DataViews/panel/tab.tsx +0 -31
  105. package/apps/lib/components/DataViews/slots.ts +0 -72
  106. package/apps/lib/components/DataViews/states.tsx +0 -73
  107. package/apps/lib/components/DataViews/types.ts +0 -510
  108. package/apps/lib/components/DataViews/views/board-view.tsx +0 -381
  109. package/apps/lib/components/DataViews/views/card-rows.tsx +0 -36
  110. package/apps/lib/components/DataViews/views/inbox-view.tsx +0 -260
  111. package/apps/lib/components/DataViews/views/pane-views.tsx +0 -196
  112. package/apps/lib/components/DataViews/views/table-view.tsx +0 -493
  113. package/apps/lib/components/DataViews/views/tree-view.tsx +0 -367
  114. package/apps/lib/components/DatePicker.tsx +0 -232
  115. package/apps/lib/components/Dialog.tsx +0 -116
  116. package/apps/lib/components/Divider.tsx +0 -28
  117. package/apps/lib/components/Drawer.tsx +0 -451
  118. package/apps/lib/components/DropdownMenu.tsx +0 -554
  119. package/apps/lib/components/FieldHint.tsx +0 -69
  120. package/apps/lib/components/Form.tsx +0 -182
  121. package/apps/lib/components/FormBuilder/context.ts +0 -85
  122. package/apps/lib/components/FormBuilder/field-kind.ts +0 -28
  123. package/apps/lib/components/FormBuilder/fields/ChoiceFields.tsx +0 -60
  124. package/apps/lib/components/FormBuilder/fields/ColorField.tsx +0 -59
  125. package/apps/lib/components/FormBuilder/fields/CustomField.tsx +0 -11
  126. package/apps/lib/components/FormBuilder/fields/DateField.tsx +0 -32
  127. package/apps/lib/components/FormBuilder/fields/FieldArray.tsx +0 -74
  128. package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +0 -168
  129. package/apps/lib/components/FormBuilder/fields/FileField.tsx +0 -39
  130. package/apps/lib/components/FormBuilder/fields/OptionListFields.tsx +0 -132
  131. package/apps/lib/components/FormBuilder/fields/OtpField.tsx +0 -31
  132. package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +0 -109
  133. package/apps/lib/components/FormBuilder/fields/RichTextEditorField.tsx +0 -31
  134. package/apps/lib/components/FormBuilder/fields/SelectField.tsx +0 -114
  135. package/apps/lib/components/FormBuilder/fields/SignatureField.tsx +0 -157
  136. package/apps/lib/components/FormBuilder/fields/SliderField.tsx +0 -67
  137. package/apps/lib/components/FormBuilder/fields/SwitchBoxField.tsx +0 -41
  138. package/apps/lib/components/FormBuilder/fields/TableField.tsx +0 -336
  139. package/apps/lib/components/FormBuilder/fields/TextField.tsx +0 -213
  140. package/apps/lib/components/FormBuilder/fields/TreeSelectField.tsx +0 -43
  141. package/apps/lib/components/FormBuilder/fields/countries.ts +0 -303
  142. package/apps/lib/components/FormBuilder/fields/index.ts +0 -25
  143. package/apps/lib/components/FormBuilder/form-builder.tsx +0 -198
  144. package/apps/lib/components/FormBuilder/index.ts +0 -31
  145. package/apps/lib/components/FormBuilder/numberFormat.ts +0 -16
  146. package/apps/lib/components/FormBuilder/submit.tsx +0 -67
  147. package/apps/lib/components/FormBuilder/types.ts +0 -316
  148. package/apps/lib/components/FormRenderer/FormDrawer.tsx +0 -250
  149. package/apps/lib/components/FormRenderer/detail.tsx +0 -256
  150. package/apps/lib/components/FormRenderer/form-renderer.tsx +0 -358
  151. package/apps/lib/components/FormRenderer/header.tsx +0 -88
  152. package/apps/lib/components/FormRenderer/index.ts +0 -17
  153. package/apps/lib/components/FormRenderer/notch-action.tsx +0 -64
  154. package/apps/lib/components/FormRenderer/section.tsx +0 -39
  155. package/apps/lib/components/FormRenderer/stepper.tsx +0 -393
  156. package/apps/lib/components/FormRenderer/types.ts +0 -115
  157. package/apps/lib/components/FormSummary.tsx +0 -282
  158. package/apps/lib/components/HeaderBar.tsx +0 -117
  159. package/apps/lib/components/ImageAttachment.tsx +0 -209
  160. package/apps/lib/components/InnerLabelField.tsx +0 -154
  161. package/apps/lib/components/Input.tsx +0 -224
  162. package/apps/lib/components/InputField.tsx +0 -147
  163. package/apps/lib/components/InputOTP.tsx +0 -84
  164. package/apps/lib/components/Label.tsx +0 -129
  165. package/apps/lib/components/LabelField.tsx +0 -76
  166. package/apps/lib/components/LabeledCheckBox.tsx +0 -51
  167. package/apps/lib/components/LabeledRadio.tsx +0 -51
  168. package/apps/lib/components/LinkButton.tsx +0 -94
  169. package/apps/lib/components/LoginButton.tsx +0 -51
  170. package/apps/lib/components/PasswordLevel.tsx +0 -58
  171. package/apps/lib/components/Popover.tsx +0 -312
  172. package/apps/lib/components/ProfileMenu.tsx +0 -85
  173. package/apps/lib/components/Radio.tsx +0 -59
  174. package/apps/lib/components/RadioCard.tsx +0 -50
  175. package/apps/lib/components/ScrollArea.tsx +0 -54
  176. package/apps/lib/components/SearchField.tsx +0 -62
  177. package/apps/lib/components/SearchableSelect.tsx +0 -318
  178. package/apps/lib/components/SearchableTable.tsx +0 -344
  179. package/apps/lib/components/SearchableTree.tsx +0 -485
  180. package/apps/lib/components/SearchableTreeDialog.tsx +0 -502
  181. package/apps/lib/components/SectionBlock.tsx +0 -168
  182. package/apps/lib/components/Select.tsx +0 -347
  183. package/apps/lib/components/SimpleSelect.tsx +0 -225
  184. package/apps/lib/components/Skeleton.tsx +0 -12
  185. package/apps/lib/components/SlideDatePicker.tsx +0 -255
  186. package/apps/lib/components/SpinLoading.tsx +0 -174
  187. package/apps/lib/components/Stepper.tsx +0 -480
  188. package/apps/lib/components/Switch.tsx +0 -82
  189. package/apps/lib/components/TabFormItem.tsx +0 -153
  190. package/apps/lib/components/TabSwitch.tsx +0 -172
  191. package/apps/lib/components/Table.tsx +0 -597
  192. package/apps/lib/components/TextEditor/ChartBlockTool.ts +0 -676
  193. package/apps/lib/components/TextEditor/RichTextField.tsx +0 -46
  194. package/apps/lib/components/TextEditor/TableDnDWrapper.ts +0 -462
  195. package/apps/lib/components/TextEditor/TextEditor.tsx +0 -1164
  196. package/apps/lib/components/TextEditor/TextEditorToolbar.tsx +0 -429
  197. package/apps/lib/components/TextEditor/editor-tools/AlignmentTune.ts +0 -70
  198. package/apps/lib/components/TextEditor/editor-tools/ColorInlineTool.ts +0 -50
  199. package/apps/lib/components/TextEditor/editor-tools/StrikethroughInlineTool.ts +0 -48
  200. package/apps/lib/components/TextEditor/editor-tools/inlineFormat.ts +0 -98
  201. package/apps/lib/components/TextEditor/editorjs.d.ts +0 -60
  202. package/apps/lib/components/TextEditor/index.ts +0 -7
  203. package/apps/lib/components/TextEditor/markdownParser.ts +0 -346
  204. package/apps/lib/components/Textarea.tsx +0 -117
  205. package/apps/lib/components/Timeline.tsx +0 -263
  206. package/apps/lib/components/Toast.tsx +0 -87
  207. package/apps/lib/components/Toggle.tsx +0 -125
  208. package/apps/lib/components/ToggleButton.tsx +0 -140
  209. package/apps/lib/components/Tooltip.tsx +0 -116
  210. package/apps/lib/components/TransparentLabel.tsx +0 -71
  211. package/apps/lib/components/TreeDropDown.tsx +0 -115
  212. package/apps/lib/components/TreeFolder/TreeFolder.tsx +0 -380
  213. package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +0 -75
  214. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +0 -354
  215. package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +0 -60
  216. package/apps/lib/components/TreeFolder/icons.tsx +0 -60
  217. package/apps/lib/components/TreeFolder/index.ts +0 -24
  218. package/apps/lib/components/TreeFolder/treeFolderUtils.ts +0 -109
  219. package/apps/lib/components/TreeFolder/types.ts +0 -77
  220. package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +0 -118
  221. package/apps/lib/hooks/useActiveTreeItem.ts +0 -61
  222. package/apps/lib/hooks/useClickOutside.ts +0 -27
  223. package/apps/lib/hooks/useDragDrop.tsx +0 -365
  224. package/apps/lib/hooks/useHtmlDir.ts +0 -31
  225. package/apps/lib/hooks/useInfiniteScroll.ts +0 -108
  226. package/apps/lib/hooks/useIsMobile.ts +0 -21
  227. package/apps/lib/hooks/useResize.ts +0 -67
  228. package/apps/lib/hooks/useTagSelection.ts +0 -300
  229. package/apps/lib/layouts/CNLayout.tsx +0 -372
  230. package/apps/lib/layouts/DataViewCard.tsx +0 -62
  231. package/apps/lib/layouts/FieldSection.tsx +0 -91
  232. package/apps/lib/layouts/TreeSubLayout.tsx +0 -173
  233. package/apps/lib/providers/ThemeProvider.tsx +0 -84
  234. package/apps/lib/registry.json +0 -1217
  235. package/apps/lib/tsconfig.json +0 -9
  236. package/apps/lib/tsconfig.tsbuildinfo +0 -1
  237. package/apps/lib/utils/cn.ts +0 -6
  238. package/apps/lib/utils/color.ts +0 -175
  239. package/apps/lib/utils/dataViews/path.ts +0 -67
  240. package/apps/lib/utils/dataViews/query.ts +0 -73
  241. package/apps/lib/utils/dataViews/types.ts +0 -187
  242. package/apps/lib/utils/dateFormat.ts +0 -108
  243. package/apps/lib/utils/resize.ts +0 -31
  244. package/apps/lib/utils/scroller.ts +0 -26
  245. package/apps/lib/utils/types.ts +0 -16
  246. package/docs/BLOCKS.md +0 -129
  247. package/docs/CHANGELOG-1.1.16.md +0 -108
  248. package/docs/Cover.png +0 -0
  249. package/docs/README.md +0 -215
  250. package/docs/components/action-button.md +0 -638
  251. package/docs/components/actions-group.md +0 -708
  252. package/docs/components/alert-dialog.md +0 -920
  253. package/docs/components/avatar.md +0 -709
  254. package/docs/components/badge-field.md +0 -738
  255. package/docs/components/badge.md +0 -301
  256. package/docs/components/breadcrumb.md +0 -930
  257. package/docs/components/button-group.md +0 -650
  258. package/docs/components/button.md +0 -639
  259. package/docs/components/calendar.md +0 -555
  260. package/docs/components/card.md +0 -622
  261. package/docs/components/chart-block-tool.md +0 -129
  262. package/docs/components/checkbox.md +0 -699
  263. package/docs/components/cn-layout.md +0 -854
  264. package/docs/components/color-picker.md +0 -101
  265. package/docs/components/conclusion-header.md +0 -80
  266. package/docs/components/context-menu.md +0 -483
  267. package/docs/components/count-badge.md +0 -560
  268. package/docs/components/data-table.md +0 -854
  269. package/docs/components/data-views/backend-response.md +0 -324
  270. package/docs/components/data-views/examples/a11y-rtl.md +0 -250
  271. package/docs/components/data-views/examples/api-orders-route.md +0 -130
  272. package/docs/components/data-views/examples/fields.md +0 -362
  273. package/docs/components/data-views/examples/filters.md +0 -307
  274. package/docs/components/data-views/examples/inbox-routing.md +0 -218
  275. package/docs/components/data-views/examples/index.md +0 -29
  276. package/docs/components/data-views/examples/overview.md +0 -244
  277. package/docs/components/data-views/examples/panel.md +0 -212
  278. package/docs/components/data-views/examples/scale.md +0 -231
  279. package/docs/components/data-views/examples/server-side.md +0 -210
  280. package/docs/components/data-views/examples/state.md +0 -250
  281. package/docs/components/data-views/examples/tree-custom.md +0 -388
  282. package/docs/components/data-views/examples/view-registry.md +0 -313
  283. package/docs/components/data-views/examples/views.md +0 -534
  284. package/docs/components/data-views/guide.md +0 -405
  285. package/docs/components/data-views/index.md +0 -1521
  286. package/docs/components/data-views/migration.md +0 -81
  287. package/docs/components/date-picker.md +0 -857
  288. package/docs/components/dialog.md +0 -929
  289. package/docs/components/divider.md +0 -766
  290. package/docs/components/drawer.md +0 -654
  291. package/docs/components/dropdown-menu.md +0 -668
  292. package/docs/components/field-hint.md +0 -898
  293. package/docs/components/field-section.md +0 -781
  294. package/docs/components/form-builder.md +0 -284
  295. package/docs/components/form-renderer.md +0 -396
  296. package/docs/components/form-summary.md +0 -138
  297. package/docs/components/form.md +0 -785
  298. package/docs/components/header-bar.md +0 -188
  299. package/docs/components/image-attachment.md +0 -906
  300. package/docs/components/inner-label-field.md +0 -819
  301. package/docs/components/input-field.md +0 -623
  302. package/docs/components/input-otp.md +0 -756
  303. package/docs/components/input.md +0 -634
  304. package/docs/components/label-field.md +0 -770
  305. package/docs/components/label.md +0 -761
  306. package/docs/components/labeled-check-box.md +0 -642
  307. package/docs/components/labeled-radio.md +0 -796
  308. package/docs/components/link-button.md +0 -685
  309. package/docs/components/login-button.md +0 -872
  310. package/docs/components/password-level.md +0 -803
  311. package/docs/components/popover.md +0 -649
  312. package/docs/components/profile-menu.md +0 -656
  313. package/docs/components/radio-card.md +0 -801
  314. package/docs/components/radio.md +0 -735
  315. package/docs/components/scroll-area.md +0 -691
  316. package/docs/components/search-field.md +0 -702
  317. package/docs/components/searchable-select.md +0 -368
  318. package/docs/components/searchable-table.md +0 -438
  319. package/docs/components/searchable-tree-dialog.md +0 -190
  320. package/docs/components/searchable-tree.md +0 -200
  321. package/docs/components/section-block.md +0 -483
  322. package/docs/components/select.md +0 -709
  323. package/docs/components/simple-select.md +0 -741
  324. package/docs/components/skeleton.md +0 -706
  325. package/docs/components/slide-date-picker.md +0 -837
  326. package/docs/components/spin-loading.md +0 -696
  327. package/docs/components/stepper.md +0 -316
  328. package/docs/components/switch.md +0 -770
  329. package/docs/components/tab-form-item.md +0 -773
  330. package/docs/components/tab-switch.md +0 -170
  331. package/docs/components/table-dnd-wrapper.md +0 -107
  332. package/docs/components/table.md +0 -1088
  333. package/docs/components/text-editor.md +0 -735
  334. package/docs/components/textarea.md +0 -681
  335. package/docs/components/timeline.md +0 -254
  336. package/docs/components/toast.md +0 -773
  337. package/docs/components/toggle-button.md +0 -643
  338. package/docs/components/toggle.md +0 -882
  339. package/docs/components/tooltip.md +0 -513
  340. package/docs/components/transparent-label.md +0 -721
  341. package/docs/components/tree-drop-down.md +0 -740
  342. package/docs/components/tree-folder.md +0 -110
  343. package/docs/components/tree-sub-layout.md +0 -707
  344. package/docs/explanation/architecture.md +0 -54
  345. package/docs/explanation/design-system.md +0 -52
  346. package/docs/how-to/form-and-list-recipes.md +0 -386
  347. package/docs/how-to/forms-with-form-builder.md +0 -472
  348. package/docs/how-to/guides.md +0 -1027
  349. package/docs/migration/changelog.md +0 -60
  350. package/docs/migration/form-builder-2.5.2.md +0 -113
  351. package/docs/reference/cli.md +0 -112
  352. package/docs/reference/components.md +0 -572
  353. package/docs/reference/hooks.md +0 -1523
  354. package/docs/reference/providers.md +0 -866
  355. package/docs/reference/tailwind-plugins.md +0 -942
  356. package/docs/reference/theme.md +0 -85
  357. package/docs/reference/types.md +0 -1016
  358. package/docs/reference/utilities.md +0 -726
  359. package/docs/tutorials/building-first-form.md +0 -846
  360. package/docs/tutorials/component-composition.md +0 -882
  361. package/docs/tutorials/getting-started.md +0 -302
  362. package/docs/tutorials/theming-basics.md +0 -785
@@ -1,1523 +0,0 @@
1
- ---
2
- title: Hooks Reference
3
- description: Custom React hooks for common UI patterns including intersection observation, click outside detection, resizing, and tag selection.
4
- component: Hooks
5
- category: Reference
6
- tags: [hooks, utilities, intersection-observer, click-outside, resize, tag-selection]
7
- related:
8
- - TreeView
9
- - Popover
10
- - TagSelector
11
- ---
12
-
13
- # Hooks
14
-
15
- Custom React hooks that provide reusable functionality for common UI patterns. All hooks follow React best practices and include TypeScript support.
16
-
17
- ## Available Hooks
18
-
19
- - **useActiveTreeItem** - Track the most visible tree item using IntersectionObserver
20
- - **useClickOutside** - Detect clicks outside a referenced element
21
- - **useResize** - Handle element resizing with RTL support
22
- - **useTagSelection** - Manage tag selection state with keyboard navigation
23
- - **useHtmlDir** - Track the document's text direction from `<html dir>`
24
-
25
- ---
26
-
27
- ## useActiveTreeItem
28
-
29
- Tracks which tree item is most visible in the viewport using the IntersectionObserver API. Useful for table of contents, navigation menus, or any scrollable list where you need to highlight the active item.
30
-
31
- ### Basic Usage
32
-
33
- ```tsx
34
- import { useActiveTreeItem } from "@/hooks/useActiveTreeItem";
35
-
36
- function TableOfContents() {
37
- const sectionIds = ['intro', 'features', 'api', 'examples'];
38
- const { activeId } = useActiveTreeItem(sectionIds);
39
-
40
- return (
41
- <nav>
42
- {sectionIds.map((id) => (
43
- <a
44
- key={id}
45
- href={`#${id}`}
46
- className={activeId === id ? 'active' : ''}
47
- >
48
- {id}
49
- </a>
50
- ))}
51
- </nav>
52
- );
53
- }
54
- ```
55
-
56
- ### Documentation Navigation
57
-
58
- ```tsx
59
- function DocsSidebar() {
60
- const headingIds = [
61
- 'getting-started',
62
- 'installation',
63
- 'configuration',
64
- 'components',
65
- 'api-reference'
66
- ];
67
-
68
- const { activeId } = useActiveTreeItem(headingIds);
69
-
70
- return (
71
- <aside className="sidebar">
72
- <ul>
73
- {headingIds.map((id) => (
74
- <li key={id}>
75
- <a
76
- href={`#${id}`}
77
- className={cn(
78
- 'nav-link',
79
- activeId === id && 'font-semibold text-primary'
80
- )}
81
- >
82
- {id.replace(/-/g, ' ')}
83
- </a>
84
- </li>
85
- ))}
86
- </ul>
87
- </aside>
88
- );
89
- }
90
- ```
91
-
92
- ### Multi-Level Navigation
93
-
94
- ```tsx
95
- interface Section {
96
- id: string;
97
- title: string;
98
- subsections: { id: string; title: string }[];
99
- }
100
-
101
- function NestedNavigation({ sections }: { sections: Section[] }) {
102
- const allIds = sections.flatMap((s) => [
103
- s.id,
104
- ...s.subsections.map((sub) => sub.id)
105
- ]);
106
-
107
- const { activeId } = useActiveTreeItem(allIds);
108
-
109
- return (
110
- <nav>
111
- {sections.map((section) => (
112
- <div key={section.id}>
113
- <a
114
- href={`#${section.id}`}
115
- className={activeId === section.id ? 'active' : ''}
116
- >
117
- {section.title}
118
- </a>
119
- <ul>
120
- {section.subsections.map((sub) => (
121
- <li key={sub.id}>
122
- <a
123
- href={`#${sub.id}`}
124
- className={activeId === sub.id ? 'active' : ''}
125
- >
126
- {sub.title}
127
- </a>
128
- </li>
129
- ))}
130
- </ul>
131
- </div>
132
- ))}
133
- </nav>
134
- );
135
- }
136
- ```
137
-
138
- ### With Smooth Scrolling
139
-
140
- ```tsx
141
- function SmoothScrollNav() {
142
- const sectionIds = ['hero', 'about', 'services', 'contact'];
143
- const { activeId } = useActiveTreeItem(sectionIds);
144
-
145
- const scrollToSection = (id: string) => {
146
- const element = document.getElementById(id);
147
- if (element) {
148
- element.scrollIntoView({ behavior: 'smooth', block: 'start' });
149
- }
150
- };
151
-
152
- return (
153
- <nav className="sticky top-0">
154
- {sectionIds.map((id) => (
155
- <button
156
- key={id}
157
- onClick={() => scrollToSection(id)}
158
- className={cn(
159
- 'nav-button',
160
- activeId === id && 'active'
161
- )}
162
- >
163
- {id}
164
- </button>
165
- ))}
166
- </nav>
167
- );
168
- }
169
- ```
170
-
171
- ### API Reference
172
-
173
- ```typescript
174
- function useActiveTreeItem(itemIds: string[]): {
175
- activeId: string | null;
176
- }
177
- ```
178
-
179
- #### Parameters
180
-
181
- | Name | Type | Default | Description |
182
- |------|------|---------|-------------|
183
- | `itemIds` | `string[]` | Required | Array of element IDs to observe |
184
-
185
- #### Return Value
186
-
187
- | Property | Type | Description |
188
- |----------|------|-------------|
189
- | `activeId` | `string \| null` | ID of the most visible element, or null if none are visible |
190
-
191
- #### IntersectionObserver Configuration
192
-
193
- The hook uses the following configuration:
194
-
195
- ```typescript
196
- {
197
- rootMargin: '-10% 0% -5% 0%',
198
- threshold: [0, 0.25, 0.5, 0.75, 1]
199
- }
200
- ```
201
-
202
- - **rootMargin**: Creates a detection zone excluding the top 10% and bottom 5% of the viewport
203
- - **threshold**: Tracks visibility at 0%, 25%, 50%, 75%, and 100% intersection points
204
-
205
- ---
206
-
207
- ## useClickOutside
208
-
209
- Detects clicks outside a referenced element and executes a callback. Perfect for closing dropdowns, modals, popovers, and menus when clicking outside.
210
-
211
- ### Basic Usage
212
-
213
- ```tsx
214
- import { useClickOutside } from "@/hooks/useClickOutside";
215
- import { useState } from 'react';
216
-
217
- function Dropdown() {
218
- const [isOpen, setIsOpen] = useState(false);
219
- const ref = useClickOutside<HTMLDivElement>(() => setIsOpen(false));
220
-
221
- return (
222
- <div ref={ref}>
223
- <button onClick={() => setIsOpen(!isOpen)}>
224
- Toggle Menu
225
- </button>
226
- {isOpen && (
227
- <ul className="dropdown-menu">
228
- <li>Item 1</li>
229
- <li>Item 2</li>
230
- <li>Item 3</li>
231
- </ul>
232
- )}
233
- </div>
234
- );
235
- }
236
- ```
237
-
238
- ### Modal Dialog
239
-
240
- ```tsx
241
- function Modal({ isOpen, onClose, children }: {
242
- isOpen: boolean;
243
- onClose: () => void;
244
- children: React.ReactNode;
245
- }) {
246
- const modalRef = useClickOutside<HTMLDivElement>(onClose);
247
-
248
- if (!isOpen) return null;
249
-
250
- return (
251
- <div className="modal-overlay">
252
- <div ref={modalRef} className="modal-content">
253
- {children}
254
- <button onClick={onClose}>Close</button>
255
- </div>
256
- </div>
257
- );
258
- }
259
- ```
260
-
261
- ### Context Menu
262
-
263
- ```tsx
264
- function ContextMenu() {
265
- const [position, setPosition] = useState<{ x: number; y: number } | null>(null);
266
- const menuRef = useClickOutside<HTMLDivElement>(() => setPosition(null));
267
-
268
- const handleContextMenu = (e: React.MouseEvent) => {
269
- e.preventDefault();
270
- setPosition({ x: e.clientX, y: e.clientY });
271
- };
272
-
273
- return (
274
- <div onContextMenu={handleContextMenu}>
275
- <p>Right-click me</p>
276
-
277
- {position && (
278
- <div
279
- ref={menuRef}
280
- className="context-menu"
281
- style={{ top: position.y, left: position.x }}
282
- >
283
- <button>Copy</button>
284
- <button>Paste</button>
285
- <button>Delete</button>
286
- </div>
287
- )}
288
- </div>
289
- );
290
- }
291
- ```
292
-
293
- ### With Click Inside Callback
294
-
295
- ```tsx
296
- function TooltipWithTracking() {
297
- const [isOpen, setIsOpen] = useState(false);
298
- const [clickCount, setClickCount] = useState(0);
299
-
300
- const ref = useClickOutside<HTMLDivElement>(
301
- () => {
302
- setIsOpen(false);
303
- console.log('Clicked outside');
304
- },
305
- () => {
306
- setClickCount((prev) => prev + 1);
307
- console.log('Clicked inside');
308
- }
309
- );
310
-
311
- return (
312
- <div ref={ref}>
313
- <button onClick={() => setIsOpen(!isOpen)}>
314
- Show Tooltip ({clickCount} clicks)
315
- </button>
316
- {isOpen && (
317
- <div className="tooltip">
318
- This is a tooltip
319
- </div>
320
- )}
321
- </div>
322
- );
323
- }
324
- ```
325
-
326
- ### Custom Popover
327
-
328
- ```tsx
329
- function Popover({ trigger, content }: {
330
- trigger: React.ReactNode;
331
- content: React.ReactNode;
332
- }) {
333
- const [isOpen, setIsOpen] = useState(false);
334
- const popoverRef = useClickOutside<HTMLDivElement>(
335
- () => setIsOpen(false)
336
- );
337
-
338
- return (
339
- <div ref={popoverRef} className="relative">
340
- <div onClick={() => setIsOpen(!isOpen)}>
341
- {trigger}
342
- </div>
343
- {isOpen && (
344
- <div className="absolute top-full left-0 mt-2 z-10">
345
- {content}
346
- </div>
347
- )}
348
- </div>
349
- );
350
- }
351
- ```
352
-
353
- ### API Reference
354
-
355
- ```typescript
356
- function useClickOutside<T extends HTMLElement>(
357
- callback: (event?: MouseEvent | PointerEvent) => void,
358
- otherwise?: (event?: MouseEvent | PointerEvent) => void
359
- ): React.RefObject<T>
360
- ```
361
-
362
- #### Parameters
363
-
364
- | Name | Type | Default | Description |
365
- |------|------|---------|-------------|
366
- | `callback` | `(event?: MouseEvent \| PointerEvent) => void` | Required | Function to call when clicking outside the element |
367
- | `otherwise` | `(event?: MouseEvent \| PointerEvent) => void` | `undefined` | Optional function to call when clicking inside the element |
368
-
369
- #### Return Value
370
-
371
- Returns a `React.RefObject<T>` that should be attached to the element you want to monitor.
372
-
373
- #### Events Monitored
374
-
375
- - `mousedown` - Standard mouse click
376
- - `pointerdown` - Touch and stylus events
377
-
378
- ---
379
-
380
- ## useResize
381
-
382
- Handles element resizing with support for both mouse and touch events, including RTL (right-to-left) layout support. Perfect for resizable panels, sidebars, and split views.
383
-
384
- ### Basic Usage
385
-
386
- ```tsx
387
- import { useResize } from "@/hooks/useResize";
388
- import { useRef } from 'react';
389
-
390
- function ResizablePanel() {
391
- const panelRef = useRef<HTMLDivElement>(null);
392
- const { width, isResizing, handleStartResize } = useResize(panelRef);
393
-
394
- return (
395
- <div className="flex">
396
- <div
397
- ref={panelRef}
398
- style={{ width: width ? `${width}px` : '300px' }}
399
- className={cn('panel', isResizing && 'resizing')}
400
- >
401
- <p>Resizable Panel</p>
402
- </div>
403
-
404
- <div
405
- onMouseDown={handleStartResize}
406
- onTouchStart={handleStartResize}
407
- className="resize-handle"
408
- >
409
- <div className="resize-bar" />
410
- </div>
411
-
412
- <div className="flex-1">
413
- <p>Main Content</p>
414
- </div>
415
- </div>
416
- );
417
- }
418
- ```
419
-
420
- ### Split View
421
-
422
- ```tsx
423
- function SplitView({ left, right }: {
424
- left: React.ReactNode;
425
- right: React.ReactNode;
426
- }) {
427
- const leftRef = useRef<HTMLDivElement>(null);
428
- const { width, isResizing, handleStartResize } = useResize(leftRef);
429
-
430
- return (
431
- <div className="flex h-screen">
432
- <div
433
- ref={leftRef}
434
- style={{ width: width ? `${width}px` : '50%' }}
435
- className="overflow-auto"
436
- >
437
- {left}
438
- </div>
439
-
440
- <div
441
- onMouseDown={handleStartResize}
442
- onTouchStart={handleStartResize}
443
- className={cn(
444
- 'w-1 bg-gray-300 cursor-col-resize hover:bg-blue-500',
445
- isResizing && 'bg-blue-500'
446
- )}
447
- />
448
-
449
- <div className="flex-1 overflow-auto">
450
- {right}
451
- </div>
452
- </div>
453
- );
454
- }
455
- ```
456
-
457
- ### Resizable Sidebar
458
-
459
- ```tsx
460
- function ResizableSidebar() {
461
- const sidebarRef = useRef<HTMLDivElement>(null);
462
- const { width, isResizing, handleStartResize } = useResize(sidebarRef);
463
- const [isCollapsed, setIsCollapsed] = useState(false);
464
-
465
- const effectiveWidth = isCollapsed ? 0 : (width || 250);
466
-
467
- return (
468
- <div className="flex">
469
- <aside
470
- ref={sidebarRef}
471
- style={{ width: `${effectiveWidth}px` }}
472
- className={cn(
473
- 'sidebar transition-all',
474
- isResizing && 'transition-none'
475
- )}
476
- >
477
- <nav>
478
- <a href="#home">Home</a>
479
- <a href="#about">About</a>
480
- <a href="#contact">Contact</a>
481
- </nav>
482
- </aside>
483
-
484
- {!isCollapsed && (
485
- <div
486
- onMouseDown={handleStartResize}
487
- onTouchStart={handleStartResize}
488
- className="resize-handle"
489
- >
490
- <svg className="resize-icon" />
491
- </div>
492
- )}
493
-
494
- <div className="flex-1">
495
- <button onClick={() => setIsCollapsed(!isCollapsed)}>
496
- {isCollapsed ? '→' : '←'}
497
- </button>
498
- <main>Content</main>
499
- </div>
500
- </div>
501
- );
502
- }
503
- ```
504
-
505
- ### Code Editor Layout
506
-
507
- ```tsx
508
- function CodeEditor() {
509
- const editorRef = useRef<HTMLDivElement>(null);
510
- const consoleRef = useRef<HTMLDivElement>(null);
511
-
512
- const editorResize = useResize(editorRef);
513
- const consoleResize = useResize(consoleRef);
514
-
515
- return (
516
- <div className="flex flex-col h-screen">
517
- {/* Top: Code Editor */}
518
- <div
519
- ref={editorRef}
520
- style={{ height: editorResize.width ? `${editorResize.width}px` : '60%' }}
521
- >
522
- <textarea className="w-full h-full" placeholder="Write code..." />
523
- </div>
524
-
525
- {/* Horizontal Resize Handle */}
526
- <div
527
- onMouseDown={editorResize.handleStartResize}
528
- className={cn(
529
- 'h-1 bg-gray-300 cursor-row-resize',
530
- editorResize.isResizing && 'bg-blue-500'
531
- )}
532
- />
533
-
534
- {/* Bottom: Split Console */}
535
- <div className="flex flex-1">
536
- <div
537
- ref={consoleRef}
538
- style={{ width: consoleResize.width ? `${consoleResize.width}px` : '50%' }}
539
- >
540
- <div className="console">Output</div>
541
- </div>
542
-
543
- <div
544
- onMouseDown={consoleResize.handleStartResize}
545
- className="w-1 bg-gray-300 cursor-col-resize"
546
- />
547
-
548
- <div className="flex-1">
549
- <div className="console">Errors</div>
550
- </div>
551
- </div>
552
- </div>
553
- );
554
- }
555
- ```
556
-
557
- ### With Min/Max Constraints
558
-
559
- ```tsx
560
- function ConstrainedResize() {
561
- const panelRef = useRef<HTMLDivElement>(null);
562
- const { width, isResizing, handleStartResize } = useResize(panelRef);
563
-
564
- const MIN_WIDTH = 200;
565
- const MAX_WIDTH = 600;
566
- const constrainedWidth = width
567
- ? Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, width))
568
- : 300;
569
-
570
- return (
571
- <div className="flex">
572
- <div
573
- ref={panelRef}
574
- style={{ width: `${constrainedWidth}px` }}
575
- className="panel"
576
- >
577
- <p>Width: {constrainedWidth}px</p>
578
- <p>Min: {MIN_WIDTH}px, Max: {MAX_WIDTH}px</p>
579
- </div>
580
-
581
- <div
582
- onMouseDown={handleStartResize}
583
- onTouchStart={handleStartResize}
584
- className="resize-handle"
585
- />
586
-
587
- <div className="flex-1">
588
- Main Content
589
- </div>
590
- </div>
591
- );
592
- }
593
- ```
594
-
595
- ### API Reference
596
-
597
- ```typescript
598
- function useResize(
599
- resizableRef: MutableRefObject<HTMLElement> | RefObject<HTMLElement>
600
- ): {
601
- width: number | undefined;
602
- isResizing: boolean;
603
- handleStartResize: (e: React.MouseEvent | React.TouchEvent) => void;
604
- }
605
- ```
606
-
607
- #### Parameters
608
-
609
- | Name | Type | Default | Description |
610
- |------|------|---------|-------------|
611
- | `resizableRef` | `MutableRefObject<HTMLElement> \| RefObject<HTMLElement>` | Required | Ref to the element being resized |
612
-
613
- #### Return Value
614
-
615
- | Property | Type | Description |
616
- |----------|------|-------------|
617
- | `width` | `number \| undefined` | Current width of the element in pixels |
618
- | `isResizing` | `boolean` | Whether the element is currently being resized |
619
- | `handleStartResize` | `(e: React.MouseEvent \| React.TouchEvent) => void` | Function to call on resize handle mousedown/touchstart |
620
-
621
- #### RTL Support
622
-
623
- The hook automatically detects RTL layout from `document.documentElement.dir` and adjusts resize calculations accordingly.
624
-
625
- ---
626
-
627
- ## useTagSelection
628
-
629
- Manages tag selection state with comprehensive keyboard navigation support. Supports both single and multi-select modes, search filtering, and accessible keyboard interactions.
630
-
631
- ### Basic Multi-Select
632
-
633
- ```tsx
634
- import { useTagSelection } from "@/hooks/useTagSelection";
635
- import { useRef } from 'react';
636
-
637
- function TagSelector() {
638
- const inputRef = useRef<HTMLInputElement>(null);
639
-
640
- const initialTags = [
641
- { id: '1', name: 'React', isSelected: false },
642
- { id: '2', name: 'TypeScript', isSelected: false },
643
- { id: '3', name: 'JavaScript', isSelected: false },
644
- { id: '4', name: 'Node.js', isSelected: false }
645
- ];
646
-
647
- const {
648
- tags,
649
- selectedTagsStack,
650
- handleSelectTag,
651
- handleUnselectTag,
652
- handleKeyDown,
653
- isPopoverOpen,
654
- setIsPopoverOpen,
655
- filteredTags
656
- } = useTagSelection({
657
- Tags: initialTags,
658
- inputRef,
659
- singleSelect: false
660
- });
661
-
662
- return (
663
- <div>
664
- <div className="selected-tags">
665
- {selectedTagsStack.map((tag) => (
666
- <span key={tag.id} className="tag">
667
- {tag.name}
668
- <button onClick={() => handleUnselectTag(tag.id)}>×</button>
669
- </span>
670
- ))}
671
- </div>
672
-
673
- <input
674
- ref={inputRef}
675
- onFocus={() => setIsPopoverOpen(true)}
676
- onKeyDown={handleKeyDown}
677
- placeholder="Select tags..."
678
- />
679
-
680
- {isPopoverOpen && (
681
- <div className="tag-popover">
682
- {filteredTags.map((tag) => (
683
- <button
684
- key={tag.id}
685
- onClick={() => handleSelectTag(tag.id)}
686
- className={tag.isSelected ? 'selected' : ''}
687
- >
688
- {tag.name}
689
- </button>
690
- ))}
691
- </div>
692
- )}
693
- </div>
694
- );
695
- }
696
- ```
697
-
698
- ### Single Select Mode
699
-
700
- ```tsx
701
- function SingleTagSelector() {
702
- const inputRef = useRef<HTMLInputElement>(null);
703
-
704
- const categories = [
705
- { id: '1', name: 'Technology', isSelected: false },
706
- { id: '2', name: 'Design', isSelected: false },
707
- { id: '3', name: 'Marketing', isSelected: false }
708
- ];
709
-
710
- const {
711
- selectedTagsStack,
712
- handleSelectTag,
713
- handleKeyDown,
714
- isPopoverOpen,
715
- setIsPopoverOpen,
716
- filteredTags
717
- } = useTagSelection({
718
- Tags: categories,
719
- inputRef,
720
- singleSelect: true // Only one tag can be selected
721
- });
722
-
723
- return (
724
- <div>
725
- <label>Category:</label>
726
- <div className="selected-category">
727
- {selectedTagsStack[0]?.name || 'None'}
728
- </div>
729
-
730
- <input
731
- ref={inputRef}
732
- onFocus={() => setIsPopoverOpen(true)}
733
- onKeyDown={handleKeyDown}
734
- placeholder="Choose category..."
735
- />
736
-
737
- {isPopoverOpen && (
738
- <div className="category-list">
739
- {filteredTags.map((category) => (
740
- <button
741
- key={category.id}
742
- onClick={() => {
743
- handleSelectTag(category.id);
744
- setIsPopoverOpen(false);
745
- }}
746
- className={category.isSelected ? 'active' : ''}
747
- >
748
- {category.name}
749
- </button>
750
- ))}
751
- </div>
752
- )}
753
- </div>
754
- );
755
- }
756
- ```
757
-
758
- ### With Search Filter
759
-
760
- ```tsx
761
- function SearchableTags() {
762
- const inputRef = useRef<HTMLInputElement>(null);
763
-
764
- const skills = [
765
- { id: '1', name: 'JavaScript', isSelected: false },
766
- { id: '2', name: 'TypeScript', isSelected: false },
767
- { id: '3', name: 'Python', isSelected: false },
768
- { id: '4', name: 'Java', isSelected: false },
769
- { id: '5', name: 'C++', isSelected: false }
770
- ];
771
-
772
- const {
773
- selectedTagsStack,
774
- handleSelectTag,
775
- handleUnselectTag,
776
- handleKeyDown,
777
- isPopoverOpen,
778
- setIsPopoverOpen,
779
- filteredTags,
780
- searchTags,
781
- filterTagsBySearch
782
- } = useTagSelection({
783
- Tags: skills,
784
- inputRef
785
- });
786
-
787
- return (
788
- <div>
789
- <div className="selected-skills">
790
- {selectedTagsStack.map((skill) => (
791
- <span key={skill.id} className="skill-tag">
792
- {skill.name}
793
- <button onClick={() => handleUnselectTag(skill.id)}>×</button>
794
- </span>
795
- ))}
796
- </div>
797
-
798
- <input
799
- ref={inputRef}
800
- value={searchTags}
801
- onChange={(e) => filterTagsBySearch(e.target.value)}
802
- onFocus={() => setIsPopoverOpen(true)}
803
- onKeyDown={handleKeyDown}
804
- placeholder="Search skills..."
805
- />
806
-
807
- {isPopoverOpen && (
808
- <div className="skills-popover">
809
- {filteredTags.length > 0 ? (
810
- filteredTags.map((skill) => (
811
- <button
812
- key={skill.id}
813
- onClick={() => handleSelectTag(skill.id)}
814
- >
815
- {skill.name}
816
- </button>
817
- ))
818
- ) : (
819
- <p>No skills found</p>
820
- )}
821
- </div>
822
- )}
823
- </div>
824
- );
825
- }
826
- ```
827
-
828
- ### With Custom Tag Rendering
829
-
830
- ```tsx
831
- interface CustomTag {
832
- id: string;
833
- name: string;
834
- color: string;
835
- icon: string;
836
- isSelected: boolean;
837
- }
838
-
839
- function ColoredTags() {
840
- const inputRef = useRef<HTMLInputElement>(null);
841
-
842
- const labels: CustomTag[] = [
843
- { id: '1', name: 'Bug', color: 'red', icon: '🐛', isSelected: false },
844
- { id: '2', name: 'Feature', color: 'green', icon: '✨', isSelected: false },
845
- { id: '3', name: 'Docs', color: 'blue', icon: '📚', isSelected: false }
846
- ];
847
-
848
- const {
849
- selectedTagsStack,
850
- handleSelectTag,
851
- handleUnselectTag,
852
- handleKeyDown,
853
- isPopoverOpen,
854
- setIsPopoverOpen,
855
- filteredTags
856
- } = useTagSelection({
857
- Tags: labels,
858
- inputRef
859
- });
860
-
861
- return (
862
- <div>
863
- <div className="flex gap-2">
864
- {selectedTagsStack.map((label) => (
865
- <span
866
- key={label.id}
867
- className="tag"
868
- style={{ backgroundColor: label.color }}
869
- >
870
- <span>{label.icon}</span>
871
- <span>{label.name}</span>
872
- <button onClick={() => handleUnselectTag(label.id)}>×</button>
873
- </span>
874
- ))}
875
- </div>
876
-
877
- <input
878
- ref={inputRef}
879
- onFocus={() => setIsPopoverOpen(true)}
880
- onKeyDown={handleKeyDown}
881
- placeholder="Add labels..."
882
- />
883
-
884
- {isPopoverOpen && (
885
- <div className="labels-popover">
886
- {filteredTags.map((label) => (
887
- <button
888
- key={label.id}
889
- onClick={() => handleSelectTag(label.id)}
890
- style={{ borderLeft: `4px solid ${label.color}` }}
891
- >
892
- <span>{label.icon}</span>
893
- <span>{label.name}</span>
894
- </button>
895
- ))}
896
- </div>
897
- )}
898
- </div>
899
- );
900
- }
901
- ```
902
-
903
- ### With Change Callback
904
-
905
- ```tsx
906
- function TagsWithCallback() {
907
- const inputRef = useRef<HTMLInputElement>(null);
908
-
909
- const [submittedTags, setSubmittedTags] = useState<string[]>([]);
910
-
911
- const topics = [
912
- { id: '1', name: 'React', isSelected: false },
913
- { id: '2', name: 'Vue', isSelected: false },
914
- { id: '3', name: 'Angular', isSelected: false }
915
- ];
916
-
917
- const {
918
- selectedTagsStack,
919
- handleSelectTag,
920
- handleUnselectTag,
921
- handleKeyDown,
922
- isPopoverOpen,
923
- setIsPopoverOpen,
924
- filteredTags
925
- } = useTagSelection({
926
- Tags: topics,
927
- inputRef,
928
- onTagsChange: (selectedTags) => {
929
- console.log('Tags changed:', selectedTags);
930
- // Update external state, make API calls, etc.
931
- setSubmittedTags(selectedTags.map(t => t.name));
932
- }
933
- });
934
-
935
- return (
936
- <div>
937
- <div className="selected-topics">
938
- {selectedTagsStack.map((topic) => (
939
- <span key={topic.id} className="topic-tag">
940
- {topic.name}
941
- <button onClick={() => handleUnselectTag(topic.id)}>×</button>
942
- </span>
943
- ))}
944
- </div>
945
-
946
- <input
947
- ref={inputRef}
948
- onFocus={() => setIsPopoverOpen(true)}
949
- onKeyDown={handleKeyDown}
950
- placeholder="Select topics..."
951
- />
952
-
953
- {isPopoverOpen && (
954
- <div className="topics-popover">
955
- {filteredTags.map((topic) => (
956
- <button
957
- key={topic.id}
958
- onClick={() => handleSelectTag(topic.id)}
959
- >
960
- {topic.name}
961
- </button>
962
- ))}
963
- </div>
964
- )}
965
-
966
- <div className="mt-4">
967
- <p>Submitted: {submittedTags.join(', ')}</p>
968
- </div>
969
- </div>
970
- );
971
- }
972
- ```
973
-
974
- ### With Keyboard Navigation
975
-
976
- The hook provides comprehensive keyboard support:
977
-
978
- ```tsx
979
- function KeyboardNavigationExample() {
980
- const inputRef = useRef<HTMLInputElement>(null);
981
-
982
- const tags = [
983
- { id: '1', name: 'JavaScript', isSelected: false },
984
- { id: '2', name: 'TypeScript', isSelected: false },
985
- { id: '3', name: 'Python', isSelected: false }
986
- ];
987
-
988
- const {
989
- selectedTagsStack,
990
- handleSelectTag,
991
- handleUnselectTag,
992
- handleKeyDown,
993
- isPopoverOpen,
994
- setIsPopoverOpen,
995
- filteredTags,
996
- focusedTagIndex,
997
- focusedPopoverIndex
998
- } = useTagSelection({
999
- Tags: tags,
1000
- inputRef
1001
- });
1002
-
1003
- return (
1004
- <div>
1005
- <div className="help-text">
1006
- <p>Keyboard shortcuts:</p>
1007
- <ul>
1008
- <li>← → : Navigate selected tags</li>
1009
- <li>↑ ↓ : Navigate popover options</li>
1010
- <li>Enter: Select/unselect tag</li>
1011
- <li>Delete/Backspace: Remove focused tag</li>
1012
- <li>Escape: Close popover</li>
1013
- </ul>
1014
- </div>
1015
-
1016
- <div className="selected-tags">
1017
- {selectedTagsStack.map((tag, index) => (
1018
- <span
1019
- key={tag.id}
1020
- className={cn(
1021
- 'tag',
1022
- focusedTagIndex === index && 'focused'
1023
- )}
1024
- >
1025
- {tag.name}
1026
- <button onClick={() => handleUnselectTag(tag.id)}>×</button>
1027
- </span>
1028
- ))}
1029
- </div>
1030
-
1031
- <input
1032
- ref={inputRef}
1033
- onFocus={() => setIsPopoverOpen(true)}
1034
- onKeyDown={handleKeyDown}
1035
- placeholder="Use arrow keys to navigate..."
1036
- />
1037
-
1038
- {isPopoverOpen && (
1039
- <div className="tag-popover">
1040
- {filteredTags.map((tag, index) => (
1041
- <button
1042
- key={tag.id}
1043
- onClick={() => handleSelectTag(tag.id)}
1044
- className={cn(
1045
- 'tag-option',
1046
- focusedPopoverIndex === index && 'focused'
1047
- )}
1048
- >
1049
- {tag.name}
1050
- </button>
1051
- ))}
1052
- </div>
1053
- )}
1054
- </div>
1055
- );
1056
- }
1057
- ```
1058
-
1059
- ### API Reference
1060
-
1061
- ```typescript
1062
- interface Tag {
1063
- id: string;
1064
- name: string;
1065
- variant?: string;
1066
- value?: string;
1067
- isSelected: boolean;
1068
- [key: string]: any;
1069
- }
1070
-
1071
- function useTagSelection(params: {
1072
- Tags: Tag[];
1073
- onTagsChange?: (selectedTags: Tag[]) => void;
1074
- inputRef?: React.RefObject<HTMLInputElement | null>;
1075
- singleSelect?: boolean;
1076
- }): {
1077
- tags: Tag[];
1078
- selectedTagsStack: Tag[];
1079
- searchTags: string;
1080
- filteredTags: Tag[];
1081
- focusedTagIndex: number | null;
1082
- focusedPopoverIndex: number | null;
1083
- isPopoverOpen: boolean;
1084
- handleSelectTag: (id: string) => void;
1085
- handleUnselectTag: (id: string) => void;
1086
- handleKeyDown: (e: React.KeyboardEvent<HTMLElement>) => void;
1087
- setIsPopoverOpen: React.Dispatch<React.SetStateAction<boolean>>;
1088
- filterTagsBySearch: React.Dispatch<React.SetStateAction<string>>;
1089
- setFocusedTagIndex: React.Dispatch<React.SetStateAction<number | null>>;
1090
- setFocusedPopoverIndex: React.Dispatch<React.SetStateAction<number | null>>;
1091
- }
1092
- ```
1093
-
1094
- #### Parameters
1095
-
1096
- | Name | Type | Default | Description |
1097
- |------|------|---------|-------------|
1098
- | `Tags` | `Tag[]` | Required | Array of tag objects with at least `id`, `name`, and `isSelected` |
1099
- | `onTagsChange` | `(selectedTags: Tag[]) => void` | `undefined` | Callback fired when selection changes |
1100
- | `inputRef` | `React.RefObject<HTMLInputElement>` | `undefined` | Ref to input element for focus management |
1101
- | `singleSelect` | `boolean` | `false` | If true, only one tag can be selected at a time |
1102
-
1103
- #### Return Value
1104
-
1105
- | Property | Type | Description |
1106
- |----------|------|-------------|
1107
- | `tags` | `Tag[]` | All tags with current selection state |
1108
- | `selectedTagsStack` | `Tag[]` | Currently selected tags in order |
1109
- | `searchTags` | `string` | Current search query |
1110
- | `filteredTags` | `Tag[]` | Tags filtered by search query |
1111
- | `focusedTagIndex` | `number \| null` | Index of focused selected tag |
1112
- | `focusedPopoverIndex` | `number \| null` | Index of focused popover option |
1113
- | `isPopoverOpen` | `boolean` | Whether popover is open |
1114
- | `handleSelectTag` | `(id: string) => void` | Select a tag by ID |
1115
- | `handleUnselectTag` | `(id: string) => void` | Unselect a tag by ID |
1116
- | `handleKeyDown` | `(e: React.KeyboardEvent) => void` | Keyboard event handler |
1117
- | `setIsPopoverOpen` | `Dispatch<SetStateAction<boolean>>` | Control popover open state |
1118
- | `filterTagsBySearch` | `Dispatch<SetStateAction<string>>` | Set search query |
1119
- | `setFocusedTagIndex` | `Dispatch<SetStateAction<number \| null>>` | Set focused selected tag |
1120
- | `setFocusedPopoverIndex` | `Dispatch<SetStateAction<number \| null>>` | Set focused popover option |
1121
-
1122
- #### Keyboard Interactions
1123
-
1124
- | Key | Action | Context |
1125
- |-----|--------|---------|
1126
- | `ArrowLeft` | Focus previous selected tag | When selected tags exist |
1127
- | `ArrowRight` | Focus next selected tag | When selected tags exist |
1128
- | `ArrowDown` | Focus next popover option | When popover is open |
1129
- | `ArrowUp` | Focus previous popover option | When popover is open |
1130
- | `Enter` | Select focused popover option OR unselect focused tag | When popover open or tag focused |
1131
- | `Delete` / `Backspace` | Remove focused selected tag | When tag is focused |
1132
- | `Escape` | Close popover | When popover is open |
1133
-
1134
- ---
1135
-
1136
- ## TypeScript Support
1137
-
1138
- All hooks are fully typed with TypeScript:
1139
-
1140
- ```typescript
1141
- // useActiveTreeItem
1142
- const { activeId }: { activeId: string | null } = useActiveTreeItem(ids);
1143
-
1144
- // useClickOutside - Generic type parameter
1145
- const ref: React.RefObject<HTMLDivElement> = useClickOutside<HTMLDivElement>(
1146
- callback,
1147
- otherwise
1148
- );
1149
-
1150
- // useResize
1151
- const {
1152
- width,
1153
- isResizing,
1154
- handleStartResize
1155
- }: {
1156
- width: number | undefined;
1157
- isResizing: boolean;
1158
- handleStartResize: (e: React.MouseEvent | React.TouchEvent) => void;
1159
- } = useResize(ref);
1160
-
1161
- // useTagSelection - Custom Tag interface
1162
- interface CustomTag extends Tag {
1163
- color: string;
1164
- priority: number;
1165
- }
1166
-
1167
- const result = useTagSelection({
1168
- Tags: customTags as CustomTag[],
1169
- // ...
1170
- });
1171
- ```
1172
-
1173
- ---
1174
-
1175
- ## Testing
1176
-
1177
- ### Testing useActiveTreeItem
1178
-
1179
- ```tsx
1180
- import { renderHook } from '@testing-library/react';
1181
- import { useActiveTreeItem } from "@/hooks/useActiveTreeItem";
1182
-
1183
- describe('useActiveTreeItem', () => {
1184
- it('should track active item', () => {
1185
- const { result } = renderHook(() =>
1186
- useActiveTreeItem(['item1', 'item2', 'item3'])
1187
- );
1188
-
1189
- expect(result.current.activeId).toBeNull();
1190
-
1191
- // Mock IntersectionObserver would trigger here
1192
- // and update activeId
1193
- });
1194
-
1195
- it('should update when items change', () => {
1196
- const { result, rerender } = renderHook(
1197
- ({ ids }) => useActiveTreeItem(ids),
1198
- { initialProps: { ids: ['a', 'b'] } }
1199
- );
1200
-
1201
- rerender({ ids: ['a', 'b', 'c'] });
1202
-
1203
- // Verify observer was updated with new IDs
1204
- });
1205
- });
1206
- ```
1207
-
1208
- ### Testing useClickOutside
1209
-
1210
- ```tsx
1211
- import { render, fireEvent } from '@testing-library/react';
1212
- import { useClickOutside } from "@/hooks/useClickOutside";
1213
-
1214
- describe('useClickOutside', () => {
1215
- it('should call callback on outside click', () => {
1216
- const callback = vi.fn();
1217
-
1218
- function TestComponent() {
1219
- const ref = useClickOutside<HTMLDivElement>(callback);
1220
- return <div ref={ref}>Content</div>;
1221
- }
1222
-
1223
- const { container } = render(<TestComponent />);
1224
-
1225
- fireEvent.mouseDown(document.body);
1226
- expect(callback).toHaveBeenCalledTimes(1);
1227
- });
1228
-
1229
- it('should not call callback on inside click', () => {
1230
- const callback = vi.fn();
1231
-
1232
- function TestComponent() {
1233
- const ref = useClickOutside<HTMLDivElement>(callback);
1234
- return <div ref={ref}>Content</div>;
1235
- }
1236
-
1237
- const { getByText } = render(<TestComponent />);
1238
-
1239
- fireEvent.mouseDown(getByText('Content'));
1240
- expect(callback).not.toHaveBeenCalled();
1241
- });
1242
- });
1243
- ```
1244
-
1245
- ### Testing useResize
1246
-
1247
- ```tsx
1248
- import { renderHook, act } from '@testing-library/react';
1249
- import { useResize } from "@/hooks/useResize";
1250
-
1251
- describe('useResize', () => {
1252
- it('should handle resize', () => {
1253
- const ref = { current: document.createElement('div') };
1254
- const { result } = renderHook(() => useResize(ref));
1255
-
1256
- expect(result.current.isResizing).toBe(false);
1257
-
1258
- act(() => {
1259
- result.current.handleStartResize({} as React.MouseEvent);
1260
- });
1261
-
1262
- expect(result.current.isResizing).toBe(true);
1263
- });
1264
-
1265
- it('should detect RTL layout', () => {
1266
- document.documentElement.setAttribute('dir', 'rtl');
1267
-
1268
- const ref = { current: document.createElement('div') };
1269
- const { result } = renderHook(() => useResize(ref));
1270
-
1271
- // Internal RTL state should be true
1272
- // Width calculations should be reversed
1273
-
1274
- document.documentElement.removeAttribute('dir');
1275
- });
1276
- });
1277
- ```
1278
-
1279
- ### Testing useTagSelection
1280
-
1281
- ```tsx
1282
- import { renderHook, act } from '@testing-library/react';
1283
- import { useTagSelection } from "@/hooks/useTagSelection";
1284
-
1285
- describe('useTagSelection', () => {
1286
- const mockTags = [
1287
- { id: '1', name: 'Tag 1', isSelected: false },
1288
- { id: '2', name: 'Tag 2', isSelected: false }
1289
- ];
1290
-
1291
- it('should select tag', () => {
1292
- const { result } = renderHook(() =>
1293
- useTagSelection({ Tags: mockTags })
1294
- );
1295
-
1296
- act(() => {
1297
- result.current.handleSelectTag('1');
1298
- });
1299
-
1300
- expect(result.current.selectedTagsStack).toHaveLength(1);
1301
- expect(result.current.selectedTagsStack[0].id).toBe('1');
1302
- });
1303
-
1304
- it('should respect single select mode', () => {
1305
- const { result } = renderHook(() =>
1306
- useTagSelection({ Tags: mockTags, singleSelect: true })
1307
- );
1308
-
1309
- act(() => {
1310
- result.current.handleSelectTag('1');
1311
- result.current.handleSelectTag('2');
1312
- });
1313
-
1314
- expect(result.current.selectedTagsStack).toHaveLength(1);
1315
- expect(result.current.selectedTagsStack[0].id).toBe('2');
1316
- });
1317
-
1318
- it('should filter tags by search', () => {
1319
- const { result } = renderHook(() =>
1320
- useTagSelection({ Tags: mockTags })
1321
- );
1322
-
1323
- act(() => {
1324
- result.current.filterTagsBySearch('Tag 1');
1325
- });
1326
-
1327
- expect(result.current.filteredTags).toHaveLength(1);
1328
- expect(result.current.filteredTags[0].name).toBe('Tag 1');
1329
- });
1330
- });
1331
- ```
1332
-
1333
- ---
1334
-
1335
- ## Performance Considerations
1336
-
1337
- ### useActiveTreeItem
1338
-
1339
- - **Cleanup**: Automatically unobserves all elements on unmount
1340
- - **Re-observation**: Updates observers when `itemIds` array changes
1341
- - **Optimization**: Use stable `itemIds` array (useMemo) to avoid re-creating observers
1342
-
1343
- ```tsx
1344
- const itemIds = useMemo(() =>
1345
- sections.map(s => s.id),
1346
- [sections]
1347
- );
1348
-
1349
- const { activeId } = useActiveTreeItem(itemIds);
1350
- ```
1351
-
1352
- ### useClickOutside
1353
-
1354
- - **Event listeners**: Automatically cleaned up on unmount
1355
- - **Re-creation**: Callback is re-attached when it changes - use `useCallback` for stable callbacks
1356
-
1357
- ```tsx
1358
- const handleClose = useCallback(() => {
1359
- setIsOpen(false);
1360
- }, []);
1361
-
1362
- const ref = useClickOutside(handleClose);
1363
- ```
1364
-
1365
- ### useResize
1366
-
1367
- - **Event listeners**: Conditionally attached only during resize
1368
- - **High-frequency updates**: Consider throttling or debouncing width updates for better performance
1369
-
1370
- ```tsx
1371
- const [width, setWidth] = useState<number>();
1372
- const resize = useResize(panelRef);
1373
-
1374
- useEffect(() => {
1375
- if (resize.width !== undefined) {
1376
- const timeoutId = setTimeout(() => {
1377
- setWidth(resize.width);
1378
- }, 16); // ~60fps
1379
-
1380
- return () => clearTimeout(timeoutId);
1381
- }
1382
- }, [resize.width]);
1383
- ```
1384
-
1385
- ### useTagSelection
1386
-
1387
- - **Large tag lists**: Filter results are computed on every search - consider virtualizing long lists
1388
- - **Memoization**: Use `useMemo` for derived values if you have complex tag transformations
1389
-
1390
- ```tsx
1391
- const heavyTags = useMemo(() =>
1392
- rawTags.map(tag => ({
1393
- ...tag,
1394
- computedValue: expensiveComputation(tag)
1395
- })),
1396
- [rawTags]
1397
- );
1398
-
1399
- const { selectedTagsStack } = useTagSelection({ Tags: heavyTags });
1400
- ```
1401
-
1402
- ---
1403
-
1404
- ## Best Practices
1405
-
1406
- ### useActiveTreeItem
1407
-
1408
- 1. **Stable IDs**: Ensure element IDs don't change between renders
1409
- 2. **Performance**: Don't observe too many elements (< 50 recommended)
1410
- 3. **Accessibility**: Update aria-current when activeId changes
1411
-
1412
- ```tsx
1413
- <a
1414
- href={`#${id}`}
1415
- aria-current={activeId === id ? 'location' : undefined}
1416
- >
1417
- {title}
1418
- </a>
1419
- ```
1420
-
1421
- ### useClickOutside
1422
-
1423
- 1. **Cleanup**: Always clean up when unmounting conditional content
1424
- 2. **Portal compatibility**: Works with React portals
1425
- 3. **Multiple instances**: Can use multiple instances on same page
1426
-
1427
- ```tsx
1428
- {isOpen && createPortal(
1429
- <div ref={useClickOutside(() => setIsOpen(false))}>
1430
- Modal content
1431
- </div>,
1432
- document.body
1433
- )}
1434
- ```
1435
-
1436
- ### useResize
1437
-
1438
- 1. **Touch support**: Always include both mouse and touch handlers
1439
- 2. **Visual feedback**: Show visual indicator during resize
1440
- 3. **Constraints**: Implement min/max width constraints
1441
-
1442
- ```tsx
1443
- <div
1444
- onMouseDown={handleStartResize}
1445
- onTouchStart={handleStartResize}
1446
- className={cn(
1447
- 'resize-handle',
1448
- isResizing && 'active'
1449
- )}
1450
- />
1451
- ```
1452
-
1453
- ### useTagSelection
1454
-
1455
- 1. **Accessibility**: Provide keyboard navigation instructions
1456
- 2. **Validation**: Validate tag selection before form submission
1457
- 3. **Performance**: Virtualize long tag lists (100+ items)
1458
-
1459
- ```tsx
1460
- const { selectedTagsStack, handleKeyDown } = useTagSelection({
1461
- Tags: tags,
1462
- onTagsChange: (selected) => {
1463
- if (selected.length < 3) {
1464
- setError('Please select at least 3 tags');
1465
- }
1466
- }
1467
- });
1468
- ```
1469
-
1470
- ---
1471
-
1472
- ## Related Components
1473
-
1474
- - **TreeView**: Uses `useActiveTreeItem` for navigation highlighting
1475
- - **Popover**: Uses `useClickOutside` for closing on outside clicks
1476
- - **TagSelector**: Built on top of `useTagSelection`
1477
- - **ResizablePanel**: Uses `useResize` for panel sizing
1478
-
1479
- ---
1480
-
1481
- ## Accessibility
1482
-
1483
- All hooks follow accessibility best practices:
1484
-
1485
- - **useActiveTreeItem**: Updates ARIA current location
1486
- - **useClickOutside**: Preserves focus management
1487
- - **useResize**: Supports keyboard-only resize (implement separately)
1488
- - **useTagSelection**: Full keyboard navigation with ARIA roles
1489
-
1490
- ---
1491
-
1492
- ## Browser Support
1493
-
1494
- All hooks support:
1495
-
1496
- - Chrome 90+
1497
- - Firefox 88+
1498
- - Safari 14+
1499
- - Edge 90+
1500
-
1501
- **IntersectionObserver** (useActiveTreeItem): Requires polyfill for older browsers.
1502
-
1503
- ---
1504
-
1505
- ## useHtmlDir
1506
-
1507
- Tracks the document's text direction from `<html dir>`, re-reading it when it changes — a language
1508
- switch, say. Returns `"ltr" | "rtl"`.
1509
-
1510
- ```tsx
1511
- import { useHtmlDir } from "@/hooks/useHtmlDir";
1512
-
1513
- const dir = useHtmlDir();
1514
- ```
1515
-
1516
- Most mirroring should be done in CSS with logical properties, which need no JS at all. Reach for
1517
- this only where a library wants the direction as a **value**: several Radix primitives default to
1518
- `"ltr"` when given no `dir` prop and no `DirectionProvider`, and vaul computes an inline transform
1519
- from its `direction` prop, which a stylesheet cannot override mid-drag.
1520
-
1521
- | Returns | Notes |
1522
- | --- | --- |
1523
- | `"ltr" \| "rtl"` | SSR-safe — returns `"ltr"` when there is no `document`. Watches the attribute with a `MutationObserver`, so a runtime language switch updates every consumer. |