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.
- 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 -338
- package/apps/lib/components/Breadcrumb.tsx +0 -280
- package/apps/lib/components/Button.tsx +0 -317
- 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 -383
- 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 -84
- 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 -63
- package/apps/lib/components/DataViews/states.tsx +0 -38
- 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 -420
- package/apps/lib/components/DropdownMenu.tsx +0 -540
- package/apps/lib/components/FieldHint.tsx +0 -69
- package/apps/lib/components/Form.tsx +0 -182
- package/apps/lib/components/FormBuilder/context.ts +0 -73
- 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 -163
- 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 -91
- 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 -47
- package/apps/lib/components/FormBuilder/types.ts +0 -311
- package/apps/lib/components/FormRenderer/FormDrawer.tsx +0 -128
- package/apps/lib/components/FormRenderer/detail.tsx +0 -207
- package/apps/lib/components/FormRenderer/form-renderer.tsx +0 -297
- package/apps/lib/components/FormRenderer/header.tsx +0 -88
- package/apps/lib/components/FormRenderer/index.ts +0 -15
- package/apps/lib/components/FormRenderer/section.tsx +0 -39
- package/apps/lib/components/FormRenderer/stepper.tsx +0 -339
- package/apps/lib/components/FormRenderer/types.ts +0 -78
- 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 -147
- 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 -253
- 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 -610
- 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 -58
- package/apps/lib/hooks/useClickOutside.ts +0 -27
- package/apps/lib/hooks/useDragDrop.tsx +0 -365
- 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 -214
- 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 -1201
- 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/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 -712
- 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 -1495
- package/docs/components/data-views/migration.md +0 -79
- 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 -276
- package/docs/components/form-renderer.md +0 -326
- 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 -477
- 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 -54
- 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 -1500
- 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 -704
- 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
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: SearchableTable
|
|
3
|
-
description: A field that opens a modal dialog to pick a row from a real Table, with single-select, client- or server-side search, and infinite-scroll pagination
|
|
4
|
-
group: Inputs
|
|
5
|
-
keywords: [searchable-table, dialog, table, search, async, infinite-scroll, pagination, select, picker]
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# SearchableTable
|
|
9
|
-
|
|
10
|
-
> A field that opens a modal `Dialog` to pick a row from a real, multi-column `Table`. The trigger shows a placeholder until something is selected, then the selected row's label. Click it to open the dialog (a search input + the data table); type to filter rows (client-side) or refetch them from a backend (server-side), click a row to single-select it and close the dialog, and scroll to the bottom to lazy-load more pages. Generic over your row type `T`.
|
|
11
|
-
|
|
12
|
-
## Installation
|
|
13
|
-
|
|
14
|
-
TORCH Glare is a copy-in library: the CLI copies this component's source into your project
|
|
15
|
-
(you do **not** install it from the npm package). Run `init` once, then `add`:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npx torch-glare@latest init
|
|
19
|
-
npx torch-glare@latest add SearchableTable
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
`add` also copies any components, hooks, and utilities that `SearchableTable` depends on.
|
|
23
|
-
|
|
24
|
-
## Import
|
|
25
|
-
|
|
26
|
-
Import from your project's local path — the alias configured in `glare.json` (e.g. `@/*`):
|
|
27
|
-
|
|
28
|
-
```tsx
|
|
29
|
-
import { SearchableTable } from "@/components/SearchableTable";
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Quick Examples
|
|
33
|
-
|
|
34
|
-
### Basic (static rows)
|
|
35
|
-
|
|
36
|
-
Provide a `columns` config and a `rows` array. Single-select is controlled via `value` / `onSelect`. Use `getLabel` to control the text shown in the input after selection, and `getRowId` to give each row a stable key.
|
|
37
|
-
|
|
38
|
-
```typescript
|
|
39
|
-
import { SearchableTable } from "@/components/SearchableTable";
|
|
40
|
-
import type { SearchableTableColumn } from "@/components/SearchableTable";
|
|
41
|
-
import { useState } from 'react'
|
|
42
|
-
|
|
43
|
-
type User = { id: string; name: string; role: string; email: string }
|
|
44
|
-
|
|
45
|
-
const users: User[] = [
|
|
46
|
-
{ id: '1', name: 'Ada Lovelace', role: 'Engineer', email: 'ada@torch.dev' },
|
|
47
|
-
{ id: '2', name: 'Alan Turing', role: 'Researcher', email: 'alan@torch.dev' },
|
|
48
|
-
{ id: '3', name: 'Grace Hopper', role: 'Admiral', email: 'grace@torch.dev' },
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
const columns: SearchableTableColumn<User>[] = [
|
|
52
|
-
{ key: 'name', header: 'Name' },
|
|
53
|
-
{ key: 'role', header: 'Role' },
|
|
54
|
-
{ key: 'email', header: 'Email' },
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
function Example() {
|
|
58
|
-
const [selected, setSelected] = useState<User | null>(null)
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<SearchableTable<User>
|
|
62
|
-
columns={columns}
|
|
63
|
-
rows={users}
|
|
64
|
-
value={selected}
|
|
65
|
-
onSelect={setSelected}
|
|
66
|
-
getLabel={(row) => row.name}
|
|
67
|
-
getRowId={(row) => row.id}
|
|
68
|
-
icon={<i className="ri-user-line" />}
|
|
69
|
-
placeholder="Select a user…"
|
|
70
|
-
title="Select a user"
|
|
71
|
-
/>
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Clicking the field opens a modal dialog with a search input and the data table. As you type, rows are filtered locally by every column key. Clicking a row selects it (single-select), closes the dialog, and shows `getLabel(row)` on the trigger. Use `placeholder` for the trigger text, `searchPlaceholder` for the in-dialog search input, and `title` for the dialog's search-field label (also the accessible dialog title).
|
|
77
|
-
|
|
78
|
-
### Custom cell rendering
|
|
79
|
-
|
|
80
|
-
Use a column's `render` to control how a cell is displayed. When omitted, the cell falls back to `String(row[key])`.
|
|
81
|
-
|
|
82
|
-
```typescript
|
|
83
|
-
const columns: SearchableTableColumn<User>[] = [
|
|
84
|
-
{ key: 'name', header: 'Name' },
|
|
85
|
-
{
|
|
86
|
-
key: 'role',
|
|
87
|
-
header: 'Role',
|
|
88
|
-
render: (row) => (
|
|
89
|
-
<span className="rounded bg-white-alpha-20 px-2 py-0.5">{row.role}</span>
|
|
90
|
-
),
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
key: 'email',
|
|
94
|
-
header: 'Contact',
|
|
95
|
-
render: (row) => <a href={`mailto:${row.email}`}>{row.email}</a>,
|
|
96
|
-
},
|
|
97
|
-
]
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### Async server search + infinite scroll
|
|
101
|
-
|
|
102
|
-
Set `filterClientSide={false}` and treat `rows` as already-filtered server data. `onSearchChange` fires (debounced by `searchDebounceMs`, default 300ms) whenever the query settles — refetch there. As the list nears the bottom, `onLoadMore` fires while `hasMore && !loading`; a loading row renders whenever `loading` is true.
|
|
103
|
-
|
|
104
|
-
```typescript
|
|
105
|
-
import { SearchableTable } from "@/components/SearchableTable";
|
|
106
|
-
import type { SearchableTableColumn } from "@/components/SearchableTable";
|
|
107
|
-
import { useEffect, useState } from 'react'
|
|
108
|
-
|
|
109
|
-
type User = { id: string; name: string; role: string; email: string }
|
|
110
|
-
|
|
111
|
-
const columns: SearchableTableColumn<User>[] = [
|
|
112
|
-
{ key: 'name', header: 'Name' },
|
|
113
|
-
{ key: 'role', header: 'Role' },
|
|
114
|
-
{ key: 'email', header: 'Email' },
|
|
115
|
-
]
|
|
116
|
-
|
|
117
|
-
async function fetchUsers(query: string, page: number) {
|
|
118
|
-
const res = await fetch(`/api/users?q=${encodeURIComponent(query)}&page=${page}`)
|
|
119
|
-
return res.json() as Promise<{ items: User[]; hasMore: boolean }>
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function AsyncExample() {
|
|
123
|
-
const [rows, setRows] = useState<User[]>([])
|
|
124
|
-
const [query, setQuery] = useState('')
|
|
125
|
-
const [page, setPage] = useState(1)
|
|
126
|
-
const [hasMore, setHasMore] = useState(true)
|
|
127
|
-
const [loading, setLoading] = useState(false)
|
|
128
|
-
const [selected, setSelected] = useState<User | null>(null)
|
|
129
|
-
|
|
130
|
-
// Refetch from page 1 whenever the debounced query changes.
|
|
131
|
-
useEffect(() => {
|
|
132
|
-
let cancelled = false
|
|
133
|
-
setLoading(true)
|
|
134
|
-
fetchUsers(query, 1).then(({ items, hasMore }) => {
|
|
135
|
-
if (cancelled) return
|
|
136
|
-
setRows(items)
|
|
137
|
-
setHasMore(hasMore)
|
|
138
|
-
setPage(1)
|
|
139
|
-
setLoading(false)
|
|
140
|
-
})
|
|
141
|
-
return () => {
|
|
142
|
-
cancelled = true
|
|
143
|
-
}
|
|
144
|
-
}, [query])
|
|
145
|
-
|
|
146
|
-
const loadMore = async () => {
|
|
147
|
-
const next = page + 1
|
|
148
|
-
setLoading(true)
|
|
149
|
-
const { items, hasMore } = await fetchUsers(query, next)
|
|
150
|
-
setRows((prev) => [...prev, ...items])
|
|
151
|
-
setHasMore(hasMore)
|
|
152
|
-
setPage(next)
|
|
153
|
-
setLoading(false)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return (
|
|
157
|
-
<SearchableTable<User>
|
|
158
|
-
columns={columns}
|
|
159
|
-
rows={rows}
|
|
160
|
-
value={selected}
|
|
161
|
-
onSelect={setSelected}
|
|
162
|
-
getRowId={(row) => row.id}
|
|
163
|
-
getLabel={(row) => row.name}
|
|
164
|
-
filterClientSide={false}
|
|
165
|
-
onSearchChange={setQuery}
|
|
166
|
-
hasMore={hasMore}
|
|
167
|
-
loading={loading}
|
|
168
|
-
onLoadMore={loadMore}
|
|
169
|
-
icon={<i className="ri-user-line" />}
|
|
170
|
-
placeholder="Select a user…"
|
|
171
|
-
title="Select a user"
|
|
172
|
-
searchPlaceholder="Search users… (scroll to load more)"
|
|
173
|
-
/>
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### Dialog labels (trigger, title, search)
|
|
179
|
-
|
|
180
|
-
Three props control the dialog's text. `placeholder` is the trigger text shown until a row is selected; `title` labels the dialog's search field (and is the accessible dialog title); `searchPlaceholder` is the placeholder of the in-dialog search input.
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
<SearchableTable<User>
|
|
184
|
-
columns={columns}
|
|
185
|
-
rows={users}
|
|
186
|
-
value={selected}
|
|
187
|
-
onSelect={setSelected}
|
|
188
|
-
getRowId={(row) => row.id}
|
|
189
|
-
icon={<i className="ri-user-line" />}
|
|
190
|
-
placeholder="Select a user…"
|
|
191
|
-
title="Select a user"
|
|
192
|
-
searchPlaceholder="Search by name or role…"
|
|
193
|
-
/>
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
The data table inside the dialog scrolls vertically once it exceeds `~55vh`; the remaining rows stay reachable via scroll, which is also what drives infinite-scroll loading.
|
|
197
|
-
|
|
198
|
-
## API Reference
|
|
199
|
-
|
|
200
|
-
### SearchableTable<T>
|
|
201
|
-
|
|
202
|
-
| Prop | Type | Default | Description |
|
|
203
|
-
|------|------|---------|-------------|
|
|
204
|
-
| `columns` | `SearchableTableColumn<T>[]` | Required | Column config — header, the row key to read, and an optional cell renderer. |
|
|
205
|
-
| `rows` | `T[]` | Required | The data rows. In server mode these are rendered as-is (already filtered upstream). |
|
|
206
|
-
| `value` | `T \| null` | - | Controlled selected row. The matching row is highlighted in the table. |
|
|
207
|
-
| `onSelect` | `(row: T) => void` | - | Called when a row is clicked. The dialog closes and the trigger shows the row's label. |
|
|
208
|
-
| `getLabel` | `(row: T) => string` | First column's value | Text shown on the trigger after selection. |
|
|
209
|
-
| `getRowId` | `(row: T) => string` | `JSON.stringify(row)` | Stable id/key per row, used for keys and selection matching. |
|
|
210
|
-
| `searchKeys` | `(keyof T & string)[]` | Every column `key` | Which fields client-side search matches against. |
|
|
211
|
-
| `placeholder` | `string` | `'Select…'` | Trigger placeholder shown until a row is selected. |
|
|
212
|
-
| `searchPlaceholder` | `string` | `'Search…'` | Placeholder for the search input inside the dialog. |
|
|
213
|
-
| `title` | `string` | `'Select an item'` | Label shown on the dialog's search field (also used as the accessible dialog title). |
|
|
214
|
-
| `size` | `'XS' \| 'S' \| 'M'` | `'M'` | Trigger size. (`XS` maps the underlying Group to `S` with a tighter input height.) |
|
|
215
|
-
| `variant` | `'PresentationStyle'` | `'PresentationStyle'` | Visual style of the trigger field. |
|
|
216
|
-
| `icon` | `ReactNode` | - | Optional leading icon rendered inside the trigger. |
|
|
217
|
-
| `theme` | `'dark' \| 'light' \| 'default'` | - | Theme variant, applied via `data-theme`. |
|
|
218
|
-
| `dir` | `string` | - | Text direction (e.g. `'rtl'`), applied to the trigger and dialog. |
|
|
219
|
-
| `className` | `string` | - | Additional classes merged onto the trigger group. |
|
|
220
|
-
| `filterClientSide` | `boolean` | `true` | When `true`, filter `rows` locally by `searchKeys`. Set `false` for server-side search. |
|
|
221
|
-
| `onSearchChange` | `(query: string) => void` | - | Debounced query callback — refetch your data here in server mode. |
|
|
222
|
-
| `searchDebounceMs` | `number` | `300` | Debounce delay (ms) for `onSearchChange`. |
|
|
223
|
-
| `hasMore` | `boolean` | `false` | Whether more pages are available; gates the infinite-scroll loader. |
|
|
224
|
-
| `loading` | `boolean` | `false` | Whether a fetch is in flight; renders a loading row and blocks `onLoadMore`. |
|
|
225
|
-
| `onLoadMore` | `() => void` | - | Called when the list nears the bottom and `hasMore && !loading`. |
|
|
226
|
-
|
|
227
|
-
### SearchableTableColumn<T>
|
|
228
|
-
|
|
229
|
-
| Prop | Type | Default | Description |
|
|
230
|
-
|------|------|---------|-------------|
|
|
231
|
-
| `key` | `keyof T & string` | Required | Property on the row used for default rendering and search. |
|
|
232
|
-
| `header` | `ReactNode` | Required | Column header content. |
|
|
233
|
-
| `render` | `(row: T) => ReactNode` | `String(row[key])` | Custom cell renderer. |
|
|
234
|
-
|
|
235
|
-
## TypeScript
|
|
236
|
-
|
|
237
|
-
### SearchableTableColumn
|
|
238
|
-
|
|
239
|
-
```typescript
|
|
240
|
-
export interface SearchableTableColumn<T> {
|
|
241
|
-
/** Property on the row used for default rendering and search. */
|
|
242
|
-
key: keyof T & string
|
|
243
|
-
header: React.ReactNode
|
|
244
|
-
/** Custom cell renderer; defaults to String(row[key]). */
|
|
245
|
-
render?: (row: T) => React.ReactNode
|
|
246
|
-
}
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
### Component signature
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
import { Themes } from "@/utils/types";
|
|
253
|
-
|
|
254
|
-
interface SearchableTableProps<T> {
|
|
255
|
-
columns: SearchableTableColumn<T>[]
|
|
256
|
-
rows: T[]
|
|
257
|
-
value?: T | null
|
|
258
|
-
onSelect?: (row: T) => void
|
|
259
|
-
getLabel?: (row: T) => string
|
|
260
|
-
getRowId?: (row: T) => string
|
|
261
|
-
searchKeys?: (keyof T & string)[]
|
|
262
|
-
placeholder?: string // trigger text until selection (default 'Select…')
|
|
263
|
-
searchPlaceholder?: string // in-dialog search input (default 'Search…')
|
|
264
|
-
title?: string // dialog search-field label / a11y title (default 'Select an item')
|
|
265
|
-
size?: 'XS' | 'S' | 'M'
|
|
266
|
-
variant?: 'PresentationStyle'
|
|
267
|
-
icon?: React.ReactNode
|
|
268
|
-
theme?: Themes
|
|
269
|
-
dir?: string
|
|
270
|
-
className?: string
|
|
271
|
-
// Async / backend pagination
|
|
272
|
-
filterClientSide?: boolean
|
|
273
|
-
onSearchChange?: (query: string) => void
|
|
274
|
-
searchDebounceMs?: number
|
|
275
|
-
hasMore?: boolean
|
|
276
|
-
loading?: boolean
|
|
277
|
-
onLoadMore?: () => void
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Generic function component, constrained so rows are object-shaped:
|
|
281
|
-
export function SearchableTable<T extends Record<string, unknown>>(
|
|
282
|
-
props: SearchableTableProps<T>
|
|
283
|
-
): JSX.Element
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
Always pass the type argument explicitly (`<SearchableTable<User> … />`) so `columns`, `value`, and the callbacks are fully type-checked against your row shape.
|
|
287
|
-
|
|
288
|
-
## Common Patterns
|
|
289
|
-
|
|
290
|
-
### React Query — `useInfiniteQuery`
|
|
291
|
-
|
|
292
|
-
Wire pagination through `useInfiniteQuery`, flatten the pages into `rows`, and map the query's state onto `hasMore` / `loading` / `onLoadMore`.
|
|
293
|
-
|
|
294
|
-
```typescript
|
|
295
|
-
import { SearchableTable } from "@/components/SearchableTable";
|
|
296
|
-
import type { SearchableTableColumn } from "@/components/SearchableTable";
|
|
297
|
-
import { useInfiniteQuery } from '@tanstack/react-query'
|
|
298
|
-
import { useState } from 'react'
|
|
299
|
-
|
|
300
|
-
type User = { id: string; name: string; role: string; email: string }
|
|
301
|
-
|
|
302
|
-
const columns: SearchableTableColumn<User>[] = [
|
|
303
|
-
{ key: 'name', header: 'Name' },
|
|
304
|
-
{ key: 'role', header: 'Role' },
|
|
305
|
-
{ key: 'email', header: 'Email' },
|
|
306
|
-
]
|
|
307
|
-
|
|
308
|
-
function UserPicker() {
|
|
309
|
-
const [query, setQuery] = useState('')
|
|
310
|
-
const [selected, setSelected] = useState<User | null>(null)
|
|
311
|
-
|
|
312
|
-
const {
|
|
313
|
-
data,
|
|
314
|
-
fetchNextPage,
|
|
315
|
-
hasNextPage,
|
|
316
|
-
isFetching,
|
|
317
|
-
} = useInfiniteQuery({
|
|
318
|
-
queryKey: ['users', query],
|
|
319
|
-
queryFn: async ({ pageParam = 1 }) => {
|
|
320
|
-
const res = await fetch(`/api/users?q=${query}&page=${pageParam}`)
|
|
321
|
-
// API row -> T mapping happens here.
|
|
322
|
-
const json = await res.json()
|
|
323
|
-
return {
|
|
324
|
-
items: json.results.map(
|
|
325
|
-
(r: any): User => ({
|
|
326
|
-
id: String(r.user_id),
|
|
327
|
-
name: r.full_name,
|
|
328
|
-
role: r.role_title,
|
|
329
|
-
email: r.email_address,
|
|
330
|
-
})
|
|
331
|
-
),
|
|
332
|
-
nextPage: json.has_more ? pageParam + 1 : undefined,
|
|
333
|
-
}
|
|
334
|
-
},
|
|
335
|
-
initialPageParam: 1,
|
|
336
|
-
getNextPageParam: (last) => last.nextPage,
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
const rows = data?.pages.flatMap((p) => p.items) ?? []
|
|
340
|
-
|
|
341
|
-
return (
|
|
342
|
-
<SearchableTable<User>
|
|
343
|
-
columns={columns}
|
|
344
|
-
rows={rows}
|
|
345
|
-
value={selected}
|
|
346
|
-
onSelect={setSelected}
|
|
347
|
-
getRowId={(row) => row.id}
|
|
348
|
-
getLabel={(row) => row.name}
|
|
349
|
-
filterClientSide={false}
|
|
350
|
-
onSearchChange={setQuery}
|
|
351
|
-
hasMore={hasNextPage}
|
|
352
|
-
loading={isFetching}
|
|
353
|
-
onLoadMore={() => fetchNextPage()}
|
|
354
|
-
/>
|
|
355
|
-
)
|
|
356
|
-
}
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
### SWR — `useSWRInfinite`
|
|
360
|
-
|
|
361
|
-
```typescript
|
|
362
|
-
import { SearchableTable } from "@/components/SearchableTable";
|
|
363
|
-
import useSWRInfinite from 'swr/infinite'
|
|
364
|
-
import { useState } from 'react'
|
|
365
|
-
|
|
366
|
-
const PAGE_SIZE = 20
|
|
367
|
-
|
|
368
|
-
function UserPicker() {
|
|
369
|
-
const [query, setQuery] = useState('')
|
|
370
|
-
const [selected, setSelected] = useState<User | null>(null)
|
|
371
|
-
|
|
372
|
-
const getKey = (index: number, prev: { items: User[] } | null) => {
|
|
373
|
-
if (prev && prev.items.length < PAGE_SIZE) return null // reached the end
|
|
374
|
-
return `/api/users?q=${query}&page=${index + 1}`
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const { data, size, setSize, isValidating } = useSWRInfinite(getKey, fetcher)
|
|
378
|
-
|
|
379
|
-
const rows = data?.flatMap((p) => p.items) ?? []
|
|
380
|
-
const hasMore = !!data && data[data.length - 1]?.items.length === PAGE_SIZE
|
|
381
|
-
|
|
382
|
-
return (
|
|
383
|
-
<SearchableTable<User>
|
|
384
|
-
columns={columns}
|
|
385
|
-
rows={rows}
|
|
386
|
-
value={selected}
|
|
387
|
-
onSelect={setSelected}
|
|
388
|
-
getRowId={(row) => row.id}
|
|
389
|
-
filterClientSide={false}
|
|
390
|
-
onSearchChange={setQuery}
|
|
391
|
-
hasMore={hasMore}
|
|
392
|
-
loading={isValidating}
|
|
393
|
-
onLoadMore={() => setSize(size + 1)}
|
|
394
|
-
/>
|
|
395
|
-
)
|
|
396
|
-
}
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
### Mapping API rows to `T`
|
|
400
|
-
|
|
401
|
-
Normalize the backend shape into your row type at the fetch boundary so that `columns`, `getLabel`, `getRowId`, and `searchKeys` all operate on consistent, typed fields:
|
|
402
|
-
|
|
403
|
-
```typescript
|
|
404
|
-
function toUser(api: ApiUser): User {
|
|
405
|
-
return {
|
|
406
|
-
id: String(api.user_id),
|
|
407
|
-
name: `${api.first_name} ${api.last_name}`,
|
|
408
|
-
role: api.role ?? '—',
|
|
409
|
-
email: api.email,
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
## Accessibility
|
|
415
|
-
|
|
416
|
-
- The trigger is a focusable group with `role="button"` and `tabIndex={0}`; activating it opens a modal dialog (Radix Dialog), which traps focus and closes on `Esc` or outside click.
|
|
417
|
-
- On open, focus moves to the in-dialog search input (`onOpenAutoFocus` is intercepted to focus the search field rather than the first row), so users can type to filter immediately.
|
|
418
|
-
- The dialog always renders a `DialogTitle` (visually hidden, sourced from `title`) so the modal has an accessible name even though the heading is not shown.
|
|
419
|
-
- Clicking a row selects it and closes the dialog; the selected row is marked with the Table's `state="selected"`.
|
|
420
|
-
- Provide a descriptive `placeholder`, a meaningful `title`, and a `getLabel` so screen-reader users hear a clear value after selection. Prefer human-readable headers in `columns`.
|
|
421
|
-
|
|
422
|
-
## Best Practices
|
|
423
|
-
|
|
424
|
-
1. **Always supply `getRowId`.** The default key is `JSON.stringify(row)`, which is slow and brittle for large or nested rows. A stable id keeps keys and selection matching cheap.
|
|
425
|
-
2. **Set `getLabel` for the selected display.** Otherwise the trigger shows the first column's raw value, which may not be the most descriptive field.
|
|
426
|
-
3. **Pick the right search mode.** Keep `filterClientSide` (default) for small, in-memory datasets. Switch to `filterClientSide={false}` + `onSearchChange` for backend-driven search so you never filter a partial page.
|
|
427
|
-
4. **Gate pagination correctly.** `onLoadMore` only fires while `hasMore && !loading` — keep `loading` accurate so a single scroll doesn't trigger duplicate fetches, and flip `hasMore` to `false` on the last page.
|
|
428
|
-
5. **The table scrolls in both directions.** Columns keep their natural width, so wide tables scroll horizontally inside the dialog, while the table caps its vertical height (~`55vh`) and the rest scrolls vertically (which is what drives infinite scroll). Keep column count and content reasonable so horizontal scroll stays usable.
|
|
429
|
-
6. **Tune `searchDebounceMs` to your backend.** The 300ms default suits most APIs; raise it for slow or rate-limited endpoints.
|
|
430
|
-
7. **Match `searchKeys` to visible columns.** In client mode, search defaults to every column key — narrow `searchKeys` if some columns hold non-searchable data (ids, formatted dates).
|
|
431
|
-
|
|
432
|
-
## Related Components
|
|
433
|
-
|
|
434
|
-
- [SearchableSelect](./searchable-select.md) - Single-column searchable combobox
|
|
435
|
-
- [Table](./table.md) - The table primitive rendered inside the dialog
|
|
436
|
-
- [Dialog](./dialog.md) - The modal surface the picker opens in
|
|
437
|
-
- [DataTable](./data-table.md) - Full-featured data grid for page-level tables
|
|
438
|
-
- [Select](./select.md) - Standard form select field
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: SearchableTreeDialog
|
|
3
|
-
description: Searchable tree-select presented in a modal dialog — a trigger opens a dialog containing a searchable hierarchical tree, with client or server-side search
|
|
4
|
-
group: Inputs
|
|
5
|
-
keywords: [searchable-tree-dialog, tree, tree-select, dialog, modal, hierarchy, search, async]
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# SearchableTreeDialog
|
|
9
|
-
|
|
10
|
-
> A searchable tree-select that opens in a modal **Dialog** instead of a popover. A trigger
|
|
11
|
-
> field opens a dialog containing a search input and a hierarchical tree of nodes, filtered as
|
|
12
|
-
> you type. Accepts nested data (via `getNodeChildren`) or a flat list it nests for you (via
|
|
13
|
-
> `parentIdKey`). Supports local filtering or debounced server-side search, optional folder
|
|
14
|
-
> selection, and controlled selection. Use it instead of [SearchableTree](./searchable-tree.md)
|
|
15
|
-
> when the tree is large or you want a focused, full-screen-friendly selection surface.
|
|
16
|
-
|
|
17
|
-
## Installation
|
|
18
|
-
|
|
19
|
-
TORCH Glare is a copy-in library: the CLI copies this component's source into your project
|
|
20
|
-
(you do **not** install it from the npm package). Run `init` once, then `add`:
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npx torch-glare@latest init
|
|
24
|
-
npx torch-glare@latest add SearchableTreeDialog
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
The CLI also copies its internal dependencies: `Button`, `Dialog`, `Input`, and the
|
|
28
|
-
`cn`/`types` utils.
|
|
29
|
-
|
|
30
|
-
## Import
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
import { SearchableTreeDialog } from "@/components/SearchableTreeDialog";
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Quick Examples
|
|
37
|
-
|
|
38
|
-
### Basic Usage (nested data)
|
|
39
|
-
|
|
40
|
-
```tsx
|
|
41
|
-
import { SearchableTreeDialog } from "@/components/SearchableTreeDialog";
|
|
42
|
-
|
|
43
|
-
type Node = { id: string; name: string; children?: Node[] };
|
|
44
|
-
|
|
45
|
-
const data: Node[] = [
|
|
46
|
-
{
|
|
47
|
-
id: "eng",
|
|
48
|
-
name: "Engineering",
|
|
49
|
-
children: [
|
|
50
|
-
{ id: "fe", name: "Frontend" },
|
|
51
|
-
{ id: "be", name: "Backend" },
|
|
52
|
-
],
|
|
53
|
-
},
|
|
54
|
-
{ id: "design", name: "Design" },
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
export function DepartmentPicker() {
|
|
58
|
-
const [value, setValue] = useState<Node | null>(null);
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<SearchableTreeDialog
|
|
62
|
-
nodes={data}
|
|
63
|
-
getNodeId={(n) => n.id}
|
|
64
|
-
getNodeLabel={(n) => n.name}
|
|
65
|
-
getNodeChildren={(n) => n.children}
|
|
66
|
-
value={value}
|
|
67
|
-
onSelect={setValue}
|
|
68
|
-
placeholder="Select a department"
|
|
69
|
-
searchPlaceholder="Search departments…"
|
|
70
|
-
title="Departments"
|
|
71
|
-
size="M"
|
|
72
|
-
/>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Flat data (built from a parent id)
|
|
78
|
-
|
|
79
|
-
```tsx
|
|
80
|
-
type Row = { id: string; label: string; parentId: string | null };
|
|
81
|
-
|
|
82
|
-
const rows: Row[] = [
|
|
83
|
-
{ id: "1", label: "Company", parentId: null },
|
|
84
|
-
{ id: "2", label: "Sales", parentId: "1" },
|
|
85
|
-
{ id: "3", label: "EMEA", parentId: "2" },
|
|
86
|
-
];
|
|
87
|
-
|
|
88
|
-
<SearchableTreeDialog
|
|
89
|
-
nodes={rows}
|
|
90
|
-
parentIdKey="parentId"
|
|
91
|
-
getNodeId={(r) => r.id}
|
|
92
|
-
getNodeLabel={(r) => r.label}
|
|
93
|
-
onSelect={(r) => console.log(r.id)}
|
|
94
|
-
/>;
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### Server-side search
|
|
98
|
-
|
|
99
|
-
```tsx
|
|
100
|
-
<SearchableTreeDialog
|
|
101
|
-
nodes={results}
|
|
102
|
-
getNodeId={(n) => n.id}
|
|
103
|
-
getNodeLabel={(n) => n.name}
|
|
104
|
-
getNodeChildren={(n) => n.children}
|
|
105
|
-
filterClientSide={false}
|
|
106
|
-
onSearchChange={(query) => fetchNodes(query)}
|
|
107
|
-
loading={isFetching}
|
|
108
|
-
searchDebounceMs={300}
|
|
109
|
-
/>
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## API Reference
|
|
113
|
-
|
|
114
|
-
### Props
|
|
115
|
-
|
|
116
|
-
| Prop | Type | Default | Description |
|
|
117
|
-
|------|------|---------|-------------|
|
|
118
|
-
| `nodes` | `T[]` | — | Tree data. Nested by default; treated as a flat list when `parentIdKey` is set. |
|
|
119
|
-
| `getNodeId` | `(node: T) => string` | — | Returns a stable unique id for a node. |
|
|
120
|
-
| `getNodeLabel` | `(node: T) => ReactNode` | — | Returns a node's display label. |
|
|
121
|
-
| `getNodeChildren` | `(node: T) => T[] \| undefined` | — | Returns a node's children (nested mode). Ignored when `parentIdKey` is set. |
|
|
122
|
-
| `parentIdKey` | `keyof T & string` | — | When set, `nodes` is a FLAT list and the tree is built from this parent-id key. |
|
|
123
|
-
| `value` | `T \| null` | — | Controlled selected node. |
|
|
124
|
-
| `onSelect` | `(node: T) => void` | — | Called when a node is selected. |
|
|
125
|
-
| `getSearchText` | `(node: T) => string` | `String(getNodeLabel(node))` | Plain text used for search matching. |
|
|
126
|
-
| `placeholder` | `string` | — | Trigger placeholder shown until a node is selected. |
|
|
127
|
-
| `searchPlaceholder` | `string` | — | Placeholder for the search input inside the dialog. |
|
|
128
|
-
| `title` | `string` | — | Label shown on the dialog's search field. |
|
|
129
|
-
| `size` | `"XS" \| "S" \| "M"` | `"M"` | Trigger size. |
|
|
130
|
-
| `variant` | `"SystemStyle" \| "PresentationStyle"` | `"PresentationStyle"` | Visual style. `SystemStyle` is reserved for internal library system surfaces. |
|
|
131
|
-
| `icon` | `ReactNode` | — | Leading icon in the trigger. |
|
|
132
|
-
| `theme` | `Themes` | — | `"dark" \| "light" \| "default"`. |
|
|
133
|
-
| `dir` | `string` | — | Text direction. |
|
|
134
|
-
| `className` | `string` | — | Extra classes for the trigger. |
|
|
135
|
-
| `defaultExpanded` | `boolean` | `true` | Expand every node when the dialog opens. |
|
|
136
|
-
| `selectableFolders` | `boolean` | `false` | When true, folder nodes are selectable too; otherwise folders only expand/collapse. |
|
|
137
|
-
| `filterClientSide` | `boolean` | `true` | Filter locally. Set `false` for server-side search. |
|
|
138
|
-
| `onSearchChange` | `(query: string) => void` | — | Debounced as the user types — refetch your data here (server search). |
|
|
139
|
-
| `searchDebounceMs` | `number` | `300` | Debounce for `onSearchChange`. |
|
|
140
|
-
| `loading` | `boolean` | `false` | Whether a fetch is in flight; shows a loading indicator. |
|
|
141
|
-
|
|
142
|
-
### TypeScript
|
|
143
|
-
|
|
144
|
-
```typescript
|
|
145
|
-
import { ReactNode } from "react";
|
|
146
|
-
import { Themes } from "@/utils/types";
|
|
147
|
-
|
|
148
|
-
interface SearchableTreeDialogProps<T> {
|
|
149
|
-
nodes: T[];
|
|
150
|
-
getNodeId: (node: T) => string;
|
|
151
|
-
getNodeLabel: (node: T) => ReactNode;
|
|
152
|
-
getNodeChildren?: (node: T) => T[] | undefined;
|
|
153
|
-
parentIdKey?: keyof T & string;
|
|
154
|
-
value?: T | null;
|
|
155
|
-
onSelect?: (node: T) => void;
|
|
156
|
-
getSearchText?: (node: T) => string;
|
|
157
|
-
placeholder?: string;
|
|
158
|
-
searchPlaceholder?: string;
|
|
159
|
-
title?: string;
|
|
160
|
-
size?: "XS" | "S" | "M";
|
|
161
|
-
variant?: "SystemStyle" | "PresentationStyle";
|
|
162
|
-
icon?: ReactNode;
|
|
163
|
-
theme?: Themes;
|
|
164
|
-
dir?: string;
|
|
165
|
-
className?: string;
|
|
166
|
-
defaultExpanded?: boolean;
|
|
167
|
-
selectableFolders?: boolean;
|
|
168
|
-
filterClientSide?: boolean;
|
|
169
|
-
onSearchChange?: (query: string) => void;
|
|
170
|
-
searchDebounceMs?: number;
|
|
171
|
-
loading?: boolean;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function SearchableTreeDialog<T>(props: SearchableTreeDialogProps<T>): JSX.Element;
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
## Common Patterns
|
|
178
|
-
|
|
179
|
-
- **Dialog vs popover:** use `SearchableTreeDialog` for large trees or a focused modal
|
|
180
|
-
selection; use [SearchableTree](./searchable-tree.md) for an inline popover field.
|
|
181
|
-
- **Nested vs flat data:** provide `getNodeChildren` for nested data, or `parentIdKey` to nest
|
|
182
|
-
a flat list. Do not use both.
|
|
183
|
-
- **Async search:** set `filterClientSide={false}`, handle `onSearchChange` to fetch, and pass
|
|
184
|
-
`loading` while the request is in flight.
|
|
185
|
-
|
|
186
|
-
## Related Components
|
|
187
|
-
|
|
188
|
-
- [SearchableTree](./searchable-tree.md) — the same tree-select on an inline Popover.
|
|
189
|
-
- [SearchableSelect](./searchable-select.md) — flat searchable single-select combobox.
|
|
190
|
-
- [Dialog](./dialog.md) — the modal surface this component builds on.
|