@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,70 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import Animated, { useAnimatedStyle } from "react-native-reanimated";
|
|
3
|
+
import { Text, type TextProps } from "../text";
|
|
4
|
+
import { useSlideButtonPart } from "./slide-button.context";
|
|
5
|
+
import {
|
|
6
|
+
resolveSlideLabelGutter,
|
|
7
|
+
resolveSlideTrailOpacity,
|
|
8
|
+
resolveSlideTrailWidth,
|
|
9
|
+
SLIDE_BUTTON_INSET,
|
|
10
|
+
slideButtonVariants,
|
|
11
|
+
} from "./slide-button.variants";
|
|
12
|
+
|
|
13
|
+
export type SlideButtonLabelProps = Omit<TextProps, "children" | "className"> & {
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* What the slide does, centred in the whole rail.
|
|
20
|
+
*
|
|
21
|
+
* **It does not fade or move.** The handle passes over it. A label that faded as
|
|
22
|
+
* the handle advanced would leave the control saying nothing for the second half
|
|
23
|
+
* of the gesture — the half where the hand most wants to know what it is about to
|
|
24
|
+
* confirm.
|
|
25
|
+
*
|
|
26
|
+
* **It changes colour where the trail covers it.** A second copy, in the trail's
|
|
27
|
+
* own foreground, is clipped to the trail's width. A state variant's trail is
|
|
28
|
+
* its full colour, and its soft label colour on that fill is the same hue on the
|
|
29
|
+
* same hue — the words would vanish exactly where the trail reached them.
|
|
30
|
+
*
|
|
31
|
+
* **It keeps a handle's width clear on both sides.** Symmetric, so it stays
|
|
32
|
+
* centred in the rail; wide enough that a long label truncates before it runs
|
|
33
|
+
* under the resting handle.
|
|
34
|
+
*
|
|
35
|
+
* Its text is also the rail's accessibility label, read off by the root, so the
|
|
36
|
+
* screen reader announces the same words the eye reads. The rail is the
|
|
37
|
+
* accessible element, so neither copy is read on its own.
|
|
38
|
+
*/
|
|
39
|
+
export function SlideButtonLabel({ className, ...props }: SlideButtonLabelProps): ReactElement {
|
|
40
|
+
const { variant, size, handleWidth, offset, travel } = useSlideButtonPart("SlideButton.Label");
|
|
41
|
+
const slots = slideButtonVariants({ size, variant });
|
|
42
|
+
|
|
43
|
+
const frameStyle = useAnimatedStyle(() => ({
|
|
44
|
+
paddingHorizontal: resolveSlideLabelGutter({ handleWidth: handleWidth.value, inset: SLIDE_BUTTON_INSET }),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
const clipStyle = useAnimatedStyle(() => ({
|
|
48
|
+
opacity: handleWidth.value > 0 ? resolveSlideTrailOpacity(offset.value) : 0,
|
|
49
|
+
width: resolveSlideTrailWidth({ handleWidth: handleWidth.value, inset: SLIDE_BUTTON_INSET, offset: offset.value }),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
const onTrailStyle = useAnimatedStyle(() => ({
|
|
53
|
+
paddingHorizontal: resolveSlideLabelGutter({ handleWidth: handleWidth.value, inset: SLIDE_BUTTON_INSET }),
|
|
54
|
+
width: travel.value + handleWidth.value + SLIDE_BUTTON_INSET * 2,
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<Animated.View className={slots.labelFrame()} pointerEvents="none" style={frameStyle}>
|
|
60
|
+
<Text className={slots.label({ className })} numberOfLines={1} {...props} />
|
|
61
|
+
</Animated.View>
|
|
62
|
+
<Animated.View className={slots.labelClip()} pointerEvents="none" style={clipStyle}>
|
|
63
|
+
<Animated.View className={slots.labelOnTrailFrame()} style={onTrailStyle}>
|
|
64
|
+
<Text className={slots.labelOnTrail({ className })} numberOfLines={1} {...props} />
|
|
65
|
+
</Animated.View>
|
|
66
|
+
</Animated.View>
|
|
67
|
+
</>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
SlideButtonLabel.displayName = "DelacourUI.SlideButton.Label";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode, useMemo } from "react";
|
|
2
|
+
import type { ViewProps } from "react-native";
|
|
3
|
+
import Animated, { useAnimatedStyle } from "react-native-reanimated";
|
|
4
|
+
import { useThemeColor } from "../../hooks/use-theme-color";
|
|
5
|
+
import { IconCheckmark1Small, IconChevronLeft, IconChevronRight } from "../../icons/central";
|
|
6
|
+
import { Icon, IconDefaultsProvider } from "../icon";
|
|
7
|
+
import { useSlideButtonPart } from "./slide-button.context";
|
|
8
|
+
import { SLIDE_BUTTON_GLYPH_TOKEN, slideButtonVariants } from "./slide-button.variants";
|
|
9
|
+
|
|
10
|
+
export type SlideButtonThumbProps = Omit<ViewProps, "children" | "style"> & {
|
|
11
|
+
/** Replaces the chevron. An `Icon` inherits the handle's glyph size and colour. */
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The handle the finger drags.
|
|
18
|
+
*
|
|
19
|
+
* Composed in automatically when the children hold none, and drawn last however
|
|
20
|
+
* the children were ordered, so it always passes *over* the label.
|
|
21
|
+
*
|
|
22
|
+
* **It holds no gesture of its own.** One `Gesture.Pan()` on the rail drives it,
|
|
23
|
+
* so a drag that lands on the rail beside the handle still moves it — the same
|
|
24
|
+
* trade `Switch.Thumb` and `Slider.Thumb` make.
|
|
25
|
+
*
|
|
26
|
+
* **One animated style, one node**: width, visibility and position together,
|
|
27
|
+
* because two `useAnimatedStyle`s on one view fight for the same props.
|
|
28
|
+
*
|
|
29
|
+
* Invisible until the rail has been measured — a handle drawn before then has no
|
|
30
|
+
* width and sits at a garbage offset for a frame.
|
|
31
|
+
*
|
|
32
|
+
* With no children it draws a chevron pointing the way it travels (mirrored
|
|
33
|
+
* under RTL), which crosses into a tick when the slide confirms. Children replace
|
|
34
|
+
* both: a custom glyph is the caller's to change.
|
|
35
|
+
*/
|
|
36
|
+
export function SlideButtonThumb({ className, children, ...props }: SlideButtonThumbProps): ReactElement {
|
|
37
|
+
const { size, variant, isRTL, offset, handleWidth } = useSlideButtonPart("SlideButton.Thumb");
|
|
38
|
+
const direction = isRTL ? -1 : 1;
|
|
39
|
+
|
|
40
|
+
const thumbStyle = useAnimatedStyle(() => ({
|
|
41
|
+
opacity: handleWidth.value > 0 ? 1 : 0,
|
|
42
|
+
transform: [{ translateX: offset.value * direction }],
|
|
43
|
+
width: handleWidth.value,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
const slots = slideButtonVariants({ size, variant });
|
|
47
|
+
const glyphColor = useThemeColor(SLIDE_BUTTON_GLYPH_TOKEN);
|
|
48
|
+
const glyphClassName = slots.thumbGlyph();
|
|
49
|
+
const iconDefaults = useMemo(
|
|
50
|
+
() => ({ className: glyphClassName, color: glyphColor ?? "" }),
|
|
51
|
+
[glyphClassName, glyphColor]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Animated.View className={slots.thumb({ className })} style={thumbStyle} {...props}>
|
|
56
|
+
<IconDefaultsProvider value={iconDefaults}>{children ?? <SlideButtonGlyph />}</IconDefaultsProvider>
|
|
57
|
+
</Animated.View>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
SlideButtonThumb.displayName = "DelacourUI.SlideButton.Thumb";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The default glyph: a chevron toward the travel that crosses into a tick.
|
|
64
|
+
*
|
|
65
|
+
* Both marks are always mounted, stacked, and swap by opacity and a quarter turn
|
|
66
|
+
* off one shared value — so the crossing runs on the UI thread and never waits on
|
|
67
|
+
* a re-render to start.
|
|
68
|
+
*/
|
|
69
|
+
function SlideButtonGlyph(): ReactElement {
|
|
70
|
+
const { isRTL, glyph } = useSlideButtonPart("SlideButton.Thumb");
|
|
71
|
+
const direction = isRTL ? -1 : 1;
|
|
72
|
+
|
|
73
|
+
const chevronStyle = useAnimatedStyle(() => ({
|
|
74
|
+
opacity: 1 - glyph.value,
|
|
75
|
+
transform: [{ rotate: `${glyph.value * 90 * direction}deg` }],
|
|
76
|
+
}));
|
|
77
|
+
const tickStyle = useAnimatedStyle(() => ({
|
|
78
|
+
opacity: glyph.value,
|
|
79
|
+
transform: [{ rotate: `${(glyph.value - 1) * 90 * direction}deg` }],
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<>
|
|
84
|
+
<Animated.View className="absolute" style={chevronStyle}>
|
|
85
|
+
<Icon icon={isRTL ? IconChevronLeft : IconChevronRight} />
|
|
86
|
+
</Animated.View>
|
|
87
|
+
<Animated.View className="absolute" style={tickStyle}>
|
|
88
|
+
<Icon icon={IconCheckmark1Small} />
|
|
89
|
+
</Animated.View>
|
|
90
|
+
</>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
SlideButtonGlyph.displayName = "DelacourUI.SlideButton.Thumb.Glyph";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createContext, type ReactElement, type ReactNode, use } from "react";
|
|
2
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
3
|
+
import type { SlideButtonSize, SlideButtonVariant } from "./slide-button.variants";
|
|
4
|
+
|
|
5
|
+
export type SlideButtonContextValue = {
|
|
6
|
+
variant: SlideButtonVariant;
|
|
7
|
+
size: SlideButtonSize;
|
|
8
|
+
/** Whether the slide has been confirmed, on the **JS thread** — what accessibility reads. */
|
|
9
|
+
isCompleted: boolean;
|
|
10
|
+
isDisabled: boolean;
|
|
11
|
+
/** Whether the layout runs right to left, so the handle travels leftwards. */
|
|
12
|
+
isRTL: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* How far the handle has travelled from its resting end, in points, on the
|
|
15
|
+
* **UI thread**. Always positive — the direction is applied where it is drawn.
|
|
16
|
+
*/
|
|
17
|
+
offset: SharedValue<number>;
|
|
18
|
+
/** How far the handle can travel, in points. `0` until the rail has been measured. */
|
|
19
|
+
travel: SharedValue<number>;
|
|
20
|
+
/** The handle's width, from the rail's measured height. `0` until then. */
|
|
21
|
+
handleWidth: SharedValue<number>;
|
|
22
|
+
/** The chevron-to-tick crossing: `0` shows the chevron, `1` the tick. */
|
|
23
|
+
glyph: SharedValue<number>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const SlideButtonContext = createContext<SlideButtonContextValue | null>(null);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Supplies one slide button's state and shared values to its own parts.
|
|
30
|
+
*
|
|
31
|
+
* Lives in its own module, importing nothing but React and types, so a part can
|
|
32
|
+
* read it without importing `./slide-button` — that import would close a cycle.
|
|
33
|
+
*/
|
|
34
|
+
export function SlideButtonProvider({
|
|
35
|
+
value,
|
|
36
|
+
children,
|
|
37
|
+
}: {
|
|
38
|
+
value: SlideButtonContextValue;
|
|
39
|
+
children: ReactNode;
|
|
40
|
+
}): ReactElement {
|
|
41
|
+
return <SlideButtonContext value={value}>{children}</SlideButtonContext>;
|
|
42
|
+
}
|
|
43
|
+
SlideButtonProvider.displayName = "DelacourUI.SlideButton.Provider";
|
|
44
|
+
|
|
45
|
+
/** The enclosing slide button's state, or null outside a `<SlideButton>`. */
|
|
46
|
+
export function useSlideButtonContext(): SlideButtonContextValue | null {
|
|
47
|
+
return use(SlideButtonContext);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reads the enclosing slide button's state.
|
|
52
|
+
*
|
|
53
|
+
* Lets a custom handle or label match the control — read `offset` against
|
|
54
|
+
* `travel` to draw anything that follows the drag. Throws outside a
|
|
55
|
+
* `<SlideButton>`; use {@link useSlideButtonContext} where it is optional.
|
|
56
|
+
*/
|
|
57
|
+
export function useSlideButton(): SlideButtonContextValue {
|
|
58
|
+
const context = useSlideButtonContext();
|
|
59
|
+
if (!context) {
|
|
60
|
+
throw new Error("useSlideButton must be called inside a <SlideButton>.");
|
|
61
|
+
}
|
|
62
|
+
return context;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The enclosing slide button's state, for a compound part that cannot work without one.
|
|
67
|
+
*
|
|
68
|
+
* Internal: deliberately not re-exported from `index.ts`.
|
|
69
|
+
*/
|
|
70
|
+
export function useSlideButtonPart(component: string): SlideButtonContextValue {
|
|
71
|
+
const context = useSlideButtonContext();
|
|
72
|
+
if (!context) {
|
|
73
|
+
throw new Error(`${component} must be rendered inside a <SlideButton>.`);
|
|
74
|
+
}
|
|
75
|
+
return context;
|
|
76
|
+
}
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
isValidElement,
|
|
4
|
+
type ReactElement,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { type AccessibilityActionEvent, I18nManager, type LayoutChangeEvent, type ViewProps } from "react-native";
|
|
12
|
+
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
13
|
+
import Animated, {
|
|
14
|
+
cancelAnimation,
|
|
15
|
+
type SharedValue,
|
|
16
|
+
useAnimatedStyle,
|
|
17
|
+
useReducedMotion,
|
|
18
|
+
useSharedValue,
|
|
19
|
+
withSpring,
|
|
20
|
+
withTiming,
|
|
21
|
+
} from "react-native-reanimated";
|
|
22
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
23
|
+
import { useControllableState } from "../../hooks/use-controllable-state";
|
|
24
|
+
import { type HapticFeedback, playHaptic } from "../pressable";
|
|
25
|
+
import { type SlideButtonContextValue, SlideButtonProvider } from "./slide-button.context";
|
|
26
|
+
import {
|
|
27
|
+
isSlideArmed,
|
|
28
|
+
resolveSlideHandleWidth,
|
|
29
|
+
resolveSlideRelease,
|
|
30
|
+
resolveSlideThreshold,
|
|
31
|
+
resolveSlideTrailOpacity,
|
|
32
|
+
resolveSlideTrailWidth,
|
|
33
|
+
resolveSlideTravel,
|
|
34
|
+
SLIDE_BUTTON_ACTIVE_OFFSET_X,
|
|
35
|
+
SLIDE_BUTTON_DEFAULT_AUTO_RESET_MS,
|
|
36
|
+
SLIDE_BUTTON_DEFAULT_SIZE,
|
|
37
|
+
SLIDE_BUTTON_DEFAULT_VARIANT,
|
|
38
|
+
SLIDE_BUTTON_FAIL_OFFSET_Y,
|
|
39
|
+
SLIDE_BUTTON_GLYPH_MS,
|
|
40
|
+
SLIDE_BUTTON_INSET,
|
|
41
|
+
SLIDE_BUTTON_LOOKAHEAD,
|
|
42
|
+
SLIDE_BUTTON_REDUCED_MOTION_MS,
|
|
43
|
+
SLIDE_BUTTON_SPRING,
|
|
44
|
+
type SlideButtonSize,
|
|
45
|
+
type SlideButtonVariant,
|
|
46
|
+
slideButtonVariants,
|
|
47
|
+
} from "./slide-button.variants";
|
|
48
|
+
import { SlideButtonLabel } from "./slide-button-label";
|
|
49
|
+
import { SlideButtonThumb } from "./slide-button-thumb";
|
|
50
|
+
|
|
51
|
+
export type SlideButtonProps = Omit<ViewProps, "children" | "style"> & {
|
|
52
|
+
children: ReactNode;
|
|
53
|
+
/** Classes for the rail. */
|
|
54
|
+
className?: string;
|
|
55
|
+
variant?: SlideButtonVariant;
|
|
56
|
+
size?: SlideButtonSize;
|
|
57
|
+
/** Stretches the rail across its parent. Otherwise it is a fixed 288pt. */
|
|
58
|
+
isFullWidth?: boolean;
|
|
59
|
+
/** How far along the travel a release has to reach, as a fraction from 0.1 to 1. Default 0.9. */
|
|
60
|
+
threshold?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Called once when the slide confirms. Return a promise and a rejection takes
|
|
63
|
+
* the handle home again, so a failed request does not look confirmed.
|
|
64
|
+
*/
|
|
65
|
+
onComplete?: () => void;
|
|
66
|
+
/** Controlled state. Pass nothing and the button holds its own. */
|
|
67
|
+
isCompleted?: boolean;
|
|
68
|
+
/** Starting state while uncontrolled. */
|
|
69
|
+
defaultCompleted?: boolean;
|
|
70
|
+
onCompletedChange?: (isCompleted: boolean) => void;
|
|
71
|
+
/** Takes the handle home on its own, `autoResetDelay` after it confirms. */
|
|
72
|
+
isAutoReset?: boolean;
|
|
73
|
+
/** In milliseconds. Default 1000. */
|
|
74
|
+
autoResetDelay?: number;
|
|
75
|
+
/** Fades the control and refuses the drag — the handle does not move at all. */
|
|
76
|
+
isDisabled?: boolean;
|
|
77
|
+
/** Played as a drag crosses the threshold; a success knock follows on commit. Off by default. */
|
|
78
|
+
haptic?: false | HapticFeedback;
|
|
79
|
+
/** What the screen reader's activate action is called. Imperative, naming the outcome. Default "Confirm". */
|
|
80
|
+
accessibilityActionLabel?: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function SlideButtonRoot({
|
|
84
|
+
children,
|
|
85
|
+
className,
|
|
86
|
+
variant = SLIDE_BUTTON_DEFAULT_VARIANT,
|
|
87
|
+
size = SLIDE_BUTTON_DEFAULT_SIZE,
|
|
88
|
+
isFullWidth = false,
|
|
89
|
+
threshold,
|
|
90
|
+
onComplete,
|
|
91
|
+
isCompleted,
|
|
92
|
+
defaultCompleted = false,
|
|
93
|
+
onCompletedChange,
|
|
94
|
+
isAutoReset = false,
|
|
95
|
+
autoResetDelay = SLIDE_BUTTON_DEFAULT_AUTO_RESET_MS,
|
|
96
|
+
isDisabled = false,
|
|
97
|
+
haptic = false,
|
|
98
|
+
accessibilityActionLabel = "Confirm",
|
|
99
|
+
accessibilityLabel,
|
|
100
|
+
...props
|
|
101
|
+
}: SlideButtonProps): ReactElement {
|
|
102
|
+
const [completed, setCompleted] = useControllableState({
|
|
103
|
+
defaultValue: defaultCompleted,
|
|
104
|
+
onChange: onCompletedChange,
|
|
105
|
+
value: isCompleted,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const isRTL = I18nManager.isRTL;
|
|
109
|
+
const direction = isRTL ? -1 : 1;
|
|
110
|
+
const isReducedMotion = useReducedMotion();
|
|
111
|
+
const resolvedThreshold = resolveSlideThreshold(threshold);
|
|
112
|
+
|
|
113
|
+
const offset = useSharedValue(0);
|
|
114
|
+
const travel = useSharedValue(0);
|
|
115
|
+
const handleWidth = useSharedValue(0);
|
|
116
|
+
const glyph = useSharedValue(completed ? 1 : 0);
|
|
117
|
+
// Where the handle was when the drag took hold, so picking it up mid-spring
|
|
118
|
+
// does not snap it to the finger.
|
|
119
|
+
const grabbed = useSharedValue(0);
|
|
120
|
+
// Whether the current drag has crossed the threshold — the arming haptic
|
|
121
|
+
// fires on the crossing, not on every frame past it.
|
|
122
|
+
const armed = useSharedValue(false);
|
|
123
|
+
// Whether the handle belongs at the far end: confirmed, or requested and not
|
|
124
|
+
// yet answered. A pinned handle takes no drag and follows a re-layout.
|
|
125
|
+
const pinned = useSharedValue(completed);
|
|
126
|
+
|
|
127
|
+
// A request is in flight: the drag confirmed, `onComplete` ran, and a
|
|
128
|
+
// controlled parent has not said `isCompleted` yet. A ref, because nothing
|
|
129
|
+
// renders differently for it — the handle already sits at the end.
|
|
130
|
+
const isPending = useRef(false);
|
|
131
|
+
|
|
132
|
+
const moveTo = useCallback(
|
|
133
|
+
(target: number) => {
|
|
134
|
+
offset.value = isReducedMotion
|
|
135
|
+
? withTiming(target, { duration: SLIDE_BUTTON_REDUCED_MOTION_MS })
|
|
136
|
+
: withSpring(target, SLIDE_BUTTON_SPRING);
|
|
137
|
+
},
|
|
138
|
+
[isReducedMotion, offset]
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const goHome = useCallback(() => {
|
|
142
|
+
pinned.value = false;
|
|
143
|
+
moveTo(0);
|
|
144
|
+
glyph.value = withTiming(0, { duration: SLIDE_BUTTON_GLYPH_MS });
|
|
145
|
+
}, [glyph, moveTo, pinned]);
|
|
146
|
+
|
|
147
|
+
const onCompleteRef = useRef(onComplete);
|
|
148
|
+
onCompleteRef.current = onComplete;
|
|
149
|
+
|
|
150
|
+
const reject = useCallback(() => {
|
|
151
|
+
isPending.current = false;
|
|
152
|
+
setCompleted(false);
|
|
153
|
+
goHome();
|
|
154
|
+
}, [goHome, setCompleted]);
|
|
155
|
+
|
|
156
|
+
const complete = useCallback(() => {
|
|
157
|
+
isPending.current = true;
|
|
158
|
+
setCompleted(true);
|
|
159
|
+
// Typed `void` so any function fits; read as `unknown` to find a promise in it.
|
|
160
|
+
const result: unknown = onCompleteRef.current?.();
|
|
161
|
+
if (isPromiseLike(result)) result.then(undefined, reject);
|
|
162
|
+
}, [reject, setCompleted]);
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (completed) {
|
|
166
|
+
isPending.current = false;
|
|
167
|
+
pinned.value = true;
|
|
168
|
+
moveTo(travel.value);
|
|
169
|
+
glyph.value = withTiming(1, { duration: SLIDE_BUTTON_GLYPH_MS });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Completion was requested and has not been answered. The handle stays at
|
|
173
|
+
// the end rather than springing back, which would read as a refusal.
|
|
174
|
+
if (isPending.current) return;
|
|
175
|
+
goHome();
|
|
176
|
+
}, [completed, glyph, goHome, moveTo, pinned, travel]);
|
|
177
|
+
|
|
178
|
+
useEffect(() => () => cancelAnimation(offset), [offset]);
|
|
179
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (!completed || !isAutoReset) return;
|
|
182
|
+
const timer = setTimeout(() => setCompleted(false), autoResetDelay);
|
|
183
|
+
return () => clearTimeout(timer);
|
|
184
|
+
}, [autoResetDelay, completed, isAutoReset, setCompleted]);
|
|
185
|
+
|
|
186
|
+
const handleLayout = useCallback(
|
|
187
|
+
(event: LayoutChangeEvent) => {
|
|
188
|
+
const { width, height } = event.nativeEvent.layout;
|
|
189
|
+
const nextHandle = resolveSlideHandleWidth({ inset: SLIDE_BUTTON_INSET, railHeight: height });
|
|
190
|
+
const nextTravel = resolveSlideTravel({ handleWidth: nextHandle, inset: SLIDE_BUTTON_INSET, railWidth: width });
|
|
191
|
+
handleWidth.value = nextHandle;
|
|
192
|
+
travel.value = nextTravel;
|
|
193
|
+
if (pinned.value) offset.value = nextTravel;
|
|
194
|
+
else if (offset.value > nextTravel) offset.value = nextTravel;
|
|
195
|
+
},
|
|
196
|
+
[handleWidth, offset, pinned, travel]
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const gesture = useMemo(() => {
|
|
200
|
+
const settle = (target: number, velocity: number) => {
|
|
201
|
+
"worklet";
|
|
202
|
+
offset.value = isReducedMotion
|
|
203
|
+
? withTiming(target, { duration: SLIDE_BUTTON_REDUCED_MOTION_MS })
|
|
204
|
+
: withSpring(target, { ...SLIDE_BUTTON_SPRING, velocity });
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Disabled still claims the drag and then does nothing with it. A disabled
|
|
208
|
+
// recognizer lets the touch through to whatever sits behind — inside a
|
|
209
|
+
// stack with a full-screen back swipe, a drag on a refused slide navigated
|
|
210
|
+
// back.
|
|
211
|
+
return Gesture.Pan()
|
|
212
|
+
.activeOffsetX([-SLIDE_BUTTON_ACTIVE_OFFSET_X, SLIDE_BUTTON_ACTIVE_OFFSET_X])
|
|
213
|
+
.failOffsetY([-SLIDE_BUTTON_FAIL_OFFSET_Y, SLIDE_BUTTON_FAIL_OFFSET_Y])
|
|
214
|
+
.onStart(() => {
|
|
215
|
+
"worklet";
|
|
216
|
+
if (isDisabled || pinned.value) return;
|
|
217
|
+
cancelAnimation(offset);
|
|
218
|
+
grabbed.value = offset.value;
|
|
219
|
+
armed.value = false;
|
|
220
|
+
})
|
|
221
|
+
.onUpdate((event) => {
|
|
222
|
+
"worklet";
|
|
223
|
+
const span = travel.value;
|
|
224
|
+
if (isDisabled || pinned.value || span <= 0) return;
|
|
225
|
+
|
|
226
|
+
// No easing on the way out: the handle tracks the finger exactly.
|
|
227
|
+
const next = Math.min(span, Math.max(0, grabbed.value + event.translationX * direction));
|
|
228
|
+
offset.value = next;
|
|
229
|
+
|
|
230
|
+
// The tick plays on the crossing, not on every frame past it, and
|
|
231
|
+
// re-arms if the drag drops back under.
|
|
232
|
+
const isArmed = isSlideArmed({ offset: next, threshold: resolvedThreshold, travel: span });
|
|
233
|
+
if (isArmed === armed.value) return;
|
|
234
|
+
armed.value = isArmed;
|
|
235
|
+
if (isArmed && haptic !== false) playHaptic(haptic);
|
|
236
|
+
})
|
|
237
|
+
.onEnd((event, success) => {
|
|
238
|
+
"worklet";
|
|
239
|
+
if (isDisabled || pinned.value) return;
|
|
240
|
+
armed.value = false;
|
|
241
|
+
const velocity = event.velocityX * direction;
|
|
242
|
+
const release = resolveSlideRelease({
|
|
243
|
+
lookahead: SLIDE_BUTTON_LOOKAHEAD,
|
|
244
|
+
offset: offset.value,
|
|
245
|
+
threshold: resolvedThreshold,
|
|
246
|
+
travel: travel.value,
|
|
247
|
+
velocity,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// A pan cancelled by a scroll taking over goes home whatever its position.
|
|
251
|
+
if (!success || release === "return") {
|
|
252
|
+
settle(0, velocity);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
pinned.value = true;
|
|
257
|
+
settle(travel.value, velocity);
|
|
258
|
+
if (haptic !== false) playHaptic("success");
|
|
259
|
+
scheduleOnRN(complete);
|
|
260
|
+
});
|
|
261
|
+
}, [
|
|
262
|
+
armed,
|
|
263
|
+
complete,
|
|
264
|
+
direction,
|
|
265
|
+
grabbed,
|
|
266
|
+
haptic,
|
|
267
|
+
isDisabled,
|
|
268
|
+
isReducedMotion,
|
|
269
|
+
offset,
|
|
270
|
+
pinned,
|
|
271
|
+
resolvedThreshold,
|
|
272
|
+
travel,
|
|
273
|
+
]);
|
|
274
|
+
|
|
275
|
+
const activate = useCallback(() => {
|
|
276
|
+
if (isDisabled || pinned.value) return;
|
|
277
|
+
pinned.value = true;
|
|
278
|
+
moveTo(travel.value);
|
|
279
|
+
complete();
|
|
280
|
+
}, [complete, isDisabled, moveTo, pinned, travel]);
|
|
281
|
+
|
|
282
|
+
const handleAccessibilityAction = useCallback(
|
|
283
|
+
(event: AccessibilityActionEvent) => {
|
|
284
|
+
if (event.nativeEvent.actionName === "activate") activate();
|
|
285
|
+
},
|
|
286
|
+
[activate]
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const accessibilityActions = useMemo(
|
|
290
|
+
() => [{ label: accessibilityActionLabel, name: "activate" }],
|
|
291
|
+
[accessibilityActionLabel]
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const context = useMemo<SlideButtonContextValue>(
|
|
295
|
+
() => ({
|
|
296
|
+
glyph,
|
|
297
|
+
handleWidth,
|
|
298
|
+
isCompleted: completed,
|
|
299
|
+
isDisabled,
|
|
300
|
+
isRTL,
|
|
301
|
+
offset,
|
|
302
|
+
size,
|
|
303
|
+
travel,
|
|
304
|
+
variant,
|
|
305
|
+
}),
|
|
306
|
+
[completed, glyph, handleWidth, isDisabled, offset, size, travel, variant]
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const content = useMemo(() => withThumb(children), [children]);
|
|
310
|
+
const label = accessibilityLabel ?? labelText(children);
|
|
311
|
+
const slots = slideButtonVariants({ isDisabled, isFullWidth, size, variant });
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<SlideButtonProvider value={context}>
|
|
315
|
+
<GestureDetector gesture={gesture}>
|
|
316
|
+
<Animated.View
|
|
317
|
+
accessibilityActions={accessibilityActions}
|
|
318
|
+
accessibilityLabel={label}
|
|
319
|
+
accessibilityRole="button"
|
|
320
|
+
accessibilityState={{ checked: completed, disabled: isDisabled }}
|
|
321
|
+
accessible
|
|
322
|
+
className={slots.root({ className })}
|
|
323
|
+
onAccessibilityAction={handleAccessibilityAction}
|
|
324
|
+
onAccessibilityTap={activate}
|
|
325
|
+
onLayout={handleLayout}
|
|
326
|
+
{...props}
|
|
327
|
+
>
|
|
328
|
+
<SlideButtonTrail className={slots.trail()} handleWidth={handleWidth} offset={offset} />
|
|
329
|
+
{content}
|
|
330
|
+
</Animated.View>
|
|
331
|
+
</GestureDetector>
|
|
332
|
+
</SlideButtonProvider>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* The fill behind the handle, from the rail's start edge to the handle's middle.
|
|
338
|
+
*
|
|
339
|
+
* It ends *under* the handle rather than at its rear edge: a stadium's rear is a
|
|
340
|
+
* curve, and a trail that stopped at its straight edge would leave a sliver of
|
|
341
|
+
* rail showing above and below that curve. Ending under the handle means the two
|
|
342
|
+
* read as one shape — the handle dragging its colour along.
|
|
343
|
+
*
|
|
344
|
+
* Internal: the root renders it, so it lives here rather than in a part file.
|
|
345
|
+
*/
|
|
346
|
+
function SlideButtonTrail({
|
|
347
|
+
className,
|
|
348
|
+
offset,
|
|
349
|
+
handleWidth,
|
|
350
|
+
}: {
|
|
351
|
+
className: string;
|
|
352
|
+
offset: SharedValue<number>;
|
|
353
|
+
handleWidth: SharedValue<number>;
|
|
354
|
+
}): ReactElement {
|
|
355
|
+
const style = useAnimatedStyle(() => ({
|
|
356
|
+
opacity: handleWidth.value > 0 ? resolveSlideTrailOpacity(offset.value) : 0,
|
|
357
|
+
width: resolveSlideTrailWidth({ handleWidth: handleWidth.value, inset: SLIDE_BUTTON_INSET, offset: offset.value }),
|
|
358
|
+
}));
|
|
359
|
+
return <Animated.View className={className} style={style} />;
|
|
360
|
+
}
|
|
361
|
+
SlideButtonTrail.displayName = "DelacourUI.SlideButton.Trail";
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Composes a `SlideButton.Thumb` in when the children hold none, and puts it
|
|
365
|
+
* last however it was written, so the handle always passes over the label —
|
|
366
|
+
* `Switch`'s rule for its thumb, for the same reason.
|
|
367
|
+
*/
|
|
368
|
+
function withThumb(children: ReactNode): ReactNode {
|
|
369
|
+
const items = Children.toArray(children);
|
|
370
|
+
const isThumb = items.map((child) => isPart(child, SlideButtonThumb));
|
|
371
|
+
|
|
372
|
+
if (!isThumb.includes(true)) return [...items, <SlideButtonThumb key="thumb" />];
|
|
373
|
+
|
|
374
|
+
return [...items.filter((_, index) => !isThumb[index]), ...items.filter((_, index) => isThumb[index])];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The label's text, for the rail's accessibility label.
|
|
379
|
+
*
|
|
380
|
+
* Only plain text is read. A label holding anything richer needs an explicit
|
|
381
|
+
* `accessibilityLabel` on the root.
|
|
382
|
+
*/
|
|
383
|
+
function labelText(children: ReactNode): string | undefined {
|
|
384
|
+
for (const child of Children.toArray(children)) {
|
|
385
|
+
if (!isPart(child, SlideButtonLabel) || !isValidElement<{ children?: ReactNode }>(child)) continue;
|
|
386
|
+
const parts = Children.toArray(child.props.children).filter(
|
|
387
|
+
(part): part is string | number => typeof part === "string" || typeof part === "number"
|
|
388
|
+
);
|
|
389
|
+
if (parts.length > 0) return parts.join("");
|
|
390
|
+
}
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Whether `onComplete` handed back something to wait on. */
|
|
395
|
+
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
396
|
+
return typeof value === "object" && value !== null && typeof (value as { then?: unknown }).then === "function";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Whether a child is a given part — by identity first, then by `displayName`,
|
|
401
|
+
* which survives React Compiler and a second module instance where identity does
|
|
402
|
+
* not. See `isSwitchThumbElement` for the afternoon that rule cost.
|
|
403
|
+
*/
|
|
404
|
+
function isPart(child: ReactNode, part: { displayName?: string }): boolean {
|
|
405
|
+
if (!isValidElement(child)) return false;
|
|
406
|
+
if (child.type === part) return true;
|
|
407
|
+
|
|
408
|
+
const type = child.type as { displayName?: string } | null;
|
|
409
|
+
return type?.displayName !== undefined && type.displayName === part.displayName;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* A control confirmed by dragging a handle across a rail — "Slide to ship".
|
|
414
|
+
*
|
|
415
|
+
* For an action that deserves deliberation where a dialog would be too much
|
|
416
|
+
* ceremony: a stray tap cannot reach it, and neither can habit. The handle tracks
|
|
417
|
+
* the finger exactly; only the release is sprung. A release past `threshold`
|
|
418
|
+
* (0.9 by default) confirms, with a small look-ahead so a committed flick near
|
|
419
|
+
* the end counts — but a flick from halfway never does, and `threshold={1}`
|
|
420
|
+
* demands the far end.
|
|
421
|
+
*
|
|
422
|
+
* **State works either way.** Uncontrolled, the handle confirms and stays at the
|
|
423
|
+
* end, ticked. Controlled, a confirmed slide reports `onCompletedChange(true)` and
|
|
424
|
+
* the handle waits at the end until `isCompleted` answers — so slow work shows as
|
|
425
|
+
* a held handle, not a bounce. `isAutoReset` takes it home after `autoResetDelay`.
|
|
426
|
+
*
|
|
427
|
+
* The label is centred in the whole rail and never fades. A `SlideButton.Thumb` is
|
|
428
|
+
* composed in when none is written; write one to put a different glyph on the handle.
|
|
429
|
+
*
|
|
430
|
+
* Under RTL the handle rests at the right and travels left. A screen reader's
|
|
431
|
+
* activate confirms it outright, named by `accessibilityActionLabel`.
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* <SlideButton onComplete={ship}>
|
|
435
|
+
* <SlideButton.Label>Slide to ship</SlideButton.Label>
|
|
436
|
+
* </SlideButton>
|
|
437
|
+
*
|
|
438
|
+
* @example
|
|
439
|
+
* <SlideButton variant="destructive" threshold={1} accessibilityActionLabel="Delete account" onComplete={remove}>
|
|
440
|
+
* <SlideButton.Label>Slide to delete</SlideButton.Label>
|
|
441
|
+
* </SlideButton>
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* <SlideButton isFullWidth haptic="selection" onComplete={unlock}>
|
|
445
|
+
* <SlideButton.Label>Slide to unlock</SlideButton.Label>
|
|
446
|
+
* <SlideButton.Thumb>
|
|
447
|
+
* <Icon icon={IconUnlocked} />
|
|
448
|
+
* </SlideButton.Thumb>
|
|
449
|
+
* </SlideButton>
|
|
450
|
+
*/
|
|
451
|
+
export const SlideButton = Object.assign(SlideButtonRoot, {
|
|
452
|
+
/** What the slide does, centred in the rail. Never fades. */
|
|
453
|
+
Label: SlideButtonLabel,
|
|
454
|
+
/** The handle. Composed in automatically; write it out to change its glyph. */
|
|
455
|
+
Thumb: SlideButtonThumb,
|
|
456
|
+
displayName: "DelacourUI.SlideButton",
|
|
457
|
+
});
|