@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,202 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import { MatchHighlight, queryToRanges, normalizeRanges } from "./match-highlight";
|
|
4
|
+
|
|
5
|
+
describe("MatchHighlight", () => {
|
|
6
|
+
it("renders one <mark> per case-insensitive query occurrence, text intact", () => {
|
|
7
|
+
const { container } = render(<MatchHighlight text="My Notes note" query="note" />);
|
|
8
|
+
const marks = container.querySelectorAll("mark");
|
|
9
|
+
expect(marks).toHaveLength(2);
|
|
10
|
+
expect(marks[0]?.textContent).toBe("Note");
|
|
11
|
+
expect(marks[1]?.textContent).toBe("note");
|
|
12
|
+
// Full string is preserved for screen readers.
|
|
13
|
+
expect(container.textContent).toBe("My Notes note");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("merges adjacent precomputed ranges into a single <mark>", () => {
|
|
17
|
+
const { container } = render(
|
|
18
|
+
<MatchHighlight
|
|
19
|
+
text="abcdef"
|
|
20
|
+
ranges={[
|
|
21
|
+
[0, 2],
|
|
22
|
+
[2, 4],
|
|
23
|
+
]}
|
|
24
|
+
/>,
|
|
25
|
+
);
|
|
26
|
+
const marks = container.querySelectorAll("mark");
|
|
27
|
+
expect(marks).toHaveLength(1);
|
|
28
|
+
expect(marks[0]?.textContent).toBe("abcd"); // [0,4)
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("renders no <mark> for a non-matching query", () => {
|
|
32
|
+
const { container } = render(<MatchHighlight text="hello world" query="xyz" />);
|
|
33
|
+
expect(container.querySelectorAll("mark")).toHaveLength(0);
|
|
34
|
+
expect(container.textContent).toBe("hello world");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("renders no <mark> for empty / whitespace-only-vs-nothing query", () => {
|
|
38
|
+
const { container } = render(<MatchHighlight text="hello" query="" />);
|
|
39
|
+
expect(container.querySelectorAll("mark")).toHaveLength(0);
|
|
40
|
+
expect(container.textContent).toBe("hello");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("highlights multiple needles", () => {
|
|
44
|
+
const { container } = render(<MatchHighlight text="red green blue" query={["red", "blue"]} />);
|
|
45
|
+
const marks = container.querySelectorAll("mark");
|
|
46
|
+
expect(marks).toHaveLength(2);
|
|
47
|
+
expect(marks[0]?.textContent).toBe("red");
|
|
48
|
+
expect(marks[1]?.textContent).toBe("blue");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("respects caseSensitive", () => {
|
|
52
|
+
const { container } = render(<MatchHighlight text="Note note" query="note" caseSensitive />);
|
|
53
|
+
const marks = container.querySelectorAll("mark");
|
|
54
|
+
expect(marks).toHaveLength(1);
|
|
55
|
+
expect(marks[0]?.textContent).toBe("note");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("ranges win over query", () => {
|
|
59
|
+
const { container } = render(<MatchHighlight text="abcdef" query="ef" ranges={[[0, 1]]} />);
|
|
60
|
+
const marks = container.querySelectorAll("mark");
|
|
61
|
+
expect(marks).toHaveLength(1);
|
|
62
|
+
expect(marks[0]?.textContent).toBe("a");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("handles matches at string start and end (non-adjacent stay separate)", () => {
|
|
66
|
+
const { container } = render(<MatchHighlight text="abc abc" query="abc" />);
|
|
67
|
+
const marks = container.querySelectorAll("mark");
|
|
68
|
+
expect(marks).toHaveLength(2);
|
|
69
|
+
expect(marks[0]?.textContent).toBe("abc"); // at index 0 (start)
|
|
70
|
+
expect(marks[1]?.textContent).toBe("abc"); // ending at text.length (end)
|
|
71
|
+
expect(container.textContent).toBe("abc abc");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("merges two adjacent query matches into one contiguous mark (no seam)", () => {
|
|
75
|
+
const { container } = render(<MatchHighlight text="abcabc" query="abc" />);
|
|
76
|
+
const marks = container.querySelectorAll("mark");
|
|
77
|
+
expect(marks).toHaveLength(1);
|
|
78
|
+
expect(marks[0]?.textContent).toBe("abcabc");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("handles unicode / accented needles without index drift", () => {
|
|
82
|
+
const { container } = render(<MatchHighlight text="café Café" query="café" />);
|
|
83
|
+
const marks = container.querySelectorAll("mark");
|
|
84
|
+
expect(marks).toHaveLength(2);
|
|
85
|
+
expect(container.textContent).toBe("café Café");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("styles the mark with the semantic highlight token pair", () => {
|
|
89
|
+
const { container } = render(<MatchHighlight text="find me" query="me" />);
|
|
90
|
+
const mark = container.querySelector("mark");
|
|
91
|
+
expect(mark?.className).toContain("bg-highlight");
|
|
92
|
+
expect(mark?.className).toContain("text-highlight-foreground");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("marks exactly one mark as current for activeIndex, and none by default", () => {
|
|
96
|
+
const { container, rerender } = render(<MatchHighlight text="abc abc abc" query="abc" />);
|
|
97
|
+
expect(container.querySelectorAll("mark[data-active]")).toHaveLength(0);
|
|
98
|
+
expect(container.querySelectorAll("mark[aria-current]")).toHaveLength(0);
|
|
99
|
+
|
|
100
|
+
rerender(<MatchHighlight text="abc abc abc" query="abc" activeIndex={1} />);
|
|
101
|
+
const active = container.querySelectorAll("mark[data-active]");
|
|
102
|
+
expect(active).toHaveLength(1);
|
|
103
|
+
expect(active[0]?.getAttribute("aria-current")).toBe("true");
|
|
104
|
+
// Second of the three, in document order.
|
|
105
|
+
expect(container.querySelectorAll("mark")[1]).toBe(active[0]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("gives the current mark its own token pair, distinct from the rest", () => {
|
|
109
|
+
const { container } = render(
|
|
110
|
+
<MatchHighlight text="abc abc" query="abc" activeIndex={0} data-testid="mh" />,
|
|
111
|
+
);
|
|
112
|
+
const [current, other] = Array.from(container.querySelectorAll("mark"));
|
|
113
|
+
expect(current?.className).toContain("bg-highlight-active");
|
|
114
|
+
expect(current?.className).toContain("text-highlight-active-foreground");
|
|
115
|
+
expect(other?.className).toContain("bg-highlight");
|
|
116
|
+
expect(other?.className).not.toContain("bg-highlight-active");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("signals the current mark without relying on colour (WCAG 1.4.1)", () => {
|
|
120
|
+
const { container } = render(<MatchHighlight text="abc abc" query="abc" activeIndex={0} />);
|
|
121
|
+
const current = container.querySelector("mark[data-active]");
|
|
122
|
+
// An outline is a shape cue that survives greyscale; aria-current carries it
|
|
123
|
+
// to assistive tech, which never sees either colour.
|
|
124
|
+
expect(current?.className).toContain("outline-2");
|
|
125
|
+
expect(current?.getAttribute("aria-current")).toBe("true");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("ignores an out-of-range activeIndex rather than clamping", () => {
|
|
129
|
+
const { container } = render(<MatchHighlight text="abc abc" query="abc" activeIndex={7} />);
|
|
130
|
+
expect(container.querySelectorAll("mark")).toHaveLength(2);
|
|
131
|
+
expect(container.querySelectorAll("mark[data-active]")).toHaveLength(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("counts activeIndex against the MERGED ranges, not the raw ones", () => {
|
|
135
|
+
// [0,2) + [2,4) merge into one "abcd", so index 1 is the SECOND mark ("f").
|
|
136
|
+
const { container } = render(
|
|
137
|
+
<MatchHighlight
|
|
138
|
+
text="abcdef"
|
|
139
|
+
ranges={[
|
|
140
|
+
[0, 2],
|
|
141
|
+
[2, 4],
|
|
142
|
+
[5, 6],
|
|
143
|
+
]}
|
|
144
|
+
activeIndex={1}
|
|
145
|
+
/>,
|
|
146
|
+
);
|
|
147
|
+
const marks = container.querySelectorAll("mark");
|
|
148
|
+
expect(marks).toHaveLength(2);
|
|
149
|
+
expect(container.querySelector("mark[data-active]")?.textContent).toBe("f");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("exposes stable data-slot selectors on the root and every mark", () => {
|
|
153
|
+
const { container } = render(<MatchHighlight text="abc abc" query="abc" />);
|
|
154
|
+
expect(container.querySelector('[data-slot="match-highlight"]')?.tagName).toBe("SPAN");
|
|
155
|
+
expect(container.querySelectorAll('[data-slot="match-highlight-mark"]')).toHaveLength(2);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("merges className last and spreads props onto the span", () => {
|
|
159
|
+
const { container } = render(
|
|
160
|
+
<MatchHighlight text="x" query="x" className="custom-x" data-testid="mh" />,
|
|
161
|
+
);
|
|
162
|
+
const span = container.querySelector("span");
|
|
163
|
+
expect(span?.className).toContain("custom-x");
|
|
164
|
+
expect(span?.getAttribute("data-testid")).toBe("mh");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("range helpers", () => {
|
|
169
|
+
it("queryToRanges finds overlapping-needle matches", () => {
|
|
170
|
+
// "ab" [0,2) and "bc" [1,3) overlap; normalize should later merge.
|
|
171
|
+
expect(queryToRanges("abc", ["ab", "bc"])).toEqual([
|
|
172
|
+
[0, 2],
|
|
173
|
+
[1, 3],
|
|
174
|
+
]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("normalizeRanges clamps, sorts, and merges overlaps + adjacency", () => {
|
|
178
|
+
expect(
|
|
179
|
+
normalizeRanges(
|
|
180
|
+
[
|
|
181
|
+
[1, 3],
|
|
182
|
+
[2, 5],
|
|
183
|
+
[5, 6],
|
|
184
|
+
],
|
|
185
|
+
10,
|
|
186
|
+
),
|
|
187
|
+
).toEqual([[1, 6]]);
|
|
188
|
+
expect(
|
|
189
|
+
normalizeRanges(
|
|
190
|
+
[
|
|
191
|
+
[-2, 2],
|
|
192
|
+
[8, 99],
|
|
193
|
+
],
|
|
194
|
+
10,
|
|
195
|
+
),
|
|
196
|
+
).toEqual([
|
|
197
|
+
[0, 2],
|
|
198
|
+
[8, 10],
|
|
199
|
+
]);
|
|
200
|
+
expect(normalizeRanges([[4, 4]], 10)).toEqual([]); // empty range dropped
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode, forwardRef, Fragment } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
/** A half-open `[start, end)` character range within the text. */
|
|
5
|
+
export type MatchRange = readonly [number, number];
|
|
6
|
+
|
|
7
|
+
export interface MatchHighlightProps extends HTMLAttributes<HTMLSpanElement> {
|
|
8
|
+
/** The full string to render. */
|
|
9
|
+
text: string;
|
|
10
|
+
/**
|
|
11
|
+
* One or more needles to highlight. Case-insensitive by default. Ignored when
|
|
12
|
+
* `ranges` is supplied.
|
|
13
|
+
*/
|
|
14
|
+
query?: string | string[];
|
|
15
|
+
/**
|
|
16
|
+
* Precomputed match ranges (e.g. from a fuzzy matcher / MiniSearch). Wins over
|
|
17
|
+
* `query` when supplied. Overlapping and adjacent ranges are merged.
|
|
18
|
+
*/
|
|
19
|
+
ranges?: readonly MatchRange[];
|
|
20
|
+
/** Match `query` case-sensitively. @default false */
|
|
21
|
+
caseSensitive?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Which mark is the CURRENT one, 0-based among the resolved (merged, sorted)
|
|
24
|
+
* ranges. `-1` or omitted means none is.
|
|
25
|
+
*
|
|
26
|
+
* "One of twelve" and "the one you are on" are different meanings, so the
|
|
27
|
+
* current mark gets its own token pair rather than an opacity tweak — and,
|
|
28
|
+
* because colour is never the only channel (WCAG 1.4.1), an outline and
|
|
29
|
+
* `aria-current="true"` as well. It also carries `data-active`, which is how
|
|
30
|
+
* a caller finds it to scroll it into view:
|
|
31
|
+
* `root.querySelector('[data-slot="match-highlight-mark"][data-active]')`.
|
|
32
|
+
*/
|
|
33
|
+
activeIndex?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const escapeRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Find every occurrence of each needle in `text` and return their `[start, end)`
|
|
40
|
+
* ranges. Uses a per-needle global regex so indices land on the ORIGINAL string
|
|
41
|
+
* (unicode-safe — no `toLowerCase()` length drift), handling multiple needles and
|
|
42
|
+
* case folding. Empty needles are ignored.
|
|
43
|
+
*/
|
|
44
|
+
export function queryToRanges(
|
|
45
|
+
text: string,
|
|
46
|
+
query: string | string[],
|
|
47
|
+
caseSensitive = false,
|
|
48
|
+
): MatchRange[] {
|
|
49
|
+
const needles = (Array.isArray(query) ? query : [query]).filter((q) => q.length > 0);
|
|
50
|
+
if (needles.length === 0 || text.length === 0) return [];
|
|
51
|
+
const flags = caseSensitive ? "gu" : "giu";
|
|
52
|
+
const out: MatchRange[] = [];
|
|
53
|
+
for (const needle of needles) {
|
|
54
|
+
let re: RegExp;
|
|
55
|
+
try {
|
|
56
|
+
re = new RegExp(escapeRegExp(needle), flags);
|
|
57
|
+
} catch {
|
|
58
|
+
continue; // pathological needle — skip rather than throw
|
|
59
|
+
}
|
|
60
|
+
for (const m of text.matchAll(re)) {
|
|
61
|
+
if (m.index === undefined) continue;
|
|
62
|
+
const end = m.index + m[0].length;
|
|
63
|
+
if (end > m.index) out.push([m.index, end]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Clamp ranges to `[0, len)`, drop empty/invalid ones, sort, and merge
|
|
71
|
+
* overlapping AND adjacent ranges (so `[0,2]` + `[2,4]` → a single `[0,4]`).
|
|
72
|
+
*/
|
|
73
|
+
export function normalizeRanges(ranges: readonly MatchRange[], len: number): MatchRange[] {
|
|
74
|
+
const clamped = ranges
|
|
75
|
+
.map(([a, b]): MatchRange => [Math.max(0, Math.min(a, b)), Math.min(len, Math.max(a, b))])
|
|
76
|
+
.filter(([a, b]) => a < b);
|
|
77
|
+
clamped.sort((x, y) => x[0] - y[0] || x[1] - y[1]);
|
|
78
|
+
const merged: [number, number][] = [];
|
|
79
|
+
for (const [a, b] of clamped) {
|
|
80
|
+
const last = merged[merged.length - 1];
|
|
81
|
+
if (last && a <= last[1]) {
|
|
82
|
+
last[1] = Math.max(last[1], b);
|
|
83
|
+
} else {
|
|
84
|
+
merged.push([a, b]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return merged;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Render `text` with the query matches wrapped in real `<mark>` elements. Purely
|
|
92
|
+
* presentational: pass `query` to let it compute ranges (case-insensitive by
|
|
93
|
+
* default), or `ranges` from your own fuzzy engine. Marks use the semantic
|
|
94
|
+
* `--highlight` token pair (AA-legible ink in every theme). Screen readers read
|
|
95
|
+
* the full string naturally.
|
|
96
|
+
*/
|
|
97
|
+
export const MatchHighlight = forwardRef<HTMLSpanElement, MatchHighlightProps>(
|
|
98
|
+
function MatchHighlight(
|
|
99
|
+
{ text, query, ranges, caseSensitive = false, activeIndex = -1, className, ...props },
|
|
100
|
+
ref,
|
|
101
|
+
) {
|
|
102
|
+
const resolved = normalizeRanges(
|
|
103
|
+
ranges ?? (query != null ? queryToRanges(text, query, caseSensitive) : []),
|
|
104
|
+
text.length,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const segments: ReactNode[] = [];
|
|
108
|
+
let cursor = 0;
|
|
109
|
+
resolved.forEach(([start, end], index) => {
|
|
110
|
+
if (start > cursor) {
|
|
111
|
+
segments.push(<Fragment key={`t${cursor}`}>{text.slice(cursor, start)}</Fragment>);
|
|
112
|
+
}
|
|
113
|
+
const active = index === activeIndex;
|
|
114
|
+
segments.push(
|
|
115
|
+
<mark
|
|
116
|
+
key={`m${start}`}
|
|
117
|
+
data-slot="match-highlight-mark"
|
|
118
|
+
// Present-or-absent, so the current mark is still identifiable in
|
|
119
|
+
// greyscale and to a screen reader — the colour swap below is the
|
|
120
|
+
// enhancement, not the signal (WCAG 1.4.1).
|
|
121
|
+
data-active={active ? "" : undefined}
|
|
122
|
+
aria-current={active ? "true" : undefined}
|
|
123
|
+
className={cn(
|
|
124
|
+
"rounded-sm px-0.5 font-medium",
|
|
125
|
+
active
|
|
126
|
+
? "outline-foreground bg-highlight-active text-highlight-active-foreground outline-2 outline-offset-1"
|
|
127
|
+
: "bg-highlight text-highlight-foreground",
|
|
128
|
+
)}
|
|
129
|
+
>
|
|
130
|
+
{text.slice(start, end)}
|
|
131
|
+
</mark>,
|
|
132
|
+
);
|
|
133
|
+
cursor = end;
|
|
134
|
+
});
|
|
135
|
+
if (cursor < text.length) {
|
|
136
|
+
segments.push(<Fragment key={`t${cursor}`}>{text.slice(cursor)}</Fragment>);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<span ref={ref} data-slot="match-highlight" className={cn(className)} {...props}>
|
|
141
|
+
{segments}
|
|
142
|
+
</span>
|
|
143
|
+
);
|
|
144
|
+
},
|
|
145
|
+
);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export {
|
|
2
|
+
MentionInput,
|
|
3
|
+
MentionInputContent,
|
|
4
|
+
MentionInputEmpty,
|
|
5
|
+
MentionInputItem,
|
|
6
|
+
MentionInputList,
|
|
7
|
+
MentionInputTextarea,
|
|
8
|
+
useMentionInput,
|
|
9
|
+
type MentionInputContentProps,
|
|
10
|
+
type MentionInputContextValue,
|
|
11
|
+
type MentionInputEmptyProps,
|
|
12
|
+
type MentionInputItemProps,
|
|
13
|
+
type MentionInputListProps,
|
|
14
|
+
type MentionInputProps,
|
|
15
|
+
type MentionInputTextareaProps,
|
|
16
|
+
} from "./mention-input";
|
|
17
|
+
export {
|
|
18
|
+
defaultMentionFilter,
|
|
19
|
+
insertMention,
|
|
20
|
+
mentionAt,
|
|
21
|
+
mentionEnd,
|
|
22
|
+
remapMentions,
|
|
23
|
+
serializeMentions,
|
|
24
|
+
type InsertMentionResult,
|
|
25
|
+
type Mention,
|
|
26
|
+
type MentionOption,
|
|
27
|
+
type MentionValue,
|
|
28
|
+
} from "./mention-value";
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, waitFor, within } from "storybook/test";
|
|
3
|
+
import { useId, type CSSProperties } 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 type { MentionOption, MentionValue } from "./mention-value";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The `MutationObserver` arm of the mirror re-measure, pinned on its own.
|
|
17
|
+
*
|
|
18
|
+
* ### Why this lives in its own file
|
|
19
|
+
*
|
|
20
|
+
* The lock is **order-dependent**, and that was measured, not guessed. With this
|
|
21
|
+
* story co-resident with the other `MentionInput` stories, deleting the
|
|
22
|
+
* `MutationObserver` left the whole suite green; running the same story in
|
|
23
|
+
* isolation, the same deletion is RED 2/2. Something a preceding story leaves
|
|
24
|
+
* behind in the shared page re-measures the mirror for an unrelated reason. A
|
|
25
|
+
* lock that only holds when it runs alone is not a lock at all in CI — so it
|
|
26
|
+
* gets its own story file, which the runner loads as its own test file.
|
|
27
|
+
*
|
|
28
|
+
* ### What it pins
|
|
29
|
+
*
|
|
30
|
+
* Every box-affecting property of the field is nailed to absolute pixels, so
|
|
31
|
+
* `ResizeObserver` is structurally unable to fire, and `document.fonts.ready` is
|
|
32
|
+
* awaited before the flip so that arm cannot fire late. `h-20 w-[26rem]` was NOT
|
|
33
|
+
* enough: padding and border derived from `rem` can move by sub-integer amounts
|
|
34
|
+
* across a theme change, which `clientWidth`'s integer rounding hides but
|
|
35
|
+
* `ResizeObserver` still reports.
|
|
36
|
+
*
|
|
37
|
+
* The play function asserts the box did not move — fractionally via
|
|
38
|
+
* `getBoundingClientRect` as well as in integers — so the isolation is proven in
|
|
39
|
+
* the test rather than argued for in a comment.
|
|
40
|
+
*/
|
|
41
|
+
const meta = {
|
|
42
|
+
title: "Forms/MentionInput/Mirror re-measure",
|
|
43
|
+
component: MentionInput,
|
|
44
|
+
parameters: {
|
|
45
|
+
docs: {
|
|
46
|
+
description: {
|
|
47
|
+
component:
|
|
48
|
+
"Regression lock for the chip mirror's re-measure. Isolated in its own " +
|
|
49
|
+
"story file so no sibling story can mask the arm it pins.",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
} satisfies Meta<typeof MentionInput>;
|
|
54
|
+
|
|
55
|
+
export default meta;
|
|
56
|
+
type Story = StoryObj<typeof meta>;
|
|
57
|
+
|
|
58
|
+
const ROSTER: MentionOption[] = [
|
|
59
|
+
{ id: "u-lovelace", label: "Ada Lovelace", description: "Analytical Engine" },
|
|
60
|
+
{ id: "u-hopper", label: "Grace Hopper", description: "Compilers" },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const MIRROR_VALUE: MentionValue = {
|
|
64
|
+
text: "@Ada Lovelace ships",
|
|
65
|
+
mentions: [{ id: "u-lovelace", label: "Ada Lovelace", start: 0 }],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Every degree of freedom the field's box has, removed. */
|
|
69
|
+
const NAILED_BOX: CSSProperties = {
|
|
70
|
+
boxSizing: "border-box",
|
|
71
|
+
width: "420px",
|
|
72
|
+
height: "80px",
|
|
73
|
+
paddingTop: "8px",
|
|
74
|
+
paddingRight: "12px",
|
|
75
|
+
paddingBottom: "8px",
|
|
76
|
+
paddingLeft: "12px",
|
|
77
|
+
borderWidth: "1px",
|
|
78
|
+
lineHeight: "20px",
|
|
79
|
+
fontSize: "14px",
|
|
80
|
+
overflow: "hidden",
|
|
81
|
+
resize: "none",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** The element the theme decorator actually wrote `data-theme` onto. */
|
|
85
|
+
function themeHost(): HTMLElement {
|
|
86
|
+
return document.body.hasAttribute("data-theme") ? document.body : document.documentElement;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* **Regression lock — the `MutationObserver` arm, individually pinned.**
|
|
91
|
+
*
|
|
92
|
+
* An in-place `data-theme` flip, exactly what `ThemeSwitcher` does (no remount).
|
|
93
|
+
* Deleting the `MutationObserver` block turns this RED with the original bug's
|
|
94
|
+
* symptom verbatim: the overlay stuck on the `:root` fallback `Inter` while the
|
|
95
|
+
* field renders `IBM Plex Mono`.
|
|
96
|
+
*/
|
|
97
|
+
export const TracksThemeChangeWithNailedBox: Story = {
|
|
98
|
+
args: { options: ROSTER, children: null },
|
|
99
|
+
render: function MirrorNailedBoxStory() {
|
|
100
|
+
const id = useId();
|
|
101
|
+
return (
|
|
102
|
+
<div className="space-y-2">
|
|
103
|
+
<Label htmlFor={id}>Comment</Label>
|
|
104
|
+
<MentionInput options={ROSTER} defaultValue={MIRROR_VALUE}>
|
|
105
|
+
<MentionInputTextarea id={id} style={NAILED_BOX} placeholder="Type @ to mention…" />
|
|
106
|
+
<MentionInputContent>
|
|
107
|
+
<MentionInputList>
|
|
108
|
+
{(option) => <MentionInputItem key={option.id} option={option} />}
|
|
109
|
+
</MentionInputList>
|
|
110
|
+
<MentionInputEmpty />
|
|
111
|
+
</MentionInputContent>
|
|
112
|
+
</MentionInput>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
},
|
|
116
|
+
play: async ({ canvasElement, step }) => {
|
|
117
|
+
const field = within(canvasElement).getByRole("textbox", {
|
|
118
|
+
name: "Comment",
|
|
119
|
+
}) as HTMLTextAreaElement;
|
|
120
|
+
const overlay = canvasElement.querySelector<HTMLElement>('[data-slot="mention-input-overlay"]');
|
|
121
|
+
await expect(overlay).not.toBeNull();
|
|
122
|
+
|
|
123
|
+
// Settle the fonts arm before touching anything, so it cannot fire late.
|
|
124
|
+
await document.fonts.ready;
|
|
125
|
+
|
|
126
|
+
const boxOf = (el: HTMLElement) => {
|
|
127
|
+
const r = el.getBoundingClientRect();
|
|
128
|
+
return { w: r.width, h: r.height, cw: el.clientWidth, ch: el.clientHeight };
|
|
129
|
+
};
|
|
130
|
+
const before = boxOf(field);
|
|
131
|
+
const host = themeHost();
|
|
132
|
+
const original = host.getAttribute("data-theme");
|
|
133
|
+
const fontBefore = getComputedStyle(field).fontFamily;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await step("the mirror starts in parity with the field", async () => {
|
|
137
|
+
await expect(getComputedStyle(overlay!).fontFamily).toBe(fontBefore);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await step("flip data-theme in place", async () => {
|
|
141
|
+
// The shipped themes share one font stack, so the attribute flip alone
|
|
142
|
+
// changes nothing measurable. Pair it with a scoped `--font-sans`
|
|
143
|
+
// override (the same root `style` the observer also watches) so the
|
|
144
|
+
// parity assertion below cannot pass vacuously.
|
|
145
|
+
host.style.setProperty("--font-sans", '"Times New Roman", serif');
|
|
146
|
+
host.setAttribute("data-theme", original === "dark" ? "light" : "dark");
|
|
147
|
+
await waitFor(() => {
|
|
148
|
+
expect(getComputedStyle(field).fontFamily).not.toBe(fontBefore);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await step("the box did not move — ResizeObserver cannot have fired", async () => {
|
|
153
|
+
const after = boxOf(field);
|
|
154
|
+
await expect(after.w).toBe(before.w);
|
|
155
|
+
await expect(after.h).toBe(before.h);
|
|
156
|
+
await expect(after.cw).toBe(before.cw);
|
|
157
|
+
await expect(after.ch).toBe(before.ch);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await step("the mirror re-measured — only the MutationObserver could have", async () => {
|
|
161
|
+
await waitFor(() => {
|
|
162
|
+
expect(getComputedStyle(overlay!).fontFamily).toBe(getComputedStyle(field).fontFamily);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
} finally {
|
|
166
|
+
host.style.removeProperty("--font-sans");
|
|
167
|
+
if (original === null) host.removeAttribute("data-theme");
|
|
168
|
+
else host.setAttribute("data-theme", original);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
};
|