@delacour/react-native-ui 0.1.0-alpha.3 → 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 +11 -7
- package/package.json +49 -5
- package/src/components/accordion/AGENTS.md +21 -9
- package/src/components/accordion/accordion-content.tsx +7 -4
- package/src/components/accordion/accordion-item.tsx +15 -13
- package/src/components/accordion/accordion.context.tsx +11 -5
- package/src/components/accordion/accordion.variants.test.ts +64 -3
- package/src/components/accordion/accordion.variants.ts +33 -9
- package/src/components/alert/AGENTS.md +102 -0
- package/src/components/alert/alert-action.tsx +17 -0
- package/src/components/alert/alert-close-button.tsx +50 -0
- package/src/components/alert/alert-content.tsx +15 -0
- package/src/components/alert/alert-description.tsx +16 -0
- package/src/components/alert/alert-glyphs.ts +25 -0
- package/src/components/alert/alert-indicator.tsx +40 -0
- package/src/components/alert/alert-title.tsx +18 -0
- package/src/components/alert/alert.context.tsx +69 -0
- package/src/components/alert/alert.tsx +160 -0
- package/src/components/alert/alert.types.ts +10 -0
- package/src/components/alert/alert.variants.test.ts +218 -0
- package/src/components/alert/alert.variants.ts +180 -0
- package/src/components/alert/index.ts +23 -0
- package/src/components/avatar/AGENTS.md +106 -0
- package/src/components/avatar/avatar-badge.tsx +51 -0
- package/src/components/avatar/avatar-group.tsx +139 -0
- package/src/components/avatar/avatar.context.tsx +93 -0
- package/src/components/avatar/avatar.tsx +201 -0
- package/src/components/avatar/avatar.utils.test.ts +174 -0
- package/src/components/avatar/avatar.utils.ts +141 -0
- package/src/components/avatar/avatar.variants.test.ts +251 -0
- package/src/components/avatar/avatar.variants.ts +270 -0
- package/src/components/avatar/index.ts +39 -0
- package/src/components/badge/AGENTS.md +5 -0
- package/src/components/badge/badge.tsx +6 -1
- package/src/components/bottom-sheet/AGENTS.md +186 -221
- 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 -207
- 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 -120
- 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 -52
- 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 -70
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -105
- 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 +9 -1
- 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/screen/screen-navbar-back-button.tsx +1 -1
- 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,180 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
resolveSurfacePlane,
|
|
6
|
+
resolveSurfaceVariant,
|
|
7
|
+
SURFACE_FILLED_VARIANTS,
|
|
8
|
+
SURFACE_FOREGROUND_TOKENS,
|
|
9
|
+
SURFACE_PADDINGS,
|
|
10
|
+
SURFACE_VARIANTS,
|
|
11
|
+
surfaceVariants,
|
|
12
|
+
} from "./surface.variants";
|
|
13
|
+
|
|
14
|
+
/** Every class string the variant function can produce, one per combination. */
|
|
15
|
+
function everyRoot(): string[] {
|
|
16
|
+
return SURFACE_VARIANTS.flatMap((variant) =>
|
|
17
|
+
SURFACE_PADDINGS.map((padding) => surfaceVariants({ padding, variant }).root())
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("surfaceVariants root slot", () => {
|
|
22
|
+
test("defaults to the card surface with md padding", () => {
|
|
23
|
+
const cls = surfaceVariants().root();
|
|
24
|
+
expect(cls).toContain("bg-card");
|
|
25
|
+
expect(cls).toContain("border-border");
|
|
26
|
+
expect(cls).toContain("p-4");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("maps each variant to its fill", () => {
|
|
30
|
+
expect(surfaceVariants({ variant: "default" }).root()).toContain("bg-card");
|
|
31
|
+
expect(surfaceVariants({ variant: "secondary" }).root()).toContain("bg-secondary");
|
|
32
|
+
expect(surfaceVariants({ variant: "tertiary" }).root()).toContain("bg-tertiary");
|
|
33
|
+
expect(surfaceVariants({ variant: "transparent" }).root()).toContain("bg-transparent");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("gives every variant a distinct surface", () => {
|
|
37
|
+
const seen = new Set(SURFACE_VARIANTS.map((variant) => surfaceVariants({ variant }).root()));
|
|
38
|
+
expect(seen.size).toBe(SURFACE_VARIANTS.length);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// A card on a page is white on near-white in light. The hairline is what
|
|
42
|
+
// holds its edge; the filled rungs below it are separated by their fill.
|
|
43
|
+
test("only the default variant draws a visible border", () => {
|
|
44
|
+
expect(surfaceVariants({ variant: "default" }).root()).toContain("border-border");
|
|
45
|
+
for (const variant of SURFACE_VARIANTS.filter((name) => name !== "default")) {
|
|
46
|
+
expect(surfaceVariants({ variant }).root()).not.toContain("border-border");
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Every variant carries the same one-point border box, so switching variant
|
|
51
|
+
// never moves the content by a pixel.
|
|
52
|
+
test("every variant reserves the same border width", () => {
|
|
53
|
+
for (const cls of everyRoot()) {
|
|
54
|
+
expect(cls).toMatch(/(^|\s)border(\s|$)/);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("is card-shaped, with a continuous corner, at every variant and padding", () => {
|
|
59
|
+
for (const cls of everyRoot()) {
|
|
60
|
+
expect(cls).toContain("rounded-lg");
|
|
61
|
+
expect(cls).toContain("border-continuous");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("maps each padding to its step", () => {
|
|
66
|
+
expect(surfaceVariants({ padding: "none" }).root()).toContain("p-0");
|
|
67
|
+
expect(surfaceVariants({ padding: "sm" }).root()).toContain("p-3");
|
|
68
|
+
expect(surfaceVariants({ padding: "md" }).root()).toContain("p-4");
|
|
69
|
+
expect(surfaceVariants({ padding: "lg" }).root()).toContain("p-6");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// `none` is for content that bleeds to the edge — an image, a chart — and
|
|
73
|
+
// that content has to take the corner with it.
|
|
74
|
+
test("clips only when its content touches the edge", () => {
|
|
75
|
+
expect(surfaceVariants({ padding: "none" }).root()).toContain("overflow-hidden");
|
|
76
|
+
for (const padding of SURFACE_PADDINGS.filter((name) => name !== "none")) {
|
|
77
|
+
expect(surfaceVariants({ padding }).root()).not.toContain("overflow-hidden");
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Rule 1: a React Native View does not cascade colour to a Text descendant.
|
|
82
|
+
test("carries no text treatment on the root", () => {
|
|
83
|
+
for (const cls of everyRoot()) {
|
|
84
|
+
expect(cls).not.toMatch(/\btext-/);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("draws no shadow anywhere", () => {
|
|
89
|
+
for (const cls of everyRoot()) {
|
|
90
|
+
expect(cls).not.toMatch(/shadow/);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("merges an incoming className last", () => {
|
|
95
|
+
expect(surfaceVariants().root({ className: "mt-3" })).toContain("mt-3");
|
|
96
|
+
expect(surfaceVariants().root({ className: "bg-popover" })).not.toContain("bg-card");
|
|
97
|
+
expect(surfaceVariants().root({ className: "p-8" })).not.toMatch(/\bp-4\b/);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// `border-continuous` is Uniwind's, and a merger that read it as a colour
|
|
101
|
+
// would drop it the moment a caller tinted the border.
|
|
102
|
+
test("keeps its corner curve when a caller recolours the border", () => {
|
|
103
|
+
expect(surfaceVariants().root({ className: "border-destructive" })).toContain("border-continuous");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("SURFACE_FOREGROUND_TOKENS", () => {
|
|
108
|
+
test("names the X-foreground token for each fill", () => {
|
|
109
|
+
expect(SURFACE_FOREGROUND_TOKENS).toEqual({
|
|
110
|
+
default: "card-foreground",
|
|
111
|
+
secondary: "secondary-foreground",
|
|
112
|
+
tertiary: "tertiary-foreground",
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Every token named here has to exist, or the class compiles to nothing and
|
|
117
|
+
// the text keeps whatever colour it inherited.
|
|
118
|
+
test("every token is declared in theme.css", () => {
|
|
119
|
+
const css = readFileSync(join(import.meta.dir, "../../styles/theme.css"), "utf8");
|
|
120
|
+
for (const token of Object.values(SURFACE_FOREGROUND_TOKENS)) {
|
|
121
|
+
expect(css).toContain(`--color-${token}: var(--${token})`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("resolveSurfaceVariant", () => {
|
|
127
|
+
test("an explicit variant always wins", () => {
|
|
128
|
+
for (const variant of SURFACE_VARIANTS) {
|
|
129
|
+
expect(resolveSurfaceVariant({ parentPlane: null, variant })).toBe(variant);
|
|
130
|
+
expect(resolveSurfaceVariant({ parentPlane: "secondary", variant })).toBe(variant);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("a top-level surface is the default card", () => {
|
|
135
|
+
expect(resolveSurfaceVariant({ parentPlane: null })).toBe("default");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("a nested surface steps to the next fill", () => {
|
|
139
|
+
expect(resolveSurfaceVariant({ parentPlane: "default" })).toBe("secondary");
|
|
140
|
+
expect(resolveSurfaceVariant({ parentPlane: "secondary" })).toBe("tertiary");
|
|
141
|
+
expect(resolveSurfaceVariant({ parentPlane: "tertiary" })).toBe("secondary");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// The point of the ladder: whatever depth it is nested at, a surface never
|
|
145
|
+
// resolves to the fill it is sitting on, or it would vanish into it.
|
|
146
|
+
test("never resolves to the fill of the plane it sits on", () => {
|
|
147
|
+
for (const parentPlane of SURFACE_FILLED_VARIANTS) {
|
|
148
|
+
expect(resolveSurfaceVariant({ parentPlane })).not.toBe(parentPlane);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("never resolves to transparent on its own", () => {
|
|
153
|
+
for (const parentPlane of [null, ...SURFACE_FILLED_VARIANTS]) {
|
|
154
|
+
expect(resolveSurfaceVariant({ parentPlane })).not.toBe("transparent");
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("resolveSurfacePlane", () => {
|
|
160
|
+
test("a filled surface is the plane its children sit on", () => {
|
|
161
|
+
for (const variant of SURFACE_FILLED_VARIANTS) {
|
|
162
|
+
expect(resolveSurfacePlane({ parentPlane: null, variant })).toBe(variant);
|
|
163
|
+
expect(resolveSurfacePlane({ parentPlane: "tertiary", variant })).toBe(variant);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// A transparent surface paints nothing, so what its children sit on is
|
|
168
|
+
// still whatever it sits on.
|
|
169
|
+
test("a transparent surface passes its parent's plane through", () => {
|
|
170
|
+
expect(resolveSurfacePlane({ parentPlane: null, variant: "transparent" })).toBeNull();
|
|
171
|
+
for (const parentPlane of SURFACE_FILLED_VARIANTS) {
|
|
172
|
+
expect(resolveSurfacePlane({ parentPlane, variant: "transparent" })).toBe(parentPlane);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("a surface inside a transparent one steps from the plane beneath both", () => {
|
|
177
|
+
const through = resolveSurfacePlane({ parentPlane: "default", variant: "transparent" });
|
|
178
|
+
expect(resolveSurfaceVariant({ parentPlane: through })).toBe("secondary");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
|
|
4
|
+
export const SURFACE_VARIANTS = ["default", "secondary", "tertiary", "transparent"] as const;
|
|
5
|
+
|
|
6
|
+
export const SURFACE_PADDINGS = ["none", "sm", "md", "lg"] as const;
|
|
7
|
+
|
|
8
|
+
export type SurfaceVariant = (typeof SURFACE_VARIANTS)[number];
|
|
9
|
+
export type SurfacePadding = (typeof SURFACE_PADDINGS)[number];
|
|
10
|
+
|
|
11
|
+
/** The variants that paint a fill — every one but `transparent`. */
|
|
12
|
+
export const SURFACE_FILLED_VARIANTS = ["default", "secondary", "tertiary"] as const;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A fill something can sit on. `transparent` is not one: it paints nothing, so
|
|
16
|
+
* whatever is inside it still sits on the plane beneath.
|
|
17
|
+
*/
|
|
18
|
+
export type SurfacePlane = (typeof SURFACE_FILLED_VARIANTS)[number];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Theme token for content drawn on each fill — `X-foreground` on an `X`
|
|
22
|
+
* surface, per the package's token rule. A class is `text-${token}`; a value,
|
|
23
|
+
* for an icon or a gradient stop, is `useThemeColor(token)`.
|
|
24
|
+
*/
|
|
25
|
+
export const SURFACE_FOREGROUND_TOKENS = {
|
|
26
|
+
default: "card-foreground",
|
|
27
|
+
secondary: "secondary-foreground",
|
|
28
|
+
tertiary: "tertiary-foreground",
|
|
29
|
+
} as const satisfies Record<SurfacePlane, string>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The fill a nested surface takes when it names none, keyed by the plane it
|
|
33
|
+
* sits on.
|
|
34
|
+
*
|
|
35
|
+
* `tertiary` is quieter than `secondary` — half a step back toward the page —
|
|
36
|
+
* so a third level returns to `secondary` rather than stopping. What matters is
|
|
37
|
+
* that each surface differs from its immediate parent; a strictly ascending
|
|
38
|
+
* ladder would run out of fills at the fourth level and start vanishing into
|
|
39
|
+
* its own parent.
|
|
40
|
+
*/
|
|
41
|
+
const NEXT_PLANE = {
|
|
42
|
+
default: "secondary",
|
|
43
|
+
secondary: "tertiary",
|
|
44
|
+
tertiary: "secondary",
|
|
45
|
+
} as const satisfies Record<SurfacePlane, SurfacePlane>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Styling for a surface.
|
|
49
|
+
*
|
|
50
|
+
* The root is card-shaped — `rounded-lg`, the corner `--radius` names, see the
|
|
51
|
+
* corner-scale note in the package AGENTS.md — with `border-continuous`, which
|
|
52
|
+
* gives iOS's continuous corner rather than a circular arc. Android ignores it.
|
|
53
|
+
*
|
|
54
|
+
* Every variant reserves the same one-point `border`, transparent unless the
|
|
55
|
+
* variant colours it, so changing variant never moves the content. Only
|
|
56
|
+
* `default` colours it: `--card` sits a percent above `--background` in light,
|
|
57
|
+
* and without the hairline a card on a page is white on near-white. The filled
|
|
58
|
+
* rungs below it are told apart by their fill.
|
|
59
|
+
*
|
|
60
|
+
* `padding: "none"` clips, and nothing else does. It is the one step whose
|
|
61
|
+
* content touches the edge — an image or a chart bled to the corners — and that
|
|
62
|
+
* content has to take the corner with it. Clipping everywhere would crop
|
|
63
|
+
* anything a child draws past its own box.
|
|
64
|
+
*
|
|
65
|
+
* No shadow, on purpose. Nothing in this package casts one — React Native's
|
|
66
|
+
* shadow props disagree between platforms — so depth here is carried by fill.
|
|
67
|
+
*
|
|
68
|
+
* The root carries no `text-*`: a React Native `View` does not cascade colour
|
|
69
|
+
* to a `Text` descendant, so a class here would reach nothing.
|
|
70
|
+
*
|
|
71
|
+
* Free of React Native imports so it stays unit-testable — `bun test` cannot
|
|
72
|
+
* parse React Native's Flow-typed source. See AGENTS.md.
|
|
73
|
+
*/
|
|
74
|
+
export const surfaceVariants = tv({
|
|
75
|
+
slots: {
|
|
76
|
+
root: "rounded-lg border border-continuous border-transparent",
|
|
77
|
+
},
|
|
78
|
+
variants: {
|
|
79
|
+
variant: {
|
|
80
|
+
default: { root: "border-border bg-card" },
|
|
81
|
+
secondary: { root: "bg-secondary" },
|
|
82
|
+
tertiary: { root: "bg-tertiary" },
|
|
83
|
+
transparent: { root: "bg-transparent" },
|
|
84
|
+
},
|
|
85
|
+
padding: {
|
|
86
|
+
none: { root: "overflow-hidden p-0" },
|
|
87
|
+
sm: { root: "p-3" },
|
|
88
|
+
md: { root: "p-4" },
|
|
89
|
+
lg: { root: "p-6" },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
defaultVariants: {
|
|
93
|
+
variant: "default",
|
|
94
|
+
padding: "md",
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export type SurfaceVariantProps = VariantProps<typeof surfaceVariants>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The fill a surface draws.
|
|
102
|
+
*
|
|
103
|
+
* An explicit `variant` always wins. Without one, a surface at the top of the
|
|
104
|
+
* tree is the `default` card, and a surface nested in another steps to the next
|
|
105
|
+
* fill, so a panel inside a card is told apart from the card with nothing said
|
|
106
|
+
* at the call site — and a `Card` built on this gets the same for free.
|
|
107
|
+
*/
|
|
108
|
+
export function resolveSurfaceVariant({
|
|
109
|
+
parentPlane,
|
|
110
|
+
variant,
|
|
111
|
+
}: {
|
|
112
|
+
parentPlane: SurfacePlane | null;
|
|
113
|
+
variant?: SurfaceVariant;
|
|
114
|
+
}): SurfaceVariant {
|
|
115
|
+
if (variant) {
|
|
116
|
+
return variant;
|
|
117
|
+
}
|
|
118
|
+
return parentPlane ? NEXT_PLANE[parentPlane] : "default";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The plane a surface's children sit on: its own fill, or — for a transparent
|
|
123
|
+
* surface, which paints none — the plane it sits on itself.
|
|
124
|
+
*/
|
|
125
|
+
export function resolveSurfacePlane({
|
|
126
|
+
parentPlane,
|
|
127
|
+
variant,
|
|
128
|
+
}: {
|
|
129
|
+
parentPlane: SurfacePlane | null;
|
|
130
|
+
variant: SurfaceVariant;
|
|
131
|
+
}): SurfacePlane | null {
|
|
132
|
+
return variant === "transparent" ? parentPlane : variant;
|
|
133
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Swipe
|
|
2
|
+
|
|
3
|
+
A row that slides aside to reveal actions behind it — delete, archive, snooze, mark done. Compound
|
|
4
|
+
root plus `Swipe.Start`, `Swipe.End`, `Swipe.Action` and `Swipe.Group`, with `useSwipe()` and
|
|
5
|
+
`useSwipeGroup()`. It acts on one row; for many rows at once, a selection mode is the pattern.
|
|
6
|
+
|
|
7
|
+
`import { Swipe } from "@delacour/react-native-ui/swipe";`
|
|
8
|
+
|
|
9
|
+
## Files
|
|
10
|
+
|
|
11
|
+
| File | What it holds |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `index.ts` | → `@delacour/react-native-ui/swipe` |
|
|
14
|
+
| `swipe.tsx` | Root — the partition, the pan, the release, the imperative handle and the accessibility actions |
|
|
15
|
+
| `swipe-start.tsx` | `Swipe.Start`, a marker that renders nothing |
|
|
16
|
+
| `swipe-end.tsx` | `Swipe.End`, a marker that renders nothing |
|
|
17
|
+
| `swipe-action.tsx` | `Swipe.Action`, one tile, positioned by its own animated style |
|
|
18
|
+
| `swipe-panel.tsx` | The layer behind one edge — internal, one per side that has tiles |
|
|
19
|
+
| `swipe-group.tsx` | `Swipe.Group`, the registry that keeps one row open |
|
|
20
|
+
| `swipe.context.tsx` | Row, tile and group contexts, `useSwipe()`, `useSwipeGroup()` |
|
|
21
|
+
| `swipe.types.ts` | `SwipePanelProps`, shared by the two markers |
|
|
22
|
+
| `swipe.variants.ts` | Pure `tv()` slots + the partition, release and tile-layout resolvers, no RN imports |
|
|
23
|
+
| `swipe.variants.test.ts` | |
|
|
24
|
+
|
|
25
|
+
## Design
|
|
26
|
+
|
|
27
|
+
- **Colours**: `default`, `primary`, `success`, `warning`, `info`, `destructive` — Badge's set. A
|
|
28
|
+
tile is `bg-{color}` with its glyph and label in `{color}-foreground`; `default` is `bg-muted`
|
|
29
|
+
with `text-foreground`, since there is no `muted` surface ink stronger than that.
|
|
30
|
+
- **Tiles are 72pt, all of them.** A panel's width is a count times `SWIPE_TILE_WIDTH`, so nothing
|
|
31
|
+
is measured and the release can be judged on the UI thread the moment the finger lifts. The
|
|
32
|
+
`tileContent` slot spells the same number as `w-18`, and a test pins the pair.
|
|
33
|
+
- **The panel is exactly as wide as the gap the row has left.** Never wider, so it cannot paint
|
|
34
|
+
over a row that has no background of its own; never narrower, so no hole opens. The row slot
|
|
35
|
+
sets no `bg-*` and a test holds it to that — the two never overlap, so the row needs none.
|
|
36
|
+
- **Past the tiles' total the outermost tile grows, and the panel wears its colour.** An overshoot
|
|
37
|
+
then reads as *more of that action* rather than as an empty strip. The outermost is the last
|
|
38
|
+
tile on `end` and the first on `start` — `resolveOutermostIndex`.
|
|
39
|
+
- **Tiles emerge from under the row.** Short of fully open each is overlapped by the one outside
|
|
40
|
+
it, spread in proportion to the gap; they are whole and edge to edge exactly when the gap
|
|
41
|
+
reaches their total. `resolveTileLayout` is the rule, and a test sweeps it to prove the tiles
|
|
42
|
+
always cover the gap and never reach past it once open.
|
|
43
|
+
- **A tile's content box is pinned to its inner edge**, so when the outermost grows its glyph
|
|
44
|
+
stays beside its neighbours rather than drifting to the middle of a widening slab.
|
|
45
|
+
|
|
46
|
+
## Anatomy
|
|
47
|
+
|
|
48
|
+
- **`Swipe.Start` and `Swipe.End` are markers, lifted out by element type.** Only the root knows
|
|
49
|
+
the gap, so only the root can lay the tiles out; a marker that rendered its own children would
|
|
50
|
+
need the offset passed back down to it anyway. Everything that is not a marker is the row, and
|
|
51
|
+
order does not matter.
|
|
52
|
+
- **The match is on `displayName`, not on function identity.** The package ships raw source, so
|
|
53
|
+
React Compiler or a second module instance can hand the root an element whose `type` is a
|
|
54
|
+
different object standing for the same component — the failure
|
|
55
|
+
[Switch](../switch/AGENTS.md)'s thumb detection records. `partitionSwipeChildren` reads only
|
|
56
|
+
`type.displayName` and `props.children`, which is also what lets the test feed it plain objects.
|
|
57
|
+
- **A tile has to be written directly inside its marker.** `Swipe.Action` reads a tile context the
|
|
58
|
+
root supplies as it lays each tile out, and throws by name if it is rendered anywhere else.
|
|
59
|
+
Wrapping actions in a custom component inside the marker therefore does not work — the wrapper
|
|
60
|
+
would be the tile. This is the trade the lift-out makes.
|
|
61
|
+
- **The icon is a component prop, and that bends the house rule on purpose.** Icons are composed
|
|
62
|
+
everywhere else, but a tile has to size its glyph to `size-icon-md` and tint it to its
|
|
63
|
+
foreground. It takes the same `IconComponent` type `Icon`'s `icon` prop takes and renders
|
|
64
|
+
`<Icon icon={…} />` inside an `IconDefaultsProvider`, so the glyph still flows through the one
|
|
65
|
+
icon path the package has.
|
|
66
|
+
|
|
67
|
+
## Gesture
|
|
68
|
+
|
|
69
|
+
- **`activeOffsetX([-10, 10])` under `failOffsetY([-8, 8])`**, [Tabs](../tabs/AGENTS.md)' pairing.
|
|
70
|
+
The pan waits for clearly sideways intent and fails the moment the finger commits vertically, so
|
|
71
|
+
a list of rows scrolls normally. This is the opposite of [Slider](../slider/AGENTS.md)'s
|
|
72
|
+
`minDistance(0)`: a slider owns every touch on its track, a row owns only the sideways ones.
|
|
73
|
+
- **A side with no panel rubber-bands at 0.2 and returns.** The row still answers the finger, so
|
|
74
|
+
the touch does not feel dead, but it never promises an action that is not there.
|
|
75
|
+
- **The release is judged on a projection**: `offset + velocity × 0.1` against half the panel. A
|
|
76
|
+
projection that crosses the rest position closes rather than flinging the row open on the other
|
|
77
|
+
side.
|
|
78
|
+
- **A full swipe needs the finger itself past the point.** `|offset| > panel + 0.35 × row`, with
|
|
79
|
+
the projection still beyond it. A flick alone never fires the outermost action, because that
|
|
80
|
+
action is routinely Delete; and pulling back from past the point before letting go cancels it.
|
|
81
|
+
This is the one reading of "or a projected equivalent" that cannot delete by accident. The
|
|
82
|
+
haptic ticks once at exactly that crossing and re-arms if the finger goes back, so the person
|
|
83
|
+
letting go knows what will happen.
|
|
84
|
+
- **The fly-off fires the action when it lands, then brings the row back a beat later.** An action
|
|
85
|
+
that removes the row from state unmounts it during that beat, so it never visibly slides back
|
|
86
|
+
in; one that does not (Done, Archive into the same list) sees its row return closed.
|
|
87
|
+
- **Tapping the open row closes it and swallows the tap.** A transparent shield covers the row
|
|
88
|
+
only while a side is open, behind its own `Gesture.Tap()`, so the row's own `Pressable`s never
|
|
89
|
+
see that tap.
|
|
90
|
+
- **The release runs in `onFinalize`, gated on the pan having started.** `onFinalize` also fires
|
|
91
|
+
for a touch that never activated; without the gate a stray tap would re-judge, and stomp on, an
|
|
92
|
+
imperative `open()` still springing.
|
|
93
|
+
- **Nothing re-renders during a drag.** The offset, the grab, the full-swipe latch and the measured
|
|
94
|
+
width are shared values; React state changes only when a release settles a side.
|
|
95
|
+
- **Every worklet body is self-contained.** `resolveSwipeRelease`, `resolveSwipeDrag` and
|
|
96
|
+
`resolveTileLayout` are restated inside the gesture and the animated styles rather than called,
|
|
97
|
+
and the tests pin the pure versions. A module-scope worklet calling a sibling is the crash
|
|
98
|
+
[Slider](../slider/AGENTS.md) documents.
|
|
99
|
+
|
|
100
|
+
## Group
|
|
101
|
+
|
|
102
|
+
- **Rows register through context, never by the group walking its children.** A row nested in a
|
|
103
|
+
`ListGroup`, a `.map()` or a custom component still belongs. The registry is a ref, so joining
|
|
104
|
+
or leaving it re-renders nothing.
|
|
105
|
+
- **The group closes only rows that are open.** A close aimed at a shut row would restart its
|
|
106
|
+
spring and cancel a drag already under way on it.
|
|
107
|
+
- **`useSwipeGroup()` outside a group is a no-op, not a throw**, so a screen can call
|
|
108
|
+
`closeAll()` whether or not its rows are grouped.
|
|
109
|
+
|
|
110
|
+
## Motion
|
|
111
|
+
|
|
112
|
+
- **Under reduced motion every spring becomes a 160ms timing, and the fly-off becomes a fade.**
|
|
113
|
+
The row still follows the finger — direct manipulation is not decorative — and the fade keeps
|
|
114
|
+
the outcome visible rather than cutting.
|
|
115
|
+
|
|
116
|
+
## RTL
|
|
117
|
+
|
|
118
|
+
- **`I18nManager.isRTL` flips one sign — the row's translation and the pan's reading of it.** The
|
|
119
|
+
offset is held in logical points (positive reveals `start`), and the panels and tiles are pinned
|
|
120
|
+
with Yoga's logical `start` / `end`, which mirror on their own. `end` is always the edge text
|
|
121
|
+
runs toward.
|
|
122
|
+
|
|
123
|
+
## Accessibility
|
|
124
|
+
|
|
125
|
+
- **Every action is published on the row as an accessibility action**, named and labelled by its
|
|
126
|
+
`label`, and dispatched by name. A swipe is invisible to a screen reader, so without this the
|
|
127
|
+
actions would not exist for one.
|
|
128
|
+
- **The row is a single accessibility element** (`accessible`), the shape a list row with custom
|
|
129
|
+
actions takes; `accessibilityLabel` and `accessibilityHint` passed to `Swipe` land on it. A row
|
|
130
|
+
that needs its own separately focusable controls is not a swipe row.
|
|
131
|
+
- **Tiles are `button`s, hidden while the row is shut** (`accessibilityElementsHidden` and
|
|
132
|
+
`importantForAccessibility="no-hide-descendants"` on each panel), so a screen reader does not
|
|
133
|
+
walk into actions nobody can see.
|
|
134
|
+
|
|
135
|
+
## Out of scope
|
|
136
|
+
|
|
137
|
+
- A controlled `openSide` prop. The imperative handle and `onOpenChange` cover the cases that came
|
|
138
|
+
up; a controlled pair can follow through `useControllableState` if one appears.
|
|
139
|
+
- Wrapped tiles (see Anatomy), per-tile widths, and vertical swipes.
|
|
140
|
+
- Undo. A demo keeps local state; a toast belongs to the overlays.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export { Swipe, type SwipeHandle, type SwipeProps } from "./swipe";
|
|
2
|
+
export {
|
|
3
|
+
type SwipeContextValue,
|
|
4
|
+
type SwipeGroupContextValue,
|
|
5
|
+
useSwipe,
|
|
6
|
+
useSwipeContext,
|
|
7
|
+
useSwipeGroup,
|
|
8
|
+
} from "./swipe.context";
|
|
9
|
+
export type { SwipePanelProps } from "./swipe.types";
|
|
10
|
+
export {
|
|
11
|
+
partitionSwipeChildren,
|
|
12
|
+
resolveOutermostIndex,
|
|
13
|
+
resolveSwipeActionForegroundToken,
|
|
14
|
+
resolveSwipeDrag,
|
|
15
|
+
resolveSwipeRelease,
|
|
16
|
+
resolveTileLayout,
|
|
17
|
+
SWIPE_ACTION_COLORS,
|
|
18
|
+
SWIPE_ACTION_FOREGROUND_TOKEN,
|
|
19
|
+
SWIPE_SIDES,
|
|
20
|
+
SWIPE_TILE_WIDTH,
|
|
21
|
+
type SwipeActionColor,
|
|
22
|
+
type SwipeChildren,
|
|
23
|
+
type SwipeOpenSide,
|
|
24
|
+
type SwipeRelease,
|
|
25
|
+
type SwipeSide,
|
|
26
|
+
type SwipeVariantProps,
|
|
27
|
+
swipeVariants,
|
|
28
|
+
toPhysicalOffset,
|
|
29
|
+
} from "./swipe.variants";
|
|
30
|
+
export type { SwipeActionProps } from "./swipe-action";
|
|
31
|
+
export type { SwipeGroupProps } from "./swipe-group";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { type ReactElement, useCallback, useMemo } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import Animated, { useAnimatedStyle } from "react-native-reanimated";
|
|
4
|
+
import { Icon, type IconComponent } from "../icon/icon";
|
|
5
|
+
import { IconDefaultsProvider } from "../icon/icon.context";
|
|
6
|
+
import { Pressable } from "../pressable/pressable";
|
|
7
|
+
import { Text } from "../text/text";
|
|
8
|
+
import { useSwipePart, useSwipeTilePart } from "./swipe.context";
|
|
9
|
+
import {
|
|
10
|
+
resolveSwipeActionForegroundToken,
|
|
11
|
+
SWIPE_TILE_WIDTH,
|
|
12
|
+
type SwipeActionColor,
|
|
13
|
+
swipeVariants,
|
|
14
|
+
} from "./swipe.variants";
|
|
15
|
+
|
|
16
|
+
export type SwipeActionProps = {
|
|
17
|
+
/** The tile's caption, and the name a screen reader offers the action under. Required. */
|
|
18
|
+
label: string;
|
|
19
|
+
/** A Central Icon component, drawn above the label at the tile's size and ink. */
|
|
20
|
+
icon?: IconComponent;
|
|
21
|
+
color?: SwipeActionColor;
|
|
22
|
+
onPress: () => void;
|
|
23
|
+
/** Leave the row open after the action runs — for a toggle such as a bookmark. */
|
|
24
|
+
isKeptOpen?: boolean;
|
|
25
|
+
className?: string;
|
|
26
|
+
labelClassName?: string;
|
|
27
|
+
/** Forwarded to the tile's pressable, for an end-to-end test or a capture flow to find it. */
|
|
28
|
+
testID?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One tile behind the row.
|
|
33
|
+
*
|
|
34
|
+
* Positioned by its own animated style off the row's offset: short of fully
|
|
35
|
+
* open it is overlapped by the tile outside it, so the set emerges from under
|
|
36
|
+
* the row; past fully open the outermost tile grows into the overshoot. The
|
|
37
|
+
* worklet restates `resolveTileLayout`, which the tests pin.
|
|
38
|
+
*
|
|
39
|
+
* Takes the icon as a **component** rather than composed children, because the
|
|
40
|
+
* tile has to size and tint the glyph itself; it renders it through
|
|
41
|
+
* `IconDefaultsProvider`, the same inheritance a `Button` uses.
|
|
42
|
+
*/
|
|
43
|
+
export function SwipeAction({
|
|
44
|
+
label,
|
|
45
|
+
icon,
|
|
46
|
+
color = "default",
|
|
47
|
+
onPress,
|
|
48
|
+
isKeptOpen = false,
|
|
49
|
+
className,
|
|
50
|
+
labelClassName,
|
|
51
|
+
testID,
|
|
52
|
+
}: SwipeActionProps): ReactElement {
|
|
53
|
+
const swipe = useSwipePart("Swipe.Action");
|
|
54
|
+
const { side, index, count } = useSwipeTilePart("Swipe.Action");
|
|
55
|
+
const { offset, runAction } = swipe;
|
|
56
|
+
const slots = swipeVariants({ color, side });
|
|
57
|
+
const order = side === "end" ? index : count - 1 - index;
|
|
58
|
+
|
|
59
|
+
const tileStyle = useAnimatedStyle(() => {
|
|
60
|
+
const tileWidth = SWIPE_TILE_WIDTH;
|
|
61
|
+
const reveal = Math.max(0, side === "start" ? offset.value : -offset.value);
|
|
62
|
+
const total = count * tileWidth;
|
|
63
|
+
const isPacked = reveal <= total;
|
|
64
|
+
const x = isPacked ? (count > 0 ? (order * reveal) / count : 0) : order * tileWidth;
|
|
65
|
+
const width = !isPacked && order === count - 1 ? reveal - order * tileWidth : tileWidth;
|
|
66
|
+
return side === "end" ? { start: x, width } : { end: x, width };
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const handlePress = useCallback(() => runAction(onPress, isKeptOpen), [isKeptOpen, onPress, runAction]);
|
|
70
|
+
const iconDefaults = useMemo(
|
|
71
|
+
() => ({ className: "size-icon-md", color: resolveSwipeActionForegroundToken(color) }),
|
|
72
|
+
[color]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Animated.View className={slots.tile({ className })} style={[{ zIndex: order }, tileStyle]}>
|
|
77
|
+
<Pressable
|
|
78
|
+
accessibilityLabel={label}
|
|
79
|
+
accessibilityRole="button"
|
|
80
|
+
className={slots.tilePressable()}
|
|
81
|
+
feedback="fade"
|
|
82
|
+
onPress={handlePress}
|
|
83
|
+
testID={testID}
|
|
84
|
+
>
|
|
85
|
+
<View className={slots.tileContent()}>
|
|
86
|
+
{icon ? (
|
|
87
|
+
<IconDefaultsProvider value={iconDefaults}>
|
|
88
|
+
<Icon icon={icon} />
|
|
89
|
+
</IconDefaultsProvider>
|
|
90
|
+
) : null}
|
|
91
|
+
<Text className={slots.tileLabel({ className: labelClassName })} numberOfLines={1}>
|
|
92
|
+
{label}
|
|
93
|
+
</Text>
|
|
94
|
+
</View>
|
|
95
|
+
</Pressable>
|
|
96
|
+
</Animated.View>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
SwipeAction.displayName = "DelacourUI.Swipe.Action";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type { SwipePanelProps } from "./swipe.types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Marks the tiles behind the row's end edge — the edge text runs toward —
|
|
6
|
+
* revealed by sliding the row toward `start`.
|
|
7
|
+
*
|
|
8
|
+
* A marker, never rendered: the root lifts its children out by element type and
|
|
9
|
+
* lays them out itself. On its own it renders nothing.
|
|
10
|
+
*/
|
|
11
|
+
export function SwipeEnd(_props: SwipePanelProps): ReactElement | null {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
SwipeEnd.displayName = "DelacourUI.Swipe.End";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type ReactElement, useCallback, useMemo, useRef } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { type SwipeGroupContextValue, SwipeGroupProvider } from "./swipe.context";
|
|
5
|
+
|
|
6
|
+
export type SwipeGroupProps = ViewProps & {
|
|
7
|
+
className?: string;
|
|
8
|
+
/** One row open at a time: opening one closes the rest. Default `true`. */
|
|
9
|
+
isExclusive?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Coordinates the rows beneath it. Renders a plain `View`.
|
|
14
|
+
*
|
|
15
|
+
* **Rows register themselves through context**, rather than the group walking
|
|
16
|
+
* its children, so a row nested in a `ListGroup`, a `.map()` or a custom
|
|
17
|
+
* component still belongs. The registry is a ref — joining or leaving it
|
|
18
|
+
* re-renders nothing.
|
|
19
|
+
*/
|
|
20
|
+
export function SwipeGroup({ className, isExclusive = true, children, ...props }: SwipeGroupProps): ReactElement {
|
|
21
|
+
const rows = useRef(new Map<string, () => void>());
|
|
22
|
+
|
|
23
|
+
const register = useCallback((id: string, close: () => void) => {
|
|
24
|
+
rows.current.set(id, close);
|
|
25
|
+
return () => {
|
|
26
|
+
if (rows.current.get(id) === close) rows.current.delete(id);
|
|
27
|
+
};
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const notifyOpen = useCallback(
|
|
31
|
+
(id: string) => {
|
|
32
|
+
if (!isExclusive) return;
|
|
33
|
+
for (const [other, close] of rows.current) if (other !== id) close();
|
|
34
|
+
},
|
|
35
|
+
[isExclusive]
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const closeAll = useCallback(() => {
|
|
39
|
+
for (const close of rows.current.values()) close();
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const value = useMemo<SwipeGroupContextValue>(
|
|
43
|
+
() => ({ closeAll, notifyOpen, register }),
|
|
44
|
+
[closeAll, notifyOpen, register]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<SwipeGroupProvider value={value}>
|
|
49
|
+
<View className={cn(className)} {...props}>
|
|
50
|
+
{children}
|
|
51
|
+
</View>
|
|
52
|
+
</SwipeGroupProvider>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
SwipeGroup.displayName = "DelacourUI.Swipe.Group";
|