@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,176 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { fieldVariants, resolveFieldTextColor } from "../field/field.variants";
|
|
3
|
+
import { TEXT_COLORS } from "../text/text.variants";
|
|
4
|
+
import {
|
|
5
|
+
LABEL_REQUIRED_MARK,
|
|
6
|
+
LABEL_REQUIRED_MARK_COLOR,
|
|
7
|
+
labelText,
|
|
8
|
+
labelVariants,
|
|
9
|
+
resolveLabelAccessibilityLabel,
|
|
10
|
+
resolveLabelColor,
|
|
11
|
+
} from "./label.variants";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A slot's class string, with `tv`'s empty-slot `undefined` flattened.
|
|
15
|
+
*
|
|
16
|
+
* Both slots are handed to a `Text` and carry state only, so they are empty most
|
|
17
|
+
* of the time — and a bare `not.toMatch()` throws on `undefined` rather than
|
|
18
|
+
* passing.
|
|
19
|
+
*/
|
|
20
|
+
function cls(value: string | undefined): string {
|
|
21
|
+
return value ?? "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SLOTS = ["root", "requiredMark"] as const;
|
|
25
|
+
|
|
26
|
+
describe("the slots", () => {
|
|
27
|
+
// The type scale belongs to `Text.Label`. A size or a weight here would be a
|
|
28
|
+
// second definition of it that could drift from the preset.
|
|
29
|
+
test("restate no type scale in any state", () => {
|
|
30
|
+
for (const isDisabled of [false, true]) {
|
|
31
|
+
for (const isInvalid of [false, true]) {
|
|
32
|
+
const slots = labelVariants({ isDisabled, isInvalid });
|
|
33
|
+
for (const slot of SLOTS) {
|
|
34
|
+
expect(cls(slots[slot]())).not.toMatch(/\btext-(xs|sm|base|lg|xl|2xl|3xl)\b/);
|
|
35
|
+
expect(cls(slots[slot]())).not.toMatch(/\bfont-(normal|medium|semibold|bold)\b/);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Colour rides `Text`'s own axis, so a caller's `color` beats it through the
|
|
42
|
+
// same channel rather than through a tailwind-merge race.
|
|
43
|
+
test("carry no colour, since that rides the preset's own axis", () => {
|
|
44
|
+
for (const isInvalid of [false, true]) {
|
|
45
|
+
const slots = labelVariants({ isInvalid });
|
|
46
|
+
for (const slot of SLOTS) {
|
|
47
|
+
expect(cls(slots[slot]())).not.toMatch(/\btext-\w+-?\w*\b/);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("merge an incoming className last", () => {
|
|
53
|
+
expect(labelVariants({ isDisabled: true }).root({ className: "opacity-80" })).toContain("opacity-80");
|
|
54
|
+
expect(labelVariants({ isDisabled: true }).root({ className: "opacity-80" })).not.toContain("opacity-50");
|
|
55
|
+
expect(labelVariants().requiredMark({ className: "ml-1" })).toContain("ml-1");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("a disabled label", () => {
|
|
60
|
+
test("fades", () => {
|
|
61
|
+
expect(cls(labelVariants({ isDisabled: true }).root())).toContain("opacity-50");
|
|
62
|
+
expect(cls(labelVariants({ isDisabled: false }).root())).not.toContain("opacity-50");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// The mark is a nested run inside the label's own text, so the root's opacity
|
|
66
|
+
// already reaches it. Fading it again would square the fade.
|
|
67
|
+
test("does not fade the required mark a second time", () => {
|
|
68
|
+
expect(cls(labelVariants({ isDisabled: true }).requiredMark())).not.toContain("opacity");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("is enabled by default", () => {
|
|
72
|
+
expect(labelVariants().root()).toBe(labelVariants({ isDisabled: false }).root());
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("resolveLabelColor", () => {
|
|
77
|
+
test("turns destructive only while invalid", () => {
|
|
78
|
+
expect(resolveLabelColor(true)).toBe("destructive");
|
|
79
|
+
expect(resolveLabelColor(false)).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// `undefined` means "leave the preset's own colour alone" — `Text`'s colour
|
|
83
|
+
// axis emits nothing when it is not named.
|
|
84
|
+
test("names a colour Text actually has", () => {
|
|
85
|
+
expect(TEXT_COLORS).toContain(resolveLabelColor(true) ?? "default");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("the required mark", () => {
|
|
90
|
+
test("is an asterisk", () => {
|
|
91
|
+
expect(LABEL_REQUIRED_MARK).toBe("*");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Required is a warning about what the form will refuse, so it is drawn in
|
|
95
|
+
// the colour of the refusal whether or not the field is currently wrong.
|
|
96
|
+
test("is destructive, and a colour Text has", () => {
|
|
97
|
+
expect(LABEL_REQUIRED_MARK_COLOR).toBe("destructive");
|
|
98
|
+
expect(TEXT_COLORS).toContain(LABEL_REQUIRED_MARK_COLOR);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("labelText", () => {
|
|
103
|
+
test("reads a string or a number", () => {
|
|
104
|
+
expect(labelText("Email")).toBe("Email");
|
|
105
|
+
expect(labelText(42)).toBe("42");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("joins an array of strings, the shape `{name} address` compiles to", () => {
|
|
109
|
+
expect(labelText(["Work", " ", "email"])).toBe("Work email");
|
|
110
|
+
expect(labelText(["Line ", 2])).toBe("Line 2");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// The holes a conditional child leaves behind are not text.
|
|
114
|
+
test("skips null, undefined and booleans inside an array", () => {
|
|
115
|
+
expect(labelText(["Email", null, undefined, false, true])).toBe("Email");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// An element could be anything — an icon, a link — and guessing at its text
|
|
119
|
+
// would put words in a screen reader's mouth the caller never wrote.
|
|
120
|
+
test("gives up on anything it cannot read as text", () => {
|
|
121
|
+
expect(labelText({ type: "Icon" })).toBeUndefined();
|
|
122
|
+
expect(labelText(["Email", { type: "Icon" }])).toBeUndefined();
|
|
123
|
+
expect(labelText(undefined)).toBeUndefined();
|
|
124
|
+
expect(labelText(null)).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("treats empty text as nothing to read", () => {
|
|
128
|
+
expect(labelText("")).toBeUndefined();
|
|
129
|
+
expect(labelText(["", null])).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("resolveLabelAccessibilityLabel", () => {
|
|
134
|
+
// A nested asterisk reads as "star" — so a required label is announced by
|
|
135
|
+
// what it means instead of by its punctuation.
|
|
136
|
+
test("reads a required label as required, not as an asterisk", () => {
|
|
137
|
+
expect(resolveLabelAccessibilityLabel({ children: "Email", isRequired: true })).toBe("Email, required");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("leaves an optional label to the text itself", () => {
|
|
141
|
+
expect(resolveLabelAccessibilityLabel({ children: "Email", isRequired: false })).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("lets the caller's own label win", () => {
|
|
145
|
+
expect(
|
|
146
|
+
resolveLabelAccessibilityLabel({ accessibilityLabel: "Work email", children: "Email", isRequired: true })
|
|
147
|
+
).toBe("Work email");
|
|
148
|
+
expect(
|
|
149
|
+
resolveLabelAccessibilityLabel({ accessibilityLabel: "Work email", children: "Email", isRequired: false })
|
|
150
|
+
).toBe("Work email");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// With children it cannot read, the text stays whatever the platform makes
|
|
154
|
+
// of it rather than becoming a bare "required" with the name dropped.
|
|
155
|
+
test("says nothing when it cannot read the children", () => {
|
|
156
|
+
expect(resolveLabelAccessibilityLabel({ children: { type: "Icon" }, isRequired: true })).toBeUndefined();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// `Field.Label` is the field-aware counterpart of `Label`, styled from
|
|
161
|
+
// `fieldVariants`. A label in a field and a label outside one must not read as
|
|
162
|
+
// two different components, so the decision written twice is held together by a
|
|
163
|
+
// test rather than by a comment hoping it stays so.
|
|
164
|
+
describe("parity with Field", () => {
|
|
165
|
+
test("colours the label the way Field's resolver does", () => {
|
|
166
|
+
for (const isInvalid of [false, true]) {
|
|
167
|
+
expect(resolveLabelColor(isInvalid)).toBe(resolveFieldTextColor("label", isInvalid));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("fades the label the way Field's slot does", () => {
|
|
172
|
+
for (const isDisabled of [false, true]) {
|
|
173
|
+
expect(cls(labelVariants({ isDisabled }).root())).toBe(cls(fieldVariants({ isDisabled }).label()));
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
import type { TextColor } from "../text/text.variants";
|
|
4
|
+
|
|
5
|
+
/** What a required label appends. */
|
|
6
|
+
export const LABEL_REQUIRED_MARK = "*";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The required mark's colour, in every state.
|
|
10
|
+
*
|
|
11
|
+
* Required is a warning about what the form will refuse, so it is drawn in the
|
|
12
|
+
* colour of that refusal whether or not the value is wrong right now.
|
|
13
|
+
*/
|
|
14
|
+
export const LABEL_REQUIRED_MARK_COLOR: TextColor = "destructive";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* State for a form label and the required mark inside it.
|
|
18
|
+
*
|
|
19
|
+
* Two slots because the mark is a second `Text` nested in the label's own, not
|
|
20
|
+
* because either carries a style of its own: **no slot here holds a type scale
|
|
21
|
+
* or a colour.** The label renders `Text.Label` and passes a colour through
|
|
22
|
+
* {@link resolveLabelColor}, so the scale lives in exactly one place — a
|
|
23
|
+
* `text-sm font-medium` written here would be a second definition of
|
|
24
|
+
* `Text.Label` that could drift from it. The mark inherits that scale by
|
|
25
|
+
* nesting.
|
|
26
|
+
*
|
|
27
|
+
* Only the root fades. The mark is a run inside the root's text, so the root's
|
|
28
|
+
* opacity already reaches it; fading it again would square the fade.
|
|
29
|
+
*
|
|
30
|
+
* Free of React Native imports so it stays unit-testable — `bun test` cannot
|
|
31
|
+
* parse React Native's Flow-typed source. See AGENTS.md.
|
|
32
|
+
*/
|
|
33
|
+
export const labelVariants = tv({
|
|
34
|
+
slots: {
|
|
35
|
+
root: "",
|
|
36
|
+
requiredMark: "",
|
|
37
|
+
},
|
|
38
|
+
variants: {
|
|
39
|
+
// The empty `false` branches are load-bearing typing, not placeholders.
|
|
40
|
+
// `tv` derives the prop type from the declared keys, so a map with only
|
|
41
|
+
// `true` types the prop as `true` rather than `boolean`. See the note in
|
|
42
|
+
// button.variants.ts.
|
|
43
|
+
isInvalid: { true: {}, false: {} },
|
|
44
|
+
isDisabled: { true: { root: "opacity-50" }, false: {} },
|
|
45
|
+
},
|
|
46
|
+
defaultVariants: {
|
|
47
|
+
isInvalid: false,
|
|
48
|
+
isDisabled: false,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The `Text` colour a label takes.
|
|
54
|
+
*
|
|
55
|
+
* Returning `undefined` is meaningful rather than lazy: `Text`'s `color` axis
|
|
56
|
+
* emits nothing when it is not named, so the label falls through to
|
|
57
|
+
* `Text.Label`'s own `text-foreground` without this module knowing which token
|
|
58
|
+
* the preset chose.
|
|
59
|
+
*
|
|
60
|
+
* Pure, so the matrix is reachable from `bun test`. See AGENTS.md.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveLabelColor(isInvalid: boolean): TextColor | undefined {
|
|
63
|
+
return isInvalid ? "destructive" : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The plain text a label's children spell, or `undefined` when they are
|
|
68
|
+
* anything but text.
|
|
69
|
+
*
|
|
70
|
+
* Strings and numbers are read, and an array of them is joined — that is what
|
|
71
|
+
* `{name} address` compiles to. The `null`, `undefined` and booleans a
|
|
72
|
+
* conditional child leaves behind are skipped. An element is not guessed at: it
|
|
73
|
+
* could be an icon or a link, and inventing its words would put something in a
|
|
74
|
+
* screen reader's mouth the caller never wrote.
|
|
75
|
+
*
|
|
76
|
+
* Typed on `unknown` rather than `ReactNode` so this module takes not even a
|
|
77
|
+
* type import from React, which is what keeps it reachable from `bun test`.
|
|
78
|
+
*/
|
|
79
|
+
export function labelText(children: unknown): string | undefined {
|
|
80
|
+
const parts = Array.isArray(children) ? children : [children];
|
|
81
|
+
let text = "";
|
|
82
|
+
|
|
83
|
+
for (const part of parts) {
|
|
84
|
+
if (part === null || part === undefined || typeof part === "boolean") continue;
|
|
85
|
+
if (typeof part === "string" || typeof part === "number") {
|
|
86
|
+
text += String(part);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return text === "" ? undefined : text;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* What a screen reader announces for a label.
|
|
97
|
+
*
|
|
98
|
+
* The caller's own `accessibilityLabel` always wins. Otherwise a required label
|
|
99
|
+
* whose children are text is announced as `"Email, required"`: the mark is a
|
|
100
|
+
* nested `Text`, and React Native reads a `Text` and its nested runs as one
|
|
101
|
+
* string, so without this VoiceOver says "Email, star". An optional label, or
|
|
102
|
+
* one whose children cannot be read as text, returns `undefined` and is left to
|
|
103
|
+
* the platform.
|
|
104
|
+
*
|
|
105
|
+
* Pure, so the matrix is reachable from `bun test`. See AGENTS.md.
|
|
106
|
+
*/
|
|
107
|
+
export function resolveLabelAccessibilityLabel({
|
|
108
|
+
accessibilityLabel,
|
|
109
|
+
children,
|
|
110
|
+
isRequired,
|
|
111
|
+
}: {
|
|
112
|
+
accessibilityLabel?: string;
|
|
113
|
+
children: unknown;
|
|
114
|
+
isRequired: boolean;
|
|
115
|
+
}): string | undefined {
|
|
116
|
+
if (accessibilityLabel !== undefined) return accessibilityLabel;
|
|
117
|
+
if (!isRequired) return undefined;
|
|
118
|
+
|
|
119
|
+
const text = labelText(children);
|
|
120
|
+
return text === undefined ? undefined : `${text}, required`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type LabelVariantProps = VariantProps<typeof labelVariants>;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Menu
|
|
2
|
+
|
|
3
|
+
A list of **verbs** dropped from a control that exists to open it — a ⋯ button,
|
|
4
|
+
a toolbar item. Rename, duplicate, share, delete. A menu is not a select: its
|
|
5
|
+
rows run actions, they never become the trigger's value. Compound root plus
|
|
6
|
+
`Menu.Trigger`, `Menu.Content`, `Menu.Background`, `Menu.Label`, `Menu.Item`,
|
|
7
|
+
`Menu.CheckboxItem`, `Menu.RadioGroup`, `Menu.RadioItem`, `Menu.Separator`,
|
|
8
|
+
`Menu.Sub`, `Menu.SubTrigger` and `Menu.SubContent`.
|
|
9
|
+
|
|
10
|
+
`import { Menu } from "@delacour/react-native-ui/menu";`
|
|
11
|
+
|
|
12
|
+
## Files
|
|
13
|
+
|
|
14
|
+
| File | What it holds |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `index.ts` | → `@delacour/react-native-ui/menu` |
|
|
17
|
+
| `menu.tsx` | Root — `useMenuRootValue` into `MenuRootProvider`; the `Object.assign` |
|
|
18
|
+
| `menu-trigger.tsx` | `Menu.Trigger` — measures itself and toggles; `asChild` donates the press |
|
|
19
|
+
| `menu-content.tsx` | `Menu.Content` — the transparent `Modal`, the outside-tap layer, the placed and animated panel, the scroller |
|
|
20
|
+
| `menu-background.tsx` | `Menu.Background` — the `bg-popover` surface behind the scroller |
|
|
21
|
+
| `menu-label.tsx` | `Menu.Label` — a section heading |
|
|
22
|
+
| `menu-item.tsx` | `MenuRow`, the row every row is built on, and `Menu.Item` |
|
|
23
|
+
| `menu-checkbox-item.tsx` | `Menu.CheckboxItem` |
|
|
24
|
+
| `menu-radio-group.tsx` | `Menu.RadioGroup` — holds the value |
|
|
25
|
+
| `menu-radio-item.tsx` | `Menu.RadioItem` |
|
|
26
|
+
| `menu-separator.tsx` | `Menu.Separator` — `Separator` with `my-1` |
|
|
27
|
+
| `menu-sub.tsx` | `Menu.Sub` — the submenu's open state, `progress` and measured height |
|
|
28
|
+
| `menu-sub-trigger.tsx` | `Menu.SubTrigger` — the row with the turning chevron |
|
|
29
|
+
| `menu-sub-content.tsx` | `Menu.SubContent` — the measured clip |
|
|
30
|
+
| `menu.context.tsx` | The menu, submenu and radio-group contexts; `useMenu`, `useMenuRootValue`, `MenuRootProvider` |
|
|
31
|
+
| `menu.types.ts` | `MenuItemProps`, shared by every row |
|
|
32
|
+
| `menu.variants.ts` | The slotted `tv()`, the constants and the pure resolvers — no RN imports |
|
|
33
|
+
| `menu.variants.test.ts` | Placement (flip, cap, clamp, RTL, the zero-size anchor), width, close rules, accessibility, tokens |
|
|
34
|
+
|
|
35
|
+
## The panel
|
|
36
|
+
|
|
37
|
+
- **It is a minimal anchored panel built here, not a Popover.** There is no
|
|
38
|
+
Popover in the package yet. `Menu.Content` renders inside a transparent RN
|
|
39
|
+
`Modal` — top-most layer, Android back through `onRequestClose`, screen-reader
|
|
40
|
+
containment — so it needs no new peer. **When Popover lands, Menu.Content moves
|
|
41
|
+
onto it and `resolveMenuPlacement` moves with it.** The resolver is exported
|
|
42
|
+
for exactly that.
|
|
43
|
+
- **Placement takes a rect, never a ref.** `resolveMenuPlacement({ anchor, content,
|
|
44
|
+
window, insets, placement, align, offset, isRTL })` places against
|
|
45
|
+
`{ x, y, width, height }`, so a zero-size point — ContextMenu's long-press —
|
|
46
|
+
goes through the same arithmetic with no special case, and a test proves it.
|
|
47
|
+
- **Flip, then cap.** The requested side wins when the content fits; otherwise
|
|
48
|
+
the other side if it fits there; otherwise the larger side, and `maxHeight`
|
|
49
|
+
caps it so the rows scroll. Room is measured inside the safe area plus an 8pt
|
|
50
|
+
margin, and the panel is clamped horizontally inside the same bounds. `align`
|
|
51
|
+
start and end swap under RTL.
|
|
52
|
+
- **Measure, place, show.** The panel's first frame is at opacity 0; the
|
|
53
|
+
scroller's `onContentSizeChange` reports the rows' height, placement runs with
|
|
54
|
+
it, and only then does the enter start — there is no jump.
|
|
55
|
+
- **The side is locked for the visit.** A submenu expanding mid-visit must not
|
|
56
|
+
flip the panel under the finger. A panel above the anchor is positioned by
|
|
57
|
+
its **bottom** edge (`MenuPlacementResult.bottom`), so it grows upward, away
|
|
58
|
+
from the anchor, with nothing re-placed; past the cap the rows scroll.
|
|
59
|
+
- **Its surface is `bg-popover`, never `bg-overlay`.** `overlay` is the
|
|
60
|
+
translucent scrim colour (`oklch(0 0 0 / 45%)`); a panel in it is a
|
|
61
|
+
see-through card. `hasScrim` (ContextMenu sets it) draws `bg-overlay` behind
|
|
62
|
+
the panel, fading with it.
|
|
63
|
+
- **The shadow is on an outer layer with no clip.** iOS clips a shadow to a
|
|
64
|
+
clipping view's bounds, so `panel` casts `shadow-lg` and the inner `content`
|
|
65
|
+
slot carries the corner, the hairline and `overflow-hidden`.
|
|
66
|
+
- **A caller's `Menu.Background` is lifted out by type** and drawn behind the
|
|
67
|
+
scroller, so it stays put while the rows scroll. The default is drawn when
|
|
68
|
+
there is none.
|
|
69
|
+
- **Enter 160ms, exit 120ms**, opacity with a 0.96 scale from the anchor's side.
|
|
70
|
+
React Native has no animatable transform origin on both platforms, so
|
|
71
|
+
`resolveMenuOrigin` approximates it with a translate that falls to zero as the
|
|
72
|
+
panel arrives. Under reduced motion it is opacity alone — the panel still
|
|
73
|
+
appears, so the information is not lost. The `Modal` unmounts when the exit's
|
|
74
|
+
timing finishes; a reopen mid-exit cancels it and runs the enter instead.
|
|
75
|
+
- **Gesture Handler needs its own root inside a `Modal`.** On Android a `Modal`
|
|
76
|
+
is a separate window the app's root view cannot see into, so every tap in the
|
|
77
|
+
panel would be dead. `Menu.Content` wraps its tree in a `GestureHandlerRootView`.
|
|
78
|
+
|
|
79
|
+
## Rows
|
|
80
|
+
|
|
81
|
+
- **One row, `MenuRow`, under every row kind.** `Menu.Item`, `CheckboxItem`,
|
|
82
|
+
`RadioItem` and `SubTrigger` are `MenuRow` with a `kind`, a leading override
|
|
83
|
+
and an `accessibilityState`. Four hand-rolled rows would be four press fills
|
|
84
|
+
that drift.
|
|
85
|
+
- **The press fill is the row's background.** An absolute `bg-accent` layer
|
|
86
|
+
(`bg-destructive-soft` on a destructive row) fades in over 90ms and out over
|
|
87
|
+
160ms while the row dips to 0.98 — one shared value for both. There is no
|
|
88
|
+
wash or ripple on top. A row is built on its own Gesture Handler tap rather
|
|
89
|
+
than `Pressable`, because `Pressable` owns its `pressed` value privately and
|
|
90
|
+
the fill has to read it.
|
|
91
|
+
- **Under reduced motion the fill snaps and the scale does not run.** The
|
|
92
|
+
scale is movement and goes; the fill is colour, and it is what says the tap
|
|
93
|
+
landed, so it stays — instantly.
|
|
94
|
+
- **A tap that travels is a scroll.** The tap fails past 10pt
|
|
95
|
+
(`MENU_TAP_MAX_DISTANCE`), so a long menu scrolls without choosing the row the
|
|
96
|
+
finger started on.
|
|
97
|
+
- **Rows take `ViewProps`, not `PressableProps`.** The spec sketched
|
|
98
|
+
`Omit<PressableProps, …>`, but a row ignores `feedback`, `asChild`, `busy` and
|
|
99
|
+
the rest; a type that accepts props it drops is a quiet bug. `isDisabled` is
|
|
100
|
+
the row's own.
|
|
101
|
+
- **Close rules live in one resolver.** `resolveCloseOnSelect(kind, explicit?)`:
|
|
102
|
+
item and radio close, checkbox and sub-trigger stay open; an explicit
|
|
103
|
+
`isClosedOnSelect` wins. `resolveRadioNext` pins that re-choosing a selected
|
|
104
|
+
radio keeps it — a radio never clears.
|
|
105
|
+
- **A checkbox's tick and a radio's mark take the leading icon column**, so the
|
|
106
|
+
labels of every row line up; an `icon` on those rows moves to the trailing
|
|
107
|
+
edge. `isInset` reserves the same column on a plain row, and on a `Menu.Label`.
|
|
108
|
+
- **The icon column is 18pt, `size-icon-md`.** The spec named `size-icon-sm`,
|
|
109
|
+
which is 16pt in `tokens.css`; the 18pt it also named wins, since that is the
|
|
110
|
+
footprint `isInset` and the label's `ps-[42px]` (12 + 18 + 12) are built on.
|
|
111
|
+
- **Haptics are the root's `haptic`, played on select**, from the tap's worklet,
|
|
112
|
+
and off by default.
|
|
113
|
+
|
|
114
|
+
## Submenus
|
|
115
|
+
|
|
116
|
+
- **They expand in place, never fly out.** A phone has no room beside the
|
|
117
|
+
panel, and a second layer over the first hides where the user came from.
|
|
118
|
+
`Menu.SubContent` is `Collapsible.Content`'s measured clip — restated, not
|
|
119
|
+
imported, so `delacour add menu` copies one folder — and `MENU_SUB_SPRING` is
|
|
120
|
+
`Collapsible`'s spring.
|
|
121
|
+
- **The chevron turns 90° off the submenu's own `progress`**, so it and the
|
|
122
|
+
height cannot drift. Under RTL it is mirrored with `scaleX: -1` on a wrapper
|
|
123
|
+
view, so it points to the trailing edge and still turns downward; the
|
|
124
|
+
rotation lives on the inner view.
|
|
125
|
+
- **Nothing on the JS thread reads `contentHeight.value`** — the accordion's
|
|
126
|
+
Release-build bug. "Measured" is React state.
|
|
127
|
+
|
|
128
|
+
## State
|
|
129
|
+
|
|
130
|
+
- **`isOpen` / `defaultOpen` / `onOpenChange` through `useControllableState`**,
|
|
131
|
+
in `useMenuRootValue`. `open(anchor?)` opens at a rect, or measures the
|
|
132
|
+
trigger first; `close()`; `toggle()`. Callbacks are ref-backed so the context
|
|
133
|
+
keeps its identity.
|
|
134
|
+
- **A controlled open is measured too.** A parent flipping `isOpen` to `true`
|
|
135
|
+
without a trigger press still gets the trigger measured, by an effect that
|
|
136
|
+
tells such an open from one `open()` started.
|
|
137
|
+
- **`Menu.Trigger asChild` donates the press**, `BottomSheet.Trigger`'s rule: a
|
|
138
|
+
`Button` wrapped in a pressable trigger would win the touch and the menu
|
|
139
|
+
would never open. The child gets `onPress`, the measured ref and the
|
|
140
|
+
accessibility props through `Slot`.
|
|
141
|
+
|
|
142
|
+
## Accessibility
|
|
143
|
+
|
|
144
|
+
- Content: `accessibilityRole="menu"`, `accessibilityViewIsModal`.
|
|
145
|
+
- Rows: `accessibilityRole="menuitem"`; checkbox → `checked`, radio →
|
|
146
|
+
`selected`, sub-trigger → `expanded`, all → `disabled`. One source:
|
|
147
|
+
`resolveMenuItemAccessibility`, a discriminated union on `kind`.
|
|
148
|
+
- Trigger: `accessibilityState={{ expanded }}`, hint "Opens a menu".
|
|
149
|
+
- A closed submenu is out of the accessibility tree, not just clipped.
|
|
150
|
+
|
|
151
|
+
## Reuse contract for ContextMenu
|
|
152
|
+
|
|
153
|
+
ContextMenu renders this component's rows and panel rather than a copy:
|
|
154
|
+
|
|
155
|
+
- **Import leaves, never `../menu`.** `../menu/menu-item` (`MenuRow`,
|
|
156
|
+
`MenuItem`), `../menu/menu-content`, `../menu/menu.context` and
|
|
157
|
+
`../menu/menu.variants` are each importable on their own. None imports
|
|
158
|
+
`./menu`, so no cycle can close (rule 3).
|
|
159
|
+
- **Drive the state with your own anchor.** Build a value with
|
|
160
|
+
`useMenuRootValue(...)`, wrap your tree in `MenuRootProvider`, and call
|
|
161
|
+
`open({ x, y, width: 0, height: 0 })` from the long-press. Or pass
|
|
162
|
+
`anchor` straight to `Menu.Content`.
|
|
163
|
+
- **`hasScrim`** tints the screen behind the panel; **`iconPlacement="trailing"`**
|
|
164
|
+
puts a row's icon at its end.
|
|
165
|
+
- **`backdrop`** draws a node inside the `Modal`, over the scrim and behind the
|
|
166
|
+
panel, fading with them and taking no touches. A `Modal` covers everything,
|
|
167
|
+
so anything meant to show above the scrim — ContextMenu's lifted preview — has
|
|
168
|
+
to be inside it; outside, it would sit under the tint.
|
|
169
|
+
|
|
170
|
+
## Out of scope
|
|
171
|
+
|
|
172
|
+
- **`presentation="bottom-sheet"`.** Importing `../bottom-sheet` would force the
|
|
173
|
+
optional sheet engine and `react-native-teleport` on every Menu consumer. A
|
|
174
|
+
follow-up.
|
|
175
|
+
- **Keyboard navigation and focus return on web.**
|
|
176
|
+
- **Typeahead, and rows that open a second panel.**
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export { Menu, type MenuProps } from "./menu";
|
|
2
|
+
export {
|
|
3
|
+
type MenuContextValue,
|
|
4
|
+
type MenuMeasurable,
|
|
5
|
+
MenuRootProvider,
|
|
6
|
+
type UseMenuRootValueOptions,
|
|
7
|
+
useMenu,
|
|
8
|
+
useMenuRootValue,
|
|
9
|
+
} from "./menu.context";
|
|
10
|
+
export type { MenuItemProps } from "./menu.types";
|
|
11
|
+
export {
|
|
12
|
+
MENU_ALIGNS,
|
|
13
|
+
MENU_DEFAULT_MIN_WIDTH,
|
|
14
|
+
MENU_DEFAULT_OFFSET,
|
|
15
|
+
MENU_EDGE_MARGIN,
|
|
16
|
+
MENU_ITEM_VARIANTS,
|
|
17
|
+
MENU_PLACEMENTS,
|
|
18
|
+
MENU_RADIO_INDICATORS,
|
|
19
|
+
type MenuAlign,
|
|
20
|
+
type MenuAnchorRect,
|
|
21
|
+
type MenuInsets,
|
|
22
|
+
type MenuItemVariant,
|
|
23
|
+
type MenuPlacement,
|
|
24
|
+
type MenuPlacementInput,
|
|
25
|
+
type MenuPlacementResult,
|
|
26
|
+
type MenuRadioIndicator,
|
|
27
|
+
type MenuVariantProps,
|
|
28
|
+
menuVariants,
|
|
29
|
+
resolveCloseOnSelect,
|
|
30
|
+
resolveMenuPlacement,
|
|
31
|
+
resolveMenuWidth,
|
|
32
|
+
resolveRadioNext,
|
|
33
|
+
} from "./menu.variants";
|
|
34
|
+
export type { MenuBackgroundProps } from "./menu-background";
|
|
35
|
+
export type { MenuCheckboxItemProps } from "./menu-checkbox-item";
|
|
36
|
+
export type { MenuContentProps } from "./menu-content";
|
|
37
|
+
export type { MenuLabelProps } from "./menu-label";
|
|
38
|
+
export type { MenuRadioGroupProps } from "./menu-radio-group";
|
|
39
|
+
export type { MenuRadioItemProps } from "./menu-radio-item";
|
|
40
|
+
export type { MenuSeparatorProps } from "./menu-separator";
|
|
41
|
+
export type { MenuSubProps } from "./menu-sub";
|
|
42
|
+
export type { MenuSubContentProps } from "./menu-sub-content";
|
|
43
|
+
export type { MenuSubTriggerProps } from "./menu-sub-trigger";
|
|
44
|
+
export type { MenuTriggerProps } from "./menu-trigger";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { menuVariants } from "./menu.variants";
|
|
4
|
+
|
|
5
|
+
export type MenuBackgroundProps = ViewProps & { className?: string };
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The panel's surface: an absolute-fill layer in `bg-popover`, behind the rows.
|
|
9
|
+
*
|
|
10
|
+
* `Menu.Content` draws one by default. Pass your own as a child of
|
|
11
|
+
* `Menu.Content` to repaint it — the content lifts it out of the children and
|
|
12
|
+
* renders it outside the scroller, where it stays put while the rows scroll.
|
|
13
|
+
*/
|
|
14
|
+
export function MenuBackground({ className, ...props }: MenuBackgroundProps): ReactElement {
|
|
15
|
+
return <View className={menuVariants().background({ className })} pointerEvents="none" {...props} />;
|
|
16
|
+
}
|
|
17
|
+
MenuBackground.displayName = "DelacourUI.Menu.Background";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { useControllableState } from "../../hooks/use-controllable-state";
|
|
4
|
+
import { IconCheckmark1 } from "../../icons/central";
|
|
5
|
+
import { Icon } from "../icon";
|
|
6
|
+
import type { MenuItemProps } from "./menu.types";
|
|
7
|
+
import { MENU_INDICATOR_TOKEN, menuVariants, resolveMenuItemAccessibility } from "./menu.variants";
|
|
8
|
+
import { MenuRow } from "./menu-item";
|
|
9
|
+
|
|
10
|
+
export type MenuCheckboxItemProps = Omit<MenuItemProps, "onSelect" | "isClosedOnSelect"> & {
|
|
11
|
+
isChecked?: boolean;
|
|
12
|
+
defaultChecked?: boolean;
|
|
13
|
+
onCheckedChange?: (isChecked: boolean) => void;
|
|
14
|
+
/** Whether choosing the row closes the menu. Default `false` — several are usually toggled in one visit. */
|
|
15
|
+
isClosedOnSelect?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A row that toggles a setting, with a tick in the icon column while it is on.
|
|
20
|
+
*
|
|
21
|
+
* Stays open on select by default. The tick takes the leading column, so an
|
|
22
|
+
* `icon` moves to the row's end. Announced as `checked`.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* <Menu.CheckboxItem isChecked={showDone} onCheckedChange={setShowDone}>Completed</Menu.CheckboxItem>
|
|
26
|
+
*/
|
|
27
|
+
export function MenuCheckboxItem({
|
|
28
|
+
isChecked: isCheckedProp,
|
|
29
|
+
defaultChecked = false,
|
|
30
|
+
onCheckedChange,
|
|
31
|
+
isDisabled = false,
|
|
32
|
+
...props
|
|
33
|
+
}: MenuCheckboxItemProps): ReactElement {
|
|
34
|
+
const [isChecked, setChecked] = useControllableState<boolean>({
|
|
35
|
+
defaultValue: defaultChecked,
|
|
36
|
+
onChange: onCheckedChange,
|
|
37
|
+
value: isCheckedProp,
|
|
38
|
+
});
|
|
39
|
+
const slots = menuVariants();
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<MenuRow
|
|
43
|
+
accessibilityState={resolveMenuItemAccessibility({ isChecked, isDisabled, kind: "checkbox" })}
|
|
44
|
+
isDisabled={isDisabled}
|
|
45
|
+
kind="checkbox"
|
|
46
|
+
leading={
|
|
47
|
+
<View className={slots.indicator()}>
|
|
48
|
+
{isChecked ? <Icon className={slots.itemIcon()} color={MENU_INDICATOR_TOKEN} icon={IconCheckmark1} /> : null}
|
|
49
|
+
</View>
|
|
50
|
+
}
|
|
51
|
+
onSelect={() => setChecked(!isChecked)}
|
|
52
|
+
{...props}
|
|
53
|
+
iconPlacement="trailing"
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
MenuCheckboxItem.displayName = "DelacourUI.Menu.CheckboxItem";
|