@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,285 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogBody,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogSection,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
} from "./dialog";
|
|
11
|
+
import { Label } from "../label";
|
|
12
|
+
|
|
13
|
+
function Wrapper({ size }: { size?: "sm" | "lg" | "xl" | "full" }) {
|
|
14
|
+
return (
|
|
15
|
+
<Dialog open>
|
|
16
|
+
<DialogContent size={size}>
|
|
17
|
+
<DialogTitle>Test dialog</DialogTitle>
|
|
18
|
+
<DialogDescription>Test description</DialogDescription>
|
|
19
|
+
Content
|
|
20
|
+
</DialogContent>
|
|
21
|
+
</Dialog>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("DialogContent size variants", () => {
|
|
26
|
+
it("default (no size prop) renders max-w-lg — regression lock", () => {
|
|
27
|
+
render(<Wrapper />);
|
|
28
|
+
const content = screen.getByRole("dialog");
|
|
29
|
+
expect(content.className).toContain("max-w-lg");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("size='lg' renders max-w-lg", () => {
|
|
33
|
+
render(<Wrapper size="lg" />);
|
|
34
|
+
const content = screen.getByRole("dialog");
|
|
35
|
+
expect(content.className).toContain("max-w-lg");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("size='sm' renders max-w-sm", () => {
|
|
39
|
+
render(<Wrapper size="sm" />);
|
|
40
|
+
const content = screen.getByRole("dialog");
|
|
41
|
+
expect(content.className).toContain("max-w-sm");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("size='xl' renders max-w-3xl", () => {
|
|
45
|
+
render(<Wrapper size="xl" />);
|
|
46
|
+
const content = screen.getByRole("dialog");
|
|
47
|
+
expect(content.className).toContain("max-w-3xl");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("size='full' renders max-w-[95vw] and h-[90vh]", () => {
|
|
51
|
+
render(<Wrapper size="full" />);
|
|
52
|
+
const content = screen.getByRole("dialog");
|
|
53
|
+
expect(content.className).toContain("max-w-[95vw]");
|
|
54
|
+
expect(content.className).toContain("h-[90vh]");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("DialogContent grid overflow guard (regression)", () => {
|
|
59
|
+
// The grid that lays out DialogContent's children must use a
|
|
60
|
+
// `minmax(0,1fr)` column, not the implicit `auto` column. Without the floor,
|
|
61
|
+
// a grid item's default `min-width:auto` lets a `truncate`/`nowrap` child or a
|
|
62
|
+
// long unbreakable token set the column's min-content size and push the dialog
|
|
63
|
+
// past its `max-w-*`. jsdom can't measure layout, so this locks the class that
|
|
64
|
+
// prevents it; the visual proof is the `OverflowGuard` Storybook story.
|
|
65
|
+
it("renders a minmax(0,1fr) grid column so truncate children can shrink", () => {
|
|
66
|
+
render(<Wrapper />);
|
|
67
|
+
const content = screen.getByRole("dialog");
|
|
68
|
+
expect(content.className).toContain("grid-cols-[minmax(0,1fr)]");
|
|
69
|
+
expect(content.className).toContain("grid");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("keeps the minmax column on every size variant", () => {
|
|
73
|
+
for (const size of ["sm", "lg", "xl", "full"] as const) {
|
|
74
|
+
const { unmount } = render(<Wrapper size={size} />);
|
|
75
|
+
expect(screen.getByRole("dialog").className).toContain("grid-cols-[minmax(0,1fr)]");
|
|
76
|
+
unmount();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("DialogContent height/scroll regime (#341)", () => {
|
|
82
|
+
// jsdom does NO layout, so these are CLASS assertions and nothing more — they
|
|
83
|
+
// lock the contract that produces the behaviour. The behaviour itself (header
|
|
84
|
+
// and footer fixed, body scrolling, at a short viewport) is proven in the
|
|
85
|
+
// `WideWithSections` / `ShortViewport` stories, which run in a real browser.
|
|
86
|
+
it("caps at the viewport and scrolls itself when no DialogBody is present", () => {
|
|
87
|
+
render(<Wrapper />);
|
|
88
|
+
const content = screen.getByRole("dialog");
|
|
89
|
+
expect(content.className).toContain("max-h-[calc(100dvh-2rem)]");
|
|
90
|
+
expect(content.className).toContain("overflow-y-auto");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("hands scroll to the body via a has-[] variant (inert without one)", () => {
|
|
94
|
+
render(<Wrapper />);
|
|
95
|
+
const content = screen.getByRole("dialog");
|
|
96
|
+
expect(content.className).toContain("has-[[data-slot=dialog-body]]:overflow-hidden");
|
|
97
|
+
expect(content.className).toContain(
|
|
98
|
+
"has-[[data-slot=dialog-body]]:grid-rows-[auto_minmax(0,1fr)_auto]",
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("size='full' keeps its own overflow/height after the merge", () => {
|
|
103
|
+
render(<Wrapper size="full" />);
|
|
104
|
+
const content = screen.getByRole("dialog");
|
|
105
|
+
// tailwind-merge resolves the base `overflow-y-auto` away in favour of the
|
|
106
|
+
// later variant utility, so `full` still lets its body own the scroll.
|
|
107
|
+
expect(content.className).toContain("overflow-hidden");
|
|
108
|
+
expect(content.className).not.toContain("overflow-y-auto");
|
|
109
|
+
expect(content.className).toContain("max-h-[90vh]");
|
|
110
|
+
expect(content.className).not.toContain("max-h-[calc(100dvh-2rem)]");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("DialogBody", () => {
|
|
115
|
+
it("declares the dialog-body slot and owns the scroll", () => {
|
|
116
|
+
render(
|
|
117
|
+
<Dialog open>
|
|
118
|
+
<DialogContent>
|
|
119
|
+
<DialogTitle>Test dialog</DialogTitle>
|
|
120
|
+
<DialogDescription>Test description</DialogDescription>
|
|
121
|
+
<DialogBody>Body</DialogBody>
|
|
122
|
+
</DialogContent>
|
|
123
|
+
</Dialog>,
|
|
124
|
+
);
|
|
125
|
+
const body = screen.getByText("Body");
|
|
126
|
+
expect(body.getAttribute("data-slot")).toBe("dialog-body");
|
|
127
|
+
expect(body.className).toContain("overflow-y-auto");
|
|
128
|
+
expect(body.className).toContain("min-h-0");
|
|
129
|
+
// Scroll must not chain to the page behind the overlay.
|
|
130
|
+
expect(body.className).toContain("overscroll-contain");
|
|
131
|
+
// Focus-ring gutter (#425): `p-1`/`-m-1`/`scroll-p-1` reserve 4px of
|
|
132
|
+
// scrollport padding so an outward `box-shadow` ring on a flush child is
|
|
133
|
+
// not clipped, then pull the border box back so body content stays flush
|
|
134
|
+
// with the header/footer. jsdom performs no layout and no paint, so this
|
|
135
|
+
// can only assert the classes are DECLARED — it can never observe a
|
|
136
|
+
// `box-shadow` actually being clipped (or not). Its job is to stop a
|
|
137
|
+
// future "tidy the class string" edit from silently deleting the trio;
|
|
138
|
+
// the real geometric proof is the `FocusRingClearance` story, run in a
|
|
139
|
+
// real browser via `@storybook/addon-vitest`.
|
|
140
|
+
expect(body.className).toContain("p-1");
|
|
141
|
+
expect(body.className).toContain("-m-1");
|
|
142
|
+
expect(body.className).toContain("scroll-p-1");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("is keyboard-operable by default and lets a caller opt out", () => {
|
|
146
|
+
const { rerender } = render(
|
|
147
|
+
<Dialog open>
|
|
148
|
+
<DialogContent>
|
|
149
|
+
<DialogTitle>Test dialog</DialogTitle>
|
|
150
|
+
<DialogDescription>Test description</DialogDescription>
|
|
151
|
+
<DialogBody>Body</DialogBody>
|
|
152
|
+
</DialogContent>
|
|
153
|
+
</Dialog>,
|
|
154
|
+
);
|
|
155
|
+
expect(screen.getByText("Body")).toHaveAttribute("tabindex", "0");
|
|
156
|
+
|
|
157
|
+
rerender(
|
|
158
|
+
<Dialog open>
|
|
159
|
+
<DialogContent>
|
|
160
|
+
<DialogTitle>Test dialog</DialogTitle>
|
|
161
|
+
<DialogDescription>Test description</DialogDescription>
|
|
162
|
+
<DialogBody tabIndex={-1}>Body</DialogBody>
|
|
163
|
+
</DialogContent>
|
|
164
|
+
</Dialog>,
|
|
165
|
+
);
|
|
166
|
+
expect(screen.getByText("Body")).toHaveAttribute("tabindex", "-1");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("DialogBody opening focus", () => {
|
|
171
|
+
function Wrapped({ withBody }: { withBody: boolean }) {
|
|
172
|
+
const fields = (
|
|
173
|
+
<>
|
|
174
|
+
<label htmlFor="first">First</label>
|
|
175
|
+
<input id="first" />
|
|
176
|
+
</>
|
|
177
|
+
);
|
|
178
|
+
return (
|
|
179
|
+
<Dialog open>
|
|
180
|
+
<DialogContent>
|
|
181
|
+
<DialogTitle>Test dialog</DialogTitle>
|
|
182
|
+
<DialogDescription>Test description</DialogDescription>
|
|
183
|
+
{withBody ? <DialogBody>{fields}</DialogBody> : fields}
|
|
184
|
+
</DialogContent>
|
|
185
|
+
</Dialog>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// The body takes a tab stop so it can be scrolled from the keyboard; it must
|
|
190
|
+
// NOT therefore become the dialog's opening focus target — that would ring the
|
|
191
|
+
// whole scroll region on open and skip the first field.
|
|
192
|
+
it("skips the scroll wrapper and opens on the first real control", async () => {
|
|
193
|
+
render(<Wrapped withBody />);
|
|
194
|
+
const input = screen.getByLabelText("First");
|
|
195
|
+
await waitFor(() => expect(document.activeElement).toBe(input));
|
|
196
|
+
expect(document.activeElement).not.toBe(screen.getByText("First").closest("div"));
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("leaves a body-less dialog's focus behaviour untouched", async () => {
|
|
200
|
+
render(<Wrapped withBody={false} />);
|
|
201
|
+
await waitFor(() => expect(document.activeElement).toBe(screen.getByLabelText("First")));
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// A body with nothing focusable in it must fall back to Radix, which focuses
|
|
205
|
+
// the body — the right target for a text-only scroll region. Reaching past it
|
|
206
|
+
// to the footer would open the dialog with the keyboard on the PRIMARY action.
|
|
207
|
+
it("leaves a control-less body to Radix, which focuses the scroll region", async () => {
|
|
208
|
+
render(
|
|
209
|
+
<Dialog open>
|
|
210
|
+
<DialogContent>
|
|
211
|
+
<DialogTitle>Test dialog</DialogTitle>
|
|
212
|
+
<DialogDescription>Test description</DialogDescription>
|
|
213
|
+
<DialogBody>Nothing focusable in here.</DialogBody>
|
|
214
|
+
<button type="button">Save</button>
|
|
215
|
+
</DialogContent>
|
|
216
|
+
</Dialog>,
|
|
217
|
+
);
|
|
218
|
+
const body = screen.getByText("Nothing focusable in here.");
|
|
219
|
+
await waitFor(() => expect(document.activeElement).toBe(body));
|
|
220
|
+
expect(document.activeElement).not.toBe(screen.getByRole("button", { name: "Save" }));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// A candidate that matches the selector but cannot take focus (a `hidden` file
|
|
224
|
+
// input is the everyday case) must not consume the redirect: preventing
|
|
225
|
+
// Radix's default for it strands focus on the TRIGGER, outside the modal.
|
|
226
|
+
// NOT assertable here — jsdom does no layout and happily focuses a `hidden`
|
|
227
|
+
// input — so the lock is the `HiddenFirstControl` story, which runs in a real
|
|
228
|
+
// browser under `test-storybook`.
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("DialogSection", () => {
|
|
232
|
+
function SectionWrapper() {
|
|
233
|
+
return (
|
|
234
|
+
<Dialog open>
|
|
235
|
+
<DialogContent>
|
|
236
|
+
<DialogTitle>Test dialog</DialogTitle>
|
|
237
|
+
<DialogDescription>Test description</DialogDescription>
|
|
238
|
+
<DialogSection title="Retention" description="How long runs are kept.">
|
|
239
|
+
<Label htmlFor="days">Days to keep</Label>
|
|
240
|
+
</DialogSection>
|
|
241
|
+
</DialogContent>
|
|
242
|
+
</Dialog>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
it("renders a real heading one level below DialogTitle", () => {
|
|
247
|
+
render(<SectionWrapper />);
|
|
248
|
+
const heading = screen.getByRole("heading", { name: "Retention" });
|
|
249
|
+
expect(heading.tagName).toBe("H3");
|
|
250
|
+
expect(heading.getAttribute("data-slot")).toBe("dialog-section-title");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("renders the section title one type ROLE above a field label", () => {
|
|
254
|
+
render(<SectionWrapper />);
|
|
255
|
+
const heading = screen.getByRole("heading", { name: "Retention" });
|
|
256
|
+
const label = screen.getByText("Days to keep");
|
|
257
|
+
// The rungs must differ: section = `subtitle` (1rem), label = body (0.875rem).
|
|
258
|
+
// jsdom applies no stylesheet, so this asserts the ROLE; the rendered pixel
|
|
259
|
+
// sizes are compared with getComputedStyle in the `WideWithSections` story.
|
|
260
|
+
expect(heading.className).toContain("text-subtitle");
|
|
261
|
+
expect(label.className).not.toContain("text-subtitle");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("accepts a deeper level and slots its description and actions", () => {
|
|
265
|
+
render(
|
|
266
|
+
<Dialog open>
|
|
267
|
+
<DialogContent>
|
|
268
|
+
<DialogTitle>Test dialog</DialogTitle>
|
|
269
|
+
<DialogDescription>Test description</DialogDescription>
|
|
270
|
+
<DialogSection
|
|
271
|
+
level={4}
|
|
272
|
+
title="Retention"
|
|
273
|
+
description="How long runs are kept."
|
|
274
|
+
actions={<button type="button">Reset</button>}
|
|
275
|
+
/>
|
|
276
|
+
</DialogContent>
|
|
277
|
+
</Dialog>,
|
|
278
|
+
);
|
|
279
|
+
expect(screen.getByRole("heading", { name: "Retention" }).tagName).toBe("H4");
|
|
280
|
+
expect(screen.getByText("How long runs are kept.").getAttribute("data-slot")).toBe(
|
|
281
|
+
"dialog-section-description",
|
|
282
|
+
);
|
|
283
|
+
expect(screen.getByRole("button", { name: "Reset" })).toBeInTheDocument();
|
|
284
|
+
});
|
|
285
|
+
});
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useRef,
|
|
5
|
+
type ComponentPropsWithoutRef,
|
|
6
|
+
type ElementRef,
|
|
7
|
+
type HTMLAttributes,
|
|
8
|
+
type MutableRefObject,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
12
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
13
|
+
import { cn } from "../../lib/cn";
|
|
14
|
+
import { Heading, Text } from "../typography";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The first keyboard-reachable control inside a dialog, EXCLUDING the
|
|
18
|
+
* `DialogBody` wrapper itself. Deliberately a pragmatic selector rather than a
|
|
19
|
+
* full tabbability implementation — Radix owns focus trapping; this only
|
|
20
|
+
* reorders the opening target (see `DialogContent`'s `onOpenAutoFocus`).
|
|
21
|
+
*/
|
|
22
|
+
const TABBABLE = [
|
|
23
|
+
"a[href]",
|
|
24
|
+
"button:not([disabled])",
|
|
25
|
+
"input:not([disabled])",
|
|
26
|
+
"select:not([disabled])",
|
|
27
|
+
"textarea:not([disabled])",
|
|
28
|
+
'[tabindex]:not([tabindex="-1"]):not([data-slot="dialog-body"])',
|
|
29
|
+
].join(",");
|
|
30
|
+
|
|
31
|
+
export const dialogContentVariants = cva(
|
|
32
|
+
// `grid-cols-[minmax(0,1fr)]` (NOT the implicit `auto` column): grid items
|
|
33
|
+
// default to `min-width:auto`, so a `nowrap`/`truncate` child or a long
|
|
34
|
+
// unbreakable token (a file path, a URL) sets the column's min-content size and
|
|
35
|
+
// pushes the dialog past its `max-w-*`. A `minmax(0,1fr)` column floors that at
|
|
36
|
+
// 0, so inner `truncate`/`w-full` shrink instead of overflowing (#regression).
|
|
37
|
+
//
|
|
38
|
+
// Height/scroll (#341). Two mutually-exclusive regimes, picked by ANATOMY —
|
|
39
|
+
// never by a `scroll` prop (`.claude/rules/component-api.md`: `cva` is for
|
|
40
|
+
// visual axes, behaviour is composition):
|
|
41
|
+
// 1. NO `DialogBody` — the content box itself caps at the viewport
|
|
42
|
+
// (`max-h-…` + `overflow-y-auto`) and scrolls as one column. Before this,
|
|
43
|
+
// a taller-than-viewport dialog overflowed past BOTH viewport edges and
|
|
44
|
+
// its top was unreachable (the `-translate-y-1/2` centring).
|
|
45
|
+
// 2. WITH a `DialogBody` — the `has-[…]` pair turns the content box into a
|
|
46
|
+
// 3-row grid (`auto | minmax(0,1fr) | auto`) and stops it scrolling, so a
|
|
47
|
+
// fixed header and footer sandwich a body that owns the scroll. The pair
|
|
48
|
+
// is INERT for every dialog that has no `DialogBody`.
|
|
49
|
+
// `size="full"`'s own `h-/max-h-/overflow-hidden` still wins — cva emits the
|
|
50
|
+
// base first, so tailwind-merge resolves the later variant utilities over it.
|
|
51
|
+
"fixed left-1/2 top-1/2 z-50 grid grid-cols-[minmax(0,1fr)] w-full -translate-x-1/2 -translate-y-1/2 gap-4 max-h-[calc(100dvh-2rem)] overflow-y-auto overscroll-contain has-[[data-slot=dialog-body]]:overflow-hidden has-[[data-slot=dialog-body]]:grid-rows-[auto_minmax(0,1fr)_auto] rounded-xl bg-card p-6 text-card-foreground shadow-ring-lg 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",
|
|
52
|
+
{
|
|
53
|
+
variants: {
|
|
54
|
+
size: {
|
|
55
|
+
sm: "max-w-sm",
|
|
56
|
+
lg: "max-w-lg",
|
|
57
|
+
xl: "max-w-3xl",
|
|
58
|
+
full: "max-w-[95vw] h-[90vh] max-h-[90vh] overflow-hidden",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
defaultVariants: { size: "lg" },
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
export const Dialog = DialogPrimitive.Root;
|
|
66
|
+
export const DialogTrigger = DialogPrimitive.Trigger;
|
|
67
|
+
export const DialogPortal = DialogPrimitive.Portal;
|
|
68
|
+
export const DialogClose = DialogPrimitive.Close;
|
|
69
|
+
|
|
70
|
+
export const DialogOverlay = forwardRef<
|
|
71
|
+
ElementRef<typeof DialogPrimitive.Overlay>,
|
|
72
|
+
ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
73
|
+
>(function DialogOverlay({ className, ...props }, ref) {
|
|
74
|
+
return (
|
|
75
|
+
<DialogPrimitive.Overlay
|
|
76
|
+
ref={ref}
|
|
77
|
+
data-slot="dialog-overlay"
|
|
78
|
+
className={cn(
|
|
79
|
+
"fixed inset-0 z-50 bg-foreground/50 backdrop-blur-sm",
|
|
80
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const DialogContent = forwardRef<
|
|
89
|
+
ElementRef<typeof DialogPrimitive.Content>,
|
|
90
|
+
ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &
|
|
91
|
+
VariantProps<typeof dialogContentVariants>
|
|
92
|
+
>(function DialogContent({ className, children, size, onOpenAutoFocus, ...props }, ref) {
|
|
93
|
+
const contentRef = useRef<HTMLDivElement | null>(null);
|
|
94
|
+
const setRefs = useCallback(
|
|
95
|
+
(node: HTMLDivElement | null) => {
|
|
96
|
+
contentRef.current = node;
|
|
97
|
+
if (typeof ref === "function") ref(node);
|
|
98
|
+
else if (ref) (ref as MutableRefObject<HTMLDivElement | null>).current = node;
|
|
99
|
+
},
|
|
100
|
+
[ref],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A `DialogBody` carries `tabIndex={0}` so a keyboard user can scroll it — but
|
|
105
|
+
* being the first tab stop would also make the scroll WRAPPER the dialog's
|
|
106
|
+
* opening focus target, which paints a focus ring around the whole body on
|
|
107
|
+
* open and skips the first field. Skip past it to the first real control
|
|
108
|
+
* INSIDE the body. Inert for dialogs with no `DialogBody`, so nothing that
|
|
109
|
+
* shipped before changes.
|
|
110
|
+
*
|
|
111
|
+
* Two things this deliberately does NOT do, both because getting them wrong
|
|
112
|
+
* strands the keyboard:
|
|
113
|
+
* * It only prevents Radix's default once a candidate has *actually taken*
|
|
114
|
+
* focus. A selector match is not proof of focusability — a `display:none`
|
|
115
|
+
* candidate (the ubiquitous `<input type="file" hidden>`) swallows
|
|
116
|
+
* `.focus()`, so preventing the default for it would leave focus on the
|
|
117
|
+
* TRIGGER, outside the modal.
|
|
118
|
+
* * It gives up when the body holds no focusable control at all. Radix's own
|
|
119
|
+
* fallback then focuses the body — which is the right target for a
|
|
120
|
+
* text-only scroll region, and better than reaching past it to the
|
|
121
|
+
* footer's primary action.
|
|
122
|
+
*/
|
|
123
|
+
const handleOpenAutoFocus = useCallback(
|
|
124
|
+
(event: Event) => {
|
|
125
|
+
onOpenAutoFocus?.(event);
|
|
126
|
+
if (event.defaultPrevented) return;
|
|
127
|
+
const body = contentRef.current?.querySelector<HTMLElement>('[data-slot="dialog-body"]');
|
|
128
|
+
if (!body) return;
|
|
129
|
+
for (const candidate of body.querySelectorAll<HTMLElement>(TABBABLE)) {
|
|
130
|
+
candidate.focus({ preventScroll: true });
|
|
131
|
+
if (candidate.ownerDocument.activeElement !== candidate) continue;
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
[onOpenAutoFocus],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<DialogPortal>
|
|
141
|
+
<DialogOverlay />
|
|
142
|
+
<DialogPrimitive.Content
|
|
143
|
+
ref={setRefs}
|
|
144
|
+
data-slot="dialog-content"
|
|
145
|
+
className={cn(dialogContentVariants({ size }), className)}
|
|
146
|
+
onOpenAutoFocus={handleOpenAutoFocus}
|
|
147
|
+
{...props}
|
|
148
|
+
>
|
|
149
|
+
{children}
|
|
150
|
+
<DialogPrimitive.Close
|
|
151
|
+
data-slot="dialog-close"
|
|
152
|
+
className="absolute end-4 top-4 rounded-md p-1 text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
153
|
+
aria-label="Close"
|
|
154
|
+
>
|
|
155
|
+
<svg
|
|
156
|
+
width="16"
|
|
157
|
+
height="16"
|
|
158
|
+
viewBox="0 0 24 24"
|
|
159
|
+
fill="none"
|
|
160
|
+
stroke="currentColor"
|
|
161
|
+
strokeWidth="2"
|
|
162
|
+
strokeLinecap="round"
|
|
163
|
+
strokeLinejoin="round"
|
|
164
|
+
aria-hidden="true"
|
|
165
|
+
>
|
|
166
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
167
|
+
</svg>
|
|
168
|
+
</DialogPrimitive.Close>
|
|
169
|
+
</DialogPrimitive.Content>
|
|
170
|
+
</DialogPortal>
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
export function DialogHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
175
|
+
return (
|
|
176
|
+
<div
|
|
177
|
+
data-slot="dialog-header"
|
|
178
|
+
className={cn("flex flex-col gap-1.5 text-start", className)}
|
|
179
|
+
{...props}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function DialogFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
185
|
+
return (
|
|
186
|
+
<div
|
|
187
|
+
data-slot="dialog-footer"
|
|
188
|
+
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
|
189
|
+
{...props}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export const DialogTitle = forwardRef<
|
|
195
|
+
ElementRef<typeof DialogPrimitive.Title>,
|
|
196
|
+
ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
197
|
+
>(function DialogTitle({ className, ...props }, ref) {
|
|
198
|
+
return (
|
|
199
|
+
<DialogPrimitive.Title
|
|
200
|
+
ref={ref}
|
|
201
|
+
data-slot="dialog-title"
|
|
202
|
+
className={cn("text-title leading-none", className)}
|
|
203
|
+
{...props}
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
export const DialogDescription = forwardRef<
|
|
209
|
+
ElementRef<typeof DialogPrimitive.Description>,
|
|
210
|
+
ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
211
|
+
>(function DialogDescription({ className, ...props }, ref) {
|
|
212
|
+
return (
|
|
213
|
+
<DialogPrimitive.Description
|
|
214
|
+
ref={ref}
|
|
215
|
+
data-slot="dialog-description"
|
|
216
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
217
|
+
{...props}
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
export type DialogBodyProps = HTMLAttributes<HTMLDivElement>;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The dialog's scroll owner (#341).
|
|
226
|
+
*
|
|
227
|
+
* Placing a `DialogBody` between `DialogHeader` and `DialogFooter` switches
|
|
228
|
+
* `DialogContent` into its fixed-header/footer regime (see
|
|
229
|
+
* `dialogContentVariants`): the header and footer stay put and ONLY this region
|
|
230
|
+
* scrolls, so the primary action never scrolls out of view. Without it a dialog
|
|
231
|
+
* behaves exactly as before — the anatomy is the switch, there is no `scroll`
|
|
232
|
+
* prop.
|
|
233
|
+
*
|
|
234
|
+
* `overscroll-contain` stops the scroll chaining to the page underneath
|
|
235
|
+
* (`.claude/rules/interaction-guidelines.md`, Touch & overscroll).
|
|
236
|
+
*
|
|
237
|
+
* **The `p-1`/`-m-1`/`scroll-p-1` trio reserves a focus-ring gutter (#425).**
|
|
238
|
+
* Per CSS Overflow 3, authoring only `overflow-y-auto` still makes this a
|
|
239
|
+
* two-axis scroll container (the `visible` axis computes to `auto` too), and a
|
|
240
|
+
* scroll container clips at its **padding box**. With no padding of its own,
|
|
241
|
+
* that padding box IS the content box, so a full-width child's border box
|
|
242
|
+
* lands exactly on the clip edge — leaving 0px for a focus ring, which is
|
|
243
|
+
* `box-shadow` (ink overflow: never part of the scrollable region, so it is
|
|
244
|
+
* simply cut, not merely scrolled out of reach). `p-1` moves the scrollport
|
|
245
|
+
* 4px outward on every side — enough for the library's widest outward paint
|
|
246
|
+
* (`ring-2 ring-offset-2` = 4px) — and `-m-1` pulls the border box back by the
|
|
247
|
+
* same amount so body content stays flush with `DialogHeader`/`DialogFooter`
|
|
248
|
+
* instead of indenting 4px against them. `scroll-p-1` keeps a keyboard-driven
|
|
249
|
+
* scroll-into-view (Tab landing on a control currently outside the scrollport)
|
|
250
|
+
* from placing that control flush against the scrollport edge again.
|
|
251
|
+
*
|
|
252
|
+
* Two consumer-facing consequences: a larger `className` padding (e.g. `p-6`)
|
|
253
|
+
* still wins through `tailwind-merge`, with the border box remaining 4px
|
|
254
|
+
* outward; but **`className="p-0"` re-opens the bug** — the merge kills `p-1`
|
|
255
|
+
* while `-m-1` survives — so a consumer who genuinely wants no gutter must
|
|
256
|
+
* clear both sides with `className="m-0 p-0"`.
|
|
257
|
+
*
|
|
258
|
+
* Known, accepted residual: at `data-density="compact"`, `--spacing` is
|
|
259
|
+
* `0.222rem` (`density.css:97`), so `p-1` is 3.55px against a 4px ring — a
|
|
260
|
+
* 0.45px shortfall on the outermost edge of a 2px stroke. Do not "fix" this
|
|
261
|
+
* with a fixed-px override; that would break the density dial's contract.
|
|
262
|
+
*/
|
|
263
|
+
export const DialogBody = forwardRef<HTMLDivElement, DialogBodyProps>(function DialogBody(
|
|
264
|
+
{ className, tabIndex, ...props },
|
|
265
|
+
ref,
|
|
266
|
+
) {
|
|
267
|
+
return (
|
|
268
|
+
<div
|
|
269
|
+
ref={ref}
|
|
270
|
+
data-slot="dialog-body"
|
|
271
|
+
// A scroll container must be keyboard-operable (WCAG 2.1.1; axe's
|
|
272
|
+
// `scrollable-region-focusable`). A body holding only text has no focusable
|
|
273
|
+
// child to scroll from, so the region itself takes the tab stop. Pass
|
|
274
|
+
// `tabIndex={-1}` to opt out when the body always contains its own
|
|
275
|
+
// focusable content.
|
|
276
|
+
tabIndex={tabIndex ?? 0}
|
|
277
|
+
className={cn(
|
|
278
|
+
"-m-1 min-h-0 overflow-y-auto overscroll-contain p-1 scroll-p-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
279
|
+
className,
|
|
280
|
+
)}
|
|
281
|
+
{...props}
|
|
282
|
+
/>
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
export interface DialogSectionProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
287
|
+
/** The section heading. States what the group of fields is for. */
|
|
288
|
+
title: ReactNode;
|
|
289
|
+
/** One line of supporting context under the heading. */
|
|
290
|
+
description?: ReactNode;
|
|
291
|
+
/**
|
|
292
|
+
* Semantic heading level. `DialogTitle` renders an `<h2>`, so a section is an
|
|
293
|
+
* `<h3>` by default and an `<h4>` when nested a rung deeper.
|
|
294
|
+
*/
|
|
295
|
+
level?: 3 | 4;
|
|
296
|
+
/** Section-scoped actions, aligned to the inline end of the heading row. */
|
|
297
|
+
actions?: ReactNode;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* A real section heading inside a dialog (#341).
|
|
302
|
+
*
|
|
303
|
+
* The recurring downstream defect was section labels rendering at the same size
|
|
304
|
+
* as the field labels beneath them, which flattens a long form into one
|
|
305
|
+
* undifferentiated list. The rungs are distinct BY TOKEN, not by eye:
|
|
306
|
+
* `DialogTitle` `text-title` (1.25rem) > `DialogSection` `text-subtitle` (1rem) >
|
|
307
|
+
* `Label` `text-body` (0.875rem).
|
|
308
|
+
*
|
|
309
|
+
* Deliberately NOT `SectionHeader` (`../section-header`): that is the page-level
|
|
310
|
+
* `<h2>` row with an eyebrow, one rung too loud and one level too high inside a
|
|
311
|
+
* dialog whose title already owns the `<h2>`.
|
|
312
|
+
*/
|
|
313
|
+
export const DialogSection = forwardRef<HTMLElement, DialogSectionProps>(function DialogSection(
|
|
314
|
+
{ className, title, description, level = 3, actions, children, ...props },
|
|
315
|
+
ref,
|
|
316
|
+
) {
|
|
317
|
+
return (
|
|
318
|
+
<section
|
|
319
|
+
ref={ref}
|
|
320
|
+
data-slot="dialog-section"
|
|
321
|
+
className={cn("flex flex-col gap-3", className)}
|
|
322
|
+
{...props}
|
|
323
|
+
>
|
|
324
|
+
<div className="flex items-start justify-between gap-3">
|
|
325
|
+
<div className="flex min-w-0 flex-col gap-1">
|
|
326
|
+
<Heading level={level} size="subtitle" data-slot="dialog-section-title">
|
|
327
|
+
{title}
|
|
328
|
+
</Heading>
|
|
329
|
+
{description ? (
|
|
330
|
+
<Text variant="caption" tone="muted" data-slot="dialog-section-description">
|
|
331
|
+
{description}
|
|
332
|
+
</Text>
|
|
333
|
+
) : null}
|
|
334
|
+
</div>
|
|
335
|
+
{actions ? <div className="shrink-0">{actions}</div> : null}
|
|
336
|
+
</div>
|
|
337
|
+
{children}
|
|
338
|
+
</section>
|
|
339
|
+
);
|
|
340
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
dialogContentVariants,
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogTrigger,
|
|
5
|
+
DialogPortal,
|
|
6
|
+
DialogClose,
|
|
7
|
+
DialogOverlay,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
DialogDescription,
|
|
13
|
+
DialogBody,
|
|
14
|
+
DialogSection,
|
|
15
|
+
} from "./dialog";
|
|
16
|
+
export type { DialogBodyProps, DialogSectionProps } from "./dialog";
|
|
17
|
+
export { useDialogDismissGuard } from "./use-dialog-dismiss-guard";
|
|
18
|
+
export type { UseDialogDismissGuardOptions, DialogDismissGuard } from "./use-dialog-dismiss-guard";
|