@delacour/react-native-ui 0.1.0-alpha.3 → 0.1.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/README.md +11 -7
- package/package.json +49 -5
- package/src/components/accordion/AGENTS.md +21 -9
- package/src/components/accordion/accordion-content.tsx +7 -4
- package/src/components/accordion/accordion-item.tsx +15 -13
- package/src/components/accordion/accordion.context.tsx +11 -5
- package/src/components/accordion/accordion.variants.test.ts +64 -3
- package/src/components/accordion/accordion.variants.ts +33 -9
- package/src/components/alert/AGENTS.md +102 -0
- package/src/components/alert/alert-action.tsx +17 -0
- package/src/components/alert/alert-close-button.tsx +50 -0
- package/src/components/alert/alert-content.tsx +15 -0
- package/src/components/alert/alert-description.tsx +16 -0
- package/src/components/alert/alert-glyphs.ts +25 -0
- package/src/components/alert/alert-indicator.tsx +40 -0
- package/src/components/alert/alert-title.tsx +18 -0
- package/src/components/alert/alert.context.tsx +69 -0
- package/src/components/alert/alert.tsx +160 -0
- package/src/components/alert/alert.types.ts +10 -0
- package/src/components/alert/alert.variants.test.ts +218 -0
- package/src/components/alert/alert.variants.ts +180 -0
- package/src/components/alert/index.ts +23 -0
- package/src/components/avatar/AGENTS.md +106 -0
- package/src/components/avatar/avatar-badge.tsx +51 -0
- package/src/components/avatar/avatar-group.tsx +139 -0
- package/src/components/avatar/avatar.context.tsx +93 -0
- package/src/components/avatar/avatar.tsx +201 -0
- package/src/components/avatar/avatar.utils.test.ts +174 -0
- package/src/components/avatar/avatar.utils.ts +141 -0
- package/src/components/avatar/avatar.variants.test.ts +251 -0
- package/src/components/avatar/avatar.variants.ts +270 -0
- package/src/components/avatar/index.ts +39 -0
- package/src/components/badge/AGENTS.md +5 -0
- package/src/components/badge/badge.tsx +6 -1
- package/src/components/bottom-sheet/AGENTS.md +186 -221
- package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
- package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
- package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -207
- package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
- package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
- package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
- package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -120
- package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
- package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
- package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
- package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
- package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -52
- package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
- package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
- package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
- package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
- package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
- package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
- package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -70
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -105
- package/src/components/bottom-sheet/index.ts +39 -17
- package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
- package/src/components/button/AGENTS.md +45 -11
- package/src/components/button/button-group.tsx +44 -8
- package/src/components/button/button.context.tsx +10 -0
- package/src/components/button/button.tsx +2 -1
- package/src/components/button/button.variants.test.ts +154 -0
- package/src/components/button/button.variants.ts +71 -6
- package/src/components/button/index.ts +2 -0
- package/src/components/calendar/AGENTS.md +142 -0
- package/src/components/calendar/calendar-caption.tsx +116 -0
- package/src/components/calendar/calendar-day.tsx +103 -0
- package/src/components/calendar/calendar-grid.tsx +183 -0
- package/src/components/calendar/calendar-header.tsx +34 -0
- package/src/components/calendar/calendar-nav.tsx +51 -0
- package/src/components/calendar/calendar-picker.tsx +128 -0
- package/src/components/calendar/calendar-weekdays.tsx +48 -0
- package/src/components/calendar/calendar.context.tsx +108 -0
- package/src/components/calendar/calendar.date.test.ts +456 -0
- package/src/components/calendar/calendar.date.ts +584 -0
- package/src/components/calendar/calendar.tsx +389 -0
- package/src/components/calendar/calendar.types.ts +87 -0
- package/src/components/calendar/calendar.variants.test.ts +363 -0
- package/src/components/calendar/calendar.variants.ts +426 -0
- package/src/components/calendar/index.ts +96 -0
- package/src/components/card/AGENTS.md +88 -0
- package/src/components/card/card-action.tsx +16 -0
- package/src/components/card/card-content.tsx +12 -0
- package/src/components/card/card-description.tsx +12 -0
- package/src/components/card/card-footer.tsx +30 -0
- package/src/components/card/card-header.tsx +45 -0
- package/src/components/card/card-title.tsx +25 -0
- package/src/components/card/card.context.tsx +64 -0
- package/src/components/card/card.tsx +94 -0
- package/src/components/card/card.types.ts +13 -0
- package/src/components/card/card.variants.test.ts +240 -0
- package/src/components/card/card.variants.ts +177 -0
- package/src/components/card/index.ts +15 -0
- package/src/components/chart/AGENTS.md +9 -1
- package/src/components/chart/chart.tsx +8 -1
- package/src/components/chip/AGENTS.md +113 -0
- package/src/components/chip/chip-close-button.tsx +48 -0
- package/src/components/chip/chip-end-content.tsx +15 -0
- package/src/components/chip/chip-label.tsx +21 -0
- package/src/components/chip/chip-start-content.tsx +16 -0
- package/src/components/chip/chip.context.tsx +66 -0
- package/src/components/chip/chip.tsx +268 -0
- package/src/components/chip/chip.types.ts +10 -0
- package/src/components/chip/chip.variants.test.ts +335 -0
- package/src/components/chip/chip.variants.ts +321 -0
- package/src/components/chip/index.ts +24 -0
- package/src/components/collapsible/AGENTS.md +97 -0
- package/src/components/collapsible/collapsible-content.tsx +97 -0
- package/src/components/collapsible/collapsible-description.tsx +17 -0
- package/src/components/collapsible/collapsible-indicator.tsx +92 -0
- package/src/components/collapsible/collapsible-title.tsx +17 -0
- package/src/components/collapsible/collapsible-trigger.tsx +133 -0
- package/src/components/collapsible/collapsible.context.tsx +91 -0
- package/src/components/collapsible/collapsible.tsx +167 -0
- package/src/components/collapsible/collapsible.types.ts +9 -0
- package/src/components/collapsible/collapsible.variants.test.ts +338 -0
- package/src/components/collapsible/collapsible.variants.ts +228 -0
- package/src/components/collapsible/index.ts +32 -0
- package/src/components/context-menu/AGENTS.md +103 -0
- package/src/components/context-menu/context-menu-content.tsx +64 -0
- package/src/components/context-menu/context-menu-item.tsx +22 -0
- package/src/components/context-menu/context-menu-preview.tsx +60 -0
- package/src/components/context-menu/context-menu-trigger.tsx +160 -0
- package/src/components/context-menu/context-menu.context.tsx +91 -0
- package/src/components/context-menu/context-menu.tsx +90 -0
- package/src/components/context-menu/context-menu.variants.test.ts +266 -0
- package/src/components/context-menu/context-menu.variants.ts +147 -0
- package/src/components/context-menu/index.ts +18 -0
- package/src/components/dialog/AGENTS.md +97 -0
- package/src/components/dialog/dialog-body.tsx +21 -0
- package/src/components/dialog/dialog-close.tsx +82 -0
- package/src/components/dialog/dialog-content.tsx +153 -0
- package/src/components/dialog/dialog-description.tsx +23 -0
- package/src/components/dialog/dialog-footer.tsx +32 -0
- package/src/components/dialog/dialog-header.tsx +20 -0
- package/src/components/dialog/dialog-title.tsx +42 -0
- package/src/components/dialog/dialog-trigger.tsx +54 -0
- package/src/components/dialog/dialog.context.tsx +90 -0
- package/src/components/dialog/dialog.tsx +119 -0
- package/src/components/dialog/dialog.variants.test.ts +224 -0
- package/src/components/dialog/dialog.variants.ts +173 -0
- package/src/components/dialog/index.ts +30 -0
- package/src/components/drawer/AGENTS.md +123 -0
- package/src/components/drawer/drawer-body.tsx +46 -0
- package/src/components/drawer/drawer-close.tsx +76 -0
- package/src/components/drawer/drawer-content.tsx +186 -0
- package/src/components/drawer/drawer-description.tsx +23 -0
- package/src/components/drawer/drawer-footer.tsx +21 -0
- package/src/components/drawer/drawer-header.tsx +42 -0
- package/src/components/drawer/drawer-title.tsx +35 -0
- package/src/components/drawer/drawer-trigger.tsx +54 -0
- package/src/components/drawer/drawer.context.tsx +114 -0
- package/src/components/drawer/drawer.tsx +119 -0
- package/src/components/drawer/drawer.variants.test.ts +392 -0
- package/src/components/drawer/drawer.variants.ts +244 -0
- package/src/components/drawer/index.ts +41 -0
- package/src/components/drawer/use-drawer-pan.ts +108 -0
- package/src/components/empty-state/AGENTS.md +90 -0
- package/src/components/empty-state/empty-state-content.tsx +17 -0
- package/src/components/empty-state/empty-state-description.tsx +12 -0
- package/src/components/empty-state/empty-state-header.tsx +17 -0
- package/src/components/empty-state/empty-state-media.tsx +56 -0
- package/src/components/empty-state/empty-state-title.tsx +18 -0
- package/src/components/empty-state/empty-state.context.tsx +65 -0
- package/src/components/empty-state/empty-state.tsx +84 -0
- package/src/components/empty-state/empty-state.types.ts +14 -0
- package/src/components/empty-state/empty-state.variants.test.ts +245 -0
- package/src/components/empty-state/empty-state.variants.ts +132 -0
- package/src/components/empty-state/index.ts +20 -0
- package/src/components/fab/AGENTS.md +119 -0
- package/src/components/fab/fab-action.tsx +118 -0
- package/src/components/fab/fab-group.tsx +214 -0
- package/src/components/fab/fab-label.tsx +18 -0
- package/src/components/fab/fab.context.tsx +125 -0
- package/src/components/fab/fab.tsx +156 -0
- package/src/components/fab/fab.types.ts +17 -0
- package/src/components/fab/fab.variants.test.ts +274 -0
- package/src/components/fab/fab.variants.ts +234 -0
- package/src/components/fab/index.ts +37 -0
- package/src/components/feedback/AGENTS.md +95 -0
- package/src/components/feedback/feedback-action.tsx +16 -0
- package/src/components/feedback/feedback-cancel.tsx +34 -0
- package/src/components/feedback/feedback-close.tsx +21 -0
- package/src/components/feedback/feedback-content.tsx +24 -0
- package/src/components/feedback/feedback-field.tsx +70 -0
- package/src/components/feedback/feedback-footer.tsx +21 -0
- package/src/components/feedback/feedback-panel.tsx +54 -0
- package/src/components/feedback/feedback-submit.tsx +74 -0
- package/src/components/feedback/feedback-title.tsx +34 -0
- package/src/components/feedback/feedback-trigger.tsx +20 -0
- package/src/components/feedback/feedback.context.tsx +73 -0
- package/src/components/feedback/feedback.tsx +135 -0
- package/src/components/feedback/feedback.variants.test.ts +185 -0
- package/src/components/feedback/feedback.variants.ts +128 -0
- package/src/components/feedback/index.ts +24 -0
- package/src/components/field/AGENTS.md +19 -0
- package/src/components/field/field-label.tsx +19 -5
- package/src/components/field/field.context.tsx +17 -0
- package/src/components/field/field.tsx +15 -2
- package/src/components/field/field.variants.test.ts +24 -0
- package/src/components/field/field.variants.ts +20 -0
- package/src/components/field/index.ts +1 -0
- package/src/components/item/AGENTS.md +92 -0
- package/src/components/item/index.ts +25 -0
- package/src/components/item/item-actions.tsx +28 -0
- package/src/components/item/item-content.tsx +15 -0
- package/src/components/item/item-description.tsx +12 -0
- package/src/components/item/item-footer.tsx +13 -0
- package/src/components/item/item-group.tsx +32 -0
- package/src/components/item/item-header.tsx +13 -0
- package/src/components/item/item-media.tsx +37 -0
- package/src/components/item/item-separator.tsx +13 -0
- package/src/components/item/item-title.tsx +17 -0
- package/src/components/item/item.context.tsx +66 -0
- package/src/components/item/item.tsx +218 -0
- package/src/components/item/item.types.ts +12 -0
- package/src/components/item/item.variants.test.ts +263 -0
- package/src/components/item/item.variants.ts +200 -0
- package/src/components/kpi/AGENTS.md +125 -0
- package/src/components/kpi/index.ts +45 -0
- package/src/components/kpi/kpi-action.tsx +12 -0
- package/src/components/kpi/kpi-content.tsx +35 -0
- package/src/components/kpi/kpi-footer.tsx +52 -0
- package/src/components/kpi/kpi-group.tsx +70 -0
- package/src/components/kpi/kpi-header.tsx +25 -0
- package/src/components/kpi/kpi-icon.tsx +28 -0
- package/src/components/kpi/kpi-sparkline.tsx +145 -0
- package/src/components/kpi/kpi-stat.tsx +20 -0
- package/src/components/kpi/kpi-title.tsx +19 -0
- package/src/components/kpi/kpi-trend.tsx +105 -0
- package/src/components/kpi/kpi-value.tsx +32 -0
- package/src/components/kpi/kpi.context.tsx +111 -0
- package/src/components/kpi/kpi.tsx +154 -0
- package/src/components/kpi/kpi.types.ts +9 -0
- package/src/components/kpi/kpi.variants.test.ts +428 -0
- package/src/components/kpi/kpi.variants.ts +347 -0
- package/src/components/label/AGENTS.md +89 -0
- package/src/components/label/index.ts +10 -0
- package/src/components/label/label.tsx +97 -0
- package/src/components/label/label.variants.test.ts +176 -0
- package/src/components/label/label.variants.ts +123 -0
- package/src/components/menu/AGENTS.md +176 -0
- package/src/components/menu/index.ts +44 -0
- package/src/components/menu/menu-background.tsx +17 -0
- package/src/components/menu/menu-checkbox-item.tsx +57 -0
- package/src/components/menu/menu-content.tsx +303 -0
- package/src/components/menu/menu-item.tsx +162 -0
- package/src/components/menu/menu-label.tsx +14 -0
- package/src/components/menu/menu-radio-group.tsx +50 -0
- package/src/components/menu/menu-radio-item.tsx +57 -0
- package/src/components/menu/menu-separator.tsx +11 -0
- package/src/components/menu/menu-sub-content.tsx +72 -0
- package/src/components/menu/menu-sub-trigger.tsx +50 -0
- package/src/components/menu/menu-sub.tsx +75 -0
- package/src/components/menu/menu-trigger.tsx +57 -0
- package/src/components/menu/menu.context.tsx +214 -0
- package/src/components/menu/menu.tsx +88 -0
- package/src/components/menu/menu.types.ts +33 -0
- package/src/components/menu/menu.variants.test.ts +400 -0
- package/src/components/menu/menu.variants.ts +353 -0
- package/src/components/meter/AGENTS.md +135 -0
- package/src/components/meter/index.ts +43 -0
- package/src/components/meter/meter-fill.tsx +19 -0
- package/src/components/meter/meter-header.tsx +17 -0
- package/src/components/meter/meter-label.tsx +18 -0
- package/src/components/meter/meter-output.tsx +42 -0
- package/src/components/meter/meter-segments.tsx +83 -0
- package/src/components/meter/meter-track.tsx +16 -0
- package/src/components/meter/meter.context.tsx +62 -0
- package/src/components/meter/meter.tsx +286 -0
- package/src/components/meter/meter.types.ts +30 -0
- package/src/components/meter/meter.variants.test.ts +430 -0
- package/src/components/meter/meter.variants.ts +347 -0
- package/src/components/overlay/AGENTS.md +89 -0
- package/src/components/overlay/index.ts +27 -0
- package/src/components/overlay/overlay-portal.tsx +84 -0
- package/src/components/overlay/overlay-presence.test.ts +46 -0
- package/src/components/overlay/overlay-presence.ts +49 -0
- package/src/components/overlay/overlay-registry.test.ts +96 -0
- package/src/components/overlay/overlay-registry.ts +99 -0
- package/src/components/overlay/overlay-scrim.tsx +49 -0
- package/src/components/overlay/overlay.context.tsx +48 -0
- package/src/components/overlay/overlay.tsx +118 -0
- package/src/components/overlay/overlay.variants.test.ts +37 -0
- package/src/components/overlay/overlay.variants.ts +33 -0
- package/src/components/overlay/use-overlay-back-handler.ts +34 -0
- package/src/components/overlay/use-overlay-presence.ts +87 -0
- package/src/components/popover/AGENTS.md +138 -0
- package/src/components/popover/index.ts +48 -0
- package/src/components/popover/popover-anchor.tsx +49 -0
- package/src/components/popover/popover-arrow.tsx +89 -0
- package/src/components/popover/popover-close.tsx +73 -0
- package/src/components/popover/popover-content.tsx +235 -0
- package/src/components/popover/popover-description.tsx +18 -0
- package/src/components/popover/popover-title.tsx +33 -0
- package/src/components/popover/popover-trigger.tsx +56 -0
- package/src/components/popover/popover.context.tsx +71 -0
- package/src/components/popover/popover.position.test.ts +354 -0
- package/src/components/popover/popover.position.ts +286 -0
- package/src/components/popover/popover.tsx +123 -0
- package/src/components/popover/popover.variants.test.ts +135 -0
- package/src/components/popover/popover.variants.ts +85 -0
- package/src/components/popover/use-anchor-measure.ts +76 -0
- package/src/components/popover/use-anchored-content.ts +219 -0
- package/src/components/pressable/AGENTS.md +11 -1
- package/src/components/pressable/pressable.tsx +7 -2
- package/src/components/pressable/pressable.variants.test.ts +29 -0
- package/src/components/pressable/pressable.variants.ts +17 -0
- package/src/components/progress/AGENTS.md +120 -0
- package/src/components/progress/index.ts +40 -0
- package/src/components/progress/progress-fill.tsx +152 -0
- package/src/components/progress/progress-header.tsx +19 -0
- package/src/components/progress/progress-label.tsx +27 -0
- package/src/components/progress/progress-output.tsx +47 -0
- package/src/components/progress/progress-track.tsx +38 -0
- package/src/components/progress/progress.context.tsx +79 -0
- package/src/components/progress/progress.tsx +171 -0
- package/src/components/progress/progress.types.ts +23 -0
- package/src/components/progress/progress.variants.test.ts +335 -0
- package/src/components/progress/progress.variants.ts +284 -0
- package/src/components/progress-button/AGENTS.md +116 -0
- package/src/components/progress-button/index.ts +36 -0
- package/src/components/progress-button/progress-button-done.tsx +33 -0
- package/src/components/progress-button/progress-button-label.tsx +27 -0
- package/src/components/progress-button/progress-button.context.tsx +93 -0
- package/src/components/progress-button/progress-button.tsx +414 -0
- package/src/components/progress-button/progress-button.variants.test.ts +314 -0
- package/src/components/progress-button/progress-button.variants.ts +236 -0
- package/src/components/provider/AGENTS.md +38 -25
- package/src/components/provider/provider.tsx +38 -11
- package/src/components/radio/AGENTS.md +8 -10
- package/src/components/radio/radio.variants.test.ts +5 -6
- package/src/components/radio/radio.variants.ts +6 -7
- package/src/components/rating/AGENTS.md +143 -0
- package/src/components/rating/index.ts +43 -0
- package/src/components/rating/rating-output.tsx +39 -0
- package/src/components/rating/rating-star.tsx +70 -0
- package/src/components/rating/rating-stars.tsx +219 -0
- package/src/components/rating/rating.context.tsx +79 -0
- package/src/components/rating/rating.tsx +163 -0
- package/src/components/rating/rating.variants.test.ts +347 -0
- package/src/components/rating/rating.variants.ts +354 -0
- package/src/components/screen/screen-navbar-back-button.tsx +1 -1
- package/src/components/selection-mode/AGENTS.md +155 -0
- package/src/components/selection-mode/index.ts +43 -0
- package/src/components/selection-mode/selection-mode-action.tsx +65 -0
- package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
- package/src/components/selection-mode/selection-mode-group.tsx +123 -0
- package/src/components/selection-mode/selection-mode-header.tsx +85 -0
- package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
- package/src/components/selection-mode/selection-mode-item.tsx +182 -0
- package/src/components/selection-mode/selection-mode.context.tsx +89 -0
- package/src/components/selection-mode/selection-mode.tsx +207 -0
- package/src/components/selection-mode/selection-mode.types.ts +36 -0
- package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
- package/src/components/selection-mode/selection-mode.variants.ts +329 -0
- package/src/components/skeleton/AGENTS.md +120 -0
- package/src/components/skeleton/index.ts +15 -0
- package/src/components/skeleton/skeleton-clock.ts +48 -0
- package/src/components/skeleton/skeleton-group.tsx +64 -0
- package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
- package/src/components/skeleton/skeleton.context.tsx +45 -0
- package/src/components/skeleton/skeleton.tsx +227 -0
- package/src/components/skeleton/skeleton.variants.test.ts +245 -0
- package/src/components/skeleton/skeleton.variants.ts +222 -0
- package/src/components/slide-button/AGENTS.md +122 -0
- package/src/components/slide-button/index.ts +43 -0
- package/src/components/slide-button/slide-button-label.tsx +70 -0
- package/src/components/slide-button/slide-button-thumb.tsx +93 -0
- package/src/components/slide-button/slide-button.context.tsx +76 -0
- package/src/components/slide-button/slide-button.tsx +457 -0
- package/src/components/slide-button/slide-button.variants.test.ts +288 -0
- package/src/components/slide-button/slide-button.variants.ts +324 -0
- package/src/components/slider/index.ts +2 -0
- package/src/components/slider/slider-thumb.tsx +33 -1
- package/src/components/slider/slider.variants.test.ts +52 -0
- package/src/components/slider/slider.variants.ts +56 -2
- package/src/components/stack-card/AGENTS.md +169 -0
- package/src/components/stack-card/index.ts +37 -0
- package/src/components/stack-card/stack-card-action.tsx +66 -0
- package/src/components/stack-card/stack-card-actions.tsx +18 -0
- package/src/components/stack-card/stack-card-card.tsx +71 -0
- package/src/components/stack-card/stack-card-empty.tsx +30 -0
- package/src/components/stack-card/stack-card-slot.tsx +90 -0
- package/src/components/stack-card/stack-card-stamp.tsx +76 -0
- package/src/components/stack-card/stack-card.context.tsx +117 -0
- package/src/components/stack-card/stack-card.tsx +557 -0
- package/src/components/stack-card/stack-card.types.ts +26 -0
- package/src/components/stack-card/stack-card.variants.test.ts +355 -0
- package/src/components/stack-card/stack-card.variants.ts +360 -0
- package/src/components/steps/AGENTS.md +153 -0
- package/src/components/steps/index.ts +45 -0
- package/src/components/steps/steps-connector.tsx +31 -0
- package/src/components/steps/steps-description.tsx +26 -0
- package/src/components/steps/steps-indicator.tsx +106 -0
- package/src/components/steps/steps-item.tsx +225 -0
- package/src/components/steps/steps-panel.tsx +41 -0
- package/src/components/steps/steps-title.tsx +32 -0
- package/src/components/steps/steps-track.tsx +59 -0
- package/src/components/steps/steps.context.tsx +123 -0
- package/src/components/steps/steps.tsx +166 -0
- package/src/components/steps/steps.variants.test.ts +388 -0
- package/src/components/steps/steps.variants.ts +468 -0
- package/src/components/surface/AGENTS.md +72 -0
- package/src/components/surface/index.ts +15 -0
- package/src/components/surface/surface.context.tsx +54 -0
- package/src/components/surface/surface.tsx +70 -0
- package/src/components/surface/surface.variants.test.ts +180 -0
- package/src/components/surface/surface.variants.ts +133 -0
- package/src/components/swipe/AGENTS.md +140 -0
- package/src/components/swipe/index.ts +31 -0
- package/src/components/swipe/swipe-action.tsx +99 -0
- package/src/components/swipe/swipe-end.tsx +14 -0
- package/src/components/swipe/swipe-group.tsx +55 -0
- package/src/components/swipe/swipe-panel.tsx +55 -0
- package/src/components/swipe/swipe-start.tsx +15 -0
- package/src/components/swipe/swipe.context.tsx +135 -0
- package/src/components/swipe/swipe.tsx +393 -0
- package/src/components/swipe/swipe.types.ts +7 -0
- package/src/components/swipe/swipe.variants.test.ts +337 -0
- package/src/components/swipe/swipe.variants.ts +290 -0
- package/src/components/switch/switch.variants.ts +2 -2
- package/src/components/tabs/AGENTS.md +4 -3
- package/src/components/tabs/tabs.variants.test.ts +1 -1
- package/src/components/tabs/tabs.variants.ts +4 -6
- package/src/components/textarea/AGENTS.md +86 -0
- package/src/components/textarea/index.ts +16 -0
- package/src/components/textarea/textarea.tsx +157 -0
- package/src/components/textarea/textarea.variants.test.ts +219 -0
- package/src/components/textarea/textarea.variants.ts +171 -0
- package/src/components/toast/AGENTS.md +140 -0
- package/src/components/toast/index.ts +74 -0
- package/src/components/toast/toast-action.tsx +59 -0
- package/src/components/toast/toast-api.ts +32 -0
- package/src/components/toast/toast-close.tsx +44 -0
- package/src/components/toast/toast-content.tsx +11 -0
- package/src/components/toast/toast-description.tsx +11 -0
- package/src/components/toast/toast-indicator.tsx +34 -0
- package/src/components/toast/toast-item.tsx +308 -0
- package/src/components/toast/toast-title.tsx +13 -0
- package/src/components/toast/toast-viewport.tsx +135 -0
- package/src/components/toast/toast.context.tsx +69 -0
- package/src/components/toast/toast.store.test.ts +259 -0
- package/src/components/toast/toast.store.ts +338 -0
- package/src/components/toast/toast.timer.test.ts +64 -0
- package/src/components/toast/toast.timer.ts +51 -0
- package/src/components/toast/toast.tsx +132 -0
- package/src/components/toast/toast.variants.test.ts +393 -0
- package/src/components/toast/toast.variants.ts +304 -0
- package/src/components/toggle-button/AGENTS.md +106 -0
- package/src/components/toggle-button/index.ts +30 -0
- package/src/components/toggle-button/toggle-button-group.tsx +156 -0
- package/src/components/toggle-button/toggle-button-label.tsx +19 -0
- package/src/components/toggle-button/toggle-button.context.tsx +130 -0
- package/src/components/toggle-button/toggle-button.tsx +172 -0
- package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
- package/src/components/toggle-button/toggle-button.variants.ts +160 -0
- package/src/components/tooltip/AGENTS.md +133 -0
- package/src/components/tooltip/index.ts +28 -0
- package/src/components/tooltip/tooltip-arrow.tsx +40 -0
- package/src/components/tooltip/tooltip-content.tsx +179 -0
- package/src/components/tooltip/tooltip-description.tsx +19 -0
- package/src/components/tooltip/tooltip-text.tsx +21 -0
- package/src/components/tooltip/tooltip-title.tsx +20 -0
- package/src/components/tooltip/tooltip-trigger.tsx +103 -0
- package/src/components/tooltip/tooltip.context.tsx +68 -0
- package/src/components/tooltip/tooltip.tsx +213 -0
- package/src/components/tooltip/tooltip.variants.test.ts +231 -0
- package/src/components/tooltip/tooltip.variants.ts +177 -0
- package/src/hooks/use-calm-motion.tsx +31 -0
- package/src/lib/calm-motion.test.ts +17 -0
- package/src/lib/calm-motion.ts +23 -0
- package/src/lib/cn.test.ts +14 -0
- package/src/lib/cn.ts +2 -2
- package/src/no-gorhom.test.ts +54 -0
- package/src/styles/geometry.test.ts +1 -0
- package/src/styles/theme.css +4 -0
- package/src/styles/tokens.css +11 -0
- package/src/styles/tokens.test.ts +22 -0
- package/src/styles/tokens.ts +20 -0
- package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
- package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isValidElement, type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { useCardPart } from "./card.context";
|
|
4
|
+
import type { CardSlotProps } from "./card.types";
|
|
5
|
+
import { cardVariants, splitCardHeaderChildren } from "./card.variants";
|
|
6
|
+
import { CardAction } from "./card-action";
|
|
7
|
+
import { CardTitle } from "./card-title";
|
|
8
|
+
|
|
9
|
+
function isCardAction(node: ReactNode): boolean {
|
|
10
|
+
return isValidElement(node) && node.type === CardAction;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wraps a bare string or number in a `Card.Title`. A raw string inside a `View`
|
|
15
|
+
* is a red box in React Native, and a header's bare text is its title.
|
|
16
|
+
*/
|
|
17
|
+
function wrapBareText(node: ReactNode, index: number): ReactNode {
|
|
18
|
+
return typeof node === "string" || typeof node === "number" ? (
|
|
19
|
+
<CardTitle key={`title-${index}`}>{node}</CardTitle>
|
|
20
|
+
) : (
|
|
21
|
+
node
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The card's opening block: a title and description stacked on the left, and
|
|
27
|
+
* any `Card.Action` pinned to the top right.
|
|
28
|
+
*
|
|
29
|
+
* The header lifts every action out of the column wherever it is written, so
|
|
30
|
+
* the order of children does not matter and the text wraps in the width the
|
|
31
|
+
* action leaves. Bare text becomes a title.
|
|
32
|
+
*/
|
|
33
|
+
export function CardHeader({ className, children, ...props }: CardSlotProps): ReactElement {
|
|
34
|
+
const { size } = useCardPart("Card.Header");
|
|
35
|
+
const slots = cardVariants({ size });
|
|
36
|
+
const { text, actions } = splitCardHeaderChildren(children, isCardAction);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<View className={slots.header({ className })} {...props}>
|
|
40
|
+
<View className={slots.headerText()}>{text.map(wrapBareText)}</View>
|
|
41
|
+
{actions}
|
|
42
|
+
</View>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
CardHeader.displayName = "DelacourUI.Card.Header";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Text } from "../text";
|
|
3
|
+
import { useCardPart } from "./card.context";
|
|
4
|
+
import type { CardTextProps } from "./card.types";
|
|
5
|
+
import { cardVariants } from "./card.variants";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The card's heading.
|
|
9
|
+
*
|
|
10
|
+
* Carries its own colour — the foreground token of the plane the card resolved
|
|
11
|
+
* to — and its own step on the type scale, read from the card's context: a
|
|
12
|
+
* React Native `View` does not cascade colour to a `Text` descendant. Announced
|
|
13
|
+
* as a header, so a screen reader's rotor stops on each card in a list.
|
|
14
|
+
*/
|
|
15
|
+
export function CardTitle({ className, ...props }: CardTextProps): ReactElement {
|
|
16
|
+
const { size, plane } = useCardPart("Card.Title");
|
|
17
|
+
return (
|
|
18
|
+
<Text
|
|
19
|
+
accessibilityRole="header"
|
|
20
|
+
className={cardVariants({ plane: plane ?? "none", size }).title({ className })}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
CardTitle.displayName = "DelacourUI.Card.Title";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createContext, type ReactElement, type ReactNode, use } from "react";
|
|
2
|
+
import type { SurfacePlane, SurfaceVariant } from "../surface/surface.variants";
|
|
3
|
+
import type { CardSize } from "./card.variants";
|
|
4
|
+
|
|
5
|
+
export type CardContextValue = {
|
|
6
|
+
/** Size of the card — the padding, the gaps and the title and description scale. */
|
|
7
|
+
size: CardSize;
|
|
8
|
+
/** The fill the card resolved to — explicit, or stepped from the surface it sits in. */
|
|
9
|
+
variant: SurfaceVariant;
|
|
10
|
+
/**
|
|
11
|
+
* The fill the card's content sits on. The same as `variant` unless that is
|
|
12
|
+
* `transparent`, which passes the plane beneath it through — `null` on the page.
|
|
13
|
+
*/
|
|
14
|
+
plane: SurfacePlane | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const CardContext = createContext<CardContextValue | null>(null);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Supplies the enclosing card's size and fill to its subtree.
|
|
21
|
+
*
|
|
22
|
+
* Lives in its own module, importing nothing but types, so a part the card
|
|
23
|
+
* renders can read it without importing `./card` and closing a cycle through
|
|
24
|
+
* `card.tsx`. See AGENTS.md rule 3.
|
|
25
|
+
*/
|
|
26
|
+
export function CardProvider({ value, children }: { value: CardContextValue; children: ReactNode }): ReactElement {
|
|
27
|
+
return <CardContext value={value}>{children}</CardContext>;
|
|
28
|
+
}
|
|
29
|
+
CardProvider.displayName = "DelacourUI.Card.Provider";
|
|
30
|
+
|
|
31
|
+
/** The enclosing card's context, or null outside a `<Card>`. */
|
|
32
|
+
export function useCardContext(): CardContextValue | null {
|
|
33
|
+
return use(CardContext);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reads the enclosing card's size and fill.
|
|
38
|
+
*
|
|
39
|
+
* Lets a custom part match the card — the same inset, a colour that reads on
|
|
40
|
+
* its plane — without the card passing props down. Throws outside a `<Card>`;
|
|
41
|
+
* use {@link useCardContext} where the enclosing card is optional.
|
|
42
|
+
*/
|
|
43
|
+
export function useCard(): CardContextValue {
|
|
44
|
+
const context = useCardContext();
|
|
45
|
+
if (!context) {
|
|
46
|
+
throw new Error("useCard must be called inside a <Card>.");
|
|
47
|
+
}
|
|
48
|
+
return context;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The enclosing card's context, for a compound part that cannot work without one.
|
|
53
|
+
*
|
|
54
|
+
* Internal: deliberately not re-exported from `index.ts`. A caller outside the
|
|
55
|
+
* library wants {@link useCard}, whose error message names the hook rather than
|
|
56
|
+
* a part.
|
|
57
|
+
*/
|
|
58
|
+
export function useCardPart(component: string): CardContextValue {
|
|
59
|
+
const context = useCardContext();
|
|
60
|
+
if (!context) {
|
|
61
|
+
throw new Error(`${component} must be rendered inside a <Card>.`);
|
|
62
|
+
}
|
|
63
|
+
return context;
|
|
64
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { type ReactElement, useMemo } from "react";
|
|
2
|
+
import { Surface, type SurfaceProps, useSurfaceContext } from "../surface";
|
|
3
|
+
import { resolveSurfacePlane, resolveSurfaceVariant } from "../surface/surface.variants";
|
|
4
|
+
import { type CardContextValue, CardProvider } from "./card.context";
|
|
5
|
+
import { type CardSize, cardVariants } from "./card.variants";
|
|
6
|
+
import { CardAction } from "./card-action";
|
|
7
|
+
import { CardContent } from "./card-content";
|
|
8
|
+
import { CardDescription } from "./card-description";
|
|
9
|
+
import { CardFooter } from "./card-footer";
|
|
10
|
+
import { CardHeader } from "./card-header";
|
|
11
|
+
import { CardTitle } from "./card-title";
|
|
12
|
+
|
|
13
|
+
export type CardProps = Omit<SurfaceProps, "padding"> & {
|
|
14
|
+
/** The inset, the gaps between the parts, and the title and description scale. */
|
|
15
|
+
size?: CardSize;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function CardRoot({ variant, size = "md", className, children, ...props }: CardProps): ReactElement {
|
|
19
|
+
const parentPlane = useSurfaceContext()?.plane ?? null;
|
|
20
|
+
|
|
21
|
+
// Resolved here as well as inside `Surface`, because the parts need the plane
|
|
22
|
+
// the card landed on — the title's colour and a band footer's fill key off it.
|
|
23
|
+
const resolved = resolveSurfaceVariant({ parentPlane, variant });
|
|
24
|
+
const plane = resolveSurfacePlane({ parentPlane, variant: resolved });
|
|
25
|
+
|
|
26
|
+
const context = useMemo<CardContextValue>(() => ({ plane, size, variant: resolved }), [plane, size, resolved]);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<CardProvider value={context}>
|
|
30
|
+
<Surface className={cardVariants({ size }).root({ className })} padding="none" variant={resolved} {...props}>
|
|
31
|
+
{children}
|
|
32
|
+
</Surface>
|
|
33
|
+
</CardProvider>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A content surface with a header, a body and a footer.
|
|
39
|
+
*
|
|
40
|
+
* Built on `Surface`, so it takes the same four fills and steps the same way:
|
|
41
|
+
* a card that names no variant is the hairlined `default` at the top of a
|
|
42
|
+
* screen and the next fill down inside another surface. The root is
|
|
43
|
+
* `padding="none"`, which clips — the padding lives on the parts, so an image
|
|
44
|
+
* placed straight in the card reaches its side edges, and `className="pt-0"`
|
|
45
|
+
* bleeds it to the top.
|
|
46
|
+
*
|
|
47
|
+
* `size` reaches every part through context, so the header, the body and the
|
|
48
|
+
* footer line up on one inset and the title picks its own scale and colour —
|
|
49
|
+
* a React Native `View` cannot cascade either to a `Text`.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* <Card>
|
|
53
|
+
* <Card.Header>
|
|
54
|
+
* <Card.Title>Monthly report</Card.Title>
|
|
55
|
+
* <Card.Description>Revenue and retention for October.</Card.Description>
|
|
56
|
+
* <Card.Action>
|
|
57
|
+
* <Badge>New</Badge>
|
|
58
|
+
* </Card.Action>
|
|
59
|
+
* </Card.Header>
|
|
60
|
+
* <Card.Content>
|
|
61
|
+
* <Text.Title>$48,120</Text.Title>
|
|
62
|
+
* </Card.Content>
|
|
63
|
+
* <Card.Footer>
|
|
64
|
+
* <Button size="sm" variant="outline">Export</Button>
|
|
65
|
+
* <Button size="sm">Open</Button>
|
|
66
|
+
* </Card.Footer>
|
|
67
|
+
* </Card>
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* <Card className="pt-0">
|
|
71
|
+
* <Image className="h-40 w-full" source={cover} />
|
|
72
|
+
* <Card.Header>
|
|
73
|
+
* <Card.Title>Lake Tekapo</Card.Title>
|
|
74
|
+
* </Card.Header>
|
|
75
|
+
* <Card.Footer variant="band">
|
|
76
|
+
* <Button size="sm">Book</Button>
|
|
77
|
+
* </Card.Footer>
|
|
78
|
+
* </Card>
|
|
79
|
+
*/
|
|
80
|
+
export const Card = Object.assign(CardRoot, {
|
|
81
|
+
/** The opening block. Stacks a title and description, and pins any `Card.Action` to the top right. */
|
|
82
|
+
Header: CardHeader,
|
|
83
|
+
/** The card's heading, on its plane's foreground token. Announced as a header. */
|
|
84
|
+
Title: CardTitle,
|
|
85
|
+
/** The muted supporting line under the title. */
|
|
86
|
+
Description: CardDescription,
|
|
87
|
+
/** A control pinned to the header's top-right corner, wherever in the header it is written. */
|
|
88
|
+
Action: CardAction,
|
|
89
|
+
/** The card's body, inset to line up with the header and footer. */
|
|
90
|
+
Content: CardContent,
|
|
91
|
+
/** The last row — a row of actions, or with `variant="band"` a strip set into the card. */
|
|
92
|
+
Footer: CardFooter,
|
|
93
|
+
displayName: "DelacourUI.Card",
|
|
94
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ViewProps } from "react-native";
|
|
3
|
+
import type { TextPresetProps } from "../text";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The shape of a card's layout parts — `Header`, `Action` and `Content`, and
|
|
7
|
+
* extended by `Footer` — so it lives in a leaf rather than in one of them
|
|
8
|
+
* arbitrarily.
|
|
9
|
+
*/
|
|
10
|
+
export type CardSlotProps = ViewProps & { className?: string; children?: ReactNode };
|
|
11
|
+
|
|
12
|
+
/** The shape of a card's two text parts, `Title` and `Description`. */
|
|
13
|
+
export type CardTextProps = TextPresetProps;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createElement, Fragment, type ReactNode } from "react";
|
|
3
|
+
import { SURFACE_FILLED_VARIANTS } from "../surface/surface.variants";
|
|
4
|
+
import {
|
|
5
|
+
CARD_FOOTER_VARIANTS,
|
|
6
|
+
CARD_PLANES,
|
|
7
|
+
CARD_SIZES,
|
|
8
|
+
cardVariants,
|
|
9
|
+
resolveCardFooterFill,
|
|
10
|
+
splitCardHeaderChildren,
|
|
11
|
+
} from "./card.variants";
|
|
12
|
+
|
|
13
|
+
/** Every slot the variant function can produce, one call per combination. */
|
|
14
|
+
function everyCombination(): ReturnType<typeof cardVariants>[] {
|
|
15
|
+
return CARD_SIZES.flatMap((size) =>
|
|
16
|
+
CARD_PLANES.flatMap((plane) =>
|
|
17
|
+
CARD_FOOTER_VARIANTS.flatMap((footer) =>
|
|
18
|
+
SURFACE_FILLED_VARIANTS.map((footerFill) => cardVariants({ footer, footerFill, plane, size }))
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The numeric step of the first `${prefix}-N` utility in a class string. */
|
|
25
|
+
function step(cls: string, prefix: string): string | undefined {
|
|
26
|
+
return cls.match(new RegExp(`(?:^|\\s)${prefix}-(\\d+(?:\\.5)?)(?:\\s|$)`))?.[1];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("cardVariants root slot", () => {
|
|
30
|
+
// The padding lives on the parts, so the root carries only the vertical
|
|
31
|
+
// rhythm between them — horizontal padding here would stop a bled image
|
|
32
|
+
// reaching the card's side edges.
|
|
33
|
+
test("carries no horizontal padding at any size", () => {
|
|
34
|
+
for (const size of CARD_SIZES) {
|
|
35
|
+
const cls = cardVariants({ size }).root();
|
|
36
|
+
expect(cls).not.toMatch(/(^|\s)(p|px|pl|pr)-/);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("spaces its parts by the same step it pads its top and bottom", () => {
|
|
41
|
+
for (const size of CARD_SIZES) {
|
|
42
|
+
const cls = cardVariants({ size }).root();
|
|
43
|
+
expect(step(cls, "pt")).toBeDefined();
|
|
44
|
+
expect(step(cls, "pb")).toBe(step(cls, "pt"));
|
|
45
|
+
expect(step(cls, "gap")).toBe(step(cls, "pt"));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// The card sits on a `padding="none"` surface, whose root carries `p-0`.
|
|
50
|
+
// Uniwind resolves that shorthand over a `py-*` on the same view, so the
|
|
51
|
+
// card's vertical padding vanished on a device; the longhands hold.
|
|
52
|
+
test("pads top and bottom with longhands, never the py shorthand", () => {
|
|
53
|
+
for (const size of CARD_SIZES) {
|
|
54
|
+
expect(cardVariants({ size }).root()).not.toMatch(/(^|\s)py-/);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Rule 1: a React Native View does not cascade colour to a Text descendant.
|
|
59
|
+
test("carries no text treatment on the root, header, content or footer", () => {
|
|
60
|
+
for (const slots of everyCombination()) {
|
|
61
|
+
for (const cls of [slots.root(), slots.header(), slots.content(), slots.footer()]) {
|
|
62
|
+
expect(cls).not.toMatch(/(^|\s)text-/);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("merges an incoming className last", () => {
|
|
68
|
+
expect(cardVariants().root({ className: "pt-0" })).toContain("pt-0");
|
|
69
|
+
expect(cardVariants().root({ className: "gap-8" })).not.toMatch(/(^|\s)gap-4(\s|$)/);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("cardVariants size", () => {
|
|
74
|
+
test("defaults to md", () => {
|
|
75
|
+
expect(cardVariants().root()).toBe(cardVariants({ size: "md" }).root());
|
|
76
|
+
expect(cardVariants().title()).toBe(cardVariants({ size: "md" }).title());
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("matches Surface's padding steps: 3, 4 and 6", () => {
|
|
80
|
+
expect(step(cardVariants({ size: "sm" }).root(), "pt")).toBe("3");
|
|
81
|
+
expect(step(cardVariants({ size: "md" }).root(), "pt")).toBe("4");
|
|
82
|
+
expect(step(cardVariants({ size: "lg" }).root(), "pt")).toBe("6");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// The header, the body and the footer have to line up on one inset, or the
|
|
86
|
+
// title sits a few points off the button beneath it.
|
|
87
|
+
test("header, content and footer share one horizontal inset, equal to the vertical one", () => {
|
|
88
|
+
for (const size of CARD_SIZES) {
|
|
89
|
+
const slots = cardVariants({ size });
|
|
90
|
+
const inset = step(slots.root(), "pt");
|
|
91
|
+
expect(step(slots.header(), "px")).toBe(inset);
|
|
92
|
+
expect(step(slots.content(), "px")).toBe(inset);
|
|
93
|
+
expect(step(slots.footer(), "px")).toBe(inset);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("steps the title and description type scale with the size", () => {
|
|
98
|
+
expect(cardVariants({ size: "sm" }).title()).toContain("text-sm");
|
|
99
|
+
expect(cardVariants({ size: "md" }).title()).toContain("text-base");
|
|
100
|
+
expect(cardVariants({ size: "lg" }).title()).toContain("text-lg");
|
|
101
|
+
expect(cardVariants({ size: "sm" }).description()).toContain("text-xs");
|
|
102
|
+
expect(cardVariants({ size: "md" }).description()).toContain("text-sm");
|
|
103
|
+
expect(cardVariants({ size: "lg" }).description()).toContain("text-base");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("gives every size a distinct root", () => {
|
|
107
|
+
const seen = new Set(CARD_SIZES.map((size) => cardVariants({ size }).root()));
|
|
108
|
+
expect(seen.size).toBe(CARD_SIZES.length);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("cardVariants title and description", () => {
|
|
113
|
+
test("the title is the X-foreground token of the plane it sits on", () => {
|
|
114
|
+
expect(cardVariants({ plane: "default" }).title()).toContain("text-card-foreground");
|
|
115
|
+
expect(cardVariants({ plane: "secondary" }).title()).toContain("text-secondary-foreground");
|
|
116
|
+
expect(cardVariants({ plane: "tertiary" }).title()).toContain("text-tertiary-foreground");
|
|
117
|
+
expect(cardVariants({ plane: "none" }).title()).toContain("text-foreground");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("the title is semibold at every size", () => {
|
|
121
|
+
for (const size of CARD_SIZES) {
|
|
122
|
+
expect(cardVariants({ size }).title()).toContain("font-semibold");
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("the description sits on the muted token on every plane", () => {
|
|
127
|
+
for (const plane of CARD_PLANES) {
|
|
128
|
+
expect(cardVariants({ plane }).description()).toContain("text-muted-foreground");
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Two colour classes on one Text and tailwind-merge keeps only the last —
|
|
133
|
+
// which would be a page colour, not the plane's.
|
|
134
|
+
test("the title carries exactly one text colour", () => {
|
|
135
|
+
for (const slots of everyCombination()) {
|
|
136
|
+
const colours = slots
|
|
137
|
+
.title()
|
|
138
|
+
.split(/\s+/)
|
|
139
|
+
.filter((name) => /^text-(card|secondary|tertiary|foreground|muted)/.test(name));
|
|
140
|
+
expect(colours).toHaveLength(1);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("cardVariants header", () => {
|
|
146
|
+
test("lays the text column and the action out in a row", () => {
|
|
147
|
+
const slots = cardVariants();
|
|
148
|
+
expect(slots.header()).toContain("flex-row");
|
|
149
|
+
expect(slots.headerText()).toContain("flex-1");
|
|
150
|
+
expect(slots.action()).toContain("shrink-0");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("cardVariants footer", () => {
|
|
155
|
+
test("defaults to the plain row of actions", () => {
|
|
156
|
+
expect(cardVariants().footer()).toBe(cardVariants({ footer: "default" }).footer());
|
|
157
|
+
expect(cardVariants().footer()).toContain("flex-row");
|
|
158
|
+
expect(cardVariants().footer()).not.toContain("border-t");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("a band draws a rule across its top and a fill of its own", () => {
|
|
162
|
+
for (const footerFill of SURFACE_FILLED_VARIANTS) {
|
|
163
|
+
const cls = cardVariants({ footer: "band", footerFill }).footer();
|
|
164
|
+
expect(cls).toContain("border-t");
|
|
165
|
+
expect(cls).toContain("border-border");
|
|
166
|
+
expect(cls).toMatch(/(^|\s)bg-/);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("only a band paints a fill", () => {
|
|
171
|
+
for (const footerFill of SURFACE_FILLED_VARIANTS) {
|
|
172
|
+
expect(cardVariants({ footer: "default", footerFill }).footer()).not.toMatch(/(^|\s)bg-/);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("maps each band fill to its surface token", () => {
|
|
177
|
+
expect(cardVariants({ footer: "band", footerFill: "default" }).footer()).toContain("bg-card");
|
|
178
|
+
expect(cardVariants({ footer: "band", footerFill: "secondary" }).footer()).toContain("bg-secondary");
|
|
179
|
+
expect(cardVariants({ footer: "band", footerFill: "tertiary" }).footer()).toContain("bg-tertiary");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// A band is set INTO the card: it pulls down over the root's bottom padding
|
|
183
|
+
// by exactly that padding, and restates it inside, so it meets the card's
|
|
184
|
+
// bottom edge and its own content keeps the same breathing room.
|
|
185
|
+
test("a band cancels the root's bottom padding and restates it inside", () => {
|
|
186
|
+
for (const size of CARD_SIZES) {
|
|
187
|
+
const inset = step(cardVariants({ size }).root(), "pb");
|
|
188
|
+
const cls = cardVariants({ footer: "band", size }).footer();
|
|
189
|
+
expect(cls).toMatch(new RegExp(`(^|\\s)-mb-${inset?.replace(".", "\\.")}(\\s|$)`));
|
|
190
|
+
expect(step(cls, "py")).toBe(inset);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("resolveCardFooterFill", () => {
|
|
196
|
+
test("a band is a step from the card's own fill, never the same one", () => {
|
|
197
|
+
for (const plane of SURFACE_FILLED_VARIANTS) {
|
|
198
|
+
expect(resolveCardFooterFill(plane)).not.toBe(plane);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("a card on the default fill gets a secondary band", () => {
|
|
203
|
+
expect(resolveCardFooterFill("default")).toBe("secondary");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// A transparent card on the page paints nothing, so its band steps from the
|
|
207
|
+
// page itself — the card fill, a step above it.
|
|
208
|
+
test("a card with no plane of its own gets a default band", () => {
|
|
209
|
+
expect(resolveCardFooterFill(null)).toBe("default");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("splitCardHeaderChildren", () => {
|
|
214
|
+
function Action(): null {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
const isAction = (node: ReactNode): boolean =>
|
|
218
|
+
typeof node === "object" && node !== null && "type" in node && node.type === Action;
|
|
219
|
+
|
|
220
|
+
test("puts everything in the text column when there is no action", () => {
|
|
221
|
+
const { text, actions } = splitCardHeaderChildren(["Title", createElement(Fragment, null, "x")], isAction);
|
|
222
|
+
expect(text).toHaveLength(2);
|
|
223
|
+
expect(actions).toHaveLength(0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("lifts every action out of the text column, keeping order", () => {
|
|
227
|
+
const first = createElement(Action, { key: "a" });
|
|
228
|
+
const second = createElement(Action, { key: "b" });
|
|
229
|
+
const { text, actions } = splitCardHeaderChildren([first, "Title", second, "Description"], isAction);
|
|
230
|
+
expect(text).toEqual(["Title", "Description"]);
|
|
231
|
+
expect(actions).toHaveLength(2);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// A conditional action leaves `false` behind; it must not count as text.
|
|
235
|
+
test("drops the nulls and booleans a conditional child leaves behind", () => {
|
|
236
|
+
const { text, actions } = splitCardHeaderChildren(["Title", false, null, undefined], isAction);
|
|
237
|
+
expect(text).toEqual(["Title"]);
|
|
238
|
+
expect(actions).toHaveLength(0);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Children, type ReactNode } from "react";
|
|
2
|
+
import type { VariantProps } from "tailwind-variants";
|
|
3
|
+
import { tv } from "../../lib/tv";
|
|
4
|
+
import { resolveSurfaceVariant, type SurfacePlane } from "../surface/surface.variants";
|
|
5
|
+
|
|
6
|
+
export const CARD_SIZES = ["sm", "md", "lg"] as const;
|
|
7
|
+
|
|
8
|
+
/** `default` is a row of actions; `band` sets the footer into the card as a strip of its own. */
|
|
9
|
+
export const CARD_FOOTER_VARIANTS = ["default", "band"] as const;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The planes a card's title can sit on: the three surface fills, plus `none`
|
|
13
|
+
* for a transparent card on the page, which has no fill of its own.
|
|
14
|
+
*/
|
|
15
|
+
export const CARD_PLANES = ["default", "secondary", "tertiary", "none"] as const;
|
|
16
|
+
|
|
17
|
+
export type CardSize = (typeof CARD_SIZES)[number];
|
|
18
|
+
export type CardFooterVariant = (typeof CARD_FOOTER_VARIANTS)[number];
|
|
19
|
+
export type CardPlane = (typeof CARD_PLANES)[number];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Styling for every part of a card.
|
|
23
|
+
*
|
|
24
|
+
* The card's own surface — fill, hairline, corner, clip — is `Surface`'s, at
|
|
25
|
+
* `padding="none"`. What this adds is the rhythm between the parts, and it
|
|
26
|
+
* puts the padding on the PARTS rather than on the root. The root holds only
|
|
27
|
+
* `pt`/`pb` and a `gap` of the same step; the header, content and footer each
|
|
28
|
+
* carry the same step as `px`. So an image placed straight in the card reaches
|
|
29
|
+
* both side edges with nothing to undo, and `className="pt-0"` on the card is
|
|
30
|
+
* all it takes to bleed one to the top as well.
|
|
31
|
+
*
|
|
32
|
+
* `pt`/`pb` rather than `py`, and that is load-bearing: the root is also the
|
|
33
|
+
* surface's, which carries `p-0` at `padding="none"`, and Uniwind resolves that
|
|
34
|
+
* shorthand over a `py-*` on the same view. The card rendered with no vertical
|
|
35
|
+
* padding at all until the longhands replaced it.
|
|
36
|
+
*
|
|
37
|
+
* Size is one axis for five numbers — the vertical padding, the gap, the
|
|
38
|
+
* horizontal inset and the title and description type scale — and the steps
|
|
39
|
+
* are Surface's own `sm`/`md`/`lg` (3, 4, 6), so a card and a surface beside
|
|
40
|
+
* it at the same size hold their content at the same distance from the edge.
|
|
41
|
+
*
|
|
42
|
+
* The title colour is keyed on `plane`, the fill the card resolved to, so a
|
|
43
|
+
* card nested in another reads `text-secondary-foreground` on the secondary
|
|
44
|
+
* fill rather than the card's token on something else. The classes are written
|
|
45
|
+
* out per plane because Tailwind's scanner cannot see a `text-${token}` built at
|
|
46
|
+
* runtime.
|
|
47
|
+
*
|
|
48
|
+
* A `band` footer is set INTO the card: `-mb-*` pulls it down over the root's
|
|
49
|
+
* bottom padding by exactly that padding and `py-*` restates it inside, so it
|
|
50
|
+
* meets the card's bottom edge — the root clips, so it takes the card's own
|
|
51
|
+
* corners — while its content keeps the same breathing room. Its fill is the
|
|
52
|
+
* next rung of the surface ladder from the card's, see
|
|
53
|
+
* {@link resolveCardFooterFill}.
|
|
54
|
+
*
|
|
55
|
+
* The root, header, content and footer carry no `text-*` (rule 1): a `View`
|
|
56
|
+
* does not cascade colour to a `Text`, so the tokens live on the title and
|
|
57
|
+
* description slots.
|
|
58
|
+
*
|
|
59
|
+
* Free of React Native imports so it stays unit-testable — `bun test` cannot
|
|
60
|
+
* parse React Native's Flow-typed source. See AGENTS.md.
|
|
61
|
+
*/
|
|
62
|
+
export const cardVariants = tv({
|
|
63
|
+
slots: {
|
|
64
|
+
root: "",
|
|
65
|
+
header: "flex-row items-start gap-3",
|
|
66
|
+
/** The column holding the title and description, beside any action. */
|
|
67
|
+
headerText: "flex-1",
|
|
68
|
+
action: "shrink-0 flex-row items-center self-start",
|
|
69
|
+
title: "font-semibold",
|
|
70
|
+
description: "text-muted-foreground",
|
|
71
|
+
content: "",
|
|
72
|
+
footer: "flex-row flex-wrap items-center gap-2",
|
|
73
|
+
},
|
|
74
|
+
variants: {
|
|
75
|
+
size: {
|
|
76
|
+
sm: {
|
|
77
|
+
root: "gap-3 pt-3 pb-3",
|
|
78
|
+
header: "px-3",
|
|
79
|
+
headerText: "gap-0.5",
|
|
80
|
+
content: "gap-2 px-3",
|
|
81
|
+
footer: "px-3",
|
|
82
|
+
title: "text-sm",
|
|
83
|
+
description: "text-xs",
|
|
84
|
+
},
|
|
85
|
+
md: {
|
|
86
|
+
root: "gap-4 pt-4 pb-4",
|
|
87
|
+
header: "px-4",
|
|
88
|
+
headerText: "gap-1",
|
|
89
|
+
content: "gap-3 px-4",
|
|
90
|
+
footer: "px-4",
|
|
91
|
+
title: "text-base",
|
|
92
|
+
description: "text-sm",
|
|
93
|
+
},
|
|
94
|
+
lg: {
|
|
95
|
+
root: "gap-6 pt-6 pb-6",
|
|
96
|
+
header: "px-6",
|
|
97
|
+
headerText: "gap-1.5",
|
|
98
|
+
content: "gap-4 px-6",
|
|
99
|
+
footer: "px-6",
|
|
100
|
+
title: "text-lg",
|
|
101
|
+
description: "text-base",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
plane: {
|
|
105
|
+
default: { title: "text-card-foreground" },
|
|
106
|
+
secondary: { title: "text-secondary-foreground" },
|
|
107
|
+
tertiary: { title: "text-tertiary-foreground" },
|
|
108
|
+
none: { title: "text-foreground" },
|
|
109
|
+
},
|
|
110
|
+
footer: {
|
|
111
|
+
default: {},
|
|
112
|
+
band: { footer: "border-t border-border" },
|
|
113
|
+
},
|
|
114
|
+
// Read only by a band; the plain footer paints nothing whatever this is.
|
|
115
|
+
footerFill: {
|
|
116
|
+
default: {},
|
|
117
|
+
secondary: {},
|
|
118
|
+
tertiary: {},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
compoundVariants: [
|
|
122
|
+
{ footer: "band", size: "sm", class: { footer: "-mb-3 py-3" } },
|
|
123
|
+
{ footer: "band", size: "md", class: { footer: "-mb-4 py-4" } },
|
|
124
|
+
{ footer: "band", size: "lg", class: { footer: "-mb-6 py-6" } },
|
|
125
|
+
{ footer: "band", footerFill: "default", class: { footer: "bg-card" } },
|
|
126
|
+
{ footer: "band", footerFill: "secondary", class: { footer: "bg-secondary" } },
|
|
127
|
+
{ footer: "band", footerFill: "tertiary", class: { footer: "bg-tertiary" } },
|
|
128
|
+
],
|
|
129
|
+
defaultVariants: {
|
|
130
|
+
size: "md",
|
|
131
|
+
plane: "default",
|
|
132
|
+
footer: "default",
|
|
133
|
+
footerFill: "secondary",
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
export type CardVariantProps = VariantProps<typeof cardVariants>;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The fill a `band` footer paints: the next rung of the surface ladder from the
|
|
141
|
+
* card's own, so the band is always told apart from the card it is set into.
|
|
142
|
+
*
|
|
143
|
+
* It is the same step a surface nested in the card would take, which is the
|
|
144
|
+
* point — a band is the card's plane stepped once, not a colour of its own. A
|
|
145
|
+
* transparent card on the page has no plane, so its band takes the card fill.
|
|
146
|
+
*/
|
|
147
|
+
export function resolveCardFooterFill(plane: SurfacePlane | null): SurfacePlane {
|
|
148
|
+
const next = resolveSurfaceVariant({ parentPlane: plane });
|
|
149
|
+
return next === "transparent" ? "default" : next;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Splits a header's children into its text column and its trailing actions.
|
|
154
|
+
*
|
|
155
|
+
* A header is a row — the title and description stacked on the left, an action
|
|
156
|
+
* pinned to the top right — and React Native has no grid to place a child by
|
|
157
|
+
* type. So the header walks its children and lifts every action out, keeping
|
|
158
|
+
* the rest in order for the column. `Children.toArray` drops the nulls and
|
|
159
|
+
* booleans a conditional child leaves behind, so an action rendered only some
|
|
160
|
+
* of the time does not leave a hole in either list.
|
|
161
|
+
*
|
|
162
|
+
* `isAction` is passed in rather than imported so this stays free of the part
|
|
163
|
+
* it looks for, and of React Native with it.
|
|
164
|
+
*/
|
|
165
|
+
export function splitCardHeaderChildren(
|
|
166
|
+
children: ReactNode,
|
|
167
|
+
isAction: (node: ReactNode) => boolean
|
|
168
|
+
): { text: ReactNode[]; actions: ReactNode[] } {
|
|
169
|
+
const text: ReactNode[] = [];
|
|
170
|
+
const actions: ReactNode[] = [];
|
|
171
|
+
|
|
172
|
+
for (const child of Children.toArray(children)) {
|
|
173
|
+
(isAction(child) ? actions : text).push(child);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { actions, text };
|
|
177
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { Card, type CardProps } from "./card";
|
|
2
|
+
export { type CardContextValue, CardProvider, useCard, useCardContext } from "./card.context";
|
|
3
|
+
export type { CardSlotProps, CardTextProps } from "./card.types";
|
|
4
|
+
export {
|
|
5
|
+
CARD_FOOTER_VARIANTS,
|
|
6
|
+
CARD_PLANES,
|
|
7
|
+
CARD_SIZES,
|
|
8
|
+
type CardFooterVariant,
|
|
9
|
+
type CardPlane,
|
|
10
|
+
type CardSize,
|
|
11
|
+
type CardVariantProps,
|
|
12
|
+
cardVariants,
|
|
13
|
+
resolveCardFooterFill,
|
|
14
|
+
} from "./card.variants";
|
|
15
|
+
export type { CardFooterProps } from "./card-footer";
|