@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,121 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { Globe, Home, Settings, Sparkles } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
SidebarGroup,
|
|
5
|
+
SidebarGroupContent,
|
|
6
|
+
SidebarInset,
|
|
7
|
+
SidebarMenu,
|
|
8
|
+
SidebarMenuButton,
|
|
9
|
+
SidebarMenuItem,
|
|
10
|
+
SidebarProvider,
|
|
11
|
+
} from "../sidebar";
|
|
12
|
+
import { NavUser } from "../nav-user";
|
|
13
|
+
import { TeamSwitcher } from "../team-switcher";
|
|
14
|
+
import { AppSidebar } from "./app-sidebar";
|
|
15
|
+
|
|
16
|
+
const meta = {
|
|
17
|
+
title: "Layout/AppSidebar",
|
|
18
|
+
component: AppSidebar,
|
|
19
|
+
tags: ["autodocs"],
|
|
20
|
+
parameters: { layout: "fullscreen" },
|
|
21
|
+
decorators: [
|
|
22
|
+
(Story) => (
|
|
23
|
+
<SidebarProvider>
|
|
24
|
+
<div className="flex h-dvh w-full">
|
|
25
|
+
<Story />
|
|
26
|
+
<SidebarInset className="flex flex-col p-6 text-body text-muted-foreground">
|
|
27
|
+
Main content area
|
|
28
|
+
</SidebarInset>
|
|
29
|
+
</div>
|
|
30
|
+
</SidebarProvider>
|
|
31
|
+
),
|
|
32
|
+
],
|
|
33
|
+
} satisfies Meta<typeof AppSidebar>;
|
|
34
|
+
export default meta;
|
|
35
|
+
type Story = StoryObj<typeof meta>;
|
|
36
|
+
|
|
37
|
+
const sampleTeams = [
|
|
38
|
+
{ name: "Acme Inc.", logo: Sparkles, plan: "Enterprise" },
|
|
39
|
+
{ name: "Beta Corp.", logo: Globe, plan: "Free" },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const sampleUser = { name: "Jane Doe", email: "jane@example.com" };
|
|
43
|
+
|
|
44
|
+
const navItems = [
|
|
45
|
+
{ title: "Home", icon: Home, url: "#" },
|
|
46
|
+
{ title: "Settings", icon: Settings, url: "#" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export const Default: Story = {
|
|
50
|
+
render: () => (
|
|
51
|
+
<AppSidebar
|
|
52
|
+
header={<TeamSwitcher teams={sampleTeams} />}
|
|
53
|
+
footer={<NavUser user={sampleUser} />}
|
|
54
|
+
>
|
|
55
|
+
<SidebarGroup>
|
|
56
|
+
<SidebarGroupContent>
|
|
57
|
+
<SidebarMenu>
|
|
58
|
+
{navItems.map((item) => (
|
|
59
|
+
<SidebarMenuItem key={item.title}>
|
|
60
|
+
<SidebarMenuButton asChild>
|
|
61
|
+
<a href={item.url}>
|
|
62
|
+
<item.icon />
|
|
63
|
+
<span>{item.title}</span>
|
|
64
|
+
</a>
|
|
65
|
+
</SidebarMenuButton>
|
|
66
|
+
</SidebarMenuItem>
|
|
67
|
+
))}
|
|
68
|
+
</SidebarMenu>
|
|
69
|
+
</SidebarGroupContent>
|
|
70
|
+
</SidebarGroup>
|
|
71
|
+
</AppSidebar>
|
|
72
|
+
),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const HeaderOnly: Story = {
|
|
76
|
+
name: "Header only (no footer)",
|
|
77
|
+
render: () => (
|
|
78
|
+
<AppSidebar header={<TeamSwitcher teams={sampleTeams} />}>
|
|
79
|
+
<SidebarGroup>
|
|
80
|
+
<SidebarGroupContent>
|
|
81
|
+
<SidebarMenu>
|
|
82
|
+
{navItems.map((item) => (
|
|
83
|
+
<SidebarMenuItem key={item.title}>
|
|
84
|
+
<SidebarMenuButton asChild>
|
|
85
|
+
<a href={item.url}>
|
|
86
|
+
<item.icon />
|
|
87
|
+
<span>{item.title}</span>
|
|
88
|
+
</a>
|
|
89
|
+
</SidebarMenuButton>
|
|
90
|
+
</SidebarMenuItem>
|
|
91
|
+
))}
|
|
92
|
+
</SidebarMenu>
|
|
93
|
+
</SidebarGroupContent>
|
|
94
|
+
</SidebarGroup>
|
|
95
|
+
</AppSidebar>
|
|
96
|
+
),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const FooterOnly: Story = {
|
|
100
|
+
name: "Footer only (no header)",
|
|
101
|
+
render: () => (
|
|
102
|
+
<AppSidebar footer={<NavUser user={sampleUser} />}>
|
|
103
|
+
<SidebarGroup>
|
|
104
|
+
<SidebarGroupContent>
|
|
105
|
+
<SidebarMenu>
|
|
106
|
+
{navItems.map((item) => (
|
|
107
|
+
<SidebarMenuItem key={item.title}>
|
|
108
|
+
<SidebarMenuButton asChild>
|
|
109
|
+
<a href={item.url}>
|
|
110
|
+
<item.icon />
|
|
111
|
+
<span>{item.title}</span>
|
|
112
|
+
</a>
|
|
113
|
+
</SidebarMenuButton>
|
|
114
|
+
</SidebarMenuItem>
|
|
115
|
+
))}
|
|
116
|
+
</SidebarMenu>
|
|
117
|
+
</SidebarGroupContent>
|
|
118
|
+
</SidebarGroup>
|
|
119
|
+
</AppSidebar>
|
|
120
|
+
),
|
|
121
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { SidebarProvider } from "../sidebar";
|
|
4
|
+
import { AppSidebar } from "./app-sidebar";
|
|
5
|
+
|
|
6
|
+
function Wrapper({ children }: { children: React.ReactNode }) {
|
|
7
|
+
return <SidebarProvider>{children}</SidebarProvider>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("AppSidebar", () => {
|
|
11
|
+
it("renders children in the content slot", () => {
|
|
12
|
+
render(
|
|
13
|
+
<Wrapper>
|
|
14
|
+
<AppSidebar>
|
|
15
|
+
<div>nav content</div>
|
|
16
|
+
</AppSidebar>
|
|
17
|
+
</Wrapper>,
|
|
18
|
+
);
|
|
19
|
+
expect(screen.getByText("nav content")).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("renders header slot when provided", () => {
|
|
23
|
+
render(
|
|
24
|
+
<Wrapper>
|
|
25
|
+
<AppSidebar header={<div>sidebar header</div>}>
|
|
26
|
+
<div>nav</div>
|
|
27
|
+
</AppSidebar>
|
|
28
|
+
</Wrapper>,
|
|
29
|
+
);
|
|
30
|
+
expect(screen.getByText("sidebar header")).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("renders footer slot when provided", () => {
|
|
34
|
+
render(
|
|
35
|
+
<Wrapper>
|
|
36
|
+
<AppSidebar footer={<div>sidebar footer</div>}>
|
|
37
|
+
<div>nav</div>
|
|
38
|
+
</AppSidebar>
|
|
39
|
+
</Wrapper>,
|
|
40
|
+
);
|
|
41
|
+
expect(screen.getByText("sidebar footer")).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("omits header DOM node when header prop is not provided", () => {
|
|
45
|
+
render(
|
|
46
|
+
<Wrapper>
|
|
47
|
+
<AppSidebar footer={<div>footer</div>}>
|
|
48
|
+
<div>nav</div>
|
|
49
|
+
</AppSidebar>
|
|
50
|
+
</Wrapper>,
|
|
51
|
+
);
|
|
52
|
+
expect(screen.getByText("footer")).toBeInTheDocument();
|
|
53
|
+
expect(screen.getByText("nav")).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, type ComponentProps, type ReactNode } from "react";
|
|
4
|
+
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader } from "../sidebar";
|
|
5
|
+
|
|
6
|
+
export interface AppSidebarProps extends ComponentProps<typeof Sidebar> {
|
|
7
|
+
/**
|
|
8
|
+
* Content rendered inside `<SidebarHeader>`. Typically a `TeamSwitcher`
|
|
9
|
+
* or a logo + title combination.
|
|
10
|
+
*/
|
|
11
|
+
header?: ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Content rendered inside `<SidebarFooter>`. Typically a `NavUser` or
|
|
14
|
+
* a `TeamSwitcher` placed at the bottom.
|
|
15
|
+
*/
|
|
16
|
+
footer?: ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* Main navigation content rendered inside `<SidebarContent>`. Pass one or
|
|
19
|
+
* more `<SidebarGroup>` / `<SidebarMenu>` compositions here.
|
|
20
|
+
*/
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parameterized application sidebar shell. Composes `@radix-ui` `Sidebar`
|
|
26
|
+
* primitives with typed `header`, `footer`, and `children` slots.
|
|
27
|
+
*
|
|
28
|
+
* Registry blocks (`sidebar-02`, `sidebar-04`, `sidebar-05`) should import
|
|
29
|
+
* this instead of constructing the `Sidebar` → `SidebarHeader` →
|
|
30
|
+
* `SidebarContent` → `SidebarFooter` skeleton themselves.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* <AppSidebar
|
|
35
|
+
* header={<TeamSwitcher teams={teams} />}
|
|
36
|
+
* footer={<NavUser user={currentUser} />}
|
|
37
|
+
* >
|
|
38
|
+
* <SidebarGroup>…</SidebarGroup>
|
|
39
|
+
* </AppSidebar>
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export const AppSidebar = forwardRef<HTMLDivElement, AppSidebarProps>(function AppSidebar(
|
|
43
|
+
{ header, footer, children, ...props },
|
|
44
|
+
ref,
|
|
45
|
+
) {
|
|
46
|
+
return (
|
|
47
|
+
<Sidebar ref={ref} {...props}>
|
|
48
|
+
{header != null && <SidebarHeader>{header}</SidebarHeader>}
|
|
49
|
+
<SidebarContent>{children}</SidebarContent>
|
|
50
|
+
{footer != null && <SidebarFooter>{footer}</SidebarFooter>}
|
|
51
|
+
</Sidebar>
|
|
52
|
+
);
|
|
53
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AppSidebar, type AppSidebarProps } from "./app-sidebar";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { AspectRatio } from "./aspect-ratio";
|
|
3
|
+
const meta = {
|
|
4
|
+
title: "Display/AspectRatio",
|
|
5
|
+
component: AspectRatio,
|
|
6
|
+
tags: ["autodocs"],
|
|
7
|
+
argTypes: {
|
|
8
|
+
ratio: {
|
|
9
|
+
description: "Aspect ratio as a number (e.g. `16/9`, `4/3`, `1`). Defaults to `1`.",
|
|
10
|
+
control: "number",
|
|
11
|
+
table: { category: "Appearance" },
|
|
12
|
+
},
|
|
13
|
+
className: {
|
|
14
|
+
description: "Extra Tailwind classes merged on the wrapper.",
|
|
15
|
+
control: "text",
|
|
16
|
+
table: { category: "Appearance" },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
} satisfies Meta<typeof AspectRatio>;
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof meta>;
|
|
22
|
+
export const Default: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<div className="w-72">
|
|
25
|
+
<AspectRatio
|
|
26
|
+
ratio={16 / 9}
|
|
27
|
+
className="flex items-center justify-center rounded-md bg-muted text-body text-muted-foreground"
|
|
28
|
+
>
|
|
29
|
+
16 / 9
|
|
30
|
+
</AspectRatio>
|
|
31
|
+
</div>
|
|
32
|
+
),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const Square: Story = {
|
|
36
|
+
render: () => (
|
|
37
|
+
<div className="w-48">
|
|
38
|
+
<AspectRatio
|
|
39
|
+
ratio={1}
|
|
40
|
+
className="flex items-center justify-center rounded-md bg-muted text-body text-muted-foreground"
|
|
41
|
+
>
|
|
42
|
+
1 / 1
|
|
43
|
+
</AspectRatio>
|
|
44
|
+
</div>
|
|
45
|
+
),
|
|
46
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AspectRatio } from "./aspect-ratio";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { AttributionPanel } from "./attribution-panel";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Core/AttributionPanel",
|
|
6
|
+
component: AttributionPanel,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
"The one surface where brand-ui's third-party notices are shown. Its content is " +
|
|
13
|
+
"GENERATED from the repo (`pnpm gen:attributions`, gated by `pnpm attributions:check`), " +
|
|
14
|
+
"so it cannot drift from what actually ships.\n\n" +
|
|
15
|
+
"**Why it matters:** `MapCanvas` disables MapLibre's in-map attribution control by " +
|
|
16
|
+
"default and the flow canvases hide the React Flow badge. The React Flow badge is an " +
|
|
17
|
+
"MIT project's request, but the default Carto basemap's OpenStreetMap data is " +
|
|
18
|
+
"ODbL-licensed and the credit is REQUIRED — mount this panel somewhere reachable " +
|
|
19
|
+
"(About dialog, settings pane, footer link) to keep that obligation met.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
argTypes: {
|
|
24
|
+
categories: {
|
|
25
|
+
description: "Restrict to these categories, in the panel's own order.",
|
|
26
|
+
table: { category: "Content" },
|
|
27
|
+
},
|
|
28
|
+
packages: {
|
|
29
|
+
description:
|
|
30
|
+
"Restrict to notices reachable through these packages — an app that never renders a map need not credit the basemap providers.",
|
|
31
|
+
table: { category: "Content" },
|
|
32
|
+
},
|
|
33
|
+
requiredOnly: {
|
|
34
|
+
description: "Show only notices a licence obliges the product to display.",
|
|
35
|
+
control: "boolean",
|
|
36
|
+
table: { category: "Content" },
|
|
37
|
+
},
|
|
38
|
+
maxHeight: {
|
|
39
|
+
description: "Cap the body height and scroll inside it.",
|
|
40
|
+
table: { category: "Layout" },
|
|
41
|
+
},
|
|
42
|
+
className: {
|
|
43
|
+
description: "Extra Tailwind classes merged via cn().",
|
|
44
|
+
control: "text",
|
|
45
|
+
table: { category: "Appearance" },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
} satisfies Meta<typeof AttributionPanel>;
|
|
49
|
+
|
|
50
|
+
export default meta;
|
|
51
|
+
type Story = StoryObj<typeof meta>;
|
|
52
|
+
|
|
53
|
+
/** The full generated dataset, scrolling inside a fixed height. */
|
|
54
|
+
export const Default: Story = {
|
|
55
|
+
args: { maxHeight: 420, className: "max-w-2xl" },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The minimum a product must display: only the notices a licence obliges. This is
|
|
60
|
+
* the variant to mount when screen space is tight — a footer "Attributions" link
|
|
61
|
+
* or an About dialog section.
|
|
62
|
+
*/
|
|
63
|
+
export const RequiredOnly: Story = {
|
|
64
|
+
args: { requiredOnly: true, className: "max-w-2xl" },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Scoped to one package — what an app that only renders maps has to credit. */
|
|
68
|
+
export const ScopedToMaps: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
packages: ["@elabs-ai/components-maps"],
|
|
71
|
+
className: "max-w-2xl",
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Adapted source and fonts only — the provenance half, without the dependency tail. */
|
|
76
|
+
export const SourceAndFonts: Story = {
|
|
77
|
+
args: { categories: ["source", "font"], className: "max-w-2xl" },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** A filter that matches nothing still renders a real empty state, never a blank box. */
|
|
81
|
+
export const Empty: Story = {
|
|
82
|
+
args: { attributions: [], className: "max-w-2xl" },
|
|
83
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { cleanup, render, screen, within } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { AttributionPanel } from "./attribution-panel";
|
|
4
|
+
import type { Attribution } from "./attribution-types";
|
|
5
|
+
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
|
|
8
|
+
const fixture: Attribution[] = [
|
|
9
|
+
{
|
|
10
|
+
id: "openstreetmap",
|
|
11
|
+
category: "data",
|
|
12
|
+
name: "OpenStreetMap",
|
|
13
|
+
version: null,
|
|
14
|
+
license: "ODbL-1.0",
|
|
15
|
+
copyright: "© OpenStreetMap contributors",
|
|
16
|
+
url: "https://www.openstreetmap.org/copyright",
|
|
17
|
+
usedBy: ["@elabs-ai/components-maps"],
|
|
18
|
+
required: true,
|
|
19
|
+
note: "Map data behind the default basemap.",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "clsx",
|
|
23
|
+
category: "dependency",
|
|
24
|
+
name: "clsx",
|
|
25
|
+
version: "2.1.1",
|
|
26
|
+
license: "MIT",
|
|
27
|
+
copyright: "Luke Edwards",
|
|
28
|
+
url: "https://github.com/lukeed/clsx",
|
|
29
|
+
usedBy: ["@elabs-ai/components-ui"],
|
|
30
|
+
required: false,
|
|
31
|
+
note: null,
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
describe("AttributionPanel", () => {
|
|
36
|
+
it("renders each attribution with its licence and copyright", () => {
|
|
37
|
+
render(<AttributionPanel attributions={fixture} />);
|
|
38
|
+
expect(screen.getByText("OpenStreetMap")).toBeInTheDocument();
|
|
39
|
+
expect(screen.getByText("© OpenStreetMap contributors")).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByText("ODbL-1.0")).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByText("clsx")).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// The obligation this component exists to meet: a required notice must be
|
|
45
|
+
// distinguishable by TEXT, not by colour alone (WCAG 1.4.1) — asserting the
|
|
46
|
+
// word means a future restyle can't reduce it to a coloured dot.
|
|
47
|
+
it("marks a licence-required notice with a word, not only a colour", () => {
|
|
48
|
+
render(<AttributionPanel attributions={fixture} />);
|
|
49
|
+
const osm = screen.getByText("OpenStreetMap").closest("li")!;
|
|
50
|
+
expect(within(osm).getByText("Required")).toBeInTheDocument();
|
|
51
|
+
const clsx = screen.getByText("clsx").closest("li")!;
|
|
52
|
+
expect(within(clsx).queryByText("Required")).not.toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("groups by category under a heading", () => {
|
|
56
|
+
render(<AttributionPanel attributions={fixture} />);
|
|
57
|
+
expect(screen.getByRole("heading", { name: "Map data & tiles" })).toBeInTheDocument();
|
|
58
|
+
expect(screen.getByRole("heading", { name: "Open-source dependencies" })).toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("filters to the licence-required notices with requiredOnly", () => {
|
|
62
|
+
render(<AttributionPanel attributions={fixture} requiredOnly />);
|
|
63
|
+
expect(screen.getByText("OpenStreetMap")).toBeInTheDocument();
|
|
64
|
+
expect(screen.queryByText("clsx")).not.toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("filters by consuming package so an app credits only what it ships", () => {
|
|
68
|
+
render(<AttributionPanel attributions={fixture} packages={["@elabs-ai/components-ui"]} />);
|
|
69
|
+
expect(screen.getByText("clsx")).toBeInTheDocument();
|
|
70
|
+
expect(screen.queryByText("OpenStreetMap")).not.toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Two panels share a page in Storybook autodocs (every story renders together)
|
|
74
|
+
// and in any app that mounts a scoped panel beside a full one. Heading ids must
|
|
75
|
+
// stay unique or aria-labelledby points at the wrong node and axe flags it.
|
|
76
|
+
it("scopes section heading ids per instance", () => {
|
|
77
|
+
const { container } = render(
|
|
78
|
+
<>
|
|
79
|
+
<AttributionPanel attributions={fixture} />
|
|
80
|
+
<AttributionPanel attributions={fixture} />
|
|
81
|
+
</>,
|
|
82
|
+
);
|
|
83
|
+
const ids = [...container.querySelectorAll("h3[id]")].map((h) => h.id);
|
|
84
|
+
expect(ids).toHaveLength(4);
|
|
85
|
+
expect(new Set(ids).size).toBe(4);
|
|
86
|
+
for (const section of container.querySelectorAll('[data-slot="attribution-panel-section"]')) {
|
|
87
|
+
const labelledBy = section.getAttribute("aria-labelledby")!;
|
|
88
|
+
expect(section.querySelector(`#${CSS.escape(labelledBy)}`)).not.toBeNull();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("renders an empty state rather than a blank panel", () => {
|
|
93
|
+
render(<AttributionPanel attributions={[]} />);
|
|
94
|
+
expect(screen.getByText("No attributions match this filter.")).toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("opens external links safely", () => {
|
|
98
|
+
render(<AttributionPanel attributions={fixture} />);
|
|
99
|
+
const link = screen.getByRole("link", { name: "OpenStreetMap" });
|
|
100
|
+
expect(link).toHaveAttribute("target", "_blank");
|
|
101
|
+
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("merges className and spreads props onto the root", () => {
|
|
105
|
+
const { container } = render(
|
|
106
|
+
<AttributionPanel attributions={fixture} className="my-panel" id="attrib" />,
|
|
107
|
+
);
|
|
108
|
+
const root = container.querySelector('[data-slot="attribution-panel"]')!;
|
|
109
|
+
expect(root).toHaveClass("my-panel");
|
|
110
|
+
expect(root).toHaveAttribute("id", "attrib");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// The real dataset is generated; if the generator ever emits nothing, the panel
|
|
114
|
+
// would render an innocuous empty state and the obligation would silently lapse.
|
|
115
|
+
it("ships a non-empty generated dataset including the required map notices", async () => {
|
|
116
|
+
const { ATTRIBUTIONS } = await import("./attributions.generated");
|
|
117
|
+
expect(ATTRIBUTIONS.length).toBeGreaterThan(0);
|
|
118
|
+
const required = ATTRIBUTIONS.filter((a) => a.required);
|
|
119
|
+
expect(required.some((a) => a.id === "openstreetmap")).toBe(true);
|
|
120
|
+
expect(required.some((a) => a.id === "carto")).toBe(true);
|
|
121
|
+
for (const entry of required) expect(entry.copyright).toBeTruthy();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { forwardRef, useId, useMemo, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
import { Badge } from "../badge";
|
|
4
|
+
import { ScrollArea } from "../scroll-area";
|
|
5
|
+
import { useLocale } from "../locale-provider";
|
|
6
|
+
import { ATTRIBUTIONS } from "./attributions.generated";
|
|
7
|
+
import type { Attribution, AttributionCategory } from "./attribution-types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Render order + the heading each group gets. `data` first because those are the
|
|
11
|
+
* notices a licence obliges us to show; `dependency` last because it is the long
|
|
12
|
+
* tail nobody scrolls to but everybody must be able to reach.
|
|
13
|
+
*/
|
|
14
|
+
const CATEGORY_ORDER: readonly AttributionCategory[] = ["data", "source", "font", "dependency"];
|
|
15
|
+
|
|
16
|
+
const CATEGORY_LABEL: Record<AttributionCategory, string> = {
|
|
17
|
+
data: "Map data & tiles",
|
|
18
|
+
source: "Adapted source",
|
|
19
|
+
font: "Fonts",
|
|
20
|
+
dependency: "Open-source dependencies",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface AttributionPanelProps extends HTMLAttributes<HTMLElement> {
|
|
24
|
+
/**
|
|
25
|
+
* The notices to render. Defaults to the GENERATED dataset
|
|
26
|
+
* (`scripts/gen-attributions.mjs`), which is what keeps this panel accurate
|
|
27
|
+
* without anyone maintaining a list — override only to scope or to test.
|
|
28
|
+
*/
|
|
29
|
+
attributions?: readonly Attribution[];
|
|
30
|
+
/** Restrict to these categories, in the panel's own order. Default: all. */
|
|
31
|
+
categories?: readonly AttributionCategory[];
|
|
32
|
+
/**
|
|
33
|
+
* Restrict to notices reachable through these packages — e.g. an app that only
|
|
34
|
+
* uses `…-ui` need not credit MapLibre's basemap providers. Matched against
|
|
35
|
+
* each entry's `usedBy`. Default: all.
|
|
36
|
+
*/
|
|
37
|
+
packages?: readonly string[];
|
|
38
|
+
/** Show only the notices a licence obliges us to display. */
|
|
39
|
+
requiredOnly?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Cap the body height and scroll inside it. Omit to let the panel grow and
|
|
42
|
+
* leave scrolling to the container (a Dialog body, a settings pane).
|
|
43
|
+
*/
|
|
44
|
+
maxHeight?: number | string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Filter + group in one pass so the render is a plain map over sections. */
|
|
48
|
+
function groupAttributions(
|
|
49
|
+
attributions: readonly Attribution[],
|
|
50
|
+
{
|
|
51
|
+
categories,
|
|
52
|
+
packages,
|
|
53
|
+
requiredOnly,
|
|
54
|
+
}: Pick<AttributionPanelProps, "categories" | "packages" | "requiredOnly">,
|
|
55
|
+
) {
|
|
56
|
+
const order = categories ?? CATEGORY_ORDER;
|
|
57
|
+
const visible = attributions.filter(
|
|
58
|
+
(a) =>
|
|
59
|
+
order.includes(a.category) &&
|
|
60
|
+
(!requiredOnly || a.required) &&
|
|
61
|
+
(!packages || a.usedBy.some((p) => packages.includes(p))),
|
|
62
|
+
);
|
|
63
|
+
return order
|
|
64
|
+
.map((category) => ({ category, items: visible.filter((a) => a.category === category) }))
|
|
65
|
+
.filter((group) => group.items.length > 0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function AttributionItem({ item, requiredLabel }: { item: Attribution; requiredLabel: string }) {
|
|
69
|
+
return (
|
|
70
|
+
<li data-slot="attribution-panel-item" className="flex flex-col gap-0.5 py-2">
|
|
71
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
72
|
+
{item.url ? (
|
|
73
|
+
<a
|
|
74
|
+
href={item.url}
|
|
75
|
+
target="_blank"
|
|
76
|
+
rel="noopener noreferrer"
|
|
77
|
+
className="text-body font-medium underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-xs"
|
|
78
|
+
>
|
|
79
|
+
{item.name}
|
|
80
|
+
</a>
|
|
81
|
+
) : (
|
|
82
|
+
<span className="text-body font-medium">{item.name}</span>
|
|
83
|
+
)}
|
|
84
|
+
{item.version ? (
|
|
85
|
+
<span className="text-meta text-muted-foreground tabular-nums">{item.version}</span>
|
|
86
|
+
) : null}
|
|
87
|
+
<span className="text-meta text-muted-foreground">{item.license}</span>
|
|
88
|
+
{item.required ? (
|
|
89
|
+
// Not colour-only: the badge carries the word itself (WCAG 1.4.1).
|
|
90
|
+
<Badge variant="outline" className="text-meta">
|
|
91
|
+
{requiredLabel}
|
|
92
|
+
</Badge>
|
|
93
|
+
) : null}
|
|
94
|
+
</div>
|
|
95
|
+
{item.copyright ? (
|
|
96
|
+
<p className="text-meta text-muted-foreground break-words">{item.copyright}</p>
|
|
97
|
+
) : null}
|
|
98
|
+
{item.note ? <p className="text-meta text-muted-foreground">{item.note}</p> : null}
|
|
99
|
+
</li>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The single surface where brand-ui's third-party notices are displayed —
|
|
105
|
+
* OSS dependencies, adapted source, self-hosted fonts, and the map data/tile
|
|
106
|
+
* credits that a licence actually obliges the product to show.
|
|
107
|
+
*
|
|
108
|
+
* The content is GENERATED from the repo (`pnpm gen:attributions`, gated by
|
|
109
|
+
* `pnpm attributions:check`), so it cannot drift from what is actually shipped
|
|
110
|
+
* the way a hand-kept credits page does.
|
|
111
|
+
*
|
|
112
|
+
* **Why this exists:** `MapCanvas` disables MapLibre's in-map attribution
|
|
113
|
+
* control by default, and the flow canvases hide the React Flow badge. The
|
|
114
|
+
* React Flow badge is an MIT project's request and carries no condition, but the
|
|
115
|
+
* default Carto basemap's OpenStreetMap data is ODbL-licensed and the credit is
|
|
116
|
+
* REQUIRED — mounting this panel somewhere a user can reach (an About dialog, a
|
|
117
|
+
* settings pane, a footer link) is what keeps that obligation met while the map
|
|
118
|
+
* corner stays clean. Ship it, or turn the in-map control back on.
|
|
119
|
+
*/
|
|
120
|
+
export const AttributionPanel = forwardRef<HTMLElement, AttributionPanelProps>(
|
|
121
|
+
function AttributionPanel(
|
|
122
|
+
{
|
|
123
|
+
attributions = ATTRIBUTIONS,
|
|
124
|
+
categories,
|
|
125
|
+
packages,
|
|
126
|
+
requiredOnly,
|
|
127
|
+
maxHeight,
|
|
128
|
+
className,
|
|
129
|
+
...props
|
|
130
|
+
},
|
|
131
|
+
ref,
|
|
132
|
+
) {
|
|
133
|
+
const { t } = useLocale();
|
|
134
|
+
// Instance-scoped so two panels on one page (Storybook autodocs renders every
|
|
135
|
+
// story together) can't emit the same heading id — duplicate ids break the
|
|
136
|
+
// aria-labelledby target and fail axe.
|
|
137
|
+
const baseId = useId();
|
|
138
|
+
const groups = useMemo(
|
|
139
|
+
() => groupAttributions(attributions, { categories, packages, requiredOnly }),
|
|
140
|
+
[attributions, categories, packages, requiredOnly],
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const body = (
|
|
144
|
+
<div className="flex flex-col gap-6">
|
|
145
|
+
{groups.map(({ category, items }) => (
|
|
146
|
+
<section
|
|
147
|
+
key={category}
|
|
148
|
+
data-slot="attribution-panel-section"
|
|
149
|
+
aria-labelledby={`${baseId}-${category}`}
|
|
150
|
+
>
|
|
151
|
+
<h3 id={`${baseId}-${category}`} className="text-subtitle text-foreground mb-1">
|
|
152
|
+
{CATEGORY_LABEL[category]}
|
|
153
|
+
</h3>
|
|
154
|
+
<ul className="divide-y divide-border">
|
|
155
|
+
{items.map((item) => (
|
|
156
|
+
<AttributionItem
|
|
157
|
+
key={item.id}
|
|
158
|
+
item={item}
|
|
159
|
+
requiredLabel={t("ui.attributionPanel.required")}
|
|
160
|
+
/>
|
|
161
|
+
))}
|
|
162
|
+
</ul>
|
|
163
|
+
</section>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<section
|
|
170
|
+
ref={ref}
|
|
171
|
+
data-slot="attribution-panel"
|
|
172
|
+
aria-label={t("ui.attributionPanel.label")}
|
|
173
|
+
className={cn("bg-card border border-border rounded-lg p-4", className)}
|
|
174
|
+
{...props}
|
|
175
|
+
>
|
|
176
|
+
{groups.length === 0 ? (
|
|
177
|
+
<p className="text-body text-muted-foreground">{t("ui.attributionPanel.empty")}</p>
|
|
178
|
+
) : maxHeight === undefined ? (
|
|
179
|
+
body
|
|
180
|
+
) : (
|
|
181
|
+
<ScrollArea style={{ maxHeight }} className="pe-3">
|
|
182
|
+
{body}
|
|
183
|
+
</ScrollArea>
|
|
184
|
+
)}
|
|
185
|
+
</section>
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
export { ATTRIBUTIONS, CATEGORY_LABEL };
|
|
191
|
+
export type { Attribution, AttributionCategory };
|