@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,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser file-download helpers — the single home for the
|
|
3
|
+
* `Blob`/URL → hidden `<a download>` → click → revoke dance that was previously
|
|
4
|
+
* copy-pasted across `@elabs-ai/components-ai` (ConversationDownload), `@elabs-ai/components-data`
|
|
5
|
+
* (downloadCsv) and now `@elabs-ai/components-ai` Gallery.
|
|
6
|
+
*
|
|
7
|
+
* Pure DOM mechanics, no React, no other deps — and SSR-guarded
|
|
8
|
+
* (`typeof document === "undefined"` → no-op) so it's safe to import from
|
|
9
|
+
* dependency-light modules. The CONTENT (CSV serialization, markdown export,
|
|
10
|
+
* image source) stays with each caller; this only triggers the save.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Internal: click a hidden anchor for `url`, optionally suggesting `filename`. */
|
|
14
|
+
function clickAnchor(url: string, filename?: string): void {
|
|
15
|
+
const a = document.createElement("a");
|
|
16
|
+
a.href = url;
|
|
17
|
+
// `download` is only honored for same-origin / blob: / data: URLs; harmless otherwise.
|
|
18
|
+
if (filename) a.download = filename;
|
|
19
|
+
a.rel = "noopener";
|
|
20
|
+
a.style.display = "none";
|
|
21
|
+
document.body.appendChild(a);
|
|
22
|
+
a.click();
|
|
23
|
+
a.remove();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Best-effort filename from a URL path; falls back to "download". */
|
|
27
|
+
function filenameFromUrl(url: string): string {
|
|
28
|
+
try {
|
|
29
|
+
const base = "http://localhost";
|
|
30
|
+
const ref = typeof window === "undefined" ? base : window.location.href;
|
|
31
|
+
const { pathname } = new URL(url, ref);
|
|
32
|
+
const last = pathname.split("/").filter(Boolean).pop();
|
|
33
|
+
return last && last.length > 0 ? decodeURIComponent(last) : "download";
|
|
34
|
+
} catch {
|
|
35
|
+
return "download";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** True for sources `<a download>` can save directly (same-origin, blob:, data:). */
|
|
40
|
+
function isDirectlyDownloadable(url: string): boolean {
|
|
41
|
+
if (url.startsWith("data:") || url.startsWith("blob:")) return true;
|
|
42
|
+
if (typeof window === "undefined") return true;
|
|
43
|
+
try {
|
|
44
|
+
return new URL(url, window.location.href).origin === window.location.origin;
|
|
45
|
+
} catch {
|
|
46
|
+
// Relative or unparseable → treat as same-origin (the anchor resolves it).
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Trigger a download for an in-memory `Blob`. No-op in non-DOM environments.
|
|
53
|
+
* Revokes the object URL immediately after the click (the browser captures the
|
|
54
|
+
* download synchronously, mirroring the prior call sites).
|
|
55
|
+
*/
|
|
56
|
+
export function downloadBlob(blob: Blob, filename: string): void {
|
|
57
|
+
if (typeof document === "undefined") return;
|
|
58
|
+
const url = URL.createObjectURL(blob);
|
|
59
|
+
try {
|
|
60
|
+
clickAnchor(url, filename);
|
|
61
|
+
} finally {
|
|
62
|
+
URL.revokeObjectURL(url);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Trigger a download for an existing `url` (an http(s)/relative URL, or a
|
|
68
|
+
* `data:`/`blob:` URI). No-op in non-DOM environments.
|
|
69
|
+
*
|
|
70
|
+
* Same-origin / `blob:` / `data:` → a direct `<a download>` (honors `filename`).
|
|
71
|
+
* Cross-origin → the browser ignores the `download` filename, so we `fetch` the
|
|
72
|
+
* bytes and save via {@link downloadBlob}. This requires the server to allow CORS;
|
|
73
|
+
* on failure (CORS/opaque/offline) we fall back to opening the source in a new
|
|
74
|
+
* tab so the user still gets the asset. Fire-and-forget: `void downloadUrl(...)`.
|
|
75
|
+
*/
|
|
76
|
+
export async function downloadUrl(url: string, filename?: string): Promise<void> {
|
|
77
|
+
if (typeof document === "undefined") return;
|
|
78
|
+
|
|
79
|
+
if (isDirectlyDownloadable(url)) {
|
|
80
|
+
clickAnchor(url, filename);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetch(url);
|
|
86
|
+
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
87
|
+
const blob = await res.blob();
|
|
88
|
+
downloadBlob(blob, filename ?? filenameFromUrl(url));
|
|
89
|
+
} catch {
|
|
90
|
+
// CORS / opaque / offline: best effort so the user still reaches the asset.
|
|
91
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { FILE_CATEGORY_ICONS, fileIconFor } from "./file-icon";
|
|
4
|
+
import type { FileCategory } from "./file-kind";
|
|
5
|
+
|
|
6
|
+
const CATEGORIES: FileCategory[] = [
|
|
7
|
+
"image",
|
|
8
|
+
"video",
|
|
9
|
+
"audio",
|
|
10
|
+
"document",
|
|
11
|
+
"spreadsheet",
|
|
12
|
+
"presentation",
|
|
13
|
+
"code",
|
|
14
|
+
"text",
|
|
15
|
+
"data",
|
|
16
|
+
"archive",
|
|
17
|
+
"unknown",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
describe("FILE_CATEGORY_ICONS", () => {
|
|
21
|
+
it("covers every category", () => {
|
|
22
|
+
for (const category of CATEGORIES) {
|
|
23
|
+
expect(FILE_CATEGORY_ICONS[category], category).toBeDefined();
|
|
24
|
+
}
|
|
25
|
+
expect(Object.keys(FILE_CATEGORY_ICONS).sort()).toEqual([...CATEGORIES].sort());
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("gives each category a distinct glyph, so the icon is a real second channel", () => {
|
|
29
|
+
const glyphs = new Set(Object.values(FILE_CATEGORY_ICONS));
|
|
30
|
+
expect(glyphs.size).toBe(CATEGORIES.length);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("fileIconFor", () => {
|
|
35
|
+
it("resolves through the file kind", () => {
|
|
36
|
+
expect(fileIconFor("a.png")).toBe(FILE_CATEGORY_ICONS.image);
|
|
37
|
+
expect(fileIconFor("a.xlsx")).toBe(FILE_CATEGORY_ICONS.spreadsheet);
|
|
38
|
+
expect(fileIconFor("download", "video/mp4")).toBe(FILE_CATEGORY_ICONS.video);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("falls back to the generic glyph rather than throwing", () => {
|
|
42
|
+
expect(fileIconFor("mystery.wat")).toBe(FILE_CATEGORY_ICONS.unknown);
|
|
43
|
+
expect(fileIconFor("")).toBe(FILE_CATEGORY_ICONS.unknown);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One glyph per {@link FileCategory} — the single icon map for file-shaped UI.
|
|
3
|
+
*
|
|
4
|
+
* Supersedes two package-private maps that could not see each other:
|
|
5
|
+
* `PRODUCED_ASSET_ICONS` (`@elabs-ai/components-ai` file-tree, keyed by
|
|
6
|
+
* the five-member `ContextAssetType`) and `mediaCategoryIcons` (same package,
|
|
7
|
+
* attachments, keyed by six MIME buckets) — so the same `.csv` drew a table glyph
|
|
8
|
+
* in one surface and a generic document glyph in the other.
|
|
9
|
+
*
|
|
10
|
+
* Lucide per `.claude/rules/icons.md`: these are generic UI glyphs, not product
|
|
11
|
+
* vocabulary, so nothing here belongs in
|
|
12
|
+
* `@elabs-ai/components-icons`.
|
|
13
|
+
*
|
|
14
|
+
* Values only — this module holds component REFERENCES, it renders nothing.
|
|
15
|
+
* Icons inherit color from `currentColor`; the category is never carried by hue
|
|
16
|
+
* alone, so a caller must still supply a text label or an `aria-label` on the
|
|
17
|
+
* control (WCAG 1.4.1).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
File,
|
|
22
|
+
FileArchive,
|
|
23
|
+
FileCode,
|
|
24
|
+
FileDigit,
|
|
25
|
+
FileImage,
|
|
26
|
+
FileMusic,
|
|
27
|
+
FileSpreadsheet,
|
|
28
|
+
FileText,
|
|
29
|
+
FileType,
|
|
30
|
+
FileVideoCamera,
|
|
31
|
+
type LucideIcon,
|
|
32
|
+
Presentation,
|
|
33
|
+
} from "lucide-react";
|
|
34
|
+
|
|
35
|
+
import { type FileCategory, resolveFileKind } from "./file-kind";
|
|
36
|
+
|
|
37
|
+
/** The glyph for each coarse file shape. Exhaustive over {@link FileCategory}. */
|
|
38
|
+
export const FILE_CATEGORY_ICONS: Readonly<Record<FileCategory, LucideIcon>> = {
|
|
39
|
+
image: FileImage,
|
|
40
|
+
video: FileVideoCamera,
|
|
41
|
+
audio: FileMusic,
|
|
42
|
+
document: FileText,
|
|
43
|
+
spreadsheet: FileSpreadsheet,
|
|
44
|
+
presentation: Presentation,
|
|
45
|
+
code: FileCode,
|
|
46
|
+
text: FileType,
|
|
47
|
+
data: FileDigit,
|
|
48
|
+
archive: FileArchive,
|
|
49
|
+
unknown: File,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The glyph for a file, resolved from its name and (optionally) MIME.
|
|
54
|
+
* Falls back to the generic file glyph rather than throwing.
|
|
55
|
+
*/
|
|
56
|
+
export function fileIconFor(name: string, mediaType?: string): LucideIcon {
|
|
57
|
+
return FILE_CATEGORY_ICONS[resolveFileKind(name, mediaType).category];
|
|
58
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { extensionOf, mediaTypeFromName, resolveFileKind } from "./file-kind";
|
|
4
|
+
|
|
5
|
+
describe("extensionOf", () => {
|
|
6
|
+
it("lowercases and drops the dot", () => {
|
|
7
|
+
expect(extensionOf("Report.PDF")).toBe("pdf");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("uses the last dot", () => {
|
|
11
|
+
expect(extensionOf("archive.tar.gz")).toBe("gz");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("strips a path", () => {
|
|
15
|
+
expect(extensionOf("/some/dir/notes.md")).toBe("md");
|
|
16
|
+
expect(extensionOf("C:\\docs\\notes.md")).toBe("md");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("strips a query and hash, so a URL-derived name still resolves", () => {
|
|
20
|
+
expect(extensionOf("chart.svg?v=2")).toBe("svg");
|
|
21
|
+
expect(extensionOf("chart.svg#top")).toBe("svg");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("is empty for a dotfile, a trailing dot and no extension", () => {
|
|
25
|
+
expect(extensionOf(".gitignore")).toBe("");
|
|
26
|
+
expect(extensionOf("noext.")).toBe("");
|
|
27
|
+
expect(extensionOf("README")).toBe("");
|
|
28
|
+
expect(extensionOf("")).toBe("");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("mediaTypeFromName", () => {
|
|
33
|
+
it("infers from the extension", () => {
|
|
34
|
+
expect(mediaTypeFromName("a.pdf")).toBe("application/pdf");
|
|
35
|
+
expect(mediaTypeFromName("a.tsx")).toBe("text/x-typescript");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("is undefined for an unknown extension, so callers own the fallback", () => {
|
|
39
|
+
expect(mediaTypeFromName("a.wat")).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("resolveFileKind", () => {
|
|
44
|
+
it("resolves the common categories", () => {
|
|
45
|
+
expect(resolveFileKind("photo.png").category).toBe("image");
|
|
46
|
+
expect(resolveFileKind("clip.mp4", "video/mp4").category).toBe("video");
|
|
47
|
+
expect(resolveFileKind("song.mp3", "audio/mpeg").category).toBe("audio");
|
|
48
|
+
expect(resolveFileKind("report.pdf").category).toBe("document");
|
|
49
|
+
expect(resolveFileKind("book.xlsx").category).toBe("spreadsheet");
|
|
50
|
+
expect(resolveFileKind("deck.pptx").category).toBe("presentation");
|
|
51
|
+
expect(resolveFileKind("main.rs").category).toBe("code");
|
|
52
|
+
expect(resolveFileKind("notes.md").category).toBe("text");
|
|
53
|
+
expect(resolveFileKind("rows.csv").category).toBe("data");
|
|
54
|
+
expect(resolveFileKind("bundle.zip").category).toBe("archive");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns the extension and resolved MIME alongside the category", () => {
|
|
58
|
+
expect(resolveFileKind("Report.PDF")).toEqual({
|
|
59
|
+
category: "document",
|
|
60
|
+
mediaType: "application/pdf",
|
|
61
|
+
extension: "pdf",
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("prefers a meaningful declared MIME over the extension", () => {
|
|
66
|
+
// The name says nothing; the server does.
|
|
67
|
+
expect(resolveFileKind("download", "image/webp")).toMatchObject({
|
|
68
|
+
category: "image",
|
|
69
|
+
mediaType: "image/webp",
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("ignores the placeholder MIMEs a source uses to mean 'unknown'", () => {
|
|
74
|
+
expect(resolveFileKind("a.pdf", "application/octet-stream").mediaType).toBe("application/pdf");
|
|
75
|
+
expect(resolveFileKind("a.pdf", "binary/octet-stream").mediaType).toBe("application/pdf");
|
|
76
|
+
expect(resolveFileKind("a.pdf", "").mediaType).toBe("application/pdf");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("strips MIME parameters and casing", () => {
|
|
80
|
+
expect(resolveFileKind("rows.csv", "TEXT/CSV; charset=utf-8").mediaType).toBe("text/csv");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("lets the extension win where it is strictly more specific than a text/* MIME", () => {
|
|
84
|
+
// Servers routinely send text/plain for these; "text" would be the wrong surface.
|
|
85
|
+
expect(resolveFileKind("rows.csv", "text/plain").category).toBe("data");
|
|
86
|
+
expect(resolveFileKind("app.ts", "text/plain").category).toBe("code");
|
|
87
|
+
expect(resolveFileKind("page.html", "text/html").category).toBe("code");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("falls back to unknown without throwing", () => {
|
|
91
|
+
expect(resolveFileKind("mystery.wat")).toEqual({
|
|
92
|
+
category: "unknown",
|
|
93
|
+
mediaType: "application/octet-stream",
|
|
94
|
+
extension: "wat",
|
|
95
|
+
});
|
|
96
|
+
expect(resolveFileKind("")).toMatchObject({ category: "unknown", extension: "" });
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one file-type resolver for the whole library — extension + MIME → a
|
|
3
|
+
* COARSE, CLOSED category.
|
|
4
|
+
*
|
|
5
|
+
* Before this existed the repo had three competing, package-private ideas of
|
|
6
|
+
* "what kind of file is this": `ContextAssetType`
|
|
7
|
+
* (`markdown|code|sql|csv|image`, `@elabs-ai/components-ai`),
|
|
8
|
+
* `AttachmentMediaCategory` (six MIME buckets, same package) and
|
|
9
|
+
* `UploadFile` (no kind at all, `@elabs-ai/components-ui`). None could
|
|
10
|
+
* see the others, so each grew its own icon map and its own gaps.
|
|
11
|
+
*
|
|
12
|
+
* ## Why the category is closed and coarse (ADR 0024 §4)
|
|
13
|
+
*
|
|
14
|
+
* {@link FileCategory} answers "what SHAPE of thing is this" — enough to pick an
|
|
15
|
+
* icon, a label, or a fallback surface. It is deliberately NOT the routing key
|
|
16
|
+
* for a renderer. Fine-grained format matching (`.docx` vs `.odt`,
|
|
17
|
+
* `.tsx` vs `.rs`) belongs to the adapter manifests in the package that owns the
|
|
18
|
+
* parsers, so adding a format never means editing this file — and never means a
|
|
19
|
+
* `@elabs-ai/components-ui` release.
|
|
20
|
+
*
|
|
21
|
+
* Pure data + string handling: no React, no DOM, no other deps.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Coarse file shape. CLOSED — adding a member is a breaking change for any
|
|
26
|
+
* consumer with an exhaustive `switch`, so extend the adapter manifests instead.
|
|
27
|
+
*/
|
|
28
|
+
export type FileCategory =
|
|
29
|
+
| "image"
|
|
30
|
+
| "video"
|
|
31
|
+
| "audio"
|
|
32
|
+
| "document"
|
|
33
|
+
| "spreadsheet"
|
|
34
|
+
| "presentation"
|
|
35
|
+
| "code"
|
|
36
|
+
| "text"
|
|
37
|
+
| "data"
|
|
38
|
+
| "archive"
|
|
39
|
+
| "unknown";
|
|
40
|
+
|
|
41
|
+
/** What {@link resolveFileKind} knows about a file before anything reads it. */
|
|
42
|
+
export interface FileKind {
|
|
43
|
+
/** Coarse shape, for icons, labels and fallback surfaces. */
|
|
44
|
+
category: FileCategory;
|
|
45
|
+
/**
|
|
46
|
+
* Best-known MIME type. The caller's value wins when it is meaningful;
|
|
47
|
+
* otherwise inferred from the extension, else `"application/octet-stream"`.
|
|
48
|
+
*/
|
|
49
|
+
mediaType: string;
|
|
50
|
+
/** Lowercased extension WITHOUT the dot (`"pdf"`), or `""` when there is none. */
|
|
51
|
+
extension: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extension → MIME. Only entries whose MIME we would otherwise have to guess;
|
|
56
|
+
* the browser fills `File.type` correctly for common web formats, but leaves it
|
|
57
|
+
* empty for most source-code and data files.
|
|
58
|
+
*/
|
|
59
|
+
const EXTENSION_MEDIA_TYPES: Readonly<Record<string, string>> = {
|
|
60
|
+
// Media. The browser fills `File.type` for these, but a NAME on its own (a
|
|
61
|
+
// URL, an agent-produced filename, a drag-and-drop path) carries no MIME —
|
|
62
|
+
// and resolving `photo.png` to "unknown" is the whole failure this map exists
|
|
63
|
+
// to prevent.
|
|
64
|
+
png: "image/png",
|
|
65
|
+
jpg: "image/jpeg",
|
|
66
|
+
jpeg: "image/jpeg",
|
|
67
|
+
gif: "image/gif",
|
|
68
|
+
webp: "image/webp",
|
|
69
|
+
avif: "image/avif",
|
|
70
|
+
bmp: "image/bmp",
|
|
71
|
+
ico: "image/x-icon",
|
|
72
|
+
tif: "image/tiff",
|
|
73
|
+
tiff: "image/tiff",
|
|
74
|
+
heic: "image/heic",
|
|
75
|
+
mp4: "video/mp4",
|
|
76
|
+
m4v: "video/mp4",
|
|
77
|
+
webm: "video/webm",
|
|
78
|
+
mov: "video/quicktime",
|
|
79
|
+
avi: "video/x-msvideo",
|
|
80
|
+
mkv: "video/x-matroska",
|
|
81
|
+
mp3: "audio/mpeg",
|
|
82
|
+
wav: "audio/wav",
|
|
83
|
+
ogg: "audio/ogg",
|
|
84
|
+
oga: "audio/ogg",
|
|
85
|
+
m4a: "audio/mp4",
|
|
86
|
+
flac: "audio/flac",
|
|
87
|
+
aac: "audio/aac",
|
|
88
|
+
// Documents
|
|
89
|
+
pdf: "application/pdf",
|
|
90
|
+
doc: "application/msword",
|
|
91
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
92
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
93
|
+
rtf: "application/rtf",
|
|
94
|
+
epub: "application/epub+zip",
|
|
95
|
+
// Spreadsheets
|
|
96
|
+
xls: "application/vnd.ms-excel",
|
|
97
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
98
|
+
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
|
99
|
+
// Presentations
|
|
100
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
101
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
102
|
+
odp: "application/vnd.oasis.opendocument.presentation",
|
|
103
|
+
// Data
|
|
104
|
+
csv: "text/csv",
|
|
105
|
+
tsv: "text/tab-separated-values",
|
|
106
|
+
json: "application/json",
|
|
107
|
+
ndjson: "application/x-ndjson",
|
|
108
|
+
jsonl: "application/x-ndjson",
|
|
109
|
+
parquet: "application/vnd.apache.parquet",
|
|
110
|
+
// Text
|
|
111
|
+
txt: "text/plain",
|
|
112
|
+
log: "text/plain",
|
|
113
|
+
md: "text/markdown",
|
|
114
|
+
markdown: "text/markdown",
|
|
115
|
+
rst: "text/x-rst",
|
|
116
|
+
// Code & markup
|
|
117
|
+
html: "text/html",
|
|
118
|
+
htm: "text/html",
|
|
119
|
+
xml: "application/xml",
|
|
120
|
+
svg: "image/svg+xml",
|
|
121
|
+
css: "text/css",
|
|
122
|
+
js: "text/javascript",
|
|
123
|
+
mjs: "text/javascript",
|
|
124
|
+
cjs: "text/javascript",
|
|
125
|
+
jsx: "text/javascript",
|
|
126
|
+
ts: "text/x-typescript",
|
|
127
|
+
tsx: "text/x-typescript",
|
|
128
|
+
py: "text/x-python",
|
|
129
|
+
rb: "text/x-ruby",
|
|
130
|
+
go: "text/x-go",
|
|
131
|
+
rs: "text/x-rust",
|
|
132
|
+
java: "text/x-java",
|
|
133
|
+
kt: "text/x-kotlin",
|
|
134
|
+
swift: "text/x-swift",
|
|
135
|
+
c: "text/x-c",
|
|
136
|
+
h: "text/x-c",
|
|
137
|
+
cpp: "text/x-c++src",
|
|
138
|
+
cs: "text/x-csharp",
|
|
139
|
+
php: "text/x-php",
|
|
140
|
+
sh: "application/x-sh",
|
|
141
|
+
bash: "application/x-sh",
|
|
142
|
+
zsh: "application/x-sh",
|
|
143
|
+
sql: "application/sql",
|
|
144
|
+
yaml: "application/yaml",
|
|
145
|
+
yml: "application/yaml",
|
|
146
|
+
toml: "application/toml",
|
|
147
|
+
ini: "text/plain",
|
|
148
|
+
// Archives
|
|
149
|
+
zip: "application/zip",
|
|
150
|
+
tar: "application/x-tar",
|
|
151
|
+
gz: "application/gzip",
|
|
152
|
+
tgz: "application/gzip",
|
|
153
|
+
bz2: "application/x-bzip2",
|
|
154
|
+
"7z": "application/x-7z-compressed",
|
|
155
|
+
rar: "application/vnd.rar",
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** Extensions whose category cannot be read off the MIME type. */
|
|
159
|
+
const EXTENSION_CATEGORIES: Readonly<Record<string, FileCategory>> = {
|
|
160
|
+
// Code & markup — most carry a `text/*` MIME, which alone would say "text".
|
|
161
|
+
html: "code",
|
|
162
|
+
htm: "code",
|
|
163
|
+
xml: "code",
|
|
164
|
+
css: "code",
|
|
165
|
+
js: "code",
|
|
166
|
+
mjs: "code",
|
|
167
|
+
cjs: "code",
|
|
168
|
+
jsx: "code",
|
|
169
|
+
ts: "code",
|
|
170
|
+
tsx: "code",
|
|
171
|
+
py: "code",
|
|
172
|
+
rb: "code",
|
|
173
|
+
go: "code",
|
|
174
|
+
rs: "code",
|
|
175
|
+
java: "code",
|
|
176
|
+
kt: "code",
|
|
177
|
+
swift: "code",
|
|
178
|
+
c: "code",
|
|
179
|
+
h: "code",
|
|
180
|
+
cpp: "code",
|
|
181
|
+
cs: "code",
|
|
182
|
+
php: "code",
|
|
183
|
+
sh: "code",
|
|
184
|
+
bash: "code",
|
|
185
|
+
zsh: "code",
|
|
186
|
+
sql: "code",
|
|
187
|
+
yaml: "code",
|
|
188
|
+
yml: "code",
|
|
189
|
+
toml: "code",
|
|
190
|
+
ini: "code",
|
|
191
|
+
// Data — `text/csv` would otherwise land in "text".
|
|
192
|
+
csv: "data",
|
|
193
|
+
tsv: "data",
|
|
194
|
+
json: "data",
|
|
195
|
+
ndjson: "data",
|
|
196
|
+
jsonl: "data",
|
|
197
|
+
parquet: "data",
|
|
198
|
+
// Prose
|
|
199
|
+
txt: "text",
|
|
200
|
+
log: "text",
|
|
201
|
+
md: "text",
|
|
202
|
+
markdown: "text",
|
|
203
|
+
rst: "text",
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/** MIME (exact match) → category, for types no prefix rule covers. */
|
|
207
|
+
const MEDIA_TYPE_CATEGORIES: Readonly<Record<string, FileCategory>> = {
|
|
208
|
+
"application/pdf": "document",
|
|
209
|
+
"application/msword": "document",
|
|
210
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "document",
|
|
211
|
+
"application/vnd.oasis.opendocument.text": "document",
|
|
212
|
+
"application/rtf": "document",
|
|
213
|
+
"application/epub+zip": "document",
|
|
214
|
+
"application/vnd.ms-excel": "spreadsheet",
|
|
215
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "spreadsheet",
|
|
216
|
+
"application/vnd.oasis.opendocument.spreadsheet": "spreadsheet",
|
|
217
|
+
"application/vnd.ms-powerpoint": "presentation",
|
|
218
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "presentation",
|
|
219
|
+
"application/vnd.oasis.opendocument.presentation": "presentation",
|
|
220
|
+
"application/json": "data",
|
|
221
|
+
"application/x-ndjson": "data",
|
|
222
|
+
"application/vnd.apache.parquet": "data",
|
|
223
|
+
"application/xml": "code",
|
|
224
|
+
"application/sql": "code",
|
|
225
|
+
"application/yaml": "code",
|
|
226
|
+
"application/toml": "code",
|
|
227
|
+
"application/x-sh": "code",
|
|
228
|
+
"application/zip": "archive",
|
|
229
|
+
"application/x-tar": "archive",
|
|
230
|
+
"application/gzip": "archive",
|
|
231
|
+
"application/x-bzip2": "archive",
|
|
232
|
+
"application/x-7z-compressed": "archive",
|
|
233
|
+
"application/vnd.rar": "archive",
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** The MIME we treat as "the caller told us nothing". */
|
|
237
|
+
const UNKNOWN_MEDIA_TYPE = "application/octet-stream";
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Lowercased extension without the dot. `""` when the name has none, is a
|
|
241
|
+
* dotfile (`.gitignore` — the whole name is the "extension" to a human, but it
|
|
242
|
+
* is not a format), or ends in a dot.
|
|
243
|
+
*/
|
|
244
|
+
export function extensionOf(name: string): string {
|
|
245
|
+
// Strip any path and any query/hash a URL-derived name may carry.
|
|
246
|
+
const base = (name.split(/[\\/]/).pop() ?? "").split(/[?#]/)[0] ?? "";
|
|
247
|
+
const dot = base.lastIndexOf(".");
|
|
248
|
+
if (dot <= 0 || dot === base.length - 1) return "";
|
|
249
|
+
return base.slice(dot + 1).toLowerCase();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Best-known MIME for a filename, from its extension alone.
|
|
254
|
+
* `undefined` when the extension is unknown — callers decide the fallback.
|
|
255
|
+
*/
|
|
256
|
+
export function mediaTypeFromName(name: string): string | undefined {
|
|
257
|
+
return EXTENSION_MEDIA_TYPES[extensionOf(name)];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Strip parameters and casing: `"TEXT/CSV; charset=utf-8"` → `"text/csv"`. */
|
|
261
|
+
function normalizeMediaType(mediaType: string | undefined): string {
|
|
262
|
+
const bare = (mediaType ?? "").split(";")[0]?.trim().toLowerCase() ?? "";
|
|
263
|
+
// Browsers and servers both use these to mean "I don't know".
|
|
264
|
+
if (bare === "" || bare === UNKNOWN_MEDIA_TYPE || bare === "binary/octet-stream") return "";
|
|
265
|
+
return bare;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function categoryFor(mediaType: string, extension: string): FileCategory {
|
|
269
|
+
// The extension is more specific than a `text/*` MIME, so it wins for the
|
|
270
|
+
// formats that would otherwise collapse into "text" (see EXTENSION_CATEGORIES).
|
|
271
|
+
const byExtension = EXTENSION_CATEGORIES[extension];
|
|
272
|
+
if (byExtension) return byExtension;
|
|
273
|
+
|
|
274
|
+
const exact = MEDIA_TYPE_CATEGORIES[mediaType];
|
|
275
|
+
if (exact) return exact;
|
|
276
|
+
|
|
277
|
+
if (mediaType.startsWith("image/")) return "image";
|
|
278
|
+
if (mediaType.startsWith("video/")) return "video";
|
|
279
|
+
if (mediaType.startsWith("audio/")) return "audio";
|
|
280
|
+
if (mediaType.startsWith("text/")) return "text";
|
|
281
|
+
|
|
282
|
+
return "unknown";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Resolve what a file is from its name and (optionally) the MIME the source
|
|
287
|
+
* reported. Never throws; unknown input resolves to
|
|
288
|
+
* `{ category: "unknown", mediaType: "application/octet-stream", extension: "" }`.
|
|
289
|
+
*
|
|
290
|
+
* A caller-supplied `mediaType` wins over the extension EXCEPT where the
|
|
291
|
+
* extension is strictly more specific — `report.csv` served as `text/plain`
|
|
292
|
+
* still resolves to `"data"`, because servers routinely under-specify.
|
|
293
|
+
*/
|
|
294
|
+
export function resolveFileKind(name: string, mediaType?: string): FileKind {
|
|
295
|
+
const extension = extensionOf(name);
|
|
296
|
+
const declared = normalizeMediaType(mediaType);
|
|
297
|
+
const resolved = declared || mediaTypeFromName(name) || UNKNOWN_MEDIA_TYPE;
|
|
298
|
+
return {
|
|
299
|
+
category: categoryFor(resolved, extension),
|
|
300
|
+
mediaType: resolved,
|
|
301
|
+
extension,
|
|
302
|
+
};
|
|
303
|
+
}
|