@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,28 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { Skeleton } from "./skeleton";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "States/Skeleton",
|
|
6
|
+
component: Skeleton,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
argTypes: {
|
|
9
|
+
className: {
|
|
10
|
+
description:
|
|
11
|
+
"Width/height and shape utilities applied to the skeleton (e.g. `h-4 w-32 rounded-full`).",
|
|
12
|
+
control: "text",
|
|
13
|
+
table: { category: "Styling" },
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
} satisfies Meta<typeof Skeleton>;
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
export const Card: Story = {
|
|
21
|
+
render: () => (
|
|
22
|
+
<div className="w-64 space-y-3">
|
|
23
|
+
<Skeleton className="h-32 w-full" />
|
|
24
|
+
<Skeleton className="h-4 w-3/4" />
|
|
25
|
+
<Skeleton className="h-4 w-1/2" />
|
|
26
|
+
</div>
|
|
27
|
+
),
|
|
28
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type SkeletonProps = HTMLAttributes<HTMLDivElement>;
|
|
5
|
+
|
|
6
|
+
/** Loading placeholder. Compose with width/height utilities, e.g. `h-4 w-32`. */
|
|
7
|
+
export function Skeleton({ className, ...props }: SkeletonProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
className={cn("animate-pulse rounded-md bg-muted", className)}
|
|
11
|
+
aria-hidden="true"
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Slider, type SliderProps, type SliderThumbProps } from "./slider";
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect } from "storybook/test";
|
|
3
|
+
import { Slider } from "./slider";
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Forms/Slider",
|
|
6
|
+
component: Slider,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
argTypes: {
|
|
9
|
+
defaultValue: {
|
|
10
|
+
description: "Uncontrolled initial value(s) as an array.",
|
|
11
|
+
control: false,
|
|
12
|
+
table: { category: "State" },
|
|
13
|
+
},
|
|
14
|
+
value: {
|
|
15
|
+
description: "Controlled value(s) as an array.",
|
|
16
|
+
control: false,
|
|
17
|
+
table: { category: "State" },
|
|
18
|
+
},
|
|
19
|
+
min: {
|
|
20
|
+
description: "Minimum value of the range.",
|
|
21
|
+
control: "number",
|
|
22
|
+
table: { category: "Behavior" },
|
|
23
|
+
},
|
|
24
|
+
max: {
|
|
25
|
+
description: "Maximum value of the range.",
|
|
26
|
+
control: "number",
|
|
27
|
+
table: { category: "Behavior" },
|
|
28
|
+
},
|
|
29
|
+
step: {
|
|
30
|
+
description: "Increment between selectable values.",
|
|
31
|
+
control: "number",
|
|
32
|
+
table: { category: "Behavior" },
|
|
33
|
+
},
|
|
34
|
+
disabled: {
|
|
35
|
+
description: "Disables the slider.",
|
|
36
|
+
control: "boolean",
|
|
37
|
+
table: { category: "State" },
|
|
38
|
+
},
|
|
39
|
+
orientation: {
|
|
40
|
+
description: "Layout direction of the slider.",
|
|
41
|
+
control: { type: "radio" },
|
|
42
|
+
options: ["horizontal", "vertical"],
|
|
43
|
+
table: { category: "Appearance" },
|
|
44
|
+
},
|
|
45
|
+
onValueChange: {
|
|
46
|
+
description: "Called when the slider value changes.",
|
|
47
|
+
control: false,
|
|
48
|
+
table: { category: "Behavior" },
|
|
49
|
+
},
|
|
50
|
+
className: {
|
|
51
|
+
description: "Additional CSS classes applied to the root element.",
|
|
52
|
+
control: "text",
|
|
53
|
+
table: { category: "Appearance" },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
} satisfies Meta<typeof Slider>;
|
|
57
|
+
export default meta;
|
|
58
|
+
type Story = StoryObj<typeof meta>;
|
|
59
|
+
export const Default: Story = {
|
|
60
|
+
render: () => <Slider defaultValue={[50]} max={100} step={1} className="w-64" />,
|
|
61
|
+
// Confirms the slider thumb is present and has the expected value attribute.
|
|
62
|
+
play: async ({ canvas }) => {
|
|
63
|
+
const thumb = canvas.getByRole("slider");
|
|
64
|
+
await expect(thumb).toBeInTheDocument();
|
|
65
|
+
await expect(thumb).toHaveAttribute("aria-valuenow", "50");
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The name has to land on the THUMB — that is the element with `role="slider"`.
|
|
71
|
+
* `aria-label` (or `aria-labelledby`, for a visible `<Label>`) is routed there.
|
|
72
|
+
*/
|
|
73
|
+
export const Labelled: Story = {
|
|
74
|
+
render: () => (
|
|
75
|
+
<Slider aria-label="Decoration level" defaultValue={[4]} max={10} step={1} className="w-64" />
|
|
76
|
+
),
|
|
77
|
+
play: async ({ canvas }) => {
|
|
78
|
+
await expect(canvas.getByRole("slider", { name: "Decoration level" })).toBeInTheDocument();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A replay scrubber announces "step 3 of 12" instead of the bare numeric
|
|
84
|
+
* value — `aria-valuetext` on `thumbProps` (or the top-level `aria-valuetext`
|
|
85
|
+
* shorthand) lands on the thumb, the element that actually carries
|
|
86
|
+
* `role="slider"`.
|
|
87
|
+
*/
|
|
88
|
+
export const CustomValueText: Story = {
|
|
89
|
+
render: () => (
|
|
90
|
+
<Slider
|
|
91
|
+
aria-label="Replay position"
|
|
92
|
+
defaultValue={[3]}
|
|
93
|
+
max={12}
|
|
94
|
+
step={1}
|
|
95
|
+
className="w-64"
|
|
96
|
+
thumbProps={{ "aria-valuetext": "step 3 of 12" }}
|
|
97
|
+
/>
|
|
98
|
+
),
|
|
99
|
+
play: async ({ canvas }) => {
|
|
100
|
+
const thumb = canvas.getByRole("slider", { name: "Replay position" });
|
|
101
|
+
await expect(thumb).toHaveAttribute("aria-valuetext", "step 3 of 12");
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A range slider (array `defaultValue`) renders one thumb per value. Passing
|
|
107
|
+
* an ARRAY to `thumbProps` (#398) gives each thumb its own accessible name
|
|
108
|
+
* and `aria-valuetext` — a top-level `aria-label`/`aria-valuetext` would
|
|
109
|
+
* apply identically to both thumbs, which isn't what a min/max range wants.
|
|
110
|
+
*/
|
|
111
|
+
export const Range: Story = {
|
|
112
|
+
render: () => (
|
|
113
|
+
<Slider
|
|
114
|
+
defaultValue={[20, 80]}
|
|
115
|
+
max={100}
|
|
116
|
+
step={1}
|
|
117
|
+
className="w-64"
|
|
118
|
+
thumbProps={[
|
|
119
|
+
{ "aria-label": "Minimum price", "aria-valuetext": "$20" },
|
|
120
|
+
{ "aria-label": "Maximum price", "aria-valuetext": "$80" },
|
|
121
|
+
]}
|
|
122
|
+
/>
|
|
123
|
+
),
|
|
124
|
+
play: async ({ canvas }) => {
|
|
125
|
+
const thumbs = canvas.getAllByRole("slider");
|
|
126
|
+
await expect(thumbs).toHaveLength(2);
|
|
127
|
+
await expect(thumbs[0]).toHaveAccessibleName("Minimum price");
|
|
128
|
+
await expect(thumbs[0]).toHaveAttribute("aria-valuetext", "$20");
|
|
129
|
+
await expect(thumbs[1]).toHaveAccessibleName("Maximum price");
|
|
130
|
+
await expect(thumbs[1]).toHaveAttribute("aria-valuetext", "$80");
|
|
131
|
+
|
|
132
|
+
// #398 AC-3 — a real accessibility-tree assertion, not just a DOM
|
|
133
|
+
// attribute snapshot: the DOM checks above prove the markup is right,
|
|
134
|
+
// not that a screen reader actually hears two distinctly-named,
|
|
135
|
+
// distinctly-valued thumbs. This reads Chromium's live AX tree via CDP
|
|
136
|
+
// (`Accessibility.getFullAXTree`), which is what assistive tech consumes.
|
|
137
|
+
//
|
|
138
|
+
// `@vitest/browser/context` is a virtual module that only resolves inside
|
|
139
|
+
// Vitest's browser-mode test runner (`pnpm --filter @elabs-ai/components-docs test-storybook` /
|
|
140
|
+
// `vitest --project storybook run`, the same engine CI's blocking
|
|
141
|
+
// "Storybook interaction + axe" job uses) — importing it anywhere else
|
|
142
|
+
// throws by the module's own design. The dynamic import + catch lets this
|
|
143
|
+
// block run there while leaving plain interactive Storybook browsing and
|
|
144
|
+
// a production `build-storybook` unaffected (they still get every DOM
|
|
145
|
+
// assertion above).
|
|
146
|
+
let browserContext: typeof import("@vitest/browser/context") | undefined;
|
|
147
|
+
try {
|
|
148
|
+
browserContext = await import("@vitest/browser/context");
|
|
149
|
+
} catch {
|
|
150
|
+
browserContext = undefined;
|
|
151
|
+
}
|
|
152
|
+
if (!browserContext) return;
|
|
153
|
+
|
|
154
|
+
interface AXProperty {
|
|
155
|
+
name: string;
|
|
156
|
+
value?: { value?: unknown };
|
|
157
|
+
}
|
|
158
|
+
interface AXNode {
|
|
159
|
+
role?: { value?: string };
|
|
160
|
+
name?: { value?: string };
|
|
161
|
+
ignored?: boolean;
|
|
162
|
+
properties?: AXProperty[];
|
|
163
|
+
}
|
|
164
|
+
interface FrameNode {
|
|
165
|
+
frame: { id: string };
|
|
166
|
+
childFrames?: FrameNode[];
|
|
167
|
+
}
|
|
168
|
+
// The base `CDPSession` type ships empty ("methods are defined by the
|
|
169
|
+
// provider type augmentation" — @vitest/browser/context.d.ts) and this
|
|
170
|
+
// file doesn't pull in the playwright provider's ambient augmentation, so
|
|
171
|
+
// `send` is typed by hand here rather than importing playwright's CDP
|
|
172
|
+
// protocol types into a story file.
|
|
173
|
+
interface CdpSession {
|
|
174
|
+
send: (method: string, params?: Record<string, unknown>) => Promise<unknown>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const session = browserContext.cdp() as unknown as CdpSession;
|
|
178
|
+
await session.send("Accessibility.enable");
|
|
179
|
+
await session.send("Page.enable");
|
|
180
|
+
const { frameTree } = (await session.send("Page.getFrameTree")) as {
|
|
181
|
+
frameTree: FrameNode;
|
|
182
|
+
};
|
|
183
|
+
const frames: { id: string }[] = [];
|
|
184
|
+
const collectFrames = (node: FrameNode) => {
|
|
185
|
+
frames.push(node.frame);
|
|
186
|
+
for (const child of node.childFrames ?? []) collectFrames(child);
|
|
187
|
+
};
|
|
188
|
+
collectFrames(frameTree);
|
|
189
|
+
|
|
190
|
+
let axNodes: AXNode[] = [];
|
|
191
|
+
for (const frame of frames) {
|
|
192
|
+
const result = (await session.send("Accessibility.getFullAXTree", {
|
|
193
|
+
frameId: frame.id,
|
|
194
|
+
})) as { nodes: AXNode[] };
|
|
195
|
+
axNodes = axNodes.concat(result.nodes);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const sliderNodes = axNodes.filter((node) => node.role?.value === "slider" && !node.ignored);
|
|
199
|
+
await expect(sliderNodes).toHaveLength(2);
|
|
200
|
+
|
|
201
|
+
const valuetextOf = (node: AXNode) =>
|
|
202
|
+
node.properties?.find((property) => property.name === "valuetext")?.value?.value;
|
|
203
|
+
const namedThumbs = sliderNodes
|
|
204
|
+
.map((node) => ({ name: node.name?.value, valuetext: valuetextOf(node) }))
|
|
205
|
+
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
206
|
+
await expect(namedThumbs).toEqual([
|
|
207
|
+
{ name: "Maximum price", valuetext: "$80" },
|
|
208
|
+
{ name: "Minimum price", valuetext: "$20" },
|
|
209
|
+
]);
|
|
210
|
+
},
|
|
211
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { Slider, type SliderThumbProps } from "./slider";
|
|
4
|
+
|
|
5
|
+
describe("Slider", () => {
|
|
6
|
+
it("forwards aria-label to the thumb (role=slider)", () => {
|
|
7
|
+
render(<Slider aria-label="Volume" defaultValue={[50]} max={100} step={1} />);
|
|
8
|
+
expect(screen.getByRole("slider", { name: "Volume" })).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("forwards the top-level aria-valuetext to the thumb", () => {
|
|
12
|
+
render(
|
|
13
|
+
<Slider
|
|
14
|
+
aria-label="Replay position"
|
|
15
|
+
aria-valuetext="step 3 of 12"
|
|
16
|
+
defaultValue={[3]}
|
|
17
|
+
max={12}
|
|
18
|
+
step={1}
|
|
19
|
+
/>,
|
|
20
|
+
);
|
|
21
|
+
expect(screen.getByRole("slider")).toHaveAttribute("aria-valuetext", "step 3 of 12");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("forwards thumbProps.aria-valuetext to the thumb", () => {
|
|
25
|
+
render(
|
|
26
|
+
<Slider
|
|
27
|
+
aria-label="Replay position"
|
|
28
|
+
defaultValue={[3]}
|
|
29
|
+
max={12}
|
|
30
|
+
step={1}
|
|
31
|
+
thumbProps={{ "aria-valuetext": "step 3 of 12" }}
|
|
32
|
+
/>,
|
|
33
|
+
);
|
|
34
|
+
expect(screen.getByRole("slider")).toHaveAttribute("aria-valuetext", "step 3 of 12");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("thumbProps wins over the top-level aria-label when both are set", () => {
|
|
38
|
+
render(
|
|
39
|
+
<Slider
|
|
40
|
+
aria-label="Root label"
|
|
41
|
+
defaultValue={[3]}
|
|
42
|
+
max={12}
|
|
43
|
+
step={1}
|
|
44
|
+
thumbProps={{ "aria-label": "Thumb label" }}
|
|
45
|
+
/>,
|
|
46
|
+
);
|
|
47
|
+
expect(screen.getByRole("slider", { name: "Thumb label" })).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("does not regress existing aria-labelledby forwarding", () => {
|
|
51
|
+
render(
|
|
52
|
+
<>
|
|
53
|
+
<label id="vol-label">Volume</label>
|
|
54
|
+
<Slider aria-labelledby="vol-label" defaultValue={[50]} max={100} step={1} />
|
|
55
|
+
</>,
|
|
56
|
+
);
|
|
57
|
+
expect(screen.getByRole("slider", { name: "Volume" })).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Radix spreads consumer thumb props AFTER its own role/value/tabIndex, so
|
|
61
|
+
// an unfiltered escape hatch lets a consumer destroy the widget.
|
|
62
|
+
it("ignores thumbProps keys that would break the slider widget", () => {
|
|
63
|
+
render(
|
|
64
|
+
<Slider
|
|
65
|
+
aria-label="Volume"
|
|
66
|
+
defaultValue={[50]}
|
|
67
|
+
max={100}
|
|
68
|
+
step={1}
|
|
69
|
+
thumbProps={{
|
|
70
|
+
role: "presentation",
|
|
71
|
+
"aria-valuenow": 999,
|
|
72
|
+
"aria-valuemin": 999,
|
|
73
|
+
"aria-valuemax": 999,
|
|
74
|
+
"aria-orientation": "vertical",
|
|
75
|
+
tabIndex: -1,
|
|
76
|
+
id: "volume-thumb",
|
|
77
|
+
}}
|
|
78
|
+
/>,
|
|
79
|
+
);
|
|
80
|
+
const thumb = screen.getByRole("slider", { name: "Volume" });
|
|
81
|
+
expect(thumb).toHaveAttribute("aria-valuenow", "50");
|
|
82
|
+
expect(thumb).toHaveAttribute("aria-valuemin", "0");
|
|
83
|
+
expect(thumb).toHaveAttribute("aria-valuemax", "100");
|
|
84
|
+
expect(thumb).toHaveAttribute("aria-orientation", "horizontal");
|
|
85
|
+
expect(thumb).toHaveAttribute("tabindex", "0");
|
|
86
|
+
// Non-protected keys still pass through.
|
|
87
|
+
expect(thumb).toHaveAttribute("id", "volume-thumb");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// #398 — a range slider (array value) renders one thumb per value, and a
|
|
91
|
+
// matching array of thumbProps gives each one its own accessible name.
|
|
92
|
+
describe("range (multi-thumb) support", () => {
|
|
93
|
+
it("renders one thumb per value in a range slider", () => {
|
|
94
|
+
render(<Slider aria-label="Price range" defaultValue={[3, 9]} max={12} step={1} />);
|
|
95
|
+
expect(screen.getAllByRole("slider")).toHaveLength(2);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("gives each thumb its own aria-valuetext via an array of thumbProps", () => {
|
|
99
|
+
render(
|
|
100
|
+
<Slider
|
|
101
|
+
defaultValue={[3, 9]}
|
|
102
|
+
max={12}
|
|
103
|
+
step={1}
|
|
104
|
+
thumbProps={[
|
|
105
|
+
{ "aria-label": "Minimum price", "aria-valuetext": "$3" },
|
|
106
|
+
{ "aria-label": "Maximum price", "aria-valuetext": "$9" },
|
|
107
|
+
]}
|
|
108
|
+
/>,
|
|
109
|
+
);
|
|
110
|
+
const [low, high] = screen.getAllByRole("slider");
|
|
111
|
+
expect(low).toHaveAttribute("aria-valuetext", "$3");
|
|
112
|
+
expect(low).toHaveAttribute("aria-label", "Minimum price");
|
|
113
|
+
expect(high).toHaveAttribute("aria-valuetext", "$9");
|
|
114
|
+
expect(high).toHaveAttribute("aria-label", "Maximum price");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("gives each thumb its own data-* attributes via an array of thumbProps", () => {
|
|
118
|
+
render(
|
|
119
|
+
<Slider
|
|
120
|
+
aria-label="Price range"
|
|
121
|
+
defaultValue={[3, 9]}
|
|
122
|
+
max={12}
|
|
123
|
+
step={1}
|
|
124
|
+
thumbProps={[
|
|
125
|
+
{ "data-thumb": "min" } as SliderThumbProps,
|
|
126
|
+
{ "data-thumb": "max" } as SliderThumbProps,
|
|
127
|
+
]}
|
|
128
|
+
/>,
|
|
129
|
+
);
|
|
130
|
+
const [low, high] = screen.getAllByRole("slider");
|
|
131
|
+
expect(low).toHaveAttribute("data-thumb", "min");
|
|
132
|
+
expect(high).toHaveAttribute("data-thumb", "max");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("still supports a single-element defaultValue with the existing object thumbProps shape", () => {
|
|
136
|
+
render(
|
|
137
|
+
<Slider
|
|
138
|
+
aria-label="Volume"
|
|
139
|
+
defaultValue={[50]}
|
|
140
|
+
max={100}
|
|
141
|
+
step={1}
|
|
142
|
+
thumbProps={{ "aria-valuetext": "50%" }}
|
|
143
|
+
/>,
|
|
144
|
+
);
|
|
145
|
+
expect(screen.getByRole("slider", { name: "Volume" })).toHaveAttribute(
|
|
146
|
+
"aria-valuetext",
|
|
147
|
+
"50%",
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("applies PROTECTED_THUMB_KEYS stripping per-thumb in the array shape", () => {
|
|
152
|
+
render(
|
|
153
|
+
<Slider
|
|
154
|
+
defaultValue={[3, 9]}
|
|
155
|
+
max={12}
|
|
156
|
+
step={1}
|
|
157
|
+
thumbProps={[
|
|
158
|
+
{ "aria-label": "Min", role: "presentation", tabIndex: -1 },
|
|
159
|
+
{ "aria-label": "Max", "aria-valuenow": 999 },
|
|
160
|
+
]}
|
|
161
|
+
/>,
|
|
162
|
+
);
|
|
163
|
+
const [low, high] = screen.getAllByRole("slider");
|
|
164
|
+
expect(low).toHaveAttribute("tabindex", "0");
|
|
165
|
+
expect(high).toHaveAttribute("aria-valuenow", "9");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
|
|
2
|
+
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
|
|
5
|
+
/** Props for a single `SliderPrimitive.Thumb` — see `SliderProps["thumbProps"]`. */
|
|
6
|
+
export type SliderThumbProps = ComponentPropsWithoutRef<typeof SliderPrimitive.Thumb>;
|
|
7
|
+
|
|
8
|
+
export type SliderProps = ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & {
|
|
9
|
+
/**
|
|
10
|
+
* Announced instead of the raw numeric value (e.g. `"step 3 of 12"` for a
|
|
11
|
+
* replay scrubber). Forwarded to the thumb alongside `aria-label`/
|
|
12
|
+
* `aria-labelledby` — see the doc comment below for why it has to land
|
|
13
|
+
* there, not on the root.
|
|
14
|
+
*/
|
|
15
|
+
"aria-valuetext"?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Escape hatch spread onto the thumb LAST — for `data-*`/`style`/etc, or to
|
|
18
|
+
* deliberately override `aria-label`/`aria-labelledby`/`aria-valuetext`
|
|
19
|
+
* (an explicit key here always wins over the matching top-level prop).
|
|
20
|
+
*
|
|
21
|
+
* The six keys that MAKE the element a slider are ignored on purpose:
|
|
22
|
+
* `role`, `aria-valuenow`, `aria-valuemin`, `aria-valuemax`,
|
|
23
|
+
* `aria-orientation` and `tabIndex`. Radix derives every one of them from
|
|
24
|
+
* the slider's live state and spreads consumer props AFTER its own, so
|
|
25
|
+
* passing them here would silently destroy the widget (a wrong
|
|
26
|
+
* `aria-valuenow` mis-announces the value; `role`/`tabIndex` make it
|
|
27
|
+
* unreachable). Change the VALUE (`value`/`min`/`max`/`orientation`/
|
|
28
|
+
* `disabled`) on `Slider` instead; use `aria-valuetext` to change how the
|
|
29
|
+
* value is spoken.
|
|
30
|
+
*
|
|
31
|
+
* Radix renders one thumb per element of `value`/`defaultValue` — pass a
|
|
32
|
+
* single object to apply the same props to every thumb (the historical
|
|
33
|
+
* single-thumb shape, unchanged), or an ARRAY to give each thumb its own
|
|
34
|
+
* props, indexed positionally against `value`/`defaultValue` (#398). A
|
|
35
|
+
* missing array slot falls back to no per-thumb override for that thumb.
|
|
36
|
+
*/
|
|
37
|
+
thumbProps?: SliderThumbProps | SliderThumbProps[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Attributes Radix computes for the thumb and then lets consumer props
|
|
42
|
+
* overwrite (`SliderThumbImpl` spreads them before `...thumbProps`). Dropping
|
|
43
|
+
* them here is what keeps the escape hatch from being a foot-gun.
|
|
44
|
+
*/
|
|
45
|
+
const PROTECTED_THUMB_KEYS = [
|
|
46
|
+
"role",
|
|
47
|
+
"aria-valuenow",
|
|
48
|
+
"aria-valuemin",
|
|
49
|
+
"aria-valuemax",
|
|
50
|
+
"aria-orientation",
|
|
51
|
+
"tabIndex",
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
54
|
+
function withoutProtectedKeys(
|
|
55
|
+
thumbProps: SliderThumbProps | undefined,
|
|
56
|
+
): SliderThumbProps | undefined {
|
|
57
|
+
if (!thumbProps) return thumbProps;
|
|
58
|
+
let safe = thumbProps;
|
|
59
|
+
for (const key of PROTECTED_THUMB_KEYS) {
|
|
60
|
+
if (!(key in safe)) continue;
|
|
61
|
+
const { [key]: _dropped, ...rest } = safe;
|
|
62
|
+
safe = rest;
|
|
63
|
+
}
|
|
64
|
+
return safe;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `thumbProps` for the thumb at `index` — the shared object for every thumb, or that array slot. */
|
|
68
|
+
function getThumbPropsAt(
|
|
69
|
+
thumbProps: SliderProps["thumbProps"],
|
|
70
|
+
index: number,
|
|
71
|
+
): SliderThumbProps | undefined {
|
|
72
|
+
if (!thumbProps) return undefined;
|
|
73
|
+
return Array.isArray(thumbProps) ? thumbProps[index] : thumbProps;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Range input — a single-thumb value by default, or a multi-thumb range
|
|
78
|
+
* slider when `value`/`defaultValue` is an array with more than one element
|
|
79
|
+
* (Radix's own range-slider shape; #398). One `Thumb` is rendered per value.
|
|
80
|
+
*
|
|
81
|
+
* `aria-label` / `aria-labelledby` / `aria-valuetext` are routed to the
|
|
82
|
+
* THUMB(S), not the root: Radix puts `role="slider"` on the thumb, so a name
|
|
83
|
+
* (or value text) on the root would never reach the widget. Pass `aria-label`
|
|
84
|
+
* (or `aria-labelledby`, for a visible `<Label>`) — an unnamed slider is an
|
|
85
|
+
* accessibility failure. For a range slider, pass an ARRAY to `thumbProps` so
|
|
86
|
+
* each thumb gets its own name/valuetext (e.g. "Minimum price"/"Maximum
|
|
87
|
+
* price") — the top-level `aria-label`/`aria-valuetext` apply identically to
|
|
88
|
+
* every thumb, which is rarely what a multi-thumb slider wants.
|
|
89
|
+
*/
|
|
90
|
+
export const Slider = forwardRef<ElementRef<typeof SliderPrimitive.Root>, SliderProps>(
|
|
91
|
+
function Slider(
|
|
92
|
+
{
|
|
93
|
+
className,
|
|
94
|
+
"aria-label": ariaLabel,
|
|
95
|
+
"aria-labelledby": ariaLabelledby,
|
|
96
|
+
"aria-valuetext": ariaValueText,
|
|
97
|
+
thumbProps,
|
|
98
|
+
value,
|
|
99
|
+
defaultValue,
|
|
100
|
+
...props
|
|
101
|
+
},
|
|
102
|
+
ref,
|
|
103
|
+
) {
|
|
104
|
+
const values = value ?? defaultValue;
|
|
105
|
+
const thumbCount = Array.isArray(values) ? values.length : 1;
|
|
106
|
+
return (
|
|
107
|
+
<SliderPrimitive.Root
|
|
108
|
+
ref={ref}
|
|
109
|
+
className={cn("relative flex w-full touch-none select-none items-center", className)}
|
|
110
|
+
value={value}
|
|
111
|
+
defaultValue={defaultValue}
|
|
112
|
+
{...props}
|
|
113
|
+
>
|
|
114
|
+
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted">
|
|
115
|
+
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
|
116
|
+
</SliderPrimitive.Track>
|
|
117
|
+
{Array.from({ length: thumbCount }, (_, index) => {
|
|
118
|
+
const safeThumbProps = withoutProtectedKeys(getThumbPropsAt(thumbProps, index));
|
|
119
|
+
return (
|
|
120
|
+
<SliderPrimitive.Thumb
|
|
121
|
+
key={index}
|
|
122
|
+
aria-label={ariaLabel}
|
|
123
|
+
aria-labelledby={ariaLabelledby}
|
|
124
|
+
aria-valuetext={ariaValueText}
|
|
125
|
+
{...safeThumbProps}
|
|
126
|
+
className={cn(
|
|
127
|
+
"block size-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
|
128
|
+
safeThumbProps?.className,
|
|
129
|
+
)}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
})}
|
|
133
|
+
</SliderPrimitive.Root>
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SliderNumber, type SliderNumberProps } from "./slider-number";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect } from "storybook/test";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { SliderNumber } from "./slider-number";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Forms/SliderNumber",
|
|
8
|
+
component: SliderNumber,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
args: {
|
|
11
|
+
"aria-label": "Temperature",
|
|
12
|
+
min: 0,
|
|
13
|
+
max: 1,
|
|
14
|
+
step: 0.1,
|
|
15
|
+
},
|
|
16
|
+
argTypes: {
|
|
17
|
+
value: {
|
|
18
|
+
description: 'Controlled value. Pass `null` for "use the provider default".',
|
|
19
|
+
control: "number",
|
|
20
|
+
table: { category: "State" },
|
|
21
|
+
},
|
|
22
|
+
defaultValue: {
|
|
23
|
+
description: "Uncontrolled initial value.",
|
|
24
|
+
control: "number",
|
|
25
|
+
table: { category: "State" },
|
|
26
|
+
},
|
|
27
|
+
min: {
|
|
28
|
+
description: "Minimum allowed value.",
|
|
29
|
+
control: "number",
|
|
30
|
+
table: { category: "Behavior" },
|
|
31
|
+
},
|
|
32
|
+
max: {
|
|
33
|
+
description: "Maximum allowed value.",
|
|
34
|
+
control: "number",
|
|
35
|
+
table: { category: "Behavior" },
|
|
36
|
+
},
|
|
37
|
+
step: {
|
|
38
|
+
description: "Step increment for both the slider and the number input.",
|
|
39
|
+
control: "number",
|
|
40
|
+
table: { category: "Behavior" },
|
|
41
|
+
},
|
|
42
|
+
precision: {
|
|
43
|
+
description: "Decimal places to round to (defaults to the decimals in `step`).",
|
|
44
|
+
control: "number",
|
|
45
|
+
table: { category: "Behavior" },
|
|
46
|
+
},
|
|
47
|
+
resetLabel: {
|
|
48
|
+
description: "Label for the reset-to-provider-default control.",
|
|
49
|
+
control: "text",
|
|
50
|
+
table: { category: "Content" },
|
|
51
|
+
},
|
|
52
|
+
disabled: {
|
|
53
|
+
description: "Disables the whole control.",
|
|
54
|
+
control: "boolean",
|
|
55
|
+
table: { category: "State" },
|
|
56
|
+
},
|
|
57
|
+
onValueChange: {
|
|
58
|
+
description: "Called when the value changes.",
|
|
59
|
+
control: false,
|
|
60
|
+
table: { category: "Behavior" },
|
|
61
|
+
},
|
|
62
|
+
className: {
|
|
63
|
+
description: "Additional CSS classes applied to the root element.",
|
|
64
|
+
control: "text",
|
|
65
|
+
table: { category: "Appearance" },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
} satisfies Meta<typeof SliderNumber>;
|
|
69
|
+
export default meta;
|
|
70
|
+
type Story = StoryObj<typeof meta>;
|
|
71
|
+
|
|
72
|
+
export const Default: Story = {
|
|
73
|
+
args: { defaultValue: 0.7 },
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Starting from "provider default" — the slider thumb renders dimmed/dashed and Reset is disabled. */
|
|
77
|
+
export const ProviderDefault: Story = {
|
|
78
|
+
args: { defaultValue: null },
|
|
79
|
+
play: async ({ canvas }) => {
|
|
80
|
+
await expect(canvas.getByRole("button", { name: "Reset" })).toBeDisabled();
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const Controlled: Story = {
|
|
85
|
+
render: () => {
|
|
86
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
87
|
+
const [value, setValue] = useState<number | null>(0.5);
|
|
88
|
+
return (
|
|
89
|
+
<div className="flex flex-col gap-2 max-w-md">
|
|
90
|
+
<SliderNumber
|
|
91
|
+
aria-label="Top-p"
|
|
92
|
+
min={0}
|
|
93
|
+
max={1}
|
|
94
|
+
step={0.05}
|
|
95
|
+
value={value}
|
|
96
|
+
onValueChange={setValue}
|
|
97
|
+
/>
|
|
98
|
+
<p className="text-body text-muted-foreground">Value: {value ?? "provider default"}</p>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Dragging (via keyboard) to a value, then resetting back to the provider default. */
|
|
105
|
+
export const DragThenReset: Story = {
|
|
106
|
+
args: { defaultValue: null },
|
|
107
|
+
play: async ({ canvas, userEvent }) => {
|
|
108
|
+
const thumb = canvas.getByRole("slider", { name: "Temperature" });
|
|
109
|
+
thumb.focus();
|
|
110
|
+
await userEvent.keyboard("{ArrowRight}{ArrowRight}{ArrowRight}");
|
|
111
|
+
const numberInput = canvas.getByRole("spinbutton", { name: "Temperature" });
|
|
112
|
+
await expect(numberInput).toHaveValue(0.3);
|
|
113
|
+
|
|
114
|
+
const resetBtn = canvas.getByRole("button", { name: "Reset" });
|
|
115
|
+
await expect(resetBtn).not.toBeDisabled();
|
|
116
|
+
await userEvent.click(resetBtn);
|
|
117
|
+
await expect(resetBtn).toBeDisabled();
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const Disabled: Story = {
|
|
122
|
+
args: { defaultValue: 0.4, disabled: true },
|
|
123
|
+
};
|