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