@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,371 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { useEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react";
|
|
3
|
+
import { expect, waitFor, within } from "storybook/test";
|
|
4
|
+
import { Calculator, Calendar, CreditCard, Settings, Smile, User } from "lucide-react";
|
|
5
|
+
import { Button } from "../button";
|
|
6
|
+
import { DialogTitle } from "../dialog";
|
|
7
|
+
import {
|
|
8
|
+
Command,
|
|
9
|
+
CommandDialog,
|
|
10
|
+
CommandEmpty,
|
|
11
|
+
CommandGroup,
|
|
12
|
+
CommandInput,
|
|
13
|
+
CommandItem,
|
|
14
|
+
CommandList,
|
|
15
|
+
CommandSeparator,
|
|
16
|
+
CommandShortcut,
|
|
17
|
+
} from "./command";
|
|
18
|
+
const meta = {
|
|
19
|
+
title: "Overlays/Command",
|
|
20
|
+
component: Command,
|
|
21
|
+
tags: ["autodocs"],
|
|
22
|
+
argTypes: {
|
|
23
|
+
label: {
|
|
24
|
+
description: "Accessible label for the command palette (cmdk `label` prop).",
|
|
25
|
+
control: "text",
|
|
26
|
+
table: { category: "Accessibility" },
|
|
27
|
+
},
|
|
28
|
+
filter: {
|
|
29
|
+
description: "Custom ranking function for filtering items (overrides fuzzy default).",
|
|
30
|
+
control: false,
|
|
31
|
+
table: { category: "Behaviour" },
|
|
32
|
+
},
|
|
33
|
+
shouldFilter: {
|
|
34
|
+
description: "Whether cmdk filters items by the current search value. Default true.",
|
|
35
|
+
control: "boolean",
|
|
36
|
+
table: { category: "Behaviour" },
|
|
37
|
+
},
|
|
38
|
+
loop: {
|
|
39
|
+
description: "Whether keyboard navigation wraps from last item back to first.",
|
|
40
|
+
control: "boolean",
|
|
41
|
+
table: { category: "Behaviour" },
|
|
42
|
+
},
|
|
43
|
+
className: {
|
|
44
|
+
description: "Additional CSS classes applied to the root element.",
|
|
45
|
+
control: "text",
|
|
46
|
+
table: { category: "Styling" },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
} satisfies Meta<typeof Command>;
|
|
50
|
+
export default meta;
|
|
51
|
+
type Story = StoryObj<typeof meta>;
|
|
52
|
+
export const Default: Story = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<Command className="w-72 rounded-lg shadow-ring-md">
|
|
55
|
+
<CommandInput placeholder="Type a command…" />
|
|
56
|
+
<CommandList>
|
|
57
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
58
|
+
<CommandGroup heading="Suggestions">
|
|
59
|
+
<CommandItem>New project</CommandItem>
|
|
60
|
+
<CommandItem>Open settings</CommandItem>
|
|
61
|
+
<CommandItem>Invite teammate</CommandItem>
|
|
62
|
+
</CommandGroup>
|
|
63
|
+
</CommandList>
|
|
64
|
+
</Command>
|
|
65
|
+
),
|
|
66
|
+
// Types a search query and confirms filtering works; then clears to see all items.
|
|
67
|
+
play: async ({ canvasElement, userEvent }) => {
|
|
68
|
+
const canvas = within(canvasElement);
|
|
69
|
+
const input = canvas.getByPlaceholderText("Type a command…");
|
|
70
|
+
await waitFor(() => expect(input).toBeVisible());
|
|
71
|
+
// Filter to "settings"
|
|
72
|
+
await userEvent.type(input, "settings");
|
|
73
|
+
await waitFor(() => expect(canvas.getByText("Open settings")).toBeVisible());
|
|
74
|
+
// "New project" should be filtered out
|
|
75
|
+
await expect(canvas.queryByText("New project")).toBeNull();
|
|
76
|
+
// Clear filter — all items return
|
|
77
|
+
await userEvent.clear(input);
|
|
78
|
+
await waitFor(() => expect(canvas.getByText("New project")).toBeVisible());
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/* -------------------------------------------------------------------------- */
|
|
83
|
+
/* Command Dialog (⌘K palette) */
|
|
84
|
+
/* -------------------------------------------------------------------------- */
|
|
85
|
+
|
|
86
|
+
/** The ⌘K command palette: a `CommandDialog` toggled by a shortcut or a button. */
|
|
87
|
+
function CommandPaletteDemo() {
|
|
88
|
+
const [open, setOpen] = useState(false);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
92
|
+
if (e.key.toLowerCase() === "k" && (e.metaKey || e.ctrlKey)) {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
setOpen((prev) => !prev);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
document.addEventListener("keydown", onKeyDown);
|
|
98
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
const run = () => setOpen(false);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="flex flex-col items-center gap-3 p-6">
|
|
105
|
+
<p className="text-body text-muted-foreground">
|
|
106
|
+
Press{" "}
|
|
107
|
+
<kbd className="rounded border bg-muted px-1.5 py-0.5 font-mono text-meta" translate="no">
|
|
108
|
+
⌘ K
|
|
109
|
+
</kbd>{" "}
|
|
110
|
+
or use the button.
|
|
111
|
+
</p>
|
|
112
|
+
<Button variant="outline" size="sm" onClick={() => setOpen(true)}>
|
|
113
|
+
Open command palette
|
|
114
|
+
</Button>
|
|
115
|
+
|
|
116
|
+
<CommandDialog open={open} onOpenChange={setOpen}>
|
|
117
|
+
{/* CommandDialog doesn't ship a built-in title, so supply an sr-only one
|
|
118
|
+
for the dialog's accessible name. */}
|
|
119
|
+
<DialogTitle className="sr-only">Command palette</DialogTitle>
|
|
120
|
+
<CommandInput placeholder="Type a command or search…" />
|
|
121
|
+
<CommandList>
|
|
122
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
123
|
+
<CommandGroup heading="Suggestions">
|
|
124
|
+
<CommandItem onSelect={run}>
|
|
125
|
+
<Calendar />
|
|
126
|
+
Calendar
|
|
127
|
+
</CommandItem>
|
|
128
|
+
<CommandItem onSelect={run}>
|
|
129
|
+
<Smile />
|
|
130
|
+
Search emoji
|
|
131
|
+
</CommandItem>
|
|
132
|
+
<CommandItem onSelect={run}>
|
|
133
|
+
<Calculator />
|
|
134
|
+
Calculator
|
|
135
|
+
</CommandItem>
|
|
136
|
+
</CommandGroup>
|
|
137
|
+
<CommandSeparator />
|
|
138
|
+
<CommandGroup heading="Settings">
|
|
139
|
+
<CommandItem onSelect={run}>
|
|
140
|
+
<User />
|
|
141
|
+
Profile
|
|
142
|
+
<CommandShortcut>⌘P</CommandShortcut>
|
|
143
|
+
</CommandItem>
|
|
144
|
+
<CommandItem onSelect={run}>
|
|
145
|
+
<CreditCard />
|
|
146
|
+
Billing
|
|
147
|
+
<CommandShortcut>⌘B</CommandShortcut>
|
|
148
|
+
</CommandItem>
|
|
149
|
+
<CommandItem onSelect={run}>
|
|
150
|
+
<Settings />
|
|
151
|
+
Settings
|
|
152
|
+
<CommandShortcut>⌘S</CommandShortcut>
|
|
153
|
+
</CommandItem>
|
|
154
|
+
</CommandGroup>
|
|
155
|
+
</CommandList>
|
|
156
|
+
</CommandDialog>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** A ⌘K command palette in a dialog, opened by shortcut or button. */
|
|
162
|
+
export const Palette: Story = {
|
|
163
|
+
name: "Command Dialog (⌘K)",
|
|
164
|
+
render: () => <CommandPaletteDemo />,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/* -------------------------------------------------------------------------- */
|
|
168
|
+
/* Groups, icons, shortcuts & separators */
|
|
169
|
+
/* -------------------------------------------------------------------------- */
|
|
170
|
+
|
|
171
|
+
/** An inline menu with grouped items, leading icons, separators and shortcuts. */
|
|
172
|
+
export const Groups: Story = {
|
|
173
|
+
name: "Groups, icons & shortcuts",
|
|
174
|
+
render: () => (
|
|
175
|
+
<Command className="w-80 rounded-lg shadow-ring-md">
|
|
176
|
+
<CommandInput placeholder="Type a command or search…" />
|
|
177
|
+
<CommandList>
|
|
178
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
179
|
+
<CommandGroup heading="Suggestions">
|
|
180
|
+
<CommandItem>
|
|
181
|
+
<Calendar />
|
|
182
|
+
Calendar
|
|
183
|
+
</CommandItem>
|
|
184
|
+
<CommandItem>
|
|
185
|
+
<Smile />
|
|
186
|
+
Search emoji
|
|
187
|
+
</CommandItem>
|
|
188
|
+
<CommandItem>
|
|
189
|
+
<Calculator />
|
|
190
|
+
Calculator
|
|
191
|
+
</CommandItem>
|
|
192
|
+
</CommandGroup>
|
|
193
|
+
<CommandSeparator />
|
|
194
|
+
<CommandGroup heading="Settings">
|
|
195
|
+
<CommandItem>
|
|
196
|
+
<User />
|
|
197
|
+
Profile
|
|
198
|
+
<CommandShortcut>⌘P</CommandShortcut>
|
|
199
|
+
</CommandItem>
|
|
200
|
+
<CommandItem>
|
|
201
|
+
<CreditCard />
|
|
202
|
+
Billing
|
|
203
|
+
<CommandShortcut>⌘B</CommandShortcut>
|
|
204
|
+
</CommandItem>
|
|
205
|
+
<CommandItem>
|
|
206
|
+
<Settings />
|
|
207
|
+
Settings
|
|
208
|
+
<CommandShortcut>⌘S</CommandShortcut>
|
|
209
|
+
</CommandItem>
|
|
210
|
+
</CommandGroup>
|
|
211
|
+
</CommandList>
|
|
212
|
+
</Command>
|
|
213
|
+
),
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/* -------------------------------------------------------------------------- */
|
|
217
|
+
/* Scrollable */
|
|
218
|
+
/* -------------------------------------------------------------------------- */
|
|
219
|
+
|
|
220
|
+
const FRAMEWORKS = [
|
|
221
|
+
"Next.js",
|
|
222
|
+
"React",
|
|
223
|
+
"Remix",
|
|
224
|
+
"Astro",
|
|
225
|
+
"SvelteKit",
|
|
226
|
+
"Nuxt",
|
|
227
|
+
"SolidStart",
|
|
228
|
+
"Qwik City",
|
|
229
|
+
"Gatsby",
|
|
230
|
+
"Vite",
|
|
231
|
+
"Angular",
|
|
232
|
+
"Vue",
|
|
233
|
+
"Ember",
|
|
234
|
+
"Preact",
|
|
235
|
+
"Lit",
|
|
236
|
+
"Alpine.js",
|
|
237
|
+
"HTMX",
|
|
238
|
+
"Eleventy",
|
|
239
|
+
"Hugo",
|
|
240
|
+
"Docusaurus",
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
/** A long list inside the `CommandList`'s built-in scroll region (max-h 300px). */
|
|
244
|
+
export const Scrollable: Story = {
|
|
245
|
+
name: "Scrollable",
|
|
246
|
+
render: () => (
|
|
247
|
+
<Command className="w-80 rounded-lg shadow-ring-md">
|
|
248
|
+
<CommandInput placeholder="Search framework…" />
|
|
249
|
+
<CommandList>
|
|
250
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
251
|
+
<CommandGroup heading="Frameworks">
|
|
252
|
+
{FRAMEWORKS.map((framework) => (
|
|
253
|
+
<CommandItem key={framework}>{framework}</CommandItem>
|
|
254
|
+
))}
|
|
255
|
+
</CommandGroup>
|
|
256
|
+
</CommandList>
|
|
257
|
+
</Command>
|
|
258
|
+
),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/* -------------------------------------------------------------------------- */
|
|
262
|
+
/* External combobox input (#365) */
|
|
263
|
+
/* -------------------------------------------------------------------------- */
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* A plain `<input role="combobox">` rendered OUTSIDE the `Command` tree (the
|
|
267
|
+
* composer-textarea / `@`-mention shape #365 was filed for) driving
|
|
268
|
+
* `aria-activedescendant` off `onActiveItemIdChange` — never by positionally
|
|
269
|
+
* querying `[role="option"]`. Arrow keys typed into the input are forwarded
|
|
270
|
+
* onto the `Command` root so cmdk's own navigation still owns selection.
|
|
271
|
+
*/
|
|
272
|
+
function ExternalInputComboboxDemo() {
|
|
273
|
+
const [activeId, setActiveId] = useState<string | undefined>();
|
|
274
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
275
|
+
|
|
276
|
+
const forwardKey = (event: ReactKeyboardEvent<HTMLInputElement>) => {
|
|
277
|
+
if (["ArrowDown", "ArrowUp", "Enter", "Home", "End"].includes(event.key)) {
|
|
278
|
+
event.preventDefault();
|
|
279
|
+
listRef.current?.dispatchEvent(
|
|
280
|
+
new KeyboardEvent("keydown", { key: event.key, bubbles: true, cancelable: true }),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<div className="w-80 space-y-2">
|
|
287
|
+
<input
|
|
288
|
+
aria-activedescendant={activeId}
|
|
289
|
+
aria-controls="mention-list"
|
|
290
|
+
aria-expanded="true"
|
|
291
|
+
role="combobox"
|
|
292
|
+
onKeyDown={forwardKey}
|
|
293
|
+
placeholder="Type @ to mention…"
|
|
294
|
+
className="flex h-10 w-full rounded-md border border-input bg-background px-3 text-body outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
295
|
+
/>
|
|
296
|
+
<Command
|
|
297
|
+
id="mention-list"
|
|
298
|
+
ref={listRef}
|
|
299
|
+
onActiveItemIdChange={setActiveId}
|
|
300
|
+
className="rounded-lg border shadow-ring-md"
|
|
301
|
+
>
|
|
302
|
+
<CommandList>
|
|
303
|
+
<CommandGroup heading="Teammates">
|
|
304
|
+
<CommandItem value="ada">Ada Lovelace</CommandItem>
|
|
305
|
+
<CommandItem value="grace">Grace Hopper</CommandItem>
|
|
306
|
+
<CommandItem value="alan">Alan Turing</CommandItem>
|
|
307
|
+
</CommandGroup>
|
|
308
|
+
</CommandList>
|
|
309
|
+
</Command>
|
|
310
|
+
</div>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export const ExternalInputCombobox: Story = {
|
|
315
|
+
name: "External input combobox (aria-activedescendant, #365)",
|
|
316
|
+
parameters: {
|
|
317
|
+
docs: {
|
|
318
|
+
description: {
|
|
319
|
+
story:
|
|
320
|
+
"`Command`'s `onActiveItemIdChange` reports the DOM id cmdk assigned to the " +
|
|
321
|
+
"highlighted item, so an input rendered outside the `Command` tree — a composer " +
|
|
322
|
+
"textarea driving an `@`-mention popup — can set `aria-activedescendant` " +
|
|
323
|
+
'correctly without positionally querying `[role="option"]`.',
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
render: () => <ExternalInputComboboxDemo />,
|
|
328
|
+
play: async ({ canvasElement }) => {
|
|
329
|
+
const canvas = within(canvasElement);
|
|
330
|
+
const input = canvas.getByRole("combobox");
|
|
331
|
+
|
|
332
|
+
await waitFor(() => expect(input).toBeVisible());
|
|
333
|
+
|
|
334
|
+
// Before ANY interaction: cmdk highlights the first item on mount, so the
|
|
335
|
+
// external input must already point at it. (cmdk's own `state.selectedItemId`
|
|
336
|
+
// is `undefined` here — `Command` resolves the id from the committed DOM
|
|
337
|
+
// instead, which is what makes this assertion pass.)
|
|
338
|
+
await waitFor(() => {
|
|
339
|
+
const onMount = canvasElement.querySelector('[aria-selected="true"]') as HTMLElement;
|
|
340
|
+
expect(onMount).not.toBeNull();
|
|
341
|
+
expect(input.getAttribute("aria-activedescendant")).toBe(onMount.id);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Forward one ArrowDown (the real keyboard-navigation path) to move
|
|
345
|
+
// cmdk's selection, then confirm the external input's
|
|
346
|
+
// aria-activedescendant follows the newly-highlighted item's DOM id —
|
|
347
|
+
// never a positional index.
|
|
348
|
+
input.focus();
|
|
349
|
+
input.dispatchEvent(
|
|
350
|
+
new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }),
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
let firstId: string | null = null;
|
|
354
|
+
await waitFor(() => {
|
|
355
|
+
const selected = canvasElement.querySelector('[aria-selected="true"]') as HTMLElement;
|
|
356
|
+
expect(selected).not.toBeNull();
|
|
357
|
+
expect(input.getAttribute("aria-activedescendant")).toBe(selected.id);
|
|
358
|
+
firstId = selected.id;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// A second ArrowDown must move it again, to a DIFFERENT id.
|
|
362
|
+
input.dispatchEvent(
|
|
363
|
+
new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }),
|
|
364
|
+
);
|
|
365
|
+
await waitFor(() => {
|
|
366
|
+
const nowSelected = canvasElement.querySelector('[aria-selected="true"]') as HTMLElement;
|
|
367
|
+
expect(nowSelected.id).not.toBe(firstId);
|
|
368
|
+
expect(input.getAttribute("aria-activedescendant")).toBe(nowSelected.id);
|
|
369
|
+
});
|
|
370
|
+
},
|
|
371
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import {
|
|
6
|
+
Command,
|
|
7
|
+
CommandGroup,
|
|
8
|
+
CommandInput,
|
|
9
|
+
CommandItem,
|
|
10
|
+
CommandList,
|
|
11
|
+
useCommandActiveItemId,
|
|
12
|
+
} from "./command";
|
|
13
|
+
|
|
14
|
+
// jsdom does not implement `Element.prototype.scrollIntoView`, and `cmdk`
|
|
15
|
+
// calls it unconditionally (not feature-detected — it's a third-party
|
|
16
|
+
// dependency, not brand-ui code) whenever the highlighted item changes.
|
|
17
|
+
// `packages/ui/vitest.setup.ts` deliberately does NOT stub this globally (it
|
|
18
|
+
// would mask a real brand-ui component skipping its own feature-detection),
|
|
19
|
+
// so — mirroring `tree.test.tsx`'s local stub for the same jsdom gap — every
|
|
20
|
+
// test in this file gets a local no-op stub instead.
|
|
21
|
+
let originalScrollIntoView: typeof Element.prototype.scrollIntoView;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
originalScrollIntoView = Element.prototype.scrollIntoView;
|
|
25
|
+
Element.prototype.scrollIntoView = vi.fn();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
Element.prototype.scrollIntoView = originalScrollIntoView;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** #365 — cmdk assigns each item's `id`/`role`/`aria-selected` internally, so
|
|
33
|
+
* `Command`'s `onActiveItemIdChange` (and the `useCommandActiveItemId` hook)
|
|
34
|
+
* are the supported way to read the highlighted item's DOM id. */
|
|
35
|
+
describe("Command — onActiveItemIdChange (#365)", () => {
|
|
36
|
+
function Harness({ onActiveItemIdChange }: { onActiveItemIdChange: (id?: string) => void }) {
|
|
37
|
+
return (
|
|
38
|
+
<Command onActiveItemIdChange={onActiveItemIdChange}>
|
|
39
|
+
<CommandInput placeholder="Search…" />
|
|
40
|
+
<CommandList>
|
|
41
|
+
<CommandGroup heading="Fruit">
|
|
42
|
+
<CommandItem value="apple">Apple</CommandItem>
|
|
43
|
+
<CommandItem value="banana">Banana</CommandItem>
|
|
44
|
+
<CommandItem value="cherry">Cherry</CommandItem>
|
|
45
|
+
</CommandGroup>
|
|
46
|
+
</CommandList>
|
|
47
|
+
</Command>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
it("reports the DOM id of the currently-selected item — never by positional index", async () => {
|
|
52
|
+
const onActiveItemIdChange = vi.fn();
|
|
53
|
+
const { container } = render(<Harness onActiveItemIdChange={onActiveItemIdChange} />);
|
|
54
|
+
|
|
55
|
+
const input = screen.getByPlaceholderText("Search…");
|
|
56
|
+
await userEvent.type(input, "{ArrowDown}{ArrowDown}");
|
|
57
|
+
|
|
58
|
+
const selected = container.querySelector('[aria-selected="true"]') as HTMLElement;
|
|
59
|
+
expect(selected).not.toBeNull();
|
|
60
|
+
|
|
61
|
+
// The LAST reported id must match the committed DOM id of the selected
|
|
62
|
+
// item — read from cmdk's own `aria-selected` attribute, never a
|
|
63
|
+
// positional/index match against the item list.
|
|
64
|
+
const lastCall = onActiveItemIdChange.mock.calls.at(-1);
|
|
65
|
+
expect(lastCall?.[0]).toBe(selected.id);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("reports undefined once filtering leaves no matches, and a defined id again once restored", async () => {
|
|
69
|
+
const onActiveItemIdChange = vi.fn();
|
|
70
|
+
const { container } = render(<Harness onActiveItemIdChange={onActiveItemIdChange} />);
|
|
71
|
+
|
|
72
|
+
const input = screen.getByPlaceholderText("Search…");
|
|
73
|
+
await userEvent.type(input, "zzz-no-match");
|
|
74
|
+
await waitFor(() => expect(onActiveItemIdChange.mock.calls.at(-1)?.[0]).toBeUndefined());
|
|
75
|
+
|
|
76
|
+
// Clearing the filter re-highlights an item, and the id must come back on
|
|
77
|
+
// its own — with NO keyboard navigation to nudge it (cmdk's own
|
|
78
|
+
// `state.selectedItemId` stays `undefined` here; see the next two tests).
|
|
79
|
+
await userEvent.clear(input);
|
|
80
|
+
await waitFor(() => {
|
|
81
|
+
const selected = container.querySelector('[aria-selected="true"]') as HTMLElement;
|
|
82
|
+
expect(selected).not.toBeNull();
|
|
83
|
+
expect(onActiveItemIdChange.mock.calls.at(-1)?.[0]).toBe(selected.id);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// The two tests below lock the gap in cmdk 1.1.1 that `Command` works around.
|
|
88
|
+
// cmdk only recomputes `state.selectedItemId` in a callback queued from
|
|
89
|
+
// `setState("value", …)`, and its own `selectFirstItem()` runs inside the very
|
|
90
|
+
// layout-effect flush that drains that queue — so the callback reads the DOM
|
|
91
|
+
// before React has committed the new `aria-selected`, and the field stays
|
|
92
|
+
// `undefined`. Reproduced with zero brand-ui code: cmdk's own `Command.Input`
|
|
93
|
+
// renders NO `aria-activedescendant` in either state. Without the workaround a
|
|
94
|
+
// consumer wiring an `@`-mention popup gets `undefined` for exactly the two
|
|
95
|
+
// states the popup spends its life in, and is pushed back to the positional
|
|
96
|
+
// DOM query this issue exists to remove.
|
|
97
|
+
it("reports the item cmdk auto-highlights on MOUNT, before any interaction", async () => {
|
|
98
|
+
const onActiveItemIdChange = vi.fn();
|
|
99
|
+
const { container } = render(<Harness onActiveItemIdChange={onActiveItemIdChange} />);
|
|
100
|
+
|
|
101
|
+
await waitFor(() => {
|
|
102
|
+
const selected = container.querySelector('[aria-selected="true"]') as HTMLElement;
|
|
103
|
+
expect(selected).not.toBeNull();
|
|
104
|
+
expect(onActiveItemIdChange.mock.calls.at(-1)?.[0]).toBe(selected.id);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("reports the item re-highlighted by FILTERING, with no keyboard navigation", async () => {
|
|
109
|
+
const onActiveItemIdChange = vi.fn();
|
|
110
|
+
const { container } = render(<Harness onActiveItemIdChange={onActiveItemIdChange} />);
|
|
111
|
+
|
|
112
|
+
// "an" matches Banana only — cmdk moves the highlight off Apple without an
|
|
113
|
+
// arrow key ever being pressed.
|
|
114
|
+
await userEvent.type(screen.getByPlaceholderText("Search…"), "an");
|
|
115
|
+
|
|
116
|
+
await waitFor(() => {
|
|
117
|
+
const selected = container.querySelector('[aria-selected="true"]') as HTMLElement;
|
|
118
|
+
expect(selected).not.toBeNull();
|
|
119
|
+
expect(selected.textContent).toBe("Banana");
|
|
120
|
+
expect(onActiveItemIdChange.mock.calls.at(-1)?.[0]).toBe(selected.id);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("does not change cmdk's own selection/filtering behaviour (control)", async () => {
|
|
125
|
+
const onSelect = vi.fn();
|
|
126
|
+
render(
|
|
127
|
+
<Command>
|
|
128
|
+
<CommandInput placeholder="Search…" />
|
|
129
|
+
<CommandList>
|
|
130
|
+
<CommandGroup heading="Fruit">
|
|
131
|
+
<CommandItem value="apple" onSelect={onSelect}>
|
|
132
|
+
Apple
|
|
133
|
+
</CommandItem>
|
|
134
|
+
<CommandItem value="banana" onSelect={onSelect}>
|
|
135
|
+
Banana
|
|
136
|
+
</CommandItem>
|
|
137
|
+
</CommandGroup>
|
|
138
|
+
</CommandList>
|
|
139
|
+
</Command>,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const input = screen.getByPlaceholderText("Search…");
|
|
143
|
+
await userEvent.type(input, "banana");
|
|
144
|
+
expect(screen.queryByText("Apple")).not.toBeInTheDocument();
|
|
145
|
+
expect(screen.getByText("Banana")).toBeInTheDocument();
|
|
146
|
+
|
|
147
|
+
await userEvent.type(input, "{Enter}");
|
|
148
|
+
expect(onSelect).toHaveBeenCalledWith("banana");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
function InnerReader({ onId }: { onId: (id?: string) => void }) {
|
|
152
|
+
const id = useCommandActiveItemId();
|
|
153
|
+
onId(id);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
it("useCommandActiveItemId reports the same id as the onActiveItemIdChange callback", async () => {
|
|
158
|
+
const fromCallback = vi.fn();
|
|
159
|
+
const fromHook = vi.fn();
|
|
160
|
+
|
|
161
|
+
function HookHarness() {
|
|
162
|
+
return (
|
|
163
|
+
<Command onActiveItemIdChange={fromCallback}>
|
|
164
|
+
<CommandInput placeholder="Search…" />
|
|
165
|
+
<InnerReader onId={fromHook} />
|
|
166
|
+
<CommandList>
|
|
167
|
+
<CommandGroup heading="Fruit">
|
|
168
|
+
<CommandItem value="apple">Apple</CommandItem>
|
|
169
|
+
<CommandItem value="banana">Banana</CommandItem>
|
|
170
|
+
</CommandGroup>
|
|
171
|
+
</CommandList>
|
|
172
|
+
</Command>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
render(<HookHarness />);
|
|
177
|
+
const input = screen.getByPlaceholderText("Search…");
|
|
178
|
+
await userEvent.type(input, "{ArrowDown}");
|
|
179
|
+
|
|
180
|
+
expect(fromHook.mock.calls.at(-1)?.[0]).toBe(fromCallback.mock.calls.at(-1)?.[0]);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("CommandItem — dev-only warning for cmdk-overridden props", () => {
|
|
185
|
+
it("warns when a consumer passes id, role, or aria-selected", () => {
|
|
186
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
187
|
+
try {
|
|
188
|
+
render(
|
|
189
|
+
<Command>
|
|
190
|
+
<CommandList>
|
|
191
|
+
<CommandItem id="my-custom-id">Apple</CommandItem>
|
|
192
|
+
</CommandList>
|
|
193
|
+
</Command>,
|
|
194
|
+
);
|
|
195
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('"id"'));
|
|
196
|
+
} finally {
|
|
197
|
+
warn.mockRestore();
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* A plain external input ABOVE the `Command` tree — the composer-textarea shape
|
|
204
|
+
* this issue was filed for. Arrow keys typed into the input are forwarded onto
|
|
205
|
+
* the `Command` root (the same technique `command.stories.tsx`'s
|
|
206
|
+
* `ExternalInputCombobox` story uses), and `aria-activedescendant` is driven off
|
|
207
|
+
* `onActiveItemIdChange` — never a positional DOM query.
|
|
208
|
+
*/
|
|
209
|
+
describe("Command — external combobox input wiring (integration)", () => {
|
|
210
|
+
function ExternalCombobox() {
|
|
211
|
+
const [activeId, setActiveId] = useState<string | undefined>();
|
|
212
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
213
|
+
|
|
214
|
+
const forwardKey = (event: ReactKeyboardEvent<HTMLInputElement>) => {
|
|
215
|
+
if (["ArrowDown", "ArrowUp", "Enter", "Home", "End"].includes(event.key)) {
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
listRef.current?.dispatchEvent(
|
|
218
|
+
new KeyboardEvent("keydown", { key: event.key, bubbles: true, cancelable: true }),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div>
|
|
225
|
+
<input
|
|
226
|
+
aria-activedescendant={activeId}
|
|
227
|
+
aria-controls="fruit-list"
|
|
228
|
+
aria-expanded="true"
|
|
229
|
+
role="combobox"
|
|
230
|
+
onKeyDown={forwardKey}
|
|
231
|
+
placeholder="Type to search fruit…"
|
|
232
|
+
/>
|
|
233
|
+
<Command id="fruit-list" ref={listRef} onActiveItemIdChange={setActiveId}>
|
|
234
|
+
<CommandList>
|
|
235
|
+
<CommandGroup heading="Fruit">
|
|
236
|
+
<CommandItem value="apple">Apple</CommandItem>
|
|
237
|
+
<CommandItem value="banana">Banana</CommandItem>
|
|
238
|
+
<CommandItem value="cherry">Cherry</CommandItem>
|
|
239
|
+
</CommandGroup>
|
|
240
|
+
</CommandList>
|
|
241
|
+
</Command>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
it("keeps the external input's aria-activedescendant in sync with the selected item's id", async () => {
|
|
247
|
+
const { container } = render(<ExternalCombobox />);
|
|
248
|
+
const input = screen.getByPlaceholderText("Type to search fruit…");
|
|
249
|
+
|
|
250
|
+
// FIRST, before any interaction at all: a screen-reader user who focuses
|
|
251
|
+
// the input while the popup is already open must be told which option is
|
|
252
|
+
// highlighted. cmdk highlights the first item on mount, so
|
|
253
|
+
// aria-activedescendant has to point at it right away.
|
|
254
|
+
await waitFor(() => {
|
|
255
|
+
const onMount = container.querySelector('[aria-selected="true"]') as HTMLElement;
|
|
256
|
+
expect(onMount).not.toBeNull();
|
|
257
|
+
expect(input).toHaveAttribute("aria-activedescendant", onMount.id);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Forward one ArrowDown to move cmdk's selection (exercising the real
|
|
261
|
+
// keyboard-navigation path the ExternalInputCombobox story drives), then
|
|
262
|
+
// assert the external input's aria-activedescendant tracks the newly
|
|
263
|
+
// highlighted item's DOM id — never a positional index.
|
|
264
|
+
await userEvent.type(input, "{ArrowDown}");
|
|
265
|
+
await waitFor(() => {
|
|
266
|
+
const nowSelected = container.querySelector('[aria-selected="true"]') as HTMLElement;
|
|
267
|
+
expect(nowSelected).not.toBeNull();
|
|
268
|
+
expect(input).toHaveAttribute("aria-activedescendant", nowSelected.id);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// A second ArrowDown must move it again, to a DIFFERENT id.
|
|
272
|
+
const firstId = input.getAttribute("aria-activedescendant");
|
|
273
|
+
await userEvent.type(input, "{ArrowDown}");
|
|
274
|
+
await waitFor(() => {
|
|
275
|
+
const nowSelected = container.querySelector('[aria-selected="true"]') as HTMLElement;
|
|
276
|
+
expect(nowSelected.id).not.toBe(firstId);
|
|
277
|
+
expect(input).toHaveAttribute("aria-activedescendant", nowSelected.id);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
});
|