@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,132 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { Ban, ShieldAlert } from "lucide-react";
|
|
3
|
+
import { STATUSES, StatusBadge, StatusIcon } from "./status-badge";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Core/StatusBadge",
|
|
7
|
+
component: StatusBadge,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
args: { status: "complete" },
|
|
10
|
+
argTypes: {
|
|
11
|
+
status: {
|
|
12
|
+
description:
|
|
13
|
+
"The canonical 7-state execution status (closed enum), or a mapped " +
|
|
14
|
+
"{label, tone, icon?} object for a domain the 7 don't cover (#363) — " +
|
|
15
|
+
"see the CustomVocabulary story. The control below drives the canonical form only.",
|
|
16
|
+
control: { type: "select" },
|
|
17
|
+
options: [
|
|
18
|
+
"pending",
|
|
19
|
+
"running",
|
|
20
|
+
"complete",
|
|
21
|
+
"awaiting-approval",
|
|
22
|
+
"denied",
|
|
23
|
+
"failed",
|
|
24
|
+
"skipped",
|
|
25
|
+
],
|
|
26
|
+
table: { category: "State" },
|
|
27
|
+
},
|
|
28
|
+
size: {
|
|
29
|
+
description: "`md` for inline use; `sm` for rail nodes / dense rows.",
|
|
30
|
+
control: { type: "radio" },
|
|
31
|
+
options: ["sm", "md"],
|
|
32
|
+
table: { category: "Appearance" },
|
|
33
|
+
},
|
|
34
|
+
hideIcon: {
|
|
35
|
+
description: "Render label-only — omits the status icon.",
|
|
36
|
+
control: "boolean",
|
|
37
|
+
table: { category: "Appearance" },
|
|
38
|
+
},
|
|
39
|
+
children: {
|
|
40
|
+
description: "Override the default status label text.",
|
|
41
|
+
control: "text",
|
|
42
|
+
table: { category: "Content" },
|
|
43
|
+
},
|
|
44
|
+
className: {
|
|
45
|
+
description: "Extra Tailwind classes merged via cn().",
|
|
46
|
+
control: "text",
|
|
47
|
+
table: { category: "Appearance" },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
} satisfies Meta<typeof StatusBadge>;
|
|
51
|
+
export default meta;
|
|
52
|
+
type Story = StoryObj<typeof meta>;
|
|
53
|
+
|
|
54
|
+
export const Default: Story = {};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The canonical 7-state vocabulary. Calm states (pending, running, complete,
|
|
58
|
+
* denied, skipped) use the quiet wash/neutral treatment; only the two
|
|
59
|
+
* attention states (awaiting-approval, failed) get the solid fill.
|
|
60
|
+
*/
|
|
61
|
+
export const AllStatuses: Story = {
|
|
62
|
+
render: () => (
|
|
63
|
+
<div className="flex flex-wrap gap-2">
|
|
64
|
+
{STATUSES.map((status) => (
|
|
65
|
+
<StatusBadge key={status} status={status} />
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const Sizes: Story = {
|
|
72
|
+
render: () => (
|
|
73
|
+
<div className="flex items-center gap-2">
|
|
74
|
+
<StatusBadge size="sm" status="running" />
|
|
75
|
+
<StatusBadge size="md" status="running" />
|
|
76
|
+
</div>
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** `children` override the default label; counts are a typical use. */
|
|
81
|
+
export const CustomLabels: Story = {
|
|
82
|
+
render: () => (
|
|
83
|
+
<div className="flex flex-wrap gap-2">
|
|
84
|
+
<StatusBadge status="complete">12 passed</StatusBadge>
|
|
85
|
+
<StatusBadge status="failed">2 failed</StatusBadge>
|
|
86
|
+
<StatusBadge status="skipped">1 skipped</StatusBadge>
|
|
87
|
+
<StatusBadge hideIcon status="running">
|
|
88
|
+
Reconciling…
|
|
89
|
+
</StatusBadge>
|
|
90
|
+
</div>
|
|
91
|
+
),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The bounded out-of-vocabulary escape hatch (#363): a run engine whose real
|
|
96
|
+
* states the 7 canonical values don't cover (`aborted`, `stopped_guardrail`, a
|
|
97
|
+
* distinct "not run" dash state) maps them to `{label, tone, icon?}` — CALM
|
|
98
|
+
* treatments only, so the calm/loud contrast against the canonical set below
|
|
99
|
+
* stays meaningful (only `awaiting-approval`/`failed` ever get a solid fill).
|
|
100
|
+
*/
|
|
101
|
+
export const CustomVocabulary: Story = {
|
|
102
|
+
render: () => (
|
|
103
|
+
<div className="flex flex-wrap gap-2">
|
|
104
|
+
{STATUSES.map((status) => (
|
|
105
|
+
<StatusBadge key={status} status={status} />
|
|
106
|
+
))}
|
|
107
|
+
<StatusBadge status={{ label: "Aborted", tone: "neutral" }} />
|
|
108
|
+
<StatusBadge status={{ label: "Stopped (guardrail)", tone: "warning", icon: ShieldAlert }} />
|
|
109
|
+
<StatusBadge
|
|
110
|
+
status={{ label: "Not run", tone: "neutral", icon: Ban }}
|
|
111
|
+
className="border-dashed"
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The `StatusIcon` atom for tight rows (test results, rail nodes) — the
|
|
119
|
+
* adjacent row text must carry the status name for AT users.
|
|
120
|
+
*/
|
|
121
|
+
export const Icons: Story = {
|
|
122
|
+
render: () => (
|
|
123
|
+
<div className="flex flex-col gap-2">
|
|
124
|
+
{STATUSES.map((status) => (
|
|
125
|
+
<div key={status} className="flex items-center gap-2 text-body">
|
|
126
|
+
<StatusIcon status={status} />
|
|
127
|
+
<span>{status}</span>
|
|
128
|
+
</div>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
),
|
|
132
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { ShieldAlert } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
STATUSES,
|
|
6
|
+
STATUS_LABELS,
|
|
7
|
+
StatusBadge,
|
|
8
|
+
StatusIcon,
|
|
9
|
+
fromTimelineStatus,
|
|
10
|
+
isCustomStatus,
|
|
11
|
+
type Status,
|
|
12
|
+
} from "./status-badge";
|
|
13
|
+
|
|
14
|
+
describe("StatusBadge", () => {
|
|
15
|
+
it("renders the default label for every canonical status", () => {
|
|
16
|
+
render(
|
|
17
|
+
<div>
|
|
18
|
+
{STATUSES.map((status) => (
|
|
19
|
+
<StatusBadge key={status} status={status} />
|
|
20
|
+
))}
|
|
21
|
+
</div>,
|
|
22
|
+
);
|
|
23
|
+
for (const status of STATUSES) {
|
|
24
|
+
expect(screen.getByText(STATUS_LABELS[status])).toBeInTheDocument();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("children override the default label", () => {
|
|
29
|
+
render(<StatusBadge status="complete">12 passed</StatusBadge>);
|
|
30
|
+
expect(screen.getByText("12 passed")).toBeInTheDocument();
|
|
31
|
+
expect(screen.queryByText("Complete")).not.toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("hides the decorative icon from AT and exposes data-status", () => {
|
|
35
|
+
const { container } = render(<StatusBadge status="failed" />);
|
|
36
|
+
const badge = container.querySelector('[data-status="failed"]');
|
|
37
|
+
expect(badge).not.toBeNull();
|
|
38
|
+
const icon = badge?.querySelector("svg");
|
|
39
|
+
expect(icon).not.toBeNull();
|
|
40
|
+
expect(icon?.getAttribute("aria-hidden")).toBe("true");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("only the two attention states get the solid fill (hybrid visual)", () => {
|
|
44
|
+
const { container } = render(
|
|
45
|
+
<div>
|
|
46
|
+
{STATUSES.map((status) => (
|
|
47
|
+
<StatusBadge key={status} status={status} />
|
|
48
|
+
))}
|
|
49
|
+
</div>,
|
|
50
|
+
);
|
|
51
|
+
const classesFor = (status: string) =>
|
|
52
|
+
container.querySelector(`[data-status="${status}"]`)?.className ?? "";
|
|
53
|
+
expect(classesFor("awaiting-approval")).toContain("bg-warning");
|
|
54
|
+
expect(classesFor("failed")).toContain("bg-destructive");
|
|
55
|
+
// calm states: wash or neutral, never the solid status fill
|
|
56
|
+
expect(classesFor("complete")).toContain("bg-success/10");
|
|
57
|
+
expect(classesFor("running")).toContain("bg-info/10");
|
|
58
|
+
expect(classesFor("pending")).toContain("bg-secondary");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("running spins with a motion-reduce neutralizer", () => {
|
|
62
|
+
const { container } = render(<StatusBadge status="running" />);
|
|
63
|
+
const icon = container.querySelector("svg");
|
|
64
|
+
expect(icon?.getAttribute("class")).toContain("animate-spin");
|
|
65
|
+
expect(icon?.getAttribute("class")).toContain("motion-reduce:animate-none");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("merges className last and spreads props", () => {
|
|
69
|
+
const { container } = render(
|
|
70
|
+
<StatusBadge className="bg-muted" data-testid="badge" status="pending" />,
|
|
71
|
+
);
|
|
72
|
+
const badge = container.querySelector('[data-testid="badge"]');
|
|
73
|
+
expect(badge?.className).toContain("bg-muted");
|
|
74
|
+
expect(badge?.className).not.toContain("bg-secondary");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("StatusIcon", () => {
|
|
79
|
+
it("is decorative (aria-hidden) and status-colored", () => {
|
|
80
|
+
const { container } = render(<StatusIcon status="complete" />);
|
|
81
|
+
const icon = container.querySelector("svg");
|
|
82
|
+
expect(icon?.getAttribute("aria-hidden")).toBe("true");
|
|
83
|
+
expect(icon?.getAttribute("class")).toContain("text-success-text");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("fromTimelineStatus", () => {
|
|
88
|
+
it("maps the timeline 3-state vocabulary losslessly", () => {
|
|
89
|
+
expect(fromTimelineStatus("done")).toBe("complete");
|
|
90
|
+
expect(fromTimelineStatus("active")).toBe("running");
|
|
91
|
+
expect(fromTimelineStatus("pending")).toBe("pending");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Canonical class regression lock (#363) — the tone axis must never restyle a
|
|
97
|
+
// shipped calm/loud hybrid status.
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
describe("StatusBadge — canonical class regression lock (#363)", () => {
|
|
101
|
+
// Copied verbatim from statusBadgeVariants' `status` variant recipes.
|
|
102
|
+
const EXPECTED_STATUS_RECIPE: Record<Status, string> = {
|
|
103
|
+
pending: "border-transparent bg-secondary text-secondary-foreground",
|
|
104
|
+
running: "border-info/40 bg-info/10 text-info-text",
|
|
105
|
+
complete: "border-success/40 bg-success/10 text-success-text",
|
|
106
|
+
"awaiting-approval": "border-transparent bg-warning text-warning-foreground",
|
|
107
|
+
denied: "border-transparent bg-muted text-muted-foreground",
|
|
108
|
+
failed: "border-transparent bg-destructive text-destructive-foreground",
|
|
109
|
+
skipped: "border-transparent bg-secondary text-muted-foreground",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
it("every canonical status still renders its exact, unchanged recipe classes", () => {
|
|
113
|
+
const { container } = render(
|
|
114
|
+
<div>
|
|
115
|
+
{STATUSES.map((status) => (
|
|
116
|
+
<StatusBadge key={status} status={status} />
|
|
117
|
+
))}
|
|
118
|
+
</div>,
|
|
119
|
+
);
|
|
120
|
+
for (const status of STATUSES) {
|
|
121
|
+
const badge = container.querySelector(`[data-status="${status}"]`);
|
|
122
|
+
expect(badge).not.toBeNull();
|
|
123
|
+
for (const cls of EXPECTED_STATUS_RECIPE[status].split(" ")) {
|
|
124
|
+
expect(badge?.classList.contains(cls)).toBe(true);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Custom-status escape hatch (#363)
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
describe("StatusBadge — custom status escape hatch (#363)", () => {
|
|
135
|
+
it("renders a custom {label, tone, icon} status with the calm tone wash, not a canonical fill", () => {
|
|
136
|
+
render(
|
|
137
|
+
<StatusBadge status={{ label: "Stopped (guardrail)", tone: "warning", icon: ShieldAlert }} />,
|
|
138
|
+
);
|
|
139
|
+
const badge = screen.getByText("Stopped (guardrail)").closest("[data-slot='status-badge']");
|
|
140
|
+
expect(badge).not.toBeNull();
|
|
141
|
+
expect(badge).toHaveAttribute("data-status", "custom");
|
|
142
|
+
expect(badge).toHaveAttribute("data-tone", "warning");
|
|
143
|
+
// calm alpha-wash, matching the canonical `running`/`complete` recipe shape
|
|
144
|
+
expect(badge?.classList.contains("bg-warning/10")).toBe(true);
|
|
145
|
+
expect(badge?.classList.contains("text-warning-text")).toBe(true);
|
|
146
|
+
// NOT the solid `awaiting-approval` attention fill
|
|
147
|
+
expect(badge?.classList.contains("bg-warning")).toBe(false);
|
|
148
|
+
// the icon rendered and is decorative
|
|
149
|
+
const icon = badge?.querySelector("svg");
|
|
150
|
+
expect(icon).not.toBeNull();
|
|
151
|
+
expect(icon?.getAttribute("aria-hidden")).toBe("true");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("a custom status with no icon renders label-only", () => {
|
|
155
|
+
render(<StatusBadge status={{ label: "Aborted", tone: "neutral" }} />);
|
|
156
|
+
const badge = screen.getByText("Aborted").closest("[data-slot='status-badge']");
|
|
157
|
+
expect(badge?.querySelector("svg")).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("hideIcon suppresses the icon on a custom status that has one", () => {
|
|
161
|
+
render(
|
|
162
|
+
<StatusBadge status={{ label: "Stopped", tone: "warning", icon: ShieldAlert }} hideIcon />,
|
|
163
|
+
);
|
|
164
|
+
const badge = screen.getByText("Stopped").closest("[data-slot='status-badge']");
|
|
165
|
+
expect(badge?.querySelector("svg")).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("children override a custom status's label too", () => {
|
|
169
|
+
render(<StatusBadge status={{ label: "Aborted", tone: "neutral" }}>Custom text</StatusBadge>);
|
|
170
|
+
expect(screen.getByText("Custom text")).toBeInTheDocument();
|
|
171
|
+
expect(screen.queryByText("Aborted")).not.toBeInTheDocument();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("isCustomStatus narrows correctly for both branches (runtime)", () => {
|
|
175
|
+
expect(isCustomStatus("complete")).toBe(false);
|
|
176
|
+
expect(isCustomStatus({ label: "Aborted", tone: "neutral" })).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("TYPE LOCK: a canonical status cannot also carry a tone prop (integrity constraint 1)", () => {
|
|
180
|
+
// `tone` is reachable ONLY through the CustomStatus object form — never as
|
|
181
|
+
// an independent prop — so a canonical status can never be recolored.
|
|
182
|
+
const typeOnlyRecolorAttempt = () => (
|
|
183
|
+
// @ts-expect-error — `tone` is not a public StatusBadge prop; passing it
|
|
184
|
+
// alongside a canonical `status` string must fail to compile (#363).
|
|
185
|
+
<StatusBadge status="failed" tone="success" />
|
|
186
|
+
);
|
|
187
|
+
expect(typeof typeOnlyRecolorAttempt).toBe("function");
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusBadge — the canonical execution-status vocabulary (#189, research 10 §B.1).
|
|
3
|
+
*
|
|
4
|
+
* One CLOSED 7-state enum unifies the five status idioms across the agent
|
|
5
|
+
* trace components (`@elabs-ai/components-ai` Tool / ChainOfThought / Confirmation /
|
|
6
|
+
* TestResults / Sandbox), so one agent run reads as one author and an agent
|
|
7
|
+
* picks a status by MEANING — it cannot emit a color or a freeform string.
|
|
8
|
+
*
|
|
9
|
+
* Hybrid visual (research 08 "green deliberate / status sparingly"): the calm
|
|
10
|
+
* states — including `complete` — render the quiet `bg-<status>/10` alpha-wash
|
|
11
|
+
* or a neutral fill; ONLY the two attention states (`awaiting-approval`,
|
|
12
|
+
* `failed`) get the solid status fill, so a settled transcript stays calm and
|
|
13
|
+
* the states that need a human grab the eye.
|
|
14
|
+
*
|
|
15
|
+
* Builds ON the existing Badge cva (badge.tsx) — same pill geometry and
|
|
16
|
+
* typography; status colors are semantic tokens only.
|
|
17
|
+
*
|
|
18
|
+
* BOUNDED ESCAPE HATCH (#363): a domain that genuinely has a state the 7
|
|
19
|
+
* canonical values don't express (e.g. a run engine's `stopped_guardrail`) can
|
|
20
|
+
* pass a `CustomStatus` object instead of a `Status` string. Three integrity
|
|
21
|
+
* constraints keep it from eroding the anti-hallucination property above:
|
|
22
|
+
*
|
|
23
|
+
* 1. `tone` is reachable ONLY through the object form (which requires a
|
|
24
|
+
* `label`) — it is NOT an independent prop, so a canonical status can
|
|
25
|
+
* never be recolored (`status="failed" tone="success"` is unrepresentable).
|
|
26
|
+
* 2. The hatch is CALM-ONLY: all five tones render the quiet alpha-wash/
|
|
27
|
+
* neutral treatments. The solid attention fill stays exclusive to the
|
|
28
|
+
* canonical `awaiting-approval` and `failed` — no consumer can mint a
|
|
29
|
+
* loud chip, or the loud/calm signal degrades to noise.
|
|
30
|
+
* 3. No raw color, ever: `tone` maps to existing semantic tokens only
|
|
31
|
+
* (secondary / info / success / warning / destructive).
|
|
32
|
+
*
|
|
33
|
+
* Map a custom status ONCE, near your domain — not per call site. `StatusIcon`
|
|
34
|
+
* stays canonical-only (the enum's own icon+color atom); there is no `emphasis`/
|
|
35
|
+
* `loud` option on the hatch; `fromTimelineStatus()` is unchanged.
|
|
36
|
+
*/
|
|
37
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
38
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
39
|
+
import {
|
|
40
|
+
AlertCircle,
|
|
41
|
+
CheckCircle2,
|
|
42
|
+
Circle,
|
|
43
|
+
Clock,
|
|
44
|
+
Loader2,
|
|
45
|
+
MinusCircle,
|
|
46
|
+
XCircle,
|
|
47
|
+
type LucideIcon,
|
|
48
|
+
type LucideProps,
|
|
49
|
+
} from "lucide-react";
|
|
50
|
+
import { badgeVariants } from "../badge/badge";
|
|
51
|
+
import { cn } from "../../lib/cn";
|
|
52
|
+
|
|
53
|
+
/** The canonical, closed 7-state status enum (research 10 §B.1). */
|
|
54
|
+
export const STATUSES = [
|
|
55
|
+
"pending",
|
|
56
|
+
"running",
|
|
57
|
+
"complete",
|
|
58
|
+
"awaiting-approval",
|
|
59
|
+
"denied",
|
|
60
|
+
"failed",
|
|
61
|
+
"skipped",
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
export type Status = (typeof STATUSES)[number];
|
|
65
|
+
|
|
66
|
+
/** Closed tone set for the out-of-vocabulary escape hatch (CALM treatments only). */
|
|
67
|
+
export const STATUS_TONES = ["neutral", "info", "success", "warning", "destructive"] as const;
|
|
68
|
+
|
|
69
|
+
export type StatusTone = (typeof STATUS_TONES)[number];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* An out-of-vocabulary status. Prefer the canonical `Status` enum; reach for
|
|
73
|
+
* this only when the domain genuinely has a state the 7 do not express (e.g. a
|
|
74
|
+
* run engine's `stopped_guardrail`). Map it ONCE, near your domain — not per
|
|
75
|
+
* call site.
|
|
76
|
+
*/
|
|
77
|
+
export interface CustomStatus {
|
|
78
|
+
label: string;
|
|
79
|
+
tone: StatusTone;
|
|
80
|
+
/** Optional Lucide glyph. Omitted → label-only (no default glyph). */
|
|
81
|
+
icon?: LucideIcon;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Narrows `Status | CustomStatus` — the object form is always the escape hatch. */
|
|
85
|
+
export function isCustomStatus(status: Status | CustomStatus): status is CustomStatus {
|
|
86
|
+
return typeof status === "object" && status !== null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The tone each CANONICAL status maps to, for the informational `data-tone`
|
|
91
|
+
* attribute only — canonical rendering always goes through the untouched
|
|
92
|
+
* `status` cva branch below, never through `tone`.
|
|
93
|
+
*/
|
|
94
|
+
const STATUS_TONE_MAP: Record<Status, StatusTone> = {
|
|
95
|
+
pending: "neutral",
|
|
96
|
+
running: "info",
|
|
97
|
+
complete: "success",
|
|
98
|
+
"awaiting-approval": "warning",
|
|
99
|
+
denied: "neutral",
|
|
100
|
+
failed: "destructive",
|
|
101
|
+
skipped: "neutral",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The canonical semantic ROLE per status — the single source of truth for
|
|
106
|
+
* status colour (#392). Any other status-keyed colour map in the repo
|
|
107
|
+
* (`Timeline`'s `NODE_STYLE`, or a future one) must be TESTED against this,
|
|
108
|
+
* not hand-authored — a second literal copy is exactly how `NODE_STYLE`
|
|
109
|
+
* drifted (`running` rendered `--primary` while this map said `--info`).
|
|
110
|
+
* Do NOT interpolate `bg-${STATUS_ROLE[status]}` into a class map: Tailwind
|
|
111
|
+
* scans source statically, so a template-literal class emits no CSS. Keep
|
|
112
|
+
* consuming maps as literal class strings, verified against this export by a
|
|
113
|
+
* test.
|
|
114
|
+
*/
|
|
115
|
+
export const STATUS_ROLE = {
|
|
116
|
+
pending: "secondary",
|
|
117
|
+
running: "info",
|
|
118
|
+
complete: "success",
|
|
119
|
+
"awaiting-approval": "warning",
|
|
120
|
+
denied: "muted",
|
|
121
|
+
failed: "destructive",
|
|
122
|
+
skipped: "secondary",
|
|
123
|
+
} as const satisfies Record<Status, string>;
|
|
124
|
+
|
|
125
|
+
/** Default human-readable label per status (override via `children`). */
|
|
126
|
+
export const STATUS_LABELS: Record<Status, string> = {
|
|
127
|
+
pending: "Pending",
|
|
128
|
+
running: "Running",
|
|
129
|
+
complete: "Complete",
|
|
130
|
+
"awaiting-approval": "Awaiting approval",
|
|
131
|
+
denied: "Denied",
|
|
132
|
+
failed: "Failed",
|
|
133
|
+
skipped: "Skipped",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const STATUS_ICONS: Record<Status, LucideIcon> = {
|
|
137
|
+
pending: Circle,
|
|
138
|
+
running: Loader2,
|
|
139
|
+
complete: CheckCircle2,
|
|
140
|
+
"awaiting-approval": Clock,
|
|
141
|
+
denied: XCircle,
|
|
142
|
+
failed: AlertCircle,
|
|
143
|
+
skipped: MinusCircle,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Lucide glyph per out-of-vocabulary TONE — the same five-tone escape hatch
|
|
148
|
+
* `CustomStatus` uses (`STATUS_TONES`). A consumer that encodes a bare TONE
|
|
149
|
+
* with no fuller `Status` semantics (e.g. `@elabs-ai/components-flow`'s `FlowNode.tone`,
|
|
150
|
+
* #387) derives its non-colour cue from here instead of inventing a second
|
|
151
|
+
* icon vocabulary — each entry reuses the SAME icon `STATUS_ICONS` already
|
|
152
|
+
* pairs with that tone's one canonical status (`success`→`complete`'s
|
|
153
|
+
* `CheckCircle2`, `warning`→`awaiting-approval`'s `Clock`,
|
|
154
|
+
* `destructive`→`failed`'s `AlertCircle`). `neutral`/`info` have no entry:
|
|
155
|
+
* nothing consumes them yet, and reaching for a glyph on every neutral/
|
|
156
|
+
* default-toned element would be assistive-tech noise on the common case —
|
|
157
|
+
* extend this map, don't fork a new one, the day something needs them.
|
|
158
|
+
*/
|
|
159
|
+
export const STATUS_TONE_ICONS: Partial<Record<StatusTone, LucideIcon>> = {
|
|
160
|
+
success: CheckCircle2,
|
|
161
|
+
warning: Clock,
|
|
162
|
+
destructive: AlertCircle,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* On-surface icon color per status for the standalone `StatusIcon` atom.
|
|
167
|
+
* (Inside `StatusBadge` the icon inherits the badge foreground instead, so it
|
|
168
|
+
* stays legible on the solid attention fills.)
|
|
169
|
+
*/
|
|
170
|
+
const STATUS_ICON_CLASSES: Record<Status, string> = {
|
|
171
|
+
pending: "text-muted-foreground",
|
|
172
|
+
running: "text-info-text",
|
|
173
|
+
complete: "text-success-text",
|
|
174
|
+
"awaiting-approval": "text-warning-text",
|
|
175
|
+
denied: "text-muted-foreground",
|
|
176
|
+
failed: "text-destructive-text",
|
|
177
|
+
skipped: "text-muted-foreground",
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** `animate-spin` is a continuous loop (ease-linear is the allowed loop ease). */
|
|
181
|
+
const RUNNING_SPIN_CLASSES = "animate-spin motion-reduce:animate-none";
|
|
182
|
+
|
|
183
|
+
export interface StatusIconProps extends Omit<LucideProps, "ref"> {
|
|
184
|
+
/** The canonical execution status (closed enum). */
|
|
185
|
+
status: Status;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The icon + status-color atom `StatusBadge` itself composes — for tight rows
|
|
190
|
+
* (test-result rows, timeline/rail nodes) where a full badge is too heavy.
|
|
191
|
+
* Decorative by default (`aria-hidden`); the adjacent row text must carry the
|
|
192
|
+
* status name for AT users.
|
|
193
|
+
*/
|
|
194
|
+
export const StatusIcon = forwardRef<SVGSVGElement, StatusIconProps>(function StatusIcon(
|
|
195
|
+
{ status, className, ...props },
|
|
196
|
+
ref,
|
|
197
|
+
) {
|
|
198
|
+
const Icon = STATUS_ICONS[status];
|
|
199
|
+
return (
|
|
200
|
+
<Icon
|
|
201
|
+
ref={ref}
|
|
202
|
+
aria-hidden="true"
|
|
203
|
+
data-status={status}
|
|
204
|
+
className={cn(
|
|
205
|
+
"size-4 shrink-0",
|
|
206
|
+
STATUS_ICON_CLASSES[status],
|
|
207
|
+
status === "running" && RUNNING_SPIN_CLASSES,
|
|
208
|
+
className,
|
|
209
|
+
)}
|
|
210
|
+
{...props}
|
|
211
|
+
/>
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
export const statusBadgeVariants = cva(
|
|
216
|
+
// Builds on the Badge pill (geometry + typography from badge.tsx). The
|
|
217
|
+
// `outline` variant carries no fill, so each status sets its own
|
|
218
|
+
// border/bg/text — conflicts resolve via cn() (later utilities win).
|
|
219
|
+
cn(badgeVariants({ variant: "outline" }), "[&_svg]:shrink-0"),
|
|
220
|
+
{
|
|
221
|
+
variants: {
|
|
222
|
+
status: {
|
|
223
|
+
// calm: neutral fill
|
|
224
|
+
pending: "border-transparent bg-secondary text-secondary-foreground",
|
|
225
|
+
// calm: status alpha-wash (escapes the decoration drawn-not-filled rule)
|
|
226
|
+
running: "border-info/40 bg-info/10 text-info-text",
|
|
227
|
+
complete: "border-success/40 bg-success/10 text-success-text",
|
|
228
|
+
// ATTENTION: solid fill — must grab the eye
|
|
229
|
+
"awaiting-approval": "border-transparent bg-warning text-warning-foreground",
|
|
230
|
+
// calm: de-emphasized neutrals
|
|
231
|
+
denied: "border-transparent bg-muted text-muted-foreground",
|
|
232
|
+
// ATTENTION: solid fill — must grab the eye
|
|
233
|
+
failed: "border-transparent bg-destructive text-destructive-foreground",
|
|
234
|
+
skipped: "border-transparent bg-secondary text-muted-foreground",
|
|
235
|
+
},
|
|
236
|
+
/**
|
|
237
|
+
* The out-of-vocabulary escape hatch (#363) — CALM treatments only (the
|
|
238
|
+
* same alpha-wash/neutral technique as the calm `status` recipes above).
|
|
239
|
+
* There is deliberately no solid/attention recipe here: the loud fill
|
|
240
|
+
* stays exclusive to the canonical `awaiting-approval`/`failed` states
|
|
241
|
+
* (integrity constraint 2, see the file docblock).
|
|
242
|
+
*/
|
|
243
|
+
tone: {
|
|
244
|
+
neutral: "border-transparent bg-secondary text-secondary-foreground",
|
|
245
|
+
info: "border-info/40 bg-info/10 text-info-text",
|
|
246
|
+
success: "border-success/40 bg-success/10 text-success-text",
|
|
247
|
+
warning: "border-warning/40 bg-warning/10 text-warning-text",
|
|
248
|
+
destructive: "border-destructive/40 bg-destructive/10 text-destructive-text",
|
|
249
|
+
},
|
|
250
|
+
/** `md` for inline use; `sm` for rail nodes / dense rows. */
|
|
251
|
+
size: {
|
|
252
|
+
sm: "px-2 [&_svg]:size-3",
|
|
253
|
+
md: "[&_svg]:size-3.5",
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
defaultVariants: { size: "md" },
|
|
257
|
+
},
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
export interface StatusBadgeProps
|
|
261
|
+
extends
|
|
262
|
+
HTMLAttributes<HTMLSpanElement>,
|
|
263
|
+
Omit<VariantProps<typeof statusBadgeVariants>, "status" | "tone"> {
|
|
264
|
+
/** The canonical execution status, or a mapped out-of-vocabulary one (#363). */
|
|
265
|
+
status: Status | CustomStatus;
|
|
266
|
+
/** Render label-only (no status icon). */
|
|
267
|
+
hideIcon?: boolean;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export const StatusBadge = forwardRef<HTMLSpanElement, StatusBadgeProps>(function StatusBadge(
|
|
271
|
+
{ status, size, hideIcon = false, className, children, ...props },
|
|
272
|
+
ref,
|
|
273
|
+
) {
|
|
274
|
+
if (isCustomStatus(status)) {
|
|
275
|
+
const { label, tone, icon: Icon } = status;
|
|
276
|
+
return (
|
|
277
|
+
<span
|
|
278
|
+
ref={ref}
|
|
279
|
+
data-slot="status-badge"
|
|
280
|
+
data-status="custom"
|
|
281
|
+
data-tone={tone}
|
|
282
|
+
className={cn(statusBadgeVariants({ tone, size }), className)}
|
|
283
|
+
{...props}
|
|
284
|
+
>
|
|
285
|
+
{hideIcon || !Icon ? null : <Icon aria-hidden="true" />}
|
|
286
|
+
{children ?? label}
|
|
287
|
+
</span>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const Icon = STATUS_ICONS[status];
|
|
292
|
+
return (
|
|
293
|
+
<span
|
|
294
|
+
ref={ref}
|
|
295
|
+
data-slot="status-badge"
|
|
296
|
+
data-status={status}
|
|
297
|
+
data-tone={STATUS_TONE_MAP[status]}
|
|
298
|
+
className={cn(statusBadgeVariants({ status, size }), className)}
|
|
299
|
+
{...props}
|
|
300
|
+
>
|
|
301
|
+
{hideIcon ? null : (
|
|
302
|
+
<Icon
|
|
303
|
+
aria-hidden="true"
|
|
304
|
+
className={status === "running" ? RUNNING_SPIN_CLASSES : undefined}
|
|
305
|
+
/>
|
|
306
|
+
)}
|
|
307
|
+
{children ?? STATUS_LABELS[status]}
|
|
308
|
+
</span>
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
/** The editor/markdown `Timeline` 3-state vocabulary (`@elabs-ai/components-editor`). */
|
|
313
|
+
export type TimelineStatus = "done" | "active" | "pending";
|
|
314
|
+
|
|
315
|
+
const TIMELINE_STATUS_MAP: Record<TimelineStatus, Status> = {
|
|
316
|
+
done: "complete",
|
|
317
|
+
active: "running",
|
|
318
|
+
pending: "pending",
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Map the Timeline `done|active|pending` vocabulary onto the canonical
|
|
323
|
+
* `Status` (research 10 §B.1 mapping b — lossless; the timeline only
|
|
324
|
+
* expresses 3 of the 7 states). The AI-SDK `ToolUIPart` mapper lives in
|
|
325
|
+
* `@elabs-ai/components-ai` (`statusFromToolState`, tool.tsx) so `@elabs-ai/components-ui` stays free of
|
|
326
|
+
* the `ai` dependency (D6).
|
|
327
|
+
*/
|
|
328
|
+
export function fromTimelineStatus(status: TimelineStatus): Status {
|
|
329
|
+
return TIMELINE_STATUS_MAP[status];
|
|
330
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Switch } from "./switch";
|