@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,637 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ChangeReview — AI-edit trust gate.
|
|
5
|
+
*
|
|
6
|
+
* When an agent proposes edits, the human reviews them hunk-by-hunk and accepts
|
|
7
|
+
* or rejects each before they apply. Feeling: calm, scannable, trustworthy —
|
|
8
|
+
* "I can see exactly what changed, who proposed it, and nothing lands without
|
|
9
|
+
* my say-so."
|
|
10
|
+
*
|
|
11
|
+
* Design references: GitHub PR file-review (per-hunk viewed/comment),
|
|
12
|
+
* Cursor/Copilot "review changes" panel, Graphite/Linear review UIs.
|
|
13
|
+
*
|
|
14
|
+
* State grid: empty (no hunks → StatePanel), single hunk, mixed, all-approved,
|
|
15
|
+
* all-rejected, very long hunk content. Approval state is signalled with color
|
|
16
|
+
* AND icon/label (colorblind + high-decoration/high-contrast safe).
|
|
17
|
+
*
|
|
18
|
+
* Compound structure (named exports, the Card/CardHeader convention — NOT dot
|
|
19
|
+
* notation). Use the `ChangeReview` root for the default layout, or compose the
|
|
20
|
+
* parts for a custom one:
|
|
21
|
+
* <ChangeReviewProvider> — lifted state (approved set + actions interface)
|
|
22
|
+
* <ChangeReviewHeader> — count summary + Approve all / Reject all
|
|
23
|
+
* <ChangeReviewProvenance> — who/what proposed (quiet attribution)
|
|
24
|
+
* <ChangeReviewList> — the hunk list
|
|
25
|
+
* <ChangeReviewHunk> — per-hunk row (title, rails, before/after, toggle)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
createContext,
|
|
30
|
+
forwardRef,
|
|
31
|
+
use,
|
|
32
|
+
useCallback,
|
|
33
|
+
useId,
|
|
34
|
+
useMemo,
|
|
35
|
+
useState,
|
|
36
|
+
type HTMLAttributes,
|
|
37
|
+
type ReactNode,
|
|
38
|
+
} from "react";
|
|
39
|
+
import { Check, X, CheckCheck, User, Bot, Clock } from "lucide-react";
|
|
40
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
41
|
+
import { cn } from "../../lib/cn";
|
|
42
|
+
import { Button } from "../button/button";
|
|
43
|
+
import { Badge } from "../badge/badge";
|
|
44
|
+
import { StatePanel } from "../state-panel/state-panel";
|
|
45
|
+
|
|
46
|
+
// ─── Public types ─────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/** Who / what proposed the change set (presentational only; never fetches). */
|
|
49
|
+
export interface ChangeProvenance {
|
|
50
|
+
/** Display name of the person or agent author. */
|
|
51
|
+
author?: string;
|
|
52
|
+
/** Model or tool identifier (e.g. "claude-sonnet-4-6"). */
|
|
53
|
+
model?: string;
|
|
54
|
+
/** ISO-8601 or human-readable timestamp. */
|
|
55
|
+
timestamp?: string;
|
|
56
|
+
/** Free-form note, e.g. commit hash or run ID. */
|
|
57
|
+
note?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Status of a single change hunk. Drives the accent-rail colour and icon. */
|
|
61
|
+
export type ChangeHunkStatus = "added" | "removed" | "modified";
|
|
62
|
+
|
|
63
|
+
/** A single proposed change in the review set. */
|
|
64
|
+
export interface ChangeHunk {
|
|
65
|
+
/** Unique identifier for this hunk (used in the approved set). */
|
|
66
|
+
id: string;
|
|
67
|
+
/** Short title shown in the hunk header (e.g. filename or section label). */
|
|
68
|
+
title?: string;
|
|
69
|
+
/** The content before the proposed edit. */
|
|
70
|
+
before?: ReactNode;
|
|
71
|
+
/** The content after the proposed edit (the proposal). */
|
|
72
|
+
after?: ReactNode;
|
|
73
|
+
/** Change type — drives accent rail + icon. @default "modified" */
|
|
74
|
+
status?: ChangeHunkStatus;
|
|
75
|
+
/** Per-hunk provenance override (overrides the review-level provenance). */
|
|
76
|
+
provenance?: ChangeProvenance;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** State surface exposed by the ChangeReview context. */
|
|
80
|
+
export interface ChangeReviewState {
|
|
81
|
+
/** Set of currently-approved hunk IDs. */
|
|
82
|
+
approved: ReadonlySet<string>;
|
|
83
|
+
/** Total number of hunks. */
|
|
84
|
+
totalCount: number;
|
|
85
|
+
/** Number of approved hunks. */
|
|
86
|
+
approvedCount: number;
|
|
87
|
+
/** True when every hunk is approved. */
|
|
88
|
+
allApproved: boolean;
|
|
89
|
+
/** True when no hunk is approved. */
|
|
90
|
+
noneApproved: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Actions exposed by the ChangeReview context. */
|
|
94
|
+
export interface ChangeReviewActions {
|
|
95
|
+
/** Toggle the approval state of a single hunk. */
|
|
96
|
+
toggle: (id: string, next: boolean) => void;
|
|
97
|
+
/** Approve all hunks. */
|
|
98
|
+
approveAll: () => void;
|
|
99
|
+
/** Reject (un-approve) all hunks. */
|
|
100
|
+
rejectAll: () => void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Context ──────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
interface ChangeReviewContextValue {
|
|
106
|
+
state: ChangeReviewState;
|
|
107
|
+
actions: ChangeReviewActions;
|
|
108
|
+
hunks: ChangeHunk[];
|
|
109
|
+
headingId: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const ChangeReviewContext = createContext<ChangeReviewContextValue | null>(null);
|
|
113
|
+
|
|
114
|
+
function useChangeReview(): ChangeReviewContextValue {
|
|
115
|
+
const ctx = use(ChangeReviewContext);
|
|
116
|
+
if (!ctx) {
|
|
117
|
+
throw new Error("ChangeReview sub-components must be rendered inside ChangeReview.Provider");
|
|
118
|
+
}
|
|
119
|
+
return ctx;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Variants ─────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Hunk-status accent rail — the separation grammar's accent-rail channel: ONE
|
|
126
|
+
* focal gesture (the status rail) on a neutral card ground (`bg-card` + the
|
|
127
|
+
* subtle, redundant-exempt `border-border` hairline). NO colored wash and NO
|
|
128
|
+
* redundant colored border — the slop the "AI card" look is built from. Status
|
|
129
|
+
* reads from the rail + the header icon + the StatusBadge (never color alone).
|
|
130
|
+
* Approval is signalled by the StatusBadge + the filled toggle (no ring wash).
|
|
131
|
+
*/
|
|
132
|
+
export const hunkVariants = cva(
|
|
133
|
+
[
|
|
134
|
+
"relative rounded-md border border-border bg-card text-card-foreground",
|
|
135
|
+
"transition-colors duration-fast ease-standard motion-reduce:transition-none",
|
|
136
|
+
],
|
|
137
|
+
{
|
|
138
|
+
variants: {
|
|
139
|
+
status: {
|
|
140
|
+
added: "border-s-4 border-s-success",
|
|
141
|
+
removed: "border-s-4 border-s-destructive",
|
|
142
|
+
modified: "border-s-4 border-s-info",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
defaultVariants: { status: "modified" },
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// ─── Provider ─────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
export interface ChangeReviewProviderProps {
|
|
152
|
+
hunks: ChangeHunk[];
|
|
153
|
+
/**
|
|
154
|
+
* Controlled approved set. Pass `Set<string>` or a string array.
|
|
155
|
+
* When provided, the component is controlled and `onToggle` / `onApproveAll` /
|
|
156
|
+
* `onRejectAll` are the only way to update the state.
|
|
157
|
+
*/
|
|
158
|
+
approved?: Set<string> | string[];
|
|
159
|
+
onToggle?: (id: string, next: boolean) => void;
|
|
160
|
+
onApproveAll?: () => void;
|
|
161
|
+
onRejectAll?: () => void;
|
|
162
|
+
children: ReactNode;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Lifts the approval state. Can be controlled (pass `approved`) or uncontrolled.
|
|
167
|
+
* Derive `isControlled = approved !== undefined`.
|
|
168
|
+
*/
|
|
169
|
+
export function ChangeReviewProvider({
|
|
170
|
+
hunks,
|
|
171
|
+
approved: approvedProp,
|
|
172
|
+
onToggle,
|
|
173
|
+
onApproveAll,
|
|
174
|
+
onRejectAll,
|
|
175
|
+
children,
|
|
176
|
+
}: ChangeReviewProviderProps) {
|
|
177
|
+
const headingId = useId();
|
|
178
|
+
|
|
179
|
+
const isControlled = approvedProp !== undefined;
|
|
180
|
+
|
|
181
|
+
// Uncontrolled internal state.
|
|
182
|
+
const [internalApproved, setInternalApproved] = useState<Set<string>>(() => new Set());
|
|
183
|
+
|
|
184
|
+
const resolvedApproved: ReadonlySet<string> = isControlled
|
|
185
|
+
? approvedProp instanceof Set
|
|
186
|
+
? approvedProp
|
|
187
|
+
: new Set(approvedProp)
|
|
188
|
+
: internalApproved;
|
|
189
|
+
|
|
190
|
+
const hunkIds = useMemo(() => hunks.map((h) => h.id), [hunks]);
|
|
191
|
+
|
|
192
|
+
const toggle = useCallback(
|
|
193
|
+
(id: string, next: boolean) => {
|
|
194
|
+
onToggle?.(id, next);
|
|
195
|
+
if (!isControlled) {
|
|
196
|
+
setInternalApproved((prev) => {
|
|
197
|
+
const next_ = new Set(prev);
|
|
198
|
+
if (next) next_.add(id);
|
|
199
|
+
else next_.delete(id);
|
|
200
|
+
return next_;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
[isControlled, onToggle],
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const approveAll = useCallback(() => {
|
|
208
|
+
onApproveAll?.();
|
|
209
|
+
if (!isControlled) {
|
|
210
|
+
setInternalApproved(new Set(hunkIds));
|
|
211
|
+
}
|
|
212
|
+
}, [isControlled, hunkIds, onApproveAll]);
|
|
213
|
+
|
|
214
|
+
const rejectAll = useCallback(() => {
|
|
215
|
+
onRejectAll?.();
|
|
216
|
+
if (!isControlled) {
|
|
217
|
+
setInternalApproved(new Set());
|
|
218
|
+
}
|
|
219
|
+
}, [isControlled, onRejectAll]);
|
|
220
|
+
|
|
221
|
+
const approvedCount = hunkIds.filter((id) => resolvedApproved.has(id)).length;
|
|
222
|
+
const totalCount = hunkIds.length;
|
|
223
|
+
|
|
224
|
+
const state: ChangeReviewState = {
|
|
225
|
+
approved: resolvedApproved,
|
|
226
|
+
totalCount,
|
|
227
|
+
approvedCount,
|
|
228
|
+
allApproved: totalCount > 0 && approvedCount === totalCount,
|
|
229
|
+
noneApproved: approvedCount === 0,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const actions: ChangeReviewActions = { toggle, approveAll, rejectAll };
|
|
233
|
+
|
|
234
|
+
const value = useMemo(
|
|
235
|
+
() => ({ state, actions, hunks, headingId }),
|
|
236
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
237
|
+
[state.approved, state.totalCount, state.approvedCount, hunks, headingId],
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
return <ChangeReviewContext.Provider value={value}>{children}</ChangeReviewContext.Provider>;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─── Header ───────────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
export type ChangeReviewHeaderProps = HTMLAttributes<HTMLDivElement>;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Count summary ("3 of 5 approved") + Approve all / Reject all bulk actions.
|
|
249
|
+
* `aria-controls` is intentionally omitted — screen readers reach the list
|
|
250
|
+
* naturally in DOM order.
|
|
251
|
+
*/
|
|
252
|
+
export const ChangeReviewHeader = forwardRef<HTMLDivElement, ChangeReviewHeaderProps>(
|
|
253
|
+
function ChangeReviewHeader({ className, ...props }, ref) {
|
|
254
|
+
const { state, actions, headingId } = useChangeReview();
|
|
255
|
+
const { approvedCount, totalCount, allApproved, noneApproved } = state;
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<div
|
|
259
|
+
ref={ref}
|
|
260
|
+
className={cn("flex flex-wrap items-center justify-between gap-3 pb-3", className)}
|
|
261
|
+
{...props}
|
|
262
|
+
>
|
|
263
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
264
|
+
<span id={headingId} className="text-subtitle font-semibold text-foreground truncate">
|
|
265
|
+
Review changes
|
|
266
|
+
</span>
|
|
267
|
+
<Badge variant={allApproved ? "success" : approvedCount > 0 ? "info" : "secondary"}>
|
|
268
|
+
{approvedCount} of {totalCount} approved
|
|
269
|
+
</Badge>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
{totalCount > 0 && (
|
|
273
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
274
|
+
<Button
|
|
275
|
+
size="sm"
|
|
276
|
+
variant="outline-subtle"
|
|
277
|
+
disabled={noneApproved}
|
|
278
|
+
onClick={actions.rejectAll}
|
|
279
|
+
aria-label="Reject all changes"
|
|
280
|
+
>
|
|
281
|
+
<X aria-hidden="true" />
|
|
282
|
+
Reject all
|
|
283
|
+
</Button>
|
|
284
|
+
<Button
|
|
285
|
+
size="sm"
|
|
286
|
+
variant={allApproved ? "secondary" : "default"}
|
|
287
|
+
disabled={allApproved}
|
|
288
|
+
onClick={actions.approveAll}
|
|
289
|
+
aria-label="Approve all changes"
|
|
290
|
+
>
|
|
291
|
+
<CheckCheck aria-hidden="true" />
|
|
292
|
+
Approve all
|
|
293
|
+
</Button>
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
},
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// ─── Provenance ───────────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
export interface ChangeReviewProvenanceProps extends HTMLAttributes<HTMLDivElement> {
|
|
304
|
+
/**
|
|
305
|
+
* Structured provenance object OR any ReactNode for custom rendering.
|
|
306
|
+
* When a `ChangeProvenance` object is passed, a default attribution line
|
|
307
|
+
* is rendered. Pass a ReactNode for full control.
|
|
308
|
+
*/
|
|
309
|
+
provenance: ReactNode | ChangeProvenance;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function isChangeProvenance(v: unknown): v is ChangeProvenance {
|
|
313
|
+
return (
|
|
314
|
+
typeof v === "object" &&
|
|
315
|
+
v !== null &&
|
|
316
|
+
!Array.isArray(v) &&
|
|
317
|
+
("author" in v || "model" in v || "timestamp" in v || "note" in v)
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Quiet attribution line showing who/what proposed the change set.
|
|
323
|
+
* Uses `text-muted-foreground` and small typography — it should not compete
|
|
324
|
+
* with the hunks themselves.
|
|
325
|
+
*/
|
|
326
|
+
export const ChangeReviewProvenance = forwardRef<HTMLDivElement, ChangeReviewProvenanceProps>(
|
|
327
|
+
function ChangeReviewProvenance({ provenance, className, ...props }, ref) {
|
|
328
|
+
if (isChangeProvenance(provenance)) {
|
|
329
|
+
const { author, model, timestamp, note } = provenance;
|
|
330
|
+
return (
|
|
331
|
+
<div
|
|
332
|
+
ref={ref}
|
|
333
|
+
className={cn(
|
|
334
|
+
"flex flex-wrap items-center gap-x-3 gap-y-1 text-caption text-muted-foreground pb-3",
|
|
335
|
+
className,
|
|
336
|
+
)}
|
|
337
|
+
aria-label="Change provenance"
|
|
338
|
+
{...props}
|
|
339
|
+
>
|
|
340
|
+
{author && (
|
|
341
|
+
<span className="flex items-center gap-1">
|
|
342
|
+
<User aria-hidden="true" className="size-3" />
|
|
343
|
+
<span>{author}</span>
|
|
344
|
+
</span>
|
|
345
|
+
)}
|
|
346
|
+
{model && (
|
|
347
|
+
<span className="flex items-center gap-1">
|
|
348
|
+
<Bot aria-hidden="true" className="size-3" />
|
|
349
|
+
<span translate="no">{model}</span>
|
|
350
|
+
</span>
|
|
351
|
+
)}
|
|
352
|
+
{timestamp && (
|
|
353
|
+
<span className="flex items-center gap-1">
|
|
354
|
+
<Clock aria-hidden="true" className="size-3" />
|
|
355
|
+
<time>{timestamp}</time>
|
|
356
|
+
</span>
|
|
357
|
+
)}
|
|
358
|
+
{note && <span className="text-muted-foreground/70">{note}</span>}
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Custom ReactNode provenance
|
|
364
|
+
return (
|
|
365
|
+
<div
|
|
366
|
+
ref={ref}
|
|
367
|
+
className={cn("pb-3 text-caption text-muted-foreground", className)}
|
|
368
|
+
aria-label="Change provenance"
|
|
369
|
+
{...props}
|
|
370
|
+
>
|
|
371
|
+
{provenance as ReactNode}
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
374
|
+
},
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
// ─── List ─────────────────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
export type ChangeReviewListProps = HTMLAttributes<HTMLOListElement>;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Ordered list of change hunks. Uses `<ol>` so AT users can navigate by
|
|
383
|
+
* item number ("item 2 of 5"). Renders a StatePanel when the list is empty.
|
|
384
|
+
*/
|
|
385
|
+
export const ChangeReviewList = forwardRef<HTMLOListElement, ChangeReviewListProps>(
|
|
386
|
+
function ChangeReviewList({ className, children, ...props }, ref) {
|
|
387
|
+
const { hunks, headingId } = useChangeReview();
|
|
388
|
+
|
|
389
|
+
if (hunks.length === 0) {
|
|
390
|
+
return (
|
|
391
|
+
<StatePanel
|
|
392
|
+
kind="empty"
|
|
393
|
+
title="No changes to review"
|
|
394
|
+
description="When an agent proposes edits, they'll appear here for your approval."
|
|
395
|
+
className="my-2"
|
|
396
|
+
/>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return (
|
|
401
|
+
<ol
|
|
402
|
+
ref={ref}
|
|
403
|
+
aria-labelledby={headingId}
|
|
404
|
+
className={cn("flex flex-col gap-3", className)}
|
|
405
|
+
{...props}
|
|
406
|
+
>
|
|
407
|
+
{children}
|
|
408
|
+
</ol>
|
|
409
|
+
);
|
|
410
|
+
},
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// ─── Hunk ─────────────────────────────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
const HUNK_STATUS_ICONS: Record<ChangeHunkStatus, ReactNode> = {
|
|
416
|
+
added: (
|
|
417
|
+
<span className="text-success-text font-bold text-meta" aria-hidden="true">
|
|
418
|
+
+
|
|
419
|
+
</span>
|
|
420
|
+
),
|
|
421
|
+
removed: (
|
|
422
|
+
<span className="text-destructive font-bold text-meta" aria-hidden="true">
|
|
423
|
+
−
|
|
424
|
+
</span>
|
|
425
|
+
),
|
|
426
|
+
modified: (
|
|
427
|
+
<span className="text-info-text font-bold text-meta" aria-hidden="true">
|
|
428
|
+
~
|
|
429
|
+
</span>
|
|
430
|
+
),
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const HUNK_STATUS_LABELS: Record<ChangeHunkStatus, string> = {
|
|
434
|
+
added: "added",
|
|
435
|
+
removed: "removed",
|
|
436
|
+
modified: "modified",
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
export interface ChangeReviewHunkProps
|
|
440
|
+
extends
|
|
441
|
+
Omit<HTMLAttributes<HTMLLIElement>, "children">,
|
|
442
|
+
Omit<VariantProps<typeof hunkVariants>, "approved"> {
|
|
443
|
+
hunk: ChangeHunk;
|
|
444
|
+
/**
|
|
445
|
+
* Override the default before/after layout. Receives the hunk data and
|
|
446
|
+
* current approval state. Return `null` to suppress the default content area.
|
|
447
|
+
*/
|
|
448
|
+
renderHunk?: (hunk: ChangeHunk, state: { approved: boolean }) => ReactNode;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* A single change hunk row.
|
|
453
|
+
*
|
|
454
|
+
* Approval state is signalled with TWO non-color cues:
|
|
455
|
+
* - icon: Check (approved) vs plain toggle (pending)
|
|
456
|
+
* - label: "Approved" badge vs "Approve" button text
|
|
457
|
+
* This ensures the state is clear in all three themes including the low-chroma
|
|
458
|
+
* monochrome surfaces (colorblind-safe).
|
|
459
|
+
*/
|
|
460
|
+
export const ChangeReviewHunk = forwardRef<HTMLLIElement, ChangeReviewHunkProps>(
|
|
461
|
+
function ChangeReviewHunk({ hunk, renderHunk, className, ...props }, ref) {
|
|
462
|
+
const { state, actions } = useChangeReview();
|
|
463
|
+
const { id, title, before, after, status = "modified", provenance } = hunk;
|
|
464
|
+
const approved = state.approved.has(id);
|
|
465
|
+
|
|
466
|
+
const statusLabel = HUNK_STATUS_LABELS[status];
|
|
467
|
+
const toggleLabel = approved ? `Reject hunk: ${title ?? id}` : `Approve hunk: ${title ?? id}`;
|
|
468
|
+
|
|
469
|
+
// Quiet, no-fill diff: before = struck muted text with a "−" marker, after =
|
|
470
|
+
// foreground text with a "+" marker — separated by space, NOT colored
|
|
471
|
+
// fill+border boxes. The hunk's status rail (added/removed/modified) carries
|
|
472
|
+
// the polarity; the markers + tone are the second, non-color cue.
|
|
473
|
+
const defaultContent = renderHunk
|
|
474
|
+
? renderHunk(hunk, { approved })
|
|
475
|
+
: (before !== undefined || after !== undefined) && (
|
|
476
|
+
<div className="flex flex-col gap-1.5 text-body">
|
|
477
|
+
{before !== undefined && (
|
|
478
|
+
<div className="flex gap-2">
|
|
479
|
+
<span
|
|
480
|
+
aria-hidden="true"
|
|
481
|
+
className="shrink-0 select-none font-mono text-muted-foreground"
|
|
482
|
+
>
|
|
483
|
+
−
|
|
484
|
+
</span>
|
|
485
|
+
<span className="sr-only">Before: </span>
|
|
486
|
+
<div className="min-w-0 break-words text-muted-foreground line-through decoration-muted-foreground/30">
|
|
487
|
+
{before}
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
)}
|
|
491
|
+
{after !== undefined && (
|
|
492
|
+
<div className="flex gap-2">
|
|
493
|
+
<span aria-hidden="true" className="shrink-0 select-none font-mono text-foreground">
|
|
494
|
+
+
|
|
495
|
+
</span>
|
|
496
|
+
<span className="sr-only">After: </span>
|
|
497
|
+
<div className="min-w-0 break-words text-foreground">{after}</div>
|
|
498
|
+
</div>
|
|
499
|
+
)}
|
|
500
|
+
</div>
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
return (
|
|
504
|
+
<li
|
|
505
|
+
ref={ref}
|
|
506
|
+
data-hunk-id={id}
|
|
507
|
+
data-approved={approved}
|
|
508
|
+
className={cn(hunkVariants({ status }), "flex flex-col gap-3 p-4", className)}
|
|
509
|
+
{...props}
|
|
510
|
+
>
|
|
511
|
+
{/* Hunk header: status icon + title + per-hunk provenance + approve toggle */}
|
|
512
|
+
<div className="flex items-start justify-between gap-3 min-w-0">
|
|
513
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
514
|
+
{/* Status indicator: NON-color cue via symbol + color together */}
|
|
515
|
+
<span
|
|
516
|
+
aria-label={`Change type: ${statusLabel}`}
|
|
517
|
+
className="shrink-0 inline-flex items-center justify-center size-5"
|
|
518
|
+
>
|
|
519
|
+
{HUNK_STATUS_ICONS[status]}
|
|
520
|
+
</span>
|
|
521
|
+
|
|
522
|
+
<span className="text-body font-medium text-foreground truncate">{title ?? id}</span>
|
|
523
|
+
|
|
524
|
+
{/* Per-hunk provenance (quiet, only when it differs from review-level) */}
|
|
525
|
+
{provenance?.author && (
|
|
526
|
+
<span className="text-caption text-muted-foreground hidden sm:inline truncate">
|
|
527
|
+
{provenance.author}
|
|
528
|
+
</span>
|
|
529
|
+
)}
|
|
530
|
+
</div>
|
|
531
|
+
|
|
532
|
+
{/* Approval toggle — must be a real button with accessible name */}
|
|
533
|
+
<Button
|
|
534
|
+
size="icon-sm"
|
|
535
|
+
variant={approved ? "default" : "outline-subtle"}
|
|
536
|
+
aria-label={toggleLabel}
|
|
537
|
+
aria-pressed={approved}
|
|
538
|
+
onClick={() => actions.toggle(id, !approved)}
|
|
539
|
+
className={cn(
|
|
540
|
+
"shrink-0 transition-colors duration-fast motion-reduce:transition-none",
|
|
541
|
+
approved && "bg-primary text-primary-foreground",
|
|
542
|
+
)}
|
|
543
|
+
>
|
|
544
|
+
{approved ? (
|
|
545
|
+
<Check aria-hidden="true" className="size-4" />
|
|
546
|
+
) : (
|
|
547
|
+
<Check aria-hidden="true" className="size-4 opacity-40" />
|
|
548
|
+
)}
|
|
549
|
+
</Button>
|
|
550
|
+
</div>
|
|
551
|
+
|
|
552
|
+
{/* Visual approved badge — second non-color cue (icon alone is not enough) */}
|
|
553
|
+
{approved && (
|
|
554
|
+
<div className="flex items-center gap-1.5">
|
|
555
|
+
<Badge variant="success" className="animate-in fade-in-0 duration-fast ease-entrance">
|
|
556
|
+
<Check aria-hidden="true" className="size-3" />
|
|
557
|
+
Approved
|
|
558
|
+
</Badge>
|
|
559
|
+
</div>
|
|
560
|
+
)}
|
|
561
|
+
|
|
562
|
+
{/* Content area: before / after or custom renderHunk */}
|
|
563
|
+
{defaultContent && <div>{defaultContent}</div>}
|
|
564
|
+
</li>
|
|
565
|
+
);
|
|
566
|
+
},
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
// ─── Root (convenience wrapper) ───────────────────────────────────────────────
|
|
570
|
+
|
|
571
|
+
export interface ChangeReviewProps
|
|
572
|
+
// Omit "children" and "onToggle" from div attrs — "onToggle" is a React 19
|
|
573
|
+
// native ToggleEvent handler that conflicts with our review onToggle prop.
|
|
574
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "onToggle"> {
|
|
575
|
+
/** The proposed changes to review. */
|
|
576
|
+
hunks: ChangeHunk[];
|
|
577
|
+
/**
|
|
578
|
+
* Controlled approved set. Pass `Set<string>` or a string array.
|
|
579
|
+
* When omitted the component manages approval state internally (uncontrolled).
|
|
580
|
+
*/
|
|
581
|
+
approved?: Set<string> | string[];
|
|
582
|
+
/** Called when the user toggles a single hunk's approval state. */
|
|
583
|
+
onToggle?: (id: string, next: boolean) => void;
|
|
584
|
+
/** Called when the user clicks "Approve all". */
|
|
585
|
+
onApproveAll?: () => void;
|
|
586
|
+
/** Called when the user clicks "Reject all". */
|
|
587
|
+
onRejectAll?: () => void;
|
|
588
|
+
/**
|
|
589
|
+
* Provenance to show above the hunk list. Accepts a `ChangeProvenance`
|
|
590
|
+
* object (renders default attribution) or any ReactNode.
|
|
591
|
+
*/
|
|
592
|
+
provenance?: ReactNode | ChangeProvenance;
|
|
593
|
+
/**
|
|
594
|
+
* Override the per-hunk body renderer (passed through to each `ChangeReviewHunk`).
|
|
595
|
+
*/
|
|
596
|
+
renderHunk?: ChangeReviewHunkProps["renderHunk"];
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Convenience composition that wires Provider → Header → Provenance → List → Hunks.
|
|
601
|
+
* For custom layouts compose the parts manually via `ChangeReview.Provider`,
|
|
602
|
+
* `ChangeReview.Header`, etc.
|
|
603
|
+
*/
|
|
604
|
+
export const ChangeReview = forwardRef<HTMLDivElement, ChangeReviewProps>(function ChangeReview(
|
|
605
|
+
{
|
|
606
|
+
hunks,
|
|
607
|
+
approved,
|
|
608
|
+
onToggle,
|
|
609
|
+
onApproveAll,
|
|
610
|
+
onRejectAll,
|
|
611
|
+
provenance,
|
|
612
|
+
renderHunk,
|
|
613
|
+
className,
|
|
614
|
+
...props
|
|
615
|
+
},
|
|
616
|
+
ref,
|
|
617
|
+
) {
|
|
618
|
+
return (
|
|
619
|
+
<ChangeReviewProvider
|
|
620
|
+
hunks={hunks}
|
|
621
|
+
approved={approved}
|
|
622
|
+
onToggle={onToggle}
|
|
623
|
+
onApproveAll={onApproveAll}
|
|
624
|
+
onRejectAll={onRejectAll}
|
|
625
|
+
>
|
|
626
|
+
<div ref={ref} className={cn("flex flex-col w-full", className)} {...props}>
|
|
627
|
+
<ChangeReviewHeader />
|
|
628
|
+
{provenance != null && <ChangeReviewProvenance provenance={provenance} />}
|
|
629
|
+
<ChangeReviewList>
|
|
630
|
+
{hunks.map((hunk) => (
|
|
631
|
+
<ChangeReviewHunk key={hunk.id} hunk={hunk} renderHunk={renderHunk} />
|
|
632
|
+
))}
|
|
633
|
+
</ChangeReviewList>
|
|
634
|
+
</div>
|
|
635
|
+
</ChangeReviewProvider>
|
|
636
|
+
);
|
|
637
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ChangeReview,
|
|
3
|
+
ChangeReviewProvider,
|
|
4
|
+
ChangeReviewHeader,
|
|
5
|
+
ChangeReviewProvenance,
|
|
6
|
+
ChangeReviewList,
|
|
7
|
+
ChangeReviewHunk,
|
|
8
|
+
hunkVariants,
|
|
9
|
+
} from "./change-review";
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
ChangeHunk,
|
|
13
|
+
ChangeHunkStatus,
|
|
14
|
+
ChangeProvenance,
|
|
15
|
+
ChangeReviewState,
|
|
16
|
+
ChangeReviewActions,
|
|
17
|
+
ChangeReviewProps,
|
|
18
|
+
ChangeReviewProviderProps,
|
|
19
|
+
ChangeReviewHeaderProps,
|
|
20
|
+
ChangeReviewProvenanceProps,
|
|
21
|
+
ChangeReviewListProps,
|
|
22
|
+
ChangeReviewHunkProps,
|
|
23
|
+
} from "./change-review";
|