@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,97 @@
|
|
|
1
|
+
# Dialog
|
|
2
|
+
|
|
3
|
+
A centred card over a dimmed app that asks for a decision or a short input before the user goes
|
|
4
|
+
on: confirm a delete, rename a file, accept terms. Compound root plus `Dialog.Trigger`,
|
|
5
|
+
`Dialog.Content`, `Dialog.Close`, `Dialog.Header`, `Dialog.Title`, `Dialog.Description`,
|
|
6
|
+
`Dialog.Body` and `Dialog.Footer`, and `useDialog()`.
|
|
7
|
+
|
|
8
|
+
`import { Dialog } from "@delacour/react-native-ui/dialog";`
|
|
9
|
+
|
|
10
|
+
It is drawn with the [overlay foundation](../overlay/AGENTS.md) — `Overlay.Portal`,
|
|
11
|
+
`Overlay.Scrim`, `useOverlayPresence`, `useOverlayBackHandler` — so the app has to mount
|
|
12
|
+
`OverlayProvider` once at its root. Without one the dialog draws inline and warns once.
|
|
13
|
+
|
|
14
|
+
## Files
|
|
15
|
+
|
|
16
|
+
| File | What it holds |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `index.ts` | → `@delacour/react-native-ui/dialog` |
|
|
19
|
+
| `dialog.tsx` | Root — the open state, the ids and the focus refs — and the `Object.assign` compound surface |
|
|
20
|
+
| `dialog.context.tsx` | **Leaf.** `DialogProvider`, `DialogSizeProvider`, `useDialog()`, `useDialogContext()`, `useDialogPart()`, `useDialogSize()` |
|
|
21
|
+
| `dialog-trigger.tsx` | `Dialog.Trigger` — donates the open with `asChild`, keeps the trigger's ref for focus return |
|
|
22
|
+
| `dialog-content.tsx` | `Dialog.Content` — presence, portal, scrim, card, motion, keyboard lift, back button, focus |
|
|
23
|
+
| `dialog-close.tsx` | `Dialog.Close` — the corner ✕, or with `asChild` any control that closes |
|
|
24
|
+
| `dialog-header.tsx` | `Dialog.Header` |
|
|
25
|
+
| `dialog-title.tsx` | `Dialog.Title` — a `Text.Header` carrying the title `nativeID` and the focus ref |
|
|
26
|
+
| `dialog-description.tsx` | `Dialog.Description` — a muted `Text.Paragraph` |
|
|
27
|
+
| `dialog-body.tsx` | `Dialog.Body` |
|
|
28
|
+
| `dialog-footer.tsx` | `Dialog.Footer` — `plain` or `panel`, row or stacked by size |
|
|
29
|
+
| `dialog.variants.ts` | The slotted `tv()`, the size classes, the motion and slop constants, and three pure resolvers — no RN imports |
|
|
30
|
+
| `dialog.variants.test.ts` | Slots per size and footer variant, tokens in both themes, the resolvers |
|
|
31
|
+
|
|
32
|
+
## Design
|
|
33
|
+
|
|
34
|
+
- **One axis on the card, one on the footer.** `size` (`sm` 320, `md` 400, `lg` 520, `full`) is a
|
|
35
|
+
`max-w-*` on a `w-full` card, so a narrow phone shrinks every size to the screen gutter and only
|
|
36
|
+
a wider screen reaches the cap. `Dialog.Footer`'s `variant` is `plain` — a right-aligned row
|
|
37
|
+
inside the card's padding — or `panel`, which bleeds out of the padding to the card's edges on a
|
|
38
|
+
`bg-muted` band under a hairline, rounding only its bottom corners.
|
|
39
|
+
- **An `sm` footer stacks.** Two labelled buttons side by side on a 320pt card leave each label a
|
|
40
|
+
few characters before it truncates. `resolveDialogFooterDirection` is the rule, and the footer
|
|
41
|
+
reads the size from `DialogSizeProvider`, which `Dialog.Content` mounts — a context of its own
|
|
42
|
+
rather than a field on the root's, because the size belongs to the content, not the dialog.
|
|
43
|
+
Stacked actions keep written order, so the primary action, written last, sits last either way.
|
|
44
|
+
- **The card is a popover surface with the card corner.** `bg-popover`, the token a layer over the
|
|
45
|
+
app paints with (as `BottomSheet` does), and `rounded-lg`, the package's card step. The scrim is
|
|
46
|
+
the foundation's `bg-overlay` at opacity 1, faded by presence.
|
|
47
|
+
- **`Dialog.Content` renders nothing until the dialog opens, and stays through the exit.**
|
|
48
|
+
`useOverlayPresence` keeps it mounted until the exit animation ends, and the portal is mounted
|
|
49
|
+
only while present, so the registry entry, the z-order and the back handler all follow presence
|
|
50
|
+
— the foundation's contract. A re-open mid-exit reverses from wherever `progress` is.
|
|
51
|
+
- **It draws in the `modal` band**, above every bottom sheet and the navigator's header. A sheet
|
|
52
|
+
opened *from* a dialog would draw under it; open a dialog from a sheet, never the reverse.
|
|
53
|
+
- **Every path to closed is one `setOpen(false)`** — the trigger, `Dialog.Close`, the scrim, the
|
|
54
|
+
back button, the escape gesture and `useDialog().close()`. The root drops a set that does not
|
|
55
|
+
change the value, so a second tap on the scrim while the card is already leaving does not report
|
|
56
|
+
`onOpenChange(false)` twice.
|
|
57
|
+
- **`isDismissible={false}` turns off the ways out the user did not choose.** The scrim still takes
|
|
58
|
+
the touch — the app under an alert dialog must not be pressable — but does nothing; the back
|
|
59
|
+
button is not subscribed; `onAccessibilityEscape` is not set; and the card announces as
|
|
60
|
+
`alertdialog`. `Dialog.Close` still closes it: it is one of the dialog's own actions.
|
|
61
|
+
- **Triggers and closes donate the press.** With `asChild` they hand `onPress` to the child,
|
|
62
|
+
chained ahead of the child's own, for `BottomSheet.Trigger`'s reason: two tap gestures in an
|
|
63
|
+
ancestor/descendant pair are not simultaneous, so a pressable wrapped around a `Button` never
|
|
64
|
+
fires. The child has to be something that handles `onPress`.
|
|
65
|
+
- **The corner ✕ presses with `fade` and 8pt of slop.** A scale on a glyph this small reads as a
|
|
66
|
+
jitter, and a bare glyph has no capsule to bring it toward 44pt. It sits out of the flow, and
|
|
67
|
+
`Dialog.Title`'s `pr-8` reserves its clearance on every dialog, so adding a close never reflows
|
|
68
|
+
the title.
|
|
69
|
+
- **The title and description carry no type of their own.** They *are* `Text.Header` and a muted
|
|
70
|
+
`Text.Paragraph`; a size in the slot would be a second definition of a preset. A test asserts
|
|
71
|
+
the `title` slot is exactly `pr-8`. There is no `description` slot: `tv` emits `undefined` for an
|
|
72
|
+
empty string, so the part merges its `className` with `cn()`, as `BottomSheet.Description` does.
|
|
73
|
+
- **Motion.** Scrim opacity is `progress`; the card fades with it and grows from
|
|
74
|
+
`DIALOG_ENTER_SCALE` (0.96) and `DIALOG_ENTER_TRANSLATE_Y` (8pt) below. Under reduce motion the
|
|
75
|
+
transforms drop and the fade stays, per the foundation. `isMotionCalm` does not still it.
|
|
76
|
+
- **The keyboard lifts the card just enough.** `resolveDialogKeyboardLift` is the smallest lift
|
|
77
|
+
that keeps the card's bottom `DIALOG_KEYBOARD_MARGIN` (16pt) above the keyboard, never negative,
|
|
78
|
+
and capped so the card's top never rises under the top safe-area inset — a card too tall for
|
|
79
|
+
both keeps its title on screen. It is a worklet read in the card's animated style against
|
|
80
|
+
`useReanimatedKeyboardAnimation`'s height, so it rides the keyboard's own frames. The card's
|
|
81
|
+
edges come from `onLayout`, which ignores transforms, so the lift never feeds back into itself.
|
|
82
|
+
The positioner fills the portal host, which fills the window, so layout `y` is window `y`.
|
|
83
|
+
- **Accessibility.** The card is `accessibilityViewIsModal` with `role="dialog"` (or
|
|
84
|
+
`"alertdialog"`, `resolveDialogRole`) and `accessibilityLabelledBy` the title's `nativeID` for
|
|
85
|
+
Android. Once the entrance finishes (`onEntered`) focus moves to the title; once the exit
|
|
86
|
+
finishes, to the trigger, if one is still mounted. `findNodeHandle` of a ref that has gone is
|
|
87
|
+
null, and focus is then left where the system puts it. `Dialog.Title` takes no `ref` for that
|
|
88
|
+
reason — the dialog holds it.
|
|
89
|
+
- **No blur backdrop.** Frosted scrims need `expo-blur`, which is not a peer; recorded as a
|
|
90
|
+
follow-up for every overlay.
|
|
91
|
+
|
|
92
|
+
## Testing
|
|
93
|
+
|
|
94
|
+
`bun test` reaches `dialogVariants`, the size classes, the footer direction, the role and the
|
|
95
|
+
keyboard lift. Opening from a trigger and controlled, every dismissal path, the alert dialog, the
|
|
96
|
+
z-order over a sheet, the exit, the keyboard, VoiceOver focus and reduce motion are verified on a
|
|
97
|
+
simulator through `/dialog` in the playground — `over-sheet` is the z-order case.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { dialogVariants } from "./dialog.variants";
|
|
4
|
+
|
|
5
|
+
export type DialogBodyProps = ViewProps & { className?: string };
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Whatever the dialog asks for between the header and the footer — a field, a
|
|
9
|
+
* checkbox, a short list. Optional: a confirmation is a header and a footer.
|
|
10
|
+
*
|
|
11
|
+
* It does not scroll. Content long enough to need it belongs in a `BottomSheet`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* <Dialog.Body>
|
|
15
|
+
* <Input placeholder="Project name" />
|
|
16
|
+
* </Dialog.Body>
|
|
17
|
+
*/
|
|
18
|
+
export function DialogBody({ className, ...props }: DialogBodyProps): ReactElement {
|
|
19
|
+
return <View className={dialogVariants().body({ className })} {...props} />;
|
|
20
|
+
}
|
|
21
|
+
DialogBody.displayName = "DelacourUI.Dialog.Body";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type ReactElement, useCallback } 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 { useDialogPart } from "./dialog.context";
|
|
7
|
+
import { DIALOG_CLOSE_HIT_SLOP, dialogVariants } from "./dialog.variants";
|
|
8
|
+
|
|
9
|
+
export type DialogCloseProps =
|
|
10
|
+
/** Donates the close to the single child — a `Button` in the footer, say. */
|
|
11
|
+
| ({ asChild: true; children: ReactElement } & Omit<PressableProps, "asChild" | "children">)
|
|
12
|
+
/** The ✕ glyph in the card's top-right corner. */
|
|
13
|
+
| ({ asChild?: false; accessibilityLabel?: string } & Omit<PressableProps, "asChild" | "children">);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Closes the dialog.
|
|
17
|
+
*
|
|
18
|
+
* Without children it is the ✕ in the card's corner: this library's
|
|
19
|
+
* `Pressable` with `fade` feedback — a scale on a glyph this small reads as a
|
|
20
|
+
* jitter — 8pt of slop because a bare glyph has no capsule to bring it toward
|
|
21
|
+
* 44pt, and `"Close"` as its label. It sits out of the flow; `Dialog.Title`
|
|
22
|
+
* reserves the clearance.
|
|
23
|
+
*
|
|
24
|
+
* With `asChild` it donates the close to its child, chained ahead of the
|
|
25
|
+
* child's own `onPress`, the way `Dialog.Trigger` donates the open. Either way
|
|
26
|
+
* the close is the same `setOpen(false)` the scrim and the back button take, so
|
|
27
|
+
* `onOpenChange` is the one thing to watch.
|
|
28
|
+
*
|
|
29
|
+
* It closes even an alert dialog: `isDismissible={false}` turns off the ways
|
|
30
|
+
* out the user did not choose, not the ones the dialog offers.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* <Dialog.Close />
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* <Dialog.Close asChild>
|
|
37
|
+
* <Button variant="secondary">Cancel</Button>
|
|
38
|
+
* </Dialog.Close>
|
|
39
|
+
*/
|
|
40
|
+
export function DialogClose(props: DialogCloseProps): ReactElement {
|
|
41
|
+
const { close } = useDialogPart("Dialog.Close");
|
|
42
|
+
const { onPress } = props;
|
|
43
|
+
|
|
44
|
+
const handlePress = useCallback(() => {
|
|
45
|
+
close();
|
|
46
|
+
onPress?.();
|
|
47
|
+
}, [close, onPress]);
|
|
48
|
+
|
|
49
|
+
if (props.asChild) {
|
|
50
|
+
const { asChild: _asChild, children, onPress: _onPress, ...rest } = props;
|
|
51
|
+
return (
|
|
52
|
+
<Slot onPress={handlePress} {...rest}>
|
|
53
|
+
{children}
|
|
54
|
+
</Slot>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const {
|
|
59
|
+
asChild: _asChild,
|
|
60
|
+
accessibilityLabel = "Close",
|
|
61
|
+
className,
|
|
62
|
+
feedback = "fade",
|
|
63
|
+
hitSlop = DIALOG_CLOSE_HIT_SLOP,
|
|
64
|
+
onPress: _onPress,
|
|
65
|
+
...rest
|
|
66
|
+
} = props;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Pressable
|
|
70
|
+
accessibilityLabel={accessibilityLabel}
|
|
71
|
+
accessibilityRole="button"
|
|
72
|
+
className={dialogVariants().close({ className })}
|
|
73
|
+
feedback={feedback}
|
|
74
|
+
hitSlop={hitSlop}
|
|
75
|
+
onPress={handlePress}
|
|
76
|
+
{...rest}
|
|
77
|
+
>
|
|
78
|
+
<Icon color="muted-foreground" icon={IconCrossSmall} />
|
|
79
|
+
</Pressable>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
DialogClose.displayName = "DelacourUI.Dialog.Close";
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { type ReactElement, useCallback, useId } from "react";
|
|
2
|
+
import {
|
|
3
|
+
AccessibilityInfo,
|
|
4
|
+
findNodeHandle,
|
|
5
|
+
type LayoutChangeEvent,
|
|
6
|
+
useWindowDimensions,
|
|
7
|
+
View,
|
|
8
|
+
type ViewProps,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
|
11
|
+
import Animated, { interpolate, useAnimatedStyle, useSharedValue } from "react-native-reanimated";
|
|
12
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
13
|
+
import { Overlay, useOverlayBackHandler, useOverlayPresence } from "../overlay";
|
|
14
|
+
import { type DialogFocusTarget, DialogSizeProvider, useDialogPart } from "./dialog.context";
|
|
15
|
+
import {
|
|
16
|
+
DIALOG_ENTER_SCALE,
|
|
17
|
+
DIALOG_ENTER_TRANSLATE_Y,
|
|
18
|
+
DIALOG_KEYBOARD_MARGIN,
|
|
19
|
+
type DialogSize,
|
|
20
|
+
dialogVariants,
|
|
21
|
+
resolveDialogKeyboardLift,
|
|
22
|
+
resolveDialogRole,
|
|
23
|
+
} from "./dialog.variants";
|
|
24
|
+
|
|
25
|
+
export type DialogContentProps = ViewProps & {
|
|
26
|
+
className?: string;
|
|
27
|
+
/** Classes for the scrim behind the card. */
|
|
28
|
+
scrimClassName?: string;
|
|
29
|
+
/** The card's width cap: `sm` 320, `md` 400, `lg` 520, `full` the gutter. Default `md`. */
|
|
30
|
+
size?: DialogSize;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Moves VoiceOver / TalkBack focus to a mounted element. A no-op when it has gone. */
|
|
34
|
+
function focus(target: DialogFocusTarget | null): void {
|
|
35
|
+
if (target === null) return;
|
|
36
|
+
const handle = findNodeHandle(target as Parameters<typeof findNodeHandle>[0]);
|
|
37
|
+
if (handle !== null) AccessibilityInfo.setAccessibilityFocus(handle);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The card, its scrim and the portal they draw through.
|
|
42
|
+
*
|
|
43
|
+
* Renders nothing until the dialog opens, and stays mounted through the exit
|
|
44
|
+
* animation (`useOverlayPresence`), so the portal, the z-order and the back
|
|
45
|
+
* button all follow presence with nothing else to keep in sync. It draws in the
|
|
46
|
+
* `modal` band, above every bottom sheet and the navigator's header.
|
|
47
|
+
*
|
|
48
|
+
* - **Motion.** The scrim's opacity is `progress`; the card fades with it and
|
|
49
|
+
* grows from 0.96 and 8pt below. Under reduce motion only the fade runs.
|
|
50
|
+
* - **Keyboard.** The card lifts by `resolveDialogKeyboardLift` — just enough
|
|
51
|
+
* to keep its bottom 16pt above the keyboard, never past the top inset — on
|
|
52
|
+
* the keyboard's own frames, and settles back when it closes.
|
|
53
|
+
* - **Dismissal.** A scrim tap, Android back and the iOS escape gesture close a
|
|
54
|
+
* dismissible dialog. With `isDismissible={false}` the scrim still takes the
|
|
55
|
+
* touch so the app under it cannot be pressed, and only the dialog's own
|
|
56
|
+
* actions close it.
|
|
57
|
+
* - **Accessibility.** The card is modal to assistive technology, announces as
|
|
58
|
+
* a `dialog` (an `alertdialog` when not dismissible), is labelled by the
|
|
59
|
+
* title, and moves focus to the title once it has entered — and back to the
|
|
60
|
+
* trigger, if one is still mounted, once it has gone.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* <Dialog.Content size="sm">
|
|
64
|
+
* <Dialog.Header>…</Dialog.Header>
|
|
65
|
+
* </Dialog.Content>
|
|
66
|
+
*/
|
|
67
|
+
export function DialogContent({
|
|
68
|
+
className,
|
|
69
|
+
scrimClassName,
|
|
70
|
+
size = "md",
|
|
71
|
+
children,
|
|
72
|
+
onLayout,
|
|
73
|
+
style,
|
|
74
|
+
...props
|
|
75
|
+
}: DialogContentProps): ReactElement | null {
|
|
76
|
+
const { isOpen, close, isDismissible, titleId, titleRef, triggerRef } = useDialogPart("Dialog.Content");
|
|
77
|
+
const id = useId();
|
|
78
|
+
|
|
79
|
+
const handleEntered = useCallback(() => focus(titleRef.current), [titleRef]);
|
|
80
|
+
const handleExited = useCallback(() => focus(triggerRef.current), [triggerRef]);
|
|
81
|
+
const { isPresent, isReduced, progress } = useOverlayPresence({
|
|
82
|
+
isOpen,
|
|
83
|
+
onEntered: handleEntered,
|
|
84
|
+
onExited: handleExited,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
useOverlayBackHandler({ id, isEnabled: isPresent && isDismissible, onBack: close });
|
|
88
|
+
|
|
89
|
+
const keyboard = useReanimatedKeyboardAnimation();
|
|
90
|
+
const { top: topInset } = useSafeAreaInsets();
|
|
91
|
+
const { height: windowHeight } = useWindowDimensions();
|
|
92
|
+
const cardTop = useSharedValue(0);
|
|
93
|
+
const cardBottom = useSharedValue(0);
|
|
94
|
+
|
|
95
|
+
const handleLayout = useCallback(
|
|
96
|
+
(event: LayoutChangeEvent) => {
|
|
97
|
+
const { y, height } = event.nativeEvent.layout;
|
|
98
|
+
cardTop.value = y;
|
|
99
|
+
cardBottom.value = y + height;
|
|
100
|
+
onLayout?.(event);
|
|
101
|
+
},
|
|
102
|
+
[cardTop, cardBottom, onLayout]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const cardStyle = useAnimatedStyle(() => {
|
|
106
|
+
const lift = resolveDialogKeyboardLift({
|
|
107
|
+
cardBottom: cardBottom.value,
|
|
108
|
+
cardTop: cardTop.value,
|
|
109
|
+
keyboardHeight: keyboard.height.value,
|
|
110
|
+
margin: DIALOG_KEYBOARD_MARGIN,
|
|
111
|
+
topInset,
|
|
112
|
+
windowHeight,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (isReduced) {
|
|
116
|
+
return { opacity: progress.value, transform: [{ translateY: -lift }] };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
opacity: progress.value,
|
|
121
|
+
transform: [
|
|
122
|
+
{ translateY: interpolate(progress.value, [0, 1], [DIALOG_ENTER_TRANSLATE_Y, 0]) - lift },
|
|
123
|
+
{ scale: interpolate(progress.value, [0, 1], [DIALOG_ENTER_SCALE, 1]) },
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}, [isReduced, topInset, windowHeight]);
|
|
127
|
+
|
|
128
|
+
if (!isPresent) return null;
|
|
129
|
+
|
|
130
|
+
const slots = dialogVariants({ size });
|
|
131
|
+
const onEscape = isDismissible ? close : undefined;
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<Overlay.Portal id={id} layer="modal">
|
|
135
|
+
<Overlay.Scrim className={slots.scrim({ className: scrimClassName })} onDismiss={onEscape} progress={progress} />
|
|
136
|
+
<View className={slots.positioner()} pointerEvents="box-none">
|
|
137
|
+
<Animated.View
|
|
138
|
+
accessibilityLabelledBy={titleId}
|
|
139
|
+
accessibilityViewIsModal
|
|
140
|
+
className={slots.content({ className })}
|
|
141
|
+
onAccessibilityEscape={onEscape}
|
|
142
|
+
onLayout={handleLayout}
|
|
143
|
+
role={resolveDialogRole(isDismissible)}
|
|
144
|
+
style={[cardStyle, style]}
|
|
145
|
+
{...props}
|
|
146
|
+
>
|
|
147
|
+
<DialogSizeProvider value={size}>{children}</DialogSizeProvider>
|
|
148
|
+
</Animated.View>
|
|
149
|
+
</View>
|
|
150
|
+
</Overlay.Portal>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
DialogContent.displayName = "DelacourUI.Dialog.Content";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
import { Text, type TextPresetProps } from "../text";
|
|
4
|
+
import { useDialogPart } from "./dialog.context";
|
|
5
|
+
|
|
6
|
+
export type DialogDescriptionProps = TextPresetProps;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Supporting copy under the title.
|
|
10
|
+
*
|
|
11
|
+
* *Is* a `Text.Paragraph`, muted by default so the title and this read as a
|
|
12
|
+
* hierarchy rather than two equal lines. `color` is an ordinary prop. It has no
|
|
13
|
+
* slot in `dialogVariants` — see the note there.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* <Dialog.Description>This cannot be undone.</Dialog.Description>
|
|
17
|
+
*/
|
|
18
|
+
export function DialogDescription({ className, color = "muted", ...props }: DialogDescriptionProps): ReactElement {
|
|
19
|
+
const { descriptionId } = useDialogPart("Dialog.Description");
|
|
20
|
+
|
|
21
|
+
return <Text.Paragraph className={cn(className)} color={color} nativeID={descriptionId} {...props} />;
|
|
22
|
+
}
|
|
23
|
+
DialogDescription.displayName = "DelacourUI.Dialog.Description";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { useDialogSize } from "./dialog.context";
|
|
4
|
+
import { type DialogFooterVariant, dialogVariants, resolveDialogFooterDirection } from "./dialog.variants";
|
|
5
|
+
|
|
6
|
+
export type DialogFooterProps = ViewProps & {
|
|
7
|
+
className?: string;
|
|
8
|
+
/** `plain` sits inside the card's padding; `panel` bleeds to its edges on a muted band. Default `plain`. */
|
|
9
|
+
variant?: DialogFooterVariant;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The dialog's actions.
|
|
14
|
+
*
|
|
15
|
+
* A right-aligned row on every size but `sm`, where two labelled buttons side
|
|
16
|
+
* by side would truncate, so they stack full width in written order — write the
|
|
17
|
+
* primary action last and it sits last either way. The direction follows the
|
|
18
|
+
* card's size from `Dialog.Content`; `resolveDialogFooterDirection` is the rule.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <Dialog.Footer variant="panel">
|
|
22
|
+
* <Dialog.Close asChild><Button variant="secondary">Cancel</Button></Dialog.Close>
|
|
23
|
+
* <Button onPress={save}>Save</Button>
|
|
24
|
+
* </Dialog.Footer>
|
|
25
|
+
*/
|
|
26
|
+
export function DialogFooter({ className, variant = "plain", ...props }: DialogFooterProps): ReactElement {
|
|
27
|
+
const size = useDialogSize();
|
|
28
|
+
const footerDirection = resolveDialogFooterDirection(size);
|
|
29
|
+
|
|
30
|
+
return <View className={dialogVariants({ footer: variant, footerDirection }).footer({ className })} {...props} />;
|
|
31
|
+
}
|
|
32
|
+
DialogFooter.displayName = "DelacourUI.Dialog.Footer";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { dialogVariants } from "./dialog.variants";
|
|
4
|
+
|
|
5
|
+
export type DialogHeaderProps = ViewProps & { className?: string };
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The column holding `Dialog.Title` and `Dialog.Description`, a tight gap apart
|
|
9
|
+
* so the two read as one block above the body.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <Dialog.Header>
|
|
13
|
+
* <Dialog.Title>Delete project?</Dialog.Title>
|
|
14
|
+
* <Dialog.Description>This cannot be undone.</Dialog.Description>
|
|
15
|
+
* </Dialog.Header>
|
|
16
|
+
*/
|
|
17
|
+
export function DialogHeader({ className, ...props }: DialogHeaderProps): ReactElement {
|
|
18
|
+
return <View className={dialogVariants().header({ className })} {...props} />;
|
|
19
|
+
}
|
|
20
|
+
DialogHeader.displayName = "DelacourUI.Dialog.Header";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type ReactElement, useCallback } from "react";
|
|
2
|
+
import { Text, type TextPresetProps } from "../text";
|
|
3
|
+
import { type DialogFocusTarget, useDialogPart } from "./dialog.context";
|
|
4
|
+
import { dialogVariants } from "./dialog.variants";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A `Text.Header`'s props less `ref`: the dialog holds the title's ref itself,
|
|
8
|
+
* to move accessibility focus onto it.
|
|
9
|
+
*/
|
|
10
|
+
export type DialogTitleProps = Omit<TextPresetProps, "ref">;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The dialog's heading.
|
|
14
|
+
*
|
|
15
|
+
* *Is* a `Text.Header` — the preset supplies the type, and this adds only the
|
|
16
|
+
* clearance the corner ✕ needs. It carries the `nativeID` the card is labelled
|
|
17
|
+
* by on Android, and it is where accessibility focus lands once the card has
|
|
18
|
+
* finished entering, so a screen reader announces what the dialog is for first.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <Dialog.Title>Delete project?</Dialog.Title>
|
|
22
|
+
*/
|
|
23
|
+
export function DialogTitle({ className, ...props }: DialogTitleProps): ReactElement {
|
|
24
|
+
const { titleId, titleRef } = useDialogPart("Dialog.Title");
|
|
25
|
+
const setTitleRef = useCallback(
|
|
26
|
+
(node: DialogFocusTarget | null) => {
|
|
27
|
+
titleRef.current = node;
|
|
28
|
+
},
|
|
29
|
+
[titleRef]
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Text.Header
|
|
34
|
+
accessibilityRole="header"
|
|
35
|
+
className={dialogVariants().title({ className })}
|
|
36
|
+
nativeID={titleId}
|
|
37
|
+
ref={setTitleRef}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
DialogTitle.displayName = "DelacourUI.Dialog.Title";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type ReactElement, useCallback } from "react";
|
|
2
|
+
import { composeRefs } from "../../lib/compose-refs";
|
|
3
|
+
import { Slot } from "../../lib/slot";
|
|
4
|
+
import { Pressable, type PressableProps } from "../pressable";
|
|
5
|
+
import { useDialogPart } from "./dialog.context";
|
|
6
|
+
|
|
7
|
+
export type DialogTriggerProps = PressableProps;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The control that opens the dialog.
|
|
11
|
+
*
|
|
12
|
+
* On its own it is this library's `Pressable`. **`asChild` donates the press
|
|
13
|
+
* rather than wrapping the child**, the way `BottomSheet.Trigger` does: two tap
|
|
14
|
+
* gestures in an ancestor/descendant pair are not simultaneous, so a `Button`
|
|
15
|
+
* wrapped in a pressable trigger would win the touch and the dialog would never
|
|
16
|
+
* open. With `asChild` the open is chained ahead of the child's own `onPress`,
|
|
17
|
+
* and the child keeps its gesture, feedback and haptic — so the child has to be
|
|
18
|
+
* something that handles `onPress`.
|
|
19
|
+
*
|
|
20
|
+
* The trigger's ref is kept so accessibility focus can return to it when the
|
|
21
|
+
* dialog closes.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* <Dialog.Trigger asChild>
|
|
25
|
+
* <Button variant="destructive">Delete</Button>
|
|
26
|
+
* </Dialog.Trigger>
|
|
27
|
+
*/
|
|
28
|
+
export function DialogTrigger({ asChild = false, children, onPress, ref, ...props }: DialogTriggerProps): ReactElement {
|
|
29
|
+
const { setOpen, triggerRef } = useDialogPart("Dialog.Trigger");
|
|
30
|
+
|
|
31
|
+
const handlePress = useCallback(() => {
|
|
32
|
+
setOpen(true);
|
|
33
|
+
onPress?.();
|
|
34
|
+
}, [setOpen, onPress]);
|
|
35
|
+
|
|
36
|
+
const composedRef = composeRefs(ref, (node) => {
|
|
37
|
+
triggerRef.current = node;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (asChild) {
|
|
41
|
+
return (
|
|
42
|
+
<Slot onPress={handlePress} ref={composedRef} {...props}>
|
|
43
|
+
{children}
|
|
44
|
+
</Slot>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Pressable accessibilityRole="button" onPress={handlePress} ref={composedRef} {...props}>
|
|
50
|
+
{children}
|
|
51
|
+
</Pressable>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
DialogTrigger.displayName = "DelacourUI.Dialog.Trigger";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createContext, type ReactElement, type ReactNode, type RefObject, use } from "react";
|
|
2
|
+
import type { DialogSize } from "./dialog.variants";
|
|
3
|
+
|
|
4
|
+
/** Anything `AccessibilityInfo.setAccessibilityFocus` can be pointed at through `findNodeHandle`. */
|
|
5
|
+
export type DialogFocusTarget = object;
|
|
6
|
+
|
|
7
|
+
export type DialogContextValue = {
|
|
8
|
+
/** Whether the dialog is open — the owner's state, not the exit animation's. */
|
|
9
|
+
isOpen: boolean;
|
|
10
|
+
/** Opens or closes it. Reports through `onOpenChange` only when the value changes. */
|
|
11
|
+
setOpen: (isOpen: boolean) => void;
|
|
12
|
+
/** `setOpen(false)`, for an action inside the card. */
|
|
13
|
+
close: () => void;
|
|
14
|
+
/** Whether the scrim, Android back and the iOS escape gesture close it. */
|
|
15
|
+
isDismissible: boolean;
|
|
16
|
+
/** The `nativeID` `Dialog.Title` carries and the card is labelled by. */
|
|
17
|
+
titleId: string;
|
|
18
|
+
/** The `nativeID` `Dialog.Description` carries. */
|
|
19
|
+
descriptionId: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* What the parts share that a caller has no use for: where focus goes on open
|
|
24
|
+
* and on close, and the card's size for the footer.
|
|
25
|
+
*/
|
|
26
|
+
export type DialogInternalValue = DialogContextValue & {
|
|
27
|
+
/** The trigger, so focus can return to it on close. */
|
|
28
|
+
triggerRef: RefObject<DialogFocusTarget | null>;
|
|
29
|
+
/** The title, so focus can land on it on open. */
|
|
30
|
+
titleRef: RefObject<DialogFocusTarget | null>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const DialogContext = createContext<DialogInternalValue | null>(null);
|
|
34
|
+
|
|
35
|
+
/** The card's size, provided by `Dialog.Content` so the footer can choose its direction. */
|
|
36
|
+
const DialogSizeContext = createContext<DialogSize>("md");
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Supplies the dialog's state to its parts.
|
|
40
|
+
*
|
|
41
|
+
* Lives in its own module, importing nothing but `dialog.variants`' types, so a
|
|
42
|
+
* part can read it without importing `./dialog` — that import would close a
|
|
43
|
+
* cycle, and Metro serves a partially initialised module for one.
|
|
44
|
+
*/
|
|
45
|
+
export function DialogProvider({ value, children }: { value: DialogInternalValue; children: ReactNode }): ReactElement {
|
|
46
|
+
return <DialogContext value={value}>{children}</DialogContext>;
|
|
47
|
+
}
|
|
48
|
+
DialogProvider.displayName = "DelacourUI.Dialog.Provider";
|
|
49
|
+
|
|
50
|
+
/** Supplies the card's size to the parts inside `Dialog.Content`. */
|
|
51
|
+
export function DialogSizeProvider({ value, children }: { value: DialogSize; children: ReactNode }): ReactElement {
|
|
52
|
+
return <DialogSizeContext value={value}>{children}</DialogSizeContext>;
|
|
53
|
+
}
|
|
54
|
+
DialogSizeProvider.displayName = "DelacourUI.Dialog.SizeProvider";
|
|
55
|
+
|
|
56
|
+
/** The enclosing dialog's context, or null outside a `<Dialog>`. */
|
|
57
|
+
export function useDialogContext(): DialogContextValue | null {
|
|
58
|
+
return use(DialogContext);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Reads the enclosing dialog's open state, its setter and its ids.
|
|
63
|
+
*
|
|
64
|
+
* Lets a custom child close the dialog — a form's submit, say — without the
|
|
65
|
+
* caller threading a setter down. Throws outside a `<Dialog>`.
|
|
66
|
+
*/
|
|
67
|
+
export function useDialog(): DialogContextValue {
|
|
68
|
+
const context = use(DialogContext);
|
|
69
|
+
if (!context) {
|
|
70
|
+
throw new Error("useDialog must be called inside a <Dialog>.");
|
|
71
|
+
}
|
|
72
|
+
return context;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The full context, for a part that cannot work without one. Internal: not
|
|
77
|
+
* re-exported from `index.ts`.
|
|
78
|
+
*/
|
|
79
|
+
export function useDialogPart(component: string): DialogInternalValue {
|
|
80
|
+
const context = use(DialogContext);
|
|
81
|
+
if (!context) {
|
|
82
|
+
throw new Error(`${component} must be rendered inside a <Dialog>.`);
|
|
83
|
+
}
|
|
84
|
+
return context;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The card's size. `md` outside `Dialog.Content`. */
|
|
88
|
+
export function useDialogSize(): DialogSize {
|
|
89
|
+
return use(DialogSizeContext);
|
|
90
|
+
}
|