@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
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type ComponentProps, type ReactElement, useCallback } from "react";
|
|
2
2
|
import type { AccessibilityActionEvent, LayoutChangeEvent, ViewProps } from "react-native";
|
|
3
3
|
import Animated, { useAnimatedStyle, useDerivedValue, withSpring } from "react-native-reanimated";
|
|
4
|
+
import { useFieldContext } from "../field/field.context";
|
|
4
5
|
import { useSliderPart } from "./slider.context";
|
|
5
6
|
import {
|
|
6
7
|
formatSliderValue,
|
|
7
8
|
progressOf,
|
|
9
|
+
resolveThumbAccessibilityLabel,
|
|
8
10
|
SLIDER_THUMB_ANIMATION,
|
|
9
11
|
SLIDER_THUMB_SPRING,
|
|
10
12
|
sliderVariants,
|
|
@@ -16,6 +18,15 @@ const CONTINUOUS_ACCESSIBILITY_STEPS = 10;
|
|
|
16
18
|
export type SliderThumbProps = Omit<ViewProps, "children" | "style"> & {
|
|
17
19
|
/** Which value this thumb drives. `0` unless the slider holds a range. */
|
|
18
20
|
index?: number;
|
|
21
|
+
/**
|
|
22
|
+
* The name a screen reader reads before the value.
|
|
23
|
+
*
|
|
24
|
+
* Defaults to the enclosing `Field.Label`'s text — "Volume", or for a range
|
|
25
|
+
* "Price range, minimum" and "Price range, maximum". Set it to name one thumb
|
|
26
|
+
* yourself; it wins outright. A slider with no `Field` and no label here reads
|
|
27
|
+
* only its value.
|
|
28
|
+
*/
|
|
29
|
+
accessibilityLabel?: string;
|
|
19
30
|
className?: string;
|
|
20
31
|
/**
|
|
21
32
|
* Passed to the knob inside the capsule.
|
|
@@ -56,8 +67,21 @@ export type SliderThumbProps = Omit<ViewProps, "children" | "style"> & {
|
|
|
56
67
|
* TalkBack read; the increment and decrement actions are what their swipe
|
|
57
68
|
* gestures call. Without them a slider with no gesture on its thumb would have no
|
|
58
69
|
* assistive path to its value at all.
|
|
70
|
+
*
|
|
71
|
+
* **The name comes from the `Field.Label` a row away.** The thumb is a capsule
|
|
72
|
+
* with no text, so a value alone — "40" — says nothing about what it is a value
|
|
73
|
+
* *of*. React Native has no `<label for>`, so the field's context carries the
|
|
74
|
+
* label's text down and `resolveThumbAccessibilityLabel` turns it into a name:
|
|
75
|
+
* the label itself for a lone thumb, the label plus the end it holds for a
|
|
76
|
+
* range. An `accessibilityLabel` written here wins over all of it.
|
|
59
77
|
*/
|
|
60
|
-
export function SliderThumb({
|
|
78
|
+
export function SliderThumb({
|
|
79
|
+
index = 0,
|
|
80
|
+
accessibilityLabel,
|
|
81
|
+
className,
|
|
82
|
+
knobProps,
|
|
83
|
+
...props
|
|
84
|
+
}: SliderThumbProps): ReactElement {
|
|
61
85
|
const {
|
|
62
86
|
positions,
|
|
63
87
|
trackSize,
|
|
@@ -75,6 +99,7 @@ export function SliderThumb({ index = 0, className, knobProps, ...props }: Slide
|
|
|
75
99
|
isDisabled,
|
|
76
100
|
updateValue,
|
|
77
101
|
} = useSliderPart("Slider.Thumb");
|
|
102
|
+
const field = useFieldContext();
|
|
78
103
|
const isVertical = orientation === "vertical";
|
|
79
104
|
const slots = sliderVariants({ color, isDisabled, isInvalid, orientation, size });
|
|
80
105
|
|
|
@@ -118,6 +143,12 @@ export function SliderThumb({ index = 0, className, knobProps, ...props }: Slide
|
|
|
118
143
|
const knobStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] }));
|
|
119
144
|
|
|
120
145
|
const value = values[index] ?? minValue;
|
|
146
|
+
const label = resolveThumbAccessibilityLabel({
|
|
147
|
+
count: values.length,
|
|
148
|
+
index,
|
|
149
|
+
label: field?.label,
|
|
150
|
+
override: accessibilityLabel,
|
|
151
|
+
});
|
|
121
152
|
const nudge = step > 0 ? step : (maxValue - minValue) / CONTINUOUS_ACCESSIBILITY_STEPS;
|
|
122
153
|
|
|
123
154
|
const handleAccessibilityAction = useCallback(
|
|
@@ -131,6 +162,7 @@ export function SliderThumb({ index = 0, className, knobProps, ...props }: Slide
|
|
|
131
162
|
return (
|
|
132
163
|
<Animated.View
|
|
133
164
|
accessibilityActions={ACCESSIBILITY_ACTIONS}
|
|
165
|
+
accessibilityLabel={label}
|
|
134
166
|
accessibilityRole="adjustable"
|
|
135
167
|
accessibilityState={{ disabled: isDisabled }}
|
|
136
168
|
accessibilityValue={{
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
nearestThumbIndex,
|
|
11
11
|
progressOf,
|
|
12
12
|
resolveSliderAxes,
|
|
13
|
+
resolveThumbAccessibilityLabel,
|
|
13
14
|
SLIDER_COLORS,
|
|
14
15
|
SLIDER_DEFAULT_COLOR,
|
|
15
16
|
SLIDER_DEFAULT_ORIENTATION,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
SLIDER_SIZES,
|
|
24
25
|
SLIDER_STEP,
|
|
25
26
|
SLIDER_THUMB_ANIMATION,
|
|
27
|
+
SLIDER_THUMB_NAMES,
|
|
26
28
|
SLIDER_THUMB_SPRING,
|
|
27
29
|
shouldTickHaptic,
|
|
28
30
|
sliderVariants,
|
|
@@ -802,3 +804,53 @@ describe("shouldTickHaptic", () => {
|
|
|
802
804
|
expect(shouldTickHaptic({ ...base, position: 0, lastPosition: SLIDER_HAPTIC_MIN_TRAVEL })).toBe(true);
|
|
803
805
|
});
|
|
804
806
|
});
|
|
807
|
+
|
|
808
|
+
describe("resolveThumbAccessibilityLabel", () => {
|
|
809
|
+
// A lone thumb *is* the control, so it takes the control's name and nothing
|
|
810
|
+
// more — "Volume", not "Volume, thumb".
|
|
811
|
+
test("names a lone thumb after the slider", () => {
|
|
812
|
+
expect(resolveThumbAccessibilityLabel({ label: "Volume", index: 0, count: 1 })).toBe("Volume");
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// Nothing to say is better than a made-up name: an unlabelled single slider
|
|
816
|
+
// falls through to the value alone, which is what the caller has to fix by
|
|
817
|
+
// wrapping it in a Field or passing a label.
|
|
818
|
+
test("names an unlabelled lone thumb nothing", () => {
|
|
819
|
+
expect(resolveThumbAccessibilityLabel({ label: null, index: 0, count: 1 })).toBeUndefined();
|
|
820
|
+
expect(resolveThumbAccessibilityLabel({ index: 0, count: 1 })).toBeUndefined();
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
test("tells a range's two thumbs apart by which end they hold", () => {
|
|
824
|
+
expect(resolveThumbAccessibilityLabel({ label: "Price range", index: 0, count: 2 })).toBe("Price range, minimum");
|
|
825
|
+
expect(resolveThumbAccessibilityLabel({ label: "Price range", index: 1, count: 2 })).toBe("Price range, maximum");
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
// Two thumbs reading the same bare number would be indistinguishable, so a
|
|
829
|
+
// range names its ends even with no label to hang them on.
|
|
830
|
+
test("still tells an unlabelled range's thumbs apart", () => {
|
|
831
|
+
expect(resolveThumbAccessibilityLabel({ label: null, index: 0, count: 2 })).toBe(SLIDER_THUMB_NAMES.minimum);
|
|
832
|
+
expect(resolveThumbAccessibilityLabel({ label: null, index: 1, count: 2 })).toBe(SLIDER_THUMB_NAMES.maximum);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
test("counts the thumbs of a range with more than two", () => {
|
|
836
|
+
expect(resolveThumbAccessibilityLabel({ label: "Stops", index: 1, count: 3 })).toBe("Stops, 2 of 3");
|
|
837
|
+
expect(resolveThumbAccessibilityLabel({ label: null, index: 2, count: 3 })).toBe("Thumb 3 of 3");
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
test("lets an explicit name win over everything, even in a range", () => {
|
|
841
|
+
expect(resolveThumbAccessibilityLabel({ label: "Price range", index: 0, count: 2, override: "Lowest price" })).toBe(
|
|
842
|
+
"Lowest price"
|
|
843
|
+
);
|
|
844
|
+
expect(resolveThumbAccessibilityLabel({ index: 0, count: 1, override: "Brightness" })).toBe("Brightness");
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
// An empty override is an absence, not a name — the `??`-not-`||` rule read the
|
|
848
|
+
// other way round, because a screen reader given "" reads nothing at all.
|
|
849
|
+
test("treats an empty override as no override", () => {
|
|
850
|
+
expect(resolveThumbAccessibilityLabel({ label: "Volume", index: 0, count: 1, override: "" })).toBe("Volume");
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
test("ignores whitespace around the label it is handed", () => {
|
|
854
|
+
expect(resolveThumbAccessibilityLabel({ label: " Volume ", index: 0, count: 1 })).toBe("Volume");
|
|
855
|
+
});
|
|
856
|
+
});
|
|
@@ -167,8 +167,7 @@ export const SLIDER_RANGE_SEPARATOR = " – ";
|
|
|
167
167
|
* wrong one.
|
|
168
168
|
*
|
|
169
169
|
* **The fade lands on the root**, which is a plain `View` here rather than a
|
|
170
|
-
* `Pressable
|
|
171
|
-
* by an animated style writing `opacity` every frame.
|
|
170
|
+
* `Pressable`, so nothing animated shares the `opacity` property with it.
|
|
172
171
|
*
|
|
173
172
|
* No slot worn by a `View` carries `text-*` or `font-*`: `Slider.Output` renders
|
|
174
173
|
* a `Text` preset and names a step — see {@link SLIDER_OUTPUT_TEXT_SIZE}.
|
|
@@ -604,4 +603,59 @@ export function resolveSliderAxes({ own, field }: { own?: SliderOwnAxes; field?:
|
|
|
604
603
|
};
|
|
605
604
|
}
|
|
606
605
|
|
|
606
|
+
/**
|
|
607
|
+
* What a range's thumbs are called when nothing names them.
|
|
608
|
+
*
|
|
609
|
+
* Written out once so the two ends and the counted form cannot drift apart, and
|
|
610
|
+
* so the playground and the docs can quote the same words.
|
|
611
|
+
*/
|
|
612
|
+
export const SLIDER_THUMB_NAMES = { minimum: "Minimum", maximum: "Maximum", thumb: "Thumb" } as const;
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* The accessible name a `Slider.Thumb` announces.
|
|
616
|
+
*
|
|
617
|
+
* A thumb is a capsule with no text, so without this a screen reader reads a
|
|
618
|
+
* bare number and nothing about what it is a number *of*. The ladder is the one
|
|
619
|
+
* every prop in this package climbs: an explicit `accessibilityLabel` on the
|
|
620
|
+
* thumb wins, then the slider's name, then nothing — an empty override counts as
|
|
621
|
+
* no override, because a screen reader handed `""` reads nothing at all.
|
|
622
|
+
*
|
|
623
|
+
* A lone thumb *is* the control, so it takes the control's name unadorned. A
|
|
624
|
+
* range has to tell its two thumbs apart, so each is named for the end it holds
|
|
625
|
+
* — "Price range, minimum" — and that holds even with no name to hang it on,
|
|
626
|
+
* since two thumbs reading the same bare number are indistinguishable. Past two
|
|
627
|
+
* thumbs there are no ends to name, so they count: "Stops, 2 of 3".
|
|
628
|
+
*
|
|
629
|
+
* Pure, so the whole ladder is reachable from `bun test`. See AGENTS.md.
|
|
630
|
+
*/
|
|
631
|
+
export function resolveThumbAccessibilityLabel({
|
|
632
|
+
label,
|
|
633
|
+
index,
|
|
634
|
+
count,
|
|
635
|
+
override,
|
|
636
|
+
}: {
|
|
637
|
+
/** The slider's own name — a `Field.Label`'s text, or nothing. */
|
|
638
|
+
label?: string | null;
|
|
639
|
+
/** Which thumb, in the order the values were given. */
|
|
640
|
+
index: number;
|
|
641
|
+
/** How many thumbs the slider holds. */
|
|
642
|
+
count: number;
|
|
643
|
+
/** An `accessibilityLabel` written on the thumb itself. */
|
|
644
|
+
override?: string;
|
|
645
|
+
}): string | undefined {
|
|
646
|
+
const explicit = override?.trim();
|
|
647
|
+
if (explicit) return explicit;
|
|
648
|
+
|
|
649
|
+
const name = label?.trim() || undefined;
|
|
650
|
+
if (count <= 1) return name;
|
|
651
|
+
|
|
652
|
+
if (count === 2) {
|
|
653
|
+
const end = index === 0 ? SLIDER_THUMB_NAMES.minimum : SLIDER_THUMB_NAMES.maximum;
|
|
654
|
+
return name ? `${name}, ${end.toLowerCase()}` : end;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const position = `${index + 1} of ${count}`;
|
|
658
|
+
return name ? `${name}, ${position}` : `${SLIDER_THUMB_NAMES.thumb} ${position}`;
|
|
659
|
+
}
|
|
660
|
+
|
|
607
661
|
export type SliderVariantProps = VariantProps<typeof sliderVariants>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# StackCard
|
|
2
|
+
|
|
3
|
+
A pile of cards taken one at a time by **throwing the top one off**. Compound
|
|
4
|
+
root plus `Card`, `Stamp`, `Empty`, `Actions` and `Action`. For a queue where
|
|
5
|
+
each item gets one decision and is then gone — a review queue, flashcards,
|
|
6
|
+
suggestions. It shows one card, so it is wrong for anything the reader must
|
|
7
|
+
compare or skim: browsing wants a carousel, and one row's actions want a swipe
|
|
8
|
+
row.
|
|
9
|
+
|
|
10
|
+
`import { StackCard } from "@delacour/react-native-ui/stack-card";`
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
<StackCard className="h-[460px]" onSwipe={(dir, i) => decide(people[i], dir)}>
|
|
14
|
+
<StackCard.Stamp direction="right" color="success">Yes</StackCard.Stamp>
|
|
15
|
+
<StackCard.Stamp direction="left" color="destructive">No</StackCard.Stamp>
|
|
16
|
+
{people.map((p) => <StackCard.Card key={p.id}>…</StackCard.Card>)}
|
|
17
|
+
<StackCard.Empty>All caught up</StackCard.Empty>
|
|
18
|
+
<StackCard.Actions>
|
|
19
|
+
<StackCard.Action action="left" icon={IconCrossSmall} label="Skip" />
|
|
20
|
+
<StackCard.Action action="undo" icon={IconArrowRotateCounterClockwise} />
|
|
21
|
+
<StackCard.Action action="right" icon={IconCheckmark2} label="Save" />
|
|
22
|
+
</StackCard.Actions>
|
|
23
|
+
</StackCard>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Files
|
|
27
|
+
|
|
28
|
+
| File | What it holds |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `index.ts` | → `@delacour/react-native-ui/stack-card` |
|
|
31
|
+
| `stack-card.tsx` | Root: partitions children, owns the pan, the throw, undo, decline, the mounted window |
|
|
32
|
+
| `stack-card-slot.tsx` | Internal animated box one card sits in — reads the shared offset, places itself |
|
|
33
|
+
| `stack-card-card.tsx` | `StackCard.Card` — the card's look, and the top card's accessibility actions |
|
|
34
|
+
| `stack-card-stamp.tsx` | `StackCard.Stamp` — fades in with progress toward its direction |
|
|
35
|
+
| `stack-card-empty.tsx` | `StackCard.Empty` |
|
|
36
|
+
| `stack-card-actions.tsx` | `StackCard.Actions` — the centred row under the pile |
|
|
37
|
+
| `stack-card-action.tsx` | `StackCard.Action` — a round `Button` that throws or undoes |
|
|
38
|
+
| `stack-card.context.tsx` | `StackCardProvider`, `useStackCard()`, `useStackCardContext()`, `useStackCardPart()`, the slot context |
|
|
39
|
+
| `stack-card.types.ts` | `StackCardHandle`, `StackCardState` — shared by the root and the context |
|
|
40
|
+
| `stack-card.variants.ts` | The slotted `tv()` and every pure resolver, the gesture's worklets included — no RN imports |
|
|
41
|
+
| `stack-card.variants.test.ts` | |
|
|
42
|
+
|
|
43
|
+
## Axes
|
|
44
|
+
|
|
45
|
+
- **`directions`**: any of `left`, `right`, `up`, `down`. Default left and right.
|
|
46
|
+
- **`layout`** (the cards behind only): `stack` steps each down 8 and smaller by
|
|
47
|
+
4%; `fan` rotates them ±3° per step, alternating, about the bottom centre;
|
|
48
|
+
`flat` hides them.
|
|
49
|
+
- **`depth`**: cards drawn behind the top, 0..4, default 2.
|
|
50
|
+
- **`threshold`**: the fraction of the card's width (or height) a drag must
|
|
51
|
+
cover, default 0.3.
|
|
52
|
+
- **Stamp `color`**: `primary`, `success`, `warning`, `info`, `destructive` —
|
|
53
|
+
each a token declared in both themes, which the test checks.
|
|
54
|
+
|
|
55
|
+
## Design
|
|
56
|
+
|
|
57
|
+
- **One shared value drives everything: the top card's `{x, y}`.** The top card
|
|
58
|
+
follows it and tilts `x / width · 12°`, the stamps read progress toward their
|
|
59
|
+
direction from it, and the cards behind interpolate toward the slot ahead of
|
|
60
|
+
them on the furthest progress (`resolveDragProgress`). A drag writes shared
|
|
61
|
+
values and nothing else, so **nothing re-renders during a drag** — React only
|
|
62
|
+
hears about a throw after it lands.
|
|
63
|
+
- **The card behind is in place at the moment of release.** Progress reaches 1
|
|
64
|
+
at the threshold, and `resolveBehindTransform` at progress 1 is exactly the
|
|
65
|
+
transform of the slot ahead — a test asserts it for every layout. So when the
|
|
66
|
+
top card is thrown, the next card is already where the top was, and advancing
|
|
67
|
+
moves nothing. Without it the pile would jump a step on every throw.
|
|
68
|
+
- **The top moves on the UI thread, ahead of React.** `top` is a shared value the
|
|
69
|
+
throw sets the frame it lands; every slot places itself from `cardIndex − top`.
|
|
70
|
+
Waiting for React to re-render the new index would leave a frame where the
|
|
71
|
+
thrown card is gone and the next one has not been promoted.
|
|
72
|
+
- **Each worklet is self-contained**, and the resolvers it calls each carry
|
|
73
|
+
`"worklet"` and call no other module helper (see
|
|
74
|
+
[Pressable](../pressable/AGENTS.md) for the crash a module-scope helper
|
|
75
|
+
causes). `resolveBehindTransform`'s inner `at` is declared inside its body for
|
|
76
|
+
that reason.
|
|
77
|
+
- **Release:** `resolveStackRelease` projects the position 0.15 s along the
|
|
78
|
+
release velocity, so a flick throws from a short drag and a flick back cancels
|
|
79
|
+
a long one. Each axis is measured as a fraction of the card's own size along
|
|
80
|
+
it, and the dominant axis alone decides. A direction the deck does not allow
|
|
81
|
+
gives `null` and the card springs back.
|
|
82
|
+
- **A disallowed direction still gives a quarter** (`resolveDragOffset`) and
|
|
83
|
+
returns. A card that does not budge at all reads as frozen, where one that
|
|
84
|
+
gives and returns reads as "not that way".
|
|
85
|
+
- **A horizontal-only deck lets vertical drags through** — `activeOffsetX` and
|
|
86
|
+
`failOffsetY`, so it sits inside a vertical `ScrollView`. Vertical-only is the
|
|
87
|
+
mirror. A deck that throws all four ways claims both axes with `minDistance`
|
|
88
|
+
and **must not be nested in a scroller** — the doc comment says so. This is
|
|
89
|
+
the opposite of `Slider`'s `minDistance(0)`: a slider must win every touch on
|
|
90
|
+
its track, a deck must give up the ones that are scrolls.
|
|
91
|
+
- **A controlled deck that does not move `index` declines.** A throw calls
|
|
92
|
+
`onSwipe`, then `onIndexChange`, and records the throw as pending. The effect
|
|
93
|
+
after the next render compares: an index that moved is accepted and pushed to
|
|
94
|
+
the undo history; an index that stayed put flies the card back in from the
|
|
95
|
+
side it left. The pending record is state, not a ref, so the deck re-renders
|
|
96
|
+
and checks even when the parent's `onIndexChange` changes nothing. A confirm
|
|
97
|
+
step is built on this: decline, ask, and on "yes" call `ref.swipe(dir)` again
|
|
98
|
+
and accept it — the `controlled-decline` demo.
|
|
99
|
+
- **Undo restores from the side it left.** The deck keeps a history of the
|
|
100
|
+
directions it threw; `undo` pops one, steps the index back and flies the card
|
|
101
|
+
in from that side. `onSwipe` is not called for an undo. An index changed from
|
|
102
|
+
outside (a refill) clears the history, since the cards it remembers may not be
|
|
103
|
+
the ones now in the deck.
|
|
104
|
+
- **A card returns on a critically damped spring.** An underdamped return
|
|
105
|
+
overshoots rest to the other side, and the stamp for the opposite direction
|
|
106
|
+
fades in there — a card undone from the left would land flashing the
|
|
107
|
+
right-hand answer. The damping sits just above `2·√(stiffness·mass)`, so the
|
|
108
|
+
card arrives without crossing.
|
|
109
|
+
- **Only a window is mounted:** `[index − 1, index + depth + 1]` —
|
|
110
|
+
one behind for undo, one beyond the visible depth so the next card fades in
|
|
111
|
+
rather than popping. A deck of 500 mounts what a deck of five does
|
|
112
|
+
(`resolveMountedWindow`). The one beyond starts at opacity 0 and reaches the
|
|
113
|
+
last visible slot's opacity at progress 1.
|
|
114
|
+
- **Slots render deepest first**, so the top card draws over the ones behind and
|
|
115
|
+
the card just thrown — kept mounted for undo, hidden — draws over everything
|
|
116
|
+
when it flies back in.
|
|
117
|
+
- **Every transform pivots on the bottom centre.** That is what makes `fan` fan,
|
|
118
|
+
and what makes a smaller `stack` card's lower edge peek out below the one in
|
|
119
|
+
front. The pile keeps `depth · 8` points clear under the cards in `stack` so
|
|
120
|
+
that edge is not clipped by whatever sits below.
|
|
121
|
+
- **Anything that is not a `Stamp`, `Empty` or `Actions` is a card.** A caller
|
|
122
|
+
who wraps `StackCard.Card` in a component of their own still gets a card,
|
|
123
|
+
rather than a child silently dropped for having the wrong type
|
|
124
|
+
(`partitionStackChildren` takes the predicate as an argument to stay free of
|
|
125
|
+
the parts and of React Native).
|
|
126
|
+
- **Stamps are declared once on the deck and drawn on the top card**, on the side
|
|
127
|
+
they answer for — a right-hand stamp sits top-start, tilted −12°, where a card
|
|
128
|
+
leaning right lifts it into view. A stamp stays dark while the other axis
|
|
129
|
+
dominates, so a diagonal drag in a four-way deck shows one answer, not two.
|
|
130
|
+
Their text colour is on `stampLabel`, never on the stamp (rule 1).
|
|
131
|
+
- **An action's glyph takes the colour of its direction's stamp**, so the button
|
|
132
|
+
and the stamp read as one answer. `undo` is a `ghost` button; the directions
|
|
133
|
+
are `secondary`; all are `icon-lg` circles. A direction button works whether or
|
|
134
|
+
not the gesture allows that direction — a button is an explicit answer.
|
|
135
|
+
- **Haptics default to `selection`**, which is a deliberate departure from the
|
|
136
|
+
package's off-by-default: the tick when a drag crosses the threshold is the
|
|
137
|
+
feedback that says "let go now", and a deck without it feels uncertain. It
|
|
138
|
+
re-arms when the drag crosses back. A throw knocks with `medium`. `haptic={false}`
|
|
139
|
+
silences both.
|
|
140
|
+
|
|
141
|
+
## Reduced motion
|
|
142
|
+
|
|
143
|
+
- **A card leaves by fading, not flying**, and an undone or declined card fades
|
|
144
|
+
back in at rest. Which card is on top is the information, so the swap itself
|
|
145
|
+
is kept.
|
|
146
|
+
- **The pile swaps without stepping:** under reduced motion the cards behind do
|
|
147
|
+
not interpolate during a drag; they take their new places when the throw lands.
|
|
148
|
+
- A short drag released returns at once rather than springing.
|
|
149
|
+
|
|
150
|
+
## Accessibility
|
|
151
|
+
|
|
152
|
+
- **The top card is one `accessible` element** with a custom action per allowed
|
|
153
|
+
direction — named by `directionLabels[dir]`, or "Swipe left" and so on — plus
|
|
154
|
+
"Undo" while there is a card to bring back. No role is set: it is not
|
|
155
|
+
`adjustable`, since the actions are not a value going up and down.
|
|
156
|
+
- **Every card but the top is hidden** — `importantForAccessibility="no-hide-descendants"`
|
|
157
|
+
on Android, `accessibilityElementsHidden` on iOS — and takes no touches.
|
|
158
|
+
- Because the top card is one element, a control inside it is not separately
|
|
159
|
+
reachable. The deck's controls belong in `StackCard.Actions`, whose buttons
|
|
160
|
+
carry the same labels.
|
|
161
|
+
- Stamps are hidden from assistive technology; the actions say the same thing in
|
|
162
|
+
words.
|
|
163
|
+
|
|
164
|
+
## Out of scope
|
|
165
|
+
|
|
166
|
+
- A carousel or any browsing mode: one card shows, by design.
|
|
167
|
+
- RTL mirroring of stamp placement — the stamps sit by physical side, because the
|
|
168
|
+
direction they answer for is physical too.
|
|
169
|
+
- A render callback per card, or virtualising beyond the mounted window.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export { StackCard, type StackCardProps } from "./stack-card";
|
|
2
|
+
export {
|
|
3
|
+
type StackCardContextValue,
|
|
4
|
+
StackCardProvider,
|
|
5
|
+
useStackCard,
|
|
6
|
+
useStackCardContext,
|
|
7
|
+
} from "./stack-card.context";
|
|
8
|
+
export type { StackCardHandle, StackCardState } from "./stack-card.types";
|
|
9
|
+
export {
|
|
10
|
+
partitionStackChildren,
|
|
11
|
+
resolveBehindTransform,
|
|
12
|
+
resolveDragOffset,
|
|
13
|
+
resolveDragProgress,
|
|
14
|
+
resolveExitOffset,
|
|
15
|
+
resolveMountedWindow,
|
|
16
|
+
resolveStackDepth,
|
|
17
|
+
resolveStackRelease,
|
|
18
|
+
resolveStampOpacity,
|
|
19
|
+
STACK_CARD_DEFAULT_DIRECTION_LABELS,
|
|
20
|
+
STACK_CARD_DEFAULT_DIRECTIONS,
|
|
21
|
+
STACK_CARD_DEFAULT_THRESHOLD,
|
|
22
|
+
STACK_CARD_DIRECTIONS,
|
|
23
|
+
STACK_CARD_LAYOUTS,
|
|
24
|
+
STACK_CARD_STAMP_COLORS,
|
|
25
|
+
type StackCardChildKind,
|
|
26
|
+
type StackCardDirection,
|
|
27
|
+
type StackCardLayout,
|
|
28
|
+
type StackCardStampColor,
|
|
29
|
+
type StackCardTransform,
|
|
30
|
+
type StackCardVariantProps,
|
|
31
|
+
stackCardVariants,
|
|
32
|
+
} from "./stack-card.variants";
|
|
33
|
+
export type { StackCardActionProps } from "./stack-card-action";
|
|
34
|
+
export type { StackCardActionsProps } from "./stack-card-actions";
|
|
35
|
+
export type { StackCardCardProps } from "./stack-card-card";
|
|
36
|
+
export type { StackCardEmptyProps } from "./stack-card-empty";
|
|
37
|
+
export type { StackCardStampProps } from "./stack-card-stamp";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type ReactElement, useCallback } from "react";
|
|
2
|
+
import { Button } from "../button";
|
|
3
|
+
import { Icon, type IconComponent } from "../icon";
|
|
4
|
+
import { useStackCardPart } from "./stack-card.context";
|
|
5
|
+
import type { StackCardDirection } from "./stack-card.variants";
|
|
6
|
+
|
|
7
|
+
export type StackCardActionProps = {
|
|
8
|
+
/** Throw the top card this way, or bring the last one back. */
|
|
9
|
+
action: StackCardDirection | "undo";
|
|
10
|
+
icon: IconComponent;
|
|
11
|
+
/**
|
|
12
|
+
* What a screen reader calls the button. Defaults to the deck's
|
|
13
|
+
* `directionLabels` for the direction, or "Undo".
|
|
14
|
+
*/
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Called before the deck acts. */
|
|
17
|
+
onPress?: () => void;
|
|
18
|
+
className?: string;
|
|
19
|
+
testID?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A round button that answers for the top card without a drag.
|
|
24
|
+
*
|
|
25
|
+
* A direction throws the card exactly as a release would — `onSwipe`, then
|
|
26
|
+
* `onIndexChange` — and `undo` brings the last one back. A direction's glyph is
|
|
27
|
+
* tinted with the colour of the stamp answering for that direction, so the
|
|
28
|
+
* button and the stamp read as one answer. `undo` is a quiet ghost button,
|
|
29
|
+
* disabled while there is nothing to bring back; the directions disable once
|
|
30
|
+
* the deck is empty.
|
|
31
|
+
*/
|
|
32
|
+
export function StackCardAction({
|
|
33
|
+
action,
|
|
34
|
+
icon,
|
|
35
|
+
label,
|
|
36
|
+
onPress,
|
|
37
|
+
className,
|
|
38
|
+
testID,
|
|
39
|
+
}: StackCardActionProps): ReactElement {
|
|
40
|
+
const { swipe, undo, canUndo, index, count, isDisabled, labelFor, stampColors } =
|
|
41
|
+
useStackCardPart("StackCard.Action");
|
|
42
|
+
const isUndo = action === "undo";
|
|
43
|
+
const isActionDisabled = isDisabled || (isUndo ? !canUndo : index >= count);
|
|
44
|
+
const color = isUndo ? undefined : stampColors[action];
|
|
45
|
+
|
|
46
|
+
const handlePress = useCallback(() => {
|
|
47
|
+
onPress?.();
|
|
48
|
+
if (action === "undo") undo();
|
|
49
|
+
else swipe(action);
|
|
50
|
+
}, [action, onPress, swipe, undo]);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Button
|
|
54
|
+
accessibilityLabel={label ?? (action === "undo" ? "Undo" : labelFor(action))}
|
|
55
|
+
className={className}
|
|
56
|
+
isDisabled={isActionDisabled}
|
|
57
|
+
onPress={handlePress}
|
|
58
|
+
size="icon-lg"
|
|
59
|
+
testID={testID}
|
|
60
|
+
variant={isUndo ? "ghost" : "secondary"}
|
|
61
|
+
>
|
|
62
|
+
<Icon color={color} icon={icon} />
|
|
63
|
+
</Button>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
StackCardAction.displayName = "DelacourUI.StackCard.Action";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { stackCardVariants } from "./stack-card.variants";
|
|
4
|
+
|
|
5
|
+
export type StackCardActionsProps = ViewProps & {
|
|
6
|
+
className?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A centred row of `StackCard.Action` buttons under the pile.
|
|
11
|
+
*
|
|
12
|
+
* Wherever it is written among the deck's children, the deck renders it below
|
|
13
|
+
* the pile, which takes the height left over.
|
|
14
|
+
*/
|
|
15
|
+
export function StackCardActions({ className, ...props }: StackCardActionsProps): ReactElement {
|
|
16
|
+
return <View className={stackCardVariants().actions({ className })} {...props} />;
|
|
17
|
+
}
|
|
18
|
+
StackCardActions.displayName = "DelacourUI.StackCard.Actions";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type ReactElement, useCallback, useMemo } from "react";
|
|
2
|
+
import { type AccessibilityActionEvent, type AccessibilityActionInfo, View, type ViewProps } from "react-native";
|
|
3
|
+
import { useStackCardPart, useStackCardSlot } from "./stack-card.context";
|
|
4
|
+
import { STACK_CARD_DIRECTIONS, type StackCardDirection, stackCardVariants } from "./stack-card.variants";
|
|
5
|
+
|
|
6
|
+
export type StackCardCardProps = ViewProps & {
|
|
7
|
+
className?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const UNDO_ACTION = "undo";
|
|
11
|
+
|
|
12
|
+
/** The accessibility action name a direction is announced under. */
|
|
13
|
+
function actionName(direction: StackCardDirection): string {
|
|
14
|
+
return `swipe-${direction}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One card in the deck.
|
|
19
|
+
*
|
|
20
|
+
* A card's look — the card fill, a hairline and the card corner — absolutely
|
|
21
|
+
* filling the pile, so every card occupies one box and only a transform tells
|
|
22
|
+
* them apart. Its position, tilt and opacity are the deck's, applied by the slot
|
|
23
|
+
* around it; the card itself holds no gesture.
|
|
24
|
+
*
|
|
25
|
+
* The top card is one `accessible` element whose custom actions are the deck's
|
|
26
|
+
* allowed directions — named by `directionLabels`, or "Swipe left" and so on —
|
|
27
|
+
* plus "Undo" while there is a card to bring back. A screen reader swipes the
|
|
28
|
+
* deck from the actions rotor rather than by dragging. Because the card is one
|
|
29
|
+
* element, a control inside it is not separately reachable: put the deck's
|
|
30
|
+
* controls in `StackCard.Actions`, not on the card.
|
|
31
|
+
*/
|
|
32
|
+
export function StackCardCard({ className, children, ...props }: StackCardCardProps): ReactElement {
|
|
33
|
+
const { directions, labelFor, swipe, undo, canUndo } = useStackCardPart("StackCard.Card");
|
|
34
|
+
const slot = useStackCardSlot();
|
|
35
|
+
const isTop = slot?.isTop ?? false;
|
|
36
|
+
|
|
37
|
+
const actions = useMemo<AccessibilityActionInfo[]>(() => {
|
|
38
|
+
const list: AccessibilityActionInfo[] = directions.map((direction) => ({
|
|
39
|
+
label: labelFor(direction),
|
|
40
|
+
name: actionName(direction),
|
|
41
|
+
}));
|
|
42
|
+
if (canUndo) list.push({ label: "Undo", name: UNDO_ACTION });
|
|
43
|
+
return list;
|
|
44
|
+
}, [canUndo, directions, labelFor]);
|
|
45
|
+
|
|
46
|
+
const handleAction = useCallback(
|
|
47
|
+
(event: AccessibilityActionEvent) => {
|
|
48
|
+
const name = event.nativeEvent.actionName;
|
|
49
|
+
if (name === UNDO_ACTION) {
|
|
50
|
+
undo();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const direction = STACK_CARD_DIRECTIONS.find((candidate) => actionName(candidate) === name);
|
|
54
|
+
if (direction) swipe(direction);
|
|
55
|
+
},
|
|
56
|
+
[swipe, undo]
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<View
|
|
61
|
+
accessibilityActions={isTop ? actions : undefined}
|
|
62
|
+
accessible={isTop}
|
|
63
|
+
className={stackCardVariants().card({ className })}
|
|
64
|
+
onAccessibilityAction={isTop ? handleAction : undefined}
|
|
65
|
+
{...props}
|
|
66
|
+
>
|
|
67
|
+
{children}
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
StackCardCard.displayName = "DelacourUI.StackCard.Card";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { Text } from "../text";
|
|
4
|
+
import { stackCardVariants } from "./stack-card.variants";
|
|
5
|
+
|
|
6
|
+
export type StackCardEmptyProps = ViewProps & {
|
|
7
|
+
className?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What the pile shows once every card has gone — centred where the cards were.
|
|
12
|
+
*
|
|
13
|
+
* Rendered by the deck only while its index has reached the card count, so an
|
|
14
|
+
* `EmptyState` or a refill button goes here rather than behind the cards. Bare
|
|
15
|
+
* text is wrapped in a muted paragraph.
|
|
16
|
+
*/
|
|
17
|
+
export function StackCardEmpty({ className, children, ...props }: StackCardEmptyProps): ReactElement {
|
|
18
|
+
return (
|
|
19
|
+
<View className={stackCardVariants().empty({ className })} {...props}>
|
|
20
|
+
{typeof children === "string" ? (
|
|
21
|
+
<Text align="center" color="muted" variant="paragraph">
|
|
22
|
+
{children}
|
|
23
|
+
</Text>
|
|
24
|
+
) : (
|
|
25
|
+
children
|
|
26
|
+
)}
|
|
27
|
+
</View>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
StackCardEmpty.displayName = "DelacourUI.StackCard.Empty";
|