@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,106 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, waitFor, within } from "storybook/test";
|
|
3
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip";
|
|
4
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
|
|
5
|
+
const meta = { title: "Forms/Select", component: Select, tags: ["autodocs"] } satisfies Meta<
|
|
6
|
+
typeof Select
|
|
7
|
+
>;
|
|
8
|
+
export default meta;
|
|
9
|
+
type Story = StoryObj<typeof meta>;
|
|
10
|
+
export const Default: Story = {
|
|
11
|
+
render: () => (
|
|
12
|
+
<Select defaultValue="prod">
|
|
13
|
+
<SelectTrigger className="w-56">
|
|
14
|
+
<SelectValue placeholder="Environment" />
|
|
15
|
+
</SelectTrigger>
|
|
16
|
+
<SelectContent>
|
|
17
|
+
<SelectItem value="prod">Production</SelectItem>
|
|
18
|
+
<SelectItem value="staging">Staging</SelectItem>
|
|
19
|
+
<SelectItem value="dev">Dev</SelectItem>
|
|
20
|
+
</SelectContent>
|
|
21
|
+
</Select>
|
|
22
|
+
),
|
|
23
|
+
// Opens the listbox (Radix portal), picks a different option, and confirms the
|
|
24
|
+
// trigger now reflects the new selection.
|
|
25
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
26
|
+
const trigger = canvas.getByRole("combobox");
|
|
27
|
+
await expect(trigger).toHaveTextContent("Production");
|
|
28
|
+
await userEvent.click(trigger);
|
|
29
|
+
const body = within(canvasElement.ownerDocument.body);
|
|
30
|
+
await userEvent.click(await body.findByRole("option", { name: "Staging" }));
|
|
31
|
+
await expect(trigger).toHaveTextContent("Staging");
|
|
32
|
+
// A value that FITS gains no native `title` (#332): an unconditional one
|
|
33
|
+
// would silently become this combobox's accessible name — a field named
|
|
34
|
+
// after its own value is not a named field.
|
|
35
|
+
await expect(trigger).not.toHaveAttribute("title");
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const LONG_LABEL = "prod-eu-west-1 · 2026-08-01 · 12 tools";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A long, composed value clipped by `line-clamp-1`, recovered on mouse hover
|
|
43
|
+
* by the automatic native `title` — the zero-extra-code path. The trigger
|
|
44
|
+
* measures its own content, so the `title` appears here and NOT on a short
|
|
45
|
+
* value (see `Default`, which has none).
|
|
46
|
+
*
|
|
47
|
+
* `aria-label` is on the trigger because the value is not a name: the `title`
|
|
48
|
+
* is a hover affordance, never the field's label.
|
|
49
|
+
*/
|
|
50
|
+
export const ClippedValueGetsTitle: Story = {
|
|
51
|
+
render: () => (
|
|
52
|
+
<Select defaultValue={LONG_LABEL}>
|
|
53
|
+
<SelectTrigger className="w-56" aria-label="Environment">
|
|
54
|
+
<SelectValue />
|
|
55
|
+
</SelectTrigger>
|
|
56
|
+
<SelectContent>
|
|
57
|
+
<SelectItem value={LONG_LABEL}>{LONG_LABEL}</SelectItem>
|
|
58
|
+
<SelectItem value="dev">Dev</SelectItem>
|
|
59
|
+
</SelectContent>
|
|
60
|
+
</Select>
|
|
61
|
+
),
|
|
62
|
+
play: async ({ canvas }) => {
|
|
63
|
+
const trigger = canvas.getByRole("combobox");
|
|
64
|
+
await waitFor(() => expect(trigger).toHaveAttribute("title", LONG_LABEL));
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The keyboard-reachable recovery: a `Tooltip` composition, which Radix opens
|
|
70
|
+
* on focus as well as hover. `autoTitle={false}` is REQUIRED here — otherwise
|
|
71
|
+
* the native `title` and the Radix tooltip both appear on hover, one on top of
|
|
72
|
+
* the other.
|
|
73
|
+
*/
|
|
74
|
+
export const LongComposedLabel: Story = {
|
|
75
|
+
render: () => (
|
|
76
|
+
<TooltipProvider delayDuration={0}>
|
|
77
|
+
<Select defaultValue={LONG_LABEL}>
|
|
78
|
+
<Tooltip>
|
|
79
|
+
<TooltipTrigger asChild>
|
|
80
|
+
<SelectTrigger className="w-56" aria-label="Environment" autoTitle={false}>
|
|
81
|
+
<SelectValue />
|
|
82
|
+
</SelectTrigger>
|
|
83
|
+
</TooltipTrigger>
|
|
84
|
+
<TooltipContent>{LONG_LABEL}</TooltipContent>
|
|
85
|
+
</Tooltip>
|
|
86
|
+
<SelectContent>
|
|
87
|
+
<SelectItem value={LONG_LABEL}>{LONG_LABEL}</SelectItem>
|
|
88
|
+
<SelectItem value="staging">Staging</SelectItem>
|
|
89
|
+
</SelectContent>
|
|
90
|
+
</Select>
|
|
91
|
+
</TooltipProvider>
|
|
92
|
+
),
|
|
93
|
+
// Focus the trigger directly (the keyboard path, not a click) and confirm the
|
|
94
|
+
// tooltip content becomes visible — and that no competing native `title` was
|
|
95
|
+
// left behind.
|
|
96
|
+
play: async ({ canvas, canvasElement }) => {
|
|
97
|
+
const trigger = canvas.getByRole("combobox");
|
|
98
|
+
await expect(trigger).not.toHaveAttribute("title");
|
|
99
|
+
|
|
100
|
+
trigger.focus();
|
|
101
|
+
await expect(trigger).toHaveFocus();
|
|
102
|
+
const body = within(canvasElement.ownerDocument.body);
|
|
103
|
+
const content = await body.findByText(LONG_LABEL, { selector: "[data-side]" });
|
|
104
|
+
await waitFor(() => expect(content).toBeVisible());
|
|
105
|
+
},
|
|
106
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import { Select, SelectTrigger, SelectValue } from "./select";
|
|
4
|
+
|
|
5
|
+
const LONG_LABEL = "prod-eu-west-1 · 2026-08-01 · 12 tools";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* jsdom has no layout: every element reports `scrollWidth`/`clientWidth` 0, so
|
|
9
|
+
* nothing is ever "clipped" there. Fake the one measurement the component
|
|
10
|
+
* makes so the clipped branch is exercisable in a unit test; the real-layout
|
|
11
|
+
* behaviour is covered by the `ClippedValueGetsTitle` Storybook story.
|
|
12
|
+
*/
|
|
13
|
+
function fakeClippedLayout() {
|
|
14
|
+
Object.defineProperty(HTMLElement.prototype, "scrollWidth", {
|
|
15
|
+
configurable: true,
|
|
16
|
+
get: () => 400,
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(HTMLElement.prototype, "clientWidth", {
|
|
19
|
+
configurable: true,
|
|
20
|
+
get: () => 100,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
// Restore jsdom's own getters (they live on the Element.prototype below).
|
|
26
|
+
Reflect.deleteProperty(HTMLElement.prototype, "scrollWidth");
|
|
27
|
+
Reflect.deleteProperty(HTMLElement.prototype, "clientWidth");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("SelectTrigger", () => {
|
|
31
|
+
it("sets no title while the rendered text fits (an unconditional title names the field after its own value)", async () => {
|
|
32
|
+
render(
|
|
33
|
+
<Select>
|
|
34
|
+
<SelectTrigger aria-label="Environment">
|
|
35
|
+
<SelectValue placeholder={LONG_LABEL} />
|
|
36
|
+
</SelectTrigger>
|
|
37
|
+
</Select>,
|
|
38
|
+
);
|
|
39
|
+
const trigger = screen.getByRole("combobox");
|
|
40
|
+
// Let the mount effect and its observers run before asserting the absence.
|
|
41
|
+
await waitFor(() => expect(trigger).toHaveAttribute("data-size", "default"));
|
|
42
|
+
expect(trigger).not.toHaveAttribute("title");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("auto-sets title from the rendered text once that text is clipped", async () => {
|
|
46
|
+
fakeClippedLayout();
|
|
47
|
+
render(
|
|
48
|
+
<Select>
|
|
49
|
+
<SelectTrigger aria-label="Environment">
|
|
50
|
+
<SelectValue placeholder={LONG_LABEL} />
|
|
51
|
+
</SelectTrigger>
|
|
52
|
+
</Select>,
|
|
53
|
+
);
|
|
54
|
+
await waitFor(() => expect(screen.getByRole("combobox")).toHaveAttribute("title", LONG_LABEL));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("separates composed trigger content instead of concatenating it", async () => {
|
|
58
|
+
fakeClippedLayout();
|
|
59
|
+
render(
|
|
60
|
+
<Select>
|
|
61
|
+
<SelectTrigger aria-label="Environment">
|
|
62
|
+
<span>Env:</span>
|
|
63
|
+
<SelectValue placeholder="Staging" />
|
|
64
|
+
</SelectTrigger>
|
|
65
|
+
</Select>,
|
|
66
|
+
);
|
|
67
|
+
await waitFor(() =>
|
|
68
|
+
expect(screen.getByRole("combobox")).toHaveAttribute("title", "Env: Staging"),
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("never overrides a caller-supplied title", async () => {
|
|
73
|
+
fakeClippedLayout();
|
|
74
|
+
render(
|
|
75
|
+
<Select>
|
|
76
|
+
<SelectTrigger title="Custom title" aria-label="Environment">
|
|
77
|
+
<SelectValue placeholder="Environment" />
|
|
78
|
+
</SelectTrigger>
|
|
79
|
+
</Select>,
|
|
80
|
+
);
|
|
81
|
+
const trigger = screen.getByRole("combobox");
|
|
82
|
+
await waitFor(() => expect(trigger).toHaveAttribute("title", "Custom title"));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("sets no title when autoTitle is disabled, even while clipped", async () => {
|
|
86
|
+
fakeClippedLayout();
|
|
87
|
+
render(
|
|
88
|
+
<Select>
|
|
89
|
+
<SelectTrigger autoTitle={false} aria-label="Environment">
|
|
90
|
+
<SelectValue placeholder={LONG_LABEL} />
|
|
91
|
+
</SelectTrigger>
|
|
92
|
+
</Select>,
|
|
93
|
+
);
|
|
94
|
+
const trigger = screen.getByRole("combobox");
|
|
95
|
+
await waitFor(() => expect(trigger).toHaveAttribute("data-size", "default"));
|
|
96
|
+
expect(trigger).not.toHaveAttribute("title");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
type ComponentPropsWithoutRef,
|
|
7
|
+
type ElementRef,
|
|
8
|
+
} from "react";
|
|
9
|
+
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
10
|
+
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
|
11
|
+
import { cn } from "../../lib/cn";
|
|
12
|
+
import { mergeRefs } from "../../lib/merge-refs";
|
|
13
|
+
|
|
14
|
+
export const Select = SelectPrimitive.Root;
|
|
15
|
+
export const SelectGroup = SelectPrimitive.Group;
|
|
16
|
+
export const SelectValue = SelectPrimitive.Value;
|
|
17
|
+
|
|
18
|
+
/** Sub-pixel layout rounding — an element is only "clipped" beyond this. */
|
|
19
|
+
const CLIP_SLACK_PX = 1;
|
|
20
|
+
|
|
21
|
+
function isClipped(el: Element) {
|
|
22
|
+
return (
|
|
23
|
+
el.scrollWidth > el.clientWidth + CLIP_SLACK_PX ||
|
|
24
|
+
el.scrollHeight > el.clientHeight + CLIP_SLACK_PX
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The trigger's visible text, ONE SEGMENT PER CHILD NODE joined by a space, so
|
|
30
|
+
* a prefix/badge beside `SelectValue` reads `"Env: Staging"` — a bare
|
|
31
|
+
* `textContent` read concatenates the DOM's text nodes with no separator and
|
|
32
|
+
* produces `"Env:Staging"`.
|
|
33
|
+
*/
|
|
34
|
+
function composeTriggerText(node: HTMLElement) {
|
|
35
|
+
return Array.from(node.childNodes)
|
|
36
|
+
.map((child) => child.textContent?.replace(/\s+/gu, " ").trim() ?? "")
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.join(" ");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type SelectTriggerProps = ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
|
42
|
+
size?: "sm" | "default";
|
|
43
|
+
/**
|
|
44
|
+
* Auto-set a native `title` from the trigger's rendered text — but ONLY
|
|
45
|
+
* while that text is actually clipped (default `true`).
|
|
46
|
+
*
|
|
47
|
+
* Set `false` when the trigger is a `TooltipTrigger`: the native tooltip and
|
|
48
|
+
* the Radix one would otherwise both appear on hover. A caller-supplied
|
|
49
|
+
* `title` also disables it (an explicit `title` is never overridden).
|
|
50
|
+
*/
|
|
51
|
+
autoTitle?: boolean;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* `[&>span]:line-clamp-1` clips the rendered `SelectValue` — necessary so a
|
|
56
|
+
* long/composed label doesn't blow out the trigger's width. To keep clipped
|
|
57
|
+
* text recoverable, the trigger MEASURES its content and sets a native `title`
|
|
58
|
+
* only while something is genuinely cut off, so it's readable on mouse hover
|
|
59
|
+
* with zero extra code.
|
|
60
|
+
*
|
|
61
|
+
* The measurement is not cosmetic: an unconditional `title` becomes the
|
|
62
|
+
* button's accessible name, which would paper over a real "this combobox has
|
|
63
|
+
* no name" a11y failure with the field's own VALUE — a value is not a name.
|
|
64
|
+
* Label the trigger properly (`aria-label`/`aria-labelledby`) regardless.
|
|
65
|
+
*
|
|
66
|
+
* `title` also never reaches keyboard users — for that, wrap the trigger in
|
|
67
|
+
* `Tooltip`/`TooltipTrigger asChild`/`TooltipContent` and pass
|
|
68
|
+
* `autoTitle={false}` (see the `LongComposedLabel` story), which Radix opens
|
|
69
|
+
* on keyboard focus too.
|
|
70
|
+
*/
|
|
71
|
+
export const SelectTrigger = forwardRef<
|
|
72
|
+
ElementRef<typeof SelectPrimitive.Trigger>,
|
|
73
|
+
SelectTriggerProps
|
|
74
|
+
>(function SelectTrigger(
|
|
75
|
+
{ className, children, size = "default", title, autoTitle = true, ...props },
|
|
76
|
+
ref,
|
|
77
|
+
) {
|
|
78
|
+
const innerRef = useRef<HTMLButtonElement>(null);
|
|
79
|
+
const mergedRef = useMemo(() => mergeRefs(ref, innerRef), [ref]);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!autoTitle) return;
|
|
83
|
+
if (title !== undefined) return; // caller owns `title` — never override it
|
|
84
|
+
const node = innerRef.current;
|
|
85
|
+
if (!node) return;
|
|
86
|
+
|
|
87
|
+
const sync = () => {
|
|
88
|
+
// The clamp lives on the child spans, but a long unbroken value can also
|
|
89
|
+
// overflow the trigger box itself — check both.
|
|
90
|
+
const clipped = isClipped(node) || Array.from(node.children).some(isClipped);
|
|
91
|
+
const text = clipped ? composeTriggerText(node) : "";
|
|
92
|
+
if (text) node.title = text;
|
|
93
|
+
else node.removeAttribute("title");
|
|
94
|
+
};
|
|
95
|
+
sync();
|
|
96
|
+
|
|
97
|
+
// The selected item's text isn't a React child of this component — Radix
|
|
98
|
+
// portals it into the (child) `SelectValue` node from `SelectContent`
|
|
99
|
+
// asynchronously, on its OWN commit, so a one-shot read here can run
|
|
100
|
+
// before that text lands. Observe the subtree instead of trusting this
|
|
101
|
+
// component's own render cycle. (Attributes are deliberately NOT observed:
|
|
102
|
+
// `sync` writes `title`, which would otherwise re-enter.)
|
|
103
|
+
const observer = new MutationObserver(sync);
|
|
104
|
+
observer.observe(node, { childList: true, subtree: true, characterData: true });
|
|
105
|
+
|
|
106
|
+
// Clipping is a function of WIDTH, so it has to be re-measured when the
|
|
107
|
+
// trigger resizes, not only when its text changes.
|
|
108
|
+
const resizeObserver =
|
|
109
|
+
typeof ResizeObserver === "function" ? new ResizeObserver(sync) : undefined;
|
|
110
|
+
resizeObserver?.observe(node);
|
|
111
|
+
|
|
112
|
+
return () => {
|
|
113
|
+
observer.disconnect();
|
|
114
|
+
resizeObserver?.disconnect();
|
|
115
|
+
};
|
|
116
|
+
}, [autoTitle, title]);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<SelectPrimitive.Trigger
|
|
120
|
+
ref={mergedRef}
|
|
121
|
+
data-size={size}
|
|
122
|
+
title={title}
|
|
123
|
+
className={cn(
|
|
124
|
+
"flex w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm",
|
|
125
|
+
size === "sm" ? "h-8" : "h-9",
|
|
126
|
+
"placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1 focus:ring-offset-background",
|
|
127
|
+
// No `disabled:cursor-*` override — disabled controls keep the arrow
|
|
128
|
+
// automatically (interaction-guidelines.md), matching Combobox's
|
|
129
|
+
// Button-based trigger (`disabled:pointer-events-none`) (#343).
|
|
130
|
+
"disabled:opacity-50 disabled:border-border [&>span]:line-clamp-1",
|
|
131
|
+
className,
|
|
132
|
+
)}
|
|
133
|
+
{...props}
|
|
134
|
+
>
|
|
135
|
+
{children}
|
|
136
|
+
<SelectPrimitive.Icon asChild>
|
|
137
|
+
<ChevronDown className="size-4 opacity-50" />
|
|
138
|
+
</SelectPrimitive.Icon>
|
|
139
|
+
</SelectPrimitive.Trigger>
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
export const SelectContent = forwardRef<
|
|
144
|
+
ElementRef<typeof SelectPrimitive.Content>,
|
|
145
|
+
ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
146
|
+
>(function SelectContent({ className, children, position = "popper", ...props }, ref) {
|
|
147
|
+
return (
|
|
148
|
+
<SelectPrimitive.Portal>
|
|
149
|
+
<SelectPrimitive.Content
|
|
150
|
+
ref={ref}
|
|
151
|
+
position={position}
|
|
152
|
+
className={cn(
|
|
153
|
+
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md bg-popover text-popover-foreground shadow-ring-md",
|
|
154
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
|
|
155
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
|
|
156
|
+
position === "popper" &&
|
|
157
|
+
"data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1",
|
|
158
|
+
className,
|
|
159
|
+
)}
|
|
160
|
+
{...props}
|
|
161
|
+
>
|
|
162
|
+
<SelectPrimitive.ScrollUpButton className="flex cursor-default items-center justify-center py-1">
|
|
163
|
+
<ChevronUp className="size-4" />
|
|
164
|
+
</SelectPrimitive.ScrollUpButton>
|
|
165
|
+
<SelectPrimitive.Viewport
|
|
166
|
+
className={cn(
|
|
167
|
+
"p-1",
|
|
168
|
+
position === "popper" &&
|
|
169
|
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
|
170
|
+
)}
|
|
171
|
+
>
|
|
172
|
+
{children}
|
|
173
|
+
</SelectPrimitive.Viewport>
|
|
174
|
+
<SelectPrimitive.ScrollDownButton className="flex cursor-default items-center justify-center py-1">
|
|
175
|
+
<ChevronDown className="size-4" />
|
|
176
|
+
</SelectPrimitive.ScrollDownButton>
|
|
177
|
+
</SelectPrimitive.Content>
|
|
178
|
+
</SelectPrimitive.Portal>
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
export const SelectLabel = forwardRef<
|
|
183
|
+
ElementRef<typeof SelectPrimitive.Label>,
|
|
184
|
+
ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
|
185
|
+
>(function SelectLabel({ className, ...props }, ref) {
|
|
186
|
+
return (
|
|
187
|
+
<SelectPrimitive.Label
|
|
188
|
+
ref={ref}
|
|
189
|
+
className={cn("px-2 py-1.5 text-xs font-semibold text-muted-foreground", className)}
|
|
190
|
+
{...props}
|
|
191
|
+
/>
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
export const SelectItem = forwardRef<
|
|
196
|
+
ElementRef<typeof SelectPrimitive.Item>,
|
|
197
|
+
ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
|
198
|
+
>(function SelectItem({ className, children, ...props }, ref) {
|
|
199
|
+
return (
|
|
200
|
+
<SelectPrimitive.Item
|
|
201
|
+
ref={ref}
|
|
202
|
+
className={cn(
|
|
203
|
+
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 ps-2 pe-8 text-sm outline-none transition-colors duration-fast",
|
|
204
|
+
"focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
205
|
+
className,
|
|
206
|
+
)}
|
|
207
|
+
{...props}
|
|
208
|
+
>
|
|
209
|
+
<span className="absolute end-2 flex size-3.5 items-center justify-center">
|
|
210
|
+
<SelectPrimitive.ItemIndicator>
|
|
211
|
+
<Check className="size-4" />
|
|
212
|
+
</SelectPrimitive.ItemIndicator>
|
|
213
|
+
</span>
|
|
214
|
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
215
|
+
</SelectPrimitive.Item>
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
export const SelectSeparator = forwardRef<
|
|
220
|
+
ElementRef<typeof SelectPrimitive.Separator>,
|
|
221
|
+
ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
|
222
|
+
>(function SelectSeparator({ className, ...props }, ref) {
|
|
223
|
+
return (
|
|
224
|
+
<SelectPrimitive.Separator
|
|
225
|
+
ref={ref}
|
|
226
|
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
227
|
+
{...props}
|
|
228
|
+
/>
|
|
229
|
+
);
|
|
230
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { Separator } from "./separator";
|
|
3
|
+
const meta = {
|
|
4
|
+
title: "Display/Separator",
|
|
5
|
+
component: Separator,
|
|
6
|
+
tags: ["autodocs"],
|
|
7
|
+
argTypes: {
|
|
8
|
+
orientation: {
|
|
9
|
+
description: "Direction of the line.",
|
|
10
|
+
control: { type: "radio" },
|
|
11
|
+
options: ["horizontal", "vertical"],
|
|
12
|
+
table: { category: "Appearance" },
|
|
13
|
+
},
|
|
14
|
+
tone: {
|
|
15
|
+
description:
|
|
16
|
+
"`subtle` uses `--border` (decorative/redundant); `strong` uses `--border-strong` (≥3:1, sole structural cue).",
|
|
17
|
+
control: { type: "radio" },
|
|
18
|
+
options: ["subtle", "strong"],
|
|
19
|
+
table: { category: "Appearance" },
|
|
20
|
+
},
|
|
21
|
+
decorative: {
|
|
22
|
+
description: "When true the separator is hidden from the accessibility tree (role=none).",
|
|
23
|
+
control: "boolean",
|
|
24
|
+
table: { category: "Behavior" },
|
|
25
|
+
},
|
|
26
|
+
className: {
|
|
27
|
+
description: "Extra Tailwind classes merged via cn().",
|
|
28
|
+
control: "text",
|
|
29
|
+
table: { category: "Appearance" },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
} satisfies Meta<typeof Separator>;
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<typeof meta>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Default (subtle) — uses `--border`. The boundary is a decorative/redundant
|
|
38
|
+
* cue (surrounding fill, elevation, or spacing already signals the edge).
|
|
39
|
+
* WCAG 1.4.11 redundant-boundary exemption applies.
|
|
40
|
+
*/
|
|
41
|
+
export const Default: Story = {
|
|
42
|
+
render: () => (
|
|
43
|
+
<div className="w-64 text-body">
|
|
44
|
+
<p>Above</p>
|
|
45
|
+
<Separator className="my-3" />
|
|
46
|
+
<p>Below</p>
|
|
47
|
+
</div>
|
|
48
|
+
),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Strong — uses `--border-strong` (≥3:1 vs `--card`/`--background`). Use when
|
|
53
|
+
* the separator is the ONLY structural cue between two same-surface regions.
|
|
54
|
+
* Decision test: "If I deleted this line, could a sighted user still tell the
|
|
55
|
+
* two regions apart?" No → `tone="strong"`.
|
|
56
|
+
*/
|
|
57
|
+
export const Strong: Story = {
|
|
58
|
+
render: () => (
|
|
59
|
+
<div className="w-64 rounded-lg bg-card p-4 text-body">
|
|
60
|
+
<p className="font-medium">Section A</p>
|
|
61
|
+
<Separator tone="strong" className="my-3" />
|
|
62
|
+
<p className="font-medium">Section B</p>
|
|
63
|
+
</div>
|
|
64
|
+
),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Vertical orientation, default (subtle) tone. */
|
|
68
|
+
export const Vertical: Story = {
|
|
69
|
+
render: () => (
|
|
70
|
+
<div className="flex h-8 items-center gap-4 text-body">
|
|
71
|
+
<span>Left</span>
|
|
72
|
+
<Separator orientation="vertical" />
|
|
73
|
+
<span>Right</span>
|
|
74
|
+
</div>
|
|
75
|
+
),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Vertical orientation, strong tone — the only structural cue between columns. */
|
|
79
|
+
export const VerticalStrong: Story = {
|
|
80
|
+
render: () => (
|
|
81
|
+
<div className="flex h-8 items-center gap-4 text-body">
|
|
82
|
+
<span>Left</span>
|
|
83
|
+
<Separator orientation="vertical" tone="strong" />
|
|
84
|
+
<span>Right</span>
|
|
85
|
+
</div>
|
|
86
|
+
),
|
|
87
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
|
|
2
|
+
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
|
|
6
|
+
const separatorVariants = cva("shrink-0", {
|
|
7
|
+
variants: {
|
|
8
|
+
/**
|
|
9
|
+
* `subtle` (default) — uses `--border` (a decorative/redundant boundary;
|
|
10
|
+
* compliant because the edge is also signalled by surrounding fill, elevation
|
|
11
|
+
* or spacing — WCAG 1.4.11 redundant-boundary exemption).
|
|
12
|
+
*
|
|
13
|
+
* `strong` — uses `--border-strong` (≥3:1 vs `--card`/`--background`).
|
|
14
|
+
* Use when the separator is the ONLY structural cue between two same-surface
|
|
15
|
+
* regions (no fill, elevation or spacing change to fall back on).
|
|
16
|
+
* Decision test: "If I deleted this line, could a sighted user still tell
|
|
17
|
+
* the two regions apart?" No → `strong`.
|
|
18
|
+
*/
|
|
19
|
+
tone: {
|
|
20
|
+
subtle: "bg-border",
|
|
21
|
+
strong: "bg-border-strong",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
tone: "subtle",
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export type SeparatorProps = ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> &
|
|
30
|
+
VariantProps<typeof separatorVariants>;
|
|
31
|
+
|
|
32
|
+
export const Separator = forwardRef<ElementRef<typeof SeparatorPrimitive.Root>, SeparatorProps>(
|
|
33
|
+
function Separator(
|
|
34
|
+
{ className, orientation = "horizontal", decorative = true, tone, ...props },
|
|
35
|
+
ref,
|
|
36
|
+
) {
|
|
37
|
+
return (
|
|
38
|
+
<SeparatorPrimitive.Root
|
|
39
|
+
ref={ref}
|
|
40
|
+
decorative={decorative}
|
|
41
|
+
orientation={orientation}
|
|
42
|
+
className={cn(
|
|
43
|
+
separatorVariants({ tone }),
|
|
44
|
+
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
|
|
45
|
+
className,
|
|
46
|
+
)}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, waitFor, within } from "storybook/test";
|
|
3
|
+
import { Button } from "../button";
|
|
4
|
+
import {
|
|
5
|
+
Sheet,
|
|
6
|
+
SheetContent,
|
|
7
|
+
SheetDescription,
|
|
8
|
+
SheetHeader,
|
|
9
|
+
SheetTitle,
|
|
10
|
+
SheetTrigger,
|
|
11
|
+
} from "./sheet";
|
|
12
|
+
const meta = {
|
|
13
|
+
title: "Overlays/Sheet",
|
|
14
|
+
component: Sheet,
|
|
15
|
+
tags: ["autodocs"],
|
|
16
|
+
argTypes: {
|
|
17
|
+
open: {
|
|
18
|
+
description: "Controlled open state of the sheet.",
|
|
19
|
+
control: "boolean",
|
|
20
|
+
table: { category: "State" },
|
|
21
|
+
},
|
|
22
|
+
defaultOpen: {
|
|
23
|
+
description: "Uncontrolled initial open state.",
|
|
24
|
+
control: "boolean",
|
|
25
|
+
table: { category: "State" },
|
|
26
|
+
},
|
|
27
|
+
onOpenChange: {
|
|
28
|
+
description: "Callback when the open state changes.",
|
|
29
|
+
control: false,
|
|
30
|
+
table: { category: "Events" },
|
|
31
|
+
},
|
|
32
|
+
modal: {
|
|
33
|
+
description: "Whether the sheet is modal (disables outside interaction). Default true.",
|
|
34
|
+
control: "boolean",
|
|
35
|
+
table: { category: "Behaviour" },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
} satisfies Meta<typeof Sheet>;
|
|
39
|
+
export default meta;
|
|
40
|
+
type Story = StoryObj<typeof meta>;
|
|
41
|
+
export const Default: Story = {
|
|
42
|
+
render: () => (
|
|
43
|
+
<Sheet>
|
|
44
|
+
<SheetTrigger asChild>
|
|
45
|
+
<Button variant="outline">Open sheet</Button>
|
|
46
|
+
</SheetTrigger>
|
|
47
|
+
<SheetContent>
|
|
48
|
+
<SheetHeader>
|
|
49
|
+
<SheetTitle>Settings</SheetTitle>
|
|
50
|
+
<SheetDescription>Manage your workspace.</SheetDescription>
|
|
51
|
+
</SheetHeader>
|
|
52
|
+
</SheetContent>
|
|
53
|
+
</Sheet>
|
|
54
|
+
),
|
|
55
|
+
// Opens the sheet (portalled to body), confirms the title and description
|
|
56
|
+
// are visible, then presses Escape to dismiss it.
|
|
57
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
58
|
+
await userEvent.click(canvas.getByRole("button", { name: /open sheet/i }));
|
|
59
|
+
const body = within(canvasElement.ownerDocument.body);
|
|
60
|
+
const sheet = await body.findByRole("dialog");
|
|
61
|
+
await waitFor(() => expect(within(sheet).getByText("Settings")).toBeVisible());
|
|
62
|
+
await waitFor(() => expect(within(sheet).getByText("Manage your workspace.")).toBeVisible());
|
|
63
|
+
await userEvent.keyboard("{Escape}");
|
|
64
|
+
},
|
|
65
|
+
};
|