@delacour/react-native-ui 0.1.0-alpha.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -6
- package/package.json +48 -4
- package/src/components/accordion/AGENTS.md +21 -9
- package/src/components/accordion/accordion-content.tsx +7 -4
- package/src/components/accordion/accordion-item.tsx +15 -13
- package/src/components/accordion/accordion.context.tsx +11 -5
- package/src/components/accordion/accordion.variants.test.ts +64 -3
- package/src/components/accordion/accordion.variants.ts +33 -9
- package/src/components/alert/AGENTS.md +102 -0
- package/src/components/alert/alert-action.tsx +17 -0
- package/src/components/alert/alert-close-button.tsx +50 -0
- package/src/components/alert/alert-content.tsx +15 -0
- package/src/components/alert/alert-description.tsx +16 -0
- package/src/components/alert/alert-glyphs.ts +25 -0
- package/src/components/alert/alert-indicator.tsx +40 -0
- package/src/components/alert/alert-title.tsx +18 -0
- package/src/components/alert/alert.context.tsx +69 -0
- package/src/components/alert/alert.tsx +160 -0
- package/src/components/alert/alert.types.ts +10 -0
- package/src/components/alert/alert.variants.test.ts +218 -0
- package/src/components/alert/alert.variants.ts +180 -0
- package/src/components/alert/index.ts +23 -0
- package/src/components/avatar/AGENTS.md +106 -0
- package/src/components/avatar/avatar-badge.tsx +51 -0
- package/src/components/avatar/avatar-group.tsx +139 -0
- package/src/components/avatar/avatar.context.tsx +93 -0
- package/src/components/avatar/avatar.tsx +201 -0
- package/src/components/avatar/avatar.utils.test.ts +174 -0
- package/src/components/avatar/avatar.utils.ts +141 -0
- package/src/components/avatar/avatar.variants.test.ts +251 -0
- package/src/components/avatar/avatar.variants.ts +270 -0
- package/src/components/avatar/index.ts +39 -0
- package/src/components/badge/AGENTS.md +5 -0
- package/src/components/badge/badge.tsx +6 -1
- package/src/components/bottom-sheet/AGENTS.md +186 -231
- package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
- package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
- package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -208
- package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
- package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
- package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
- package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -122
- package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
- package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
- package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
- package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
- package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -62
- package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
- package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
- package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
- package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
- package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
- package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
- package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -87
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -126
- package/src/components/bottom-sheet/index.ts +39 -17
- package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
- package/src/components/button/AGENTS.md +45 -11
- package/src/components/button/button-group.tsx +44 -8
- package/src/components/button/button.context.tsx +10 -0
- package/src/components/button/button.tsx +2 -1
- package/src/components/button/button.variants.test.ts +154 -0
- package/src/components/button/button.variants.ts +71 -6
- package/src/components/button/index.ts +2 -0
- package/src/components/calendar/AGENTS.md +142 -0
- package/src/components/calendar/calendar-caption.tsx +116 -0
- package/src/components/calendar/calendar-day.tsx +103 -0
- package/src/components/calendar/calendar-grid.tsx +183 -0
- package/src/components/calendar/calendar-header.tsx +34 -0
- package/src/components/calendar/calendar-nav.tsx +51 -0
- package/src/components/calendar/calendar-picker.tsx +128 -0
- package/src/components/calendar/calendar-weekdays.tsx +48 -0
- package/src/components/calendar/calendar.context.tsx +108 -0
- package/src/components/calendar/calendar.date.test.ts +456 -0
- package/src/components/calendar/calendar.date.ts +584 -0
- package/src/components/calendar/calendar.tsx +389 -0
- package/src/components/calendar/calendar.types.ts +87 -0
- package/src/components/calendar/calendar.variants.test.ts +363 -0
- package/src/components/calendar/calendar.variants.ts +426 -0
- package/src/components/calendar/index.ts +96 -0
- package/src/components/card/AGENTS.md +88 -0
- package/src/components/card/card-action.tsx +16 -0
- package/src/components/card/card-content.tsx +12 -0
- package/src/components/card/card-description.tsx +12 -0
- package/src/components/card/card-footer.tsx +30 -0
- package/src/components/card/card-header.tsx +45 -0
- package/src/components/card/card-title.tsx +25 -0
- package/src/components/card/card.context.tsx +64 -0
- package/src/components/card/card.tsx +94 -0
- package/src/components/card/card.types.ts +13 -0
- package/src/components/card/card.variants.test.ts +240 -0
- package/src/components/card/card.variants.ts +177 -0
- package/src/components/card/index.ts +15 -0
- package/src/components/chart/AGENTS.md +8 -0
- package/src/components/chart/chart.tsx +8 -1
- package/src/components/chip/AGENTS.md +113 -0
- package/src/components/chip/chip-close-button.tsx +48 -0
- package/src/components/chip/chip-end-content.tsx +15 -0
- package/src/components/chip/chip-label.tsx +21 -0
- package/src/components/chip/chip-start-content.tsx +16 -0
- package/src/components/chip/chip.context.tsx +66 -0
- package/src/components/chip/chip.tsx +268 -0
- package/src/components/chip/chip.types.ts +10 -0
- package/src/components/chip/chip.variants.test.ts +335 -0
- package/src/components/chip/chip.variants.ts +321 -0
- package/src/components/chip/index.ts +24 -0
- package/src/components/collapsible/AGENTS.md +97 -0
- package/src/components/collapsible/collapsible-content.tsx +97 -0
- package/src/components/collapsible/collapsible-description.tsx +17 -0
- package/src/components/collapsible/collapsible-indicator.tsx +92 -0
- package/src/components/collapsible/collapsible-title.tsx +17 -0
- package/src/components/collapsible/collapsible-trigger.tsx +133 -0
- package/src/components/collapsible/collapsible.context.tsx +91 -0
- package/src/components/collapsible/collapsible.tsx +167 -0
- package/src/components/collapsible/collapsible.types.ts +9 -0
- package/src/components/collapsible/collapsible.variants.test.ts +338 -0
- package/src/components/collapsible/collapsible.variants.ts +228 -0
- package/src/components/collapsible/index.ts +32 -0
- package/src/components/context-menu/AGENTS.md +103 -0
- package/src/components/context-menu/context-menu-content.tsx +64 -0
- package/src/components/context-menu/context-menu-item.tsx +22 -0
- package/src/components/context-menu/context-menu-preview.tsx +60 -0
- package/src/components/context-menu/context-menu-trigger.tsx +160 -0
- package/src/components/context-menu/context-menu.context.tsx +91 -0
- package/src/components/context-menu/context-menu.tsx +90 -0
- package/src/components/context-menu/context-menu.variants.test.ts +266 -0
- package/src/components/context-menu/context-menu.variants.ts +147 -0
- package/src/components/context-menu/index.ts +18 -0
- package/src/components/dialog/AGENTS.md +97 -0
- package/src/components/dialog/dialog-body.tsx +21 -0
- package/src/components/dialog/dialog-close.tsx +82 -0
- package/src/components/dialog/dialog-content.tsx +153 -0
- package/src/components/dialog/dialog-description.tsx +23 -0
- package/src/components/dialog/dialog-footer.tsx +32 -0
- package/src/components/dialog/dialog-header.tsx +20 -0
- package/src/components/dialog/dialog-title.tsx +42 -0
- package/src/components/dialog/dialog-trigger.tsx +54 -0
- package/src/components/dialog/dialog.context.tsx +90 -0
- package/src/components/dialog/dialog.tsx +119 -0
- package/src/components/dialog/dialog.variants.test.ts +224 -0
- package/src/components/dialog/dialog.variants.ts +173 -0
- package/src/components/dialog/index.ts +30 -0
- package/src/components/drawer/AGENTS.md +123 -0
- package/src/components/drawer/drawer-body.tsx +46 -0
- package/src/components/drawer/drawer-close.tsx +76 -0
- package/src/components/drawer/drawer-content.tsx +186 -0
- package/src/components/drawer/drawer-description.tsx +23 -0
- package/src/components/drawer/drawer-footer.tsx +21 -0
- package/src/components/drawer/drawer-header.tsx +42 -0
- package/src/components/drawer/drawer-title.tsx +35 -0
- package/src/components/drawer/drawer-trigger.tsx +54 -0
- package/src/components/drawer/drawer.context.tsx +114 -0
- package/src/components/drawer/drawer.tsx +119 -0
- package/src/components/drawer/drawer.variants.test.ts +392 -0
- package/src/components/drawer/drawer.variants.ts +244 -0
- package/src/components/drawer/index.ts +41 -0
- package/src/components/drawer/use-drawer-pan.ts +108 -0
- package/src/components/empty-state/AGENTS.md +90 -0
- package/src/components/empty-state/empty-state-content.tsx +17 -0
- package/src/components/empty-state/empty-state-description.tsx +12 -0
- package/src/components/empty-state/empty-state-header.tsx +17 -0
- package/src/components/empty-state/empty-state-media.tsx +56 -0
- package/src/components/empty-state/empty-state-title.tsx +18 -0
- package/src/components/empty-state/empty-state.context.tsx +65 -0
- package/src/components/empty-state/empty-state.tsx +84 -0
- package/src/components/empty-state/empty-state.types.ts +14 -0
- package/src/components/empty-state/empty-state.variants.test.ts +245 -0
- package/src/components/empty-state/empty-state.variants.ts +132 -0
- package/src/components/empty-state/index.ts +20 -0
- package/src/components/fab/AGENTS.md +119 -0
- package/src/components/fab/fab-action.tsx +118 -0
- package/src/components/fab/fab-group.tsx +214 -0
- package/src/components/fab/fab-label.tsx +18 -0
- package/src/components/fab/fab.context.tsx +125 -0
- package/src/components/fab/fab.tsx +156 -0
- package/src/components/fab/fab.types.ts +17 -0
- package/src/components/fab/fab.variants.test.ts +274 -0
- package/src/components/fab/fab.variants.ts +234 -0
- package/src/components/fab/index.ts +37 -0
- package/src/components/feedback/AGENTS.md +95 -0
- package/src/components/feedback/feedback-action.tsx +16 -0
- package/src/components/feedback/feedback-cancel.tsx +34 -0
- package/src/components/feedback/feedback-close.tsx +21 -0
- package/src/components/feedback/feedback-content.tsx +24 -0
- package/src/components/feedback/feedback-field.tsx +70 -0
- package/src/components/feedback/feedback-footer.tsx +21 -0
- package/src/components/feedback/feedback-panel.tsx +54 -0
- package/src/components/feedback/feedback-submit.tsx +74 -0
- package/src/components/feedback/feedback-title.tsx +34 -0
- package/src/components/feedback/feedback-trigger.tsx +20 -0
- package/src/components/feedback/feedback.context.tsx +73 -0
- package/src/components/feedback/feedback.tsx +135 -0
- package/src/components/feedback/feedback.variants.test.ts +185 -0
- package/src/components/feedback/feedback.variants.ts +128 -0
- package/src/components/feedback/index.ts +24 -0
- package/src/components/field/AGENTS.md +19 -0
- package/src/components/field/field-label.tsx +19 -5
- package/src/components/field/field.context.tsx +17 -0
- package/src/components/field/field.tsx +15 -2
- package/src/components/field/field.variants.test.ts +24 -0
- package/src/components/field/field.variants.ts +20 -0
- package/src/components/field/index.ts +1 -0
- package/src/components/item/AGENTS.md +92 -0
- package/src/components/item/index.ts +25 -0
- package/src/components/item/item-actions.tsx +28 -0
- package/src/components/item/item-content.tsx +15 -0
- package/src/components/item/item-description.tsx +12 -0
- package/src/components/item/item-footer.tsx +13 -0
- package/src/components/item/item-group.tsx +32 -0
- package/src/components/item/item-header.tsx +13 -0
- package/src/components/item/item-media.tsx +37 -0
- package/src/components/item/item-separator.tsx +13 -0
- package/src/components/item/item-title.tsx +17 -0
- package/src/components/item/item.context.tsx +66 -0
- package/src/components/item/item.tsx +218 -0
- package/src/components/item/item.types.ts +12 -0
- package/src/components/item/item.variants.test.ts +263 -0
- package/src/components/item/item.variants.ts +200 -0
- package/src/components/kpi/AGENTS.md +125 -0
- package/src/components/kpi/index.ts +45 -0
- package/src/components/kpi/kpi-action.tsx +12 -0
- package/src/components/kpi/kpi-content.tsx +35 -0
- package/src/components/kpi/kpi-footer.tsx +52 -0
- package/src/components/kpi/kpi-group.tsx +70 -0
- package/src/components/kpi/kpi-header.tsx +25 -0
- package/src/components/kpi/kpi-icon.tsx +28 -0
- package/src/components/kpi/kpi-sparkline.tsx +145 -0
- package/src/components/kpi/kpi-stat.tsx +20 -0
- package/src/components/kpi/kpi-title.tsx +19 -0
- package/src/components/kpi/kpi-trend.tsx +105 -0
- package/src/components/kpi/kpi-value.tsx +32 -0
- package/src/components/kpi/kpi.context.tsx +111 -0
- package/src/components/kpi/kpi.tsx +154 -0
- package/src/components/kpi/kpi.types.ts +9 -0
- package/src/components/kpi/kpi.variants.test.ts +428 -0
- package/src/components/kpi/kpi.variants.ts +347 -0
- package/src/components/label/AGENTS.md +89 -0
- package/src/components/label/index.ts +10 -0
- package/src/components/label/label.tsx +97 -0
- package/src/components/label/label.variants.test.ts +176 -0
- package/src/components/label/label.variants.ts +123 -0
- package/src/components/menu/AGENTS.md +176 -0
- package/src/components/menu/index.ts +44 -0
- package/src/components/menu/menu-background.tsx +17 -0
- package/src/components/menu/menu-checkbox-item.tsx +57 -0
- package/src/components/menu/menu-content.tsx +303 -0
- package/src/components/menu/menu-item.tsx +162 -0
- package/src/components/menu/menu-label.tsx +14 -0
- package/src/components/menu/menu-radio-group.tsx +50 -0
- package/src/components/menu/menu-radio-item.tsx +57 -0
- package/src/components/menu/menu-separator.tsx +11 -0
- package/src/components/menu/menu-sub-content.tsx +72 -0
- package/src/components/menu/menu-sub-trigger.tsx +50 -0
- package/src/components/menu/menu-sub.tsx +75 -0
- package/src/components/menu/menu-trigger.tsx +57 -0
- package/src/components/menu/menu.context.tsx +214 -0
- package/src/components/menu/menu.tsx +88 -0
- package/src/components/menu/menu.types.ts +33 -0
- package/src/components/menu/menu.variants.test.ts +400 -0
- package/src/components/menu/menu.variants.ts +353 -0
- package/src/components/meter/AGENTS.md +135 -0
- package/src/components/meter/index.ts +43 -0
- package/src/components/meter/meter-fill.tsx +19 -0
- package/src/components/meter/meter-header.tsx +17 -0
- package/src/components/meter/meter-label.tsx +18 -0
- package/src/components/meter/meter-output.tsx +42 -0
- package/src/components/meter/meter-segments.tsx +83 -0
- package/src/components/meter/meter-track.tsx +16 -0
- package/src/components/meter/meter.context.tsx +62 -0
- package/src/components/meter/meter.tsx +286 -0
- package/src/components/meter/meter.types.ts +30 -0
- package/src/components/meter/meter.variants.test.ts +430 -0
- package/src/components/meter/meter.variants.ts +347 -0
- package/src/components/overlay/AGENTS.md +89 -0
- package/src/components/overlay/index.ts +27 -0
- package/src/components/overlay/overlay-portal.tsx +84 -0
- package/src/components/overlay/overlay-presence.test.ts +46 -0
- package/src/components/overlay/overlay-presence.ts +49 -0
- package/src/components/overlay/overlay-registry.test.ts +96 -0
- package/src/components/overlay/overlay-registry.ts +99 -0
- package/src/components/overlay/overlay-scrim.tsx +49 -0
- package/src/components/overlay/overlay.context.tsx +48 -0
- package/src/components/overlay/overlay.tsx +118 -0
- package/src/components/overlay/overlay.variants.test.ts +37 -0
- package/src/components/overlay/overlay.variants.ts +33 -0
- package/src/components/overlay/use-overlay-back-handler.ts +34 -0
- package/src/components/overlay/use-overlay-presence.ts +87 -0
- package/src/components/popover/AGENTS.md +138 -0
- package/src/components/popover/index.ts +48 -0
- package/src/components/popover/popover-anchor.tsx +49 -0
- package/src/components/popover/popover-arrow.tsx +89 -0
- package/src/components/popover/popover-close.tsx +73 -0
- package/src/components/popover/popover-content.tsx +235 -0
- package/src/components/popover/popover-description.tsx +18 -0
- package/src/components/popover/popover-title.tsx +33 -0
- package/src/components/popover/popover-trigger.tsx +56 -0
- package/src/components/popover/popover.context.tsx +71 -0
- package/src/components/popover/popover.position.test.ts +354 -0
- package/src/components/popover/popover.position.ts +286 -0
- package/src/components/popover/popover.tsx +123 -0
- package/src/components/popover/popover.variants.test.ts +135 -0
- package/src/components/popover/popover.variants.ts +85 -0
- package/src/components/popover/use-anchor-measure.ts +76 -0
- package/src/components/popover/use-anchored-content.ts +219 -0
- package/src/components/pressable/AGENTS.md +11 -1
- package/src/components/pressable/pressable.tsx +7 -2
- package/src/components/pressable/pressable.variants.test.ts +29 -0
- package/src/components/pressable/pressable.variants.ts +17 -0
- package/src/components/progress/AGENTS.md +120 -0
- package/src/components/progress/index.ts +40 -0
- package/src/components/progress/progress-fill.tsx +152 -0
- package/src/components/progress/progress-header.tsx +19 -0
- package/src/components/progress/progress-label.tsx +27 -0
- package/src/components/progress/progress-output.tsx +47 -0
- package/src/components/progress/progress-track.tsx +38 -0
- package/src/components/progress/progress.context.tsx +79 -0
- package/src/components/progress/progress.tsx +171 -0
- package/src/components/progress/progress.types.ts +23 -0
- package/src/components/progress/progress.variants.test.ts +335 -0
- package/src/components/progress/progress.variants.ts +284 -0
- package/src/components/progress-button/AGENTS.md +116 -0
- package/src/components/progress-button/index.ts +36 -0
- package/src/components/progress-button/progress-button-done.tsx +33 -0
- package/src/components/progress-button/progress-button-label.tsx +27 -0
- package/src/components/progress-button/progress-button.context.tsx +93 -0
- package/src/components/progress-button/progress-button.tsx +414 -0
- package/src/components/progress-button/progress-button.variants.test.ts +314 -0
- package/src/components/progress-button/progress-button.variants.ts +236 -0
- package/src/components/provider/AGENTS.md +38 -25
- package/src/components/provider/provider.tsx +38 -11
- package/src/components/radio/AGENTS.md +8 -10
- package/src/components/radio/radio.variants.test.ts +5 -6
- package/src/components/radio/radio.variants.ts +6 -7
- package/src/components/rating/AGENTS.md +143 -0
- package/src/components/rating/index.ts +43 -0
- package/src/components/rating/rating-output.tsx +39 -0
- package/src/components/rating/rating-star.tsx +70 -0
- package/src/components/rating/rating-stars.tsx +219 -0
- package/src/components/rating/rating.context.tsx +79 -0
- package/src/components/rating/rating.tsx +163 -0
- package/src/components/rating/rating.variants.test.ts +347 -0
- package/src/components/rating/rating.variants.ts +354 -0
- package/src/components/selection-mode/AGENTS.md +155 -0
- package/src/components/selection-mode/index.ts +43 -0
- package/src/components/selection-mode/selection-mode-action.tsx +65 -0
- package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
- package/src/components/selection-mode/selection-mode-group.tsx +123 -0
- package/src/components/selection-mode/selection-mode-header.tsx +85 -0
- package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
- package/src/components/selection-mode/selection-mode-item.tsx +182 -0
- package/src/components/selection-mode/selection-mode.context.tsx +89 -0
- package/src/components/selection-mode/selection-mode.tsx +207 -0
- package/src/components/selection-mode/selection-mode.types.ts +36 -0
- package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
- package/src/components/selection-mode/selection-mode.variants.ts +329 -0
- package/src/components/skeleton/AGENTS.md +120 -0
- package/src/components/skeleton/index.ts +15 -0
- package/src/components/skeleton/skeleton-clock.ts +48 -0
- package/src/components/skeleton/skeleton-group.tsx +64 -0
- package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
- package/src/components/skeleton/skeleton.context.tsx +45 -0
- package/src/components/skeleton/skeleton.tsx +227 -0
- package/src/components/skeleton/skeleton.variants.test.ts +245 -0
- package/src/components/skeleton/skeleton.variants.ts +222 -0
- package/src/components/slide-button/AGENTS.md +122 -0
- package/src/components/slide-button/index.ts +43 -0
- package/src/components/slide-button/slide-button-label.tsx +70 -0
- package/src/components/slide-button/slide-button-thumb.tsx +93 -0
- package/src/components/slide-button/slide-button.context.tsx +76 -0
- package/src/components/slide-button/slide-button.tsx +457 -0
- package/src/components/slide-button/slide-button.variants.test.ts +288 -0
- package/src/components/slide-button/slide-button.variants.ts +324 -0
- package/src/components/slider/index.ts +2 -0
- package/src/components/slider/slider-thumb.tsx +33 -1
- package/src/components/slider/slider.variants.test.ts +52 -0
- package/src/components/slider/slider.variants.ts +56 -2
- package/src/components/stack-card/AGENTS.md +169 -0
- package/src/components/stack-card/index.ts +37 -0
- package/src/components/stack-card/stack-card-action.tsx +66 -0
- package/src/components/stack-card/stack-card-actions.tsx +18 -0
- package/src/components/stack-card/stack-card-card.tsx +71 -0
- package/src/components/stack-card/stack-card-empty.tsx +30 -0
- package/src/components/stack-card/stack-card-slot.tsx +90 -0
- package/src/components/stack-card/stack-card-stamp.tsx +76 -0
- package/src/components/stack-card/stack-card.context.tsx +117 -0
- package/src/components/stack-card/stack-card.tsx +557 -0
- package/src/components/stack-card/stack-card.types.ts +26 -0
- package/src/components/stack-card/stack-card.variants.test.ts +355 -0
- package/src/components/stack-card/stack-card.variants.ts +360 -0
- package/src/components/steps/AGENTS.md +153 -0
- package/src/components/steps/index.ts +45 -0
- package/src/components/steps/steps-connector.tsx +31 -0
- package/src/components/steps/steps-description.tsx +26 -0
- package/src/components/steps/steps-indicator.tsx +106 -0
- package/src/components/steps/steps-item.tsx +225 -0
- package/src/components/steps/steps-panel.tsx +41 -0
- package/src/components/steps/steps-title.tsx +32 -0
- package/src/components/steps/steps-track.tsx +59 -0
- package/src/components/steps/steps.context.tsx +123 -0
- package/src/components/steps/steps.tsx +166 -0
- package/src/components/steps/steps.variants.test.ts +388 -0
- package/src/components/steps/steps.variants.ts +468 -0
- package/src/components/surface/AGENTS.md +72 -0
- package/src/components/surface/index.ts +15 -0
- package/src/components/surface/surface.context.tsx +54 -0
- package/src/components/surface/surface.tsx +70 -0
- package/src/components/surface/surface.variants.test.ts +180 -0
- package/src/components/surface/surface.variants.ts +133 -0
- package/src/components/swipe/AGENTS.md +140 -0
- package/src/components/swipe/index.ts +31 -0
- package/src/components/swipe/swipe-action.tsx +99 -0
- package/src/components/swipe/swipe-end.tsx +14 -0
- package/src/components/swipe/swipe-group.tsx +55 -0
- package/src/components/swipe/swipe-panel.tsx +55 -0
- package/src/components/swipe/swipe-start.tsx +15 -0
- package/src/components/swipe/swipe.context.tsx +135 -0
- package/src/components/swipe/swipe.tsx +393 -0
- package/src/components/swipe/swipe.types.ts +7 -0
- package/src/components/swipe/swipe.variants.test.ts +337 -0
- package/src/components/swipe/swipe.variants.ts +290 -0
- package/src/components/switch/switch.variants.ts +2 -2
- package/src/components/tabs/AGENTS.md +4 -3
- package/src/components/tabs/tabs.variants.test.ts +1 -1
- package/src/components/tabs/tabs.variants.ts +4 -6
- package/src/components/textarea/AGENTS.md +86 -0
- package/src/components/textarea/index.ts +16 -0
- package/src/components/textarea/textarea.tsx +157 -0
- package/src/components/textarea/textarea.variants.test.ts +219 -0
- package/src/components/textarea/textarea.variants.ts +171 -0
- package/src/components/toast/AGENTS.md +140 -0
- package/src/components/toast/index.ts +74 -0
- package/src/components/toast/toast-action.tsx +59 -0
- package/src/components/toast/toast-api.ts +32 -0
- package/src/components/toast/toast-close.tsx +44 -0
- package/src/components/toast/toast-content.tsx +11 -0
- package/src/components/toast/toast-description.tsx +11 -0
- package/src/components/toast/toast-indicator.tsx +34 -0
- package/src/components/toast/toast-item.tsx +308 -0
- package/src/components/toast/toast-title.tsx +13 -0
- package/src/components/toast/toast-viewport.tsx +135 -0
- package/src/components/toast/toast.context.tsx +69 -0
- package/src/components/toast/toast.store.test.ts +259 -0
- package/src/components/toast/toast.store.ts +338 -0
- package/src/components/toast/toast.timer.test.ts +64 -0
- package/src/components/toast/toast.timer.ts +51 -0
- package/src/components/toast/toast.tsx +132 -0
- package/src/components/toast/toast.variants.test.ts +393 -0
- package/src/components/toast/toast.variants.ts +304 -0
- package/src/components/toggle-button/AGENTS.md +106 -0
- package/src/components/toggle-button/index.ts +30 -0
- package/src/components/toggle-button/toggle-button-group.tsx +156 -0
- package/src/components/toggle-button/toggle-button-label.tsx +19 -0
- package/src/components/toggle-button/toggle-button.context.tsx +130 -0
- package/src/components/toggle-button/toggle-button.tsx +172 -0
- package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
- package/src/components/toggle-button/toggle-button.variants.ts +160 -0
- package/src/components/tooltip/AGENTS.md +133 -0
- package/src/components/tooltip/index.ts +28 -0
- package/src/components/tooltip/tooltip-arrow.tsx +40 -0
- package/src/components/tooltip/tooltip-content.tsx +179 -0
- package/src/components/tooltip/tooltip-description.tsx +19 -0
- package/src/components/tooltip/tooltip-text.tsx +21 -0
- package/src/components/tooltip/tooltip-title.tsx +20 -0
- package/src/components/tooltip/tooltip-trigger.tsx +103 -0
- package/src/components/tooltip/tooltip.context.tsx +68 -0
- package/src/components/tooltip/tooltip.tsx +213 -0
- package/src/components/tooltip/tooltip.variants.test.ts +231 -0
- package/src/components/tooltip/tooltip.variants.ts +177 -0
- package/src/hooks/use-calm-motion.tsx +31 -0
- package/src/lib/calm-motion.test.ts +17 -0
- package/src/lib/calm-motion.ts +23 -0
- package/src/lib/cn.test.ts +14 -0
- package/src/lib/cn.ts +2 -2
- package/src/no-gorhom.test.ts +54 -0
- package/src/styles/geometry.test.ts +1 -0
- package/src/styles/theme.css +4 -0
- package/src/styles/tokens.css +11 -0
- package/src/styles/tokens.test.ts +22 -0
- package/src/styles/tokens.ts +20 -0
- package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
- package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode, useCallback, useId, useMemo, useRef } from "react";
|
|
2
|
+
import { useControllableState } from "../../hooks/use-controllable-state";
|
|
3
|
+
import { type DialogFocusTarget, type DialogInternalValue, DialogProvider } from "./dialog.context";
|
|
4
|
+
import { DialogBody } from "./dialog-body";
|
|
5
|
+
import { DialogClose } from "./dialog-close";
|
|
6
|
+
import { DialogContent } from "./dialog-content";
|
|
7
|
+
import { DialogDescription } from "./dialog-description";
|
|
8
|
+
import { DialogFooter } from "./dialog-footer";
|
|
9
|
+
import { DialogHeader } from "./dialog-header";
|
|
10
|
+
import { DialogTitle } from "./dialog-title";
|
|
11
|
+
import { DialogTrigger } from "./dialog-trigger";
|
|
12
|
+
|
|
13
|
+
export type DialogProps = {
|
|
14
|
+
/** Whether the dialog is open. Pass it to control the dialog; omit it and the dialog holds its own state. */
|
|
15
|
+
isOpen?: boolean;
|
|
16
|
+
/** Whether an uncontrolled dialog starts open. Default false. */
|
|
17
|
+
defaultOpen?: boolean;
|
|
18
|
+
/** Called with the new value whenever the dialog opens or closes, from any path. */
|
|
19
|
+
onOpenChange?: (isOpen: boolean) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Whether a scrim tap, Android back and the iOS escape gesture close it.
|
|
22
|
+
* `false` makes an alert dialog: only its own actions close it. Default true.
|
|
23
|
+
*/
|
|
24
|
+
isDismissible?: boolean;
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function DialogRoot({
|
|
29
|
+
isOpen,
|
|
30
|
+
defaultOpen = false,
|
|
31
|
+
onOpenChange,
|
|
32
|
+
isDismissible = true,
|
|
33
|
+
children,
|
|
34
|
+
}: DialogProps): ReactElement {
|
|
35
|
+
const [open, setControllableOpen] = useControllableState({
|
|
36
|
+
defaultValue: defaultOpen,
|
|
37
|
+
onChange: onOpenChange,
|
|
38
|
+
value: isOpen,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Every path — trigger, close, scrim, back, escape — lands here, and a
|
|
42
|
+
// second close while the first is still exiting would report twice.
|
|
43
|
+
const setOpen = useCallback(
|
|
44
|
+
(next: boolean) => {
|
|
45
|
+
if (next !== open) setControllableOpen(next);
|
|
46
|
+
},
|
|
47
|
+
[open, setControllableOpen]
|
|
48
|
+
);
|
|
49
|
+
const close = useCallback(() => setOpen(false), [setOpen]);
|
|
50
|
+
|
|
51
|
+
const titleId = useId();
|
|
52
|
+
const descriptionId = useId();
|
|
53
|
+
const triggerRef = useRef<DialogFocusTarget | null>(null);
|
|
54
|
+
const titleRef = useRef<DialogFocusTarget | null>(null);
|
|
55
|
+
|
|
56
|
+
const value = useMemo<DialogInternalValue>(
|
|
57
|
+
() => ({ close, descriptionId, isDismissible, isOpen: open, setOpen, titleId, titleRef, triggerRef }),
|
|
58
|
+
[close, descriptionId, isDismissible, open, setOpen, titleId]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return <DialogProvider value={value}>{children}</DialogProvider>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A centred card over a dimmed app that asks for a decision or a short input
|
|
66
|
+
* before the user goes on — confirm a delete, rename a file, accept terms.
|
|
67
|
+
*
|
|
68
|
+
* It takes the whole screen. Use a `Popover` when the surrounding context must
|
|
69
|
+
* stay visible, and a `BottomSheet` when the content is long or scrolls. With
|
|
70
|
+
* `isDismissible={false}` it is an alert dialog: only its own actions close it.
|
|
71
|
+
*
|
|
72
|
+
* Draws through `Overlay.Portal`, so the app needs `OverlayProvider` mounted
|
|
73
|
+
* once at its root.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* <Dialog>
|
|
77
|
+
* <Dialog.Trigger asChild>
|
|
78
|
+
* <Button variant="destructive">Delete</Button>
|
|
79
|
+
* </Dialog.Trigger>
|
|
80
|
+
* <Dialog.Content>
|
|
81
|
+
* <Dialog.Close />
|
|
82
|
+
* <Dialog.Header>
|
|
83
|
+
* <Dialog.Title>Delete project?</Dialog.Title>
|
|
84
|
+
* <Dialog.Description>This cannot be undone.</Dialog.Description>
|
|
85
|
+
* </Dialog.Header>
|
|
86
|
+
* <Dialog.Footer>
|
|
87
|
+
* <Dialog.Close asChild>
|
|
88
|
+
* <Button variant="secondary">Cancel</Button>
|
|
89
|
+
* </Dialog.Close>
|
|
90
|
+
* <Button variant="destructive" onPress={remove}>Delete</Button>
|
|
91
|
+
* </Dialog.Footer>
|
|
92
|
+
* </Dialog.Content>
|
|
93
|
+
* </Dialog>
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* // Controlled, and an alert dialog.
|
|
97
|
+
* <Dialog isOpen={isOpen} onOpenChange={setIsOpen} isDismissible={false}>
|
|
98
|
+
* …
|
|
99
|
+
* </Dialog>
|
|
100
|
+
*/
|
|
101
|
+
export const Dialog = Object.assign(DialogRoot, {
|
|
102
|
+
/** The control that opens it. `asChild` donates the press to a `Button`. */
|
|
103
|
+
Trigger: DialogTrigger,
|
|
104
|
+
/** The portal, scrim and card. Renders nothing while closed. */
|
|
105
|
+
Content: DialogContent,
|
|
106
|
+
/** The ✕ in the corner, or — with `asChild` — any control that closes it. */
|
|
107
|
+
Close: DialogClose,
|
|
108
|
+
/** The column holding the title and description. */
|
|
109
|
+
Header: DialogHeader,
|
|
110
|
+
/** The heading — a `Text.Header` the card is labelled by. */
|
|
111
|
+
Title: DialogTitle,
|
|
112
|
+
/** Muted supporting copy — a `Text.Paragraph`. */
|
|
113
|
+
Description: DialogDescription,
|
|
114
|
+
/** What the dialog asks for, between the header and the footer. */
|
|
115
|
+
Body: DialogBody,
|
|
116
|
+
/** The actions — a row, or a stacked column on an `sm` card. */
|
|
117
|
+
Footer: DialogFooter,
|
|
118
|
+
displayName: "DelacourUI.Dialog",
|
|
119
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { declaredTokens } from "../../styles/theme-tokens.test";
|
|
3
|
+
import {
|
|
4
|
+
DIALOG_CLOSE_HIT_SLOP,
|
|
5
|
+
DIALOG_ENTER_SCALE,
|
|
6
|
+
DIALOG_ENTER_TRANSLATE_Y,
|
|
7
|
+
DIALOG_FOOTER_VARIANTS,
|
|
8
|
+
DIALOG_KEYBOARD_MARGIN,
|
|
9
|
+
DIALOG_SCRIM_TOKEN,
|
|
10
|
+
DIALOG_SIZE_CLASS,
|
|
11
|
+
DIALOG_SIZES,
|
|
12
|
+
dialogVariants,
|
|
13
|
+
resolveDialogFooterDirection,
|
|
14
|
+
resolveDialogKeyboardLift,
|
|
15
|
+
resolveDialogRole,
|
|
16
|
+
} from "./dialog.variants";
|
|
17
|
+
|
|
18
|
+
const LIGHT = declaredTokens("light");
|
|
19
|
+
const DARK = declaredTokens("dark");
|
|
20
|
+
|
|
21
|
+
/** `border-t` and friends set a width, not a colour, and name no token. */
|
|
22
|
+
const STRUCTURAL_BORDER_SUFFIXES = new Set(["t", "b", "l", "r", "x", "y", "s", "e"]);
|
|
23
|
+
|
|
24
|
+
/** Every theme token a class string paints with, with any `/alpha` suffix dropped. */
|
|
25
|
+
function colorTokens(cls: string): string[] {
|
|
26
|
+
const tokens: string[] = [];
|
|
27
|
+
|
|
28
|
+
for (const [, utility, token] of cls.matchAll(/\b(bg|border|text)-([a-z][\w-]*)(?:\/\d+)?\b/g)) {
|
|
29
|
+
if (utility === "border" && STRUCTURAL_BORDER_SUFFIXES.has(token)) continue;
|
|
30
|
+
tokens.push(token);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return tokens;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The slots this component declares, pinned rather than derived — `tv` adds a
|
|
38
|
+
* `base` slot of its own, and a new slot has to be listed here before the
|
|
39
|
+
* token sweep can miss it.
|
|
40
|
+
*/
|
|
41
|
+
const SLOT_NAMES = ["scrim", "positioner", "content", "close", "header", "title", "body", "footer"] as const;
|
|
42
|
+
|
|
43
|
+
describe("dialogVariants", () => {
|
|
44
|
+
test("the scrim paints the overlay token, which exists in both themes", () => {
|
|
45
|
+
expect(dialogVariants().scrim()).toContain(`bg-${DIALOG_SCRIM_TOKEN}`);
|
|
46
|
+
expect(LIGHT.has(DIALOG_SCRIM_TOKEN)).toBe(true);
|
|
47
|
+
expect(DARK.has(DIALOG_SCRIM_TOKEN)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("the positioner fills the window, centres the card and keeps the screen gutter", () => {
|
|
51
|
+
const positioner = dialogVariants().positioner();
|
|
52
|
+
expect(positioner).toContain("absolute");
|
|
53
|
+
expect(positioner).toContain("inset-0");
|
|
54
|
+
expect(positioner).toContain("items-center");
|
|
55
|
+
expect(positioner).toContain("justify-center");
|
|
56
|
+
expect(positioner).toContain("px-screen-gutter");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("the card is a popover surface with the card corner and a hairline", () => {
|
|
60
|
+
const content = dialogVariants().content();
|
|
61
|
+
expect(content).toContain("bg-popover");
|
|
62
|
+
expect(content).toContain("rounded-lg");
|
|
63
|
+
expect(content).toContain("border");
|
|
64
|
+
expect(content).toContain("border-border");
|
|
65
|
+
expect(content).toContain("w-full");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("each size pins its own max width", () => {
|
|
69
|
+
expect(DIALOG_SIZE_CLASS).toEqual({
|
|
70
|
+
full: "max-w-full",
|
|
71
|
+
lg: "max-w-[520px]",
|
|
72
|
+
md: "max-w-[400px]",
|
|
73
|
+
sm: "max-w-[320px]",
|
|
74
|
+
});
|
|
75
|
+
for (const size of DIALOG_SIZES) {
|
|
76
|
+
expect(dialogVariants({ size }).content()).toContain(DIALOG_SIZE_CLASS[size]);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("defaults to md", () => {
|
|
81
|
+
expect(dialogVariants().content()).toContain(DIALOG_SIZE_CLASS.md);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a caller's className reaches the card and wins a conflict", () => {
|
|
85
|
+
const content = dialogVariants().content({ className: "rounded-2xl" });
|
|
86
|
+
expect(content).toContain("rounded-2xl");
|
|
87
|
+
expect(content).not.toContain("rounded-lg");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("the title reserves clearance for the close glyph and carries no type of its own", () => {
|
|
91
|
+
const title = dialogVariants().title();
|
|
92
|
+
expect(title).toBe("pr-8");
|
|
93
|
+
expect(title).not.toMatch(/\btext-(xs|sm|base|lg|xl|\dxl)\b/);
|
|
94
|
+
expect(title).not.toMatch(/\bfont-/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("the close glyph sits in the card's top-right corner", () => {
|
|
98
|
+
const close = dialogVariants().close();
|
|
99
|
+
expect(close).toContain("absolute");
|
|
100
|
+
expect(close).toContain("top-");
|
|
101
|
+
expect(close).toContain("right-");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a plain footer is a right-aligned row inside the card padding", () => {
|
|
105
|
+
const footer = dialogVariants({ footer: "plain" }).footer();
|
|
106
|
+
expect(footer).toContain("flex-row");
|
|
107
|
+
expect(footer).toContain("justify-end");
|
|
108
|
+
expect(footer).toContain("gap-2");
|
|
109
|
+
expect(footer).not.toContain("bg-muted");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("a panel footer bleeds to the card's edges on a muted band under a hairline", () => {
|
|
113
|
+
const footer = dialogVariants({ footer: "panel" }).footer();
|
|
114
|
+
expect(footer).toContain("bg-muted");
|
|
115
|
+
expect(footer).toContain("border-t");
|
|
116
|
+
expect(footer).toContain("border-border");
|
|
117
|
+
expect(footer).toMatch(/-mx-\d/);
|
|
118
|
+
expect(footer).toMatch(/-mb-\d/);
|
|
119
|
+
expect(footer).toContain("rounded-b-lg");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("a column footer stacks its actions full width", () => {
|
|
123
|
+
const footer = dialogVariants({ footer: "plain", footerDirection: "column" }).footer();
|
|
124
|
+
expect(footer).toContain("flex-col");
|
|
125
|
+
expect(footer).not.toContain("flex-row");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("the footer variants are the two the spec names", () => {
|
|
129
|
+
expect([...DIALOG_FOOTER_VARIANTS]).toEqual(["plain", "panel"]);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("every token the slots name", () => {
|
|
134
|
+
test("is declared in both variants of theme.css", () => {
|
|
135
|
+
for (const size of DIALOG_SIZES) {
|
|
136
|
+
for (const footer of DIALOG_FOOTER_VARIANTS) {
|
|
137
|
+
const slots = dialogVariants({ footer, size });
|
|
138
|
+
for (const name of SLOT_NAMES) {
|
|
139
|
+
for (const token of colorTokens(slots[name]())) {
|
|
140
|
+
expect({ inLight: LIGHT.has(token), slot: name, token }).toEqual({ inLight: true, slot: name, token });
|
|
141
|
+
expect({ inDark: DARK.has(token), slot: name, token }).toEqual({ inDark: true, slot: name, token });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("the reader found tokens at all", () => {
|
|
149
|
+
expect(colorTokens(dialogVariants().content())).toEqual(expect.arrayContaining(["popover", "border"]));
|
|
150
|
+
expect(colorTokens(dialogVariants({ footer: "panel" }).footer())).toEqual(
|
|
151
|
+
expect.arrayContaining(["muted", "border"])
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("resolveDialogFooterDirection", () => {
|
|
157
|
+
test("stacks on the narrow card only", () => {
|
|
158
|
+
expect(resolveDialogFooterDirection("sm")).toBe("column");
|
|
159
|
+
expect(resolveDialogFooterDirection("md")).toBe("row");
|
|
160
|
+
expect(resolveDialogFooterDirection("lg")).toBe("row");
|
|
161
|
+
expect(resolveDialogFooterDirection("full")).toBe("row");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("resolveDialogRole", () => {
|
|
166
|
+
test("a dismissible dialog is a dialog, a non-dismissible one an alert dialog", () => {
|
|
167
|
+
expect(resolveDialogRole(true)).toBe("dialog");
|
|
168
|
+
expect(resolveDialogRole(false)).toBe("alertdialog");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("motion constants", () => {
|
|
173
|
+
test("the card grows into place from just under full size and just below", () => {
|
|
174
|
+
expect(DIALOG_ENTER_SCALE).toBe(0.96);
|
|
175
|
+
expect(DIALOG_ENTER_TRANSLATE_Y).toBe(8);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("DIALOG_CLOSE_HIT_SLOP", () => {
|
|
180
|
+
test("brings a bare corner glyph up toward the 44pt minimum", () => {
|
|
181
|
+
expect(DIALOG_CLOSE_HIT_SLOP).toBe(8);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("resolveDialogKeyboardLift", () => {
|
|
186
|
+
const base = { cardBottom: 500, cardTop: 300, margin: DIALOG_KEYBOARD_MARGIN, topInset: 59, windowHeight: 852 };
|
|
187
|
+
|
|
188
|
+
test("the margin is 16", () => {
|
|
189
|
+
expect(DIALOG_KEYBOARD_MARGIN).toBe(16);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("no keyboard, no lift", () => {
|
|
193
|
+
expect(resolveDialogKeyboardLift({ ...base, keyboardHeight: 0 })).toBe(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("a keyboard that stays below the card and its margin does not lift it", () => {
|
|
197
|
+
// The keyboard's top is at 852 - 300 = 552, clear of 500 + 16.
|
|
198
|
+
expect(resolveDialogKeyboardLift({ ...base, keyboardHeight: 300 })).toBe(0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("a keyboard overlapping the card lifts it just clear, plus the margin", () => {
|
|
202
|
+
// The keyboard's top is at 852 - 400 = 452; the card's bottom needs to be at 436.
|
|
203
|
+
expect(resolveDialogKeyboardLift({ ...base, keyboardHeight: 400 })).toBe(64);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("accepts the keyboard controller's negative height", () => {
|
|
207
|
+
expect(resolveDialogKeyboardLift({ ...base, keyboardHeight: -400 })).toBe(64);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("never pushes the card's top under the top safe-area inset", () => {
|
|
211
|
+
// The card would need 500 + 16 - (852 - 700) = 364, but its top can rise only 300 - 59.
|
|
212
|
+
expect(resolveDialogKeyboardLift({ ...base, keyboardHeight: 700 })).toBe(241);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("never negative, even for a card already above the inset", () => {
|
|
216
|
+
expect(resolveDialogKeyboardLift({ ...base, cardTop: 20, keyboardHeight: 700 })).toBe(0);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("an unmeasured card is not lifted", () => {
|
|
220
|
+
expect(
|
|
221
|
+
resolveDialogKeyboardLift({ ...base, cardBottom: 0, cardTop: 0, keyboardHeight: 400, windowHeight: 852 })
|
|
222
|
+
).toBe(0);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
|
|
4
|
+
/** The card's widths, narrowest first. */
|
|
5
|
+
export const DIALOG_SIZES = ["sm", "md", "lg", "full"] as const;
|
|
6
|
+
export type DialogSize = (typeof DIALOG_SIZES)[number];
|
|
7
|
+
|
|
8
|
+
/** How `Dialog.Footer` sits in the card: a row inside the padding, or a muted band bled to the edges. */
|
|
9
|
+
export const DIALOG_FOOTER_VARIANTS = ["plain", "panel"] as const;
|
|
10
|
+
export type DialogFooterVariant = (typeof DIALOG_FOOTER_VARIANTS)[number];
|
|
11
|
+
|
|
12
|
+
/** Which way `Dialog.Footer` lays out its actions. */
|
|
13
|
+
export type DialogFooterDirection = "row" | "column";
|
|
14
|
+
|
|
15
|
+
/** The accessibility role the card announces. */
|
|
16
|
+
export type DialogRole = "dialog" | "alertdialog";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Each size's cap on the card's width.
|
|
20
|
+
*
|
|
21
|
+
* Classes, not numbers: the card is `w-full` and the cap is a `max-w-*`, so a
|
|
22
|
+
* narrow phone shrinks every size to the gutter and a tablet stops it at the
|
|
23
|
+
* cap. The arbitrary values are written out in full because Tailwind's scanner
|
|
24
|
+
* is static — a width computed at runtime would compile to nothing.
|
|
25
|
+
*/
|
|
26
|
+
export const DIALOG_SIZE_CLASS = {
|
|
27
|
+
sm: "max-w-[320px]",
|
|
28
|
+
md: "max-w-[400px]",
|
|
29
|
+
lg: "max-w-[520px]",
|
|
30
|
+
full: "max-w-full",
|
|
31
|
+
} as const satisfies Record<DialogSize, string>;
|
|
32
|
+
|
|
33
|
+
/** The scrim's token — the foundation's, named here so the test can pin it. */
|
|
34
|
+
export const DIALOG_SCRIM_TOKEN = "overlay";
|
|
35
|
+
|
|
36
|
+
/** Slop around the corner ✕ — a bare glyph has no capsule to bring it toward 44pt. */
|
|
37
|
+
export const DIALOG_CLOSE_HIT_SLOP = 8;
|
|
38
|
+
|
|
39
|
+
/** The scale the card grows from on enter. */
|
|
40
|
+
export const DIALOG_ENTER_SCALE = 0.96;
|
|
41
|
+
|
|
42
|
+
/** How far below its resting place the card starts on enter, in points. */
|
|
43
|
+
export const DIALOG_ENTER_TRANSLATE_Y = 8;
|
|
44
|
+
|
|
45
|
+
/** The clearance kept between the card's bottom edge and the keyboard's top, in points. */
|
|
46
|
+
export const DIALOG_KEYBOARD_MARGIN = 16;
|
|
47
|
+
|
|
48
|
+
export const dialogVariants = tv({
|
|
49
|
+
slots: {
|
|
50
|
+
/** The foundation's scrim token. The fade is `Overlay.Scrim`'s own. */
|
|
51
|
+
scrim: "bg-overlay",
|
|
52
|
+
/**
|
|
53
|
+
* The window-sized frame the card is centred in. The gutter is the
|
|
54
|
+
* screen's, so a `full` card lines up with the content behind it.
|
|
55
|
+
*/
|
|
56
|
+
positioner: "absolute inset-0 items-center justify-center px-screen-gutter",
|
|
57
|
+
/**
|
|
58
|
+
* The card. `popover` because a dialog is a layer over the app — the
|
|
59
|
+
* token `BottomSheet` paints with too — and `rounded-lg` because that is
|
|
60
|
+
* the card corner (see the package's Sizing section).
|
|
61
|
+
*/
|
|
62
|
+
content: "w-full gap-4 rounded-lg border border-border bg-popover p-5",
|
|
63
|
+
close: "absolute top-4 right-4 z-10",
|
|
64
|
+
header: "gap-1.5",
|
|
65
|
+
/**
|
|
66
|
+
* Clearance for `Dialog.Close`, and nothing else — reserved on every
|
|
67
|
+
* dialog so adding a close never reflows the title. The type is
|
|
68
|
+
* `Text.Header`'s; a size here would be a second definition of it.
|
|
69
|
+
*/
|
|
70
|
+
title: "pr-8",
|
|
71
|
+
body: "gap-3",
|
|
72
|
+
footer: "gap-2",
|
|
73
|
+
},
|
|
74
|
+
variants: {
|
|
75
|
+
size: {
|
|
76
|
+
sm: { content: DIALOG_SIZE_CLASS.sm },
|
|
77
|
+
md: { content: DIALOG_SIZE_CLASS.md },
|
|
78
|
+
lg: { content: DIALOG_SIZE_CLASS.lg },
|
|
79
|
+
full: { content: DIALOG_SIZE_CLASS.full },
|
|
80
|
+
},
|
|
81
|
+
footer: {
|
|
82
|
+
plain: {},
|
|
83
|
+
/**
|
|
84
|
+
* Bleeds out of the card's `p-5` on three sides, then restores it as
|
|
85
|
+
* its own padding, so the band reaches the edges and the hairline runs
|
|
86
|
+
* corner to corner. Only the bottom corners round: the top edge meets
|
|
87
|
+
* the card's own fill.
|
|
88
|
+
*/
|
|
89
|
+
panel: { footer: "-mx-5 -mb-5 mt-1 rounded-b-lg border-border border-t bg-muted px-5 py-4" },
|
|
90
|
+
},
|
|
91
|
+
footerDirection: {
|
|
92
|
+
row: { footer: "flex-row items-center justify-end" },
|
|
93
|
+
/** Stacked full width, in written order — so the primary action, written last, sits last. */
|
|
94
|
+
column: { footer: "flex-col" },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
defaultVariants: {
|
|
98
|
+
size: "md",
|
|
99
|
+
footer: "plain",
|
|
100
|
+
footerDirection: "row",
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* There is no `description` slot. A description is a muted `Text.Paragraph` in
|
|
106
|
+
* the header's gap column with no layout of its own, and `tv` emits `undefined`
|
|
107
|
+
* for an empty class string — a slot no test could assert against. The part
|
|
108
|
+
* merges the caller's `className` with `cn()` instead, as `BottomSheet`'s does.
|
|
109
|
+
*/
|
|
110
|
+
export type DialogVariantProps = VariantProps<typeof dialogVariants>;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Whether a footer's actions sit side by side or stack.
|
|
114
|
+
*
|
|
115
|
+
* Only the `sm` card stacks: two labelled buttons in a row on a 320pt card
|
|
116
|
+
* leave each label a few characters before it truncates. Pure so the rule is
|
|
117
|
+
* reachable from `bun test`.
|
|
118
|
+
*/
|
|
119
|
+
export function resolveDialogFooterDirection(size: DialogSize): DialogFooterDirection {
|
|
120
|
+
return size === "sm" ? "column" : "row";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The role the card announces. A dialog the user cannot dismiss is an alert
|
|
125
|
+
* dialog: assistive technology tells them only an action will close it.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveDialogRole(isDismissible: boolean): DialogRole {
|
|
128
|
+
return isDismissible ? "dialog" : "alertdialog";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type DialogKeyboardLiftInput = {
|
|
132
|
+
/** The keyboard's height. Either sign — the keyboard controller reports it negative while open. */
|
|
133
|
+
keyboardHeight: number;
|
|
134
|
+
/** The card's top edge in window coordinates, before any lift. */
|
|
135
|
+
cardTop: number;
|
|
136
|
+
/** The card's bottom edge in window coordinates, before any lift. */
|
|
137
|
+
cardBottom: number;
|
|
138
|
+
windowHeight: number;
|
|
139
|
+
/** The top safe-area inset — the card's top never rises past it. */
|
|
140
|
+
topInset: number;
|
|
141
|
+
/** The clearance to keep between the card and the keyboard. */
|
|
142
|
+
margin: number;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* How far to lift the card so the keyboard does not cover it.
|
|
147
|
+
*
|
|
148
|
+
* The smallest lift that keeps the card's bottom `margin` above the keyboard's
|
|
149
|
+
* top — zero when the keyboard is closed or already clear — capped so the
|
|
150
|
+
* card's top never rises under the top safe-area inset, and never negative. A
|
|
151
|
+
* card too tall to fit both ways keeps its top on screen: the title and the
|
|
152
|
+
* field being typed into matter more than the footer.
|
|
153
|
+
*
|
|
154
|
+
* A worklet, because it runs inside the card's animated style on every frame
|
|
155
|
+
* of the keyboard's own animation.
|
|
156
|
+
*/
|
|
157
|
+
export function resolveDialogKeyboardLift({
|
|
158
|
+
keyboardHeight,
|
|
159
|
+
cardTop,
|
|
160
|
+
cardBottom,
|
|
161
|
+
windowHeight,
|
|
162
|
+
topInset,
|
|
163
|
+
margin,
|
|
164
|
+
}: DialogKeyboardLiftInput): number {
|
|
165
|
+
"worklet";
|
|
166
|
+
const keyboard = Math.abs(keyboardHeight);
|
|
167
|
+
if (keyboard === 0 || cardBottom <= 0) return 0;
|
|
168
|
+
|
|
169
|
+
const needed = cardBottom + margin - (windowHeight - keyboard);
|
|
170
|
+
const room = cardTop - topInset;
|
|
171
|
+
|
|
172
|
+
return Math.max(0, Math.min(needed, room));
|
|
173
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export { Dialog, type DialogProps } from "./dialog";
|
|
2
|
+
export { type DialogContextValue, useDialog, useDialogContext } from "./dialog.context";
|
|
3
|
+
export {
|
|
4
|
+
DIALOG_CLOSE_HIT_SLOP,
|
|
5
|
+
DIALOG_ENTER_SCALE,
|
|
6
|
+
DIALOG_ENTER_TRANSLATE_Y,
|
|
7
|
+
DIALOG_FOOTER_VARIANTS,
|
|
8
|
+
DIALOG_KEYBOARD_MARGIN,
|
|
9
|
+
DIALOG_SCRIM_TOKEN,
|
|
10
|
+
DIALOG_SIZE_CLASS,
|
|
11
|
+
DIALOG_SIZES,
|
|
12
|
+
type DialogFooterDirection,
|
|
13
|
+
type DialogFooterVariant,
|
|
14
|
+
type DialogKeyboardLiftInput,
|
|
15
|
+
type DialogRole,
|
|
16
|
+
type DialogSize,
|
|
17
|
+
type DialogVariantProps,
|
|
18
|
+
dialogVariants,
|
|
19
|
+
resolveDialogFooterDirection,
|
|
20
|
+
resolveDialogKeyboardLift,
|
|
21
|
+
resolveDialogRole,
|
|
22
|
+
} from "./dialog.variants";
|
|
23
|
+
export type { DialogBodyProps } from "./dialog-body";
|
|
24
|
+
export type { DialogCloseProps } from "./dialog-close";
|
|
25
|
+
export type { DialogContentProps } from "./dialog-content";
|
|
26
|
+
export type { DialogDescriptionProps } from "./dialog-description";
|
|
27
|
+
export type { DialogFooterProps } from "./dialog-footer";
|
|
28
|
+
export type { DialogHeaderProps } from "./dialog-header";
|
|
29
|
+
export type { DialogTitleProps } from "./dialog-title";
|
|
30
|
+
export type { DialogTriggerProps } from "./dialog-trigger";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Drawer
|
|
2
|
+
|
|
3
|
+
A panel that slides in from an edge and covers the app until dismissed: a navigation menu from the
|
|
4
|
+
start edge, a filter panel from the end, a notifications tray from the top. Compound root plus
|
|
5
|
+
`Drawer.Trigger`, `Drawer.Content`, `Drawer.Header`, `Drawer.Title`, `Drawer.Description`,
|
|
6
|
+
`Drawer.Body`, `Drawer.Footer` and `Drawer.Close`, and `useDrawer()`.
|
|
7
|
+
|
|
8
|
+
`import { Drawer } from "@delacour/react-native-ui/drawer";`
|
|
9
|
+
|
|
10
|
+
It is drawn with the [overlay foundation](../overlay/AGENTS.md) — `Overlay.Portal`,
|
|
11
|
+
`Overlay.Scrim`, `useOverlayPresence`, `useOverlayBackHandler` — so the app has to mount
|
|
12
|
+
`OverlayProvider` once at its root. Without one the drawer draws inline and warns once.
|
|
13
|
+
|
|
14
|
+
## Files
|
|
15
|
+
|
|
16
|
+
| File | What it holds |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `index.ts` | → `@delacour/react-native-ui/drawer` |
|
|
19
|
+
| `drawer.tsx` | Root — the open state, the ids and the focus refs — and the `Object.assign` compound surface |
|
|
20
|
+
| `drawer.context.tsx` | **Leaf.** `DrawerProvider`, `DrawerPanelProvider`, `useDrawer()`, `useDrawerContext()`, `useDrawerPart()`, `useDrawerPanel()` |
|
|
21
|
+
| `drawer-trigger.tsx` | `Drawer.Trigger` — donates the open with `asChild`, keeps the trigger's ref for focus return |
|
|
22
|
+
| `drawer-content.tsx` | `Drawer.Content` — presence, portal, scrim, edge and extent, insets, slide, swipe, back button, focus |
|
|
23
|
+
| `drawer-header.tsx` | `Drawer.Header` — the title column and a ✕ at the trailing edge |
|
|
24
|
+
| `drawer-title.tsx` | `Drawer.Title` — a `Text.Header` carrying the title `nativeID` and the focus ref |
|
|
25
|
+
| `drawer-description.tsx` | `Drawer.Description` — a muted `Text.Paragraph` |
|
|
26
|
+
| `drawer-body.tsx` | `Drawer.Body` — a `ScrollView`, or a plain column with `isScrollable={false}` |
|
|
27
|
+
| `drawer-footer.tsx` | `Drawer.Footer` — a row under a hairline |
|
|
28
|
+
| `drawer-close.tsx` | `Drawer.Close` — the ✕, or with `asChild` any control that closes |
|
|
29
|
+
| `use-drawer-pan.ts` | `useDrawerPan` — the swipe-to-dismiss pan, its threshold haptic and its release |
|
|
30
|
+
| `drawer.variants.ts` | The slotted `tv()`, the size table, the release constants and the pure geometry — no RN imports |
|
|
31
|
+
| `drawer.variants.test.ts` | Edges both directions, extents, frame extents, offsets, drag, release, exit duration, insets, slots per edge, tokens in both themes |
|
|
32
|
+
|
|
33
|
+
## Design
|
|
34
|
+
|
|
35
|
+
- **A side is logical, an edge is physical, and only the edge is ever measured.** `side` is what a
|
|
36
|
+
caller writes — `start`, `end`, `top`, `bottom` — and `resolveDrawerEdge(side, I18nManager.isRTL)`
|
|
37
|
+
turns it into `left`, `right`, `top` or `bottom`. Transforms and a pan's translation are physical
|
|
38
|
+
in React Native whatever the layout direction, so the offset, the drag, the release and the insets
|
|
39
|
+
are all computed against the edge. `useDrawer()` returns both.
|
|
40
|
+
- **The panel is placed in a left-to-right frame, and its content gets the app's direction back.**
|
|
41
|
+
React Native swaps `left`/`right` styles under RTL by default (`swapLeftAndRightInRTL`), and an app
|
|
42
|
+
may turn that off; either way a `left-0` class would mean something different from the
|
|
43
|
+
`translateX` beside it. The `positioner` slot is an absolute fill with `direction: "ltr"`, so the
|
|
44
|
+
panel's `edge` classes, its corners and its safe-area padding are physical, and `inner` sets the
|
|
45
|
+
app's direction again so text, rows and the header's ✕ lay out as everywhere else. That is also why
|
|
46
|
+
the spec's `edge` variant could stay keyed on the physical edge.
|
|
47
|
+
- **The size is a fraction of the window with a cap.** `sm` 62% to 280, `md` 78% to 320, `lg` 88% to
|
|
48
|
+
400, `full` 94% uncapped — of the width for a start/end drawer, of the height for top/bottom. A
|
|
49
|
+
fraction keeps a strip of the app in view to tap away on; the cap keeps a tablet's drawer a drawer.
|
|
50
|
+
The extent is a fixed width or height rather than a measured one, so the first frame already knows
|
|
51
|
+
how far off-screen the panel starts. **The docked edge's inset is added on top**
|
|
52
|
+
(`resolveDrawerFrameExtent`): the size measures the content, not the band under the status bar. A
|
|
53
|
+
top `sm` drawer once spent 62 of its 280pt there and clipped its third row.
|
|
54
|
+
- **Insets go on every side that meets a screen edge.** `resolveDrawerInsets` pads the docked side and
|
|
55
|
+
the two beside it, and nothing on the side facing the app, so a start drawer clears the status bar,
|
|
56
|
+
the home indicator and a landscape notch, and a top drawer does not pad for the home indicator it
|
|
57
|
+
never reaches.
|
|
58
|
+
- **Docked corners square, free corners round.** `bg-popover` — a layer over the app, as `Dialog` and
|
|
59
|
+
`BottomSheet` paint — with `rounded-lg`, the card step, only on the two corners facing the app.
|
|
60
|
+
- **The swipe continues, it does not spring back.** The pan waits for `DRAWER_PAN_ACTIVE_OFFSET` (10pt)
|
|
61
|
+
along the panel's axis and fails on the same travel across it, so a tap reaches a row and a vertical
|
|
62
|
+
scroll in a start/end drawer's body never moves the drawer. Toward the edge the panel follows the
|
|
63
|
+
finger; away it rubber-bands toward 40pt and never reaches it. On release, past 40% of the extent or
|
|
64
|
+
on a fling toward the edge faster than 800 pt/s dismisses — a fling away always restores. A dismiss
|
|
65
|
+
carries on from where the finger left the panel at the release speed (`resolveDrawerExitDuration`,
|
|
66
|
+
80–220 ms, linear) and closes the state only when it lands; the presence exit then runs off-screen
|
|
67
|
+
and unmounts. A restore eases back in the entrance time.
|
|
68
|
+
- **The scrim thins with the drag.** Its opacity is `progress × (1 − dragFraction)`. `Overlay.Scrim`
|
|
69
|
+
takes a writable shared value, so an animated reaction writes the product into one of the drawer's
|
|
70
|
+
own rather than handing it a derived value.
|
|
71
|
+
- **A haptic marks the threshold, nothing else.** `selection` each time the drag crosses 40% — the
|
|
72
|
+
moment letting go changes what happens — in both directions, from the worklet through `playHaptic`.
|
|
73
|
+
None on open.
|
|
74
|
+
- **The pan's worklets live in the hook.** `useDrawerPan` builds them inside a `useMemo`, so they may
|
|
75
|
+
call `drawer.variants`' module-scope worklets; those are flat — a module-scope worklet never calls
|
|
76
|
+
another function — which is why each restates the edge's sign rather than sharing a helper.
|
|
77
|
+
Per-gesture memory (`isDragging`, `isPastThreshold`) is shared values. A touch that never activated
|
|
78
|
+
returns early from `onFinalize`, so a tap during a dismiss cannot restart a restore under it.
|
|
79
|
+
- **Reduce motion fades, and the swipe still works.** Under `useReducedMotion()` the open and close do
|
|
80
|
+
not slide — the panel fades in place on `progress` — but a drag still moves it: that is direct
|
|
81
|
+
manipulation, not animation. `isMotionCalm` does not still it, per the foundation.
|
|
82
|
+
- **`isDismissible={false}` turns off every way out the drawer did not offer** — the scrim (which still
|
|
83
|
+
takes the touch), the back button, the escape gesture and the swipe. `Drawer.Close` still closes it.
|
|
84
|
+
`isSwipeDismissible={false}` turns off the swipe alone.
|
|
85
|
+
- **Every path to closed is one `setOpen(false)`**, and the root drops a set that does not change the
|
|
86
|
+
value, so a scrim tap during a swipe's exit does not report `onOpenChange(false)` twice.
|
|
87
|
+
- **The header writes its own ✕, in the flow.** A row: the title column takes the slack and the ✕
|
|
88
|
+
sits at the trailing edge, so a long title wraps before it and RTL needs no rule. `isCloseHidden`
|
|
89
|
+
drops it. There is no `title` or `description` slot — both are text presets with no layout of their
|
|
90
|
+
own, and `tv` emits `undefined` for an empty slot; the parts merge `className` with `cn()`.
|
|
91
|
+
- **The body scrolls by default and takes the slack, and the footer is `mt-auto`**, so the footer sits
|
|
92
|
+
at the panel's end however short the content — even with no body, which the simulator caught: a
|
|
93
|
+
header-and-footer bottom drawer left its footer floating under the header. A top or bottom drawer's pan shares the vertical axis with a scroll — keep that
|
|
94
|
+
content short, or use a `BottomSheet`.
|
|
95
|
+
- **It draws in the `modal` band**, over the navigator's header and over an open bottom sheet.
|
|
96
|
+
- **Triggers and closes donate the press** with `asChild`, for `BottomSheet.Trigger`'s reason.
|
|
97
|
+
- **Accessibility.** The panel is `accessibilityViewIsModal`, `role="dialog"`, labelled by the title's
|
|
98
|
+
`nativeID`; focus moves to the title once the entrance finishes and back to the trigger once the
|
|
99
|
+
exit does. `onAccessibilityEscape` and the back button close it when dismissible. The scrim is not
|
|
100
|
+
accessible. The ✕ is labelled `"Close"`.
|
|
101
|
+
- **No blur backdrop.** Frosted scrims need `expo-blur`, not a peer; a follow-up for every overlay.
|
|
102
|
+
|
|
103
|
+
## Preview capture (not yet shot)
|
|
104
|
+
|
|
105
|
+
No demo is marked `capture` until a capture tool is available. When one is, these are the settings,
|
|
106
|
+
and the flows are in `.argent/flows/previews/drawer/`:
|
|
107
|
+
|
|
108
|
+
| Demo | Capture |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| `navigation` | `{ flow: "drawer/navigation", frame: "device", hero: true }` |
|
|
111
|
+
| `filters` | `{ flow: "drawer/filters", frame: "device" }` |
|
|
112
|
+
| `sizes` | `{ flow: "drawer/sizes", frame: "device" }` |
|
|
113
|
+
| `notifications` | `{ flow: "drawer/notifications", frame: "device" }` |
|
|
114
|
+
|
|
115
|
+
## Testing
|
|
116
|
+
|
|
117
|
+
`bun test` reaches the edge resolution in both directions, the extents on a small and a wide window, the frame extent with the docked inset,
|
|
118
|
+
the offset, the drag and its rubber band, the drag fraction, the release (threshold, fling toward,
|
|
119
|
+
fling away), the exit duration, the insets per edge and the slots per edge with their tokens in both
|
|
120
|
+
themes. All four sides from every dismissal path, the swipe's continue-and-restore, the scrolling
|
|
121
|
+
body, the z-order over a sheet, VoiceOver and reduce motion are verified on a simulator through
|
|
122
|
+
`/drawer` in the playground. RTL needs an app restarted with `I18nManager.forceRTL(true)`; the `rtl`
|
|
123
|
+
demo shows the resolution the drawer will make.
|