@delacour/react-native-ui 0.1.0-alpha.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/package.json +49 -5
- package/src/components/accordion/AGENTS.md +21 -9
- package/src/components/accordion/accordion-content.tsx +7 -4
- package/src/components/accordion/accordion-item.tsx +15 -13
- package/src/components/accordion/accordion.context.tsx +11 -5
- package/src/components/accordion/accordion.variants.test.ts +64 -3
- package/src/components/accordion/accordion.variants.ts +33 -9
- package/src/components/alert/AGENTS.md +102 -0
- package/src/components/alert/alert-action.tsx +17 -0
- package/src/components/alert/alert-close-button.tsx +50 -0
- package/src/components/alert/alert-content.tsx +15 -0
- package/src/components/alert/alert-description.tsx +16 -0
- package/src/components/alert/alert-glyphs.ts +25 -0
- package/src/components/alert/alert-indicator.tsx +40 -0
- package/src/components/alert/alert-title.tsx +18 -0
- package/src/components/alert/alert.context.tsx +69 -0
- package/src/components/alert/alert.tsx +160 -0
- package/src/components/alert/alert.types.ts +10 -0
- package/src/components/alert/alert.variants.test.ts +218 -0
- package/src/components/alert/alert.variants.ts +180 -0
- package/src/components/alert/index.ts +23 -0
- package/src/components/avatar/AGENTS.md +106 -0
- package/src/components/avatar/avatar-badge.tsx +51 -0
- package/src/components/avatar/avatar-group.tsx +139 -0
- package/src/components/avatar/avatar.context.tsx +93 -0
- package/src/components/avatar/avatar.tsx +201 -0
- package/src/components/avatar/avatar.utils.test.ts +174 -0
- package/src/components/avatar/avatar.utils.ts +141 -0
- package/src/components/avatar/avatar.variants.test.ts +251 -0
- package/src/components/avatar/avatar.variants.ts +270 -0
- package/src/components/avatar/index.ts +39 -0
- package/src/components/badge/AGENTS.md +5 -0
- package/src/components/badge/badge.tsx +6 -1
- package/src/components/bottom-sheet/AGENTS.md +186 -221
- package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
- package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
- package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -207
- package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
- package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
- package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
- package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -120
- package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
- package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
- package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
- package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
- package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -52
- package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
- package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
- package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
- package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
- package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
- package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
- package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -70
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -105
- package/src/components/bottom-sheet/index.ts +39 -17
- package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
- package/src/components/button/AGENTS.md +45 -11
- package/src/components/button/button-group.tsx +44 -8
- package/src/components/button/button.context.tsx +10 -0
- package/src/components/button/button.tsx +2 -1
- package/src/components/button/button.variants.test.ts +154 -0
- package/src/components/button/button.variants.ts +71 -6
- package/src/components/button/index.ts +2 -0
- package/src/components/calendar/AGENTS.md +142 -0
- package/src/components/calendar/calendar-caption.tsx +116 -0
- package/src/components/calendar/calendar-day.tsx +103 -0
- package/src/components/calendar/calendar-grid.tsx +183 -0
- package/src/components/calendar/calendar-header.tsx +34 -0
- package/src/components/calendar/calendar-nav.tsx +51 -0
- package/src/components/calendar/calendar-picker.tsx +128 -0
- package/src/components/calendar/calendar-weekdays.tsx +48 -0
- package/src/components/calendar/calendar.context.tsx +108 -0
- package/src/components/calendar/calendar.date.test.ts +456 -0
- package/src/components/calendar/calendar.date.ts +584 -0
- package/src/components/calendar/calendar.tsx +389 -0
- package/src/components/calendar/calendar.types.ts +87 -0
- package/src/components/calendar/calendar.variants.test.ts +363 -0
- package/src/components/calendar/calendar.variants.ts +426 -0
- package/src/components/calendar/index.ts +96 -0
- package/src/components/card/AGENTS.md +88 -0
- package/src/components/card/card-action.tsx +16 -0
- package/src/components/card/card-content.tsx +12 -0
- package/src/components/card/card-description.tsx +12 -0
- package/src/components/card/card-footer.tsx +30 -0
- package/src/components/card/card-header.tsx +45 -0
- package/src/components/card/card-title.tsx +25 -0
- package/src/components/card/card.context.tsx +64 -0
- package/src/components/card/card.tsx +94 -0
- package/src/components/card/card.types.ts +13 -0
- package/src/components/card/card.variants.test.ts +240 -0
- package/src/components/card/card.variants.ts +177 -0
- package/src/components/card/index.ts +15 -0
- package/src/components/chart/AGENTS.md +9 -1
- package/src/components/chart/chart.tsx +8 -1
- package/src/components/chip/AGENTS.md +113 -0
- package/src/components/chip/chip-close-button.tsx +48 -0
- package/src/components/chip/chip-end-content.tsx +15 -0
- package/src/components/chip/chip-label.tsx +21 -0
- package/src/components/chip/chip-start-content.tsx +16 -0
- package/src/components/chip/chip.context.tsx +66 -0
- package/src/components/chip/chip.tsx +268 -0
- package/src/components/chip/chip.types.ts +10 -0
- package/src/components/chip/chip.variants.test.ts +335 -0
- package/src/components/chip/chip.variants.ts +321 -0
- package/src/components/chip/index.ts +24 -0
- package/src/components/collapsible/AGENTS.md +97 -0
- package/src/components/collapsible/collapsible-content.tsx +97 -0
- package/src/components/collapsible/collapsible-description.tsx +17 -0
- package/src/components/collapsible/collapsible-indicator.tsx +92 -0
- package/src/components/collapsible/collapsible-title.tsx +17 -0
- package/src/components/collapsible/collapsible-trigger.tsx +133 -0
- package/src/components/collapsible/collapsible.context.tsx +91 -0
- package/src/components/collapsible/collapsible.tsx +167 -0
- package/src/components/collapsible/collapsible.types.ts +9 -0
- package/src/components/collapsible/collapsible.variants.test.ts +338 -0
- package/src/components/collapsible/collapsible.variants.ts +228 -0
- package/src/components/collapsible/index.ts +32 -0
- package/src/components/context-menu/AGENTS.md +103 -0
- package/src/components/context-menu/context-menu-content.tsx +64 -0
- package/src/components/context-menu/context-menu-item.tsx +22 -0
- package/src/components/context-menu/context-menu-preview.tsx +60 -0
- package/src/components/context-menu/context-menu-trigger.tsx +160 -0
- package/src/components/context-menu/context-menu.context.tsx +91 -0
- package/src/components/context-menu/context-menu.tsx +90 -0
- package/src/components/context-menu/context-menu.variants.test.ts +266 -0
- package/src/components/context-menu/context-menu.variants.ts +147 -0
- package/src/components/context-menu/index.ts +18 -0
- package/src/components/dialog/AGENTS.md +97 -0
- package/src/components/dialog/dialog-body.tsx +21 -0
- package/src/components/dialog/dialog-close.tsx +82 -0
- package/src/components/dialog/dialog-content.tsx +153 -0
- package/src/components/dialog/dialog-description.tsx +23 -0
- package/src/components/dialog/dialog-footer.tsx +32 -0
- package/src/components/dialog/dialog-header.tsx +20 -0
- package/src/components/dialog/dialog-title.tsx +42 -0
- package/src/components/dialog/dialog-trigger.tsx +54 -0
- package/src/components/dialog/dialog.context.tsx +90 -0
- package/src/components/dialog/dialog.tsx +119 -0
- package/src/components/dialog/dialog.variants.test.ts +224 -0
- package/src/components/dialog/dialog.variants.ts +173 -0
- package/src/components/dialog/index.ts +30 -0
- package/src/components/drawer/AGENTS.md +123 -0
- package/src/components/drawer/drawer-body.tsx +46 -0
- package/src/components/drawer/drawer-close.tsx +76 -0
- package/src/components/drawer/drawer-content.tsx +186 -0
- package/src/components/drawer/drawer-description.tsx +23 -0
- package/src/components/drawer/drawer-footer.tsx +21 -0
- package/src/components/drawer/drawer-header.tsx +42 -0
- package/src/components/drawer/drawer-title.tsx +35 -0
- package/src/components/drawer/drawer-trigger.tsx +54 -0
- package/src/components/drawer/drawer.context.tsx +114 -0
- package/src/components/drawer/drawer.tsx +119 -0
- package/src/components/drawer/drawer.variants.test.ts +392 -0
- package/src/components/drawer/drawer.variants.ts +244 -0
- package/src/components/drawer/index.ts +41 -0
- package/src/components/drawer/use-drawer-pan.ts +108 -0
- package/src/components/empty-state/AGENTS.md +90 -0
- package/src/components/empty-state/empty-state-content.tsx +17 -0
- package/src/components/empty-state/empty-state-description.tsx +12 -0
- package/src/components/empty-state/empty-state-header.tsx +17 -0
- package/src/components/empty-state/empty-state-media.tsx +56 -0
- package/src/components/empty-state/empty-state-title.tsx +18 -0
- package/src/components/empty-state/empty-state.context.tsx +65 -0
- package/src/components/empty-state/empty-state.tsx +84 -0
- package/src/components/empty-state/empty-state.types.ts +14 -0
- package/src/components/empty-state/empty-state.variants.test.ts +245 -0
- package/src/components/empty-state/empty-state.variants.ts +132 -0
- package/src/components/empty-state/index.ts +20 -0
- package/src/components/fab/AGENTS.md +119 -0
- package/src/components/fab/fab-action.tsx +118 -0
- package/src/components/fab/fab-group.tsx +214 -0
- package/src/components/fab/fab-label.tsx +18 -0
- package/src/components/fab/fab.context.tsx +125 -0
- package/src/components/fab/fab.tsx +156 -0
- package/src/components/fab/fab.types.ts +17 -0
- package/src/components/fab/fab.variants.test.ts +274 -0
- package/src/components/fab/fab.variants.ts +234 -0
- package/src/components/fab/index.ts +37 -0
- package/src/components/feedback/AGENTS.md +95 -0
- package/src/components/feedback/feedback-action.tsx +16 -0
- package/src/components/feedback/feedback-cancel.tsx +34 -0
- package/src/components/feedback/feedback-close.tsx +21 -0
- package/src/components/feedback/feedback-content.tsx +24 -0
- package/src/components/feedback/feedback-field.tsx +70 -0
- package/src/components/feedback/feedback-footer.tsx +21 -0
- package/src/components/feedback/feedback-panel.tsx +54 -0
- package/src/components/feedback/feedback-submit.tsx +74 -0
- package/src/components/feedback/feedback-title.tsx +34 -0
- package/src/components/feedback/feedback-trigger.tsx +20 -0
- package/src/components/feedback/feedback.context.tsx +73 -0
- package/src/components/feedback/feedback.tsx +135 -0
- package/src/components/feedback/feedback.variants.test.ts +185 -0
- package/src/components/feedback/feedback.variants.ts +128 -0
- package/src/components/feedback/index.ts +24 -0
- package/src/components/field/AGENTS.md +19 -0
- package/src/components/field/field-label.tsx +19 -5
- package/src/components/field/field.context.tsx +17 -0
- package/src/components/field/field.tsx +15 -2
- package/src/components/field/field.variants.test.ts +24 -0
- package/src/components/field/field.variants.ts +20 -0
- package/src/components/field/index.ts +1 -0
- package/src/components/item/AGENTS.md +92 -0
- package/src/components/item/index.ts +25 -0
- package/src/components/item/item-actions.tsx +28 -0
- package/src/components/item/item-content.tsx +15 -0
- package/src/components/item/item-description.tsx +12 -0
- package/src/components/item/item-footer.tsx +13 -0
- package/src/components/item/item-group.tsx +32 -0
- package/src/components/item/item-header.tsx +13 -0
- package/src/components/item/item-media.tsx +37 -0
- package/src/components/item/item-separator.tsx +13 -0
- package/src/components/item/item-title.tsx +17 -0
- package/src/components/item/item.context.tsx +66 -0
- package/src/components/item/item.tsx +218 -0
- package/src/components/item/item.types.ts +12 -0
- package/src/components/item/item.variants.test.ts +263 -0
- package/src/components/item/item.variants.ts +200 -0
- package/src/components/kpi/AGENTS.md +125 -0
- package/src/components/kpi/index.ts +45 -0
- package/src/components/kpi/kpi-action.tsx +12 -0
- package/src/components/kpi/kpi-content.tsx +35 -0
- package/src/components/kpi/kpi-footer.tsx +52 -0
- package/src/components/kpi/kpi-group.tsx +70 -0
- package/src/components/kpi/kpi-header.tsx +25 -0
- package/src/components/kpi/kpi-icon.tsx +28 -0
- package/src/components/kpi/kpi-sparkline.tsx +145 -0
- package/src/components/kpi/kpi-stat.tsx +20 -0
- package/src/components/kpi/kpi-title.tsx +19 -0
- package/src/components/kpi/kpi-trend.tsx +105 -0
- package/src/components/kpi/kpi-value.tsx +32 -0
- package/src/components/kpi/kpi.context.tsx +111 -0
- package/src/components/kpi/kpi.tsx +154 -0
- package/src/components/kpi/kpi.types.ts +9 -0
- package/src/components/kpi/kpi.variants.test.ts +428 -0
- package/src/components/kpi/kpi.variants.ts +347 -0
- package/src/components/label/AGENTS.md +89 -0
- package/src/components/label/index.ts +10 -0
- package/src/components/label/label.tsx +97 -0
- package/src/components/label/label.variants.test.ts +176 -0
- package/src/components/label/label.variants.ts +123 -0
- package/src/components/menu/AGENTS.md +176 -0
- package/src/components/menu/index.ts +44 -0
- package/src/components/menu/menu-background.tsx +17 -0
- package/src/components/menu/menu-checkbox-item.tsx +57 -0
- package/src/components/menu/menu-content.tsx +303 -0
- package/src/components/menu/menu-item.tsx +162 -0
- package/src/components/menu/menu-label.tsx +14 -0
- package/src/components/menu/menu-radio-group.tsx +50 -0
- package/src/components/menu/menu-radio-item.tsx +57 -0
- package/src/components/menu/menu-separator.tsx +11 -0
- package/src/components/menu/menu-sub-content.tsx +72 -0
- package/src/components/menu/menu-sub-trigger.tsx +50 -0
- package/src/components/menu/menu-sub.tsx +75 -0
- package/src/components/menu/menu-trigger.tsx +57 -0
- package/src/components/menu/menu.context.tsx +214 -0
- package/src/components/menu/menu.tsx +88 -0
- package/src/components/menu/menu.types.ts +33 -0
- package/src/components/menu/menu.variants.test.ts +400 -0
- package/src/components/menu/menu.variants.ts +353 -0
- package/src/components/meter/AGENTS.md +135 -0
- package/src/components/meter/index.ts +43 -0
- package/src/components/meter/meter-fill.tsx +19 -0
- package/src/components/meter/meter-header.tsx +17 -0
- package/src/components/meter/meter-label.tsx +18 -0
- package/src/components/meter/meter-output.tsx +42 -0
- package/src/components/meter/meter-segments.tsx +83 -0
- package/src/components/meter/meter-track.tsx +16 -0
- package/src/components/meter/meter.context.tsx +62 -0
- package/src/components/meter/meter.tsx +286 -0
- package/src/components/meter/meter.types.ts +30 -0
- package/src/components/meter/meter.variants.test.ts +430 -0
- package/src/components/meter/meter.variants.ts +347 -0
- package/src/components/overlay/AGENTS.md +89 -0
- package/src/components/overlay/index.ts +27 -0
- package/src/components/overlay/overlay-portal.tsx +84 -0
- package/src/components/overlay/overlay-presence.test.ts +46 -0
- package/src/components/overlay/overlay-presence.ts +49 -0
- package/src/components/overlay/overlay-registry.test.ts +96 -0
- package/src/components/overlay/overlay-registry.ts +99 -0
- package/src/components/overlay/overlay-scrim.tsx +49 -0
- package/src/components/overlay/overlay.context.tsx +48 -0
- package/src/components/overlay/overlay.tsx +118 -0
- package/src/components/overlay/overlay.variants.test.ts +37 -0
- package/src/components/overlay/overlay.variants.ts +33 -0
- package/src/components/overlay/use-overlay-back-handler.ts +34 -0
- package/src/components/overlay/use-overlay-presence.ts +87 -0
- package/src/components/popover/AGENTS.md +138 -0
- package/src/components/popover/index.ts +48 -0
- package/src/components/popover/popover-anchor.tsx +49 -0
- package/src/components/popover/popover-arrow.tsx +89 -0
- package/src/components/popover/popover-close.tsx +73 -0
- package/src/components/popover/popover-content.tsx +235 -0
- package/src/components/popover/popover-description.tsx +18 -0
- package/src/components/popover/popover-title.tsx +33 -0
- package/src/components/popover/popover-trigger.tsx +56 -0
- package/src/components/popover/popover.context.tsx +71 -0
- package/src/components/popover/popover.position.test.ts +354 -0
- package/src/components/popover/popover.position.ts +286 -0
- package/src/components/popover/popover.tsx +123 -0
- package/src/components/popover/popover.variants.test.ts +135 -0
- package/src/components/popover/popover.variants.ts +85 -0
- package/src/components/popover/use-anchor-measure.ts +76 -0
- package/src/components/popover/use-anchored-content.ts +219 -0
- package/src/components/pressable/AGENTS.md +11 -1
- package/src/components/pressable/pressable.tsx +7 -2
- package/src/components/pressable/pressable.variants.test.ts +29 -0
- package/src/components/pressable/pressable.variants.ts +17 -0
- package/src/components/progress/AGENTS.md +120 -0
- package/src/components/progress/index.ts +40 -0
- package/src/components/progress/progress-fill.tsx +152 -0
- package/src/components/progress/progress-header.tsx +19 -0
- package/src/components/progress/progress-label.tsx +27 -0
- package/src/components/progress/progress-output.tsx +47 -0
- package/src/components/progress/progress-track.tsx +38 -0
- package/src/components/progress/progress.context.tsx +79 -0
- package/src/components/progress/progress.tsx +171 -0
- package/src/components/progress/progress.types.ts +23 -0
- package/src/components/progress/progress.variants.test.ts +335 -0
- package/src/components/progress/progress.variants.ts +284 -0
- package/src/components/progress-button/AGENTS.md +116 -0
- package/src/components/progress-button/index.ts +36 -0
- package/src/components/progress-button/progress-button-done.tsx +33 -0
- package/src/components/progress-button/progress-button-label.tsx +27 -0
- package/src/components/progress-button/progress-button.context.tsx +93 -0
- package/src/components/progress-button/progress-button.tsx +414 -0
- package/src/components/progress-button/progress-button.variants.test.ts +314 -0
- package/src/components/progress-button/progress-button.variants.ts +236 -0
- package/src/components/provider/AGENTS.md +38 -25
- package/src/components/provider/provider.tsx +38 -11
- package/src/components/radio/AGENTS.md +8 -10
- package/src/components/radio/radio.variants.test.ts +5 -6
- package/src/components/radio/radio.variants.ts +6 -7
- package/src/components/rating/AGENTS.md +143 -0
- package/src/components/rating/index.ts +43 -0
- package/src/components/rating/rating-output.tsx +39 -0
- package/src/components/rating/rating-star.tsx +70 -0
- package/src/components/rating/rating-stars.tsx +219 -0
- package/src/components/rating/rating.context.tsx +79 -0
- package/src/components/rating/rating.tsx +163 -0
- package/src/components/rating/rating.variants.test.ts +347 -0
- package/src/components/rating/rating.variants.ts +354 -0
- package/src/components/screen/screen-navbar-back-button.tsx +1 -1
- package/src/components/selection-mode/AGENTS.md +155 -0
- package/src/components/selection-mode/index.ts +43 -0
- package/src/components/selection-mode/selection-mode-action.tsx +65 -0
- package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
- package/src/components/selection-mode/selection-mode-group.tsx +123 -0
- package/src/components/selection-mode/selection-mode-header.tsx +85 -0
- package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
- package/src/components/selection-mode/selection-mode-item.tsx +182 -0
- package/src/components/selection-mode/selection-mode.context.tsx +89 -0
- package/src/components/selection-mode/selection-mode.tsx +207 -0
- package/src/components/selection-mode/selection-mode.types.ts +36 -0
- package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
- package/src/components/selection-mode/selection-mode.variants.ts +329 -0
- package/src/components/skeleton/AGENTS.md +120 -0
- package/src/components/skeleton/index.ts +15 -0
- package/src/components/skeleton/skeleton-clock.ts +48 -0
- package/src/components/skeleton/skeleton-group.tsx +64 -0
- package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
- package/src/components/skeleton/skeleton.context.tsx +45 -0
- package/src/components/skeleton/skeleton.tsx +227 -0
- package/src/components/skeleton/skeleton.variants.test.ts +245 -0
- package/src/components/skeleton/skeleton.variants.ts +222 -0
- package/src/components/slide-button/AGENTS.md +122 -0
- package/src/components/slide-button/index.ts +43 -0
- package/src/components/slide-button/slide-button-label.tsx +70 -0
- package/src/components/slide-button/slide-button-thumb.tsx +93 -0
- package/src/components/slide-button/slide-button.context.tsx +76 -0
- package/src/components/slide-button/slide-button.tsx +457 -0
- package/src/components/slide-button/slide-button.variants.test.ts +288 -0
- package/src/components/slide-button/slide-button.variants.ts +324 -0
- package/src/components/slider/index.ts +2 -0
- package/src/components/slider/slider-thumb.tsx +33 -1
- package/src/components/slider/slider.variants.test.ts +52 -0
- package/src/components/slider/slider.variants.ts +56 -2
- package/src/components/stack-card/AGENTS.md +169 -0
- package/src/components/stack-card/index.ts +37 -0
- package/src/components/stack-card/stack-card-action.tsx +66 -0
- package/src/components/stack-card/stack-card-actions.tsx +18 -0
- package/src/components/stack-card/stack-card-card.tsx +71 -0
- package/src/components/stack-card/stack-card-empty.tsx +30 -0
- package/src/components/stack-card/stack-card-slot.tsx +90 -0
- package/src/components/stack-card/stack-card-stamp.tsx +76 -0
- package/src/components/stack-card/stack-card.context.tsx +117 -0
- package/src/components/stack-card/stack-card.tsx +557 -0
- package/src/components/stack-card/stack-card.types.ts +26 -0
- package/src/components/stack-card/stack-card.variants.test.ts +355 -0
- package/src/components/stack-card/stack-card.variants.ts +360 -0
- package/src/components/steps/AGENTS.md +153 -0
- package/src/components/steps/index.ts +45 -0
- package/src/components/steps/steps-connector.tsx +31 -0
- package/src/components/steps/steps-description.tsx +26 -0
- package/src/components/steps/steps-indicator.tsx +106 -0
- package/src/components/steps/steps-item.tsx +225 -0
- package/src/components/steps/steps-panel.tsx +41 -0
- package/src/components/steps/steps-title.tsx +32 -0
- package/src/components/steps/steps-track.tsx +59 -0
- package/src/components/steps/steps.context.tsx +123 -0
- package/src/components/steps/steps.tsx +166 -0
- package/src/components/steps/steps.variants.test.ts +388 -0
- package/src/components/steps/steps.variants.ts +468 -0
- package/src/components/surface/AGENTS.md +72 -0
- package/src/components/surface/index.ts +15 -0
- package/src/components/surface/surface.context.tsx +54 -0
- package/src/components/surface/surface.tsx +70 -0
- package/src/components/surface/surface.variants.test.ts +180 -0
- package/src/components/surface/surface.variants.ts +133 -0
- package/src/components/swipe/AGENTS.md +140 -0
- package/src/components/swipe/index.ts +31 -0
- package/src/components/swipe/swipe-action.tsx +99 -0
- package/src/components/swipe/swipe-end.tsx +14 -0
- package/src/components/swipe/swipe-group.tsx +55 -0
- package/src/components/swipe/swipe-panel.tsx +55 -0
- package/src/components/swipe/swipe-start.tsx +15 -0
- package/src/components/swipe/swipe.context.tsx +135 -0
- package/src/components/swipe/swipe.tsx +393 -0
- package/src/components/swipe/swipe.types.ts +7 -0
- package/src/components/swipe/swipe.variants.test.ts +337 -0
- package/src/components/swipe/swipe.variants.ts +290 -0
- package/src/components/switch/switch.variants.ts +2 -2
- package/src/components/tabs/AGENTS.md +4 -3
- package/src/components/tabs/tabs.variants.test.ts +1 -1
- package/src/components/tabs/tabs.variants.ts +4 -6
- package/src/components/textarea/AGENTS.md +86 -0
- package/src/components/textarea/index.ts +16 -0
- package/src/components/textarea/textarea.tsx +157 -0
- package/src/components/textarea/textarea.variants.test.ts +219 -0
- package/src/components/textarea/textarea.variants.ts +171 -0
- package/src/components/toast/AGENTS.md +140 -0
- package/src/components/toast/index.ts +74 -0
- package/src/components/toast/toast-action.tsx +59 -0
- package/src/components/toast/toast-api.ts +32 -0
- package/src/components/toast/toast-close.tsx +44 -0
- package/src/components/toast/toast-content.tsx +11 -0
- package/src/components/toast/toast-description.tsx +11 -0
- package/src/components/toast/toast-indicator.tsx +34 -0
- package/src/components/toast/toast-item.tsx +308 -0
- package/src/components/toast/toast-title.tsx +13 -0
- package/src/components/toast/toast-viewport.tsx +135 -0
- package/src/components/toast/toast.context.tsx +69 -0
- package/src/components/toast/toast.store.test.ts +259 -0
- package/src/components/toast/toast.store.ts +338 -0
- package/src/components/toast/toast.timer.test.ts +64 -0
- package/src/components/toast/toast.timer.ts +51 -0
- package/src/components/toast/toast.tsx +132 -0
- package/src/components/toast/toast.variants.test.ts +393 -0
- package/src/components/toast/toast.variants.ts +304 -0
- package/src/components/toggle-button/AGENTS.md +106 -0
- package/src/components/toggle-button/index.ts +30 -0
- package/src/components/toggle-button/toggle-button-group.tsx +156 -0
- package/src/components/toggle-button/toggle-button-label.tsx +19 -0
- package/src/components/toggle-button/toggle-button.context.tsx +130 -0
- package/src/components/toggle-button/toggle-button.tsx +172 -0
- package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
- package/src/components/toggle-button/toggle-button.variants.ts +160 -0
- package/src/components/tooltip/AGENTS.md +133 -0
- package/src/components/tooltip/index.ts +28 -0
- package/src/components/tooltip/tooltip-arrow.tsx +40 -0
- package/src/components/tooltip/tooltip-content.tsx +179 -0
- package/src/components/tooltip/tooltip-description.tsx +19 -0
- package/src/components/tooltip/tooltip-text.tsx +21 -0
- package/src/components/tooltip/tooltip-title.tsx +20 -0
- package/src/components/tooltip/tooltip-trigger.tsx +103 -0
- package/src/components/tooltip/tooltip.context.tsx +68 -0
- package/src/components/tooltip/tooltip.tsx +213 -0
- package/src/components/tooltip/tooltip.variants.test.ts +231 -0
- package/src/components/tooltip/tooltip.variants.ts +177 -0
- package/src/hooks/use-calm-motion.tsx +31 -0
- package/src/lib/calm-motion.test.ts +17 -0
- package/src/lib/calm-motion.ts +23 -0
- package/src/lib/cn.test.ts +14 -0
- package/src/lib/cn.ts +2 -2
- package/src/no-gorhom.test.ts +54 -0
- package/src/styles/geometry.test.ts +1 -0
- package/src/styles/theme.css +4 -0
- package/src/styles/tokens.css +11 -0
- package/src/styles/tokens.test.ts +22 -0
- package/src/styles/tokens.ts +20 -0
- package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
- package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Skeleton
|
|
2
|
+
|
|
3
|
+
A placeholder standing in for content that is still loading: a tinted shape
|
|
4
|
+
with a glint sweeping across it, or a pulse, on the UI thread. Compound root plus
|
|
5
|
+
`Skeleton.Group`, which synchronises the skeletons inside it, and
|
|
6
|
+
`Skeleton.Lines`, a paragraph of placeholder lines.
|
|
7
|
+
|
|
8
|
+
`import { Skeleton } from "@delacour/react-native-ui/skeleton";`
|
|
9
|
+
|
|
10
|
+
## Files
|
|
11
|
+
|
|
12
|
+
| File | What it holds |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `index.ts` | → `@delacour/react-native-ui/skeleton` |
|
|
15
|
+
| `skeleton.tsx` | Root, `Skeleton.Lines` and the `Object.assign` compound surface |
|
|
16
|
+
| `skeleton-group.tsx` | `Skeleton.Group` — the shared clock and loading flag |
|
|
17
|
+
| `skeleton-shimmer.tsx` | The sweeping band, an internal leaf |
|
|
18
|
+
| `skeleton-clock.ts` | `useSkeletonClock`, the 0 → 1 UI-thread loop both animations read |
|
|
19
|
+
| `skeleton.context.tsx` | `SkeletonGroupProvider`, `useSkeletonGroup()` |
|
|
20
|
+
| `skeleton.variants.ts` | Pure `tv()` slots + resolvers + worklets, no RN imports |
|
|
21
|
+
| `skeleton.variants.test.ts` | |
|
|
22
|
+
|
|
23
|
+
## Design
|
|
24
|
+
|
|
25
|
+
- **Three shapes, and a shape is a corner plus a default size.** `rect` is a
|
|
26
|
+
card-shaped `rounded-lg` (see **Sizing** in the package doc for why a card is
|
|
27
|
+
that step), `line` is a line of text at `rounded-sm`, `circle` is an avatar at
|
|
28
|
+
`rounded-full`. A childless skeleton also takes a default size — `h-24 w-full`,
|
|
29
|
+
`h-3.5 w-full`, `size-10` — that a caller's `className` overrides, since
|
|
30
|
+
tailwind-merge sees both in one chain. The defaults exist so an unsized
|
|
31
|
+
skeleton is visible rather than a zero-height nothing.
|
|
32
|
+
- **Children are the content, and they size the placeholder.** A skeleton that
|
|
33
|
+
wraps children renders them underneath at opacity 0 while loading, so the grey
|
|
34
|
+
shape is exactly the size the content will be and **nothing moves when it
|
|
35
|
+
lands**. The shape then contributes only its corner: a default height would
|
|
36
|
+
fight the content, then clip it once shown. Flip `isLoading` off and the fill
|
|
37
|
+
and the clip drop and the content fades in over `SKELETON_REVEAL_MS`. The
|
|
38
|
+
children are never unmounted between the two states, so their own state
|
|
39
|
+
survives the swap. `pointerEvents` is off on them while they are invisible —
|
|
40
|
+
an invisible button that still takes a tap is a trap.
|
|
41
|
+
- **One clock drives both animations.** `useSkeletonClock` loops 0 → 1 every
|
|
42
|
+
`SKELETON_CYCLE_MS`. A shimmer reads it as the band's position
|
|
43
|
+
(`skeletonShimmerOffset`), a pulse as a point on a cosine
|
|
44
|
+
(`skeletonPulseOpacity`). That is what lets a `Skeleton.Group` hold a single
|
|
45
|
+
clock for every skeleton inside it without caring which each one draws.
|
|
46
|
+
- **`Skeleton.Group` exists to keep a region in step.** Separate skeletons
|
|
47
|
+
start their clocks when they mount, so a list whose rows mount a frame apart
|
|
48
|
+
shimmers as a ripple of unrelated glints. Inside a group they read the
|
|
49
|
+
group's clock and move together, and one `isLoading` reveals the whole
|
|
50
|
+
region. A skeleton's own `isLoading` and `animation` still win over the
|
|
51
|
+
group's; it starts a clock of its own only when there is no running group
|
|
52
|
+
clock to share.
|
|
53
|
+
- **The fill is a tint, `bg-muted-foreground/20`, not `bg-muted`.** `muted`
|
|
54
|
+
sits within 0.015 lightness of the light page, and a placeholder drawn in it
|
|
55
|
+
was verified on the simulator to all but vanish there. A fifth of the muted
|
|
56
|
+
foreground lands a clear step off whatever it sits on — page or card, light or
|
|
57
|
+
dark — because it is translucent, and it still leaves headroom above it for
|
|
58
|
+
the glint. `SKELETON_FILL_CLASS` names it and a test pins it to a tint.
|
|
59
|
+
- **The glint is painted in `elevated`, not white.** `elevated` is the raised
|
|
60
|
+
surface in both themes — white in light, a lifted grey in dark — so it sits
|
|
61
|
+
above the tinted fill either way. White would glare on dark; a translucent
|
|
62
|
+
black would darken rather than lighten in light mode. Following the token
|
|
63
|
+
also means a pasted palette carries the glint with it. The band is
|
|
64
|
+
a react-native-svg gradient, transparent at both ends, with low quarter stops
|
|
65
|
+
so it reads as a soft glint rather than a ridge.
|
|
66
|
+
- **The band sweeps from wholly off one edge to wholly off the other.**
|
|
67
|
+
`skeletonShimmerOffset(0)` is `-bandWidth`, `(1)` is the placeholder width, so
|
|
68
|
+
the band enters from nothing and leaves to nothing and the wrap back to 0 has
|
|
69
|
+
no visible jump. Its width is a share of the placeholder, bounded both ways —
|
|
70
|
+
a chip still gets a visible band and a full-bleed card does not get a wash.
|
|
71
|
+
The band is sized by Yoga from its class (`w-3/5 min-w-12 max-w-60`), and
|
|
72
|
+
both widths are read with `measure` inside the animated style, so the skeleton
|
|
73
|
+
never re-renders to animate and each frame writes only `transform` and
|
|
74
|
+
`opacity`. Until both are measured the band is transparent.
|
|
75
|
+
- **The gradient is a unit viewBox stretched over an absolutely filled `Svg`.**
|
|
76
|
+
Found on the simulator: an `Svg` at `width="100%"` kept its first size when
|
|
77
|
+
the band resized, so when raising `Skeleton.Lines`' count turned the old last
|
|
78
|
+
line into a full one, that line's glint sat off-centre and swept visibly out
|
|
79
|
+
of step with the rest. `StyleSheet.absoluteFill` plus
|
|
80
|
+
`preserveAspectRatio="none"` makes the native view follow the band.
|
|
81
|
+
- **Calm motion stills the skeleton, it does not hide it.**
|
|
82
|
+
`resolveSkeletonAnimation` returns `none` while `useCalmMotion()` is true — the
|
|
83
|
+
OS reduce-motion setting, or `DelacourProvider`'s `isMotionCalm` — and the
|
|
84
|
+
clock is never started. The second half is for E2E builds: a test runner that
|
|
85
|
+
waits for the screen to settle before each gesture pays its whole timeout on a
|
|
86
|
+
screen holding a shimmer that never ends. The shape is the message: a placeholder that stops
|
|
87
|
+
moving still reads as content on its way. The clock's own timing sets
|
|
88
|
+
`ReduceMotion.Never` because the decision has already been made one level up —
|
|
89
|
+
under the default `System` policy `withTiming` completes instantly and
|
|
90
|
+
`withRepeat(-1)` would spin a zero-length animation forever, the trap
|
|
91
|
+
[Spinner](../spinner/AGENTS.md) documents too.
|
|
92
|
+
- **The pulse owns `opacity`.** Its animated style goes after the caller's
|
|
93
|
+
`style`, so an `opacity` passed there has no effect while it pulses. The floor
|
|
94
|
+
is `SKELETON_PULSE_MIN_OPACITY`, well above zero — a placeholder that blinks
|
|
95
|
+
out reads as a failing layout, not a loading one.
|
|
96
|
+
- **Hidden from assistive technology unless labelled.** An unlabelled grey box
|
|
97
|
+
announces nothing worth hearing, and a screen of them announces it many times
|
|
98
|
+
over, so the default is `accessibilityElementsHidden` plus
|
|
99
|
+
`importantForAccessibility="no-hide-descendants"`. A `label` turns the
|
|
100
|
+
skeleton into one busy `progressbar` status. Label the one that stands for the
|
|
101
|
+
region — or the `Skeleton.Group`, or the `Skeleton.Lines` paragraph — never
|
|
102
|
+
each line. Once loaded the skeleton contributes no accessibility props at all
|
|
103
|
+
and the content speaks for itself. `resolveSkeletonAccessibility` returns a
|
|
104
|
+
discriminated union (`hidden` / `status` / `content`) so the three cases are
|
|
105
|
+
pinned in `bun test`.
|
|
106
|
+
- **An unlabelled group takes no role.** It may hold real content beside its
|
|
107
|
+
placeholders, and hiding the group would hide that too; each skeleton already
|
|
108
|
+
hides itself.
|
|
109
|
+
- **`Skeleton.Lines` lives in the root file.** It renders `SkeletonRoot`
|
|
110
|
+
directly, and a file of its own would have to import `./skeleton` and close a
|
|
111
|
+
cycle (package rule 3). Only the last line is shortened, and only with two or
|
|
112
|
+
more — a single line is a title whose length the caller already knows. Its
|
|
113
|
+
line keys are positional because lines never reorder. **Outside a group it is
|
|
114
|
+
a `Skeleton.Group` of its own.** Found on the simulator: with a clock per line,
|
|
115
|
+
a line added by raising `lines` started from zero and glinted out of step with
|
|
116
|
+
the rest. Inside a group it is a plain container, since the group's clock
|
|
117
|
+
already serves every line.
|
|
118
|
+
- **A number is not a class here either.** An arbitrary height like `h-[200px]`
|
|
119
|
+
only exists once Tailwind has scanned it; pass a computed dimension through
|
|
120
|
+
`style`, which the root spreads before the pulse.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { Skeleton, type SkeletonLinesProps, type SkeletonProps } from "./skeleton";
|
|
2
|
+
export { type SkeletonGroupContextValue, useSkeletonGroup } from "./skeleton.context";
|
|
3
|
+
export {
|
|
4
|
+
resolveSkeletonAccessibility,
|
|
5
|
+
resolveSkeletonAnimation,
|
|
6
|
+
resolveSkeletonLineWidths,
|
|
7
|
+
SKELETON_ANIMATIONS,
|
|
8
|
+
SKELETON_CYCLE_MS,
|
|
9
|
+
SKELETON_SHAPES,
|
|
10
|
+
type SkeletonAccessibility,
|
|
11
|
+
type SkeletonAnimation,
|
|
12
|
+
type SkeletonShape,
|
|
13
|
+
skeletonVariants,
|
|
14
|
+
} from "./skeleton.variants";
|
|
15
|
+
export type { SkeletonGroupProps } from "./skeleton-group";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
cancelAnimation,
|
|
4
|
+
Easing,
|
|
5
|
+
ReduceMotion,
|
|
6
|
+
type SharedValue,
|
|
7
|
+
useSharedValue,
|
|
8
|
+
withRepeat,
|
|
9
|
+
withTiming,
|
|
10
|
+
} from "react-native-reanimated";
|
|
11
|
+
import { SKELETON_CYCLE_MS } from "./skeleton.variants";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A 0 → 1 clock that loops on the UI thread while `isRunning`.
|
|
15
|
+
*
|
|
16
|
+
* Both animations read the same clock — a shimmer as the band's position, a
|
|
17
|
+
* pulse as a point on a cosine — which is what lets a `Skeleton.Group` hold one
|
|
18
|
+
* for every placeholder inside it, whatever each of them draws.
|
|
19
|
+
*
|
|
20
|
+
* The timing sets `ReduceMotion.Never` deliberately. Under the default
|
|
21
|
+
* `System` policy `withTiming` completes instantly while the OS setting is on,
|
|
22
|
+
* so `withRepeat(-1)` would spin a zero-length animation forever. Reduce-motion
|
|
23
|
+
* is honoured one level up instead, where `resolveSkeletonAnimation` stills the
|
|
24
|
+
* skeleton and this clock is never started.
|
|
25
|
+
*/
|
|
26
|
+
export function useSkeletonClock(isRunning: boolean): SharedValue<number> {
|
|
27
|
+
const progress = useSharedValue(0);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!isRunning) {
|
|
31
|
+
cancelAnimation(progress);
|
|
32
|
+
progress.value = 0;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
progress.value = 0;
|
|
37
|
+
progress.value = withRepeat(
|
|
38
|
+
withTiming(1, { duration: SKELETON_CYCLE_MS, easing: Easing.linear, reduceMotion: ReduceMotion.Never }),
|
|
39
|
+
-1,
|
|
40
|
+
false
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Without this the repeat outlives the unmount when the content lands.
|
|
44
|
+
return () => cancelAnimation(progress);
|
|
45
|
+
}, [isRunning, progress]);
|
|
46
|
+
|
|
47
|
+
return progress;
|
|
48
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode, useMemo } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { useCalmMotion } from "../../hooks/use-calm-motion";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import { type SkeletonGroupContextValue, SkeletonGroupProvider } from "./skeleton.context";
|
|
6
|
+
import { resolveSkeletonAccessibility, resolveSkeletonAnimation, type SkeletonAnimation } from "./skeleton.variants";
|
|
7
|
+
import { useSkeletonClock } from "./skeleton-clock";
|
|
8
|
+
|
|
9
|
+
export type SkeletonGroupProps = Omit<ViewProps, "children"> & {
|
|
10
|
+
/** Whether the group's content is still loading. Every skeleton inside follows it unless told otherwise. Defaults to `true`. */
|
|
11
|
+
isLoading?: boolean;
|
|
12
|
+
/** The animation every skeleton inside runs unless it names its own. Defaults to `shimmer`. */
|
|
13
|
+
animation?: SkeletonAnimation;
|
|
14
|
+
/**
|
|
15
|
+
* What is loading, for a screen reader. Announced once for the whole group, as
|
|
16
|
+
* a busy status, while it loads. Leave the skeletons inside unlabelled.
|
|
17
|
+
*/
|
|
18
|
+
label?: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Holds one clock for every skeleton inside it, and one loading flag.
|
|
25
|
+
*
|
|
26
|
+
* Separate placeholders each start their own clock when they mount, so a list
|
|
27
|
+
* whose rows arrive a frame apart shimmers out of step — a ripple of unrelated
|
|
28
|
+
* glints instead of one sweep. Inside a group they all read this clock, so they
|
|
29
|
+
* move together, and flipping one `isLoading` reveals the whole region at once.
|
|
30
|
+
*
|
|
31
|
+
* It lays nothing out; it is a plain `View` for the caller to arrange.
|
|
32
|
+
*/
|
|
33
|
+
export function SkeletonGroup({
|
|
34
|
+
isLoading = true,
|
|
35
|
+
animation,
|
|
36
|
+
label,
|
|
37
|
+
className,
|
|
38
|
+
children,
|
|
39
|
+
...props
|
|
40
|
+
}: SkeletonGroupProps): ReactElement {
|
|
41
|
+
const isCalm = useCalmMotion();
|
|
42
|
+
const resolvedAnimation = resolveSkeletonAnimation(animation, isCalm);
|
|
43
|
+
const isRunning = isLoading && resolvedAnimation !== "none";
|
|
44
|
+
const progress = useSkeletonClock(isRunning);
|
|
45
|
+
|
|
46
|
+
const context = useMemo<SkeletonGroupContextValue>(
|
|
47
|
+
() => ({ animation: resolvedAnimation, isLoading, isRunning, progress }),
|
|
48
|
+
[resolvedAnimation, isLoading, isRunning, progress]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Only a labelled group takes a role. An unlabelled one may hold real
|
|
52
|
+
// content beside its placeholders, and hiding it would hide that too — each
|
|
53
|
+
// skeleton already hides itself.
|
|
54
|
+
const accessibility = label ? resolveSkeletonAccessibility({ isLoading, label }).props : undefined;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<SkeletonGroupProvider value={context}>
|
|
58
|
+
<View className={cn(className)} {...accessibility} {...props}>
|
|
59
|
+
{children}
|
|
60
|
+
</View>
|
|
61
|
+
</SkeletonGroupProvider>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
SkeletonGroup.displayName = "DelacourUI.Skeleton.Group";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type ReactElement, useId } from "react";
|
|
2
|
+
import { StyleSheet } from "react-native";
|
|
3
|
+
import Animated, {
|
|
4
|
+
type AnimatedRef,
|
|
5
|
+
measure,
|
|
6
|
+
type SharedValue,
|
|
7
|
+
useAnimatedRef,
|
|
8
|
+
useAnimatedStyle,
|
|
9
|
+
} from "react-native-reanimated";
|
|
10
|
+
import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";
|
|
11
|
+
import { useThemeColor } from "../../hooks/use-theme-color";
|
|
12
|
+
import { SKELETON_SHIMMER_STOPS, skeletonShimmerOffset, skeletonVariants } from "./skeleton.variants";
|
|
13
|
+
|
|
14
|
+
/** The token the band is painted in — a surface that rises above the placeholder fill in both themes. */
|
|
15
|
+
const SHIMMER_TOKEN = "elevated";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The glint that sweeps across a loading skeleton.
|
|
19
|
+
*
|
|
20
|
+
* A band as tall as the skeleton and 60% as wide — bounded to 48–240pt by its
|
|
21
|
+
* class — carried from off one edge to off the other by the clock. The root clips
|
|
22
|
+
* it, so a circle's glint stays inside the circle.
|
|
23
|
+
*
|
|
24
|
+
* Painted in `elevated` rather than white or a translucent black. That token is
|
|
25
|
+
* the raised surface in both themes — white in light, a lifted grey in dark —
|
|
26
|
+
* so it lands above the skeleton's tinted fill either way, reads as a
|
|
27
|
+
* highlight, and follows a pasted palette with no support from here.
|
|
28
|
+
*
|
|
29
|
+
* Everything runs on the UI thread, so a skeleton never re-renders to animate.
|
|
30
|
+
*/
|
|
31
|
+
export function SkeletonShimmer({
|
|
32
|
+
progress,
|
|
33
|
+
container,
|
|
34
|
+
}: {
|
|
35
|
+
progress: SharedValue<number>;
|
|
36
|
+
container: AnimatedRef<Animated.View>;
|
|
37
|
+
}): ReactElement {
|
|
38
|
+
const color = useThemeColor(SHIMMER_TOKEN);
|
|
39
|
+
const band = useAnimatedRef<Animated.View>();
|
|
40
|
+
|
|
41
|
+
// Gradient ids are global to the document, and React 19's `«r1»`-style ids
|
|
42
|
+
// carry delimiters that are invalid inside `url(#…)`.
|
|
43
|
+
const id = `skeleton-shimmer-${useId().replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
44
|
+
|
|
45
|
+
// Both widths are measured on the UI thread, so the sweep always uses the
|
|
46
|
+
// layout as it is now and nothing round-trips through JavaScript. Only
|
|
47
|
+
// `transform` and `opacity` are written, so every frame takes Reanimated's
|
|
48
|
+
// fast path and never commits layout; Yoga sizes the band from its class.
|
|
49
|
+
const motionStyle = useAnimatedStyle(() => {
|
|
50
|
+
const outer = measure(container);
|
|
51
|
+
const inner = measure(band);
|
|
52
|
+
if (!outer || !inner) return { opacity: 0, transform: [{ translateX: 0 }] };
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
opacity: 1,
|
|
56
|
+
transform: [{ translateX: skeletonShimmerOffset(progress.value, outer.width, inner.width) }],
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// The gradient is drawn in a unit viewBox stretched over an absolutely
|
|
61
|
+
// filled Svg, never at `width="100%"`. On the simulator a percentage-sized
|
|
62
|
+
// Svg kept its first size when the band resized — the last line of a
|
|
63
|
+
// paragraph becoming a full one — so its bright centre sat off the band's
|
|
64
|
+
// centre and the line glinted out of step with its neighbours.
|
|
65
|
+
return (
|
|
66
|
+
<Animated.View className={skeletonVariants().band()} pointerEvents="none" ref={band} style={motionStyle}>
|
|
67
|
+
<Svg preserveAspectRatio="none" style={StyleSheet.absoluteFill} viewBox="0 0 1 1">
|
|
68
|
+
<Defs>
|
|
69
|
+
<LinearGradient id={id} x1="0" x2="1" y1="0" y2="0">
|
|
70
|
+
{SKELETON_SHIMMER_STOPS.map((stop) => (
|
|
71
|
+
<Stop key={stop.offset} offset={stop.offset} stopColor={color} stopOpacity={stop.opacity} />
|
|
72
|
+
))}
|
|
73
|
+
</LinearGradient>
|
|
74
|
+
</Defs>
|
|
75
|
+
<Rect fill={`url(#${id})`} height={1} width={1} x={0} y={0} />
|
|
76
|
+
</Svg>
|
|
77
|
+
</Animated.View>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
SkeletonShimmer.displayName = "DelacourUI.Skeleton.Shimmer";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createContext, type ReactElement, type ReactNode, use } from "react";
|
|
2
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
3
|
+
import type { SkeletonAnimation } from "./skeleton.variants";
|
|
4
|
+
|
|
5
|
+
export type SkeletonGroupContextValue = {
|
|
6
|
+
/** The group's clock, 0 → 1 once per cycle. Only meaningful while `isRunning`. */
|
|
7
|
+
progress: SharedValue<number>;
|
|
8
|
+
/** Whether the clock is ticking — the group is loading and has an animation to run. */
|
|
9
|
+
isRunning: boolean;
|
|
10
|
+
/** The animation the group resolved, reduce-motion already applied. */
|
|
11
|
+
animation: SkeletonAnimation;
|
|
12
|
+
/** Whether the group's content is still loading. */
|
|
13
|
+
isLoading: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const SkeletonGroupContext = createContext<SkeletonGroupContextValue | null>(null);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Supplies a `Skeleton.Group`'s clock and loading state to every skeleton in
|
|
20
|
+
* its subtree.
|
|
21
|
+
*
|
|
22
|
+
* Lives in its own module, importing nothing but React and types, so the root
|
|
23
|
+
* and the group can both read it without importing each other. See AGENTS.md
|
|
24
|
+
* rule 3.
|
|
25
|
+
*/
|
|
26
|
+
export function SkeletonGroupProvider({
|
|
27
|
+
value,
|
|
28
|
+
children,
|
|
29
|
+
}: {
|
|
30
|
+
value: SkeletonGroupContextValue;
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
}): ReactElement {
|
|
33
|
+
return <SkeletonGroupContext value={value}>{children}</SkeletonGroupContext>;
|
|
34
|
+
}
|
|
35
|
+
SkeletonGroupProvider.displayName = "DelacourUI.Skeleton.GroupProvider";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The enclosing `Skeleton.Group`, or null outside one.
|
|
39
|
+
*
|
|
40
|
+
* Optional by design: a skeleton is complete on its own, and a group only
|
|
41
|
+
* synchronises the ones it holds.
|
|
42
|
+
*/
|
|
43
|
+
export function useSkeletonGroup(): SkeletonGroupContextValue | null {
|
|
44
|
+
return use(SkeletonGroupContext);
|
|
45
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode, useEffect } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import Animated, { useAnimatedRef, useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
|
|
4
|
+
import { useCalmMotion } from "../../hooks/use-calm-motion";
|
|
5
|
+
import { cn } from "../../lib/cn";
|
|
6
|
+
import { useSkeletonGroup } from "./skeleton.context";
|
|
7
|
+
import {
|
|
8
|
+
resolveSkeletonAccessibility,
|
|
9
|
+
resolveSkeletonAnimation,
|
|
10
|
+
resolveSkeletonLineWidths,
|
|
11
|
+
SKELETON_REVEAL_MS,
|
|
12
|
+
type SkeletonAnimation,
|
|
13
|
+
type SkeletonShape,
|
|
14
|
+
skeletonPulseOpacity,
|
|
15
|
+
skeletonVariants,
|
|
16
|
+
} from "./skeleton.variants";
|
|
17
|
+
import { useSkeletonClock } from "./skeleton-clock";
|
|
18
|
+
import { SkeletonGroup } from "./skeleton-group";
|
|
19
|
+
import { SkeletonShimmer } from "./skeleton-shimmer";
|
|
20
|
+
|
|
21
|
+
export type SkeletonProps = Omit<ViewProps, "children"> & {
|
|
22
|
+
/** `rect` (a card or an image), `line` (a line of text) or `circle` (an avatar). Defaults to `rect`. */
|
|
23
|
+
shape?: SkeletonShape;
|
|
24
|
+
/**
|
|
25
|
+
* `shimmer` sweeps a glint across the placeholder, `pulse` breathes its
|
|
26
|
+
* opacity, `none` holds it still. Inherited from an enclosing `Skeleton.Group`,
|
|
27
|
+
* then `shimmer`. Every animation stills under the OS reduce-motion setting, and under `DelacourProvider`'s `isMotionCalm`.
|
|
28
|
+
*/
|
|
29
|
+
animation?: SkeletonAnimation;
|
|
30
|
+
/**
|
|
31
|
+
* Whether the content is still loading. Inherited from an enclosing
|
|
32
|
+
* `Skeleton.Group`, then `true`. Flipped off, the placeholder gives way to its
|
|
33
|
+
* children, which fade in.
|
|
34
|
+
*/
|
|
35
|
+
isLoading?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* What is loading, for a screen reader. Setting it makes this skeleton
|
|
38
|
+
* announce as a busy status; leaving it unset keeps the placeholder out of the
|
|
39
|
+
* accessibility tree. Label the one skeleton standing for a region, not all of them.
|
|
40
|
+
*/
|
|
41
|
+
label?: string;
|
|
42
|
+
className?: string;
|
|
43
|
+
/**
|
|
44
|
+
* The real content. Rendered invisibly underneath while loading, so the
|
|
45
|
+
* placeholder takes its exact size and nothing moves when it lands.
|
|
46
|
+
*/
|
|
47
|
+
children?: ReactNode;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function SkeletonRoot({
|
|
51
|
+
shape = "rect",
|
|
52
|
+
animation,
|
|
53
|
+
isLoading,
|
|
54
|
+
label,
|
|
55
|
+
className,
|
|
56
|
+
style,
|
|
57
|
+
children,
|
|
58
|
+
...props
|
|
59
|
+
}: SkeletonProps): ReactElement {
|
|
60
|
+
const group = useSkeletonGroup();
|
|
61
|
+
const isCalm = useCalmMotion();
|
|
62
|
+
|
|
63
|
+
const loading = isLoading ?? group?.isLoading ?? true;
|
|
64
|
+
const resolvedAnimation = resolveSkeletonAnimation(animation ?? group?.animation, isCalm);
|
|
65
|
+
const isAnimating = loading && resolvedAnimation !== "none";
|
|
66
|
+
|
|
67
|
+
// One clock drives either animation, so a group's clock serves every
|
|
68
|
+
// skeleton inside it whatever each of them draws. A skeleton starts its own
|
|
69
|
+
// only when there is no running group clock to share.
|
|
70
|
+
const groupClock = group?.isRunning ? group.progress : undefined;
|
|
71
|
+
const ownClock = useSkeletonClock(isAnimating && groupClock === undefined);
|
|
72
|
+
const progress = groupClock ?? ownClock;
|
|
73
|
+
|
|
74
|
+
const root = useAnimatedRef<Animated.View>();
|
|
75
|
+
const reveal = useSharedValue(loading ? 0 : 1);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
// Hidden at once when loading starts again; faded in when it stops. The
|
|
79
|
+
// default reduce-motion policy makes the fade instant under that setting.
|
|
80
|
+
reveal.value = loading ? 0 : withTiming(1, { duration: SKELETON_REVEAL_MS });
|
|
81
|
+
}, [loading, reveal]);
|
|
82
|
+
|
|
83
|
+
const isPulsing = isAnimating && resolvedAnimation === "pulse";
|
|
84
|
+
|
|
85
|
+
const pulseStyle = useAnimatedStyle(() => ({
|
|
86
|
+
opacity: isPulsing ? skeletonPulseOpacity(progress.value) : 1,
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
const revealStyle = useAnimatedStyle(() => ({ opacity: reveal.value }));
|
|
90
|
+
|
|
91
|
+
const hasChildren = children !== undefined && children !== null && children !== false;
|
|
92
|
+
const slots = skeletonVariants({ isEmpty: !hasChildren, isLoading: loading, shape });
|
|
93
|
+
const accessibility = resolveSkeletonAccessibility({ isLoading: loading, label });
|
|
94
|
+
|
|
95
|
+
// The pulse owns opacity, so its style goes after the caller's.
|
|
96
|
+
return (
|
|
97
|
+
<Animated.View
|
|
98
|
+
className={slots.root({ className })}
|
|
99
|
+
ref={root}
|
|
100
|
+
style={[style, pulseStyle]}
|
|
101
|
+
{...accessibility.props}
|
|
102
|
+
{...props}
|
|
103
|
+
>
|
|
104
|
+
{hasChildren ? (
|
|
105
|
+
<Animated.View className={slots.content()} pointerEvents={loading ? "none" : "auto"} style={revealStyle}>
|
|
106
|
+
{children}
|
|
107
|
+
</Animated.View>
|
|
108
|
+
) : null}
|
|
109
|
+
{isAnimating && resolvedAnimation === "shimmer" ? <SkeletonShimmer container={root} progress={progress} /> : null}
|
|
110
|
+
</Animated.View>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type SkeletonLinesProps = Omit<SkeletonProps, "shape" | "children" | "style"> & {
|
|
115
|
+
/** How many lines to draw. Defaults to 3. */
|
|
116
|
+
lines?: number;
|
|
117
|
+
/** How far across the last line runs, 0–1. Only applies with two or more lines. Defaults to 0.6. */
|
|
118
|
+
lastLineWidth?: number;
|
|
119
|
+
/** Classes for each line — a height to match the text it stands in for, say. */
|
|
120
|
+
lineClassName?: string;
|
|
121
|
+
/** The paragraph itself, shown in place of the lines once loaded. */
|
|
122
|
+
children?: ReactNode;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A paragraph of placeholder lines, the last one shortened the way prose ends.
|
|
127
|
+
*
|
|
128
|
+
* Lives in the root file because it composes `SkeletonRoot` directly — a file of
|
|
129
|
+
* its own would have to import `./skeleton` and close a cycle. Its lines read
|
|
130
|
+
* the enclosing group's clock, or one the paragraph holds itself when there is
|
|
131
|
+
* no group, so they always glint in step. The label goes on the paragraph,
|
|
132
|
+
* never on each line.
|
|
133
|
+
*/
|
|
134
|
+
function SkeletonLines({
|
|
135
|
+
lines = 3,
|
|
136
|
+
lastLineWidth,
|
|
137
|
+
lineClassName,
|
|
138
|
+
animation,
|
|
139
|
+
isLoading,
|
|
140
|
+
label,
|
|
141
|
+
className,
|
|
142
|
+
children,
|
|
143
|
+
...props
|
|
144
|
+
}: SkeletonLinesProps): ReactElement {
|
|
145
|
+
const group = useSkeletonGroup();
|
|
146
|
+
const loading = isLoading ?? group?.isLoading ?? true;
|
|
147
|
+
|
|
148
|
+
if (!loading) {
|
|
149
|
+
return (
|
|
150
|
+
<View className={className} {...props}>
|
|
151
|
+
{children}
|
|
152
|
+
</View>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const widths = resolveSkeletonLineWidths(lines, lastLineWidth);
|
|
157
|
+
const containerClassName = cn("w-full gap-2", className);
|
|
158
|
+
|
|
159
|
+
// Lines are positional and never reorder, so the index is their identity.
|
|
160
|
+
const content = widths.map((width, index) => (
|
|
161
|
+
<SkeletonRoot
|
|
162
|
+
animation={animation}
|
|
163
|
+
className={lineClassName}
|
|
164
|
+
isLoading
|
|
165
|
+
key={`line-${index.toString()}`}
|
|
166
|
+
shape="line"
|
|
167
|
+
style={{ width }}
|
|
168
|
+
/>
|
|
169
|
+
));
|
|
170
|
+
|
|
171
|
+
// Inside a group the lines already share its clock, so the paragraph is a
|
|
172
|
+
// plain container. Alone, it becomes a group of its own — otherwise a line
|
|
173
|
+
// added later starts a clock of its own and glints out of step with the rest.
|
|
174
|
+
if (group) {
|
|
175
|
+
const accessibility = resolveSkeletonAccessibility({ isLoading: true, label });
|
|
176
|
+
return (
|
|
177
|
+
<View className={containerClassName} {...accessibility.props} {...props}>
|
|
178
|
+
{content}
|
|
179
|
+
</View>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<SkeletonGroup animation={animation} className={containerClassName} label={label} {...props}>
|
|
185
|
+
{content}
|
|
186
|
+
</SkeletonGroup>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
SkeletonLines.displayName = "DelacourUI.Skeleton.Lines";
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A placeholder standing in for content that is still loading.
|
|
193
|
+
*
|
|
194
|
+
* Drawn as a tinted shape — `rect`, `line` or `circle` — with a glint of
|
|
195
|
+
* `elevated` sweeping across it on the UI thread, or a pulse of its opacity.
|
|
196
|
+
* Under the OS reduce-motion setting it holds still: the shape is the message.
|
|
197
|
+
*
|
|
198
|
+
* Give it the size of the content it replaces, or pass that content as children
|
|
199
|
+
* and let it take the size itself — the content is rendered invisibly
|
|
200
|
+
* underneath, so nothing moves when `isLoading` flips off and it fades in.
|
|
201
|
+
*
|
|
202
|
+
* Hidden from screen readers unless given a `label`; label the one skeleton
|
|
203
|
+
* that stands for a region and leave the rest silent.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* <View className="flex-row items-center gap-3">
|
|
207
|
+
* <Skeleton shape="circle" />
|
|
208
|
+
* <Skeleton.Lines className="flex-1" lines={2} />
|
|
209
|
+
* </View>
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* <Skeleton isLoading={!user} shape="circle">
|
|
213
|
+
* <Avatar user={user} />
|
|
214
|
+
* </Skeleton>
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* <Skeleton.Group isLoading={isFetching} label="Loading inbox">
|
|
218
|
+
* {rows.map((row) => <Row key={row.id} row={row} />)}
|
|
219
|
+
* </Skeleton.Group>
|
|
220
|
+
*/
|
|
221
|
+
export const Skeleton = Object.assign(SkeletonRoot, {
|
|
222
|
+
/** One clock and one loading flag for every skeleton inside — they shimmer in step and reveal together. */
|
|
223
|
+
Group: SkeletonGroup,
|
|
224
|
+
/** A paragraph of placeholder lines, the last one shortened. */
|
|
225
|
+
Lines: SkeletonLines,
|
|
226
|
+
displayName: "DelacourUI.Skeleton",
|
|
227
|
+
});
|