@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,280 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useCallback, useEffect, useState, type HTMLAttributes } from "react";
|
|
4
|
+
import { Check, Monitor, Moon, Sun } from "lucide-react";
|
|
5
|
+
import { useTheme, type ThemeDefinition, type ThemeName } from "@elabs-ai/components-tokens";
|
|
6
|
+
|
|
7
|
+
import { Button } from "../button";
|
|
8
|
+
import {
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuContent,
|
|
11
|
+
DropdownMenuItem,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from "../dropdown-menu";
|
|
14
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip";
|
|
15
|
+
import { useThemeTransition, type ThemeTransitionEffect } from "./use-theme-transition";
|
|
16
|
+
|
|
17
|
+
const SYSTEM_STORAGE_KEY = "brand-ui-theme-system";
|
|
18
|
+
const DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
19
|
+
|
|
20
|
+
type ButtonSize = "sm" | "default" | "lg";
|
|
21
|
+
const ICON_SIZE: Record<ButtonSize, "icon-sm" | "icon" | "icon-lg"> = {
|
|
22
|
+
sm: "icon-sm",
|
|
23
|
+
default: "icon",
|
|
24
|
+
lg: "icon-lg",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** A theme choice, or "system" to follow the OS color scheme. */
|
|
28
|
+
export type ThemePreference = ThemeName | "system";
|
|
29
|
+
|
|
30
|
+
export interface ThemeSwitcherProps extends Omit<HTMLAttributes<HTMLButtonElement>, "onChange"> {
|
|
31
|
+
/**
|
|
32
|
+
* Themes to offer, as names. ≤2 → toggle; >2 → dropdown.
|
|
33
|
+
*
|
|
34
|
+
* Defaults to the PROVIDER's registry (ADR 0029) — so an app that registers
|
|
35
|
+
* its own themes gets a working switcher with no prop at all. Pass this only
|
|
36
|
+
* to offer a subset, or to fix the order; names the provider doesn't know are
|
|
37
|
+
* dropped.
|
|
38
|
+
*/
|
|
39
|
+
themes?: ThemeName[];
|
|
40
|
+
/** Force presentation; "auto" (default) picks toggle vs dropdown by theme count. */
|
|
41
|
+
mode?: "auto" | "toggle" | "dropdown";
|
|
42
|
+
/** Offer a "System" option that follows the OS color scheme. Default true. */
|
|
43
|
+
showSystem?: boolean;
|
|
44
|
+
/** Whole-screen reveal effect for the animated switch. Default "polygon". */
|
|
45
|
+
effect?: ThemeTransitionEffect;
|
|
46
|
+
/** Control size. Default "default". */
|
|
47
|
+
size?: ButtonSize;
|
|
48
|
+
/**
|
|
49
|
+
* Controlled preference (#366). When set, the switcher derives its "current"
|
|
50
|
+
* state — including "system" — from this prop instead of its own internal,
|
|
51
|
+
* `localStorage`-backed state, and reports every user pick via
|
|
52
|
+
* `onPreferenceChange` instead of writing its own storage key. Pass this pair
|
|
53
|
+
* when your app's actual source of truth is a different persisted preference
|
|
54
|
+
* (a settings API, a different storage key, a server-synced setting). Omit
|
|
55
|
+
* both for the default, uncontrolled behavior (unchanged).
|
|
56
|
+
*/
|
|
57
|
+
preference?: ThemePreference;
|
|
58
|
+
/** Called with the new preference on every user pick, when `preference` is controlled. */
|
|
59
|
+
onPreferenceChange?: (preference: ThemePreference) => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function prefersDark(): boolean {
|
|
63
|
+
return typeof window !== "undefined" && window.matchMedia(DARK_QUERY).matches;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** First theme in `list` matching `dark`, or the list's own first entry as a last resort. */
|
|
67
|
+
function pickByDarkness(list: readonly ThemeDefinition[], dark: boolean): ThemeName | undefined {
|
|
68
|
+
return (list.find((t) => t.dark === dark) ?? list[0])?.value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Adaptive theme control. With ≤2 themes it renders a light/dark/system toggle
|
|
73
|
+
* (a single icon button that cycles); with >2 it auto-upgrades to a dropdown
|
|
74
|
+
* over the offered themes. Every switch animates the whole screen via
|
|
75
|
+
* `useThemeTransition` (respecting reduced motion). Icons follow each theme's
|
|
76
|
+
* `dark` flag, so it works for any theme — including one you authored.
|
|
77
|
+
*
|
|
78
|
+
* Uncontrolled by default (tracks its own "system" choice via `localStorage`).
|
|
79
|
+
* Pass `preference`/`onPreferenceChange` to drive it from an external store
|
|
80
|
+
* instead (#366) — see the `Controlled` story.
|
|
81
|
+
*
|
|
82
|
+
* **It renders the PROVIDER's registry** (ADR 0029). Register your themes on
|
|
83
|
+
* `<ThemeProvider themes={…}>` and this switcher offers them with no prop —
|
|
84
|
+
* which is also why `allowedThemes` (#355) is honoured for free: the provider
|
|
85
|
+
* has already narrowed what it exposes. The `themes` prop is now purely an
|
|
86
|
+
* additional, optional narrowing on top; a name the provider does not offer is
|
|
87
|
+
* dropped rather than rendered, so a disallowed or unregistered theme is never
|
|
88
|
+
* reachable via a menu item, the toggle, "System", or the OS
|
|
89
|
+
* `prefers-color-scheme` listener.
|
|
90
|
+
*
|
|
91
|
+
* The pre-ADR-0029 hard-coded `["light","dark"]` default is gone. Behaviour for
|
|
92
|
+
* an app on the default provider is unchanged — that registry IS light + dark.
|
|
93
|
+
*/
|
|
94
|
+
export const ThemeSwitcher = forwardRef<HTMLButtonElement, ThemeSwitcherProps>(
|
|
95
|
+
function ThemeSwitcher(
|
|
96
|
+
{
|
|
97
|
+
themes,
|
|
98
|
+
mode = "auto",
|
|
99
|
+
showSystem = true,
|
|
100
|
+
effect = "polygon",
|
|
101
|
+
size = "default",
|
|
102
|
+
preference,
|
|
103
|
+
onPreferenceChange,
|
|
104
|
+
className,
|
|
105
|
+
...props
|
|
106
|
+
},
|
|
107
|
+
ref,
|
|
108
|
+
) {
|
|
109
|
+
const { theme, themeDefinitions, setTheme } = useTheme();
|
|
110
|
+
const switchTheme = useThemeTransition(effect);
|
|
111
|
+
const isControlled = preference !== undefined;
|
|
112
|
+
const [uncontrolledIsSystem, setUncontrolledIsSystem] = useState(false);
|
|
113
|
+
const isSystem = isControlled ? preference === "system" : uncontrolledIsSystem;
|
|
114
|
+
|
|
115
|
+
// The provider's registry is the universe; the prop only narrows it, in the
|
|
116
|
+
// prop's own order. A name the provider doesn't offer is DROPPED (it has no
|
|
117
|
+
// CSS block here and no label to render), and an empty intersection falls
|
|
118
|
+
// back to the whole registry rather than to a hard-coded pair — falling back
|
|
119
|
+
// to literals is what could resurrect a theme the provider disallowed.
|
|
120
|
+
const narrowed = themes
|
|
121
|
+
? themes
|
|
122
|
+
.map((name) => themeDefinitions.find((d) => d.value === name))
|
|
123
|
+
.filter((d): d is ThemeDefinition => d !== undefined)
|
|
124
|
+
: themeDefinitions;
|
|
125
|
+
const offered: readonly ThemeDefinition[] = narrowed.length > 0 ? narrowed : themeDefinitions;
|
|
126
|
+
const safeThemes: ThemeName[] = offered.map((d) => d.value);
|
|
127
|
+
const labelOf = (name: ThemeName) => offered.find((d) => d.value === name)?.label ?? name;
|
|
128
|
+
const isDark = (name: ThemeName) => offered.find((d) => d.value === name)?.dark ?? false;
|
|
129
|
+
|
|
130
|
+
// `?? theme` (the active theme), never a "light"/"dark" literal: with an open
|
|
131
|
+
// registry those names may not exist at all in this app.
|
|
132
|
+
const lightTheme: ThemeName = pickByDarkness(offered, false) ?? theme;
|
|
133
|
+
const darkTheme: ThemeName = pickByDarkness(offered, true) ?? theme;
|
|
134
|
+
|
|
135
|
+
// The concrete theme to DISPLAY as current: the controlled preference when
|
|
136
|
+
// it names one, otherwise the theme actually applied via the provider.
|
|
137
|
+
const resolvedTheme: ThemeName =
|
|
138
|
+
isControlled && preference !== undefined && preference !== "system" ? preference : theme;
|
|
139
|
+
|
|
140
|
+
// Hydrate the "system" choice from localStorage and apply it on mount —
|
|
141
|
+
// uncontrolled mode only. Controlled mode's "system" state comes from the
|
|
142
|
+
// `preference` prop and never touches this component's own storage key.
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (isControlled) return;
|
|
145
|
+
if (typeof window === "undefined") return;
|
|
146
|
+
const active = window.localStorage.getItem(SYSTEM_STORAGE_KEY) === "1";
|
|
147
|
+
setUncontrolledIsSystem(active);
|
|
148
|
+
if (active) setTheme(prefersDark() ? darkTheme : lightTheme);
|
|
149
|
+
// Mount-once hydration by design (mirrors the pre-#366 behavior); a
|
|
150
|
+
// component must not flip controlled/uncontrolled mid-lifecycle
|
|
151
|
+
// (.claude/rules/component-api.md), so `isControlled` cannot change here.
|
|
152
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
// Keep the applied theme tracking the OS scheme while "system" is active —
|
|
156
|
+
// controlled or not. Depends on every value it reads (unlike the effect
|
|
157
|
+
// above) so it never captures a stale `lightTheme`/`darkTheme` once those
|
|
158
|
+
// become provider-derived (the staleness risk #384 flags).
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
161
|
+
const mql = window.matchMedia(DARK_QUERY);
|
|
162
|
+
const onChange = () => {
|
|
163
|
+
const active = isControlled
|
|
164
|
+
? preference === "system"
|
|
165
|
+
: window.localStorage.getItem(SYSTEM_STORAGE_KEY) === "1";
|
|
166
|
+
if (active) setTheme(prefersDark() ? darkTheme : lightTheme);
|
|
167
|
+
};
|
|
168
|
+
mql.addEventListener("change", onChange);
|
|
169
|
+
return () => mql.removeEventListener("change", onChange);
|
|
170
|
+
}, [isControlled, preference, darkTheme, lightTheme, setTheme]);
|
|
171
|
+
|
|
172
|
+
const pickTheme = useCallback(
|
|
173
|
+
(next: ThemeName) => {
|
|
174
|
+
if (isControlled) {
|
|
175
|
+
onPreferenceChange?.(next);
|
|
176
|
+
} else {
|
|
177
|
+
setUncontrolledIsSystem(false);
|
|
178
|
+
if (typeof window !== "undefined") window.localStorage.setItem(SYSTEM_STORAGE_KEY, "0");
|
|
179
|
+
}
|
|
180
|
+
switchTheme(next);
|
|
181
|
+
},
|
|
182
|
+
[switchTheme, isControlled, onPreferenceChange],
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const pickSystem = useCallback(() => {
|
|
186
|
+
if (isControlled) {
|
|
187
|
+
onPreferenceChange?.("system");
|
|
188
|
+
} else {
|
|
189
|
+
setUncontrolledIsSystem(true);
|
|
190
|
+
if (typeof window !== "undefined") window.localStorage.setItem(SYSTEM_STORAGE_KEY, "1");
|
|
191
|
+
}
|
|
192
|
+
switchTheme(prefersDark() ? darkTheme : lightTheme);
|
|
193
|
+
}, [switchTheme, darkTheme, lightTheme, isControlled, onPreferenceChange]);
|
|
194
|
+
|
|
195
|
+
const TriggerIcon = isSystem ? Monitor : isDark(resolvedTheme) ? Moon : Sun;
|
|
196
|
+
const useDropdown = mode === "dropdown" || (mode === "auto" && safeThemes.length > 2);
|
|
197
|
+
|
|
198
|
+
// ---- Dropdown mode (>2 themes) ----
|
|
199
|
+
if (useDropdown) {
|
|
200
|
+
return (
|
|
201
|
+
<DropdownMenu>
|
|
202
|
+
<DropdownMenuTrigger asChild>
|
|
203
|
+
<Button
|
|
204
|
+
ref={ref}
|
|
205
|
+
variant="outline"
|
|
206
|
+
size={ICON_SIZE[size]}
|
|
207
|
+
aria-label="Theme"
|
|
208
|
+
className={className}
|
|
209
|
+
{...props}
|
|
210
|
+
>
|
|
211
|
+
<TriggerIcon />
|
|
212
|
+
</Button>
|
|
213
|
+
</DropdownMenuTrigger>
|
|
214
|
+
<DropdownMenuContent align="end">
|
|
215
|
+
{showSystem ? (
|
|
216
|
+
<DropdownMenuItem onSelect={() => pickSystem()}>
|
|
217
|
+
<Monitor />
|
|
218
|
+
<span>System</span>
|
|
219
|
+
{isSystem ? <Check className="ml-auto" /> : null}
|
|
220
|
+
</DropdownMenuItem>
|
|
221
|
+
) : null}
|
|
222
|
+
{offered.map((d) => (
|
|
223
|
+
<DropdownMenuItem key={d.value} onSelect={() => pickTheme(d.value)}>
|
|
224
|
+
{d.dark ? <Moon /> : <Sun />}
|
|
225
|
+
<span>{d.label}</span>
|
|
226
|
+
{!isSystem && resolvedTheme === d.value ? <Check className="ml-auto" /> : null}
|
|
227
|
+
</DropdownMenuItem>
|
|
228
|
+
))}
|
|
229
|
+
</DropdownMenuContent>
|
|
230
|
+
</DropdownMenu>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---- Toggle mode (≤2 themes): cycle light → dark → (system) ----
|
|
235
|
+
// These three are ROLES in the cycle, not theme names — `lightTheme` and
|
|
236
|
+
// `darkTheme` resolve each role to whatever this app actually registered.
|
|
237
|
+
const order: Array<"light" | "dark" | "system"> = showSystem
|
|
238
|
+
? ["light", "dark", "system"]
|
|
239
|
+
: ["light", "dark"];
|
|
240
|
+
const current: "light" | "dark" | "system" = isSystem
|
|
241
|
+
? "system"
|
|
242
|
+
: resolvedTheme === darkTheme
|
|
243
|
+
? "dark"
|
|
244
|
+
: "light";
|
|
245
|
+
const next = order[(order.indexOf(current) + 1) % order.length];
|
|
246
|
+
const apply = () => {
|
|
247
|
+
if (next === "system") pickSystem();
|
|
248
|
+
else pickTheme(next === "dark" ? darkTheme : lightTheme);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Announce the THEME'S OWN label, not the role: an app whose themes are
|
|
252
|
+
// "Daylight"/"Midnight" would otherwise announce "Theme: light" — naming a
|
|
253
|
+
// theme that does not exist in that app.
|
|
254
|
+
// `role` is optional only because `order[i]` is an indexed read under
|
|
255
|
+
// `noUncheckedIndexedAccess`; the modulo above makes it always present.
|
|
256
|
+
const roleLabel = (role: "light" | "dark" | "system" | undefined) =>
|
|
257
|
+
role === "system" ? "System" : labelOf(role === "dark" ? darkTheme : lightTheme);
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<TooltipProvider>
|
|
261
|
+
<Tooltip>
|
|
262
|
+
<TooltipTrigger asChild>
|
|
263
|
+
<Button
|
|
264
|
+
ref={ref}
|
|
265
|
+
variant="ghost"
|
|
266
|
+
size={ICON_SIZE[size]}
|
|
267
|
+
aria-label={`Theme: ${roleLabel(current)}. Activate to switch to ${roleLabel(next)}.`}
|
|
268
|
+
onClick={apply}
|
|
269
|
+
className={className}
|
|
270
|
+
{...props}
|
|
271
|
+
>
|
|
272
|
+
<TriggerIcon />
|
|
273
|
+
</Button>
|
|
274
|
+
</TooltipTrigger>
|
|
275
|
+
<TooltipContent>{roleLabel(current)}</TooltipContent>
|
|
276
|
+
</Tooltip>
|
|
277
|
+
</TooltipProvider>
|
|
278
|
+
);
|
|
279
|
+
},
|
|
280
|
+
);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import { useReducedMotion, useTheme, type ThemeName } from "@elabs-ai/components-tokens";
|
|
5
|
+
|
|
6
|
+
/** Whole-screen reveal effects for the animated theme switch (View Transitions API). */
|
|
7
|
+
export type ThemeTransitionEffect = "polygon" | "circle" | "circle-blur" | "triangle";
|
|
8
|
+
|
|
9
|
+
type ViewTransitionDocument = Document & {
|
|
10
|
+
startViewTransition?: (callback: () => void | Promise<void>) => { finished: Promise<void> };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns a theme setter that animates the whole-screen switch via the View
|
|
15
|
+
* Transitions API (the chanhdai.com "theme toggle effect", adapted to brand-ui).
|
|
16
|
+
*
|
|
17
|
+
* - Direction-aware: sets `data-vt="to-dark|to-light"` on `<html>` from the
|
|
18
|
+
* target theme's `dark` flag, so the clip-path wipes the right way for ANY of
|
|
19
|
+
* the three themes (not just light/dark).
|
|
20
|
+
* - `data-vt-effect` selects the reveal shape (default "polygon").
|
|
21
|
+
* - Motion-aware: under `useReducedMotion()` (OS or in-app preference) — or when
|
|
22
|
+
* the browser lacks `startViewTransition` — it falls back to an instant,
|
|
23
|
+
* unanimated `setTheme`.
|
|
24
|
+
*
|
|
25
|
+
* The visual is pure CSS on `::view-transition-*` in `@elabs-ai/components-tokens` themes.css;
|
|
26
|
+
* this hook only orchestrates it.
|
|
27
|
+
*/
|
|
28
|
+
export function useThemeTransition(effect: ThemeTransitionEffect = "polygon") {
|
|
29
|
+
const { theme, setTheme, themeDefinitions } = useTheme();
|
|
30
|
+
const reduced = useReducedMotion();
|
|
31
|
+
|
|
32
|
+
return useCallback(
|
|
33
|
+
(next: ThemeName) => {
|
|
34
|
+
if (next === theme) return;
|
|
35
|
+
if (typeof document === "undefined") {
|
|
36
|
+
setTheme(next);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const doc = document as ViewTransitionDocument;
|
|
40
|
+
if (reduced || typeof doc.startViewTransition !== "function") {
|
|
41
|
+
setTheme(next);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const root = document.documentElement;
|
|
45
|
+
// The direction of the reveal comes from the provider's REGISTRY entry
|
|
46
|
+
// for the incoming theme (ADR 0029) — a consumer theme animates correctly
|
|
47
|
+
// without registering here. `resolveThemeIsDark` is deliberately NOT used:
|
|
48
|
+
// it reads the CURRENT computed `color-scheme`, and `next` has not been
|
|
49
|
+
// applied yet, so it would report the OUTGOING theme's direction.
|
|
50
|
+
root.dataset.vt = themeDefinitions.find((d) => d.value === next)?.dark
|
|
51
|
+
? "to-dark"
|
|
52
|
+
: "to-light";
|
|
53
|
+
root.dataset.vtEffect = effect;
|
|
54
|
+
const transition = doc.startViewTransition(() => setTheme(next));
|
|
55
|
+
transition.finished.finally(() => {
|
|
56
|
+
delete root.dataset.vt;
|
|
57
|
+
delete root.dataset.vtEffect;
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
[theme, setTheme, reduced, effect, themeDefinitions],
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// NOTE: `TimelineStatus` (the editor's 3-state vocabulary) is deliberately NOT
|
|
2
|
+
// re-exported here — it already reaches the @elabs-ai/components-ui barrel via
|
|
3
|
+
// ./components/status-badge; exporting it twice would make the star-exports
|
|
4
|
+
// ambiguous.
|
|
5
|
+
export {
|
|
6
|
+
Timeline,
|
|
7
|
+
TimelineItem,
|
|
8
|
+
TimelineRoot,
|
|
9
|
+
type TimelineEntry,
|
|
10
|
+
type TimelineItemProps,
|
|
11
|
+
type TimelineProps,
|
|
12
|
+
type TimelineRootProps,
|
|
13
|
+
} from "./timeline";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, waitFor, within } from "storybook/test";
|
|
3
|
+
import { Timeline, TimelineItem, TimelineRoot } from "./timeline";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Core/Timeline",
|
|
7
|
+
component: Timeline,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: "padded",
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
"Ordered steps with a vertical connector and status-colored nodes — THE shared " +
|
|
15
|
+
"rail spine (#190). Two front doors over one rail: the array `items` API (the " +
|
|
16
|
+
"editor's `:::timeline` target, `done|active|pending`) and the compound " +
|
|
17
|
+
"`TimelineRoot`/`TimelineItem` parts, whose nodes take the canonical 7-state " +
|
|
18
|
+
"`Status` (status-badge). Token-driven and motion-free.",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
argTypes: {
|
|
23
|
+
items: {
|
|
24
|
+
description:
|
|
25
|
+
"Array of timeline entries (array-API front door; maps `done|active|pending` status).",
|
|
26
|
+
control: false,
|
|
27
|
+
table: { category: "Content" },
|
|
28
|
+
},
|
|
29
|
+
className: {
|
|
30
|
+
description: "Extra Tailwind classes merged via cn().",
|
|
31
|
+
control: "text",
|
|
32
|
+
table: { category: "Appearance" },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
} satisfies Meta<typeof Timeline>;
|
|
36
|
+
|
|
37
|
+
export default meta;
|
|
38
|
+
type Story = StoryObj<typeof meta>;
|
|
39
|
+
|
|
40
|
+
export const Default: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
items: [
|
|
43
|
+
{ title: "Draft", description: "Initial scope frozen", status: "done", timestamp: "Jun 4" },
|
|
44
|
+
{
|
|
45
|
+
title: "Review",
|
|
46
|
+
description: "With the migration team",
|
|
47
|
+
status: "active",
|
|
48
|
+
timestamp: "Jun 6",
|
|
49
|
+
},
|
|
50
|
+
{ title: "Publish", status: "pending" },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
play: async ({ canvasElement }) => {
|
|
54
|
+
const canvas = within(canvasElement);
|
|
55
|
+
await waitFor(() => expect(canvas.getByText("Review")).toBeVisible());
|
|
56
|
+
expect(canvas.getAllByRole("listitem")).toHaveLength(3);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const Compound: Story = {
|
|
61
|
+
args: { items: [] },
|
|
62
|
+
render: () => (
|
|
63
|
+
<TimelineRoot aria-label="Agent run">
|
|
64
|
+
<TimelineItem status="complete" timestamp="14:02">
|
|
65
|
+
Searched financial filings
|
|
66
|
+
</TimelineItem>
|
|
67
|
+
<TimelineItem status="complete" description="8 rows reconciled · 0 variances">
|
|
68
|
+
Queried finance.revenue
|
|
69
|
+
</TimelineItem>
|
|
70
|
+
<TimelineItem status="running">Computing QoQ deltas</TimelineItem>
|
|
71
|
+
<TimelineItem status="pending">Draft the board note</TimelineItem>
|
|
72
|
+
</TimelineRoot>
|
|
73
|
+
),
|
|
74
|
+
play: async ({ canvasElement }) => {
|
|
75
|
+
const canvas = within(canvasElement);
|
|
76
|
+
await waitFor(() => expect(canvas.getByText("Computing QoQ deltas")).toBeVisible());
|
|
77
|
+
expect(canvas.getAllByRole("listitem")).toHaveLength(4);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const CanonicalStatuses: Story = {
|
|
82
|
+
args: { items: [] },
|
|
83
|
+
render: () => (
|
|
84
|
+
<TimelineRoot aria-label="All canonical statuses">
|
|
85
|
+
<TimelineItem status="pending">Pending</TimelineItem>
|
|
86
|
+
<TimelineItem status="running">Running</TimelineItem>
|
|
87
|
+
<TimelineItem status="complete">Complete</TimelineItem>
|
|
88
|
+
<TimelineItem status="awaiting-approval">Awaiting approval</TimelineItem>
|
|
89
|
+
<TimelineItem status="denied">Denied</TimelineItem>
|
|
90
|
+
<TimelineItem status="failed">Failed</TimelineItem>
|
|
91
|
+
<TimelineItem status="skipped">Skipped</TimelineItem>
|
|
92
|
+
</TimelineRoot>
|
|
93
|
+
),
|
|
94
|
+
play: async ({ canvasElement }) => {
|
|
95
|
+
const canvas = within(canvasElement);
|
|
96
|
+
expect(canvas.getAllByRole("listitem")).toHaveLength(7);
|
|
97
|
+
await waitFor(() => expect(canvas.getByText("Awaiting approval")).toBeVisible());
|
|
98
|
+
},
|
|
99
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import { NODE_STYLE, Timeline, TimelineItem, TimelineRoot } from "./timeline";
|
|
5
|
+
import { STATUS_ROLE, STATUSES, statusBadgeVariants, type Status } from "../status-badge";
|
|
6
|
+
|
|
7
|
+
afterEach(cleanup);
|
|
8
|
+
|
|
9
|
+
// #392 — one vocabulary, one colour. `NODE_STYLE` (the rail dot) is a literal
|
|
10
|
+
// duplicate of the status→role mapping `STATUS_ROLE`/`statusBadgeVariants` own;
|
|
11
|
+
// this block is what stops the duplicate from silently drifting again (it did,
|
|
12
|
+
// for `running`: the dot rendered `--primary` while the badge rendered `--info`).
|
|
13
|
+
// Only the four CHROMATIC statuses are asserted — `pending`/`skipped`/`denied`
|
|
14
|
+
// are a deliberate, documented neutral divergence (a hollow dot vs a filled
|
|
15
|
+
// pill), not a bug: `NODE_STYLE.pending` uses `bg-background` (hollow, "not yet")
|
|
16
|
+
// where `statusBadgeVariants({status:"pending"})` uses a filled `bg-secondary`
|
|
17
|
+
// pill — a legitimate design choice a dot and a pill make differently.
|
|
18
|
+
describe("Timeline NODE_STYLE — one vocabulary, one colour (#392)", () => {
|
|
19
|
+
const CHROMATIC: Status[] = ["running", "complete", "awaiting-approval", "failed"];
|
|
20
|
+
const role = (classes: string) => classes.match(/\bbg-([a-z-]+?)(?:\/\d+)?\b/)?.[1];
|
|
21
|
+
|
|
22
|
+
it.each(CHROMATIC)(
|
|
23
|
+
"%s renders the same semantic role on the rail node and the badge",
|
|
24
|
+
(status) => {
|
|
25
|
+
expect(role(NODE_STYLE[status])).toBe(role(statusBadgeVariants({ status })));
|
|
26
|
+
expect(role(NODE_STYLE[status])).toBe(STATUS_ROLE[status]);
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
it("keeps the running ring halo on the same role as its fill", () => {
|
|
31
|
+
expect(NODE_STYLE.running).toMatch(/\bring-info\/25\b/);
|
|
32
|
+
expect(role(NODE_STYLE.running)).toBe("info");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// #387 — colour is never the only channel (WCAG 1.4.1). `NODE_STYLE` used to
|
|
37
|
+
// encode status in fill hue alone: `denied`/`skipped` were byte-identical, and
|
|
38
|
+
// `complete`/`awaiting-approval`/`failed` were shape-identical filled dots
|
|
39
|
+
// differing only by hue. An earlier version of this fix reached for
|
|
40
|
+
// `border-style` (dashed/dotted) on the vivid quartet too — a RENDERED check
|
|
41
|
+
// caught that it is invisible there (border colour == fill colour, so a dash
|
|
42
|
+
// gap reveals nothing) even though it read as "clearly different" in the
|
|
43
|
+
// class string. The vivid quartet (`running`/`complete`/`awaiting-approval`/
|
|
44
|
+
// `failed`) is differentiated by `ring-*` WIDTH instead (0/1/2/4 — a ring
|
|
45
|
+
// contrasts against the PAGE, not the fill, which is why it actually paints).
|
|
46
|
+
// `denied`/`skipped` keep `border-style` because THEIR border and fill
|
|
47
|
+
// genuinely differ in colour, where it does paint. See the class-map comment
|
|
48
|
+
// in `timeline.tsx` and the #387 rendered-proof zoomed/greyscale screenshots.
|
|
49
|
+
describe("Timeline NODE_STYLE — colour is never the only channel (#387)", () => {
|
|
50
|
+
const signature = (status: Status) => {
|
|
51
|
+
const classes = NODE_STYLE[status];
|
|
52
|
+
const fill = classes.match(/\bbg-[a-z-]+\b/)?.[0];
|
|
53
|
+
const style = classes.match(/\bborder-(dashed|dotted|double)\b/)?.[1] ?? "solid";
|
|
54
|
+
const ringWidth = classes.match(/\bring-(\d+)\b/)?.[1] ?? "0";
|
|
55
|
+
return `${fill}|${style}|ring-${ringWidth}`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
it("gives every status a unique non-colour (fill + border-style + ring-width) signature", () => {
|
|
59
|
+
const signatures = STATUSES.map(signature);
|
|
60
|
+
expect(new Set(signatures).size).toBe(STATUSES.length);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("distinguishes denied, skipped and pending from one another (previously denied ≡ skipped)", () => {
|
|
64
|
+
expect(NODE_STYLE.denied).not.toBe(NODE_STYLE.skipped);
|
|
65
|
+
expect(signature("denied")).not.toBe(signature("skipped"));
|
|
66
|
+
expect(signature("pending")).not.toBe(signature("denied"));
|
|
67
|
+
expect(signature("pending")).not.toBe(signature("skipped"));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("distinguishes complete, awaiting-approval and failed from one another (previously hue-only)", () => {
|
|
71
|
+
expect(signature("complete")).not.toBe(signature("awaiting-approval"));
|
|
72
|
+
expect(signature("complete")).not.toBe(signature("failed"));
|
|
73
|
+
expect(signature("awaiting-approval")).not.toBe(signature("failed"));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("gives the vivid quartet (running/complete/awaiting-approval/failed) four distinct ring widths", () => {
|
|
77
|
+
const widths = ["running", "complete", "awaiting-approval", "failed"].map(
|
|
78
|
+
(s) => NODE_STYLE[s as Status].match(/\bring-(\d+)\b/)?.[1] ?? "0",
|
|
79
|
+
);
|
|
80
|
+
expect(new Set(widths).size).toBe(widths.length);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("names the status for assistive tech — data-status alone isn't AT-visible", () => {
|
|
84
|
+
render(
|
|
85
|
+
<TimelineRoot>
|
|
86
|
+
<TimelineItem status="running">Computing QoQ deltas</TimelineItem>
|
|
87
|
+
<TimelineItem status="failed">Queried finance.revenue</TimelineItem>
|
|
88
|
+
<TimelineItem status="skipped">Draft the board note</TimelineItem>
|
|
89
|
+
</TimelineRoot>,
|
|
90
|
+
);
|
|
91
|
+
const [running, failed, skipped] = screen.getAllByRole("listitem");
|
|
92
|
+
expect(running).toHaveTextContent("Running: Computing QoQ deltas");
|
|
93
|
+
expect(failed).toHaveTextContent("Failed: Queried finance.revenue");
|
|
94
|
+
expect(skipped).toHaveTextContent("Skipped: Draft the board note");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("Timeline (array API — the editor-facing surface, #190)", () => {
|
|
99
|
+
it("renders one list item per timeline step", () => {
|
|
100
|
+
render(
|
|
101
|
+
<Timeline
|
|
102
|
+
items={[
|
|
103
|
+
{ title: "Draft", status: "done" },
|
|
104
|
+
{ title: "Review", status: "active", description: "in progress" },
|
|
105
|
+
{ title: "Publish", status: "pending" },
|
|
106
|
+
]}
|
|
107
|
+
/>,
|
|
108
|
+
);
|
|
109
|
+
expect(screen.getAllByRole("listitem")).toHaveLength(3);
|
|
110
|
+
expect(screen.getByText("Review")).toBeInTheDocument();
|
|
111
|
+
expect(screen.getByText("in progress")).toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("maps the editor 3-state vocabulary onto the canonical Status", () => {
|
|
115
|
+
render(
|
|
116
|
+
<Timeline
|
|
117
|
+
items={[
|
|
118
|
+
{ title: "Draft", status: "done" },
|
|
119
|
+
{ title: "Review", status: "active" },
|
|
120
|
+
{ title: "Publish" }, // status omitted → pending
|
|
121
|
+
]}
|
|
122
|
+
/>,
|
|
123
|
+
);
|
|
124
|
+
const [done, active, pending] = screen.getAllByRole("listitem");
|
|
125
|
+
expect(done).toHaveAttribute("data-status", "complete");
|
|
126
|
+
expect(active).toHaveAttribute("data-status", "running");
|
|
127
|
+
expect(pending).toHaveAttribute("data-status", "pending");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("TimelineRoot/TimelineItem (compound API)", () => {
|
|
132
|
+
it("composes items with canonical statuses and rich children", () => {
|
|
133
|
+
render(
|
|
134
|
+
<TimelineRoot data-testid="rail">
|
|
135
|
+
<TimelineItem status="complete" timestamp="Jun 4">
|
|
136
|
+
Searched filings
|
|
137
|
+
</TimelineItem>
|
|
138
|
+
<TimelineItem status="failed" description={<em>quota exceeded</em>}>
|
|
139
|
+
Queried finance.revenue
|
|
140
|
+
</TimelineItem>
|
|
141
|
+
</TimelineRoot>,
|
|
142
|
+
);
|
|
143
|
+
expect(screen.getByTestId("rail").tagName).toBe("OL");
|
|
144
|
+
const items = screen.getAllByRole("listitem");
|
|
145
|
+
expect(items).toHaveLength(2);
|
|
146
|
+
expect(items[0]).toHaveAttribute("data-status", "complete");
|
|
147
|
+
expect(items[1]).toHaveAttribute("data-status", "failed");
|
|
148
|
+
expect(screen.getByText("Jun 4")).toBeInTheDocument();
|
|
149
|
+
expect(screen.getByText("quota exceeded")).toBeInTheDocument();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("renders the block detail slot under the node (#192 AgentStep host)", () => {
|
|
153
|
+
render(
|
|
154
|
+
<TimelineRoot>
|
|
155
|
+
<TimelineItem
|
|
156
|
+
status="complete"
|
|
157
|
+
detail={<div data-testid="detail">8 rows · 0 variances</div>}
|
|
158
|
+
>
|
|
159
|
+
Queried finance.revenue
|
|
160
|
+
</TimelineItem>
|
|
161
|
+
</TimelineRoot>,
|
|
162
|
+
);
|
|
163
|
+
expect(screen.getByTestId("detail")).toBeInTheDocument();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// #381 — the `awaiting-approval` node is a colour-ONLY mark (a ~10px dot whose
|
|
167
|
+
// border is the same tone, so there is no rescuing hairline). It must keep
|
|
168
|
+
// reaching for the status FILL rung, which is the rung the token contract
|
|
169
|
+
// guarantees ≥3:1 on every content surface (themes-contrast.test.ts). A future
|
|
170
|
+
// edit that swaps it for a wash (`bg-warning/10`) or the plate-ink rung
|
|
171
|
+
// (`text-warning-foreground`) would silently drop it back under 1.4.11.
|
|
172
|
+
it("draws the awaiting-approval node with the warning MARK rung (1.4.11)", () => {
|
|
173
|
+
render(
|
|
174
|
+
<TimelineRoot>
|
|
175
|
+
<TimelineItem status="awaiting-approval">Awaiting sign-off</TimelineItem>
|
|
176
|
+
</TimelineRoot>,
|
|
177
|
+
);
|
|
178
|
+
const node = screen.getByRole("listitem").querySelector(".bg-warning");
|
|
179
|
+
expect(node).not.toBeNull();
|
|
180
|
+
expect(node).toHaveClass("border-warning");
|
|
181
|
+
expect(node?.className).not.toMatch(/bg-warning\//);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("merges className and spreads props on the li", () => {
|
|
185
|
+
render(
|
|
186
|
+
<TimelineRoot>
|
|
187
|
+
<TimelineItem className="custom-item" aria-label="step">
|
|
188
|
+
Step
|
|
189
|
+
</TimelineItem>
|
|
190
|
+
</TimelineRoot>,
|
|
191
|
+
);
|
|
192
|
+
const li = screen.getByRole("listitem");
|
|
193
|
+
expect(li).toHaveClass("custom-item");
|
|
194
|
+
expect(li).toHaveAttribute("aria-label", "step");
|
|
195
|
+
});
|
|
196
|
+
});
|