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.
- package/README.md +4 -3
- package/dist/bin/index.js +9 -24
- package/dist/bin/index.js.map +1 -1
- package/dist/src/commands/add.d.ts +2 -2
- package/dist/src/commands/add.d.ts.map +1 -1
- package/dist/src/commands/add.js +4 -70
- package/dist/src/commands/add.js.map +1 -1
- package/dist/src/commands/hook.d.ts +5 -4
- package/dist/src/commands/hook.d.ts.map +1 -1
- package/dist/src/commands/hook.js +7 -75
- package/dist/src/commands/hook.js.map +1 -1
- package/dist/src/commands/init.js +1 -1
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/layout.d.ts +5 -4
- package/dist/src/commands/layout.d.ts.map +1 -1
- package/dist/src/commands/layout.js +7 -75
- package/dist/src/commands/layout.js.map +1 -1
- package/dist/src/commands/provider.d.ts +5 -4
- package/dist/src/commands/provider.d.ts.map +1 -1
- package/dist/src/commands/provider.js +7 -75
- package/dist/src/commands/provider.js.map +1 -1
- package/dist/src/commands/update.d.ts.map +1 -1
- package/dist/src/commands/update.js +78 -43
- package/dist/src/commands/update.js.map +1 -1
- package/dist/src/commands/utils.d.ts +5 -4
- package/dist/src/commands/utils.d.ts.map +1 -1
- package/dist/src/commands/utils.js +7 -80
- package/dist/src/commands/utils.js.map +1 -1
- package/dist/src/shared/addFromRegistry.d.ts +16 -0
- package/dist/src/shared/addFromRegistry.d.ts.map +1 -0
- package/dist/src/shared/addFromRegistry.js +88 -0
- package/dist/src/shared/addFromRegistry.js.map +1 -0
- package/dist/src/shared/getInstallCommand.d.ts.map +1 -1
- package/dist/src/shared/getInstallCommand.js +5 -0
- package/dist/src/shared/getInstallCommand.js.map +1 -1
- package/dist/src/shared/installDependencies.d.ts +0 -10
- package/dist/src/shared/installDependencies.d.ts.map +1 -1
- package/dist/src/shared/installDependencies.js +0 -17
- package/dist/src/shared/installDependencies.js.map +1 -1
- package/dist/src/shared/installFromPlan.d.ts +7 -5
- package/dist/src/shared/installFromPlan.d.ts.map +1 -1
- package/dist/src/shared/installFromPlan.js +103 -36
- package/dist/src/shared/installFromPlan.js.map +1 -1
- package/dist/src/shared/loadRegistry.d.ts +12 -3
- package/dist/src/shared/loadRegistry.d.ts.map +1 -1
- package/dist/src/shared/loadRegistry.js +19 -20
- package/dist/src/shared/loadRegistry.js.map +1 -1
- package/dist/src/shared/registryClient.d.ts +19 -0
- package/dist/src/shared/registryClient.d.ts.map +1 -0
- package/dist/src/shared/registryClient.js +128 -0
- package/dist/src/shared/registryClient.js.map +1 -0
- package/dist/src/shared/resolveEntry.d.ts +10 -7
- package/dist/src/shared/resolveEntry.d.ts.map +1 -1
- package/dist/src/shared/resolveEntry.js +18 -13
- package/dist/src/shared/resolveEntry.js.map +1 -1
- package/dist/src/shared/suggestOtherCommand.d.ts +7 -1
- package/dist/src/shared/suggestOtherCommand.d.ts.map +1 -1
- package/dist/src/shared/suggestOtherCommand.js +29 -23
- package/dist/src/shared/suggestOtherCommand.js.map +1 -1
- package/dist/src/types/main.d.ts +31 -2
- package/dist/src/types/main.d.ts.map +1 -1
- package/package.json +6 -11
- package/apps/lib/components/ActionButton.tsx +0 -93
- package/apps/lib/components/ActionsGroup.tsx +0 -27
- package/apps/lib/components/AlertDialog.tsx +0 -204
- package/apps/lib/components/Avatar.tsx +0 -46
- package/apps/lib/components/Badge.tsx +0 -250
- package/apps/lib/components/BadgeField.tsx +0 -403
- package/apps/lib/components/Breadcrumb.tsx +0 -280
- package/apps/lib/components/Button.tsx +0 -325
- package/apps/lib/components/ButtonGroup.tsx +0 -190
- package/apps/lib/components/Calendar.tsx +0 -115
- package/apps/lib/components/Card.tsx +0 -95
- package/apps/lib/components/Checkbox.tsx +0 -42
- package/apps/lib/components/ColorPicker.tsx +0 -441
- package/apps/lib/components/ConclusionHeader.tsx +0 -148
- package/apps/lib/components/ContextMenu.tsx +0 -571
- package/apps/lib/components/CountBadge.tsx +0 -52
- package/apps/lib/components/DataTable.tsx +0 -213
- package/apps/lib/components/DataViews/badge.ts +0 -49
- package/apps/lib/components/DataViews/cell.tsx +0 -324
- package/apps/lib/components/DataViews/context.ts +0 -144
- package/apps/lib/components/DataViews/data-views.tsx +0 -395
- package/apps/lib/components/DataViews/filters/children.tsx +0 -98
- package/apps/lib/components/DataViews/filters/custom.tsx +0 -34
- package/apps/lib/components/DataViews/filters/filters.tsx +0 -157
- package/apps/lib/components/DataViews/filters/index.ts +0 -4
- package/apps/lib/components/DataViews/filters/labelled.tsx +0 -20
- package/apps/lib/components/DataViews/filters/presets.tsx +0 -65
- package/apps/lib/components/DataViews/filters/sync.tsx +0 -35
- package/apps/lib/components/DataViews/filters/values.ts +0 -173
- package/apps/lib/components/DataViews/header.tsx +0 -220
- package/apps/lib/components/DataViews/hooks/index.ts +0 -5
- package/apps/lib/components/DataViews/hooks/useActiveRow.ts +0 -22
- package/apps/lib/components/DataViews/hooks/useControllable.ts +0 -52
- package/apps/lib/components/DataViews/index.ts +0 -88
- package/apps/lib/components/DataViews/panel/columns.tsx +0 -153
- package/apps/lib/components/DataViews/panel/controls.tsx +0 -106
- package/apps/lib/components/DataViews/panel/index.ts +0 -3
- package/apps/lib/components/DataViews/panel/panel.tsx +0 -164
- package/apps/lib/components/DataViews/panel/saved-views.tsx +0 -67
- package/apps/lib/components/DataViews/panel/section.tsx +0 -81
- package/apps/lib/components/DataViews/panel/sort.tsx +0 -42
- package/apps/lib/components/DataViews/panel/tab.tsx +0 -31
- package/apps/lib/components/DataViews/slots.ts +0 -72
- package/apps/lib/components/DataViews/states.tsx +0 -73
- package/apps/lib/components/DataViews/types.ts +0 -510
- package/apps/lib/components/DataViews/views/board-view.tsx +0 -381
- package/apps/lib/components/DataViews/views/card-rows.tsx +0 -36
- package/apps/lib/components/DataViews/views/inbox-view.tsx +0 -260
- package/apps/lib/components/DataViews/views/pane-views.tsx +0 -196
- package/apps/lib/components/DataViews/views/table-view.tsx +0 -493
- package/apps/lib/components/DataViews/views/tree-view.tsx +0 -367
- package/apps/lib/components/DatePicker.tsx +0 -232
- package/apps/lib/components/Dialog.tsx +0 -116
- package/apps/lib/components/Divider.tsx +0 -28
- package/apps/lib/components/Drawer.tsx +0 -451
- package/apps/lib/components/DropdownMenu.tsx +0 -554
- package/apps/lib/components/FieldHint.tsx +0 -69
- package/apps/lib/components/Form.tsx +0 -182
- package/apps/lib/components/FormBuilder/context.ts +0 -85
- package/apps/lib/components/FormBuilder/field-kind.ts +0 -28
- package/apps/lib/components/FormBuilder/fields/ChoiceFields.tsx +0 -60
- package/apps/lib/components/FormBuilder/fields/ColorField.tsx +0 -59
- package/apps/lib/components/FormBuilder/fields/CustomField.tsx +0 -11
- package/apps/lib/components/FormBuilder/fields/DateField.tsx +0 -32
- package/apps/lib/components/FormBuilder/fields/FieldArray.tsx +0 -74
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +0 -168
- package/apps/lib/components/FormBuilder/fields/FileField.tsx +0 -39
- package/apps/lib/components/FormBuilder/fields/OptionListFields.tsx +0 -132
- package/apps/lib/components/FormBuilder/fields/OtpField.tsx +0 -31
- package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +0 -109
- package/apps/lib/components/FormBuilder/fields/RichTextEditorField.tsx +0 -31
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +0 -114
- package/apps/lib/components/FormBuilder/fields/SignatureField.tsx +0 -157
- package/apps/lib/components/FormBuilder/fields/SliderField.tsx +0 -67
- package/apps/lib/components/FormBuilder/fields/SwitchBoxField.tsx +0 -41
- package/apps/lib/components/FormBuilder/fields/TableField.tsx +0 -336
- package/apps/lib/components/FormBuilder/fields/TextField.tsx +0 -213
- package/apps/lib/components/FormBuilder/fields/TreeSelectField.tsx +0 -43
- package/apps/lib/components/FormBuilder/fields/countries.ts +0 -303
- package/apps/lib/components/FormBuilder/fields/index.ts +0 -25
- package/apps/lib/components/FormBuilder/form-builder.tsx +0 -198
- package/apps/lib/components/FormBuilder/index.ts +0 -31
- package/apps/lib/components/FormBuilder/numberFormat.ts +0 -16
- package/apps/lib/components/FormBuilder/submit.tsx +0 -67
- package/apps/lib/components/FormBuilder/types.ts +0 -316
- package/apps/lib/components/FormRenderer/FormDrawer.tsx +0 -250
- package/apps/lib/components/FormRenderer/detail.tsx +0 -256
- package/apps/lib/components/FormRenderer/form-renderer.tsx +0 -358
- package/apps/lib/components/FormRenderer/header.tsx +0 -88
- package/apps/lib/components/FormRenderer/index.ts +0 -17
- package/apps/lib/components/FormRenderer/notch-action.tsx +0 -64
- package/apps/lib/components/FormRenderer/section.tsx +0 -39
- package/apps/lib/components/FormRenderer/stepper.tsx +0 -393
- package/apps/lib/components/FormRenderer/types.ts +0 -115
- package/apps/lib/components/FormSummary.tsx +0 -282
- package/apps/lib/components/HeaderBar.tsx +0 -117
- package/apps/lib/components/ImageAttachment.tsx +0 -209
- package/apps/lib/components/InnerLabelField.tsx +0 -154
- package/apps/lib/components/Input.tsx +0 -224
- package/apps/lib/components/InputField.tsx +0 -147
- package/apps/lib/components/InputOTP.tsx +0 -84
- package/apps/lib/components/Label.tsx +0 -129
- package/apps/lib/components/LabelField.tsx +0 -76
- package/apps/lib/components/LabeledCheckBox.tsx +0 -51
- package/apps/lib/components/LabeledRadio.tsx +0 -51
- package/apps/lib/components/LinkButton.tsx +0 -94
- package/apps/lib/components/LoginButton.tsx +0 -51
- package/apps/lib/components/PasswordLevel.tsx +0 -58
- package/apps/lib/components/Popover.tsx +0 -312
- package/apps/lib/components/ProfileMenu.tsx +0 -85
- package/apps/lib/components/Radio.tsx +0 -59
- package/apps/lib/components/RadioCard.tsx +0 -50
- package/apps/lib/components/ScrollArea.tsx +0 -54
- package/apps/lib/components/SearchField.tsx +0 -62
- package/apps/lib/components/SearchableSelect.tsx +0 -318
- package/apps/lib/components/SearchableTable.tsx +0 -344
- package/apps/lib/components/SearchableTree.tsx +0 -485
- package/apps/lib/components/SearchableTreeDialog.tsx +0 -502
- package/apps/lib/components/SectionBlock.tsx +0 -168
- package/apps/lib/components/Select.tsx +0 -347
- package/apps/lib/components/SimpleSelect.tsx +0 -225
- package/apps/lib/components/Skeleton.tsx +0 -12
- package/apps/lib/components/SlideDatePicker.tsx +0 -255
- package/apps/lib/components/SpinLoading.tsx +0 -174
- package/apps/lib/components/Stepper.tsx +0 -480
- package/apps/lib/components/Switch.tsx +0 -82
- package/apps/lib/components/TabFormItem.tsx +0 -153
- package/apps/lib/components/TabSwitch.tsx +0 -172
- package/apps/lib/components/Table.tsx +0 -597
- package/apps/lib/components/TextEditor/ChartBlockTool.ts +0 -676
- package/apps/lib/components/TextEditor/RichTextField.tsx +0 -46
- package/apps/lib/components/TextEditor/TableDnDWrapper.ts +0 -462
- package/apps/lib/components/TextEditor/TextEditor.tsx +0 -1164
- package/apps/lib/components/TextEditor/TextEditorToolbar.tsx +0 -429
- package/apps/lib/components/TextEditor/editor-tools/AlignmentTune.ts +0 -70
- package/apps/lib/components/TextEditor/editor-tools/ColorInlineTool.ts +0 -50
- package/apps/lib/components/TextEditor/editor-tools/StrikethroughInlineTool.ts +0 -48
- package/apps/lib/components/TextEditor/editor-tools/inlineFormat.ts +0 -98
- package/apps/lib/components/TextEditor/editorjs.d.ts +0 -60
- package/apps/lib/components/TextEditor/index.ts +0 -7
- package/apps/lib/components/TextEditor/markdownParser.ts +0 -346
- package/apps/lib/components/Textarea.tsx +0 -117
- package/apps/lib/components/Timeline.tsx +0 -263
- package/apps/lib/components/Toast.tsx +0 -87
- package/apps/lib/components/Toggle.tsx +0 -125
- package/apps/lib/components/ToggleButton.tsx +0 -140
- package/apps/lib/components/Tooltip.tsx +0 -116
- package/apps/lib/components/TransparentLabel.tsx +0 -71
- package/apps/lib/components/TreeDropDown.tsx +0 -115
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +0 -380
- package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +0 -75
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +0 -354
- package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +0 -60
- package/apps/lib/components/TreeFolder/icons.tsx +0 -60
- package/apps/lib/components/TreeFolder/index.ts +0 -24
- package/apps/lib/components/TreeFolder/treeFolderUtils.ts +0 -109
- package/apps/lib/components/TreeFolder/types.ts +0 -77
- package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +0 -118
- package/apps/lib/hooks/useActiveTreeItem.ts +0 -61
- package/apps/lib/hooks/useClickOutside.ts +0 -27
- package/apps/lib/hooks/useDragDrop.tsx +0 -365
- package/apps/lib/hooks/useHtmlDir.ts +0 -31
- package/apps/lib/hooks/useInfiniteScroll.ts +0 -108
- package/apps/lib/hooks/useIsMobile.ts +0 -21
- package/apps/lib/hooks/useResize.ts +0 -67
- package/apps/lib/hooks/useTagSelection.ts +0 -300
- package/apps/lib/layouts/CNLayout.tsx +0 -372
- package/apps/lib/layouts/DataViewCard.tsx +0 -62
- package/apps/lib/layouts/FieldSection.tsx +0 -91
- package/apps/lib/layouts/TreeSubLayout.tsx +0 -173
- package/apps/lib/providers/ThemeProvider.tsx +0 -84
- package/apps/lib/registry.json +0 -1217
- package/apps/lib/tsconfig.json +0 -9
- package/apps/lib/tsconfig.tsbuildinfo +0 -1
- package/apps/lib/utils/cn.ts +0 -6
- package/apps/lib/utils/color.ts +0 -175
- package/apps/lib/utils/dataViews/path.ts +0 -67
- package/apps/lib/utils/dataViews/query.ts +0 -73
- package/apps/lib/utils/dataViews/types.ts +0 -187
- package/apps/lib/utils/dateFormat.ts +0 -108
- package/apps/lib/utils/resize.ts +0 -31
- package/apps/lib/utils/scroller.ts +0 -26
- package/apps/lib/utils/types.ts +0 -16
- package/docs/BLOCKS.md +0 -129
- package/docs/CHANGELOG-1.1.16.md +0 -108
- package/docs/Cover.png +0 -0
- package/docs/README.md +0 -215
- package/docs/components/action-button.md +0 -638
- package/docs/components/actions-group.md +0 -708
- package/docs/components/alert-dialog.md +0 -920
- package/docs/components/avatar.md +0 -709
- package/docs/components/badge-field.md +0 -738
- package/docs/components/badge.md +0 -301
- package/docs/components/breadcrumb.md +0 -930
- package/docs/components/button-group.md +0 -650
- package/docs/components/button.md +0 -639
- package/docs/components/calendar.md +0 -555
- package/docs/components/card.md +0 -622
- package/docs/components/chart-block-tool.md +0 -129
- package/docs/components/checkbox.md +0 -699
- package/docs/components/cn-layout.md +0 -854
- package/docs/components/color-picker.md +0 -101
- package/docs/components/conclusion-header.md +0 -80
- package/docs/components/context-menu.md +0 -483
- package/docs/components/count-badge.md +0 -560
- package/docs/components/data-table.md +0 -854
- package/docs/components/data-views/backend-response.md +0 -324
- package/docs/components/data-views/examples/a11y-rtl.md +0 -250
- package/docs/components/data-views/examples/api-orders-route.md +0 -130
- package/docs/components/data-views/examples/fields.md +0 -362
- package/docs/components/data-views/examples/filters.md +0 -307
- package/docs/components/data-views/examples/inbox-routing.md +0 -218
- package/docs/components/data-views/examples/index.md +0 -29
- package/docs/components/data-views/examples/overview.md +0 -244
- package/docs/components/data-views/examples/panel.md +0 -212
- package/docs/components/data-views/examples/scale.md +0 -231
- package/docs/components/data-views/examples/server-side.md +0 -210
- package/docs/components/data-views/examples/state.md +0 -250
- package/docs/components/data-views/examples/tree-custom.md +0 -388
- package/docs/components/data-views/examples/view-registry.md +0 -313
- package/docs/components/data-views/examples/views.md +0 -534
- package/docs/components/data-views/guide.md +0 -405
- package/docs/components/data-views/index.md +0 -1521
- package/docs/components/data-views/migration.md +0 -81
- package/docs/components/date-picker.md +0 -857
- package/docs/components/dialog.md +0 -929
- package/docs/components/divider.md +0 -766
- package/docs/components/drawer.md +0 -654
- package/docs/components/dropdown-menu.md +0 -668
- package/docs/components/field-hint.md +0 -898
- package/docs/components/field-section.md +0 -781
- package/docs/components/form-builder.md +0 -284
- package/docs/components/form-renderer.md +0 -396
- package/docs/components/form-summary.md +0 -138
- package/docs/components/form.md +0 -785
- package/docs/components/header-bar.md +0 -188
- package/docs/components/image-attachment.md +0 -906
- package/docs/components/inner-label-field.md +0 -819
- package/docs/components/input-field.md +0 -623
- package/docs/components/input-otp.md +0 -756
- package/docs/components/input.md +0 -634
- package/docs/components/label-field.md +0 -770
- package/docs/components/label.md +0 -761
- package/docs/components/labeled-check-box.md +0 -642
- package/docs/components/labeled-radio.md +0 -796
- package/docs/components/link-button.md +0 -685
- package/docs/components/login-button.md +0 -872
- package/docs/components/password-level.md +0 -803
- package/docs/components/popover.md +0 -649
- package/docs/components/profile-menu.md +0 -656
- package/docs/components/radio-card.md +0 -801
- package/docs/components/radio.md +0 -735
- package/docs/components/scroll-area.md +0 -691
- package/docs/components/search-field.md +0 -702
- package/docs/components/searchable-select.md +0 -368
- package/docs/components/searchable-table.md +0 -438
- package/docs/components/searchable-tree-dialog.md +0 -190
- package/docs/components/searchable-tree.md +0 -200
- package/docs/components/section-block.md +0 -483
- package/docs/components/select.md +0 -709
- package/docs/components/simple-select.md +0 -741
- package/docs/components/skeleton.md +0 -706
- package/docs/components/slide-date-picker.md +0 -837
- package/docs/components/spin-loading.md +0 -696
- package/docs/components/stepper.md +0 -316
- package/docs/components/switch.md +0 -770
- package/docs/components/tab-form-item.md +0 -773
- package/docs/components/tab-switch.md +0 -170
- package/docs/components/table-dnd-wrapper.md +0 -107
- package/docs/components/table.md +0 -1088
- package/docs/components/text-editor.md +0 -735
- package/docs/components/textarea.md +0 -681
- package/docs/components/timeline.md +0 -254
- package/docs/components/toast.md +0 -773
- package/docs/components/toggle-button.md +0 -643
- package/docs/components/toggle.md +0 -882
- package/docs/components/tooltip.md +0 -513
- package/docs/components/transparent-label.md +0 -721
- package/docs/components/tree-drop-down.md +0 -740
- package/docs/components/tree-folder.md +0 -110
- package/docs/components/tree-sub-layout.md +0 -707
- package/docs/explanation/architecture.md +0 -54
- package/docs/explanation/design-system.md +0 -52
- package/docs/how-to/form-and-list-recipes.md +0 -386
- package/docs/how-to/forms-with-form-builder.md +0 -472
- package/docs/how-to/guides.md +0 -1027
- package/docs/migration/changelog.md +0 -60
- package/docs/migration/form-builder-2.5.2.md +0 -113
- package/docs/reference/cli.md +0 -112
- package/docs/reference/components.md +0 -572
- package/docs/reference/hooks.md +0 -1523
- package/docs/reference/providers.md +0 -866
- package/docs/reference/tailwind-plugins.md +0 -942
- package/docs/reference/theme.md +0 -85
- package/docs/reference/types.md +0 -1016
- package/docs/reference/utilities.md +0 -726
- package/docs/tutorials/building-first-form.md +0 -846
- package/docs/tutorials/component-composition.md +0 -882
- package/docs/tutorials/getting-started.md +0 -302
- package/docs/tutorials/theming-basics.md +0 -785
package/docs/reference/hooks.md
DELETED
|
@@ -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. |
|