@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,58 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect } from "storybook/test";
|
|
3
|
+
import { Switch } from "./switch";
|
|
4
|
+
import { Label } from "../label";
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Forms/Switch",
|
|
7
|
+
component: Switch,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
argTypes: {
|
|
10
|
+
checked: {
|
|
11
|
+
description: "Controlled checked state.",
|
|
12
|
+
control: "boolean",
|
|
13
|
+
table: { category: "State" },
|
|
14
|
+
},
|
|
15
|
+
defaultChecked: {
|
|
16
|
+
description: "Uncontrolled initial checked state.",
|
|
17
|
+
control: "boolean",
|
|
18
|
+
table: { category: "State" },
|
|
19
|
+
},
|
|
20
|
+
disabled: {
|
|
21
|
+
description: "Disables the switch.",
|
|
22
|
+
control: "boolean",
|
|
23
|
+
table: { category: "State" },
|
|
24
|
+
},
|
|
25
|
+
required: {
|
|
26
|
+
description: "Marks the switch as required in a form context.",
|
|
27
|
+
control: "boolean",
|
|
28
|
+
table: { category: "Behavior" },
|
|
29
|
+
},
|
|
30
|
+
onCheckedChange: {
|
|
31
|
+
description: "Called when the checked state changes.",
|
|
32
|
+
control: false,
|
|
33
|
+
table: { category: "Behavior" },
|
|
34
|
+
},
|
|
35
|
+
className: {
|
|
36
|
+
description: "Additional CSS classes applied to the root element.",
|
|
37
|
+
control: "text",
|
|
38
|
+
table: { category: "Appearance" },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
} satisfies Meta<typeof Switch>;
|
|
42
|
+
export default meta;
|
|
43
|
+
type Story = StoryObj<typeof meta>;
|
|
44
|
+
export const Default: Story = {
|
|
45
|
+
render: () => (
|
|
46
|
+
<div className="flex items-center gap-2">
|
|
47
|
+
<Switch id="airplane" defaultChecked />
|
|
48
|
+
<Label htmlFor="airplane">Airplane mode</Label>
|
|
49
|
+
</div>
|
|
50
|
+
),
|
|
51
|
+
// Starts on (defaultChecked); clicking toggles the reflected aria-checked state.
|
|
52
|
+
play: async ({ canvas, userEvent }) => {
|
|
53
|
+
const sw = canvas.getByRole("switch");
|
|
54
|
+
await expect(sw).toHaveAttribute("aria-checked", "true");
|
|
55
|
+
await userEvent.click(sw);
|
|
56
|
+
await expect(sw).toHaveAttribute("aria-checked", "false");
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Switch } from "./switch";
|
|
5
|
+
describe("Switch", () => {
|
|
6
|
+
it("toggles on click", async () => {
|
|
7
|
+
render(<Switch aria-label="s" />);
|
|
8
|
+
const sw = screen.getByRole("switch", { name: "s" });
|
|
9
|
+
await userEvent.click(sw);
|
|
10
|
+
expect(sw).toBeChecked();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
|
|
2
|
+
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
|
|
5
|
+
export const Switch = forwardRef<
|
|
6
|
+
ElementRef<typeof SwitchPrimitive.Root>,
|
|
7
|
+
ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
|
|
8
|
+
>(function Switch({ className, ...props }, ref) {
|
|
9
|
+
return (
|
|
10
|
+
<SwitchPrimitive.Root
|
|
11
|
+
ref={ref}
|
|
12
|
+
className={cn(
|
|
13
|
+
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors duration-fast ease-standard",
|
|
14
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
15
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
16
|
+
"data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
<SwitchPrimitive.Thumb
|
|
22
|
+
className={cn(
|
|
23
|
+
"pointer-events-none block size-4 rounded-full bg-background shadow-xs ring-0 transition-transform duration-fast ease-standard",
|
|
24
|
+
"data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
|
|
25
|
+
)}
|
|
26
|
+
/>
|
|
27
|
+
</SwitchPrimitive.Root>
|
|
28
|
+
);
|
|
29
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./table";
|
|
3
|
+
const meta = {
|
|
4
|
+
title: "Data/Table",
|
|
5
|
+
component: Table,
|
|
6
|
+
tags: ["autodocs"],
|
|
7
|
+
parameters: {
|
|
8
|
+
docs: {
|
|
9
|
+
description: {
|
|
10
|
+
component:
|
|
11
|
+
"Low-level static table primitives (header/body/row/cell styling). For sorting, filtering, " +
|
|
12
|
+
"pagination, virtualization, row selection and column management, use the TanStack-powered " +
|
|
13
|
+
"DataTable (see Data/DataTable, @elabs-ai/components-data) instead.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
argTypes: {
|
|
18
|
+
className: {
|
|
19
|
+
description: "Additional CSS classes applied to the `<table>` element.",
|
|
20
|
+
control: "text",
|
|
21
|
+
table: { category: "Styling" },
|
|
22
|
+
},
|
|
23
|
+
children: {
|
|
24
|
+
description: "Table content — compose with TableHeader, TableBody, TableRow, TableCell, etc.",
|
|
25
|
+
control: false,
|
|
26
|
+
table: { category: "Content" },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
} satisfies Meta<typeof Table>;
|
|
30
|
+
export default meta;
|
|
31
|
+
type Story = StoryObj<typeof meta>;
|
|
32
|
+
export const Default: Story = {
|
|
33
|
+
render: () => (
|
|
34
|
+
<Table>
|
|
35
|
+
<TableHeader>
|
|
36
|
+
<TableRow>
|
|
37
|
+
<TableHead>Service</TableHead>
|
|
38
|
+
<TableHead>Status</TableHead>
|
|
39
|
+
</TableRow>
|
|
40
|
+
</TableHeader>
|
|
41
|
+
<TableBody>
|
|
42
|
+
<TableRow>
|
|
43
|
+
<TableCell>api-gateway</TableCell>
|
|
44
|
+
<TableCell>healthy</TableCell>
|
|
45
|
+
</TableRow>
|
|
46
|
+
<TableRow>
|
|
47
|
+
<TableCell>billing</TableCell>
|
|
48
|
+
<TableCell>degraded</TableCell>
|
|
49
|
+
</TableRow>
|
|
50
|
+
</TableBody>
|
|
51
|
+
</Table>
|
|
52
|
+
),
|
|
53
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { Table, TableBody, TableCell, TableRow } from "./table";
|
|
4
|
+
describe("Table", () => {
|
|
5
|
+
it("renders cells", () => {
|
|
6
|
+
render(
|
|
7
|
+
<Table>
|
|
8
|
+
<TableBody>
|
|
9
|
+
<TableRow>
|
|
10
|
+
<TableCell>Alpha</TableCell>
|
|
11
|
+
</TableRow>
|
|
12
|
+
</TableBody>
|
|
13
|
+
</Table>,
|
|
14
|
+
);
|
|
15
|
+
expect(screen.getByText("Alpha")).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
type HTMLAttributes,
|
|
4
|
+
type TdHTMLAttributes,
|
|
5
|
+
type ThHTMLAttributes,
|
|
6
|
+
} from "react";
|
|
7
|
+
import { cn } from "../../lib/cn";
|
|
8
|
+
|
|
9
|
+
export const Table = forwardRef<HTMLTableElement, HTMLAttributes<HTMLTableElement>>(function Table(
|
|
10
|
+
{ className, ...props },
|
|
11
|
+
ref,
|
|
12
|
+
) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="relative w-full overflow-auto">
|
|
15
|
+
<table ref={ref} className={cn("w-full caption-bottom text-body", className)} {...props} />
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
export const TableHeader = forwardRef<
|
|
20
|
+
HTMLTableSectionElement,
|
|
21
|
+
HTMLAttributes<HTMLTableSectionElement>
|
|
22
|
+
>(function TableHeader({ className, ...props }, ref) {
|
|
23
|
+
// border-border-strong: the header-row bottom divider is the ONLY structural
|
|
24
|
+
// cue between the header and the first data row on a shared card/background
|
|
25
|
+
// surface (TableHeader itself carries no fill). Decision test: "If deleted,
|
|
26
|
+
// could a sighted user still tell header from body?" No → border-strong.
|
|
27
|
+
return (
|
|
28
|
+
<thead
|
|
29
|
+
ref={ref}
|
|
30
|
+
className={cn("[&_tr]:border-b [&_tr]:border-border-strong", className)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
export const TableBody = forwardRef<
|
|
36
|
+
HTMLTableSectionElement,
|
|
37
|
+
HTMLAttributes<HTMLTableSectionElement>
|
|
38
|
+
>(function TableBody({ className, ...props }, ref) {
|
|
39
|
+
return <tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />;
|
|
40
|
+
});
|
|
41
|
+
export const TableFooter = forwardRef<
|
|
42
|
+
HTMLTableSectionElement,
|
|
43
|
+
HTMLAttributes<HTMLTableSectionElement>
|
|
44
|
+
>(function TableFooter({ className, ...props }, ref) {
|
|
45
|
+
// border-t stays on plain `border`: the footer's bg-surface-muted/50 fill is
|
|
46
|
+
// a redundant cue — deleting the border still leaves a visible region change.
|
|
47
|
+
// Decision test: "If deleted, could a sighted user still tell footer from
|
|
48
|
+
// body?" Yes (fill differs) → redundant boundary → border.
|
|
49
|
+
return (
|
|
50
|
+
<tfoot
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={cn("border-t bg-surface-muted/50 font-medium", className)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
export const TableRow = forwardRef<HTMLTableRowElement, HTMLAttributes<HTMLTableRowElement>>(
|
|
58
|
+
function TableRow({ className, ...props }, ref) {
|
|
59
|
+
return (
|
|
60
|
+
<tr
|
|
61
|
+
ref={ref}
|
|
62
|
+
className={cn(
|
|
63
|
+
// border-border-strong: each row divider is the ONLY structural cue
|
|
64
|
+
// between adjacent rows on a shared card/background surface (no fill
|
|
65
|
+
// alternation on the plain Table primitive). Decision test: "If
|
|
66
|
+
// deleted, could a sighted user still tell the two rows apart?" No
|
|
67
|
+
// (no fill change) → load-bearing → border-strong.
|
|
68
|
+
"border-b border-border-strong transition-colors duration-fast ease-standard hover:bg-surface-muted/50 data-[state=selected]:bg-accent",
|
|
69
|
+
className,
|
|
70
|
+
)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
export const TableHead = forwardRef<HTMLTableCellElement, ThHTMLAttributes<HTMLTableCellElement>>(
|
|
77
|
+
function TableHead({ className, ...props }, ref) {
|
|
78
|
+
return (
|
|
79
|
+
<th
|
|
80
|
+
ref={ref}
|
|
81
|
+
className={cn(
|
|
82
|
+
"h-10 px-3 text-start align-middle font-medium text-muted-foreground",
|
|
83
|
+
className,
|
|
84
|
+
)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
export const TableCell = forwardRef<HTMLTableCellElement, TdHTMLAttributes<HTMLTableCellElement>>(
|
|
91
|
+
function TableCell({ className, ...props }, ref) {
|
|
92
|
+
return <td ref={ref} className={cn("px-3 py-2 align-middle", className)} {...props} />;
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
export const TableCaption = forwardRef<
|
|
96
|
+
HTMLTableCaptionElement,
|
|
97
|
+
HTMLAttributes<HTMLTableCaptionElement>
|
|
98
|
+
>(function TableCaption({ className, ...props }, ref) {
|
|
99
|
+
return (
|
|
100
|
+
<caption
|
|
101
|
+
ref={ref}
|
|
102
|
+
className={cn("mt-4 text-body text-muted-foreground", className)}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent } from "./tabs";
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { expect, waitFor } from "storybook/test";
|
|
4
|
+
import { Button } from "../button";
|
|
5
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "Core/Tabs",
|
|
9
|
+
component: Tabs,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
argTypes: {
|
|
12
|
+
defaultValue: {
|
|
13
|
+
description: "Uncontrolled: the tab value active on first render.",
|
|
14
|
+
control: "text",
|
|
15
|
+
table: { category: "State" },
|
|
16
|
+
},
|
|
17
|
+
value: {
|
|
18
|
+
description: "Controlled active tab value.",
|
|
19
|
+
control: "text",
|
|
20
|
+
table: { category: "State" },
|
|
21
|
+
},
|
|
22
|
+
onValueChange: {
|
|
23
|
+
description: "Callback fired when the active tab changes.",
|
|
24
|
+
control: false,
|
|
25
|
+
table: { category: "Behavior" },
|
|
26
|
+
},
|
|
27
|
+
orientation: {
|
|
28
|
+
description: "Layout orientation of the tab list.",
|
|
29
|
+
control: { type: "radio" },
|
|
30
|
+
options: ["horizontal", "vertical"],
|
|
31
|
+
table: { category: "Appearance" },
|
|
32
|
+
},
|
|
33
|
+
activationMode: {
|
|
34
|
+
description: "`automatic` activates on focus; `manual` requires an explicit click/Enter.",
|
|
35
|
+
control: { type: "radio" },
|
|
36
|
+
options: ["automatic", "manual"],
|
|
37
|
+
table: { category: "Behavior" },
|
|
38
|
+
},
|
|
39
|
+
className: {
|
|
40
|
+
description: "Extra Tailwind classes merged via cn().",
|
|
41
|
+
control: "text",
|
|
42
|
+
table: { category: "Appearance" },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
} satisfies Meta<typeof Tabs>;
|
|
46
|
+
export default meta;
|
|
47
|
+
type Story = StoryObj<typeof meta>;
|
|
48
|
+
|
|
49
|
+
export const Default: Story = {
|
|
50
|
+
render: () => (
|
|
51
|
+
<Tabs defaultValue="overview" className="w-80">
|
|
52
|
+
<TabsList>
|
|
53
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
54
|
+
<TabsTrigger value="activity">Activity</TabsTrigger>
|
|
55
|
+
<TabsTrigger value="settings">Settings</TabsTrigger>
|
|
56
|
+
</TabsList>
|
|
57
|
+
<TabsContent value="overview" className="text-body text-muted-foreground">
|
|
58
|
+
Overview panel.
|
|
59
|
+
</TabsContent>
|
|
60
|
+
<TabsContent value="activity" className="text-body text-muted-foreground">
|
|
61
|
+
Activity panel.
|
|
62
|
+
</TabsContent>
|
|
63
|
+
<TabsContent value="settings" className="text-body text-muted-foreground">
|
|
64
|
+
Settings panel.
|
|
65
|
+
</TabsContent>
|
|
66
|
+
</Tabs>
|
|
67
|
+
),
|
|
68
|
+
// Selecting a tab reveals its panel and marks the trigger aria-selected — the
|
|
69
|
+
// panel content is unmounted until its tab is active, so this proves the
|
|
70
|
+
// switch actually happened.
|
|
71
|
+
play: async ({ canvas, userEvent }) => {
|
|
72
|
+
const activity = canvas.getByRole("tab", { name: "Activity" });
|
|
73
|
+
await userEvent.click(activity);
|
|
74
|
+
await expect(activity).toHaveAttribute("aria-selected", "true");
|
|
75
|
+
// The panel mounts on switch and fades in (gated entrance) — wait for it to settle.
|
|
76
|
+
const panel = await canvas.findByText("Activity panel.");
|
|
77
|
+
await waitFor(() => expect(panel).toBeVisible());
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const OVERFLOW_TABS = Array.from({ length: 8 }, (_, i) => `tab-${i + 1}`);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* An 8-tab strip inside a 320px-wide container (phone-width) — narrower than
|
|
85
|
+
* the strip's natural width, so it must scroll rather than clip. Confirms
|
|
86
|
+
* every tab is reachable by keyboard: repeated `ArrowRight` walks focus/
|
|
87
|
+
* selection all the way to the last tab, and the newly-active tab scrolls
|
|
88
|
+
* itself into the container's visible bounds (real layout — this assertion
|
|
89
|
+
* only means anything in a real browser, which is how `test-storybook` /
|
|
90
|
+
* `run-story-tests` execute it).
|
|
91
|
+
*/
|
|
92
|
+
export const OverflowScrollable: Story = {
|
|
93
|
+
render: () => (
|
|
94
|
+
<div className="w-[320px]">
|
|
95
|
+
<Tabs defaultValue="tab-1">
|
|
96
|
+
<TabsList>
|
|
97
|
+
{OVERFLOW_TABS.map((value, i) => (
|
|
98
|
+
<TabsTrigger key={value} value={value}>
|
|
99
|
+
Tab {i + 1}
|
|
100
|
+
</TabsTrigger>
|
|
101
|
+
))}
|
|
102
|
+
</TabsList>
|
|
103
|
+
{OVERFLOW_TABS.map((value, i) => (
|
|
104
|
+
<TabsContent key={value} value={value} className="text-body text-muted-foreground">
|
|
105
|
+
Panel {i + 1}.
|
|
106
|
+
</TabsContent>
|
|
107
|
+
))}
|
|
108
|
+
</Tabs>
|
|
109
|
+
</div>
|
|
110
|
+
),
|
|
111
|
+
play: async ({ canvas, userEvent }) => {
|
|
112
|
+
const tablist = canvas.getByRole("tablist");
|
|
113
|
+
// The strip is wider than its 320px container — it must scroll, not clip.
|
|
114
|
+
await expect(tablist.scrollWidth).toBeGreaterThan(tablist.clientWidth);
|
|
115
|
+
|
|
116
|
+
const first = canvas.getByRole("tab", { name: "Tab 1" });
|
|
117
|
+
// `justify-center-safe` (not `justify-center`) — the initially-active
|
|
118
|
+
// first tab must be fully within the visible scroll area on mount, not
|
|
119
|
+
// stranded off the left edge by a naive center-alignment of the
|
|
120
|
+
// overflowing strip.
|
|
121
|
+
await waitFor(() => {
|
|
122
|
+
const firstRect = first.getBoundingClientRect();
|
|
123
|
+
const containerRect = tablist.getBoundingClientRect();
|
|
124
|
+
expect(firstRect.left).toBeGreaterThanOrEqual(containerRect.left - 1);
|
|
125
|
+
expect(firstRect.right).toBeLessThanOrEqual(containerRect.right + 1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
first.focus();
|
|
129
|
+
await expect(first).toHaveFocus();
|
|
130
|
+
|
|
131
|
+
for (let i = 0; i < OVERFLOW_TABS.length - 1; i++) {
|
|
132
|
+
await userEvent.keyboard("{ArrowRight}");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const last = canvas.getByRole("tab", { name: "Tab 8" });
|
|
136
|
+
await waitFor(() => expect(last).toHaveFocus());
|
|
137
|
+
await expect(last).toHaveAttribute("aria-selected", "true");
|
|
138
|
+
|
|
139
|
+
// Real-layout assertion: the newly-active tab scrolled itself into the
|
|
140
|
+
// scroll container's visible bounds (a ±1px tolerance for rounding).
|
|
141
|
+
await waitFor(
|
|
142
|
+
() => {
|
|
143
|
+
const triggerRect = last.getBoundingClientRect();
|
|
144
|
+
const containerRect = tablist.getBoundingClientRect();
|
|
145
|
+
expect(triggerRect.left).toBeGreaterThanOrEqual(containerRect.left - 1);
|
|
146
|
+
expect(triggerRect.right).toBeLessThanOrEqual(containerRect.right + 1);
|
|
147
|
+
},
|
|
148
|
+
{ timeout: 3000 },
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Regression guard: a strip that FITS its container renders exactly as
|
|
155
|
+
* before — `justify-center-safe` behaves identically to `justify-center`
|
|
156
|
+
* when there's nothing to overflow, so no scrollbar appears — AND mounting it
|
|
157
|
+
* moves NOTHING.
|
|
158
|
+
*
|
|
159
|
+
* The tabs sit below a tall spacer inside a scroll region, out of view. The
|
|
160
|
+
* first cut of #344 called `Element.scrollIntoView()` on mount, which walks
|
|
161
|
+
* every scrollable ancestor: it scrolled this region (and the page) to reveal
|
|
162
|
+
* the tabs nobody had asked for. Asserting "no scrollbar" alone did not catch
|
|
163
|
+
* that — asserting the scroll POSITION does.
|
|
164
|
+
*/
|
|
165
|
+
export const WideViewportFits: Story = {
|
|
166
|
+
render: () => (
|
|
167
|
+
<div data-testid="scroll-region" className="h-[220px] w-[640px] overflow-y-auto">
|
|
168
|
+
<p className="h-[600px] text-body text-muted-foreground">
|
|
169
|
+
This scroll region starts at the top, and must stay there. The tabs below are deliberately
|
|
170
|
+
out of view on mount.
|
|
171
|
+
</p>
|
|
172
|
+
<Tabs defaultValue="overview">
|
|
173
|
+
<TabsList>
|
|
174
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
175
|
+
<TabsTrigger value="activity">Activity</TabsTrigger>
|
|
176
|
+
<TabsTrigger value="settings">Settings</TabsTrigger>
|
|
177
|
+
</TabsList>
|
|
178
|
+
<TabsContent value="overview" className="text-body text-muted-foreground">
|
|
179
|
+
Overview panel.
|
|
180
|
+
</TabsContent>
|
|
181
|
+
<TabsContent value="activity" className="text-body text-muted-foreground">
|
|
182
|
+
Activity panel.
|
|
183
|
+
</TabsContent>
|
|
184
|
+
<TabsContent value="settings" className="text-body text-muted-foreground">
|
|
185
|
+
Settings panel.
|
|
186
|
+
</TabsContent>
|
|
187
|
+
</Tabs>
|
|
188
|
+
</div>
|
|
189
|
+
),
|
|
190
|
+
play: async ({ canvas }) => {
|
|
191
|
+
const tablist = canvas.getByRole("tablist");
|
|
192
|
+
await waitFor(() => expect(tablist.scrollWidth).toBeLessThanOrEqual(tablist.clientWidth + 1));
|
|
193
|
+
|
|
194
|
+
// A smooth scroll is asynchronous — wait long enough for a stray one to
|
|
195
|
+
// land before asserting it never happened.
|
|
196
|
+
const region = canvas.getByTestId("scroll-region");
|
|
197
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
198
|
+
await expect(region.scrollTop).toBe(0);
|
|
199
|
+
await expect(window.scrollY).toBe(0);
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The scroll-on-activation behaviour proved WITHOUT the browser's own
|
|
205
|
+
* focus-scrolling doing the work: a button beside the strip sets the value
|
|
206
|
+
* programmatically, so the newly-active tab is never focused and nothing but
|
|
207
|
+
* `TabsTrigger`'s own effect can bring it into view.
|
|
208
|
+
*
|
|
209
|
+
* The strip starts at scroll 0 (mount is not an activation), then moves — and
|
|
210
|
+
* only the strip moves.
|
|
211
|
+
*/
|
|
212
|
+
export const ProgrammaticActivation: Story = {
|
|
213
|
+
render: function ProgrammaticActivationStory() {
|
|
214
|
+
const [value, setValue] = useState("tab-1");
|
|
215
|
+
return (
|
|
216
|
+
<div className="w-[320px]">
|
|
217
|
+
<Button
|
|
218
|
+
type="button"
|
|
219
|
+
size="sm"
|
|
220
|
+
variant="secondary"
|
|
221
|
+
className="mb-2"
|
|
222
|
+
onClick={() => setValue("tab-8")}
|
|
223
|
+
>
|
|
224
|
+
Jump to Tab 8
|
|
225
|
+
</Button>
|
|
226
|
+
<Tabs value={value} onValueChange={setValue}>
|
|
227
|
+
<TabsList>
|
|
228
|
+
{OVERFLOW_TABS.map((tab, i) => (
|
|
229
|
+
<TabsTrigger key={tab} value={tab}>
|
|
230
|
+
Tab {i + 1}
|
|
231
|
+
</TabsTrigger>
|
|
232
|
+
))}
|
|
233
|
+
</TabsList>
|
|
234
|
+
{OVERFLOW_TABS.map((tab, i) => (
|
|
235
|
+
<TabsContent key={tab} value={tab} className="text-body text-muted-foreground">
|
|
236
|
+
Panel {i + 1}.
|
|
237
|
+
</TabsContent>
|
|
238
|
+
))}
|
|
239
|
+
</Tabs>
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
},
|
|
243
|
+
play: async ({ canvas, userEvent }) => {
|
|
244
|
+
const tablist = canvas.getByRole("tablist");
|
|
245
|
+
// Mount is a state, not an activation: the strip never scrolled itself.
|
|
246
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
247
|
+
await expect(tablist.scrollLeft).toBe(0);
|
|
248
|
+
|
|
249
|
+
await userEvent.click(canvas.getByRole("button", { name: "Jump to Tab 8" }));
|
|
250
|
+
|
|
251
|
+
const last = canvas.getByRole("tab", { name: "Tab 8" });
|
|
252
|
+
await expect(last).toHaveAttribute("aria-selected", "true");
|
|
253
|
+
// The activated tab scrolled itself into the STRIP's visible bounds…
|
|
254
|
+
await waitFor(
|
|
255
|
+
() => {
|
|
256
|
+
const triggerRect = last.getBoundingClientRect();
|
|
257
|
+
const stripRect = tablist.getBoundingClientRect();
|
|
258
|
+
expect(triggerRect.left).toBeGreaterThanOrEqual(stripRect.left - 1);
|
|
259
|
+
expect(triggerRect.right).toBeLessThanOrEqual(stripRect.right + 1);
|
|
260
|
+
},
|
|
261
|
+
{ timeout: 3000 },
|
|
262
|
+
);
|
|
263
|
+
await expect(tablist.scrollLeft).toBeGreaterThan(0);
|
|
264
|
+
// …and only the strip: the page itself never moved.
|
|
265
|
+
await expect(window.scrollY).toBe(0);
|
|
266
|
+
},
|
|
267
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
|
|
5
|
+
|
|
6
|
+
describe("Tabs", () => {
|
|
7
|
+
// jsdom implements neither `Element.prototype.scrollIntoView` nor a real
|
|
8
|
+
// layout, so a component that calls a scroll API unconditionally CRASHES
|
|
9
|
+
// every jsdom consumer of @elabs-ai/components-ui (it did: 8/8
|
|
10
|
+
// of @elabs-ai/components-editor's CodeWorkspace tests died on
|
|
11
|
+
// `node.scrollIntoView is not a function`). There is deliberately no stub in
|
|
12
|
+
// `vitest.setup.ts` — this renders on bare jsdom, which is the assertion.
|
|
13
|
+
it("mounts under jsdom without calling any scroll API", async () => {
|
|
14
|
+
const scrollTo = vi.fn();
|
|
15
|
+
const originalScrollTo = Element.prototype.scrollTo;
|
|
16
|
+
Element.prototype.scrollTo = scrollTo;
|
|
17
|
+
try {
|
|
18
|
+
// No stub in vitest.setup.ts — the guard has to live in the component.
|
|
19
|
+
expect(Element.prototype.scrollIntoView).toBeUndefined();
|
|
20
|
+
render(
|
|
21
|
+
<Tabs defaultValue="a">
|
|
22
|
+
<TabsList>
|
|
23
|
+
<TabsTrigger value="a">A</TabsTrigger>
|
|
24
|
+
<TabsTrigger value="b">B</TabsTrigger>
|
|
25
|
+
</TabsList>
|
|
26
|
+
<TabsContent value="a">Panel A</TabsContent>
|
|
27
|
+
</Tabs>,
|
|
28
|
+
);
|
|
29
|
+
// Mount is a state, not an activation transition: nothing scrolls. (The
|
|
30
|
+
// observers are asynchronous — let a macrotask pass before asserting.)
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
32
|
+
expect(scrollTo).not.toHaveBeenCalled();
|
|
33
|
+
} finally {
|
|
34
|
+
Element.prototype.scrollTo = originalScrollTo;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// The activation path itself is layout-dependent (jsdom reports every
|
|
39
|
+
// scrollWidth/clientWidth as 0, so the overflow gate always short-circuits);
|
|
40
|
+
// it is measured for real in the `ProgrammaticActivation` /
|
|
41
|
+
// `OverflowScrollable` Storybook interaction tests.
|
|
42
|
+
it("does not throw when a tab is activated under jsdom", async () => {
|
|
43
|
+
const user = userEvent.setup();
|
|
44
|
+
render(
|
|
45
|
+
<Tabs defaultValue="a">
|
|
46
|
+
<TabsList>
|
|
47
|
+
<TabsTrigger value="a">A</TabsTrigger>
|
|
48
|
+
<TabsTrigger value="b">B</TabsTrigger>
|
|
49
|
+
</TabsList>
|
|
50
|
+
<TabsContent value="a">Panel A</TabsContent>
|
|
51
|
+
<TabsContent value="b">Panel B</TabsContent>
|
|
52
|
+
</Tabs>,
|
|
53
|
+
);
|
|
54
|
+
await user.click(screen.getByRole("tab", { name: "B" }));
|
|
55
|
+
expect(await screen.findByText("Panel B")).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("switches the active panel", () => {
|
|
59
|
+
render(
|
|
60
|
+
<Tabs defaultValue="a">
|
|
61
|
+
<TabsList>
|
|
62
|
+
<TabsTrigger value="a">A</TabsTrigger>
|
|
63
|
+
<TabsTrigger value="b">B</TabsTrigger>
|
|
64
|
+
</TabsList>
|
|
65
|
+
<TabsContent value="a">Panel A</TabsContent>
|
|
66
|
+
<TabsContent value="b">Panel B</TabsContent>
|
|
67
|
+
</Tabs>,
|
|
68
|
+
);
|
|
69
|
+
expect(screen.getByText("Panel A")).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Cheap class-level regression guard for #344: TabsList must be a scroll
|
|
73
|
+
// container (`overflow-x-auto`, bounded by `max-w-full`) with the
|
|
74
|
+
// overflow-safe centering utility, not the plain `justify-center` that
|
|
75
|
+
// strands the first tab off-screen once the strip overflows. The real
|
|
76
|
+
// scroll-into-view/320px-container behavior is layout-dependent and is
|
|
77
|
+
// covered by the `OverflowScrollable` Storybook interaction test instead
|
|
78
|
+
// (jsdom doesn't lay out real scroll geometry).
|
|
79
|
+
it("renders TabsList as a bounded scroll container with safe centering", () => {
|
|
80
|
+
render(
|
|
81
|
+
<Tabs defaultValue="a">
|
|
82
|
+
<TabsList data-testid="list">
|
|
83
|
+
<TabsTrigger value="a">A</TabsTrigger>
|
|
84
|
+
</TabsList>
|
|
85
|
+
<TabsContent value="a">Panel A</TabsContent>
|
|
86
|
+
</Tabs>,
|
|
87
|
+
);
|
|
88
|
+
const list = screen.getByTestId("list");
|
|
89
|
+
expect(list.className).toContain("overflow-x-auto");
|
|
90
|
+
expect(list.className).toContain("max-w-full");
|
|
91
|
+
expect(list.className).toContain("justify-center-safe");
|
|
92
|
+
expect(list.className).not.toMatch(/(?<!-)justify-center(?!-safe)/);
|
|
93
|
+
});
|
|
94
|
+
});
|