@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,119 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { expect } from "storybook/test";
|
|
4
|
+
import { Checkbox } from "./checkbox";
|
|
5
|
+
import { Label } from "../label";
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Forms/Checkbox",
|
|
8
|
+
component: Checkbox,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
argTypes: {
|
|
11
|
+
checked: {
|
|
12
|
+
description: 'Controlled checked state. Use `"indeterminate"` for mixed state.',
|
|
13
|
+
control: { type: "select" },
|
|
14
|
+
options: [true, false, "indeterminate"],
|
|
15
|
+
table: { category: "State" },
|
|
16
|
+
},
|
|
17
|
+
defaultChecked: {
|
|
18
|
+
description: "Uncontrolled initial checked state.",
|
|
19
|
+
control: "boolean",
|
|
20
|
+
table: { category: "State" },
|
|
21
|
+
},
|
|
22
|
+
disabled: {
|
|
23
|
+
description: "Disables the checkbox.",
|
|
24
|
+
control: "boolean",
|
|
25
|
+
table: { category: "State" },
|
|
26
|
+
},
|
|
27
|
+
required: {
|
|
28
|
+
description: "Marks the checkbox as required in a form context.",
|
|
29
|
+
control: "boolean",
|
|
30
|
+
table: { category: "Behavior" },
|
|
31
|
+
},
|
|
32
|
+
onCheckedChange: {
|
|
33
|
+
description: "Called when the checked state changes.",
|
|
34
|
+
control: false,
|
|
35
|
+
table: { category: "Behavior" },
|
|
36
|
+
},
|
|
37
|
+
className: {
|
|
38
|
+
description: "Additional CSS classes applied to the root element.",
|
|
39
|
+
control: "text",
|
|
40
|
+
table: { category: "Appearance" },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
} satisfies Meta<typeof Checkbox>;
|
|
44
|
+
export default meta;
|
|
45
|
+
type Story = StoryObj<typeof meta>;
|
|
46
|
+
export const Default: Story = {
|
|
47
|
+
render: () => (
|
|
48
|
+
<div className="flex items-center gap-2">
|
|
49
|
+
<Checkbox id="terms" defaultChecked />
|
|
50
|
+
<Label htmlFor="terms">Accept terms</Label>
|
|
51
|
+
</div>
|
|
52
|
+
),
|
|
53
|
+
// Starts checked (defaultChecked); clicking toggles the reflected aria-checked state.
|
|
54
|
+
play: async ({ canvas, userEvent }) => {
|
|
55
|
+
const box = canvas.getByRole("checkbox");
|
|
56
|
+
await expect(box).toHaveAttribute("aria-checked", "true");
|
|
57
|
+
await userEvent.click(box);
|
|
58
|
+
await expect(box).toHaveAttribute("aria-checked", "false");
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const CHILDREN = ["Alpha", "Beta", "Gamma"];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A real tri-state "select all" group (#348) — the master shows
|
|
66
|
+
* `indeterminate` (a dash, visually distinct from `checked`'s tick) when
|
|
67
|
+
* some-but-not-all children are checked, `checked` when all are, and
|
|
68
|
+
* `unchecked` when none are.
|
|
69
|
+
*/
|
|
70
|
+
function TriStateGroup() {
|
|
71
|
+
const [checkedIds, setCheckedIds] = useState<string[]>([CHILDREN[0]!]);
|
|
72
|
+
const allChecked = checkedIds.length === CHILDREN.length;
|
|
73
|
+
const someChecked = checkedIds.length > 0 && !allChecked;
|
|
74
|
+
const masterState = allChecked ? true : someChecked ? "indeterminate" : false;
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className="flex flex-col gap-2">
|
|
78
|
+
<div className="flex items-center gap-2">
|
|
79
|
+
<Checkbox
|
|
80
|
+
id="select-all"
|
|
81
|
+
checked={masterState}
|
|
82
|
+
onCheckedChange={(next) => setCheckedIds(next ? [...CHILDREN] : [])}
|
|
83
|
+
/>
|
|
84
|
+
<Label htmlFor="select-all">Select all</Label>
|
|
85
|
+
</div>
|
|
86
|
+
<div className="ms-6 flex flex-col gap-2">
|
|
87
|
+
{CHILDREN.map((child) => (
|
|
88
|
+
<div key={child} className="flex items-center gap-2">
|
|
89
|
+
<Checkbox
|
|
90
|
+
id={child}
|
|
91
|
+
checked={checkedIds.includes(child)}
|
|
92
|
+
onCheckedChange={(next) =>
|
|
93
|
+
setCheckedIds((prev) =>
|
|
94
|
+
next ? [...prev, child] : prev.filter((id) => id !== child),
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
/>
|
|
98
|
+
<Label htmlFor={child}>{child}</Label>
|
|
99
|
+
</div>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const TriState: Story = {
|
|
107
|
+
render: () => <TriStateGroup />,
|
|
108
|
+
// One child is pre-checked, so the master starts indeterminate. Checking
|
|
109
|
+
// the remaining children flips it to fully checked.
|
|
110
|
+
play: async ({ canvas, userEvent }) => {
|
|
111
|
+
const master = canvas.getByRole("checkbox", { name: "Select all" });
|
|
112
|
+
await expect(master).toHaveAttribute("aria-checked", "mixed");
|
|
113
|
+
await expect(master).toBeVisible();
|
|
114
|
+
|
|
115
|
+
await userEvent.click(canvas.getByRole("checkbox", { name: "Beta" }));
|
|
116
|
+
await userEvent.click(canvas.getByRole("checkbox", { name: "Gamma" }));
|
|
117
|
+
await expect(master).toHaveAttribute("aria-checked", "true");
|
|
118
|
+
},
|
|
119
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
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 { Checkbox } from "./checkbox";
|
|
5
|
+
describe("Checkbox", () => {
|
|
6
|
+
it("toggles checked state", async () => {
|
|
7
|
+
render(<Checkbox aria-label="c" />);
|
|
8
|
+
const box = screen.getByRole("checkbox", { name: "c" });
|
|
9
|
+
expect(box).toHaveAttribute("data-state", "unchecked");
|
|
10
|
+
await userEvent.click(box);
|
|
11
|
+
expect(box).toHaveAttribute("data-state", "checked");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// #348 — indeterminate renders a visually distinct glyph (Minus), not Check.
|
|
15
|
+
// The swap is CSS-only (`group-data-[state=indeterminate]:` on each icon,
|
|
16
|
+
// mirroring the `data-[state=checked]:` pattern already used elsewhere in
|
|
17
|
+
// the library) so jsdom — which doesn't evaluate Tailwind's stylesheet —
|
|
18
|
+
// can only assert the WIRING: both glyphs render, and each carries the
|
|
19
|
+
// selector that shows it for the correct state. The actual rendered
|
|
20
|
+
// visibility is verified in a real browser via the Storybook story/test.
|
|
21
|
+
it("renders both glyphs with the indeterminate/checked CSS toggle wired correctly", () => {
|
|
22
|
+
const { container } = render(<Checkbox checked="indeterminate" aria-label="select all" />);
|
|
23
|
+
expect(screen.getByRole("checkbox")).toHaveAttribute("aria-checked", "mixed");
|
|
24
|
+
expect(container.querySelector('[data-state="indeterminate"]')).toBeInTheDocument();
|
|
25
|
+
|
|
26
|
+
const minus = container.querySelector("svg.lucide-minus");
|
|
27
|
+
const check = container.querySelector("svg.lucide-check");
|
|
28
|
+
expect(minus).toBeInTheDocument();
|
|
29
|
+
expect(check).toBeInTheDocument();
|
|
30
|
+
// Minus is shown ONLY when indeterminate; Check is hidden then.
|
|
31
|
+
expect(minus).toHaveClass("group-data-[state=indeterminate]:block");
|
|
32
|
+
expect(check).toHaveClass("group-data-[state=indeterminate]:hidden");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("keeps the Check glyph (not Minus) for the plain checked state", () => {
|
|
36
|
+
render(<Checkbox checked={true} aria-label="c" />);
|
|
37
|
+
expect(screen.getByRole("checkbox")).toHaveAttribute("data-state", "checked");
|
|
38
|
+
expect(screen.getByRole("checkbox")).toHaveAttribute("aria-checked", "true");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
|
|
2
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
|
3
|
+
import { Check, Minus } from "lucide-react";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
|
|
6
|
+
export const Checkbox = forwardRef<
|
|
7
|
+
ElementRef<typeof CheckboxPrimitive.Root>,
|
|
8
|
+
ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
|
9
|
+
>(function Checkbox({ className, ...props }, ref) {
|
|
10
|
+
return (
|
|
11
|
+
<CheckboxPrimitive.Root
|
|
12
|
+
ref={ref}
|
|
13
|
+
className={cn(
|
|
14
|
+
"peer size-4 shrink-0 rounded-sm border border-input shadow-sm transition-colors duration-fast ease-standard",
|
|
15
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
16
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
17
|
+
"data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
>
|
|
22
|
+
{/*
|
|
23
|
+
* `group` + `data-[state]` (Radix sets `data-state` on the Indicator
|
|
24
|
+
* itself: "checked" | "indeterminate") swaps the glyph purely via CSS —
|
|
25
|
+
* `Minus` for indeterminate, `Check` for checked — so the two states
|
|
26
|
+
* are visually distinct instead of both drawing a checkmark (#348).
|
|
27
|
+
*/}
|
|
28
|
+
<CheckboxPrimitive.Indicator className="group flex items-center justify-center text-current animate-in zoom-in-95 fade-in duration-fast ease-entrance">
|
|
29
|
+
<Check className="size-3.5 group-data-[state=indeterminate]:hidden" />
|
|
30
|
+
<Minus className="hidden size-3.5 group-data-[state=indeterminate]:block" />
|
|
31
|
+
</CheckboxPrimitive.Indicator>
|
|
32
|
+
</CheckboxPrimitive.Root>
|
|
33
|
+
);
|
|
34
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Checkbox } from "./checkbox";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect } from "storybook/test";
|
|
3
|
+
import { Button } from "../button";
|
|
4
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./collapsible";
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Disclosure/Collapsible",
|
|
7
|
+
component: Collapsible,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
argTypes: {
|
|
10
|
+
open: {
|
|
11
|
+
description: "Controlled open state.",
|
|
12
|
+
control: "boolean",
|
|
13
|
+
table: { category: "State" },
|
|
14
|
+
},
|
|
15
|
+
defaultOpen: {
|
|
16
|
+
description: "Uncontrolled: whether open on first render.",
|
|
17
|
+
control: "boolean",
|
|
18
|
+
table: { category: "State" },
|
|
19
|
+
},
|
|
20
|
+
onOpenChange: {
|
|
21
|
+
description: "Callback fired when the open state changes.",
|
|
22
|
+
control: false,
|
|
23
|
+
table: { category: "Behavior" },
|
|
24
|
+
},
|
|
25
|
+
disabled: {
|
|
26
|
+
description: "Prevents the user from toggling the collapsible.",
|
|
27
|
+
control: "boolean",
|
|
28
|
+
table: { category: "State" },
|
|
29
|
+
},
|
|
30
|
+
className: {
|
|
31
|
+
description: "Extra Tailwind classes merged via cn().",
|
|
32
|
+
control: "text",
|
|
33
|
+
table: { category: "Appearance" },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
} satisfies Meta<typeof Collapsible>;
|
|
37
|
+
export default meta;
|
|
38
|
+
type Story = StoryObj<typeof meta>;
|
|
39
|
+
export const Default: Story = {
|
|
40
|
+
render: () => (
|
|
41
|
+
<Collapsible className="w-80 space-y-2">
|
|
42
|
+
<CollapsibleTrigger asChild>
|
|
43
|
+
<Button variant="outline" size="sm">
|
|
44
|
+
Toggle details
|
|
45
|
+
</Button>
|
|
46
|
+
</CollapsibleTrigger>
|
|
47
|
+
<CollapsibleContent className="rounded-md border p-3 text-body text-muted-foreground">
|
|
48
|
+
Hidden details revealed.
|
|
49
|
+
</CollapsibleContent>
|
|
50
|
+
</Collapsible>
|
|
51
|
+
),
|
|
52
|
+
// Clicking the trigger toggles the content panel open.
|
|
53
|
+
play: async ({ canvas, userEvent }) => {
|
|
54
|
+
const trigger = canvas.getByRole("button", { name: "Toggle details" });
|
|
55
|
+
// Initially closed — content is not visible.
|
|
56
|
+
await expect(trigger).toBeInTheDocument();
|
|
57
|
+
await userEvent.click(trigger);
|
|
58
|
+
// After click, the content should be in the DOM and becoming visible.
|
|
59
|
+
const content = await canvas.findByText("Hidden details revealed.");
|
|
60
|
+
await expect(content).toBeInTheDocument();
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
|
|
2
|
+
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
|
|
5
|
+
export const Collapsible = CollapsiblePrimitive.Root;
|
|
6
|
+
export const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
|
7
|
+
|
|
8
|
+
export const CollapsibleContent = forwardRef<
|
|
9
|
+
ElementRef<typeof CollapsiblePrimitive.CollapsibleContent>,
|
|
10
|
+
ComponentPropsWithoutRef<typeof CollapsiblePrimitive.CollapsibleContent>
|
|
11
|
+
>(function CollapsibleContent({ className, ...props }, ref) {
|
|
12
|
+
return (
|
|
13
|
+
<CollapsiblePrimitive.CollapsibleContent
|
|
14
|
+
ref={ref}
|
|
15
|
+
className={cn(
|
|
16
|
+
"overflow-hidden data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-up data-[state=open]:[--tw-ease:var(--ease-entrance)] data-[state=closed]:[--tw-ease:var(--ease-exit)]",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent } from "./collapsible";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { act, renderHook } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { useCollapsiblePanel } from "./use-collapsible-panel";
|
|
5
|
+
|
|
6
|
+
describe("useCollapsiblePanel (#190)", () => {
|
|
7
|
+
it("is uncontrolled by default (defaultOpen=true) and toggles", () => {
|
|
8
|
+
const { result } = renderHook(() => useCollapsiblePanel());
|
|
9
|
+
expect(result.current.open).toBe(true);
|
|
10
|
+
expect(result.current.state).toBe("expanded");
|
|
11
|
+
expect(result.current.attrs).toEqual({ "data-state": "expanded", "data-side": "left" });
|
|
12
|
+
|
|
13
|
+
act(() => result.current.toggle());
|
|
14
|
+
expect(result.current.open).toBe(false);
|
|
15
|
+
expect(result.current.state).toBe("collapsed");
|
|
16
|
+
expect(result.current.attrs["data-state"]).toBe("collapsed");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("respects defaultOpen=false and setOpen with an updater fn", () => {
|
|
20
|
+
const { result } = renderHook(() => useCollapsiblePanel({ defaultOpen: false }));
|
|
21
|
+
expect(result.current.state).toBe("collapsed");
|
|
22
|
+
act(() => result.current.setOpen((o) => !o));
|
|
23
|
+
expect(result.current.state).toBe("expanded");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("controlled mode: the `open` prop wins and never flips to uncontrolled", () => {
|
|
27
|
+
const onOpenChange = vi.fn();
|
|
28
|
+
const { result, rerender } = renderHook(
|
|
29
|
+
({ open }: { open: boolean }) => useCollapsiblePanel({ open, onOpenChange }),
|
|
30
|
+
{ initialProps: { open: false } },
|
|
31
|
+
);
|
|
32
|
+
expect(result.current.state).toBe("collapsed");
|
|
33
|
+
|
|
34
|
+
// setOpen does NOT mutate internal state in controlled mode — it only
|
|
35
|
+
// notifies; the prop stays the source of truth.
|
|
36
|
+
act(() => result.current.setOpen(true));
|
|
37
|
+
expect(onOpenChange).toHaveBeenCalledWith(true);
|
|
38
|
+
expect(result.current.open).toBe(false);
|
|
39
|
+
|
|
40
|
+
rerender({ open: true });
|
|
41
|
+
expect(result.current.state).toBe("expanded");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("uncontrolled mode still notifies onOpenChange", () => {
|
|
45
|
+
const onOpenChange = vi.fn();
|
|
46
|
+
const { result } = renderHook(() => useCollapsiblePanel({ onOpenChange }));
|
|
47
|
+
act(() => result.current.toggle());
|
|
48
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
49
|
+
expect(result.current.open).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("data-side follows the side option", () => {
|
|
53
|
+
const { result } = renderHook(() => useCollapsiblePanel({ side: "right" }));
|
|
54
|
+
expect(result.current.attrs["data-side"]).toBe("right");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("default fragments use the gated motion utilities (duration-base ease-linear)", () => {
|
|
58
|
+
const { result } = renderHook(() => useCollapsiblePanel());
|
|
59
|
+
expect(result.current.spacerClassName).toBe(
|
|
60
|
+
"relative w-(--collapsible-panel-width) bg-transparent transition-[width] duration-base ease-linear group-data-[state=collapsed]:w-0",
|
|
61
|
+
);
|
|
62
|
+
expect(result.current.containerClassName).toBe(
|
|
63
|
+
"fixed inset-y-0 z-10 hidden h-svh w-(--collapsible-panel-width) transition-[left,right,width] duration-base ease-linear md:flex left-0 group-data-[state=collapsed]:left-[calc(var(--collapsible-panel-width)*-1)]",
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// The byte-identity lock (#190 acceptance): configured with Sidebar's
|
|
68
|
+
// literals, the hook must emit EXACTLY the class fragments sidebar.tsx
|
|
69
|
+
// shipped before the extraction (the pre-#190 strings, copied verbatim).
|
|
70
|
+
it("emits Sidebar's original fragments byte-identically when given its literals", () => {
|
|
71
|
+
const sidebarOptions = {
|
|
72
|
+
widthClassName: "w-(--sidebar-width)",
|
|
73
|
+
spacerCollapsedClassName: "group-data-[collapsible=offcanvas]:w-0",
|
|
74
|
+
containerSlideClassNames: {
|
|
75
|
+
left: "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]",
|
|
76
|
+
right: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
|
77
|
+
},
|
|
78
|
+
} as const;
|
|
79
|
+
|
|
80
|
+
const left = renderHook(() => useCollapsiblePanel({ side: "left", ...sidebarOptions }));
|
|
81
|
+
expect(left.result.current.spacerClassName).toBe(
|
|
82
|
+
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-base ease-linear group-data-[collapsible=offcanvas]:w-0",
|
|
83
|
+
);
|
|
84
|
+
expect(left.result.current.containerClassName).toBe(
|
|
85
|
+
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-base ease-linear md:flex left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]",
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const right = renderHook(() => useCollapsiblePanel({ side: "right", ...sidebarOptions }));
|
|
89
|
+
expect(right.result.current.containerClassName).toBe(
|
|
90
|
+
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-base ease-linear md:flex right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useCollapsiblePanel — THE canonical collapsing-panel mechanism (#190,
|
|
5
|
+
* research 09 §B.2/§E.1).
|
|
6
|
+
*
|
|
7
|
+
* Extracted from Sidebar's collapse (sidebar.tsx): an always-mounted,
|
|
8
|
+
* two-element width tween — a gap SPACER that reserves/releases the inline
|
|
9
|
+
* width (`transition-[width] duration-base ease-linear`, collapsing to `w-0`)
|
|
10
|
+
* and a fixed CONTAINER that slides off-screen
|
|
11
|
+
* (`transition-[left,right,width] duration-base ease-linear`). `duration-base`
|
|
12
|
+
* is the gated `--t-base` motion token, so reduced-motion is honoured for free
|
|
13
|
+
* (pure CSS — the shared base stays motion-free, research 09 decision 2).
|
|
14
|
+
*
|
|
15
|
+
* The hook owns:
|
|
16
|
+
* - the state machine — controlled + uncontrolled `open` per
|
|
17
|
+
* component-api.md (`isControlled = open !== undefined`, never flips
|
|
18
|
+
* modes) with `setOpen`/`toggle`;
|
|
19
|
+
* - the `data-state`/`data-side` attrs for the group element;
|
|
20
|
+
* - the assembly of the two class fragments in the canonical order.
|
|
21
|
+
*
|
|
22
|
+
* Width / collapse-trigger / slide utilities are passed as LITERAL class
|
|
23
|
+
* strings by the consumer (Tailwind's scanner must see each literal in
|
|
24
|
+
* source — a template-interpolated class is never generated), with
|
|
25
|
+
* state-keyed defaults for a generic panel. Sidebar passes its
|
|
26
|
+
* `--sidebar-width` + `group-data-[collapsible=offcanvas]` literals and gets
|
|
27
|
+
* back its exact original fragments (the byte-identical re-point).
|
|
28
|
+
*
|
|
29
|
+
* Fork-prevention: a second gap-spacer + fixed-slide width tween outside this
|
|
30
|
+
* folder fails `pnpm collapse-fork:check` (scripts/check-collapse-fork.mjs).
|
|
31
|
+
*/
|
|
32
|
+
import { useCallback, useMemo, useState } from "react";
|
|
33
|
+
import { cn } from "../../lib/cn";
|
|
34
|
+
|
|
35
|
+
export type CollapsiblePanelSide = "left" | "right";
|
|
36
|
+
export type CollapsiblePanelState = "expanded" | "collapsed";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* State-keyed defaults for a generic panel: the consumer wraps the panel in a
|
|
40
|
+
* `group` element carrying `attrs` and sets `--collapsible-panel-width` on it.
|
|
41
|
+
*/
|
|
42
|
+
const DEFAULT_WIDTH_CLASSNAME = "w-(--collapsible-panel-width)";
|
|
43
|
+
const DEFAULT_SPACER_COLLAPSED_CLASSNAME = "group-data-[state=collapsed]:w-0";
|
|
44
|
+
const DEFAULT_CONTAINER_SLIDE_CLASSNAMES: Record<CollapsiblePanelSide, string> = {
|
|
45
|
+
left: "left-0 group-data-[state=collapsed]:left-[calc(var(--collapsible-panel-width)*-1)]",
|
|
46
|
+
right: "right-0 group-data-[state=collapsed]:right-[calc(var(--collapsible-panel-width)*-1)]",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export interface UseCollapsiblePanelOptions {
|
|
50
|
+
/** Which edge the panel lives on. Default `"left"`. */
|
|
51
|
+
side?: CollapsiblePanelSide;
|
|
52
|
+
/** Controlled open state (`isControlled = open !== undefined`). */
|
|
53
|
+
open?: boolean;
|
|
54
|
+
/** Uncontrolled initial open state. Default `true`. */
|
|
55
|
+
defaultOpen?: boolean;
|
|
56
|
+
/** Called with the next open state on `setOpen`/`toggle`. */
|
|
57
|
+
onOpenChange?: (open: boolean) => void;
|
|
58
|
+
/**
|
|
59
|
+
* Literal expanded-width utility (e.g. `"w-(--sidebar-width)"`). Must be a
|
|
60
|
+
* literal in the consumer's source so Tailwind generates it.
|
|
61
|
+
*/
|
|
62
|
+
widthClassName?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Literal collapsed-state utility for the gap spacer (e.g.
|
|
65
|
+
* `"group-data-[collapsible=offcanvas]:w-0"`).
|
|
66
|
+
*/
|
|
67
|
+
spacerCollapsedClassName?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Literal off-screen slide utilities for the fixed container, keyed by side
|
|
70
|
+
* (e.g. `left: "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"`).
|
|
71
|
+
*/
|
|
72
|
+
containerSlideClassNames?: Record<CollapsiblePanelSide, string>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Data attributes for the panel's `group` element. */
|
|
76
|
+
export interface CollapsiblePanelAttrs {
|
|
77
|
+
"data-state": CollapsiblePanelState;
|
|
78
|
+
"data-side": CollapsiblePanelSide;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface UseCollapsiblePanelReturn {
|
|
82
|
+
/** Resolved open state (controlled prop wins). */
|
|
83
|
+
open: boolean;
|
|
84
|
+
/** `"expanded"` when open, `"collapsed"` when closed. */
|
|
85
|
+
state: CollapsiblePanelState;
|
|
86
|
+
side: CollapsiblePanelSide;
|
|
87
|
+
/** Set the open state (accepts a value or an updater fn). */
|
|
88
|
+
setOpen: (open: boolean | ((open: boolean) => boolean)) => void;
|
|
89
|
+
toggle: () => void;
|
|
90
|
+
/** `data-state`/`data-side` for the group element wrapping both fragments. */
|
|
91
|
+
attrs: CollapsiblePanelAttrs;
|
|
92
|
+
/** The gap-spacer fragment: reserves/releases the inline width. */
|
|
93
|
+
spacerClassName: string;
|
|
94
|
+
/** The fixed-container fragment: slides the panel off-screen. */
|
|
95
|
+
containerClassName: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function useCollapsiblePanel({
|
|
99
|
+
side = "left",
|
|
100
|
+
open: openProp,
|
|
101
|
+
defaultOpen = true,
|
|
102
|
+
onOpenChange,
|
|
103
|
+
widthClassName = DEFAULT_WIDTH_CLASSNAME,
|
|
104
|
+
spacerCollapsedClassName = DEFAULT_SPACER_COLLAPSED_CLASSNAME,
|
|
105
|
+
containerSlideClassNames = DEFAULT_CONTAINER_SLIDE_CLASSNAMES,
|
|
106
|
+
}: UseCollapsiblePanelOptions = {}): UseCollapsiblePanelReturn {
|
|
107
|
+
// Controlled/uncontrolled per component-api.md: controlled iff the `open`
|
|
108
|
+
// prop is provided; the mode never flips mid-life.
|
|
109
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
110
|
+
const isControlled = openProp !== undefined;
|
|
111
|
+
const open = isControlled ? openProp : uncontrolledOpen;
|
|
112
|
+
|
|
113
|
+
const setOpen = useCallback(
|
|
114
|
+
(value: boolean | ((open: boolean) => boolean)) => {
|
|
115
|
+
const next = typeof value === "function" ? value(open) : value;
|
|
116
|
+
if (!isControlled) setUncontrolledOpen(next);
|
|
117
|
+
onOpenChange?.(next);
|
|
118
|
+
},
|
|
119
|
+
[isControlled, onOpenChange, open],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const toggle = useCallback(() => setOpen((o) => !o), [setOpen]);
|
|
123
|
+
|
|
124
|
+
const state: CollapsiblePanelState = open ? "expanded" : "collapsed";
|
|
125
|
+
|
|
126
|
+
// The two class fragments — the Sidebar collapse mechanism verbatim
|
|
127
|
+
// (research 09 §A.1): assembly order is part of the contract (it is what
|
|
128
|
+
// keeps Sidebar's emitted className strings byte-identical).
|
|
129
|
+
const spacerClassName = useMemo(
|
|
130
|
+
() =>
|
|
131
|
+
cn(
|
|
132
|
+
"relative",
|
|
133
|
+
widthClassName,
|
|
134
|
+
"bg-transparent transition-[width] duration-base ease-linear",
|
|
135
|
+
spacerCollapsedClassName,
|
|
136
|
+
),
|
|
137
|
+
[spacerCollapsedClassName, widthClassName],
|
|
138
|
+
);
|
|
139
|
+
const containerClassName = useMemo(
|
|
140
|
+
() =>
|
|
141
|
+
cn(
|
|
142
|
+
"fixed inset-y-0 z-10 hidden h-svh",
|
|
143
|
+
widthClassName,
|
|
144
|
+
"transition-[left,right,width] duration-base ease-linear md:flex",
|
|
145
|
+
containerSlideClassNames[side],
|
|
146
|
+
),
|
|
147
|
+
[containerSlideClassNames, side, widthClassName],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const attrs = useMemo<CollapsiblePanelAttrs>(
|
|
151
|
+
() => ({ "data-state": state, "data-side": side }),
|
|
152
|
+
[side, state],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return useMemo(
|
|
156
|
+
() => ({ open, state, side, setOpen, toggle, attrs, spacerClassName, containerClassName }),
|
|
157
|
+
[open, state, side, setOpen, toggle, attrs, spacerClassName, containerClassName],
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, within } from "storybook/test";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { ColorPicker } from "./color-picker";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Forms/ColorPicker",
|
|
8
|
+
component: ColorPicker,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
argTypes: {
|
|
11
|
+
value: {
|
|
12
|
+
description: "Controlled color token name or raw hex string.",
|
|
13
|
+
control: "text",
|
|
14
|
+
table: { category: "State" },
|
|
15
|
+
},
|
|
16
|
+
defaultValue: {
|
|
17
|
+
description: "Uncontrolled initial color value.",
|
|
18
|
+
control: "text",
|
|
19
|
+
table: { category: "State" },
|
|
20
|
+
},
|
|
21
|
+
allowCustom: {
|
|
22
|
+
description: "Show a free-form hex input below the swatches (not theme-aware).",
|
|
23
|
+
control: "boolean",
|
|
24
|
+
table: { category: "Behavior" },
|
|
25
|
+
},
|
|
26
|
+
swatches: {
|
|
27
|
+
description: "Subset of ColorToken names to display as swatches.",
|
|
28
|
+
control: false,
|
|
29
|
+
table: { category: "Content" },
|
|
30
|
+
},
|
|
31
|
+
onValueChange: {
|
|
32
|
+
description: "Called when the selected color changes.",
|
|
33
|
+
control: false,
|
|
34
|
+
table: { category: "Behavior" },
|
|
35
|
+
},
|
|
36
|
+
"aria-label": {
|
|
37
|
+
description: "Accessible label for the trigger button.",
|
|
38
|
+
control: "text",
|
|
39
|
+
table: { category: "Appearance" },
|
|
40
|
+
},
|
|
41
|
+
className: {
|
|
42
|
+
description: "Additional CSS classes applied to the root element.",
|
|
43
|
+
control: "text",
|
|
44
|
+
table: { category: "Appearance" },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
} satisfies Meta<typeof ColorPicker>;
|
|
48
|
+
export default meta;
|
|
49
|
+
type Story = StoryObj<typeof meta>;
|
|
50
|
+
|
|
51
|
+
export const Default: Story = {
|
|
52
|
+
args: { defaultValue: "chart-1" },
|
|
53
|
+
// Opens the popover, confirms the swatch radiogroup is visible.
|
|
54
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
55
|
+
const trigger = canvas.getByRole("button", { name: /pick color/i });
|
|
56
|
+
await expect(trigger).toBeInTheDocument();
|
|
57
|
+
await userEvent.click(trigger);
|
|
58
|
+
const body = within(canvasElement.ownerDocument.body);
|
|
59
|
+
const group = await body.findByRole("radiogroup", { name: /color swatches/i });
|
|
60
|
+
await expect(group).toBeInTheDocument();
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const Controlled: Story = {
|
|
65
|
+
render: () => {
|
|
66
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
67
|
+
const [color, setColor] = useState<string>("primary");
|
|
68
|
+
return (
|
|
69
|
+
<div className="flex flex-col gap-2">
|
|
70
|
+
<ColorPicker value={color} onValueChange={setColor} />
|
|
71
|
+
<p className="text-body text-muted-foreground">Selected: {color}</p>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const ChartTokensOnly: Story = {
|
|
78
|
+
args: {
|
|
79
|
+
defaultValue: "chart-1",
|
|
80
|
+
swatches: ["chart-1", "chart-2", "chart-3", "chart-4", "chart-5"],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const WithCustomHex: Story = {
|
|
85
|
+
args: { defaultValue: "primary", allowCustom: true },
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const StatusTokens: Story = {
|
|
89
|
+
args: {
|
|
90
|
+
defaultValue: "success",
|
|
91
|
+
swatches: ["success", "warning", "destructive", "info"],
|
|
92
|
+
},
|
|
93
|
+
};
|