@elabs-ai/components-ui 4.0.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/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/index.d.ts +4497 -0
- package/dist/index.js +15192 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cn.d.ts +10 -0
- package/dist/lib/cn.js +20 -0
- package/dist/lib/cn.js.map +1 -0
- package/package.json +106 -0
- package/src/blocks/sidebar-02/app-sidebar.tsx +160 -0
- package/src/blocks/sidebar-02/logo.tsx +18 -0
- package/src/blocks/sidebar-02/nav-main.tsx +13 -0
- package/src/blocks/sidebar-02/nav-notifications.tsx +7 -0
- package/src/blocks/sidebar-02/sidebar-02.stories.tsx +23 -0
- package/src/blocks/sidebar-02/team-switcher.tsx +7 -0
- package/src/blocks/sidebar-04/app-sidebar.tsx +236 -0
- package/src/blocks/sidebar-04/mail-context.tsx +29 -0
- package/src/blocks/sidebar-04/nav-user.tsx +6 -0
- package/src/blocks/sidebar-04/sidebar-04.stories.tsx +150 -0
- package/src/blocks/sidebar-05/app-sidebar.tsx +375 -0
- package/src/blocks/sidebar-05/sidebar-05.stories.tsx +18 -0
- package/src/blocks/sidebar-05/team-switcher.tsx +6 -0
- package/src/blocks-comparison-table.stories.tsx +165 -0
- package/src/components/accordion/accordion.stories.tsx +71 -0
- package/src/components/accordion/accordion.test.tsx +18 -0
- package/src/components/accordion/accordion.tsx +49 -0
- package/src/components/accordion/index.ts +1 -0
- package/src/components/advanced-group/advanced-group.stories.tsx +143 -0
- package/src/components/advanced-group/advanced-group.test.tsx +81 -0
- package/src/components/advanced-group/advanced-group.tsx +111 -0
- package/src/components/advanced-group/index.ts +2 -0
- package/src/components/alert/alert.stories.tsx +156 -0
- package/src/components/alert/alert.test.tsx +57 -0
- package/src/components/alert/alert.tsx +91 -0
- package/src/components/alert/index.ts +8 -0
- package/src/components/alert-dialog/alert-dialog.stories.tsx +73 -0
- package/src/components/alert-dialog/alert-dialog.tsx +104 -0
- package/src/components/alert-dialog/index.ts +12 -0
- package/src/components/app-shell/app-shell.stories.tsx +61 -0
- package/src/components/app-shell/app-shell.tsx +31 -0
- package/src/components/app-shell/index.ts +1 -0
- package/src/components/app-sidebar/app-sidebar.stories.tsx +121 -0
- package/src/components/app-sidebar/app-sidebar.test.tsx +55 -0
- package/src/components/app-sidebar/app-sidebar.tsx +53 -0
- package/src/components/app-sidebar/index.ts +1 -0
- package/src/components/aspect-ratio/aspect-ratio.stories.tsx +46 -0
- package/src/components/aspect-ratio/aspect-ratio.tsx +3 -0
- package/src/components/aspect-ratio/index.ts +1 -0
- package/src/components/attribution-panel/attribution-panel.stories.tsx +83 -0
- package/src/components/attribution-panel/attribution-panel.test.tsx +123 -0
- package/src/components/attribution-panel/attribution-panel.tsx +191 -0
- package/src/components/attribution-panel/attribution-types.ts +44 -0
- package/src/components/attribution-panel/attributions.generated.ts +1422 -0
- package/src/components/attribution-panel/index.ts +3 -0
- package/src/components/avatar/avatar.stories.tsx +45 -0
- package/src/components/avatar/avatar.test.tsx +13 -0
- package/src/components/avatar/avatar.tsx +46 -0
- package/src/components/avatar/index.ts +1 -0
- package/src/components/badge/badge.stories.tsx +44 -0
- package/src/components/badge/badge.test.tsx +10 -0
- package/src/components/badge/badge.tsx +28 -0
- package/src/components/badge/index.ts +1 -0
- package/src/components/bento-grid/bento-grid.stories.tsx +165 -0
- package/src/components/bento-grid/bento-grid.test.tsx +119 -0
- package/src/components/bento-grid/bento-grid.tsx +339 -0
- package/src/components/bento-grid/index.ts +9 -0
- package/src/components/bounded-number/bounded-number.stories.tsx +118 -0
- package/src/components/bounded-number/bounded-number.test.tsx +73 -0
- package/src/components/bounded-number/bounded-number.tsx +110 -0
- package/src/components/bounded-number/index.ts +1 -0
- package/src/components/breadcrumb/breadcrumb.stories.tsx +47 -0
- package/src/components/breadcrumb/breadcrumb.test.tsx +17 -0
- package/src/components/breadcrumb/breadcrumb.tsx +86 -0
- package/src/components/breadcrumb/index.ts +9 -0
- package/src/components/button/button.stories.tsx +70 -0
- package/src/components/button/button.test.tsx +50 -0
- package/src/components/button/button.tsx +63 -0
- package/src/components/button/index.ts +1 -0
- package/src/components/button-group/button-group.stories.tsx +45 -0
- package/src/components/button-group/button-group.tsx +70 -0
- package/src/components/button-group/index.ts +6 -0
- package/src/components/calendar/calendar.stories.tsx +95 -0
- package/src/components/calendar/calendar.test.tsx +140 -0
- package/src/components/calendar/calendar.tsx +70 -0
- package/src/components/calendar/index.ts +1 -0
- package/src/components/card/card.stories.tsx +438 -0
- package/src/components/card/card.test.tsx +153 -0
- package/src/components/card/card.tsx +332 -0
- package/src/components/card/index.ts +13 -0
- package/src/components/carousel/carousel.stories.tsx +102 -0
- package/src/components/carousel/carousel.tsx +225 -0
- package/src/components/carousel/index.ts +8 -0
- package/src/components/change-review/change-review.stories.tsx +288 -0
- package/src/components/change-review/change-review.test.tsx +261 -0
- package/src/components/change-review/change-review.tsx +637 -0
- package/src/components/change-review/index.ts +23 -0
- package/src/components/checkbox/checkbox.stories.tsx +119 -0
- package/src/components/checkbox/checkbox.test.tsx +40 -0
- package/src/components/checkbox/checkbox.tsx +34 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/collapsible/collapsible.stories.tsx +62 -0
- package/src/components/collapsible/collapsible.tsx +22 -0
- package/src/components/collapsible/index.ts +1 -0
- package/src/components/collapsible-panel/index.ts +8 -0
- package/src/components/collapsible-panel/use-collapsible-panel.test.tsx +93 -0
- package/src/components/collapsible-panel/use-collapsible-panel.ts +159 -0
- package/src/components/color-picker/color-picker.stories.tsx +93 -0
- package/src/components/color-picker/color-picker.test.tsx +79 -0
- package/src/components/color-picker/color-picker.tsx +341 -0
- package/src/components/color-picker/index.ts +1 -0
- package/src/components/combobox/combobox.stories.tsx +154 -0
- package/src/components/combobox/combobox.test.tsx +246 -0
- package/src/components/combobox/combobox.tsx +210 -0
- package/src/components/combobox/index.ts +1 -0
- package/src/components/command/command.stories.tsx +371 -0
- package/src/components/command/command.test.tsx +280 -0
- package/src/components/command/command.tsx +301 -0
- package/src/components/command/index.ts +13 -0
- package/src/components/confirm-dialog/confirm-dialog.stories.tsx +183 -0
- package/src/components/confirm-dialog/confirm-dialog.test.tsx +147 -0
- package/src/components/confirm-dialog/confirm-dialog.tsx +146 -0
- package/src/components/confirm-dialog/index.ts +2 -0
- package/src/components/context-menu/context-menu.stories.tsx +70 -0
- package/src/components/context-menu/context-menu.tsx +172 -0
- package/src/components/context-menu/index.ts +16 -0
- package/src/components/copyable-value/copyable-value.stories.tsx +80 -0
- package/src/components/copyable-value/copyable-value.test.tsx +128 -0
- package/src/components/copyable-value/copyable-value.tsx +101 -0
- package/src/components/copyable-value/index.ts +1 -0
- package/src/components/date-picker/date-picker.stories.tsx +42 -0
- package/src/components/date-picker/date-picker.tsx +49 -0
- package/src/components/date-picker/index.ts +1 -0
- package/src/components/date-range-picker/date-range-picker.stories.tsx +151 -0
- package/src/components/date-range-picker/date-range-picker.test.tsx +62 -0
- package/src/components/date-range-picker/date-range-picker.tsx +126 -0
- package/src/components/date-range-picker/index.ts +6 -0
- package/src/components/descriptions/descriptions.stories.tsx +43 -0
- package/src/components/descriptions/descriptions.test.tsx +41 -0
- package/src/components/descriptions/descriptions.tsx +95 -0
- package/src/components/descriptions/index.ts +6 -0
- package/src/components/dialog/dialog.stories.tsx +728 -0
- package/src/components/dialog/dialog.test.tsx +285 -0
- package/src/components/dialog/dialog.tsx +340 -0
- package/src/components/dialog/index.ts +18 -0
- package/src/components/dialog/use-dialog-dismiss-guard.test.tsx +80 -0
- package/src/components/dialog/use-dialog-dismiss-guard.ts +109 -0
- package/src/components/drawer/drawer.stories.tsx +76 -0
- package/src/components/drawer/drawer.tsx +85 -0
- package/src/components/drawer/index.ts +11 -0
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +176 -0
- package/src/components/dropdown-menu/dropdown-menu.test.tsx +109 -0
- package/src/components/dropdown-menu/dropdown-menu.tsx +179 -0
- package/src/components/dropdown-menu/index.ts +17 -0
- package/src/components/empty-state/empty-state.stories.tsx +54 -0
- package/src/components/empty-state/empty-state.test.tsx +11 -0
- package/src/components/empty-state/empty-state.tsx +30 -0
- package/src/components/empty-state/index.ts +1 -0
- package/src/components/error-state/error-state.stories.tsx +51 -0
- package/src/components/error-state/error-state.tsx +27 -0
- package/src/components/error-state/index.ts +1 -0
- package/src/components/expand-dialog/expand-dialog.stories.tsx +246 -0
- package/src/components/expand-dialog/expand-dialog.test.tsx +185 -0
- package/src/components/expand-dialog/expand-dialog.tsx +387 -0
- package/src/components/expand-dialog/index.ts +11 -0
- package/src/components/field-row/field-row.stories.tsx +114 -0
- package/src/components/field-row/field-row.test.tsx +188 -0
- package/src/components/field-row/field-row.tsx +115 -0
- package/src/components/field-row/index.ts +1 -0
- package/src/components/file-upload/file-upload.stories.tsx +183 -0
- package/src/components/file-upload/file-upload.test.tsx +169 -0
- package/src/components/file-upload/file-upload.tsx +459 -0
- package/src/components/file-upload/index.ts +13 -0
- package/src/components/form/form.stories.tsx +71 -0
- package/src/components/form/form.tsx +141 -0
- package/src/components/form/index.ts +10 -0
- package/src/components/hover-card/hover-card.stories.tsx +57 -0
- package/src/components/hover-card/hover-card.tsx +25 -0
- package/src/components/hover-card/index.ts +1 -0
- package/src/components/icon-button/icon-button.stories.tsx +108 -0
- package/src/components/icon-button/icon-button.test.tsx +93 -0
- package/src/components/icon-button/icon-button.tsx +139 -0
- package/src/components/icon-button/index.ts +1 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input/input.stories.tsx +44 -0
- package/src/components/input/input.test.tsx +19 -0
- package/src/components/input/input.tsx +26 -0
- package/src/components/input-group/index.ts +11 -0
- package/src/components/input-group/input-group.stories.tsx +87 -0
- package/src/components/input-group/input-group.test.tsx +48 -0
- package/src/components/input-group/input-group.tsx +170 -0
- package/src/components/input-otp/index.ts +1 -0
- package/src/components/input-otp/input-otp.stories.tsx +66 -0
- package/src/components/input-otp/input-otp.tsx +68 -0
- package/src/components/kbd/index.ts +1 -0
- package/src/components/kbd/kbd.stories.tsx +45 -0
- package/src/components/kbd/kbd.tsx +20 -0
- package/src/components/key-value-editor/index.ts +1 -0
- package/src/components/key-value-editor/key-value-editor.stories.tsx +130 -0
- package/src/components/key-value-editor/key-value-editor.test.tsx +231 -0
- package/src/components/key-value-editor/key-value-editor.tsx +263 -0
- package/src/components/label/index.ts +1 -0
- package/src/components/label/label.stories.tsx +35 -0
- package/src/components/label/label.tsx +19 -0
- package/src/components/link-preview/index.ts +2 -0
- package/src/components/link-preview/link-preview.stories.tsx +178 -0
- package/src/components/link-preview/link-preview.test.tsx +123 -0
- package/src/components/link-preview/link-preview.tsx +194 -0
- package/src/components/list-editor/index.ts +1 -0
- package/src/components/list-editor/list-editor.stories.tsx +119 -0
- package/src/components/list-editor/list-editor.test.tsx +96 -0
- package/src/components/list-editor/list-editor.tsx +192 -0
- package/src/components/loading-state/index.ts +1 -0
- package/src/components/loading-state/loading-state.stories.tsx +38 -0
- package/src/components/loading-state/loading-state.tsx +26 -0
- package/src/components/locale-provider/index.ts +8 -0
- package/src/components/locale-provider/locale-provider.stories.tsx +83 -0
- package/src/components/locale-provider/locale-provider.test.tsx +238 -0
- package/src/components/locale-provider/locale-provider.tsx +187 -0
- package/src/components/locale-provider/messages.ts +300 -0
- package/src/components/match-highlight/index.ts +7 -0
- package/src/components/match-highlight/match-highlight.stories.tsx +146 -0
- package/src/components/match-highlight/match-highlight.test.tsx +202 -0
- package/src/components/match-highlight/match-highlight.tsx +145 -0
- package/src/components/mention-input/index.ts +28 -0
- package/src/components/mention-input/mention-input-mirror.stories.tsx +171 -0
- package/src/components/mention-input/mention-input.stories.tsx +463 -0
- package/src/components/mention-input/mention-input.test.tsx +617 -0
- package/src/components/mention-input/mention-input.tsx +1102 -0
- package/src/components/mention-input/mention-value.test.ts +253 -0
- package/src/components/mention-input/mention-value.ts +193 -0
- package/src/components/menubar/index.ts +17 -0
- package/src/components/menubar/menubar.stories.tsx +67 -0
- package/src/components/menubar/menubar.tsx +198 -0
- package/src/components/metric-card/index.ts +1 -0
- package/src/components/metric-card/metric-card.stories.tsx +224 -0
- package/src/components/metric-card/metric-card.test.tsx +198 -0
- package/src/components/metric-card/metric-card.tsx +229 -0
- package/src/components/model-picker/index.ts +11 -0
- package/src/components/model-picker/model-picker-state.ts +85 -0
- package/src/components/model-picker/model-picker.stories.tsx +128 -0
- package/src/components/model-picker/model-picker.test.tsx +241 -0
- package/src/components/model-picker/model-picker.tsx +270 -0
- package/src/components/nav-main/index.ts +1 -0
- package/src/components/nav-main/nav-main.tsx +147 -0
- package/src/components/nav-notifications/index.ts +5 -0
- package/src/components/nav-notifications/nav-notifications.test.tsx +15 -0
- package/src/components/nav-notifications/nav-notifications.tsx +70 -0
- package/src/components/nav-user/index.ts +1 -0
- package/src/components/nav-user/nav-user.test.tsx +29 -0
- package/src/components/nav-user/nav-user.tsx +114 -0
- package/src/components/navigation-menu/index.ts +10 -0
- package/src/components/navigation-menu/navigation-menu.stories.tsx +92 -0
- package/src/components/navigation-menu/navigation-menu.tsx +99 -0
- package/src/components/number-input/index.ts +1 -0
- package/src/components/number-input/number-input.stories.tsx +120 -0
- package/src/components/number-input/number-input.test.tsx +96 -0
- package/src/components/number-input/number-input.tsx +219 -0
- package/src/components/page-shell/index.ts +1 -0
- package/src/components/page-shell/page-shell.stories.tsx +105 -0
- package/src/components/page-shell/page-shell.test.tsx +57 -0
- package/src/components/page-shell/page-shell.tsx +62 -0
- package/src/components/pagination/index.ts +9 -0
- package/src/components/pagination/pagination.stories.tsx +57 -0
- package/src/components/pagination/pagination.tsx +77 -0
- package/src/components/popover/index.ts +1 -0
- package/src/components/popover/popover.stories.tsx +28 -0
- package/src/components/popover/popover.test.tsx +16 -0
- package/src/components/popover/popover.tsx +29 -0
- package/src/components/progress/index.ts +1 -0
- package/src/components/progress/progress.stories.tsx +80 -0
- package/src/components/progress/progress.test.tsx +35 -0
- package/src/components/progress/progress.tsx +50 -0
- package/src/components/radio-group/index.ts +1 -0
- package/src/components/radio-group/radio-group.stories.tsx +75 -0
- package/src/components/radio-group/radio-group.test.tsx +16 -0
- package/src/components/radio-group/radio-group.tsx +33 -0
- package/src/components/rating/index.ts +1 -0
- package/src/components/rating/rating.stories.tsx +111 -0
- package/src/components/rating/rating.test.tsx +41 -0
- package/src/components/rating/rating.tsx +226 -0
- package/src/components/resizable/index.ts +1 -0
- package/src/components/resizable/resizable.stories.tsx +25 -0
- package/src/components/resizable/resizable.tsx +40 -0
- package/src/components/reveal/index.ts +7 -0
- package/src/components/reveal/reveal.stories.tsx +90 -0
- package/src/components/reveal/reveal.test.tsx +43 -0
- package/src/components/reveal/reveal.tsx +95 -0
- package/src/components/revision-timeline/index.ts +10 -0
- package/src/components/revision-timeline/revision-timeline.stories.tsx +785 -0
- package/src/components/revision-timeline/revision-timeline.test.tsx +509 -0
- package/src/components/revision-timeline/revision-timeline.tsx +1107 -0
- package/src/components/scroll-area/index.ts +1 -0
- package/src/components/scroll-area/scroll-area.stories.tsx +45 -0
- package/src/components/scroll-area/scroll-area.tsx +43 -0
- package/src/components/section-header/index.ts +1 -0
- package/src/components/section-header/section-header.stories.tsx +20 -0
- package/src/components/section-header/section-header.tsx +36 -0
- package/src/components/segmented-field/index.ts +5 -0
- package/src/components/segmented-field/segmented-field.stories.tsx +121 -0
- package/src/components/segmented-field/segmented-field.test.tsx +163 -0
- package/src/components/segmented-field/segmented-field.tsx +235 -0
- package/src/components/select/index.ts +10 -0
- package/src/components/select/select.stories.tsx +106 -0
- package/src/components/select/select.test.tsx +98 -0
- package/src/components/select/select.tsx +230 -0
- package/src/components/separator/index.ts +2 -0
- package/src/components/separator/separator.stories.tsx +87 -0
- package/src/components/separator/separator.tsx +51 -0
- package/src/components/sheet/index.ts +12 -0
- package/src/components/sheet/sheet.stories.tsx +65 -0
- package/src/components/sheet/sheet.tsx +109 -0
- package/src/components/sidebar/index.ts +27 -0
- package/src/components/sidebar/sidebar.stories.tsx +69 -0
- package/src/components/sidebar/sidebar.test.tsx +43 -0
- package/src/components/sidebar/sidebar.tsx +688 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/skeleton/skeleton.stories.tsx +28 -0
- package/src/components/skeleton/skeleton.tsx +15 -0
- package/src/components/slider/index.ts +1 -0
- package/src/components/slider/slider.stories.tsx +211 -0
- package/src/components/slider/slider.test.tsx +168 -0
- package/src/components/slider/slider.tsx +136 -0
- package/src/components/slider-number/index.ts +1 -0
- package/src/components/slider-number/slider-number.stories.tsx +123 -0
- package/src/components/slider-number/slider-number.test.tsx +165 -0
- package/src/components/slider-number/slider-number.tsx +191 -0
- package/src/components/sonner/index.ts +1 -0
- package/src/components/sonner/sonner.stories.tsx +124 -0
- package/src/components/sonner/sonner.test.tsx +76 -0
- package/src/components/sonner/sonner.tsx +101 -0
- package/src/components/spinner/index.ts +1 -0
- package/src/components/spinner/spinner.stories.tsx +27 -0
- package/src/components/spinner/spinner.tsx +19 -0
- package/src/components/split-panel/index.ts +1 -0
- package/src/components/split-panel/split-panel.stories.tsx +60 -0
- package/src/components/split-panel/split-panel.test.tsx +38 -0
- package/src/components/split-panel/split-panel.tsx +95 -0
- package/src/components/state-panel/index.ts +1 -0
- package/src/components/state-panel/state-panel.stories.tsx +80 -0
- package/src/components/state-panel/state-panel.test.tsx +64 -0
- package/src/components/state-panel/state-panel.tsx +167 -0
- package/src/components/status-badge/index.ts +18 -0
- package/src/components/status-badge/status-badge.stories.tsx +132 -0
- package/src/components/status-badge/status-badge.test.tsx +189 -0
- package/src/components/status-badge/status-badge.tsx +330 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.stories.tsx +58 -0
- package/src/components/switch/switch.test.tsx +12 -0
- package/src/components/switch/switch.tsx +29 -0
- package/src/components/table/index.ts +10 -0
- package/src/components/table/table.stories.tsx +53 -0
- package/src/components/table/table.test.tsx +17 -0
- package/src/components/table/table.tsx +106 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/tabs/tabs.stories.tsx +267 -0
- package/src/components/tabs/tabs.test.tsx +94 -0
- package/src/components/tabs/tabs.tsx +170 -0
- package/src/components/tag-input/index.ts +1 -0
- package/src/components/tag-input/tag-input.stories.tsx +120 -0
- package/src/components/tag-input/tag-input.test.tsx +97 -0
- package/src/components/tag-input/tag-input.tsx +253 -0
- package/src/components/team-switcher/index.ts +1 -0
- package/src/components/team-switcher/team-switcher.test.tsx +39 -0
- package/src/components/team-switcher/team-switcher.tsx +100 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/textarea.stories.tsx +42 -0
- package/src/components/textarea/textarea.tsx +24 -0
- package/src/components/theme-switcher/index.ts +2 -0
- package/src/components/theme-switcher/theme-switcher.stories.tsx +146 -0
- package/src/components/theme-switcher/theme-switcher.test.tsx +253 -0
- package/src/components/theme-switcher/theme-switcher.tsx +280 -0
- package/src/components/theme-switcher/use-theme-transition.ts +62 -0
- package/src/components/timeline/index.ts +13 -0
- package/src/components/timeline/timeline.stories.tsx +99 -0
- package/src/components/timeline/timeline.test.tsx +196 -0
- package/src/components/timeline/timeline.tsx +188 -0
- package/src/components/toggle/index.ts +1 -0
- package/src/components/toggle/toggle.stories.tsx +116 -0
- package/src/components/toggle/toggle.test.tsx +119 -0
- package/src/components/toggle/toggle.tsx +64 -0
- package/src/components/toggle-group/index.ts +1 -0
- package/src/components/toggle-group/toggle-group.stories.tsx +110 -0
- package/src/components/toggle-group/toggle-group.tsx +59 -0
- package/src/components/toolbar/index.ts +14 -0
- package/src/components/toolbar/toolbar.stories.tsx +126 -0
- package/src/components/toolbar/toolbar.test.tsx +73 -0
- package/src/components/toolbar/toolbar.tsx +177 -0
- package/src/components/tooltip/index.ts +1 -0
- package/src/components/tooltip/tooltip.stories.tsx +67 -0
- package/src/components/tooltip/tooltip.tsx +27 -0
- package/src/components/top-nav/index.ts +1 -0
- package/src/components/top-nav/top-nav.stories.tsx +18 -0
- package/src/components/top-nav/top-nav.tsx +34 -0
- package/src/components/transfer/index.ts +2 -0
- package/src/components/transfer/transfer.stories.tsx +162 -0
- package/src/components/transfer/transfer.test.tsx +177 -0
- package/src/components/transfer/transfer.tsx +481 -0
- package/src/components/tree/index.ts +6 -0
- package/src/components/tree/tree.stories.tsx +618 -0
- package/src/components/tree/tree.test.tsx +699 -0
- package/src/components/tree/tree.tsx +1149 -0
- package/src/components/tree/use-tree-keyboard.ts +314 -0
- package/src/components/tree-select/index.ts +1 -0
- package/src/components/tree-select/tree-select.stories.tsx +127 -0
- package/src/components/tree-select/tree-select.test.tsx +40 -0
- package/src/components/tree-select/tree-select.tsx +137 -0
- package/src/components/typography/index.ts +31 -0
- package/src/components/typography/prose.test.tsx +48 -0
- package/src/components/typography/prose.tsx +194 -0
- package/src/components/typography/typography.stories.tsx +244 -0
- package/src/components/typography/typography.test.tsx +140 -0
- package/src/components/typography/typography.tsx +126 -0
- package/src/components/view-toolbar/index.ts +10 -0
- package/src/components/view-toolbar/view-toolbar.stories.tsx +342 -0
- package/src/components/view-toolbar/view-toolbar.test.tsx +262 -0
- package/src/components/view-toolbar/view-toolbar.tsx +320 -0
- package/src/components/virtual-select/index.ts +2 -0
- package/src/components/virtual-select/use-virtual-listbox.ts +132 -0
- package/src/components/virtual-select/virtual-select.stories.tsx +203 -0
- package/src/components/virtual-select/virtual-select.test.tsx +106 -0
- package/src/components/virtual-select/virtual-select.tsx +361 -0
- package/src/components/wizard/index.ts +12 -0
- package/src/components/wizard/wizard.stories.tsx +148 -0
- package/src/components/wizard/wizard.test.tsx +67 -0
- package/src/components/wizard/wizard.tsx +354 -0
- package/src/decoration.stories.tsx +236 -0
- package/src/index.ts +174 -0
- package/src/lib/cn.test.ts +39 -0
- package/src/lib/cn.ts +31 -0
- package/src/lib/compact-number.ts +61 -0
- package/src/lib/document-address.test.ts +75 -0
- package/src/lib/document-address.ts +265 -0
- package/src/lib/download.ts +93 -0
- package/src/lib/file-icon.test.ts +45 -0
- package/src/lib/file-icon.ts +58 -0
- package/src/lib/file-kind.test.ts +98 -0
- package/src/lib/file-kind.ts +303 -0
- package/src/lib/file-source.test.ts +217 -0
- package/src/lib/file-source.ts +269 -0
- package/src/lib/merge-refs.test.ts +37 -0
- package/src/lib/merge-refs.ts +17 -0
- package/src/lib/streamdown-translations.ts +99 -0
- package/src/lib/use-copy-to-clipboard.ts +75 -0
- package/src/lib/use-mobile.ts +16 -0
- package/src/motion.stories.tsx +266 -0
- package/src/taste-profile.test.tsx +252 -0
- package/src/templates-object-detail-hub.stories.tsx +466 -0
- package/src/templates-screen-states.stories.tsx +306 -0
- package/src/templates-settings.stories.tsx +245 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import { Combobox } from "./combobox";
|
|
6
|
+
describe("Combobox", () => {
|
|
7
|
+
it("renders the placeholder on the trigger", () => {
|
|
8
|
+
render(<Combobox options={[{ label: "A", value: "a" }]} placeholder="Pick one" />);
|
|
9
|
+
// The trigger uses role="combobox"; that ARIA role does not take its
|
|
10
|
+
// accessible name from text content, so assert the visible placeholder text.
|
|
11
|
+
expect(screen.getByText("Pick one")).toBeInTheDocument();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// #343 — disabled prop
|
|
15
|
+
it("disables the trigger and never opens the popover when disabled", async () => {
|
|
16
|
+
const ref = createRef<HTMLButtonElement>();
|
|
17
|
+
render(
|
|
18
|
+
<Combobox ref={ref} disabled options={[{ label: "A", value: "a" }]} placeholder="Pick one" />,
|
|
19
|
+
);
|
|
20
|
+
const trigger = screen.getByRole("combobox");
|
|
21
|
+
expect(trigger).toBeDisabled();
|
|
22
|
+
expect(ref.current).toBe(trigger);
|
|
23
|
+
|
|
24
|
+
await userEvent.click(trigger);
|
|
25
|
+
expect(screen.queryByPlaceholderText("Search…")).not.toBeInTheDocument();
|
|
26
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("is excluded from tab order when disabled (native disabled semantics)", () => {
|
|
30
|
+
render(<Combobox disabled options={[{ label: "A", value: "a" }]} />);
|
|
31
|
+
const trigger = screen.getByRole("combobox");
|
|
32
|
+
// A native `disabled` button is always excluded from the tab order —
|
|
33
|
+
// no `tabIndex` gymnastics needed.
|
|
34
|
+
expect(trigger).toBeDisabled();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// #347 — accessible-name passthrough
|
|
38
|
+
it("names the trigger via aria-label, distinct from the selected value", () => {
|
|
39
|
+
render(
|
|
40
|
+
<Combobox
|
|
41
|
+
aria-label="Session switcher"
|
|
42
|
+
options={[{ label: "Alpha", value: "a" }]}
|
|
43
|
+
value="a"
|
|
44
|
+
/>,
|
|
45
|
+
);
|
|
46
|
+
const trigger = screen.getByRole("combobox", { name: "Session switcher" });
|
|
47
|
+
// The value is still visible as the control's current state.
|
|
48
|
+
expect(trigger).toHaveTextContent("Alpha");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("spreads triggerProps onto the trigger without clobbering role/aria-expanded", () => {
|
|
52
|
+
render(
|
|
53
|
+
<Combobox
|
|
54
|
+
options={[{ label: "A", value: "a" }]}
|
|
55
|
+
triggerProps={{ "data-testid": "env-combobox" }}
|
|
56
|
+
/>,
|
|
57
|
+
);
|
|
58
|
+
const trigger = screen.getByTestId("env-combobox");
|
|
59
|
+
expect(trigger).toHaveAttribute("role", "combobox");
|
|
60
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("shows the selected custom value on the trigger even though it isn't in options", async () => {
|
|
64
|
+
render(
|
|
65
|
+
<Combobox allowCustomValue options={[{ label: "Alpha", value: "a" }]} value="Custom Value" />,
|
|
66
|
+
);
|
|
67
|
+
expect(screen.getByRole("combobox")).toHaveTextContent("Custom Value");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// #359 — allowCustomValue. A nested describe because these tests TYPE into the
|
|
72
|
+
// cmdk-backed search input, which internally calls `Element.scrollIntoView` to
|
|
73
|
+
// keep the auto-selected item visible — jsdom doesn't implement it, and the
|
|
74
|
+
// project's global vitest setup deliberately provides no stub (see
|
|
75
|
+
// packages/ui/vitest.setup.ts), so third-party libraries that don't
|
|
76
|
+
// feature-detect need a local, scoped stub (same pattern as tree.test.tsx).
|
|
77
|
+
describe("Combobox allowCustomValue", () => {
|
|
78
|
+
let originalScrollIntoView: typeof Element.prototype.scrollIntoView;
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
originalScrollIntoView = Element.prototype.scrollIntoView;
|
|
82
|
+
Element.prototype.scrollIntoView = vi.fn();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
Element.prototype.scrollIntoView = originalScrollIntoView;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("accepts a typed value not present in options when allowCustomValue is set (Enter)", async () => {
|
|
90
|
+
const onValueChange = vi.fn();
|
|
91
|
+
render(
|
|
92
|
+
<Combobox
|
|
93
|
+
allowCustomValue
|
|
94
|
+
options={[{ label: "Alpha", value: "a" }]}
|
|
95
|
+
onValueChange={onValueChange}
|
|
96
|
+
/>,
|
|
97
|
+
);
|
|
98
|
+
await userEvent.click(screen.getByRole("combobox"));
|
|
99
|
+
const input = await screen.findByPlaceholderText("Search…");
|
|
100
|
+
await userEvent.type(input, "Custom Value{Enter}");
|
|
101
|
+
expect(onValueChange).toHaveBeenCalledWith("Custom Value");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("accepts a typed value not present in options when allowCustomValue is set (click)", async () => {
|
|
105
|
+
const onValueChange = vi.fn();
|
|
106
|
+
render(
|
|
107
|
+
<Combobox
|
|
108
|
+
allowCustomValue
|
|
109
|
+
options={[{ label: "Alpha", value: "a" }]}
|
|
110
|
+
onValueChange={onValueChange}
|
|
111
|
+
/>,
|
|
112
|
+
);
|
|
113
|
+
await userEvent.click(screen.getByRole("combobox"));
|
|
114
|
+
const input = await screen.findByPlaceholderText("Search…");
|
|
115
|
+
await userEvent.type(input, "Canary");
|
|
116
|
+
const customItem = await screen.findByRole("option", { name: /use.*canary/i });
|
|
117
|
+
await userEvent.click(customItem);
|
|
118
|
+
expect(onValueChange).toHaveBeenCalledWith("Canary");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("does NOT emit an unlisted value when allowCustomValue is unset (regression guard)", async () => {
|
|
122
|
+
const onValueChange = vi.fn();
|
|
123
|
+
render(<Combobox options={[{ label: "Alpha", value: "a" }]} onValueChange={onValueChange} />);
|
|
124
|
+
await userEvent.click(screen.getByRole("combobox"));
|
|
125
|
+
const input = await screen.findByPlaceholderText("Search…");
|
|
126
|
+
await userEvent.type(input, "Custom Value{Enter}");
|
|
127
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
128
|
+
expect(screen.queryByRole("option", { name: /use/i })).not.toBeInTheDocument();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("still filters real suggestions while allowCustomValue is set", async () => {
|
|
132
|
+
render(
|
|
133
|
+
<Combobox
|
|
134
|
+
allowCustomValue
|
|
135
|
+
options={[
|
|
136
|
+
{ label: "Alpha", value: "a" },
|
|
137
|
+
{ label: "Beta", value: "b" },
|
|
138
|
+
]}
|
|
139
|
+
/>,
|
|
140
|
+
);
|
|
141
|
+
await userEvent.click(screen.getByRole("combobox"));
|
|
142
|
+
const input = await screen.findByPlaceholderText("Search…");
|
|
143
|
+
await userEvent.type(input, "Alpha");
|
|
144
|
+
expect(await screen.findByRole("option", { name: "Alpha" })).toBeInTheDocument();
|
|
145
|
+
expect(screen.queryByRole("option", { name: "Beta" })).not.toBeInTheDocument();
|
|
146
|
+
// An exact match to an existing option must not also offer a redundant
|
|
147
|
+
// "Use…" custom-value entry.
|
|
148
|
+
expect(screen.queryByRole("option", { name: /use/i })).not.toBeInTheDocument();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("keyboard: ArrowDown moves the highlight off the auto-selected top match, and Enter accepts it", async () => {
|
|
152
|
+
const onValueChange = vi.fn();
|
|
153
|
+
render(
|
|
154
|
+
<Combobox
|
|
155
|
+
allowCustomValue
|
|
156
|
+
options={[
|
|
157
|
+
{ label: "Alpha", value: "alpha" },
|
|
158
|
+
{ label: "Alphabet", value: "alphabet" },
|
|
159
|
+
]}
|
|
160
|
+
onValueChange={onValueChange}
|
|
161
|
+
/>,
|
|
162
|
+
);
|
|
163
|
+
await userEvent.click(screen.getByRole("combobox"));
|
|
164
|
+
const input = await screen.findByPlaceholderText("Search…");
|
|
165
|
+
// "Alpha" is an EXACT match (case-insensitive) for the typed text, so no
|
|
166
|
+
// custom-value item is offered — only the two real options are ranked.
|
|
167
|
+
await userEvent.type(input, "Alpha");
|
|
168
|
+
await screen.findByRole("option", { name: "Alpha" });
|
|
169
|
+
await screen.findByRole("option", { name: "Alphabet" });
|
|
170
|
+
expect(screen.queryByRole("option", { name: /use/i })).not.toBeInTheDocument();
|
|
171
|
+
// The exact match ("Alpha") auto-ranks first/highlighted; one ArrowDown
|
|
172
|
+
// moves the highlight to "Alphabet", and Enter accepts THAT option — not
|
|
173
|
+
// whatever was auto-selected by default.
|
|
174
|
+
await userEvent.keyboard("{ArrowDown}{Enter}");
|
|
175
|
+
expect(onValueChange).toHaveBeenCalledWith("alphabet");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Regression lock: cmdk re-sorts items by score and auto-highlights the top
|
|
179
|
+
// one. Giving the synthetic "Use …" row the SEARCH TEXT as its `value` made
|
|
180
|
+
// it score a perfect 1, so it outranked every real PARTIAL match and stole
|
|
181
|
+
// the highlight — typing "Alp" and pressing Enter emitted "Alp" instead of
|
|
182
|
+
// selecting "Alpha". The row now carries a sentinel value scored just above
|
|
183
|
+
// zero, so it always renders but always sorts last.
|
|
184
|
+
it("ranks the custom-value row LAST, so Enter still selects a real partial match", async () => {
|
|
185
|
+
const onValueChange = vi.fn();
|
|
186
|
+
render(
|
|
187
|
+
<Combobox
|
|
188
|
+
allowCustomValue
|
|
189
|
+
options={[
|
|
190
|
+
{ label: "Alpha", value: "alpha" },
|
|
191
|
+
{ label: "Beta", value: "beta" },
|
|
192
|
+
]}
|
|
193
|
+
onValueChange={onValueChange}
|
|
194
|
+
/>,
|
|
195
|
+
);
|
|
196
|
+
await userEvent.click(screen.getByRole("combobox"));
|
|
197
|
+
const input = await screen.findByPlaceholderText("Search…");
|
|
198
|
+
// A PARTIAL match — "Alpha" is offered AND the custom row is offered.
|
|
199
|
+
await userEvent.type(input, "Alp");
|
|
200
|
+
const options = screen.getAllByRole("option");
|
|
201
|
+
expect(options.map((o) => o.textContent)).toEqual(["Alpha", 'Use "Alp"']);
|
|
202
|
+
// The real suggestion — not the custom row — is the auto-highlighted one.
|
|
203
|
+
expect(options[0]).toHaveAttribute("aria-selected", "true");
|
|
204
|
+
expect(options[1]).toHaveAttribute("aria-selected", "false");
|
|
205
|
+
await userEvent.keyboard("{Enter}");
|
|
206
|
+
expect(onValueChange).toHaveBeenCalledWith("alpha");
|
|
207
|
+
expect(onValueChange).not.toHaveBeenCalledWith("Alp");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("still accepts the custom value from a partial match via ArrowDown + Enter", async () => {
|
|
211
|
+
const onValueChange = vi.fn();
|
|
212
|
+
render(
|
|
213
|
+
<Combobox
|
|
214
|
+
allowCustomValue
|
|
215
|
+
options={[{ label: "Alpha", value: "alpha" }]}
|
|
216
|
+
onValueChange={onValueChange}
|
|
217
|
+
/>,
|
|
218
|
+
);
|
|
219
|
+
await userEvent.click(screen.getByRole("combobox"));
|
|
220
|
+
const input = await screen.findByPlaceholderText("Search…");
|
|
221
|
+
await userEvent.type(input, "Alp");
|
|
222
|
+
// Move off the auto-highlighted "Alpha" onto the trailing custom row.
|
|
223
|
+
await userEvent.keyboard("{ArrowDown}{Enter}");
|
|
224
|
+
expect(onValueChange).toHaveBeenCalledWith("Alp");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("Escape closes the popover without clearing the already-selected value", async () => {
|
|
228
|
+
const onValueChange = vi.fn();
|
|
229
|
+
render(
|
|
230
|
+
<Combobox
|
|
231
|
+
allowCustomValue
|
|
232
|
+
options={[{ label: "Alpha", value: "a" }]}
|
|
233
|
+
value="a"
|
|
234
|
+
onValueChange={onValueChange}
|
|
235
|
+
/>,
|
|
236
|
+
);
|
|
237
|
+
const trigger = screen.getByRole("combobox");
|
|
238
|
+
await userEvent.click(trigger);
|
|
239
|
+
const input = await screen.findByPlaceholderText("Search…");
|
|
240
|
+
await userEvent.type(input, "something");
|
|
241
|
+
await userEvent.keyboard("{Escape}");
|
|
242
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
243
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
244
|
+
expect(trigger).toHaveTextContent("Alpha");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { forwardRef, useState, type ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import { defaultFilter } from "cmdk";
|
|
3
|
+
import { Check, ChevronsUpDown, Plus } from "lucide-react";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import { Button } from "../button";
|
|
6
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
|
7
|
+
import { useLocale } from "../locale-provider";
|
|
8
|
+
import {
|
|
9
|
+
Command,
|
|
10
|
+
CommandEmpty,
|
|
11
|
+
CommandGroup,
|
|
12
|
+
CommandInput,
|
|
13
|
+
CommandItem,
|
|
14
|
+
CommandList,
|
|
15
|
+
} from "../command";
|
|
16
|
+
|
|
17
|
+
export interface ComboboxOption {
|
|
18
|
+
label: string;
|
|
19
|
+
value: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ComboboxProps {
|
|
23
|
+
options: ComboboxOption[];
|
|
24
|
+
value?: string;
|
|
25
|
+
onValueChange?: (value: string) => void;
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
searchPlaceholder?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Message shown when the search yields no results. Defaults to "No
|
|
30
|
+
* results." — or, when `allowCustomValue` is set, a message that also
|
|
31
|
+
* signals a custom value is acceptable.
|
|
32
|
+
*/
|
|
33
|
+
emptyText?: string;
|
|
34
|
+
className?: string;
|
|
35
|
+
/** Disables the trigger and prevents the popover from opening. */
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* When `true`, typed text that doesn't match any option becomes a
|
|
39
|
+
* submittable value: a synthetic "Use…" suggestion is shown, and pressing
|
|
40
|
+
* Enter with no listed option highlighted accepts the typed text via
|
|
41
|
+
* `onValueChange`. Suggestions still filter as usual. Default `false`
|
|
42
|
+
* (selection-only, unchanged behavior) — #359.
|
|
43
|
+
*/
|
|
44
|
+
allowCustomValue?: boolean;
|
|
45
|
+
/** `id` applied to the trigger button. */
|
|
46
|
+
id?: string;
|
|
47
|
+
/**
|
|
48
|
+
* The trigger's accessible name always defaults to the SELECTED VALUE (or
|
|
49
|
+
* the placeholder) — that tells AT what is currently chosen, never what the
|
|
50
|
+
* control is FOR. Pass `aria-label`/`aria-labelledby` to also name the
|
|
51
|
+
* control's purpose (e.g. "Session switcher").
|
|
52
|
+
*/
|
|
53
|
+
"aria-label"?: string;
|
|
54
|
+
"aria-labelledby"?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Escape hatch spread onto the trigger `Button` (after the named props
|
|
57
|
+
* above, so it wins if both are set) for anything not covered by a named
|
|
58
|
+
* prop — `data-*`, `name`, `data-testid`, etc. `role`/`aria-expanded` stay
|
|
59
|
+
* component-owned and can't be passed here; disabling stays on the
|
|
60
|
+
* top-level `disabled` prop so the popover-open guard below can't drift
|
|
61
|
+
* out of sync with the trigger's actual disabled state.
|
|
62
|
+
*/
|
|
63
|
+
triggerProps?: Omit<
|
|
64
|
+
ComponentPropsWithoutRef<typeof Button>,
|
|
65
|
+
"role" | "aria-expanded" | "disabled"
|
|
66
|
+
> & {
|
|
67
|
+
/** Arbitrary `data-*` attributes (test hooks, analytics, …). */
|
|
68
|
+
[key: `data-${string}`]: string | number | boolean | undefined;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Sentinel `value` for the synthetic "Use …" row (`allowCustomValue`).
|
|
74
|
+
*
|
|
75
|
+
* It must NOT be the search text. cmdk scores every item against the search and
|
|
76
|
+
* re-sorts the list by score, then auto-highlights the top one — so a row whose
|
|
77
|
+
* `value` IS the search text scores a perfect 1 and outranks every real partial
|
|
78
|
+
* match. Typing "Prod" against a "Production" option then highlighted
|
|
79
|
+
* `Use "Prod"`, and Enter emitted the raw string instead of selecting the
|
|
80
|
+
* option — the exact opposite of #359's "Enter accepts the highlighted
|
|
81
|
+
* suggestion, or the typed value if none is highlighted".
|
|
82
|
+
*/
|
|
83
|
+
const CUSTOM_VALUE_ITEM = "__brand-ui-combobox-custom-value__";
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Keeps the "Use …" row visible (score > 0) but always ranked LAST, behind
|
|
87
|
+
* every real suggestion, so the top match stays highlighted and Enter still
|
|
88
|
+
* selects it. When nothing else matches, the row is the only item and Enter
|
|
89
|
+
* accepts the typed value — which is the behavior #359 asks for.
|
|
90
|
+
*/
|
|
91
|
+
const customValueFilter = (value: string, search: string, keywords?: string[]) =>
|
|
92
|
+
value === CUSTOM_VALUE_ITEM ? Number.MIN_VALUE : defaultFilter(value, search, keywords);
|
|
93
|
+
|
|
94
|
+
/** Single-select combobox (Popover + Command). Controlled or uncontrolled. */
|
|
95
|
+
export const Combobox = forwardRef<HTMLButtonElement, ComboboxProps>(function Combobox(
|
|
96
|
+
{
|
|
97
|
+
options,
|
|
98
|
+
value,
|
|
99
|
+
onValueChange,
|
|
100
|
+
placeholder = "Select…",
|
|
101
|
+
searchPlaceholder = "Search…",
|
|
102
|
+
emptyText,
|
|
103
|
+
className,
|
|
104
|
+
disabled = false,
|
|
105
|
+
allowCustomValue = false,
|
|
106
|
+
id,
|
|
107
|
+
"aria-label": ariaLabel,
|
|
108
|
+
"aria-labelledby": ariaLabelledby,
|
|
109
|
+
triggerProps,
|
|
110
|
+
},
|
|
111
|
+
ref,
|
|
112
|
+
) {
|
|
113
|
+
const { t } = useLocale();
|
|
114
|
+
const [open, setOpen] = useState(false);
|
|
115
|
+
const [internal, setInternal] = useState("");
|
|
116
|
+
const [search, setSearch] = useState("");
|
|
117
|
+
const selected = value ?? internal;
|
|
118
|
+
const setValue = (v: string) => (onValueChange ? onValueChange(v) : setInternal(v));
|
|
119
|
+
const current = options.find((o) => o.value === selected);
|
|
120
|
+
|
|
121
|
+
const trimmedSearch = search.trim();
|
|
122
|
+
const hasExactMatch = options.some(
|
|
123
|
+
(o) => o.label.trim().toLowerCase() === trimmedSearch.toLowerCase(),
|
|
124
|
+
);
|
|
125
|
+
const showCustomValueItem = allowCustomValue && trimmedSearch.length > 0 && !hasExactMatch;
|
|
126
|
+
const resolvedEmptyText =
|
|
127
|
+
emptyText ?? (allowCustomValue ? t("ui.combobox.emptyAllowCustom") : t("noResults"));
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Popover
|
|
131
|
+
open={disabled ? false : open}
|
|
132
|
+
onOpenChange={(o) => {
|
|
133
|
+
if (disabled) return;
|
|
134
|
+
setOpen(o);
|
|
135
|
+
// Reset the search text on close, matching the previous (uncontrolled
|
|
136
|
+
// cmdk-internal) behavior where the popover content unmounted and its
|
|
137
|
+
// own internal search state reset with it.
|
|
138
|
+
if (!o) setSearch("");
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
<PopoverTrigger asChild>
|
|
142
|
+
<Button
|
|
143
|
+
ref={ref}
|
|
144
|
+
variant="outline"
|
|
145
|
+
id={id}
|
|
146
|
+
aria-label={ariaLabel}
|
|
147
|
+
aria-labelledby={ariaLabelledby}
|
|
148
|
+
{...triggerProps}
|
|
149
|
+
role="combobox"
|
|
150
|
+
aria-expanded={open}
|
|
151
|
+
disabled={disabled}
|
|
152
|
+
data-slot="combobox"
|
|
153
|
+
className={cn("w-56 justify-between font-normal", triggerProps?.className, className)}
|
|
154
|
+
>
|
|
155
|
+
{current ? current.label : selected || placeholder}
|
|
156
|
+
<ChevronsUpDown className="ms-2 size-4 shrink-0 opacity-50" />
|
|
157
|
+
</Button>
|
|
158
|
+
</PopoverTrigger>
|
|
159
|
+
<PopoverContent
|
|
160
|
+
className="w-56 p-0"
|
|
161
|
+
data-slot="combobox-content"
|
|
162
|
+
// Radix Popover.Content renders role="dialog"; without a name axe's
|
|
163
|
+
// aria-dialog-name rule (rightly) flags it. Reuse the trigger's own
|
|
164
|
+
// purpose label when the consumer set one, else fall back to the
|
|
165
|
+
// (already-translatable) search placeholder — no new hardcoded copy.
|
|
166
|
+
aria-label={ariaLabel ?? searchPlaceholder}
|
|
167
|
+
>
|
|
168
|
+
<Command filter={allowCustomValue ? customValueFilter : undefined}>
|
|
169
|
+
<CommandInput placeholder={searchPlaceholder} value={search} onValueChange={setSearch} />
|
|
170
|
+
<CommandList>
|
|
171
|
+
<CommandEmpty>{resolvedEmptyText}</CommandEmpty>
|
|
172
|
+
<CommandGroup>
|
|
173
|
+
{options.map((o) => (
|
|
174
|
+
<CommandItem
|
|
175
|
+
key={o.value}
|
|
176
|
+
value={o.label}
|
|
177
|
+
onSelect={() => {
|
|
178
|
+
setValue(o.value === selected ? "" : o.value);
|
|
179
|
+
setOpen(false);
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
<Check
|
|
183
|
+
className={cn(
|
|
184
|
+
"me-2 size-4",
|
|
185
|
+
selected === o.value ? "opacity-100" : "opacity-0",
|
|
186
|
+
)}
|
|
187
|
+
/>
|
|
188
|
+
{o.label}
|
|
189
|
+
</CommandItem>
|
|
190
|
+
))}
|
|
191
|
+
{showCustomValueItem ? (
|
|
192
|
+
<CommandItem
|
|
193
|
+
value={CUSTOM_VALUE_ITEM}
|
|
194
|
+
data-slot="combobox-custom-value"
|
|
195
|
+
onSelect={() => {
|
|
196
|
+
setValue(trimmedSearch);
|
|
197
|
+
setOpen(false);
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
<Plus className="me-2 size-4 shrink-0 opacity-50" />
|
|
201
|
+
{t("ui.combobox.useCustomValue", { value: trimmedSearch })}
|
|
202
|
+
</CommandItem>
|
|
203
|
+
) : null}
|
|
204
|
+
</CommandGroup>
|
|
205
|
+
</CommandList>
|
|
206
|
+
</Command>
|
|
207
|
+
</PopoverContent>
|
|
208
|
+
</Popover>
|
|
209
|
+
);
|
|
210
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Combobox, type ComboboxProps, type ComboboxOption } from "./combobox";
|