@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
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ arrive with its classNames already dead.
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
bun add @delacour/react-native-ui
|
|
14
|
+
bun add @delacour/react-native-ui
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Native modules are **peer dependencies**, because two copies of a native module
|
|
@@ -19,14 +19,16 @@ register twice and break at runtime. The complete list — native modules throug
|
|
|
19
19
|
`expo install`, the rest through your package manager — is derived from the
|
|
20
20
|
components' own imports and kept on the
|
|
21
21
|
[Installation page](https://ui.delacour.co.nz/docs/native/getting-started/installation).
|
|
22
|
-
|
|
22
|
+
Six peers are optional and resolve only if you import the subpath that needs them:
|
|
23
23
|
|
|
24
24
|
| Peer | Needed for |
|
|
25
25
|
| --- | --- |
|
|
26
26
|
| `expo-router` | `@delacour/react-native-ui/expo/navigation-theme` |
|
|
27
|
-
| `@legendapp/list` | `Screen.LegendList` and `
|
|
27
|
+
| `@legendapp/list` | `Screen.LegendList`, `Screen.ChatList` and `BottomSheet.LegendList` |
|
|
28
28
|
| `react-native-screens` | native stack navigation |
|
|
29
29
|
| `@delacour/react-native-charts` | `Chart` |
|
|
30
|
+
| `@delacour/react-native-bottom-sheet` | `BottomSheet` — the engine it skins |
|
|
31
|
+
| `react-native-teleport` | `BottomSheet` — the engine's portal; a native module, so rebuild the dev client after installing |
|
|
30
32
|
|
|
31
33
|
Granular exports are what make that safe: an app that never imports a subpath
|
|
32
34
|
never makes Metro resolve its peers.
|
|
@@ -53,8 +55,10 @@ import { DelacourProvider } from "@delacour/react-native-ui/provider";
|
|
|
53
55
|
```
|
|
54
56
|
|
|
55
57
|
That is the gesture root every `Pressable` needs above it, the safe-area provider
|
|
56
|
-
and keyboard provider `Screen` reads, the
|
|
57
|
-
|
|
58
|
+
and keyboard provider `Screen` reads, and the keyboard state sync that keeps them
|
|
59
|
+
honest. An app with a `BottomSheet` mounts `BottomSheetProvider` inside it, from
|
|
60
|
+
`@delacour/react-native-ui/bottom-sheet` — the sheet's engine is an optional peer,
|
|
61
|
+
so the provider cannot.
|
|
58
62
|
|
|
59
63
|
## Usage
|
|
60
64
|
|
|
@@ -78,7 +82,7 @@ import { IconArrowRight } from "@delacour/react-native-ui/icons/central";
|
|
|
78
82
|
| --- | --- | --- |
|
|
79
83
|
| Accordion | `@delacour/react-native-ui/accordion` | Selection modes, measured panels, indicators |
|
|
80
84
|
| Badge | `@delacour/react-native-ui/badge` | Variants, colours, sizes, dismiss |
|
|
81
|
-
| BottomSheet | `@delacour/react-native-ui/bottom-sheet` |
|
|
85
|
+
| BottomSheet | `@delacour/react-native-ui/bottom-sheet` | Snap points, keyboard, sticky footer, scrollables, steps, detached, teleported portal |
|
|
82
86
|
| Button | `@delacour/react-native-ui/button` | Variants, sizes, icons, loading |
|
|
83
87
|
| Checkbox | `@delacour/react-native-ui/checkbox` | Colours, sizes, indeterminate, groups |
|
|
84
88
|
| Field | `@delacour/react-native-ui/field` | Form layout, grouping, state cascade |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delacour/react-native-ui",
|
|
3
|
-
"version": "0.1.0
|
|
4
|
-
"description": "React Native UI components
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React Native UI components — Uniwind, Reanimated, Gesture Handler",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -34,30 +34,65 @@
|
|
|
34
34
|
"./styles/tokens": "./src/styles/tokens.css",
|
|
35
35
|
"./styles/theme": "./src/styles/theme.css",
|
|
36
36
|
"./accordion": "./src/components/accordion/index.ts",
|
|
37
|
+
"./alert": "./src/components/alert/index.ts",
|
|
38
|
+
"./avatar": "./src/components/avatar/index.ts",
|
|
37
39
|
"./badge": "./src/components/badge/index.ts",
|
|
38
40
|
"./bottom-sheet": "./src/components/bottom-sheet/index.ts",
|
|
39
41
|
"./button": "./src/components/button/index.ts",
|
|
42
|
+
"./calendar": "./src/components/calendar/index.ts",
|
|
43
|
+
"./card": "./src/components/card/index.ts",
|
|
40
44
|
"./chart": "./src/components/chart/index.ts",
|
|
41
45
|
"./checkbox": "./src/components/checkbox/index.ts",
|
|
46
|
+
"./chip": "./src/components/chip/index.ts",
|
|
47
|
+
"./collapsible": "./src/components/collapsible/index.ts",
|
|
48
|
+
"./context-menu": "./src/components/context-menu/index.ts",
|
|
49
|
+
"./dialog": "./src/components/dialog/index.ts",
|
|
50
|
+
"./drawer": "./src/components/drawer/index.ts",
|
|
51
|
+
"./empty-state": "./src/components/empty-state/index.ts",
|
|
52
|
+
"./fab": "./src/components/fab/index.ts",
|
|
53
|
+
"./feedback": "./src/components/feedback/index.ts",
|
|
42
54
|
"./field": "./src/components/field/index.ts",
|
|
43
55
|
"./icon": "./src/components/icon/index.ts",
|
|
44
56
|
"./input": "./src/components/input/index.ts",
|
|
57
|
+
"./item": "./src/components/item/index.ts",
|
|
58
|
+
"./kpi": "./src/components/kpi/index.ts",
|
|
59
|
+
"./label": "./src/components/label/index.ts",
|
|
45
60
|
"./list-group": "./src/components/list-group/index.ts",
|
|
61
|
+
"./menu": "./src/components/menu/index.ts",
|
|
62
|
+
"./meter": "./src/components/meter/index.ts",
|
|
63
|
+
"./overlay": "./src/components/overlay/index.ts",
|
|
64
|
+
"./popover": "./src/components/popover/index.ts",
|
|
46
65
|
"./pressable": "./src/components/pressable/index.ts",
|
|
66
|
+
"./progress": "./src/components/progress/index.ts",
|
|
67
|
+
"./progress-button": "./src/components/progress-button/index.ts",
|
|
47
68
|
"./provider": "./src/components/provider/index.ts",
|
|
48
69
|
"./radio": "./src/components/radio/index.ts",
|
|
70
|
+
"./rating": "./src/components/rating/index.ts",
|
|
49
71
|
"./screen": "./src/components/screen/index.ts",
|
|
72
|
+
"./selection-mode": "./src/components/selection-mode/index.ts",
|
|
50
73
|
"./separator": "./src/components/separator/index.ts",
|
|
74
|
+
"./skeleton": "./src/components/skeleton/index.ts",
|
|
75
|
+
"./slide-button": "./src/components/slide-button/index.ts",
|
|
51
76
|
"./slider": "./src/components/slider/index.ts",
|
|
52
77
|
"./spinner": "./src/components/spinner/index.ts",
|
|
78
|
+
"./stack-card": "./src/components/stack-card/index.ts",
|
|
79
|
+
"./steps": "./src/components/steps/index.ts",
|
|
80
|
+
"./surface": "./src/components/surface/index.ts",
|
|
81
|
+
"./swipe": "./src/components/swipe/index.ts",
|
|
53
82
|
"./switch": "./src/components/switch/index.ts",
|
|
54
83
|
"./tabs": "./src/components/tabs/index.ts",
|
|
55
84
|
"./text": "./src/components/text/index.ts",
|
|
85
|
+
"./textarea": "./src/components/textarea/index.ts",
|
|
86
|
+
"./toast": "./src/components/toast/index.ts",
|
|
87
|
+
"./toggle-button": "./src/components/toggle-button/index.ts",
|
|
88
|
+
"./tooltip": "./src/components/tooltip/index.ts",
|
|
56
89
|
"./expo/navigation-theme": "./src/expo/navigation-theme.tsx",
|
|
90
|
+
"./hooks/use-calm-motion": "./src/hooks/use-calm-motion.tsx",
|
|
57
91
|
"./hooks/use-controllable-state": "./src/hooks/use-controllable-state.ts",
|
|
58
92
|
"./hooks/use-keyboard-state-sync": "./src/hooks/use-keyboard-state-sync.tsx",
|
|
59
93
|
"./hooks/use-navigation-theme": "./src/hooks/use-navigation-theme.ts",
|
|
60
94
|
"./hooks/use-theme-color": "./src/hooks/use-theme-color.ts",
|
|
95
|
+
"./lib/calm-motion": "./src/lib/calm-motion.ts",
|
|
61
96
|
"./lib/cn": "./src/lib/cn.ts",
|
|
62
97
|
"./lib/color": "./src/lib/color.ts",
|
|
63
98
|
"./lib/compose-refs": "./src/lib/compose-refs.ts",
|
|
@@ -81,8 +116,8 @@
|
|
|
81
116
|
},
|
|
82
117
|
"peerDependencies": {
|
|
83
118
|
"@central-icons-react-native/round-outlined-radius-1-stroke-1.5": "^1.1",
|
|
84
|
-
"@delacour/react-native-
|
|
85
|
-
"@
|
|
119
|
+
"@delacour/react-native-bottom-sheet": ">=0.1.0",
|
|
120
|
+
"@delacour/react-native-charts": ">=0.1.0",
|
|
86
121
|
"@legendapp/list": ">=3.3",
|
|
87
122
|
"expo-linear-gradient": ">=15",
|
|
88
123
|
"expo-router": ">=57",
|
|
@@ -95,11 +130,15 @@
|
|
|
95
130
|
"react-native-safe-area-context": ">=5",
|
|
96
131
|
"react-native-screens": ">=4.16",
|
|
97
132
|
"react-native-svg": ">=15",
|
|
133
|
+
"react-native-teleport": ">=1.2",
|
|
98
134
|
"react-native-worklets": ">=0.5",
|
|
99
135
|
"tailwindcss": "^4",
|
|
100
136
|
"uniwind": "^1.11.0"
|
|
101
137
|
},
|
|
102
138
|
"peerDependenciesMeta": {
|
|
139
|
+
"@delacour/react-native-bottom-sheet": {
|
|
140
|
+
"optional": true
|
|
141
|
+
},
|
|
103
142
|
"@delacour/react-native-charts": {
|
|
104
143
|
"optional": true
|
|
105
144
|
},
|
|
@@ -111,15 +150,20 @@
|
|
|
111
150
|
},
|
|
112
151
|
"react-native-screens": {
|
|
113
152
|
"optional": true
|
|
153
|
+
},
|
|
154
|
+
"react-native-teleport": {
|
|
155
|
+
"optional": true
|
|
114
156
|
}
|
|
115
157
|
},
|
|
116
158
|
"devDependencies": {
|
|
117
159
|
"@delacour/biome-config": "workspace:*",
|
|
160
|
+
"@delacour/react-native-bottom-sheet": "workspace:*",
|
|
118
161
|
"@delacour/react-native-charts": "workspace:*",
|
|
119
162
|
"@delacour/tsconfig": "workspace:*",
|
|
120
163
|
"@types/bun": "^1.3.8",
|
|
121
164
|
"@types/react": "catalog:",
|
|
122
165
|
"expo-router": "~57.0.15",
|
|
166
|
+
"react-native-teleport": "^1.2.2",
|
|
123
167
|
"typescript": "^5.9.3"
|
|
124
168
|
}
|
|
125
169
|
}
|
|
@@ -24,7 +24,7 @@ a list of rows.
|
|
|
24
24
|
| `accordion.context.tsx` | `AccordionContext` and `AccordionItemContext`, with their hooks |
|
|
25
25
|
| `accordion.types.ts` | Prop types shared by two or more parts |
|
|
26
26
|
| `accordion.variants.ts` | Pure `tv()` slots + the selection maths, no RN imports |
|
|
27
|
-
| `accordion.variants.test.ts` | |
|
|
27
|
+
| `accordion.variants.test.ts` | `accordionTravelTarget`, and the sweep for a JS-thread `contentHeight.value` read (exports `jsThreadSource` and `CONTENT_HEIGHT_READ` for `Collapsible`'s) |
|
|
28
28
|
|
|
29
29
|
## Design
|
|
30
30
|
|
|
@@ -90,12 +90,23 @@ a list of rows.
|
|
|
90
90
|
effect.** A panel that has never mounted has no height to travel against, so
|
|
91
91
|
springing on the state change would run the whole animation at zero and jump
|
|
92
92
|
the moment a measurement arrived. The item's effect skips exactly that one
|
|
93
|
-
case and `accordion-content.tsx`'s `onLayout`
|
|
94
|
-
|
|
93
|
+
case and `accordion-content.tsx`'s `onLayout` reports the measurement, which
|
|
94
|
+
flips the item's `isMeasured` state and re-runs the effect. Every later toggle,
|
|
95
|
+
in either direction, is the item's. The decision is `accordionTravelTarget`,
|
|
96
|
+
pure and tested.
|
|
97
|
+
- **Nothing on the JS thread reads `contentHeight.value`.** "Has this panel
|
|
98
|
+
measured?" is React state (`isMeasured`, on both the item and the panel), never
|
|
99
|
+
a read of the shared value. A JS-thread write to a shared value is *queued* onto
|
|
100
|
+
the UI runtime, while a JS-thread read runs there synchronously under its lock
|
|
101
|
+
without draining that queue — so the item's effect, running straight after the
|
|
102
|
+
`onLayout` that wrote the height, can read `ACCORDION_UNMEASURED`. A Release
|
|
103
|
+
build does, deterministically: the effect bailed, nothing re-ran it, and the
|
|
104
|
+
panel reported `expanded` while never opening (a Debug client is slow enough
|
|
105
|
+
for the write to land first, which is why it hid). `accordion.variants.test.ts`
|
|
106
|
+
sweeps both files for such a read outside `useAnimatedStyle`.
|
|
95
107
|
- **`ACCORDION_UNMEASURED` is negative, and that is load-bearing.** A panel that
|
|
96
|
-
measured `0` is a real answer — content that rendered nothing — and
|
|
97
|
-
|
|
98
|
-
indicator pointing the wrong way. Only a value no layout can produce can mean
|
|
108
|
+
measured `0` is a real answer — content that rendered nothing — and must still
|
|
109
|
+
open, indicator and all. Only a value no layout can produce can mean
|
|
99
110
|
*unmeasured*, so the height style floors it.
|
|
100
111
|
|
|
101
112
|
## Selection
|
|
@@ -139,9 +150,10 @@ a list of rows.
|
|
|
139
150
|
`onPress` is the one prop `Omit`ed rather than forwarded: the press *is* the
|
|
140
151
|
toggle, and a side effect belongs on `onValueChange`. `Checkbox`'s trade.
|
|
141
152
|
- **The disabled fade lands on the item, never on the trigger.** The trigger is a
|
|
142
|
-
`Pressable`,
|
|
143
|
-
|
|
144
|
-
the
|
|
153
|
+
`Pressable`, which now multiplies a className's opacity into its press rather
|
|
154
|
+
than overwriting it — but the item already fades, header and panel together,
|
|
155
|
+
and a fade on the trigger as well would compound to a quarter. A test sweeps
|
|
156
|
+
for it.
|
|
145
157
|
- **The trigger assembles its own row.** Titles and descriptions stack in a
|
|
146
158
|
column, anything else — a leading `Icon`, a `Badge` — stays where it was
|
|
147
159
|
written as a row sibling, and the indicator is moved to the end and composed in
|
|
@@ -2,7 +2,7 @@ import { type ReactElement, type ReactNode, useCallback, useState } from "react"
|
|
|
2
2
|
import { type LayoutChangeEvent, StyleSheet, View, type ViewProps } from "react-native";
|
|
3
3
|
import Animated, { interpolate, useAnimatedStyle } from "react-native-reanimated";
|
|
4
4
|
import { useAccordionItemPart, useAccordionPart } from "./accordion.context";
|
|
5
|
-
import { ACCORDION_CONTENT_FADE,
|
|
5
|
+
import { ACCORDION_CONTENT_FADE, accordionVariants } from "./accordion.variants";
|
|
6
6
|
|
|
7
7
|
export type AccordionContentProps = Omit<ViewProps, "style"> & {
|
|
8
8
|
className?: string;
|
|
@@ -55,7 +55,6 @@ export function AccordionContent({ className, children, ...props }: AccordionCon
|
|
|
55
55
|
|
|
56
56
|
const handleLayout = useCallback(
|
|
57
57
|
(event: LayoutChangeEvent) => {
|
|
58
|
-
const wasMeasured = contentHeight.value > ACCORDION_UNMEASURED;
|
|
59
58
|
contentHeight.value = event.nativeEvent.layout.height;
|
|
60
59
|
|
|
61
60
|
// The panel reports its first measurement and stops there — it must not
|
|
@@ -64,11 +63,15 @@ export function AccordionContent({ className, children, ...props }: AccordionCon
|
|
|
64
63
|
// is sometimes cancelled a moment later by the cleanup of the item's
|
|
65
64
|
// effect it raced, leaving the panel shut and the indicator pointing the
|
|
66
65
|
// wrong way. Telling the item instead leaves one owner of the spring.
|
|
67
|
-
|
|
66
|
+
//
|
|
67
|
+
// "First" is this component's own state, never `contentHeight.value`: the
|
|
68
|
+
// write above is queued onto the UI runtime, and a JS-thread read straight
|
|
69
|
+
// after it can still see the old value.
|
|
70
|
+
if (isMeasured) return;
|
|
68
71
|
setMeasured(true);
|
|
69
72
|
onMeasured();
|
|
70
73
|
},
|
|
71
|
-
[contentHeight, onMeasured]
|
|
74
|
+
[contentHeight, isMeasured, onMeasured]
|
|
72
75
|
);
|
|
73
76
|
|
|
74
77
|
// Height and opacity off the one `progress`, never off a timing of their own —
|
|
@@ -5,6 +5,7 @@ import { type AccordionItemContextValue, AccordionItemProvider, useAccordionPart
|
|
|
5
5
|
import {
|
|
6
6
|
ACCORDION_SPRING,
|
|
7
7
|
ACCORDION_UNMEASURED,
|
|
8
|
+
accordionTravelTarget,
|
|
8
9
|
accordionVariants,
|
|
9
10
|
isItemExpanded,
|
|
10
11
|
resolveAccordionItemAxes,
|
|
@@ -42,25 +43,26 @@ export function AccordionItem({ value, isDisabled, className, children, ...props
|
|
|
42
43
|
const progress = useSharedValue(isExpanded ? 1 : 0);
|
|
43
44
|
const contentHeight = useSharedValue(ACCORDION_UNMEASURED);
|
|
44
45
|
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
const
|
|
46
|
+
// Whether this item's panel has ever reported a height. React state, not a read
|
|
47
|
+
// of `contentHeight` on the JS thread: that write is queued onto the UI runtime
|
|
48
|
+
// and a read straight after it can still see `ACCORDION_UNMEASURED` — which a
|
|
49
|
+
// Release build does, every time. See `accordionTravelTarget`.
|
|
50
|
+
const [isMeasured, setMeasured] = useState(false);
|
|
51
|
+
const onMeasured = useCallback(() => setMeasured(true), []);
|
|
50
52
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: the extra dependency is the re-run trigger, see above
|
|
53
|
+
// The first expand of a panel that has never been mounted has no height to
|
|
54
|
+
// travel against, so this run waits; the measurement that follows flips
|
|
55
|
+
// `isMeasured` and brings it back. See `onMeasured` on the item context for why
|
|
56
|
+
// the panel must not start the spring itself.
|
|
56
57
|
useEffect(() => {
|
|
57
|
-
|
|
58
|
+
const target = accordionTravelTarget({ isExpanded, isMeasured });
|
|
59
|
+
if (target === null) return;
|
|
58
60
|
|
|
59
|
-
progress.value = withSpring(
|
|
61
|
+
progress.value = withSpring(target, ACCORDION_SPRING);
|
|
60
62
|
|
|
61
63
|
// Without this an item unmounted mid-travel leaves its spring running.
|
|
62
64
|
return () => cancelAnimation(progress);
|
|
63
|
-
}, [
|
|
65
|
+
}, [isExpanded, isMeasured, progress]);
|
|
64
66
|
|
|
65
67
|
const context = useMemo<AccordionItemContextValue>(
|
|
66
68
|
() => ({ contentHeight, isDisabled: axes.isDisabled, isExpanded, onMeasured, progress, value }),
|
|
@@ -35,8 +35,9 @@ export type AccordionItemContextValue = {
|
|
|
35
35
|
* The panel's natural height in points, from its own `onLayout`.
|
|
36
36
|
*
|
|
37
37
|
* Holds {@link ACCORDION_UNMEASURED} until a panel has reported, which is not
|
|
38
|
-
* the same as a panel that measured zero
|
|
39
|
-
*
|
|
38
|
+
* the same as a panel that measured zero. Read on the UI runtime only — by the
|
|
39
|
+
* height style — and written from the panel's `onLayout`; nothing on the JS
|
|
40
|
+
* thread reads it, because a JS read can lag a JS write. See {@link onMeasured}.
|
|
40
41
|
*/
|
|
41
42
|
contentHeight: SharedValue<number>;
|
|
42
43
|
/**
|
|
@@ -46,9 +47,14 @@ export type AccordionItemContextValue = {
|
|
|
46
47
|
* spring. The panel cannot start the travel itself: `onLayout` is dispatched
|
|
47
48
|
* from the native side and can land either side of React's effects, so a panel
|
|
48
49
|
* that started its own spring would sometimes have it cancelled a moment later
|
|
49
|
-
* by the cleanup of the effect it raced. This
|
|
50
|
-
*
|
|
51
|
-
*
|
|
50
|
+
* by the cleanup of the effect it raced. This flips the item's own
|
|
51
|
+
* `isMeasured` state instead, which is what re-runs the item's effect.
|
|
52
|
+
*
|
|
53
|
+
* That state, not a JS-thread read of {@link contentHeight}, is how the item
|
|
54
|
+
* knows a panel has measured. The panel's write is queued onto the UI runtime
|
|
55
|
+
* and a read straight after it can still see `ACCORDION_UNMEASURED` — a Release
|
|
56
|
+
* build does, every time, and the panel stayed shut. See
|
|
57
|
+
* `accordionTravelTarget`.
|
|
52
58
|
*/
|
|
53
59
|
onMeasured: () => void;
|
|
54
60
|
};
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
ACCORDION_SPRING,
|
|
17
17
|
ACCORDION_UNMEASURED,
|
|
18
18
|
ACCORDION_VARIANTS,
|
|
19
|
+
accordionTravelTarget,
|
|
19
20
|
accordionVariants,
|
|
20
21
|
isItemExpanded,
|
|
21
22
|
resolveAccordionItemAxes,
|
|
@@ -344,9 +345,8 @@ describe("accordionVariants slots", () => {
|
|
|
344
345
|
});
|
|
345
346
|
|
|
346
347
|
test("the disabled fade lands on the item, never on the trigger", () => {
|
|
347
|
-
// The
|
|
348
|
-
//
|
|
349
|
-
// is overwritten silently — the failure `Switch` and `Radio` both record.
|
|
348
|
+
// The item fades header and panel together; a fade on the trigger as well
|
|
349
|
+
// would compound the two to a quarter.
|
|
350
350
|
for (const size of ACCORDION_SIZES) {
|
|
351
351
|
const slots = accordionVariants({ isDisabled: true, size, variant: ACCORDION_DEFAULT_VARIANT });
|
|
352
352
|
expect(cls(slots.item())).toMatch(/\bopacity-50\b/);
|
|
@@ -433,3 +433,64 @@ describe("accordion animation", () => {
|
|
|
433
433
|
expect(end).toBeLessThan(1);
|
|
434
434
|
});
|
|
435
435
|
});
|
|
436
|
+
|
|
437
|
+
describe("accordionTravelTarget", () => {
|
|
438
|
+
test("an expand waits for the panel's first measurement", () => {
|
|
439
|
+
expect(accordionTravelTarget({ isExpanded: true, isMeasured: false })).toBeNull();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("an expand travels to 1 once the panel has measured", () => {
|
|
443
|
+
expect(accordionTravelTarget({ isExpanded: true, isMeasured: true })).toBe(1);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("a collapse never waits, measured or not", () => {
|
|
447
|
+
expect(accordionTravelTarget({ isExpanded: false, isMeasured: false })).toBe(0);
|
|
448
|
+
expect(accordionTravelTarget({ isExpanded: false, isMeasured: true })).toBe(0);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* A file's source with comments and every `useAnimatedStyle(...)` call stripped — the one
|
|
454
|
+
* place a read of a shared value runs on the UI runtime — leaving only code that runs on the
|
|
455
|
+
* JS thread.
|
|
456
|
+
*/
|
|
457
|
+
export function jsThreadSource(path: string): string {
|
|
458
|
+
const source = readFileSync(path, "utf8")
|
|
459
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
460
|
+
.replace(/\/\/.*$/gm, "");
|
|
461
|
+
let out = "";
|
|
462
|
+
let index = 0;
|
|
463
|
+
for (;;) {
|
|
464
|
+
const start = source.indexOf("useAnimatedStyle(", index);
|
|
465
|
+
if (start === -1) return out + source.slice(index);
|
|
466
|
+
out += source.slice(index, start);
|
|
467
|
+
let depth = 0;
|
|
468
|
+
let cursor = start + "useAnimatedStyle".length;
|
|
469
|
+
do {
|
|
470
|
+
if (source[cursor] === "(") depth++;
|
|
471
|
+
if (source[cursor] === ")") depth--;
|
|
472
|
+
cursor++;
|
|
473
|
+
} while (depth > 0 && cursor < source.length);
|
|
474
|
+
index = cursor;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** `contentHeight.value` as a read: anything but the left side of a plain `=`. */
|
|
479
|
+
export const CONTENT_HEIGHT_READ = /\bcontentHeight\.value\b(?!\s*=(?!=))/;
|
|
480
|
+
|
|
481
|
+
describe("the measured height is never read on the JS thread", () => {
|
|
482
|
+
// A JS-thread write to a shared value is queued onto the UI runtime, while a JS-thread read
|
|
483
|
+
// runs there synchronously under its lock without draining that queue. A read straight after
|
|
484
|
+
// the write — the item's effect after the panel's `onLayout` — can see the old value. In a
|
|
485
|
+
// Release build it does, every time: the item read `ACCORDION_UNMEASURED`, bailed, and nothing
|
|
486
|
+
// re-ran it, so the panel said `expanded` and stayed shut.
|
|
487
|
+
test.each(["accordion-item.tsx", "accordion-content.tsx"])("%s", (file) => {
|
|
488
|
+
expect(jsThreadSource(join(import.meta.dirname, file))).not.toMatch(CONTENT_HEIGHT_READ);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("the guard sees a read and ignores a write", () => {
|
|
492
|
+
expect("if (contentHeight.value === -1) return;").toMatch(CONTENT_HEIGHT_READ);
|
|
493
|
+
expect("const was = contentHeight.value > -1;").toMatch(CONTENT_HEIGHT_READ);
|
|
494
|
+
expect("contentHeight.value = event.nativeEvent.layout.height;").not.toMatch(CONTENT_HEIGHT_READ);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
@@ -76,17 +76,41 @@ export const ACCORDION_INDICATOR_ROTATION = { collapsed: 0, expanded: 180 } as c
|
|
|
76
76
|
* What {@link AccordionItemContextValue.contentHeight} holds before a panel has
|
|
77
77
|
* ever reported its own layout.
|
|
78
78
|
*
|
|
79
|
-
* Negative rather than zero, because the two mean different things
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* *unmeasured*.
|
|
79
|
+
* Negative rather than zero, because the two mean different things. A panel that
|
|
80
|
+
* measured `0` is a real answer — a panel whose content rendered nothing — and
|
|
81
|
+
* still opens, indicator and all. Only a value no layout can produce can mean
|
|
82
|
+
* *unmeasured*. Whether a panel *has* measured is never decided by comparing
|
|
83
|
+
* against this on the JS thread — see {@link accordionTravelTarget}.
|
|
85
84
|
*
|
|
86
85
|
* The height style therefore floors it: `progress * max(contentHeight, 0)`.
|
|
87
86
|
*/
|
|
88
87
|
export const ACCORDION_UNMEASURED = -1;
|
|
89
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Where an item's `progress` should spring to, or `null` to wait.
|
|
91
|
+
*
|
|
92
|
+
* Only an expand waits, and only for a panel that has never measured: it has no
|
|
93
|
+
* height to travel against yet, and the measurement is what starts it. A collapse
|
|
94
|
+
* never waits.
|
|
95
|
+
*
|
|
96
|
+
* `isMeasured` is **React state**, never a read of `contentHeight` on the JS
|
|
97
|
+
* thread. A JS-thread write to a shared value is queued onto the UI runtime, while
|
|
98
|
+
* a JS-thread read runs there synchronously without draining that queue, so a read
|
|
99
|
+
* straight after the panel's `onLayout` wrote the height can still see
|
|
100
|
+
* {@link ACCORDION_UNMEASURED}. A Release build does, every time — the item bailed
|
|
101
|
+
* and nothing re-ran it, so the panel said `expanded` and stayed shut.
|
|
102
|
+
*/
|
|
103
|
+
export function accordionTravelTarget({
|
|
104
|
+
isExpanded,
|
|
105
|
+
isMeasured,
|
|
106
|
+
}: {
|
|
107
|
+
isExpanded: boolean;
|
|
108
|
+
isMeasured: boolean;
|
|
109
|
+
}): 0 | 1 | null {
|
|
110
|
+
if (!isExpanded) return 0;
|
|
111
|
+
return isMeasured ? 1 : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
90
114
|
/**
|
|
91
115
|
* The window of the travel the panel's opacity ramps across.
|
|
92
116
|
*
|
|
@@ -142,9 +166,9 @@ export const ACCORDION_CONTENT_FADE = { start: 0.1, end: 0.6 } as const;
|
|
|
142
166
|
* because the travel has not started yet.
|
|
143
167
|
*
|
|
144
168
|
* **The disabled fade lands on `item`, never on `trigger`.** The trigger is a
|
|
145
|
-
* `Pressable`,
|
|
146
|
-
*
|
|
147
|
-
*
|
|
169
|
+
* `Pressable`, which now multiplies a className's opacity into its press rather
|
|
170
|
+
* than overwriting it — but the item already fades, header and panel together,
|
|
171
|
+
* and a fade on the trigger as well would compound to a quarter.
|
|
148
172
|
*
|
|
149
173
|
* No slot worn by a `View` carries `text-*`; a React Native `View` does not
|
|
150
174
|
* cascade colour to a `Text` descendant, so the treatment lives on the `title` and
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Alert
|
|
2
|
+
|
|
3
|
+
A status message on a `Surface`: a glyph picked from its status, a title, a
|
|
4
|
+
description and, optionally, an action row and a dismiss control. Compound root
|
|
5
|
+
plus `Alert.Indicator`, `Alert.Content`, `Alert.Title`, `Alert.Description`,
|
|
6
|
+
`Alert.Action` and `Alert.CloseButton`.
|
|
7
|
+
|
|
8
|
+
`import { Alert } from "@delacour/react-native-ui/alert";`
|
|
9
|
+
|
|
10
|
+
## Files
|
|
11
|
+
|
|
12
|
+
| File | What it holds |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `index.ts` | → `@delacour/react-native-ui/alert` |
|
|
15
|
+
| `alert.tsx` | Root + the `Object.assign` compound surface |
|
|
16
|
+
| `alert-indicator.tsx` | `Alert.Indicator` |
|
|
17
|
+
| `alert-glyphs.ts` | **Leaf.** `ALERT_GLYPHS`, the status → glyph map — `toast/` imports it |
|
|
18
|
+
| `alert-content.tsx` | `Alert.Content` |
|
|
19
|
+
| `alert-title.tsx` | `Alert.Title` |
|
|
20
|
+
| `alert-description.tsx` | `Alert.Description` |
|
|
21
|
+
| `alert-action.tsx` | `Alert.Action` |
|
|
22
|
+
| `alert-close-button.tsx` | `Alert.CloseButton`, the dismiss pressable |
|
|
23
|
+
| `alert.context.tsx` | `AlertProvider`, `useAlert()`, `useAlertContext()`, `useAlertPart()` |
|
|
24
|
+
| `alert.types.ts` | `AlertSlotProps`, shared by `Content` and `Action` |
|
|
25
|
+
| `alert.variants.ts` | The slotted `tv()`, `ALERT_FOREGROUND_TOKEN`, `ALERT_SURFACE_PADDING` and three pure resolvers — no RN imports |
|
|
26
|
+
| `alert.variants.test.ts` | |
|
|
27
|
+
|
|
28
|
+
## Design
|
|
29
|
+
|
|
30
|
+
- **Two axes: `status` and `variant`.** `status` says what the message means —
|
|
31
|
+
`default`, `info`, `success`, `warning`, `destructive` — and `variant` says
|
|
32
|
+
how loudly: `soft` (the default) washes the surface in the status's `-soft`
|
|
33
|
+
fill, `surface` keeps a neutral fill and lets the glyph and title carry the
|
|
34
|
+
colour. **Sizes**: `sm`, `md`, `lg`, which move padding, gap, type and glyph
|
|
35
|
+
together. `destructive` rather than `danger`, per the package's token rule.
|
|
36
|
+
- **It is built on `Surface`, and it adds only what a surface lacks.** The
|
|
37
|
+
corner, the continuous curve, the padding (`ALERT_SURFACE_PADDING` maps each
|
|
38
|
+
size onto the surface's own step) and the neutral fill all come from
|
|
39
|
+
[Surface](../surface/AGENTS.md). `alertVariants`' root carries the row and the
|
|
40
|
+
tint, and a test asserts it sets no padding of its own — a second one would
|
|
41
|
+
fight the surface's through the merge.
|
|
42
|
+
- **Only a tinted alert names a fill.** `resolveAlertTinted` is true for `soft`
|
|
43
|
+
plus a status, and those four cells swap `bg-card` for `bg-X-soft` and clear
|
|
44
|
+
the card's hairline: a grey rule around a red wash reads as two components
|
|
45
|
+
stacked. Every other alert leaves the surface's `variant` undefined
|
|
46
|
+
(`resolveAlertSurfaceVariant`), so it steps from the plane it sits on — an
|
|
47
|
+
alert inside a card lands on `secondary` rather than vanishing into the card.
|
|
48
|
+
A tinted alert pins the surface to `default` instead, so what nests in it
|
|
49
|
+
steps from a card like anything else.
|
|
50
|
+
- **One colour token per status, not per status and variant.**
|
|
51
|
+
`ALERT_FOREGROUND_TOKEN` names `X-soft-foreground` for each status and the
|
|
52
|
+
page's `foreground` for `default`; those are tuned to read on the soft fill
|
|
53
|
+
and on the neutral fills alike. The title slot emits the same token as a
|
|
54
|
+
class and the root publishes it through `IconDefaultsProvider`, so the glyph
|
|
55
|
+
and the title are always one shade. A test pins the pair and asserts every
|
|
56
|
+
token exists in both themes. The classes are written out literally —
|
|
57
|
+
Tailwind's scanner is static, so a class assembled from the map at runtime
|
|
58
|
+
would never be compiled.
|
|
59
|
+
- **The description is always muted.** A long explanation under a red title
|
|
60
|
+
must not shout; the title carries the status.
|
|
61
|
+
- **`items-start`, and the indicator is as tall as the title's line.** A
|
|
62
|
+
description that wraps to four lines must not drag the glyph to its middle.
|
|
63
|
+
The indicator's height is the title's line height at each size (`h-5`/`h-6`/
|
|
64
|
+
`h-7` against `text-sm`/`base`/`lg`), and it centres the glyph in that box, so
|
|
65
|
+
the glyph sits level with the first line. Nothing else is a fixed height:
|
|
66
|
+
`Text` respects OS font scaling, and a fixed box would clip a wrapped title.
|
|
67
|
+
- **Glyphs differ in shape, not just colour.** `warning` is a triangle and
|
|
68
|
+
`destructive` a circle, so the two stay apart for anyone who cannot tell amber
|
|
69
|
+
from red. `default` shares `info`'s circle. The map lives in
|
|
70
|
+
`alert-glyphs.ts`, a leaf `Toast` shares, because the glyphs are RN SVG components and
|
|
71
|
+
`alert.variants.ts` must stay importable from `bun test`.
|
|
72
|
+
- **The indicator is hidden from assistive technology.** The title says what
|
|
73
|
+
happened; "image" announced before it adds nothing. Children replace the
|
|
74
|
+
glyph and inherit the alert's icon size and colour — a `Spinner` for a
|
|
75
|
+
pending state needs nothing else.
|
|
76
|
+
- **Announced as an `alert`, and as a live region on Android.**
|
|
77
|
+
`resolveAlertLiveRegion` makes a `warning` or `destructive` alert assertive
|
|
78
|
+
and anything else polite. The root is not made `accessible`: grouping it
|
|
79
|
+
would swallow the close control and any action into one element.
|
|
80
|
+
- **Dismissal is controllable.** `isDismissible` composes an
|
|
81
|
+
`Alert.CloseButton` in at the end. Uncontrolled, the alert hides itself when
|
|
82
|
+
dismissed (`defaultOpen`); pass `isOpen` with `onOpenChange` to own it.
|
|
83
|
+
`useAlert().dismiss` is on the context so an action ("Got it") can close the
|
|
84
|
+
alert without the caller threading a setter down. A dismissed alert fades out
|
|
85
|
+
over 150 ms rather than vanishing — the one `Animated.View` wrapper exists for
|
|
86
|
+
that `exiting`, and a caller's `className` still reaches the surface.
|
|
87
|
+
- **The close glyph is muted, not the status colour.** The control is about the
|
|
88
|
+
alert rather than part of what it says, and a red cross beside a red title
|
|
89
|
+
reads as a second warning. It presses with `fade` — a spring on a glyph that
|
|
90
|
+
small is a jitter — and carries `hitSlop={10}` to reach the 44-point target.
|
|
91
|
+
- **`closeTestID` reaches the composed close control.** The root's own `testID`
|
|
92
|
+
names the alert's surface; the close part is a separate pressable with no id of its own
|
|
93
|
+
unless the root forwards one, so a flow or test could only find it by its label.
|
|
94
|
+
`closeTestID` is named like `closeAccessibilityLabel`, the other prop the root
|
|
95
|
+
passes through to it.
|
|
96
|
+
- **Actions are the caller's.** `Alert.Action` is a wrapping row and styles
|
|
97
|
+
nothing in it; a `Button` in there is sized and painted however the caller
|
|
98
|
+
says. It wraps rather than overflowing, because two buttons and a long label
|
|
99
|
+
outgrow a phone's width.
|
|
100
|
+
- **Parts are composed, not configured.** There is no `title` or `icon` prop:
|
|
101
|
+
omit `Alert.Indicator` for a text-only alert, omit `Alert.Description` for a
|
|
102
|
+
one-liner. The same reason `Button` and `Badge` compose their icons.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import type { AlertSlotProps } from "./alert.types";
|
|
4
|
+
import { alertVariants } from "./alert.variants";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A wrapping row for the alert's buttons, placed under the description inside
|
|
8
|
+
* `Alert.Content`.
|
|
9
|
+
*
|
|
10
|
+
* Takes any children — a `Button`, a `Text.Link` — and styles none of them: an
|
|
11
|
+
* action is the caller's control, sized and painted the way they want it. Read
|
|
12
|
+
* `useAlert().dismiss` from inside to close the alert from an action.
|
|
13
|
+
*/
|
|
14
|
+
export function AlertAction({ className, ...props }: AlertSlotProps): ReactElement {
|
|
15
|
+
return <View className={alertVariants().action({ className })} {...props} />;
|
|
16
|
+
}
|
|
17
|
+
AlertAction.displayName = "DelacourUI.Alert.Action";
|