@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,244 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
|
|
4
|
+
/** The logical sides a drawer opens from. `start` and `end` follow the layout direction. */
|
|
5
|
+
export const DRAWER_SIDES = ["start", "end", "top", "bottom"] as const;
|
|
6
|
+
export type DrawerSide = (typeof DRAWER_SIDES)[number];
|
|
7
|
+
|
|
8
|
+
/** The physical screen edges a side resolves to — what the motion and the pan are measured against. */
|
|
9
|
+
export const DRAWER_EDGES = ["left", "right", "top", "bottom"] as const;
|
|
10
|
+
export type DrawerEdge = (typeof DRAWER_EDGES)[number];
|
|
11
|
+
|
|
12
|
+
/** The panel's extents, narrowest first. */
|
|
13
|
+
export const DRAWER_SIZES = ["sm", "md", "lg", "full"] as const;
|
|
14
|
+
export type DrawerSize = (typeof DRAWER_SIZES)[number];
|
|
15
|
+
|
|
16
|
+
/** What a released swipe does. */
|
|
17
|
+
export type DrawerRelease = "dismiss" | "restore";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Each size as a fraction of the window along the panel's axis, capped.
|
|
21
|
+
*
|
|
22
|
+
* A fraction so a phone always keeps a strip of the app in view to tap away
|
|
23
|
+
* on; a cap so a tablet's drawer stays a drawer and not a second screen.
|
|
24
|
+
* `full` keeps a 6% strip and has no cap.
|
|
25
|
+
*/
|
|
26
|
+
export const DRAWER_SIZE_EXTENT = {
|
|
27
|
+
sm: { fraction: 0.62, max: 280 },
|
|
28
|
+
md: { fraction: 0.78, max: 320 },
|
|
29
|
+
lg: { fraction: 0.88, max: 400 },
|
|
30
|
+
full: { fraction: 0.94, max: Number.POSITIVE_INFINITY },
|
|
31
|
+
} as const satisfies Record<DrawerSize, { fraction: number; max: number }>;
|
|
32
|
+
|
|
33
|
+
/** A release past this fraction of the extent toward the edge dismisses. */
|
|
34
|
+
export const DRAWER_DISMISS_FRACTION = 0.4;
|
|
35
|
+
|
|
36
|
+
/** A release faster than this toward the edge dismisses from any distance, pt/s. */
|
|
37
|
+
export const DRAWER_FLING_VELOCITY = 800;
|
|
38
|
+
|
|
39
|
+
/** The asymptote a drag away from the edge approaches, in points. */
|
|
40
|
+
export const DRAWER_RUBBER_BAND = 40;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Travel along the panel's axis before the pan claims the touch, in points.
|
|
44
|
+
* Travel across it first fails the pan, so a vertical scroll in a start
|
|
45
|
+
* drawer's body never moves the drawer.
|
|
46
|
+
*/
|
|
47
|
+
export const DRAWER_PAN_ACTIVE_OFFSET = 10;
|
|
48
|
+
|
|
49
|
+
/** The bounds on a release animation, ms — finite so an E2E settle wait ends. */
|
|
50
|
+
export const DRAWER_RELEASE_MS = { min: 80, max: 220 } as const;
|
|
51
|
+
|
|
52
|
+
/** The scrim's token — the foundation's, named here so the test can pin it. */
|
|
53
|
+
export const DRAWER_SCRIM_TOKEN = "overlay";
|
|
54
|
+
|
|
55
|
+
/** Slop around the header ✕ — a bare glyph has no capsule to bring it toward 44pt. */
|
|
56
|
+
export const DRAWER_CLOSE_HIT_SLOP = 8;
|
|
57
|
+
|
|
58
|
+
export const drawerVariants = tv({
|
|
59
|
+
slots: {
|
|
60
|
+
/** The foundation's scrim token. The fade is `Overlay.Scrim`'s own. */
|
|
61
|
+
scrim: "bg-overlay",
|
|
62
|
+
/**
|
|
63
|
+
* A window-sized frame the panel is positioned in, laid out left to
|
|
64
|
+
* right whatever the app's direction, so the panel's classes and its
|
|
65
|
+
* safe-area padding are physical — the same terms its motion and its pan
|
|
66
|
+
* are measured in. `inner` restores the app's direction for the content.
|
|
67
|
+
*/
|
|
68
|
+
positioner: "absolute inset-0",
|
|
69
|
+
/**
|
|
70
|
+
* The panel. Docked flush to its edge with square corners there, and the
|
|
71
|
+
* card corner on the two corners facing the app. `popover` because a
|
|
72
|
+
* drawer is a layer over the app, as `Dialog` and `BottomSheet` are.
|
|
73
|
+
*/
|
|
74
|
+
content: "absolute overflow-hidden bg-popover",
|
|
75
|
+
/** The column inside the safe-area padding, laid out in the app's direction. */
|
|
76
|
+
inner: "flex-1",
|
|
77
|
+
/** Title block and ✕ in a row; the ✕ sits at the trailing edge. */
|
|
78
|
+
header: "flex-row items-start gap-3 px-5 pt-5 pb-3",
|
|
79
|
+
/** The title and description column, taking the row's slack. */
|
|
80
|
+
heading: "flex-1 gap-1",
|
|
81
|
+
body: "flex-1",
|
|
82
|
+
/** The body's padding, on the scroll content so it scrolls with it. */
|
|
83
|
+
bodyContent: "gap-3 px-5 pb-5",
|
|
84
|
+
/** `mt-auto` so it sits at the panel's end even when no body takes the slack. */
|
|
85
|
+
footer: "mt-auto flex-row items-center justify-end gap-2 border-border border-t px-5 py-4",
|
|
86
|
+
close: "shrink-0",
|
|
87
|
+
},
|
|
88
|
+
variants: {
|
|
89
|
+
edge: {
|
|
90
|
+
left: { content: "inset-y-0 left-0 rounded-r-lg" },
|
|
91
|
+
right: { content: "inset-y-0 right-0 rounded-l-lg" },
|
|
92
|
+
top: { content: "inset-x-0 top-0 rounded-b-lg" },
|
|
93
|
+
bottom: { content: "inset-x-0 bottom-0 rounded-t-lg" },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
defaultVariants: {
|
|
97
|
+
edge: "left",
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* There is no `title` or `description` slot. Both *are* text presets with no
|
|
103
|
+
* layout of their own — the ✕ sits in the flow, so the title needs no
|
|
104
|
+
* clearance — and `tv` emits `undefined` for an empty class string. The parts
|
|
105
|
+
* merge a caller's `className` with `cn()` instead, as `Dialog`'s description does.
|
|
106
|
+
*/
|
|
107
|
+
export type DrawerVariantProps = VariantProps<typeof drawerVariants>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The physical edge a side docks to. `start` is the left edge in a
|
|
111
|
+
* left-to-right layout and the right edge in a right-to-left one.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveDrawerEdge(side: DrawerSide, isRTL: boolean): DrawerEdge {
|
|
114
|
+
if (side === "start") return isRTL ? "right" : "left";
|
|
115
|
+
if (side === "end") return isRTL ? "left" : "right";
|
|
116
|
+
return side;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The panel's width (a left or right edge) or height (top or bottom), from the
|
|
121
|
+
* window's extent along the same axis. Whole points, never negative.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveDrawerExtent(size: DrawerSize, windowExtent: number): number {
|
|
124
|
+
if (windowExtent <= 0) return 0;
|
|
125
|
+
const { fraction, max } = DRAWER_SIZE_EXTENT[size];
|
|
126
|
+
return Math.round(Math.min(windowExtent * fraction, max));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The translate that puts the panel `progress` of the way in: 0 is the whole
|
|
131
|
+
* extent off its edge, 1 is docked.
|
|
132
|
+
*
|
|
133
|
+
* A worklet, read in the panel's animated style. Flat on purpose — a
|
|
134
|
+
* module-scope worklet never calls another function.
|
|
135
|
+
*/
|
|
136
|
+
export function resolveDrawerOffset(
|
|
137
|
+
edge: DrawerEdge,
|
|
138
|
+
extent: number,
|
|
139
|
+
progress: number
|
|
140
|
+
): { translateX: number; translateY: number } {
|
|
141
|
+
"worklet";
|
|
142
|
+
const hidden = extent * (1 - progress);
|
|
143
|
+
if (edge === "left") return { translateX: -hidden, translateY: 0 };
|
|
144
|
+
if (edge === "right") return { translateX: hidden, translateY: 0 };
|
|
145
|
+
if (edge === "top") return { translateX: 0, translateY: -hidden };
|
|
146
|
+
return { translateX: 0, translateY: hidden };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Where the panel sits for a finger's travel along its axis.
|
|
151
|
+
*
|
|
152
|
+
* Toward the docked edge — the way it would leave — the panel follows the
|
|
153
|
+
* finger one to one. Away from it, into the app, it rubber-bands toward
|
|
154
|
+
* `DRAWER_RUBBER_BAND` and never reaches it, so the panel gives a little and
|
|
155
|
+
* says it goes no further. The result has the translation's sign.
|
|
156
|
+
*/
|
|
157
|
+
export function resolveDrawerDrag(edge: DrawerEdge, translation: number): number {
|
|
158
|
+
"worklet";
|
|
159
|
+
const sign = edge === "left" || edge === "top" ? -1 : 1;
|
|
160
|
+
const toward = translation * sign;
|
|
161
|
+
if (toward >= 0) return translation;
|
|
162
|
+
const away = -toward;
|
|
163
|
+
return -sign * (away / (1 + away / DRAWER_RUBBER_BAND));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** How much of the extent a drag has carried the panel toward its edge, 0 to 1. */
|
|
167
|
+
export function resolveDrawerDragFraction(edge: DrawerEdge, translation: number, extent: number): number {
|
|
168
|
+
"worklet";
|
|
169
|
+
if (extent <= 0) return 0;
|
|
170
|
+
const sign = edge === "left" || edge === "top" ? -1 : 1;
|
|
171
|
+
return Math.min(1, Math.max(0, (translation * sign) / extent));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export type DrawerReleaseInput = {
|
|
175
|
+
edge: DrawerEdge;
|
|
176
|
+
/** The finger's travel along the panel's axis, physical sign. */
|
|
177
|
+
translation: number;
|
|
178
|
+
/** The release velocity along the same axis, pt/s, physical sign. */
|
|
179
|
+
velocity: number;
|
|
180
|
+
extent: number;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Whether a released swipe dismisses the drawer or puts it back.
|
|
185
|
+
*
|
|
186
|
+
* A fling away from the edge always restores — the user changed their mind
|
|
187
|
+
* mid-swipe. Otherwise a fling toward the edge faster than
|
|
188
|
+
* `DRAWER_FLING_VELOCITY`, or a drag past `DRAWER_DISMISS_FRACTION` of the
|
|
189
|
+
* extent, dismisses.
|
|
190
|
+
*/
|
|
191
|
+
export function resolveDrawerRelease({ edge, translation, velocity, extent }: DrawerReleaseInput): DrawerRelease {
|
|
192
|
+
"worklet";
|
|
193
|
+
if (extent <= 0) return "restore";
|
|
194
|
+
const sign = edge === "left" || edge === "top" ? -1 : 1;
|
|
195
|
+
const toward = velocity * sign;
|
|
196
|
+
if (toward < -DRAWER_FLING_VELOCITY) return "restore";
|
|
197
|
+
if (toward > DRAWER_FLING_VELOCITY) return "dismiss";
|
|
198
|
+
return (translation * sign) / extent > DRAWER_DISMISS_FRACTION ? "dismiss" : "restore";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* How long the panel takes to cover the rest of its way out after a dismissing
|
|
203
|
+
* release: the remaining distance at the release speed, held inside
|
|
204
|
+
* `DRAWER_RELEASE_MS` so a flick does not snap and a slow release does not drift.
|
|
205
|
+
*/
|
|
206
|
+
export function resolveDrawerExitDuration({ remaining, velocity }: { remaining: number; velocity: number }): number {
|
|
207
|
+
"worklet";
|
|
208
|
+
const speed = Math.abs(velocity);
|
|
209
|
+
if (remaining <= 0) return DRAWER_RELEASE_MS.min;
|
|
210
|
+
if (speed === 0) return DRAWER_RELEASE_MS.max;
|
|
211
|
+
return Math.min(DRAWER_RELEASE_MS.max, Math.max(DRAWER_RELEASE_MS.min, (remaining / speed) * 1000));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export type DrawerInsets = { top: number; right: number; bottom: number; left: number };
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The safe-area padding a panel docked to `edge` takes.
|
|
218
|
+
*
|
|
219
|
+
* Every side that meets a screen edge pads its inset — the docked side and the
|
|
220
|
+
* two beside it — so a start drawer clears the status bar and the home
|
|
221
|
+
* indicator. The side facing the app sits nowhere near a screen edge, and pads
|
|
222
|
+
* nothing.
|
|
223
|
+
*/
|
|
224
|
+
export function resolveDrawerInsets(edge: DrawerEdge, insets: DrawerInsets): DrawerInsets {
|
|
225
|
+
return {
|
|
226
|
+
top: edge === "bottom" ? 0 : insets.top,
|
|
227
|
+
right: edge === "left" ? 0 : insets.right,
|
|
228
|
+
bottom: edge === "top" ? 0 : insets.bottom,
|
|
229
|
+
left: edge === "right" ? 0 : insets.left,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The panel's full width or height: the size's extent plus the inset on the
|
|
235
|
+
* docked edge.
|
|
236
|
+
*
|
|
237
|
+
* The size measures the content, not the safe-area band it sits under — a
|
|
238
|
+
* top drawer at `sm` would otherwise hand a third of its 280pt to the status
|
|
239
|
+
* bar and clip its rows. The motion uses this, so the panel starts fully off
|
|
240
|
+
* screen.
|
|
241
|
+
*/
|
|
242
|
+
export function resolveDrawerFrameExtent(edge: DrawerEdge, extent: number, insets: DrawerInsets): number {
|
|
243
|
+
return extent + insets[edge];
|
|
244
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export { Drawer, type DrawerProps } from "./drawer";
|
|
2
|
+
export { type DrawerContextValue, useDrawer, useDrawerContext } from "./drawer.context";
|
|
3
|
+
export {
|
|
4
|
+
DRAWER_CLOSE_HIT_SLOP,
|
|
5
|
+
DRAWER_DISMISS_FRACTION,
|
|
6
|
+
DRAWER_EDGES,
|
|
7
|
+
DRAWER_FLING_VELOCITY,
|
|
8
|
+
DRAWER_PAN_ACTIVE_OFFSET,
|
|
9
|
+
DRAWER_RELEASE_MS,
|
|
10
|
+
DRAWER_RUBBER_BAND,
|
|
11
|
+
DRAWER_SCRIM_TOKEN,
|
|
12
|
+
DRAWER_SIDES,
|
|
13
|
+
DRAWER_SIZE_EXTENT,
|
|
14
|
+
DRAWER_SIZES,
|
|
15
|
+
type DrawerEdge,
|
|
16
|
+
type DrawerInsets,
|
|
17
|
+
type DrawerRelease,
|
|
18
|
+
type DrawerReleaseInput,
|
|
19
|
+
type DrawerSide,
|
|
20
|
+
type DrawerSize,
|
|
21
|
+
type DrawerVariantProps,
|
|
22
|
+
drawerVariants,
|
|
23
|
+
resolveDrawerDrag,
|
|
24
|
+
resolveDrawerDragFraction,
|
|
25
|
+
resolveDrawerEdge,
|
|
26
|
+
resolveDrawerExitDuration,
|
|
27
|
+
resolveDrawerExtent,
|
|
28
|
+
resolveDrawerFrameExtent,
|
|
29
|
+
resolveDrawerInsets,
|
|
30
|
+
resolveDrawerOffset,
|
|
31
|
+
resolveDrawerRelease,
|
|
32
|
+
} from "./drawer.variants";
|
|
33
|
+
export type { DrawerBodyProps } from "./drawer-body";
|
|
34
|
+
export type { DrawerCloseProps } from "./drawer-close";
|
|
35
|
+
export type { DrawerContentProps } from "./drawer-content";
|
|
36
|
+
export type { DrawerDescriptionProps } from "./drawer-description";
|
|
37
|
+
export type { DrawerFooterProps } from "./drawer-footer";
|
|
38
|
+
export type { DrawerHeaderProps } from "./drawer-header";
|
|
39
|
+
export type { DrawerTitleProps } from "./drawer-title";
|
|
40
|
+
export type { DrawerTriggerProps } from "./drawer-trigger";
|
|
41
|
+
export type { UseDrawerPanOptions } from "./use-drawer-pan";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { Gesture, type PanGesture } from "react-native-gesture-handler";
|
|
3
|
+
import { cancelAnimation, Easing, type SharedValue, useSharedValue, withTiming } from "react-native-reanimated";
|
|
4
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
5
|
+
import { OVERLAY_MOTION } from "../overlay/overlay.variants";
|
|
6
|
+
import { playHaptic } from "../pressable";
|
|
7
|
+
import {
|
|
8
|
+
DRAWER_DISMISS_FRACTION,
|
|
9
|
+
DRAWER_PAN_ACTIVE_OFFSET,
|
|
10
|
+
type DrawerEdge,
|
|
11
|
+
resolveDrawerDrag,
|
|
12
|
+
resolveDrawerDragFraction,
|
|
13
|
+
resolveDrawerExitDuration,
|
|
14
|
+
resolveDrawerRelease,
|
|
15
|
+
} from "./drawer.variants";
|
|
16
|
+
|
|
17
|
+
export type UseDrawerPanOptions = {
|
|
18
|
+
edge: DrawerEdge;
|
|
19
|
+
/** The panel's width or height along its axis. */
|
|
20
|
+
extent: number;
|
|
21
|
+
isEnabled: boolean;
|
|
22
|
+
/** The panel's displacement along its axis, physical sign — written by the pan, read by the panel's style. */
|
|
23
|
+
drag: SharedValue<number>;
|
|
24
|
+
/** Called once a dismissing release has carried the panel off its edge. */
|
|
25
|
+
onDismiss: () => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const RESTORE_EASING = Easing.out(Easing.cubic);
|
|
29
|
+
const PAN_RANGE: [number, number] = [-DRAWER_PAN_ACTIVE_OFFSET, DRAWER_PAN_ACTIVE_OFFSET];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The panel's swipe-to-dismiss.
|
|
33
|
+
*
|
|
34
|
+
* The pan claims the touch only after `DRAWER_PAN_ACTIVE_OFFSET` of travel
|
|
35
|
+
* along the panel's axis and fails on the same travel across it, so a tap
|
|
36
|
+
* reaches the row under it and a vertical scroll in a start or end drawer's
|
|
37
|
+
* body never moves the drawer.
|
|
38
|
+
*
|
|
39
|
+
* While it runs the panel follows the finger toward its edge and rubber-bands
|
|
40
|
+
* away from it (`resolveDrawerDrag`), and a `selection` haptic marks each
|
|
41
|
+
* crossing of the dismiss threshold — the moment letting go changes what
|
|
42
|
+
* happens. On release `resolveDrawerRelease` decides: a dismiss **continues
|
|
43
|
+
* from where the finger left the panel**, at the release speed, and closes the
|
|
44
|
+
* drawer when it lands; a restore eases back to docked.
|
|
45
|
+
*
|
|
46
|
+
* The handlers are built in this hook, so they may call the module-scope
|
|
47
|
+
* worklets in `drawer.variants` — those, being module-scope, call nothing.
|
|
48
|
+
* Per-gesture memory lives in shared values: each worklet gets its own copy of
|
|
49
|
+
* a captured `let`.
|
|
50
|
+
*/
|
|
51
|
+
export function useDrawerPan({ edge, extent, isEnabled, drag, onDismiss }: UseDrawerPanOptions): PanGesture {
|
|
52
|
+
const isDragging = useSharedValue(false);
|
|
53
|
+
const isPastThreshold = useSharedValue(false);
|
|
54
|
+
|
|
55
|
+
return useMemo(() => {
|
|
56
|
+
const isHorizontal = edge === "left" || edge === "right";
|
|
57
|
+
const sign = edge === "left" || edge === "top" ? -1 : 1;
|
|
58
|
+
const pan = Gesture.Pan().enabled(isEnabled).shouldCancelWhenOutside(false);
|
|
59
|
+
if (isHorizontal) {
|
|
60
|
+
pan.activeOffsetX(PAN_RANGE).failOffsetY(PAN_RANGE);
|
|
61
|
+
} else {
|
|
62
|
+
pan.activeOffsetY(PAN_RANGE).failOffsetX(PAN_RANGE);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return pan
|
|
66
|
+
.onStart(() => {
|
|
67
|
+
"worklet";
|
|
68
|
+
cancelAnimation(drag);
|
|
69
|
+
isDragging.value = true;
|
|
70
|
+
isPastThreshold.value = false;
|
|
71
|
+
})
|
|
72
|
+
.onUpdate((event) => {
|
|
73
|
+
"worklet";
|
|
74
|
+
const translation = isHorizontal ? event.translationX : event.translationY;
|
|
75
|
+
drag.value = resolveDrawerDrag(edge, translation);
|
|
76
|
+
|
|
77
|
+
const isPast = resolveDrawerDragFraction(edge, translation, extent) > DRAWER_DISMISS_FRACTION;
|
|
78
|
+
if (isPast !== isPastThreshold.value) {
|
|
79
|
+
isPastThreshold.value = isPast;
|
|
80
|
+
playHaptic("selection");
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
.onFinalize((event) => {
|
|
84
|
+
"worklet";
|
|
85
|
+
// A touch that never activated — a tap, a scroll across the axis —
|
|
86
|
+
// must not touch `drag`: a dismiss may still be animating it.
|
|
87
|
+
if (!isDragging.value) return;
|
|
88
|
+
isDragging.value = false;
|
|
89
|
+
|
|
90
|
+
const translation = isHorizontal ? event.translationX : event.translationY;
|
|
91
|
+
const velocity = isHorizontal ? event.velocityX : event.velocityY;
|
|
92
|
+
const release = resolveDrawerRelease({ edge, extent, translation, velocity });
|
|
93
|
+
|
|
94
|
+
if (release === "dismiss") {
|
|
95
|
+
const duration = resolveDrawerExitDuration({
|
|
96
|
+
remaining: Math.max(0, extent - Math.abs(drag.value)),
|
|
97
|
+
velocity,
|
|
98
|
+
});
|
|
99
|
+
drag.value = withTiming(sign * extent, { duration, easing: Easing.linear }, (finished) => {
|
|
100
|
+
if (finished) scheduleOnRN(onDismiss);
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
drag.value = withTiming(0, { duration: OVERLAY_MOTION.enterMs, easing: RESTORE_EASING });
|
|
106
|
+
});
|
|
107
|
+
}, [drag, edge, extent, isDragging, isEnabled, isPastThreshold, onDismiss]);
|
|
108
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# EmptyState
|
|
2
|
+
|
|
3
|
+
A placeholder for a list or a screen with nothing in it yet. Compound root plus
|
|
4
|
+
five parts: `Header`, `Media`, `Title`, `Description`, `Content`.
|
|
5
|
+
|
|
6
|
+
`import { EmptyState } from "@delacour/react-native-ui/empty-state";`
|
|
7
|
+
|
|
8
|
+
## Files
|
|
9
|
+
|
|
10
|
+
| File | What it holds |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `index.ts` | → `@delacour/react-native-ui/empty-state` |
|
|
13
|
+
| `empty-state.tsx` | Root + the `Object.assign` compound surface |
|
|
14
|
+
| `empty-state-header.tsx` | `EmptyState.Header` |
|
|
15
|
+
| `empty-state-media.tsx` | `EmptyState.Media`, and the icon cascade it owns |
|
|
16
|
+
| `empty-state-title.tsx` | `EmptyState.Title` |
|
|
17
|
+
| `empty-state-description.tsx` | `EmptyState.Description` |
|
|
18
|
+
| `empty-state-content.tsx` | `EmptyState.Content` |
|
|
19
|
+
| `empty-state.context.tsx` | `EmptyStateProvider`, `useEmptyState()`, `useEmptyStateContext()`, `useEmptyStatePart()` |
|
|
20
|
+
| `empty-state.types.ts` | Prop types shared by two or more parts |
|
|
21
|
+
| `empty-state.variants.ts` | Pure `tv()` slots and the media colour map, no RN imports |
|
|
22
|
+
| `empty-state.variants.test.ts` | |
|
|
23
|
+
|
|
24
|
+
## Anatomy
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
<EmptyState>
|
|
28
|
+
<EmptyState.Header>
|
|
29
|
+
<EmptyState.Media variant="icon">
|
|
30
|
+
<Icon icon={IconInbox} />
|
|
31
|
+
</EmptyState.Media>
|
|
32
|
+
<EmptyState.Title>No messages</EmptyState.Title>
|
|
33
|
+
<EmptyState.Description>New conversations will show up here.</EmptyState.Description>
|
|
34
|
+
</EmptyState.Header>
|
|
35
|
+
<EmptyState.Content>
|
|
36
|
+
<Button onPress={compose}>New message</Button>
|
|
37
|
+
</EmptyState.Content>
|
|
38
|
+
</EmptyState>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Design
|
|
42
|
+
|
|
43
|
+
- **Variants**: `default`, `card`. **Sizes**: `sm`, `md`, `lg`. **Media
|
|
44
|
+
variants**: `default`, `icon`. Size drives the padding, both gaps, the media
|
|
45
|
+
box, the glyph inside it and both type scales — one axis rather than seven
|
|
46
|
+
numbers that drift apart. The tests assert every one of them ascends.
|
|
47
|
+
- **`default` grows; it never uses `flex-1`.** `flex-1` is a zero flex basis, so
|
|
48
|
+
inside a `ScrollView`'s content container, which has no height to grow into,
|
|
49
|
+
the block collapsed to nothing. `grow` keeps the content height as a floor and
|
|
50
|
+
fills whatever spare height a bounded parent has — which is what "fills its
|
|
51
|
+
container" has to mean in both places. A test holds the class.
|
|
52
|
+
- **`card` is dashed.** A dashed outline is the conventional mark of a slot
|
|
53
|
+
waiting for content, and it stops an embedded empty state reading as one
|
|
54
|
+
more populated card beside real ones. The corner is the card step,
|
|
55
|
+
`rounded-lg`, stepping to `rounded-md` at `sm` like `ListGroup` and
|
|
56
|
+
`Accordion`. The border is reserved on both variants (`border-transparent` on
|
|
57
|
+
`default`), so switching variant changes a colour and never the layout.
|
|
58
|
+
- **A size is padding, never a height.** The description respects OS font
|
|
59
|
+
scaling; a fixed height would clip it at a large accessibility step.
|
|
60
|
+
- **Icons are composed, never passed as props.** `Media` wraps its subtree in an
|
|
61
|
+
`IconDefaultsProvider` carrying the `mediaIcon` slot — a step on the shared
|
|
62
|
+
`--spacing-icon-*` scale — and `EMPTY_STATE_MEDIA_FOREGROUND_TOKEN[variant]`.
|
|
63
|
+
A bare glyph is `muted-foreground`, because the picture is decoration and must
|
|
64
|
+
not outweigh the title. Inside the tinted `icon` box it takes `foreground`:
|
|
65
|
+
`muted-foreground` on `bg-muted` is two greys a step apart.
|
|
66
|
+
- **The glyph uses the same three steps inside the box and out of it.** The
|
|
67
|
+
icon scale tops out at `icon-2xl`, so a bare glyph one step larger than the
|
|
68
|
+
boxed one would have left `md` and `lg` identical.
|
|
69
|
+
- **The media sits further from the title than the title from its
|
|
70
|
+
description.** The header's `gap` spaces the two lines; the media adds a
|
|
71
|
+
bottom margin on top of it, so text reads as one unit and the picture as
|
|
72
|
+
something above it.
|
|
73
|
+
- **`Media` is hidden from assistive technology by default.** The title already
|
|
74
|
+
says what the picture shows, and a screen reader saying "image" first only
|
|
75
|
+
delays it. Media that carries meaning of its own opts back in with
|
|
76
|
+
`accessibilityElementsHidden={false}` and `importantForAccessibility="auto"` —
|
|
77
|
+
both are spread after the defaults.
|
|
78
|
+
- **`Title` is announced as a header.** An empty screen's title is what the
|
|
79
|
+
screen *is*, and the heading rotor should land on it.
|
|
80
|
+
- **Actions are a wrapping row, not a column.** Two actions sit side by side and
|
|
81
|
+
fall to a second line only when they must. `className="flex-col"` stacks them.
|
|
82
|
+
Button size is the caller's choice — `Content` sets nothing on its children, so
|
|
83
|
+
a `Button` keeps every prop it already has.
|
|
84
|
+
- **Header and content cap at `max-w-sm`.** On a tablet or in landscape a
|
|
85
|
+
description stretched edge to edge is a line too long to read.
|
|
86
|
+
- **Colour goes on the text parts.** No `View` slot carries a `text-*` utility;
|
|
87
|
+
a React Native `View` cannot cascade colour to a `Text`. The tests assert it.
|
|
88
|
+
- **No state of its own.** An empty state is a presentation of *someone else's*
|
|
89
|
+
state — a list's length, a query's result. Loading and disabled belong to the
|
|
90
|
+
`Button`s inside it, which already have them.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { useEmptyStatePart } from "./empty-state.context";
|
|
4
|
+
import type { EmptyStateSlotProps } from "./empty-state.types";
|
|
5
|
+
import { emptyStateVariants } from "./empty-state.variants";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The action slot under the header — usually one or two `Button`s.
|
|
9
|
+
*
|
|
10
|
+
* A centred row that wraps, so two actions sit side by side and fall onto a
|
|
11
|
+
* second line only when they have to. Pass `className="flex-col"` to stack them.
|
|
12
|
+
*/
|
|
13
|
+
export function EmptyStateContent({ className, ...props }: EmptyStateSlotProps): ReactElement {
|
|
14
|
+
const { size } = useEmptyStatePart("EmptyState.Content");
|
|
15
|
+
return <View className={emptyStateVariants({ size }).content({ className })} {...props} />;
|
|
16
|
+
}
|
|
17
|
+
EmptyStateContent.displayName = "DelacourUI.EmptyState.Content";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Text } from "../text";
|
|
3
|
+
import { useEmptyStatePart } from "./empty-state.context";
|
|
4
|
+
import type { EmptyStateTextProps } from "./empty-state.types";
|
|
5
|
+
import { emptyStateVariants } from "./empty-state.variants";
|
|
6
|
+
|
|
7
|
+
/** The supporting line under the title — a step down in scale, on the muted token. */
|
|
8
|
+
export function EmptyStateDescription({ className, ...props }: EmptyStateTextProps): ReactElement {
|
|
9
|
+
const { size } = useEmptyStatePart("EmptyState.Description");
|
|
10
|
+
return <Text className={emptyStateVariants({ size }).description({ className })} {...props} />;
|
|
11
|
+
}
|
|
12
|
+
EmptyStateDescription.displayName = "DelacourUI.EmptyState.Description";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { useEmptyStatePart } from "./empty-state.context";
|
|
4
|
+
import type { EmptyStateSlotProps } from "./empty-state.types";
|
|
5
|
+
import { emptyStateVariants } from "./empty-state.variants";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The centred column holding the media, the title and the description.
|
|
9
|
+
*
|
|
10
|
+
* Its own gap spaces the title from the description; the media adds a margin
|
|
11
|
+
* of its own on top, so the picture sits apart from the two lines of text.
|
|
12
|
+
*/
|
|
13
|
+
export function EmptyStateHeader({ className, ...props }: EmptyStateSlotProps): ReactElement {
|
|
14
|
+
const { size } = useEmptyStatePart("EmptyState.Header");
|
|
15
|
+
return <View className={emptyStateVariants({ size }).header({ className })} {...props} />;
|
|
16
|
+
}
|
|
17
|
+
EmptyStateHeader.displayName = "DelacourUI.EmptyState.Header";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type ReactElement, useMemo } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { IconDefaultsProvider } from "../icon";
|
|
4
|
+
import { useEmptyStatePart } from "./empty-state.context";
|
|
5
|
+
import type { EmptyStateSlotProps } from "./empty-state.types";
|
|
6
|
+
import {
|
|
7
|
+
EMPTY_STATE_MEDIA_FOREGROUND_TOKEN,
|
|
8
|
+
type EmptyStateMediaVariant,
|
|
9
|
+
emptyStateVariants,
|
|
10
|
+
} from "./empty-state.variants";
|
|
11
|
+
|
|
12
|
+
export type EmptyStateMediaProps = EmptyStateSlotProps & {
|
|
13
|
+
/**
|
|
14
|
+
* `icon` draws a tinted, rounded box sized for a glyph. `default` draws
|
|
15
|
+
* nothing behind its content, for an illustration or an image that sizes
|
|
16
|
+
* itself.
|
|
17
|
+
*/
|
|
18
|
+
variant?: EmptyStateMediaVariant;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The picture above the title.
|
|
23
|
+
*
|
|
24
|
+
* Its subtree inherits an icon size on the shared scale and a colour token, so a
|
|
25
|
+
* bare `<Icon icon={IconInbox} />` comes out right with nothing said at the call
|
|
26
|
+
* site — the same cascade a `Button` gives its icons.
|
|
27
|
+
*
|
|
28
|
+
* Hidden from assistive technology by default: the title already says what the
|
|
29
|
+
* picture shows, and a screen reader announcing "image" first only delays it.
|
|
30
|
+
* Pass `accessibilityElementsHidden={false}` and
|
|
31
|
+
* `importantForAccessibility="auto"` for media that carries meaning of its own.
|
|
32
|
+
*/
|
|
33
|
+
export function EmptyStateMedia({
|
|
34
|
+
variant = "default",
|
|
35
|
+
className,
|
|
36
|
+
children,
|
|
37
|
+
...props
|
|
38
|
+
}: EmptyStateMediaProps): ReactElement {
|
|
39
|
+
const { size } = useEmptyStatePart("EmptyState.Media");
|
|
40
|
+
const slots = emptyStateVariants({ size, media: variant });
|
|
41
|
+
const iconClassName = slots.mediaIcon();
|
|
42
|
+
const color = EMPTY_STATE_MEDIA_FOREGROUND_TOKEN[variant];
|
|
43
|
+
const iconDefaults = useMemo(() => ({ className: iconClassName, color }), [iconClassName, color]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<View
|
|
47
|
+
accessibilityElementsHidden
|
|
48
|
+
className={slots.media({ className })}
|
|
49
|
+
importantForAccessibility="no-hide-descendants"
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
<IconDefaultsProvider value={iconDefaults}>{children}</IconDefaultsProvider>
|
|
53
|
+
</View>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
EmptyStateMedia.displayName = "DelacourUI.EmptyState.Media";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Text } from "../text";
|
|
3
|
+
import { useEmptyStatePart } from "./empty-state.context";
|
|
4
|
+
import type { EmptyStateTextProps } from "./empty-state.types";
|
|
5
|
+
import { emptyStateVariants } from "./empty-state.variants";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The empty state's heading.
|
|
9
|
+
*
|
|
10
|
+
* Announced as a header, so a screen reader's heading rotor lands on it — the
|
|
11
|
+
* title is what an empty screen *is*. Carries its own colour and type scale
|
|
12
|
+
* from context: a React Native `View` does not cascade colour to a `Text`.
|
|
13
|
+
*/
|
|
14
|
+
export function EmptyStateTitle({ className, ...props }: EmptyStateTextProps): ReactElement {
|
|
15
|
+
const { size } = useEmptyStatePart("EmptyState.Title");
|
|
16
|
+
return <Text accessibilityRole="header" className={emptyStateVariants({ size }).title({ className })} {...props} />;
|
|
17
|
+
}
|
|
18
|
+
EmptyStateTitle.displayName = "DelacourUI.EmptyState.Title";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createContext, type ReactElement, type ReactNode, use } from "react";
|
|
2
|
+
import type { EmptyStateSize, EmptyStateVariant } from "./empty-state.variants";
|
|
3
|
+
|
|
4
|
+
export type EmptyStateContextValue = {
|
|
5
|
+
/** Size of the empty state. */
|
|
6
|
+
size: EmptyStateSize;
|
|
7
|
+
/** How the empty state sits on the screen. */
|
|
8
|
+
variant: EmptyStateVariant;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const EmptyStateContext = createContext<EmptyStateContextValue | null>(null);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Supplies the enclosing empty state's size and variant to its subtree.
|
|
15
|
+
*
|
|
16
|
+
* Lives in its own module, importing nothing but `empty-state.variants`, so a
|
|
17
|
+
* part can read it without importing `./empty-state` and closing a cycle
|
|
18
|
+
* through the root. See the package AGENTS.md, rule 3.
|
|
19
|
+
*/
|
|
20
|
+
export function EmptyStateProvider({
|
|
21
|
+
value,
|
|
22
|
+
children,
|
|
23
|
+
}: {
|
|
24
|
+
value: EmptyStateContextValue;
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}): ReactElement {
|
|
27
|
+
return <EmptyStateContext value={value}>{children}</EmptyStateContext>;
|
|
28
|
+
}
|
|
29
|
+
EmptyStateProvider.displayName = "DelacourUI.EmptyState.Provider";
|
|
30
|
+
|
|
31
|
+
/** The enclosing empty state's context, or null outside an `<EmptyState>`. */
|
|
32
|
+
export function useEmptyStateContext(): EmptyStateContextValue | null {
|
|
33
|
+
return use(EmptyStateContext);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reads the enclosing empty state's size and variant.
|
|
38
|
+
*
|
|
39
|
+
* Lets a custom part — an illustration, a link row — scale with the block
|
|
40
|
+
* without the root passing props down. Throws outside an `<EmptyState>`; use
|
|
41
|
+
* {@link useEmptyStateContext} where the enclosing empty state is optional.
|
|
42
|
+
*/
|
|
43
|
+
export function useEmptyState(): EmptyStateContextValue {
|
|
44
|
+
const context = useEmptyStateContext();
|
|
45
|
+
if (!context) {
|
|
46
|
+
throw new Error("useEmptyState must be called inside an <EmptyState>.");
|
|
47
|
+
}
|
|
48
|
+
return context;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The enclosing empty state's context, for a compound part that cannot work
|
|
53
|
+
* without one.
|
|
54
|
+
*
|
|
55
|
+
* Internal: deliberately not re-exported from `index.ts`. A caller outside the
|
|
56
|
+
* library wants {@link useEmptyState}, whose error names the hook rather than a
|
|
57
|
+
* part.
|
|
58
|
+
*/
|
|
59
|
+
export function useEmptyStatePart(component: string): EmptyStateContextValue {
|
|
60
|
+
const context = useEmptyStateContext();
|
|
61
|
+
if (!context) {
|
|
62
|
+
throw new Error(`${component} must be rendered inside an <EmptyState>.`);
|
|
63
|
+
}
|
|
64
|
+
return context;
|
|
65
|
+
}
|