@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,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline — THE rail/node/connector spine (#190, research 10 §B.2): an
|
|
3
|
+
* ordered list of steps with a vertical connector and a status-colored node
|
|
4
|
+
* per step. Moved from `@elabs-ai/components-editor` (which re-exports it for back-compat —
|
|
5
|
+
* the ADR-0012 own/re-export model); the branded target for the editor's
|
|
6
|
+
* `:::timeline` markdown directive, and reusable anywhere a process/sequence
|
|
7
|
+
* needs showing. Token-driven, theme-safe, motion-free (animation belongs to
|
|
8
|
+
* composing consumers, e.g. the `@elabs-ai/components-ai` agent grammar).
|
|
9
|
+
*
|
|
10
|
+
* One rail, two front doors:
|
|
11
|
+
* - compound `TimelineRoot` + `TimelineItem` — for composing consumers that
|
|
12
|
+
* interleave rich children per node. The node takes the CANONICAL 7-state
|
|
13
|
+
* `Status` (status-badge, #189) — closed enum, no freeform strings.
|
|
14
|
+
* - array `Timeline items={TimelineEntry[]}` — a thin map over the compound
|
|
15
|
+
* parts. Keeps the editor-facing `done|active|pending` API unchanged by
|
|
16
|
+
* mapping in via `fromTimelineStatus` at the boundary.
|
|
17
|
+
*
|
|
18
|
+
* Fork-prevention: a Timeline re-implementation outside this folder fails
|
|
19
|
+
* `pnpm timeline-fork:check` (scripts/check-timeline-fork.mjs).
|
|
20
|
+
*/
|
|
21
|
+
import { forwardRef, type HTMLAttributes, type LiHTMLAttributes, type ReactNode } from "react";
|
|
22
|
+
import { cn } from "../../lib/cn";
|
|
23
|
+
import {
|
|
24
|
+
fromTimelineStatus,
|
|
25
|
+
STATUS_LABELS,
|
|
26
|
+
type Status,
|
|
27
|
+
type TimelineStatus,
|
|
28
|
+
} from "../status-badge";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Node visual per canonical status. This is a DUPLICATE of the status→role
|
|
32
|
+
* mapping `status-badge.tsx` owns (`STATUS_ROLE`) — it exists because the rail
|
|
33
|
+
* node is a filled dot, not a badge pill, so it can't share `statusBadgeVariants`
|
|
34
|
+
* directly. A duplicate is only safe when it is TESTED against the source of
|
|
35
|
+
* truth: `timeline.test.tsx` asserts every chromatic status here resolves to
|
|
36
|
+
* the same role as `STATUS_ROLE`/`statusBadgeVariants` (#392 — `running` had
|
|
37
|
+
* silently drifted to `--primary` here while the canonical map said `--info`,
|
|
38
|
+
* because nothing checked the two agreed). Semantic tokens only.
|
|
39
|
+
*
|
|
40
|
+
* Every status ALSO carries a non-colour signature (#387, WCAG 1.4.1) — a
|
|
41
|
+
* 12px dot can't host a legible glyph, so the cue is geometric. It took an
|
|
42
|
+
* empirical (rendered) pass to find a signature that actually PAINTS
|
|
43
|
+
* distinctly, not merely one that differs as a class string:
|
|
44
|
+
* - `denied`/`skipped` (hollow-ish `bg-muted`, `border-border`) get
|
|
45
|
+
* `border-dashed`/`border-dotted` — this WORKS because border and fill
|
|
46
|
+
* are different colours there, so the gaps in the dash/dot pattern show
|
|
47
|
+
* the (lighter) fill through the (darker) border.
|
|
48
|
+
* - `running`/`complete`/`awaiting-approval`/`failed` are all `bg-<status>
|
|
49
|
+
* border-<status>` — the vivid MARK rung, where border and fill are the
|
|
50
|
+
* SAME token. A `border-style` on a same-colour border is invisible (no
|
|
51
|
+
* colour boundary for the dash gaps to reveal), so this quartet is
|
|
52
|
+
* differentiated by `ring-*` WIDTH instead — the ring sits outside the
|
|
53
|
+
* dot, contrasting against the PAGE, not the fill, which is why
|
|
54
|
+
* `running`'s existing halo was legible in the first place. Widths
|
|
55
|
+
* `0 / 1 / 2 / 4` give four unmistakably different silhouettes:
|
|
56
|
+
* `complete` none (calm/settled, matches `StatusBadge`'s quiet
|
|
57
|
+
* treatment for `complete`) → `awaiting-approval` `ring-1` (a nudge) →
|
|
58
|
+
* `running` `ring-2` (active) → `failed` `ring-4` (the widest, most
|
|
59
|
+
* emphatic — the state that most needs to grab the eye).
|
|
60
|
+
* `denied`/`skipped` were byte-identical before this; the ring-width
|
|
61
|
+
* progression replaces an earlier border-style-only attempt that turned out
|
|
62
|
+
* to be visually void on the vivid quartet — verified in `timeline.test.tsx`
|
|
63
|
+
* (structural) and by rendered zoomed + greyscale screenshots (see #387
|
|
64
|
+
* rendered proof) — don't reintroduce border-style there without re-checking
|
|
65
|
+
* a render, it will look correct in code and disappear on screen.
|
|
66
|
+
*/
|
|
67
|
+
export const NODE_STYLE: Record<Status, string> = {
|
|
68
|
+
pending: "border-border bg-background",
|
|
69
|
+
running: "border-info bg-info ring-2 ring-info/25",
|
|
70
|
+
complete: "border-success bg-success",
|
|
71
|
+
"awaiting-approval": "border-warning bg-warning ring-1 ring-warning/40",
|
|
72
|
+
denied: "border-border bg-muted border-dashed",
|
|
73
|
+
failed: "border-destructive bg-destructive ring-4 ring-destructive/20",
|
|
74
|
+
skipped: "border-border bg-muted border-dotted",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Statuses whose title reads de-emphasized (not-yet/never-run steps). */
|
|
78
|
+
const MUTED_TITLE_STATUSES: ReadonlySet<Status> = new Set(["pending", "skipped", "denied"]);
|
|
79
|
+
|
|
80
|
+
export type TimelineRootProps = HTMLAttributes<HTMLOListElement>;
|
|
81
|
+
|
|
82
|
+
/** The `<ol>` that owns the rail geometry; compose `TimelineItem`s inside. */
|
|
83
|
+
export const TimelineRoot = forwardRef<HTMLOListElement, TimelineRootProps>(function TimelineRoot(
|
|
84
|
+
{ className, ...props },
|
|
85
|
+
ref,
|
|
86
|
+
) {
|
|
87
|
+
return <ol ref={ref} className={cn("relative", className)} {...props} />;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export interface TimelineItemProps extends LiHTMLAttributes<HTMLLIElement> {
|
|
91
|
+
/** The canonical execution status (closed 7-state enum, status-badge #189). */
|
|
92
|
+
status?: Status;
|
|
93
|
+
/** Right-aligned meta (e.g. a date) on the title row. */
|
|
94
|
+
timestamp?: ReactNode;
|
|
95
|
+
/** Secondary line under the title. */
|
|
96
|
+
description?: ReactNode;
|
|
97
|
+
/**
|
|
98
|
+
* Rich block content under the title/description, indented with the rail —
|
|
99
|
+
* the slot composing consumers (e.g. `@elabs-ai/components-ai`'s `AgentStep`, #192)
|
|
100
|
+
* interleave per node. Block-level, so it must NOT ride the inline title
|
|
101
|
+
* slot (`children`).
|
|
102
|
+
*/
|
|
103
|
+
detail?: ReactNode;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* One `<li>`: status node + connector + the title row; `children` is the
|
|
108
|
+
* title slot. The connector hides itself on the last item via CSS
|
|
109
|
+
* (`group-last`), so streaming/compound consumers never track "is last".
|
|
110
|
+
*/
|
|
111
|
+
export const TimelineItem = forwardRef<HTMLLIElement, TimelineItemProps>(function TimelineItem(
|
|
112
|
+
{ status = "pending", timestamp, description, detail, className, children, ...props },
|
|
113
|
+
ref,
|
|
114
|
+
) {
|
|
115
|
+
return (
|
|
116
|
+
<li
|
|
117
|
+
ref={ref}
|
|
118
|
+
data-status={status}
|
|
119
|
+
className={cn("group/timeline-item relative pb-5 ps-7 last:pb-0", className)}
|
|
120
|
+
{...props}
|
|
121
|
+
>
|
|
122
|
+
<span
|
|
123
|
+
aria-hidden="true"
|
|
124
|
+
className="absolute start-[5px] top-2.5 h-full w-px bg-border group-last/timeline-item:hidden"
|
|
125
|
+
/>
|
|
126
|
+
<span
|
|
127
|
+
aria-hidden="true"
|
|
128
|
+
className={cn("absolute start-0 top-1 size-3 rounded-full border-2", NODE_STYLE[status])}
|
|
129
|
+
/>
|
|
130
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
131
|
+
<span
|
|
132
|
+
className={cn(
|
|
133
|
+
"text-body font-medium",
|
|
134
|
+
MUTED_TITLE_STATUSES.has(status) ? "text-muted-foreground" : "text-foreground",
|
|
135
|
+
)}
|
|
136
|
+
>
|
|
137
|
+
{/* The dot is `aria-hidden` and `data-status` isn't AT-visible (#387) —
|
|
138
|
+
this is the step's only announced status; kept inline so it reads
|
|
139
|
+
naturally as "<Status>: <title>" instead of a second live region. */}
|
|
140
|
+
<span className="sr-only">{STATUS_LABELS[status]}: </span>
|
|
141
|
+
{children}
|
|
142
|
+
</span>
|
|
143
|
+
{timestamp ? (
|
|
144
|
+
<span className="shrink-0 text-meta text-muted-foreground">{timestamp}</span>
|
|
145
|
+
) : null}
|
|
146
|
+
</div>
|
|
147
|
+
{description ? <p className="mt-0.5 text-body text-muted-foreground">{description}</p> : null}
|
|
148
|
+
{detail ? <div className="mt-2 space-y-2">{detail}</div> : null}
|
|
149
|
+
</li>
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* One step of the array-API `Timeline` (the editor's original item shape —
|
|
155
|
+
* re-exported by `@elabs-ai/components-editor` as `TimelineItem` for back-compat).
|
|
156
|
+
*/
|
|
157
|
+
export interface TimelineEntry {
|
|
158
|
+
title: ReactNode;
|
|
159
|
+
description?: ReactNode;
|
|
160
|
+
/** The editor's 3-state vocabulary; mapped in via `fromTimelineStatus`. */
|
|
161
|
+
status?: TimelineStatus;
|
|
162
|
+
timestamp?: ReactNode;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface TimelineProps extends HTMLAttributes<HTMLOListElement> {
|
|
166
|
+
items: TimelineEntry[];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Array convenience over the compound parts (data in, list out). */
|
|
170
|
+
export const Timeline = forwardRef<HTMLOListElement, TimelineProps>(function Timeline(
|
|
171
|
+
{ items, className, ...props },
|
|
172
|
+
ref,
|
|
173
|
+
) {
|
|
174
|
+
return (
|
|
175
|
+
<TimelineRoot ref={ref} className={className} {...props}>
|
|
176
|
+
{items.map((item, i) => (
|
|
177
|
+
<TimelineItem
|
|
178
|
+
key={i}
|
|
179
|
+
status={fromTimelineStatus(item.status ?? "pending")}
|
|
180
|
+
timestamp={item.timestamp}
|
|
181
|
+
description={item.description}
|
|
182
|
+
>
|
|
183
|
+
{item.title}
|
|
184
|
+
</TimelineItem>
|
|
185
|
+
))}
|
|
186
|
+
</TimelineRoot>
|
|
187
|
+
);
|
|
188
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Toggle, toggleVariants } from "./toggle";
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect } from "storybook/test";
|
|
3
|
+
import { Bold } from "lucide-react";
|
|
4
|
+
import { Toggle } from "./toggle";
|
|
5
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip";
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Forms/Toggle",
|
|
8
|
+
component: Toggle,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
argTypes: {
|
|
11
|
+
variant: {
|
|
12
|
+
description: "Visual style of the toggle.",
|
|
13
|
+
control: { type: "select" },
|
|
14
|
+
options: ["default", "outline", "segmented"],
|
|
15
|
+
table: { category: "Appearance" },
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
description: "Size of the toggle button.",
|
|
19
|
+
control: { type: "radio" },
|
|
20
|
+
options: ["default", "sm", "lg"],
|
|
21
|
+
table: { category: "Appearance" },
|
|
22
|
+
},
|
|
23
|
+
pressed: {
|
|
24
|
+
description: "Controlled pressed state.",
|
|
25
|
+
control: "boolean",
|
|
26
|
+
table: { category: "State" },
|
|
27
|
+
},
|
|
28
|
+
defaultPressed: {
|
|
29
|
+
description: "Uncontrolled initial pressed state.",
|
|
30
|
+
control: "boolean",
|
|
31
|
+
table: { category: "State" },
|
|
32
|
+
},
|
|
33
|
+
disabled: {
|
|
34
|
+
description: "Disables the toggle.",
|
|
35
|
+
control: "boolean",
|
|
36
|
+
table: { category: "State" },
|
|
37
|
+
},
|
|
38
|
+
onPressedChange: {
|
|
39
|
+
description: "Called when the pressed state changes.",
|
|
40
|
+
control: false,
|
|
41
|
+
table: { category: "Behavior" },
|
|
42
|
+
},
|
|
43
|
+
"aria-label": {
|
|
44
|
+
description: "Accessible label (required for icon-only toggles).",
|
|
45
|
+
control: "text",
|
|
46
|
+
table: { category: "Appearance" },
|
|
47
|
+
},
|
|
48
|
+
children: {
|
|
49
|
+
description: "Toggle content — icon and/or text.",
|
|
50
|
+
control: false,
|
|
51
|
+
table: { category: "Content" },
|
|
52
|
+
},
|
|
53
|
+
className: {
|
|
54
|
+
description: "Additional CSS classes applied to the root element.",
|
|
55
|
+
control: "text",
|
|
56
|
+
table: { category: "Appearance" },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
} satisfies Meta<typeof Toggle>;
|
|
60
|
+
export default meta;
|
|
61
|
+
type Story = StoryObj<typeof meta>;
|
|
62
|
+
export const Default: Story = {
|
|
63
|
+
args: { "aria-label": "Bold", children: <Bold /> },
|
|
64
|
+
// Clicks the toggle and confirms the pressed state flips.
|
|
65
|
+
play: async ({ canvas, userEvent }) => {
|
|
66
|
+
const btn = canvas.getByRole("button", { name: /bold/i });
|
|
67
|
+
await expect(btn).toHaveAttribute("aria-pressed", "false");
|
|
68
|
+
await userEvent.click(btn);
|
|
69
|
+
await expect(btn).toHaveAttribute("aria-pressed", "true");
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
export const Pressed: Story = {
|
|
73
|
+
args: { "aria-label": "Bold", children: <Bold />, pressed: true },
|
|
74
|
+
};
|
|
75
|
+
export const Outline: Story = { args: { variant: "outline", children: "Bold" } };
|
|
76
|
+
export const OutlinePressed: Story = {
|
|
77
|
+
args: { variant: "outline", children: "Bold", pressed: true },
|
|
78
|
+
};
|
|
79
|
+
// `segmented` was selectable in the controls panel (`argTypes.variant.options`)
|
|
80
|
+
// but never actually rendered by any story (#388), so it never reached the
|
|
81
|
+
// blocking interaction + axe job. This is the mode-switch look — no border, an
|
|
82
|
+
// elevated `bg-surface-elevated` active segment — normally composed inside a
|
|
83
|
+
// `ToggleGroup variant="segmented"` track (see Forms/ToggleGroup → Segmented);
|
|
84
|
+
// rendered standalone here purely to exercise the variant itself.
|
|
85
|
+
export const Segmented: Story = {
|
|
86
|
+
args: { variant: "segmented", "aria-label": "Bold", children: <Bold /> },
|
|
87
|
+
};
|
|
88
|
+
export const Disabled: Story = {
|
|
89
|
+
args: { "aria-label": "Bold", children: <Bold />, disabled: true },
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Regression story for #214: a pressed Toggle wrapped in TooltipTrigger asChild.
|
|
93
|
+
// The tooltip's data-state="closed" overwrites the toggle's data-state="on", but
|
|
94
|
+
// the active style must still render because it is also keyed on aria-pressed.
|
|
95
|
+
export const TooltipWrappedPressed: Story = {
|
|
96
|
+
render: () => (
|
|
97
|
+
<TooltipProvider>
|
|
98
|
+
<Tooltip>
|
|
99
|
+
<TooltipTrigger asChild>
|
|
100
|
+
<Toggle aria-label="Bold" pressed>
|
|
101
|
+
<Bold />
|
|
102
|
+
</Toggle>
|
|
103
|
+
</TooltipTrigger>
|
|
104
|
+
<TooltipContent>Bold</TooltipContent>
|
|
105
|
+
</Tooltip>
|
|
106
|
+
</TooltipProvider>
|
|
107
|
+
),
|
|
108
|
+
play: async ({ canvas }) => {
|
|
109
|
+
const btn = canvas.getByRole("button", { name: /bold/i });
|
|
110
|
+
// aria-pressed must survive the TooltipTrigger asChild merge.
|
|
111
|
+
await expect(btn).toHaveAttribute("aria-pressed", "true");
|
|
112
|
+
// aria-keyed class tokens must be present so the active style renders.
|
|
113
|
+
await expect(btn.className).toContain("aria-pressed:bg-accent");
|
|
114
|
+
await expect(btn.className).toContain("aria-pressed:border-primary");
|
|
115
|
+
},
|
|
116
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
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 { Toggle } from "./toggle";
|
|
5
|
+
import { ToggleGroup, ToggleGroupItem } from "../toggle-group";
|
|
6
|
+
|
|
7
|
+
describe("Toggle", () => {
|
|
8
|
+
it("toggles pressed state", async () => {
|
|
9
|
+
render(<Toggle aria-label="t">B</Toggle>);
|
|
10
|
+
const t = screen.getByRole("button", { name: "t" });
|
|
11
|
+
await userEvent.click(t);
|
|
12
|
+
expect(t).toHaveAttribute("data-state", "on");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Regression guard for #214: when a TooltipTrigger asChild wrapper merges
|
|
16
|
+
// onto the toggle element it overwrites data-state="on" → "closed". The
|
|
17
|
+
// active styles must survive via aria-pressed / aria-checked keying.
|
|
18
|
+
it("carries aria-pressed=true and aria-keyed active classes when pressed, regardless of data-state", () => {
|
|
19
|
+
render(
|
|
20
|
+
<Toggle aria-label="bold" pressed>
|
|
21
|
+
B
|
|
22
|
+
</Toggle>,
|
|
23
|
+
);
|
|
24
|
+
const btn = screen.getByRole("button", { name: "bold" });
|
|
25
|
+
|
|
26
|
+
// aria-pressed must be "true" — set by the Radix Toggle primitive alone,
|
|
27
|
+
// not clobbered by a TooltipTrigger wrapper.
|
|
28
|
+
expect(btn).toHaveAttribute("aria-pressed", "true");
|
|
29
|
+
|
|
30
|
+
// The className must contain the aria-pressed-keyed active utilities so
|
|
31
|
+
// the style applies even when data-state is overwritten to "closed".
|
|
32
|
+
expect(btn.className).toContain("aria-pressed:bg-accent");
|
|
33
|
+
expect(btn.className).toContain("aria-pressed:text-accent-foreground");
|
|
34
|
+
expect(btn.className).toContain("aria-pressed:font-semibold");
|
|
35
|
+
expect(btn.className).toContain("aria-pressed:border-primary");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("carries aria-pressed=true and aria-keyed active classes on outline variant when pressed", () => {
|
|
39
|
+
render(
|
|
40
|
+
<Toggle aria-label="bold" variant="outline" pressed>
|
|
41
|
+
B
|
|
42
|
+
</Toggle>,
|
|
43
|
+
);
|
|
44
|
+
const btn = screen.getByRole("button", { name: "bold" });
|
|
45
|
+
|
|
46
|
+
expect(btn).toHaveAttribute("aria-pressed", "true");
|
|
47
|
+
expect(btn.className).toContain("aria-pressed:bg-accent");
|
|
48
|
+
expect(btn.className).toContain("aria-pressed:text-accent-foreground");
|
|
49
|
+
expect(btn.className).toContain("aria-pressed:font-semibold");
|
|
50
|
+
expect(btn.className).toContain("aria-pressed:border-primary");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Simulate the exact TooltipTrigger collision: data-state is clobbered to
|
|
54
|
+
// "closed" after mount; the aria-pressed attribute (set by the toggle
|
|
55
|
+
// primitive) must remain "true" and the class tokens must still be present.
|
|
56
|
+
it("active style survives data-state being overwritten to closed (TooltipTrigger collision)", () => {
|
|
57
|
+
render(
|
|
58
|
+
<Toggle aria-label="bold" pressed>
|
|
59
|
+
B
|
|
60
|
+
</Toggle>,
|
|
61
|
+
);
|
|
62
|
+
const btn = screen.getByRole("button", { name: "bold" });
|
|
63
|
+
|
|
64
|
+
// Simulate what TooltipTrigger asChild does: clobber data-state.
|
|
65
|
+
btn.setAttribute("data-state", "closed");
|
|
66
|
+
|
|
67
|
+
// aria-pressed is NOT affected by the data-state clobber.
|
|
68
|
+
expect(btn).toHaveAttribute("aria-pressed", "true");
|
|
69
|
+
|
|
70
|
+
// The aria-keyed class tokens must still be present in className — they
|
|
71
|
+
// will activate in the browser via CSS [aria-pressed="true"] selectors.
|
|
72
|
+
expect(btn.className).toContain("aria-pressed:bg-accent");
|
|
73
|
+
expect(btn.className).toContain("aria-pressed:border-primary");
|
|
74
|
+
|
|
75
|
+
// data-state is now "closed" (confirming the collision scenario).
|
|
76
|
+
expect(btn).toHaveAttribute("data-state", "closed");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("ToggleGroup / ToggleGroupItem (#214 regression)", () => {
|
|
81
|
+
it("selected ToggleGroupItem carries aria-checked=true and aria-keyed active classes", () => {
|
|
82
|
+
render(
|
|
83
|
+
<ToggleGroup type="single" value="a">
|
|
84
|
+
<ToggleGroupItem value="a" aria-label="item-a">
|
|
85
|
+
A
|
|
86
|
+
</ToggleGroupItem>
|
|
87
|
+
</ToggleGroup>,
|
|
88
|
+
);
|
|
89
|
+
const item = screen.getByRole("radio", { name: "item-a" });
|
|
90
|
+
|
|
91
|
+
// ToggleGroupItem sets aria-checked, not aria-pressed.
|
|
92
|
+
expect(item).toHaveAttribute("aria-checked", "true");
|
|
93
|
+
|
|
94
|
+
// aria-checked-keyed active class tokens must be present.
|
|
95
|
+
expect(item.className).toContain("aria-checked:bg-accent");
|
|
96
|
+
expect(item.className).toContain("aria-checked:text-accent-foreground");
|
|
97
|
+
expect(item.className).toContain("aria-checked:font-semibold");
|
|
98
|
+
expect(item.className).toContain("aria-checked:border-primary");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("active style on ToggleGroupItem survives data-state being overwritten to closed", () => {
|
|
102
|
+
render(
|
|
103
|
+
<ToggleGroup type="single" value="a">
|
|
104
|
+
<ToggleGroupItem value="a" aria-label="item-a">
|
|
105
|
+
A
|
|
106
|
+
</ToggleGroupItem>
|
|
107
|
+
</ToggleGroup>,
|
|
108
|
+
);
|
|
109
|
+
const item = screen.getByRole("radio", { name: "item-a" });
|
|
110
|
+
|
|
111
|
+
// Simulate TooltipTrigger asChild clobbering data-state.
|
|
112
|
+
item.setAttribute("data-state", "closed");
|
|
113
|
+
|
|
114
|
+
expect(item).toHaveAttribute("aria-checked", "true");
|
|
115
|
+
expect(item.className).toContain("aria-checked:bg-accent");
|
|
116
|
+
expect(item.className).toContain("aria-checked:border-primary");
|
|
117
|
+
expect(item).toHaveAttribute("data-state", "closed");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
|
|
2
|
+
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
|
|
6
|
+
export const toggleVariants = cva(
|
|
7
|
+
// Pressed (data-[state=on]) reads as a clearly-distinct, AA-safe state: an
|
|
8
|
+
// accent fill + bolded label (accent/accent-foreground is a guaranteed AA
|
|
9
|
+
// pair) plus a solid `primary` border as a ≥3:1 non-text boundary cue. We
|
|
10
|
+
// deliberately do NOT use `bg-primary`/`text-primary-foreground`: under the
|
|
11
|
+
// pre-debrand green palette that pair measured only 3.61:1 in `light`, which
|
|
12
|
+
// fails AA for text labels (#148). The retuned palette flipped
|
|
13
|
+
// `--primary-foreground` to near-black on a light primary and clears AA
|
|
14
|
+
// comfortably, so the CONTRAST argument is spent — the shape stands on design
|
|
15
|
+
// grounds instead: a pressed toggle is a secondary state and an accent fill is
|
|
16
|
+
// the right weight for it. The border carries the strength; the focus ring is
|
|
17
|
+
// untouched.
|
|
18
|
+
//
|
|
19
|
+
// Active styles are keyed on BOTH data-[state=on]: AND aria-pressed:/aria-checked:
|
|
20
|
+
// (belt-and-suspenders, mirroring the `segmented` variant). A TooltipTrigger
|
|
21
|
+
// asChild wrapper overwrites data-state="on" with "closed" — aria-pressed
|
|
22
|
+
// (Toggle) and aria-checked (ToggleGroupItem) are set by the toggle primitive
|
|
23
|
+
// alone and survive the merge. (#214)
|
|
24
|
+
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors duration-fast ease-standard hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground data-[state=on]:font-semibold aria-pressed:bg-accent aria-pressed:text-accent-foreground aria-pressed:font-semibold aria-checked:bg-accent aria-checked:text-accent-foreground aria-checked:font-semibold [&_svg]:size-4 [&_svg]:shrink-0",
|
|
25
|
+
{
|
|
26
|
+
variants: {
|
|
27
|
+
variant: {
|
|
28
|
+
// `border border-transparent` reserves the 1px so the pressed
|
|
29
|
+
// `border-primary` does not shift layout.
|
|
30
|
+
default:
|
|
31
|
+
"border border-transparent bg-transparent data-[state=on]:border-primary aria-pressed:border-primary aria-checked:border-primary",
|
|
32
|
+
outline:
|
|
33
|
+
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground data-[state=on]:border-primary aria-pressed:border-primary aria-checked:border-primary",
|
|
34
|
+
// Segmented mode switch: the SAME elevation grammar as TabsTrigger —
|
|
35
|
+
// raised elevated segment on a recessed `bg-muted` track (the track
|
|
36
|
+
// comes from ToggleGroup `variant="segmented"`). No `border-primary`
|
|
37
|
+
// here: within a group the active boundary is the fill/elevation
|
|
38
|
+
// change (1.4.11 redundant boundary), and no weight shift on press
|
|
39
|
+
// (labels would jiggle the segment widths).
|
|
40
|
+
// Keyed off aria-checked/aria-pressed, NOT data-[state=on]: a
|
|
41
|
+
// TooltipTrigger asChild wrapper overwrites the toggle's data-state
|
|
42
|
+
// with the tooltip's ("closed"), silently killing the active style —
|
|
43
|
+
// the aria attributes are set by the toggle primitive alone.
|
|
44
|
+
segmented:
|
|
45
|
+
"border-0 bg-transparent text-muted-foreground hover:bg-transparent hover:text-foreground aria-checked:bg-surface-elevated aria-checked:text-foreground aria-checked:font-medium aria-checked:shadow-sm aria-pressed:bg-surface-elevated aria-pressed:text-foreground aria-pressed:font-medium aria-pressed:shadow-sm data-[state=on]:bg-surface-elevated data-[state=on]:text-foreground data-[state=on]:font-medium data-[state=on]:shadow-sm",
|
|
46
|
+
},
|
|
47
|
+
size: { default: "h-9 px-2.5 min-w-9", sm: "h-8 px-2 min-w-8", lg: "h-10 px-3 min-w-10" },
|
|
48
|
+
},
|
|
49
|
+
defaultVariants: { variant: "default", size: "default" },
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
export const Toggle = forwardRef<
|
|
54
|
+
ElementRef<typeof TogglePrimitive.Root>,
|
|
55
|
+
ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>
|
|
56
|
+
>(function Toggle({ className, variant, size, ...props }, ref) {
|
|
57
|
+
return (
|
|
58
|
+
<TogglePrimitive.Root
|
|
59
|
+
ref={ref}
|
|
60
|
+
className={cn(toggleVariants({ variant, size }), className)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ToggleGroup, ToggleGroupItem } from "./toggle-group";
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect } from "storybook/test";
|
|
3
|
+
import { Bold, BookOpen, Italic, PencilLine, Underline } from "lucide-react";
|
|
4
|
+
import { ToggleGroup, ToggleGroupItem } from "./toggle-group";
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Forms/ToggleGroup",
|
|
7
|
+
component: ToggleGroup,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
argTypes: {
|
|
10
|
+
type: {
|
|
11
|
+
description: '`"single"` — at most one item active. `"multiple"` — any number active.',
|
|
12
|
+
control: { type: "radio" },
|
|
13
|
+
options: ["single", "multiple"],
|
|
14
|
+
table: { category: "Behavior" },
|
|
15
|
+
},
|
|
16
|
+
variant: {
|
|
17
|
+
description: "Visual style applied to all items in the group.",
|
|
18
|
+
control: { type: "select" },
|
|
19
|
+
options: ["default", "outline", "segmented"],
|
|
20
|
+
table: { category: "Appearance" },
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
description: "Size applied to all items.",
|
|
24
|
+
control: { type: "radio" },
|
|
25
|
+
options: ["default", "sm", "lg"],
|
|
26
|
+
table: { category: "Appearance" },
|
|
27
|
+
},
|
|
28
|
+
value: {
|
|
29
|
+
description: "Controlled active value(s).",
|
|
30
|
+
control: false,
|
|
31
|
+
table: { category: "State" },
|
|
32
|
+
},
|
|
33
|
+
defaultValue: {
|
|
34
|
+
description: "Uncontrolled initial active value(s).",
|
|
35
|
+
control: false,
|
|
36
|
+
table: { category: "State" },
|
|
37
|
+
},
|
|
38
|
+
disabled: {
|
|
39
|
+
description: "Disables all items.",
|
|
40
|
+
control: "boolean",
|
|
41
|
+
table: { category: "State" },
|
|
42
|
+
},
|
|
43
|
+
onValueChange: {
|
|
44
|
+
description: "Called when the active value(s) change.",
|
|
45
|
+
control: false,
|
|
46
|
+
table: { category: "Behavior" },
|
|
47
|
+
},
|
|
48
|
+
children: {
|
|
49
|
+
description: "ToggleGroupItem elements.",
|
|
50
|
+
control: false,
|
|
51
|
+
table: { category: "Content" },
|
|
52
|
+
},
|
|
53
|
+
className: {
|
|
54
|
+
description: "Additional CSS classes applied to the root element.",
|
|
55
|
+
control: "text",
|
|
56
|
+
table: { category: "Appearance" },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
} satisfies Meta<typeof ToggleGroup>;
|
|
60
|
+
export default meta;
|
|
61
|
+
type Story = StoryObj<typeof meta>;
|
|
62
|
+
export const Default: Story = {
|
|
63
|
+
render: () => (
|
|
64
|
+
<ToggleGroup type="multiple" variant="outline">
|
|
65
|
+
<ToggleGroupItem value="bold" aria-label="Bold">
|
|
66
|
+
<Bold />
|
|
67
|
+
</ToggleGroupItem>
|
|
68
|
+
<ToggleGroupItem value="italic" aria-label="Italic">
|
|
69
|
+
<Italic />
|
|
70
|
+
</ToggleGroupItem>
|
|
71
|
+
<ToggleGroupItem value="underline" aria-label="Underline">
|
|
72
|
+
<Underline />
|
|
73
|
+
</ToggleGroupItem>
|
|
74
|
+
</ToggleGroup>
|
|
75
|
+
),
|
|
76
|
+
// Clicks "Bold" to activate it, then clicks again to deactivate.
|
|
77
|
+
play: async ({ canvas, userEvent }) => {
|
|
78
|
+
const boldBtn = canvas.getByRole("button", { name: /bold/i });
|
|
79
|
+
await expect(boldBtn).toHaveAttribute("aria-pressed", "false");
|
|
80
|
+
await userEvent.click(boldBtn);
|
|
81
|
+
await expect(boldBtn).toHaveAttribute("aria-pressed", "true");
|
|
82
|
+
await userEvent.click(boldBtn);
|
|
83
|
+
await expect(boldBtn).toHaveAttribute("aria-pressed", "false");
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Segmented mode switch — a single-select group on a recessed `bg-muted`
|
|
89
|
+
* track with a raised elevated active segment: the same elevation grammar
|
|
90
|
+
* as Tabs. Use it for exclusive view/mode controls (Read ↔ Write, editor
|
|
91
|
+
* source/split/wysiwyg) instead of the bordered toggle variants.
|
|
92
|
+
*/
|
|
93
|
+
export const Segmented: Story = {
|
|
94
|
+
render: () => (
|
|
95
|
+
<ToggleGroup
|
|
96
|
+
type="single"
|
|
97
|
+
variant="segmented"
|
|
98
|
+
size="sm"
|
|
99
|
+
defaultValue="read"
|
|
100
|
+
aria-label="Document mode"
|
|
101
|
+
>
|
|
102
|
+
<ToggleGroupItem value="read" aria-label="Read">
|
|
103
|
+
<BookOpen /> Read
|
|
104
|
+
</ToggleGroupItem>
|
|
105
|
+
<ToggleGroupItem value="write" aria-label="Write">
|
|
106
|
+
<PencilLine /> Write
|
|
107
|
+
</ToggleGroupItem>
|
|
108
|
+
</ToggleGroup>
|
|
109
|
+
),
|
|
110
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
forwardRef,
|
|
4
|
+
useContext,
|
|
5
|
+
type ComponentPropsWithoutRef,
|
|
6
|
+
type ElementRef,
|
|
7
|
+
} from "react";
|
|
8
|
+
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
|
9
|
+
import { type VariantProps } from "class-variance-authority";
|
|
10
|
+
import { cn } from "../../lib/cn";
|
|
11
|
+
import { toggleVariants } from "../toggle";
|
|
12
|
+
|
|
13
|
+
type ToggleVariantProps = VariantProps<typeof toggleVariants>;
|
|
14
|
+
const ToggleGroupContext = createContext<ToggleVariantProps>({
|
|
15
|
+
size: "default",
|
|
16
|
+
variant: "default",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const ToggleGroup = forwardRef<
|
|
20
|
+
ElementRef<typeof ToggleGroupPrimitive.Root>,
|
|
21
|
+
ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & ToggleVariantProps
|
|
22
|
+
>(function ToggleGroup({ className, variant, size, children, ...props }, ref) {
|
|
23
|
+
return (
|
|
24
|
+
<ToggleGroupPrimitive.Root
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn(
|
|
27
|
+
"flex items-center gap-1",
|
|
28
|
+
// Segmented groups carry the TabsList track (recessed muted fill);
|
|
29
|
+
// the raised active segment comes from the item variant.
|
|
30
|
+
variant === "segmented" && "gap-0 rounded-lg bg-muted p-1 text-muted-foreground",
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
<ToggleGroupContext.Provider value={{ variant, size }}>
|
|
36
|
+
{children}
|
|
37
|
+
</ToggleGroupContext.Provider>
|
|
38
|
+
</ToggleGroupPrimitive.Root>
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const ToggleGroupItem = forwardRef<
|
|
43
|
+
ElementRef<typeof ToggleGroupPrimitive.Item>,
|
|
44
|
+
ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & ToggleVariantProps
|
|
45
|
+
>(function ToggleGroupItem({ className, children, variant, size, ...props }, ref) {
|
|
46
|
+
const ctx = useContext(ToggleGroupContext);
|
|
47
|
+
return (
|
|
48
|
+
<ToggleGroupPrimitive.Item
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn(
|
|
51
|
+
toggleVariants({ variant: ctx.variant ?? variant, size: ctx.size ?? size }),
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</ToggleGroupPrimitive.Item>
|
|
58
|
+
);
|
|
59
|
+
});
|