@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,180 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
import type { SurfacePadding, SurfaceVariant } from "../surface/surface.variants";
|
|
4
|
+
|
|
5
|
+
/** What the alert says about the thing it describes. */
|
|
6
|
+
export const ALERT_STATUSES = ["default", "info", "success", "warning", "destructive"] as const;
|
|
7
|
+
|
|
8
|
+
/** The statuses that carry a colour — every one but `default`. */
|
|
9
|
+
export const ALERT_TINTED_STATUSES = ["info", "success", "warning", "destructive"] as const;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* How the surface is painted. `soft` washes it in the status's soft fill;
|
|
13
|
+
* `surface` keeps the neutral fill the surface ladder gives it and colours only
|
|
14
|
+
* the indicator and the title.
|
|
15
|
+
*/
|
|
16
|
+
export const ALERT_VARIANTS = ["soft", "surface"] as const;
|
|
17
|
+
|
|
18
|
+
export const ALERT_SIZES = ["sm", "md", "lg"] as const;
|
|
19
|
+
|
|
20
|
+
export type AlertStatus = (typeof ALERT_STATUSES)[number];
|
|
21
|
+
export type AlertVariant = (typeof ALERT_VARIANTS)[number];
|
|
22
|
+
export type AlertSize = (typeof ALERT_SIZES)[number];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Theme token for the indicator's glyph and the title's colour.
|
|
26
|
+
*
|
|
27
|
+
* One token per status, not per status and variant: `X-soft-foreground` is
|
|
28
|
+
* tuned to read on `X-soft` and on the neutral fills alike, so the same shade
|
|
29
|
+
* holds on both variants. A test pins each entry to the class the `title` slot
|
|
30
|
+
* emits, so the glyph and the words beside it cannot drift apart.
|
|
31
|
+
*/
|
|
32
|
+
export const ALERT_FOREGROUND_TOKEN = {
|
|
33
|
+
default: "foreground",
|
|
34
|
+
info: "info-soft-foreground",
|
|
35
|
+
success: "success-soft-foreground",
|
|
36
|
+
warning: "warning-soft-foreground",
|
|
37
|
+
destructive: "destructive-soft-foreground",
|
|
38
|
+
} as const satisfies Record<AlertStatus, string>;
|
|
39
|
+
|
|
40
|
+
/** The surface's padding step for each alert size. */
|
|
41
|
+
export const ALERT_SURFACE_PADDING = {
|
|
42
|
+
sm: "sm",
|
|
43
|
+
md: "md",
|
|
44
|
+
lg: "lg",
|
|
45
|
+
} as const satisfies Record<AlertSize, SurfacePadding>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Styling for every part of an alert.
|
|
49
|
+
*
|
|
50
|
+
* The root is laid over a `Surface`, which owns the corner, the padding and —
|
|
51
|
+
* unless the alert is tinted — the fill. This `tv()` adds only what an alert
|
|
52
|
+
* has and a surface does not: the row, the tint and the text treatment.
|
|
53
|
+
*
|
|
54
|
+
* A tinted alert (`soft`, with a status) swaps the card's fill for the status's
|
|
55
|
+
* soft fill and clears the card's hairline. A grey hairline around a red wash
|
|
56
|
+
* reads as two components stacked; the wash is its own edge. Everything else
|
|
57
|
+
* names no background, so the surface ladder picks one and an alert inside a
|
|
58
|
+
* card steps off the card rather than vanishing into it.
|
|
59
|
+
*
|
|
60
|
+
* `items-start`, not `items-center`: a description that wraps to four lines
|
|
61
|
+
* must not drag the glyph down to its middle. The indicator instead carries the
|
|
62
|
+
* title's line height as its own height and centres the glyph in it, which
|
|
63
|
+
* puts the glyph level with the first line at every size.
|
|
64
|
+
*
|
|
65
|
+
* No slot carries a fixed height apart from that indicator — `Text` respects OS
|
|
66
|
+
* font scaling, and a fixed box would clip a wrapped title.
|
|
67
|
+
*
|
|
68
|
+
* The root holds no `text-*` (rule 1). The title takes its status's colour,
|
|
69
|
+
* the description is always muted, so a long explanation never shouts.
|
|
70
|
+
*
|
|
71
|
+
* Free of React Native imports so it stays unit-testable — `bun test` cannot
|
|
72
|
+
* parse React Native's Flow-typed source. See AGENTS.md.
|
|
73
|
+
*/
|
|
74
|
+
export const alertVariants = tv({
|
|
75
|
+
slots: {
|
|
76
|
+
root: "flex-row items-start",
|
|
77
|
+
indicator: "items-center justify-center",
|
|
78
|
+
content: "min-w-0 flex-1 gap-1",
|
|
79
|
+
title: "font-semibold",
|
|
80
|
+
description: "text-muted-foreground",
|
|
81
|
+
action: "mt-2 flex-row flex-wrap items-center gap-2",
|
|
82
|
+
closeButton: "items-center justify-center rounded-full",
|
|
83
|
+
/** Edge length an `Icon` composed into the alert inherits. */
|
|
84
|
+
icon: "",
|
|
85
|
+
},
|
|
86
|
+
variants: {
|
|
87
|
+
variant: {
|
|
88
|
+
soft: {},
|
|
89
|
+
surface: {},
|
|
90
|
+
},
|
|
91
|
+
// Written out rather than built from `ALERT_FOREGROUND_TOKEN`: Tailwind's
|
|
92
|
+
// scanner is static, and a class assembled at runtime is never compiled.
|
|
93
|
+
status: {
|
|
94
|
+
default: { title: "text-foreground" },
|
|
95
|
+
info: { title: "text-info-soft-foreground" },
|
|
96
|
+
success: { title: "text-success-soft-foreground" },
|
|
97
|
+
warning: { title: "text-warning-soft-foreground" },
|
|
98
|
+
destructive: { title: "text-destructive-soft-foreground" },
|
|
99
|
+
},
|
|
100
|
+
size: {
|
|
101
|
+
sm: {
|
|
102
|
+
root: "gap-2",
|
|
103
|
+
indicator: "h-5",
|
|
104
|
+
title: "text-sm",
|
|
105
|
+
description: "text-xs",
|
|
106
|
+
closeButton: "size-5",
|
|
107
|
+
icon: "size-icon-sm",
|
|
108
|
+
},
|
|
109
|
+
md: {
|
|
110
|
+
root: "gap-3",
|
|
111
|
+
indicator: "h-6",
|
|
112
|
+
title: "text-base",
|
|
113
|
+
description: "text-sm",
|
|
114
|
+
closeButton: "size-6",
|
|
115
|
+
icon: "size-icon-md",
|
|
116
|
+
},
|
|
117
|
+
lg: {
|
|
118
|
+
root: "gap-3.5",
|
|
119
|
+
indicator: "h-7",
|
|
120
|
+
title: "text-lg",
|
|
121
|
+
description: "text-base",
|
|
122
|
+
closeButton: "size-7",
|
|
123
|
+
icon: "size-icon-lg",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
compoundVariants: [
|
|
128
|
+
{ variant: "soft", status: "info", class: { root: "border-transparent bg-info-soft" } },
|
|
129
|
+
{ variant: "soft", status: "success", class: { root: "border-transparent bg-success-soft" } },
|
|
130
|
+
{ variant: "soft", status: "warning", class: { root: "border-transparent bg-warning-soft" } },
|
|
131
|
+
{ variant: "soft", status: "destructive", class: { root: "border-transparent bg-destructive-soft" } },
|
|
132
|
+
],
|
|
133
|
+
defaultVariants: {
|
|
134
|
+
variant: "soft",
|
|
135
|
+
status: "default",
|
|
136
|
+
size: "md",
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export type AlertVariantProps = VariantProps<typeof alertVariants>;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Whether the alert paints its status's soft fill over the surface.
|
|
144
|
+
*
|
|
145
|
+
* Only a `soft` alert with a status is: a neutral alert has no tint to paint,
|
|
146
|
+
* and a `surface` alert keeps the ladder's fill on purpose.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveAlertTinted({ status, variant }: { status: AlertStatus; variant: AlertVariant }): boolean {
|
|
149
|
+
return variant === "soft" && status !== "default";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The `variant` the alert hands its `Surface`.
|
|
154
|
+
*
|
|
155
|
+
* A tinted alert pins it to `default`: the tint replaces the fill, and what
|
|
156
|
+
* nests inside then steps from the card like anything else on one. Every other
|
|
157
|
+
* alert leaves it `undefined`, so the surface reads the plane it sits on and
|
|
158
|
+
* steps to the next fill — an alert inside a card is told apart from the card
|
|
159
|
+
* with nothing said at the call site.
|
|
160
|
+
*/
|
|
161
|
+
export function resolveAlertSurfaceVariant({
|
|
162
|
+
status,
|
|
163
|
+
variant,
|
|
164
|
+
}: {
|
|
165
|
+
status: AlertStatus;
|
|
166
|
+
variant: AlertVariant;
|
|
167
|
+
}): SurfaceVariant | undefined {
|
|
168
|
+
return resolveAlertTinted({ status, variant }) ? "default" : undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* How urgently Android's TalkBack announces a change inside the alert.
|
|
173
|
+
*
|
|
174
|
+
* A warning or a failure interrupts whatever is being read; anything else waits
|
|
175
|
+
* its turn. iOS has no live regions — the root's `alert` role is what VoiceOver
|
|
176
|
+
* reads there.
|
|
177
|
+
*/
|
|
178
|
+
export function resolveAlertLiveRegion(status: AlertStatus): "polite" | "assertive" {
|
|
179
|
+
return status === "destructive" || status === "warning" ? "assertive" : "polite";
|
|
180
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { Alert, type AlertProps } from "./alert";
|
|
2
|
+
export { type AlertContextValue, AlertProvider, useAlert, useAlertContext } from "./alert.context";
|
|
3
|
+
export type { AlertSlotProps } from "./alert.types";
|
|
4
|
+
export {
|
|
5
|
+
ALERT_FOREGROUND_TOKEN,
|
|
6
|
+
ALERT_SIZES,
|
|
7
|
+
ALERT_STATUSES,
|
|
8
|
+
ALERT_SURFACE_PADDING,
|
|
9
|
+
ALERT_TINTED_STATUSES,
|
|
10
|
+
ALERT_VARIANTS,
|
|
11
|
+
type AlertSize,
|
|
12
|
+
type AlertStatus,
|
|
13
|
+
type AlertVariant,
|
|
14
|
+
type AlertVariantProps,
|
|
15
|
+
alertVariants,
|
|
16
|
+
resolveAlertLiveRegion,
|
|
17
|
+
resolveAlertSurfaceVariant,
|
|
18
|
+
resolveAlertTinted,
|
|
19
|
+
} from "./alert.variants";
|
|
20
|
+
export type { AlertCloseButtonProps } from "./alert-close-button";
|
|
21
|
+
export type { AlertDescriptionProps } from "./alert-description";
|
|
22
|
+
export type { AlertIndicatorProps } from "./alert-indicator";
|
|
23
|
+
export type { AlertTitleProps } from "./alert-title";
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Avatar
|
|
2
|
+
|
|
3
|
+
A person, as a picture — with an initials fallback, an overlay slot for a count
|
|
4
|
+
or a presence dot, and a group that stacks several faces and counts the rest.
|
|
5
|
+
Compound root plus `Avatar.Badge` and `Avatar.Group`.
|
|
6
|
+
|
|
7
|
+
`import { Avatar } from "@delacour/react-native-ui/avatar";`
|
|
8
|
+
|
|
9
|
+
## Files
|
|
10
|
+
|
|
11
|
+
| File | What it holds |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `index.ts` | → `@delacour/react-native-ui/avatar` |
|
|
14
|
+
| `avatar.tsx` | Root + the `Object.assign` compound surface |
|
|
15
|
+
| `avatar-badge.tsx` | `Avatar.Badge` — the corner overlay, a presence dot when empty |
|
|
16
|
+
| `avatar-group.tsx` | `Avatar.Group`, plus its internal `Item` and `Overflow` leaves |
|
|
17
|
+
| `avatar.context.tsx` | `AvatarProvider`, `AvatarGroupProvider`, `useAvatar()`, `useAvatarContext()`, `useAvatarGroupContext()`, `useAvatarPart()` |
|
|
18
|
+
| `avatar.variants.ts` | Pure `tv()` slots, `AVATAR_SIZE_POINTS`, `resolveAvatarSize`, `resolveAvatarOverlap`, `resolveAvatarInteractive` |
|
|
19
|
+
| `avatar.variants.test.ts` | |
|
|
20
|
+
| `avatar.utils.ts` | Pure non-styling logic — initials, the image-retry key, group overflow maths, the spoken label |
|
|
21
|
+
| `avatar.utils.test.ts` | |
|
|
22
|
+
|
|
23
|
+
## Design
|
|
24
|
+
|
|
25
|
+
- **The fallback is always painted; the image goes on top.** The face renders
|
|
26
|
+
its initials (or a person glyph) first and mounts the `Image` absolutely over
|
|
27
|
+
them. A picture that is still loading, or that never loads, leaves the fallback
|
|
28
|
+
showing rather than an empty circle — there is no loading state to manage
|
|
29
|
+
because there is nothing to swap.
|
|
30
|
+
- **A failure is remembered by key, not by object.** `resolveAvatarSourceKey`
|
|
31
|
+
turns the source into a string — URI plus sorted headers, or the asset's module
|
|
32
|
+
id — and `onError` stores the key that failed. Keyed by object identity, an
|
|
33
|
+
inline `source={{ uri }}` is new every render and a dead URL would retry
|
|
34
|
+
forever; keyed by URI alone, a token refresh in the headers would never retry.
|
|
35
|
+
A consumer's `imageProps.onError` fires after the fallback has taken over.
|
|
36
|
+
- **The root carries the edge; there is no `self-start`.** A fixed width and
|
|
37
|
+
height is something a column's `stretch` never overrides, so the avatar keeps
|
|
38
|
+
its size without the `self-start` a [`Badge`](../badge/AGENTS.md) needs — and
|
|
39
|
+
it leaves the parent's alignment alone. With `self-start`, every avatar in an
|
|
40
|
+
`items-end` or `items-center` row was pinned to the row's top: mixed sizes lost
|
|
41
|
+
their baseline and a list row's avatar sat above its title. Found on the
|
|
42
|
+
simulator; a test now forbids any `self-*` on the root or the group.
|
|
43
|
+
- **Two boxes, not one.** `root` is unclipped and holds the overlays; `face` is
|
|
44
|
+
the clipped circle. An `Avatar.Badge` hangs over the circle's edge, and a
|
|
45
|
+
single clipped box would cut it in half. A test asserts `overflow-hidden` is on
|
|
46
|
+
`face` and never on `root`.
|
|
47
|
+
- **Initials are the first letter of the first and last word.** `Anahera Rose
|
|
48
|
+
Tui` is `AT`, the way a person signs, and `Mihi` is `M`.
|
|
49
|
+
`resolveAvatarInitials` walks code points rather than UTF-16 units, so a letter
|
|
50
|
+
outside the basic plane is never split into a replacement box, and skips
|
|
51
|
+
leading punctuation so `"(Aria)"` still gives `A`. `fallback` overrides the
|
|
52
|
+
drawn text only; `name` is still what a screen reader reads.
|
|
53
|
+
- **Two axes paint the fallback, on `Badge`'s six colours.** `variant` is `soft`
|
|
54
|
+
or `solid`, `color` is `default` … `info`, and the twelve cells live in
|
|
55
|
+
`compoundVariants` with the same fills `Badge` uses, so an avatar and a badge
|
|
56
|
+
of one colour read as one family. `AVATAR_FOREGROUND_TOKEN` gives the person
|
|
57
|
+
glyph the same shade as the initials; a test pins each entry to the token the
|
|
58
|
+
`fallbackLabel` slot resolves to, and checks every token exists in both themes.
|
|
59
|
+
- **A neutral face has a hairline edge; a photo does not.** `muted` and
|
|
60
|
+
`secondary` sit a percent or two from the page in light — on the simulator a
|
|
61
|
+
`default` fallback and the `+N` tile all but vanished. They draw
|
|
62
|
+
`border-border` inside the box, so nothing moves, and the edge goes while a
|
|
63
|
+
photo is mounted (`hasImage`), since a grey line around a face reads as a
|
|
64
|
+
frame. The coloured fallbacks have fill enough without one.
|
|
65
|
+
- **A size is a fixed edge, unlike a badge.** `sm`/`md`/`lg`/`xl` are
|
|
66
|
+
32/40/48/64 points. Faces line up against each other and against list rows, and
|
|
67
|
+
a circle that grew with OS font scaling would break the stack; the initials
|
|
68
|
+
therefore set `allowFontScaling={false}` and are sized to fit the smallest
|
|
69
|
+
step. `AVATAR_SIZE_POINTS` restates the scale as numbers because the group's
|
|
70
|
+
overlap is a runtime margin and cannot be a class; a test pins the two
|
|
71
|
+
together.
|
|
72
|
+
- **An avatar is content until it is given something to do** — the rule
|
|
73
|
+
[`Badge`](../badge/AGENTS.md) follows. With no `onPress` or `onLongPress` it
|
|
74
|
+
is a plain `View` announced as an image (or not announced at all when it has
|
|
75
|
+
no name); supply either and it becomes a [`Pressable`](../pressable/AGENTS.md)
|
|
76
|
+
announced as a button. `resolveAvatarInteractive` is that decision.
|
|
77
|
+
- **The avatar is one element to assistive technology.** The root is
|
|
78
|
+
`accessible`, so an `Avatar.Badge` inside it is never focused on its own. A
|
|
79
|
+
presence dot has no words; put the status into the avatar's
|
|
80
|
+
`accessibilityLabel` (`"Aria Whitlock, online"`).
|
|
81
|
+
- **`Avatar.Badge` is a pin, and a dot only when empty.** Given children — a
|
|
82
|
+
`Badge` with a count, an `Icon` — it positions them and adds nothing else, so a
|
|
83
|
+
count keeps the look it has everywhere else. Empty, it draws a dot sized to
|
|
84
|
+
the face and ringed in `border-background`, which separates it from the photo
|
|
85
|
+
underneath. `placement` is `top-right` or `bottom-right`.
|
|
86
|
+
- **The group's ring belongs to the slot, not the face.** Each child is wrapped
|
|
87
|
+
in an item with `border-2 border-background`, so a face in a group keeps the
|
|
88
|
+
same edge it has alone, and the stack reads as separate people on any surface.
|
|
89
|
+
The `+N` tile sits in the same wrapper, so it lines up with the faces.
|
|
90
|
+
- **The first face is on top, and the list still reads first to last.**
|
|
91
|
+
`zIndex` counts down across the items instead of the children being reversed,
|
|
92
|
+
and the overlap is a negative `marginStart`, so the order in the tree — and
|
|
93
|
+
therefore the order a screen reader walks — is the written order in both
|
|
94
|
+
writing directions. The group is `role="list"` and each item `listitem`.
|
|
95
|
+
- **`max` caps the faces, not the row; `total` counts people never passed.**
|
|
96
|
+
Five children at `max={3}` is three faces and `+2`; three children with
|
|
97
|
+
`total={40}` is `+37`, without forty elements in the tree.
|
|
98
|
+
`resolveAvatarGroup` clamps a negative or fractional `max` and ignores a
|
|
99
|
+
`total` below the child count rather than drawing `+-2`.
|
|
100
|
+
- **The overflow tile draws at most `99+` and reads the real number.** Three
|
|
101
|
+
digits do not fit a small circle; a screen reader has room for `1234 more`.
|
|
102
|
+
`onOverflowPress` turns the tile into a button — to open the full list.
|
|
103
|
+
- **The overlap defaults to a third of the edge and is clamped.**
|
|
104
|
+
`resolveAvatarOverlap` never slides a face further than a whole face nor
|
|
105
|
+
backwards, and `overlap={0}` closes the stack into a plain row. A child's own
|
|
106
|
+
`size` still beats the group's, but the overlap is computed from the group's.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { useAvatarPart } from "./avatar.context";
|
|
5
|
+
import { type AvatarBadgePlacement, type AvatarColor, avatarVariants } from "./avatar.variants";
|
|
6
|
+
|
|
7
|
+
export type AvatarBadgeProps = ViewProps & {
|
|
8
|
+
/** Which corner the overlay is pinned to. Default `top-right`. */
|
|
9
|
+
placement?: AvatarBadgePlacement;
|
|
10
|
+
/** The presence dot's colour, when there are no children. Default `success`. */
|
|
11
|
+
color?: AvatarColor;
|
|
12
|
+
className?: string;
|
|
13
|
+
/** What to pin — a `Badge` with a count, an `Icon`. Leave empty for a presence dot. */
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* An overlay pinned to a corner of the avatar — an unread count, a presence dot.
|
|
19
|
+
*
|
|
20
|
+
* **Empty, it is a presence dot** sized to the face it sits on and ringed in the
|
|
21
|
+
* page background, so it reads as separate from the photo underneath. Given
|
|
22
|
+
* children, it is only the pin: a `Badge` or an `Icon` dropped in keeps its own
|
|
23
|
+
* look, which is what lets a count and a dot share one part.
|
|
24
|
+
*
|
|
25
|
+
* It sits in the avatar's unclipped root, never inside the circle, so it can
|
|
26
|
+
* hang over the edge without being cut in half.
|
|
27
|
+
*
|
|
28
|
+
* A dot says nothing to a screen reader on its own: the avatar is one element to
|
|
29
|
+
* assistive technology, so put the status into its `accessibilityLabel`.
|
|
30
|
+
*/
|
|
31
|
+
export function AvatarBadge({
|
|
32
|
+
placement = "top-right",
|
|
33
|
+
color = "success",
|
|
34
|
+
className,
|
|
35
|
+
children,
|
|
36
|
+
...props
|
|
37
|
+
}: AvatarBadgeProps): ReactElement {
|
|
38
|
+
const { size } = useAvatarPart("Avatar.Badge");
|
|
39
|
+
const slots = avatarVariants({ size, placement, color });
|
|
40
|
+
|
|
41
|
+
if (children === undefined || children === null || children === false) {
|
|
42
|
+
return <View className={cn(slots.badge(), slots.dot(), className)} pointerEvents="none" {...props} />;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<View className={slots.badge({ className })} pointerEvents="box-none" {...props}>
|
|
47
|
+
{children}
|
|
48
|
+
</View>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
AvatarBadge.displayName = "DelacourUI.Avatar.Badge";
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Children, isValidElement, type ReactElement, type ReactNode, useMemo } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { Pressable } from "../pressable";
|
|
4
|
+
import { Text } from "../text";
|
|
5
|
+
import { type AvatarGroupContextValue, AvatarGroupProvider } from "./avatar.context";
|
|
6
|
+
import { resolveAvatarGroup, resolveAvatarOverflowLabel } from "./avatar.utils";
|
|
7
|
+
import { type AvatarSize, avatarVariants, resolveAvatarOverlap } from "./avatar.variants";
|
|
8
|
+
|
|
9
|
+
export type AvatarGroupProps = ViewProps & {
|
|
10
|
+
/** Size for every avatar in the stack. A child's own `size` still wins. Default `md`. */
|
|
11
|
+
size?: AvatarSize;
|
|
12
|
+
/** How many faces to show; the rest are counted into a trailing `+N`. Caps the faces, not the row. */
|
|
13
|
+
max?: number;
|
|
14
|
+
/** How many people there are, when the children are only the first few of them. */
|
|
15
|
+
total?: number;
|
|
16
|
+
/** Points each face slides under the one before it. Default a third of the size; `0` makes a plain row. */
|
|
17
|
+
overlap?: number;
|
|
18
|
+
/** Makes the `+N` tile a button — to open the full list, say. */
|
|
19
|
+
onOverflowPress?: () => void;
|
|
20
|
+
className?: string;
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type AvatarGroupItemProps = {
|
|
25
|
+
/** Position in the stack, first face first. */
|
|
26
|
+
index: number;
|
|
27
|
+
/** How many items the stack holds, overflow tile included. */
|
|
28
|
+
count: number;
|
|
29
|
+
/** The overlap, in points. */
|
|
30
|
+
shift: number;
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One slot in the stack: the ring, the overlap and the stacking order.
|
|
36
|
+
*
|
|
37
|
+
* The ring is here rather than on the face so a face in a group is the same edge
|
|
38
|
+
* as one outside it. `zIndex` counts down so the first face sits on top without
|
|
39
|
+
* reversing the order a screen reader walks — the list is still read first to
|
|
40
|
+
* last, in both writing directions, because the overlap is a `marginStart`.
|
|
41
|
+
*/
|
|
42
|
+
function AvatarGroupItem({ index, count, shift, children }: AvatarGroupItemProps): ReactElement {
|
|
43
|
+
return (
|
|
44
|
+
<View
|
|
45
|
+
className={avatarVariants().groupItem()}
|
|
46
|
+
role="listitem"
|
|
47
|
+
style={{ marginStart: index === 0 ? 0 : -shift, zIndex: count - index }}
|
|
48
|
+
>
|
|
49
|
+
{children}
|
|
50
|
+
</View>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
AvatarGroupItem.displayName = "DelacourUI.Avatar.Group.Item";
|
|
54
|
+
|
|
55
|
+
type AvatarGroupOverflowProps = {
|
|
56
|
+
size: AvatarSize;
|
|
57
|
+
overflow: number;
|
|
58
|
+
onPress?: () => void;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The trailing `+N` tile, the same edge as a face.
|
|
63
|
+
*
|
|
64
|
+
* It draws at most `99+` and reads the real number, because three digits do not
|
|
65
|
+
* fit a small circle and a screen reader has room for all of them.
|
|
66
|
+
*/
|
|
67
|
+
function AvatarGroupOverflow({ size, overflow, onPress }: AvatarGroupOverflowProps): ReactElement {
|
|
68
|
+
const slots = avatarVariants({ size });
|
|
69
|
+
const { text, accessibilityLabel } = resolveAvatarOverflowLabel(overflow);
|
|
70
|
+
const label = (
|
|
71
|
+
<Text allowFontScaling={false} className={slots.overflowLabel()} numberOfLines={1}>
|
|
72
|
+
{text}
|
|
73
|
+
</Text>
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (onPress) {
|
|
77
|
+
return (
|
|
78
|
+
<Pressable
|
|
79
|
+
accessibilityLabel={accessibilityLabel}
|
|
80
|
+
accessibilityRole="button"
|
|
81
|
+
className={slots.overflow()}
|
|
82
|
+
feedback="scale"
|
|
83
|
+
onPress={onPress}
|
|
84
|
+
>
|
|
85
|
+
{label}
|
|
86
|
+
</Pressable>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<View accessibilityLabel={accessibilityLabel} accessible className={slots.overflow()}>
|
|
92
|
+
{label}
|
|
93
|
+
</View>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
AvatarGroupOverflow.displayName = "DelacourUI.Avatar.Group.Overflow";
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A row of avatars, each overlapping the one after it, with the people who did
|
|
100
|
+
* not fit counted at the end.
|
|
101
|
+
*
|
|
102
|
+
* Children are kept in the order they were written. `max` caps the faces and
|
|
103
|
+
* `total` says how many people there really are, so three children out of forty
|
|
104
|
+
* reads `+37` without forty elements in the tree.
|
|
105
|
+
*/
|
|
106
|
+
export function AvatarGroup({
|
|
107
|
+
size,
|
|
108
|
+
max,
|
|
109
|
+
total,
|
|
110
|
+
overlap,
|
|
111
|
+
onOverflowPress,
|
|
112
|
+
className,
|
|
113
|
+
children,
|
|
114
|
+
...props
|
|
115
|
+
}: AvatarGroupProps): ReactElement {
|
|
116
|
+
const items = Children.toArray(children).filter(isValidElement);
|
|
117
|
+
const { visible, overflow } = resolveAvatarGroup({ count: items.length, max, total });
|
|
118
|
+
const shift = resolveAvatarOverlap({ size: size ?? "md", overlap });
|
|
119
|
+
const count = visible + (overflow > 0 ? 1 : 0);
|
|
120
|
+
const context = useMemo<AvatarGroupContextValue>(() => ({ size }), [size]);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<AvatarGroupProvider value={context}>
|
|
124
|
+
<View className={avatarVariants().group({ className })} role="list" {...props}>
|
|
125
|
+
{items.slice(0, visible).map((child, index) => (
|
|
126
|
+
<AvatarGroupItem count={count} index={index} key={child.key ?? index} shift={shift}>
|
|
127
|
+
{child}
|
|
128
|
+
</AvatarGroupItem>
|
|
129
|
+
))}
|
|
130
|
+
{overflow > 0 ? (
|
|
131
|
+
<AvatarGroupItem count={count} index={visible} shift={shift}>
|
|
132
|
+
<AvatarGroupOverflow onPress={onOverflowPress} overflow={overflow} size={size ?? "md"} />
|
|
133
|
+
</AvatarGroupItem>
|
|
134
|
+
) : null}
|
|
135
|
+
</View>
|
|
136
|
+
</AvatarGroupProvider>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
AvatarGroup.displayName = "DelacourUI.Avatar.Group";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createContext, type ReactElement, type ReactNode, use } from "react";
|
|
2
|
+
import type { AvatarColor, AvatarSize, AvatarVariant } from "./avatar.variants";
|
|
3
|
+
|
|
4
|
+
export type AvatarContextValue = {
|
|
5
|
+
/** The avatar's resolved size — its own, else its group's, else `md`. */
|
|
6
|
+
size: AvatarSize;
|
|
7
|
+
/** How the fallback surface is painted. */
|
|
8
|
+
variant: AvatarVariant;
|
|
9
|
+
/** What the fallback surface means. */
|
|
10
|
+
color: AvatarColor;
|
|
11
|
+
/** Whether the avatar is disabled. */
|
|
12
|
+
isDisabled: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type AvatarGroupContextValue = {
|
|
16
|
+
/** The size every avatar in the group takes unless it states its own. */
|
|
17
|
+
size?: AvatarSize;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const AvatarContext = createContext<AvatarContextValue | null>(null);
|
|
21
|
+
const AvatarGroupContext = createContext<AvatarGroupContextValue | null>(null);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Supplies the enclosing avatar's size, paint and state to its subtree.
|
|
25
|
+
*
|
|
26
|
+
* Lives in its own module, importing nothing but `avatar.variants`, so a part
|
|
27
|
+
* can read it without importing `./avatar`. That import would close a cycle, and
|
|
28
|
+
* Metro serves a partially initialised module for a cycle — leaving the context
|
|
29
|
+
* `undefined` at import time and red-boxing the app on a cold start.
|
|
30
|
+
*/
|
|
31
|
+
export function AvatarProvider({ value, children }: { value: AvatarContextValue; children: ReactNode }): ReactElement {
|
|
32
|
+
return <AvatarContext value={value}>{children}</AvatarContext>;
|
|
33
|
+
}
|
|
34
|
+
AvatarProvider.displayName = "DelacourUI.Avatar.Provider";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Supplies a group's size to every avatar inside it.
|
|
38
|
+
*
|
|
39
|
+
* Separate from {@link AvatarProvider} because the two nest the other way round:
|
|
40
|
+
* a group holds avatars, and an avatar must not read a size its group never set
|
|
41
|
+
* as though it had been told one.
|
|
42
|
+
*/
|
|
43
|
+
export function AvatarGroupProvider({
|
|
44
|
+
value,
|
|
45
|
+
children,
|
|
46
|
+
}: {
|
|
47
|
+
value: AvatarGroupContextValue;
|
|
48
|
+
children: ReactNode;
|
|
49
|
+
}): ReactElement {
|
|
50
|
+
return <AvatarGroupContext value={value}>{children}</AvatarGroupContext>;
|
|
51
|
+
}
|
|
52
|
+
AvatarGroupProvider.displayName = "DelacourUI.Avatar.Group.Provider";
|
|
53
|
+
|
|
54
|
+
/** The enclosing avatar's context, or null outside an `<Avatar>`. */
|
|
55
|
+
export function useAvatarContext(): AvatarContextValue | null {
|
|
56
|
+
return use(AvatarContext);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The enclosing group's context, or null outside an `<Avatar.Group>`. */
|
|
60
|
+
export function useAvatarGroupContext(): AvatarGroupContextValue | null {
|
|
61
|
+
return use(AvatarGroupContext);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reads the enclosing avatar's size, paint and state.
|
|
66
|
+
*
|
|
67
|
+
* Lets a custom overlay size itself to the face it sits on without the avatar
|
|
68
|
+
* passing props down. Throws outside an `<Avatar>` — use
|
|
69
|
+
* {@link useAvatarContext} where the enclosing avatar is optional.
|
|
70
|
+
*/
|
|
71
|
+
export function useAvatar(): AvatarContextValue {
|
|
72
|
+
const context = useAvatarContext();
|
|
73
|
+
if (!context) {
|
|
74
|
+
throw new Error("useAvatar must be called inside an <Avatar>.");
|
|
75
|
+
}
|
|
76
|
+
return context;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The enclosing avatar's context, for a compound part that cannot work without
|
|
81
|
+
* one.
|
|
82
|
+
*
|
|
83
|
+
* Internal: deliberately not re-exported from `index.ts`. A caller outside the
|
|
84
|
+
* library wants {@link useAvatar}, whose error message names the hook rather
|
|
85
|
+
* than a part.
|
|
86
|
+
*/
|
|
87
|
+
export function useAvatarPart(component: string): AvatarContextValue {
|
|
88
|
+
const context = useAvatarContext();
|
|
89
|
+
if (!context) {
|
|
90
|
+
throw new Error(`${component} must be rendered inside an <Avatar>.`);
|
|
91
|
+
}
|
|
92
|
+
return context;
|
|
93
|
+
}
|