@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @elabs-ai/components-ui — foundation + app UI components.
|
|
3
|
+
*
|
|
4
|
+
* Source-owned shadcn-style components built on Radix primitives and the
|
|
5
|
+
* @elabs-ai/components-tokens semantic theme. Import the token stylesheet once at the app
|
|
6
|
+
* root ("import \"@elabs-ai/components-tokens/styles.css\"") and wrap in <ThemeProvider>.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: this barrel is generated to include every component folder. Some
|
|
9
|
+
* components require their own peer libs (declared in package.json), e.g.
|
|
10
|
+
* Drawer (vaul), Command (cmdk), Toast (sonner), Calendar (react-day-picker),
|
|
11
|
+
* Carousel (embla-carousel-react), Resizable (react-resizable-panels),
|
|
12
|
+
* Form (react-hook-form + zod), InputOTP (input-otp).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Utilities
|
|
16
|
+
export { cn } from "./lib/cn";
|
|
17
|
+
// The canonical mobile-breakpoint check (Sidebar / ContextPanel Sheet fallback
|
|
18
|
+
// pattern, research 09 §B.4) — exported so siblings don't fork a matchMedia hook.
|
|
19
|
+
export { useIsMobile } from "./lib/use-mobile";
|
|
20
|
+
// Browser file-download mechanics — one home for the Blob/URL → <a download> dance
|
|
21
|
+
// (shared by ConversationDownload, downloadCsv, and Gallery). SSR-guarded.
|
|
22
|
+
export { downloadBlob, downloadUrl } from "./lib/download";
|
|
23
|
+
// Clipboard write + transient "copied" flag — one implementation for the
|
|
24
|
+
// editor's copy button and `CopyableValue`, so the timing and the
|
|
25
|
+
// "no clipboard in this context" answer are defined once.
|
|
26
|
+
export {
|
|
27
|
+
COPY_FEEDBACK_MS,
|
|
28
|
+
useCopyToClipboard,
|
|
29
|
+
type CopyToClipboard,
|
|
30
|
+
type UseCopyToClipboardOptions,
|
|
31
|
+
} from "./lib/use-copy-to-clipboard";
|
|
32
|
+
// The shared file model (ADR 0024) — one input union + one coarse, CLOSED
|
|
33
|
+
// category, so `ai`, `viewer` and future surfaces stop each inventing their own.
|
|
34
|
+
// Fine-grained format matching belongs to the adapter manifests, not here.
|
|
35
|
+
export { type FileSource, type ResolvedFileSource, normalizeFileSource } from "./lib/file-source";
|
|
36
|
+
export {
|
|
37
|
+
type FileCategory,
|
|
38
|
+
type FileKind,
|
|
39
|
+
extensionOf,
|
|
40
|
+
mediaTypeFromName,
|
|
41
|
+
resolveFileKind,
|
|
42
|
+
} from "./lib/file-kind";
|
|
43
|
+
export { FILE_CATEGORY_ICONS, fileIconFor } from "./lib/file-icon";
|
|
44
|
+
// How one PART of a document is addressed (ADR 0025). Lives here for the same
|
|
45
|
+
// reason `FileSource` does: the producer of a citation (`ai`) and its consumer
|
|
46
|
+
// (`viewer`) are Layer-2 siblings that may not import each other.
|
|
47
|
+
export {
|
|
48
|
+
DOCUMENT_ADDRESS_KINDS,
|
|
49
|
+
type DocumentAddress,
|
|
50
|
+
type DocumentAddressKind,
|
|
51
|
+
type DocumentRect,
|
|
52
|
+
type NormalizedText,
|
|
53
|
+
type QuoteAddress,
|
|
54
|
+
type RangeAddress,
|
|
55
|
+
type RectAddress,
|
|
56
|
+
normalizeQuoteText,
|
|
57
|
+
normalizeQuoteTextWithOffsets,
|
|
58
|
+
} from "./lib/document-address";
|
|
59
|
+
// Streamdown's in-markdown chrome, resolved through `t()` (#310). Shared here
|
|
60
|
+
// because both `ai` and `viewer` render Streamdown and may not import each other.
|
|
61
|
+
export {
|
|
62
|
+
STREAMDOWN_TRANSLATION_KEYS,
|
|
63
|
+
useStreamdownTranslations,
|
|
64
|
+
type StreamdownTranslationKey,
|
|
65
|
+
type StreamdownTranslationMap,
|
|
66
|
+
} from "./lib/streamdown-translations";
|
|
67
|
+
|
|
68
|
+
// Components
|
|
69
|
+
export * from "./components/accordion";
|
|
70
|
+
export * from "./components/advanced-group";
|
|
71
|
+
export * from "./components/app-sidebar";
|
|
72
|
+
export * from "./components/alert";
|
|
73
|
+
export * from "./components/alert-dialog";
|
|
74
|
+
export * from "./components/app-shell";
|
|
75
|
+
export * from "./components/aspect-ratio";
|
|
76
|
+
export * from "./components/attribution-panel";
|
|
77
|
+
export * from "./components/avatar";
|
|
78
|
+
export * from "./components/badge";
|
|
79
|
+
export * from "./components/bento-grid";
|
|
80
|
+
export * from "./components/bounded-number";
|
|
81
|
+
export * from "./components/breadcrumb";
|
|
82
|
+
export * from "./components/button";
|
|
83
|
+
export * from "./components/button-group";
|
|
84
|
+
export * from "./components/calendar";
|
|
85
|
+
export * from "./components/card";
|
|
86
|
+
export * from "./components/carousel";
|
|
87
|
+
export * from "./components/change-review";
|
|
88
|
+
export * from "./components/checkbox";
|
|
89
|
+
export * from "./components/collapsible";
|
|
90
|
+
export * from "./components/collapsible-panel";
|
|
91
|
+
export * from "./components/color-picker";
|
|
92
|
+
export * from "./components/combobox";
|
|
93
|
+
export * from "./components/command";
|
|
94
|
+
export * from "./components/confirm-dialog";
|
|
95
|
+
export * from "./components/context-menu";
|
|
96
|
+
export * from "./components/copyable-value";
|
|
97
|
+
export * from "./components/date-picker";
|
|
98
|
+
export * from "./components/date-range-picker";
|
|
99
|
+
export * from "./components/descriptions";
|
|
100
|
+
export * from "./components/dialog";
|
|
101
|
+
export * from "./components/drawer";
|
|
102
|
+
export * from "./components/dropdown-menu";
|
|
103
|
+
export * from "./components/empty-state";
|
|
104
|
+
export * from "./components/error-state";
|
|
105
|
+
export * from "./components/expand-dialog";
|
|
106
|
+
export * from "./components/field-row";
|
|
107
|
+
export * from "./components/file-upload";
|
|
108
|
+
export * from "./components/form";
|
|
109
|
+
export * from "./components/hover-card";
|
|
110
|
+
export * from "./components/icon-button";
|
|
111
|
+
export * from "./components/input";
|
|
112
|
+
export * from "./components/input-group";
|
|
113
|
+
export * from "./components/input-otp";
|
|
114
|
+
export * from "./components/kbd";
|
|
115
|
+
export * from "./components/key-value-editor";
|
|
116
|
+
export * from "./components/label";
|
|
117
|
+
export * from "./components/link-preview";
|
|
118
|
+
export * from "./components/list-editor";
|
|
119
|
+
export * from "./components/locale-provider";
|
|
120
|
+
export * from "./components/loading-state";
|
|
121
|
+
export * from "./components/match-highlight";
|
|
122
|
+
export * from "./components/mention-input";
|
|
123
|
+
export * from "./components/menubar";
|
|
124
|
+
export * from "./components/model-picker";
|
|
125
|
+
export * from "./components/metric-card";
|
|
126
|
+
export * from "./components/nav-main";
|
|
127
|
+
export * from "./components/nav-notifications";
|
|
128
|
+
export * from "./components/nav-user";
|
|
129
|
+
export * from "./components/navigation-menu";
|
|
130
|
+
export * from "./components/number-input";
|
|
131
|
+
export * from "./components/page-shell";
|
|
132
|
+
export * from "./components/pagination";
|
|
133
|
+
export * from "./components/popover";
|
|
134
|
+
export * from "./components/progress";
|
|
135
|
+
export * from "./components/radio-group";
|
|
136
|
+
export * from "./components/rating";
|
|
137
|
+
export * from "./components/resizable";
|
|
138
|
+
export * from "./components/reveal";
|
|
139
|
+
export * from "./components/scroll-area";
|
|
140
|
+
export * from "./components/section-header";
|
|
141
|
+
export * from "./components/segmented-field";
|
|
142
|
+
export * from "./components/select";
|
|
143
|
+
export * from "./components/separator";
|
|
144
|
+
export * from "./components/sheet";
|
|
145
|
+
export * from "./components/sidebar";
|
|
146
|
+
export * from "./components/skeleton";
|
|
147
|
+
export * from "./components/slider";
|
|
148
|
+
export * from "./components/slider-number";
|
|
149
|
+
export * from "./components/sonner";
|
|
150
|
+
export * from "./components/spinner";
|
|
151
|
+
export * from "./components/state-panel";
|
|
152
|
+
export * from "./components/status-badge";
|
|
153
|
+
export * from "./components/split-panel";
|
|
154
|
+
export * from "./components/switch";
|
|
155
|
+
export * from "./components/table";
|
|
156
|
+
export * from "./components/tabs";
|
|
157
|
+
export * from "./components/tag-input";
|
|
158
|
+
export * from "./components/team-switcher";
|
|
159
|
+
export * from "./components/textarea";
|
|
160
|
+
export * from "./components/theme-switcher";
|
|
161
|
+
export * from "./components/revision-timeline";
|
|
162
|
+
export * from "./components/timeline";
|
|
163
|
+
export * from "./components/toggle";
|
|
164
|
+
export * from "./components/toggle-group";
|
|
165
|
+
export * from "./components/toolbar";
|
|
166
|
+
export * from "./components/tooltip";
|
|
167
|
+
export * from "./components/top-nav";
|
|
168
|
+
export * from "./components/transfer";
|
|
169
|
+
export * from "./components/tree";
|
|
170
|
+
export * from "./components/tree-select";
|
|
171
|
+
export * from "./components/typography";
|
|
172
|
+
export * from "./components/view-toolbar";
|
|
173
|
+
export * from "./components/virtual-select";
|
|
174
|
+
export * from "./components/wizard";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
|
|
4
|
+
describe("cn", () => {
|
|
5
|
+
it("later utilities win for a genuine conflict (e.g. padding)", () => {
|
|
6
|
+
expect(cn("p-2", "p-4")).toBe("p-4");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("keeps semantic type-role and text-color independent (#187)", () => {
|
|
10
|
+
expect(cn("text-kpi", "text-foreground")).toContain("text-kpi");
|
|
11
|
+
expect(cn("text-kpi", "text-foreground")).toContain("text-foreground");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// #336: the CardDescription regression was NOT a tailwind-merge conflict — it
|
|
15
|
+
// was an invalid, misspelled utility name that isn't a real Tailwind class,
|
|
16
|
+
// which tailwind-merge's default `text-*` color group happily classified as
|
|
17
|
+
// an unrecognized color and dropped in favor of whichever `text-<color>`
|
|
18
|
+
// utility came later. `text-balance` is a real, already-registered Tailwind
|
|
19
|
+
// utility that tailwind-merge groups under its own `text-wrap` conflict
|
|
20
|
+
// group by default — no `cn.ts` registration is (or ever was) needed to
|
|
21
|
+
// keep it independent from `text-<color>`.
|
|
22
|
+
it("keeps text-balance and text-<color> independent regardless of order (#336)", () => {
|
|
23
|
+
const colorFirst = cn("text-sm", "text-muted-foreground", "text-balance");
|
|
24
|
+
expect(colorFirst).toContain("text-muted-foreground");
|
|
25
|
+
expect(colorFirst).toContain("text-balance");
|
|
26
|
+
|
|
27
|
+
const wrapFirst = cn("text-sm", "text-balance", "text-muted-foreground");
|
|
28
|
+
expect(wrapFirst).toContain("text-muted-foreground");
|
|
29
|
+
expect(wrapFirst).toContain("text-balance");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("still dedupes conflicting text-wrap utilities against each other", () => {
|
|
33
|
+
expect(cn("text-balance", "text-pretty")).toBe("text-pretty");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("still dedupes conflicting text-<color> utilities against each other", () => {
|
|
37
|
+
expect(cn("text-red-500", "text-blue-500")).toBe("text-blue-500");
|
|
38
|
+
});
|
|
39
|
+
});
|
package/src/lib/cn.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from "clsx";
|
|
2
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The semantic type roles (`--text-<role>` tokens → `text-<role>` utilities, #187).
|
|
6
|
+
* tailwind-merge doesn't know custom theme keys, so without registration it
|
|
7
|
+
* classifies `text-title` as a text COLOR — and `cn("text-kpi", "text-foreground")`
|
|
8
|
+
* would silently drop the role. Registering the roles in the `font-size` class
|
|
9
|
+
* group makes role-vs-role and role-vs-raw-size conflicts resolve correctly while
|
|
10
|
+
* keeping role + color independent.
|
|
11
|
+
*/
|
|
12
|
+
const TEXT_ROLES = ["display", "title", "subtitle", "body", "caption", "meta", "kpi", "code"];
|
|
13
|
+
|
|
14
|
+
const twMerge = extendTailwindMerge({
|
|
15
|
+
extend: {
|
|
16
|
+
classGroups: {
|
|
17
|
+
"font-size": [{ text: TEXT_ROLES }],
|
|
18
|
+
// `font-display` is the display-face seam (sibling of font-mono, #187).
|
|
19
|
+
"font-family": [{ font: ["display"] }],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Merge class names with conflict resolution. `clsx` handles conditionals;
|
|
26
|
+
* `tailwind-merge` ensures later Tailwind utilities win (e.g. `p-2 p-4` -> `p-4`).
|
|
27
|
+
* Use this in every component that accepts a `className` prop.
|
|
28
|
+
*/
|
|
29
|
+
export function cn(...inputs: ClassValue[]): string {
|
|
30
|
+
return twMerge(clsx(inputs));
|
|
31
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compact-number.ts — the same "shown short, copied exact" number rule the
|
|
3
|
+
* charts package applies to axis ticks and tooltips, available to `ui` surfaces
|
|
4
|
+
* that display a figure (today: `MetricCard`).
|
|
5
|
+
*
|
|
6
|
+
* DELIBERATE TWIN of `packages/charts/src/charts/value-format.ts`. Dependencies
|
|
7
|
+
* flow one way — `tokens → ui → charts` — so `ui` cannot import the charts
|
|
8
|
+
* module, and a chart-domain union does not belong in the `ui` public API just
|
|
9
|
+
* to be shared. The duplication is ~20 lines and the two must stay in step:
|
|
10
|
+
* **change one, change the other**, and keep {@link COMPACT_THRESHOLD} equal in
|
|
11
|
+
* both. `metric-card.test.tsx` and `value-format.test.ts` pin the same cases on
|
|
12
|
+
* each side so a drift shows up as a failing test rather than as two surfaces
|
|
13
|
+
* disagreeing about what `1500` looks like.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** How a numeric value is rendered. Mirrors the charts `ChartValueFormat`. */
|
|
17
|
+
export type NumberFormatKind = "number" | "compact" | "currency" | "percent";
|
|
18
|
+
|
|
19
|
+
/** Magnitude at or above which `compact`/`currency` switch to compact notation. */
|
|
20
|
+
export const COMPACT_THRESHOLD = 1000;
|
|
21
|
+
|
|
22
|
+
/** Used when a caller asks for currency without naming one. */
|
|
23
|
+
export const DEFAULT_CURRENCY = "USD";
|
|
24
|
+
|
|
25
|
+
/** One decimal keeps `1.5M` readable without implying precision it lacks. */
|
|
26
|
+
export const DEFAULT_MAX_FRACTION_DIGITS = 1;
|
|
27
|
+
|
|
28
|
+
/** `Intl.NumberFormatOptions` for `format` at this magnitude. */
|
|
29
|
+
export function numberFormatOptions(
|
|
30
|
+
format: NumberFormatKind,
|
|
31
|
+
value: number,
|
|
32
|
+
currency: string = DEFAULT_CURRENCY,
|
|
33
|
+
maxFractionDigits?: number,
|
|
34
|
+
): Intl.NumberFormatOptions {
|
|
35
|
+
const compact = Number.isFinite(value) && Math.abs(value) >= COMPACT_THRESHOLD;
|
|
36
|
+
// `"number"` is the exact-value escape hatch and keeps `Intl`'s own default
|
|
37
|
+
// (3 digits) unless the caller states one — capping it at the compact rung's
|
|
38
|
+
// single decimal would render 0.0512 as "0.1".
|
|
39
|
+
const digits = maxFractionDigits ?? DEFAULT_MAX_FRACTION_DIGITS;
|
|
40
|
+
switch (format) {
|
|
41
|
+
case "number":
|
|
42
|
+
return maxFractionDigits === undefined ? {} : { maximumFractionDigits: maxFractionDigits };
|
|
43
|
+
case "percent":
|
|
44
|
+
return { style: "percent", maximumFractionDigits: digits };
|
|
45
|
+
case "currency":
|
|
46
|
+
return compact
|
|
47
|
+
? {
|
|
48
|
+
style: "currency",
|
|
49
|
+
currency,
|
|
50
|
+
notation: "compact",
|
|
51
|
+
compactDisplay: "short",
|
|
52
|
+
maximumFractionDigits: digits,
|
|
53
|
+
}
|
|
54
|
+
: { style: "currency", currency, maximumFractionDigits: digits };
|
|
55
|
+
case "compact":
|
|
56
|
+
default:
|
|
57
|
+
return compact
|
|
58
|
+
? { notation: "compact", compactDisplay: "short", maximumFractionDigits: digits }
|
|
59
|
+
: { maximumFractionDigits: digits };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { normalizeQuoteText, normalizeQuoteTextWithOffsets } from "./document-address";
|
|
4
|
+
|
|
5
|
+
/** Map a normalized range back and read the slice it names in the original. */
|
|
6
|
+
function sliceVia(original: string, from: number, to: number): string {
|
|
7
|
+
const { offsets } = normalizeQuoteTextWithOffsets(original);
|
|
8
|
+
return original.slice(offsets[from], offsets[to]);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("normalizeQuoteText", () => {
|
|
12
|
+
it("collapses every whitespace run to one space, and trims", () => {
|
|
13
|
+
expect(normalizeQuoteText(" the\n\n quick\tbrown \r\n fox ")).toBe("the quick brown fox");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("straightens the quotes and dashes an extractor disagrees about", () => {
|
|
17
|
+
// The same sentence out of a PDF and out of a model. Neither side should
|
|
18
|
+
// have to know which glyph the other used.
|
|
19
|
+
expect(normalizeQuoteText("“Don’t”—she said")).toBe(normalizeQuoteText('"Don\'t"-she said'));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("folds case, so a citation matches without a caller-set flag", () => {
|
|
23
|
+
expect(normalizeQuoteText("Total Revenue")).toBe(normalizeQuoteText("total revenue"));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("drops the invisibles a line break leaves behind", () => {
|
|
27
|
+
// A soft hyphen at a PDF's line wrap, and a zero-width space.
|
|
28
|
+
expect(normalizeQuoteText("revenue")).toBe("revenue");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("leaves a ligature alone — folding it would be a 1:many expansion", () => {
|
|
32
|
+
// The offset map is only trustworthy while every rule is a collapse, a drop
|
|
33
|
+
// or a 1:1 swap. A fuzzier match is not worth losing that.
|
|
34
|
+
expect(normalizeQuoteText("final")).toBe("final");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("normalizeQuoteTextWithOffsets", () => {
|
|
39
|
+
it("ends one past the last SURVIVING character, not at the input's length", () => {
|
|
40
|
+
// Ending at the input's length would drag the trailing space into every
|
|
41
|
+
// range that runs to the end of the document.
|
|
42
|
+
const input = " hello world ";
|
|
43
|
+
const { text, offsets } = normalizeQuoteTextWithOffsets(input);
|
|
44
|
+
expect(text).toBe("hello world");
|
|
45
|
+
expect(offsets).toHaveLength(text.length + 1);
|
|
46
|
+
expect(offsets[offsets.length - 1]).toBe(input.lastIndexOf("d") + 1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("maps a normalized range back onto the ORIGINAL characters", () => {
|
|
50
|
+
// "world" in the normalized string is at [6, 11); in the original it sits
|
|
51
|
+
// past a three-space run and a leading pair, at completely different indices.
|
|
52
|
+
const original = " Hello World ";
|
|
53
|
+
expect(sliceVia(original, 6, 11)).toBe("World");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("points a collapsed space at where its run began", () => {
|
|
57
|
+
// Selecting across the gap must include the whole run, not just its last
|
|
58
|
+
// character — otherwise a highlight starts mid-whitespace.
|
|
59
|
+
const original = "a \n\t b";
|
|
60
|
+
expect(sliceVia(original, 0, 3)).toBe("a \n\t b");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("keeps the map monotone across folds, drops and case changes", () => {
|
|
64
|
+
const { offsets } = normalizeQuoteTextWithOffsets(" “Süß” — DONE ");
|
|
65
|
+
for (let i = 1; i < offsets.length; i += 1) {
|
|
66
|
+
expect(offsets[i]).toBeGreaterThan(offsets[i - 1] as number);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("survives a string that is nothing but whitespace", () => {
|
|
71
|
+
const { text, offsets } = normalizeQuoteTextWithOffsets(" \n ");
|
|
72
|
+
expect(text).toBe("");
|
|
73
|
+
expect(offsets).toEqual([0]);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How one part of a document is addressed.
|
|
3
|
+
*
|
|
4
|
+
* A viewer that can only open a file answers "here is the document". A viewer
|
|
5
|
+
* that can be pointed at a PART of one answers "here is the sentence that
|
|
6
|
+
* claim came from" — the RAG-citation case, and the same machinery behind
|
|
7
|
+
* find-in-document.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this lives in `@elabs-ai/components-ui`
|
|
10
|
+
*
|
|
11
|
+
* The producer of an address (a chat answer's citation, in
|
|
12
|
+
* `@elabs-ai/components-ai`) and its consumer (the file viewer, in
|
|
13
|
+
* `@elabs-ai/components-viewer`) are Layer-2 siblings that may not
|
|
14
|
+
* import each other. `ui` is the only layer both reach, which is exactly why
|
|
15
|
+
* `FileSource` and `MatchRange` already live here. This module is a
|
|
16
|
+
* dependency-free leaf: types plus pure string functions, no React.
|
|
17
|
+
*
|
|
18
|
+
* ## Three kinds, one union
|
|
19
|
+
*
|
|
20
|
+
* Real producers disagree about what they can emit, so the union carries all
|
|
21
|
+
* three rather than forcing everyone through the weakest:
|
|
22
|
+
*
|
|
23
|
+
* - **`quote`** — the snippet text. The only kind that survives crossing a tool
|
|
24
|
+
* boundary: an indexing pipeline's character offsets are computed against ITS
|
|
25
|
+
* extraction of the PDF, never against ours, so its numbers are meaningless
|
|
26
|
+
* here while its text still is. Lossy (a repeated phrase is ambiguous) and the
|
|
27
|
+
* most useful.
|
|
28
|
+
* - **`range`** — exact character offsets, and exact only against the SAME text
|
|
29
|
+
* projection: `AdapterDocument.text` as this build's adapter produced it. Use
|
|
30
|
+
* it when both ends are ours (find-in-document, a second pass over a document
|
|
31
|
+
* already open).
|
|
32
|
+
* - **`rect`** — page-relative boxes, `0..1` fractions of the page. Needs no
|
|
33
|
+
* text at all, which is what makes it the answer for a scanned page, a chart,
|
|
34
|
+
* a photo region, or any layout-analysis model whose output is geometry.
|
|
35
|
+
*
|
|
36
|
+
* ## The rule that lets this union grow
|
|
37
|
+
*
|
|
38
|
+
* A consumer matches only the kinds it declared it honours and reports the rest
|
|
39
|
+
* as unsupported — it never writes an exhaustive `switch` with a `never`
|
|
40
|
+
* fallthrough. That is what lets a fourth kind (a spreadsheet `cell` address,
|
|
41
|
+
* say) be added later without breaking every consumer that predates it.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** The address kinds, as a value — for capability declarations and iteration. */
|
|
45
|
+
export const DOCUMENT_ADDRESS_KINDS = ["quote", "range", "rect"] as const;
|
|
46
|
+
|
|
47
|
+
/** Which way a part of a document is addressed. */
|
|
48
|
+
export type DocumentAddressKind = (typeof DOCUMENT_ADDRESS_KINDS)[number];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A box on a page, as fractions of that page's own box.
|
|
52
|
+
*
|
|
53
|
+
* Fractions, not pixels, because the page is not a fixed size: it rescales with
|
|
54
|
+
* zoom, with the pane's width and with the device's pixel ratio. A consumer
|
|
55
|
+
* multiplies by the page element's live measured size at paint time, so the box
|
|
56
|
+
* stays put through every one of those. Origin is top-left (CSS), so `y` grows
|
|
57
|
+
* downward — matching the DOM rather than the PDF's own bottom-left space.
|
|
58
|
+
*/
|
|
59
|
+
export interface DocumentRect {
|
|
60
|
+
/** Distance from the page's left edge, `0..1`. */
|
|
61
|
+
x: number;
|
|
62
|
+
/** Distance from the page's TOP edge, `0..1`. */
|
|
63
|
+
y: number;
|
|
64
|
+
/** Fraction of the page's width. */
|
|
65
|
+
width: number;
|
|
66
|
+
/** Fraction of the page's height. */
|
|
67
|
+
height: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Address by the text itself — the interoperable kind.
|
|
72
|
+
*
|
|
73
|
+
* Matching is done on the normalized form of both sides
|
|
74
|
+
* ({@link normalizeQuoteText}), so a quote survives the whitespace, quote-glyph
|
|
75
|
+
* and casing differences that every extractor introduces.
|
|
76
|
+
*/
|
|
77
|
+
export interface QuoteAddress {
|
|
78
|
+
kind: "quote";
|
|
79
|
+
/** The passage to find. Whitespace and quote glyphs need not match exactly. */
|
|
80
|
+
text: string;
|
|
81
|
+
/**
|
|
82
|
+
* Which occurrence to take when the passage appears more than once, 1-based.
|
|
83
|
+
* Omitted means the first — or the one nearest {@link QuoteAddress.near},
|
|
84
|
+
* when that is given.
|
|
85
|
+
*/
|
|
86
|
+
occurrence?: number;
|
|
87
|
+
/**
|
|
88
|
+
* A hint about where to look. It disambiguates repeats rather than restricting
|
|
89
|
+
* the search: the occurrence closest to it wins, and a passage that turns out
|
|
90
|
+
* to be elsewhere is still found.
|
|
91
|
+
*/
|
|
92
|
+
near?: {
|
|
93
|
+
/**
|
|
94
|
+
* 1-based page (or slide) the passage is expected on.
|
|
95
|
+
*
|
|
96
|
+
* Advisory, and deliberately NOT used to navigate: the viewer turns to the
|
|
97
|
+
* page the passage was actually located on, so a stale hint cannot send a
|
|
98
|
+
* reader to a blank page. Only {@link QuoteAddress.near.offset} takes part in
|
|
99
|
+
* tie-breaking today; a renderer may read this off the address if it wants
|
|
100
|
+
* more.
|
|
101
|
+
*/
|
|
102
|
+
page?: number;
|
|
103
|
+
/** Approximate character offset the passage is expected near. */
|
|
104
|
+
offset?: number;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Address by character offsets into `AdapterDocument.text`, half-open
|
|
110
|
+
* `[start, end)` — the same convention as `MatchRange`.
|
|
111
|
+
*
|
|
112
|
+
* Only meaningful against the text projection THIS build produced. An offset
|
|
113
|
+
* from a foreign pipeline is not this kind; it is a {@link QuoteAddress}.
|
|
114
|
+
*/
|
|
115
|
+
export interface RangeAddress {
|
|
116
|
+
kind: "range";
|
|
117
|
+
start: number;
|
|
118
|
+
end: number;
|
|
119
|
+
/**
|
|
120
|
+
* 1-based page hint, when the producer knows it. Advisory only — the viewer
|
|
121
|
+
* derives the page from the offsets against its own index, so a hint that
|
|
122
|
+
* disagrees cannot misdirect the pager.
|
|
123
|
+
*/
|
|
124
|
+
page?: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Address by geometry — one or more boxes on one page. */
|
|
128
|
+
export interface RectAddress {
|
|
129
|
+
kind: "rect";
|
|
130
|
+
/** 1-based page (or slide) number. */
|
|
131
|
+
page: number;
|
|
132
|
+
/**
|
|
133
|
+
* The boxes making up this one addressed part. An array, not a single box,
|
|
134
|
+
* because a real passage wraps: a sentence spanning three lines is three
|
|
135
|
+
* boxes, and a two-column paragraph is more. Painting them as one shape each
|
|
136
|
+
* is what avoids stitching DOM ranges together.
|
|
137
|
+
*/
|
|
138
|
+
rects: readonly DocumentRect[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Which part of a document something refers to. */
|
|
142
|
+
export type DocumentAddress = QuoteAddress | RangeAddress | RectAddress;
|
|
143
|
+
|
|
144
|
+
/* -------------------------------------------------------------------------- */
|
|
145
|
+
/* Quote normalization */
|
|
146
|
+
/* -------------------------------------------------------------------------- */
|
|
147
|
+
|
|
148
|
+
/** Straightened 1:1 — every one of these is one code unit in and one out. */
|
|
149
|
+
const CHARACTER_FOLDS: Readonly<Record<string, string>> = {
|
|
150
|
+
// Curly quotes and primes. A model quoting a PDF re-types these constantly;
|
|
151
|
+
// the file has the other one.
|
|
152
|
+
"‘": "'",
|
|
153
|
+
"’": "'",
|
|
154
|
+
"‚": "'",
|
|
155
|
+
"‛": "'",
|
|
156
|
+
"′": "'",
|
|
157
|
+
"“": '"',
|
|
158
|
+
"”": '"',
|
|
159
|
+
"„": '"',
|
|
160
|
+
"‟": '"',
|
|
161
|
+
"″": '"',
|
|
162
|
+
// Dashes. PDF extraction yields the typographic one, prose yields a hyphen.
|
|
163
|
+
"‐": "-",
|
|
164
|
+
"‑": "-",
|
|
165
|
+
"‒": "-",
|
|
166
|
+
"–": "-",
|
|
167
|
+
"—": "-",
|
|
168
|
+
"―": "-",
|
|
169
|
+
"−": "-",
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Invisible, and typically present in exactly one of the two strings being
|
|
174
|
+
* compared: soft hyphen (a PDF's line-break artefact), zero-width space, ZWNJ,
|
|
175
|
+
* ZWJ, word joiner. Written as escapes because a literal here is unreviewable —
|
|
176
|
+
* it renders as nothing.
|
|
177
|
+
*/
|
|
178
|
+
const DROPPED = new Set(["", "", "", "", ""]);
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Anything that collapses into one space. Plain `\s` is already the full set in
|
|
182
|
+
* JavaScript — it covers NBSP, the `U+2000`–`U+200A` spaces, the line and
|
|
183
|
+
* paragraph separators, narrow NBSP, ideographic space and the BOM.
|
|
184
|
+
*/
|
|
185
|
+
const WHITESPACE = /\s/u;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The result of normalizing, with the way back.
|
|
189
|
+
*
|
|
190
|
+
* `offsets` has one more entry than `text` has characters: `offsets[i]` is the
|
|
191
|
+
* index in the ORIGINAL string where normalized character `i` came from, and
|
|
192
|
+
* the final entry is one past the last character that survived — NOT the
|
|
193
|
+
* original's length, which would drag trailing whitespace into every range that
|
|
194
|
+
* runs to the end. So a normalized range `[a, b)` maps back to
|
|
195
|
+
* `[offsets[a], offsets[b])` with no arithmetic, which is the whole point of
|
|
196
|
+
* building the map rather than searching twice.
|
|
197
|
+
*/
|
|
198
|
+
export interface NormalizedText {
|
|
199
|
+
text: string;
|
|
200
|
+
offsets: readonly number[];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Fold a string to its canonical match key, keeping a map back to the original.
|
|
205
|
+
*
|
|
206
|
+
* Every rule is a collapse, a drop or a 1:1 swap — never an expansion — so the
|
|
207
|
+
* map stays monotone and a range maps back exactly. That constraint is why
|
|
208
|
+
* ligatures (`fi`) and the ellipsis character are deliberately left alone: both
|
|
209
|
+
* would be 1:many, and a fuzzier match is not worth an offset map that can no
|
|
210
|
+
* longer be trusted.
|
|
211
|
+
*
|
|
212
|
+
* What it does: collapses every whitespace run to one space and trims; drops
|
|
213
|
+
* zero-width characters and soft hyphens; straightens curly quotes and dashes;
|
|
214
|
+
* folds case — a citation is matched case-insensitively, and that is a property
|
|
215
|
+
* of the match key rather than a flag every caller has to remember.
|
|
216
|
+
*/
|
|
217
|
+
export function normalizeQuoteTextWithOffsets(input: string): NormalizedText {
|
|
218
|
+
const out: string[] = [];
|
|
219
|
+
const offsets: number[] = [];
|
|
220
|
+
let pendingSpaceAt: number | undefined;
|
|
221
|
+
/** One past the last surviving character — the map's terminal entry. */
|
|
222
|
+
let end = 0;
|
|
223
|
+
|
|
224
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
225
|
+
const character = input[i] as string;
|
|
226
|
+
|
|
227
|
+
if (WHITESPACE.test(character)) {
|
|
228
|
+
// Held rather than emitted, so a trailing run never reaches the output
|
|
229
|
+
// and a leading one is dropped by the `out.length` test below.
|
|
230
|
+
pendingSpaceAt ??= i;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (DROPPED.has(character)) continue;
|
|
234
|
+
|
|
235
|
+
if (pendingSpaceAt !== undefined) {
|
|
236
|
+
if (out.length > 0) {
|
|
237
|
+
out.push(" ");
|
|
238
|
+
// The single space stands for the whole run, so it points at where the
|
|
239
|
+
// run began — not at the character that ended it.
|
|
240
|
+
offsets.push(pendingSpaceAt);
|
|
241
|
+
}
|
|
242
|
+
pendingSpaceAt = undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const folded = CHARACTER_FOLDS[character] ?? character;
|
|
246
|
+
const lowered = folded.toLowerCase();
|
|
247
|
+
// Guard the 1:1 invariant: a few code points grow when lowercased
|
|
248
|
+
// (`İ` → `i̇`). Keeping the original is a worse match and a correct map; the
|
|
249
|
+
// reverse trade would silently corrupt every offset after it.
|
|
250
|
+
out.push(lowered.length === folded.length ? lowered : folded);
|
|
251
|
+
offsets.push(i);
|
|
252
|
+
end = i + 1;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
offsets.push(end);
|
|
256
|
+
return { text: out.join(""), offsets };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The canonical match key for a quote — {@link normalizeQuoteTextWithOffsets}
|
|
261
|
+
* without the map, for comparing or de-duplicating two quotes.
|
|
262
|
+
*/
|
|
263
|
+
export function normalizeQuoteText(input: string): string {
|
|
264
|
+
return normalizeQuoteTextWithOffsets(input).text;
|
|
265
|
+
}
|