@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,140 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { render, screen, within } from "@testing-library/react";
|
|
4
|
+
import { Calendar } from "./calendar";
|
|
5
|
+
import { Default as DefaultCalendarStory } from "./calendar.stories";
|
|
6
|
+
|
|
7
|
+
/** Class list of an element, safe for SVG (whose `className` is not a string). */
|
|
8
|
+
function classes(el: Element) {
|
|
9
|
+
return (el.getAttribute("class") ?? "").split(/\s+/);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Walks up from `el` looking for a `relative` ancestor at or below `root`. */
|
|
13
|
+
function hasPositionedAncestorWithin(el: Element, root: Element) {
|
|
14
|
+
let node = el.parentElement;
|
|
15
|
+
while (node) {
|
|
16
|
+
if (classes(node).includes("relative")) return true;
|
|
17
|
+
if (node === root) return false;
|
|
18
|
+
node = node.parentElement;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("Calendar", () => {
|
|
24
|
+
it("renders a month grid", () => {
|
|
25
|
+
render(<Calendar mode="single" />);
|
|
26
|
+
expect(screen.getByRole("grid")).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("renders both month-navigation buttons", () => {
|
|
30
|
+
render(<Calendar mode="single" />);
|
|
31
|
+
expect(screen.getByRole("button", { name: /previous month/i })).toBeInTheDocument();
|
|
32
|
+
expect(screen.getByRole("button", { name: /next month/i })).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// react-day-picker v9 renders <nav> as a SIBLING of the month (not inside the
|
|
36
|
+
// caption, as v8 did). Any absolutely-positioned part must therefore be anchored
|
|
37
|
+
// by a `relative` ancestor INSIDE the calendar — otherwise it escapes to whatever
|
|
38
|
+
// positioned ancestor the page happens to have, and the nav buttons fly to the
|
|
39
|
+
// corners of the viewport / popover.
|
|
40
|
+
it("anchors every absolutely-positioned part inside the calendar", () => {
|
|
41
|
+
const { container } = render(<Calendar mode="single" />);
|
|
42
|
+
const root = container.firstElementChild as HTMLElement;
|
|
43
|
+
const absolute = [...root.querySelectorAll("*")].filter((el) =>
|
|
44
|
+
classes(el).includes("absolute"),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(absolute.length).toBeGreaterThan(0);
|
|
48
|
+
for (const el of absolute) {
|
|
49
|
+
expect(hasPositionedAncestorWithin(el, root)).toBe(true);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("keeps the navigation in the caption row, not in the page flow above it", () => {
|
|
54
|
+
const { container } = render(<Calendar mode="single" />);
|
|
55
|
+
const nav = container.querySelector("nav");
|
|
56
|
+
expect(nav).not.toBeNull();
|
|
57
|
+
// The nav overlays the caption row; the buttons themselves stay in normal
|
|
58
|
+
// flow inside it, so they can never be positioned against an outside ancestor.
|
|
59
|
+
expect(nav?.className).toContain("absolute");
|
|
60
|
+
for (const button of nav?.querySelectorAll("button") ?? []) {
|
|
61
|
+
expect(classes(button)).not.toContain("absolute");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// #430 — `forms-calendar--default` used to seed its selected day from
|
|
67
|
+
// `new Date()`, so the calendar cell axe scored for `color-contrast` had 1
|
|
68
|
+
// character on days 1-9 and 2 characters on days 10-31 of the *host machine's*
|
|
69
|
+
// calendar. axe-core's `color-contrast` rule reports single-character text as
|
|
70
|
+
// `incomplete` (`shortTextContent`) rather than scoring it, and `incomplete`
|
|
71
|
+
// results never fail `toHaveNoViolations()` — so the story was red on ~22 days
|
|
72
|
+
// a month and green on ~9, entirely independent of any code change.
|
|
73
|
+
//
|
|
74
|
+
// This lock renders the STORY's actual `Default.render()` output (not a
|
|
75
|
+
// hand-rolled `<Calendar>`) under two faked system clocks, so it fails against
|
|
76
|
+
// the pre-fix story (whose `selected` tracked `new Date()`) and passes once
|
|
77
|
+
// the story pins `selected`/`defaultMonth` instead. It proves the fixture is
|
|
78
|
+
// what it claims to be: (a) genuinely independent of the wall clock, and (b)
|
|
79
|
+
// pinned to a TWO-digit day specifically, so the a11y assertion stays capable
|
|
80
|
+
// of failing rather than being silently routed into axe's unscoreable-text
|
|
81
|
+
// path. The real, accepted 4.31:1 ink pair this cell renders
|
|
82
|
+
// (`--primary-foreground` on `--primary`, light) is `INK_EXEMPT` in
|
|
83
|
+
// `packages/tokens/src/themes-contrast.test.ts` (#180) — fixing that colour is
|
|
84
|
+
// explicitly out of scope here.
|
|
85
|
+
describe("Calendar — story fixture is clock-independent (#430)", () => {
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
vi.useRealTimers();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// CSF3 story `render` fns are typed `(args, context) => ReactNode`, but this
|
|
91
|
+
// one (like most in this repo) ignores both, calls `useState`, and reads only
|
|
92
|
+
// component state — i.e. it has the exact shape of a function component. It
|
|
93
|
+
// must be mounted as JSX (`<StoryComponent />`), not invoked directly:
|
|
94
|
+
// calling it as a plain function runs its hooks outside of React's render
|
|
95
|
+
// cycle ("Invalid hook call"), since Storybook itself always mounts `render`
|
|
96
|
+
// through the same component tree, never calls it as a helper.
|
|
97
|
+
const StoryComponent = DefaultCalendarStory.render as unknown as () => ReactElement;
|
|
98
|
+
|
|
99
|
+
function renderDefaultCalendarStory() {
|
|
100
|
+
const { container } = render(<StoryComponent />);
|
|
101
|
+
const scope = within(container);
|
|
102
|
+
const caption = scope.getByRole("status");
|
|
103
|
+
const selectedCell = container.querySelector('[role="gridcell"][aria-selected="true"]');
|
|
104
|
+
const selectedButton = selectedCell?.querySelector("button") ?? null;
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
captionText: caption.textContent,
|
|
108
|
+
selectedAriaLabel: selectedButton?.getAttribute("aria-label") ?? null,
|
|
109
|
+
selectedDayText: selectedButton?.textContent ?? "",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
it("renders identically whether the system clock reads a single- or a two-digit day", () => {
|
|
114
|
+
// Neither faked date is the pinned selected day (2026-08-17) itself — react-day-picker
|
|
115
|
+
// legitimately prefixes the selected cell's aria-label with "Today, " when the
|
|
116
|
+
// system clock's date happens to match it, which is correct "today" behavior, not
|
|
117
|
+
// the bug under test. Picking two dates that are never "today" isolates the thing
|
|
118
|
+
// this lock exists to catch: the rendered day/caption tracking the clock at all.
|
|
119
|
+
vi.setSystemTime(new Date(2026, 7, 5)); // single-digit "today" — must not matter
|
|
120
|
+
const fromSingleDigitToday = renderDefaultCalendarStory();
|
|
121
|
+
|
|
122
|
+
vi.setSystemTime(new Date(2026, 10, 10)); // two-digit "today", different month — must not matter either
|
|
123
|
+
const fromTwoDigitToday = renderDefaultCalendarStory();
|
|
124
|
+
|
|
125
|
+
expect(fromTwoDigitToday.captionText).toBe(fromSingleDigitToday.captionText);
|
|
126
|
+
expect(fromTwoDigitToday.selectedAriaLabel).toBe(fromSingleDigitToday.selectedAriaLabel);
|
|
127
|
+
expect(fromTwoDigitToday.selectedDayText).toBe(fromSingleDigitToday.selectedDayText);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// The load-bearing assertion: the check above alone would still pass if
|
|
131
|
+
// someone "fixed" the flake by pinning the selected day to the 5th instead
|
|
132
|
+
// of the 17th — two renders of a single-digit day also match each other.
|
|
133
|
+
// Only this digit-count check catches that regression.
|
|
134
|
+
it("pins the selected day's rendered text to 2+ digits (10-31), not 1", () => {
|
|
135
|
+
vi.setSystemTime(new Date(2026, 7, 5));
|
|
136
|
+
const { selectedDayText } = renderDefaultCalendarStory();
|
|
137
|
+
|
|
138
|
+
expect(selectedDayText.length).toBeGreaterThanOrEqual(2);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
2
|
+
import { DayPicker } from "react-day-picker";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { buttonVariants } from "../button";
|
|
5
|
+
|
|
6
|
+
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
|
7
|
+
|
|
8
|
+
/** Date picker calendar (react-day-picker v9), themed with brand tokens. */
|
|
9
|
+
export function Calendar({
|
|
10
|
+
className,
|
|
11
|
+
classNames,
|
|
12
|
+
showOutsideDays = true,
|
|
13
|
+
...props
|
|
14
|
+
}: CalendarProps) {
|
|
15
|
+
return (
|
|
16
|
+
<DayPicker
|
|
17
|
+
showOutsideDays={showOutsideDays}
|
|
18
|
+
className={cn("p-3", className)}
|
|
19
|
+
classNames={{
|
|
20
|
+
// `months` is the positioning context for the nav overlay below: in
|
|
21
|
+
// react-day-picker v9 the <nav> is a SIBLING of the month(s), not a child
|
|
22
|
+
// of the caption, so the caption cannot anchor it.
|
|
23
|
+
months: "relative flex flex-col gap-2 sm:flex-row",
|
|
24
|
+
month: "flex flex-col gap-4",
|
|
25
|
+
// `min-h-7` matches the nav button row so the two align; it can still grow
|
|
26
|
+
// for a taller caption (e.g. `captionLayout="dropdown"`), and `px-8` keeps
|
|
27
|
+
// the label clear of the buttons overlaying either edge.
|
|
28
|
+
month_caption: "flex min-h-7 items-center justify-center px-8",
|
|
29
|
+
caption_label: "text-sm font-medium",
|
|
30
|
+
// Overlays the caption row and pushes the buttons to either edge; the
|
|
31
|
+
// buttons themselves stay in normal flow inside it.
|
|
32
|
+
nav: "absolute inset-x-0 top-0 flex h-7 items-center justify-between",
|
|
33
|
+
button_previous: cn(
|
|
34
|
+
buttonVariants({ variant: "outline" }),
|
|
35
|
+
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
|
36
|
+
),
|
|
37
|
+
button_next: cn(
|
|
38
|
+
buttonVariants({ variant: "outline" }),
|
|
39
|
+
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
|
40
|
+
),
|
|
41
|
+
month_grid: "w-full border-collapse space-y-1",
|
|
42
|
+
weekdays: "flex",
|
|
43
|
+
weekday: "w-8 rounded-md text-[0.8rem] font-normal text-muted-foreground",
|
|
44
|
+
week: "mt-2 flex w-full",
|
|
45
|
+
day: "relative size-8 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-e-md first:[&:has([aria-selected])]:rounded-s-md last:[&:has([aria-selected])]:rounded-e-md",
|
|
46
|
+
day_button: cn(
|
|
47
|
+
buttonVariants({ variant: "ghost" }),
|
|
48
|
+
"size-8 p-0 font-normal aria-selected:opacity-100",
|
|
49
|
+
),
|
|
50
|
+
range_start: "day-range-start rounded-s-md",
|
|
51
|
+
range_end: "day-range-end rounded-e-md",
|
|
52
|
+
range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
|
|
53
|
+
selected:
|
|
54
|
+
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
|
55
|
+
today: "bg-accent text-accent-foreground",
|
|
56
|
+
outside: "day-outside text-muted-foreground aria-selected:text-muted-foreground",
|
|
57
|
+
disabled: "text-muted-foreground opacity-50",
|
|
58
|
+
hidden: "invisible",
|
|
59
|
+
...classNames,
|
|
60
|
+
}}
|
|
61
|
+
components={{
|
|
62
|
+
Chevron: ({ orientation }) => {
|
|
63
|
+
const Icon = orientation === "left" ? ChevronLeft : ChevronRight;
|
|
64
|
+
return <Icon className="size-4" data-rtl-flip />;
|
|
65
|
+
},
|
|
66
|
+
}}
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Calendar, type CalendarProps } from "./calendar";
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, userEvent } from "storybook/test";
|
|
3
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./card";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Core/Card",
|
|
7
|
+
component: Card,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
argTypes: {
|
|
10
|
+
interactive: {
|
|
11
|
+
description:
|
|
12
|
+
"Opt-in hover-lift for clickable cards (motion-tokened, respects reduced-motion).",
|
|
13
|
+
control: "boolean",
|
|
14
|
+
table: { category: "Behavior" },
|
|
15
|
+
},
|
|
16
|
+
detail: {
|
|
17
|
+
description: "Optional detail-panel content (ReactNode). Undefined = plain card.",
|
|
18
|
+
control: false,
|
|
19
|
+
table: { category: "Content" },
|
|
20
|
+
},
|
|
21
|
+
detailPlacement: {
|
|
22
|
+
description: "Which edge the detail panel sits on.",
|
|
23
|
+
control: { type: "radio" },
|
|
24
|
+
options: ["side", "bottom"],
|
|
25
|
+
table: { category: "Appearance" },
|
|
26
|
+
},
|
|
27
|
+
detailReveal: {
|
|
28
|
+
description: "`fixed` — always visible; `hover` — revealed on hover and focus-within.",
|
|
29
|
+
control: { type: "radio" },
|
|
30
|
+
options: ["fixed", "hover"],
|
|
31
|
+
table: { category: "Behavior" },
|
|
32
|
+
},
|
|
33
|
+
detailSize: {
|
|
34
|
+
description:
|
|
35
|
+
'CSS length for the detail track (e.g. `"16rem"`). Defaults to 16rem (side) / auto (bottom).',
|
|
36
|
+
control: "text",
|
|
37
|
+
table: { category: "Appearance" },
|
|
38
|
+
},
|
|
39
|
+
detailLabel: {
|
|
40
|
+
description: 'Accessible label for the detail region (aria-label). Default: "Details".',
|
|
41
|
+
control: "text",
|
|
42
|
+
table: { category: "Appearance" },
|
|
43
|
+
},
|
|
44
|
+
className: {
|
|
45
|
+
description: "Extra Tailwind classes merged via cn().",
|
|
46
|
+
control: "text",
|
|
47
|
+
table: { category: "Appearance" },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
} satisfies Meta<typeof Card>;
|
|
51
|
+
export default meta;
|
|
52
|
+
type Story = StoryObj<typeof meta>;
|
|
53
|
+
|
|
54
|
+
export const Default: Story = {
|
|
55
|
+
render: () => (
|
|
56
|
+
<Card className="max-w-sm">
|
|
57
|
+
<CardHeader>
|
|
58
|
+
<CardTitle>Monthly active users</CardTitle>
|
|
59
|
+
<CardDescription>Across all workspaces</CardDescription>
|
|
60
|
+
</CardHeader>
|
|
61
|
+
<CardContent>
|
|
62
|
+
<div className="text-display font-semibold">24,512</div>
|
|
63
|
+
</CardContent>
|
|
64
|
+
<CardFooter>
|
|
65
|
+
<span className="text-body text-muted-foreground">+12.4% vs last month</span>
|
|
66
|
+
</CardFooter>
|
|
67
|
+
</Card>
|
|
68
|
+
),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Opt-in hover-lift for clickable cards (gated by the motion system). */
|
|
72
|
+
export const Interactive: Story = {
|
|
73
|
+
render: () => (
|
|
74
|
+
<Card interactive className="max-w-sm" tabIndex={0} role="button">
|
|
75
|
+
<CardHeader>
|
|
76
|
+
<CardTitle>Production pipeline</CardTitle>
|
|
77
|
+
<CardDescription>Hover or focus — lifts over the gated duration</CardDescription>
|
|
78
|
+
</CardHeader>
|
|
79
|
+
<CardContent>
|
|
80
|
+
<span className="text-body text-muted-foreground">Last run 4m ago · 1.2M rows</span>
|
|
81
|
+
</CardContent>
|
|
82
|
+
</Card>
|
|
83
|
+
),
|
|
84
|
+
play: async ({ canvas }) => {
|
|
85
|
+
const card = canvas.getByRole("button");
|
|
86
|
+
const cs = getComputedStyle(card);
|
|
87
|
+
// The lift uses Tailwind v4's standalone `translate` property — the
|
|
88
|
+
// transition MUST list `translate` (not the `transform` shorthand) or it
|
|
89
|
+
// snaps instead of gliding. Guards against the v4 transform-split gotcha.
|
|
90
|
+
await expect(cs.transitionProperty).toContain("translate");
|
|
91
|
+
await expect(parseFloat(cs.transitionDuration)).toBeGreaterThanOrEqual(0.2); // gated --t-base
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Detail panel stories (#118 DP-01)
|
|
97
|
+
// Each story is wrapped in a sized parent so the grid layout has a defined
|
|
98
|
+
// height/width to work with (side panels need a concrete height).
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/** Side panel always visible (default detailReveal). */
|
|
102
|
+
export const DetailSideFixed: Story = {
|
|
103
|
+
render: () => (
|
|
104
|
+
<div className="h-64 w-[480px]">
|
|
105
|
+
<Card
|
|
106
|
+
className="h-full"
|
|
107
|
+
detail={
|
|
108
|
+
<div className="flex flex-col gap-2">
|
|
109
|
+
<p className="text-meta font-semibold text-muted-foreground uppercase tracking-wide">
|
|
110
|
+
Metadata
|
|
111
|
+
</p>
|
|
112
|
+
<ul className="text-body text-foreground space-y-1">
|
|
113
|
+
<li className="flex justify-between">
|
|
114
|
+
<span className="text-muted-foreground">Owner</span>
|
|
115
|
+
<span>analytics-team</span>
|
|
116
|
+
</li>
|
|
117
|
+
<li className="flex justify-between">
|
|
118
|
+
<span className="text-muted-foreground">Updated</span>
|
|
119
|
+
<span>2 days ago</span>
|
|
120
|
+
</li>
|
|
121
|
+
<li className="flex justify-between">
|
|
122
|
+
<span className="text-muted-foreground">Rows</span>
|
|
123
|
+
<span>1.2M</span>
|
|
124
|
+
</li>
|
|
125
|
+
</ul>
|
|
126
|
+
</div>
|
|
127
|
+
}
|
|
128
|
+
detailPlacement="side"
|
|
129
|
+
detailReveal="fixed"
|
|
130
|
+
>
|
|
131
|
+
<CardHeader>
|
|
132
|
+
<CardTitle>Sales pipeline</CardTitle>
|
|
133
|
+
<CardDescription>Q2 2026 opportunities</CardDescription>
|
|
134
|
+
</CardHeader>
|
|
135
|
+
<CardContent>
|
|
136
|
+
<div className="text-display font-semibold">$4.7M</div>
|
|
137
|
+
</CardContent>
|
|
138
|
+
</Card>
|
|
139
|
+
</div>
|
|
140
|
+
),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** Side panel revealed on hover / keyboard focus-within (outer card size stays constant). */
|
|
144
|
+
export const DetailSideHover: Story = {
|
|
145
|
+
render: () => (
|
|
146
|
+
<div className="h-64 w-[480px]">
|
|
147
|
+
<Card
|
|
148
|
+
className="h-full"
|
|
149
|
+
detail={
|
|
150
|
+
<div className="flex flex-col gap-2">
|
|
151
|
+
<p className="text-meta font-semibold text-muted-foreground uppercase tracking-wide">
|
|
152
|
+
Quick actions
|
|
153
|
+
</p>
|
|
154
|
+
<ul className="text-body space-y-1">
|
|
155
|
+
<li>
|
|
156
|
+
<button className="text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm px-1">
|
|
157
|
+
View report
|
|
158
|
+
</button>
|
|
159
|
+
</li>
|
|
160
|
+
<li>
|
|
161
|
+
<button className="text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm px-1">
|
|
162
|
+
Export CSV
|
|
163
|
+
</button>
|
|
164
|
+
</li>
|
|
165
|
+
<li>
|
|
166
|
+
<button className="text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm px-1">
|
|
167
|
+
Share
|
|
168
|
+
</button>
|
|
169
|
+
</li>
|
|
170
|
+
</ul>
|
|
171
|
+
</div>
|
|
172
|
+
}
|
|
173
|
+
detailPlacement="side"
|
|
174
|
+
detailReveal="hover"
|
|
175
|
+
>
|
|
176
|
+
<CardHeader>
|
|
177
|
+
<CardTitle>Revenue report</CardTitle>
|
|
178
|
+
<CardDescription>Hover or tab into the card to reveal actions</CardDescription>
|
|
179
|
+
</CardHeader>
|
|
180
|
+
<CardContent>
|
|
181
|
+
<div className="text-display font-semibold">$182K</div>
|
|
182
|
+
</CardContent>
|
|
183
|
+
</Card>
|
|
184
|
+
</div>
|
|
185
|
+
),
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/** Bottom panel always visible. */
|
|
189
|
+
export const DetailBottomFixed: Story = {
|
|
190
|
+
render: () => (
|
|
191
|
+
<div className="h-80 w-96">
|
|
192
|
+
<Card
|
|
193
|
+
className="h-full"
|
|
194
|
+
detail={
|
|
195
|
+
<div className="flex items-center gap-4">
|
|
196
|
+
<span className="text-body text-muted-foreground">Last synced 5 min ago</span>
|
|
197
|
+
<span className="ms-auto rounded-full bg-surface px-2 py-0.5 text-meta border border-border text-foreground">
|
|
198
|
+
Live
|
|
199
|
+
</span>
|
|
200
|
+
</div>
|
|
201
|
+
}
|
|
202
|
+
detailPlacement="bottom"
|
|
203
|
+
detailReveal="fixed"
|
|
204
|
+
detailSize="3.5rem"
|
|
205
|
+
>
|
|
206
|
+
<CardHeader>
|
|
207
|
+
<CardTitle>Active sessions</CardTitle>
|
|
208
|
+
<CardDescription>Across all regions</CardDescription>
|
|
209
|
+
</CardHeader>
|
|
210
|
+
<CardContent>
|
|
211
|
+
<div className="text-display font-semibold">8,340</div>
|
|
212
|
+
</CardContent>
|
|
213
|
+
</Card>
|
|
214
|
+
</div>
|
|
215
|
+
),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/** Bottom panel revealed on hover / keyboard focus-within. */
|
|
219
|
+
export const DetailBottomHover: Story = {
|
|
220
|
+
render: () => (
|
|
221
|
+
<div className="h-80 w-96">
|
|
222
|
+
<Card
|
|
223
|
+
className="h-full"
|
|
224
|
+
detail={
|
|
225
|
+
<div className="flex items-center gap-3">
|
|
226
|
+
<button className="text-body text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm px-1">
|
|
227
|
+
Drill down
|
|
228
|
+
</button>
|
|
229
|
+
<button className="text-body text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm px-1">
|
|
230
|
+
Download
|
|
231
|
+
</button>
|
|
232
|
+
</div>
|
|
233
|
+
}
|
|
234
|
+
detailPlacement="bottom"
|
|
235
|
+
detailReveal="hover"
|
|
236
|
+
detailSize="3rem"
|
|
237
|
+
>
|
|
238
|
+
<CardHeader>
|
|
239
|
+
<CardTitle>Ingest volume</CardTitle>
|
|
240
|
+
<CardDescription>Hover to reveal bottom actions</CardDescription>
|
|
241
|
+
</CardHeader>
|
|
242
|
+
<CardContent>
|
|
243
|
+
<div className="text-display font-semibold">94 GB/hr</div>
|
|
244
|
+
</CardContent>
|
|
245
|
+
</Card>
|
|
246
|
+
</div>
|
|
247
|
+
),
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/** Proves `interactive` (hover-lift) and `detail` panel coexist without conflict. */
|
|
251
|
+
export const InteractiveWithDetail: Story = {
|
|
252
|
+
render: () => (
|
|
253
|
+
<div className="h-64 w-[480px]">
|
|
254
|
+
<Card
|
|
255
|
+
className="h-full"
|
|
256
|
+
interactive
|
|
257
|
+
tabIndex={0}
|
|
258
|
+
role="button"
|
|
259
|
+
detail={
|
|
260
|
+
<div className="flex flex-col gap-2">
|
|
261
|
+
<p className="text-meta font-semibold text-muted-foreground uppercase tracking-wide">
|
|
262
|
+
Details
|
|
263
|
+
</p>
|
|
264
|
+
<ul className="text-body space-y-1">
|
|
265
|
+
<li className="flex justify-between">
|
|
266
|
+
<span className="text-muted-foreground">Status</span>
|
|
267
|
+
<span className="text-foreground">Healthy</span>
|
|
268
|
+
</li>
|
|
269
|
+
<li className="flex justify-between">
|
|
270
|
+
<span className="text-muted-foreground">SLA</span>
|
|
271
|
+
<span className="text-foreground">99.9%</span>
|
|
272
|
+
</li>
|
|
273
|
+
</ul>
|
|
274
|
+
</div>
|
|
275
|
+
}
|
|
276
|
+
detailPlacement="side"
|
|
277
|
+
detailReveal="fixed"
|
|
278
|
+
>
|
|
279
|
+
<CardHeader>
|
|
280
|
+
<CardTitle>Data pipeline</CardTitle>
|
|
281
|
+
<CardDescription>Interactive card with a fixed side panel</CardDescription>
|
|
282
|
+
</CardHeader>
|
|
283
|
+
<CardContent>
|
|
284
|
+
<span className="text-body text-muted-foreground">
|
|
285
|
+
Hover to lift · detail always visible
|
|
286
|
+
</span>
|
|
287
|
+
</CardContent>
|
|
288
|
+
</Card>
|
|
289
|
+
</div>
|
|
290
|
+
),
|
|
291
|
+
play: async ({ canvas }) => {
|
|
292
|
+
const card = canvas.getByRole("button");
|
|
293
|
+
// Both interactive lift AND detail panel are rendered — card is in the DOM.
|
|
294
|
+
await expect(card).toBeInTheDocument();
|
|
295
|
+
const cs = getComputedStyle(card);
|
|
296
|
+
await expect(cs.transitionProperty).toContain("translate");
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* `CardTitle` defaults to a `<div>` (unchanged) but accepts `as` to render a
|
|
302
|
+
* real heading (`h1`-`h6`) so a page built from cards contributes to the
|
|
303
|
+
* document outline (#328). Visual is identical across every tag — only the
|
|
304
|
+
* underlying element and its accessible heading level change.
|
|
305
|
+
*/
|
|
306
|
+
export const HeadingLevels: Story = {
|
|
307
|
+
render: () => (
|
|
308
|
+
<div className="flex max-w-sm flex-col gap-4">
|
|
309
|
+
<Card>
|
|
310
|
+
<CardHeader>
|
|
311
|
+
<CardTitle>Default — a plain div (no change)</CardTitle>
|
|
312
|
+
<CardDescription>Not reachable via heading navigation</CardDescription>
|
|
313
|
+
</CardHeader>
|
|
314
|
+
</Card>
|
|
315
|
+
<Card>
|
|
316
|
+
<CardHeader>
|
|
317
|
+
<CardTitle as="h2">as="h2" — a real heading</CardTitle>
|
|
318
|
+
<CardDescription>Reachable via AT heading navigation at level 2</CardDescription>
|
|
319
|
+
</CardHeader>
|
|
320
|
+
</Card>
|
|
321
|
+
</div>
|
|
322
|
+
),
|
|
323
|
+
play: async ({ canvas }) => {
|
|
324
|
+
// Default stays a div — no accessible heading for that title.
|
|
325
|
+
await expect(
|
|
326
|
+
canvas.queryByRole("heading", { name: /Default — a plain div/ }),
|
|
327
|
+
).not.toBeInTheDocument();
|
|
328
|
+
// `as="h2"` produces a real, level-2 heading.
|
|
329
|
+
const heading = canvas.getByRole("heading", { level: 2, name: /a real heading/ });
|
|
330
|
+
await expect(heading.tagName).toBe("H2");
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* `CardDescription` gains an opt-in `measure` prop that caps genuine prose to
|
|
336
|
+
* a readable line length (`max-w-prose`, ~65ch) instead of running edge to
|
|
337
|
+
* edge in a wide card (#339). Off by default — short subtitle-style
|
|
338
|
+
* descriptions stay full width.
|
|
339
|
+
*/
|
|
340
|
+
export const DescriptionMeasure: Story = {
|
|
341
|
+
render: () => (
|
|
342
|
+
<div className="flex w-[1000px] flex-col gap-6">
|
|
343
|
+
<Card className="w-full">
|
|
344
|
+
<CardHeader>
|
|
345
|
+
<CardTitle>Uncapped (default)</CardTitle>
|
|
346
|
+
<CardDescription>
|
|
347
|
+
This paragraph runs the full width of a very wide card, which makes it materially harder
|
|
348
|
+
to read once a line gets long enough — there is no natural place for the eye to return
|
|
349
|
+
to the start of the next line, so reading speed and comprehension both suffer on wide
|
|
350
|
+
viewports.
|
|
351
|
+
</CardDescription>
|
|
352
|
+
</CardHeader>
|
|
353
|
+
</Card>
|
|
354
|
+
<Card className="w-full">
|
|
355
|
+
<CardHeader>
|
|
356
|
+
<CardTitle>Capped via `measure`</CardTitle>
|
|
357
|
+
<CardDescription measure>
|
|
358
|
+
This paragraph is capped to a readable prose measure even inside a very wide card, so
|
|
359
|
+
each line stays short enough to read comfortably and the eye can find the start of the
|
|
360
|
+
next line without losing its place.
|
|
361
|
+
</CardDescription>
|
|
362
|
+
</CardHeader>
|
|
363
|
+
</Card>
|
|
364
|
+
</div>
|
|
365
|
+
),
|
|
366
|
+
play: async ({ canvas }) => {
|
|
367
|
+
const capped = canvas.getByText(/capped to a readable prose measure/);
|
|
368
|
+
await expect(capped).toHaveClass("max-w-prose");
|
|
369
|
+
const uncapped = canvas.getByText(/runs the full width/);
|
|
370
|
+
await expect(uncapped).not.toHaveClass("max-w-prose");
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Keyboard path: tab into a side+hover card and assert the detail's focusable
|
|
376
|
+
* control is reachable. The `focus-within` CSS selector reveals the panel; DOM
|
|
377
|
+
* assertions confirm the control is present and accessible (layout is opaque to
|
|
378
|
+
* jsdom so we verify focusability, not visual appearance).
|
|
379
|
+
*/
|
|
380
|
+
export const DetailHoverKeyboardReveal: Story = {
|
|
381
|
+
render: () => (
|
|
382
|
+
<div className="h-64 w-[480px]">
|
|
383
|
+
{/* Sentinel button before the card so Tab lands inside the card next */}
|
|
384
|
+
<button className="sr-only" data-testid="before-sentinel">
|
|
385
|
+
Before card
|
|
386
|
+
</button>
|
|
387
|
+
<Card
|
|
388
|
+
className="h-full"
|
|
389
|
+
detail={
|
|
390
|
+
<div className="flex flex-col gap-2 pt-1">
|
|
391
|
+
<button
|
|
392
|
+
data-testid="detail-action"
|
|
393
|
+
className="text-body text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm px-1 text-start"
|
|
394
|
+
>
|
|
395
|
+
Open details
|
|
396
|
+
</button>
|
|
397
|
+
</div>
|
|
398
|
+
}
|
|
399
|
+
detailPlacement="side"
|
|
400
|
+
detailReveal="hover"
|
|
401
|
+
>
|
|
402
|
+
<CardHeader>
|
|
403
|
+
<CardTitle>Keyboard reveal demo</CardTitle>
|
|
404
|
+
<CardDescription>Tab into the card — focus-within opens the panel</CardDescription>
|
|
405
|
+
</CardHeader>
|
|
406
|
+
<CardContent>
|
|
407
|
+
<span className="text-body text-muted-foreground">Press Tab to navigate in</span>
|
|
408
|
+
</CardContent>
|
|
409
|
+
</Card>
|
|
410
|
+
</div>
|
|
411
|
+
),
|
|
412
|
+
play: async ({ canvas }) => {
|
|
413
|
+
// The detail button stays in the DOM even when the panel is visually collapsed
|
|
414
|
+
// (overflow-hidden clips it; display:none is NOT used so AT reaches it).
|
|
415
|
+
const detailBtn = canvas.getByTestId("detail-action");
|
|
416
|
+
await expect(detailBtn).toBeInTheDocument();
|
|
417
|
+
|
|
418
|
+
// Drive the keyboard path: focus the sentinel, then Tab to move into the card.
|
|
419
|
+
const sentinel = canvas.getByTestId("before-sentinel");
|
|
420
|
+
sentinel.focus();
|
|
421
|
+
|
|
422
|
+
// Tab through focusable elements inside the card until we reach the detail button.
|
|
423
|
+
const user = userEvent.setup({ delay: null });
|
|
424
|
+
// Tab once to enter the card area (the first tabbable element inside).
|
|
425
|
+
await user.tab();
|
|
426
|
+
|
|
427
|
+
// The detail button is always in the DOM (clipped, not removed), so focus can
|
|
428
|
+
// reach it via Tab — assert it is focusable and present.
|
|
429
|
+
await expect(detailBtn).toBeInTheDocument();
|
|
430
|
+
// Tab until we land on the detail button (at most a few extra tabs).
|
|
431
|
+
let attempts = 0;
|
|
432
|
+
while (document.activeElement !== detailBtn && attempts < 6) {
|
|
433
|
+
await user.tab();
|
|
434
|
+
attempts++;
|
|
435
|
+
}
|
|
436
|
+
await expect(document.activeElement).toBe(detailBtn);
|
|
437
|
+
},
|
|
438
|
+
};
|