@delacour/react-native-ui 0.1.0-alpha.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -6
- package/package.json +48 -4
- package/src/components/accordion/AGENTS.md +21 -9
- package/src/components/accordion/accordion-content.tsx +7 -4
- package/src/components/accordion/accordion-item.tsx +15 -13
- package/src/components/accordion/accordion.context.tsx +11 -5
- package/src/components/accordion/accordion.variants.test.ts +64 -3
- package/src/components/accordion/accordion.variants.ts +33 -9
- package/src/components/alert/AGENTS.md +102 -0
- package/src/components/alert/alert-action.tsx +17 -0
- package/src/components/alert/alert-close-button.tsx +50 -0
- package/src/components/alert/alert-content.tsx +15 -0
- package/src/components/alert/alert-description.tsx +16 -0
- package/src/components/alert/alert-glyphs.ts +25 -0
- package/src/components/alert/alert-indicator.tsx +40 -0
- package/src/components/alert/alert-title.tsx +18 -0
- package/src/components/alert/alert.context.tsx +69 -0
- package/src/components/alert/alert.tsx +160 -0
- package/src/components/alert/alert.types.ts +10 -0
- package/src/components/alert/alert.variants.test.ts +218 -0
- package/src/components/alert/alert.variants.ts +180 -0
- package/src/components/alert/index.ts +23 -0
- package/src/components/avatar/AGENTS.md +106 -0
- package/src/components/avatar/avatar-badge.tsx +51 -0
- package/src/components/avatar/avatar-group.tsx +139 -0
- package/src/components/avatar/avatar.context.tsx +93 -0
- package/src/components/avatar/avatar.tsx +201 -0
- package/src/components/avatar/avatar.utils.test.ts +174 -0
- package/src/components/avatar/avatar.utils.ts +141 -0
- package/src/components/avatar/avatar.variants.test.ts +251 -0
- package/src/components/avatar/avatar.variants.ts +270 -0
- package/src/components/avatar/index.ts +39 -0
- package/src/components/badge/AGENTS.md +5 -0
- package/src/components/badge/badge.tsx +6 -1
- package/src/components/bottom-sheet/AGENTS.md +186 -231
- package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
- package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
- package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -208
- package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
- package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
- package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
- package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -122
- package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
- package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
- package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
- package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
- package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -62
- package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
- package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
- package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
- package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
- package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
- package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
- package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -87
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -126
- package/src/components/bottom-sheet/index.ts +39 -17
- package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
- package/src/components/button/AGENTS.md +45 -11
- package/src/components/button/button-group.tsx +44 -8
- package/src/components/button/button.context.tsx +10 -0
- package/src/components/button/button.tsx +2 -1
- package/src/components/button/button.variants.test.ts +154 -0
- package/src/components/button/button.variants.ts +71 -6
- package/src/components/button/index.ts +2 -0
- package/src/components/calendar/AGENTS.md +142 -0
- package/src/components/calendar/calendar-caption.tsx +116 -0
- package/src/components/calendar/calendar-day.tsx +103 -0
- package/src/components/calendar/calendar-grid.tsx +183 -0
- package/src/components/calendar/calendar-header.tsx +34 -0
- package/src/components/calendar/calendar-nav.tsx +51 -0
- package/src/components/calendar/calendar-picker.tsx +128 -0
- package/src/components/calendar/calendar-weekdays.tsx +48 -0
- package/src/components/calendar/calendar.context.tsx +108 -0
- package/src/components/calendar/calendar.date.test.ts +456 -0
- package/src/components/calendar/calendar.date.ts +584 -0
- package/src/components/calendar/calendar.tsx +389 -0
- package/src/components/calendar/calendar.types.ts +87 -0
- package/src/components/calendar/calendar.variants.test.ts +363 -0
- package/src/components/calendar/calendar.variants.ts +426 -0
- package/src/components/calendar/index.ts +96 -0
- package/src/components/card/AGENTS.md +88 -0
- package/src/components/card/card-action.tsx +16 -0
- package/src/components/card/card-content.tsx +12 -0
- package/src/components/card/card-description.tsx +12 -0
- package/src/components/card/card-footer.tsx +30 -0
- package/src/components/card/card-header.tsx +45 -0
- package/src/components/card/card-title.tsx +25 -0
- package/src/components/card/card.context.tsx +64 -0
- package/src/components/card/card.tsx +94 -0
- package/src/components/card/card.types.ts +13 -0
- package/src/components/card/card.variants.test.ts +240 -0
- package/src/components/card/card.variants.ts +177 -0
- package/src/components/card/index.ts +15 -0
- package/src/components/chart/AGENTS.md +8 -0
- package/src/components/chart/chart.tsx +8 -1
- package/src/components/chip/AGENTS.md +113 -0
- package/src/components/chip/chip-close-button.tsx +48 -0
- package/src/components/chip/chip-end-content.tsx +15 -0
- package/src/components/chip/chip-label.tsx +21 -0
- package/src/components/chip/chip-start-content.tsx +16 -0
- package/src/components/chip/chip.context.tsx +66 -0
- package/src/components/chip/chip.tsx +268 -0
- package/src/components/chip/chip.types.ts +10 -0
- package/src/components/chip/chip.variants.test.ts +335 -0
- package/src/components/chip/chip.variants.ts +321 -0
- package/src/components/chip/index.ts +24 -0
- package/src/components/collapsible/AGENTS.md +97 -0
- package/src/components/collapsible/collapsible-content.tsx +97 -0
- package/src/components/collapsible/collapsible-description.tsx +17 -0
- package/src/components/collapsible/collapsible-indicator.tsx +92 -0
- package/src/components/collapsible/collapsible-title.tsx +17 -0
- package/src/components/collapsible/collapsible-trigger.tsx +133 -0
- package/src/components/collapsible/collapsible.context.tsx +91 -0
- package/src/components/collapsible/collapsible.tsx +167 -0
- package/src/components/collapsible/collapsible.types.ts +9 -0
- package/src/components/collapsible/collapsible.variants.test.ts +338 -0
- package/src/components/collapsible/collapsible.variants.ts +228 -0
- package/src/components/collapsible/index.ts +32 -0
- package/src/components/context-menu/AGENTS.md +103 -0
- package/src/components/context-menu/context-menu-content.tsx +64 -0
- package/src/components/context-menu/context-menu-item.tsx +22 -0
- package/src/components/context-menu/context-menu-preview.tsx +60 -0
- package/src/components/context-menu/context-menu-trigger.tsx +160 -0
- package/src/components/context-menu/context-menu.context.tsx +91 -0
- package/src/components/context-menu/context-menu.tsx +90 -0
- package/src/components/context-menu/context-menu.variants.test.ts +266 -0
- package/src/components/context-menu/context-menu.variants.ts +147 -0
- package/src/components/context-menu/index.ts +18 -0
- package/src/components/dialog/AGENTS.md +97 -0
- package/src/components/dialog/dialog-body.tsx +21 -0
- package/src/components/dialog/dialog-close.tsx +82 -0
- package/src/components/dialog/dialog-content.tsx +153 -0
- package/src/components/dialog/dialog-description.tsx +23 -0
- package/src/components/dialog/dialog-footer.tsx +32 -0
- package/src/components/dialog/dialog-header.tsx +20 -0
- package/src/components/dialog/dialog-title.tsx +42 -0
- package/src/components/dialog/dialog-trigger.tsx +54 -0
- package/src/components/dialog/dialog.context.tsx +90 -0
- package/src/components/dialog/dialog.tsx +119 -0
- package/src/components/dialog/dialog.variants.test.ts +224 -0
- package/src/components/dialog/dialog.variants.ts +173 -0
- package/src/components/dialog/index.ts +30 -0
- package/src/components/drawer/AGENTS.md +123 -0
- package/src/components/drawer/drawer-body.tsx +46 -0
- package/src/components/drawer/drawer-close.tsx +76 -0
- package/src/components/drawer/drawer-content.tsx +186 -0
- package/src/components/drawer/drawer-description.tsx +23 -0
- package/src/components/drawer/drawer-footer.tsx +21 -0
- package/src/components/drawer/drawer-header.tsx +42 -0
- package/src/components/drawer/drawer-title.tsx +35 -0
- package/src/components/drawer/drawer-trigger.tsx +54 -0
- package/src/components/drawer/drawer.context.tsx +114 -0
- package/src/components/drawer/drawer.tsx +119 -0
- package/src/components/drawer/drawer.variants.test.ts +392 -0
- package/src/components/drawer/drawer.variants.ts +244 -0
- package/src/components/drawer/index.ts +41 -0
- package/src/components/drawer/use-drawer-pan.ts +108 -0
- package/src/components/empty-state/AGENTS.md +90 -0
- package/src/components/empty-state/empty-state-content.tsx +17 -0
- package/src/components/empty-state/empty-state-description.tsx +12 -0
- package/src/components/empty-state/empty-state-header.tsx +17 -0
- package/src/components/empty-state/empty-state-media.tsx +56 -0
- package/src/components/empty-state/empty-state-title.tsx +18 -0
- package/src/components/empty-state/empty-state.context.tsx +65 -0
- package/src/components/empty-state/empty-state.tsx +84 -0
- package/src/components/empty-state/empty-state.types.ts +14 -0
- package/src/components/empty-state/empty-state.variants.test.ts +245 -0
- package/src/components/empty-state/empty-state.variants.ts +132 -0
- package/src/components/empty-state/index.ts +20 -0
- package/src/components/fab/AGENTS.md +119 -0
- package/src/components/fab/fab-action.tsx +118 -0
- package/src/components/fab/fab-group.tsx +214 -0
- package/src/components/fab/fab-label.tsx +18 -0
- package/src/components/fab/fab.context.tsx +125 -0
- package/src/components/fab/fab.tsx +156 -0
- package/src/components/fab/fab.types.ts +17 -0
- package/src/components/fab/fab.variants.test.ts +274 -0
- package/src/components/fab/fab.variants.ts +234 -0
- package/src/components/fab/index.ts +37 -0
- package/src/components/feedback/AGENTS.md +95 -0
- package/src/components/feedback/feedback-action.tsx +16 -0
- package/src/components/feedback/feedback-cancel.tsx +34 -0
- package/src/components/feedback/feedback-close.tsx +21 -0
- package/src/components/feedback/feedback-content.tsx +24 -0
- package/src/components/feedback/feedback-field.tsx +70 -0
- package/src/components/feedback/feedback-footer.tsx +21 -0
- package/src/components/feedback/feedback-panel.tsx +54 -0
- package/src/components/feedback/feedback-submit.tsx +74 -0
- package/src/components/feedback/feedback-title.tsx +34 -0
- package/src/components/feedback/feedback-trigger.tsx +20 -0
- package/src/components/feedback/feedback.context.tsx +73 -0
- package/src/components/feedback/feedback.tsx +135 -0
- package/src/components/feedback/feedback.variants.test.ts +185 -0
- package/src/components/feedback/feedback.variants.ts +128 -0
- package/src/components/feedback/index.ts +24 -0
- package/src/components/field/AGENTS.md +19 -0
- package/src/components/field/field-label.tsx +19 -5
- package/src/components/field/field.context.tsx +17 -0
- package/src/components/field/field.tsx +15 -2
- package/src/components/field/field.variants.test.ts +24 -0
- package/src/components/field/field.variants.ts +20 -0
- package/src/components/field/index.ts +1 -0
- package/src/components/item/AGENTS.md +92 -0
- package/src/components/item/index.ts +25 -0
- package/src/components/item/item-actions.tsx +28 -0
- package/src/components/item/item-content.tsx +15 -0
- package/src/components/item/item-description.tsx +12 -0
- package/src/components/item/item-footer.tsx +13 -0
- package/src/components/item/item-group.tsx +32 -0
- package/src/components/item/item-header.tsx +13 -0
- package/src/components/item/item-media.tsx +37 -0
- package/src/components/item/item-separator.tsx +13 -0
- package/src/components/item/item-title.tsx +17 -0
- package/src/components/item/item.context.tsx +66 -0
- package/src/components/item/item.tsx +218 -0
- package/src/components/item/item.types.ts +12 -0
- package/src/components/item/item.variants.test.ts +263 -0
- package/src/components/item/item.variants.ts +200 -0
- package/src/components/kpi/AGENTS.md +125 -0
- package/src/components/kpi/index.ts +45 -0
- package/src/components/kpi/kpi-action.tsx +12 -0
- package/src/components/kpi/kpi-content.tsx +35 -0
- package/src/components/kpi/kpi-footer.tsx +52 -0
- package/src/components/kpi/kpi-group.tsx +70 -0
- package/src/components/kpi/kpi-header.tsx +25 -0
- package/src/components/kpi/kpi-icon.tsx +28 -0
- package/src/components/kpi/kpi-sparkline.tsx +145 -0
- package/src/components/kpi/kpi-stat.tsx +20 -0
- package/src/components/kpi/kpi-title.tsx +19 -0
- package/src/components/kpi/kpi-trend.tsx +105 -0
- package/src/components/kpi/kpi-value.tsx +32 -0
- package/src/components/kpi/kpi.context.tsx +111 -0
- package/src/components/kpi/kpi.tsx +154 -0
- package/src/components/kpi/kpi.types.ts +9 -0
- package/src/components/kpi/kpi.variants.test.ts +428 -0
- package/src/components/kpi/kpi.variants.ts +347 -0
- package/src/components/label/AGENTS.md +89 -0
- package/src/components/label/index.ts +10 -0
- package/src/components/label/label.tsx +97 -0
- package/src/components/label/label.variants.test.ts +176 -0
- package/src/components/label/label.variants.ts +123 -0
- package/src/components/menu/AGENTS.md +176 -0
- package/src/components/menu/index.ts +44 -0
- package/src/components/menu/menu-background.tsx +17 -0
- package/src/components/menu/menu-checkbox-item.tsx +57 -0
- package/src/components/menu/menu-content.tsx +303 -0
- package/src/components/menu/menu-item.tsx +162 -0
- package/src/components/menu/menu-label.tsx +14 -0
- package/src/components/menu/menu-radio-group.tsx +50 -0
- package/src/components/menu/menu-radio-item.tsx +57 -0
- package/src/components/menu/menu-separator.tsx +11 -0
- package/src/components/menu/menu-sub-content.tsx +72 -0
- package/src/components/menu/menu-sub-trigger.tsx +50 -0
- package/src/components/menu/menu-sub.tsx +75 -0
- package/src/components/menu/menu-trigger.tsx +57 -0
- package/src/components/menu/menu.context.tsx +214 -0
- package/src/components/menu/menu.tsx +88 -0
- package/src/components/menu/menu.types.ts +33 -0
- package/src/components/menu/menu.variants.test.ts +400 -0
- package/src/components/menu/menu.variants.ts +353 -0
- package/src/components/meter/AGENTS.md +135 -0
- package/src/components/meter/index.ts +43 -0
- package/src/components/meter/meter-fill.tsx +19 -0
- package/src/components/meter/meter-header.tsx +17 -0
- package/src/components/meter/meter-label.tsx +18 -0
- package/src/components/meter/meter-output.tsx +42 -0
- package/src/components/meter/meter-segments.tsx +83 -0
- package/src/components/meter/meter-track.tsx +16 -0
- package/src/components/meter/meter.context.tsx +62 -0
- package/src/components/meter/meter.tsx +286 -0
- package/src/components/meter/meter.types.ts +30 -0
- package/src/components/meter/meter.variants.test.ts +430 -0
- package/src/components/meter/meter.variants.ts +347 -0
- package/src/components/overlay/AGENTS.md +89 -0
- package/src/components/overlay/index.ts +27 -0
- package/src/components/overlay/overlay-portal.tsx +84 -0
- package/src/components/overlay/overlay-presence.test.ts +46 -0
- package/src/components/overlay/overlay-presence.ts +49 -0
- package/src/components/overlay/overlay-registry.test.ts +96 -0
- package/src/components/overlay/overlay-registry.ts +99 -0
- package/src/components/overlay/overlay-scrim.tsx +49 -0
- package/src/components/overlay/overlay.context.tsx +48 -0
- package/src/components/overlay/overlay.tsx +118 -0
- package/src/components/overlay/overlay.variants.test.ts +37 -0
- package/src/components/overlay/overlay.variants.ts +33 -0
- package/src/components/overlay/use-overlay-back-handler.ts +34 -0
- package/src/components/overlay/use-overlay-presence.ts +87 -0
- package/src/components/popover/AGENTS.md +138 -0
- package/src/components/popover/index.ts +48 -0
- package/src/components/popover/popover-anchor.tsx +49 -0
- package/src/components/popover/popover-arrow.tsx +89 -0
- package/src/components/popover/popover-close.tsx +73 -0
- package/src/components/popover/popover-content.tsx +235 -0
- package/src/components/popover/popover-description.tsx +18 -0
- package/src/components/popover/popover-title.tsx +33 -0
- package/src/components/popover/popover-trigger.tsx +56 -0
- package/src/components/popover/popover.context.tsx +71 -0
- package/src/components/popover/popover.position.test.ts +354 -0
- package/src/components/popover/popover.position.ts +286 -0
- package/src/components/popover/popover.tsx +123 -0
- package/src/components/popover/popover.variants.test.ts +135 -0
- package/src/components/popover/popover.variants.ts +85 -0
- package/src/components/popover/use-anchor-measure.ts +76 -0
- package/src/components/popover/use-anchored-content.ts +219 -0
- package/src/components/pressable/AGENTS.md +11 -1
- package/src/components/pressable/pressable.tsx +7 -2
- package/src/components/pressable/pressable.variants.test.ts +29 -0
- package/src/components/pressable/pressable.variants.ts +17 -0
- package/src/components/progress/AGENTS.md +120 -0
- package/src/components/progress/index.ts +40 -0
- package/src/components/progress/progress-fill.tsx +152 -0
- package/src/components/progress/progress-header.tsx +19 -0
- package/src/components/progress/progress-label.tsx +27 -0
- package/src/components/progress/progress-output.tsx +47 -0
- package/src/components/progress/progress-track.tsx +38 -0
- package/src/components/progress/progress.context.tsx +79 -0
- package/src/components/progress/progress.tsx +171 -0
- package/src/components/progress/progress.types.ts +23 -0
- package/src/components/progress/progress.variants.test.ts +335 -0
- package/src/components/progress/progress.variants.ts +284 -0
- package/src/components/progress-button/AGENTS.md +116 -0
- package/src/components/progress-button/index.ts +36 -0
- package/src/components/progress-button/progress-button-done.tsx +33 -0
- package/src/components/progress-button/progress-button-label.tsx +27 -0
- package/src/components/progress-button/progress-button.context.tsx +93 -0
- package/src/components/progress-button/progress-button.tsx +414 -0
- package/src/components/progress-button/progress-button.variants.test.ts +314 -0
- package/src/components/progress-button/progress-button.variants.ts +236 -0
- package/src/components/provider/AGENTS.md +38 -25
- package/src/components/provider/provider.tsx +38 -11
- package/src/components/radio/AGENTS.md +8 -10
- package/src/components/radio/radio.variants.test.ts +5 -6
- package/src/components/radio/radio.variants.ts +6 -7
- package/src/components/rating/AGENTS.md +143 -0
- package/src/components/rating/index.ts +43 -0
- package/src/components/rating/rating-output.tsx +39 -0
- package/src/components/rating/rating-star.tsx +70 -0
- package/src/components/rating/rating-stars.tsx +219 -0
- package/src/components/rating/rating.context.tsx +79 -0
- package/src/components/rating/rating.tsx +163 -0
- package/src/components/rating/rating.variants.test.ts +347 -0
- package/src/components/rating/rating.variants.ts +354 -0
- package/src/components/selection-mode/AGENTS.md +155 -0
- package/src/components/selection-mode/index.ts +43 -0
- package/src/components/selection-mode/selection-mode-action.tsx +65 -0
- package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
- package/src/components/selection-mode/selection-mode-group.tsx +123 -0
- package/src/components/selection-mode/selection-mode-header.tsx +85 -0
- package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
- package/src/components/selection-mode/selection-mode-item.tsx +182 -0
- package/src/components/selection-mode/selection-mode.context.tsx +89 -0
- package/src/components/selection-mode/selection-mode.tsx +207 -0
- package/src/components/selection-mode/selection-mode.types.ts +36 -0
- package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
- package/src/components/selection-mode/selection-mode.variants.ts +329 -0
- package/src/components/skeleton/AGENTS.md +120 -0
- package/src/components/skeleton/index.ts +15 -0
- package/src/components/skeleton/skeleton-clock.ts +48 -0
- package/src/components/skeleton/skeleton-group.tsx +64 -0
- package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
- package/src/components/skeleton/skeleton.context.tsx +45 -0
- package/src/components/skeleton/skeleton.tsx +227 -0
- package/src/components/skeleton/skeleton.variants.test.ts +245 -0
- package/src/components/skeleton/skeleton.variants.ts +222 -0
- package/src/components/slide-button/AGENTS.md +122 -0
- package/src/components/slide-button/index.ts +43 -0
- package/src/components/slide-button/slide-button-label.tsx +70 -0
- package/src/components/slide-button/slide-button-thumb.tsx +93 -0
- package/src/components/slide-button/slide-button.context.tsx +76 -0
- package/src/components/slide-button/slide-button.tsx +457 -0
- package/src/components/slide-button/slide-button.variants.test.ts +288 -0
- package/src/components/slide-button/slide-button.variants.ts +324 -0
- package/src/components/slider/index.ts +2 -0
- package/src/components/slider/slider-thumb.tsx +33 -1
- package/src/components/slider/slider.variants.test.ts +52 -0
- package/src/components/slider/slider.variants.ts +56 -2
- package/src/components/stack-card/AGENTS.md +169 -0
- package/src/components/stack-card/index.ts +37 -0
- package/src/components/stack-card/stack-card-action.tsx +66 -0
- package/src/components/stack-card/stack-card-actions.tsx +18 -0
- package/src/components/stack-card/stack-card-card.tsx +71 -0
- package/src/components/stack-card/stack-card-empty.tsx +30 -0
- package/src/components/stack-card/stack-card-slot.tsx +90 -0
- package/src/components/stack-card/stack-card-stamp.tsx +76 -0
- package/src/components/stack-card/stack-card.context.tsx +117 -0
- package/src/components/stack-card/stack-card.tsx +557 -0
- package/src/components/stack-card/stack-card.types.ts +26 -0
- package/src/components/stack-card/stack-card.variants.test.ts +355 -0
- package/src/components/stack-card/stack-card.variants.ts +360 -0
- package/src/components/steps/AGENTS.md +153 -0
- package/src/components/steps/index.ts +45 -0
- package/src/components/steps/steps-connector.tsx +31 -0
- package/src/components/steps/steps-description.tsx +26 -0
- package/src/components/steps/steps-indicator.tsx +106 -0
- package/src/components/steps/steps-item.tsx +225 -0
- package/src/components/steps/steps-panel.tsx +41 -0
- package/src/components/steps/steps-title.tsx +32 -0
- package/src/components/steps/steps-track.tsx +59 -0
- package/src/components/steps/steps.context.tsx +123 -0
- package/src/components/steps/steps.tsx +166 -0
- package/src/components/steps/steps.variants.test.ts +388 -0
- package/src/components/steps/steps.variants.ts +468 -0
- package/src/components/surface/AGENTS.md +72 -0
- package/src/components/surface/index.ts +15 -0
- package/src/components/surface/surface.context.tsx +54 -0
- package/src/components/surface/surface.tsx +70 -0
- package/src/components/surface/surface.variants.test.ts +180 -0
- package/src/components/surface/surface.variants.ts +133 -0
- package/src/components/swipe/AGENTS.md +140 -0
- package/src/components/swipe/index.ts +31 -0
- package/src/components/swipe/swipe-action.tsx +99 -0
- package/src/components/swipe/swipe-end.tsx +14 -0
- package/src/components/swipe/swipe-group.tsx +55 -0
- package/src/components/swipe/swipe-panel.tsx +55 -0
- package/src/components/swipe/swipe-start.tsx +15 -0
- package/src/components/swipe/swipe.context.tsx +135 -0
- package/src/components/swipe/swipe.tsx +393 -0
- package/src/components/swipe/swipe.types.ts +7 -0
- package/src/components/swipe/swipe.variants.test.ts +337 -0
- package/src/components/swipe/swipe.variants.ts +290 -0
- package/src/components/switch/switch.variants.ts +2 -2
- package/src/components/tabs/AGENTS.md +4 -3
- package/src/components/tabs/tabs.variants.test.ts +1 -1
- package/src/components/tabs/tabs.variants.ts +4 -6
- package/src/components/textarea/AGENTS.md +86 -0
- package/src/components/textarea/index.ts +16 -0
- package/src/components/textarea/textarea.tsx +157 -0
- package/src/components/textarea/textarea.variants.test.ts +219 -0
- package/src/components/textarea/textarea.variants.ts +171 -0
- package/src/components/toast/AGENTS.md +140 -0
- package/src/components/toast/index.ts +74 -0
- package/src/components/toast/toast-action.tsx +59 -0
- package/src/components/toast/toast-api.ts +32 -0
- package/src/components/toast/toast-close.tsx +44 -0
- package/src/components/toast/toast-content.tsx +11 -0
- package/src/components/toast/toast-description.tsx +11 -0
- package/src/components/toast/toast-indicator.tsx +34 -0
- package/src/components/toast/toast-item.tsx +308 -0
- package/src/components/toast/toast-title.tsx +13 -0
- package/src/components/toast/toast-viewport.tsx +135 -0
- package/src/components/toast/toast.context.tsx +69 -0
- package/src/components/toast/toast.store.test.ts +259 -0
- package/src/components/toast/toast.store.ts +338 -0
- package/src/components/toast/toast.timer.test.ts +64 -0
- package/src/components/toast/toast.timer.ts +51 -0
- package/src/components/toast/toast.tsx +132 -0
- package/src/components/toast/toast.variants.test.ts +393 -0
- package/src/components/toast/toast.variants.ts +304 -0
- package/src/components/toggle-button/AGENTS.md +106 -0
- package/src/components/toggle-button/index.ts +30 -0
- package/src/components/toggle-button/toggle-button-group.tsx +156 -0
- package/src/components/toggle-button/toggle-button-label.tsx +19 -0
- package/src/components/toggle-button/toggle-button.context.tsx +130 -0
- package/src/components/toggle-button/toggle-button.tsx +172 -0
- package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
- package/src/components/toggle-button/toggle-button.variants.ts +160 -0
- package/src/components/tooltip/AGENTS.md +133 -0
- package/src/components/tooltip/index.ts +28 -0
- package/src/components/tooltip/tooltip-arrow.tsx +40 -0
- package/src/components/tooltip/tooltip-content.tsx +179 -0
- package/src/components/tooltip/tooltip-description.tsx +19 -0
- package/src/components/tooltip/tooltip-text.tsx +21 -0
- package/src/components/tooltip/tooltip-title.tsx +20 -0
- package/src/components/tooltip/tooltip-trigger.tsx +103 -0
- package/src/components/tooltip/tooltip.context.tsx +68 -0
- package/src/components/tooltip/tooltip.tsx +213 -0
- package/src/components/tooltip/tooltip.variants.test.ts +231 -0
- package/src/components/tooltip/tooltip.variants.ts +177 -0
- package/src/hooks/use-calm-motion.tsx +31 -0
- package/src/lib/calm-motion.test.ts +17 -0
- package/src/lib/calm-motion.ts +23 -0
- package/src/lib/cn.test.ts +14 -0
- package/src/lib/cn.ts +2 -2
- package/src/no-gorhom.test.ts +54 -0
- package/src/styles/geometry.test.ts +1 -0
- package/src/styles/theme.css +4 -0
- package/src/styles/tokens.css +11 -0
- package/src/styles/tokens.test.ts +22 -0
- package/src/styles/tokens.ts +20 -0
- package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
- package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isValidElement,
|
|
3
|
+
type ReactElement,
|
|
4
|
+
type ReactNode,
|
|
5
|
+
type Ref,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useImperativeHandle,
|
|
9
|
+
useLayoutEffect,
|
|
10
|
+
useMemo,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from "react";
|
|
14
|
+
import { type LayoutChangeEvent, View, type ViewProps } from "react-native";
|
|
15
|
+
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
16
|
+
import Animated, { useReducedMotion, useSharedValue, withSpring, withTiming } from "react-native-reanimated";
|
|
17
|
+
import { scheduleOnRN, scheduleOnUI } from "react-native-worklets";
|
|
18
|
+
import { useControllableState } from "../../hooks/use-controllable-state";
|
|
19
|
+
import { cn } from "../../lib/cn";
|
|
20
|
+
import { type HapticFeedback, playHaptic } from "../pressable";
|
|
21
|
+
import { type StackCardContextValue, StackCardProvider } from "./stack-card.context";
|
|
22
|
+
import type { StackCardHandle } from "./stack-card.types";
|
|
23
|
+
import {
|
|
24
|
+
partitionStackChildren,
|
|
25
|
+
resolveDragOffset,
|
|
26
|
+
resolveExitOffset,
|
|
27
|
+
resolveMountedWindow,
|
|
28
|
+
resolveStackDepth,
|
|
29
|
+
resolveStackRelease,
|
|
30
|
+
STACK_CARD_DEFAULT_DIRECTION_LABELS,
|
|
31
|
+
STACK_CARD_DEFAULT_DIRECTIONS,
|
|
32
|
+
STACK_CARD_DEFAULT_THRESHOLD,
|
|
33
|
+
type StackCardChildKind,
|
|
34
|
+
type StackCardDirection,
|
|
35
|
+
type StackCardLayout,
|
|
36
|
+
type StackCardStampColor,
|
|
37
|
+
stackCardVariants,
|
|
38
|
+
} from "./stack-card.variants";
|
|
39
|
+
import { StackCardAction } from "./stack-card-action";
|
|
40
|
+
import { StackCardActions } from "./stack-card-actions";
|
|
41
|
+
import { StackCardCard } from "./stack-card-card";
|
|
42
|
+
import { StackCardEmpty } from "./stack-card-empty";
|
|
43
|
+
import { StackCardSlot } from "./stack-card-slot";
|
|
44
|
+
import { StackCardStamp, type StackCardStampProps } from "./stack-card-stamp";
|
|
45
|
+
|
|
46
|
+
export type StackCardProps = Omit<ViewProps, "children"> & {
|
|
47
|
+
/** Stamps, cards in order, an `Empty` and an `Actions` row, in any order. */
|
|
48
|
+
children: ReactNode;
|
|
49
|
+
/** Give it a height: the pile takes whatever the actions row leaves. */
|
|
50
|
+
className?: string;
|
|
51
|
+
pileClassName?: string;
|
|
52
|
+
/** Controlled top-card index. */
|
|
53
|
+
index?: number;
|
|
54
|
+
/** Starting top-card index while uncontrolled. Defaults to 0. */
|
|
55
|
+
defaultIndex?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Called with the next index after a throw or an undo. A controlled deck that
|
|
58
|
+
* leaves `index` where it was **declines** the throw, and the card flies back.
|
|
59
|
+
*/
|
|
60
|
+
onIndexChange?: (index: number) => void;
|
|
61
|
+
/** Called with the direction and the thrown card's index, before `onIndexChange`. */
|
|
62
|
+
onSwipe?: (direction: StackCardDirection, index: number) => void;
|
|
63
|
+
/** Called once, when the last card leaves. */
|
|
64
|
+
onEmpty?: () => void;
|
|
65
|
+
/** The directions a drag throws in. Defaults to left and right. */
|
|
66
|
+
directions?: readonly StackCardDirection[];
|
|
67
|
+
/** How the cards behind the top are drawn. Defaults to `stack`. */
|
|
68
|
+
layout?: StackCardLayout;
|
|
69
|
+
/** Cards drawn behind the top: 0..4, default 2. */
|
|
70
|
+
depth?: number;
|
|
71
|
+
/** Fraction of the card's width or height a drag must cover. Defaults to 0.3. */
|
|
72
|
+
threshold?: number;
|
|
73
|
+
isDisabled?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Ticks when a drag first crosses the threshold, and again each time it
|
|
76
|
+
* crosses back out and in; a throw knocks with `medium`. `false` silences
|
|
77
|
+
* both. Defaults to `selection`.
|
|
78
|
+
*/
|
|
79
|
+
haptic?: false | HapticFeedback;
|
|
80
|
+
/** What a screen reader and an action button call each direction. */
|
|
81
|
+
directionLabels?: Partial<Record<StackCardDirection, string>>;
|
|
82
|
+
ref?: Ref<StackCardHandle>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** A throw or an undo the deck asked for, waiting to see whether the index moved. */
|
|
86
|
+
type StackCardPending =
|
|
87
|
+
| { kind: "throw"; from: number; direction: StackCardDirection }
|
|
88
|
+
| { kind: "undo"; from: number; direction: StackCardDirection };
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The spring a card returns to the pile on. Critically damped — damping is
|
|
92
|
+
* `2·√(stiffness·mass)` ≈ 26.5, rounded up — because any overshoot carries the
|
|
93
|
+
* card past rest to the other side, where the opposite stamp fades in: a card
|
|
94
|
+
* undone from the left would flash the right-hand answer as it lands.
|
|
95
|
+
*/
|
|
96
|
+
const RETURN_SPRING = { damping: 27, mass: 0.8, stiffness: 220 } as const;
|
|
97
|
+
|
|
98
|
+
/** A reduced-motion fade, in milliseconds. */
|
|
99
|
+
const FADE_MS = 180;
|
|
100
|
+
|
|
101
|
+
/** Which part a child of the deck is — anything unrecognised is a card. */
|
|
102
|
+
function kindOf(node: ReactNode): StackCardChildKind {
|
|
103
|
+
if (!isValidElement(node)) return "card";
|
|
104
|
+
if (node.type === StackCardStamp) return "stamp";
|
|
105
|
+
if (node.type === StackCardEmpty) return "empty";
|
|
106
|
+
if (node.type === StackCardActions) return "actions";
|
|
107
|
+
return "card";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function StackCardRoot({
|
|
111
|
+
children,
|
|
112
|
+
className,
|
|
113
|
+
pileClassName,
|
|
114
|
+
index: indexProp,
|
|
115
|
+
defaultIndex = 0,
|
|
116
|
+
onIndexChange,
|
|
117
|
+
onSwipe,
|
|
118
|
+
onEmpty,
|
|
119
|
+
directions = STACK_CARD_DEFAULT_DIRECTIONS,
|
|
120
|
+
layout = "stack",
|
|
121
|
+
depth: depthProp,
|
|
122
|
+
threshold = STACK_CARD_DEFAULT_THRESHOLD,
|
|
123
|
+
isDisabled = false,
|
|
124
|
+
haptic = "selection",
|
|
125
|
+
directionLabels,
|
|
126
|
+
ref,
|
|
127
|
+
...props
|
|
128
|
+
}: StackCardProps): ReactElement {
|
|
129
|
+
const { stamps, cards, empty, actions } = partitionStackChildren(children, kindOf);
|
|
130
|
+
const count = cards.length;
|
|
131
|
+
const depth = resolveStackDepth(depthProp);
|
|
132
|
+
const isReducedMotion = useReducedMotion();
|
|
133
|
+
|
|
134
|
+
const [index, setIndex] = useControllableState<number>({
|
|
135
|
+
defaultValue: defaultIndex,
|
|
136
|
+
onChange: onIndexChange,
|
|
137
|
+
value: indexProp,
|
|
138
|
+
});
|
|
139
|
+
const [history, setHistory] = useState<StackCardDirection[]>([]);
|
|
140
|
+
const [pending, setPending] = useState<StackCardPending | null>(null);
|
|
141
|
+
|
|
142
|
+
const x = useSharedValue(0);
|
|
143
|
+
const y = useSharedValue(0);
|
|
144
|
+
const top = useSharedValue(index);
|
|
145
|
+
const fade = useSharedValue(1);
|
|
146
|
+
const width = useSharedValue(0);
|
|
147
|
+
const height = useSharedValue(0);
|
|
148
|
+
const isBusy = useSharedValue(false);
|
|
149
|
+
const isArmed = useSharedValue(false);
|
|
150
|
+
|
|
151
|
+
// The newest callbacks, for the JS functions a worklet calls back into. A
|
|
152
|
+
// worklet captures what it closed over when it was built, so it reaches the
|
|
153
|
+
// caller's handlers through this rather than through a stale closure.
|
|
154
|
+
const latest = useRef({ onEmpty, onSwipe, setIndex });
|
|
155
|
+
useLayoutEffect(() => {
|
|
156
|
+
latest.current = { onEmpty, onSwipe, setIndex };
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const advance = useCallback((direction: StackCardDirection, from: number) => {
|
|
160
|
+
latest.current.onSwipe?.(direction, from);
|
|
161
|
+
latest.current.setIndex(from + 1);
|
|
162
|
+
setPending({ direction, from, kind: "throw" });
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
// Throws the top card. Runs on the UI thread, from the pan's release or from
|
|
166
|
+
// a JS call through `scheduleOnUI`. The top moves the frame the throw lands,
|
|
167
|
+
// before React hears of it, so the card behind is already the top.
|
|
168
|
+
const fling = useCallback(
|
|
169
|
+
(direction: StackCardDirection, vx: number, vy: number) => {
|
|
170
|
+
"worklet";
|
|
171
|
+
if (isBusy.value) return;
|
|
172
|
+
const from = top.value;
|
|
173
|
+
if (from >= count) return;
|
|
174
|
+
isBusy.value = true;
|
|
175
|
+
isArmed.value = false;
|
|
176
|
+
if (haptic !== false) playHaptic("medium");
|
|
177
|
+
const land = () => {
|
|
178
|
+
"worklet";
|
|
179
|
+
top.value = from + 1;
|
|
180
|
+
x.value = 0;
|
|
181
|
+
y.value = 0;
|
|
182
|
+
fade.value = 1;
|
|
183
|
+
isBusy.value = false;
|
|
184
|
+
scheduleOnRN(advance, direction, from);
|
|
185
|
+
};
|
|
186
|
+
if (isReducedMotion) {
|
|
187
|
+
fade.value = withTiming(0, { duration: FADE_MS }, () => land());
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const exit = resolveExitOffset({ direction, height: height.value, width: width.value });
|
|
191
|
+
const isHorizontal = direction === "left" || direction === "right";
|
|
192
|
+
const targetX = isHorizontal ? exit.x : x.value + vx * 0.1;
|
|
193
|
+
const targetY = isHorizontal ? y.value + vy * 0.1 : exit.y;
|
|
194
|
+
const speed = Math.max(1, Math.hypot(vx, vy));
|
|
195
|
+
const distance = Math.hypot(targetX - x.value, targetY - y.value);
|
|
196
|
+
const duration = Math.min(320, Math.max(160, (distance / speed) * 1000));
|
|
197
|
+
x.value = withTiming(targetX, { duration });
|
|
198
|
+
y.value = withTiming(targetY, { duration }, () => land());
|
|
199
|
+
},
|
|
200
|
+
[advance, count, fade, haptic, height, isArmed, isBusy, isReducedMotion, top, width, x, y]
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Brings the top card back to rest from wherever it is: a short drag
|
|
204
|
+
// released, or a card arriving from `fromX`/`fromY`.
|
|
205
|
+
const settle = useCallback(
|
|
206
|
+
(fromX: number, fromY: number, fadeIn: boolean) => {
|
|
207
|
+
"worklet";
|
|
208
|
+
if (isReducedMotion) {
|
|
209
|
+
x.value = 0;
|
|
210
|
+
y.value = 0;
|
|
211
|
+
if (fadeIn) {
|
|
212
|
+
fade.value = 0;
|
|
213
|
+
fade.value = withTiming(1, { duration: FADE_MS }, () => {
|
|
214
|
+
isBusy.value = false;
|
|
215
|
+
});
|
|
216
|
+
} else {
|
|
217
|
+
fade.value = 1;
|
|
218
|
+
isBusy.value = false;
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
fade.value = 1;
|
|
223
|
+
x.value = fromX;
|
|
224
|
+
y.value = fromY;
|
|
225
|
+
x.value = withSpring(0, RETURN_SPRING);
|
|
226
|
+
y.value = withSpring(0, RETURN_SPRING, () => {
|
|
227
|
+
isBusy.value = false;
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
[fade, isBusy, isReducedMotion, x, y]
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const swipe = useCallback(
|
|
234
|
+
(direction: StackCardDirection) => {
|
|
235
|
+
scheduleOnUI(fling, direction, 0, 0);
|
|
236
|
+
},
|
|
237
|
+
[fling]
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const canUndo = !isDisabled && pending === null && index > 0 && history.length > 0;
|
|
241
|
+
|
|
242
|
+
const undo = useCallback(() => {
|
|
243
|
+
const direction = history.at(-1);
|
|
244
|
+
if (!canUndo || direction === undefined) return;
|
|
245
|
+
const to = index - 1;
|
|
246
|
+
setPending({ direction, from: index, kind: "undo" });
|
|
247
|
+
setIndex(to);
|
|
248
|
+
scheduleOnUI(() => {
|
|
249
|
+
"worklet";
|
|
250
|
+
isBusy.value = true;
|
|
251
|
+
top.value = to;
|
|
252
|
+
const exit = resolveExitOffset({ direction, height: height.value, width: width.value });
|
|
253
|
+
settle(exit.x, exit.y, true);
|
|
254
|
+
});
|
|
255
|
+
}, [canUndo, height, history, index, isBusy, setIndex, settle, top, width]);
|
|
256
|
+
|
|
257
|
+
useImperativeHandle(ref, () => ({ swipe, undo }), [swipe, undo]);
|
|
258
|
+
|
|
259
|
+
// Settles a throw or an undo once React has the index the caller chose. A
|
|
260
|
+
// controlled deck that left `index` where it was declined it: a thrown card
|
|
261
|
+
// flies back in from where it left, and a refused undo snaps back. Any other
|
|
262
|
+
// change to `index` came from outside, and the pile simply follows it.
|
|
263
|
+
const syncedIndex = useRef(index);
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
if (pending) {
|
|
266
|
+
setPending(null);
|
|
267
|
+
syncedIndex.current = index;
|
|
268
|
+
if (pending.kind === "throw") {
|
|
269
|
+
if (index !== pending.from) {
|
|
270
|
+
setHistory((current) => [...current, pending.direction]);
|
|
271
|
+
scheduleOnUI(() => {
|
|
272
|
+
"worklet";
|
|
273
|
+
top.value = index;
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const { direction, from } = pending;
|
|
278
|
+
scheduleOnUI(() => {
|
|
279
|
+
"worklet";
|
|
280
|
+
isBusy.value = true;
|
|
281
|
+
top.value = from;
|
|
282
|
+
const exit = resolveExitOffset({ direction, height: height.value, width: width.value });
|
|
283
|
+
settle(exit.x, exit.y, true);
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (index !== pending.from) {
|
|
288
|
+
setHistory((current) => current.slice(0, -1));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
scheduleOnUI(() => {
|
|
292
|
+
"worklet";
|
|
293
|
+
top.value = index;
|
|
294
|
+
x.value = 0;
|
|
295
|
+
y.value = 0;
|
|
296
|
+
fade.value = 1;
|
|
297
|
+
isBusy.value = false;
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (syncedIndex.current === index) return;
|
|
302
|
+
syncedIndex.current = index;
|
|
303
|
+
setHistory([]);
|
|
304
|
+
scheduleOnUI(() => {
|
|
305
|
+
"worklet";
|
|
306
|
+
top.value = index;
|
|
307
|
+
x.value = 0;
|
|
308
|
+
y.value = 0;
|
|
309
|
+
fade.value = 1;
|
|
310
|
+
isBusy.value = false;
|
|
311
|
+
});
|
|
312
|
+
}, [fade, height, index, isBusy, pending, settle, top, width, x, y]);
|
|
313
|
+
|
|
314
|
+
// `onEmpty` fires on the step that empties the deck, once — not on mount for
|
|
315
|
+
// a deck that starts empty, and not again until it has been refilled.
|
|
316
|
+
const previousIndex = useRef(index);
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
const previous = previousIndex.current;
|
|
319
|
+
previousIndex.current = index;
|
|
320
|
+
if (count > 0 && previous < count && index >= count) latest.current.onEmpty?.();
|
|
321
|
+
}, [count, index]);
|
|
322
|
+
|
|
323
|
+
const isHorizontalOnly = directions.every((direction) => direction === "left" || direction === "right");
|
|
324
|
+
const isVerticalOnly = directions.every((direction) => direction === "up" || direction === "down");
|
|
325
|
+
const isGestureEnabled = !isDisabled && index < count;
|
|
326
|
+
|
|
327
|
+
const gesture = useMemo(() => {
|
|
328
|
+
const pan = Gesture.Pan().enabled(isGestureEnabled);
|
|
329
|
+
// A horizontal deck lets a vertical drag fall through to the scroll view
|
|
330
|
+
// around it. A deck that throws both ways claims both axes, so it cannot
|
|
331
|
+
// sit inside a scroller.
|
|
332
|
+
if (isHorizontalOnly) pan.activeOffsetX([-10, 10]).failOffsetY([-10, 10]);
|
|
333
|
+
else if (isVerticalOnly) pan.activeOffsetY([-10, 10]).failOffsetX([-10, 10]);
|
|
334
|
+
else pan.minDistance(10);
|
|
335
|
+
|
|
336
|
+
return pan
|
|
337
|
+
.onUpdate((event) => {
|
|
338
|
+
"worklet";
|
|
339
|
+
if (isBusy.value) return;
|
|
340
|
+
const offset = resolveDragOffset({
|
|
341
|
+
directions,
|
|
342
|
+
translationX: event.translationX,
|
|
343
|
+
translationY: event.translationY,
|
|
344
|
+
});
|
|
345
|
+
x.value = offset.x;
|
|
346
|
+
y.value = offset.y;
|
|
347
|
+
const isPast =
|
|
348
|
+
resolveStackRelease({
|
|
349
|
+
directions,
|
|
350
|
+
height: height.value,
|
|
351
|
+
threshold,
|
|
352
|
+
vx: 0,
|
|
353
|
+
vy: 0,
|
|
354
|
+
width: width.value,
|
|
355
|
+
x: offset.x,
|
|
356
|
+
y: offset.y,
|
|
357
|
+
}) !== null;
|
|
358
|
+
if (isPast && !isArmed.value) {
|
|
359
|
+
isArmed.value = true;
|
|
360
|
+
if (haptic !== false) playHaptic(haptic);
|
|
361
|
+
} else if (!isPast && isArmed.value) {
|
|
362
|
+
isArmed.value = false;
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
.onEnd((event) => {
|
|
366
|
+
"worklet";
|
|
367
|
+
if (isBusy.value) return;
|
|
368
|
+
isArmed.value = false;
|
|
369
|
+
const direction = resolveStackRelease({
|
|
370
|
+
directions,
|
|
371
|
+
height: height.value,
|
|
372
|
+
threshold,
|
|
373
|
+
vx: event.velocityX,
|
|
374
|
+
vy: event.velocityY,
|
|
375
|
+
width: width.value,
|
|
376
|
+
x: x.value,
|
|
377
|
+
y: y.value,
|
|
378
|
+
});
|
|
379
|
+
if (direction !== null) {
|
|
380
|
+
fling(direction, event.velocityX, event.velocityY);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
settle(x.value, y.value, false);
|
|
384
|
+
})
|
|
385
|
+
.onFinalize(() => {
|
|
386
|
+
"worklet";
|
|
387
|
+
if (isBusy.value || (x.value === 0 && y.value === 0)) return;
|
|
388
|
+
settle(x.value, y.value, false);
|
|
389
|
+
});
|
|
390
|
+
}, [
|
|
391
|
+
directions,
|
|
392
|
+
fling,
|
|
393
|
+
haptic,
|
|
394
|
+
height,
|
|
395
|
+
isArmed,
|
|
396
|
+
isBusy,
|
|
397
|
+
isGestureEnabled,
|
|
398
|
+
isHorizontalOnly,
|
|
399
|
+
isVerticalOnly,
|
|
400
|
+
settle,
|
|
401
|
+
threshold,
|
|
402
|
+
width,
|
|
403
|
+
x,
|
|
404
|
+
y,
|
|
405
|
+
]);
|
|
406
|
+
|
|
407
|
+
const handleLayout = useCallback(
|
|
408
|
+
(event: LayoutChangeEvent) => {
|
|
409
|
+
width.value = event.nativeEvent.layout.width;
|
|
410
|
+
height.value = event.nativeEvent.layout.height;
|
|
411
|
+
},
|
|
412
|
+
[height, width]
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
const stampColors = useMemo(() => {
|
|
416
|
+
const colors: Partial<Record<StackCardDirection, StackCardStampColor>> = {};
|
|
417
|
+
for (const stamp of stamps) {
|
|
418
|
+
if (!isValidElement<StackCardStampProps>(stamp)) continue;
|
|
419
|
+
colors[stamp.props.direction ?? "right"] ??= stamp.props.color ?? "primary";
|
|
420
|
+
}
|
|
421
|
+
return colors;
|
|
422
|
+
}, [stamps]);
|
|
423
|
+
|
|
424
|
+
const labelFor = useCallback(
|
|
425
|
+
(direction: StackCardDirection) => directionLabels?.[direction] ?? STACK_CARD_DEFAULT_DIRECTION_LABELS[direction],
|
|
426
|
+
[directionLabels]
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
const context = useMemo<StackCardContextValue>(
|
|
430
|
+
() => ({
|
|
431
|
+
canUndo,
|
|
432
|
+
count,
|
|
433
|
+
depth,
|
|
434
|
+
directions,
|
|
435
|
+
fade,
|
|
436
|
+
height,
|
|
437
|
+
index,
|
|
438
|
+
isDisabled,
|
|
439
|
+
isReducedMotion,
|
|
440
|
+
labelFor,
|
|
441
|
+
layout,
|
|
442
|
+
stampColors,
|
|
443
|
+
swipe,
|
|
444
|
+
threshold,
|
|
445
|
+
top,
|
|
446
|
+
undo,
|
|
447
|
+
width,
|
|
448
|
+
x,
|
|
449
|
+
y,
|
|
450
|
+
}),
|
|
451
|
+
[
|
|
452
|
+
canUndo,
|
|
453
|
+
count,
|
|
454
|
+
depth,
|
|
455
|
+
directions,
|
|
456
|
+
fade,
|
|
457
|
+
height,
|
|
458
|
+
index,
|
|
459
|
+
isDisabled,
|
|
460
|
+
isReducedMotion,
|
|
461
|
+
labelFor,
|
|
462
|
+
layout,
|
|
463
|
+
stampColors,
|
|
464
|
+
swipe,
|
|
465
|
+
threshold,
|
|
466
|
+
top,
|
|
467
|
+
undo,
|
|
468
|
+
width,
|
|
469
|
+
x,
|
|
470
|
+
y,
|
|
471
|
+
]
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const [from, to] = resolveMountedWindow({ count, depth, index });
|
|
475
|
+
const inset = layout === "stack" ? depth * 8 : 0;
|
|
476
|
+
const slots: ReactElement[] = [];
|
|
477
|
+
// Deepest first, so the top card draws over the ones behind it — and the
|
|
478
|
+
// card just thrown, kept for undo, draws over everything when it flies back.
|
|
479
|
+
for (let cardIndex = to - 1; cardIndex >= from; cardIndex--) {
|
|
480
|
+
const card = cards[cardIndex];
|
|
481
|
+
const key = isValidElement(card) && card.key !== null ? card.key : cardIndex;
|
|
482
|
+
const isTop = cardIndex === index;
|
|
483
|
+
slots.push(
|
|
484
|
+
<StackCardSlot cardIndex={cardIndex} inset={inset} isTop={isTop} key={key}>
|
|
485
|
+
{card}
|
|
486
|
+
{isTop ? stamps : null}
|
|
487
|
+
</StackCardSlot>
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const variants = stackCardVariants();
|
|
492
|
+
|
|
493
|
+
return (
|
|
494
|
+
<StackCardProvider value={context}>
|
|
495
|
+
<View className={variants.root({ className })} {...props}>
|
|
496
|
+
<GestureDetector gesture={gesture}>
|
|
497
|
+
<Animated.View className={cn(variants.pile(), pileClassName)} onLayout={handleLayout}>
|
|
498
|
+
{index >= count ? empty : null}
|
|
499
|
+
{slots}
|
|
500
|
+
</Animated.View>
|
|
501
|
+
</GestureDetector>
|
|
502
|
+
{actions}
|
|
503
|
+
</View>
|
|
504
|
+
</StackCardProvider>
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* A pile of cards taken one at a time by throwing the top one off.
|
|
510
|
+
*
|
|
511
|
+
* For a queue where each item gets one decision and is then gone — a review
|
|
512
|
+
* queue, flashcards, suggestions. The gesture is the answer: drag past the
|
|
513
|
+
* threshold, or flick, and the card leaves; a short drag springs back. A
|
|
514
|
+
* direction the deck does not allow gives a little and returns. It shows one
|
|
515
|
+
* card, so it is wrong for anything the reader must compare or skim.
|
|
516
|
+
*
|
|
517
|
+
* One shared value — the top card's offset — drives every part: the stamps fade
|
|
518
|
+
* in toward their direction and the cards behind step up as it moves, so the
|
|
519
|
+
* next card is in place before the top one leaves, and nothing re-renders
|
|
520
|
+
* during a drag. Only a window around the top is mounted, so a deck of 500
|
|
521
|
+
* costs what a deck of five does.
|
|
522
|
+
*
|
|
523
|
+
* Controlled with `index` and `onIndexChange`, or uncontrolled from
|
|
524
|
+
* `defaultIndex`. A controlled deck declines a throw by leaving `index` where it
|
|
525
|
+
* was, and the card flies back. A `ref` or `useStackCard()` throws and undoes
|
|
526
|
+
* from code.
|
|
527
|
+
*
|
|
528
|
+
* A deck that throws only left and right lets a vertical drag through to a
|
|
529
|
+
* scroll view around it. One that throws all four ways claims both axes, so do
|
|
530
|
+
* not nest it in a scroller.
|
|
531
|
+
*
|
|
532
|
+
* @example
|
|
533
|
+
* <StackCard className="h-[460px]" onSwipe={(dir, i) => decide(people[i], dir)}>
|
|
534
|
+
* <StackCard.Stamp direction="right" color="success">Yes</StackCard.Stamp>
|
|
535
|
+
* <StackCard.Stamp direction="left" color="destructive">No</StackCard.Stamp>
|
|
536
|
+
* {people.map((p) => <StackCard.Card key={p.id}>…</StackCard.Card>)}
|
|
537
|
+
* <StackCard.Empty>All caught up</StackCard.Empty>
|
|
538
|
+
* <StackCard.Actions>
|
|
539
|
+
* <StackCard.Action action="left" icon={IconCrossSmall} label="Skip" />
|
|
540
|
+
* <StackCard.Action action="undo" icon={IconArrowRotateCounterClockwise} />
|
|
541
|
+
* <StackCard.Action action="right" icon={IconCheckmark2} label="Save" />
|
|
542
|
+
* </StackCard.Actions>
|
|
543
|
+
* </StackCard>
|
|
544
|
+
*/
|
|
545
|
+
export const StackCard = Object.assign(StackCardRoot, {
|
|
546
|
+
/** One card in the deck. Its order among the children is its place in the pile. */
|
|
547
|
+
Card: StackCardCard,
|
|
548
|
+
/** The answer a throw in one direction gives, printed on the top card as it moves. */
|
|
549
|
+
Stamp: StackCardStamp,
|
|
550
|
+
/** Shown, centred in the pile, once every card has gone. */
|
|
551
|
+
Empty: StackCardEmpty,
|
|
552
|
+
/** A centred row of action buttons under the pile. */
|
|
553
|
+
Actions: StackCardActions,
|
|
554
|
+
/** A round button that throws the top card one way, or undoes the last throw. */
|
|
555
|
+
Action: StackCardAction,
|
|
556
|
+
displayName: "DelacourUI.StackCard",
|
|
557
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { StackCardDirection } from "./stack-card.variants";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a `ref` on `StackCard` reaches, and what `useStackCard()` hands a custom
|
|
5
|
+
* control: throw the top card, or bring the last one back.
|
|
6
|
+
*/
|
|
7
|
+
export type StackCardHandle = {
|
|
8
|
+
/**
|
|
9
|
+
* Throws the top card `direction`, exactly as a release would — `onSwipe`,
|
|
10
|
+
* then `onIndexChange`. Any direction works, allowed for the gesture or not,
|
|
11
|
+
* because a button is an explicit answer.
|
|
12
|
+
*/
|
|
13
|
+
swipe: (direction: StackCardDirection) => void;
|
|
14
|
+
/** Brings the last thrown card back from the side it left. `onSwipe` is not called. */
|
|
15
|
+
undo: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** The deck's state, as `useStackCard()` returns it. */
|
|
19
|
+
export type StackCardState = StackCardHandle & {
|
|
20
|
+
/** The index of the top card. Equal to `count` once the deck is empty. */
|
|
21
|
+
index: number;
|
|
22
|
+
/** How many cards the deck holds. */
|
|
23
|
+
count: number;
|
|
24
|
+
/** Whether there is a thrown card to bring back. */
|
|
25
|
+
canUndo: boolean;
|
|
26
|
+
};
|