@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,165 @@
|
|
|
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 { SliderNumber } from "./slider-number";
|
|
5
|
+
|
|
6
|
+
describe("SliderNumber", () => {
|
|
7
|
+
it("renders a slider and a synced number input", () => {
|
|
8
|
+
render(<SliderNumber aria-label="Temperature" min={0} max={1} step={0.1} defaultValue={0.5} />);
|
|
9
|
+
expect(screen.getByRole("slider", { name: "Temperature" })).toHaveAttribute(
|
|
10
|
+
"aria-valuenow",
|
|
11
|
+
"0.5",
|
|
12
|
+
);
|
|
13
|
+
expect(screen.getByRole("spinbutton", { name: "Temperature" })).toHaveValue(0.5);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("rounds slider-driven and typed values identically (lockstep)", async () => {
|
|
17
|
+
const onValueChange = vi.fn();
|
|
18
|
+
render(
|
|
19
|
+
<SliderNumber
|
|
20
|
+
aria-label="Temperature"
|
|
21
|
+
min={0}
|
|
22
|
+
max={1}
|
|
23
|
+
step={0.1}
|
|
24
|
+
defaultValue={0}
|
|
25
|
+
onValueChange={onValueChange}
|
|
26
|
+
/>,
|
|
27
|
+
);
|
|
28
|
+
const thumb = screen.getByRole("slider", { name: "Temperature" });
|
|
29
|
+
thumb.focus();
|
|
30
|
+
await userEvent.keyboard("{ArrowRight}{ArrowRight}{ArrowRight}");
|
|
31
|
+
expect(onValueChange).toHaveBeenLastCalledWith(0.3);
|
|
32
|
+
|
|
33
|
+
const numberInput = screen.getByRole("spinbutton", { name: "Temperature" });
|
|
34
|
+
await userEvent.clear(numberInput);
|
|
35
|
+
await userEvent.type(numberInput, "0.3");
|
|
36
|
+
await userEvent.tab();
|
|
37
|
+
expect(onValueChange).toHaveBeenLastCalledWith(0.3);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("null is a real state distinct from 0, reachable via reset", async () => {
|
|
41
|
+
const onValueChange = vi.fn();
|
|
42
|
+
render(
|
|
43
|
+
<SliderNumber
|
|
44
|
+
aria-label="Temperature"
|
|
45
|
+
min={0}
|
|
46
|
+
max={1}
|
|
47
|
+
step={0.1}
|
|
48
|
+
defaultValue={0.6}
|
|
49
|
+
onValueChange={onValueChange}
|
|
50
|
+
/>,
|
|
51
|
+
);
|
|
52
|
+
await userEvent.click(screen.getByRole("button", { name: "Reset" }));
|
|
53
|
+
expect(onValueChange).toHaveBeenCalledWith(null);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("disables the reset control once the value is already null", () => {
|
|
57
|
+
render(<SliderNumber aria-label="Temperature" defaultValue={null} />);
|
|
58
|
+
expect(screen.getByRole("button", { name: "Reset" })).toBeDisabled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("shows the slider thumb as dimmed/inactive while value is the provider default (null)", () => {
|
|
62
|
+
render(
|
|
63
|
+
<SliderNumber aria-label="Temperature" min={0} max={1} step={0.1} defaultValue={null} />,
|
|
64
|
+
);
|
|
65
|
+
const classes = screen.getByRole("slider", { name: "Temperature" }).className.split(/\s+/);
|
|
66
|
+
// Exact token match — `disabled:opacity-50` is always present as a base
|
|
67
|
+
// (disabled-state) utility, so a substring match would false-positive.
|
|
68
|
+
expect(classes).toContain("opacity-50");
|
|
69
|
+
expect(classes).toContain("border-dashed");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("shows the slider thumb as active (not dimmed) once a real value is set", () => {
|
|
73
|
+
render(<SliderNumber aria-label="Temperature" min={0} max={1} step={0.1} defaultValue={0.4} />);
|
|
74
|
+
const classes = screen.getByRole("slider", { name: "Temperature" }).className.split(/\s+/);
|
|
75
|
+
expect(classes).not.toContain("opacity-50");
|
|
76
|
+
expect(classes).not.toContain("border-dashed");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("round-trips null -> non-null -> reset -> null", async () => {
|
|
80
|
+
const onValueChange = vi.fn();
|
|
81
|
+
render(
|
|
82
|
+
<SliderNumber
|
|
83
|
+
aria-label="Temperature"
|
|
84
|
+
min={0}
|
|
85
|
+
max={1}
|
|
86
|
+
step={0.1}
|
|
87
|
+
defaultValue={null}
|
|
88
|
+
onValueChange={onValueChange}
|
|
89
|
+
/>,
|
|
90
|
+
);
|
|
91
|
+
const thumb = screen.getByRole("slider", { name: "Temperature" });
|
|
92
|
+
thumb.focus();
|
|
93
|
+
await userEvent.keyboard("{ArrowRight}");
|
|
94
|
+
expect(onValueChange).toHaveBeenLastCalledWith(0.1);
|
|
95
|
+
|
|
96
|
+
const resetBtn = screen.getByRole("button", { name: "Reset" });
|
|
97
|
+
await userEvent.click(resetBtn);
|
|
98
|
+
expect(onValueChange).toHaveBeenLastCalledWith(null);
|
|
99
|
+
expect(resetBtn).toBeDisabled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("supports the controlled mode via value/onValueChange", async () => {
|
|
103
|
+
const onValueChange = vi.fn();
|
|
104
|
+
render(
|
|
105
|
+
<SliderNumber
|
|
106
|
+
aria-label="Temperature"
|
|
107
|
+
min={0}
|
|
108
|
+
max={1}
|
|
109
|
+
step={0.1}
|
|
110
|
+
value={0.2}
|
|
111
|
+
onValueChange={onValueChange}
|
|
112
|
+
/>,
|
|
113
|
+
);
|
|
114
|
+
const numberInput = screen.getByRole("spinbutton", { name: "Temperature" });
|
|
115
|
+
await userEvent.clear(numberInput);
|
|
116
|
+
await userEvent.type(numberInput, "0.9");
|
|
117
|
+
await userEvent.tab();
|
|
118
|
+
expect(onValueChange).toHaveBeenLastCalledWith(0.9);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it.each([
|
|
122
|
+
["rounds to the value already held", "0.44", "0.4"],
|
|
123
|
+
["rounds to a different value", "0.66", "0.7"],
|
|
124
|
+
["is clamped to max", "9", "1"],
|
|
125
|
+
])(
|
|
126
|
+
"input display never disagrees with the slider when a typed value %s",
|
|
127
|
+
async (_case, typed, expected) => {
|
|
128
|
+
render(<SliderNumber aria-label="T" min={0} max={1} step={0.1} defaultValue={0.4} />);
|
|
129
|
+
await userEvent.clear(screen.getByRole("spinbutton", { name: "T" }));
|
|
130
|
+
await userEvent.type(screen.getByRole("spinbutton", { name: "T" }), typed);
|
|
131
|
+
await userEvent.tab();
|
|
132
|
+
// Re-query: correcting the display can remount the input, so a node
|
|
133
|
+
// captured before the blur would be a detached copy.
|
|
134
|
+
const shown = (screen.getByRole("spinbutton", { name: "T" }) as HTMLInputElement).value;
|
|
135
|
+
const announced = screen.getByRole("slider", { name: "T" }).getAttribute("aria-valuenow");
|
|
136
|
+
expect(announced).toBe(expected);
|
|
137
|
+
// The "rounds to the value already held" row is the one that regressed:
|
|
138
|
+
// `NumberInput` only re-derives its text when the controlled value
|
|
139
|
+
// CHANGES, so it kept showing "0.44" beside a slider reading 0.4.
|
|
140
|
+
expect(shown).toBe(expected);
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
it("keeps its default width shrinkable and caller-overridable", () => {
|
|
145
|
+
const { container, rerender } = render(<SliderNumber aria-label="Temperature" />);
|
|
146
|
+
const root = () => container.querySelector('[data-slot="slider-number"]')!;
|
|
147
|
+
// `w-80` is a PREFERRED width — `max-w-full` stops it overflowing a
|
|
148
|
+
// narrower container instead of forcing a horizontal scrollbar.
|
|
149
|
+
expect(root()).toHaveClass("w-80", "max-w-full");
|
|
150
|
+
|
|
151
|
+
rerender(<SliderNumber aria-label="Temperature" className="w-full" />);
|
|
152
|
+
expect(root()).toHaveClass("w-full");
|
|
153
|
+
expect(root()).not.toHaveClass("w-80");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("is disabled when disabled prop is set", () => {
|
|
157
|
+
render(<SliderNumber aria-label="Temperature" defaultValue={0.5} disabled />);
|
|
158
|
+
expect(screen.getByRole("slider", { name: "Temperature" })).toHaveAttribute(
|
|
159
|
+
"data-disabled",
|
|
160
|
+
"",
|
|
161
|
+
);
|
|
162
|
+
expect(screen.getByRole("spinbutton", { name: "Temperature" })).toBeDisabled();
|
|
163
|
+
expect(screen.getByRole("button", { name: "Reset" })).toBeDisabled();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useCallback, useState, type HTMLAttributes, type ReactNode } from "react";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import { Button } from "../button";
|
|
6
|
+
import { NumberInput } from "../number-input";
|
|
7
|
+
import { Slider } from "../slider";
|
|
8
|
+
import { useLocale } from "../locale-provider";
|
|
9
|
+
|
|
10
|
+
export interface SliderNumberProps extends Omit<
|
|
11
|
+
HTMLAttributes<HTMLDivElement>,
|
|
12
|
+
"onChange" | "defaultValue"
|
|
13
|
+
> {
|
|
14
|
+
/** Controlled value. Pass `null` for "use the provider default". */
|
|
15
|
+
value?: number | null;
|
|
16
|
+
/** Uncontrolled initial value. */
|
|
17
|
+
defaultValue?: number | null;
|
|
18
|
+
/** Called when the value changes — always rounded identically on the drag and type paths. */
|
|
19
|
+
onValueChange?: (value: number | null) => void;
|
|
20
|
+
/** Minimum allowed value. Defaults to 0. */
|
|
21
|
+
min?: number;
|
|
22
|
+
/** Maximum allowed value. Defaults to 100. */
|
|
23
|
+
max?: number;
|
|
24
|
+
/** Step increment for both the slider and the number input's arrow keys. Defaults to 1. */
|
|
25
|
+
step?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Number of decimal places to round to. Defaults to the decimal places in
|
|
28
|
+
* `step` (e.g. `step={0.1}` rounds to 1 decimal). Applied identically on
|
|
29
|
+
* BOTH the slider path and the number-input path so drag-then-type never
|
|
30
|
+
* disagrees.
|
|
31
|
+
*/
|
|
32
|
+
precision?: number;
|
|
33
|
+
/** Label for the reset-to-provider-default control. Defaults to "Reset". */
|
|
34
|
+
resetLabel?: ReactNode;
|
|
35
|
+
/** Disables the whole control. */
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
id?: string;
|
|
38
|
+
name?: string;
|
|
39
|
+
"aria-label"?: string;
|
|
40
|
+
"aria-labelledby"?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getStepDecimals(step: number): number {
|
|
44
|
+
const str = step.toString();
|
|
45
|
+
const dot = str.indexOf(".");
|
|
46
|
+
return dot === -1 ? 0 : str.length - dot - 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function clampValue(value: number, min?: number, max?: number): number {
|
|
50
|
+
let result = value;
|
|
51
|
+
if (min !== undefined) result = Math.max(min, result);
|
|
52
|
+
if (max !== undefined) result = Math.min(max, result);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Rounds to a fixed decimal precision — the ONE function both paths call. */
|
|
57
|
+
function roundValue(value: number, step: number, precision?: number): number {
|
|
58
|
+
const decimals = precision ?? getStepDecimals(step);
|
|
59
|
+
const factor = 10 ** decimals;
|
|
60
|
+
return Math.round(value * factor) / factor;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* SliderNumber — a `Slider` and a `NumberInput` bound to one value, clamped
|
|
65
|
+
* and rounded in lockstep so they can never disagree, plus an explicit
|
|
66
|
+
* `null` state meaning "use the provider default" with a reset control.
|
|
67
|
+
*
|
|
68
|
+
* The subtle part (#372): both the slider's `onValueChange` and the number
|
|
69
|
+
* input's `onValueChange` are routed through the SAME `commit` function,
|
|
70
|
+
* which rounds to a fixed decimal precision before storing — so dragging to
|
|
71
|
+
* a value and then typing the same value never produces two representations
|
|
72
|
+
* of the same intent (e.g. floating-point drift like `0.30000000000000004`).
|
|
73
|
+
*/
|
|
74
|
+
export const SliderNumber = forwardRef<HTMLDivElement, SliderNumberProps>(function SliderNumber(
|
|
75
|
+
{
|
|
76
|
+
value: valueProp,
|
|
77
|
+
defaultValue,
|
|
78
|
+
onValueChange,
|
|
79
|
+
min = 0,
|
|
80
|
+
max = 100,
|
|
81
|
+
step = 1,
|
|
82
|
+
precision,
|
|
83
|
+
resetLabel,
|
|
84
|
+
disabled,
|
|
85
|
+
id,
|
|
86
|
+
name,
|
|
87
|
+
"aria-label": ariaLabel,
|
|
88
|
+
"aria-labelledby": ariaLabelledby,
|
|
89
|
+
className,
|
|
90
|
+
...props
|
|
91
|
+
},
|
|
92
|
+
ref,
|
|
93
|
+
) {
|
|
94
|
+
const { t } = useLocale();
|
|
95
|
+
const isControlled = valueProp !== undefined;
|
|
96
|
+
const [internalValue, setInternalValue] = useState<number | null>(
|
|
97
|
+
defaultValue !== undefined ? defaultValue : null,
|
|
98
|
+
);
|
|
99
|
+
const currentValue = isControlled ? valueProp : internalValue;
|
|
100
|
+
|
|
101
|
+
// Bumped whenever this component normalises a typed value back to the value
|
|
102
|
+
// it ALREADY holds. `NumberInput` only re-derives its display text when the
|
|
103
|
+
// controlled `value` prop CHANGES, so in that one case it would keep showing
|
|
104
|
+
// the raw keystrokes ("0.44") while the slider and the committed value read
|
|
105
|
+
// the rounded one (0.4) — the exact "slider and input disagree" failure this
|
|
106
|
+
// component exists to prevent. Remounting it re-derives the text from the
|
|
107
|
+
// value that was actually accepted. It only ever fires on blur, when the
|
|
108
|
+
// input is no longer focused, so the remount is invisible.
|
|
109
|
+
const [inputEpoch, setInputEpoch] = useState(0);
|
|
110
|
+
|
|
111
|
+
const commit = useCallback(
|
|
112
|
+
(next: number | null) => {
|
|
113
|
+
const rounded = next == null ? null : roundValue(clampValue(next, min, max), step, precision);
|
|
114
|
+
if (next != null && rounded !== next && rounded === currentValue) {
|
|
115
|
+
setInputEpoch((n) => n + 1);
|
|
116
|
+
}
|
|
117
|
+
if (!isControlled) setInternalValue(rounded);
|
|
118
|
+
onValueChange?.(rounded);
|
|
119
|
+
},
|
|
120
|
+
[isControlled, onValueChange, min, max, step, precision, currentValue],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const isDefault = currentValue == null;
|
|
124
|
+
const sliderValue = currentValue ?? min;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div
|
|
128
|
+
ref={ref}
|
|
129
|
+
data-slot="slider-number"
|
|
130
|
+
// A default FIXED width (matching Slider's own convention — it has no
|
|
131
|
+
// intrinsic width either, and its stories always pass one via
|
|
132
|
+
// className) so the Slider's `flex-1` has real space to grow into.
|
|
133
|
+
// `w-full`/percentage widths are unresolvable inside an ancestor with
|
|
134
|
+
// no definite width (e.g. Storybook's default "centered" layout, or a
|
|
135
|
+
// bare inline/flex context), which collapses `flex-1` to near-zero.
|
|
136
|
+
// Callers in a definite-width layout can still override via `className`
|
|
137
|
+
// (e.g. `className="w-full"` inside a form field's own fixed-width row).
|
|
138
|
+
// `max-w-full` keeps the fixed width from OVERFLOWING a container
|
|
139
|
+
// narrower than 20rem (a settings drawer, a 320px mobile column) — it is
|
|
140
|
+
// a preferred width, not a floor.
|
|
141
|
+
className={cn("flex w-80 max-w-full items-center gap-3", className)}
|
|
142
|
+
{...props}
|
|
143
|
+
>
|
|
144
|
+
<Slider
|
|
145
|
+
data-slot="slider-number-slider"
|
|
146
|
+
aria-label={ariaLabel}
|
|
147
|
+
aria-labelledby={ariaLabelledby}
|
|
148
|
+
min={min}
|
|
149
|
+
max={max}
|
|
150
|
+
step={step}
|
|
151
|
+
disabled={disabled}
|
|
152
|
+
value={[sliderValue]}
|
|
153
|
+
onValueChange={(values) => commit(values[0] ?? null)}
|
|
154
|
+
thumbProps={{
|
|
155
|
+
// Visually distinct "provider default" thumb — dimmed + dashed,
|
|
156
|
+
// never confusable with a real value (including 0).
|
|
157
|
+
className: cn(isDefault && "opacity-50 border-dashed"),
|
|
158
|
+
}}
|
|
159
|
+
className="flex-1"
|
|
160
|
+
/>
|
|
161
|
+
<NumberInput
|
|
162
|
+
key={inputEpoch}
|
|
163
|
+
data-slot="slider-number-input"
|
|
164
|
+
id={id}
|
|
165
|
+
name={name}
|
|
166
|
+
aria-label={ariaLabel}
|
|
167
|
+
aria-labelledby={ariaLabelledby}
|
|
168
|
+
min={min}
|
|
169
|
+
max={max}
|
|
170
|
+
step={step}
|
|
171
|
+
disabled={disabled}
|
|
172
|
+
value={currentValue}
|
|
173
|
+
onValueChange={commit}
|
|
174
|
+
// NumberInput's own default is w-36 (144px); shrinking it below
|
|
175
|
+
// ~w-32 leaves too little room for the digits once its own
|
|
176
|
+
// decrease/increase buttons are subtracted, clipping the text.
|
|
177
|
+
className="w-32 shrink-0"
|
|
178
|
+
/>
|
|
179
|
+
<Button
|
|
180
|
+
type="button"
|
|
181
|
+
variant="ghost"
|
|
182
|
+
size="sm"
|
|
183
|
+
data-slot="slider-number-reset"
|
|
184
|
+
disabled={disabled || isDefault}
|
|
185
|
+
onClick={() => commit(null)}
|
|
186
|
+
>
|
|
187
|
+
{resetLabel ?? t("ui.sliderNumber.reset")}
|
|
188
|
+
</Button>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Toaster, toast, type ToasterProps } from "./sonner";
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, waitFor, within } from "storybook/test";
|
|
3
|
+
import { Button } from "../button";
|
|
4
|
+
import { Toaster, toast } from "./sonner";
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Feedback/Toast",
|
|
7
|
+
component: Toaster,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
argTypes: {
|
|
10
|
+
position: {
|
|
11
|
+
description: "Where the toast stack appears on screen.",
|
|
12
|
+
control: { type: "select" },
|
|
13
|
+
options: [
|
|
14
|
+
"top-left",
|
|
15
|
+
"top-center",
|
|
16
|
+
"top-right",
|
|
17
|
+
"bottom-left",
|
|
18
|
+
"bottom-center",
|
|
19
|
+
"bottom-right",
|
|
20
|
+
],
|
|
21
|
+
table: { category: "Layout" },
|
|
22
|
+
},
|
|
23
|
+
expand: {
|
|
24
|
+
description: "Expand all toasts in the stack by default.",
|
|
25
|
+
control: "boolean",
|
|
26
|
+
table: { category: "Behaviour" },
|
|
27
|
+
},
|
|
28
|
+
richColors: {
|
|
29
|
+
description: "Use rich semantic colours for success/error/warning toasts.",
|
|
30
|
+
control: "boolean",
|
|
31
|
+
table: { category: "Appearance" },
|
|
32
|
+
},
|
|
33
|
+
closeButton: {
|
|
34
|
+
description: "Show an explicit close button on each toast.",
|
|
35
|
+
control: "boolean",
|
|
36
|
+
table: { category: "Behaviour" },
|
|
37
|
+
},
|
|
38
|
+
duration: {
|
|
39
|
+
description: "Auto-dismiss duration in milliseconds. Default 4000.",
|
|
40
|
+
control: "number",
|
|
41
|
+
table: { category: "Behaviour" },
|
|
42
|
+
},
|
|
43
|
+
visibleToasts: {
|
|
44
|
+
description: "Maximum number of toasts visible at one time.",
|
|
45
|
+
control: "number",
|
|
46
|
+
table: { category: "Behaviour" },
|
|
47
|
+
},
|
|
48
|
+
className: {
|
|
49
|
+
description: "Additional CSS classes applied to the Toaster root.",
|
|
50
|
+
control: "text",
|
|
51
|
+
table: { category: "Styling" },
|
|
52
|
+
},
|
|
53
|
+
toastOptions: {
|
|
54
|
+
description: "Default options applied to every toast (classNames, style, etc.).",
|
|
55
|
+
control: false,
|
|
56
|
+
table: { category: "Behaviour" },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
} satisfies Meta<typeof Toaster>;
|
|
60
|
+
export default meta;
|
|
61
|
+
type Story = StoryObj<typeof meta>;
|
|
62
|
+
export const Default: Story = {
|
|
63
|
+
render: () => (
|
|
64
|
+
<div>
|
|
65
|
+
<Button onClick={() => toast("Deployment started", { description: "billing@v1.4.2" })}>
|
|
66
|
+
Show toast
|
|
67
|
+
</Button>
|
|
68
|
+
<Toaster duration={4000} />
|
|
69
|
+
</div>
|
|
70
|
+
),
|
|
71
|
+
// Clicks the trigger button, then confirms the toast message appears in the
|
|
72
|
+
// document body (Sonner renders outside the story canvas in a portal).
|
|
73
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
74
|
+
await userEvent.click(canvas.getByRole("button", { name: /show toast/i }));
|
|
75
|
+
const body = within(canvasElement.ownerDocument.body);
|
|
76
|
+
const toastEl = await body.findByText("Deployment started");
|
|
77
|
+
await waitFor(() => expect(toastEl).toBeVisible());
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const CustomizedWithClassName: Story = {
|
|
82
|
+
render: () => (
|
|
83
|
+
<div>
|
|
84
|
+
<Button
|
|
85
|
+
onClick={() => toast("Hello", { description: "Custom className is merged with defaults" })}
|
|
86
|
+
>
|
|
87
|
+
Show toast
|
|
88
|
+
</Button>
|
|
89
|
+
<Toaster className="custom-wrapper-class" duration={4000} />
|
|
90
|
+
</div>
|
|
91
|
+
),
|
|
92
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
93
|
+
await userEvent.click(canvas.getByRole("button", { name: /show toast/i }));
|
|
94
|
+
const body = within(canvasElement.ownerDocument.body);
|
|
95
|
+
const toastEl = await body.findByText("Hello");
|
|
96
|
+
await waitFor(() => expect(toastEl).toBeVisible());
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const CustomizedWithToastOptions: Story = {
|
|
101
|
+
render: () => (
|
|
102
|
+
<div>
|
|
103
|
+
<Button
|
|
104
|
+
onClick={() => toast("Success!", { description: "Custom toast styles via toastOptions" })}
|
|
105
|
+
>
|
|
106
|
+
Show toast
|
|
107
|
+
</Button>
|
|
108
|
+
<Toaster
|
|
109
|
+
toastOptions={{
|
|
110
|
+
classNames: {
|
|
111
|
+
toast: "group-[.toaster]:bg-primary group-[.toaster]:text-primary-foreground",
|
|
112
|
+
},
|
|
113
|
+
}}
|
|
114
|
+
duration={4000}
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
),
|
|
118
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
119
|
+
await userEvent.click(canvas.getByRole("button", { name: /show toast/i }));
|
|
120
|
+
const body = within(canvasElement.ownerDocument.body);
|
|
121
|
+
const toastEl = await body.findByText("Success!");
|
|
122
|
+
await waitFor(() => expect(toastEl).toBeVisible());
|
|
123
|
+
},
|
|
124
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { render, waitFor } from "@testing-library/react";
|
|
3
|
+
import { Toaster, toast } from "./sonner";
|
|
4
|
+
|
|
5
|
+
// jsdom does not provide ResizeObserver, which sonner uses internally to
|
|
6
|
+
// measure each toast's height.
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
if (typeof window !== "undefined" && !("ResizeObserver" in window)) {
|
|
9
|
+
// @ts-expect-error jsdom stub
|
|
10
|
+
window.ResizeObserver = class ResizeObserver {
|
|
11
|
+
observe() {}
|
|
12
|
+
unobserve() {}
|
|
13
|
+
disconnect() {}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("Toaster", () => {
|
|
19
|
+
it('merges a consumer\'s className with the default "toaster group" wrapper classes (#389)', async () => {
|
|
20
|
+
render(<Toaster className="my-custom-class" />);
|
|
21
|
+
toast("Hello world");
|
|
22
|
+
|
|
23
|
+
// Wait for the wrapper (ol element) to appear
|
|
24
|
+
const wrapper = await waitFor(() => {
|
|
25
|
+
const el = document.querySelector("[data-sonner-toaster]");
|
|
26
|
+
expect(el).not.toBeNull();
|
|
27
|
+
return el as HTMLElement;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// The wrapper must contain BOTH the default classes AND the consumer's class
|
|
31
|
+
expect(wrapper).toHaveClass("toaster");
|
|
32
|
+
expect(wrapper).toHaveClass("group");
|
|
33
|
+
expect(wrapper).toHaveClass("my-custom-class");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("merges a consumer's toastOptions.classNames into the defaults instead of replacing them (#362)", async () => {
|
|
37
|
+
render(<Toaster toastOptions={{ classNames: { toast: "extra-class" } }} />);
|
|
38
|
+
toast("Deployment started", {
|
|
39
|
+
description: "billing@v1.4.2",
|
|
40
|
+
action: { label: "Undo", onClick: () => {} },
|
|
41
|
+
cancel: { label: "Dismiss", onClick: () => {} },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const toastEl = await waitFor(() => {
|
|
45
|
+
const el = document.querySelector("[data-sonner-toast]");
|
|
46
|
+
expect(el).not.toBeNull();
|
|
47
|
+
return el as HTMLElement;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// The consumer's override is present...
|
|
51
|
+
expect(toastEl.className).toContain("extra-class");
|
|
52
|
+
// ...EXTENDING the default `toast` classes, not replacing them.
|
|
53
|
+
expect(toastEl.className).toContain("group-[.toaster]:bg-card");
|
|
54
|
+
|
|
55
|
+
// Sibling classNames the consumer did NOT touch still carry their
|
|
56
|
+
// library-default token-backed classes — proving the merge is deep
|
|
57
|
+
// (per-key), not just that `toast` itself survived.
|
|
58
|
+
expect(document.querySelector("[data-description]")).toHaveClass(
|
|
59
|
+
"group-[.toast]:text-muted-foreground",
|
|
60
|
+
);
|
|
61
|
+
expect(document.querySelector("[data-action]")).toHaveClass("group-[.toast]:bg-primary");
|
|
62
|
+
expect(document.querySelector("[data-cancel]")).toHaveClass("group-[.toast]:bg-muted");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("<Toaster /> with no toastOptions renders the default classes unchanged (regression guard)", async () => {
|
|
66
|
+
render(<Toaster />);
|
|
67
|
+
toast("Plain toast");
|
|
68
|
+
|
|
69
|
+
const toastEl = await waitFor(() => {
|
|
70
|
+
const el = document.querySelector("[data-sonner-toast]");
|
|
71
|
+
expect(el).not.toBeNull();
|
|
72
|
+
return el as HTMLElement;
|
|
73
|
+
});
|
|
74
|
+
expect(toastEl).toHaveClass("group-[.toaster]:bg-card");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Toaster as SonnerToaster, toast } from "sonner";
|
|
3
|
+
import { resolveThemeIsDark } from "@elabs-ai/components-tokens";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The library's default token-backed toast class names. Kept as a standalone
|
|
8
|
+
* constant (rather than inline in the JSX) so `Toaster` can merge a
|
|
9
|
+
* consumer's `toastOptions.classNames` INTO these defaults instead of
|
|
10
|
+
* shallow-replacing them (#362) — each key is combined via `cn()` so a
|
|
11
|
+
* consumer's override extends the default class string rather than
|
|
12
|
+
* discarding it.
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_TOAST_CLASS_NAMES = {
|
|
15
|
+
toast:
|
|
16
|
+
"group toast group-[.toaster]:bg-card group-[.toaster]:text-card-foreground group-[.toaster]:shadow-ring-lg",
|
|
17
|
+
description: "group-[.toast]:text-muted-foreground",
|
|
18
|
+
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
|
19
|
+
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Reads the active theme's light/dark-ness off the document. Uses
|
|
24
|
+
* `resolveThemeIsDark` — the theme's own `color-scheme` — rather than a registry
|
|
25
|
+
* lookup, so a consumer-authored dark theme gets dark toasts with no
|
|
26
|
+
* registration (ADR 0029).
|
|
27
|
+
*/
|
|
28
|
+
function useDocumentThemeMode(): "light" | "dark" {
|
|
29
|
+
const [mode, setMode] = useState<"light" | "dark">("light");
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (typeof document === "undefined") return;
|
|
32
|
+
const el = document.documentElement;
|
|
33
|
+
const read = () => setMode(resolveThemeIsDark(el) ? "dark" : "light");
|
|
34
|
+
read();
|
|
35
|
+
const obs = new MutationObserver(read);
|
|
36
|
+
obs.observe(el, { attributes: true, attributeFilter: ["data-theme"] });
|
|
37
|
+
return () => obs.disconnect();
|
|
38
|
+
}, []);
|
|
39
|
+
return mode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type ToasterProps = React.ComponentProps<typeof SonnerToaster>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Toast host. Mount once near the app root. Trigger toasts with the exported
|
|
46
|
+
* `toast()`. Theme follows the active brand theme (via `data-theme`), so it
|
|
47
|
+
* works with or without <ThemeProvider>.
|
|
48
|
+
*
|
|
49
|
+
* ## Customizing toast styling
|
|
50
|
+
*
|
|
51
|
+
* The Toaster wrapper renders with the classes `toaster group` by default.
|
|
52
|
+
* These classes are **merged** with any consumer-supplied `className`, not
|
|
53
|
+
* replaced, so that built-in styling (the `.group-[.toaster]:*` selectors
|
|
54
|
+
* on individual toasts) continues to work.
|
|
55
|
+
*
|
|
56
|
+
* To override a built-in toast class, **mirror the variant prefix** when you
|
|
57
|
+
* pass `toastOptions.classNames`. Built-in toast classes are variant-scoped
|
|
58
|
+
* (e.g. `group-[.toaster]:bg-card`), so a bare utility override will be
|
|
59
|
+
* outspecificity'd by the built-in and appear to do nothing. Instead, use:
|
|
60
|
+
*
|
|
61
|
+
* ```tsx
|
|
62
|
+
* <Toaster
|
|
63
|
+
* toastOptions={{
|
|
64
|
+
* classNames: {
|
|
65
|
+
* toast: "group-[.toaster]:bg-primary", // ✓ mirrors the prefix
|
|
66
|
+
* }
|
|
67
|
+
* }}
|
|
68
|
+
* />
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* ## Theme handling
|
|
72
|
+
*
|
|
73
|
+
* The Toaster's `theme` prop is automatically set from the active theme's own
|
|
74
|
+
* `color-scheme` declaration, so toasts visually track the app's theme by
|
|
75
|
+
* default — including a theme you authored yourself. However, you may pass
|
|
76
|
+
* `theme="light"` or `theme="dark"` directly to override this derivation if needed.
|
|
77
|
+
*/
|
|
78
|
+
export function Toaster({ className, toastOptions, ...rest }: ToasterProps) {
|
|
79
|
+
const mode = useDocumentThemeMode();
|
|
80
|
+
const userClassNames = toastOptions?.classNames;
|
|
81
|
+
// Merge (not replace): every default key is extended with the consumer's
|
|
82
|
+
// override via cn(); any extra keys the consumer passes (e.g. `success`,
|
|
83
|
+
// `closeButton`) pass through untouched.
|
|
84
|
+
const classNames = {
|
|
85
|
+
...userClassNames,
|
|
86
|
+
toast: cn(DEFAULT_TOAST_CLASS_NAMES.toast, userClassNames?.toast),
|
|
87
|
+
description: cn(DEFAULT_TOAST_CLASS_NAMES.description, userClassNames?.description),
|
|
88
|
+
actionButton: cn(DEFAULT_TOAST_CLASS_NAMES.actionButton, userClassNames?.actionButton),
|
|
89
|
+
cancelButton: cn(DEFAULT_TOAST_CLASS_NAMES.cancelButton, userClassNames?.cancelButton),
|
|
90
|
+
};
|
|
91
|
+
return (
|
|
92
|
+
<SonnerToaster
|
|
93
|
+
theme={mode}
|
|
94
|
+
className={cn("toaster group", className)}
|
|
95
|
+
toastOptions={{ ...toastOptions, classNames }}
|
|
96
|
+
{...rest}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { toast };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Spinner, type SpinnerProps } from "./spinner";
|