@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,262 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import { FilterChip, ResultCount, ViewToolbar, ViewToolbarFilters } from "./view-toolbar";
|
|
6
|
+
|
|
7
|
+
describe("ViewToolbar", () => {
|
|
8
|
+
it("renders the row shell with stable data-slot seams", () => {
|
|
9
|
+
const { container } = render(
|
|
10
|
+
<ViewToolbar actions={<button type="button">New run</button>}>
|
|
11
|
+
<span>Left</span>
|
|
12
|
+
</ViewToolbar>,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
expect(container.querySelector("[data-slot='view-toolbar']")).not.toBeNull();
|
|
16
|
+
expect(container.querySelector("[data-slot='view-toolbar-content']")).not.toBeNull();
|
|
17
|
+
expect(container.querySelector("[data-slot='view-toolbar-actions']")).not.toBeNull();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("forwards its ref, merges className and spreads props onto the root", () => {
|
|
21
|
+
const ref = createRef<HTMLDivElement>();
|
|
22
|
+
render(<ViewToolbar ref={ref} className="custom-row" id="runs-toolbar" data-testid="row" />);
|
|
23
|
+
|
|
24
|
+
const root = screen.getByTestId("row");
|
|
25
|
+
expect(ref.current).toBe(root);
|
|
26
|
+
expect(root).toHaveAttribute("id", "runs-toolbar");
|
|
27
|
+
expect(root.className).toContain("custom-row");
|
|
28
|
+
// The row wraps rather than clipping — the narrow-width contract.
|
|
29
|
+
expect(root.className).toContain("flex-wrap");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("omits the actions cluster when no actions are supplied", () => {
|
|
33
|
+
const { container } = render(<ViewToolbar>Left</ViewToolbar>);
|
|
34
|
+
expect(container.querySelector("[data-slot='view-toolbar-actions']")).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("renders the info affordance only when `info` is supplied", async () => {
|
|
38
|
+
const { rerender } = render(<ViewToolbar>Left</ViewToolbar>);
|
|
39
|
+
expect(screen.queryByRole("button", { name: "About this view" })).toBeNull();
|
|
40
|
+
|
|
41
|
+
rerender(<ViewToolbar info="Everything that ran in the last 30 days.">Left</ViewToolbar>);
|
|
42
|
+
expect(screen.getByRole("button", { name: "About this view" })).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The narrow-width AC in DOM terms: nothing between the action and the row may
|
|
47
|
+
* carry `overflow: hidden`, which is what would put the action cluster out of
|
|
48
|
+
* reach once the row wraps. (jsdom has no layout, so the geometric half of the
|
|
49
|
+
* assertion lives in the `Overflowing` story's play function, which runs in a
|
|
50
|
+
* real browser.)
|
|
51
|
+
*/
|
|
52
|
+
it("never clips the action cluster out of the row", () => {
|
|
53
|
+
const { container } = render(
|
|
54
|
+
<ViewToolbar
|
|
55
|
+
info="Runs"
|
|
56
|
+
actions={
|
|
57
|
+
<button type="button" data-testid="primary-action">
|
|
58
|
+
New run
|
|
59
|
+
</button>
|
|
60
|
+
}
|
|
61
|
+
>
|
|
62
|
+
<ViewToolbarFilters>
|
|
63
|
+
<FilterChip label="Status: Failed" onRemove={() => undefined} />
|
|
64
|
+
</ViewToolbarFilters>
|
|
65
|
+
<ResultCount count={3} total={128} />
|
|
66
|
+
</ViewToolbar>,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const root = container.querySelector("[data-slot='view-toolbar']") as HTMLElement;
|
|
70
|
+
const action = screen.getByTestId("primary-action");
|
|
71
|
+
|
|
72
|
+
for (let node: HTMLElement | null = action; node && node !== root; node = node.parentElement) {
|
|
73
|
+
expect(node.className).not.toMatch(/\boverflow-hidden\b/);
|
|
74
|
+
}
|
|
75
|
+
expect(root.className).not.toMatch(/\boverflow-hidden\b/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The other half of the narrow-width AC, and the reason the DOM assertion
|
|
80
|
+
* above is not enough: a `shrink-0` action cluster whose buttons cannot wrap
|
|
81
|
+
* does not get clipped — it OVERFLOWS the row, escaping the row's box by
|
|
82
|
+
* ~8px at a 320px viewport. The cluster must be able to shrink AND wrap.
|
|
83
|
+
* (The geometric proof runs in a real browser — see the `Overflowing`
|
|
84
|
+
* story's play function; jsdom has no layout.)
|
|
85
|
+
*/
|
|
86
|
+
it("lets the action cluster wrap instead of overflowing the row", () => {
|
|
87
|
+
const { container } = render(
|
|
88
|
+
<ViewToolbar actions={<button type="button">New run</button>}>Left</ViewToolbar>,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const actions = container.querySelector("[data-slot='view-toolbar-actions']") as HTMLElement;
|
|
92
|
+
expect(actions.className).toMatch(/\bflex-wrap\b/);
|
|
93
|
+
expect(actions.className).not.toMatch(/\bshrink-0\b/);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("ViewToolbarFilters", () => {
|
|
98
|
+
it("names the chip run as a group so it is findable in assistive tech", () => {
|
|
99
|
+
render(
|
|
100
|
+
<ViewToolbarFilters>
|
|
101
|
+
<FilterChip label="Status: Failed" onRemove={() => undefined} />
|
|
102
|
+
</ViewToolbarFilters>,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(screen.getByRole("group", { name: "Active filters" })).toBeInTheDocument();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("renders 'Clear all' only when onClearAll is supplied, and calls it", async () => {
|
|
109
|
+
const onClearAll = vi.fn();
|
|
110
|
+
const { rerender } = render(
|
|
111
|
+
<ViewToolbarFilters>
|
|
112
|
+
<FilterChip label="Status: Failed" onRemove={() => undefined} />
|
|
113
|
+
</ViewToolbarFilters>,
|
|
114
|
+
);
|
|
115
|
+
expect(screen.queryByRole("button", { name: "Clear all" })).toBeNull();
|
|
116
|
+
|
|
117
|
+
rerender(
|
|
118
|
+
<ViewToolbarFilters onClearAll={onClearAll}>
|
|
119
|
+
<FilterChip label="Status: Failed" onRemove={() => undefined} />
|
|
120
|
+
</ViewToolbarFilters>,
|
|
121
|
+
);
|
|
122
|
+
await userEvent.click(screen.getByRole("button", { name: "Clear all" }));
|
|
123
|
+
expect(onClearAll).toHaveBeenCalledTimes(1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("lets a caller override the group's accessible name", () => {
|
|
127
|
+
render(
|
|
128
|
+
<ViewToolbarFilters aria-label="Active run filters">
|
|
129
|
+
<FilterChip label="Status: Failed" onRemove={() => undefined} />
|
|
130
|
+
</ViewToolbarFilters>,
|
|
131
|
+
);
|
|
132
|
+
expect(screen.getByRole("group", { name: "Active run filters" })).toBeInTheDocument();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("FilterChip", () => {
|
|
137
|
+
it("renders label-in-value text verbatim", () => {
|
|
138
|
+
render(<FilterChip label="Status: Failed" onRemove={() => undefined} />);
|
|
139
|
+
expect(screen.getByText("Status: Failed")).toBeInTheDocument();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("is a real button whose accessible name contains the visible label", () => {
|
|
143
|
+
render(<FilterChip label="Status: Failed" onRemove={() => undefined} />);
|
|
144
|
+
const chip = screen.getByRole("button", { name: "Remove filter: Status: Failed" });
|
|
145
|
+
expect(chip.tagName).toBe("BUTTON");
|
|
146
|
+
// WCAG 2.5.3 Label in Name.
|
|
147
|
+
expect(chip).toHaveAccessibleName(expect.stringContaining("Status: Failed"));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("calls onRemove on click", async () => {
|
|
151
|
+
const onRemove = vi.fn();
|
|
152
|
+
render(<FilterChip label="Status: Failed" onRemove={onRemove} />);
|
|
153
|
+
await userEvent.click(screen.getByRole("button", { name: "Remove filter: Status: Failed" }));
|
|
154
|
+
expect(onRemove).toHaveBeenCalledTimes(1);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("calls onRemove on Enter", async () => {
|
|
158
|
+
const onRemove = vi.fn();
|
|
159
|
+
render(<FilterChip label="Status: Failed" onRemove={onRemove} />);
|
|
160
|
+
screen.getByRole("button", { name: "Remove filter: Status: Failed" }).focus();
|
|
161
|
+
await userEvent.keyboard("{Enter}");
|
|
162
|
+
expect(onRemove).toHaveBeenCalledTimes(1);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("calls onRemove on Space", async () => {
|
|
166
|
+
const onRemove = vi.fn();
|
|
167
|
+
render(<FilterChip label="Status: Failed" onRemove={onRemove} />);
|
|
168
|
+
screen.getByRole("button", { name: "Remove filter: Status: Failed" }).focus();
|
|
169
|
+
await userEvent.keyboard(" ");
|
|
170
|
+
expect(onRemove).toHaveBeenCalledTimes(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("is reachable by keyboard", async () => {
|
|
174
|
+
render(
|
|
175
|
+
<ViewToolbarFilters>
|
|
176
|
+
<FilterChip label="Status: Failed" onRemove={() => undefined} />
|
|
177
|
+
<FilterChip label="Owner: Data platform" onRemove={() => undefined} />
|
|
178
|
+
</ViewToolbarFilters>,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
await userEvent.tab();
|
|
182
|
+
expect(screen.getByRole("button", { name: "Remove filter: Status: Failed" })).toHaveFocus();
|
|
183
|
+
await userEvent.tab();
|
|
184
|
+
expect(
|
|
185
|
+
screen.getByRole("button", { name: "Remove filter: Owner: Data platform" }),
|
|
186
|
+
).toHaveFocus();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("lets a caller's onClick run first and cancel the removal", async () => {
|
|
190
|
+
const onRemove = vi.fn();
|
|
191
|
+
render(
|
|
192
|
+
<FilterChip
|
|
193
|
+
label="Status: Failed"
|
|
194
|
+
onRemove={onRemove}
|
|
195
|
+
onClick={(event) => event.preventDefault()}
|
|
196
|
+
/>,
|
|
197
|
+
);
|
|
198
|
+
await userEvent.click(screen.getByRole("button", { name: "Remove filter: Status: Failed" }));
|
|
199
|
+
expect(onRemove).not.toHaveBeenCalled();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("carries the view-toolbar-filter-chip slot and forwards its ref", () => {
|
|
203
|
+
const ref = createRef<HTMLButtonElement>();
|
|
204
|
+
const { container } = render(
|
|
205
|
+
<FilterChip ref={ref} label="Status: Failed" onRemove={() => undefined} />,
|
|
206
|
+
);
|
|
207
|
+
const chip = container.querySelector("[data-slot='view-toolbar-filter-chip']");
|
|
208
|
+
expect(chip).not.toBeNull();
|
|
209
|
+
expect(ref.current).toBe(chip);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("ResultCount", () => {
|
|
214
|
+
it("renders 'N of M' when both counts are supplied", () => {
|
|
215
|
+
const { container } = render(<ResultCount count={24} total={128} />);
|
|
216
|
+
expect(container.textContent).toBe("24 of 128");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("falls back to a bare count when no total is supplied", () => {
|
|
220
|
+
const { container } = render(<ResultCount count={128} />);
|
|
221
|
+
expect(container.textContent).toBe("128");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("appends an optional trailing noun", () => {
|
|
225
|
+
const { container } = render(
|
|
226
|
+
<ResultCount count={24} total={128}>
|
|
227
|
+
runs
|
|
228
|
+
</ResultCount>,
|
|
229
|
+
);
|
|
230
|
+
// Joined with a NON-BREAKING space (U+00A0, written as an escape so the
|
|
231
|
+
// assertion cannot be silently "fixed" into a plain space): the number must
|
|
232
|
+
// never wrap off its noun. See the micro-typography rule.
|
|
233
|
+
expect(container.textContent).toBe(`24 of 128\u00A0runs`);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("renders 0 honestly rather than hiding an emptied view", () => {
|
|
237
|
+
const { container } = render(<ResultCount count={0} total={128} />);
|
|
238
|
+
expect(container.textContent).toBe("0 of 128");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("uses tabular figures so counts do not jitter", () => {
|
|
242
|
+
const { container } = render(<ResultCount count={24} total={128} />);
|
|
243
|
+
const span = container.querySelector("[data-slot='view-toolbar-result-count']") as HTMLElement;
|
|
244
|
+
expect(span.className).toContain("tabular-nums");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("renders a placeholder — never a number — while loading", () => {
|
|
248
|
+
const { container } = render(<ResultCount count={0} total={128} loading />);
|
|
249
|
+
expect(container.textContent).not.toContain("0 of 128");
|
|
250
|
+
expect(screen.getByRole("status")).toBeInTheDocument();
|
|
251
|
+
expect(screen.getByText("Loading…")).toBeInTheDocument();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("forwards its ref and keeps the slot in both states", () => {
|
|
255
|
+
const ref = createRef<HTMLSpanElement>();
|
|
256
|
+
const { container, rerender } = render(<ResultCount ref={ref} count={24} total={128} />);
|
|
257
|
+
expect(ref.current).toBe(container.querySelector("[data-slot='view-toolbar-result-count']"));
|
|
258
|
+
|
|
259
|
+
rerender(<ResultCount ref={ref} count={24} total={128} loading />);
|
|
260
|
+
expect(ref.current).toBe(container.querySelector("[data-slot='view-toolbar-result-count']"));
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ViewToolbar — the GRAMMAR for the one row that sits above every list, table
|
|
5
|
+
* or board view (#331).
|
|
6
|
+
*
|
|
7
|
+
* The failure this fixes was ambiguity, not capability: with no named row
|
|
8
|
+
* contract, ~40 routes of one consuming app grew 6 filter-chip idioms, 5
|
|
9
|
+
* result-count renderings and ~11px of control-height scatter in a single filter
|
|
10
|
+
* row. So the deliverable is a small, closed vocabulary — not a configurable
|
|
11
|
+
* widget:
|
|
12
|
+
*
|
|
13
|
+
* ⓘ info │ ── left cluster: status · filters · count ── │ actions
|
|
14
|
+
*
|
|
15
|
+
* - `ViewToolbar` is the row shell (slots only — it renders no view semantics).
|
|
16
|
+
* - `ViewToolbarFilters` is the ONE home for active-filter chips + "clear all".
|
|
17
|
+
* - `FilterChip` is the ONE removable chip (label-in-value, whole chip removes).
|
|
18
|
+
* - `ResultCount` is the ONE count (`tabular-nums`, "N of M" when filtered).
|
|
19
|
+
*
|
|
20
|
+
* Deliberate non-decisions, so a reader doesn't mistake them for oversights:
|
|
21
|
+
*
|
|
22
|
+
* - **No `cva`.** None of the four parts has a second visual axis today, and the
|
|
23
|
+
* rules forbid dead variants. A sticky/bounded bar is a caller `className`
|
|
24
|
+
* recipe (documented in `Docs/View Toolbar Contract`), not a variant.
|
|
25
|
+
* - **No `role="toolbar"` on the root.** That role promises roving-tabindex
|
|
26
|
+
* arrow-key navigation, which this row does not implement; claiming it would
|
|
27
|
+
* mislead assistive tech. Every control is an ordinary tab stop instead, and
|
|
28
|
+
* the one region that benefits from a name — the filter set — carries
|
|
29
|
+
* `role="group"` + an accessible name.
|
|
30
|
+
* - **The row wraps; it never scrolls or clips.** A hard fixed height would put
|
|
31
|
+
* the action cluster out of reach at narrow widths (AC), so the row keeps a
|
|
32
|
+
* `min-h-9` floor (== `Button` default height, so single-line rows are
|
|
33
|
+
* consistent) and wraps beyond it. The action cluster wraps INTERNALLY too —
|
|
34
|
+
* a `shrink-0` cluster whose buttons cannot wrap simply overflows the row's
|
|
35
|
+
* box once the row is narrower than the buttons are wide (measured: it
|
|
36
|
+
* escaped by ~8px at a 320px viewport), which is the very thing the AC
|
|
37
|
+
* forbids. It stays right-aligned (`justify-end`) on every line.
|
|
38
|
+
*
|
|
39
|
+
* Relationship to neighbours: `PageShell`'s generic `header` slot is where this
|
|
40
|
+
* row goes on a page (#367's toolbar header variant should PLACE a
|
|
41
|
+
* `<ViewToolbar>`, not invent a second row concept). `@elabs-ai/components-data`'s `FilterBar`
|
|
42
|
+
* stays the DataTable-scoped toolbar render-prop row; it could later be
|
|
43
|
+
* recomposed on this shell, but that is a separate change.
|
|
44
|
+
*/
|
|
45
|
+
import { forwardRef, type ButtonHTMLAttributes, type HTMLAttributes, type ReactNode } from "react";
|
|
46
|
+
import { Info, X } from "lucide-react";
|
|
47
|
+
import { badgeVariants } from "../badge/badge";
|
|
48
|
+
import { Button } from "../button";
|
|
49
|
+
import { Skeleton } from "../skeleton";
|
|
50
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip";
|
|
51
|
+
import { useLocale } from "../locale-provider";
|
|
52
|
+
import { cn } from "../../lib/cn";
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// ViewToolbar (the row shell)
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
export interface ViewToolbarProps extends HTMLAttributes<HTMLDivElement> {
|
|
59
|
+
/**
|
|
60
|
+
* What this view is, as a tooltip on an ⓘ trigger at the start of the row —
|
|
61
|
+
* so a view does not need an in-page `<h1>` + paragraph block just to explain
|
|
62
|
+
* itself. Omit it when the surrounding page already says what the view is.
|
|
63
|
+
*
|
|
64
|
+
* **Pointer/keyboard only.** A Radix tooltip opens on hover and on keyboard
|
|
65
|
+
* focus but deliberately NOT on tap (and Radix suppresses the focus-open when
|
|
66
|
+
* focus arrived from a pointer), so on a touch device this text is
|
|
67
|
+
* unreachable. Treat `info` as progressive enhancement: never put something a
|
|
68
|
+
* phone user cannot do without in it. See `Docs/View Toolbar Contract`.
|
|
69
|
+
*/
|
|
70
|
+
info?: ReactNode;
|
|
71
|
+
/** Left cluster — status/context, `ViewToolbarFilters`, `ResultCount`. */
|
|
72
|
+
children?: ReactNode;
|
|
73
|
+
/** Right cluster — the view's actions. Stays reachable at every width. */
|
|
74
|
+
actions?: ReactNode;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const ViewToolbar = forwardRef<HTMLDivElement, ViewToolbarProps>(function ViewToolbar(
|
|
78
|
+
{ info, children, actions, className, ...props },
|
|
79
|
+
ref,
|
|
80
|
+
) {
|
|
81
|
+
const { t } = useLocale();
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
ref={ref}
|
|
86
|
+
data-slot="view-toolbar"
|
|
87
|
+
className={cn("flex min-h-9 w-full flex-wrap items-center gap-x-3 gap-y-2", className)}
|
|
88
|
+
{...props}
|
|
89
|
+
>
|
|
90
|
+
{info ? (
|
|
91
|
+
<TooltipProvider>
|
|
92
|
+
<Tooltip>
|
|
93
|
+
<TooltipTrigger asChild>
|
|
94
|
+
<Button
|
|
95
|
+
variant="ghost"
|
|
96
|
+
size="icon-sm"
|
|
97
|
+
data-slot="view-toolbar-info"
|
|
98
|
+
aria-label={t("ui.viewToolbar.about")}
|
|
99
|
+
className="shrink-0 text-muted-foreground hover:text-foreground"
|
|
100
|
+
>
|
|
101
|
+
<Info aria-hidden="true" />
|
|
102
|
+
</Button>
|
|
103
|
+
</TooltipTrigger>
|
|
104
|
+
<TooltipContent className="max-w-xs text-pretty">{info}</TooltipContent>
|
|
105
|
+
</Tooltip>
|
|
106
|
+
</TooltipProvider>
|
|
107
|
+
) : null}
|
|
108
|
+
|
|
109
|
+
{/*
|
|
110
|
+
* Always rendered, even when empty: it owns the free space (`flex-1`), so
|
|
111
|
+
* the actions cluster stays anchored to the row's end, and it is a stable
|
|
112
|
+
* `data-slot` seam for consumers/tests.
|
|
113
|
+
*
|
|
114
|
+
* `basis-64` when it HAS content is what makes the narrow-width degrade
|
|
115
|
+
* work: with a 0 basis the left cluster would be squeezed to a few pixels
|
|
116
|
+
* so the actions could stay on line 1, shredding the chips. With a 16rem
|
|
117
|
+
* basis the two clusters stack instead. An EMPTY cluster keeps the 0 basis
|
|
118
|
+
* so it never forces a pointless wrap.
|
|
119
|
+
*/}
|
|
120
|
+
<div
|
|
121
|
+
data-slot="view-toolbar-content"
|
|
122
|
+
className={cn(
|
|
123
|
+
"flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-2",
|
|
124
|
+
children ? "basis-64" : "basis-0",
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
{children}
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{actions ? (
|
|
131
|
+
<div
|
|
132
|
+
data-slot="view-toolbar-actions"
|
|
133
|
+
className="ms-auto flex min-w-0 flex-wrap items-center justify-end gap-2"
|
|
134
|
+
>
|
|
135
|
+
{actions}
|
|
136
|
+
</div>
|
|
137
|
+
) : null}
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// ViewToolbarFilters (the active-filter region)
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
export interface ViewToolbarFiltersProps extends HTMLAttributes<HTMLDivElement> {
|
|
147
|
+
/**
|
|
148
|
+
* Clear every active filter. Supplying it renders the "clear all" affordance
|
|
149
|
+
* at the end of the chip run; omit it when a view has no bulk reset.
|
|
150
|
+
*/
|
|
151
|
+
onClearAll?: () => void;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The chip run. Render it ONLY when at least one filter is active — the
|
|
156
|
+
* "no filters applied" state is the region's ABSENCE, not an empty box.
|
|
157
|
+
*
|
|
158
|
+
* Runs at chip height (24px) so the whole group reads as one control rung; the
|
|
159
|
+
* `actions` cluster runs at button height. That split is the fix for the
|
|
160
|
+
* control-height scatter #331 measured.
|
|
161
|
+
*/
|
|
162
|
+
export const ViewToolbarFilters = forwardRef<HTMLDivElement, ViewToolbarFiltersProps>(
|
|
163
|
+
function ViewToolbarFilters({ onClearAll, children, className, ...props }, ref) {
|
|
164
|
+
const { t } = useLocale();
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div
|
|
168
|
+
ref={ref}
|
|
169
|
+
data-slot="view-toolbar-filters"
|
|
170
|
+
role="group"
|
|
171
|
+
aria-label={t("ui.viewToolbar.activeFilters")}
|
|
172
|
+
className={cn("flex min-w-0 flex-wrap items-center gap-1.5", className)}
|
|
173
|
+
{...props}
|
|
174
|
+
>
|
|
175
|
+
{children}
|
|
176
|
+
{onClearAll ? (
|
|
177
|
+
<Button
|
|
178
|
+
variant="ghost"
|
|
179
|
+
size="sm"
|
|
180
|
+
data-slot="view-toolbar-clear-all"
|
|
181
|
+
className="h-6 px-2"
|
|
182
|
+
onClick={onClearAll}
|
|
183
|
+
>
|
|
184
|
+
{t("ui.viewToolbar.clearAll")}
|
|
185
|
+
</Button>
|
|
186
|
+
) : null}
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// FilterChip
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
export interface FilterChipProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
|
|
197
|
+
/**
|
|
198
|
+
* Label-in-value text — `"Status: Failed"`, never `"Status = failed"` and
|
|
199
|
+
* never a bare `"Failed"`. The caller composes the string so the chip reads
|
|
200
|
+
* as a sentence fragment out of context (and in a screen reader).
|
|
201
|
+
*/
|
|
202
|
+
label: string;
|
|
203
|
+
/** Remove this filter. The WHOLE chip is the remove control — see below. */
|
|
204
|
+
onRemove: () => void;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A removable active-filter chip.
|
|
209
|
+
*
|
|
210
|
+
* The whole chip is a single `<button>` whose action is "remove", rather than a
|
|
211
|
+
* span with a nested ✕: one tab stop instead of two, a ≥24px target (WCAG 2.5.8)
|
|
212
|
+
* with no dead zone, and Enter/Space for free from the native element. Its
|
|
213
|
+
* accessible name is "Remove filter: <label>", which CONTAINS the visible label
|
|
214
|
+
* (WCAG 2.5.3 Label in Name).
|
|
215
|
+
*
|
|
216
|
+
* A chip that must EDIT a filter rather than remove it is a different control —
|
|
217
|
+
* reach for `FacetFilter` (`@elabs-ai/components-data`), not a variant of this.
|
|
218
|
+
*/
|
|
219
|
+
export const FilterChip = forwardRef<HTMLButtonElement, FilterChipProps>(function FilterChip(
|
|
220
|
+
{ label, onRemove, className, onClick, ...props },
|
|
221
|
+
ref,
|
|
222
|
+
) {
|
|
223
|
+
const { t } = useLocale();
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<button
|
|
227
|
+
ref={ref}
|
|
228
|
+
type="button"
|
|
229
|
+
data-slot="view-toolbar-filter-chip"
|
|
230
|
+
aria-label={t("ui.viewToolbar.removeFilter", { label })}
|
|
231
|
+
className={cn(
|
|
232
|
+
badgeVariants({ variant: "secondary" }),
|
|
233
|
+
"group min-h-6 max-w-full gap-1 ps-2.5 pe-1.5",
|
|
234
|
+
"transition-colors duration-fast ease-standard hover:bg-secondary/70",
|
|
235
|
+
// Badge's own `focus:` ring is dead weight on a <span> but would fire on
|
|
236
|
+
// every MOUSE click here; neutralise it and keep the focus-visible ring.
|
|
237
|
+
"focus:ring-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
238
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
239
|
+
className,
|
|
240
|
+
)}
|
|
241
|
+
onClick={(event) => {
|
|
242
|
+
onClick?.(event);
|
|
243
|
+
if (!event.defaultPrevented) onRemove();
|
|
244
|
+
}}
|
|
245
|
+
{...props}
|
|
246
|
+
>
|
|
247
|
+
<span className="truncate">{label}</span>
|
|
248
|
+
<X
|
|
249
|
+
aria-hidden="true"
|
|
250
|
+
className="size-3 shrink-0 opacity-60 transition-opacity duration-fast ease-standard group-hover:opacity-100"
|
|
251
|
+
/>
|
|
252
|
+
</button>
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// ResultCount
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
export interface ResultCountProps extends HTMLAttributes<HTMLSpanElement> {
|
|
261
|
+
/** How many rows/cards the view is showing right now. */
|
|
262
|
+
count: number;
|
|
263
|
+
/**
|
|
264
|
+
* The unfiltered total. Supplying it renders "N of M" — the honest reading
|
|
265
|
+
* when a filter is active. Omit it for an unfiltered view's bare count.
|
|
266
|
+
*/
|
|
267
|
+
total?: number;
|
|
268
|
+
/**
|
|
269
|
+
* No count yet. Renders a layout-shaped placeholder instead of a `0` that
|
|
270
|
+
* lies while the data is in flight (see `.claude/rules/loading-states.md`).
|
|
271
|
+
*/
|
|
272
|
+
loading?: boolean;
|
|
273
|
+
/**
|
|
274
|
+
* Optional trailing noun ("results", "runs", "rows") joined with a
|
|
275
|
+
* non-breaking space, so the number is never announced bare.
|
|
276
|
+
*/
|
|
277
|
+
children?: ReactNode;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const ResultCount = forwardRef<HTMLSpanElement, ResultCountProps>(function ResultCount(
|
|
281
|
+
{ count, total, loading = false, children, className, ...props },
|
|
282
|
+
ref,
|
|
283
|
+
) {
|
|
284
|
+
const { t, formatNumber } = useLocale();
|
|
285
|
+
|
|
286
|
+
if (loading) {
|
|
287
|
+
return (
|
|
288
|
+
<span
|
|
289
|
+
ref={ref}
|
|
290
|
+
data-slot="view-toolbar-result-count"
|
|
291
|
+
role="status"
|
|
292
|
+
className={cn("inline-flex items-center", className)}
|
|
293
|
+
{...props}
|
|
294
|
+
>
|
|
295
|
+
<span className="sr-only">{t("loading")}</span>
|
|
296
|
+
<Skeleton className="h-3 w-16" />
|
|
297
|
+
</span>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const text =
|
|
302
|
+
total === undefined
|
|
303
|
+
? formatNumber(count)
|
|
304
|
+
: t("ui.viewToolbar.countOfTotal", {
|
|
305
|
+
count: formatNumber(count),
|
|
306
|
+
total: formatNumber(total),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<span
|
|
311
|
+
ref={ref}
|
|
312
|
+
data-slot="view-toolbar-result-count"
|
|
313
|
+
className={cn("text-caption tabular-nums text-muted-foreground", className)}
|
|
314
|
+
{...props}
|
|
315
|
+
>
|
|
316
|
+
{text}
|
|
317
|
+
{children ? <> {children}</> : null}
|
|
318
|
+
</span>
|
|
319
|
+
);
|
|
320
|
+
});
|