@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,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prose primitives — reading-surface typography, promoted from @elabs-ai/components-editor
|
|
3
|
+
* (#188; ADR-0012 own/re-export model: @elabs-ai/components-ui owns the canonical source,
|
|
4
|
+
* `@elabs-ai/components-editor/markdown` re-exports them under their original names —
|
|
5
|
+
* Heading, Text, Link, List, Blockquote, InlineCode). The markdown preview maps
|
|
6
|
+
* onto them (`#` → ProseHeading, paragraph → ProseText, `[]()` → ProseLink,
|
|
7
|
+
* `-` → ProseList, `>` → ProseBlockquote, `` `code` `` → ProseInlineCode).
|
|
8
|
+
* Generic, composable, theme-safe; usable in any app surface, not just markdown.
|
|
9
|
+
*
|
|
10
|
+
* `Prose*`-prefixed here because this barrel also exports the app `Heading` /
|
|
11
|
+
* `Text` primitives (typography.tsx): the app scale ranks UI chrome against the
|
|
12
|
+
* page; the prose scale ranks rungs WITHIN one reading column, so a document h1
|
|
13
|
+
* deliberately sits BELOW the app `display` rung.
|
|
14
|
+
*/
|
|
15
|
+
import {
|
|
16
|
+
forwardRef,
|
|
17
|
+
type AnchorHTMLAttributes,
|
|
18
|
+
type HTMLAttributes,
|
|
19
|
+
type OlHTMLAttributes,
|
|
20
|
+
} from "react";
|
|
21
|
+
import { cn } from "../../lib/cn";
|
|
22
|
+
import { TEXT_ROLE_REM } from "./typography";
|
|
23
|
+
|
|
24
|
+
export type ProseHeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Canonical reading-scale heading sizes in rem — the single source the markdown
|
|
28
|
+
* WYSIWYG editor derives its CSS vars from (@elabs-ai/components-editor `markdown-scale`).
|
|
29
|
+
* h2/h4/h5/h6 sit exactly on the title/subtitle/body roles; h1 (1.5rem) and
|
|
30
|
+
* h3 (1.125rem) are intermediate reading rungs with no app role.
|
|
31
|
+
*/
|
|
32
|
+
export const PROSE_HEADING_REM: Record<ProseHeadingLevel, number> = {
|
|
33
|
+
1: 1.5,
|
|
34
|
+
2: TEXT_ROLE_REM.title,
|
|
35
|
+
3: 1.125,
|
|
36
|
+
4: TEXT_ROLE_REM.subtitle,
|
|
37
|
+
5: TEXT_ROLE_REM.body,
|
|
38
|
+
6: TEXT_ROLE_REM.body,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Canonical prose heading weight (Tailwind `font-semibold`). */
|
|
42
|
+
export const PROSE_HEADING_WEIGHT = 600;
|
|
43
|
+
|
|
44
|
+
/** Canonical prose heading letter-spacing (Tailwind `tracking-tight`). */
|
|
45
|
+
export const PROSE_HEADING_TRACKING = "-0.025em";
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
* h1/h3 keep raw Tailwind steps (2xl / lg): the reading scale needs rungs
|
|
49
|
+
* BETWEEN the app roles (see PROSE_HEADING_REM) — baselined in the text-scale
|
|
50
|
+
* ratchet. The base classes' font-semibold / tracking-tight override the role
|
|
51
|
+
* companions, so the role re-points are render-identical to the old raw steps.
|
|
52
|
+
*/
|
|
53
|
+
const HEADING_SIZE: Record<ProseHeadingLevel, string> = {
|
|
54
|
+
1: "text-2xl",
|
|
55
|
+
2: "text-title",
|
|
56
|
+
3: "text-lg",
|
|
57
|
+
4: "text-subtitle",
|
|
58
|
+
5: "text-body",
|
|
59
|
+
6: "text-body text-muted-foreground",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export interface ProseHeadingProps extends HTMLAttributes<HTMLHeadingElement> {
|
|
63
|
+
level?: ProseHeadingLevel;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const ProseHeading = forwardRef<HTMLHeadingElement, ProseHeadingProps>(function ProseHeading(
|
|
67
|
+
{ level = 2, className, ...props },
|
|
68
|
+
ref,
|
|
69
|
+
) {
|
|
70
|
+
const Tag = `h${level}` as const;
|
|
71
|
+
return (
|
|
72
|
+
<Tag
|
|
73
|
+
ref={ref}
|
|
74
|
+
className={cn(
|
|
75
|
+
"scroll-m-20 font-semibold tracking-tight text-foreground",
|
|
76
|
+
HEADING_SIZE[level],
|
|
77
|
+
className,
|
|
78
|
+
)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const TEXT_VARIANT = {
|
|
85
|
+
body: "text-body leading-relaxed text-foreground",
|
|
86
|
+
muted: "text-body leading-relaxed text-muted-foreground",
|
|
87
|
+
lead: "text-subtitle font-normal leading-relaxed text-foreground",
|
|
88
|
+
small: "text-meta font-normal text-muted-foreground",
|
|
89
|
+
} as const;
|
|
90
|
+
|
|
91
|
+
export interface ProseTextProps extends HTMLAttributes<HTMLParagraphElement> {
|
|
92
|
+
variant?: keyof typeof TEXT_VARIANT;
|
|
93
|
+
/** Render as a different element (e.g. "span"). Defaults to "p". */
|
|
94
|
+
as?: "p" | "span" | "div";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const ProseText = forwardRef<HTMLParagraphElement, ProseTextProps>(function ProseText(
|
|
98
|
+
{ variant = "body", as: Tag = "p", className, ...props },
|
|
99
|
+
ref,
|
|
100
|
+
) {
|
|
101
|
+
return <Tag ref={ref} className={cn(TEXT_VARIANT[variant], className)} {...props} />;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export interface ProseLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
105
|
+
/** Opens in a new tab with safe rel. Auto-detected for http(s) hrefs when unset. */
|
|
106
|
+
external?: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const ProseLink = forwardRef<HTMLAnchorElement, ProseLinkProps>(function ProseLink(
|
|
110
|
+
{ external, href, className, ...props },
|
|
111
|
+
ref,
|
|
112
|
+
) {
|
|
113
|
+
const isExternal = external ?? (typeof href === "string" && /^https?:\/\//.test(href));
|
|
114
|
+
return (
|
|
115
|
+
<a
|
|
116
|
+
ref={ref}
|
|
117
|
+
href={href}
|
|
118
|
+
className={cn(
|
|
119
|
+
// #399 — `text-primary-text`, not the `text-primary` FILL rung: a prose
|
|
120
|
+
// link is ordinary body text and owes WCAG 1.4.3 AA (4.5:1), which the
|
|
121
|
+
// fill only guarantees at the 1.4.11 mark bar (3:1).
|
|
122
|
+
"font-medium text-primary-text underline underline-offset-4 hover:no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
123
|
+
className,
|
|
124
|
+
)}
|
|
125
|
+
{...(isExternal ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
|
126
|
+
{...props}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export interface ProseListProps extends OlHTMLAttributes<HTMLElement> {
|
|
132
|
+
ordered?: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const ProseList = forwardRef<HTMLUListElement & HTMLOListElement, ProseListProps>(
|
|
136
|
+
function ProseList({ ordered = false, className, ...props }, ref) {
|
|
137
|
+
const Tag = ordered ? "ol" : "ul";
|
|
138
|
+
return (
|
|
139
|
+
<Tag
|
|
140
|
+
ref={ref}
|
|
141
|
+
className={cn(
|
|
142
|
+
"my-1 space-y-1 ps-6 text-body text-foreground",
|
|
143
|
+
ordered ? "list-decimal" : "list-disc",
|
|
144
|
+
className,
|
|
145
|
+
)}
|
|
146
|
+
{...props}
|
|
147
|
+
/>
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
export const ProseListItem = forwardRef<HTMLLIElement, HTMLAttributes<HTMLLIElement>>(
|
|
153
|
+
function ProseListItem({ className, ...props }, ref) {
|
|
154
|
+
return (
|
|
155
|
+
<li
|
|
156
|
+
ref={ref}
|
|
157
|
+
className={cn("leading-relaxed [&>ul]:mt-1 [&>ol]:mt-1", className)}
|
|
158
|
+
{...props}
|
|
159
|
+
/>
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
export const ProseBlockquote = forwardRef<HTMLQuoteElement, HTMLAttributes<HTMLQuoteElement>>(
|
|
165
|
+
function ProseBlockquote({ className, ...props }, ref) {
|
|
166
|
+
return (
|
|
167
|
+
<blockquote
|
|
168
|
+
ref={ref}
|
|
169
|
+
className={cn(
|
|
170
|
+
"border-s-2 border-border ps-4 text-body italic text-muted-foreground",
|
|
171
|
+
className,
|
|
172
|
+
)}
|
|
173
|
+
{...props}
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
export const ProseInlineCode = forwardRef<HTMLElement, HTMLAttributes<HTMLElement>>(
|
|
180
|
+
function ProseInlineCode({ className, ...props }, ref) {
|
|
181
|
+
return (
|
|
182
|
+
<code
|
|
183
|
+
ref={ref}
|
|
184
|
+
// 0.85em (not the fixed `code` role): inline code must scale RELATIVE to
|
|
185
|
+
// whatever text it sits in (body, a heading, …). Baselined in the ratchet.
|
|
186
|
+
className={cn(
|
|
187
|
+
"rounded bg-muted px-1.5 py-0.5 font-mono text-[0.85em] text-foreground",
|
|
188
|
+
className,
|
|
189
|
+
)}
|
|
190
|
+
{...props}
|
|
191
|
+
/>
|
|
192
|
+
);
|
|
193
|
+
},
|
|
194
|
+
);
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { expect } from "storybook/test";
|
|
4
|
+
import { Heading, Text } from "./typography";
|
|
5
|
+
import {
|
|
6
|
+
ProseBlockquote,
|
|
7
|
+
ProseHeading,
|
|
8
|
+
ProseInlineCode,
|
|
9
|
+
ProseLink,
|
|
10
|
+
ProseList,
|
|
11
|
+
ProseListItem,
|
|
12
|
+
ProseText,
|
|
13
|
+
} from "./prose";
|
|
14
|
+
|
|
15
|
+
const meta = {
|
|
16
|
+
title: "Foundations/Typography",
|
|
17
|
+
component: Text,
|
|
18
|
+
tags: ["autodocs"],
|
|
19
|
+
parameters: {
|
|
20
|
+
layout: "padded",
|
|
21
|
+
docs: {
|
|
22
|
+
description: {
|
|
23
|
+
component:
|
|
24
|
+
"The semantic type-role API (#187/#188): `Heading` maps a level to the " +
|
|
25
|
+
"display/title/subtitle rungs; `Text` maps intent (lead/body/caption/meta/kpi/code) " +
|
|
26
|
+
"to a role. Roles bundle size + leading + weight + tracking and stay composable. " +
|
|
27
|
+
"`Prose*` primitives carry the reading-column scale the markdown preview maps onto.\n\n" +
|
|
28
|
+
'**Type is density-aware** (#340): `data-density="compact"` shrinks the whole scale ' +
|
|
29
|
+
"(−6.25%) as well as the spacing, and `spacious` grows it (+6.25%) — see the " +
|
|
30
|
+
"*Density scale* story. `comfortable` is the exact identity.\n\n" +
|
|
31
|
+
"**Font smoothing ships with the tokens stylesheet** (#345): the `@layer base` `body` rule in " +
|
|
32
|
+
"`@elabs-ai/components-tokens/styles.css` sets `-webkit-font-smoothing: antialiased` and " +
|
|
33
|
+
"`-moz-osx-font-smoothing: grayscale`, so every consumer inherits it from the one import. Do not " +
|
|
34
|
+
"re-add those two lines in an app stylesheet. It is a base-layer rule, so an app that genuinely " +
|
|
35
|
+
"wants subpixel rendering can still override it.",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
} satisfies Meta<typeof Text>;
|
|
40
|
+
export default meta;
|
|
41
|
+
type Story = StoryObj<typeof meta>;
|
|
42
|
+
|
|
43
|
+
export const Default: Story = {
|
|
44
|
+
args: { children: "Body text — the default role (text-body == text-sm by design)." },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const TextVariants: Story = {
|
|
48
|
+
render: () => (
|
|
49
|
+
<div className="max-w-2xl space-y-3">
|
|
50
|
+
<Text variant="lead">Lead — a lead paragraph: the subtitle rung at body weight.</Text>
|
|
51
|
+
<Text variant="body">Body — default reading text for app surfaces.</Text>
|
|
52
|
+
<Text variant="caption">Caption — secondary / supporting body.</Text>
|
|
53
|
+
<Text variant="meta">Meta — metadata, eyebrows, timestamps.</Text>
|
|
54
|
+
<Text variant="kpi">12,480</Text>
|
|
55
|
+
<Text variant="code">pnpm --filter @elabs-ai/components-ui test</Text>
|
|
56
|
+
</div>
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const Tones: Story = {
|
|
61
|
+
render: () => (
|
|
62
|
+
<div className="space-y-2">
|
|
63
|
+
<Text tone="default">Default tone — foreground.</Text>
|
|
64
|
+
<Text tone="muted">Muted tone — secondary information.</Text>
|
|
65
|
+
<Text tone="primary">Primary tone — brand emphasis.</Text>
|
|
66
|
+
</div>
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const Headings: Story = {
|
|
71
|
+
render: () => (
|
|
72
|
+
<div className="space-y-3">
|
|
73
|
+
<Heading level={1}>Level 1 — display</Heading>
|
|
74
|
+
<Heading level={2}>Level 2 — title</Heading>
|
|
75
|
+
<Heading level={3}>Level 3 — title</Heading>
|
|
76
|
+
<Heading level={4}>Level 4 — subtitle</Heading>
|
|
77
|
+
<Heading level={5}>Level 5 — subtitle</Heading>
|
|
78
|
+
<Heading level={6}>Level 6 — subtitle</Heading>
|
|
79
|
+
</div>
|
|
80
|
+
),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const HeadingSizeOverride: Story = {
|
|
84
|
+
render: () => (
|
|
85
|
+
<div className="space-y-3">
|
|
86
|
+
<Heading level={2} size="display">
|
|
87
|
+
An h2 that reads as display
|
|
88
|
+
</Heading>
|
|
89
|
+
<Heading level={2} size="subtitle">
|
|
90
|
+
An h2 that reads as subtitle — the tag keeps the document outline
|
|
91
|
+
</Heading>
|
|
92
|
+
</div>
|
|
93
|
+
),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const AsAndAsChild: Story = {
|
|
97
|
+
render: () => (
|
|
98
|
+
<div className="space-y-3">
|
|
99
|
+
<Text as="span" variant="meta" tone="muted">
|
|
100
|
+
Rendered as a span
|
|
101
|
+
</Text>
|
|
102
|
+
<Heading level={2} asChild>
|
|
103
|
+
<a href="#typography">A heading that is a link (asChild)</a>
|
|
104
|
+
</Heading>
|
|
105
|
+
</div>
|
|
106
|
+
),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The DENSITY dial rescales type as well as spacing (#340). Literal `text-<role>`
|
|
111
|
+
* classes (not a template string) so Tailwind statically emits every rung.
|
|
112
|
+
*/
|
|
113
|
+
const DENSITY_COLUMNS = [
|
|
114
|
+
{ mode: "compact", label: "Compact", note: "−6.25% type · −11% spacing" },
|
|
115
|
+
{ mode: "comfortable", label: "Comfortable", note: "identity — the default" },
|
|
116
|
+
{ mode: "spacious", label: "Spacious", note: "+6.25% type · +12% spacing" },
|
|
117
|
+
] as const;
|
|
118
|
+
|
|
119
|
+
const DENSITY_ROLES = [
|
|
120
|
+
{ role: "display", cls: "text-display", sample: "Pipeline health" },
|
|
121
|
+
{ role: "title", cls: "text-title", sample: "Ingestion failures" },
|
|
122
|
+
{ role: "subtitle", cls: "text-subtitle", sample: "Last 24 hours" },
|
|
123
|
+
{
|
|
124
|
+
role: "body",
|
|
125
|
+
cls: "text-body",
|
|
126
|
+
sample: "Four sources missed their window; two recovered on retry.",
|
|
127
|
+
},
|
|
128
|
+
{ role: "caption", cls: "text-caption", sample: "Retries are counted per source, not per file." },
|
|
129
|
+
{ role: "meta", cls: "text-meta", sample: "Updated 3 minutes ago" },
|
|
130
|
+
{ role: "kpi", cls: "text-kpi tabular-nums", sample: "12,480" },
|
|
131
|
+
{ role: "code", cls: "text-code font-mono", sample: "sources.retry(max=3)" },
|
|
132
|
+
] as const;
|
|
133
|
+
|
|
134
|
+
/** One role row, with its measured rendered size so the dial is observable. */
|
|
135
|
+
function DensityRoleRow({ role, cls, sample }: (typeof DENSITY_ROLES)[number]) {
|
|
136
|
+
const ref = useRef<HTMLParagraphElement>(null);
|
|
137
|
+
const [measured, setMeasured] = useState("");
|
|
138
|
+
useLayoutEffect(() => {
|
|
139
|
+
const el = ref.current;
|
|
140
|
+
if (!el) return;
|
|
141
|
+
const { fontSize, lineHeight } = getComputedStyle(el);
|
|
142
|
+
setMeasured(fontSize && lineHeight ? `${fontSize} / ${lineHeight}` : "");
|
|
143
|
+
}, []);
|
|
144
|
+
return (
|
|
145
|
+
<div className="space-y-1 border-t border-border py-2 first:border-t-0">
|
|
146
|
+
<p className="m-0 flex items-baseline justify-between gap-3 text-meta text-muted-foreground">
|
|
147
|
+
<span>{role}</span>
|
|
148
|
+
<span className="tabular-nums" data-testid={`measured-${role}`}>
|
|
149
|
+
{measured}
|
|
150
|
+
</span>
|
|
151
|
+
</p>
|
|
152
|
+
<p ref={ref} className={`m-0 ${cls}`} data-testid={`sample-${role}`}>
|
|
153
|
+
{sample}
|
|
154
|
+
</p>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const DensityScale: Story = {
|
|
160
|
+
name: "Density scale",
|
|
161
|
+
parameters: {
|
|
162
|
+
docs: {
|
|
163
|
+
description: {
|
|
164
|
+
story:
|
|
165
|
+
"The same eight roles at all three densities, side by side. Density scales the " +
|
|
166
|
+
"type SIZE and LINE-HEIGHT (weight and tracking are untouched) alongside the " +
|
|
167
|
+
"spacing, so a compact surface tightens as a whole. `comfortable` is the exact " +
|
|
168
|
+
"identity — pixel-identical to a pre-#340 build. The measured `font-size / " +
|
|
169
|
+
"line-height` under each role name is read live from the browser, so the columns " +
|
|
170
|
+
"cannot claim a difference they do not render.",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
render: () => (
|
|
175
|
+
<div className="space-y-4">
|
|
176
|
+
<p className="m-0 max-w-prose text-caption text-muted-foreground">
|
|
177
|
+
Each column pins its own <code className="text-code">data-density</code>. Type shrinks at
|
|
178
|
+
roughly HALF the rate of spacing — whitespace can be cut hard before a layout stops working,
|
|
179
|
+
text cannot. Compact body lands at 13.125px, above the 13px reading floor.
|
|
180
|
+
</p>
|
|
181
|
+
<div className="flex flex-wrap gap-6">
|
|
182
|
+
{DENSITY_COLUMNS.map(({ mode, label, note }) => (
|
|
183
|
+
<section key={mode} className="min-w-64 flex-1 space-y-2">
|
|
184
|
+
<Heading level={3} size="subtitle">
|
|
185
|
+
{label}
|
|
186
|
+
</Heading>
|
|
187
|
+
<Text variant="meta" tone="muted">
|
|
188
|
+
{note}
|
|
189
|
+
</Text>
|
|
190
|
+
{/* The dial itself — everything below scales, the label above does not,
|
|
191
|
+
so the three columns stay comparable. */}
|
|
192
|
+
<div data-density={mode} className="rounded-lg border border-border p-3">
|
|
193
|
+
{DENSITY_ROLES.map((r) => (
|
|
194
|
+
<DensityRoleRow key={r.role} {...r} />
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
</section>
|
|
198
|
+
))}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
),
|
|
202
|
+
// Asserts the dial on the REAL rendered surface: the source-parsed token test
|
|
203
|
+
// proves the arithmetic, this proves a browser actually applies it.
|
|
204
|
+
play: async ({ canvas }) => {
|
|
205
|
+
const px = (el: HTMLElement) => parseFloat(getComputedStyle(el).fontSize);
|
|
206
|
+
const columns = canvas.getAllByTestId("sample-body");
|
|
207
|
+
expect(columns).toHaveLength(3);
|
|
208
|
+
const [compact, comfortable, spacious] = columns.map(px);
|
|
209
|
+
|
|
210
|
+
// Identity: comfortable is still the shipped 14px body.
|
|
211
|
+
expect(comfortable).toBe(14);
|
|
212
|
+
// Direction: the dial actually moves type, both ways.
|
|
213
|
+
expect(compact).toBeLessThan(comfortable);
|
|
214
|
+
expect(spacious).toBeGreaterThan(comfortable);
|
|
215
|
+
// Legibility floor: compact body never drops below 13px.
|
|
216
|
+
expect(compact).toBeGreaterThanOrEqual(13);
|
|
217
|
+
// Weight is a semantic rung — it does NOT scale with density.
|
|
218
|
+
for (const el of columns) expect(getComputedStyle(el).fontWeight).toBe("400");
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export const Prose: Story = {
|
|
223
|
+
render: () => (
|
|
224
|
+
<div className="max-w-2xl space-y-3">
|
|
225
|
+
<ProseHeading level={1}>Prose heading level 1</ProseHeading>
|
|
226
|
+
<ProseHeading level={2}>Prose heading level 2</ProseHeading>
|
|
227
|
+
<ProseHeading level={3}>Prose heading level 3</ProseHeading>
|
|
228
|
+
<ProseText>
|
|
229
|
+
Body text with an <ProseLink href="https://example.com">external link</ProseLink> and some{" "}
|
|
230
|
+
<ProseInlineCode>inline code</ProseInlineCode>.
|
|
231
|
+
</ProseText>
|
|
232
|
+
<ProseText variant="muted">Muted helper text.</ProseText>
|
|
233
|
+
<ProseBlockquote>A blockquote rendered through the branded primitive.</ProseBlockquote>
|
|
234
|
+
<ProseList>
|
|
235
|
+
<ProseListItem>First bullet</ProseListItem>
|
|
236
|
+
<ProseListItem>Second bullet</ProseListItem>
|
|
237
|
+
</ProseList>
|
|
238
|
+
<ProseList ordered>
|
|
239
|
+
<ProseListItem>First step</ProseListItem>
|
|
240
|
+
<ProseListItem>Second step</ProseListItem>
|
|
241
|
+
</ProseList>
|
|
242
|
+
</div>
|
|
243
|
+
),
|
|
244
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../lib/cn";
|
|
8
|
+
import { PROSE_HEADING_REM, ProseHeading } from "./prose";
|
|
9
|
+
import { Heading, TEXT_ROLE_REM, Text } from "./typography";
|
|
10
|
+
|
|
11
|
+
afterEach(cleanup);
|
|
12
|
+
|
|
13
|
+
describe("Text", () => {
|
|
14
|
+
it("maps variants to type roles (kpi bundles tabular-nums, code bundles mono)", () => {
|
|
15
|
+
render(
|
|
16
|
+
<>
|
|
17
|
+
<Text>body</Text>
|
|
18
|
+
<Text variant="kpi">42</Text>
|
|
19
|
+
<Text variant="code">cn()</Text>
|
|
20
|
+
<Text variant="lead">lead</Text>
|
|
21
|
+
</>,
|
|
22
|
+
);
|
|
23
|
+
expect(screen.getByText("body")).toHaveClass("text-body", "text-foreground");
|
|
24
|
+
expect(screen.getByText("42")).toHaveClass("text-kpi", "tabular-nums");
|
|
25
|
+
expect(screen.getByText("cn()")).toHaveClass("text-code", "font-mono");
|
|
26
|
+
// lead == the subtitle rung at body weight (research 07 §B.3).
|
|
27
|
+
expect(screen.getByText("lead")).toHaveClass("text-subtitle", "font-normal");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("applies tone and renders as p / span / asChild", () => {
|
|
31
|
+
render(
|
|
32
|
+
<>
|
|
33
|
+
<Text tone="muted">muted</Text>
|
|
34
|
+
<Text as="span" data-testid="span-text">
|
|
35
|
+
span
|
|
36
|
+
</Text>
|
|
37
|
+
<Text asChild>
|
|
38
|
+
<em>emphasized</em>
|
|
39
|
+
</Text>
|
|
40
|
+
</>,
|
|
41
|
+
);
|
|
42
|
+
expect(screen.getByText("muted")).toHaveClass("text-muted-foreground");
|
|
43
|
+
expect(screen.getByText("muted").tagName).toBe("P");
|
|
44
|
+
expect(screen.getByTestId("span-text").tagName).toBe("SPAN");
|
|
45
|
+
expect(screen.getByText("emphasized").tagName).toBe("EM");
|
|
46
|
+
expect(screen.getByText("emphasized")).toHaveClass("text-body");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("merges className last (a caller's role wins over the variant's)", () => {
|
|
50
|
+
render(<Text className="text-title">override</Text>);
|
|
51
|
+
const el = screen.getByText("override");
|
|
52
|
+
expect(el).toHaveClass("text-title");
|
|
53
|
+
expect(el).not.toHaveClass("text-body");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("Heading", () => {
|
|
58
|
+
it("derives the tag and the default size from level (1→display, 2-3→title, 4-6→subtitle)", () => {
|
|
59
|
+
render(
|
|
60
|
+
<>
|
|
61
|
+
<Heading level={1}>one</Heading>
|
|
62
|
+
<Heading level={3}>three</Heading>
|
|
63
|
+
<Heading level={5}>five</Heading>
|
|
64
|
+
</>,
|
|
65
|
+
);
|
|
66
|
+
const h1 = screen.getByRole("heading", { level: 1 });
|
|
67
|
+
expect(h1.tagName).toBe("H1");
|
|
68
|
+
expect(h1).toHaveClass("text-display", "font-display", "text-balance");
|
|
69
|
+
const h3 = screen.getByRole("heading", { level: 3 });
|
|
70
|
+
expect(h3.tagName).toBe("H3");
|
|
71
|
+
expect(h3).toHaveClass("text-title");
|
|
72
|
+
const h5 = screen.getByRole("heading", { level: 5 });
|
|
73
|
+
expect(h5.tagName).toBe("H5");
|
|
74
|
+
expect(h5).toHaveClass("text-subtitle");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("size overrides the visual rung while keeping the tag", () => {
|
|
78
|
+
render(
|
|
79
|
+
<Heading level={2} size="subtitle">
|
|
80
|
+
small h2
|
|
81
|
+
</Heading>,
|
|
82
|
+
);
|
|
83
|
+
const h2 = screen.getByRole("heading", { level: 2 });
|
|
84
|
+
expect(h2.tagName).toBe("H2");
|
|
85
|
+
expect(h2).toHaveClass("text-subtitle");
|
|
86
|
+
expect(h2).not.toHaveClass("text-title");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("supports asChild (the child element becomes the heading root)", () => {
|
|
90
|
+
render(
|
|
91
|
+
<Heading level={2} asChild>
|
|
92
|
+
<a href="#section">linked heading</a>
|
|
93
|
+
</Heading>,
|
|
94
|
+
);
|
|
95
|
+
const a = screen.getByRole("link", { name: "linked heading" });
|
|
96
|
+
expect(a).toHaveClass("text-title");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("cn() role merging (lib/cn extension)", () => {
|
|
101
|
+
it("keeps a role alongside a text color, and lets later role/raw sizes win", () => {
|
|
102
|
+
// Without the font-size registration tailwind-merge classifies text-<role>
|
|
103
|
+
// as a COLOR and `text-foreground` would silently drop `text-kpi`.
|
|
104
|
+
expect(cn("text-kpi", "tabular-nums", "text-foreground")).toBe(
|
|
105
|
+
"text-kpi tabular-nums text-foreground",
|
|
106
|
+
);
|
|
107
|
+
expect(cn("text-body", "text-title")).toBe("text-title");
|
|
108
|
+
expect(cn("text-sm", "text-body")).toBe("text-body");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("type-role rems (drift guards)", () => {
|
|
113
|
+
it("TEXT_ROLE_REM mirrors the --text-<role> tokens in themes.css", () => {
|
|
114
|
+
// vitest runs with cwd = packages/ui.
|
|
115
|
+
const css = readFileSync(resolve(process.cwd(), "../tokens/src/themes.css"), "utf8");
|
|
116
|
+
for (const [role, rem] of Object.entries(TEXT_ROLE_REM)) {
|
|
117
|
+
// Since #340 the rem LITERAL lives one layer down, in themes.css
|
|
118
|
+
// § TYPE SCALE BASE, so the density dial can scale from it (density.css);
|
|
119
|
+
// `--text-<role>` aliases it. Both halves are pinned here: the mirror must
|
|
120
|
+
// track the literal, AND the role must still resolve to that literal —
|
|
121
|
+
// a role re-inlined or re-pointed elsewhere would silently detach the
|
|
122
|
+
// runtime mirror @elabs-ai/components-editor's markdown scale reads.
|
|
123
|
+
expect(css).toContain(`--type-size-${role}: ${rem}rem;`);
|
|
124
|
+
expect(css).toContain(`--text-${role}: var(--type-size-${role});`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("the prose reading scale keeps the canonical markdown rems (issue #18 contract)", () => {
|
|
129
|
+
expect(PROSE_HEADING_REM).toEqual({ 1: 1.5, 2: 1.25, 3: 1.125, 4: 1, 5: 0.875, 6: 0.875 });
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("ProseHeading", () => {
|
|
134
|
+
it("renders the heading element for the given level with the role class", () => {
|
|
135
|
+
render(<ProseHeading level={4}>Doc title</ProseHeading>);
|
|
136
|
+
const h = screen.getByRole("heading", { level: 4, name: "Doc title" });
|
|
137
|
+
expect(h.tagName).toBe("H4");
|
|
138
|
+
expect(h).toHaveClass("text-subtitle");
|
|
139
|
+
});
|
|
140
|
+
});
|