@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,57 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, waitFor, within } from "storybook/test";
|
|
3
|
+
import { Button } from "../button";
|
|
4
|
+
import { HoverCard, HoverCardContent, HoverCardTrigger } from "./hover-card";
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Overlays/HoverCard",
|
|
7
|
+
component: HoverCard,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
argTypes: {
|
|
10
|
+
open: {
|
|
11
|
+
description: "Controlled open state of the hover card.",
|
|
12
|
+
control: "boolean",
|
|
13
|
+
table: { category: "State" },
|
|
14
|
+
},
|
|
15
|
+
defaultOpen: {
|
|
16
|
+
description: "Uncontrolled initial open state.",
|
|
17
|
+
control: "boolean",
|
|
18
|
+
table: { category: "State" },
|
|
19
|
+
},
|
|
20
|
+
onOpenChange: {
|
|
21
|
+
description: "Callback when the open state changes.",
|
|
22
|
+
control: false,
|
|
23
|
+
table: { category: "Events" },
|
|
24
|
+
},
|
|
25
|
+
openDelay: {
|
|
26
|
+
description: "Milliseconds to wait before opening on pointer enter. Default 700.",
|
|
27
|
+
control: "number",
|
|
28
|
+
table: { category: "Behaviour" },
|
|
29
|
+
},
|
|
30
|
+
closeDelay: {
|
|
31
|
+
description: "Milliseconds to wait before closing on pointer leave. Default 300.",
|
|
32
|
+
control: "number",
|
|
33
|
+
table: { category: "Behaviour" },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
} satisfies Meta<typeof HoverCard>;
|
|
37
|
+
export default meta;
|
|
38
|
+
type Story = StoryObj<typeof meta>;
|
|
39
|
+
export const Default: Story = {
|
|
40
|
+
render: () => (
|
|
41
|
+
<HoverCard openDelay={0} closeDelay={0}>
|
|
42
|
+
<HoverCardTrigger asChild>
|
|
43
|
+
<Button variant="link">@brand</Button>
|
|
44
|
+
</HoverCardTrigger>
|
|
45
|
+
<HoverCardContent>The brand-ui design system.</HoverCardContent>
|
|
46
|
+
</HoverCard>
|
|
47
|
+
),
|
|
48
|
+
// Hovers over the trigger (delay zeroed for determinism) to open the portalled
|
|
49
|
+
// hover card and confirms the content is visible.
|
|
50
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
51
|
+
const trigger = canvas.getByRole("button", { name: "@brand" });
|
|
52
|
+
await userEvent.hover(trigger);
|
|
53
|
+
const body = within(canvasElement.ownerDocument.body);
|
|
54
|
+
const card = await body.findByText("The brand-ui design system.");
|
|
55
|
+
await waitFor(() => expect(card).toBeVisible());
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
|
|
2
|
+
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
|
|
5
|
+
export const HoverCard = HoverCardPrimitive.Root;
|
|
6
|
+
export const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
|
7
|
+
|
|
8
|
+
export const HoverCardContent = forwardRef<
|
|
9
|
+
ElementRef<typeof HoverCardPrimitive.Content>,
|
|
10
|
+
ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
|
11
|
+
>(function HoverCardContent({ className, align = "center", sideOffset = 4, ...props }, ref) {
|
|
12
|
+
return (
|
|
13
|
+
<HoverCardPrimitive.Content
|
|
14
|
+
ref={ref}
|
|
15
|
+
align={align}
|
|
16
|
+
sideOffset={sideOffset}
|
|
17
|
+
className={cn(
|
|
18
|
+
"z-50 w-64 rounded-md bg-popover p-4 text-popover-foreground shadow-ring-md outline-none",
|
|
19
|
+
"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",
|
|
20
|
+
className,
|
|
21
|
+
)}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { HoverCard, HoverCardTrigger, HoverCardContent } from "./hover-card";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, waitFor, within } from "storybook/test";
|
|
3
|
+
import { RefreshCw, Trash2 } from "lucide-react";
|
|
4
|
+
import { IconButton } from "./icon-button";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Core/IconButton",
|
|
8
|
+
component: IconButton,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
args: {
|
|
11
|
+
label: "Refresh",
|
|
12
|
+
icon: <RefreshCw aria-hidden="true" />,
|
|
13
|
+
},
|
|
14
|
+
argTypes: {
|
|
15
|
+
label: {
|
|
16
|
+
description: "Accessible name AND tooltip text — a single source of truth.",
|
|
17
|
+
control: "text",
|
|
18
|
+
table: { category: "Content" },
|
|
19
|
+
},
|
|
20
|
+
icon: { control: false, table: { category: "Content" } },
|
|
21
|
+
variant: {
|
|
22
|
+
control: "select",
|
|
23
|
+
options: [
|
|
24
|
+
"default",
|
|
25
|
+
"secondary",
|
|
26
|
+
"destructive",
|
|
27
|
+
"outline",
|
|
28
|
+
"outline-subtle",
|
|
29
|
+
"ghost",
|
|
30
|
+
"link",
|
|
31
|
+
],
|
|
32
|
+
table: { category: "Appearance" },
|
|
33
|
+
},
|
|
34
|
+
size: {
|
|
35
|
+
control: "select",
|
|
36
|
+
options: ["icon", "icon-sm", "icon-lg"],
|
|
37
|
+
table: { category: "Appearance" },
|
|
38
|
+
},
|
|
39
|
+
disabled: { control: "boolean", table: { category: "State" } },
|
|
40
|
+
disabledReason: {
|
|
41
|
+
description:
|
|
42
|
+
"Shown appended to the tooltip and exposed via aria-describedby, even while disabled.",
|
|
43
|
+
control: "text",
|
|
44
|
+
table: { category: "State" },
|
|
45
|
+
},
|
|
46
|
+
side: {
|
|
47
|
+
control: "select",
|
|
48
|
+
options: ["top", "right", "bottom", "left"],
|
|
49
|
+
table: { category: "Appearance" },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
} satisfies Meta<typeof IconButton>;
|
|
53
|
+
export default meta;
|
|
54
|
+
type Story = StoryObj<typeof meta>;
|
|
55
|
+
|
|
56
|
+
export const Default: Story = {
|
|
57
|
+
// Opens on keyboard focus (no pointer) — the a11y-critical path this
|
|
58
|
+
// component exists for (#335). The tooltip content is Radix-portalled to
|
|
59
|
+
// `document.body`, outside the story's own canvas root — query the owner
|
|
60
|
+
// document's body, not `canvas` (mirrors Overlays/Tooltip's Default story).
|
|
61
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
62
|
+
const button = canvas.getByRole("button", { name: "Refresh" });
|
|
63
|
+
await expect(button).not.toHaveAttribute("title");
|
|
64
|
+
await userEvent.tab();
|
|
65
|
+
await expect(button).toHaveFocus();
|
|
66
|
+
const body = within(canvasElement.ownerDocument.body);
|
|
67
|
+
const tooltip = await body.findByText("Refresh", { selector: "[data-side]" });
|
|
68
|
+
await waitFor(() => expect(tooltip).toBeVisible());
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const Disabled: Story = {
|
|
73
|
+
args: { disabled: true },
|
|
74
|
+
play: async ({ canvas }) => {
|
|
75
|
+
const button = canvas.getByRole("button", { name: "Refresh" });
|
|
76
|
+
await expect(button).toBeDisabled();
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** A disabled control that still explains WHY via the tooltip + `aria-describedby`. */
|
|
81
|
+
export const DisabledWithReason: Story = {
|
|
82
|
+
name: "Disabled (with reason)",
|
|
83
|
+
args: {
|
|
84
|
+
label: "Delete",
|
|
85
|
+
icon: <Trash2 aria-hidden="true" />,
|
|
86
|
+
variant: "destructive",
|
|
87
|
+
disabled: true,
|
|
88
|
+
disabledReason: "You don't have permission to delete this item.",
|
|
89
|
+
},
|
|
90
|
+
play: async ({ canvas }) => {
|
|
91
|
+
const button = canvas.getByRole("button", { name: "Delete" });
|
|
92
|
+
await expect(button).toBeDisabled();
|
|
93
|
+
const describedBy = button.getAttribute("aria-describedby");
|
|
94
|
+
await expect(describedBy).toBeTruthy();
|
|
95
|
+
const description = canvas.getByText("You don't have permission to delete this item.");
|
|
96
|
+
await expect(description.id).toBe(describedBy);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const Sizes: Story = {
|
|
101
|
+
render: (args) => (
|
|
102
|
+
<div className="flex items-center gap-2">
|
|
103
|
+
<IconButton {...args} size="icon-sm" label="Small" />
|
|
104
|
+
<IconButton {...args} size="icon" label="Default" />
|
|
105
|
+
<IconButton {...args} size="icon-lg" label="Large" />
|
|
106
|
+
</div>
|
|
107
|
+
),
|
|
108
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import { RefreshCw } from "lucide-react";
|
|
6
|
+
import { IconButton } from "./icon-button";
|
|
7
|
+
|
|
8
|
+
describe("IconButton", () => {
|
|
9
|
+
it("has exactly one accessible name and no title attribute anywhere in the output", () => {
|
|
10
|
+
render(<IconButton label="Refresh" icon={<RefreshCw aria-hidden="true" />} />);
|
|
11
|
+
const button = screen.getByRole("button", { name: "Refresh" });
|
|
12
|
+
expect(button).not.toHaveAttribute("title");
|
|
13
|
+
expect(document.querySelector("[title]")).toBeNull();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("opens the tooltip on keyboard focus alone (no pointer interaction)", async () => {
|
|
17
|
+
render(<IconButton label="Refresh" icon={<RefreshCw aria-hidden="true" />} />);
|
|
18
|
+
await userEvent.tab();
|
|
19
|
+
expect(screen.getByRole("button", { name: "Refresh" })).toHaveFocus();
|
|
20
|
+
const tooltip = await screen.findByText("Refresh", { selector: "[data-side]" });
|
|
21
|
+
await waitFor(() => expect(tooltip).toBeVisible());
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("exposes disabledReason via aria-describedby on an always-present element, even while pointer-events-none", () => {
|
|
25
|
+
render(
|
|
26
|
+
<IconButton
|
|
27
|
+
label="Delete"
|
|
28
|
+
icon={<RefreshCw aria-hidden="true" />}
|
|
29
|
+
disabled
|
|
30
|
+
disabledReason="You don't have permission"
|
|
31
|
+
/>,
|
|
32
|
+
);
|
|
33
|
+
const button = screen.getByRole("button", { name: "Delete" });
|
|
34
|
+
expect(button).toBeDisabled();
|
|
35
|
+
const describedBy = button.getAttribute("aria-describedby");
|
|
36
|
+
expect(describedBy).toBeTruthy();
|
|
37
|
+
const node = describedBy ? document.getElementById(describedBy) : null;
|
|
38
|
+
expect(node).not.toBeNull();
|
|
39
|
+
expect(node).toHaveTextContent("You don't have permission");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("does not render a description node (or aria-describedby) when no disabledReason is given", () => {
|
|
43
|
+
render(<IconButton label="Refresh" icon={<RefreshCw aria-hidden="true" />} />);
|
|
44
|
+
const button = screen.getByRole("button", { name: "Refresh" });
|
|
45
|
+
expect(button).not.toHaveAttribute("aria-describedby");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// While disabled, the wrapper span — not the button — is the tab stop, so it
|
|
49
|
+
// must carry the tokened focus ring; without it the browser's own hardcoded
|
|
50
|
+
// blue outline shows through, identically in every theme.
|
|
51
|
+
it("gives the disabled-state tab stop the tokened focus ring", async () => {
|
|
52
|
+
render(
|
|
53
|
+
<IconButton
|
|
54
|
+
label="Delete"
|
|
55
|
+
icon={<RefreshCw aria-hidden="true" />}
|
|
56
|
+
disabled
|
|
57
|
+
disabledReason="No permission"
|
|
58
|
+
/>,
|
|
59
|
+
);
|
|
60
|
+
const wrapper = document.querySelector('[data-slot="icon-button"]')!;
|
|
61
|
+
expect(wrapper).toHaveAttribute("tabindex", "0");
|
|
62
|
+
expect(wrapper.className).toContain("focus-visible:ring-ring");
|
|
63
|
+
await userEvent.tab();
|
|
64
|
+
expect(wrapper).toHaveFocus();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("does not put the wrapper in the tab order when enabled (no phantom stop)", async () => {
|
|
68
|
+
render(<IconButton label="Refresh" icon={<RefreshCw aria-hidden="true" />} />);
|
|
69
|
+
const wrapper = document.querySelector('[data-slot="icon-button"]')!;
|
|
70
|
+
expect(wrapper).not.toHaveAttribute("tabindex");
|
|
71
|
+
await userEvent.tab();
|
|
72
|
+
expect(screen.getByRole("button", { name: "Refresh" })).toHaveFocus();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("forwards the ref to the underlying <button>", () => {
|
|
76
|
+
const ref = createRef<HTMLButtonElement>();
|
|
77
|
+
render(<IconButton ref={ref} label="Refresh" icon={<RefreshCw aria-hidden="true" />} />);
|
|
78
|
+
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
|
|
79
|
+
expect(ref.current).toBe(screen.getByRole("button", { name: "Refresh" }));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("cannot have its computed aria-label overridden by a stray prop (single source of truth)", () => {
|
|
83
|
+
// TypeScript's JSX checker special-cases aria-*/data-* attribute names on
|
|
84
|
+
// ANY element regardless of the declared prop type, so `aria-label` is
|
|
85
|
+
// syntactically accepted here even though `IconButtonProps` omits it —
|
|
86
|
+
// this test locks the RUNTIME guarantee that it can never win.
|
|
87
|
+
render(
|
|
88
|
+
<IconButton label="Refresh" icon={<RefreshCw aria-hidden="true" />} aria-label="Wrong" />,
|
|
89
|
+
);
|
|
90
|
+
expect(screen.getByRole("button", { name: "Refresh" })).toBeInTheDocument();
|
|
91
|
+
expect(screen.queryByRole("button", { name: "Wrong" })).not.toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useId,
|
|
4
|
+
type ButtonHTMLAttributes,
|
|
5
|
+
type ComponentProps,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react";
|
|
8
|
+
import type { VariantProps } from "class-variance-authority";
|
|
9
|
+
import { cn } from "../../lib/cn";
|
|
10
|
+
import { Button } from "../button";
|
|
11
|
+
import type { buttonVariants } from "../button";
|
|
12
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip";
|
|
13
|
+
|
|
14
|
+
/** The icon-only size axis (a deliberate subset of `buttonVariants`' `size`). */
|
|
15
|
+
export type IconButtonSize = "icon" | "icon-sm" | "icon-lg";
|
|
16
|
+
|
|
17
|
+
export interface IconButtonProps
|
|
18
|
+
extends
|
|
19
|
+
Omit<
|
|
20
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
21
|
+
"title" | "children" | "aria-label" | "aria-describedby"
|
|
22
|
+
>,
|
|
23
|
+
Pick<VariantProps<typeof buttonVariants>, "variant"> {
|
|
24
|
+
/**
|
|
25
|
+
* Accessible name AND tooltip text — a single source of truth, so the two
|
|
26
|
+
* can never drift out of sync (the failure this component exists to close:
|
|
27
|
+
* `aria-label` and a tooltip's children set independently, most call sites
|
|
28
|
+
* only setting one).
|
|
29
|
+
*/
|
|
30
|
+
label: string;
|
|
31
|
+
/** The glyph (typically a `lucide-react` element). */
|
|
32
|
+
icon: ReactNode;
|
|
33
|
+
/** Icon-only size axis. Default `"icon"`. */
|
|
34
|
+
size?: IconButtonSize;
|
|
35
|
+
/**
|
|
36
|
+
* Shown appended to the tooltip text, and — independently — exposed via
|
|
37
|
+
* `aria-describedby` through an `sr-only` node that is present whenever this
|
|
38
|
+
* prop is (i.e. it does NOT depend on the tooltip being open, and it is not
|
|
39
|
+
* rendered at all when there is no reason — so it can never be read as stray
|
|
40
|
+
* text), so assistive tech gets the reason even while the button is disabled
|
|
41
|
+
* (a disabled
|
|
42
|
+
* `<button>` still exposes its ARIA attributes; it just isn't
|
|
43
|
+
* focusable/hoverable itself, which is why the tooltip trigger wraps a
|
|
44
|
+
* focusable `<span>` instead — see the render below).
|
|
45
|
+
*/
|
|
46
|
+
disabledReason?: string;
|
|
47
|
+
/** Tooltip placement. Default `"top"`. */
|
|
48
|
+
side?: ComponentProps<typeof TooltipContent>["side"];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A single affordance for an icon-only control: `label` becomes both the
|
|
53
|
+
* `aria-label` and the tooltip text, so they cannot diverge. Composes
|
|
54
|
+
* `Button` + `Tooltip` — it does not reimplement either.
|
|
55
|
+
*
|
|
56
|
+
* Deliberately has **no `asChild`**: accepting one would dissolve the button
|
|
57
|
+
* into a Radix `Slot` and break the tooltip-trigger wrapper below. A
|
|
58
|
+
* link-styled icon button should compose `Button asChild` + `Tooltip`
|
|
59
|
+
* directly instead.
|
|
60
|
+
*/
|
|
61
|
+
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(function IconButton(
|
|
62
|
+
{
|
|
63
|
+
label,
|
|
64
|
+
icon,
|
|
65
|
+
variant = "ghost",
|
|
66
|
+
size = "icon",
|
|
67
|
+
disabled,
|
|
68
|
+
disabledReason,
|
|
69
|
+
side = "top",
|
|
70
|
+
className,
|
|
71
|
+
...props
|
|
72
|
+
},
|
|
73
|
+
ref,
|
|
74
|
+
) {
|
|
75
|
+
const reasonId = useId();
|
|
76
|
+
// The visible tooltip text always LEADS with `label` verbatim (never only
|
|
77
|
+
// the reason), so it can never contradict the accessible name (WCAG 2.5.3
|
|
78
|
+
// Label in Name) — the reason is appended context, not a replacement name.
|
|
79
|
+
const tooltipText = disabledReason ? `${label} — ${disabledReason}` : label;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
// Self-provided so this works anywhere — a consumer shouldn't have to
|
|
83
|
+
// remember to mount a global TooltipProvider for one icon button.
|
|
84
|
+
<TooltipProvider>
|
|
85
|
+
<Tooltip>
|
|
86
|
+
<TooltipTrigger asChild>
|
|
87
|
+
{/*
|
|
88
|
+
* A disabled `Button` carries `pointer-events-none`, so hover never
|
|
89
|
+
* reaches it and the tooltip would never open. Wrap the trigger
|
|
90
|
+
* around a plain <span> instead of the <button> itself: it stays
|
|
91
|
+
* hoverable always, and gains `tabIndex={0}` only while the inner
|
|
92
|
+
* control is disabled (and therefore removed from the tab order),
|
|
93
|
+
* so keyboard users can still reach the tooltip without a doubled
|
|
94
|
+
* stop when the button is enabled.
|
|
95
|
+
*/}
|
|
96
|
+
<span
|
|
97
|
+
data-slot="icon-button"
|
|
98
|
+
tabIndex={disabled ? 0 : undefined}
|
|
99
|
+
className={cn(
|
|
100
|
+
"inline-flex rounded-md",
|
|
101
|
+
// While disabled this span IS the focus target (the button is out
|
|
102
|
+
// of the tab order), so it must carry the system's focus ring —
|
|
103
|
+
// without this it falls back to the UA default outline, which is
|
|
104
|
+
// a hardcoded browser blue identical in all three themes.
|
|
105
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
106
|
+
disabled && "cursor-not-allowed",
|
|
107
|
+
)}
|
|
108
|
+
>
|
|
109
|
+
<Button
|
|
110
|
+
ref={ref}
|
|
111
|
+
variant={variant}
|
|
112
|
+
size={size}
|
|
113
|
+
disabled={disabled}
|
|
114
|
+
{...props}
|
|
115
|
+
// Computed AFTER the spread so a stray `aria-label`/`aria-describedby`
|
|
116
|
+
// in `...props` can never win over the derived, single-source-of-truth
|
|
117
|
+
// values (the type above already excludes both, this is defense in
|
|
118
|
+
// depth for untyped/JS callers).
|
|
119
|
+
aria-label={label}
|
|
120
|
+
aria-describedby={disabledReason ? reasonId : undefined}
|
|
121
|
+
data-slot="icon-button-control"
|
|
122
|
+
className={className}
|
|
123
|
+
>
|
|
124
|
+
{icon}
|
|
125
|
+
</Button>
|
|
126
|
+
</span>
|
|
127
|
+
</TooltipTrigger>
|
|
128
|
+
<TooltipContent side={side} data-slot="icon-button-tooltip">
|
|
129
|
+
{tooltipText}
|
|
130
|
+
</TooltipContent>
|
|
131
|
+
</Tooltip>
|
|
132
|
+
{disabledReason ? (
|
|
133
|
+
<span id={reasonId} data-slot="icon-button-description" className="sr-only">
|
|
134
|
+
{disabledReason}
|
|
135
|
+
</span>
|
|
136
|
+
) : null}
|
|
137
|
+
</TooltipProvider>
|
|
138
|
+
);
|
|
139
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { IconButton, type IconButtonProps, type IconButtonSize } from "./icon-button";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Input, type InputProps } from "./input";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { Input } from "./input";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Core/Input",
|
|
6
|
+
component: Input,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
args: { placeholder: "you@example.com" },
|
|
9
|
+
argTypes: {
|
|
10
|
+
type: {
|
|
11
|
+
description:
|
|
12
|
+
"HTML input type — controls browser behaviour, autocomplete, and mobile keyboard.",
|
|
13
|
+
control: { type: "select" },
|
|
14
|
+
options: ["text", "email", "password", "number", "tel", "url", "search", "date", "file"],
|
|
15
|
+
table: { category: "Behavior" },
|
|
16
|
+
},
|
|
17
|
+
placeholder: {
|
|
18
|
+
description: "Placeholder hint (should show an example, end with …).",
|
|
19
|
+
control: "text",
|
|
20
|
+
table: { category: "Content" },
|
|
21
|
+
},
|
|
22
|
+
disabled: {
|
|
23
|
+
description: "Disables the input — cursor-not-allowed, reduced opacity, muted background.",
|
|
24
|
+
control: "boolean",
|
|
25
|
+
table: { category: "State" },
|
|
26
|
+
},
|
|
27
|
+
"aria-invalid": {
|
|
28
|
+
description: "Marks the field invalid — shows destructive ring / border.",
|
|
29
|
+
control: "boolean",
|
|
30
|
+
table: { category: "State" },
|
|
31
|
+
},
|
|
32
|
+
className: {
|
|
33
|
+
description: "Extra Tailwind classes merged via cn().",
|
|
34
|
+
control: "text",
|
|
35
|
+
table: { category: "Appearance" },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
} satisfies Meta<typeof Input>;
|
|
39
|
+
export default meta;
|
|
40
|
+
type Story = StoryObj<typeof meta>;
|
|
41
|
+
|
|
42
|
+
export const Default: Story = {};
|
|
43
|
+
export const Invalid: Story = { args: { "aria-invalid": true, defaultValue: "not-an-email" } };
|
|
44
|
+
export const Disabled: Story = { args: { disabled: true, value: "Disabled" } };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { Input } from "./input";
|
|
5
|
+
|
|
6
|
+
describe("Input", () => {
|
|
7
|
+
it("accepts typed text", async () => {
|
|
8
|
+
render(<Input aria-label="email" />);
|
|
9
|
+
const el = screen.getByLabelText<HTMLInputElement>("email");
|
|
10
|
+
await userEvent.type(el, "hi");
|
|
11
|
+
expect(el.value).toBe("hi");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("is disabled when disabled prop is set", () => {
|
|
15
|
+
render(<Input aria-label="email" disabled />);
|
|
16
|
+
const el = screen.getByLabelText<HTMLInputElement>("email");
|
|
17
|
+
expect(el).toBeDisabled();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { forwardRef, type InputHTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type InputProps = InputHTMLAttributes<HTMLInputElement>;
|
|
5
|
+
|
|
6
|
+
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|
7
|
+
{ className, type = "text", ...props },
|
|
8
|
+
ref,
|
|
9
|
+
) {
|
|
10
|
+
return (
|
|
11
|
+
<input
|
|
12
|
+
ref={ref}
|
|
13
|
+
type={type}
|
|
14
|
+
className={cn(
|
|
15
|
+
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-[color,background-color,border-color,box-shadow] duration-fast ease-standard",
|
|
16
|
+
"placeholder:text-muted-foreground",
|
|
17
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
18
|
+
"disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-muted disabled:border-border",
|
|
19
|
+
"aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive",
|
|
20
|
+
"file:border-0 file:bg-transparent file:text-sm file:font-medium",
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { Search } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
InputGroup,
|
|
5
|
+
InputGroupAddon,
|
|
6
|
+
InputGroupButton,
|
|
7
|
+
InputGroupInput,
|
|
8
|
+
InputGroupText,
|
|
9
|
+
} from "./input-group";
|
|
10
|
+
const meta = {
|
|
11
|
+
title: "Forms/InputGroup",
|
|
12
|
+
component: InputGroup,
|
|
13
|
+
tags: ["autodocs"],
|
|
14
|
+
argTypes: {
|
|
15
|
+
variant: {
|
|
16
|
+
description:
|
|
17
|
+
"`outline` — bordered form-field look (default). `surface` — soft filled composer look with no hard border. `card` — `bg-card` well for nesting inside an already-tinted outer frame (the double card, #254); its fill is theme-driven, not universally white — raised on light themes, recessed on dark.",
|
|
18
|
+
control: { type: "radio" },
|
|
19
|
+
options: ["outline", "surface", "card"],
|
|
20
|
+
table: { category: "Appearance" },
|
|
21
|
+
},
|
|
22
|
+
className: {
|
|
23
|
+
description: "Additional CSS classes applied to the root element.",
|
|
24
|
+
control: "text",
|
|
25
|
+
table: { category: "Appearance" },
|
|
26
|
+
},
|
|
27
|
+
children: {
|
|
28
|
+
description: "Compose with InputGroupAddon, InputGroupInput, InputGroupButton, etc.",
|
|
29
|
+
control: false,
|
|
30
|
+
table: { category: "Content" },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
} satisfies Meta<typeof InputGroup>;
|
|
34
|
+
export default meta;
|
|
35
|
+
type Story = StoryObj<typeof meta>;
|
|
36
|
+
export const Search_: Story = {
|
|
37
|
+
name: "Search",
|
|
38
|
+
render: () => (
|
|
39
|
+
<InputGroup className="max-w-sm">
|
|
40
|
+
<InputGroupAddon>
|
|
41
|
+
<Search />
|
|
42
|
+
</InputGroupAddon>
|
|
43
|
+
<InputGroupInput placeholder="Search…" />
|
|
44
|
+
</InputGroup>
|
|
45
|
+
),
|
|
46
|
+
};
|
|
47
|
+
export const WithButton: Story = {
|
|
48
|
+
render: () => (
|
|
49
|
+
<InputGroup className="max-w-sm">
|
|
50
|
+
<InputGroupAddon>
|
|
51
|
+
<InputGroupText>$</InputGroupText>
|
|
52
|
+
</InputGroupAddon>
|
|
53
|
+
<InputGroupInput placeholder="0.00" />
|
|
54
|
+
<InputGroupAddon align="inline-end">
|
|
55
|
+
<InputGroupButton size="sm">USD</InputGroupButton>
|
|
56
|
+
</InputGroupAddon>
|
|
57
|
+
</InputGroup>
|
|
58
|
+
),
|
|
59
|
+
};
|
|
60
|
+
// The composer look (#194, research 08 §C.1): soft `bg-surface-muted` fill +
|
|
61
|
+
// the focus-within ring — no hard border. Used by `PromptInput` in @elabs-ai/components-ai.
|
|
62
|
+
export const Surface: Story = {
|
|
63
|
+
render: () => (
|
|
64
|
+
<InputGroup variant="surface" className="max-w-sm">
|
|
65
|
+
<InputGroupAddon>
|
|
66
|
+
<Search />
|
|
67
|
+
</InputGroupAddon>
|
|
68
|
+
<InputGroupInput placeholder="Ask anything…" />
|
|
69
|
+
</InputGroup>
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
// The "double card" well (#254): nest a `card` InputGroup inside an
|
|
73
|
+
// already-tinted outer frame — `PromptInput`'s `tone="card"` prop wires this
|
|
74
|
+
// into the AI composer. The well's fill is theme-driven (raised on light
|
|
75
|
+
// themes, recessed on dark), not universally white.
|
|
76
|
+
export const Card: Story = {
|
|
77
|
+
render: () => (
|
|
78
|
+
<div className="max-w-sm rounded-xl bg-surface-muted p-1.5">
|
|
79
|
+
<InputGroup variant="card">
|
|
80
|
+
<InputGroupAddon>
|
|
81
|
+
<Search />
|
|
82
|
+
</InputGroupAddon>
|
|
83
|
+
<InputGroupInput placeholder="Ask anything…" />
|
|
84
|
+
</InputGroup>
|
|
85
|
+
</div>
|
|
86
|
+
),
|
|
87
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import { InputGroup } from "./input-group";
|
|
4
|
+
|
|
5
|
+
describe("InputGroup", () => {
|
|
6
|
+
// Regression lock for #8: the auto-height selector must match ANY descendant
|
|
7
|
+
// textarea (so it works through PromptInputBody's `display:contents` wrapper),
|
|
8
|
+
// not just a direct child. jsdom can't compute `:has()` layout, so we lock the
|
|
9
|
+
// class contract instead.
|
|
10
|
+
it("uses a descendant `has-[textarea]` selector (not direct-child) to auto-grow", () => {
|
|
11
|
+
const { container } = render(
|
|
12
|
+
<InputGroup>
|
|
13
|
+
<div className="contents">
|
|
14
|
+
<textarea data-slot="input-group-control" />
|
|
15
|
+
</div>
|
|
16
|
+
</InputGroup>,
|
|
17
|
+
);
|
|
18
|
+
const group = container.querySelector('[data-slot="input-group"]') as HTMLElement;
|
|
19
|
+
expect(group).not.toBeNull();
|
|
20
|
+
expect(group.className).toContain("has-[textarea]:h-auto");
|
|
21
|
+
expect(group.className).not.toContain("has-[>textarea]");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// #194 (research 08 §C.1/§D): the composer variant separates by soft fill +
|
|
25
|
+
// focus ring, never a hard border; the default stays the form-field outline.
|
|
26
|
+
it("variant='surface' uses the soft fill with no border; default keeps border-input", () => {
|
|
27
|
+
const { container } = render(<InputGroup variant="surface" />);
|
|
28
|
+
const surface = container.querySelector('[data-slot="input-group"]') as HTMLElement;
|
|
29
|
+
expect(surface.className.split(" ")).toContain("bg-surface-muted");
|
|
30
|
+
expect(surface.className.split(" ")).not.toContain("border");
|
|
31
|
+
expect(surface.className).not.toContain("border-input");
|
|
32
|
+
|
|
33
|
+
const { container: c2 } = render(<InputGroup />);
|
|
34
|
+
const outline = c2.querySelector('[data-slot="input-group"]') as HTMLElement;
|
|
35
|
+
expect(outline.className.split(" ")).toContain("border-input");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// #254: the "double card" needs a `bg-card` well with no border of its own
|
|
39
|
+
// (the outer frame supplies the tint; the well's own fill is theme-driven,
|
|
40
|
+
// not universally white — see the JSDoc on `inputGroupVariants`).
|
|
41
|
+
it("variant='card' uses bg-card with no border", () => {
|
|
42
|
+
const { container } = render(<InputGroup variant="card" />);
|
|
43
|
+
const card = container.querySelector('[data-slot="input-group"]') as HTMLElement;
|
|
44
|
+
expect(card.className.split(" ")).toContain("bg-card");
|
|
45
|
+
expect(card.className.split(" ")).not.toContain("border");
|
|
46
|
+
expect(card.className).not.toContain("border-input");
|
|
47
|
+
});
|
|
48
|
+
});
|