@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,146 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type MouseEvent, type ReactNode } from "react";
|
|
4
|
+
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogAction,
|
|
7
|
+
AlertDialogCancel,
|
|
8
|
+
AlertDialogContent,
|
|
9
|
+
AlertDialogDescription,
|
|
10
|
+
AlertDialogFooter,
|
|
11
|
+
AlertDialogHeader,
|
|
12
|
+
AlertDialogTitle,
|
|
13
|
+
} from "../alert-dialog";
|
|
14
|
+
import { buttonVariants } from "../button";
|
|
15
|
+
import { cn } from "../../lib/cn";
|
|
16
|
+
import { useLocale } from "../locale-provider";
|
|
17
|
+
import { Spinner } from "../spinner";
|
|
18
|
+
|
|
19
|
+
export interface ConfirmDialogProps {
|
|
20
|
+
/** Controlled open state — the app owns it. */
|
|
21
|
+
open: boolean;
|
|
22
|
+
/** Fires on cancel, Escape, and any other Radix dismissal. */
|
|
23
|
+
onOpenChange: (open: boolean) => void;
|
|
24
|
+
/** What is about to happen, as a question or a statement. */
|
|
25
|
+
title: ReactNode;
|
|
26
|
+
/**
|
|
27
|
+
* The consequence, spelled out. **Required**: `AlertDialogContent` is
|
|
28
|
+
* inaccessible to screen-reader users without a description (Radix warns), and
|
|
29
|
+
* a confirmation whose only text is its title cannot state what is at stake.
|
|
30
|
+
*/
|
|
31
|
+
description: ReactNode;
|
|
32
|
+
/**
|
|
33
|
+
* The confirm action's label. States the CONSEQUENCE ("Delete skill"), never
|
|
34
|
+
* "OK" — a label that names the outcome is the last chance to catch a mistake.
|
|
35
|
+
* Defaults to a generic "Confirm", which you should almost always replace.
|
|
36
|
+
*/
|
|
37
|
+
confirmLabel?: ReactNode;
|
|
38
|
+
/** The safe action's label. Defaults to "Cancel". */
|
|
39
|
+
cancelLabel?: ReactNode;
|
|
40
|
+
/** Runs when the confirm action is activated. */
|
|
41
|
+
onConfirm: () => void;
|
|
42
|
+
/** Runs when the safe action is activated (in addition to `onOpenChange`). */
|
|
43
|
+
onCancel?: () => void;
|
|
44
|
+
/** Visual tone of the CONFIRM action only. The dialog chrome never changes. */
|
|
45
|
+
tone?: "default" | "destructive";
|
|
46
|
+
/**
|
|
47
|
+
* The confirm request is in flight: the action swaps its label for a spinner
|
|
48
|
+
* and reports `aria-busy`. It deliberately stays ENABLED
|
|
49
|
+
* (`.claude/rules/interaction-guidelines.md`, Forms — a submit is never
|
|
50
|
+
* pre-disabled); repeat activations are ignored while it is set.
|
|
51
|
+
*/
|
|
52
|
+
loading?: boolean;
|
|
53
|
+
/** Extra body content between the description and the actions. */
|
|
54
|
+
children?: ReactNode;
|
|
55
|
+
className?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A destructive/irreversible confirmation (#341).
|
|
60
|
+
*
|
|
61
|
+
* Built on `AlertDialog`, not `Dialog`, and that is the entire point of it
|
|
62
|
+
* existing as a named component rather than a recipe: `AlertDialogContent`
|
|
63
|
+
* prevents its own open-autofocus and focuses the mounted `AlertDialogCancel`
|
|
64
|
+
* instead, so **initial focus lands on the safe action** — but only if a Cancel
|
|
65
|
+
* is actually mounted, which a hand-rolled confirmation routinely forgets (and a
|
|
66
|
+
* plain `Dialog` cannot do at all). `ConfirmDialog` always renders one, so the
|
|
67
|
+
* wrong choice is unrepresentable. It also inherits `AlertDialog`'s refusal to
|
|
68
|
+
* dismiss on an outside click.
|
|
69
|
+
*
|
|
70
|
+
* Cancel is FIRST in the DOM so assistive tech reads the safe action first;
|
|
71
|
+
* `AlertDialogFooter`'s `flex-col-reverse sm:flex-row sm:justify-end` still
|
|
72
|
+
* paints the confirm action bottom-right on every breakpoint.
|
|
73
|
+
*
|
|
74
|
+
* **Confirming does not close the dialog** — `onConfirm` fires and the app sets
|
|
75
|
+
* `open={false}` when it is ready (immediately for a synchronous action, after
|
|
76
|
+
* the request for an async one, which is what makes `loading` coherent).
|
|
77
|
+
* Cancelling closes through `onOpenChange(false)` as usual.
|
|
78
|
+
*
|
|
79
|
+
* ```tsx
|
|
80
|
+
* <ConfirmDialog
|
|
81
|
+
* open={open}
|
|
82
|
+
* onOpenChange={setOpen}
|
|
83
|
+
* tone="destructive"
|
|
84
|
+
* title={`Delete ${skill.name}?`}
|
|
85
|
+
* description={`Removing it also deletes its ${skill.runs} saved runs. This cannot be undone.`}
|
|
86
|
+
* confirmLabel={`Delete ${skill.name}`}
|
|
87
|
+
* onConfirm={() => { deleteSkill(); setOpen(false); }}
|
|
88
|
+
* />
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export function ConfirmDialog({
|
|
92
|
+
open,
|
|
93
|
+
onOpenChange,
|
|
94
|
+
title,
|
|
95
|
+
description,
|
|
96
|
+
confirmLabel,
|
|
97
|
+
cancelLabel,
|
|
98
|
+
onConfirm,
|
|
99
|
+
onCancel,
|
|
100
|
+
tone = "default",
|
|
101
|
+
loading = false,
|
|
102
|
+
children,
|
|
103
|
+
className,
|
|
104
|
+
}: ConfirmDialogProps) {
|
|
105
|
+
const { t } = useLocale();
|
|
106
|
+
|
|
107
|
+
function handleConfirm(event: MouseEvent<HTMLButtonElement>) {
|
|
108
|
+
// Radix's Action is a `Dialog.Close`; preventing the default stops it
|
|
109
|
+
// closing so the app stays in charge of `open` (and an in-flight request
|
|
110
|
+
// keeps its spinner on screen).
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
if (loading) return;
|
|
113
|
+
onConfirm();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
118
|
+
<AlertDialogContent data-slot="confirm-dialog" className={className}>
|
|
119
|
+
<AlertDialogHeader>
|
|
120
|
+
<AlertDialogTitle data-slot="confirm-dialog-title">{title}</AlertDialogTitle>
|
|
121
|
+
<AlertDialogDescription data-slot="confirm-dialog-description">
|
|
122
|
+
{description}
|
|
123
|
+
</AlertDialogDescription>
|
|
124
|
+
</AlertDialogHeader>
|
|
125
|
+
{children}
|
|
126
|
+
<AlertDialogFooter>
|
|
127
|
+
<AlertDialogCancel data-slot="confirm-dialog-cancel" onClick={onCancel}>
|
|
128
|
+
{cancelLabel ?? t("ui.confirmDialog.cancel")}
|
|
129
|
+
</AlertDialogCancel>
|
|
130
|
+
<AlertDialogAction
|
|
131
|
+
data-slot="confirm-dialog-confirm"
|
|
132
|
+
className={cn(tone === "destructive" && buttonVariants({ variant: "destructive" }))}
|
|
133
|
+
aria-busy={loading || undefined}
|
|
134
|
+
onClick={handleConfirm}
|
|
135
|
+
>
|
|
136
|
+
{/* `text-current` so the spinner inherits the action's foreground
|
|
137
|
+
token instead of `Spinner`'s muted default, which would drop
|
|
138
|
+
below 3:1 on a filled button. */}
|
|
139
|
+
{loading ? <Spinner className="text-current" label={t("loading")} /> : null}
|
|
140
|
+
{confirmLabel ?? t("ui.confirmDialog.confirm")}
|
|
141
|
+
</AlertDialogAction>
|
|
142
|
+
</AlertDialogFooter>
|
|
143
|
+
</AlertDialogContent>
|
|
144
|
+
</AlertDialog>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, waitFor, within } from "storybook/test";
|
|
3
|
+
import {
|
|
4
|
+
ContextMenu,
|
|
5
|
+
ContextMenuContent,
|
|
6
|
+
ContextMenuItem,
|
|
7
|
+
ContextMenuSeparator,
|
|
8
|
+
ContextMenuTrigger,
|
|
9
|
+
} from "./context-menu";
|
|
10
|
+
const meta = {
|
|
11
|
+
title: "Overlays/ContextMenu",
|
|
12
|
+
component: ContextMenu,
|
|
13
|
+
tags: ["autodocs"],
|
|
14
|
+
argTypes: {
|
|
15
|
+
open: {
|
|
16
|
+
description: "Controlled open state of the context menu.",
|
|
17
|
+
control: "boolean",
|
|
18
|
+
table: { category: "State" },
|
|
19
|
+
},
|
|
20
|
+
defaultOpen: {
|
|
21
|
+
description: "Uncontrolled initial open state.",
|
|
22
|
+
control: "boolean",
|
|
23
|
+
table: { category: "State" },
|
|
24
|
+
},
|
|
25
|
+
onOpenChange: {
|
|
26
|
+
description: "Callback when the open state changes.",
|
|
27
|
+
control: false,
|
|
28
|
+
table: { category: "Events" },
|
|
29
|
+
},
|
|
30
|
+
modal: {
|
|
31
|
+
description: "Whether the context menu is modal (traps focus). Default true.",
|
|
32
|
+
control: "boolean",
|
|
33
|
+
table: { category: "Behaviour" },
|
|
34
|
+
},
|
|
35
|
+
dir: {
|
|
36
|
+
description: "Reading direction for the context menu.",
|
|
37
|
+
control: { type: "select" },
|
|
38
|
+
options: ["ltr", "rtl"],
|
|
39
|
+
table: { category: "Behaviour" },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
} satisfies Meta<typeof ContextMenu>;
|
|
43
|
+
export default meta;
|
|
44
|
+
type Story = StoryObj<typeof meta>;
|
|
45
|
+
export const Default: Story = {
|
|
46
|
+
render: () => (
|
|
47
|
+
<ContextMenu>
|
|
48
|
+
<ContextMenuTrigger className="flex h-32 w-72 items-center justify-center rounded-md border border-dashed text-body text-muted-foreground">
|
|
49
|
+
Right-click here
|
|
50
|
+
</ContextMenuTrigger>
|
|
51
|
+
<ContextMenuContent>
|
|
52
|
+
<ContextMenuItem>Back</ContextMenuItem>
|
|
53
|
+
<ContextMenuItem>Reload</ContextMenuItem>
|
|
54
|
+
<ContextMenuSeparator />
|
|
55
|
+
<ContextMenuItem>Inspect</ContextMenuItem>
|
|
56
|
+
</ContextMenuContent>
|
|
57
|
+
</ContextMenu>
|
|
58
|
+
),
|
|
59
|
+
// Right-clicks the trigger to open the portalled context menu and confirms
|
|
60
|
+
// the menu items are visible, then presses Escape to dismiss.
|
|
61
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
62
|
+
const trigger = canvas.getByText("Right-click here");
|
|
63
|
+
await userEvent.pointer({ target: trigger, keys: "[MouseRight]" });
|
|
64
|
+
const body = within(canvasElement.ownerDocument.body);
|
|
65
|
+
const menu = await body.findByRole("menu");
|
|
66
|
+
await waitFor(() => expect(within(menu).getByText("Back")).toBeVisible());
|
|
67
|
+
await waitFor(() => expect(within(menu).getByText("Reload")).toBeVisible());
|
|
68
|
+
await userEvent.keyboard("{Escape}");
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
type ComponentPropsWithoutRef,
|
|
4
|
+
type ElementRef,
|
|
5
|
+
type HTMLAttributes,
|
|
6
|
+
} from "react";
|
|
7
|
+
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
|
8
|
+
import { Check, ChevronRight, Circle } from "lucide-react";
|
|
9
|
+
import { cn } from "../../lib/cn";
|
|
10
|
+
|
|
11
|
+
export const ContextMenu = ContextMenuPrimitive.Root;
|
|
12
|
+
export const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
|
13
|
+
export const ContextMenuGroup = ContextMenuPrimitive.Group;
|
|
14
|
+
export const ContextMenuSub = ContextMenuPrimitive.Sub;
|
|
15
|
+
export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
|
16
|
+
|
|
17
|
+
export const ContextMenuSubTrigger = forwardRef<
|
|
18
|
+
ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
|
19
|
+
ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & { inset?: boolean }
|
|
20
|
+
>(function ContextMenuSubTrigger({ className, inset, children, ...props }, ref) {
|
|
21
|
+
return (
|
|
22
|
+
<ContextMenuPrimitive.SubTrigger
|
|
23
|
+
ref={ref}
|
|
24
|
+
className={cn(
|
|
25
|
+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors duration-fast focus:bg-accent data-[state=open]:bg-accent",
|
|
26
|
+
inset && "ps-8",
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
{children}
|
|
32
|
+
<ChevronRight className="ms-auto size-4" />
|
|
33
|
+
</ContextMenuPrimitive.SubTrigger>
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const ContextMenuSubContent = forwardRef<
|
|
38
|
+
ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
|
39
|
+
ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
|
40
|
+
>(function ContextMenuSubContent({ className, ...props }, ref) {
|
|
41
|
+
return (
|
|
42
|
+
<ContextMenuPrimitive.SubContent
|
|
43
|
+
ref={ref}
|
|
44
|
+
className={cn(
|
|
45
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
|
|
46
|
+
className,
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const ContextMenuContent = forwardRef<
|
|
54
|
+
ElementRef<typeof ContextMenuPrimitive.Content>,
|
|
55
|
+
ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
|
56
|
+
>(function ContextMenuContent({ className, ...props }, ref) {
|
|
57
|
+
return (
|
|
58
|
+
<ContextMenuPrimitive.Portal>
|
|
59
|
+
<ContextMenuPrimitive.Content
|
|
60
|
+
ref={ref}
|
|
61
|
+
className={cn(
|
|
62
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
|
|
63
|
+
className,
|
|
64
|
+
)}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
</ContextMenuPrimitive.Portal>
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export const ContextMenuItem = forwardRef<
|
|
72
|
+
ElementRef<typeof ContextMenuPrimitive.Item>,
|
|
73
|
+
ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & { inset?: boolean }
|
|
74
|
+
>(function ContextMenuItem({ className, inset, ...props }, ref) {
|
|
75
|
+
return (
|
|
76
|
+
<ContextMenuPrimitive.Item
|
|
77
|
+
ref={ref}
|
|
78
|
+
className={cn(
|
|
79
|
+
"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 focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4",
|
|
80
|
+
inset && "ps-8",
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const ContextMenuCheckboxItem = forwardRef<
|
|
89
|
+
ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
|
90
|
+
ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
|
91
|
+
>(function ContextMenuCheckboxItem({ className, children, checked, ...props }, ref) {
|
|
92
|
+
return (
|
|
93
|
+
<ContextMenuPrimitive.CheckboxItem
|
|
94
|
+
ref={ref}
|
|
95
|
+
checked={checked}
|
|
96
|
+
className={cn(
|
|
97
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-sm outline-none transition-colors duration-fast focus:bg-accent",
|
|
98
|
+
className,
|
|
99
|
+
)}
|
|
100
|
+
{...props}
|
|
101
|
+
>
|
|
102
|
+
<span className="absolute start-2 flex size-3.5 items-center justify-center">
|
|
103
|
+
<ContextMenuPrimitive.ItemIndicator>
|
|
104
|
+
<Check className="size-4" />
|
|
105
|
+
</ContextMenuPrimitive.ItemIndicator>
|
|
106
|
+
</span>
|
|
107
|
+
{children}
|
|
108
|
+
</ContextMenuPrimitive.CheckboxItem>
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export const ContextMenuRadioItem = forwardRef<
|
|
113
|
+
ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
|
114
|
+
ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
|
115
|
+
>(function ContextMenuRadioItem({ className, children, ...props }, ref) {
|
|
116
|
+
return (
|
|
117
|
+
<ContextMenuPrimitive.RadioItem
|
|
118
|
+
ref={ref}
|
|
119
|
+
className={cn(
|
|
120
|
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-sm outline-none transition-colors duration-fast focus:bg-accent",
|
|
121
|
+
className,
|
|
122
|
+
)}
|
|
123
|
+
{...props}
|
|
124
|
+
>
|
|
125
|
+
<span className="absolute start-2 flex size-3.5 items-center justify-center">
|
|
126
|
+
<ContextMenuPrimitive.ItemIndicator>
|
|
127
|
+
<Circle className="size-2 fill-current" />
|
|
128
|
+
</ContextMenuPrimitive.ItemIndicator>
|
|
129
|
+
</span>
|
|
130
|
+
{children}
|
|
131
|
+
</ContextMenuPrimitive.RadioItem>
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export const ContextMenuLabel = forwardRef<
|
|
136
|
+
ElementRef<typeof ContextMenuPrimitive.Label>,
|
|
137
|
+
ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & { inset?: boolean }
|
|
138
|
+
>(function ContextMenuLabel({ className, inset, ...props }, ref) {
|
|
139
|
+
return (
|
|
140
|
+
<ContextMenuPrimitive.Label
|
|
141
|
+
ref={ref}
|
|
142
|
+
className={cn(
|
|
143
|
+
"px-2 py-1.5 text-xs font-semibold text-muted-foreground",
|
|
144
|
+
inset && "ps-8",
|
|
145
|
+
className,
|
|
146
|
+
)}
|
|
147
|
+
{...props}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
export const ContextMenuSeparator = forwardRef<
|
|
153
|
+
ElementRef<typeof ContextMenuPrimitive.Separator>,
|
|
154
|
+
ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
|
155
|
+
>(function ContextMenuSeparator({ className, ...props }, ref) {
|
|
156
|
+
return (
|
|
157
|
+
<ContextMenuPrimitive.Separator
|
|
158
|
+
ref={ref}
|
|
159
|
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
160
|
+
{...props}
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export function ContextMenuShortcut({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
|
|
166
|
+
return (
|
|
167
|
+
<span
|
|
168
|
+
className={cn("ms-auto text-xs tracking-widest text-muted-foreground", className)}
|
|
169
|
+
{...props}
|
|
170
|
+
/>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ContextMenu,
|
|
3
|
+
ContextMenuTrigger,
|
|
4
|
+
ContextMenuGroup,
|
|
5
|
+
ContextMenuSub,
|
|
6
|
+
ContextMenuRadioGroup,
|
|
7
|
+
ContextMenuSubTrigger,
|
|
8
|
+
ContextMenuSubContent,
|
|
9
|
+
ContextMenuContent,
|
|
10
|
+
ContextMenuItem,
|
|
11
|
+
ContextMenuCheckboxItem,
|
|
12
|
+
ContextMenuRadioItem,
|
|
13
|
+
ContextMenuLabel,
|
|
14
|
+
ContextMenuSeparator,
|
|
15
|
+
ContextMenuShortcut,
|
|
16
|
+
} from "./context-menu";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, userEvent, within } from "storybook/test";
|
|
3
|
+
import { CopyableValue } from "./copyable-value";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Display/CopyableValue",
|
|
7
|
+
component: CopyableValue,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
"Shown short, copied exact. Numbers are formatted compact across the library — this keeps the unrounded figure one click (or one Enter) away.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
argTypes: {
|
|
18
|
+
value: {
|
|
19
|
+
description: "The exact text written to the clipboard.",
|
|
20
|
+
control: "text",
|
|
21
|
+
table: { category: "Content" },
|
|
22
|
+
},
|
|
23
|
+
children: {
|
|
24
|
+
description: "What the reader sees. Defaults to `value`.",
|
|
25
|
+
table: { category: "Content" },
|
|
26
|
+
},
|
|
27
|
+
hint: {
|
|
28
|
+
description: 'Screen-reader hint appended to the visible text. Default: "Copy exact value".',
|
|
29
|
+
control: "text",
|
|
30
|
+
table: { category: "Content" },
|
|
31
|
+
},
|
|
32
|
+
resetAfterMs: {
|
|
33
|
+
description: 'ms the "Copied" confirmation stays. Default: 1500.',
|
|
34
|
+
control: "number",
|
|
35
|
+
table: { category: "Behavior" },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
} satisfies Meta<typeof CopyableValue>;
|
|
39
|
+
export default meta;
|
|
40
|
+
type Story = StoryObj<typeof meta>;
|
|
41
|
+
|
|
42
|
+
/** With no children the exact value is also what is shown. */
|
|
43
|
+
export const Default: Story = {
|
|
44
|
+
args: { value: "50012102.632741" },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** The usual case: a compact display over an unrounded clipboard payload. */
|
|
48
|
+
export const Compacted: Story = {
|
|
49
|
+
args: { value: "50012102.632741", children: "50M" },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Inline inside a sentence — it reads as text, not as a link. */
|
|
53
|
+
export const InProse: Story = {
|
|
54
|
+
args: { value: "1500000" },
|
|
55
|
+
render: (args) => (
|
|
56
|
+
<p className="text-body text-foreground">
|
|
57
|
+
Revenue reached <CopyableValue {...args}>1.5M</CopyableValue> across the quarter.
|
|
58
|
+
</p>
|
|
59
|
+
),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** The hint is part of the accessible name, so voice control can say what it reads. */
|
|
63
|
+
export const CustomHint: Story = {
|
|
64
|
+
args: { value: "0.9612", children: "96.1%", hint: "Copy unrounded rate" },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Keyboard path: the value is a real button, so it is a tab stop and Enter copies.
|
|
69
|
+
* jsdom/Chromium may deny clipboard permission — the assertion is on the control's
|
|
70
|
+
* reachability and name, not on the clipboard contents.
|
|
71
|
+
*/
|
|
72
|
+
export const KeyboardReachable: Story = {
|
|
73
|
+
args: { value: "50012102.632741", children: "50M" },
|
|
74
|
+
play: async ({ canvasElement }) => {
|
|
75
|
+
const canvas = within(canvasElement);
|
|
76
|
+
const button = canvas.getByRole("button", { name: /50M Copy exact value/i });
|
|
77
|
+
await userEvent.tab();
|
|
78
|
+
await expect(button).toHaveFocus();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* copyable-value.test.tsx — "shown short, copied exact" is only defensible if
|
|
3
|
+
* the exact value is genuinely reachable, so the locks here are: the clipboard
|
|
4
|
+
* gets the UNROUNDED string, the visible text stays inside the accessible name
|
|
5
|
+
* (WCAG 2.5.3), the live region exists before the copy (ARIA22), and a missing
|
|
6
|
+
* clipboard degrades instead of throwing.
|
|
7
|
+
*
|
|
8
|
+
* `userEvent.setup()` installs its OWN clipboard stub, so every clipboard stub
|
|
9
|
+
* below is installed AFTER setup — otherwise the spy is silently replaced.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, it, vi } from "vitest";
|
|
12
|
+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
13
|
+
import userEvent from "@testing-library/user-event";
|
|
14
|
+
import { CopyableValue } from "./copyable-value";
|
|
15
|
+
|
|
16
|
+
/** Install a clipboard stub for one test; returns the writeText spy. */
|
|
17
|
+
function stubClipboard(impl: () => Promise<void> = () => Promise.resolve()) {
|
|
18
|
+
const writeText = vi.fn(impl);
|
|
19
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
20
|
+
value: { writeText },
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
});
|
|
24
|
+
return writeText;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("CopyableValue", () => {
|
|
28
|
+
it("shows the children and copies the exact value", async () => {
|
|
29
|
+
const writeText = stubClipboard();
|
|
30
|
+
render(<CopyableValue value="50012102.632741">50M</CopyableValue>);
|
|
31
|
+
|
|
32
|
+
const button = screen.getByRole("button");
|
|
33
|
+
expect(button).toHaveTextContent("50M");
|
|
34
|
+
|
|
35
|
+
fireEvent.click(button);
|
|
36
|
+
await waitFor(() => {
|
|
37
|
+
expect(writeText).toHaveBeenCalledWith("50012102.632741");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("falls back to the exact value as its own display", () => {
|
|
42
|
+
render(<CopyableValue value="1234" />);
|
|
43
|
+
expect(screen.getByRole("button")).toHaveTextContent("1234");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("keeps the visible text inside the accessible name (WCAG 2.5.3)", () => {
|
|
47
|
+
render(<CopyableValue value="1500000">1.5M</CopyableValue>);
|
|
48
|
+
// The hint is APPENDED, never substituted — a voice-control user must be
|
|
49
|
+
// able to say what they read.
|
|
50
|
+
expect(screen.getByRole("button", { name: "1.5M Copy exact value" })).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("accepts a custom hint", () => {
|
|
54
|
+
render(
|
|
55
|
+
<CopyableValue hint="Copy unrounded rate" value="0.9612">
|
|
56
|
+
96.1%
|
|
57
|
+
</CopyableValue>,
|
|
58
|
+
);
|
|
59
|
+
expect(screen.getByRole("button", { name: "96.1% Copy unrounded rate" })).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("mounts the live region before the copy so it can be announced (ARIA22)", async () => {
|
|
63
|
+
stubClipboard();
|
|
64
|
+
render(<CopyableValue value="42">42</CopyableValue>);
|
|
65
|
+
|
|
66
|
+
const status = screen.getByRole("status");
|
|
67
|
+
expect(status).toBeInTheDocument();
|
|
68
|
+
expect(status).toHaveTextContent("");
|
|
69
|
+
|
|
70
|
+
fireEvent.click(screen.getByRole("button"));
|
|
71
|
+
await waitFor(() => {
|
|
72
|
+
expect(screen.getByRole("status")).toHaveTextContent("Copied");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("marks the copied state on the control", async () => {
|
|
77
|
+
stubClipboard();
|
|
78
|
+
render(<CopyableValue value="42">42</CopyableValue>);
|
|
79
|
+
|
|
80
|
+
const button = screen.getByRole("button");
|
|
81
|
+
expect(button).not.toHaveAttribute("data-copied");
|
|
82
|
+
|
|
83
|
+
fireEvent.click(button);
|
|
84
|
+
await waitFor(() => {
|
|
85
|
+
expect(button).toHaveAttribute("data-copied");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("is a real tab stop and copies on Enter", async () => {
|
|
90
|
+
const user = userEvent.setup();
|
|
91
|
+
const writeText = stubClipboard();
|
|
92
|
+
render(<CopyableValue value="42">42</CopyableValue>);
|
|
93
|
+
|
|
94
|
+
await user.tab();
|
|
95
|
+
expect(screen.getByRole("button")).toHaveFocus();
|
|
96
|
+
await user.keyboard("{Enter}");
|
|
97
|
+
await waitFor(() => {
|
|
98
|
+
expect(writeText).toHaveBeenCalledWith("42");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("lets a consumer's onClick pre-empt the copy", () => {
|
|
103
|
+
const writeText = stubClipboard();
|
|
104
|
+
render(
|
|
105
|
+
<CopyableValue
|
|
106
|
+
onClick={(event) => {
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
}}
|
|
109
|
+
value="42"
|
|
110
|
+
>
|
|
111
|
+
42
|
|
112
|
+
</CopyableValue>,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
fireEvent.click(screen.getByRole("button"));
|
|
116
|
+
expect(writeText).not.toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("degrades where there is no clipboard (insecure context / old browser)", async () => {
|
|
120
|
+
Object.defineProperty(navigator, "clipboard", { value: undefined, configurable: true });
|
|
121
|
+
render(<CopyableValue value="42">42</CopyableValue>);
|
|
122
|
+
|
|
123
|
+
fireEvent.click(screen.getByRole("button"));
|
|
124
|
+
await waitFor(() => {
|
|
125
|
+
expect(screen.getByRole("status")).toHaveTextContent("");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|