@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,219 @@
|
|
|
1
|
+
// a2ui.exposed: yes
|
|
2
|
+
import { forwardRef, useCallback, useRef, useState, type HTMLAttributes } from "react";
|
|
3
|
+
import { Minus, Plus } from "lucide-react";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import { InputGroup, InputGroupButton, InputGroupInput } from "../input-group";
|
|
6
|
+
|
|
7
|
+
export interface NumberInputProps extends Omit<
|
|
8
|
+
HTMLAttributes<HTMLDivElement>,
|
|
9
|
+
"onChange" | "defaultValue"
|
|
10
|
+
> {
|
|
11
|
+
/** Controlled value. Pass `null` to represent an empty state. */
|
|
12
|
+
value?: number | null;
|
|
13
|
+
/** Uncontrolled default value. */
|
|
14
|
+
defaultValue?: number | null;
|
|
15
|
+
/** Called when the numeric value changes. */
|
|
16
|
+
onValueChange?: (value: number | null) => void;
|
|
17
|
+
/** Minimum allowed value. */
|
|
18
|
+
min?: number;
|
|
19
|
+
/** Maximum allowed value. */
|
|
20
|
+
max?: number;
|
|
21
|
+
/** Step increment/decrement amount (default 1). */
|
|
22
|
+
step?: number;
|
|
23
|
+
/**
|
|
24
|
+
* When true (default), the value is clamped to [min, max] on blur.
|
|
25
|
+
* Clamping does NOT happen per-keystroke — only on blur.
|
|
26
|
+
*/
|
|
27
|
+
clamp?: boolean;
|
|
28
|
+
/** Intl.NumberFormat options used to format the displayed value. */
|
|
29
|
+
formatOptions?: Intl.NumberFormatOptions;
|
|
30
|
+
/** Placeholder text for the underlying input. */
|
|
31
|
+
placeholder?: string;
|
|
32
|
+
/** Whether the input is disabled. */
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
/** Whether the input is read-only. */
|
|
35
|
+
readOnly?: boolean;
|
|
36
|
+
/** Name attribute forwarded to the underlying <input>. */
|
|
37
|
+
name?: string;
|
|
38
|
+
/** id attribute forwarded to the underlying <input>. */
|
|
39
|
+
id?: string;
|
|
40
|
+
/** aria-label forwarded to the underlying <input>. */
|
|
41
|
+
"aria-label"?: string;
|
|
42
|
+
/** aria-labelledby forwarded to the underlying <input>. */
|
|
43
|
+
"aria-labelledby"?: string;
|
|
44
|
+
/** aria-describedby forwarded to the underlying <input>. */
|
|
45
|
+
"aria-describedby"?: string;
|
|
46
|
+
/** aria-invalid forwarded to the underlying <input>. */
|
|
47
|
+
"aria-invalid"?: boolean | "true" | "false" | "grammar" | "spelling";
|
|
48
|
+
/** aria-required forwarded to the underlying <input>. */
|
|
49
|
+
"aria-required"?: boolean | "true" | "false";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function clampValue(val: number, min?: number, max?: number): number {
|
|
53
|
+
let result = val;
|
|
54
|
+
if (min !== undefined) result = Math.max(min, result);
|
|
55
|
+
if (max !== undefined) result = Math.min(max, result);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatNum(value: number, formatOptions?: Intl.NumberFormatOptions): string {
|
|
60
|
+
if (formatOptions) {
|
|
61
|
+
return new Intl.NumberFormat(undefined, formatOptions).format(value);
|
|
62
|
+
}
|
|
63
|
+
return String(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* NumberInput — a numeric stepper that composes InputGroup + two step buttons.
|
|
68
|
+
*
|
|
69
|
+
* Controlled via `value`/`onValueChange`; uncontrolled via `defaultValue`.
|
|
70
|
+
* Keyboard ↑/↓ = ±step; Shift+↑/↓ = ±step×10. Clamps on blur (not per keystroke).
|
|
71
|
+
*/
|
|
72
|
+
export const NumberInput = forwardRef<HTMLDivElement, NumberInputProps>(function NumberInput(
|
|
73
|
+
{
|
|
74
|
+
value: valueProp,
|
|
75
|
+
defaultValue,
|
|
76
|
+
onValueChange,
|
|
77
|
+
min,
|
|
78
|
+
max,
|
|
79
|
+
step = 1,
|
|
80
|
+
clamp = true,
|
|
81
|
+
formatOptions,
|
|
82
|
+
placeholder,
|
|
83
|
+
disabled,
|
|
84
|
+
readOnly,
|
|
85
|
+
name,
|
|
86
|
+
id,
|
|
87
|
+
"aria-label": ariaLabel,
|
|
88
|
+
"aria-labelledby": ariaLabelledby,
|
|
89
|
+
"aria-describedby": ariaDescribedby,
|
|
90
|
+
"aria-invalid": ariaInvalid,
|
|
91
|
+
"aria-required": ariaRequired,
|
|
92
|
+
className,
|
|
93
|
+
...props
|
|
94
|
+
},
|
|
95
|
+
ref,
|
|
96
|
+
) {
|
|
97
|
+
const isControlled = valueProp !== undefined;
|
|
98
|
+
|
|
99
|
+
const [internalValue, setInternalValue] = useState<number | null>(
|
|
100
|
+
defaultValue !== undefined ? defaultValue : null,
|
|
101
|
+
);
|
|
102
|
+
const [inputText, setInputText] = useState<string>(() => {
|
|
103
|
+
const initial = isControlled ? valueProp : defaultValue;
|
|
104
|
+
return initial != null ? formatNum(initial, formatOptions) : "";
|
|
105
|
+
});
|
|
106
|
+
// Track whether the user is actively editing to avoid overwriting their input
|
|
107
|
+
const isEditing = useRef(false);
|
|
108
|
+
|
|
109
|
+
const currentValue = isControlled ? valueProp : internalValue;
|
|
110
|
+
|
|
111
|
+
const commit = useCallback(
|
|
112
|
+
(next: number | null) => {
|
|
113
|
+
if (!isControlled) setInternalValue(next);
|
|
114
|
+
onValueChange?.(next);
|
|
115
|
+
},
|
|
116
|
+
[isControlled, onValueChange],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const applyStep = useCallback(
|
|
120
|
+
(delta: number) => {
|
|
121
|
+
if (disabled || readOnly) return;
|
|
122
|
+
const base = currentValue ?? 0;
|
|
123
|
+
let next = base + delta;
|
|
124
|
+
if (clamp) next = clampValue(next, min, max);
|
|
125
|
+
setInputText(next != null ? formatNum(next, formatOptions) : "");
|
|
126
|
+
commit(next);
|
|
127
|
+
},
|
|
128
|
+
[disabled, readOnly, currentValue, clamp, min, max, formatOptions, commit],
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const handleKeyDown = useCallback(
|
|
132
|
+
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
133
|
+
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
const multiplier = e.shiftKey ? 10 : 1;
|
|
136
|
+
const delta = e.key === "ArrowUp" ? step * multiplier : -step * multiplier;
|
|
137
|
+
applyStep(delta);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
[applyStep, step],
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
144
|
+
isEditing.current = true;
|
|
145
|
+
setInputText(e.target.value);
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
148
|
+
const handleBlur = useCallback(() => {
|
|
149
|
+
isEditing.current = false;
|
|
150
|
+
const raw = inputText.trim();
|
|
151
|
+
if (raw === "") {
|
|
152
|
+
commit(null);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const parsed = parseFloat(raw);
|
|
156
|
+
if (isNaN(parsed)) {
|
|
157
|
+
// Revert to last known good value
|
|
158
|
+
setInputText(currentValue != null ? formatNum(currentValue, formatOptions) : "");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const clamped = clamp ? clampValue(parsed, min, max) : parsed;
|
|
162
|
+
setInputText(formatNum(clamped, formatOptions));
|
|
163
|
+
commit(clamped);
|
|
164
|
+
}, [inputText, currentValue, clamp, min, max, formatOptions, commit]);
|
|
165
|
+
|
|
166
|
+
// Keep display text in sync with controlled value changes (only when not editing)
|
|
167
|
+
const prevControlledValue = useRef(valueProp);
|
|
168
|
+
if (isControlled && valueProp !== prevControlledValue.current && !isEditing.current) {
|
|
169
|
+
prevControlledValue.current = valueProp;
|
|
170
|
+
const nextText = valueProp != null ? formatNum(valueProp, formatOptions) : "";
|
|
171
|
+
if (nextText !== inputText) setInputText(nextText);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const decrementDisabled =
|
|
175
|
+
disabled || readOnly || (min !== undefined && (currentValue ?? 0) <= min);
|
|
176
|
+
const incrementDisabled =
|
|
177
|
+
disabled || readOnly || (max !== undefined && (currentValue ?? 0) >= max);
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<InputGroup ref={ref} className={cn("w-36", className)} {...props}>
|
|
181
|
+
<InputGroupButton
|
|
182
|
+
aria-label="Decrease"
|
|
183
|
+
tabIndex={-1}
|
|
184
|
+
disabled={decrementDisabled}
|
|
185
|
+
onClick={() => applyStep(-step)}
|
|
186
|
+
className="rounded-r-none"
|
|
187
|
+
>
|
|
188
|
+
<Minus aria-hidden="true" />
|
|
189
|
+
</InputGroupButton>
|
|
190
|
+
<InputGroupInput
|
|
191
|
+
type="number"
|
|
192
|
+
id={id}
|
|
193
|
+
name={name}
|
|
194
|
+
value={inputText}
|
|
195
|
+
placeholder={placeholder}
|
|
196
|
+
disabled={disabled}
|
|
197
|
+
readOnly={readOnly}
|
|
198
|
+
aria-label={ariaLabel}
|
|
199
|
+
aria-labelledby={ariaLabelledby}
|
|
200
|
+
aria-describedby={ariaDescribedby}
|
|
201
|
+
aria-invalid={ariaInvalid}
|
|
202
|
+
aria-required={ariaRequired}
|
|
203
|
+
className="text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
|
204
|
+
onChange={handleChange}
|
|
205
|
+
onKeyDown={handleKeyDown}
|
|
206
|
+
onBlur={handleBlur}
|
|
207
|
+
/>
|
|
208
|
+
<InputGroupButton
|
|
209
|
+
aria-label="Increase"
|
|
210
|
+
tabIndex={-1}
|
|
211
|
+
disabled={incrementDisabled}
|
|
212
|
+
onClick={() => applyStep(step)}
|
|
213
|
+
className="rounded-l-none"
|
|
214
|
+
>
|
|
215
|
+
<Plus aria-hidden="true" />
|
|
216
|
+
</InputGroupButton>
|
|
217
|
+
</InputGroup>
|
|
218
|
+
);
|
|
219
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PageShell, type PageShellProps } from "./page-shell";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { Plus } from "lucide-react";
|
|
3
|
+
import { expect, waitFor } from "storybook/test";
|
|
4
|
+
import { PageShell } from "./page-shell";
|
|
5
|
+
import { SectionHeader } from "../section-header";
|
|
6
|
+
import { Button } from "../button";
|
|
7
|
+
import { StatusBadge } from "../status-badge";
|
|
8
|
+
import { ResultCount, ViewToolbar } from "../view-toolbar";
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
title: "Layout/PageShell",
|
|
12
|
+
component: PageShell,
|
|
13
|
+
parameters: { layout: "fullscreen" },
|
|
14
|
+
} satisfies Meta<typeof PageShell>;
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof meta>;
|
|
17
|
+
|
|
18
|
+
export const Default: Story = {
|
|
19
|
+
render: () => (
|
|
20
|
+
<PageShell header={<SectionHeader title="Settings" description="Manage your workspace." />}>
|
|
21
|
+
<div className="rounded-xl border bg-card p-6 text-body text-muted-foreground">Content</div>
|
|
22
|
+
</PageShell>
|
|
23
|
+
),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `headerVariant="toolbar"` places a real `<ViewToolbar>` in `PageShell`'s
|
|
28
|
+
* header slot and pins it in POSITION (never a fixed height — see
|
|
29
|
+
* `Docs/View Toolbar Contract` R7) while the body scrolls beneath it. The
|
|
30
|
+
* outer `h-[420px] overflow-y-auto` div stands in for the `SidebarInset`
|
|
31
|
+
* scroll region `PageShell` is normally embedded in — `PageShell` itself
|
|
32
|
+
* never owns a scroll container (#367).
|
|
33
|
+
*/
|
|
34
|
+
export const ToolbarSticky: Story = {
|
|
35
|
+
render: function ToolbarStickyStory() {
|
|
36
|
+
return (
|
|
37
|
+
<div data-testid="scroll-region" className="h-[420px] overflow-y-auto">
|
|
38
|
+
<PageShell
|
|
39
|
+
headerVariant="toolbar"
|
|
40
|
+
header={
|
|
41
|
+
<ViewToolbar
|
|
42
|
+
info="Every pipeline run in this workspace from the last 30 days, newest first."
|
|
43
|
+
actions={
|
|
44
|
+
<Button size="sm">
|
|
45
|
+
<Plus aria-hidden="true" />
|
|
46
|
+
New
|
|
47
|
+
</Button>
|
|
48
|
+
}
|
|
49
|
+
>
|
|
50
|
+
<StatusBadge status="running" size="sm" />
|
|
51
|
+
<ResultCount count={24} total={128}>
|
|
52
|
+
runs
|
|
53
|
+
</ResultCount>
|
|
54
|
+
</ViewToolbar>
|
|
55
|
+
}
|
|
56
|
+
>
|
|
57
|
+
<div className="flex flex-col gap-3">
|
|
58
|
+
{Array.from({ length: 40 }, (_, i) => (
|
|
59
|
+
<div
|
|
60
|
+
key={i}
|
|
61
|
+
data-testid={i === 20 ? "marker-row" : undefined}
|
|
62
|
+
className="rounded-lg border bg-card p-4 text-body"
|
|
63
|
+
>
|
|
64
|
+
Row {i + 1}
|
|
65
|
+
</div>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
</PageShell>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
play: async ({ canvasElement }) => {
|
|
73
|
+
const scrollRegion = canvasElement.querySelector<HTMLElement>('[data-testid="scroll-region"]');
|
|
74
|
+
const header = canvasElement.querySelector<HTMLElement>(
|
|
75
|
+
'[data-slot="page-shell-toolbar-header"]',
|
|
76
|
+
);
|
|
77
|
+
const marker = canvasElement.querySelector<HTMLElement>('[data-testid="marker-row"]');
|
|
78
|
+
|
|
79
|
+
await expect(scrollRegion).not.toBeNull();
|
|
80
|
+
await expect(header).not.toBeNull();
|
|
81
|
+
await expect(marker).not.toBeNull();
|
|
82
|
+
|
|
83
|
+
await expect(getComputedStyle(header!).position).toBe("sticky");
|
|
84
|
+
|
|
85
|
+
// Scroll past PageShell's own top padding first, so the header is already
|
|
86
|
+
// in its "stuck" (top: 0) state before the two measurements below — at
|
|
87
|
+
// scrollTop 0 the header still sits at its natural, un-stuck flow
|
|
88
|
+
// position (offset by the page padding), which is not what "pinned" means.
|
|
89
|
+
scrollRegion!.scrollTop = 100;
|
|
90
|
+
const headerTopStuck = header!.getBoundingClientRect().top;
|
|
91
|
+
const markerTopFirstScroll = marker!.getBoundingClientRect().top;
|
|
92
|
+
|
|
93
|
+
// Scroll the body further beneath the already-pinned header.
|
|
94
|
+
scrollRegion!.scrollTop = 600;
|
|
95
|
+
|
|
96
|
+
// The body actually moved underneath the header.
|
|
97
|
+
await waitFor(() => {
|
|
98
|
+
expect(marker!.getBoundingClientRect().top).toBeLessThan(markerTopFirstScroll);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// The header stayed pinned: its position is unchanged across the further scroll.
|
|
102
|
+
const headerTopAfterScroll = header!.getBoundingClientRect().top;
|
|
103
|
+
await expect(Math.abs(headerTopAfterScroll - headerTopStuck)).toBeLessThanOrEqual(1);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { PageShell } from "./page-shell";
|
|
4
|
+
|
|
5
|
+
describe("PageShell", () => {
|
|
6
|
+
it("headerVariant omitted: renders the header inline with no sticky wrapper (AC2 — byte-identical default)", () => {
|
|
7
|
+
render(<PageShell header={<div data-testid="hdr">H</div>}>body</PageShell>);
|
|
8
|
+
|
|
9
|
+
const header = screen.getByTestId("hdr");
|
|
10
|
+
expect(screen.queryByTestId("page-shell-toolbar-header")).toBeNull();
|
|
11
|
+
expect(document.querySelector('[data-slot="page-shell-toolbar-header"]')).toBeNull();
|
|
12
|
+
|
|
13
|
+
let node: HTMLElement | null = header;
|
|
14
|
+
while (node) {
|
|
15
|
+
expect(node.className).not.toMatch(/(^|\s)sticky(\s|$)/);
|
|
16
|
+
node = node.parentElement;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('headerVariant="default": same as omitted — no sticky wrapper', () => {
|
|
21
|
+
render(
|
|
22
|
+
<PageShell header={<div data-testid="hdr">H</div>} headerVariant="default">
|
|
23
|
+
body
|
|
24
|
+
</PageShell>,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
expect(document.querySelector('[data-slot="page-shell-toolbar-header"]')).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('headerVariant="toolbar": wraps the header in a sticky, bordered, blurred bar', () => {
|
|
31
|
+
render(
|
|
32
|
+
<PageShell header={<div data-testid="hdr">H</div>} headerVariant="toolbar">
|
|
33
|
+
body
|
|
34
|
+
</PageShell>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const wrapper = document.querySelector('[data-slot="page-shell-toolbar-header"]');
|
|
38
|
+
expect(wrapper).not.toBeNull();
|
|
39
|
+
expect(wrapper).toContainElement(screen.getByTestId("hdr"));
|
|
40
|
+
|
|
41
|
+
const className = (wrapper as HTMLElement).className;
|
|
42
|
+
expect(className).toMatch(/(^|\s)sticky(\s|$)/);
|
|
43
|
+
expect(className).toContain("top-0");
|
|
44
|
+
expect(className).toContain("border-b");
|
|
45
|
+
expect(className).toContain("bg-background/95");
|
|
46
|
+
|
|
47
|
+
// Regression guard for ViewToolbar's R7 (wrap, never clip/fixed-height).
|
|
48
|
+
expect(className).not.toMatch(/(^|\s)h-\d/);
|
|
49
|
+
expect(className).not.toMatch(/overflow-hidden/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('headerVariant="toolbar" with no header prop: renders nothing, no empty sticky bar', () => {
|
|
53
|
+
render(<PageShell headerVariant="toolbar">body</PageShell>);
|
|
54
|
+
|
|
55
|
+
expect(document.querySelector('[data-slot="page-shell-toolbar-header"]')).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface PageShellProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
/** Optional header row (often a <SectionHeader />, or a <ViewToolbar> — see headerVariant). */
|
|
7
|
+
header?: ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* "default" (unset) — `header` renders inline, scrolls with the body.
|
|
10
|
+
* Byte-identical to today.
|
|
11
|
+
*
|
|
12
|
+
* "toolbar" — for a `<ViewToolbar>` header (`@elabs-ai/components-ui`,
|
|
13
|
+
* `Docs/View Toolbar Contract`): wraps `header` in a `sticky top-0` container
|
|
14
|
+
* with an opaque/blurred fill and a bottom hairline, so it stays pinned while
|
|
15
|
+
* the body scrolls beneath it. PLACES whatever node `header` is — it does not
|
|
16
|
+
* reimplement ViewToolbar's own info/left-cluster/actions grammar, and it does
|
|
17
|
+
* NOT cap the row's height (a `<ViewToolbar>` wraps at narrow widths; see
|
|
18
|
+
* `Docs/View Toolbar Contract` R7).
|
|
19
|
+
*/
|
|
20
|
+
headerVariant?: "default" | "toolbar";
|
|
21
|
+
/** Constrain content width. Defaults to "xl". */
|
|
22
|
+
width?: "md" | "lg" | "xl" | "full";
|
|
23
|
+
className?: string;
|
|
24
|
+
contentClassName?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const widthMap = {
|
|
28
|
+
md: "max-w-3xl",
|
|
29
|
+
lg: "max-w-5xl",
|
|
30
|
+
xl: "max-w-7xl",
|
|
31
|
+
full: "max-w-none",
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
/** Page-level content container with consistent padding and max width. */
|
|
35
|
+
export function PageShell({
|
|
36
|
+
children,
|
|
37
|
+
header,
|
|
38
|
+
headerVariant = "default",
|
|
39
|
+
width = "xl",
|
|
40
|
+
className,
|
|
41
|
+
contentClassName,
|
|
42
|
+
}: PageShellProps) {
|
|
43
|
+
return (
|
|
44
|
+
<div className={cn("w-full px-4 py-6 sm:px-6 lg:px-8", className)}>
|
|
45
|
+
<div className={cn("mx-auto w-full space-y-6", widthMap[width], contentClassName)}>
|
|
46
|
+
{header ? (
|
|
47
|
+
headerVariant === "toolbar" ? (
|
|
48
|
+
<div
|
|
49
|
+
data-slot="page-shell-toolbar-header"
|
|
50
|
+
className="sticky top-0 z-10 border-b border-border bg-background/95 py-3 backdrop-blur"
|
|
51
|
+
>
|
|
52
|
+
{header}
|
|
53
|
+
</div>
|
|
54
|
+
) : (
|
|
55
|
+
header
|
|
56
|
+
)
|
|
57
|
+
) : null}
|
|
58
|
+
{children}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import {
|
|
3
|
+
Pagination,
|
|
4
|
+
PaginationContent,
|
|
5
|
+
PaginationEllipsis,
|
|
6
|
+
PaginationItem,
|
|
7
|
+
PaginationLink,
|
|
8
|
+
PaginationNext,
|
|
9
|
+
PaginationPrevious,
|
|
10
|
+
} from "./pagination";
|
|
11
|
+
const meta = {
|
|
12
|
+
title: "Navigation/Pagination",
|
|
13
|
+
component: Pagination,
|
|
14
|
+
tags: ["autodocs"],
|
|
15
|
+
argTypes: {
|
|
16
|
+
className: {
|
|
17
|
+
description: "Additional CSS classes applied to the pagination nav element.",
|
|
18
|
+
control: "text",
|
|
19
|
+
table: { category: "Styling" },
|
|
20
|
+
},
|
|
21
|
+
children: {
|
|
22
|
+
description: "Pagination content — compose with PaginationContent, PaginationItem, etc.",
|
|
23
|
+
control: false,
|
|
24
|
+
table: { category: "Content" },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
} satisfies Meta<typeof Pagination>;
|
|
28
|
+
export default meta;
|
|
29
|
+
type Story = StoryObj<typeof meta>;
|
|
30
|
+
export const Default: Story = {
|
|
31
|
+
render: () => (
|
|
32
|
+
<Pagination>
|
|
33
|
+
<PaginationContent>
|
|
34
|
+
<PaginationItem>
|
|
35
|
+
<PaginationPrevious href="#" />
|
|
36
|
+
</PaginationItem>
|
|
37
|
+
<PaginationItem>
|
|
38
|
+
<PaginationLink href="#">1</PaginationLink>
|
|
39
|
+
</PaginationItem>
|
|
40
|
+
<PaginationItem>
|
|
41
|
+
<PaginationLink href="#" isActive>
|
|
42
|
+
2
|
|
43
|
+
</PaginationLink>
|
|
44
|
+
</PaginationItem>
|
|
45
|
+
<PaginationItem>
|
|
46
|
+
<PaginationLink href="#">3</PaginationLink>
|
|
47
|
+
</PaginationItem>
|
|
48
|
+
<PaginationItem>
|
|
49
|
+
<PaginationEllipsis />
|
|
50
|
+
</PaginationItem>
|
|
51
|
+
<PaginationItem>
|
|
52
|
+
<PaginationNext href="#" />
|
|
53
|
+
</PaginationItem>
|
|
54
|
+
</PaginationContent>
|
|
55
|
+
</Pagination>
|
|
56
|
+
),
|
|
57
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { forwardRef, type ComponentProps } from "react";
|
|
2
|
+
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { buttonVariants } from "../button";
|
|
5
|
+
|
|
6
|
+
export function Pagination({ className, ...props }: ComponentProps<"nav">) {
|
|
7
|
+
return (
|
|
8
|
+
<nav
|
|
9
|
+
role="navigation"
|
|
10
|
+
aria-label="pagination"
|
|
11
|
+
className={cn("mx-auto flex w-full justify-center", className)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
export const PaginationContent = forwardRef<HTMLUListElement, ComponentProps<"ul">>(
|
|
17
|
+
function PaginationContent({ className, ...props }, ref) {
|
|
18
|
+
return (
|
|
19
|
+
<ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
|
|
20
|
+
);
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
export const PaginationItem = forwardRef<HTMLLIElement, ComponentProps<"li">>(
|
|
24
|
+
function PaginationItem(props, ref) {
|
|
25
|
+
return <li ref={ref} {...props} />;
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
type PaginationLinkProps = { isActive?: boolean } & ComponentProps<"a">;
|
|
29
|
+
export function PaginationLink({ className, isActive, ...props }: PaginationLinkProps) {
|
|
30
|
+
return (
|
|
31
|
+
<a
|
|
32
|
+
aria-current={isActive ? "page" : undefined}
|
|
33
|
+
className={cn(
|
|
34
|
+
buttonVariants({ variant: isActive ? "outline" : "ghost", size: "icon" }),
|
|
35
|
+
"cursor-pointer",
|
|
36
|
+
className,
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
export function PaginationPrevious({ className, ...props }: ComponentProps<"a">) {
|
|
43
|
+
return (
|
|
44
|
+
<a
|
|
45
|
+
aria-label="Go to previous page"
|
|
46
|
+
className={cn(buttonVariants({ variant: "ghost" }), "gap-1 ps-2.5 cursor-pointer", className)}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
<ChevronLeft className="size-4" data-rtl-flip />
|
|
50
|
+
<span>Previous</span>
|
|
51
|
+
</a>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
export function PaginationNext({ className, ...props }: ComponentProps<"a">) {
|
|
55
|
+
return (
|
|
56
|
+
<a
|
|
57
|
+
aria-label="Go to next page"
|
|
58
|
+
className={cn(buttonVariants({ variant: "ghost" }), "gap-1 pe-2.5 cursor-pointer", className)}
|
|
59
|
+
{...props}
|
|
60
|
+
>
|
|
61
|
+
<span>Next</span>
|
|
62
|
+
<ChevronRight className="size-4" data-rtl-flip />
|
|
63
|
+
</a>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
export function PaginationEllipsis({ className, ...props }: ComponentProps<"span">) {
|
|
67
|
+
return (
|
|
68
|
+
<span
|
|
69
|
+
aria-hidden
|
|
70
|
+
className={cn("flex size-9 items-center justify-center", className)}
|
|
71
|
+
{...props}
|
|
72
|
+
>
|
|
73
|
+
<MoreHorizontal className="size-4" />
|
|
74
|
+
<span className="sr-only">More pages</span>
|
|
75
|
+
</span>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent } from "./popover";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, waitFor, within } from "storybook/test";
|
|
3
|
+
import { Button } from "../button";
|
|
4
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
5
|
+
const meta = { title: "Overlays/Popover", component: Popover, tags: ["autodocs"] } satisfies Meta<
|
|
6
|
+
typeof Popover
|
|
7
|
+
>;
|
|
8
|
+
export default meta;
|
|
9
|
+
type Story = StoryObj<typeof meta>;
|
|
10
|
+
export const Default: Story = {
|
|
11
|
+
render: () => (
|
|
12
|
+
<Popover>
|
|
13
|
+
<PopoverTrigger asChild>
|
|
14
|
+
<Button variant="outline">Open</Button>
|
|
15
|
+
</PopoverTrigger>
|
|
16
|
+
<PopoverContent>Place content for the popover here.</PopoverContent>
|
|
17
|
+
</Popover>
|
|
18
|
+
),
|
|
19
|
+
// Clicking the trigger mounts the popover content into the Radix portal.
|
|
20
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
21
|
+
await userEvent.click(canvas.getByRole("button", { name: "Open" }));
|
|
22
|
+
const body = within(canvasElement.ownerDocument.body);
|
|
23
|
+
const content = await body.findByText("Place content for the popover here.");
|
|
24
|
+
// waitFor rides out the Radix fade-in/zoom entrance animation (opacity
|
|
25
|
+
// starts at 0, which jest-dom counts as not-visible).
|
|
26
|
+
await waitFor(() => expect(content).toBeVisible());
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
5
|
+
describe("Popover", () => {
|
|
6
|
+
it("opens content on trigger click", async () => {
|
|
7
|
+
render(
|
|
8
|
+
<Popover>
|
|
9
|
+
<PopoverTrigger>Open</PopoverTrigger>
|
|
10
|
+
<PopoverContent>Inside</PopoverContent>
|
|
11
|
+
</Popover>,
|
|
12
|
+
);
|
|
13
|
+
await userEvent.click(screen.getByText("Open"));
|
|
14
|
+
expect(await screen.findByText("Inside")).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
|
|
2
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
|
|
5
|
+
export const Popover = PopoverPrimitive.Root;
|
|
6
|
+
export const PopoverTrigger = PopoverPrimitive.Trigger;
|
|
7
|
+
export const PopoverAnchor = PopoverPrimitive.Anchor;
|
|
8
|
+
|
|
9
|
+
export const PopoverContent = forwardRef<
|
|
10
|
+
ElementRef<typeof PopoverPrimitive.Content>,
|
|
11
|
+
ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
|
12
|
+
>(function PopoverContent({ className, align = "center", sideOffset = 4, ...props }, ref) {
|
|
13
|
+
return (
|
|
14
|
+
<PopoverPrimitive.Portal>
|
|
15
|
+
<PopoverPrimitive.Content
|
|
16
|
+
ref={ref}
|
|
17
|
+
align={align}
|
|
18
|
+
sideOffset={sideOffset}
|
|
19
|
+
className={cn(
|
|
20
|
+
"z-50 w-72 rounded-md bg-popover p-4 text-popover-foreground shadow-ring-md outline-none",
|
|
21
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
|
|
22
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
|
|
23
|
+
className,
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
</PopoverPrimitive.Portal>
|
|
28
|
+
);
|
|
29
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Progress, progressIndicatorVariants, type ProgressProps } from "./progress";
|