@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,699 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree component unit tests.
|
|
3
|
+
*
|
|
4
|
+
* Environment notes (jsdom):
|
|
5
|
+
* - jsdom has zero layout: offsetHeight / getBoundingClientRect always return 0.
|
|
6
|
+
* `useVirtualizer` therefore computes zero total-size and renders 0 virtual rows
|
|
7
|
+
* unless we stub layout APIs. We apply a minimal stub (offsetHeight + getBCR) on
|
|
8
|
+
* the scroll container so the virtualizer sees a non-zero viewport and renders
|
|
9
|
+
* at least the overscan window.
|
|
10
|
+
* - The "only a small window is mounted" assertion is best-effort under jsdom: we
|
|
11
|
+
* verify that fewer than all 5 000 treeitems are in the DOM.
|
|
12
|
+
* - Interaction (ArrowDown scrolls virtualizer) and a11y (axe) are verified via
|
|
13
|
+
* the Storybook `run-story-tests` gate on the real browser surface.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
|
|
16
|
+
import { render, screen, waitFor, act } from "@testing-library/react";
|
|
17
|
+
import userEvent from "@testing-library/user-event";
|
|
18
|
+
import { Tree, type TreeNode } from "./tree";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// jsdom layout stubs for @tanstack/react-virtual
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
const VIEWPORT_HEIGHT = 400;
|
|
25
|
+
const ROW_HEIGHT = 32;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Stub `offsetHeight` and `getBoundingClientRect` on any element that has the
|
|
29
|
+
* data-virtual-scroll attribute, so the virtualizer computes a non-zero
|
|
30
|
+
* viewport and renders rows. We patch at the Element prototype level so every
|
|
31
|
+
* scroll container benefits.
|
|
32
|
+
*/
|
|
33
|
+
let originalGetBCR: () => DOMRect;
|
|
34
|
+
|
|
35
|
+
beforeAll(() => {
|
|
36
|
+
originalGetBCR = Element.prototype.getBoundingClientRect;
|
|
37
|
+
|
|
38
|
+
// Give every element a height. The virtualizer uses this for its scroll container.
|
|
39
|
+
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
|
|
40
|
+
configurable: true,
|
|
41
|
+
get() {
|
|
42
|
+
return VIEWPORT_HEIGHT;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
Element.prototype.getBoundingClientRect = function () {
|
|
47
|
+
return {
|
|
48
|
+
width: 300,
|
|
49
|
+
height: VIEWPORT_HEIGHT,
|
|
50
|
+
top: 0,
|
|
51
|
+
left: 0,
|
|
52
|
+
bottom: VIEWPORT_HEIGHT,
|
|
53
|
+
right: 300,
|
|
54
|
+
x: 0,
|
|
55
|
+
y: 0,
|
|
56
|
+
toJSON() {
|
|
57
|
+
return this;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterAll(() => {
|
|
64
|
+
Element.prototype.getBoundingClientRect = originalGetBCR;
|
|
65
|
+
// Remove the accessor we installed by redefining with a plain value descriptor.
|
|
66
|
+
// We cannot mix accessor (get/set) and data (value/writable) in one call —
|
|
67
|
+
// so delete the property first, then reinstall as a data property (jsdom default).
|
|
68
|
+
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
|
|
69
|
+
configurable: true,
|
|
70
|
+
get() {
|
|
71
|
+
return 0;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Fixtures
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
const nodes: TreeNode[] = [
|
|
81
|
+
{
|
|
82
|
+
id: "fruits",
|
|
83
|
+
label: "Fruits",
|
|
84
|
+
children: [
|
|
85
|
+
{ id: "apple", label: "Apple" },
|
|
86
|
+
{ id: "banana", label: "Banana" },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "veggies",
|
|
91
|
+
label: "Vegetables",
|
|
92
|
+
children: [{ id: "carrot", label: "Carrot" }],
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// scrollToId / scrollSelectionIntoView — reveal a programmatically-selected node
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
describe("Tree reveal-into-view (scrollToId / scrollSelectionIntoView)", () => {
|
|
101
|
+
it("scrollToId expands a collapsed ancestor so the node is revealed (non-virtual)", async () => {
|
|
102
|
+
const scrollSpy = vi.fn();
|
|
103
|
+
const orig = Element.prototype.scrollIntoView;
|
|
104
|
+
Element.prototype.scrollIntoView = scrollSpy;
|
|
105
|
+
try {
|
|
106
|
+
const { rerender } = render(<Tree nodes={nodes} />);
|
|
107
|
+
// "Carrot" lives under the collapsed "Vegetables" — not in the DOM yet.
|
|
108
|
+
expect(screen.queryByText("Carrot")).toBeNull();
|
|
109
|
+
rerender(<Tree nodes={nodes} scrollToId="carrot" />);
|
|
110
|
+
await waitFor(() => expect(screen.getByText("Carrot")).toBeInTheDocument());
|
|
111
|
+
await waitFor(() => expect(scrollSpy).toHaveBeenCalled());
|
|
112
|
+
} finally {
|
|
113
|
+
Element.prototype.scrollIntoView = orig;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("scrollToId scrolls an already-visible node into view (non-virtual)", async () => {
|
|
118
|
+
const scrollSpy = vi.fn();
|
|
119
|
+
const orig = Element.prototype.scrollIntoView;
|
|
120
|
+
Element.prototype.scrollIntoView = scrollSpy;
|
|
121
|
+
try {
|
|
122
|
+
const { rerender } = render(<Tree nodes={nodes} defaultExpandedIds={["fruits"]} />);
|
|
123
|
+
rerender(<Tree nodes={nodes} defaultExpandedIds={["fruits"]} scrollToId="apple" />);
|
|
124
|
+
await waitFor(() => expect(scrollSpy).toHaveBeenCalled());
|
|
125
|
+
} finally {
|
|
126
|
+
Element.prototype.scrollIntoView = orig;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("scrollSelectionIntoView reveals a newly-selected collapsed node", async () => {
|
|
131
|
+
const scrollSpy = vi.fn();
|
|
132
|
+
const orig = Element.prototype.scrollIntoView;
|
|
133
|
+
Element.prototype.scrollIntoView = scrollSpy;
|
|
134
|
+
try {
|
|
135
|
+
const { rerender } = render(<Tree nodes={nodes} scrollSelectionIntoView selectedIds={[]} />);
|
|
136
|
+
expect(screen.queryByText("Carrot")).toBeNull();
|
|
137
|
+
rerender(<Tree nodes={nodes} scrollSelectionIntoView selectedIds={["carrot"]} />);
|
|
138
|
+
await waitFor(() => expect(screen.getByText("Carrot")).toBeInTheDocument());
|
|
139
|
+
} finally {
|
|
140
|
+
Element.prototype.scrollIntoView = orig;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("scrollToId requests ancestor expansion in virtualize mode (controlled)", async () => {
|
|
145
|
+
const onExpandedChange = vi.fn();
|
|
146
|
+
const { rerender } = render(
|
|
147
|
+
<Tree
|
|
148
|
+
nodes={nodes}
|
|
149
|
+
virtualize
|
|
150
|
+
maxHeight="400px"
|
|
151
|
+
estimateRowHeight={ROW_HEIGHT}
|
|
152
|
+
expandedIds={[]}
|
|
153
|
+
onExpandedChange={onExpandedChange}
|
|
154
|
+
/>,
|
|
155
|
+
);
|
|
156
|
+
rerender(
|
|
157
|
+
<Tree
|
|
158
|
+
nodes={nodes}
|
|
159
|
+
virtualize
|
|
160
|
+
maxHeight="400px"
|
|
161
|
+
estimateRowHeight={ROW_HEIGHT}
|
|
162
|
+
expandedIds={[]}
|
|
163
|
+
onExpandedChange={onExpandedChange}
|
|
164
|
+
scrollToId="carrot"
|
|
165
|
+
/>,
|
|
166
|
+
);
|
|
167
|
+
// Reveal calls setExpandedIds → onExpandedChange (controlled) with the ancestor.
|
|
168
|
+
await waitFor(() => expect(onExpandedChange).toHaveBeenCalled());
|
|
169
|
+
const lastCall = onExpandedChange.mock.calls.at(-1)?.[0] as string[];
|
|
170
|
+
expect([...lastCall]).toContain("veggies");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("scrollToId to an unknown id is a graceful no-op", () => {
|
|
174
|
+
const onExpandedChange = vi.fn();
|
|
175
|
+
const { rerender } = render(<Tree nodes={nodes} onExpandedChange={onExpandedChange} />);
|
|
176
|
+
expect(() =>
|
|
177
|
+
rerender(
|
|
178
|
+
<Tree nodes={nodes} onExpandedChange={onExpandedChange} scrollToId="does-not-exist" />,
|
|
179
|
+
),
|
|
180
|
+
).not.toThrow();
|
|
181
|
+
expect(onExpandedChange).not.toHaveBeenCalled();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Existing non-virtualized tests (must stay green)
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
describe("Tree (non-virtualized)", () => {
|
|
190
|
+
it("renders a tree with treeitems", () => {
|
|
191
|
+
render(<Tree nodes={nodes} />);
|
|
192
|
+
expect(screen.getByRole("tree")).toBeInTheDocument();
|
|
193
|
+
expect(screen.getByRole("treeitem", { name: /Fruits/ })).toBeInTheDocument();
|
|
194
|
+
expect(screen.getByRole("treeitem", { name: /Vegetables/ })).toBeInTheDocument();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("collapses children by default (descendants are not rendered)", () => {
|
|
198
|
+
render(<Tree nodes={nodes} />);
|
|
199
|
+
expect(screen.queryByRole("treeitem", { name: /Apple/ })).not.toBeInTheDocument();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("uses roving tabindex — exactly one treeitem is tabbable", () => {
|
|
203
|
+
render(<Tree nodes={nodes} defaultExpandedIds={["fruits"]} />);
|
|
204
|
+
const items = screen.getAllByRole("treeitem");
|
|
205
|
+
const tabbable = items.filter((el) => el.getAttribute("tabindex") === "0");
|
|
206
|
+
expect(tabbable).toHaveLength(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("marks expanded parents with aria-expanded and renders their children", () => {
|
|
210
|
+
render(<Tree nodes={nodes} defaultExpandedIds={["fruits"]} />);
|
|
211
|
+
expect(screen.getByRole("treeitem", { name: /Fruits/ })).toHaveAttribute(
|
|
212
|
+
"aria-expanded",
|
|
213
|
+
"true",
|
|
214
|
+
);
|
|
215
|
+
expect(screen.getByRole("treeitem", { name: /Apple/ })).toBeInTheDocument();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("exposes hierarchy semantics (aria-level / setsize / posinset)", () => {
|
|
219
|
+
render(<Tree nodes={nodes} defaultExpandedIds={["fruits"]} />);
|
|
220
|
+
const fruits = screen.getByRole("treeitem", { name: /Fruits/ });
|
|
221
|
+
expect(fruits).toHaveAttribute("aria-level", "1");
|
|
222
|
+
expect(fruits).toHaveAttribute("aria-setsize", "2");
|
|
223
|
+
expect(fruits).toHaveAttribute("aria-posinset", "1");
|
|
224
|
+
const apple = screen.getByRole("treeitem", { name: /Apple/ });
|
|
225
|
+
expect(apple).toHaveAttribute("aria-level", "2");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("toggles expansion when a parent row is clicked", async () => {
|
|
229
|
+
const user = userEvent.setup();
|
|
230
|
+
render(<Tree nodes={nodes} selectionMode="none" />);
|
|
231
|
+
const fruits = screen.getByRole("treeitem", { name: /Fruits/ });
|
|
232
|
+
expect(fruits).toHaveAttribute("aria-expanded", "false");
|
|
233
|
+
await user.click(fruits);
|
|
234
|
+
expect(fruits).toHaveAttribute("aria-expanded", "true");
|
|
235
|
+
expect(screen.getByRole("treeitem", { name: /Apple/ })).toBeInTheDocument();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("fires onSelectionChange and marks aria-selected on select", async () => {
|
|
239
|
+
const user = userEvent.setup();
|
|
240
|
+
const onSelectionChange = vi.fn();
|
|
241
|
+
render(
|
|
242
|
+
<Tree nodes={nodes} defaultExpandedIds={["fruits"]} onSelectionChange={onSelectionChange} />,
|
|
243
|
+
);
|
|
244
|
+
await user.click(screen.getByRole("treeitem", { name: /Apple/ }));
|
|
245
|
+
expect(onSelectionChange).toHaveBeenCalledWith(["apple"]);
|
|
246
|
+
expect(screen.getByRole("treeitem", { name: /Apple/ })).toHaveAttribute(
|
|
247
|
+
"aria-selected",
|
|
248
|
+
"true",
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("moves the active item with ArrowDown (roving focus)", async () => {
|
|
253
|
+
const user = userEvent.setup();
|
|
254
|
+
render(<Tree nodes={nodes} defaultExpandedIds={["fruits"]} />);
|
|
255
|
+
const fruits = screen.getByRole("treeitem", { name: /Fruits/ });
|
|
256
|
+
fruits.focus();
|
|
257
|
+
expect(fruits).toHaveAttribute("tabindex", "0");
|
|
258
|
+
await user.keyboard("{ArrowDown}");
|
|
259
|
+
const apple = screen.getByRole("treeitem", { name: /Apple/ });
|
|
260
|
+
expect(apple).toHaveAttribute("tabindex", "0");
|
|
261
|
+
expect(fruits).toHaveAttribute("tabindex", "-1");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("collapses an expanded parent with ArrowLeft", async () => {
|
|
265
|
+
const user = userEvent.setup();
|
|
266
|
+
render(<Tree nodes={nodes} defaultExpandedIds={["fruits"]} />);
|
|
267
|
+
const fruits = screen.getByRole("treeitem", { name: /Fruits/ });
|
|
268
|
+
fruits.focus();
|
|
269
|
+
await user.keyboard("{ArrowLeft}");
|
|
270
|
+
expect(fruits).toHaveAttribute("aria-expanded", "false");
|
|
271
|
+
expect(screen.queryByRole("treeitem", { name: /Apple/ })).not.toBeInTheDocument();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("does not select disabled nodes", async () => {
|
|
275
|
+
const user = userEvent.setup();
|
|
276
|
+
const onSelectionChange = vi.fn();
|
|
277
|
+
const withDisabled: TreeNode[] = [
|
|
278
|
+
{ id: "a", label: "Alpha", disabled: true },
|
|
279
|
+
{ id: "b", label: "Beta" },
|
|
280
|
+
];
|
|
281
|
+
render(<Tree nodes={withDisabled} onSelectionChange={onSelectionChange} />);
|
|
282
|
+
await user.click(screen.getByRole("treeitem", { name: /Alpha/ }));
|
|
283
|
+
expect(onSelectionChange).not.toHaveBeenCalled();
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// Lazy loading tests
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
describe("Tree (lazy loading)", () => {
|
|
292
|
+
function makeLazyNodes(): TreeNode[] {
|
|
293
|
+
return [
|
|
294
|
+
{ id: "root-a", label: "Category A", hasChildren: true },
|
|
295
|
+
{ id: "root-b", label: "Category B" },
|
|
296
|
+
];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
it("shows the expand affordance for hasChildren nodes even without loaded children", () => {
|
|
300
|
+
render(<Tree nodes={makeLazyNodes()} />);
|
|
301
|
+
const catA = screen.getByRole("treeitem", { name: /Category A/ });
|
|
302
|
+
// aria-expanded should be present (false) because it's expandable
|
|
303
|
+
expect(catA).toHaveAttribute("aria-expanded", "false");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("calls loadChildren on first expand and shows loading state (aria-busy)", async () => {
|
|
307
|
+
const user = userEvent.setup();
|
|
308
|
+
let resolveLoad!: (nodes: TreeNode[]) => void;
|
|
309
|
+
const loadChildren = vi.fn(
|
|
310
|
+
() =>
|
|
311
|
+
new Promise<TreeNode[]>((res) => {
|
|
312
|
+
resolveLoad = res;
|
|
313
|
+
}),
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
render(<Tree nodes={makeLazyNodes()} loadChildren={loadChildren} />);
|
|
317
|
+
const catA = screen.getByRole("treeitem", { name: /Category A/ });
|
|
318
|
+
|
|
319
|
+
await user.click(catA);
|
|
320
|
+
|
|
321
|
+
// loadChildren must have been called
|
|
322
|
+
expect(loadChildren).toHaveBeenCalledTimes(1);
|
|
323
|
+
expect(loadChildren).toHaveBeenCalledWith(expect.objectContaining({ id: "root-a" }));
|
|
324
|
+
|
|
325
|
+
// While loading, aria-busy is set on the parent treeitem
|
|
326
|
+
await waitFor(() => {
|
|
327
|
+
expect(screen.getByRole("treeitem", { name: /Category A/ })).toHaveAttribute(
|
|
328
|
+
"aria-busy",
|
|
329
|
+
"true",
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Resolve the promise with children
|
|
334
|
+
await act(async () => {
|
|
335
|
+
resolveLoad([
|
|
336
|
+
{ id: "child-1", label: "Child 1" },
|
|
337
|
+
{ id: "child-2", label: "Child 2" },
|
|
338
|
+
]);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Children should now be rendered
|
|
342
|
+
await waitFor(() => {
|
|
343
|
+
expect(screen.getByRole("treeitem", { name: /Child 1/ })).toBeInTheDocument();
|
|
344
|
+
expect(screen.getByRole("treeitem", { name: /Child 2/ })).toBeInTheDocument();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// aria-busy should be cleared
|
|
348
|
+
expect(screen.getByRole("treeitem", { name: /Category A/ })).not.toHaveAttribute("aria-busy");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("does not call loadChildren again on re-expand (cached)", async () => {
|
|
352
|
+
const user = userEvent.setup();
|
|
353
|
+
const loadChildren = vi.fn().mockResolvedValue([{ id: "child-1", label: "Child 1" }]);
|
|
354
|
+
|
|
355
|
+
render(<Tree nodes={makeLazyNodes()} loadChildren={loadChildren} />);
|
|
356
|
+
const catA = screen.getByRole("treeitem", { name: /Category A/ });
|
|
357
|
+
|
|
358
|
+
// First expand — triggers fetch
|
|
359
|
+
await user.click(catA);
|
|
360
|
+
await waitFor(() => {
|
|
361
|
+
expect(screen.getByRole("treeitem", { name: /Child 1/ })).toBeInTheDocument();
|
|
362
|
+
});
|
|
363
|
+
expect(loadChildren).toHaveBeenCalledTimes(1);
|
|
364
|
+
|
|
365
|
+
// Collapse
|
|
366
|
+
await user.click(screen.getByRole("treeitem", { name: /Category A/ }));
|
|
367
|
+
await waitFor(() => {
|
|
368
|
+
expect(screen.queryByRole("treeitem", { name: /Child 1/ })).not.toBeInTheDocument();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Re-expand — should NOT call loadChildren again
|
|
372
|
+
await user.click(screen.getByRole("treeitem", { name: /Category A/ }));
|
|
373
|
+
await waitFor(() => {
|
|
374
|
+
expect(screen.getByRole("treeitem", { name: /Child 1/ })).toBeInTheDocument();
|
|
375
|
+
});
|
|
376
|
+
expect(loadChildren).toHaveBeenCalledTimes(1);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("shows error state and retry button when loadChildren rejects", async () => {
|
|
380
|
+
const user = userEvent.setup();
|
|
381
|
+
const loadChildren = vi.fn().mockRejectedValue(new Error("Network error"));
|
|
382
|
+
|
|
383
|
+
render(<Tree nodes={makeLazyNodes()} loadChildren={loadChildren} />);
|
|
384
|
+
const catA = screen.getByRole("treeitem", { name: /Category A/ });
|
|
385
|
+
|
|
386
|
+
await user.click(catA);
|
|
387
|
+
|
|
388
|
+
// Error state: "Failed to load" text visible, no crash
|
|
389
|
+
await waitFor(() => {
|
|
390
|
+
expect(screen.getByText(/Failed to load/i)).toBeInTheDocument();
|
|
391
|
+
});
|
|
392
|
+
expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
|
|
393
|
+
|
|
394
|
+
// aria-busy is NOT set in error state (it's cleared)
|
|
395
|
+
expect(screen.getByRole("treeitem", { name: /Category A/ })).not.toHaveAttribute("aria-busy");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("retries successfully after error", async () => {
|
|
399
|
+
const user = userEvent.setup();
|
|
400
|
+
const loadChildren = vi
|
|
401
|
+
.fn()
|
|
402
|
+
.mockRejectedValueOnce(new Error("fail"))
|
|
403
|
+
.mockResolvedValue([{ id: "retry-child", label: "Retry Child" }]);
|
|
404
|
+
|
|
405
|
+
render(<Tree nodes={makeLazyNodes()} loadChildren={loadChildren} />);
|
|
406
|
+
await user.click(screen.getByRole("treeitem", { name: /Category A/ }));
|
|
407
|
+
|
|
408
|
+
await waitFor(() => expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument());
|
|
409
|
+
|
|
410
|
+
await user.click(screen.getByRole("button", { name: /retry/i }));
|
|
411
|
+
|
|
412
|
+
await waitFor(() => {
|
|
413
|
+
expect(screen.getByRole("treeitem", { name: /Retry Child/ })).toBeInTheDocument();
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
// surface prop tests
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
describe("Tree (surface prop)", () => {
|
|
423
|
+
const simpleNodes: TreeNode[] = [
|
|
424
|
+
{ id: "a", label: "Alpha" },
|
|
425
|
+
{ id: "b", label: "Beta" },
|
|
426
|
+
];
|
|
427
|
+
|
|
428
|
+
it("default surface uses bg-accent on the selected row", async () => {
|
|
429
|
+
const user = userEvent.setup();
|
|
430
|
+
render(<Tree nodes={simpleNodes} />);
|
|
431
|
+
await user.click(screen.getByRole("treeitem", { name: /Alpha/ }));
|
|
432
|
+
const selected = screen.getByRole("treeitem", { name: /Alpha/ });
|
|
433
|
+
expect(selected).toHaveAttribute("aria-selected", "true");
|
|
434
|
+
// classList gives exact token membership — avoids false matches from hover:bg-accent
|
|
435
|
+
expect(selected.classList.contains("bg-accent")).toBe(true);
|
|
436
|
+
expect(selected.classList.contains("bg-sidebar-accent")).toBe(false);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("surface='default' (explicit) uses bg-accent on the selected row", async () => {
|
|
440
|
+
const user = userEvent.setup();
|
|
441
|
+
render(<Tree nodes={simpleNodes} surface="default" />);
|
|
442
|
+
await user.click(screen.getByRole("treeitem", { name: /Alpha/ }));
|
|
443
|
+
const selected = screen.getByRole("treeitem", { name: /Alpha/ });
|
|
444
|
+
expect(selected).toHaveAttribute("aria-selected", "true");
|
|
445
|
+
expect(selected.classList.contains("bg-accent")).toBe(true);
|
|
446
|
+
expect(selected.classList.contains("bg-sidebar-accent")).toBe(false);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("surface='sidebar' uses bg-sidebar-accent + text-sidebar-accent-foreground on the selected row", async () => {
|
|
450
|
+
const user = userEvent.setup();
|
|
451
|
+
render(<Tree nodes={simpleNodes} surface="sidebar" />);
|
|
452
|
+
await user.click(screen.getByRole("treeitem", { name: /Alpha/ }));
|
|
453
|
+
const selected = screen.getByRole("treeitem", { name: /Alpha/ });
|
|
454
|
+
expect(selected).toHaveAttribute("aria-selected", "true");
|
|
455
|
+
// classList gives exact token membership — avoids false matches from hover:bg-accent
|
|
456
|
+
expect(selected.classList.contains("bg-sidebar-accent")).toBe(true);
|
|
457
|
+
expect(selected.classList.contains("text-sidebar-accent-foreground")).toBe(true);
|
|
458
|
+
expect(selected.classList.contains("bg-accent")).toBe(false);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("surface='sidebar' threads through to nested child rows", async () => {
|
|
462
|
+
const user = userEvent.setup();
|
|
463
|
+
const nestedNodes: TreeNode[] = [
|
|
464
|
+
{
|
|
465
|
+
id: "parent",
|
|
466
|
+
label: "Parent",
|
|
467
|
+
children: [{ id: "child", label: "Child" }],
|
|
468
|
+
},
|
|
469
|
+
];
|
|
470
|
+
render(<Tree nodes={nestedNodes} surface="sidebar" defaultExpandedIds={["parent"]} />);
|
|
471
|
+
await user.click(screen.getByRole("treeitem", { name: /Child/ }));
|
|
472
|
+
const selected = screen.getByRole("treeitem", { name: /Child/ });
|
|
473
|
+
expect(selected).toHaveAttribute("aria-selected", "true");
|
|
474
|
+
expect(selected.classList.contains("bg-sidebar-accent")).toBe(true);
|
|
475
|
+
expect(selected.classList.contains("bg-accent")).toBe(false);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("surface='sidebar' works in virtualized mode", async () => {
|
|
479
|
+
const user = userEvent.setup();
|
|
480
|
+
const smallNodes: TreeNode[] = Array.from({ length: 20 }, (_, i) => ({
|
|
481
|
+
id: `n-${i}`,
|
|
482
|
+
label: `Node ${i + 1}`,
|
|
483
|
+
}));
|
|
484
|
+
render(
|
|
485
|
+
<Tree
|
|
486
|
+
nodes={smallNodes}
|
|
487
|
+
surface="sidebar"
|
|
488
|
+
virtualize
|
|
489
|
+
maxHeight="400px"
|
|
490
|
+
estimateRowHeight={32}
|
|
491
|
+
/>,
|
|
492
|
+
);
|
|
493
|
+
await user.click(screen.getByRole("treeitem", { name: "Node 1" }));
|
|
494
|
+
const selected = screen.getByRole("treeitem", { name: "Node 1" });
|
|
495
|
+
expect(selected).toHaveAttribute("aria-selected", "true");
|
|
496
|
+
expect(selected.classList.contains("bg-sidebar-accent")).toBe(true);
|
|
497
|
+
expect(selected.classList.contains("bg-accent")).toBe(false);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
// Virtualization tests
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
|
|
505
|
+
describe("Tree (virtualized)", () => {
|
|
506
|
+
function build(count: number): TreeNode[] {
|
|
507
|
+
return Array.from({ length: count }, (_, i) => ({ id: `n-${i}`, label: `Node ${i + 1}` }));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
it("renders a scroll container and the role=tree root when virtualize=true", () => {
|
|
511
|
+
render(
|
|
512
|
+
<Tree nodes={build(5000)} virtualize maxHeight="400px" estimateRowHeight={ROW_HEIGHT} />,
|
|
513
|
+
);
|
|
514
|
+
expect(screen.getByRole("tree")).toBeInTheDocument();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it("renders far fewer than 5 000 treeitems in the DOM (windowing is active)", () => {
|
|
518
|
+
// Environment note: jsdom layout is stubbed above so the virtualizer can
|
|
519
|
+
// compute a non-zero window. The exact rendered count depends on the stub
|
|
520
|
+
// heights but should always be ≪ 5 000.
|
|
521
|
+
render(
|
|
522
|
+
<Tree nodes={build(5000)} virtualize maxHeight="400px" estimateRowHeight={ROW_HEIGHT} />,
|
|
523
|
+
);
|
|
524
|
+
const items = screen.queryAllByRole("treeitem");
|
|
525
|
+
// Expect far fewer than all 5000 nodes in the DOM.
|
|
526
|
+
// Under the jsdom stub (400px viewport / 32px rows = ~12–26 with overscan).
|
|
527
|
+
expect(items.length).toBeLessThan(100);
|
|
528
|
+
expect(items.length).toBeGreaterThan(0);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it("carries correct ARIA attributes on virtual rows (level / setsize / posinset)", () => {
|
|
532
|
+
const smallNodes = build(20);
|
|
533
|
+
render(<Tree nodes={smallNodes} virtualize maxHeight="400px" estimateRowHeight={ROW_HEIGHT} />);
|
|
534
|
+
// First rendered item should be node-0 at level 1, setsize 20, posinset 1
|
|
535
|
+
const first = screen.getByRole("treeitem", { name: "Node 1" });
|
|
536
|
+
expect(first).toHaveAttribute("aria-level", "1");
|
|
537
|
+
expect(first).toHaveAttribute("aria-setsize", "20");
|
|
538
|
+
expect(first).toHaveAttribute("aria-posinset", "1");
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it("ArrowDown moves active tabindex in the virtual flat list", async () => {
|
|
542
|
+
const user = userEvent.setup();
|
|
543
|
+
const smallNodes = build(30);
|
|
544
|
+
render(<Tree nodes={smallNodes} virtualize maxHeight="400px" estimateRowHeight={ROW_HEIGHT} />);
|
|
545
|
+
|
|
546
|
+
const first = screen.getByRole("treeitem", { name: "Node 1" });
|
|
547
|
+
first.focus();
|
|
548
|
+
expect(first).toHaveAttribute("tabindex", "0");
|
|
549
|
+
|
|
550
|
+
await user.keyboard("{ArrowDown}");
|
|
551
|
+
|
|
552
|
+
const second = screen.getByRole("treeitem", { name: "Node 2" });
|
|
553
|
+
expect(second).toHaveAttribute("tabindex", "0");
|
|
554
|
+
expect(first).toHaveAttribute("tabindex", "-1");
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("selects a node on click in virtual mode", async () => {
|
|
558
|
+
const user = userEvent.setup();
|
|
559
|
+
const onSelectionChange = vi.fn();
|
|
560
|
+
const smallNodes = build(20);
|
|
561
|
+
render(
|
|
562
|
+
<Tree
|
|
563
|
+
nodes={smallNodes}
|
|
564
|
+
virtualize
|
|
565
|
+
maxHeight="400px"
|
|
566
|
+
estimateRowHeight={ROW_HEIGHT}
|
|
567
|
+
onSelectionChange={onSelectionChange}
|
|
568
|
+
/>,
|
|
569
|
+
);
|
|
570
|
+
await user.click(screen.getByRole("treeitem", { name: "Node 3" }));
|
|
571
|
+
expect(onSelectionChange).toHaveBeenCalledWith(["n-2"]);
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
// accessory / trailing slot (#369)
|
|
577
|
+
// ---------------------------------------------------------------------------
|
|
578
|
+
|
|
579
|
+
describe("Tree (accessory / trailing slot, #369)", () => {
|
|
580
|
+
const nodesWithAccessory: TreeNode[] = [
|
|
581
|
+
{ id: "a", label: "package.json", accessory: <span>1.2 KB</span> },
|
|
582
|
+
{ id: "b", label: "index.ts" },
|
|
583
|
+
];
|
|
584
|
+
|
|
585
|
+
it("renders the accessory content in the row", () => {
|
|
586
|
+
render(<Tree nodes={nodesWithAccessory} />);
|
|
587
|
+
expect(screen.getByText("1.2 KB")).toBeInTheDocument();
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it("the row's accessible name is exactly the node label — the accessory never leaks in", () => {
|
|
591
|
+
render(<Tree nodes={nodesWithAccessory} />);
|
|
592
|
+
expect(screen.getByRole("treeitem", { name: "package.json" })).toBeInTheDocument();
|
|
593
|
+
expect(screen.queryByRole("treeitem", { name: "package.json1.2 KB" })).not.toBeInTheDocument();
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it("clicking the accessory does not select or expand the row (non-virtual path)", async () => {
|
|
597
|
+
const user = userEvent.setup();
|
|
598
|
+
const onSelectionChange = vi.fn();
|
|
599
|
+
render(<Tree nodes={nodesWithAccessory} onSelectionChange={onSelectionChange} />);
|
|
600
|
+
await user.click(screen.getByText("1.2 KB"));
|
|
601
|
+
expect(onSelectionChange).not.toHaveBeenCalled();
|
|
602
|
+
expect(screen.getByRole("treeitem", { name: "package.json" })).not.toHaveAttribute(
|
|
603
|
+
"aria-selected",
|
|
604
|
+
"true",
|
|
605
|
+
);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it("clicking the row itself (not the accessory) still selects it (non-virtual path)", async () => {
|
|
609
|
+
const user = userEvent.setup();
|
|
610
|
+
const onSelectionChange = vi.fn();
|
|
611
|
+
render(<Tree nodes={nodesWithAccessory} onSelectionChange={onSelectionChange} />);
|
|
612
|
+
await user.click(screen.getByRole("treeitem", { name: "package.json" }));
|
|
613
|
+
expect(onSelectionChange).toHaveBeenCalledWith(["a"]);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it("keyboard select (Enter) is unaffected by the accessory's presence", async () => {
|
|
617
|
+
const user = userEvent.setup();
|
|
618
|
+
const onSelectionChange = vi.fn();
|
|
619
|
+
render(<Tree nodes={nodesWithAccessory} onSelectionChange={onSelectionChange} />);
|
|
620
|
+
const row = screen.getByRole("treeitem", { name: "package.json" });
|
|
621
|
+
row.focus();
|
|
622
|
+
await user.keyboard("{Enter}");
|
|
623
|
+
expect(onSelectionChange).toHaveBeenCalledWith(["a"]);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it("renders the accessory and guards clicks in virtualized mode too (the duplicated render path)", async () => {
|
|
627
|
+
const user = userEvent.setup();
|
|
628
|
+
const onSelectionChange = vi.fn();
|
|
629
|
+
render(
|
|
630
|
+
<Tree
|
|
631
|
+
nodes={nodesWithAccessory}
|
|
632
|
+
virtualize
|
|
633
|
+
maxHeight="400px"
|
|
634
|
+
estimateRowHeight={ROW_HEIGHT}
|
|
635
|
+
onSelectionChange={onSelectionChange}
|
|
636
|
+
/>,
|
|
637
|
+
);
|
|
638
|
+
expect(screen.getByText("1.2 KB")).toBeInTheDocument();
|
|
639
|
+
expect(screen.getByRole("treeitem", { name: "package.json" })).toBeInTheDocument();
|
|
640
|
+
await user.click(screen.getByText("1.2 KB"));
|
|
641
|
+
expect(onSelectionChange).not.toHaveBeenCalled();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// `TreeNode.label` is a ReactNode, so `aria-label` (string-only) cannot name
|
|
645
|
+
// every row — without `aria-labelledby` the name falls back to the row's
|
|
646
|
+
// CONTENTS and the accessory leaks in. Locks both render paths.
|
|
647
|
+
it.each([
|
|
648
|
+
["non-virtual", false],
|
|
649
|
+
["virtualized", true],
|
|
650
|
+
])("a ReactNode label still excludes the accessory from the name (%s)", (_name, virtualize) => {
|
|
651
|
+
const nodes: TreeNode[] = [
|
|
652
|
+
{ id: "a", label: <span>package.json</span>, accessory: <span>1.2 KB</span> },
|
|
653
|
+
];
|
|
654
|
+
render(
|
|
655
|
+
<Tree
|
|
656
|
+
nodes={nodes}
|
|
657
|
+
virtualize={virtualize}
|
|
658
|
+
maxHeight={virtualize ? "400px" : undefined}
|
|
659
|
+
estimateRowHeight={virtualize ? ROW_HEIGHT : undefined}
|
|
660
|
+
/>,
|
|
661
|
+
);
|
|
662
|
+
expect(screen.getByRole("treeitem", { name: "package.json" })).toBeInTheDocument();
|
|
663
|
+
expect(screen.queryByRole("treeitem", { name: /1\.2 KB/ })).not.toBeInTheDocument();
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// The tree's keyboard handler lives on the ROOT and preventDefaults
|
|
667
|
+
// Enter/Space/arrows; the row's onFocus (focusin — it bubbles) re-focuses the
|
|
668
|
+
// row. Both must stop at the accessory or interactive content there is dead.
|
|
669
|
+
it("interactive accessory content is focusable and activates itself, not the row", async () => {
|
|
670
|
+
const user = userEvent.setup();
|
|
671
|
+
const onSelectionChange = vi.fn();
|
|
672
|
+
const onAccessoryClick = vi.fn();
|
|
673
|
+
const nodes: TreeNode[] = [
|
|
674
|
+
{
|
|
675
|
+
id: "a",
|
|
676
|
+
label: "package.json",
|
|
677
|
+
accessory: (
|
|
678
|
+
<button type="button" onClick={onAccessoryClick}>
|
|
679
|
+
Details
|
|
680
|
+
</button>
|
|
681
|
+
),
|
|
682
|
+
},
|
|
683
|
+
{ id: "b", label: "index.ts" },
|
|
684
|
+
];
|
|
685
|
+
render(<Tree nodes={nodes} onSelectionChange={onSelectionChange} />);
|
|
686
|
+
|
|
687
|
+
const accessoryButton = screen.getByRole("button", { name: "Details" });
|
|
688
|
+
accessoryButton.focus();
|
|
689
|
+
expect(document.activeElement).toBe(accessoryButton);
|
|
690
|
+
|
|
691
|
+
await user.keyboard("{Enter}");
|
|
692
|
+
expect(onAccessoryClick).toHaveBeenCalledTimes(1);
|
|
693
|
+
expect(onSelectionChange).not.toHaveBeenCalled();
|
|
694
|
+
|
|
695
|
+
// Tree navigation must not steal focus out of the accessory.
|
|
696
|
+
await user.keyboard("{ArrowDown}");
|
|
697
|
+
expect(document.activeElement).toBe(accessoryButton);
|
|
698
|
+
});
|
|
699
|
+
});
|