@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,155 @@
|
|
|
1
|
+
# SelectionMode
|
|
2
|
+
|
|
3
|
+
Pick several things at once — messages to archive, people to share with,
|
|
4
|
+
swatches to apply — then act on them from a bar. A compound root plus
|
|
5
|
+
`SelectionMode.Item`, `.Indicator`, `.Header`, `.Bar`, `.Action` and `.Group`,
|
|
6
|
+
and `useSelectionMode()`.
|
|
7
|
+
|
|
8
|
+
`import { SelectionMode } from "@delacour/react-native-ui/selection-mode";`
|
|
9
|
+
|
|
10
|
+
## Files
|
|
11
|
+
|
|
12
|
+
| File | What it holds |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `index.ts` | → `@delacour/react-native-ui/selection-mode` |
|
|
15
|
+
| `selection-mode.tsx` | Root — owns the mode and the selection — plus the `Object.assign` compound surface |
|
|
16
|
+
| `selection-mode-item.tsx` | `SelectionMode.Item`: wraps a child, decides what a press does, slides the leading mark in |
|
|
17
|
+
| `selection-mode-indicator.tsx` | `SelectionMode.Indicator`, the round mark, and the internal `ring` (`DelacourUI.SelectionMode.Item.Ring`) |
|
|
18
|
+
| `selection-mode-header.tsx` | `SelectionMode.Header`: close, the count and select-all, while the mode is on |
|
|
19
|
+
| `selection-mode-bar.tsx` | `SelectionMode.Bar`: the absolute action bar over the bottom edge |
|
|
20
|
+
| `selection-mode-action.tsx` | `SelectionMode.Action`: an icon over a label, handed the selection |
|
|
21
|
+
| `selection-mode-group.tsx` | `SelectionMode.Group`: a stacked card, a grid or a horizontal strip |
|
|
22
|
+
| `selection-mode.context.tsx` | The selection context and the per-item context, with their hooks |
|
|
23
|
+
| `selection-mode.types.ts` | `SelectionModeState` — what the root builds and the context carries |
|
|
24
|
+
| `selection-mode.variants.ts` | The slotted `tv()`, the motion constants, the tokens and every pure resolver |
|
|
25
|
+
| `selection-mode.variants.test.ts` | |
|
|
26
|
+
|
|
27
|
+
## Design
|
|
28
|
+
|
|
29
|
+
- **Axes**: `indicator` (`leading` default, `ring`, `none`) on an item;
|
|
30
|
+
`placement` (`edge` default, `floating`) on the bar; `isSelected` and
|
|
31
|
+
`isDestructive` drive the indicator's fill and an action's colour.
|
|
32
|
+
- **Selection is a set of ids, never indices.** A `string[]` in the order things
|
|
33
|
+
were picked. An index changes meaning the moment a list is sorted, filtered or
|
|
34
|
+
paged; an id does not, so a selection survives a refetch.
|
|
35
|
+
- **It is a mode, not a property of each row.** Off, the list is for reading and
|
|
36
|
+
each item runs its own `onPress`. A long press turns the mode on with that item
|
|
37
|
+
picked; then a press toggles and the row's own handler does not run.
|
|
38
|
+
`resolveItemPress` is the whole decision and is pure.
|
|
39
|
+
- **A disabled item keeps its own press, even while the mode is on.** It never
|
|
40
|
+
toggles and never starts the mode, but a section header or a "load more" row
|
|
41
|
+
still has to work while someone is picking.
|
|
42
|
+
- **Exit clears an uncontrolled selection and leaves a controlled one alone.**
|
|
43
|
+
The close button and `exit()` call `setActive(false)`; only when `selected` is
|
|
44
|
+
uncontrolled do they also clear. A caller that owns `selected` may want to keep
|
|
45
|
+
it — a draft that survives leaving the mode — and clearing behind its back
|
|
46
|
+
would fight it.
|
|
47
|
+
- **Both pairs are controllable.** `isActive`/`defaultActive`/`onActiveChange`
|
|
48
|
+
and `selected`/`defaultSelected`/`onSelectedChange`, through
|
|
49
|
+
`useControllableState`. A change that changes nothing — a toggle refused at
|
|
50
|
+
the cap, a select-all with nothing left to add — notifies nobody
|
|
51
|
+
(`resolveIsSameSelection`), the rule `ToggleButton.Group` keeps.
|
|
52
|
+
- **`max` caps toggling and select-all, and never takes a pick away.** An
|
|
53
|
+
unpicked item at the cap does not go on, but a picked one still comes off, so
|
|
54
|
+
the way out of a full selection is always open. Select-all fills up to the cap
|
|
55
|
+
keeping existing picks first, and never drops a controlled pick that is already
|
|
56
|
+
past it.
|
|
57
|
+
- **Select-all flips to "Deselect all" at the cap, not only at the total.**
|
|
58
|
+
`resolveIsAllSelected` is true once every value is picked or once the cap is
|
|
59
|
+
reached below the total — past that point select-all would do nothing, and a
|
|
60
|
+
control that does nothing reads as broken.
|
|
61
|
+
- **`values` is optional, and the header degrades without it.** With it the
|
|
62
|
+
label is `"{title} · n of m"` and select-all shows; without it the label is
|
|
63
|
+
`"n selected"` and select-all is dropped, because there is nothing to pick all
|
|
64
|
+
of.
|
|
65
|
+
- **The indicator is round on purpose.** A square box is a form control being
|
|
66
|
+
filled in; a round one is a thing picked out of a set. `Checkbox` stays square.
|
|
67
|
+
22pt (`size-5.5` — the icon scale skips 22), `border-2 border-border`, picked
|
|
68
|
+
`bg-primary` with a `primary-foreground` check. It pops from 0.8 on a spring.
|
|
69
|
+
- **The leading mark pushes the content rather than covering it.** A spacer
|
|
70
|
+
opens from 0 to 34pt (`SELECTION_MODE_INDICATOR_OFFSET`, the mark plus a 12pt
|
|
71
|
+
gap) and the mark is drawn *after* the content, absolutely positioned over the
|
|
72
|
+
gap, so a child with a fill of its own never paints over it. A picked leading
|
|
73
|
+
row also takes `bg-primary/5`.
|
|
74
|
+
- **`ring` draws a 2pt primary ring a 2pt gap outside the item** (`-inset-1`)
|
|
75
|
+
with a small check badge at the top-end. Its corner is `rounded-xl` by default;
|
|
76
|
+
`ringClassName` reshapes it — `rounded-full` around a round swatch. That prop
|
|
77
|
+
is an addition: a ring that cannot match the item's own corner looks wrong on
|
|
78
|
+
anything round.
|
|
79
|
+
- **`none` draws nothing.** The caller reads `useSelectionMode().isSelected` and
|
|
80
|
+
paints its own state, or places a `SelectionMode.Indicator` itself.
|
|
81
|
+
- **The bar is absolute, over the list.** It never shrinks the list, so the list
|
|
82
|
+
must pad its own bottom (`pb-24`). It is hidden while nothing is picked unless
|
|
83
|
+
`isShownWhenEmpty`, and never shown with the mode off. `edge` pads its own
|
|
84
|
+
bottom with the safe-area inset (at least 8pt); `floating` lifts the whole
|
|
85
|
+
card by the inset instead, so the card keeps its own padding.
|
|
86
|
+
- **An action is handed the selection as it stands at the press.** A handler
|
|
87
|
+
that closed over `selected` would see a stale list; `onPress(selected)` cannot.
|
|
88
|
+
`isExitOnPress` exits afterwards — a delete, whose picks no longer exist.
|
|
89
|
+
- **The root fills the height it is offered** (`flex-1`). Give it one: inside a
|
|
90
|
+
`ScrollView` with no height the bar has no bottom to sit on. An `h-*` class on
|
|
91
|
+
the root itself is not enough — `flex-1` sets a zero basis, and in a parent
|
|
92
|
+
with no height of its own the root collapses to nothing. Size the parent, or
|
|
93
|
+
add `flex-none` to a root that should take its content's height (an always-on
|
|
94
|
+
picker with no bar).
|
|
95
|
+
- **Haptics are off by default.** `haptic` plays on entering the mode and on each
|
|
96
|
+
toggle that changed something — called from JS through `playHaptic`, since
|
|
97
|
+
neither moment is a gesture callback.
|
|
98
|
+
- **Every part but the root throws outside a `SelectionMode`**, by name.
|
|
99
|
+
- **The header's two controls take their own `testID`s** — `closeTestID` and
|
|
100
|
+
`selectAllTestID` — the way `Alert` and `Chip` forward one to their close
|
|
101
|
+
control. A `testID` on the header lands on its row, and a test that has to
|
|
102
|
+
leave the mode cannot reach a button the component draws for itself.
|
|
103
|
+
|
|
104
|
+
## Motion
|
|
105
|
+
|
|
106
|
+
- Indicator: the check fades in 200ms and the mark pops on a spring. The leading
|
|
107
|
+
spacer opens on the same spring while the mark fades.
|
|
108
|
+
- Header: fades in from 8pt above (`FadeInDown` with an 8pt initial value).
|
|
109
|
+
- Bar: slides up and fades in (`FadeInUp`), fades out downward.
|
|
110
|
+
- **Reduced motion is opacity-only, 150ms, and never disappears.** Every
|
|
111
|
+
transition sets `ReduceMotion.Never` on its 150ms fade, because under the
|
|
112
|
+
default policy a fade completes instantly — and the fade *is* the information
|
|
113
|
+
that the mode changed. The spacer snaps rather than sliding.
|
|
114
|
+
|
|
115
|
+
## Accessibility
|
|
116
|
+
|
|
117
|
+
- **While the mode is on, an item is a checkbox.** `accessibilityRole="checkbox"`,
|
|
118
|
+
`accessibilityState={{ checked, disabled }}`, hint "Double-tap to select".
|
|
119
|
+
`resolveItemAccessibility` is the decision and is pure.
|
|
120
|
+
- **While off, an item keeps the semantics of what it wraps** — a `button` when
|
|
121
|
+
it has an `onPress`, no role otherwise — and offers a `longpress` action
|
|
122
|
+
labelled "Start selecting", so the mode can be entered without the gesture. A
|
|
123
|
+
disabled item offers nothing it cannot do.
|
|
124
|
+
- **The count is announced.** The header label is a polite live region on
|
|
125
|
+
Android; iOS has none, so the header calls
|
|
126
|
+
`AccessibilityInfo.announceForAccessibility` when the label changes.
|
|
127
|
+
- **A group's `label` is its `accessibilityLabel`**, and a grid or strip is a
|
|
128
|
+
`list`.
|
|
129
|
+
|
|
130
|
+
## Group
|
|
131
|
+
|
|
132
|
+
- **`resolveGroupLayout` decides the shape.** `isHorizontal` is a strip and wins
|
|
133
|
+
over `columns`; two or more columns is a grid (floored); otherwise a stack.
|
|
134
|
+
- **A stack is a card** (`rounded-lg border border-border bg-card`) with a
|
|
135
|
+
`Separator` between rows unless `hasSeparators={false}`.
|
|
136
|
+
- **A grid measures itself.** Each cell is `resolveGridItemWidth` — the measured
|
|
137
|
+
width less the gaps, split evenly — and nothing renders until the first layout,
|
|
138
|
+
so no cell flashes at the wrong width. `hasSeparators` does not apply.
|
|
139
|
+
- **A strip pads its scroll content by 8pt** so a ring and its badge are not
|
|
140
|
+
clipped by the scroll view.
|
|
141
|
+
|
|
142
|
+
## Out of scope
|
|
143
|
+
|
|
144
|
+
- **`SelectionMode.Sheet`.** Importing `../bottom-sheet` would force the optional
|
|
145
|
+
sheet engine and `react-native-teleport` on every consumer of this component.
|
|
146
|
+
The `in-a-sheet` demo shows the composition instead: `SelectionMode` wraps the
|
|
147
|
+
`BottomSheet` (context reaches through the portal), the items go in the sheet's
|
|
148
|
+
scroll view, and the bar goes in `BottomSheet.Footer sticky` as
|
|
149
|
+
`className="relative border-t-0 bg-transparent"` with `isSafeAreaAware={false}`. A
|
|
150
|
+
first-class part is a follow-up, possibly on its own subpath.
|
|
151
|
+
- **Drag to select a range.** A pan across rows would fight the list's own
|
|
152
|
+
scroll; not in v1.
|
|
153
|
+
- **Replacing the screen's navbar.** The header renders where it is placed; a
|
|
154
|
+
screen that wants it in place of its navbar hides the navbar itself while
|
|
155
|
+
`isActive`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export { SelectionMode, type SelectionModeProps } from "./selection-mode";
|
|
2
|
+
export {
|
|
3
|
+
type SelectionModeContextValue,
|
|
4
|
+
type SelectionModeItemContextValue,
|
|
5
|
+
SelectionModeProvider,
|
|
6
|
+
useSelectionMode,
|
|
7
|
+
useSelectionModeContext,
|
|
8
|
+
useSelectionModeItemContext,
|
|
9
|
+
} from "./selection-mode.context";
|
|
10
|
+
export type { SelectionModeState } from "./selection-mode.types";
|
|
11
|
+
export {
|
|
12
|
+
resolveBarVisible,
|
|
13
|
+
resolveGridItemWidth,
|
|
14
|
+
resolveGroupLayout,
|
|
15
|
+
resolveHeaderCount,
|
|
16
|
+
resolveIndicatorShown,
|
|
17
|
+
resolveIsAllSelected,
|
|
18
|
+
resolveIsSameSelection,
|
|
19
|
+
resolveItemAccessibility,
|
|
20
|
+
resolveItemPress,
|
|
21
|
+
resolveSelectAll,
|
|
22
|
+
resolveToggle,
|
|
23
|
+
SELECTION_BAR_PLACEMENTS,
|
|
24
|
+
SELECTION_INDICATORS,
|
|
25
|
+
SELECTION_MODE_ACTION_FOREGROUND_TOKEN,
|
|
26
|
+
SELECTION_MODE_CHECK_TOKEN,
|
|
27
|
+
SELECTION_MODE_INDICATOR_OFFSET,
|
|
28
|
+
SELECTION_MODE_MOTION,
|
|
29
|
+
SELECTION_MODE_START_ACTION,
|
|
30
|
+
type SelectionBarPlacement,
|
|
31
|
+
type SelectionGroupLayout,
|
|
32
|
+
type SelectionIndicator,
|
|
33
|
+
type SelectionItemAccessibility,
|
|
34
|
+
type SelectionItemPress,
|
|
35
|
+
type SelectionModeVariantProps,
|
|
36
|
+
selectionModeVariants,
|
|
37
|
+
} from "./selection-mode.variants";
|
|
38
|
+
export type { SelectionModeActionProps } from "./selection-mode-action";
|
|
39
|
+
export type { SelectionModeBarProps } from "./selection-mode-bar";
|
|
40
|
+
export type { SelectionModeGroupProps } from "./selection-mode-group";
|
|
41
|
+
export type { SelectionModeHeaderProps } from "./selection-mode-header";
|
|
42
|
+
export type { SelectionModeIndicatorProps } from "./selection-mode-indicator";
|
|
43
|
+
export type { SelectionModeItemProps } from "./selection-mode-item";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode, useCallback } from "react";
|
|
2
|
+
import { Icon, type IconComponent } from "../icon";
|
|
3
|
+
import { Pressable } from "../pressable";
|
|
4
|
+
import { Text } from "../text";
|
|
5
|
+
import { useSelectionModePart } from "./selection-mode.context";
|
|
6
|
+
import { SELECTION_MODE_ACTION_FOREGROUND_TOKEN, selectionModeVariants } from "./selection-mode.variants";
|
|
7
|
+
|
|
8
|
+
export type SelectionModeActionProps = {
|
|
9
|
+
/** The label, under the icon. */
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
icon?: IconComponent;
|
|
12
|
+
/** Called with the selection as it stands at the press. */
|
|
13
|
+
onPress: (selected: string[]) => void;
|
|
14
|
+
/** Leave the mode once `onPress` has run. */
|
|
15
|
+
isExitOnPress?: boolean;
|
|
16
|
+
/** Draw the label and icon in the destructive colour. */
|
|
17
|
+
isDestructive?: boolean;
|
|
18
|
+
isDisabled?: boolean;
|
|
19
|
+
className?: string;
|
|
20
|
+
labelClassName?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One action in the bar: an icon over a label, sharing the bar's width evenly
|
|
25
|
+
* with its siblings.
|
|
26
|
+
*
|
|
27
|
+
* `onPress` receives the current selection, so the handler never closes over a
|
|
28
|
+
* stale one. `isExitOnPress` leaves the mode afterwards — a delete or an archive,
|
|
29
|
+
* whose picks no longer exist to stay picked.
|
|
30
|
+
*/
|
|
31
|
+
export function SelectionModeAction({
|
|
32
|
+
children,
|
|
33
|
+
icon,
|
|
34
|
+
onPress,
|
|
35
|
+
isExitOnPress = false,
|
|
36
|
+
isDestructive = false,
|
|
37
|
+
isDisabled = false,
|
|
38
|
+
className,
|
|
39
|
+
labelClassName,
|
|
40
|
+
}: SelectionModeActionProps): ReactElement {
|
|
41
|
+
const { selected, exit } = useSelectionModePart("SelectionMode.Action");
|
|
42
|
+
const slots = selectionModeVariants({ isDestructive, isDisabled });
|
|
43
|
+
const token = SELECTION_MODE_ACTION_FOREGROUND_TOKEN[isDestructive ? "destructive" : "default"];
|
|
44
|
+
|
|
45
|
+
const handlePress = useCallback(() => {
|
|
46
|
+
onPress([...selected]);
|
|
47
|
+
if (isExitOnPress) exit();
|
|
48
|
+
}, [exit, isExitOnPress, onPress, selected]);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Pressable
|
|
52
|
+
accessibilityLabel={typeof children === "string" ? children : undefined}
|
|
53
|
+
className={slots.action({ className })}
|
|
54
|
+
disabled={isDisabled}
|
|
55
|
+
feedback="fade"
|
|
56
|
+
onPress={handlePress}
|
|
57
|
+
>
|
|
58
|
+
{icon ? <Icon color={token} icon={icon} size="xl" /> : null}
|
|
59
|
+
<Text className={slots.actionLabel({ className: labelClassName })} numberOfLines={1}>
|
|
60
|
+
{children}
|
|
61
|
+
</Text>
|
|
62
|
+
</Pressable>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
SelectionModeAction.displayName = "DelacourUI.SelectionMode.Action";
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type { ViewProps } from "react-native";
|
|
3
|
+
import Animated, {
|
|
4
|
+
FadeIn,
|
|
5
|
+
FadeInUp,
|
|
6
|
+
FadeOut,
|
|
7
|
+
FadeOutDown,
|
|
8
|
+
ReduceMotion,
|
|
9
|
+
useReducedMotion,
|
|
10
|
+
} from "react-native-reanimated";
|
|
11
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
12
|
+
import { useSelectionModePart } from "./selection-mode.context";
|
|
13
|
+
import {
|
|
14
|
+
resolveBarVisible,
|
|
15
|
+
SELECTION_MODE_MOTION,
|
|
16
|
+
type SelectionBarPlacement,
|
|
17
|
+
selectionModeVariants,
|
|
18
|
+
} from "./selection-mode.variants";
|
|
19
|
+
|
|
20
|
+
export type SelectionModeBarProps = ViewProps & {
|
|
21
|
+
/** `edge` spans the bottom of the page; `floating` is an inset card above it. Default `edge`. */
|
|
22
|
+
placement?: SelectionBarPlacement;
|
|
23
|
+
/** Keep the bar up while nothing is picked. Default `false`. */
|
|
24
|
+
isShownWhenEmpty?: boolean;
|
|
25
|
+
/** Clear the home indicator with the bottom safe-area inset. Default `true`. */
|
|
26
|
+
isSafeAreaAware?: boolean;
|
|
27
|
+
className?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ENTERING = FadeInUp.duration(SELECTION_MODE_MOTION.fadeMs);
|
|
31
|
+
const EXITING = FadeOutDown.duration(SELECTION_MODE_MOTION.reducedFadeMs);
|
|
32
|
+
const REDUCED_ENTERING = FadeIn.duration(SELECTION_MODE_MOTION.reducedFadeMs).reduceMotion(ReduceMotion.Never);
|
|
33
|
+
const REDUCED_EXITING = FadeOut.duration(SELECTION_MODE_MOTION.reducedFadeMs).reduceMotion(ReduceMotion.Never);
|
|
34
|
+
|
|
35
|
+
/** The breathing room under the actions when there is no inset to clear. */
|
|
36
|
+
const BAR_MIN_BOTTOM = 8;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The actions for what is picked, over the bottom edge of the root.
|
|
40
|
+
*
|
|
41
|
+
* Absolute, so it sits over the list rather than shrinking it — pad the bottom
|
|
42
|
+
* of the list so its last row can scroll clear. Hidden while nothing is picked
|
|
43
|
+
* unless `isShownWhenEmpty`, and never shown with the mode off. It slides up
|
|
44
|
+
* and fades in; under reduced motion it only fades.
|
|
45
|
+
*
|
|
46
|
+
* `edge` pads its own bottom with the safe-area inset. `floating` lifts the
|
|
47
|
+
* whole card by it instead, so the card keeps its own padding.
|
|
48
|
+
*/
|
|
49
|
+
export function SelectionModeBar({
|
|
50
|
+
placement = "edge",
|
|
51
|
+
isShownWhenEmpty = false,
|
|
52
|
+
isSafeAreaAware = true,
|
|
53
|
+
className,
|
|
54
|
+
style,
|
|
55
|
+
children,
|
|
56
|
+
...props
|
|
57
|
+
}: SelectionModeBarProps): ReactElement | null {
|
|
58
|
+
const { isActive, count } = useSelectionModePart("SelectionMode.Bar");
|
|
59
|
+
const isReducedMotion = useReducedMotion();
|
|
60
|
+
const insets = useSafeAreaInsets();
|
|
61
|
+
const inset = isSafeAreaAware ? insets.bottom : 0;
|
|
62
|
+
|
|
63
|
+
if (!resolveBarVisible({ count, isActive, isShownWhenEmpty })) return null;
|
|
64
|
+
|
|
65
|
+
const placementStyle = placement === "edge" ? { paddingBottom: Math.max(inset, BAR_MIN_BOTTOM) } : { bottom: inset };
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Animated.View
|
|
69
|
+
className={selectionModeVariants({ placement }).bar({ className })}
|
|
70
|
+
entering={isReducedMotion ? REDUCED_ENTERING : ENTERING}
|
|
71
|
+
exiting={isReducedMotion ? REDUCED_EXITING : EXITING}
|
|
72
|
+
style={[placementStyle, style]}
|
|
73
|
+
{...props}
|
|
74
|
+
>
|
|
75
|
+
{children}
|
|
76
|
+
</Animated.View>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
SelectionModeBar.displayName = "DelacourUI.SelectionMode.Bar";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Children, Fragment, type ReactElement, type ReactNode, useCallback, useState } from "react";
|
|
2
|
+
import { type LayoutChangeEvent, ScrollView, View, type ViewProps } from "react-native";
|
|
3
|
+
import { Separator } from "../separator";
|
|
4
|
+
import { Text } from "../text";
|
|
5
|
+
import { useSelectionModePart } from "./selection-mode.context";
|
|
6
|
+
import { resolveGridItemWidth, resolveGroupLayout, selectionModeVariants } from "./selection-mode.variants";
|
|
7
|
+
|
|
8
|
+
export type SelectionModeGroupProps = ViewProps & {
|
|
9
|
+
/** Lay the items out in a grid of this many columns. */
|
|
10
|
+
columns?: number;
|
|
11
|
+
/** A single scrolling row. Wins over `columns`. */
|
|
12
|
+
isHorizontal?: boolean;
|
|
13
|
+
/** Each item's width in a horizontal strip. Default 44. */
|
|
14
|
+
itemWidth?: number;
|
|
15
|
+
/** Space between items in a grid or a strip. Default 12. */
|
|
16
|
+
gap?: number;
|
|
17
|
+
/** A caption above the items, and the group's accessibility label. */
|
|
18
|
+
label?: string;
|
|
19
|
+
/** Hairlines between stacked items. Default `true` for a stack, `false` for a grid or a strip. */
|
|
20
|
+
hasSeparators?: boolean;
|
|
21
|
+
className?: string;
|
|
22
|
+
labelClassName?: string;
|
|
23
|
+
children?: ReactNode;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The room a ring and its badge need outside an item, so a strip's scroll view
|
|
28
|
+
* does not clip them.
|
|
29
|
+
*/
|
|
30
|
+
const STRIP_INSET = 8;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Lays items out as a stacked card list, a grid or a horizontal strip.
|
|
34
|
+
*
|
|
35
|
+
* A stack is a card with hairlines between its rows. A grid splits the measured
|
|
36
|
+
* width evenly between `columns`, less the gaps. A strip scrolls sideways, each
|
|
37
|
+
* item `itemWidth` wide, with room around it for a `ring` to draw.
|
|
38
|
+
*
|
|
39
|
+
* A grid or a strip is announced as a list; `label` names it either way.
|
|
40
|
+
*/
|
|
41
|
+
export function SelectionModeGroup({
|
|
42
|
+
columns,
|
|
43
|
+
isHorizontal = false,
|
|
44
|
+
itemWidth = 44,
|
|
45
|
+
gap = 12,
|
|
46
|
+
label,
|
|
47
|
+
hasSeparators,
|
|
48
|
+
className,
|
|
49
|
+
labelClassName,
|
|
50
|
+
children,
|
|
51
|
+
...props
|
|
52
|
+
}: SelectionModeGroupProps): ReactElement {
|
|
53
|
+
useSelectionModePart("SelectionMode.Group");
|
|
54
|
+
const layout = resolveGroupLayout({ columns, isHorizontal });
|
|
55
|
+
const slots = selectionModeVariants({ layout: layout.kind });
|
|
56
|
+
const items = Children.toArray(children);
|
|
57
|
+
const [width, setWidth] = useState(0);
|
|
58
|
+
|
|
59
|
+
const handleLayout = useCallback((event: LayoutChangeEvent) => setWidth(event.nativeEvent.layout.width), []);
|
|
60
|
+
|
|
61
|
+
const renderBody = (): ReactElement => {
|
|
62
|
+
if (layout.kind === "strip") {
|
|
63
|
+
return (
|
|
64
|
+
<ScrollView
|
|
65
|
+
contentContainerStyle={{ gap, padding: STRIP_INSET }}
|
|
66
|
+
horizontal
|
|
67
|
+
showsHorizontalScrollIndicator={false}
|
|
68
|
+
>
|
|
69
|
+
{items.map((child, index) => (
|
|
70
|
+
<View key={keyOf(child, index)} style={{ width: itemWidth }}>
|
|
71
|
+
{child}
|
|
72
|
+
</View>
|
|
73
|
+
))}
|
|
74
|
+
</ScrollView>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (layout.kind === "grid") {
|
|
79
|
+
const cellWidth = resolveGridItemWidth({ columns: layout.columns, containerWidth: width, gap });
|
|
80
|
+
return (
|
|
81
|
+
<View className={slots.group()} onLayout={handleLayout} style={{ gap }}>
|
|
82
|
+
{width > 0
|
|
83
|
+
? items.map((child, index) => (
|
|
84
|
+
<View key={keyOf(child, index)} style={{ width: cellWidth }}>
|
|
85
|
+
{child}
|
|
86
|
+
</View>
|
|
87
|
+
))
|
|
88
|
+
: null}
|
|
89
|
+
</View>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const isSeparated = hasSeparators ?? true;
|
|
94
|
+
return (
|
|
95
|
+
<View className={slots.group()}>
|
|
96
|
+
{items.map((child, index) => (
|
|
97
|
+
<Fragment key={keyOf(child, index)}>
|
|
98
|
+
{isSeparated && index > 0 ? <Separator /> : null}
|
|
99
|
+
{child}
|
|
100
|
+
</Fragment>
|
|
101
|
+
))}
|
|
102
|
+
</View>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<View
|
|
108
|
+
accessibilityLabel={label}
|
|
109
|
+
accessibilityRole={layout.kind === "stack" ? undefined : "list"}
|
|
110
|
+
className={className}
|
|
111
|
+
{...props}
|
|
112
|
+
>
|
|
113
|
+
{label ? <Text className={slots.groupLabel({ className: labelClassName })}>{label}</Text> : null}
|
|
114
|
+
{renderBody()}
|
|
115
|
+
</View>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
SelectionModeGroup.displayName = "DelacourUI.SelectionMode.Group";
|
|
119
|
+
|
|
120
|
+
/** The key `Children.toArray` already gave a child, or its index for a bare value. */
|
|
121
|
+
function keyOf(child: ReturnType<typeof Children.toArray>[number], index: number): string | number {
|
|
122
|
+
return typeof child === "object" && child !== null && "key" in child && child.key !== null ? child.key : index;
|
|
123
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type ReactElement, useEffect } from "react";
|
|
2
|
+
import { AccessibilityInfo, Platform, type ViewProps } from "react-native";
|
|
3
|
+
import Animated, { FadeIn, FadeInDown, FadeOut, ReduceMotion, useReducedMotion } from "react-native-reanimated";
|
|
4
|
+
import { IconCrossMedium } from "../../icons/central";
|
|
5
|
+
import { Button } from "../button";
|
|
6
|
+
import { Icon } from "../icon";
|
|
7
|
+
import { Text } from "../text";
|
|
8
|
+
import { useSelectionModePart } from "./selection-mode.context";
|
|
9
|
+
import { resolveHeaderCount, SELECTION_MODE_MOTION, selectionModeVariants } from "./selection-mode.variants";
|
|
10
|
+
|
|
11
|
+
export type SelectionModeHeaderProps = ViewProps & {
|
|
12
|
+
/** Leads the count when the root knows its `values`. Default `"Select"`. */
|
|
13
|
+
title?: string;
|
|
14
|
+
/** Drop the select-all control. It is also dropped when the root has no `values` to pick from. */
|
|
15
|
+
isSelectAllHidden?: boolean;
|
|
16
|
+
/** `testID` for the close control, so a test or automation can leave the mode. */
|
|
17
|
+
closeTestID?: string;
|
|
18
|
+
/** `testID` for the select-all control. */
|
|
19
|
+
selectAllTestID?: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const ENTERING = FadeInDown.duration(SELECTION_MODE_MOTION.fadeMs).withInitialValues({
|
|
24
|
+
opacity: 0,
|
|
25
|
+
transform: [{ translateY: -SELECTION_MODE_MOTION.headerTravel }],
|
|
26
|
+
});
|
|
27
|
+
const EXITING = FadeOut.duration(SELECTION_MODE_MOTION.reducedFadeMs);
|
|
28
|
+
const REDUCED_ENTERING = FadeIn.duration(SELECTION_MODE_MOTION.reducedFadeMs).reduceMotion(ReduceMotion.Never);
|
|
29
|
+
const REDUCED_EXITING = FadeOut.duration(SELECTION_MODE_MOTION.reducedFadeMs).reduceMotion(ReduceMotion.Never);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Says the mode is on and offers the way out.
|
|
33
|
+
*
|
|
34
|
+
* Renders only while the mode is on: a close button that exits, the count —
|
|
35
|
+
* `"{title} · n of m"`, or `"n selected"` when the root has no `values` — and
|
|
36
|
+
* select-all, which turns into "Deselect all" once there is nothing left to add.
|
|
37
|
+
* It fades in from 8pt above; under reduced motion it only fades.
|
|
38
|
+
*
|
|
39
|
+
* The count is announced as it changes: a polite live region on Android, an
|
|
40
|
+
* announcement on iOS, which has no live regions.
|
|
41
|
+
*/
|
|
42
|
+
export function SelectionModeHeader({
|
|
43
|
+
title = "Select",
|
|
44
|
+
isSelectAllHidden = false,
|
|
45
|
+
closeTestID,
|
|
46
|
+
selectAllTestID,
|
|
47
|
+
className,
|
|
48
|
+
...props
|
|
49
|
+
}: SelectionModeHeaderProps): ReactElement | null {
|
|
50
|
+
const { isActive, count, total, isAllSelected, exit, selectAll, clear } =
|
|
51
|
+
useSelectionModePart("SelectionMode.Header");
|
|
52
|
+
const isReducedMotion = useReducedMotion();
|
|
53
|
+
const label = resolveHeaderCount({ count, title, total });
|
|
54
|
+
const slots = selectionModeVariants();
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (isActive && Platform.OS === "ios") AccessibilityInfo.announceForAccessibility(label);
|
|
58
|
+
}, [isActive, label]);
|
|
59
|
+
|
|
60
|
+
if (!isActive) return null;
|
|
61
|
+
|
|
62
|
+
const hasSelectAll = !isSelectAllHidden && total !== undefined && total > 0;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Animated.View
|
|
66
|
+
className={slots.header({ className })}
|
|
67
|
+
entering={isReducedMotion ? REDUCED_ENTERING : ENTERING}
|
|
68
|
+
exiting={isReducedMotion ? REDUCED_EXITING : EXITING}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
<Button accessibilityLabel="Stop selecting" onPress={exit} size="icon-md" testID={closeTestID} variant="ghost">
|
|
72
|
+
<Icon icon={IconCrossMedium} />
|
|
73
|
+
</Button>
|
|
74
|
+
<Text accessibilityLiveRegion="polite" className={slots.headerTitle()} numberOfLines={1}>
|
|
75
|
+
{label}
|
|
76
|
+
</Text>
|
|
77
|
+
{hasSelectAll ? (
|
|
78
|
+
<Button onPress={isAllSelected ? clear : selectAll} size="sm" testID={selectAllTestID} variant="ghost">
|
|
79
|
+
<Button.Label>{isAllSelected ? "Deselect all" : "Select all"}</Button.Label>
|
|
80
|
+
</Button>
|
|
81
|
+
) : null}
|
|
82
|
+
</Animated.View>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
SelectionModeHeader.displayName = "DelacourUI.SelectionMode.Header";
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { type ReactElement, useEffect } from "react";
|
|
2
|
+
import type { ViewProps, ViewStyle } from "react-native";
|
|
3
|
+
import Animated, {
|
|
4
|
+
ReduceMotion,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
useReducedMotion,
|
|
7
|
+
useSharedValue,
|
|
8
|
+
withSequence,
|
|
9
|
+
withSpring,
|
|
10
|
+
withTiming,
|
|
11
|
+
} from "react-native-reanimated";
|
|
12
|
+
import { IconCheckmark1Small } from "../../icons/central";
|
|
13
|
+
import { Icon } from "../icon";
|
|
14
|
+
import { useSelectionModeItemContext, useSelectionModePart } from "./selection-mode.context";
|
|
15
|
+
import { SELECTION_MODE_CHECK_TOKEN, SELECTION_MODE_MOTION, selectionModeVariants } from "./selection-mode.variants";
|
|
16
|
+
|
|
17
|
+
export type SelectionModeIndicatorProps = ViewProps & {
|
|
18
|
+
/** The id it reports on. Defaults to the enclosing `SelectionMode.Item`'s. */
|
|
19
|
+
value?: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Animates a picked state in: the check fades, and the whole mark pops from 0.8
|
|
25
|
+
* on a spring. Under reduced motion the pop is dropped and the fade is 150ms.
|
|
26
|
+
*/
|
|
27
|
+
function usePickedStyle(isSelected: boolean): ReturnType<typeof useAnimatedStyle<ViewStyle>> {
|
|
28
|
+
const isReducedMotion = useReducedMotion();
|
|
29
|
+
const picked = useSharedValue(isSelected ? 1 : 0);
|
|
30
|
+
const scale = useSharedValue(1);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const target = isSelected ? 1 : 0;
|
|
34
|
+
if (isReducedMotion) {
|
|
35
|
+
picked.value = withTiming(target, {
|
|
36
|
+
duration: SELECTION_MODE_MOTION.reducedFadeMs,
|
|
37
|
+
reduceMotion: ReduceMotion.Never,
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
picked.value = withTiming(target, { duration: SELECTION_MODE_MOTION.fadeMs });
|
|
42
|
+
if (isSelected)
|
|
43
|
+
scale.value = withSequence(withTiming(0.8, { duration: 0 }), withSpring(1, SELECTION_MODE_MOTION.spring));
|
|
44
|
+
}, [isReducedMotion, isSelected, picked, scale]);
|
|
45
|
+
|
|
46
|
+
return useAnimatedStyle(() => ({ opacity: picked.value, transform: [{ scale: scale.value }] }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The round mark a `leading` item shows: an empty ring, or a filled circle with
|
|
51
|
+
* a check.
|
|
52
|
+
*
|
|
53
|
+
* Round on purpose. A square box is a form control being filled in; a round
|
|
54
|
+
* one is a thing picked out of a set. `Checkbox` stays square.
|
|
55
|
+
*
|
|
56
|
+
* Rendered by `SelectionMode.Item` itself. Render one by hand only for an item
|
|
57
|
+
* drawn with `indicator="none"` that wants the mark somewhere else in its
|
|
58
|
+
* layout — inside an item it reads that item's id, elsewhere it takes `value`.
|
|
59
|
+
*/
|
|
60
|
+
export function SelectionModeIndicator({ value, className, ...props }: SelectionModeIndicatorProps): ReactElement {
|
|
61
|
+
const selection = useSelectionModePart("SelectionMode.Indicator");
|
|
62
|
+
const item = useSelectionModeItemContext();
|
|
63
|
+
const id = value ?? item?.value;
|
|
64
|
+
if (id === undefined) {
|
|
65
|
+
throw new Error("SelectionMode.Indicator needs a `value`, or an enclosing <SelectionMode.Item>.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const isSelected = selection.isSelected(id);
|
|
69
|
+
const slots = selectionModeVariants({ isSelected });
|
|
70
|
+
const checkStyle = usePickedStyle(isSelected);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Animated.View
|
|
74
|
+
accessibilityElementsHidden
|
|
75
|
+
className={slots.indicator({ className })}
|
|
76
|
+
importantForAccessibility="no-hide-descendants"
|
|
77
|
+
{...props}
|
|
78
|
+
>
|
|
79
|
+
<Animated.View style={checkStyle}>
|
|
80
|
+
<Icon className={slots.indicatorCheck()} color={SELECTION_MODE_CHECK_TOKEN} icon={IconCheckmark1Small} />
|
|
81
|
+
</Animated.View>
|
|
82
|
+
</Animated.View>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
SelectionModeIndicator.displayName = "DelacourUI.SelectionMode.Indicator";
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The `ring` indicator: a primary ring a 2pt gap outside the item, and a small
|
|
89
|
+
* check badge at its top-end corner. Drawn only while the item is picked.
|
|
90
|
+
*
|
|
91
|
+
* Internal — `SelectionMode.Item` renders it. `className` reshapes the ring to
|
|
92
|
+
* the item's own corner: `rounded-full` around a round swatch.
|
|
93
|
+
*/
|
|
94
|
+
export function SelectionModeRing({
|
|
95
|
+
isSelected,
|
|
96
|
+
className,
|
|
97
|
+
}: {
|
|
98
|
+
isSelected: boolean;
|
|
99
|
+
className?: string;
|
|
100
|
+
}): ReactElement {
|
|
101
|
+
const slots = selectionModeVariants({ indicator: "ring", isSelected });
|
|
102
|
+
const pickedStyle = usePickedStyle(isSelected);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Animated.View
|
|
106
|
+
accessibilityElementsHidden
|
|
107
|
+
className={slots.ring({ className })}
|
|
108
|
+
importantForAccessibility="no-hide-descendants"
|
|
109
|
+
pointerEvents="none"
|
|
110
|
+
style={pickedStyle}
|
|
111
|
+
>
|
|
112
|
+
<Animated.View className={slots.ringBadge()}>
|
|
113
|
+
<Icon className={slots.indicatorCheck()} color={SELECTION_MODE_CHECK_TOKEN} icon={IconCheckmark1Small} />
|
|
114
|
+
</Animated.View>
|
|
115
|
+
</Animated.View>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
SelectionModeRing.displayName = "DelacourUI.SelectionMode.Item.Ring";
|