@delacour/react-native-ui 0.1.0-alpha.4 → 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 +10 -6
- package/package.json +48 -4
- 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 -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 +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 +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/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 +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,138 @@
|
|
|
1
|
+
# Popover
|
|
2
|
+
|
|
3
|
+
A small panel anchored to the control that opened it, with the screen around it still visible —
|
|
4
|
+
a rename field beside a title, a note on a badge, a short list of options.
|
|
5
|
+
|
|
6
|
+
`import { Popover, usePopover } from "@delacour/react-native-ui/popover";`
|
|
7
|
+
|
|
8
|
+
It draws through the overlay foundation, so it needs `OverlayProvider` at the app root and
|
|
9
|
+
`react-native-teleport` installed — see [Overlay](../overlay/AGENTS.md). Without the provider it
|
|
10
|
+
renders inline and may be clipped.
|
|
11
|
+
|
|
12
|
+
## Anatomy
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
<Popover isOpen? defaultOpen? onOpenChange? isDismissible?>
|
|
16
|
+
<Popover.Trigger asChild><Button /></Popover.Trigger>
|
|
17
|
+
<Popover.Anchor /> // optional — anchor to this instead of the trigger
|
|
18
|
+
<Popover.Content placement? align? offset? alignOffset? width? minWidth? maxHeight?
|
|
19
|
+
isScrollable? isUnstyled? background? hasScrim? scrimClassName?>
|
|
20
|
+
<Popover.Arrow />
|
|
21
|
+
<Popover.Close />
|
|
22
|
+
<Popover.Title /> <Popover.Description />
|
|
23
|
+
…
|
|
24
|
+
</Popover.Content>
|
|
25
|
+
</Popover>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`usePopover()` returns `{ isOpen, setOpen, close, placement }` — `placement` is the side the panel
|
|
29
|
+
actually landed on, after any flip.
|
|
30
|
+
|
|
31
|
+
## Files
|
|
32
|
+
|
|
33
|
+
| File | What it holds |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `index.ts` | → `@delacour/react-native-ui/popover` |
|
|
36
|
+
| `popover.tsx` | `Popover` — open state, the two anchor measures, the context; the `Object.assign` names every part |
|
|
37
|
+
| `popover.context.tsx` | **Leaf.** `PopoverContext`, `usePopover`, and the content context the arrow and title read |
|
|
38
|
+
| `popover.position.ts` | **Leaf, pure, imports nothing.** `resolveAnchoredPosition`, `resolvePopoverWidth`, `resolveEnterTranslate`, `resolveTransformOrigin`, `resolveArrowFrame` and their types |
|
|
39
|
+
| `popover.position.test.ts` | The placement matrix — every placement and align, flip and no-flip, shift at each edge, `maxHeight`, the arrow, RTL, `alignOffset`, widths, motion geometry |
|
|
40
|
+
| `popover.variants.ts` | The slotted `tv()` — `scrim`, `dismissLayer`, `content`, `arrow`, `title`, `description`, `close` — and the numeric constants |
|
|
41
|
+
| `popover.variants.test.ts` | Slots, `isUnstyled`, tokens in both themes, the arrow inset against `--radius` |
|
|
42
|
+
| `use-anchor-measure.ts` | **Leaf.** `useAnchorMeasure` — `measureInWindow` on enable and on window-size change |
|
|
43
|
+
| `use-anchored-content.ts` | **Leaf.** `useAnchoredContent` — the off-screen measure frame, the resolve, presence, the animated style |
|
|
44
|
+
| `popover-arrow.tsx` | **Leaf.** `AnchoredArrow` (placement passed in) and `Popover.Arrow` (placement read from the panel) |
|
|
45
|
+
| `popover-trigger.tsx` | `Popover.Trigger` — `Pressable`, or `asChild` to donate the press; the default anchor |
|
|
46
|
+
| `popover-anchor.tsx` | `Popover.Anchor` — a non-collapsing `View`, or `asChild` |
|
|
47
|
+
| `popover-content.tsx` | `Popover.Content` — the portal, the dismiss layer or scrim, the panel |
|
|
48
|
+
| `popover-title.tsx` | `Popover.Title` — `Text.Label` as a header, the panel's label |
|
|
49
|
+
| `popover-description.tsx` | `Popover.Description` — `Text.Caption` |
|
|
50
|
+
| `popover-close.tsx` | `Popover.Close` — the corner ✕, or `asChild` around a button |
|
|
51
|
+
|
|
52
|
+
## The anchored leaves — Tooltip imports them
|
|
53
|
+
|
|
54
|
+
Tooltip, and later Menu, Context Menu and Select, are anchored panels too. They import
|
|
55
|
+
`popover.position.ts`, `use-anchor-measure.ts`, `use-anchored-content.ts` and `popover-arrow.tsx`
|
|
56
|
+
**directly**, never `./popover` or `./index` — package rule 3's leaf exception. **None of the four
|
|
57
|
+
may import `./popover`, `./index` or any part file.** `popover-arrow.tsx` reads
|
|
58
|
+
`popover.context.tsx`, itself a leaf; the other three import only `popover.position.ts`, the
|
|
59
|
+
variants and the overlay foundation. Break that and a Tooltip import closes a cycle Metro serves
|
|
60
|
+
half-initialised.
|
|
61
|
+
|
|
62
|
+
## Design
|
|
63
|
+
|
|
64
|
+
- **Placement is a preference, and the rules are a test each.** `resolveAnchoredPosition` keeps
|
|
65
|
+
the preferred side, and flips to the opposite **only** when the preferred side cannot hold the
|
|
66
|
+
panel *and* the opposite has more room — a panel that fits nowhere does not ping-pong to a side
|
|
67
|
+
that is just as bad. Then it aligns along the cross axis, shifts to stay inside
|
|
68
|
+
`window − safe area − 8pt`, and clamps both axes, so it is never off-screen. `maxHeight` is
|
|
69
|
+
`min(maxHeight, room on the resolved side)`, which is what keeps a tall panel on screen.
|
|
70
|
+
- **`align` is logical on top and bottom.** Under RTL `start` is the right edge; on a side
|
|
71
|
+
placement it is always the top. `alignOffset` nudges inward from whichever edge is aligned.
|
|
72
|
+
- **The arrow points at the anchor's centre, not the panel's.** After a shift the panel is no
|
|
73
|
+
longer centred on its trigger; the arrow still is, clamped `POPOVER_ARROW_INSET` clear of each
|
|
74
|
+
corner so it never hangs off the curve. The inset is the card radius plus the arrow's
|
|
75
|
+
half-diagonal, pinned against `tokens.css`'s `--radius` by a test.
|
|
76
|
+
- **The arrow is a square turned 45°, bordered on two edges.** `resolveArrowFrame` turns its
|
|
77
|
+
bordered corner toward the anchor in each placement, so the two bordered edges are the ones
|
|
78
|
+
outside the panel and the bare inner half covers the panel's own border where they meet.
|
|
79
|
+
- **Measured in window coordinates, placed by translate.** `measureInWindow` is what makes a
|
|
80
|
+
trigger inside a `ScrollView`, a bottom sheet or under the header land the panel in the right
|
|
81
|
+
place — the panel is teleported to a host that fills the window. The panel sits at the window's
|
|
82
|
+
origin and moves by `translateX/Y`, never `left`/`top`: a translate is not layout, so moving it
|
|
83
|
+
never re-wraps a content-fit panel and never fires another `onLayout`.
|
|
84
|
+
- **The panel sits inside a positioner, and the safe-span cap is on the positioner.** The
|
|
85
|
+
positioner is measured, translated and animated, and carries `maxWidth` = the safe span; the
|
|
86
|
+
panel inside it carries the classes and the resolved width and `maxHeight`. An inline
|
|
87
|
+
`maxWidth` on the panel itself beat every class, so a caller's `max-w-64` was silently ignored
|
|
88
|
+
— found on a simulator with the `arrow` demo.
|
|
89
|
+
- **One invisible frame before the entrance.** The panel mounts at opacity 0, reports its size,
|
|
90
|
+
is resolved, and only then is presence told to open — so the entrance always starts from the
|
|
91
|
+
right side and the right place. Presence follows `isOpen && position !== null`; the exit does
|
|
92
|
+
not wait for anything.
|
|
93
|
+
- **The entrance comes from the anchor.** A 6pt slide back toward the anchor and a `0.96 → 1`
|
|
94
|
+
scale about the arrow (`transformOrigin`), driven by presence's `progress`. Under reduce motion
|
|
95
|
+
both collapse and only the fade runs. A window-size change — a rotation — re-measures the anchor
|
|
96
|
+
a frame later and moves the panel there directly, with no animation.
|
|
97
|
+
- **The keyboard is the bottom of the screen.** `useAnchoredContent` tracks keyboard-controller's
|
|
98
|
+
show/hide events and takes `max(safe bottom, keyboard height)` as the bottom inset, so a field
|
|
99
|
+
inside the panel flips it above its trigger instead of typing under the keyboard.
|
|
100
|
+
- **Outside taps close, and are swallowed.** Under the panel sits an invisible absolute-fill
|
|
101
|
+
React Native `Pressable` — or `Overlay.Scrim` with `hasScrim` — that closes on a tap when
|
|
102
|
+
dismissible. It never passes the tap through: the mobile convention, and the reason a tap
|
|
103
|
+
outside never presses the button under it. With `isDismissible={false}` it still takes the
|
|
104
|
+
touch and does nothing, for the scrim's reason.
|
|
105
|
+
- **`width`.** `"trigger"` is the anchor's measured width, `"full"` the safe span, a number as
|
|
106
|
+
given, each raised to `minWidth` and capped at the safe span; `"content-fit"` (the default)
|
|
107
|
+
lets the content size itself, with `minWidth` as a floor and the safe span as `maxWidth`.
|
|
108
|
+
- **`isScrollable` wraps the body in a `ScrollView`, and lifts `Popover.Arrow` out of it** —
|
|
109
|
+
detected by element type — so the arrow is never scrolled or clipped. Without it a body taller
|
|
110
|
+
than `maxHeight` is cut off.
|
|
111
|
+
- **`isUnstyled` strips the surface, the border, the corner and the padding** of the panel and the
|
|
112
|
+
arrow's paint, keeping the gap. `background` is drawn absolutely behind the content, for a
|
|
113
|
+
caller painting their own surface.
|
|
114
|
+
- **Triggers donate the press.** `Popover.Trigger asChild` hands the toggle to the child's
|
|
115
|
+
`onPress` and composes the measuring ref onto the child's, for `BottomSheet.Trigger`'s reason —
|
|
116
|
+
so the child must be built on `Pressable`. It reports `accessibilityState.expanded`.
|
|
117
|
+
- **`Popover.Anchor` takes the measuring from the trigger while it is mounted.** The root keeps one
|
|
118
|
+
`useAnchorMeasure` per candidate and enables only the one in use; the trigger still opens the
|
|
119
|
+
panel and still takes focus back.
|
|
120
|
+
- **Modal for assistive technology.** The panel is `accessibilityViewIsModal`, `role="dialog"`,
|
|
121
|
+
labelled by the title's `nativeID`; on entry focus goes to the title (or the panel), on exit back
|
|
122
|
+
to the trigger, and `onAccessibilityEscape` closes it. Android back closes it while it is the top
|
|
123
|
+
overlay (`useOverlayBackHandler`).
|
|
124
|
+
- **Draws in the `anchored` band.** Over every sheet and over the dialog it was opened from — the
|
|
125
|
+
foundation's z-order.
|
|
126
|
+
|
|
127
|
+
## Out of scope
|
|
128
|
+
|
|
129
|
+
- A sheet presentation on small screens — compose `BottomSheet` instead.
|
|
130
|
+
- A frosted backdrop — needs `expo-blur`, not a peer.
|
|
131
|
+
- Following an anchor that scrolls while the panel is open — the panel stays where it opened.
|
|
132
|
+
|
|
133
|
+
## Testing
|
|
134
|
+
|
|
135
|
+
`bun test` reaches the position resolver and the variants. Everything else — the measure frame,
|
|
136
|
+
the motion, the keyboard, dismissal, focus — is verified on a simulator through
|
|
137
|
+
`apps/playground`'s `/popover` gallery, whose `edge-collision` demo puts triggers in the four
|
|
138
|
+
corners to prove flip and shift.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export { Popover, type PopoverProps } from "./popover";
|
|
2
|
+
export {
|
|
3
|
+
PopoverContext,
|
|
4
|
+
type PopoverContextValue,
|
|
5
|
+
usePopover,
|
|
6
|
+
} from "./popover.context";
|
|
7
|
+
export {
|
|
8
|
+
type AnchoredBounds,
|
|
9
|
+
type AnchoredInput,
|
|
10
|
+
type AnchoredInsets,
|
|
11
|
+
type AnchoredPosition,
|
|
12
|
+
type AnchoredSize,
|
|
13
|
+
type AnchorRect,
|
|
14
|
+
type PopoverAlign,
|
|
15
|
+
type PopoverPlacement,
|
|
16
|
+
type PopoverWidth,
|
|
17
|
+
type ResolvedPopoverWidth,
|
|
18
|
+
resolveAnchoredPosition,
|
|
19
|
+
resolveArrowFrame,
|
|
20
|
+
resolveEnterTranslate,
|
|
21
|
+
resolvePopoverWidth,
|
|
22
|
+
resolveTransformOrigin,
|
|
23
|
+
} from "./popover.position";
|
|
24
|
+
export {
|
|
25
|
+
POPOVER_ARROW_INSET,
|
|
26
|
+
POPOVER_ARROW_SIZE,
|
|
27
|
+
POPOVER_CLOSE_HIT_SLOP,
|
|
28
|
+
POPOVER_COLLISION_PADDING,
|
|
29
|
+
POPOVER_DEFAULTS,
|
|
30
|
+
POPOVER_ENTER_DISTANCE,
|
|
31
|
+
POPOVER_ENTER_SCALE,
|
|
32
|
+
type PopoverVariantProps,
|
|
33
|
+
popoverVariants,
|
|
34
|
+
} from "./popover.variants";
|
|
35
|
+
export type { PopoverAnchorProps } from "./popover-anchor";
|
|
36
|
+
export type { AnchoredArrowProps, PopoverArrowProps } from "./popover-arrow";
|
|
37
|
+
export type { PopoverCloseProps } from "./popover-close";
|
|
38
|
+
export type { PopoverContentProps } from "./popover-content";
|
|
39
|
+
export type { PopoverDescriptionProps } from "./popover-description";
|
|
40
|
+
export type { PopoverTitleProps } from "./popover-title";
|
|
41
|
+
export type { PopoverTriggerProps } from "./popover-trigger";
|
|
42
|
+
export {
|
|
43
|
+
type AnchorMeasure,
|
|
44
|
+
type MeasurableNode,
|
|
45
|
+
type UseAnchorMeasureOptions,
|
|
46
|
+
useAnchorMeasure,
|
|
47
|
+
} from "./use-anchor-measure";
|
|
48
|
+
export { type AnchoredContent, type UseAnchoredContentOptions, useAnchoredContent } from "./use-anchored-content";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type ReactElement, useEffect } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { Slot } from "../../lib/slot";
|
|
4
|
+
import { usePopoverContext } from "./popover.context";
|
|
5
|
+
|
|
6
|
+
export type PopoverAnchorProps = ViewProps & {
|
|
7
|
+
className?: string;
|
|
8
|
+
/** Anchor to the single child itself rather than a wrapping view. */
|
|
9
|
+
asChild?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Anchors the panel to a different view than the trigger — a whole row whose
|
|
14
|
+
* trailing button opens it, a field whose icon does.
|
|
15
|
+
*
|
|
16
|
+
* While it is mounted the panel measures this instead of the trigger; the
|
|
17
|
+
* trigger still opens it and still takes focus back on close. Without
|
|
18
|
+
* `asChild` it is a plain `View` that never collapses, so it always has a
|
|
19
|
+
* native frame to measure.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* <Popover>
|
|
23
|
+
* <Popover.Anchor className="flex-row items-center gap-2">
|
|
24
|
+
* <Input value={tag} />
|
|
25
|
+
* <Popover.Trigger asChild><Button size="icon-md" variant="ghost">…</Button></Popover.Trigger>
|
|
26
|
+
* </Popover.Anchor>
|
|
27
|
+
* <Popover.Content width="trigger">…</Popover.Content>
|
|
28
|
+
* </Popover>
|
|
29
|
+
*/
|
|
30
|
+
export function PopoverAnchor({ asChild = false, children, ...props }: PopoverAnchorProps): ReactElement {
|
|
31
|
+
const { anchorRef, registerAnchor } = usePopoverContext();
|
|
32
|
+
|
|
33
|
+
useEffect(() => registerAnchor(), [registerAnchor]);
|
|
34
|
+
|
|
35
|
+
if (asChild) {
|
|
36
|
+
return (
|
|
37
|
+
<Slot {...props} ref={anchorRef}>
|
|
38
|
+
{children}
|
|
39
|
+
</Slot>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<View collapsable={false} {...props} ref={anchorRef}>
|
|
45
|
+
{children}
|
|
46
|
+
</View>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
PopoverAnchor.displayName = "DelacourUI.Popover.Anchor";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { useOptionalPopoverContent } from "./popover.context";
|
|
4
|
+
import { type AnchoredSize, type PopoverPlacement, resolveArrowFrame } from "./popover.position";
|
|
5
|
+
import { POPOVER_ARROW_SIZE, popoverVariants } from "./popover.variants";
|
|
6
|
+
|
|
7
|
+
export type AnchoredArrowProps = {
|
|
8
|
+
/** The side the panel was placed on — the arrow sits on the edge facing the anchor. */
|
|
9
|
+
placement: PopoverPlacement;
|
|
10
|
+
/** From `resolveAnchoredPosition`: along that edge, toward the anchor's centre. */
|
|
11
|
+
arrowOffset: number;
|
|
12
|
+
/** The panel's measured size. */
|
|
13
|
+
size: AnchoredSize;
|
|
14
|
+
/** Strips the arrow's fill and border, for a panel drawn with its own `background`. */
|
|
15
|
+
isUnstyled?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A square turned 45°, half of it past the panel's edge, pointing at the anchor.
|
|
21
|
+
*
|
|
22
|
+
* It wears the panel's fill and the panel's border on its two outer edges, and
|
|
23
|
+
* its inner half lies over the panel — covering the panel's own border where
|
|
24
|
+
* the two meet, so the arrow reads as part of the panel rather than a tab stuck
|
|
25
|
+
* to it. `resolveArrowFrame` turns it per placement.
|
|
26
|
+
*
|
|
27
|
+
* Hidden from assistive technology: it is decoration.
|
|
28
|
+
*
|
|
29
|
+
* **A leaf.** Tooltip imports it with its own placement; it imports nothing
|
|
30
|
+
* from `./popover` or `./index`.
|
|
31
|
+
*/
|
|
32
|
+
export function AnchoredArrow({
|
|
33
|
+
placement,
|
|
34
|
+
arrowOffset,
|
|
35
|
+
size,
|
|
36
|
+
isUnstyled = false,
|
|
37
|
+
className,
|
|
38
|
+
}: AnchoredArrowProps): ReactElement {
|
|
39
|
+
const frame = resolveArrowFrame(placement, arrowOffset, size, POPOVER_ARROW_SIZE);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<View
|
|
43
|
+
accessibilityElementsHidden
|
|
44
|
+
accessible={false}
|
|
45
|
+
className={popoverVariants({ isUnstyled }).arrow({ className })}
|
|
46
|
+
importantForAccessibility="no-hide-descendants"
|
|
47
|
+
pointerEvents="none"
|
|
48
|
+
style={{
|
|
49
|
+
left: frame.left,
|
|
50
|
+
top: frame.top,
|
|
51
|
+
width: POPOVER_ARROW_SIZE,
|
|
52
|
+
height: POPOVER_ARROW_SIZE,
|
|
53
|
+
transform: [{ rotate: `${frame.rotate}deg` }],
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
AnchoredArrow.displayName = "DelacourUI.Popover.AnchoredArrow";
|
|
59
|
+
|
|
60
|
+
export type PopoverArrowProps = {
|
|
61
|
+
className?: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The panel's arrow. Write it anywhere inside `Popover.Content` — it reads the
|
|
66
|
+
* resolved placement and offset from the panel, follows a flip, and is lifted
|
|
67
|
+
* out of a scrollable body so it is never scrolled or clipped.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* <Popover.Content>
|
|
71
|
+
* <Popover.Arrow />
|
|
72
|
+
* <Popover.Title>Rename</Popover.Title>
|
|
73
|
+
* </Popover.Content>
|
|
74
|
+
*/
|
|
75
|
+
export function PopoverArrow({ className }: PopoverArrowProps): ReactElement | null {
|
|
76
|
+
const content = useOptionalPopoverContent();
|
|
77
|
+
if (content === null) return null;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<AnchoredArrow
|
|
81
|
+
arrowOffset={content.arrowOffset}
|
|
82
|
+
className={className}
|
|
83
|
+
isUnstyled={content.isUnstyled}
|
|
84
|
+
placement={content.placement}
|
|
85
|
+
size={content.size}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
PopoverArrow.displayName = "DelacourUI.Popover.Arrow";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { IconCrossSmall } from "../../icons/central";
|
|
3
|
+
import { Slot } from "../../lib/slot";
|
|
4
|
+
import { Icon } from "../icon";
|
|
5
|
+
import { Pressable, type PressableProps } from "../pressable";
|
|
6
|
+
import { usePopoverContext } from "./popover.context";
|
|
7
|
+
import { POPOVER_CLOSE_HIT_SLOP, popoverVariants } from "./popover.variants";
|
|
8
|
+
|
|
9
|
+
export type PopoverCloseProps =
|
|
10
|
+
| ({ asChild: true; children: ReactElement } & Omit<PressableProps, "asChild" | "children">)
|
|
11
|
+
| ({ asChild?: false; accessibilityLabel?: string } & Omit<PressableProps, "asChild" | "children">);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Closes the popover.
|
|
15
|
+
*
|
|
16
|
+
* Without `asChild` it is the ✕ in the panel's top-right corner: out of the
|
|
17
|
+
* flow, `fade` feedback and an 8pt slop for `BottomSheet.Close`'s reasons, and
|
|
18
|
+
* `Popover.Title` reserves its clearance. With `asChild` it donates the close to
|
|
19
|
+
* its child's `onPress` — a "Done" button in the panel's footer.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* <Popover.Close />
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* <Popover.Close asChild>
|
|
26
|
+
* <Button size="sm">Save</Button>
|
|
27
|
+
* </Popover.Close>
|
|
28
|
+
*/
|
|
29
|
+
export function PopoverClose(props: PopoverCloseProps): ReactElement {
|
|
30
|
+
const { close } = usePopoverContext();
|
|
31
|
+
|
|
32
|
+
if (props.asChild) {
|
|
33
|
+
const { asChild: _asChild, children, onPress, ...rest } = props;
|
|
34
|
+
const press = () => {
|
|
35
|
+
close();
|
|
36
|
+
onPress?.();
|
|
37
|
+
};
|
|
38
|
+
return (
|
|
39
|
+
<Slot {...rest} onPress={press}>
|
|
40
|
+
{children}
|
|
41
|
+
</Slot>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
asChild: _asChild,
|
|
47
|
+
accessibilityLabel = "Close",
|
|
48
|
+
className,
|
|
49
|
+
feedback = "fade",
|
|
50
|
+
hitSlop = POPOVER_CLOSE_HIT_SLOP,
|
|
51
|
+
onPress,
|
|
52
|
+
...rest
|
|
53
|
+
} = props;
|
|
54
|
+
const press = () => {
|
|
55
|
+
close();
|
|
56
|
+
onPress?.();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Pressable
|
|
61
|
+
accessibilityLabel={accessibilityLabel}
|
|
62
|
+
accessibilityRole="button"
|
|
63
|
+
className={popoverVariants().close({ className })}
|
|
64
|
+
feedback={feedback}
|
|
65
|
+
hitSlop={hitSlop}
|
|
66
|
+
onPress={press}
|
|
67
|
+
{...rest}
|
|
68
|
+
>
|
|
69
|
+
<Icon color="muted-foreground" icon={IconCrossSmall} />
|
|
70
|
+
</Pressable>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
PopoverClose.displayName = "DelacourUI.Popover.Close";
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
isValidElement,
|
|
4
|
+
type ReactElement,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useId,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { AccessibilityInfo, Pressable as NativePressable, ScrollView, View, type ViewProps } from "react-native";
|
|
13
|
+
import Animated from "react-native-reanimated";
|
|
14
|
+
import { Overlay, useOverlayBackHandler } from "../overlay";
|
|
15
|
+
import { PopoverContentContext, type PopoverContentContextValue, usePopoverContext } from "./popover.context";
|
|
16
|
+
import type { PopoverAlign, PopoverPlacement, PopoverWidth } from "./popover.position";
|
|
17
|
+
import {
|
|
18
|
+
POPOVER_ARROW_INSET,
|
|
19
|
+
POPOVER_COLLISION_PADDING,
|
|
20
|
+
POPOVER_DEFAULTS,
|
|
21
|
+
POPOVER_ENTER_DISTANCE,
|
|
22
|
+
POPOVER_ENTER_SCALE,
|
|
23
|
+
popoverVariants,
|
|
24
|
+
} from "./popover.variants";
|
|
25
|
+
import { PopoverArrow } from "./popover-arrow";
|
|
26
|
+
import type { MeasurableNode } from "./use-anchor-measure";
|
|
27
|
+
import { useAnchoredContent } from "./use-anchored-content";
|
|
28
|
+
|
|
29
|
+
export type PopoverContentProps = ViewProps & {
|
|
30
|
+
className?: string;
|
|
31
|
+
/** The side of the anchor the panel prefers. It flips when that side lacks room. Default `"bottom"`. */
|
|
32
|
+
placement?: PopoverPlacement;
|
|
33
|
+
/** Which edges line up along the cross axis; logical under RTL. Default `"center"`. */
|
|
34
|
+
align?: PopoverAlign;
|
|
35
|
+
/** The gap between anchor and panel, in points. Default 8. */
|
|
36
|
+
offset?: number;
|
|
37
|
+
/** A nudge along the cross axis, inward from the aligned edge. Default 0. */
|
|
38
|
+
alignOffset?: number;
|
|
39
|
+
/** A number, the anchor's width, the content's own, or the safe span. Default `"content-fit"`. */
|
|
40
|
+
width?: PopoverWidth;
|
|
41
|
+
minWidth?: number;
|
|
42
|
+
/** Clamped to the room on the resolved side. */
|
|
43
|
+
maxHeight?: number;
|
|
44
|
+
/** Scroll the body when it is taller than `maxHeight`. Default false. */
|
|
45
|
+
isScrollable?: boolean;
|
|
46
|
+
/** No surface, border, corner or padding — draw your own with `background`. */
|
|
47
|
+
isUnstyled?: boolean;
|
|
48
|
+
/** Drawn behind the content, filling the panel. */
|
|
49
|
+
background?: ReactNode;
|
|
50
|
+
/** Dim the app behind the panel. Default false — an outside tap still closes it. */
|
|
51
|
+
hasScrim?: boolean;
|
|
52
|
+
scrimClassName?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Pulls `Popover.Arrow` out of the body, so a scrolling body never scrolls or clips it. */
|
|
56
|
+
function partitionArrow(children: ReactNode): { arrows: ReactNode[]; body: ReactNode[] } {
|
|
57
|
+
const arrows: ReactNode[] = [];
|
|
58
|
+
const body: ReactNode[] = [];
|
|
59
|
+
Children.forEach(children, (child) => {
|
|
60
|
+
if (isValidElement(child) && child.type === PopoverArrow) arrows.push(child);
|
|
61
|
+
else body.push(child);
|
|
62
|
+
});
|
|
63
|
+
return { arrows, body };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The panel, drawn over the app and anchored to the trigger.
|
|
68
|
+
*
|
|
69
|
+
* Renders nothing while closed. On open it mounts in the `anchored` band of
|
|
70
|
+
* the overlay z-order — above every sheet and dialog — measures itself
|
|
71
|
+
* invisibly, resolves where it fits, and enters from its resolved side. The
|
|
72
|
+
* placement is a preference: the panel flips across the anchor when that side
|
|
73
|
+
* lacks room and slides along it to stay inside the safe area and above the
|
|
74
|
+
* keyboard.
|
|
75
|
+
*
|
|
76
|
+
* Under the panel, an invisible layer the size of the screen — or the scrim,
|
|
77
|
+
* with `hasScrim` — closes the popover on a tap when it is dismissible. It
|
|
78
|
+
* swallows the tap rather than passing it through to what is under it. Android
|
|
79
|
+
* back closes it while it is the top overlay.
|
|
80
|
+
*
|
|
81
|
+
* The panel is a modal view labelled by its title: VoiceOver focus is held
|
|
82
|
+
* inside it, moves to the title (or the panel) on entry and back to the
|
|
83
|
+
* trigger on exit, and the escape gesture closes it.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* <Popover.Content align="start" width="trigger" minWidth={260}>
|
|
87
|
+
* <Popover.Arrow />
|
|
88
|
+
* <Popover.Title>Rename</Popover.Title>
|
|
89
|
+
* <Input value={name} onChangeText={setName} />
|
|
90
|
+
* </Popover.Content>
|
|
91
|
+
*/
|
|
92
|
+
export function PopoverContent({
|
|
93
|
+
children,
|
|
94
|
+
className,
|
|
95
|
+
placement = POPOVER_DEFAULTS.placement,
|
|
96
|
+
align = POPOVER_DEFAULTS.align,
|
|
97
|
+
offset = POPOVER_DEFAULTS.offset,
|
|
98
|
+
alignOffset = POPOVER_DEFAULTS.alignOffset,
|
|
99
|
+
width = POPOVER_DEFAULTS.width,
|
|
100
|
+
minWidth,
|
|
101
|
+
maxHeight,
|
|
102
|
+
isScrollable = false,
|
|
103
|
+
isUnstyled = false,
|
|
104
|
+
background,
|
|
105
|
+
hasScrim = false,
|
|
106
|
+
scrimClassName,
|
|
107
|
+
style,
|
|
108
|
+
...props
|
|
109
|
+
}: PopoverContentProps): ReactElement | null {
|
|
110
|
+
const { isOpen, close, isDismissible, anchorRect, triggerNode, onPlaced } = usePopoverContext();
|
|
111
|
+
const overlayId = useId();
|
|
112
|
+
const titleId = useId();
|
|
113
|
+
const panelNode = useRef<MeasurableNode | null>(null);
|
|
114
|
+
const titleNode = useRef<MeasurableNode | null>(null);
|
|
115
|
+
|
|
116
|
+
const focusIn = useCallback(() => {
|
|
117
|
+
const target = titleNode.current ?? panelNode.current;
|
|
118
|
+
if (target !== null) AccessibilityInfo.sendAccessibilityEvent(target, "focus");
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
const focusBack = useCallback(() => {
|
|
122
|
+
if (triggerNode.current !== null) AccessibilityInfo.sendAccessibilityEvent(triggerNode.current, "focus");
|
|
123
|
+
}, [triggerNode]);
|
|
124
|
+
|
|
125
|
+
const anchored = useAnchoredContent({
|
|
126
|
+
isOpen,
|
|
127
|
+
anchor: anchorRect,
|
|
128
|
+
placement,
|
|
129
|
+
align,
|
|
130
|
+
offset,
|
|
131
|
+
alignOffset,
|
|
132
|
+
collisionPadding: POPOVER_COLLISION_PADDING,
|
|
133
|
+
arrowInset: POPOVER_ARROW_INSET,
|
|
134
|
+
width,
|
|
135
|
+
minWidth,
|
|
136
|
+
maxHeight,
|
|
137
|
+
enterDistance: POPOVER_ENTER_DISTANCE,
|
|
138
|
+
enterScale: POPOVER_ENTER_SCALE,
|
|
139
|
+
onEntered: focusIn,
|
|
140
|
+
onExited: focusBack,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const resolvedPlacement = anchored.position?.placement ?? placement;
|
|
144
|
+
useEffect(() => onPlaced(resolvedPlacement), [onPlaced, resolvedPlacement]);
|
|
145
|
+
|
|
146
|
+
const dismiss = useCallback(() => {
|
|
147
|
+
if (isDismissible) close();
|
|
148
|
+
}, [isDismissible, close]);
|
|
149
|
+
|
|
150
|
+
useOverlayBackHandler({ id: overlayId, isEnabled: anchored.isMounted && isDismissible, onBack: close });
|
|
151
|
+
|
|
152
|
+
const titleRef = useCallback((node: MeasurableNode | null) => {
|
|
153
|
+
titleNode.current = node;
|
|
154
|
+
}, []);
|
|
155
|
+
const panelRef = useCallback((node: MeasurableNode | null) => {
|
|
156
|
+
panelNode.current = node;
|
|
157
|
+
}, []);
|
|
158
|
+
|
|
159
|
+
const contentContext = useMemo<PopoverContentContextValue>(
|
|
160
|
+
() => ({
|
|
161
|
+
placement: resolvedPlacement,
|
|
162
|
+
arrowOffset: anchored.position?.arrowOffset ?? 0,
|
|
163
|
+
size: anchored.size ?? { width: 0, height: 0 },
|
|
164
|
+
isUnstyled,
|
|
165
|
+
titleId,
|
|
166
|
+
titleRef,
|
|
167
|
+
}),
|
|
168
|
+
[resolvedPlacement, anchored.position?.arrowOffset, anchored.size, isUnstyled, titleId, titleRef]
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
if (!anchored.isMounted) return null;
|
|
172
|
+
|
|
173
|
+
const slots = popoverVariants({ isUnstyled });
|
|
174
|
+
const { arrows, body } = partitionArrow(children);
|
|
175
|
+
|
|
176
|
+
// The catcher is written before the panel, so view order gives a touch on
|
|
177
|
+
// the panel to the panel; it is React Native's own Pressable for the scrim's
|
|
178
|
+
// reason — no feedback, no haptic, no gesture to race.
|
|
179
|
+
return (
|
|
180
|
+
<Overlay.Portal id={overlayId} layer="anchored">
|
|
181
|
+
{hasScrim ? (
|
|
182
|
+
<Overlay.Scrim
|
|
183
|
+
className={slots.scrim({ className: scrimClassName })}
|
|
184
|
+
onDismiss={dismiss}
|
|
185
|
+
progress={anchored.presence.progress}
|
|
186
|
+
/>
|
|
187
|
+
) : (
|
|
188
|
+
<NativePressable
|
|
189
|
+
accessibilityElementsHidden
|
|
190
|
+
accessible={false}
|
|
191
|
+
className={slots.dismissLayer()}
|
|
192
|
+
importantForAccessibility="no-hide-descendants"
|
|
193
|
+
onPress={dismiss}
|
|
194
|
+
/>
|
|
195
|
+
)}
|
|
196
|
+
<Animated.View
|
|
197
|
+
onLayout={anchored.onLayout}
|
|
198
|
+
pointerEvents="box-none"
|
|
199
|
+
style={[anchored.positionerStyle, anchored.animatedStyle]}
|
|
200
|
+
>
|
|
201
|
+
<View
|
|
202
|
+
accessibilityLabelledBy={titleId}
|
|
203
|
+
accessibilityViewIsModal
|
|
204
|
+
className={slots.content({ className })}
|
|
205
|
+
onAccessibilityEscape={isDismissible ? close : undefined}
|
|
206
|
+
ref={panelRef}
|
|
207
|
+
role="dialog"
|
|
208
|
+
style={[anchored.frameStyle, style]}
|
|
209
|
+
{...props}
|
|
210
|
+
>
|
|
211
|
+
<PopoverContentContext.Provider value={contentContext}>
|
|
212
|
+
{background === undefined ? null : (
|
|
213
|
+
<View className="absolute inset-0" pointerEvents="none">
|
|
214
|
+
{background}
|
|
215
|
+
</View>
|
|
216
|
+
)}
|
|
217
|
+
{isScrollable ? (
|
|
218
|
+
<ScrollView
|
|
219
|
+
className="shrink grow-0"
|
|
220
|
+
contentContainerClassName="gap-2"
|
|
221
|
+
keyboardShouldPersistTaps="handled"
|
|
222
|
+
>
|
|
223
|
+
{body}
|
|
224
|
+
</ScrollView>
|
|
225
|
+
) : (
|
|
226
|
+
body
|
|
227
|
+
)}
|
|
228
|
+
{arrows}
|
|
229
|
+
</PopoverContentContext.Provider>
|
|
230
|
+
</View>
|
|
231
|
+
</Animated.View>
|
|
232
|
+
</Overlay.Portal>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
PopoverContent.displayName = "DelacourUI.Popover.Content";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Text, type TextPresetProps } from "../text";
|
|
3
|
+
import { popoverVariants } from "./popover.variants";
|
|
4
|
+
|
|
5
|
+
export type PopoverDescriptionProps = TextPresetProps;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Supporting copy under the title — a `Text.Caption`, so it sits on the muted
|
|
9
|
+
* token at the label's size and reads as the title's explanation rather than a
|
|
10
|
+
* second heading.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <Popover.Description>Shown to everyone in the workspace.</Popover.Description>
|
|
14
|
+
*/
|
|
15
|
+
export function PopoverDescription({ className, ...props }: PopoverDescriptionProps): ReactElement {
|
|
16
|
+
return <Text.Caption className={popoverVariants().description({ className })} {...props} />;
|
|
17
|
+
}
|
|
18
|
+
PopoverDescription.displayName = "DelacourUI.Popover.Description";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Text, type TextPresetProps } from "../text";
|
|
3
|
+
import { useOptionalPopoverContent } from "./popover.context";
|
|
4
|
+
import { popoverVariants } from "./popover.variants";
|
|
5
|
+
|
|
6
|
+
export type PopoverTitleProps = TextPresetProps;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The panel's heading.
|
|
10
|
+
*
|
|
11
|
+
* *Is* a `Text.Label`, announced as a header — a popover is a small panel, and a
|
|
12
|
+
* section heading's 20pt would outweigh the trigger it hangs from. The slot adds
|
|
13
|
+
* clearance for `Popover.Close` and no type of its own. It carries the
|
|
14
|
+
* `nativeID` the panel is labelled by, and accessibility focus lands on it when
|
|
15
|
+
* the panel opens.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* <Popover.Title>Rename</Popover.Title>
|
|
19
|
+
*/
|
|
20
|
+
export function PopoverTitle({ className, ...props }: PopoverTitleProps): ReactElement {
|
|
21
|
+
const content = useOptionalPopoverContent();
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Text.Label
|
|
25
|
+
accessibilityRole="header"
|
|
26
|
+
className={popoverVariants().title({ className })}
|
|
27
|
+
nativeID={content?.titleId}
|
|
28
|
+
ref={content?.titleRef}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
PopoverTitle.displayName = "DelacourUI.Popover.Title";
|