@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,301 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
forwardRef,
|
|
4
|
+
use,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type ComponentPropsWithoutRef,
|
|
10
|
+
type ElementRef,
|
|
11
|
+
type HTMLAttributes,
|
|
12
|
+
type ReactNode,
|
|
13
|
+
type RefObject,
|
|
14
|
+
} from "react";
|
|
15
|
+
import { Command as CommandPrimitive, useCommandState } from "cmdk";
|
|
16
|
+
import { Search } from "lucide-react";
|
|
17
|
+
import { cn } from "../../lib/cn";
|
|
18
|
+
import { mergeRefs } from "../../lib/merge-refs";
|
|
19
|
+
import { Dialog, DialogContent } from "../dialog";
|
|
20
|
+
|
|
21
|
+
/** cmdk's own marker attribute on an item row — the scope for the id lookup. */
|
|
22
|
+
const CMDK_ITEM_SELECTOR = '[cmdk-item=""][aria-selected="true"]';
|
|
23
|
+
|
|
24
|
+
/** Distinguishes "no `<Command>` ancestor" from "nothing is highlighted". */
|
|
25
|
+
const NO_COMMAND_PROVIDER = Symbol("no-command-provider");
|
|
26
|
+
|
|
27
|
+
const CommandActiveItemIdContext = createContext<string | undefined | typeof NO_COMMAND_PROVIDER>(
|
|
28
|
+
NO_COMMAND_PROVIDER,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolves the DOM `id` of the currently highlighted `CommandItem`, publishes
|
|
33
|
+
* it on a context and reports it to `Command`'s `onActiveItemIdChange`.
|
|
34
|
+
*
|
|
35
|
+
* It renders no DOM of its own, and it deliberately does NOT simply forward
|
|
36
|
+
* cmdk's `state.selectedItemId`. cmdk only recomputes that field inside a
|
|
37
|
+
* `schedule(7, …)` callback queued from `setState("value", …)`, and its own
|
|
38
|
+
* `selectFirstItem()` runs INSIDE the layout-effect flush that drains that
|
|
39
|
+
* queue — so the callback executes before React has committed the new
|
|
40
|
+
* `aria-selected` attribute and cmdk's `getSelectedItem()` DOM query finds
|
|
41
|
+
* nothing. The field therefore stays `undefined` for BOTH cases that matter
|
|
42
|
+
* here: the item auto-highlighted on mount, and the item re-highlighted after
|
|
43
|
+
* every filter keystroke. (cmdk's own `Command.Input` inherits the same hole —
|
|
44
|
+
* it renders no `aria-activedescendant` in either state. Verified against
|
|
45
|
+
* cmdk 1.1.1 in jsdom and Chromium.)
|
|
46
|
+
*
|
|
47
|
+
* Reading the committed DOM from a post-commit `useEffect` — the same query
|
|
48
|
+
* cmdk itself uses, just at a point where the attribute exists — closes both
|
|
49
|
+
* gaps. `state.selectedItemId` is kept as a fallback and as one of the change
|
|
50
|
+
* triggers.
|
|
51
|
+
*/
|
|
52
|
+
function CommandActiveItemProvider({
|
|
53
|
+
rootRef,
|
|
54
|
+
onChange,
|
|
55
|
+
children,
|
|
56
|
+
}: {
|
|
57
|
+
rootRef: RefObject<HTMLDivElement | null>;
|
|
58
|
+
onChange?: (id: string | undefined) => void;
|
|
59
|
+
children: ReactNode;
|
|
60
|
+
}) {
|
|
61
|
+
const value = useCommandState((state) => state.value);
|
|
62
|
+
const selectedItemId = useCommandState((state) => state.selectedItemId);
|
|
63
|
+
const filteredCount = useCommandState((state) => state.filtered.count);
|
|
64
|
+
|
|
65
|
+
const [activeId, setActiveId] = useState<string | undefined>(undefined);
|
|
66
|
+
|
|
67
|
+
// The callback lives in a ref so an inline arrow prop doesn't re-fire the
|
|
68
|
+
// effect on every render.
|
|
69
|
+
const onChangeRef = useRef(onChange);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
onChangeRef.current = onChange;
|
|
72
|
+
}, [onChange]);
|
|
73
|
+
|
|
74
|
+
// `undefined` is a legitimate reported value ("nothing highlighted"), so the
|
|
75
|
+
// "not reported yet" sentinel has to be something else.
|
|
76
|
+
const lastReportedRef = useRef<string | undefined | typeof NO_COMMAND_PROVIDER>(
|
|
77
|
+
NO_COMMAND_PROVIDER,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const element = rootRef.current?.querySelector<HTMLElement>(CMDK_ITEM_SELECTOR);
|
|
82
|
+
const next = element?.id ?? selectedItemId ?? undefined;
|
|
83
|
+
|
|
84
|
+
setActiveId(next);
|
|
85
|
+
if (lastReportedRef.current !== next) {
|
|
86
|
+
lastReportedRef.current = next;
|
|
87
|
+
onChangeRef.current?.(next);
|
|
88
|
+
}
|
|
89
|
+
}, [value, selectedItemId, filteredCount, rootRef]);
|
|
90
|
+
|
|
91
|
+
return <CommandActiveItemIdContext value={activeId}>{children}</CommandActiveItemIdContext>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface CommandProps extends ComponentPropsWithoutRef<typeof CommandPrimitive> {
|
|
95
|
+
/**
|
|
96
|
+
* Reports the DOM `id` cmdk assigned to the currently highlighted item, so
|
|
97
|
+
* an input rendered OUTSIDE this `Command` tree (a composer textarea, a
|
|
98
|
+
* combobox trigger) can set `aria-activedescendant` without querying
|
|
99
|
+
* `[role="option"]` positionally. Fires with `undefined` when nothing is
|
|
100
|
+
* highlighted.
|
|
101
|
+
*
|
|
102
|
+
* cmdk owns each item's `id`/`role`/`aria-selected` and applies them AFTER
|
|
103
|
+
* the consumer spread, so a consumer-supplied `id` on `CommandItem` is
|
|
104
|
+
* silently dropped (`CommandItem` warns about this in development) — this
|
|
105
|
+
* callback is the supported way to obtain it. See also the
|
|
106
|
+
* `useCommandActiveItemId` hook for reading the same value from a
|
|
107
|
+
* descendant already inside the `<Command>` tree.
|
|
108
|
+
*
|
|
109
|
+
* It fires for EVERY highlight change, including the item cmdk
|
|
110
|
+
* auto-highlights on mount and the one it re-highlights after each filter
|
|
111
|
+
* keystroke — the two states cmdk's own `state.selectedItemId` misses.
|
|
112
|
+
*/
|
|
113
|
+
onActiveItemIdChange?: (id: string | undefined) => void;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const Command = forwardRef<ElementRef<typeof CommandPrimitive>, CommandProps>(
|
|
117
|
+
function Command({ className, children, onActiveItemIdChange, ...props }, ref) {
|
|
118
|
+
const innerRef = useRef<HTMLDivElement>(null);
|
|
119
|
+
const mergedRef = useMemo(() => mergeRefs<HTMLDivElement>(ref, innerRef), [ref]);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<CommandPrimitive
|
|
123
|
+
ref={mergedRef}
|
|
124
|
+
data-slot="command"
|
|
125
|
+
className={cn(
|
|
126
|
+
"flex size-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
|
127
|
+
className,
|
|
128
|
+
)}
|
|
129
|
+
{...props}
|
|
130
|
+
>
|
|
131
|
+
<CommandActiveItemProvider rootRef={innerRef} onChange={onActiveItemIdChange}>
|
|
132
|
+
{children}
|
|
133
|
+
</CommandActiveItemProvider>
|
|
134
|
+
</CommandPrimitive>
|
|
135
|
+
);
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The DOM `id` of the currently highlighted `CommandItem`. Must be called
|
|
141
|
+
* from a component rendered inside a `<Command>` tree; returns `undefined`
|
|
142
|
+
* when nothing is highlighted (or when there is no `<Command>` ancestor).
|
|
143
|
+
* Reports exactly the same value as `Command`'s `onActiveItemIdChange`
|
|
144
|
+
* callback, for a consumer that is already a descendant (rather than an
|
|
145
|
+
* external input above the tree).
|
|
146
|
+
*/
|
|
147
|
+
export function useCommandActiveItemId(): string | undefined {
|
|
148
|
+
const id = use(CommandActiveItemIdContext);
|
|
149
|
+
return id === NO_COMMAND_PROVIDER ? undefined : id;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function CommandDialog({
|
|
153
|
+
children,
|
|
154
|
+
filter,
|
|
155
|
+
...props
|
|
156
|
+
}: ComponentPropsWithoutRef<typeof Dialog> & {
|
|
157
|
+
/** Custom cmdk ranking (e.g. strict substring instead of fuzzy). */
|
|
158
|
+
filter?: ComponentPropsWithoutRef<typeof CommandPrimitive>["filter"];
|
|
159
|
+
}) {
|
|
160
|
+
return (
|
|
161
|
+
<Dialog {...props}>
|
|
162
|
+
<DialogContent className="overflow-hidden p-0">
|
|
163
|
+
<Command
|
|
164
|
+
filter={filter}
|
|
165
|
+
className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-input-wrapper]_svg]:size-5 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3"
|
|
166
|
+
>
|
|
167
|
+
{children}
|
|
168
|
+
</Command>
|
|
169
|
+
</DialogContent>
|
|
170
|
+
</Dialog>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const CommandInput = forwardRef<
|
|
175
|
+
ElementRef<typeof CommandPrimitive.Input>,
|
|
176
|
+
ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
|
177
|
+
>(function CommandInput({ className, ...props }, ref) {
|
|
178
|
+
return (
|
|
179
|
+
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
|
180
|
+
<Search className="me-2 size-4 shrink-0 opacity-50" />
|
|
181
|
+
<CommandPrimitive.Input
|
|
182
|
+
ref={ref}
|
|
183
|
+
data-slot="command-input"
|
|
184
|
+
className={cn(
|
|
185
|
+
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
186
|
+
className,
|
|
187
|
+
)}
|
|
188
|
+
{...props}
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
export const CommandList = forwardRef<
|
|
195
|
+
ElementRef<typeof CommandPrimitive.List>,
|
|
196
|
+
ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
|
197
|
+
>(function CommandList({ className, ...props }, ref) {
|
|
198
|
+
return (
|
|
199
|
+
<CommandPrimitive.List
|
|
200
|
+
ref={ref}
|
|
201
|
+
data-slot="command-list"
|
|
202
|
+
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
|
203
|
+
{...props}
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
export const CommandEmpty = forwardRef<
|
|
209
|
+
ElementRef<typeof CommandPrimitive.Empty>,
|
|
210
|
+
ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
|
211
|
+
>(function CommandEmpty(props, ref) {
|
|
212
|
+
return (
|
|
213
|
+
<CommandPrimitive.Empty
|
|
214
|
+
ref={ref}
|
|
215
|
+
data-slot="command-empty"
|
|
216
|
+
className="py-6 text-center text-sm"
|
|
217
|
+
{...props}
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
export const CommandGroup = forwardRef<
|
|
223
|
+
ElementRef<typeof CommandPrimitive.Group>,
|
|
224
|
+
ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
|
225
|
+
>(function CommandGroup({ className, ...props }, ref) {
|
|
226
|
+
return (
|
|
227
|
+
<CommandPrimitive.Group
|
|
228
|
+
ref={ref}
|
|
229
|
+
data-slot="command-group"
|
|
230
|
+
className={cn(
|
|
231
|
+
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
|
232
|
+
className,
|
|
233
|
+
)}
|
|
234
|
+
{...props}
|
|
235
|
+
/>
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
export const CommandSeparator = forwardRef<
|
|
240
|
+
ElementRef<typeof CommandPrimitive.Separator>,
|
|
241
|
+
ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
|
242
|
+
>(function CommandSeparator({ className, ...props }, ref) {
|
|
243
|
+
return (
|
|
244
|
+
<CommandPrimitive.Separator
|
|
245
|
+
ref={ref}
|
|
246
|
+
data-slot="command-separator"
|
|
247
|
+
className={cn("-mx-1 h-px bg-border", className)}
|
|
248
|
+
{...props}
|
|
249
|
+
/>
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Props cmdk owns and applies AFTER a consumer spread — see the `Command`
|
|
255
|
+
* `onActiveItemIdChange` docblock. Passing any of these is a silent no-op;
|
|
256
|
+
* `CommandItem` warns once per key in development.
|
|
257
|
+
*/
|
|
258
|
+
const CMDK_OVERRIDDEN_ITEM_PROPS = ["id", "role", "aria-selected"] as const;
|
|
259
|
+
const warnedAboutOverriddenItemProps = new Set<string>();
|
|
260
|
+
|
|
261
|
+
export const CommandItem = forwardRef<
|
|
262
|
+
ElementRef<typeof CommandPrimitive.Item>,
|
|
263
|
+
ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
|
264
|
+
>(function CommandItem({ className, ...props }, ref) {
|
|
265
|
+
if (process.env.NODE_ENV !== "production") {
|
|
266
|
+
for (const key of CMDK_OVERRIDDEN_ITEM_PROPS) {
|
|
267
|
+
if (key in props && !warnedAboutOverriddenItemProps.has(key)) {
|
|
268
|
+
warnedAboutOverriddenItemProps.add(key);
|
|
269
|
+
// oxlint-disable-next-line no-console
|
|
270
|
+
console.warn(
|
|
271
|
+
`CommandItem: "${key}" is assigned internally by cmdk, so the value you passed is ` +
|
|
272
|
+
'ignored. Use <Command onActiveItemIdChange> (or the "useCommandActiveItemId" hook) ' +
|
|
273
|
+
"to read the DOM id of the highlighted item instead — see " +
|
|
274
|
+
"packages/ui/src/components/command/command.tsx.",
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<CommandPrimitive.Item
|
|
282
|
+
ref={ref}
|
|
283
|
+
data-slot="command-item"
|
|
284
|
+
className={cn(
|
|
285
|
+
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors duration-fast data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:size-4",
|
|
286
|
+
className,
|
|
287
|
+
)}
|
|
288
|
+
{...props}
|
|
289
|
+
/>
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
export function CommandShortcut({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
|
|
294
|
+
return (
|
|
295
|
+
<span
|
|
296
|
+
data-slot="command-shortcut"
|
|
297
|
+
className={cn("ms-auto text-xs tracking-widest text-muted-foreground", className)}
|
|
298
|
+
{...props}
|
|
299
|
+
/>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { expect, waitFor, within } from "storybook/test";
|
|
4
|
+
import { ThemeProvider } from "@elabs-ai/components-tokens";
|
|
5
|
+
import { Button } from "../button";
|
|
6
|
+
import { Text } from "../typography";
|
|
7
|
+
import { ConfirmDialog } from "./confirm-dialog";
|
|
8
|
+
|
|
9
|
+
const meta = {
|
|
10
|
+
title: "Overlays/ConfirmDialog",
|
|
11
|
+
component: ConfirmDialog,
|
|
12
|
+
tags: ["autodocs"],
|
|
13
|
+
// Every story drives its own `open` state through a `render`; these args exist
|
|
14
|
+
// so the autodocs props table has real values to describe.
|
|
15
|
+
args: {
|
|
16
|
+
open: false,
|
|
17
|
+
onOpenChange: () => {},
|
|
18
|
+
title: "Delete this skill?",
|
|
19
|
+
description: "Removing it also deletes its 42 saved runs. This cannot be undone.",
|
|
20
|
+
confirmLabel: "Delete skill",
|
|
21
|
+
onConfirm: () => {},
|
|
22
|
+
},
|
|
23
|
+
} satisfies Meta<typeof ConfirmDialog>;
|
|
24
|
+
export default meta;
|
|
25
|
+
type Story = StoryObj<typeof meta>;
|
|
26
|
+
|
|
27
|
+
function DestructiveConfirm({ loading = false }: { loading?: boolean }) {
|
|
28
|
+
const [open, setOpen] = useState(false);
|
|
29
|
+
const [deleted, setDeleted] = useState(false);
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<Button variant="destructive" onClick={() => setOpen(true)}>
|
|
33
|
+
Delete skill
|
|
34
|
+
</Button>
|
|
35
|
+
{deleted ? <Text variant="caption">Skill deleted</Text> : null}
|
|
36
|
+
<ConfirmDialog
|
|
37
|
+
open={open}
|
|
38
|
+
onOpenChange={setOpen}
|
|
39
|
+
tone="destructive"
|
|
40
|
+
loading={loading}
|
|
41
|
+
title="Delete this skill?"
|
|
42
|
+
description="Removing it also deletes its 42 saved runs. This cannot be undone."
|
|
43
|
+
confirmLabel="Delete skill"
|
|
44
|
+
onConfirm={() => {
|
|
45
|
+
setDeleted(true);
|
|
46
|
+
setOpen(false);
|
|
47
|
+
}}
|
|
48
|
+
/>
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The confirm tier. Initial focus lands on **Cancel**, not on the destructive
|
|
55
|
+
* action — asserted below rather than eyeballed, because that is the whole
|
|
56
|
+
* reason this component exists instead of a hand-built `Dialog`.
|
|
57
|
+
*/
|
|
58
|
+
export const Default: Story = {
|
|
59
|
+
render: () => <DestructiveConfirm />,
|
|
60
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
61
|
+
await userEvent.click(canvas.getByRole("button", { name: "Delete skill" }));
|
|
62
|
+
const doc = within(canvasElement.ownerDocument.body);
|
|
63
|
+
const dialog = await doc.findByRole("alertdialog");
|
|
64
|
+
await waitFor(() => expect(within(dialog).getByText("Delete this skill?")).toBeVisible());
|
|
65
|
+
|
|
66
|
+
const cancel = within(dialog).getByRole("button", { name: "Cancel" });
|
|
67
|
+
const confirm = within(dialog).getByRole("button", { name: "Delete skill" });
|
|
68
|
+
await waitFor(() => expect(document.activeElement).toBe(cancel));
|
|
69
|
+
expect(document.activeElement).not.toBe(confirm);
|
|
70
|
+
|
|
71
|
+
// Cancel is FIRST in the DOM (so AT reads the safe action first) while the
|
|
72
|
+
// footer still paints the confirm action to its inline end.
|
|
73
|
+
expect(cancel.compareDocumentPosition(confirm) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
74
|
+
expect(confirm.getBoundingClientRect().left).toBeGreaterThan(
|
|
75
|
+
cancel.getBoundingClientRect().left,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
await userEvent.click(confirm);
|
|
79
|
+
await waitFor(() => expect(canvas.getByText("Skill deleted")).toBeVisible());
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Escape cancels — the keyboard route to the safe outcome. */
|
|
84
|
+
export const EscapeCancels: Story = {
|
|
85
|
+
render: () => <DestructiveConfirm />,
|
|
86
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
87
|
+
await userEvent.click(canvas.getByRole("button", { name: "Delete skill" }));
|
|
88
|
+
const doc = within(canvasElement.ownerDocument.body);
|
|
89
|
+
await doc.findByRole("alertdialog");
|
|
90
|
+
await userEvent.keyboard("{Escape}");
|
|
91
|
+
await waitFor(() => expect(doc.queryByRole("alertdialog")).toBeNull());
|
|
92
|
+
expect(canvas.queryByText("Skill deleted")).toBeNull();
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The request is in flight. The action swaps in a spinner and reports
|
|
98
|
+
* `aria-busy`, but it is never pre-disabled — and repeat activations are
|
|
99
|
+
* swallowed, so a double-click cannot fire the deletion twice.
|
|
100
|
+
*/
|
|
101
|
+
export const Loading: Story = {
|
|
102
|
+
render: () => <DestructiveConfirm loading />,
|
|
103
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
104
|
+
await userEvent.click(canvas.getByRole("button", { name: "Delete skill" }));
|
|
105
|
+
const doc = within(canvasElement.ownerDocument.body);
|
|
106
|
+
const dialog = await doc.findByRole("alertdialog");
|
|
107
|
+
const confirm = within(dialog).getByRole("button", { name: /delete skill/i });
|
|
108
|
+
|
|
109
|
+
await waitFor(() => expect(confirm).toHaveAttribute("aria-busy", "true"));
|
|
110
|
+
expect(confirm).not.toBeDisabled();
|
|
111
|
+
expect(within(dialog).getByRole("button", { name: "Cancel" })).not.toBeDisabled();
|
|
112
|
+
|
|
113
|
+
await userEvent.click(confirm);
|
|
114
|
+
expect(canvas.queryByText("Skill deleted")).toBeNull();
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
function NeutralConfirm() {
|
|
119
|
+
const [open, setOpen] = useState(false);
|
|
120
|
+
const [published, setPublished] = useState(false);
|
|
121
|
+
return (
|
|
122
|
+
<>
|
|
123
|
+
<Button onClick={() => setOpen(true)}>Publish version</Button>
|
|
124
|
+
{published ? <Text variant="caption">Version 4 published</Text> : null}
|
|
125
|
+
<ConfirmDialog
|
|
126
|
+
open={open}
|
|
127
|
+
onOpenChange={setOpen}
|
|
128
|
+
title="Publish version 4?"
|
|
129
|
+
description="Everyone in the workspace gets the new prompt on their next run."
|
|
130
|
+
confirmLabel="Publish version"
|
|
131
|
+
onConfirm={() => {
|
|
132
|
+
setPublished(true);
|
|
133
|
+
setOpen(false);
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
</>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const openNeutral: Story["play"] = async ({ canvas, canvasElement, userEvent }) => {
|
|
141
|
+
await userEvent.click(canvas.getByRole("button", { name: "Publish version" }));
|
|
142
|
+
const doc = within(canvasElement.ownerDocument.body);
|
|
143
|
+
const dialog = await doc.findByRole("alertdialog");
|
|
144
|
+
const cancel = within(dialog).getByRole("button", { name: "Cancel" });
|
|
145
|
+
await waitFor(() => expect(document.activeElement).toBe(cancel));
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** A neutral, non-destructive confirmation — same contract, calmer action. */
|
|
149
|
+
export const NeutralTone: Story = {
|
|
150
|
+
render: () => <NeutralConfirm />,
|
|
151
|
+
play: openNeutral,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The DESTRUCTIVE tone in dark — the second theme of the sweep, alongside
|
|
156
|
+
* light (`Default`).
|
|
157
|
+
*
|
|
158
|
+
* This story used to sweep the *neutral* tone here on purpose, because
|
|
159
|
+
* `--destructive-foreground` on `--destructive` measured 3.02:1 in dark —
|
|
160
|
+
* a token defect every `variant="destructive"` control in the theme shared,
|
|
161
|
+
* which this component could not fix. That token is now the theme's own
|
|
162
|
+
* warm-dark ink at 5.50:1 (#321), so the destructive tone finally gets a real
|
|
163
|
+
* dark sweep and the workaround is retired.
|
|
164
|
+
*/
|
|
165
|
+
export const DestructiveDark: Story = {
|
|
166
|
+
name: "Destructive — dark",
|
|
167
|
+
decorators: [
|
|
168
|
+
(Story) => (
|
|
169
|
+
<ThemeProvider defaultTheme="dark" storageKey={null}>
|
|
170
|
+
<Story />
|
|
171
|
+
</ThemeProvider>
|
|
172
|
+
),
|
|
173
|
+
],
|
|
174
|
+
render: () => <DestructiveConfirm />,
|
|
175
|
+
play: Default.play,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const DestructiveHighDecoration: Story = {
|
|
179
|
+
name: "Destructive — high decoration",
|
|
180
|
+
globals: { decoration: "10" },
|
|
181
|
+
render: () => <DestructiveConfirm />,
|
|
182
|
+
play: Default.play,
|
|
183
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { ConfirmDialog } from "./confirm-dialog";
|
|
5
|
+
import { Button } from "../button";
|
|
6
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogTitle } from "../dialog";
|
|
7
|
+
|
|
8
|
+
function open(props: Partial<React.ComponentProps<typeof ConfirmDialog>> = {}) {
|
|
9
|
+
const onConfirm = vi.fn();
|
|
10
|
+
const onOpenChange = vi.fn();
|
|
11
|
+
render(
|
|
12
|
+
<ConfirmDialog
|
|
13
|
+
open
|
|
14
|
+
onOpenChange={onOpenChange}
|
|
15
|
+
title="Delete this skill?"
|
|
16
|
+
description="Removing it also deletes its saved runs. This cannot be undone."
|
|
17
|
+
confirmLabel="Delete skill"
|
|
18
|
+
tone="destructive"
|
|
19
|
+
onConfirm={onConfirm}
|
|
20
|
+
{...props}
|
|
21
|
+
/>,
|
|
22
|
+
);
|
|
23
|
+
return { onConfirm, onOpenChange };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("ConfirmDialog initial focus", () => {
|
|
27
|
+
// The headline AC: opening a confirmation must NOT put the keyboard on the
|
|
28
|
+
// destructive action. `AlertDialogContent` prevents its own open-autofocus and
|
|
29
|
+
// focuses the mounted Cancel — which is why ConfirmDialog always renders one.
|
|
30
|
+
it("lands focus on the safe action, not the destructive one", async () => {
|
|
31
|
+
open();
|
|
32
|
+
const cancel = await screen.findByRole("button", { name: "Cancel" });
|
|
33
|
+
await waitFor(() => expect(document.activeElement).toBe(cancel));
|
|
34
|
+
expect(document.activeElement).not.toBe(screen.getByRole("button", { name: "Delete skill" }));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// The negative twin — this documents WHY the named component exists. A plain
|
|
38
|
+
// `Dialog` focuses whatever comes first in the DOM, so a confirmation built
|
|
39
|
+
// from one is only ever safe by accident: reorder the footer and the keyboard
|
|
40
|
+
// now starts on the destructive action.
|
|
41
|
+
it("a plain Dialog focuses by DOM order — the destructive action when it is first", async () => {
|
|
42
|
+
render(
|
|
43
|
+
<Dialog open>
|
|
44
|
+
<DialogContent>
|
|
45
|
+
<DialogTitle>Delete this skill?</DialogTitle>
|
|
46
|
+
<DialogDescription>This cannot be undone.</DialogDescription>
|
|
47
|
+
<DialogFooter>
|
|
48
|
+
<Button variant="destructive">Delete skill</Button>
|
|
49
|
+
<Button variant="outline">Cancel</Button>
|
|
50
|
+
</DialogFooter>
|
|
51
|
+
</DialogContent>
|
|
52
|
+
</Dialog>,
|
|
53
|
+
);
|
|
54
|
+
const destructive = await screen.findByRole("button", { name: "Delete skill" });
|
|
55
|
+
await waitFor(() => expect(document.activeElement).toBe(destructive));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// …whereas ConfirmDialog cannot be mis-ordered: Cancel is always rendered, and
|
|
59
|
+
// Radix focuses IT rather than the first tabbable child.
|
|
60
|
+
it("keeps focus on Cancel even with extra focusable content above it", async () => {
|
|
61
|
+
open({
|
|
62
|
+
children: (
|
|
63
|
+
<a href="#docs" data-testid="learn-more">
|
|
64
|
+
What happens to my runs?
|
|
65
|
+
</a>
|
|
66
|
+
),
|
|
67
|
+
});
|
|
68
|
+
const cancel = await screen.findByRole("button", { name: "Cancel" });
|
|
69
|
+
await waitFor(() => expect(document.activeElement).toBe(cancel));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("uses role=alertdialog and is described by its description", async () => {
|
|
73
|
+
open();
|
|
74
|
+
const dialog = await screen.findByRole("alertdialog");
|
|
75
|
+
const describedBy = dialog.getAttribute("aria-describedby");
|
|
76
|
+
expect(describedBy).toBeTruthy();
|
|
77
|
+
expect(document.getElementById(describedBy!)?.textContent).toContain("cannot be undone");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("ConfirmDialog actions", () => {
|
|
82
|
+
it("fires onConfirm once and leaves closing to the app", async () => {
|
|
83
|
+
const user = userEvent.setup();
|
|
84
|
+
const { onConfirm, onOpenChange } = open();
|
|
85
|
+
|
|
86
|
+
await user.click(await screen.findByRole("button", { name: "Delete skill" }));
|
|
87
|
+
|
|
88
|
+
expect(onConfirm).toHaveBeenCalledTimes(1);
|
|
89
|
+
// The app owns `open` — confirming must not close behind its back, which is
|
|
90
|
+
// what makes an async `loading` state possible.
|
|
91
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("cancel closes through onOpenChange(false) and calls onCancel", async () => {
|
|
95
|
+
const user = userEvent.setup();
|
|
96
|
+
const onCancel = vi.fn();
|
|
97
|
+
const { onOpenChange, onConfirm } = open({ onCancel });
|
|
98
|
+
|
|
99
|
+
await user.click(await screen.findByRole("button", { name: "Cancel" }));
|
|
100
|
+
|
|
101
|
+
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
102
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
103
|
+
expect(onConfirm).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("Escape cancels", async () => {
|
|
107
|
+
const user = userEvent.setup();
|
|
108
|
+
const { onOpenChange, onConfirm } = open();
|
|
109
|
+
await screen.findByRole("alertdialog");
|
|
110
|
+
|
|
111
|
+
await user.keyboard("{Escape}");
|
|
112
|
+
|
|
113
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
114
|
+
expect(onConfirm).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("labels the confirm action with the consequence, defaulting to Confirm", async () => {
|
|
118
|
+
open({ confirmLabel: undefined });
|
|
119
|
+
expect(await screen.findByRole("button", { name: "Confirm" })).toBeInTheDocument();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("ConfirmDialog loading", () => {
|
|
124
|
+
it("shows a spinner, reports aria-busy and stays ENABLED", async () => {
|
|
125
|
+
open({ loading: true });
|
|
126
|
+
const confirm = await screen.findByRole("button", { name: /delete skill/i });
|
|
127
|
+
|
|
128
|
+
expect(confirm).toHaveAttribute("aria-busy", "true");
|
|
129
|
+
// interaction-guidelines (Forms): a submit is never pre-disabled.
|
|
130
|
+
expect(confirm).not.toBeDisabled();
|
|
131
|
+
expect(screen.getByRole("status", { name: "Loading…" })).toBeInTheDocument();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("ignores repeat activations while the request is in flight", async () => {
|
|
135
|
+
const user = userEvent.setup();
|
|
136
|
+
const { onConfirm } = open({ loading: true });
|
|
137
|
+
|
|
138
|
+
await user.click(await screen.findByRole("button", { name: /delete skill/i }));
|
|
139
|
+
|
|
140
|
+
expect(onConfirm).not.toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("never disables the safe action", async () => {
|
|
144
|
+
open({ loading: true });
|
|
145
|
+
expect(await screen.findByRole("button", { name: "Cancel" })).not.toBeDisabled();
|
|
146
|
+
});
|
|
147
|
+
});
|