@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,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
/** How long the transient "copied" state stays true before it resets. */
|
|
6
|
+
export const COPY_FEEDBACK_MS = 1500;
|
|
7
|
+
|
|
8
|
+
export interface UseCopyToClipboardOptions {
|
|
9
|
+
/** ms the `copied` flag stays true. Default: {@link COPY_FEEDBACK_MS}. */
|
|
10
|
+
resetAfterMs?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CopyToClipboard {
|
|
14
|
+
/** True for `resetAfterMs` after a successful copy. */
|
|
15
|
+
copied: boolean;
|
|
16
|
+
/** Writes `text` to the clipboard. Resolves `false` when it could not. */
|
|
17
|
+
copy: (text: string) => Promise<boolean>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Copy-to-clipboard with a transient confirmation flag.
|
|
22
|
+
*
|
|
23
|
+
* One implementation for the whole library — the copy button in
|
|
24
|
+
* `@elabs-ai/components-editor` and `CopyableValue` share it, so the
|
|
25
|
+
* timing, the unmount cleanup and the "no clipboard here" answer are defined
|
|
26
|
+
* once instead of re-derived per component.
|
|
27
|
+
*
|
|
28
|
+
* `navigator.clipboard` is absent under SSR, in jsdom, and in any insecure
|
|
29
|
+
* (non-`https`, non-`localhost`) context. That is reported as `false` rather
|
|
30
|
+
* than thrown: a copy affordance failing is a papercut, not a crash, and the
|
|
31
|
+
* caller decides whether to say anything about it. The deprecated
|
|
32
|
+
* `document.execCommand("copy")` fallback is deliberately not used.
|
|
33
|
+
*/
|
|
34
|
+
export function useCopyToClipboard(options?: UseCopyToClipboardOptions): CopyToClipboard {
|
|
35
|
+
const resetAfterMs = options?.resetAfterMs ?? COPY_FEEDBACK_MS;
|
|
36
|
+
const [copied, setCopied] = useState(false);
|
|
37
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
38
|
+
|
|
39
|
+
// A component that unmounts inside the feedback window must not have its
|
|
40
|
+
// state set afterwards, and a second copy must restart the window, not race
|
|
41
|
+
// the first one's reset.
|
|
42
|
+
useEffect(
|
|
43
|
+
() => () => {
|
|
44
|
+
if (timerRef.current !== null) {
|
|
45
|
+
clearTimeout(timerRef.current);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
[],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const copy = useCallback(
|
|
52
|
+
async (text: string): Promise<boolean> => {
|
|
53
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
await navigator.clipboard.writeText(text);
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
setCopied(true);
|
|
62
|
+
if (timerRef.current !== null) {
|
|
63
|
+
clearTimeout(timerRef.current);
|
|
64
|
+
}
|
|
65
|
+
timerRef.current = setTimeout(() => {
|
|
66
|
+
timerRef.current = null;
|
|
67
|
+
setCopied(false);
|
|
68
|
+
}, resetAfterMs);
|
|
69
|
+
return true;
|
|
70
|
+
},
|
|
71
|
+
[resetAfterMs],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return { copied, copy };
|
|
75
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768;
|
|
4
|
+
|
|
5
|
+
/** Returns true when the viewport is below the mobile breakpoint (768px). */
|
|
6
|
+
export function useIsMobile(): boolean {
|
|
7
|
+
const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
10
|
+
const onChange = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
11
|
+
mql.addEventListener("change", onChange);
|
|
12
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
13
|
+
return () => mql.removeEventListener("change", onChange);
|
|
14
|
+
}, []);
|
|
15
|
+
return !!isMobile;
|
|
16
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect } from "storybook/test";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { Button } from "./components/button";
|
|
5
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./components/card";
|
|
6
|
+
import { RevealGroup } from "./components/reveal";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Foundation/Motion — demonstrates and VERIFIES the token-driven motion gate.
|
|
10
|
+
*
|
|
11
|
+
* The gate is a single CSS multiplier (`--motion-factor`) that scales every
|
|
12
|
+
* duration token (`--t-fast`/`--t-base`/...). It is set by, in precedence order,
|
|
13
|
+
* the user's `data-motion-pref` (toolbar "Motion" global), the OS
|
|
14
|
+
* `prefers-reduced-motion` setting, and the per-theme default. Flip the **Motion**
|
|
15
|
+
* toolbar control (System / Reduce / Full) and the **theme** control to sweep all
|
|
16
|
+
* combinations. The play functions below assert the gate mechanically.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Live readout of the resolved gate values (updates when data-motion-pref changes). */
|
|
20
|
+
function GateReadout() {
|
|
21
|
+
const probeRef = useRef<HTMLSpanElement>(null);
|
|
22
|
+
const [factor, setFactor] = useState("—");
|
|
23
|
+
const [effective, setEffective] = useState("—");
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const read = () => {
|
|
27
|
+
const root = document.documentElement;
|
|
28
|
+
setFactor(getComputedStyle(root).getPropertyValue("--motion-factor").trim() || "1");
|
|
29
|
+
if (probeRef.current) {
|
|
30
|
+
setEffective(getComputedStyle(probeRef.current).transitionDuration);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
read();
|
|
34
|
+
const observer = new MutationObserver(read);
|
|
35
|
+
observer.observe(document.documentElement, {
|
|
36
|
+
attributes: true,
|
|
37
|
+
attributeFilter: ["data-motion-pref"],
|
|
38
|
+
});
|
|
39
|
+
const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
40
|
+
mql.addEventListener("change", read);
|
|
41
|
+
return () => {
|
|
42
|
+
observer.disconnect();
|
|
43
|
+
mql.removeEventListener("change", read);
|
|
44
|
+
};
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="text-muted-foreground flex flex-col gap-1 text-body">
|
|
49
|
+
{/* Probe: drives a real property off the gated token so its computed value
|
|
50
|
+
reflects the effective duration the gate produces. */}
|
|
51
|
+
<span
|
|
52
|
+
ref={probeRef}
|
|
53
|
+
data-testid="motion-probe"
|
|
54
|
+
style={{ transitionProperty: "opacity", transitionDuration: "var(--t-base)" }}
|
|
55
|
+
aria-hidden
|
|
56
|
+
className="sr-only"
|
|
57
|
+
/>
|
|
58
|
+
<span>
|
|
59
|
+
<span className="text-foreground font-medium">--motion-factor:</span> {factor}
|
|
60
|
+
</span>
|
|
61
|
+
<span>
|
|
62
|
+
<span className="text-foreground font-medium">effective --t-base:</span> {effective}
|
|
63
|
+
</span>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function MotionDemo() {
|
|
69
|
+
const [open, setOpen] = useState(false);
|
|
70
|
+
const [runId, setRunId] = useState(0);
|
|
71
|
+
return (
|
|
72
|
+
<div className="flex w-[28rem] max-w-full flex-col gap-6">
|
|
73
|
+
<Card>
|
|
74
|
+
<CardHeader>
|
|
75
|
+
<CardTitle>Micro-interactions</CardTitle>
|
|
76
|
+
<CardDescription>
|
|
77
|
+
Flip the <strong>Motion</strong> toolbar (System / Reduce / Full) and the theme — every
|
|
78
|
+
transition below is gated by <code>--motion-factor</code>.
|
|
79
|
+
</CardDescription>
|
|
80
|
+
</CardHeader>
|
|
81
|
+
<CardContent className="flex flex-col gap-5">
|
|
82
|
+
{/* Hover-lift: Card's `interactive` variant glides up + shadow on hover. */}
|
|
83
|
+
<div className="grid grid-cols-3 gap-3">
|
|
84
|
+
{["Hover", "these", "cards"].map((label) => (
|
|
85
|
+
<Card
|
|
86
|
+
key={label}
|
|
87
|
+
interactive
|
|
88
|
+
className="flex h-16 items-center justify-center text-body shadow-none"
|
|
89
|
+
>
|
|
90
|
+
{label}
|
|
91
|
+
</Card>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{/* Press: scale feedback (gated --t-fast, from the Button base recipe). */}
|
|
96
|
+
<div className="flex flex-wrap gap-3">
|
|
97
|
+
<Button>Press me</Button>
|
|
98
|
+
<Button variant="secondary">Secondary</Button>
|
|
99
|
+
<Button variant="outline" onClick={() => setOpen((o) => !o)} aria-expanded={open}>
|
|
100
|
+
{open ? "Collapse" : "Expand"}
|
|
101
|
+
</Button>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Expand/collapse: grid-rows trick, animated over the gated --t-base. */}
|
|
105
|
+
<div
|
|
106
|
+
className="grid transition-[grid-template-rows] duration-base ease-standard"
|
|
107
|
+
style={{ gridTemplateRows: open ? "1fr" : "0fr" }}
|
|
108
|
+
>
|
|
109
|
+
<div className="overflow-hidden">
|
|
110
|
+
<p className="text-muted-foreground text-body">
|
|
111
|
+
Expands over <code>--t-base</code>; snaps near-instantly when motion is reduced (but
|
|
112
|
+
never a literal 0s, so transitionend still fires).
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Staggered entrance — the clearest on/off tell. Replay remounts the list. */}
|
|
118
|
+
<div className="flex items-center justify-between">
|
|
119
|
+
<span className="text-muted-foreground text-body">Staggered entrance</span>
|
|
120
|
+
<Button size="sm" variant="ghost" onClick={() => setRunId((n) => n + 1)}>
|
|
121
|
+
Replay
|
|
122
|
+
</Button>
|
|
123
|
+
</div>
|
|
124
|
+
<RevealGroup key={runId} className="flex flex-col gap-2" appear="up" staggerMs={60}>
|
|
125
|
+
{["First row", "Second row", "Third row", "Fourth row"].map((row) => (
|
|
126
|
+
<div key={row} className="bg-surface-muted rounded-md border px-3 py-2 text-body">
|
|
127
|
+
{row}
|
|
128
|
+
</div>
|
|
129
|
+
))}
|
|
130
|
+
</RevealGroup>
|
|
131
|
+
</CardContent>
|
|
132
|
+
</Card>
|
|
133
|
+
<GateReadout />
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const meta = {
|
|
139
|
+
title: "Foundations/Motion",
|
|
140
|
+
tags: ["autodocs"],
|
|
141
|
+
parameters: { layout: "centered" },
|
|
142
|
+
} satisfies Meta;
|
|
143
|
+
|
|
144
|
+
export default meta;
|
|
145
|
+
type Story = StoryObj<typeof meta>;
|
|
146
|
+
|
|
147
|
+
/** Visual demo — drive the toolbar "Motion" + theme controls to sweep states. */
|
|
148
|
+
export const Playground: Story = {
|
|
149
|
+
render: () => <MotionDemo />,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Reads the EFFECTIVE transition duration of a probe element under explicit
|
|
154
|
+
* `data-motion-pref="full"`. Explicit "full" overrides any OS reduce request, so
|
|
155
|
+
* this is environment-independent: it must resolve to the full ~0.18s base.
|
|
156
|
+
*/
|
|
157
|
+
export const GateFullMotion: Story = {
|
|
158
|
+
render: () => <MotionDemo />,
|
|
159
|
+
play: async ({ canvasElement }) => {
|
|
160
|
+
const root = document.documentElement;
|
|
161
|
+
const previous = root.getAttribute("data-motion-pref");
|
|
162
|
+
try {
|
|
163
|
+
root.setAttribute("data-motion-pref", "full");
|
|
164
|
+
const probe = canvasElement.querySelector<HTMLElement>('[data-testid="motion-probe"]')!;
|
|
165
|
+
// getComputedStyle forces a synchronous style flush, so the new gate value
|
|
166
|
+
// is reflected immediately.
|
|
167
|
+
const seconds = parseFloat(getComputedStyle(probe).transitionDuration);
|
|
168
|
+
await expect(seconds).toBeGreaterThanOrEqual(0.15);
|
|
169
|
+
} finally {
|
|
170
|
+
if (previous) root.setAttribute("data-motion-pref", previous);
|
|
171
|
+
else root.removeAttribute("data-motion-pref");
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Verifies the gate actually GATES: under `data-motion-pref="reduced"` the same
|
|
178
|
+
* probe collapses to a tiny fraction of the full duration (≥100× smaller) while
|
|
179
|
+
* staying strictly > 0 (the --motion-min floor — a literal 0s would never fire
|
|
180
|
+
* transitionend and would strand Radix's unmount-suspension).
|
|
181
|
+
*/
|
|
182
|
+
export const GateReducedMotion: Story = {
|
|
183
|
+
render: () => <MotionDemo />,
|
|
184
|
+
play: async ({ canvasElement }) => {
|
|
185
|
+
const root = document.documentElement;
|
|
186
|
+
const previous = root.getAttribute("data-motion-pref");
|
|
187
|
+
const probe = canvasElement.querySelector<HTMLElement>('[data-testid="motion-probe"]')!;
|
|
188
|
+
try {
|
|
189
|
+
root.setAttribute("data-motion-pref", "full");
|
|
190
|
+
const full = parseFloat(getComputedStyle(probe).transitionDuration);
|
|
191
|
+
root.setAttribute("data-motion-pref", "reduced");
|
|
192
|
+
const reduced = parseFloat(getComputedStyle(probe).transitionDuration);
|
|
193
|
+
|
|
194
|
+
await expect(full).toBeGreaterThanOrEqual(0.15); // full motion intact
|
|
195
|
+
await expect(reduced).toBeLessThan(full * 0.5); // gate clearly engaged
|
|
196
|
+
await expect(reduced).toBeLessThan(0.05); // effectively off
|
|
197
|
+
await expect(reduced).toBeGreaterThanOrEqual(0); // never negative / NaN
|
|
198
|
+
} finally {
|
|
199
|
+
if (previous) root.setAttribute("data-motion-pref", previous);
|
|
200
|
+
else root.removeAttribute("data-motion-pref");
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Guardrail for the load-bearing `--motion-min` floor: even at the worst case
|
|
207
|
+
* (`--motion-factor` = 0) the effective duration must stay strictly > 0s. A
|
|
208
|
+
* literal 0s never fires `transitionend`, which would strand Radix's CSS
|
|
209
|
+
* unmount-suspension (overlays would never unmount). Locks the `max(…, --motion-min)`
|
|
210
|
+
* in themes.css against a future "simplify" that drops the floor.
|
|
211
|
+
*/
|
|
212
|
+
export const GateFloorNeverZero: Story = {
|
|
213
|
+
render: () => (
|
|
214
|
+
<span
|
|
215
|
+
data-testid="floor-probe"
|
|
216
|
+
aria-hidden
|
|
217
|
+
style={{
|
|
218
|
+
transitionProperty: "opacity",
|
|
219
|
+
transitionDuration: "max(calc(var(--duration-base) * 0), var(--motion-min))",
|
|
220
|
+
}}
|
|
221
|
+
/>
|
|
222
|
+
),
|
|
223
|
+
play: async ({ canvasElement }) => {
|
|
224
|
+
const probe = canvasElement.querySelector<HTMLElement>('[data-testid="floor-probe"]')!;
|
|
225
|
+
const seconds = parseFloat(getComputedStyle(probe).transitionDuration);
|
|
226
|
+
await expect(seconds).toBeGreaterThan(0);
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Proves the Tailwind UTILITIES (not just the raw CSS vars) are generated and
|
|
232
|
+
* resolve to the tuned, gated scale (fast ≈0.16s, base ≈0.26s, slow ≈0.38s), and
|
|
233
|
+
* that `ease-standard` is the standard cubic-bezier — these are what components
|
|
234
|
+
* actually use. If a long-running dev server hasn't picked up the @theme
|
|
235
|
+
* additions, these resolve to defaults/none and this fails.
|
|
236
|
+
*/
|
|
237
|
+
export const UtilitiesGenerated: Story = {
|
|
238
|
+
render: () => (
|
|
239
|
+
<div>
|
|
240
|
+
<span data-testid="util-fast" className="transition-opacity duration-fast ease-standard" />
|
|
241
|
+
<span data-testid="util-base" className="transition-opacity duration-base ease-standard" />
|
|
242
|
+
<span data-testid="util-slow" className="transition-opacity duration-slow ease-standard" />
|
|
243
|
+
</div>
|
|
244
|
+
),
|
|
245
|
+
play: async ({ canvasElement }) => {
|
|
246
|
+
const dur = (id: string) =>
|
|
247
|
+
parseFloat(
|
|
248
|
+
getComputedStyle(canvasElement.querySelector<HTMLElement>(`[data-testid="${id}"]`)!)
|
|
249
|
+
.transitionDuration,
|
|
250
|
+
);
|
|
251
|
+
const fast = dur("util-fast");
|
|
252
|
+
const base = dur("util-base");
|
|
253
|
+
const slow = dur("util-slow");
|
|
254
|
+
// Tuned scale (full motion): fast 0.16s < base 0.26s < slow 0.38s — and clearly
|
|
255
|
+
// in the "smooth" range, not the original snappy 0.12/0.18/0.28.
|
|
256
|
+
await expect(fast).toBeGreaterThanOrEqual(0.14);
|
|
257
|
+
await expect(base).toBeGreaterThan(fast);
|
|
258
|
+
await expect(base).toBeGreaterThanOrEqual(0.24);
|
|
259
|
+
await expect(slow).toBeGreaterThan(base);
|
|
260
|
+
await expect(slow).toBeGreaterThanOrEqual(0.34);
|
|
261
|
+
const tf = getComputedStyle(
|
|
262
|
+
canvasElement.querySelector<HTMLElement>('[data-testid="util-fast"]')!,
|
|
263
|
+
).transitionTimingFunction;
|
|
264
|
+
await expect(tf).toContain("cubic-bezier"); // ease-standard applied
|
|
265
|
+
},
|
|
266
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* taste-profile.test.tsx — the RENDERED half of the taste profile (#108).
|
|
3
|
+
*
|
|
4
|
+
* The type/default surface is locked in `packages/tokens/src/taste-profile.test.ts`;
|
|
5
|
+
* this file exercises the real `<ThemeProvider>` in a DOM, which `@…-tokens`
|
|
6
|
+
* cannot do (its vitest environment is `node` by design — no React, no jsdom).
|
|
7
|
+
* `@…-ui` already ships the jsdom + Testing Library setup and legitimately
|
|
8
|
+
* depends on tokens (one-way dep graph), so the provider's live behaviour is
|
|
9
|
+
* proven here rather than mocked anywhere.
|
|
10
|
+
*/
|
|
11
|
+
import { useState } from "react";
|
|
12
|
+
import { describe, expect, it, beforeEach } from "vitest";
|
|
13
|
+
import { render, screen } from "@testing-library/react";
|
|
14
|
+
import userEvent from "@testing-library/user-event";
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_TASTE_PROFILE,
|
|
17
|
+
DEFAULT_THEME,
|
|
18
|
+
BUILT_IN_THEME_META,
|
|
19
|
+
ThemeProvider,
|
|
20
|
+
useDecoration,
|
|
21
|
+
useDensity,
|
|
22
|
+
useMotionPreference,
|
|
23
|
+
useTasteProfile,
|
|
24
|
+
type DecorationLevel,
|
|
25
|
+
} from "@elabs-ai/components-tokens";
|
|
26
|
+
|
|
27
|
+
/** A probe that prints the live profile and drives every axis from context. */
|
|
28
|
+
function Probe() {
|
|
29
|
+
const { profile, setRegister } = useTasteProfile();
|
|
30
|
+
const { setDensity } = useDensity();
|
|
31
|
+
const { setDecoration } = useDecoration();
|
|
32
|
+
const { setMotionPreference } = useMotionPreference();
|
|
33
|
+
const [level, setLevel] = useState<DecorationLevel>(0);
|
|
34
|
+
return (
|
|
35
|
+
<div>
|
|
36
|
+
<output data-testid="profile">{JSON.stringify(profile)}</output>
|
|
37
|
+
<button onClick={() => setRegister("brand")}>brand</button>
|
|
38
|
+
<button onClick={() => setDensity("compact")}>compact</button>
|
|
39
|
+
{/* stands in for a real app's motion control (Settings → Motion) */}
|
|
40
|
+
<button onClick={() => setMotionPreference("full")}>full motion</button>
|
|
41
|
+
<button
|
|
42
|
+
onClick={() => {
|
|
43
|
+
const next = ((level + 6) % 12) as DecorationLevel;
|
|
44
|
+
setLevel(next);
|
|
45
|
+
setDecoration(next);
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
decorate
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const profile = () => JSON.parse(screen.getByTestId("profile").textContent ?? "{}");
|
|
55
|
+
|
|
56
|
+
describe("useTasteProfile", () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
window.localStorage.clear();
|
|
59
|
+
for (const attr of ["data-register", "data-density", "data-decoration"]) {
|
|
60
|
+
document.documentElement.removeAttribute(attr);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns the restrained defaults inside a bare ThemeProvider", async () => {
|
|
65
|
+
render(
|
|
66
|
+
<ThemeProvider>
|
|
67
|
+
<Probe />
|
|
68
|
+
</ThemeProvider>,
|
|
69
|
+
);
|
|
70
|
+
expect(profile()).toEqual(DEFAULT_TASTE_PROFILE);
|
|
71
|
+
// The register is an inspectable seam on the root — no CSS keys off it.
|
|
72
|
+
expect(document.documentElement.getAttribute("data-register")).toBe("product");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("setRegister writes data-register and persists it", async () => {
|
|
76
|
+
render(
|
|
77
|
+
<ThemeProvider>
|
|
78
|
+
<Probe />
|
|
79
|
+
</ThemeProvider>,
|
|
80
|
+
);
|
|
81
|
+
await userEvent.click(screen.getByRole("button", { name: "brand" }));
|
|
82
|
+
expect(profile().register).toBe("brand");
|
|
83
|
+
expect(document.documentElement.getAttribute("data-register")).toBe("brand");
|
|
84
|
+
expect(window.localStorage.getItem("brand-ui-taste-register")).toBe("brand");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("rehydrates the persisted register on mount", () => {
|
|
88
|
+
window.localStorage.setItem("brand-ui-taste-register", "brand");
|
|
89
|
+
render(
|
|
90
|
+
<ThemeProvider>
|
|
91
|
+
<Probe />
|
|
92
|
+
</ThemeProvider>,
|
|
93
|
+
);
|
|
94
|
+
expect(profile().register).toBe("brand");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("ignores a junk persisted register and falls back to the default", () => {
|
|
98
|
+
window.localStorage.setItem("brand-ui-taste-register", "marketing");
|
|
99
|
+
render(
|
|
100
|
+
<ThemeProvider>
|
|
101
|
+
<Probe />
|
|
102
|
+
</ThemeProvider>,
|
|
103
|
+
);
|
|
104
|
+
expect(profile().register).toBe("product");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("tracks live changes on the density axis", async () => {
|
|
108
|
+
render(
|
|
109
|
+
<ThemeProvider>
|
|
110
|
+
<Probe />
|
|
111
|
+
</ThemeProvider>,
|
|
112
|
+
);
|
|
113
|
+
await userEvent.click(screen.getByRole("button", { name: "compact" }));
|
|
114
|
+
expect(profile().density).toBe("compact");
|
|
115
|
+
expect(document.documentElement.getAttribute("data-density")).toBe("compact");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("expressiveness IS the effective decoration level (one dial, two names)", async () => {
|
|
119
|
+
render(
|
|
120
|
+
<ThemeProvider>
|
|
121
|
+
<Probe />
|
|
122
|
+
</ThemeProvider>,
|
|
123
|
+
);
|
|
124
|
+
expect(profile().expressiveness).toBe(0);
|
|
125
|
+
await userEvent.click(screen.getByRole("button", { name: "decorate" }));
|
|
126
|
+
expect(profile().expressiveness).toBe(6);
|
|
127
|
+
expect(document.documentElement.getAttribute("data-decoration")).toBe("6");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("expressiveness follows the THEME's own decoration default when unset", () => {
|
|
131
|
+
render(
|
|
132
|
+
<ThemeProvider defaultTheme={DEFAULT_THEME}>
|
|
133
|
+
<Probe />
|
|
134
|
+
</ThemeProvider>,
|
|
135
|
+
);
|
|
136
|
+
// With no override the profile must report the theme's EFFECTIVE level (its
|
|
137
|
+
// registry default), never the (null) override. Read from the registry so a
|
|
138
|
+
// retuned theme can't silently make this vacuous.
|
|
139
|
+
expect(profile().expressiveness).toBe(
|
|
140
|
+
BUILT_IN_THEME_META[DEFAULT_THEME as "light" | "dark"].decorationLevel ?? 0,
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("honours an explicit defaultRegister", () => {
|
|
145
|
+
render(
|
|
146
|
+
<ThemeProvider defaultRegister="brand">
|
|
147
|
+
<Probe />
|
|
148
|
+
</ThemeProvider>,
|
|
149
|
+
);
|
|
150
|
+
expect(profile().register).toBe("brand");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The scaffold contract (#109): an app-spec's `taste` block becomes ThemeProvider
|
|
156
|
+
* props, and NOTHING else — no hardcoded density/expressiveness in component
|
|
157
|
+
* source. These lock that a spec-driven provider actually writes the dials, for
|
|
158
|
+
* both shipped presets.
|
|
159
|
+
*/
|
|
160
|
+
describe("a spec-driven ThemeProvider writes the profile onto the root", () => {
|
|
161
|
+
beforeEach(() => {
|
|
162
|
+
window.localStorage.clear();
|
|
163
|
+
for (const attr of ["data-register", "data-density", "data-decoration", "data-motion-pref"]) {
|
|
164
|
+
document.documentElement.removeAttribute(attr);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("product / calm — the restrained default omits the identity dials", () => {
|
|
169
|
+
render(
|
|
170
|
+
<ThemeProvider
|
|
171
|
+
defaultRegister="product"
|
|
172
|
+
defaultDensity="comfortable"
|
|
173
|
+
defaultMotionPreference="system"
|
|
174
|
+
defaultDecoration={0}
|
|
175
|
+
>
|
|
176
|
+
<Probe />
|
|
177
|
+
</ThemeProvider>,
|
|
178
|
+
);
|
|
179
|
+
const root = document.documentElement;
|
|
180
|
+
expect(root.getAttribute("data-register")).toBe("product");
|
|
181
|
+
// comfortable + "system" are identity values: no attribute, so the first
|
|
182
|
+
// paint is pixel-identical to a provider-less build (zero-flash).
|
|
183
|
+
expect(root.getAttribute("data-density")).toBeNull();
|
|
184
|
+
expect(root.getAttribute("data-motion-pref")).toBeNull();
|
|
185
|
+
expect(root.getAttribute("data-decoration")).toBe("0");
|
|
186
|
+
expect(profile()).toEqual({
|
|
187
|
+
register: "product",
|
|
188
|
+
density: "comfortable",
|
|
189
|
+
motion: "system",
|
|
190
|
+
expressiveness: 0,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("brand / expressive — every non-identity dial lands on the root", () => {
|
|
195
|
+
render(
|
|
196
|
+
<ThemeProvider
|
|
197
|
+
defaultRegister="brand"
|
|
198
|
+
defaultDensity="spacious"
|
|
199
|
+
defaultMotionPreference="reduced"
|
|
200
|
+
defaultDecoration={4}
|
|
201
|
+
>
|
|
202
|
+
<Probe />
|
|
203
|
+
</ThemeProvider>,
|
|
204
|
+
);
|
|
205
|
+
const root = document.documentElement;
|
|
206
|
+
expect(root.getAttribute("data-register")).toBe("brand");
|
|
207
|
+
expect(root.getAttribute("data-density")).toBe("spacious");
|
|
208
|
+
expect(root.getAttribute("data-motion-pref")).toBe("reduced");
|
|
209
|
+
// expressiveness IS decoration — one dial, one attribute.
|
|
210
|
+
expect(root.getAttribute("data-decoration")).toBe("4");
|
|
211
|
+
expect(profile()).toEqual({
|
|
212
|
+
register: "brand",
|
|
213
|
+
density: "spacious",
|
|
214
|
+
motion: "reduced",
|
|
215
|
+
expressiveness: 4,
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The a11y half of #108 AC3. `[data-motion-pref="full"]` is the ONE state that
|
|
221
|
+
* keeps `--motion-factor: 1` under an OS `prefers-reduced-motion: reduce` and
|
|
222
|
+
* opts out of the third-party animation cap (packages/tokens/src/themes.css) —
|
|
223
|
+
* informed consent, given by a PERSON for themselves. So:
|
|
224
|
+
* · no scaffold preset ships it (stages.md), and the app-spec schema rejects a
|
|
225
|
+
* spec that defaults to it (scripts/check-app-spec.test.mjs);
|
|
226
|
+
* · `system` — what the presets DO ship — writes no attribute at all, which is
|
|
227
|
+
* precisely what lets the OS rule win.
|
|
228
|
+
*/
|
|
229
|
+
it('the scaffold defaults ("system") write NO motion attribute, so the OS rule wins', () => {
|
|
230
|
+
render(
|
|
231
|
+
<ThemeProvider defaultRegister="brand" defaultMotionPreference="system" defaultDecoration={4}>
|
|
232
|
+
<Probe />
|
|
233
|
+
</ThemeProvider>,
|
|
234
|
+
);
|
|
235
|
+
expect(document.documentElement.getAttribute("data-motion-pref")).toBeNull();
|
|
236
|
+
expect(profile().motion).toBe("system");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('"full" is reachable only as the person\'s own explicit choice', async () => {
|
|
240
|
+
render(
|
|
241
|
+
<ThemeProvider>
|
|
242
|
+
<Probe />
|
|
243
|
+
</ThemeProvider>,
|
|
244
|
+
);
|
|
245
|
+
// Nothing an app is scaffolded with puts the override on the root…
|
|
246
|
+
expect(document.documentElement.getAttribute("data-motion-pref")).toBeNull();
|
|
247
|
+
// …only a motion control the person operates does.
|
|
248
|
+
await userEvent.click(screen.getByRole("button", { name: "full motion" }));
|
|
249
|
+
expect(document.documentElement.getAttribute("data-motion-pref")).toBe("full");
|
|
250
|
+
expect(profile().motion).toBe("full");
|
|
251
|
+
});
|
|
252
|
+
});
|