@delacour/react-native-ui 0.1.0-alpha.4 → 0.2.0-alpha.20261010061731
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 +10 -6
- 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 +10 -0
- package/src/components/badge/badge-close-button.tsx +2 -2
- package/src/components/badge/badge.context.tsx +2 -1
- package/src/components/badge/badge.tsx +13 -4
- package/src/components/badge/badge.variants.test.ts +26 -0
- package/src/components/badge/badge.variants.ts +21 -0
- package/src/components/bottom-sheet/AGENTS.md +186 -231
- 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 -208
- 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 -122
- 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 -62
- 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 -87
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -126
- 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 +51 -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 +7 -1
- package/src/components/button/button.variants.test.ts +190 -0
- package/src/components/button/button.variants.ts +87 -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 +92 -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 +100 -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 +8 -0
- 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/input/AGENTS.md +5 -0
- package/src/components/input/input.variants.test.ts +32 -0
- package/src/components/input/input.variants.ts +13 -1
- 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/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 +82 -0
- package/src/components/surface/index.ts +15 -0
- package/src/components/surface/surface.context.tsx +56 -0
- package/src/components/surface/surface.tsx +87 -0
- package/src/components/surface/surface.variants.test.ts +227 -0
- package/src/components/surface/surface.variants.ts +165 -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/text/AGENTS.md +8 -2
- package/src/components/text/text.tsx +7 -0
- package/src/components/text/text.variants.test.ts +24 -1
- package/src/components/text/text.variants.ts +5 -0
- package/src/components/textarea/AGENTS.md +89 -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.parity.test.ts +62 -0
- package/src/lib/cn.test.ts +14 -0
- package/src/lib/cn.ts +11 -6
- 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 +44 -0
- package/src/styles/tokens.test.ts +56 -0
- package/src/styles/tokens.ts +29 -1
- 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,96 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
INITIAL_OVERLAY_REGISTRY,
|
|
4
|
+
isTopOverlay,
|
|
5
|
+
OVERLAY_LAYER_BASE,
|
|
6
|
+
type OverlayRegistryState,
|
|
7
|
+
reduceOverlayRegistry,
|
|
8
|
+
topOverlay,
|
|
9
|
+
zIndexOfOverlay,
|
|
10
|
+
} from "./overlay-registry";
|
|
11
|
+
|
|
12
|
+
function apply(...actions: Parameters<typeof reduceOverlayRegistry>[1][]): OverlayRegistryState {
|
|
13
|
+
return actions.reduce(reduceOverlayRegistry, INITIAL_OVERLAY_REGISTRY);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("OVERLAY_LAYER_BASE", () => {
|
|
17
|
+
test("orders modal under anchored under toast, all above the sheet engine's 1-up stamps", () => {
|
|
18
|
+
expect(OVERLAY_LAYER_BASE.modal).toBeGreaterThanOrEqual(1000);
|
|
19
|
+
expect(OVERLAY_LAYER_BASE.anchored).toBeGreaterThan(OVERLAY_LAYER_BASE.modal);
|
|
20
|
+
expect(OVERLAY_LAYER_BASE.toast).toBeGreaterThan(OVERLAY_LAYER_BASE.anchored);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("reduceOverlayRegistry", () => {
|
|
25
|
+
test("an open overlay draws above its layer's base", () => {
|
|
26
|
+
const state = apply({ type: "open", id: "a", layer: "modal" });
|
|
27
|
+
expect(zIndexOfOverlay(state, "a")).toBe(OVERLAY_LAYER_BASE.modal + 1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("a later open in the same layer draws above the earlier one", () => {
|
|
31
|
+
const state = apply({ type: "open", id: "a", layer: "modal" }, { type: "open", id: "b", layer: "modal" });
|
|
32
|
+
expect(zIndexOfOverlay(state, "b")).toBeGreaterThan(zIndexOfOverlay(state, "a"));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("the layer wins over open order", () => {
|
|
36
|
+
const state = apply({ type: "open", id: "toast", layer: "toast" }, { type: "open", id: "dialog", layer: "modal" });
|
|
37
|
+
expect(zIndexOfOverlay(state, "toast")).toBeGreaterThan(zIndexOfOverlay(state, "dialog"));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("re-opening brings an overlay forward instead of duplicating it", () => {
|
|
41
|
+
const state = apply(
|
|
42
|
+
{ type: "open", id: "a", layer: "modal" },
|
|
43
|
+
{ type: "open", id: "b", layer: "modal" },
|
|
44
|
+
{ type: "open", id: "a", layer: "modal" }
|
|
45
|
+
);
|
|
46
|
+
expect(state.open.filter((entry) => entry.id === "a")).toHaveLength(1);
|
|
47
|
+
expect(zIndexOfOverlay(state, "a")).toBeGreaterThan(zIndexOfOverlay(state, "b"));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("a z never leaves its layer's band, however many opens came before", () => {
|
|
51
|
+
let state = INITIAL_OVERLAY_REGISTRY;
|
|
52
|
+
for (let i = 0; i < 5000; i++) {
|
|
53
|
+
state = reduceOverlayRegistry(state, { type: "open", id: `m${i}`, layer: "modal" });
|
|
54
|
+
state = reduceOverlayRegistry(state, { type: "close", id: `m${i}` });
|
|
55
|
+
}
|
|
56
|
+
state = reduceOverlayRegistry(state, { type: "open", id: "last", layer: "modal" });
|
|
57
|
+
expect(zIndexOfOverlay(state, "last")).toBeLessThan(OVERLAY_LAYER_BASE.anchored);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("closing an overlay that is not open returns the same state", () => {
|
|
61
|
+
const state = apply({ type: "open", id: "a", layer: "modal" });
|
|
62
|
+
expect(reduceOverlayRegistry(state, { type: "close", id: "nope" })).toBe(state);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a closed overlay has no z", () => {
|
|
66
|
+
const state = apply({ type: "open", id: "a", layer: "modal" }, { type: "close", id: "a" });
|
|
67
|
+
expect(zIndexOfOverlay(state, "a")).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("topOverlay / isTopOverlay", () => {
|
|
72
|
+
test("is null with nothing open", () => {
|
|
73
|
+
expect(topOverlay(INITIAL_OVERLAY_REGISTRY)).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("an anchored overlay over a dialog is the top", () => {
|
|
77
|
+
const state = apply({ type: "open", id: "dialog", layer: "modal" }, { type: "open", id: "pop", layer: "anchored" });
|
|
78
|
+
expect(topOverlay(state)).toBe("pop");
|
|
79
|
+
expect(isTopOverlay(state, "dialog")).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("a toast never captures the back button", () => {
|
|
83
|
+
const state = apply({ type: "open", id: "dialog", layer: "modal" }, { type: "open", id: "toast", layer: "toast" });
|
|
84
|
+
expect(topOverlay(state)).toBe("dialog");
|
|
85
|
+
expect(isTopOverlay(state, "toast")).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("closing the top hands it to the one beneath", () => {
|
|
89
|
+
const state = apply(
|
|
90
|
+
{ type: "open", id: "a", layer: "modal" },
|
|
91
|
+
{ type: "open", id: "b", layer: "modal" },
|
|
92
|
+
{ type: "close", id: "b" }
|
|
93
|
+
);
|
|
94
|
+
expect(isTopOverlay(state, "a")).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/** The band of the z-order an overlay draws in. */
|
|
2
|
+
export type OverlayLayer = "modal" | "anchored" | "toast";
|
|
3
|
+
|
|
4
|
+
export const OVERLAY_LAYERS = ["modal", "anchored", "toast"] as const satisfies readonly OverlayLayer[];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Where each layer's band starts.
|
|
8
|
+
*
|
|
9
|
+
* Every overlay and every bottom sheet teleports into the same `"root"` host, so
|
|
10
|
+
* they are siblings ordered by `zIndex`. The sheet engine stamps its sheets from
|
|
11
|
+
* 1 upward; starting the first band at 1000 puts every overlay above every sheet.
|
|
12
|
+
* A dialog sits under a popover opened from it, and a toast sits above both.
|
|
13
|
+
*/
|
|
14
|
+
export const OVERLAY_LAYER_BASE = {
|
|
15
|
+
modal: 1000,
|
|
16
|
+
anchored: 2000,
|
|
17
|
+
toast: 3000,
|
|
18
|
+
} as const satisfies Record<OverlayLayer, number>;
|
|
19
|
+
|
|
20
|
+
/** The layers whose top overlay answers Android's back button. A toast never does. */
|
|
21
|
+
const BACK_CAPTURING_LAYERS: ReadonlySet<OverlayLayer> = new Set(["modal", "anchored"]);
|
|
22
|
+
|
|
23
|
+
/** One overlay that is presented — open, or animating closed. */
|
|
24
|
+
export type OverlayRegistryEntry = {
|
|
25
|
+
id: string;
|
|
26
|
+
layer: OverlayLayer;
|
|
27
|
+
/** Open order across every layer; later is larger. */
|
|
28
|
+
seq: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type OverlayRegistryState = {
|
|
32
|
+
nextSeq: number;
|
|
33
|
+
open: readonly OverlayRegistryEntry[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const INITIAL_OVERLAY_REGISTRY: OverlayRegistryState = { nextSeq: 1, open: [] };
|
|
37
|
+
|
|
38
|
+
export type OverlayRegistryAction = { type: "open"; id: string; layer: OverlayLayer } | { type: "close"; id: string };
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The registry as a pure reducer, so the z-order is testable without a renderer.
|
|
42
|
+
*
|
|
43
|
+
* An `open` stamps the next sequence number; an overlay already open is
|
|
44
|
+
* re-stamped rather than duplicated, which brings it forward. A `close` of an
|
|
45
|
+
* overlay that is not open returns the same state object, so an unmount racing
|
|
46
|
+
* an exit causes no render.
|
|
47
|
+
*/
|
|
48
|
+
export function reduceOverlayRegistry(
|
|
49
|
+
state: OverlayRegistryState,
|
|
50
|
+
action: OverlayRegistryAction
|
|
51
|
+
): OverlayRegistryState {
|
|
52
|
+
if (action.type === "close") {
|
|
53
|
+
if (!state.open.some((entry) => entry.id === action.id)) return state;
|
|
54
|
+
return { ...state, open: state.open.filter((entry) => entry.id !== action.id) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const others = state.open.filter((entry) => entry.id !== action.id);
|
|
58
|
+
return {
|
|
59
|
+
nextSeq: state.nextSeq + 1,
|
|
60
|
+
open: [...others, { id: action.id, layer: action.layer, seq: state.nextSeq }],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The overlay's `zIndex` while presented, `0` otherwise.
|
|
66
|
+
*
|
|
67
|
+
* Its layer's base plus its rank among the open overlays of that layer — a rank,
|
|
68
|
+
* not the raw sequence number, so a long session's thousandth dialog still sits
|
|
69
|
+
* below the first popover.
|
|
70
|
+
*/
|
|
71
|
+
export function zIndexOfOverlay(state: OverlayRegistryState, id: string): number {
|
|
72
|
+
const entry = state.open.find((candidate) => candidate.id === id);
|
|
73
|
+
if (entry === undefined) return 0;
|
|
74
|
+
let rank = 1;
|
|
75
|
+
for (const other of state.open) {
|
|
76
|
+
if (other.layer === entry.layer && other.seq < entry.seq) rank++;
|
|
77
|
+
}
|
|
78
|
+
return OVERLAY_LAYER_BASE[entry.layer] + rank;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The back-capturing overlay drawn on top, or `null` when none is presented. */
|
|
82
|
+
export function topOverlay(state: OverlayRegistryState): string | null {
|
|
83
|
+
let top: string | null = null;
|
|
84
|
+
let topZ = 0;
|
|
85
|
+
for (const entry of state.open) {
|
|
86
|
+
if (!BACK_CAPTURING_LAYERS.has(entry.layer)) continue;
|
|
87
|
+
const z = zIndexOfOverlay(state, entry.id);
|
|
88
|
+
if (z > topZ) {
|
|
89
|
+
top = entry.id;
|
|
90
|
+
topZ = z;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return top;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Whether `id` is the overlay a back press should close. */
|
|
97
|
+
export function isTopOverlay(state: OverlayRegistryState, id: string): boolean {
|
|
98
|
+
return topOverlay(state) === id;
|
|
99
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Pressable, type PressableProps } from "react-native";
|
|
3
|
+
import Animated, { type SharedValue, useAnimatedStyle } from "react-native-reanimated";
|
|
4
|
+
import { overlayVariants } from "./overlay.variants";
|
|
5
|
+
|
|
6
|
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
7
|
+
|
|
8
|
+
export type OverlayScrimProps = Omit<PressableProps, "style" | "children" | "onPress"> & {
|
|
9
|
+
/** The 0 → 1 presence progress from `useOverlayPresence`; the scrim's opacity follows it. */
|
|
10
|
+
progress: SharedValue<number>;
|
|
11
|
+
/**
|
|
12
|
+
* Called on a tap. Omit it and the scrim still takes the touch — the app
|
|
13
|
+
* under a non-dismissible overlay must not be pressable — but does nothing.
|
|
14
|
+
*/
|
|
15
|
+
onDismiss?: () => void;
|
|
16
|
+
className?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The dim layer between the app and a modal overlay.
|
|
21
|
+
*
|
|
22
|
+
* `bg-overlay` at opacity 1 — the token carries its own alpha — faded in and
|
|
23
|
+
* out by presence. Written before the panel in the same portal, so view order
|
|
24
|
+
* gives a touch on the panel to the panel.
|
|
25
|
+
*
|
|
26
|
+
* Invisible to assistive technology. A scrim is not a control a screen reader
|
|
27
|
+
* user can find; they dismiss through the panel's `onAccessibilityEscape` and
|
|
28
|
+
* the back button instead.
|
|
29
|
+
*
|
|
30
|
+
* React Native's own `Pressable`, not this library's: it needs no feedback, no
|
|
31
|
+
* haptic and no gesture handler — a gesture-handler tap here would race the
|
|
32
|
+
* panel's own pans for the touch.
|
|
33
|
+
*/
|
|
34
|
+
export function OverlayScrim({ progress, onDismiss, className, ...props }: OverlayScrimProps): ReactElement {
|
|
35
|
+
const fade = useAnimatedStyle(() => ({ opacity: progress.value }));
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<AnimatedPressable
|
|
39
|
+
accessibilityElementsHidden
|
|
40
|
+
accessible={false}
|
|
41
|
+
className={overlayVariants().scrim({ className })}
|
|
42
|
+
importantForAccessibility="no-hide-descendants"
|
|
43
|
+
onPress={onDismiss}
|
|
44
|
+
style={fade}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
OverlayScrim.displayName = "DelacourUI.Overlay.Scrim";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { OverlayLayer, OverlayRegistryState } from "./overlay-registry";
|
|
3
|
+
|
|
4
|
+
/** What `OverlayProvider` shares with every overlay beneath it. */
|
|
5
|
+
export type OverlayContextValue = {
|
|
6
|
+
/** The open set and its z-order. A change re-renders every presented portal. */
|
|
7
|
+
state: OverlayRegistryState;
|
|
8
|
+
/** A portal reports that its overlay is presented, and in which layer. */
|
|
9
|
+
present: (id: string, layer: OverlayLayer) => void;
|
|
10
|
+
/** The overlay's portal unmounted. */
|
|
11
|
+
dismissed: (id: string) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Hear every touch that starts anywhere under the provider — the app, a
|
|
14
|
+
* sheet, another overlay — without taking it. A non-modal overlay closes on
|
|
15
|
+
* an outside tap this way and the tap still reaches what was under it.
|
|
16
|
+
* Returns the unsubscribe.
|
|
17
|
+
*/
|
|
18
|
+
subscribeTouchStart: (listener: () => void) => () => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const OverlayContext = createContext<OverlayContextValue | null>(null);
|
|
22
|
+
OverlayContext.displayName = "DelacourUI.Overlay.Context";
|
|
23
|
+
|
|
24
|
+
/** The overlay registry, or `null` outside an `OverlayProvider`. */
|
|
25
|
+
export function useOptionalOverlay(): OverlayContextValue | null {
|
|
26
|
+
return useContext(OverlayContext);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether a teleport `PortalProvider` is already mounted above.
|
|
31
|
+
*
|
|
32
|
+
* Teleport registers its hosts natively by name, so two providers would mean
|
|
33
|
+
* two hosts called `"root"`. `OverlayProvider` and this library's
|
|
34
|
+
* `BottomSheetProvider` both set this when they mount one, and both skip
|
|
35
|
+
* their own when it is already set — which is what makes either mount order
|
|
36
|
+
* work.
|
|
37
|
+
*
|
|
38
|
+
* This file is a leaf on purpose: `bottom-sheet/` imports it across folders,
|
|
39
|
+
* and it imports nothing but React and types, so the import can never close a
|
|
40
|
+
* cycle (package rule 3).
|
|
41
|
+
*/
|
|
42
|
+
export const TeleportProvidedContext = createContext(false);
|
|
43
|
+
TeleportProvidedContext.displayName = "DelacourUI.Overlay.TeleportProvided";
|
|
44
|
+
|
|
45
|
+
/** True beneath a provider that mounted teleport's `PortalProvider`. */
|
|
46
|
+
export function useIsTeleportProvided(): boolean {
|
|
47
|
+
return useContext(TeleportProvidedContext);
|
|
48
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode, useCallback, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
|
+
import { PortalProvider } from "react-native-teleport";
|
|
4
|
+
import {
|
|
5
|
+
OverlayContext,
|
|
6
|
+
type OverlayContextValue,
|
|
7
|
+
TeleportProvidedContext,
|
|
8
|
+
useIsTeleportProvided,
|
|
9
|
+
} from "./overlay.context";
|
|
10
|
+
import { OverlayPortal } from "./overlay-portal";
|
|
11
|
+
import {
|
|
12
|
+
INITIAL_OVERLAY_REGISTRY,
|
|
13
|
+
type OverlayLayer,
|
|
14
|
+
type OverlayRegistryState,
|
|
15
|
+
reduceOverlayRegistry,
|
|
16
|
+
} from "./overlay-registry";
|
|
17
|
+
import { OverlayScrim } from "./overlay-scrim";
|
|
18
|
+
|
|
19
|
+
export type OverlayProviderProps = {
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function OverlayRoot({ children }: OverlayProviderProps): ReactElement {
|
|
24
|
+
const isTeleportProvided = useIsTeleportProvided();
|
|
25
|
+
const current = useRef<OverlayRegistryState>(INITIAL_OVERLAY_REGISTRY);
|
|
26
|
+
const [state, setState] = useState<OverlayRegistryState>(INITIAL_OVERLAY_REGISTRY);
|
|
27
|
+
|
|
28
|
+
const present = useCallback((id: string, layer: OverlayLayer) => {
|
|
29
|
+
current.current = reduceOverlayRegistry(current.current, { type: "open", id, layer });
|
|
30
|
+
setState(current.current);
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const dismissed = useCallback((id: string) => {
|
|
34
|
+
const next = reduceOverlayRegistry(current.current, { type: "close", id });
|
|
35
|
+
if (next === current.current) return;
|
|
36
|
+
current.current = next;
|
|
37
|
+
setState(next);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const touchListeners = useRef(new Set<() => void>());
|
|
41
|
+
const subscribeTouchStart = useCallback((listener: () => void) => {
|
|
42
|
+
touchListeners.current.add(listener);
|
|
43
|
+
return () => {
|
|
44
|
+
touchListeners.current.delete(listener);
|
|
45
|
+
};
|
|
46
|
+
}, []);
|
|
47
|
+
const onTouchStart = useCallback(() => {
|
|
48
|
+
for (const listener of [...touchListeners.current]) listener();
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const value = useMemo<OverlayContextValue>(
|
|
52
|
+
() => ({ state, present, dismissed, subscribeTouchStart }),
|
|
53
|
+
[state, present, dismissed, subscribeTouchStart]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// `onTouchStart` is a bubbling touch event, not a responder claim: it fires
|
|
57
|
+
// for a touch on any descendant in the React tree — teleported overlays
|
|
58
|
+
// included, since teleport leaves the fiber where it was — and never stops
|
|
59
|
+
// the touch reaching its target or a gesture handler.
|
|
60
|
+
const registry = (
|
|
61
|
+
<OverlayContext.Provider value={value}>
|
|
62
|
+
<View onTouchStart={onTouchStart} style={styles.fill}>
|
|
63
|
+
{children}
|
|
64
|
+
</View>
|
|
65
|
+
</OverlayContext.Provider>
|
|
66
|
+
);
|
|
67
|
+
if (isTeleportProvided) return registry;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<PortalProvider>
|
|
71
|
+
<TeleportProvidedContext.Provider value>{registry}</TeleportProvidedContext.Provider>
|
|
72
|
+
</PortalProvider>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const styles = StyleSheet.create({ fill: { flex: 1 } });
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The layer every overlay in this library draws into. Mount it once, inside
|
|
80
|
+
* `DelacourProvider` and around the navigator — and around
|
|
81
|
+
* `BottomSheetProvider` when the app has sheets.
|
|
82
|
+
*
|
|
83
|
+
* Two things. Teleport's `PortalProvider`, whose `"root"` host fills the app
|
|
84
|
+
* and is where every dialog, drawer, popover, tooltip, toast and sheet draws —
|
|
85
|
+
* skipped when a provider above already mounted one, because two would mean
|
|
86
|
+
* two native hosts with one name. And the overlay registry, which gives every
|
|
87
|
+
* presented overlay a `zIndex` in its layer's band and tells each one whether it
|
|
88
|
+
* is the top, the one Android's back button closes. It also hears every touch
|
|
89
|
+
* that starts beneath it, without claiming one, so a tooltip can close on an
|
|
90
|
+
* outside tap that still lands where it was aimed.
|
|
91
|
+
*
|
|
92
|
+
* `DelacourProvider` cannot mount this, for the reason it cannot mount
|
|
93
|
+
* `BottomSheetProvider`: `react-native-teleport` is an optional peer, and an
|
|
94
|
+
* import there would make every app resolve it.
|
|
95
|
+
*
|
|
96
|
+
* `OverlayProvider` is the same component under the name a root layout reads
|
|
97
|
+
* best with; `Overlay.Portal` and `Overlay.Scrim` are the parts every overlay
|
|
98
|
+
* is drawn with.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* <DelacourProvider>
|
|
102
|
+
* <OverlayProvider>
|
|
103
|
+
* <BottomSheetProvider>
|
|
104
|
+
* <Stack />
|
|
105
|
+
* </BottomSheetProvider>
|
|
106
|
+
* </OverlayProvider>
|
|
107
|
+
* </DelacourProvider>
|
|
108
|
+
*/
|
|
109
|
+
export const Overlay = Object.assign(OverlayRoot, {
|
|
110
|
+
/** Teleports its children over the app, in a layer of the z-order. */
|
|
111
|
+
Portal: OverlayPortal,
|
|
112
|
+
/** The dim layer under a modal overlay, faded by presence. */
|
|
113
|
+
Scrim: OverlayScrim,
|
|
114
|
+
displayName: "DelacourUI.Overlay",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/** `Overlay` under the name a root layout reads best with. */
|
|
118
|
+
export const OverlayProvider = Overlay;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { declaredTokens } from "../../styles/theme-tokens.test";
|
|
3
|
+
import { OVERLAY_MOTION, OVERLAY_SCRIM_TOKEN, overlayVariants } from "./overlay.variants";
|
|
4
|
+
|
|
5
|
+
const LIGHT = declaredTokens("light");
|
|
6
|
+
const DARK = declaredTokens("dark");
|
|
7
|
+
|
|
8
|
+
describe("overlayVariants", () => {
|
|
9
|
+
test("the scrim fills its parent in the overlay token", () => {
|
|
10
|
+
const scrim = overlayVariants().scrim();
|
|
11
|
+
expect(scrim).toContain("absolute");
|
|
12
|
+
expect(scrim).toContain("inset-0");
|
|
13
|
+
expect(scrim).toContain(`bg-${OVERLAY_SCRIM_TOKEN}`);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("a caller's className reaches the scrim and wins", () => {
|
|
17
|
+
expect(overlayVariants().scrim({ className: "bg-black/30" })).toContain("bg-black/30");
|
|
18
|
+
expect(overlayVariants().scrim({ className: "bg-black/30" })).not.toContain(`bg-${OVERLAY_SCRIM_TOKEN}`);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("the scrim token exists in both themes", () => {
|
|
22
|
+
expect(LIGHT.has(OVERLAY_SCRIM_TOKEN)).toBe(true);
|
|
23
|
+
expect(DARK.has(OVERLAY_SCRIM_TOKEN)).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("OVERLAY_MOTION", () => {
|
|
28
|
+
test("is finite, so an E2E runner's settle wait always ends", () => {
|
|
29
|
+
expect(Number.isFinite(OVERLAY_MOTION.enterMs)).toBe(true);
|
|
30
|
+
expect(Number.isFinite(OVERLAY_MOTION.exitMs)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("leaves faster than it arrives", () => {
|
|
34
|
+
expect(OVERLAY_MOTION.exitMs).toBeLessThan(OVERLAY_MOTION.enterMs);
|
|
35
|
+
expect(OVERLAY_MOTION.exitMs).toBeGreaterThan(0);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The theme token every overlay's scrim paints with.
|
|
6
|
+
*
|
|
7
|
+
* `--overlay` carries its own alpha, and the two themes carry different ones,
|
|
8
|
+
* so a scrim draws it at opacity 1 and fades only through presence — the same
|
|
9
|
+
* rule `BottomSheet.Overlay` follows.
|
|
10
|
+
*/
|
|
11
|
+
export const OVERLAY_SCRIM_TOKEN = "overlay";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* How long an overlay takes to arrive and to leave, in milliseconds.
|
|
15
|
+
*
|
|
16
|
+
* Leaving is quicker than arriving: an overlay the user dismissed should get
|
|
17
|
+
* out of the way. Both are finite on purpose — overlay motion is behaviour, not
|
|
18
|
+
* decoration, so `isMotionCalm` does not still it, and a finite animation is
|
|
19
|
+
* what lets an E2E runner's settle wait end anyway.
|
|
20
|
+
*/
|
|
21
|
+
export const OVERLAY_MOTION = {
|
|
22
|
+
enterMs: 220,
|
|
23
|
+
exitMs: 160,
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
/** Styling for the foundation's own parts. */
|
|
27
|
+
export const overlayVariants = tv({
|
|
28
|
+
slots: {
|
|
29
|
+
scrim: "absolute inset-0 bg-overlay",
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export type OverlayVariantProps = VariantProps<typeof overlayVariants>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { BackHandler } from "react-native";
|
|
3
|
+
import { useOptionalOverlay } from "./overlay.context";
|
|
4
|
+
import { isTopOverlay } from "./overlay-registry";
|
|
5
|
+
|
|
6
|
+
export type UseOverlayBackHandlerOptions = {
|
|
7
|
+
/** The id the overlay's `Overlay.Portal` was given. */
|
|
8
|
+
id: string;
|
|
9
|
+
/** Listen only while true — typically `isPresent && isDismissible`. */
|
|
10
|
+
isEnabled: boolean;
|
|
11
|
+
onBack: () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Closes the overlay on Android's back button, but only while it is the top
|
|
16
|
+
* one: a popover over a dialog closes, the dialog under it stays.
|
|
17
|
+
*
|
|
18
|
+
* Without an `OverlayProvider` every overlay is its own top. React Native calls
|
|
19
|
+
* the most recently added listener first, and an overlay drawn over a bottom
|
|
20
|
+
* sheet subscribed after it, so the overlay answers before the sheet does.
|
|
21
|
+
*/
|
|
22
|
+
export function useOverlayBackHandler({ id, isEnabled, onBack }: UseOverlayBackHandlerOptions): void {
|
|
23
|
+
const registry = useOptionalOverlay();
|
|
24
|
+
const isTop = registry === null ? true : isTopOverlay(registry.state, id);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!isEnabled || !isTop) return;
|
|
28
|
+
const subscription = BackHandler.addEventListener("hardwareBackPress", () => {
|
|
29
|
+
onBack();
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
return () => subscription.remove();
|
|
33
|
+
}, [isEnabled, isTop, onBack]);
|
|
34
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { Easing, type SharedValue, useReducedMotion, useSharedValue, withTiming } from "react-native-reanimated";
|
|
3
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
4
|
+
import { OVERLAY_MOTION } from "./overlay.variants";
|
|
5
|
+
import { isPresent, type PresenceEvent, type PresencePhase, presenceTarget, reducePresence } from "./overlay-presence";
|
|
6
|
+
|
|
7
|
+
export type UseOverlayPresenceOptions = {
|
|
8
|
+
isOpen: boolean;
|
|
9
|
+
/** Entrance duration, ms. Default `OVERLAY_MOTION.enterMs`. */
|
|
10
|
+
enterMs?: number;
|
|
11
|
+
/** Exit duration, ms. Default `OVERLAY_MOTION.exitMs`. */
|
|
12
|
+
exitMs?: number;
|
|
13
|
+
/** The entrance finished — the moment to move accessibility focus in. */
|
|
14
|
+
onEntered?: () => void;
|
|
15
|
+
/** The exit finished and the overlay is about to unmount. */
|
|
16
|
+
onExited?: () => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type OverlayPresence = {
|
|
20
|
+
/** Keep the overlay mounted while true: from the open until the exit animation ends. */
|
|
21
|
+
isPresent: boolean;
|
|
22
|
+
/** 0 hidden → 1 shown, animated on the UI thread. Drive opacity and transforms from it. */
|
|
23
|
+
progress: SharedValue<number>;
|
|
24
|
+
/** Reduce motion is on: drop scales and translates, keep the fade. */
|
|
25
|
+
isReduced: boolean;
|
|
26
|
+
phase: PresencePhase;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const ENTER_EASING = Easing.out(Easing.cubic);
|
|
30
|
+
const EXIT_EASING = Easing.in(Easing.cubic);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Keeps an overlay mounted through its exit animation and drives its
|
|
34
|
+
* `progress` from 0 to 1 and back.
|
|
35
|
+
*
|
|
36
|
+
* `isOpen` is the owner's state; this follows it through `reducePresence`,
|
|
37
|
+
* which is what lets a re-open during the exit reverse smoothly from wherever
|
|
38
|
+
* the animation is. The finish of each animation reaches React through
|
|
39
|
+
* `scheduleOnRN`, and only when the animation ran to the end — an interrupted
|
|
40
|
+
* one reports nothing, and the reducer drops a stale finish anyway.
|
|
41
|
+
*
|
|
42
|
+
* Under reduce motion the fade still runs — an opacity change is not motion —
|
|
43
|
+
* and `isReduced` tells the component to drop its transforms.
|
|
44
|
+
*/
|
|
45
|
+
export function useOverlayPresence({
|
|
46
|
+
isOpen,
|
|
47
|
+
enterMs = OVERLAY_MOTION.enterMs,
|
|
48
|
+
exitMs = OVERLAY_MOTION.exitMs,
|
|
49
|
+
onEntered,
|
|
50
|
+
onExited,
|
|
51
|
+
}: UseOverlayPresenceOptions): OverlayPresence {
|
|
52
|
+
const [phase, setPhase] = useState<PresencePhase>(isOpen ? "entering" : "closed");
|
|
53
|
+
const progress = useSharedValue(0);
|
|
54
|
+
const isReduced = useReducedMotion();
|
|
55
|
+
|
|
56
|
+
const send = useCallback((event: PresenceEvent) => {
|
|
57
|
+
setPhase((current) => reducePresence(current, event));
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const finishEnter = useCallback(() => {
|
|
61
|
+
send({ type: "entered" });
|
|
62
|
+
onEntered?.();
|
|
63
|
+
}, [send, onEntered]);
|
|
64
|
+
|
|
65
|
+
const finishExit = useCallback(() => {
|
|
66
|
+
send({ type: "exited" });
|
|
67
|
+
onExited?.();
|
|
68
|
+
}, [send, onExited]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
send({ type: isOpen ? "open" : "close" });
|
|
72
|
+
}, [isOpen, send]);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (phase === "entering") {
|
|
76
|
+
progress.value = withTiming(presenceTarget(phase), { duration: enterMs, easing: ENTER_EASING }, (finished) => {
|
|
77
|
+
if (finished) scheduleOnRN(finishEnter);
|
|
78
|
+
});
|
|
79
|
+
} else if (phase === "exiting") {
|
|
80
|
+
progress.value = withTiming(presenceTarget(phase), { duration: exitMs, easing: EXIT_EASING }, (finished) => {
|
|
81
|
+
if (finished) scheduleOnRN(finishExit);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}, [phase, progress, enterMs, exitMs, finishEnter, finishExit]);
|
|
85
|
+
|
|
86
|
+
return { isPresent: isPresent(phase), progress, isReduced, phase };
|
|
87
|
+
}
|