@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,463 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, userEvent, waitFor, within } from "storybook/test";
|
|
3
|
+
import { useId, useMemo, useState } from "react";
|
|
4
|
+
import { Label } from "../label";
|
|
5
|
+
import {
|
|
6
|
+
MentionInput,
|
|
7
|
+
MentionInputContent,
|
|
8
|
+
MentionInputEmpty,
|
|
9
|
+
MentionInputItem,
|
|
10
|
+
MentionInputList,
|
|
11
|
+
MentionInputTextarea,
|
|
12
|
+
} from "./mention-input";
|
|
13
|
+
import { serializeMentions, type MentionOption, type MentionValue } from "./mention-value";
|
|
14
|
+
|
|
15
|
+
const ROSTER: MentionOption[] = [
|
|
16
|
+
{ id: "u-lovelace", label: "Ada Lovelace", description: "Analytical Engine", keywords: ["math"] },
|
|
17
|
+
{ id: "u-hopper", label: "Grace Hopper", description: "Compilers", keywords: ["cobol"] },
|
|
18
|
+
{ id: "u-johnson", label: "Katherine Johnson", description: "Orbital mechanics" },
|
|
19
|
+
{ id: "u-noether", label: "Emmy Noether", description: "Abstract algebra" },
|
|
20
|
+
{ id: "u-turing", label: "Alan Turing", description: "On sabbatical", disabled: true },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `MentionInput` is an `@`-mention text field built on a **real `<textarea>`**.
|
|
25
|
+
*
|
|
26
|
+
* Typing the trigger character at the start of a word opens the component's own
|
|
27
|
+
* listbox; arrow keys move the highlight, Enter or Tab inserts, Escape dismisses
|
|
28
|
+
* — and **focus never leaves the field**, so the caret is never lost. An
|
|
29
|
+
* inserted mention behaves as one atomic unit: a single Backspace beside it
|
|
30
|
+
* removes the whole `@Name`, and the arrow keys step over it rather than into
|
|
31
|
+
* it.
|
|
32
|
+
*
|
|
33
|
+
* The value is `{ text, mentions }`; `serializeMentions()` turns it into the
|
|
34
|
+
* `{ text, mentionedIds }` payload an API wants.
|
|
35
|
+
*/
|
|
36
|
+
const meta = {
|
|
37
|
+
title: "Forms/MentionInput",
|
|
38
|
+
component: MentionInput,
|
|
39
|
+
tags: ["autodocs"],
|
|
40
|
+
parameters: {
|
|
41
|
+
docs: {
|
|
42
|
+
description: {
|
|
43
|
+
component:
|
|
44
|
+
"An @-mention text field on a real <textarea>. Focus never leaves the field; " +
|
|
45
|
+
"mentions behave as atomic tokens; the value serializes to { text, mentionedIds }.",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
argTypes: {
|
|
50
|
+
options: { control: false, table: { category: "Content" } },
|
|
51
|
+
value: { control: false, table: { category: "State" } },
|
|
52
|
+
defaultValue: { control: false, table: { category: "State" } },
|
|
53
|
+
open: { control: false, table: { category: "State" } },
|
|
54
|
+
trigger: { control: "text", table: { category: "Behavior" } },
|
|
55
|
+
filter: { control: false, table: { category: "Behavior" } },
|
|
56
|
+
},
|
|
57
|
+
} satisfies Meta<typeof MentionInput>;
|
|
58
|
+
|
|
59
|
+
export default meta;
|
|
60
|
+
type Story = StoryObj<typeof meta>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The full compound arrangement every story below composes.
|
|
64
|
+
*
|
|
65
|
+
* Note the consumer-supplied `id` on `MentionInputTextarea`: it wins over the
|
|
66
|
+
* component's internal one (props are spread last), which is what keeps
|
|
67
|
+
* `<Label htmlFor>` working. `useId` rather than a literal because autodocs
|
|
68
|
+
* renders every story on one page and duplicate ids are an axe violation.
|
|
69
|
+
*/
|
|
70
|
+
function Field({
|
|
71
|
+
options = ROSTER,
|
|
72
|
+
label = "Comment",
|
|
73
|
+
...props
|
|
74
|
+
}: Partial<React.ComponentProps<typeof MentionInput>> & {
|
|
75
|
+
options?: MentionOption[];
|
|
76
|
+
label?: string;
|
|
77
|
+
}) {
|
|
78
|
+
const id = useId();
|
|
79
|
+
return (
|
|
80
|
+
<div className="w-[28rem] space-y-2">
|
|
81
|
+
<Label htmlFor={id}>{label}</Label>
|
|
82
|
+
<MentionInput options={options} {...props}>
|
|
83
|
+
<MentionInputTextarea id={id} placeholder="Type @ to mention someone…" />
|
|
84
|
+
<MentionInputContent>
|
|
85
|
+
<MentionInputList>
|
|
86
|
+
{(option) => <MentionInputItem key={option.id} option={option} />}
|
|
87
|
+
</MentionInputList>
|
|
88
|
+
<MentionInputEmpty />
|
|
89
|
+
</MentionInputContent>
|
|
90
|
+
</MentionInput>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Resting state — an empty field with nothing but its placeholder. */
|
|
96
|
+
export const Empty: Story = {
|
|
97
|
+
args: { options: ROSTER, children: null },
|
|
98
|
+
render: () => <Field />,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Ordinary prose already in the field, with the popup closed. */
|
|
102
|
+
export const Typing: Story = {
|
|
103
|
+
args: { options: ROSTER, children: null },
|
|
104
|
+
render: () => (
|
|
105
|
+
<Field defaultValue={{ text: "Thanks for the review — one follow-up ", mentions: [] }} />
|
|
106
|
+
),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The popup open on a partial query, driven entirely from the keyboard. The
|
|
111
|
+
* play function types the trigger and arrows down, then asserts the field is
|
|
112
|
+
* still the focused element and that `aria-activedescendant` names the option
|
|
113
|
+
* carrying `aria-selected="true"`.
|
|
114
|
+
*/
|
|
115
|
+
export const PopupOpen: Story = {
|
|
116
|
+
args: { options: ROSTER, children: null },
|
|
117
|
+
render: () => <Field />,
|
|
118
|
+
play: async ({ canvasElement, step }) => {
|
|
119
|
+
const canvas = within(canvasElement);
|
|
120
|
+
const field = canvas.getByRole("textbox", { name: "Comment" });
|
|
121
|
+
|
|
122
|
+
await step("typing the trigger opens the roster", async () => {
|
|
123
|
+
await userEvent.click(field);
|
|
124
|
+
await userEvent.keyboard("Nice work @");
|
|
125
|
+
await expect(field).toHaveAttribute("data-state", "open");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await step("arrow keys move the highlight without moving focus", async () => {
|
|
129
|
+
await userEvent.keyboard("{ArrowDown}");
|
|
130
|
+
await expect(field).toHaveFocus();
|
|
131
|
+
|
|
132
|
+
const activeId = field.getAttribute("aria-activedescendant");
|
|
133
|
+
await expect(activeId).toBeTruthy();
|
|
134
|
+
const active = document.getElementById(activeId!);
|
|
135
|
+
await expect(active).not.toBeNull();
|
|
136
|
+
await expect(active).toHaveAttribute("aria-selected", "true");
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Two inserted mentions. Each `@Name` run is painted by an `aria-hidden` mirror
|
|
143
|
+
* layer sitting over the field, so it reads as a chip while the text underneath
|
|
144
|
+
* stays ordinary, selectable, spellcheckable textarea content.
|
|
145
|
+
*
|
|
146
|
+
* The play function proves atomicity: one Backspace beside a mention removes
|
|
147
|
+
* the entire token.
|
|
148
|
+
*/
|
|
149
|
+
export const WithChips: Story = {
|
|
150
|
+
args: { options: ROSTER, children: null },
|
|
151
|
+
render: () => (
|
|
152
|
+
<Field
|
|
153
|
+
defaultValue={{
|
|
154
|
+
text: "@Ada Lovelace and @Grace Hopper should both see this before it ships.",
|
|
155
|
+
mentions: [
|
|
156
|
+
{ id: "u-lovelace", label: "Ada Lovelace", start: 0 },
|
|
157
|
+
{ id: "u-hopper", label: "Grace Hopper", start: 18 },
|
|
158
|
+
],
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
),
|
|
162
|
+
play: async ({ canvasElement, step }) => {
|
|
163
|
+
const canvas = within(canvasElement);
|
|
164
|
+
const field = canvas.getByRole("textbox", { name: "Comment" }) as HTMLTextAreaElement;
|
|
165
|
+
|
|
166
|
+
await step("one Backspace removes the whole @Grace Hopper token", async () => {
|
|
167
|
+
await userEvent.click(field);
|
|
168
|
+
// Caret to the end offset of the second mention: 18 + "@Grace Hopper".length
|
|
169
|
+
field.setSelectionRange(31, 31);
|
|
170
|
+
field.dispatchEvent(new Event("select", { bubbles: true }));
|
|
171
|
+
await userEvent.keyboard("{Backspace}");
|
|
172
|
+
await expect(field.value).toBe("@Ada Lovelace and should both see this before it ships.");
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* What a consumer actually sends. `serializeMentions()` returns
|
|
179
|
+
* `{ text, mentionedIds }` with the ids **deduped** and in **document order**.
|
|
180
|
+
*/
|
|
181
|
+
export const SerializedOutput: Story = {
|
|
182
|
+
args: { options: ROSTER, children: null },
|
|
183
|
+
render: function SerializedOutputStory() {
|
|
184
|
+
const id = useId();
|
|
185
|
+
const [value, setValue] = useState<MentionValue>({
|
|
186
|
+
text: "@Ada Lovelace can you review this with @Grace Hopper and @Ada Lovelace again?",
|
|
187
|
+
mentions: [
|
|
188
|
+
{ id: "u-lovelace", label: "Ada Lovelace", start: 0 },
|
|
189
|
+
{ id: "u-hopper", label: "Grace Hopper", start: 39 },
|
|
190
|
+
{ id: "u-lovelace", label: "Ada Lovelace", start: 57 },
|
|
191
|
+
],
|
|
192
|
+
});
|
|
193
|
+
const payload = useMemo(() => serializeMentions(value), [value]);
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div className="w-[28rem] space-y-4">
|
|
197
|
+
<div className="space-y-2">
|
|
198
|
+
<Label htmlFor={id}>Comment</Label>
|
|
199
|
+
<MentionInput options={ROSTER} value={value} onValueChange={setValue}>
|
|
200
|
+
<MentionInputTextarea id={id} placeholder="Type @ to mention someone…" />
|
|
201
|
+
<MentionInputContent>
|
|
202
|
+
<MentionInputList>
|
|
203
|
+
{(option) => <MentionInputItem key={option.id} option={option} />}
|
|
204
|
+
</MentionInputList>
|
|
205
|
+
<MentionInputEmpty />
|
|
206
|
+
</MentionInputContent>
|
|
207
|
+
</MentionInput>
|
|
208
|
+
</div>
|
|
209
|
+
{/* A scrollable region needs to be keyboard-reachable (axe
|
|
210
|
+
`scrollable-region-focusable`), and a focusable region needs a name. */}
|
|
211
|
+
<pre
|
|
212
|
+
tabIndex={0}
|
|
213
|
+
role="region"
|
|
214
|
+
aria-label="Serialized payload"
|
|
215
|
+
className="overflow-x-auto rounded-md bg-surface-muted p-3 font-mono text-meta"
|
|
216
|
+
>
|
|
217
|
+
{JSON.stringify(payload, null, 2)}
|
|
218
|
+
</pre>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* An app-side roster. The consumer passes `filter={() => true}` (the popup shows
|
|
226
|
+
* whatever it is given) and fetches from `onQueryChange`. There is deliberately
|
|
227
|
+
* no `loading` prop — render whatever placeholder the surface wants inside
|
|
228
|
+
* `MentionInputEmpty`.
|
|
229
|
+
*/
|
|
230
|
+
export const AsyncRoster: Story = {
|
|
231
|
+
args: { options: ROSTER, children: null },
|
|
232
|
+
render: function AsyncRosterStory() {
|
|
233
|
+
const id = useId();
|
|
234
|
+
const [query, setQuery] = useState<string | null>(null);
|
|
235
|
+
const [options, setOptions] = useState<MentionOption[]>([]);
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div className="w-[28rem] space-y-2">
|
|
239
|
+
<Label htmlFor={id}>Assign reviewers</Label>
|
|
240
|
+
<MentionInput
|
|
241
|
+
options={options}
|
|
242
|
+
filter={() => true}
|
|
243
|
+
onQueryChange={(next) => {
|
|
244
|
+
setQuery(next);
|
|
245
|
+
// Stands in for a request; the roster is owned by the app.
|
|
246
|
+
setOptions(
|
|
247
|
+
next === null
|
|
248
|
+
? []
|
|
249
|
+
: ROSTER.filter((option) =>
|
|
250
|
+
option.label.toLowerCase().includes(next.toLowerCase()),
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
<MentionInputTextarea id={id} placeholder="Type @ to mention someone…" />
|
|
256
|
+
<MentionInputContent>
|
|
257
|
+
<MentionInputList>
|
|
258
|
+
{(option) => <MentionInputItem key={option.id} option={option} />}
|
|
259
|
+
</MentionInputList>
|
|
260
|
+
<MentionInputEmpty>
|
|
261
|
+
{query === null || query.length === 0 ? "Type to search…" : "No matches."}
|
|
262
|
+
</MentionInputEmpty>
|
|
263
|
+
</MentionInputContent>
|
|
264
|
+
</MentionInput>
|
|
265
|
+
<p className="text-meta text-muted-foreground">
|
|
266
|
+
Last query sent to the app: {query === null ? "— (popup closed)" : `"${query}"`}
|
|
267
|
+
</p>
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/** A non-default trigger — the same machinery drives `#`-style references. */
|
|
274
|
+
export const CustomTrigger: Story = {
|
|
275
|
+
args: { options: ROSTER, children: null },
|
|
276
|
+
render: () => (
|
|
277
|
+
<Field
|
|
278
|
+
label="Link an issue"
|
|
279
|
+
trigger="#"
|
|
280
|
+
options={[
|
|
281
|
+
{ id: "i-101", label: "Roadmap", description: "Milestone" },
|
|
282
|
+
{ id: "i-102", label: "Retention", description: "Epic" },
|
|
283
|
+
{ id: "i-103", label: "Rendering", description: "Epic" },
|
|
284
|
+
]}
|
|
285
|
+
/>
|
|
286
|
+
),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/* -------------------------------------------------------------------------- */
|
|
290
|
+
/* Mirror re-measure regression locks */
|
|
291
|
+
/* */
|
|
292
|
+
/* The chip overlay copies the field's glyph metrics into an inline style. A */
|
|
293
|
+
/* ONE-SHOT snapshot is not enough: the first layout effect runs before the */
|
|
294
|
+
/* theme attribute is applied, so it captured the `:root` fallback font and the */
|
|
295
|
+
/* wash rendered ~14% wide in a different typeface. The component re-measures */
|
|
296
|
+
/* on a MutationObserver (root `data-theme`/`data-density`), a ResizeObserver */
|
|
297
|
+
/* (the field's box) and `document.fonts.ready`. */
|
|
298
|
+
/* */
|
|
299
|
+
/* They MUST run in a real browser: the drift was invisible to a */
|
|
300
|
+
/* self-consistent `getComputedStyle` read and jsdom has no layout at all. */
|
|
301
|
+
/* */
|
|
302
|
+
/* WHAT THESE ACTUALLY LOCK — measured by deleting each arm and re-running, */
|
|
303
|
+
/* twice per arm. Corrected in fix round 3: an earlier version of this block */
|
|
304
|
+
/* claimed `ResizeObserver` was the ONLY individually-pinnable arm and that it */
|
|
305
|
+
/* "re-measures in every scenario that could be constructed". A reviewer built */
|
|
306
|
+
/* a counterexample, so that generalisation was false — it described the */
|
|
307
|
+
/* scenarios that had been tried, not the code. */
|
|
308
|
+
/* */
|
|
309
|
+
/* * All three arms removed -> 3 stories RED with the ORIGINAL symptom */
|
|
310
|
+
/* (overlay stuck on `Inter` while the field renders IBM Plex Mono). */
|
|
311
|
+
/* * `ResizeObserver` alone removed -> `MirrorTracksFieldResize` RED. */
|
|
312
|
+
/* * `MutationObserver` alone removed -> the nailed-box theme-flip story */
|
|
313
|
+
/* RED. It lives in `mention-input-mirror.stories.tsx`, NOT here: with it */
|
|
314
|
+
/* co-resident in this file the same deletion came back green, because a */
|
|
315
|
+
/* preceding story in the shared page re-measured the mirror for an */
|
|
316
|
+
/* unrelated reason. Its own file is what makes the lock hold in CI. */
|
|
317
|
+
/* * `document.fonts.ready` alone removed -> everything still green. It */
|
|
318
|
+
/* resisted every scenario ATTEMPTED (theme flip; pinned box; nailed box; */
|
|
319
|
+
/* a field-level letter-spacing change; a late-loading face above the */
|
|
320
|
+
/* field shifting its offsetTop). That is a statement about those */
|
|
321
|
+
/* scenarios, not a proof the arm is dead — it is kept as defence in */
|
|
322
|
+
/* depth, and is covered compositely by the all-arms case above. */
|
|
323
|
+
/* -------------------------------------------------------------------------- */
|
|
324
|
+
|
|
325
|
+
const MIRROR_TEXT = "@Ada Lovelace ships";
|
|
326
|
+
const MIRROR_VALUE: MentionValue = {
|
|
327
|
+
text: MIRROR_TEXT,
|
|
328
|
+
mentions: [{ id: "u-lovelace", label: "Ada Lovelace", start: 0 }],
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* **Regression lock — the mirror follows a resize of the field.**
|
|
333
|
+
*
|
|
334
|
+
* Narrows an ancestor that sits OUTSIDE the `MentionInput` root, so neither the
|
|
335
|
+
* field's own attributes nor the document root's change — the `MutationObserver`
|
|
336
|
+
* arms are structurally blind to it and only the `ResizeObserver` can keep the
|
|
337
|
+
* overlay's box on top of the field's.
|
|
338
|
+
*/
|
|
339
|
+
export const MirrorTracksFieldResize: Story = {
|
|
340
|
+
args: { options: ROSTER, children: null },
|
|
341
|
+
render: function MirrorResizeStory() {
|
|
342
|
+
const id = useId();
|
|
343
|
+
return (
|
|
344
|
+
<div data-testid="resize-host" style={{ width: "28rem" }} className="space-y-2">
|
|
345
|
+
<Label htmlFor={id}>Comment</Label>
|
|
346
|
+
<MentionInput options={ROSTER} defaultValue={MIRROR_VALUE}>
|
|
347
|
+
<MentionInputTextarea id={id} placeholder="Type @ to mention someone…" />
|
|
348
|
+
<MentionInputContent>
|
|
349
|
+
<MentionInputList>
|
|
350
|
+
{(option) => <MentionInputItem key={option.id} option={option} />}
|
|
351
|
+
</MentionInputList>
|
|
352
|
+
<MentionInputEmpty />
|
|
353
|
+
</MentionInputContent>
|
|
354
|
+
</MentionInput>
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
},
|
|
358
|
+
play: async ({ canvasElement, step }) => {
|
|
359
|
+
const field = within(canvasElement).getByRole("textbox", {
|
|
360
|
+
name: "Comment",
|
|
361
|
+
}) as HTMLTextAreaElement;
|
|
362
|
+
const overlay = canvasElement.querySelector<HTMLElement>('[data-slot="mention-input-overlay"]');
|
|
363
|
+
const host = canvasElement.querySelector<HTMLElement>('[data-testid="resize-host"]');
|
|
364
|
+
await expect(overlay).not.toBeNull();
|
|
365
|
+
await expect(host).not.toBeNull();
|
|
366
|
+
|
|
367
|
+
await step("the mirror starts on top of the field", async () => {
|
|
368
|
+
await expect(overlay!.getBoundingClientRect().width).toBeCloseTo(
|
|
369
|
+
field.getBoundingClientRect().width,
|
|
370
|
+
0,
|
|
371
|
+
);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
await step("narrow an ancestor outside the MentionInput root", async () => {
|
|
375
|
+
const widthBefore = field.getBoundingClientRect().width;
|
|
376
|
+
host!.style.width = "18rem";
|
|
377
|
+
await waitFor(() => {
|
|
378
|
+
expect(field.getBoundingClientRect().width).toBeLessThan(widthBefore - 20);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
await step("the mirror re-measures onto the new box", async () => {
|
|
383
|
+
await waitFor(() => {
|
|
384
|
+
const f = field.getBoundingClientRect();
|
|
385
|
+
const o = overlay!.getBoundingClientRect();
|
|
386
|
+
expect(o.width).toBeCloseTo(f.width, 0);
|
|
387
|
+
expect(o.height).toBeCloseTo(f.height, 0);
|
|
388
|
+
expect(o.left).toBeCloseTo(f.left, 0);
|
|
389
|
+
expect(o.top).toBeCloseTo(f.top, 0);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* **Regression lock — the mirror follows a change to the FIELD's own style.**
|
|
397
|
+
*
|
|
398
|
+
* A third, font-free lever: `letter-spacing` changes glyph advance while the
|
|
399
|
+
* pinned box stays put (the play function asserts the box did not move) and no
|
|
400
|
+
* font loads. It was written to isolate the `MutationObserver` arm — and it does
|
|
401
|
+
* not: measured, it still passes with that observer deleted, so `ResizeObserver`
|
|
402
|
+
* reacts here too. Kept because it is a third independent scenario in the
|
|
403
|
+
* all-arms lock, and because a consumer restyling the field is a real case.
|
|
404
|
+
*/
|
|
405
|
+
export const MirrorTracksFieldStyleChange: Story = {
|
|
406
|
+
args: { options: ROSTER, children: null },
|
|
407
|
+
render: function MirrorStyleStory() {
|
|
408
|
+
const id = useId();
|
|
409
|
+
return (
|
|
410
|
+
<div className="space-y-2">
|
|
411
|
+
<Label htmlFor={id}>Comment</Label>
|
|
412
|
+
<MentionInput options={ROSTER} defaultValue={MIRROR_VALUE}>
|
|
413
|
+
<MentionInputTextarea
|
|
414
|
+
id={id}
|
|
415
|
+
className="h-20 w-[26rem] resize-none"
|
|
416
|
+
placeholder="Type @ to mention someone…"
|
|
417
|
+
/>
|
|
418
|
+
<MentionInputContent>
|
|
419
|
+
<MentionInputList>
|
|
420
|
+
{(option) => <MentionInputItem key={option.id} option={option} />}
|
|
421
|
+
</MentionInputList>
|
|
422
|
+
<MentionInputEmpty />
|
|
423
|
+
</MentionInputContent>
|
|
424
|
+
</MentionInput>
|
|
425
|
+
</div>
|
|
426
|
+
);
|
|
427
|
+
},
|
|
428
|
+
play: async ({ canvasElement, step }) => {
|
|
429
|
+
const field = within(canvasElement).getByRole("textbox", {
|
|
430
|
+
name: "Comment",
|
|
431
|
+
}) as HTMLTextAreaElement;
|
|
432
|
+
const overlay = canvasElement.querySelector<HTMLElement>('[data-slot="mention-input-overlay"]');
|
|
433
|
+
await expect(overlay).not.toBeNull();
|
|
434
|
+
await document.fonts.ready;
|
|
435
|
+
|
|
436
|
+
const boxBefore = field.getBoundingClientRect();
|
|
437
|
+
|
|
438
|
+
await step("the mirror starts in parity with the field", async () => {
|
|
439
|
+
await expect(getComputedStyle(overlay!).letterSpacing).toBe(
|
|
440
|
+
getComputedStyle(field).letterSpacing,
|
|
441
|
+
);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
await step("change letter-spacing on the field's own style attribute", async () => {
|
|
445
|
+
field.style.letterSpacing = "3px";
|
|
446
|
+
await waitFor(() => {
|
|
447
|
+
expect(getComputedStyle(field).letterSpacing).toBe("3px");
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
await step("the box did NOT move", async () => {
|
|
452
|
+
const boxAfter = field.getBoundingClientRect();
|
|
453
|
+
await expect(boxAfter.width).toBeCloseTo(boxBefore.width, 1);
|
|
454
|
+
await expect(boxAfter.height).toBeCloseTo(boxBefore.height, 1);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
await step("the mirror re-measured onto the new metrics", async () => {
|
|
458
|
+
await waitFor(() => {
|
|
459
|
+
expect(getComputedStyle(overlay!).letterSpacing).toBe("3px");
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
},
|
|
463
|
+
};
|