@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,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { BellIcon } from "lucide-react";
|
|
4
|
+
import { Avatar, AvatarFallback } from "../avatar";
|
|
5
|
+
import { Button } from "../button";
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownMenuContent,
|
|
9
|
+
DropdownMenuItem,
|
|
10
|
+
DropdownMenuLabel,
|
|
11
|
+
DropdownMenuSeparator,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from "../dropdown-menu";
|
|
14
|
+
|
|
15
|
+
export interface NavNotification {
|
|
16
|
+
id: string;
|
|
17
|
+
/** Two-letter fallback initials for the avatar. */
|
|
18
|
+
fallback: string;
|
|
19
|
+
text: string;
|
|
20
|
+
time: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface NavNotificationsProps {
|
|
24
|
+
notifications: NavNotification[];
|
|
25
|
+
/** Label for the "view all" footer item. @default "View all notifications" */
|
|
26
|
+
viewAllLabel?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Sidebar notifications dropdown button. Shared primitive from sidebar-02 (issue #99).
|
|
31
|
+
* Renders a bell-icon ghost button that opens a dropdown listing recent notifications.
|
|
32
|
+
*/
|
|
33
|
+
export function NavNotifications({
|
|
34
|
+
notifications,
|
|
35
|
+
viewAllLabel = "View all notifications",
|
|
36
|
+
}: NavNotificationsProps) {
|
|
37
|
+
return (
|
|
38
|
+
<DropdownMenu>
|
|
39
|
+
<DropdownMenuTrigger asChild>
|
|
40
|
+
<Button
|
|
41
|
+
variant="ghost"
|
|
42
|
+
size="icon"
|
|
43
|
+
className="rounded-full"
|
|
44
|
+
aria-label="Open notifications"
|
|
45
|
+
>
|
|
46
|
+
<BellIcon className="size-5" />
|
|
47
|
+
</Button>
|
|
48
|
+
</DropdownMenuTrigger>
|
|
49
|
+
<DropdownMenuContent side="right" className="my-6 w-80">
|
|
50
|
+
<DropdownMenuLabel>Notifications</DropdownMenuLabel>
|
|
51
|
+
<DropdownMenuSeparator />
|
|
52
|
+
{notifications.map(({ id, fallback, text, time }) => (
|
|
53
|
+
<DropdownMenuItem key={id} className="flex items-start gap-3">
|
|
54
|
+
<Avatar className="size-8">
|
|
55
|
+
<AvatarFallback>{fallback}</AvatarFallback>
|
|
56
|
+
</Avatar>
|
|
57
|
+
<div className="flex flex-col">
|
|
58
|
+
<span className="text-sm font-medium">{text}</span>
|
|
59
|
+
<span className="text-xs text-muted-foreground">{time}</span>
|
|
60
|
+
</div>
|
|
61
|
+
</DropdownMenuItem>
|
|
62
|
+
))}
|
|
63
|
+
<DropdownMenuSeparator />
|
|
64
|
+
<DropdownMenuItem className="justify-center text-sm text-muted-foreground">
|
|
65
|
+
{viewAllLabel}
|
|
66
|
+
</DropdownMenuItem>
|
|
67
|
+
</DropdownMenuContent>
|
|
68
|
+
</DropdownMenu>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { NavUser, type NavUserProps, type NavUserUser } from "./nav-user";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { SidebarProvider } from "../sidebar";
|
|
4
|
+
import { NavUser } from "./nav-user";
|
|
5
|
+
|
|
6
|
+
function Wrapper({ children }: { children: React.ReactNode }) {
|
|
7
|
+
return <SidebarProvider>{children}</SidebarProvider>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("NavUser", () => {
|
|
11
|
+
it("renders the user name and email in the trigger", () => {
|
|
12
|
+
render(
|
|
13
|
+
<Wrapper>
|
|
14
|
+
<NavUser user={{ name: "Jane Doe", email: "jane@example.com" }} />
|
|
15
|
+
</Wrapper>,
|
|
16
|
+
);
|
|
17
|
+
expect(screen.getAllByText("Jane Doe").length).toBeGreaterThan(0);
|
|
18
|
+
expect(screen.getAllByText("jane@example.com").length).toBeGreaterThan(0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("derives two-letter initials from the name", () => {
|
|
22
|
+
render(
|
|
23
|
+
<Wrapper>
|
|
24
|
+
<NavUser user={{ name: "John Smith", email: "j@example.com" }} />
|
|
25
|
+
</Wrapper>,
|
|
26
|
+
);
|
|
27
|
+
expect(screen.getAllByText("JS").length).toBeGreaterThan(0);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { BadgeCheck, Bell, ChevronsUpDown, CreditCard, LogOut, Sparkles } from "lucide-react";
|
|
4
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../avatar";
|
|
5
|
+
import {
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuGroup,
|
|
9
|
+
DropdownMenuItem,
|
|
10
|
+
DropdownMenuLabel,
|
|
11
|
+
DropdownMenuSeparator,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from "../dropdown-menu";
|
|
14
|
+
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from "../sidebar";
|
|
15
|
+
|
|
16
|
+
export interface NavUserUser {
|
|
17
|
+
name: string;
|
|
18
|
+
email: string;
|
|
19
|
+
/** URL for the user's avatar image. */
|
|
20
|
+
avatar?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface NavUserProps {
|
|
24
|
+
user: NavUserUser;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sidebar account / user menu. Shared primitive from sidebar-04 (issue #99).
|
|
29
|
+
* Renders a trigger showing name + email + avatar; the dropdown exposes
|
|
30
|
+
* account, billing, notifications and log-out actions.
|
|
31
|
+
*
|
|
32
|
+
* Wire your own action handlers by composing custom `DropdownMenuItem` children
|
|
33
|
+
* if you need more than the default actions — or extend via className.
|
|
34
|
+
*/
|
|
35
|
+
export function NavUser({ user }: NavUserProps) {
|
|
36
|
+
const { isMobile } = useSidebar();
|
|
37
|
+
const initials = user.name
|
|
38
|
+
.split(" ")
|
|
39
|
+
.map((n) => n[0])
|
|
40
|
+
.join("")
|
|
41
|
+
.slice(0, 2)
|
|
42
|
+
.toUpperCase();
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<SidebarMenu>
|
|
46
|
+
<SidebarMenuItem>
|
|
47
|
+
<DropdownMenu>
|
|
48
|
+
<DropdownMenuTrigger asChild>
|
|
49
|
+
<SidebarMenuButton
|
|
50
|
+
size="lg"
|
|
51
|
+
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground md:h-8 md:p-0"
|
|
52
|
+
>
|
|
53
|
+
<Avatar className="h-8 w-8 rounded-lg">
|
|
54
|
+
{user.avatar && <AvatarImage src={user.avatar} alt={user.name} />}
|
|
55
|
+
<AvatarFallback className="rounded-lg">{initials}</AvatarFallback>
|
|
56
|
+
</Avatar>
|
|
57
|
+
<div className="grid flex-1 text-start text-sm leading-tight">
|
|
58
|
+
<span className="truncate font-medium">{user.name}</span>
|
|
59
|
+
<span className="truncate text-xs">{user.email}</span>
|
|
60
|
+
</div>
|
|
61
|
+
<ChevronsUpDown className="ms-auto size-4" />
|
|
62
|
+
</SidebarMenuButton>
|
|
63
|
+
</DropdownMenuTrigger>
|
|
64
|
+
<DropdownMenuContent
|
|
65
|
+
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
|
66
|
+
side={isMobile ? "bottom" : "right"}
|
|
67
|
+
align="end"
|
|
68
|
+
sideOffset={4}
|
|
69
|
+
>
|
|
70
|
+
<DropdownMenuLabel className="p-0 font-normal">
|
|
71
|
+
<div className="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
|
|
72
|
+
<Avatar className="h-8 w-8 rounded-lg">
|
|
73
|
+
{user.avatar && <AvatarImage src={user.avatar} alt={user.name} />}
|
|
74
|
+
<AvatarFallback className="rounded-lg">{initials}</AvatarFallback>
|
|
75
|
+
</Avatar>
|
|
76
|
+
<div className="grid flex-1 text-start text-sm leading-tight">
|
|
77
|
+
<span className="truncate font-medium">{user.name}</span>
|
|
78
|
+
<span className="truncate text-xs">{user.email}</span>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</DropdownMenuLabel>
|
|
82
|
+
<DropdownMenuSeparator />
|
|
83
|
+
<DropdownMenuGroup>
|
|
84
|
+
<DropdownMenuItem>
|
|
85
|
+
<Sparkles />
|
|
86
|
+
Upgrade to Pro
|
|
87
|
+
</DropdownMenuItem>
|
|
88
|
+
</DropdownMenuGroup>
|
|
89
|
+
<DropdownMenuSeparator />
|
|
90
|
+
<DropdownMenuGroup>
|
|
91
|
+
<DropdownMenuItem>
|
|
92
|
+
<BadgeCheck />
|
|
93
|
+
Account
|
|
94
|
+
</DropdownMenuItem>
|
|
95
|
+
<DropdownMenuItem>
|
|
96
|
+
<CreditCard />
|
|
97
|
+
Billing
|
|
98
|
+
</DropdownMenuItem>
|
|
99
|
+
<DropdownMenuItem>
|
|
100
|
+
<Bell />
|
|
101
|
+
Notifications
|
|
102
|
+
</DropdownMenuItem>
|
|
103
|
+
</DropdownMenuGroup>
|
|
104
|
+
<DropdownMenuSeparator />
|
|
105
|
+
<DropdownMenuItem>
|
|
106
|
+
<LogOut />
|
|
107
|
+
Log out
|
|
108
|
+
</DropdownMenuItem>
|
|
109
|
+
</DropdownMenuContent>
|
|
110
|
+
</DropdownMenu>
|
|
111
|
+
</SidebarMenuItem>
|
|
112
|
+
</SidebarMenu>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, waitFor } from "storybook/test";
|
|
3
|
+
import {
|
|
4
|
+
NavigationMenu,
|
|
5
|
+
NavigationMenuContent,
|
|
6
|
+
NavigationMenuItem,
|
|
7
|
+
NavigationMenuLink,
|
|
8
|
+
NavigationMenuList,
|
|
9
|
+
NavigationMenuTrigger,
|
|
10
|
+
} from "./navigation-menu";
|
|
11
|
+
const meta = {
|
|
12
|
+
title: "Navigation/NavigationMenu",
|
|
13
|
+
component: NavigationMenu,
|
|
14
|
+
tags: ["autodocs"],
|
|
15
|
+
parameters: { layout: "padded" },
|
|
16
|
+
argTypes: {
|
|
17
|
+
value: {
|
|
18
|
+
description: "Controlled value of the currently active item.",
|
|
19
|
+
control: "text",
|
|
20
|
+
table: { category: "State" },
|
|
21
|
+
},
|
|
22
|
+
defaultValue: {
|
|
23
|
+
description: "Uncontrolled initial active item value.",
|
|
24
|
+
control: "text",
|
|
25
|
+
table: { category: "State" },
|
|
26
|
+
},
|
|
27
|
+
onValueChange: {
|
|
28
|
+
description: "Callback when the active item changes.",
|
|
29
|
+
control: false,
|
|
30
|
+
table: { category: "Events" },
|
|
31
|
+
},
|
|
32
|
+
delayDuration: {
|
|
33
|
+
description: "Milliseconds to delay before content opens on pointer enter.",
|
|
34
|
+
control: "number",
|
|
35
|
+
table: { category: "Behaviour" },
|
|
36
|
+
},
|
|
37
|
+
skipDelayDuration: {
|
|
38
|
+
description: "Milliseconds before the close delay resets after leaving a trigger.",
|
|
39
|
+
control: "number",
|
|
40
|
+
table: { category: "Behaviour" },
|
|
41
|
+
},
|
|
42
|
+
dir: {
|
|
43
|
+
description: "Reading direction for the navigation menu.",
|
|
44
|
+
control: { type: "select" },
|
|
45
|
+
options: ["ltr", "rtl"],
|
|
46
|
+
table: { category: "Behaviour" },
|
|
47
|
+
},
|
|
48
|
+
className: {
|
|
49
|
+
description: "Additional CSS classes applied to the navigation menu root.",
|
|
50
|
+
control: "text",
|
|
51
|
+
table: { category: "Styling" },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
} satisfies Meta<typeof NavigationMenu>;
|
|
55
|
+
export default meta;
|
|
56
|
+
type Story = StoryObj<typeof meta>;
|
|
57
|
+
export const Default: Story = {
|
|
58
|
+
render: () => (
|
|
59
|
+
<NavigationMenu>
|
|
60
|
+
<NavigationMenuList>
|
|
61
|
+
<NavigationMenuItem>
|
|
62
|
+
<NavigationMenuTrigger>Products</NavigationMenuTrigger>
|
|
63
|
+
<NavigationMenuContent>
|
|
64
|
+
<ul className="grid w-56 gap-1 p-2">
|
|
65
|
+
<li>
|
|
66
|
+
<NavigationMenuLink className="block rounded-md p-2 text-body hover:bg-accent">
|
|
67
|
+
Analytics
|
|
68
|
+
</NavigationMenuLink>
|
|
69
|
+
</li>
|
|
70
|
+
<li>
|
|
71
|
+
<NavigationMenuLink className="block rounded-md p-2 text-body hover:bg-accent">
|
|
72
|
+
Data Integration
|
|
73
|
+
</NavigationMenuLink>
|
|
74
|
+
</li>
|
|
75
|
+
</ul>
|
|
76
|
+
</NavigationMenuContent>
|
|
77
|
+
</NavigationMenuItem>
|
|
78
|
+
</NavigationMenuList>
|
|
79
|
+
</NavigationMenu>
|
|
80
|
+
),
|
|
81
|
+
// Clicks the "Products" trigger, confirms the dropdown content appears in the
|
|
82
|
+
// viewport (NavigationMenu renders inline, not portalled), then presses Escape.
|
|
83
|
+
play: async ({ canvas, userEvent }) => {
|
|
84
|
+
const trigger = canvas.getByRole("button", { name: /products/i });
|
|
85
|
+
await userEvent.click(trigger);
|
|
86
|
+
const analytics = await canvas.findByText("Analytics");
|
|
87
|
+
await waitFor(() => expect(analytics).toBeVisible());
|
|
88
|
+
await waitFor(() => expect(canvas.getByText("Data Integration")).toBeVisible());
|
|
89
|
+
// Escape dismisses the open panel
|
|
90
|
+
await userEvent.keyboard("{Escape}");
|
|
91
|
+
},
|
|
92
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
|
|
2
|
+
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
|
3
|
+
import { cva } from "class-variance-authority";
|
|
4
|
+
import { ChevronDown } from "lucide-react";
|
|
5
|
+
import { cn } from "../../lib/cn";
|
|
6
|
+
|
|
7
|
+
export const NavigationMenu = forwardRef<
|
|
8
|
+
ElementRef<typeof NavigationMenuPrimitive.Root>,
|
|
9
|
+
ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
|
10
|
+
>(function NavigationMenu({ className, children, ...props }, ref) {
|
|
11
|
+
return (
|
|
12
|
+
<NavigationMenuPrimitive.Root
|
|
13
|
+
ref={ref}
|
|
14
|
+
className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
|
|
15
|
+
{...props}
|
|
16
|
+
>
|
|
17
|
+
{children}
|
|
18
|
+
<NavigationMenuViewport />
|
|
19
|
+
</NavigationMenuPrimitive.Root>
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const NavigationMenuList = forwardRef<
|
|
24
|
+
ElementRef<typeof NavigationMenuPrimitive.List>,
|
|
25
|
+
ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
|
26
|
+
>(function NavigationMenuList({ className, ...props }, ref) {
|
|
27
|
+
return (
|
|
28
|
+
<NavigationMenuPrimitive.List
|
|
29
|
+
ref={ref}
|
|
30
|
+
className={cn("group flex flex-1 list-none items-center justify-center gap-1", className)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const NavigationMenuItem = NavigationMenuPrimitive.Item;
|
|
37
|
+
|
|
38
|
+
export const navigationMenuTriggerStyle = cva(
|
|
39
|
+
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export const NavigationMenuTrigger = forwardRef<
|
|
43
|
+
ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
|
44
|
+
ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
|
45
|
+
>(function NavigationMenuTrigger({ className, children, ...props }, ref) {
|
|
46
|
+
return (
|
|
47
|
+
<NavigationMenuPrimitive.Trigger
|
|
48
|
+
ref={ref}
|
|
49
|
+
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
<ChevronDown
|
|
54
|
+
className="relative top-px ms-1 size-3 transition-transform duration-base ease-standard group-data-[state=open]:rotate-180"
|
|
55
|
+
aria-hidden="true"
|
|
56
|
+
/>
|
|
57
|
+
</NavigationMenuPrimitive.Trigger>
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const NavigationMenuContent = forwardRef<
|
|
62
|
+
ElementRef<typeof NavigationMenuPrimitive.Content>,
|
|
63
|
+
ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
|
64
|
+
>(function NavigationMenuContent({ className, ...props }, ref) {
|
|
65
|
+
return (
|
|
66
|
+
<NavigationMenuPrimitive.Content
|
|
67
|
+
ref={ref}
|
|
68
|
+
className={cn(
|
|
69
|
+
"left-0 top-0 w-full p-2 md:absolute md:w-auto",
|
|
70
|
+
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out",
|
|
71
|
+
"data-[motion=from-end]:slide-in-from-right-2 data-[motion=from-start]:slide-in-from-left-2 data-[motion=to-end]:slide-out-to-right-2 data-[motion=to-start]:slide-out-to-left-2",
|
|
72
|
+
"data-[motion^=from-]:[--tw-ease:var(--ease-entrance)]",
|
|
73
|
+
className,
|
|
74
|
+
)}
|
|
75
|
+
{...props}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export const NavigationMenuLink = NavigationMenuPrimitive.Link;
|
|
81
|
+
|
|
82
|
+
export const NavigationMenuViewport = forwardRef<
|
|
83
|
+
ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
|
84
|
+
ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
|
85
|
+
>(function NavigationMenuViewport({ className, ...props }, ref) {
|
|
86
|
+
return (
|
|
87
|
+
<div className="absolute left-0 top-full flex justify-center">
|
|
88
|
+
<NavigationMenuPrimitive.Viewport
|
|
89
|
+
ref={ref}
|
|
90
|
+
className={cn(
|
|
91
|
+
"relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
|
92
|
+
"origin-top transition-[width,height] duration-base ease-standard data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:zoom-in-90 data-[state=closed]:zoom-out-95",
|
|
93
|
+
className,
|
|
94
|
+
)}
|
|
95
|
+
{...props}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { NumberInput, type NumberInputProps } from "./number-input";
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect } from "storybook/test";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { NumberInput } from "./number-input";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Forms/NumberInput",
|
|
8
|
+
component: NumberInput,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
args: { step: 1 },
|
|
11
|
+
argTypes: {
|
|
12
|
+
value: {
|
|
13
|
+
description: "Controlled numeric value. Pass `null` for empty state.",
|
|
14
|
+
control: "number",
|
|
15
|
+
table: { category: "State" },
|
|
16
|
+
},
|
|
17
|
+
defaultValue: {
|
|
18
|
+
description: "Uncontrolled initial value.",
|
|
19
|
+
control: "number",
|
|
20
|
+
table: { category: "State" },
|
|
21
|
+
},
|
|
22
|
+
min: {
|
|
23
|
+
description: "Minimum allowed value.",
|
|
24
|
+
control: "number",
|
|
25
|
+
table: { category: "Behavior" },
|
|
26
|
+
},
|
|
27
|
+
max: {
|
|
28
|
+
description: "Maximum allowed value.",
|
|
29
|
+
control: "number",
|
|
30
|
+
table: { category: "Behavior" },
|
|
31
|
+
},
|
|
32
|
+
step: {
|
|
33
|
+
description: "Increment/decrement amount per click or arrow key.",
|
|
34
|
+
control: "number",
|
|
35
|
+
table: { category: "Behavior" },
|
|
36
|
+
},
|
|
37
|
+
clamp: {
|
|
38
|
+
description: "Clamp the value to [min, max] on blur (default true).",
|
|
39
|
+
control: "boolean",
|
|
40
|
+
table: { category: "Behavior" },
|
|
41
|
+
},
|
|
42
|
+
disabled: {
|
|
43
|
+
description: "Disables all interaction.",
|
|
44
|
+
control: "boolean",
|
|
45
|
+
table: { category: "State" },
|
|
46
|
+
},
|
|
47
|
+
readOnly: {
|
|
48
|
+
description: "Makes the input read-only.",
|
|
49
|
+
control: "boolean",
|
|
50
|
+
table: { category: "State" },
|
|
51
|
+
},
|
|
52
|
+
placeholder: {
|
|
53
|
+
description: "Placeholder text shown when the field is empty.",
|
|
54
|
+
control: "text",
|
|
55
|
+
table: { category: "Content" },
|
|
56
|
+
},
|
|
57
|
+
formatOptions: {
|
|
58
|
+
description: "`Intl.NumberFormat` options to format the displayed value.",
|
|
59
|
+
control: false,
|
|
60
|
+
table: { category: "Appearance" },
|
|
61
|
+
},
|
|
62
|
+
onValueChange: {
|
|
63
|
+
description: "Called when the numeric value changes.",
|
|
64
|
+
control: false,
|
|
65
|
+
table: { category: "Behavior" },
|
|
66
|
+
},
|
|
67
|
+
className: {
|
|
68
|
+
description: "Additional CSS classes applied to the root element.",
|
|
69
|
+
control: "text",
|
|
70
|
+
table: { category: "Appearance" },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
} satisfies Meta<typeof NumberInput>;
|
|
74
|
+
export default meta;
|
|
75
|
+
type Story = StoryObj<typeof meta>;
|
|
76
|
+
|
|
77
|
+
export const Default: Story = {
|
|
78
|
+
args: { defaultValue: 5, min: 0, max: 10 },
|
|
79
|
+
// Clicks the Increase button and confirms the value increments.
|
|
80
|
+
play: async ({ canvas, userEvent }) => {
|
|
81
|
+
const input = canvas.getByRole("spinbutton");
|
|
82
|
+
await expect(input).toHaveValue(5);
|
|
83
|
+
const increaseBtn = canvas.getByRole("button", { name: /increase/i });
|
|
84
|
+
await userEvent.click(increaseBtn);
|
|
85
|
+
await expect(input).toHaveValue(6);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const Controlled: Story = {
|
|
90
|
+
render: () => {
|
|
91
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
92
|
+
const [value, setValue] = useState<number | null>(3);
|
|
93
|
+
return (
|
|
94
|
+
<div className="flex flex-col gap-2">
|
|
95
|
+
<NumberInput value={value} onValueChange={setValue} min={0} max={20} />
|
|
96
|
+
<p className="text-body text-muted-foreground">Value: {value ?? "empty"}</p>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const WithFormatOptions: Story = {
|
|
103
|
+
args: {
|
|
104
|
+
defaultValue: 1234.5,
|
|
105
|
+
formatOptions: { style: "currency", currency: "USD" },
|
|
106
|
+
step: 100,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const NoClamp: Story = {
|
|
111
|
+
args: { defaultValue: 5, min: 0, max: 10, clamp: false },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const Disabled: Story = {
|
|
115
|
+
args: { defaultValue: 5, disabled: true },
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const WithBounds: Story = {
|
|
119
|
+
args: { defaultValue: 0, min: 0, max: 100, step: 10 },
|
|
120
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
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 { NumberInput } from "./number-input";
|
|
5
|
+
|
|
6
|
+
describe("NumberInput", () => {
|
|
7
|
+
it("renders a spinbutton input", () => {
|
|
8
|
+
render(<NumberInput aria-label="Quantity" defaultValue={5} />);
|
|
9
|
+
expect(screen.getByRole("spinbutton", { name: "Quantity" })).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("renders Increase and Decrease buttons", () => {
|
|
13
|
+
render(<NumberInput aria-label="Qty" />);
|
|
14
|
+
expect(screen.getByRole("button", { name: "Decrease" })).toBeInTheDocument();
|
|
15
|
+
expect(screen.getByRole("button", { name: "Increase" })).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("increments value when Increase is clicked (uncontrolled)", async () => {
|
|
19
|
+
const onValueChange = vi.fn();
|
|
20
|
+
render(<NumberInput aria-label="Qty" defaultValue={3} onValueChange={onValueChange} />);
|
|
21
|
+
await userEvent.click(screen.getByRole("button", { name: "Increase" }));
|
|
22
|
+
expect(onValueChange).toHaveBeenCalledWith(4);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("decrements value when Decrease is clicked (uncontrolled)", async () => {
|
|
26
|
+
const onValueChange = vi.fn();
|
|
27
|
+
render(<NumberInput aria-label="Qty" defaultValue={3} onValueChange={onValueChange} />);
|
|
28
|
+
await userEvent.click(screen.getByRole("button", { name: "Decrease" }));
|
|
29
|
+
expect(onValueChange).toHaveBeenCalledWith(2);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("clamps to max on Increase at boundary", async () => {
|
|
33
|
+
const onValueChange = vi.fn();
|
|
34
|
+
render(
|
|
35
|
+
<NumberInput
|
|
36
|
+
aria-label="Qty"
|
|
37
|
+
defaultValue={10}
|
|
38
|
+
min={0}
|
|
39
|
+
max={10}
|
|
40
|
+
onValueChange={onValueChange}
|
|
41
|
+
/>,
|
|
42
|
+
);
|
|
43
|
+
const increaseBtn = screen.getByRole("button", { name: "Increase" });
|
|
44
|
+
expect(increaseBtn).toBeDisabled();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("clamps to min on Decrease at boundary", async () => {
|
|
48
|
+
render(<NumberInput aria-label="Qty" defaultValue={0} min={0} max={10} />);
|
|
49
|
+
expect(screen.getByRole("button", { name: "Decrease" })).toBeDisabled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("is disabled when disabled prop is set", () => {
|
|
53
|
+
render(<NumberInput aria-label="Qty" defaultValue={5} disabled />);
|
|
54
|
+
expect(screen.getByRole("spinbutton", { name: "Qty" })).toBeDisabled();
|
|
55
|
+
expect(screen.getByRole("button", { name: "Increase" })).toBeDisabled();
|
|
56
|
+
expect(screen.getByRole("button", { name: "Decrease" })).toBeDisabled();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("clamps value on blur when clamp=true", async () => {
|
|
60
|
+
const onValueChange = vi.fn();
|
|
61
|
+
render(
|
|
62
|
+
<NumberInput
|
|
63
|
+
aria-label="Qty"
|
|
64
|
+
defaultValue={5}
|
|
65
|
+
min={0}
|
|
66
|
+
max={10}
|
|
67
|
+
onValueChange={onValueChange}
|
|
68
|
+
/>,
|
|
69
|
+
);
|
|
70
|
+
const input = screen.getByRole("spinbutton", { name: "Qty" });
|
|
71
|
+
await userEvent.clear(input);
|
|
72
|
+
await userEvent.type(input, "99");
|
|
73
|
+
await userEvent.tab(); // trigger blur
|
|
74
|
+
expect(onValueChange).toHaveBeenLastCalledWith(10);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("calls onValueChange with null when cleared and blurred", async () => {
|
|
78
|
+
const onValueChange = vi.fn();
|
|
79
|
+
render(<NumberInput aria-label="Qty" defaultValue={5} onValueChange={onValueChange} />);
|
|
80
|
+
const input = screen.getByRole("spinbutton", { name: "Qty" });
|
|
81
|
+
await userEvent.clear(input);
|
|
82
|
+
await userEvent.tab();
|
|
83
|
+
expect(onValueChange).toHaveBeenLastCalledWith(null);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("responds to keyboard ArrowUp/ArrowDown", async () => {
|
|
87
|
+
const onValueChange = vi.fn();
|
|
88
|
+
render(<NumberInput aria-label="Qty" defaultValue={5} onValueChange={onValueChange} />);
|
|
89
|
+
const input = screen.getByRole("spinbutton", { name: "Qty" });
|
|
90
|
+
input.focus();
|
|
91
|
+
await userEvent.keyboard("{ArrowUp}");
|
|
92
|
+
expect(onValueChange).toHaveBeenLastCalledWith(6);
|
|
93
|
+
await userEvent.keyboard("{ArrowDown}");
|
|
94
|
+
expect(onValueChange).toHaveBeenLastCalledWith(5);
|
|
95
|
+
});
|
|
96
|
+
});
|