@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,337 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ReactElement, ReactNode } from "react";
|
|
3
|
+
import { declaredTokens } from "../../styles/theme-tokens.test";
|
|
4
|
+
import {
|
|
5
|
+
partitionSwipeChildren,
|
|
6
|
+
resolveOutermostIndex,
|
|
7
|
+
resolveSwipeActionForegroundToken,
|
|
8
|
+
resolveSwipeDrag,
|
|
9
|
+
resolveSwipeRelease,
|
|
10
|
+
resolveTileLayout,
|
|
11
|
+
SWIPE_ACTION_COLORS,
|
|
12
|
+
SWIPE_END_DISPLAY_NAME,
|
|
13
|
+
SWIPE_FULL_FRACTION,
|
|
14
|
+
SWIPE_PROJECTION,
|
|
15
|
+
SWIPE_RUBBER_BAND,
|
|
16
|
+
SWIPE_START_DISPLAY_NAME,
|
|
17
|
+
SWIPE_TILE_WIDTH,
|
|
18
|
+
type SwipeTileProps,
|
|
19
|
+
swipeVariants,
|
|
20
|
+
toPhysicalOffset,
|
|
21
|
+
} from "./swipe.variants";
|
|
22
|
+
|
|
23
|
+
const LIGHT = declaredTokens("light");
|
|
24
|
+
const DARK = declaredTokens("dark");
|
|
25
|
+
|
|
26
|
+
/** A plain object shaped like a React element — the partition reads nothing else. */
|
|
27
|
+
function element(displayName: string, children?: ReactNode, key?: string): ReactElement<SwipeTileProps> {
|
|
28
|
+
return { key: key ?? null, props: { children }, type: { displayName } } as unknown as ReactElement<SwipeTileProps>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const START = (children?: ReactNode) => element(SWIPE_START_DISPLAY_NAME, children);
|
|
32
|
+
const END = (children?: ReactNode) => element(SWIPE_END_DISPLAY_NAME, children);
|
|
33
|
+
const ACTION = (key: string) => element("DelacourUI.Swipe.Action", undefined, key);
|
|
34
|
+
const ROW = (key: string) => element("DelacourUI.Item", undefined, key);
|
|
35
|
+
|
|
36
|
+
describe("partitionSwipeChildren", () => {
|
|
37
|
+
test("lifts each marker's children out by element type, and keeps the rest as the row", () => {
|
|
38
|
+
const done = ACTION("done");
|
|
39
|
+
const snooze = ACTION("snooze");
|
|
40
|
+
const remove = ACTION("delete");
|
|
41
|
+
const row = ROW("row");
|
|
42
|
+
|
|
43
|
+
const parts = partitionSwipeChildren([START(done), END([snooze, remove]), row]);
|
|
44
|
+
|
|
45
|
+
expect(parts.start).toEqual([done]);
|
|
46
|
+
expect(parts.end).toEqual([snooze, remove]);
|
|
47
|
+
expect(parts.row).toEqual([row]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("order does not matter", () => {
|
|
51
|
+
const row = ROW("row");
|
|
52
|
+
const remove = ACTION("delete");
|
|
53
|
+
const parts = partitionSwipeChildren([row, END(remove)]);
|
|
54
|
+
|
|
55
|
+
expect(parts.start).toEqual([]);
|
|
56
|
+
expect(parts.end).toEqual([remove]);
|
|
57
|
+
expect(parts.row).toEqual([row]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("flattens nested arrays, and drops holes inside a panel", () => {
|
|
61
|
+
const a = ACTION("a");
|
|
62
|
+
const b = ACTION("b");
|
|
63
|
+
const parts = partitionSwipeChildren([[END([null, [a, false], undefined, b])]]);
|
|
64
|
+
|
|
65
|
+
expect(parts.end).toEqual([a, b]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("keeps plain text and numbers in the row, and drops holes there too", () => {
|
|
69
|
+
const parts = partitionSwipeChildren(["Label", 3, null, false, undefined]);
|
|
70
|
+
expect(parts.row).toEqual(["Label", 3]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("two markers for one side concatenate in source order", () => {
|
|
74
|
+
const a = ACTION("a");
|
|
75
|
+
const b = ACTION("b");
|
|
76
|
+
expect(partitionSwipeChildren([END(a), END(b)]).end).toEqual([a, b]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("a lone child that is not an array still partitions", () => {
|
|
80
|
+
const row = ROW("row");
|
|
81
|
+
expect(partitionSwipeChildren(row).row).toEqual([row]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("resolveOutermostIndex", () => {
|
|
86
|
+
test("end: the last tile", () => {
|
|
87
|
+
expect(resolveOutermostIndex("end", 3)).toBe(2);
|
|
88
|
+
expect(resolveOutermostIndex("end", 1)).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("start: the first tile", () => {
|
|
92
|
+
expect(resolveOutermostIndex("start", 3)).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("an empty side has no outermost tile", () => {
|
|
96
|
+
expect(resolveOutermostIndex("end", 0)).toBe(-1);
|
|
97
|
+
expect(resolveOutermostIndex("start", 0)).toBe(-1);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("resolveSwipeDrag", () => {
|
|
102
|
+
test("passes a drag through on a side with a panel", () => {
|
|
103
|
+
expect(resolveSwipeDrag({ endWidth: 144, raw: -60, startWidth: 72 })).toBe(-60);
|
|
104
|
+
expect(resolveSwipeDrag({ endWidth: 144, raw: 60, startWidth: 72 })).toBe(60);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("rubber-bands a side with no panel", () => {
|
|
108
|
+
expect(resolveSwipeDrag({ endWidth: 144, raw: 100, startWidth: 0 })).toBeCloseTo(100 * SWIPE_RUBBER_BAND);
|
|
109
|
+
expect(resolveSwipeDrag({ endWidth: 0, raw: -100, startWidth: 72 })).toBeCloseTo(-100 * SWIPE_RUBBER_BAND);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("the resistance is the spec's 0.2", () => {
|
|
113
|
+
expect(SWIPE_RUBBER_BAND).toBe(0.2);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("resolveSwipeRelease", () => {
|
|
118
|
+
const base = { endWidth: 144, isFullSwipe: true, rowWidth: 400, startWidth: 72 };
|
|
119
|
+
const fullAt = base.endWidth + SWIPE_FULL_FRACTION * base.rowWidth;
|
|
120
|
+
|
|
121
|
+
test("a release short of half the panel closes", () => {
|
|
122
|
+
expect(resolveSwipeRelease({ ...base, offset: -60, velocity: 0 })).toEqual({ kind: "close" });
|
|
123
|
+
expect(resolveSwipeRelease({ ...base, offset: 30, velocity: 0 })).toEqual({ kind: "close" });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a release past half the panel opens that side", () => {
|
|
127
|
+
expect(resolveSwipeRelease({ ...base, offset: -80, velocity: 0 })).toEqual({ kind: "open", side: "end" });
|
|
128
|
+
expect(resolveSwipeRelease({ ...base, offset: 40, velocity: 0 })).toEqual({ kind: "open", side: "start" });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("projects the release by velocity", () => {
|
|
132
|
+
expect(SWIPE_PROJECTION).toBe(0.1);
|
|
133
|
+
// -40 + -500 * 0.1 = -90, past half of 144.
|
|
134
|
+
expect(resolveSwipeRelease({ ...base, offset: -40, velocity: -500 })).toEqual({ kind: "open", side: "end" });
|
|
135
|
+
// -100 + 500 * 0.1 = -50, short of half.
|
|
136
|
+
expect(resolveSwipeRelease({ ...base, offset: -100, velocity: 500 })).toEqual({ kind: "close" });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("a flick across the rest position closes rather than opening the other side", () => {
|
|
140
|
+
expect(resolveSwipeRelease({ ...base, offset: -20, velocity: 2000 })).toEqual({ kind: "close" });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("a side with no panel never opens", () => {
|
|
144
|
+
expect(resolveSwipeRelease({ ...base, offset: 50, startWidth: 0, velocity: 800 })).toEqual({ kind: "close" });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("past the full-swipe point, a release fires the outermost action", () => {
|
|
148
|
+
expect(resolveSwipeRelease({ ...base, offset: -(fullAt + 1), velocity: 0 })).toEqual({
|
|
149
|
+
kind: "full",
|
|
150
|
+
side: "end",
|
|
151
|
+
});
|
|
152
|
+
const startFullAt = base.startWidth + SWIPE_FULL_FRACTION * base.rowWidth;
|
|
153
|
+
expect(resolveSwipeRelease({ ...base, offset: startFullAt + 1, velocity: 0 })).toEqual({
|
|
154
|
+
kind: "full",
|
|
155
|
+
side: "start",
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("a flick alone never reaches a full swipe — the finger has to cross the point", () => {
|
|
160
|
+
expect(resolveSwipeRelease({ ...base, offset: -(fullAt - 40), velocity: -3000 })).toEqual({
|
|
161
|
+
kind: "open",
|
|
162
|
+
side: "end",
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("pulling back from past the point before letting go does not fire", () => {
|
|
167
|
+
expect(resolveSwipeRelease({ ...base, offset: -(fullAt + 10), velocity: 1500 })).toEqual({
|
|
168
|
+
kind: "open",
|
|
169
|
+
side: "end",
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("isFullSwipe false opens instead", () => {
|
|
174
|
+
expect(resolveSwipeRelease({ ...base, isFullSwipe: false, offset: -(fullAt + 50), velocity: 0 })).toEqual({
|
|
175
|
+
kind: "open",
|
|
176
|
+
side: "end",
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("an unmeasured row never full-swipes", () => {
|
|
181
|
+
expect(resolveSwipeRelease({ ...base, offset: -400, rowWidth: 0, velocity: 0 })).toEqual({
|
|
182
|
+
kind: "open",
|
|
183
|
+
side: "end",
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("resolveTileLayout", () => {
|
|
189
|
+
const tileWidth = SWIPE_TILE_WIDTH;
|
|
190
|
+
|
|
191
|
+
test("closed: every tile packs against the row edge", () => {
|
|
192
|
+
for (const index of [0, 1, 2]) {
|
|
193
|
+
expect(resolveTileLayout({ count: 3, index, offset: 0, side: "end", tileWidth })).toEqual({
|
|
194
|
+
width: tileWidth,
|
|
195
|
+
x: 0,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("part open: tiles spread in proportion, innermost against the row", () => {
|
|
201
|
+
const offset = -108;
|
|
202
|
+
expect(resolveTileLayout({ count: 3, index: 0, offset, side: "end", tileWidth }).x).toBe(0);
|
|
203
|
+
expect(resolveTileLayout({ count: 3, index: 1, offset, side: "end", tileWidth }).x).toBe(36);
|
|
204
|
+
expect(resolveTileLayout({ count: 3, index: 2, offset, side: "end", tileWidth }).x).toBe(72);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("fully open: tiles are whole and edge to edge", () => {
|
|
208
|
+
const offset = -3 * tileWidth;
|
|
209
|
+
for (const index of [0, 1, 2]) {
|
|
210
|
+
expect(resolveTileLayout({ count: 3, index, offset, side: "end", tileWidth })).toEqual({
|
|
211
|
+
width: tileWidth,
|
|
212
|
+
x: index * tileWidth,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("on start the order runs the other way: the first tile is outermost", () => {
|
|
218
|
+
const offset = 2 * tileWidth;
|
|
219
|
+
expect(resolveTileLayout({ count: 2, index: 1, offset, side: "start", tileWidth }).x).toBe(0);
|
|
220
|
+
expect(resolveTileLayout({ count: 2, index: 0, offset, side: "start", tileWidth }).x).toBe(tileWidth);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("overshoot: the outermost tile grows to fill it, the others hold", () => {
|
|
224
|
+
const offset = -(2 * tileWidth + 50);
|
|
225
|
+
expect(resolveTileLayout({ count: 2, index: 0, offset, side: "end", tileWidth })).toEqual({
|
|
226
|
+
width: tileWidth,
|
|
227
|
+
x: 0,
|
|
228
|
+
});
|
|
229
|
+
expect(resolveTileLayout({ count: 2, index: 1, offset, side: "end", tileWidth })).toEqual({
|
|
230
|
+
width: tileWidth + 50,
|
|
231
|
+
x: tileWidth,
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("the tiles always cover the gap exactly — no hole, nothing past the panel", () => {
|
|
236
|
+
for (const reveal of [0, 10, 72, 100, 144, 216, 300, 500]) {
|
|
237
|
+
const count = 3;
|
|
238
|
+
let reach = 0;
|
|
239
|
+
for (let index = 0; index < count; index++) {
|
|
240
|
+
const { x, width } = resolveTileLayout({ count, index, offset: -reveal, side: "end", tileWidth });
|
|
241
|
+
reach = Math.max(reach, x + width);
|
|
242
|
+
}
|
|
243
|
+
expect(reach).toBeGreaterThanOrEqual(reveal);
|
|
244
|
+
if (reveal >= count * tileWidth) expect(reach).toBe(reveal);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("the other side's offset reveals nothing here", () => {
|
|
249
|
+
expect(resolveTileLayout({ count: 2, index: 1, offset: 120, side: "end", tileWidth })).toEqual({
|
|
250
|
+
width: tileWidth,
|
|
251
|
+
x: 0,
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("toPhysicalOffset", () => {
|
|
257
|
+
test("left to right, the logical offset is the translation", () => {
|
|
258
|
+
expect(toPhysicalOffset(40, false)).toBe(40);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("right to left mirrors it, so end stays the edge text runs toward", () => {
|
|
262
|
+
expect(toPhysicalOffset(40, true)).toBe(-40);
|
|
263
|
+
expect(toPhysicalOffset(-40, true)).toBe(40);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("tile width", () => {
|
|
268
|
+
test("is 72pt, and the content box class says the same", () => {
|
|
269
|
+
expect(SWIPE_TILE_WIDTH).toBe(72);
|
|
270
|
+
expect(swipeVariants().tileContent()).toContain("w-18");
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("swipeVariants", () => {
|
|
275
|
+
test("default paints muted with foreground ink", () => {
|
|
276
|
+
const slots = swipeVariants({ color: "default" });
|
|
277
|
+
expect(slots.tile()).toContain("bg-muted");
|
|
278
|
+
expect(slots.tileLabel()).toContain("text-foreground");
|
|
279
|
+
expect(slots.panel()).toContain("bg-muted");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("every other colour paints its own surface and its own foreground", () => {
|
|
283
|
+
for (const color of SWIPE_ACTION_COLORS) {
|
|
284
|
+
if (color === "default") continue;
|
|
285
|
+
const slots = swipeVariants({ color });
|
|
286
|
+
expect(slots.tile()).toContain(`bg-${color}`);
|
|
287
|
+
expect(slots.panel()).toContain(`bg-${color}`);
|
|
288
|
+
expect(slots.tileLabel()).toContain(`text-${color}-foreground`);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("label is text-xs and its colour sits on the label, never the tile", () => {
|
|
293
|
+
for (const color of SWIPE_ACTION_COLORS) {
|
|
294
|
+
const slots = swipeVariants({ color });
|
|
295
|
+
expect(slots.tileLabel()).toContain("text-xs");
|
|
296
|
+
expect(slots.tile()).not.toMatch(/\btext-/);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("the row takes no background of its own", () => {
|
|
301
|
+
expect(swipeVariants().row()).not.toMatch(/\bbg-/);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("the root clips, so a fly-off never paints outside the row", () => {
|
|
305
|
+
expect(swipeVariants().root()).toContain("overflow-hidden");
|
|
306
|
+
expect(swipeVariants().panel()).toContain("overflow-hidden");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("a caller's className wins", () => {
|
|
310
|
+
expect(swipeVariants().root({ className: "rounded-lg" })).toContain("rounded-lg");
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe("tokens", () => {
|
|
315
|
+
test("every surface and foreground named is declared in both themes", () => {
|
|
316
|
+
for (const color of SWIPE_ACTION_COLORS) {
|
|
317
|
+
const slots = swipeVariants({ color });
|
|
318
|
+
const named = [
|
|
319
|
+
...(slots.tile().match(/\bbg-([a-z-]+)/g) ?? []).map((cls) => cls.slice(3)),
|
|
320
|
+
...(slots.tileLabel().match(/\btext-([a-z]+(?:-[a-z]+)*)/g) ?? [])
|
|
321
|
+
.map((cls) => cls.slice(5))
|
|
322
|
+
.filter((token) => token !== "xs" && token !== "center"),
|
|
323
|
+
resolveSwipeActionForegroundToken(color),
|
|
324
|
+
];
|
|
325
|
+
for (const token of named) {
|
|
326
|
+
expect(LIGHT.has(token)).toBe(true);
|
|
327
|
+
expect(DARK.has(token)).toBe(true);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("the glyph's tint token matches the label's foreground class", () => {
|
|
333
|
+
for (const color of SWIPE_ACTION_COLORS) {
|
|
334
|
+
expect(swipeVariants({ color }).tileLabel()).toContain(`text-${resolveSwipeActionForegroundToken(color)}`);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
});
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { VariantProps } from "tailwind-variants";
|
|
3
|
+
import { tv } from "../../lib/tv";
|
|
4
|
+
|
|
5
|
+
/** The two edges a row can slide away from. `end` is the edge text runs toward. */
|
|
6
|
+
export const SWIPE_SIDES = ["start", "end"] as const;
|
|
7
|
+
|
|
8
|
+
/** Badge's, Slider's and Switch's set. `default` is the neutral tile. */
|
|
9
|
+
export const SWIPE_ACTION_COLORS = ["default", "primary", "success", "warning", "info", "destructive"] as const;
|
|
10
|
+
|
|
11
|
+
export type SwipeSide = (typeof SWIPE_SIDES)[number];
|
|
12
|
+
export type SwipeOpenSide = SwipeSide | null;
|
|
13
|
+
export type SwipeActionColor = (typeof SWIPE_ACTION_COLORS)[number];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* One tile's width, in points. Every tile is the same width, so a panel's width
|
|
17
|
+
* is a count times this and nothing has to be measured.
|
|
18
|
+
*
|
|
19
|
+
* The `tileContent` slot writes it out as `w-18` (18 × 4 = 72) and a test pins
|
|
20
|
+
* the two together.
|
|
21
|
+
*/
|
|
22
|
+
export const SWIPE_TILE_WIDTH = 72;
|
|
23
|
+
|
|
24
|
+
/** How much of a drag toward a side with no panel the row follows. */
|
|
25
|
+
export const SWIPE_RUBBER_BAND = 0.2;
|
|
26
|
+
|
|
27
|
+
/** Seconds of release velocity added to the offset before it is judged. */
|
|
28
|
+
export const SWIPE_PROJECTION = 0.1;
|
|
29
|
+
|
|
30
|
+
/** How far past the panel, as a share of the row's width, a full swipe sits. */
|
|
31
|
+
export const SWIPE_FULL_FRACTION = 0.35;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The spring a row settles on after a release or a tap.
|
|
35
|
+
*
|
|
36
|
+
* Damped to settle with the barest overshoot: a row that bounced past its panel
|
|
37
|
+
* would flash the colour behind the outermost tile on every open.
|
|
38
|
+
*/
|
|
39
|
+
export const SWIPE_SPRING = { damping: 26, mass: 0.6, stiffness: 320 } as const;
|
|
40
|
+
|
|
41
|
+
/** What every spring becomes under reduced motion, in milliseconds. */
|
|
42
|
+
export const SWIPE_REDUCED_DURATION = 160;
|
|
43
|
+
|
|
44
|
+
/** How long a full swipe takes to carry the row off, in milliseconds. */
|
|
45
|
+
export const SWIPE_FLY_OFF_DURATION = 200;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The markers' display names. The partition matches on these rather than on
|
|
49
|
+
* function identity — Switch's `isSwitchThumbElement` records why identity is
|
|
50
|
+
* not trusted alone in a package that ships raw source.
|
|
51
|
+
*/
|
|
52
|
+
export const SWIPE_START_DISPLAY_NAME = "DelacourUI.Swipe.Start";
|
|
53
|
+
export const SWIPE_END_DISPLAY_NAME = "DelacourUI.Swipe.End";
|
|
54
|
+
|
|
55
|
+
/** What the root reads off a lifted tile — `Swipe.Action`'s props, as far as layout and dispatch need. */
|
|
56
|
+
export type SwipeTileProps = { color?: SwipeActionColor; label?: string; onPress?: () => void };
|
|
57
|
+
|
|
58
|
+
export type SwipeChildren = {
|
|
59
|
+
/** The tiles behind the start edge, in source order. */
|
|
60
|
+
start: ReactElement<SwipeTileProps>[];
|
|
61
|
+
/** The tiles behind the end edge, in source order. */
|
|
62
|
+
end: ReactElement<SwipeTileProps>[];
|
|
63
|
+
/** Everything that is not a panel — the row itself. */
|
|
64
|
+
row: ReactNode[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type ElementLike = { type: unknown; props: { children?: ReactNode } };
|
|
68
|
+
|
|
69
|
+
function isElementLike(node: unknown): node is ElementLike {
|
|
70
|
+
return typeof node === "object" && node !== null && "type" in node && "props" in node;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function displayNameOf(node: ElementLike): string | undefined {
|
|
74
|
+
const type = node.type as { displayName?: unknown } | null;
|
|
75
|
+
if (typeof type !== "function" && (typeof type !== "object" || type === null)) return undefined;
|
|
76
|
+
return typeof type.displayName === "string" ? type.displayName : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function flatten(node: ReactNode, into: ReactNode[]): void {
|
|
80
|
+
if (node === null || node === undefined || typeof node === "boolean") return;
|
|
81
|
+
if (Array.isArray(node)) {
|
|
82
|
+
for (const child of node) flatten(child, into);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
into.push(node);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Splits a `Swipe`'s children into the two panels and the row.
|
|
90
|
+
*
|
|
91
|
+
* `Swipe.Start` and `Swipe.End` are markers: their children are lifted out and
|
|
92
|
+
* the markers themselves are dropped, because only the root knows the gap the
|
|
93
|
+
* tiles have to fill. Anything else is the row, in order. Order between the
|
|
94
|
+
* markers and the row does not matter.
|
|
95
|
+
*
|
|
96
|
+
* Reads only `type.displayName` and `props.children`, so a test can hand it
|
|
97
|
+
* plain objects.
|
|
98
|
+
*/
|
|
99
|
+
export function partitionSwipeChildren(children: ReactNode): SwipeChildren {
|
|
100
|
+
const result: SwipeChildren = { end: [], row: [], start: [] };
|
|
101
|
+
const items: ReactNode[] = [];
|
|
102
|
+
flatten(children, items);
|
|
103
|
+
|
|
104
|
+
for (const item of items) {
|
|
105
|
+
if (!isElementLike(item)) {
|
|
106
|
+
result.row.push(item);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const name = displayNameOf(item);
|
|
110
|
+
const side = name === SWIPE_START_DISPLAY_NAME ? "start" : name === SWIPE_END_DISPLAY_NAME ? "end" : null;
|
|
111
|
+
if (side === null) {
|
|
112
|
+
result.row.push(item);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const tiles: ReactNode[] = [];
|
|
116
|
+
flatten(item.props.children, tiles);
|
|
117
|
+
for (const tile of tiles) if (isElementLike(tile)) result[side].push(tile as ReactElement<SwipeTileProps>);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The tile that grows into an overshoot and fires on a full swipe: the one farthest from the row. */
|
|
124
|
+
export function resolveOutermostIndex(side: SwipeSide, count: number): number {
|
|
125
|
+
if (count <= 0) return -1;
|
|
126
|
+
return side === "end" ? count - 1 : 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Where the row sits for a raw drag, in logical points (positive reveals `start`).
|
|
131
|
+
*
|
|
132
|
+
* A side with no panel follows the finger at {@link SWIPE_RUBBER_BAND}, so the
|
|
133
|
+
* row still answers the touch without promising an action that is not there.
|
|
134
|
+
* The gesture worklet restates this.
|
|
135
|
+
*/
|
|
136
|
+
export function resolveSwipeDrag({
|
|
137
|
+
raw,
|
|
138
|
+
startWidth,
|
|
139
|
+
endWidth,
|
|
140
|
+
}: {
|
|
141
|
+
raw: number;
|
|
142
|
+
startWidth: number;
|
|
143
|
+
endWidth: number;
|
|
144
|
+
}): number {
|
|
145
|
+
if (raw > 0 && startWidth <= 0) return raw * SWIPE_RUBBER_BAND;
|
|
146
|
+
if (raw < 0 && endWidth <= 0) return raw * SWIPE_RUBBER_BAND;
|
|
147
|
+
return raw;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export type SwipeRelease = { kind: "close" } | { kind: "open"; side: SwipeSide } | { kind: "full"; side: SwipeSide };
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* What a release does.
|
|
154
|
+
*
|
|
155
|
+
* The offset is projected by {@link SWIPE_PROJECTION} seconds of velocity and
|
|
156
|
+
* compared with half the panel on the side it lands. A projection that crosses
|
|
157
|
+
* the rest position closes rather than opening the other side.
|
|
158
|
+
*
|
|
159
|
+
* **A full swipe needs the finger itself past the point**, and the projection
|
|
160
|
+
* still beyond it — a fast flick alone never fires the outermost action,
|
|
161
|
+
* because that action is routinely Delete. The haptic ticks at exactly that
|
|
162
|
+
* crossing, so the person releasing knows what will happen. A row not yet
|
|
163
|
+
* measured never full-swipes.
|
|
164
|
+
*
|
|
165
|
+
* The gesture worklet restates this.
|
|
166
|
+
*/
|
|
167
|
+
export function resolveSwipeRelease({
|
|
168
|
+
offset,
|
|
169
|
+
velocity,
|
|
170
|
+
startWidth,
|
|
171
|
+
endWidth,
|
|
172
|
+
rowWidth,
|
|
173
|
+
isFullSwipe,
|
|
174
|
+
}: {
|
|
175
|
+
offset: number;
|
|
176
|
+
velocity: number;
|
|
177
|
+
startWidth: number;
|
|
178
|
+
endWidth: number;
|
|
179
|
+
rowWidth: number;
|
|
180
|
+
isFullSwipe: boolean;
|
|
181
|
+
}): SwipeRelease {
|
|
182
|
+
const projected = offset + velocity * SWIPE_PROJECTION;
|
|
183
|
+
if (projected === 0) return { kind: "close" };
|
|
184
|
+
if (offset !== 0 && Math.sign(projected) !== Math.sign(offset)) return { kind: "close" };
|
|
185
|
+
|
|
186
|
+
const side: SwipeSide = projected > 0 ? "start" : "end";
|
|
187
|
+
const width = side === "start" ? startWidth : endWidth;
|
|
188
|
+
if (width <= 0) return { kind: "close" };
|
|
189
|
+
|
|
190
|
+
const reach = Math.abs(offset);
|
|
191
|
+
const projectedReach = Math.abs(projected);
|
|
192
|
+
const fullAt = width + SWIPE_FULL_FRACTION * rowWidth;
|
|
193
|
+
if (isFullSwipe && rowWidth > 0 && reach > fullAt && projectedReach > fullAt) return { kind: "full", side };
|
|
194
|
+
|
|
195
|
+
return projectedReach > width / 2 ? { kind: "open", side } : { kind: "close" };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Where one tile sits inside its panel, measured outward from the row's edge.
|
|
200
|
+
*
|
|
201
|
+
* Short of fully open the tiles spread in proportion to the gap, the innermost
|
|
202
|
+
* pinned to the row and each overlapped by the one outside it, so they emerge
|
|
203
|
+
* from under the row and are whole and edge to edge the moment the gap reaches
|
|
204
|
+
* their total. Past that the others hold and the outermost grows into the
|
|
205
|
+
* overshoot, so the gap is always covered exactly and never by a hole.
|
|
206
|
+
*
|
|
207
|
+
* `index` is source order; on `start` the first tile is the outermost. The tile
|
|
208
|
+
* worklet restates this.
|
|
209
|
+
*/
|
|
210
|
+
export function resolveTileLayout({
|
|
211
|
+
side,
|
|
212
|
+
index,
|
|
213
|
+
count,
|
|
214
|
+
tileWidth,
|
|
215
|
+
offset,
|
|
216
|
+
}: {
|
|
217
|
+
side: SwipeSide;
|
|
218
|
+
index: number;
|
|
219
|
+
count: number;
|
|
220
|
+
tileWidth: number;
|
|
221
|
+
offset: number;
|
|
222
|
+
}): { x: number; width: number } {
|
|
223
|
+
const reveal = Math.max(0, side === "start" ? offset : -offset);
|
|
224
|
+
const order = side === "end" ? index : count - 1 - index;
|
|
225
|
+
const total = count * tileWidth;
|
|
226
|
+
|
|
227
|
+
if (reveal <= total) return { width: tileWidth, x: count > 0 ? (order * reveal) / count : 0 };
|
|
228
|
+
|
|
229
|
+
const isOutermost = order === count - 1;
|
|
230
|
+
return { width: isOutermost ? reveal - order * tileWidth : tileWidth, x: order * tileWidth };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The physical translation for a logical offset. Right to left, `end` is the
|
|
235
|
+
* left edge, so the sign flips. The worklets restate this.
|
|
236
|
+
*/
|
|
237
|
+
export function toPhysicalOffset(offset: number, isRTL: boolean): number {
|
|
238
|
+
return isRTL ? -offset : offset;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** The theme token a tile's glyph and label are drawn in, per colour. */
|
|
242
|
+
export const SWIPE_ACTION_FOREGROUND_TOKEN: Record<SwipeActionColor, string> = {
|
|
243
|
+
default: "foreground",
|
|
244
|
+
primary: "primary-foreground",
|
|
245
|
+
success: "success-foreground",
|
|
246
|
+
warning: "warning-foreground",
|
|
247
|
+
info: "info-foreground",
|
|
248
|
+
destructive: "destructive-foreground",
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export function resolveSwipeActionForegroundToken(color: SwipeActionColor): string {
|
|
252
|
+
return SWIPE_ACTION_FOREGROUND_TOKEN[color];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Every styled part of a swipe row, in one slotted `tv()`.
|
|
257
|
+
*
|
|
258
|
+
* The `color` axis paints a tile, its label and a panel together — the panel
|
|
259
|
+
* takes the outermost tile's colour, so an overshoot extends that action rather
|
|
260
|
+
* than opening a hole. The row slot sets no background: the row and the panel
|
|
261
|
+
* never overlap, so it needs none.
|
|
262
|
+
*/
|
|
263
|
+
export const swipeVariants = tv({
|
|
264
|
+
slots: {
|
|
265
|
+
root: "relative overflow-hidden",
|
|
266
|
+
row: "w-full",
|
|
267
|
+
panel: "absolute inset-y-0 overflow-hidden",
|
|
268
|
+
tile: "absolute inset-y-0 overflow-hidden",
|
|
269
|
+
tilePressable: "flex-1 flex-row",
|
|
270
|
+
tileContent: "w-18 items-center justify-center gap-1 px-1",
|
|
271
|
+
tileLabel: "text-center font-medium text-xs",
|
|
272
|
+
},
|
|
273
|
+
variants: {
|
|
274
|
+
color: {
|
|
275
|
+
default: { panel: "bg-muted", tile: "bg-muted", tileLabel: "text-foreground" },
|
|
276
|
+
primary: { panel: "bg-primary", tile: "bg-primary", tileLabel: "text-primary-foreground" },
|
|
277
|
+
success: { panel: "bg-success", tile: "bg-success", tileLabel: "text-success-foreground" },
|
|
278
|
+
warning: { panel: "bg-warning", tile: "bg-warning", tileLabel: "text-warning-foreground" },
|
|
279
|
+
info: { panel: "bg-info", tile: "bg-info", tileLabel: "text-info-foreground" },
|
|
280
|
+
destructive: { panel: "bg-destructive", tile: "bg-destructive", tileLabel: "text-destructive-foreground" },
|
|
281
|
+
},
|
|
282
|
+
side: {
|
|
283
|
+
start: { tilePressable: "justify-end" },
|
|
284
|
+
end: { tilePressable: "justify-start" },
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
defaultVariants: { color: "default", side: "end" },
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
export type SwipeVariantProps = VariantProps<typeof swipeVariants>;
|
|
@@ -259,8 +259,8 @@ export const SWITCH_INVALID_CONTENT_TEXT_CLASS = "text-destructive-foreground";
|
|
|
259
259
|
* **The fade lands on `touchArea`, the one slot no animated style touches.**
|
|
260
260
|
* `track` and `thumb` both carry a `useAnimatedStyle`, and while neither writes
|
|
261
261
|
* `opacity` today, a disabled treatment that would break the moment one did is a
|
|
262
|
-
* treatment waiting to fail silently — the failure mode `
|
|
263
|
-
* class on
|
|
262
|
+
* treatment waiting to fail silently — the failure mode `Pressable` once had
|
|
263
|
+
* with a class on its own animated node.
|
|
264
264
|
*
|
|
265
265
|
* **`self-start` is load-bearing.** A switch is sized by its own geometry, and
|
|
266
266
|
* inside a gap column every child is stretch-aligned by default — without it the
|
|
@@ -223,9 +223,10 @@ A row of tabs and the panels they switch between. Compound root plus `Tabs.List`
|
|
|
223
223
|
state.
|
|
224
224
|
- **The disabled fade lands on the label, never on the trigger.** The `trigger`
|
|
225
225
|
slot is worn by [`Pressable`](../pressable/AGENTS.md)'s own `Animated.View`,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
226
|
+
which now multiplies a className's opacity into its press rather than
|
|
227
|
+
overwriting it — but the label already fades, and a fade on the trigger as
|
|
228
|
+
well would compound the two to a quarter. [`Radio`](../radio/AGENTS.md)'s
|
|
229
|
+
arrangement.
|
|
229
230
|
- **Selection changes the label's colour and nothing else.** A weight change
|
|
230
231
|
would re-measure the label, which moves the frame the indicator is sitting on,
|
|
231
232
|
on every tap. A test asserts the two class sets differ by exactly one `text-*`.
|
|
@@ -236,7 +236,7 @@ describe("the indicator slot", () => {
|
|
|
236
236
|
});
|
|
237
237
|
|
|
238
238
|
describe("the trigger slot", () => {
|
|
239
|
-
test("never carries an opacity, which
|
|
239
|
+
test("never carries an opacity, which would compound with the label's", () => {
|
|
240
240
|
for (const cell of CELLS) expect(cls(tabsVariants(cell).trigger())).not.toMatch(/\bopacity-/);
|
|
241
241
|
});
|
|
242
242
|
|
|
@@ -559,12 +559,10 @@ export function resolveScrollOffset(state: {
|
|
|
559
559
|
* three are an animated style, and a class fighting a `useAnimatedStyle` for the
|
|
560
560
|
* same property is an indicator that never appears, with no error anywhere.
|
|
561
561
|
*
|
|
562
|
-
* **The `trigger` slot carries no `opacity-*`, in any cell.**
|
|
563
|
-
* `Pressable`
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
* disabled fade lands on the label instead. This is `Radio`'s lesson, and it bites
|
|
567
|
-
* twice here because `feedback` defaults to `fade`.
|
|
562
|
+
* **The `trigger` slot carries no `opacity-*`, in any cell.** The disabled fade
|
|
563
|
+
* lands on the label instead. `Pressable` now multiplies a className's opacity
|
|
564
|
+
* into its press, so a fade on the trigger would draw — but the label already
|
|
565
|
+
* fades, and the two would compound to a quarter. This is `Radio`'s arrangement.
|
|
568
566
|
*
|
|
569
567
|
* **Selection changes the label's colour and nothing else.** A weight change would
|
|
570
568
|
* re-measure the label, which moves the frame the indicator is sitting on, on
|