@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,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comparison Table — the feature/plan matrix brand-ui was missing.
|
|
3
|
+
*
|
|
4
|
+
* shadcnblocks' Compare group is a real capability gap (zero coverage). This
|
|
5
|
+
* block is the canonical plan/feature comparison: a sticky-ish header with one
|
|
6
|
+
* featured column (badge + emphasis), feature rows with check / dash / value
|
|
7
|
+
* cells, a price row, and a per-plan CTA row. Reuses brand-ui's strongest
|
|
8
|
+
* assets — Table + Badge + Button — and stays token-only.
|
|
9
|
+
*
|
|
10
|
+
* Reads in all three themes. Verify with globals=theme:<slug>.
|
|
11
|
+
*/
|
|
12
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
13
|
+
import { Check, Minus } from "lucide-react";
|
|
14
|
+
import {
|
|
15
|
+
Badge,
|
|
16
|
+
Button,
|
|
17
|
+
Table,
|
|
18
|
+
TableBody,
|
|
19
|
+
TableCell,
|
|
20
|
+
TableHead,
|
|
21
|
+
TableHeader,
|
|
22
|
+
TableRow,
|
|
23
|
+
} from "./index";
|
|
24
|
+
|
|
25
|
+
interface Plan {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
price: string;
|
|
29
|
+
per: string;
|
|
30
|
+
cta: string;
|
|
31
|
+
featured?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const PLANS: Plan[] = [
|
|
35
|
+
{ id: "starter", name: "Starter", price: "$0", per: "free forever", cta: "Start free" },
|
|
36
|
+
{
|
|
37
|
+
id: "business",
|
|
38
|
+
name: "Business",
|
|
39
|
+
price: "$49",
|
|
40
|
+
per: "per user / mo",
|
|
41
|
+
cta: "Start trial",
|
|
42
|
+
featured: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "enterprise",
|
|
46
|
+
name: "Enterprise",
|
|
47
|
+
price: "Custom",
|
|
48
|
+
per: "contact us",
|
|
49
|
+
cta: "Contact sales",
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
type Cell = string | boolean;
|
|
54
|
+
const FEATURES: { label: string; values: [Cell, Cell, Cell] }[] = [
|
|
55
|
+
{ label: "Seats", values: ["Up to 3", "Up to 50", "Unlimited"] },
|
|
56
|
+
{ label: "Projects", values: ["3", "Unlimited", "Unlimited"] },
|
|
57
|
+
{ label: "Data history", values: ["7 days", "1 year", "Unlimited"] },
|
|
58
|
+
{ label: "SSO / SAML", values: [false, false, true] },
|
|
59
|
+
{ label: "Audit log", values: [false, true, true] },
|
|
60
|
+
{ label: "Priority support", values: [false, true, true] },
|
|
61
|
+
{ label: "Uptime SLA", values: [false, false, true] },
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
/** featured column gets a recessed wash so the eye lands on it. */
|
|
65
|
+
function colClass(featured?: boolean) {
|
|
66
|
+
return featured ? "bg-surface-muted" : "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ValueCell({ value }: { value: Cell }) {
|
|
70
|
+
if (typeof value === "boolean") {
|
|
71
|
+
return value ? (
|
|
72
|
+
<>
|
|
73
|
+
<Check className="mx-auto size-4 text-success-text" aria-hidden="true" />
|
|
74
|
+
<span className="sr-only">Included</span>
|
|
75
|
+
</>
|
|
76
|
+
) : (
|
|
77
|
+
<>
|
|
78
|
+
<Minus className="mx-auto size-4 text-muted-foreground" aria-hidden="true" />
|
|
79
|
+
<span className="sr-only">Not included</span>
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return <span className="text-body text-foreground">{value}</span>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function ComparisonTable() {
|
|
87
|
+
return (
|
|
88
|
+
<div className="bg-background p-6 text-foreground">
|
|
89
|
+
<div className="mx-auto max-w-4xl overflow-hidden rounded-xl border bg-card">
|
|
90
|
+
<Table>
|
|
91
|
+
<TableHeader>
|
|
92
|
+
<TableRow className="hover:bg-transparent">
|
|
93
|
+
<TableHead className="w-[34%] align-bottom">
|
|
94
|
+
<span className="text-body font-medium text-muted-foreground">Compare plans</span>
|
|
95
|
+
</TableHead>
|
|
96
|
+
{PLANS.map((p) => (
|
|
97
|
+
<TableHead
|
|
98
|
+
key={p.id}
|
|
99
|
+
className={`text-center align-bottom ${colClass(p.featured)}`}
|
|
100
|
+
>
|
|
101
|
+
<div className="flex flex-col items-center gap-1 py-2">
|
|
102
|
+
{p.featured ? <Badge>Most popular</Badge> : null}
|
|
103
|
+
<span className="text-subtitle font-semibold text-foreground">{p.name}</span>
|
|
104
|
+
<span className="text-title font-semibold tabular-nums text-foreground">
|
|
105
|
+
{p.price}
|
|
106
|
+
</span>
|
|
107
|
+
<span className="text-meta text-muted-foreground">{p.per}</span>
|
|
108
|
+
</div>
|
|
109
|
+
</TableHead>
|
|
110
|
+
))}
|
|
111
|
+
</TableRow>
|
|
112
|
+
</TableHeader>
|
|
113
|
+
|
|
114
|
+
<TableBody>
|
|
115
|
+
{FEATURES.map((f) => (
|
|
116
|
+
<TableRow key={f.label}>
|
|
117
|
+
<TableCell className="font-medium text-foreground">{f.label}</TableCell>
|
|
118
|
+
{PLANS.map((p, i) => (
|
|
119
|
+
<TableCell key={p.id} className={`text-center ${colClass(p.featured)}`}>
|
|
120
|
+
<ValueCell value={f.values[i] ?? "—"} />
|
|
121
|
+
</TableCell>
|
|
122
|
+
))}
|
|
123
|
+
</TableRow>
|
|
124
|
+
))}
|
|
125
|
+
|
|
126
|
+
{/* CTA row */}
|
|
127
|
+
<TableRow className="hover:bg-transparent">
|
|
128
|
+
<TableCell />
|
|
129
|
+
{PLANS.map((p) => (
|
|
130
|
+
<TableCell key={p.id} className={`text-center ${colClass(p.featured)}`}>
|
|
131
|
+
<Button
|
|
132
|
+
size="sm"
|
|
133
|
+
variant={p.featured ? "default" : "outline"}
|
|
134
|
+
className="my-2 w-full max-w-[12rem]"
|
|
135
|
+
>
|
|
136
|
+
{p.cta}
|
|
137
|
+
</Button>
|
|
138
|
+
</TableCell>
|
|
139
|
+
))}
|
|
140
|
+
</TableRow>
|
|
141
|
+
</TableBody>
|
|
142
|
+
</Table>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const meta = {
|
|
149
|
+
title: "Patterns/Blocks/Comparison Table",
|
|
150
|
+
parameters: {
|
|
151
|
+
layout: "fullscreen",
|
|
152
|
+
docs: {
|
|
153
|
+
description: {
|
|
154
|
+
component:
|
|
155
|
+
"The canonical plan/feature comparison matrix (brand-ui's Compare gap): a header with one featured column (badge + recessed wash), feature rows with check / dash / value cells, a price row, and a per-plan CTA row. Composes Table + Badge + Button; semantic tokens only; reads in all three themes.",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
tags: ["autodocs"],
|
|
160
|
+
} satisfies Meta;
|
|
161
|
+
|
|
162
|
+
export default meta;
|
|
163
|
+
type Story = StoryObj<typeof meta>;
|
|
164
|
+
|
|
165
|
+
export const Default: Story = { render: () => <ComparisonTable /> };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, waitFor } from "storybook/test";
|
|
3
|
+
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./accordion";
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Disclosure/Accordion",
|
|
6
|
+
component: Accordion,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
argTypes: {
|
|
9
|
+
type: {
|
|
10
|
+
description: "`single` allows one item open at a time; `multiple` allows any number.",
|
|
11
|
+
control: { type: "radio" },
|
|
12
|
+
options: ["single", "multiple"],
|
|
13
|
+
table: { category: "Behavior" },
|
|
14
|
+
},
|
|
15
|
+
collapsible: {
|
|
16
|
+
description: 'When `type="single"`, allows the open item to be closed by clicking it again.',
|
|
17
|
+
control: "boolean",
|
|
18
|
+
table: { category: "Behavior" },
|
|
19
|
+
},
|
|
20
|
+
defaultValue: {
|
|
21
|
+
description: "Uncontrolled: value(s) open on first render.",
|
|
22
|
+
control: "text",
|
|
23
|
+
table: { category: "State" },
|
|
24
|
+
},
|
|
25
|
+
value: {
|
|
26
|
+
description: "Controlled open value(s).",
|
|
27
|
+
control: "text",
|
|
28
|
+
table: { category: "State" },
|
|
29
|
+
},
|
|
30
|
+
onValueChange: {
|
|
31
|
+
description: "Callback fired when the open value(s) change.",
|
|
32
|
+
control: false,
|
|
33
|
+
table: { category: "Behavior" },
|
|
34
|
+
},
|
|
35
|
+
disabled: {
|
|
36
|
+
description: "Disables all accordion items.",
|
|
37
|
+
control: "boolean",
|
|
38
|
+
table: { category: "State" },
|
|
39
|
+
},
|
|
40
|
+
className: {
|
|
41
|
+
description: "Extra Tailwind classes merged via cn().",
|
|
42
|
+
control: "text",
|
|
43
|
+
table: { category: "Appearance" },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
} satisfies Meta<typeof Accordion>;
|
|
47
|
+
export default meta;
|
|
48
|
+
type Story = StoryObj<typeof meta>;
|
|
49
|
+
export const Default: Story = {
|
|
50
|
+
render: () => (
|
|
51
|
+
<Accordion type="single" collapsible className="w-80">
|
|
52
|
+
<AccordionItem value="a">
|
|
53
|
+
<AccordionTrigger>What is brand-ui?</AccordionTrigger>
|
|
54
|
+
<AccordionContent>A token-driven component system.</AccordionContent>
|
|
55
|
+
</AccordionItem>
|
|
56
|
+
<AccordionItem value="b">
|
|
57
|
+
<AccordionTrigger>Is it themeable?</AccordionTrigger>
|
|
58
|
+
<AccordionContent>Yes — via data-theme tokens.</AccordionContent>
|
|
59
|
+
</AccordionItem>
|
|
60
|
+
</Accordion>
|
|
61
|
+
),
|
|
62
|
+
// Expanding an item flips its trigger to aria-expanded and mounts the panel
|
|
63
|
+
// content (Radix keeps closed panels out of the DOM).
|
|
64
|
+
play: async ({ canvas, userEvent }) => {
|
|
65
|
+
const trigger = canvas.getByRole("button", { name: "What is brand-ui?" });
|
|
66
|
+
await expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
67
|
+
await userEvent.click(trigger);
|
|
68
|
+
await expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
69
|
+
await waitFor(() => expect(canvas.getByText("A token-driven component system.")).toBeVisible());
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
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 { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./accordion";
|
|
5
|
+
describe("Accordion", () => {
|
|
6
|
+
it("expands an item on click", async () => {
|
|
7
|
+
render(
|
|
8
|
+
<Accordion type="single" collapsible>
|
|
9
|
+
<AccordionItem value="a">
|
|
10
|
+
<AccordionTrigger>Q</AccordionTrigger>
|
|
11
|
+
<AccordionContent>Answer</AccordionContent>
|
|
12
|
+
</AccordionItem>
|
|
13
|
+
</Accordion>,
|
|
14
|
+
);
|
|
15
|
+
await userEvent.click(screen.getByRole("button", { name: "Q" }));
|
|
16
|
+
expect(screen.getByText("Answer")).toBeVisible();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from "react";
|
|
2
|
+
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
|
3
|
+
import { ChevronDown } from "lucide-react";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
|
|
6
|
+
export const Accordion = AccordionPrimitive.Root;
|
|
7
|
+
|
|
8
|
+
export const AccordionItem = forwardRef<
|
|
9
|
+
ElementRef<typeof AccordionPrimitive.Item>,
|
|
10
|
+
ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
11
|
+
>(function AccordionItem({ className, ...props }, ref) {
|
|
12
|
+
return <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const AccordionTrigger = forwardRef<
|
|
16
|
+
ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
17
|
+
ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
|
18
|
+
>(function AccordionTrigger({ className, children, ...props }, ref) {
|
|
19
|
+
return (
|
|
20
|
+
<AccordionPrimitive.Header className="flex">
|
|
21
|
+
<AccordionPrimitive.Trigger
|
|
22
|
+
ref={ref}
|
|
23
|
+
className={cn(
|
|
24
|
+
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&[data-state=open]>svg]:rotate-180",
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
{...props}
|
|
28
|
+
>
|
|
29
|
+
{children}
|
|
30
|
+
<ChevronDown className="size-4 shrink-0 text-muted-foreground transition-transform duration-base ease-standard" />
|
|
31
|
+
</AccordionPrimitive.Trigger>
|
|
32
|
+
</AccordionPrimitive.Header>
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const AccordionContent = forwardRef<
|
|
37
|
+
ElementRef<typeof AccordionPrimitive.Content>,
|
|
38
|
+
ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
39
|
+
>(function AccordionContent({ className, children, ...props }, ref) {
|
|
40
|
+
return (
|
|
41
|
+
<AccordionPrimitive.Content
|
|
42
|
+
ref={ref}
|
|
43
|
+
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
<div className={cn("pb-4 pt-0 text-muted-foreground", className)}>{children}</div>
|
|
47
|
+
</AccordionPrimitive.Content>
|
|
48
|
+
);
|
|
49
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "./accordion";
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, waitFor, within } from "storybook/test";
|
|
3
|
+
import { ThemeProvider } from "@elabs-ai/components-tokens";
|
|
4
|
+
import { Button } from "../button";
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogClose,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogSection,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
DialogTrigger,
|
|
15
|
+
} from "../dialog";
|
|
16
|
+
import { Input } from "../input";
|
|
17
|
+
import { Label } from "../label";
|
|
18
|
+
import { AdvancedGroup } from "./advanced-group";
|
|
19
|
+
|
|
20
|
+
const meta = {
|
|
21
|
+
title: "Forms/AdvancedGroup",
|
|
22
|
+
component: AdvancedGroup,
|
|
23
|
+
tags: ["autodocs"],
|
|
24
|
+
} satisfies Meta<typeof AdvancedGroup>;
|
|
25
|
+
export default meta;
|
|
26
|
+
type Story = StoryObj<typeof meta>;
|
|
27
|
+
|
|
28
|
+
function Field({ id, label, defaultValue }: { id: string; label: string; defaultValue?: string }) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="flex flex-col gap-1.5">
|
|
31
|
+
<Label htmlFor={id}>{label}</Label>
|
|
32
|
+
<Input id={id} name={id} autoComplete="off" defaultValue={defaultValue} />
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Collapsed by default — and it says what is hidden. "3 changed" is the whole
|
|
39
|
+
* point: a reader can tell the group holds edits without opening it.
|
|
40
|
+
*/
|
|
41
|
+
export const Default: Story = {
|
|
42
|
+
args: { changedCount: 3 },
|
|
43
|
+
render: (args) => (
|
|
44
|
+
<div className="w-96">
|
|
45
|
+
<AdvancedGroup {...args}>
|
|
46
|
+
<Field id="adv-retries" label="Max retries" defaultValue="5" />
|
|
47
|
+
<Field id="adv-timeout" label="Timeout (seconds)" defaultValue="120" />
|
|
48
|
+
<Field id="adv-endpoint" label="Custom endpoint" defaultValue="eu-west-1.internal" />
|
|
49
|
+
</AdvancedGroup>
|
|
50
|
+
</div>
|
|
51
|
+
),
|
|
52
|
+
play: async ({ canvas, userEvent }) => {
|
|
53
|
+
const trigger = canvas.getByRole("button", { name: /advanced/i });
|
|
54
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
55
|
+
expect(canvas.getByText("3 changed")).toBeVisible();
|
|
56
|
+
|
|
57
|
+
await userEvent.click(trigger);
|
|
58
|
+
|
|
59
|
+
await waitFor(() => expect(trigger).toHaveAttribute("aria-expanded", "true"));
|
|
60
|
+
await waitFor(() => expect(canvas.getByLabelText("Max retries")).toBeVisible());
|
|
61
|
+
// Once the values are on screen the summary is noise.
|
|
62
|
+
expect(canvas.queryByText("3 changed")).toBeNull();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** A free-form summary when "N changed" is not the useful thing to say. */
|
|
67
|
+
export const CustomSummary: Story = {
|
|
68
|
+
args: { summary: "Using a custom endpoint", title: "Connection details" },
|
|
69
|
+
render: Default.render,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Opened, for anyone auditing the expanded layout. */
|
|
73
|
+
export const Expanded: Story = {
|
|
74
|
+
args: { defaultOpen: true, changedCount: 3 },
|
|
75
|
+
render: Default.render,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Nothing has been touched, so there is nothing to summarise. */
|
|
79
|
+
export const Untouched: Story = {
|
|
80
|
+
args: { changedCount: 0 },
|
|
81
|
+
render: Default.render,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Its real home: the quiet tail of a settings dialog, under a `DialogSection`. */
|
|
85
|
+
export const InsideDialog: Story = {
|
|
86
|
+
render: () => (
|
|
87
|
+
<Dialog>
|
|
88
|
+
<DialogTrigger asChild>
|
|
89
|
+
<Button>Edit schedule</Button>
|
|
90
|
+
</DialogTrigger>
|
|
91
|
+
<DialogContent>
|
|
92
|
+
<DialogHeader>
|
|
93
|
+
<DialogTitle>Schedule settings</DialogTitle>
|
|
94
|
+
<DialogDescription>
|
|
95
|
+
When the pipeline runs, and what happens if it fails.
|
|
96
|
+
</DialogDescription>
|
|
97
|
+
</DialogHeader>
|
|
98
|
+
<DialogSection title="Cadence" description="Times are in the workspace time zone.">
|
|
99
|
+
<Field id="dlg-cron" label="Run at" defaultValue="02:00" />
|
|
100
|
+
</DialogSection>
|
|
101
|
+
<AdvancedGroup changedCount={2}>
|
|
102
|
+
<Field id="dlg-retries" label="Max retries" defaultValue="5" />
|
|
103
|
+
<Field id="dlg-timeout" label="Timeout (seconds)" defaultValue="120" />
|
|
104
|
+
</AdvancedGroup>
|
|
105
|
+
<DialogFooter>
|
|
106
|
+
<DialogClose asChild>
|
|
107
|
+
<Button variant="outline">Cancel</Button>
|
|
108
|
+
</DialogClose>
|
|
109
|
+
<Button>Save schedule</Button>
|
|
110
|
+
</DialogFooter>
|
|
111
|
+
</DialogContent>
|
|
112
|
+
</Dialog>
|
|
113
|
+
),
|
|
114
|
+
play: async ({ canvas, canvasElement, userEvent }) => {
|
|
115
|
+
await userEvent.click(canvas.getByRole("button", { name: /edit schedule/i }));
|
|
116
|
+
const doc = within(canvasElement.ownerDocument.body);
|
|
117
|
+
const dialog = await doc.findByRole("dialog");
|
|
118
|
+
const trigger = within(dialog).getByRole("button", { name: /advanced/i });
|
|
119
|
+
await waitFor(() => expect(within(dialog).getByText("2 changed")).toBeVisible());
|
|
120
|
+
await userEvent.click(trigger);
|
|
121
|
+
await waitFor(() => expect(within(dialog).getByLabelText("Max retries")).toBeVisible());
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const DefaultDark: Story = {
|
|
126
|
+
name: "Default — dark",
|
|
127
|
+
args: { changedCount: 3 },
|
|
128
|
+
decorators: [
|
|
129
|
+
(Story) => (
|
|
130
|
+
<ThemeProvider defaultTheme="dark" storageKey={null}>
|
|
131
|
+
<Story />
|
|
132
|
+
</ThemeProvider>
|
|
133
|
+
),
|
|
134
|
+
],
|
|
135
|
+
render: Default.render,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const DefaultHighDecoration: Story = {
|
|
139
|
+
name: "Default — high decoration",
|
|
140
|
+
args: { changedCount: 3 },
|
|
141
|
+
globals: { decoration: "10" },
|
|
142
|
+
render: Default.render,
|
|
143
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { AdvancedGroup } from "./advanced-group";
|
|
5
|
+
|
|
6
|
+
describe("AdvancedGroup", () => {
|
|
7
|
+
it("is collapsed by default and hides its content", () => {
|
|
8
|
+
render(
|
|
9
|
+
<AdvancedGroup>
|
|
10
|
+
<p>Retry budget</p>
|
|
11
|
+
</AdvancedGroup>,
|
|
12
|
+
);
|
|
13
|
+
const trigger = screen.getByRole("button", { name: /advanced/i });
|
|
14
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
15
|
+
expect(screen.queryByText("Retry budget")).not.toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("summarises non-default values while collapsed, then drops the summary", async () => {
|
|
19
|
+
const user = userEvent.setup();
|
|
20
|
+
render(
|
|
21
|
+
<AdvancedGroup changedCount={3}>
|
|
22
|
+
<p>Retry budget</p>
|
|
23
|
+
</AdvancedGroup>,
|
|
24
|
+
);
|
|
25
|
+
expect(screen.getByText("3 changed")).toBeInTheDocument();
|
|
26
|
+
|
|
27
|
+
await user.click(screen.getByRole("button", { name: /advanced/i }));
|
|
28
|
+
|
|
29
|
+
expect(screen.getByText("Retry budget")).toBeInTheDocument();
|
|
30
|
+
expect(screen.queryByText("3 changed")).not.toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("shows no summary when nothing changed", () => {
|
|
34
|
+
render(<AdvancedGroup changedCount={0}>content</AdvancedGroup>);
|
|
35
|
+
expect(screen.queryByText(/changed/)).not.toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("prefers an explicit summary node over changedCount", () => {
|
|
39
|
+
render(
|
|
40
|
+
<AdvancedGroup changedCount={3} summary="Using a custom endpoint">
|
|
41
|
+
content
|
|
42
|
+
</AdvancedGroup>,
|
|
43
|
+
);
|
|
44
|
+
expect(screen.getByText("Using a custom endpoint")).toBeInTheDocument();
|
|
45
|
+
expect(screen.queryByText("3 changed")).not.toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("honours defaultOpen", () => {
|
|
49
|
+
render(<AdvancedGroup defaultOpen>Retry budget</AdvancedGroup>);
|
|
50
|
+
expect(screen.getByRole("button", { name: /advanced/i })).toHaveAttribute(
|
|
51
|
+
"aria-expanded",
|
|
52
|
+
"true",
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("stays controlled: a click reports out but never self-toggles", async () => {
|
|
57
|
+
const user = userEvent.setup();
|
|
58
|
+
const onOpenChange = vi.fn();
|
|
59
|
+
render(
|
|
60
|
+
<AdvancedGroup open={false} onOpenChange={onOpenChange} changedCount={2}>
|
|
61
|
+
<p>Retry budget</p>
|
|
62
|
+
</AdvancedGroup>,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
await user.click(screen.getByRole("button", { name: /advanced/i }));
|
|
66
|
+
|
|
67
|
+
expect(onOpenChange).toHaveBeenCalledWith(true);
|
|
68
|
+
// The owner did not move `open`, so the group must not have opened itself —
|
|
69
|
+
// a controlled component never flips to uncontrolled.
|
|
70
|
+
expect(screen.getByRole("button", { name: /advanced/i })).toHaveAttribute(
|
|
71
|
+
"aria-expanded",
|
|
72
|
+
"false",
|
|
73
|
+
);
|
|
74
|
+
expect(screen.getByText("2 changed")).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("takes a custom title", () => {
|
|
78
|
+
render(<AdvancedGroup title="Developer options">content</AdvancedGroup>);
|
|
79
|
+
expect(screen.getByRole("button", { name: /developer options/i })).toBeInTheDocument();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState, type HTMLAttributes, type ReactNode } from "react";
|
|
4
|
+
import { ChevronDown } from "lucide-react";
|
|
5
|
+
import { cn } from "../../lib/cn";
|
|
6
|
+
import { Badge } from "../badge";
|
|
7
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../collapsible";
|
|
8
|
+
import { useLocale } from "../locale-provider";
|
|
9
|
+
|
|
10
|
+
export interface AdvancedGroupProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
11
|
+
/** The disclosure's label. Defaults to "Advanced". */
|
|
12
|
+
title?: ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* What is hidden inside, shown ONLY while collapsed — so a reader can tell
|
|
15
|
+
* whether the group holds defaults or edits without opening it.
|
|
16
|
+
*/
|
|
17
|
+
summary?: ReactNode;
|
|
18
|
+
/** Shorthand summary: renders "N changed" when `summary` is not given. */
|
|
19
|
+
changedCount?: number;
|
|
20
|
+
/** Uncontrolled initial state. Collapsed by default — that is the point. */
|
|
21
|
+
defaultOpen?: boolean;
|
|
22
|
+
/** Controlled open state. */
|
|
23
|
+
open?: boolean;
|
|
24
|
+
/** Fires on every open/close, controlled or not. */
|
|
25
|
+
onOpenChange?: (open: boolean) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A collapsed-by-default group of secondary settings (#341).
|
|
30
|
+
*
|
|
31
|
+
* Long dialogs go wrong in one of two ways: everything is visible (so nothing
|
|
32
|
+
* reads as primary), or the extras are hidden with no hint that they were
|
|
33
|
+
* touched. `AdvancedGroup` takes the third option — hidden, but SUMMARISED:
|
|
34
|
+
* while collapsed it shows what is inside ("3 changed"), and the summary
|
|
35
|
+
* disappears once the group is open and the values speak for themselves.
|
|
36
|
+
*
|
|
37
|
+
* A preset over `Collapsible`, not a new disclosure primitive: Radix keeps the
|
|
38
|
+
* trigger/content wiring, `aria-expanded`, and keyboard operation.
|
|
39
|
+
*
|
|
40
|
+
* ```tsx
|
|
41
|
+
* <AdvancedGroup changedCount={3}>
|
|
42
|
+
* <Field … />
|
|
43
|
+
* </AdvancedGroup>
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function AdvancedGroup({
|
|
47
|
+
className,
|
|
48
|
+
title,
|
|
49
|
+
summary,
|
|
50
|
+
changedCount,
|
|
51
|
+
defaultOpen = false,
|
|
52
|
+
open: openProp,
|
|
53
|
+
onOpenChange,
|
|
54
|
+
children,
|
|
55
|
+
...props
|
|
56
|
+
}: AdvancedGroupProps) {
|
|
57
|
+
const { t } = useLocale();
|
|
58
|
+
// Mirror the platform: the mode is fixed at mount by whether `open` was
|
|
59
|
+
// passed, and never flips (`.claude/rules/component-api.md`).
|
|
60
|
+
const isControlled = openProp !== undefined;
|
|
61
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
62
|
+
const open = isControlled ? openProp : uncontrolledOpen;
|
|
63
|
+
|
|
64
|
+
const handleOpenChange = useCallback(
|
|
65
|
+
(next: boolean) => {
|
|
66
|
+
if (!isControlled) setUncontrolledOpen(next);
|
|
67
|
+
onOpenChange?.(next);
|
|
68
|
+
},
|
|
69
|
+
[isControlled, onOpenChange],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const summaryContent =
|
|
73
|
+
summary ??
|
|
74
|
+
(changedCount !== undefined && changedCount > 0 ? (
|
|
75
|
+
<Badge variant="secondary">{t("ui.advancedGroup.changed", { count: changedCount })}</Badge>
|
|
76
|
+
) : null);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Collapsible
|
|
80
|
+
data-slot="advanced-group"
|
|
81
|
+
open={open}
|
|
82
|
+
onOpenChange={handleOpenChange}
|
|
83
|
+
className={cn("flex flex-col", className)}
|
|
84
|
+
{...props}
|
|
85
|
+
>
|
|
86
|
+
<CollapsibleTrigger
|
|
87
|
+
data-slot="advanced-group-trigger"
|
|
88
|
+
className="group -mx-2 flex items-center gap-2 rounded-md px-2 py-1.5 text-body font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
89
|
+
>
|
|
90
|
+
<ChevronDown
|
|
91
|
+
aria-hidden="true"
|
|
92
|
+
className="size-4 shrink-0 text-muted-foreground transition-transform duration-base ease-standard motion-reduce:transition-none group-data-[state=closed]:-rotate-90"
|
|
93
|
+
/>
|
|
94
|
+
{title ?? t("ui.advancedGroup.title")}
|
|
95
|
+
{/* The summary is a COLLAPSED-state affordance: once the group is open
|
|
96
|
+
the real values are on screen and repeating "3 changed" is noise. */}
|
|
97
|
+
{!open && summaryContent ? (
|
|
98
|
+
<span
|
|
99
|
+
data-slot="advanced-group-summary"
|
|
100
|
+
className="ms-auto text-caption font-normal text-muted-foreground"
|
|
101
|
+
>
|
|
102
|
+
{summaryContent}
|
|
103
|
+
</span>
|
|
104
|
+
) : null}
|
|
105
|
+
</CollapsibleTrigger>
|
|
106
|
+
<CollapsibleContent data-slot="advanced-group-content">
|
|
107
|
+
<div className="flex flex-col gap-4 pt-3">{children}</div>
|
|
108
|
+
</CollapsibleContent>
|
|
109
|
+
</Collapsible>
|
|
110
|
+
);
|
|
111
|
+
}
|