@delacour/react-native-ui 0.1.0-alpha.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -6
- package/package.json +48 -4
- package/src/components/accordion/AGENTS.md +21 -9
- package/src/components/accordion/accordion-content.tsx +7 -4
- package/src/components/accordion/accordion-item.tsx +15 -13
- package/src/components/accordion/accordion.context.tsx +11 -5
- package/src/components/accordion/accordion.variants.test.ts +64 -3
- package/src/components/accordion/accordion.variants.ts +33 -9
- package/src/components/alert/AGENTS.md +102 -0
- package/src/components/alert/alert-action.tsx +17 -0
- package/src/components/alert/alert-close-button.tsx +50 -0
- package/src/components/alert/alert-content.tsx +15 -0
- package/src/components/alert/alert-description.tsx +16 -0
- package/src/components/alert/alert-glyphs.ts +25 -0
- package/src/components/alert/alert-indicator.tsx +40 -0
- package/src/components/alert/alert-title.tsx +18 -0
- package/src/components/alert/alert.context.tsx +69 -0
- package/src/components/alert/alert.tsx +160 -0
- package/src/components/alert/alert.types.ts +10 -0
- package/src/components/alert/alert.variants.test.ts +218 -0
- package/src/components/alert/alert.variants.ts +180 -0
- package/src/components/alert/index.ts +23 -0
- package/src/components/avatar/AGENTS.md +106 -0
- package/src/components/avatar/avatar-badge.tsx +51 -0
- package/src/components/avatar/avatar-group.tsx +139 -0
- package/src/components/avatar/avatar.context.tsx +93 -0
- package/src/components/avatar/avatar.tsx +201 -0
- package/src/components/avatar/avatar.utils.test.ts +174 -0
- package/src/components/avatar/avatar.utils.ts +141 -0
- package/src/components/avatar/avatar.variants.test.ts +251 -0
- package/src/components/avatar/avatar.variants.ts +270 -0
- package/src/components/avatar/index.ts +39 -0
- package/src/components/badge/AGENTS.md +5 -0
- package/src/components/badge/badge.tsx +6 -1
- package/src/components/bottom-sheet/AGENTS.md +186 -231
- package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
- package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
- package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -208
- package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
- package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
- package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
- package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -122
- package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
- package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
- package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
- package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
- package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -62
- package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
- package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
- package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
- package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
- package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
- package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
- package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -87
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -126
- package/src/components/bottom-sheet/index.ts +39 -17
- package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
- package/src/components/button/AGENTS.md +45 -11
- package/src/components/button/button-group.tsx +44 -8
- package/src/components/button/button.context.tsx +10 -0
- package/src/components/button/button.tsx +2 -1
- package/src/components/button/button.variants.test.ts +154 -0
- package/src/components/button/button.variants.ts +71 -6
- package/src/components/button/index.ts +2 -0
- package/src/components/calendar/AGENTS.md +142 -0
- package/src/components/calendar/calendar-caption.tsx +116 -0
- package/src/components/calendar/calendar-day.tsx +103 -0
- package/src/components/calendar/calendar-grid.tsx +183 -0
- package/src/components/calendar/calendar-header.tsx +34 -0
- package/src/components/calendar/calendar-nav.tsx +51 -0
- package/src/components/calendar/calendar-picker.tsx +128 -0
- package/src/components/calendar/calendar-weekdays.tsx +48 -0
- package/src/components/calendar/calendar.context.tsx +108 -0
- package/src/components/calendar/calendar.date.test.ts +456 -0
- package/src/components/calendar/calendar.date.ts +584 -0
- package/src/components/calendar/calendar.tsx +389 -0
- package/src/components/calendar/calendar.types.ts +87 -0
- package/src/components/calendar/calendar.variants.test.ts +363 -0
- package/src/components/calendar/calendar.variants.ts +426 -0
- package/src/components/calendar/index.ts +96 -0
- package/src/components/card/AGENTS.md +88 -0
- package/src/components/card/card-action.tsx +16 -0
- package/src/components/card/card-content.tsx +12 -0
- package/src/components/card/card-description.tsx +12 -0
- package/src/components/card/card-footer.tsx +30 -0
- package/src/components/card/card-header.tsx +45 -0
- package/src/components/card/card-title.tsx +25 -0
- package/src/components/card/card.context.tsx +64 -0
- package/src/components/card/card.tsx +94 -0
- package/src/components/card/card.types.ts +13 -0
- package/src/components/card/card.variants.test.ts +240 -0
- package/src/components/card/card.variants.ts +177 -0
- package/src/components/card/index.ts +15 -0
- package/src/components/chart/AGENTS.md +8 -0
- package/src/components/chart/chart.tsx +8 -1
- package/src/components/chip/AGENTS.md +113 -0
- package/src/components/chip/chip-close-button.tsx +48 -0
- package/src/components/chip/chip-end-content.tsx +15 -0
- package/src/components/chip/chip-label.tsx +21 -0
- package/src/components/chip/chip-start-content.tsx +16 -0
- package/src/components/chip/chip.context.tsx +66 -0
- package/src/components/chip/chip.tsx +268 -0
- package/src/components/chip/chip.types.ts +10 -0
- package/src/components/chip/chip.variants.test.ts +335 -0
- package/src/components/chip/chip.variants.ts +321 -0
- package/src/components/chip/index.ts +24 -0
- package/src/components/collapsible/AGENTS.md +97 -0
- package/src/components/collapsible/collapsible-content.tsx +97 -0
- package/src/components/collapsible/collapsible-description.tsx +17 -0
- package/src/components/collapsible/collapsible-indicator.tsx +92 -0
- package/src/components/collapsible/collapsible-title.tsx +17 -0
- package/src/components/collapsible/collapsible-trigger.tsx +133 -0
- package/src/components/collapsible/collapsible.context.tsx +91 -0
- package/src/components/collapsible/collapsible.tsx +167 -0
- package/src/components/collapsible/collapsible.types.ts +9 -0
- package/src/components/collapsible/collapsible.variants.test.ts +338 -0
- package/src/components/collapsible/collapsible.variants.ts +228 -0
- package/src/components/collapsible/index.ts +32 -0
- package/src/components/context-menu/AGENTS.md +103 -0
- package/src/components/context-menu/context-menu-content.tsx +64 -0
- package/src/components/context-menu/context-menu-item.tsx +22 -0
- package/src/components/context-menu/context-menu-preview.tsx +60 -0
- package/src/components/context-menu/context-menu-trigger.tsx +160 -0
- package/src/components/context-menu/context-menu.context.tsx +91 -0
- package/src/components/context-menu/context-menu.tsx +90 -0
- package/src/components/context-menu/context-menu.variants.test.ts +266 -0
- package/src/components/context-menu/context-menu.variants.ts +147 -0
- package/src/components/context-menu/index.ts +18 -0
- package/src/components/dialog/AGENTS.md +97 -0
- package/src/components/dialog/dialog-body.tsx +21 -0
- package/src/components/dialog/dialog-close.tsx +82 -0
- package/src/components/dialog/dialog-content.tsx +153 -0
- package/src/components/dialog/dialog-description.tsx +23 -0
- package/src/components/dialog/dialog-footer.tsx +32 -0
- package/src/components/dialog/dialog-header.tsx +20 -0
- package/src/components/dialog/dialog-title.tsx +42 -0
- package/src/components/dialog/dialog-trigger.tsx +54 -0
- package/src/components/dialog/dialog.context.tsx +90 -0
- package/src/components/dialog/dialog.tsx +119 -0
- package/src/components/dialog/dialog.variants.test.ts +224 -0
- package/src/components/dialog/dialog.variants.ts +173 -0
- package/src/components/dialog/index.ts +30 -0
- package/src/components/drawer/AGENTS.md +123 -0
- package/src/components/drawer/drawer-body.tsx +46 -0
- package/src/components/drawer/drawer-close.tsx +76 -0
- package/src/components/drawer/drawer-content.tsx +186 -0
- package/src/components/drawer/drawer-description.tsx +23 -0
- package/src/components/drawer/drawer-footer.tsx +21 -0
- package/src/components/drawer/drawer-header.tsx +42 -0
- package/src/components/drawer/drawer-title.tsx +35 -0
- package/src/components/drawer/drawer-trigger.tsx +54 -0
- package/src/components/drawer/drawer.context.tsx +114 -0
- package/src/components/drawer/drawer.tsx +119 -0
- package/src/components/drawer/drawer.variants.test.ts +392 -0
- package/src/components/drawer/drawer.variants.ts +244 -0
- package/src/components/drawer/index.ts +41 -0
- package/src/components/drawer/use-drawer-pan.ts +108 -0
- package/src/components/empty-state/AGENTS.md +90 -0
- package/src/components/empty-state/empty-state-content.tsx +17 -0
- package/src/components/empty-state/empty-state-description.tsx +12 -0
- package/src/components/empty-state/empty-state-header.tsx +17 -0
- package/src/components/empty-state/empty-state-media.tsx +56 -0
- package/src/components/empty-state/empty-state-title.tsx +18 -0
- package/src/components/empty-state/empty-state.context.tsx +65 -0
- package/src/components/empty-state/empty-state.tsx +84 -0
- package/src/components/empty-state/empty-state.types.ts +14 -0
- package/src/components/empty-state/empty-state.variants.test.ts +245 -0
- package/src/components/empty-state/empty-state.variants.ts +132 -0
- package/src/components/empty-state/index.ts +20 -0
- package/src/components/fab/AGENTS.md +119 -0
- package/src/components/fab/fab-action.tsx +118 -0
- package/src/components/fab/fab-group.tsx +214 -0
- package/src/components/fab/fab-label.tsx +18 -0
- package/src/components/fab/fab.context.tsx +125 -0
- package/src/components/fab/fab.tsx +156 -0
- package/src/components/fab/fab.types.ts +17 -0
- package/src/components/fab/fab.variants.test.ts +274 -0
- package/src/components/fab/fab.variants.ts +234 -0
- package/src/components/fab/index.ts +37 -0
- package/src/components/feedback/AGENTS.md +95 -0
- package/src/components/feedback/feedback-action.tsx +16 -0
- package/src/components/feedback/feedback-cancel.tsx +34 -0
- package/src/components/feedback/feedback-close.tsx +21 -0
- package/src/components/feedback/feedback-content.tsx +24 -0
- package/src/components/feedback/feedback-field.tsx +70 -0
- package/src/components/feedback/feedback-footer.tsx +21 -0
- package/src/components/feedback/feedback-panel.tsx +54 -0
- package/src/components/feedback/feedback-submit.tsx +74 -0
- package/src/components/feedback/feedback-title.tsx +34 -0
- package/src/components/feedback/feedback-trigger.tsx +20 -0
- package/src/components/feedback/feedback.context.tsx +73 -0
- package/src/components/feedback/feedback.tsx +135 -0
- package/src/components/feedback/feedback.variants.test.ts +185 -0
- package/src/components/feedback/feedback.variants.ts +128 -0
- package/src/components/feedback/index.ts +24 -0
- package/src/components/field/AGENTS.md +19 -0
- package/src/components/field/field-label.tsx +19 -5
- package/src/components/field/field.context.tsx +17 -0
- package/src/components/field/field.tsx +15 -2
- package/src/components/field/field.variants.test.ts +24 -0
- package/src/components/field/field.variants.ts +20 -0
- package/src/components/field/index.ts +1 -0
- package/src/components/item/AGENTS.md +92 -0
- package/src/components/item/index.ts +25 -0
- package/src/components/item/item-actions.tsx +28 -0
- package/src/components/item/item-content.tsx +15 -0
- package/src/components/item/item-description.tsx +12 -0
- package/src/components/item/item-footer.tsx +13 -0
- package/src/components/item/item-group.tsx +32 -0
- package/src/components/item/item-header.tsx +13 -0
- package/src/components/item/item-media.tsx +37 -0
- package/src/components/item/item-separator.tsx +13 -0
- package/src/components/item/item-title.tsx +17 -0
- package/src/components/item/item.context.tsx +66 -0
- package/src/components/item/item.tsx +218 -0
- package/src/components/item/item.types.ts +12 -0
- package/src/components/item/item.variants.test.ts +263 -0
- package/src/components/item/item.variants.ts +200 -0
- package/src/components/kpi/AGENTS.md +125 -0
- package/src/components/kpi/index.ts +45 -0
- package/src/components/kpi/kpi-action.tsx +12 -0
- package/src/components/kpi/kpi-content.tsx +35 -0
- package/src/components/kpi/kpi-footer.tsx +52 -0
- package/src/components/kpi/kpi-group.tsx +70 -0
- package/src/components/kpi/kpi-header.tsx +25 -0
- package/src/components/kpi/kpi-icon.tsx +28 -0
- package/src/components/kpi/kpi-sparkline.tsx +145 -0
- package/src/components/kpi/kpi-stat.tsx +20 -0
- package/src/components/kpi/kpi-title.tsx +19 -0
- package/src/components/kpi/kpi-trend.tsx +105 -0
- package/src/components/kpi/kpi-value.tsx +32 -0
- package/src/components/kpi/kpi.context.tsx +111 -0
- package/src/components/kpi/kpi.tsx +154 -0
- package/src/components/kpi/kpi.types.ts +9 -0
- package/src/components/kpi/kpi.variants.test.ts +428 -0
- package/src/components/kpi/kpi.variants.ts +347 -0
- package/src/components/label/AGENTS.md +89 -0
- package/src/components/label/index.ts +10 -0
- package/src/components/label/label.tsx +97 -0
- package/src/components/label/label.variants.test.ts +176 -0
- package/src/components/label/label.variants.ts +123 -0
- package/src/components/menu/AGENTS.md +176 -0
- package/src/components/menu/index.ts +44 -0
- package/src/components/menu/menu-background.tsx +17 -0
- package/src/components/menu/menu-checkbox-item.tsx +57 -0
- package/src/components/menu/menu-content.tsx +303 -0
- package/src/components/menu/menu-item.tsx +162 -0
- package/src/components/menu/menu-label.tsx +14 -0
- package/src/components/menu/menu-radio-group.tsx +50 -0
- package/src/components/menu/menu-radio-item.tsx +57 -0
- package/src/components/menu/menu-separator.tsx +11 -0
- package/src/components/menu/menu-sub-content.tsx +72 -0
- package/src/components/menu/menu-sub-trigger.tsx +50 -0
- package/src/components/menu/menu-sub.tsx +75 -0
- package/src/components/menu/menu-trigger.tsx +57 -0
- package/src/components/menu/menu.context.tsx +214 -0
- package/src/components/menu/menu.tsx +88 -0
- package/src/components/menu/menu.types.ts +33 -0
- package/src/components/menu/menu.variants.test.ts +400 -0
- package/src/components/menu/menu.variants.ts +353 -0
- package/src/components/meter/AGENTS.md +135 -0
- package/src/components/meter/index.ts +43 -0
- package/src/components/meter/meter-fill.tsx +19 -0
- package/src/components/meter/meter-header.tsx +17 -0
- package/src/components/meter/meter-label.tsx +18 -0
- package/src/components/meter/meter-output.tsx +42 -0
- package/src/components/meter/meter-segments.tsx +83 -0
- package/src/components/meter/meter-track.tsx +16 -0
- package/src/components/meter/meter.context.tsx +62 -0
- package/src/components/meter/meter.tsx +286 -0
- package/src/components/meter/meter.types.ts +30 -0
- package/src/components/meter/meter.variants.test.ts +430 -0
- package/src/components/meter/meter.variants.ts +347 -0
- package/src/components/overlay/AGENTS.md +89 -0
- package/src/components/overlay/index.ts +27 -0
- package/src/components/overlay/overlay-portal.tsx +84 -0
- package/src/components/overlay/overlay-presence.test.ts +46 -0
- package/src/components/overlay/overlay-presence.ts +49 -0
- package/src/components/overlay/overlay-registry.test.ts +96 -0
- package/src/components/overlay/overlay-registry.ts +99 -0
- package/src/components/overlay/overlay-scrim.tsx +49 -0
- package/src/components/overlay/overlay.context.tsx +48 -0
- package/src/components/overlay/overlay.tsx +118 -0
- package/src/components/overlay/overlay.variants.test.ts +37 -0
- package/src/components/overlay/overlay.variants.ts +33 -0
- package/src/components/overlay/use-overlay-back-handler.ts +34 -0
- package/src/components/overlay/use-overlay-presence.ts +87 -0
- package/src/components/popover/AGENTS.md +138 -0
- package/src/components/popover/index.ts +48 -0
- package/src/components/popover/popover-anchor.tsx +49 -0
- package/src/components/popover/popover-arrow.tsx +89 -0
- package/src/components/popover/popover-close.tsx +73 -0
- package/src/components/popover/popover-content.tsx +235 -0
- package/src/components/popover/popover-description.tsx +18 -0
- package/src/components/popover/popover-title.tsx +33 -0
- package/src/components/popover/popover-trigger.tsx +56 -0
- package/src/components/popover/popover.context.tsx +71 -0
- package/src/components/popover/popover.position.test.ts +354 -0
- package/src/components/popover/popover.position.ts +286 -0
- package/src/components/popover/popover.tsx +123 -0
- package/src/components/popover/popover.variants.test.ts +135 -0
- package/src/components/popover/popover.variants.ts +85 -0
- package/src/components/popover/use-anchor-measure.ts +76 -0
- package/src/components/popover/use-anchored-content.ts +219 -0
- package/src/components/pressable/AGENTS.md +11 -1
- package/src/components/pressable/pressable.tsx +7 -2
- package/src/components/pressable/pressable.variants.test.ts +29 -0
- package/src/components/pressable/pressable.variants.ts +17 -0
- package/src/components/progress/AGENTS.md +120 -0
- package/src/components/progress/index.ts +40 -0
- package/src/components/progress/progress-fill.tsx +152 -0
- package/src/components/progress/progress-header.tsx +19 -0
- package/src/components/progress/progress-label.tsx +27 -0
- package/src/components/progress/progress-output.tsx +47 -0
- package/src/components/progress/progress-track.tsx +38 -0
- package/src/components/progress/progress.context.tsx +79 -0
- package/src/components/progress/progress.tsx +171 -0
- package/src/components/progress/progress.types.ts +23 -0
- package/src/components/progress/progress.variants.test.ts +335 -0
- package/src/components/progress/progress.variants.ts +284 -0
- package/src/components/progress-button/AGENTS.md +116 -0
- package/src/components/progress-button/index.ts +36 -0
- package/src/components/progress-button/progress-button-done.tsx +33 -0
- package/src/components/progress-button/progress-button-label.tsx +27 -0
- package/src/components/progress-button/progress-button.context.tsx +93 -0
- package/src/components/progress-button/progress-button.tsx +414 -0
- package/src/components/progress-button/progress-button.variants.test.ts +314 -0
- package/src/components/progress-button/progress-button.variants.ts +236 -0
- package/src/components/provider/AGENTS.md +38 -25
- package/src/components/provider/provider.tsx +38 -11
- package/src/components/radio/AGENTS.md +8 -10
- package/src/components/radio/radio.variants.test.ts +5 -6
- package/src/components/radio/radio.variants.ts +6 -7
- package/src/components/rating/AGENTS.md +143 -0
- package/src/components/rating/index.ts +43 -0
- package/src/components/rating/rating-output.tsx +39 -0
- package/src/components/rating/rating-star.tsx +70 -0
- package/src/components/rating/rating-stars.tsx +219 -0
- package/src/components/rating/rating.context.tsx +79 -0
- package/src/components/rating/rating.tsx +163 -0
- package/src/components/rating/rating.variants.test.ts +347 -0
- package/src/components/rating/rating.variants.ts +354 -0
- package/src/components/selection-mode/AGENTS.md +155 -0
- package/src/components/selection-mode/index.ts +43 -0
- package/src/components/selection-mode/selection-mode-action.tsx +65 -0
- package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
- package/src/components/selection-mode/selection-mode-group.tsx +123 -0
- package/src/components/selection-mode/selection-mode-header.tsx +85 -0
- package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
- package/src/components/selection-mode/selection-mode-item.tsx +182 -0
- package/src/components/selection-mode/selection-mode.context.tsx +89 -0
- package/src/components/selection-mode/selection-mode.tsx +207 -0
- package/src/components/selection-mode/selection-mode.types.ts +36 -0
- package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
- package/src/components/selection-mode/selection-mode.variants.ts +329 -0
- package/src/components/skeleton/AGENTS.md +120 -0
- package/src/components/skeleton/index.ts +15 -0
- package/src/components/skeleton/skeleton-clock.ts +48 -0
- package/src/components/skeleton/skeleton-group.tsx +64 -0
- package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
- package/src/components/skeleton/skeleton.context.tsx +45 -0
- package/src/components/skeleton/skeleton.tsx +227 -0
- package/src/components/skeleton/skeleton.variants.test.ts +245 -0
- package/src/components/skeleton/skeleton.variants.ts +222 -0
- package/src/components/slide-button/AGENTS.md +122 -0
- package/src/components/slide-button/index.ts +43 -0
- package/src/components/slide-button/slide-button-label.tsx +70 -0
- package/src/components/slide-button/slide-button-thumb.tsx +93 -0
- package/src/components/slide-button/slide-button.context.tsx +76 -0
- package/src/components/slide-button/slide-button.tsx +457 -0
- package/src/components/slide-button/slide-button.variants.test.ts +288 -0
- package/src/components/slide-button/slide-button.variants.ts +324 -0
- package/src/components/slider/index.ts +2 -0
- package/src/components/slider/slider-thumb.tsx +33 -1
- package/src/components/slider/slider.variants.test.ts +52 -0
- package/src/components/slider/slider.variants.ts +56 -2
- package/src/components/stack-card/AGENTS.md +169 -0
- package/src/components/stack-card/index.ts +37 -0
- package/src/components/stack-card/stack-card-action.tsx +66 -0
- package/src/components/stack-card/stack-card-actions.tsx +18 -0
- package/src/components/stack-card/stack-card-card.tsx +71 -0
- package/src/components/stack-card/stack-card-empty.tsx +30 -0
- package/src/components/stack-card/stack-card-slot.tsx +90 -0
- package/src/components/stack-card/stack-card-stamp.tsx +76 -0
- package/src/components/stack-card/stack-card.context.tsx +117 -0
- package/src/components/stack-card/stack-card.tsx +557 -0
- package/src/components/stack-card/stack-card.types.ts +26 -0
- package/src/components/stack-card/stack-card.variants.test.ts +355 -0
- package/src/components/stack-card/stack-card.variants.ts +360 -0
- package/src/components/steps/AGENTS.md +153 -0
- package/src/components/steps/index.ts +45 -0
- package/src/components/steps/steps-connector.tsx +31 -0
- package/src/components/steps/steps-description.tsx +26 -0
- package/src/components/steps/steps-indicator.tsx +106 -0
- package/src/components/steps/steps-item.tsx +225 -0
- package/src/components/steps/steps-panel.tsx +41 -0
- package/src/components/steps/steps-title.tsx +32 -0
- package/src/components/steps/steps-track.tsx +59 -0
- package/src/components/steps/steps.context.tsx +123 -0
- package/src/components/steps/steps.tsx +166 -0
- package/src/components/steps/steps.variants.test.ts +388 -0
- package/src/components/steps/steps.variants.ts +468 -0
- package/src/components/surface/AGENTS.md +72 -0
- package/src/components/surface/index.ts +15 -0
- package/src/components/surface/surface.context.tsx +54 -0
- package/src/components/surface/surface.tsx +70 -0
- package/src/components/surface/surface.variants.test.ts +180 -0
- package/src/components/surface/surface.variants.ts +133 -0
- package/src/components/swipe/AGENTS.md +140 -0
- package/src/components/swipe/index.ts +31 -0
- package/src/components/swipe/swipe-action.tsx +99 -0
- package/src/components/swipe/swipe-end.tsx +14 -0
- package/src/components/swipe/swipe-group.tsx +55 -0
- package/src/components/swipe/swipe-panel.tsx +55 -0
- package/src/components/swipe/swipe-start.tsx +15 -0
- package/src/components/swipe/swipe.context.tsx +135 -0
- package/src/components/swipe/swipe.tsx +393 -0
- package/src/components/swipe/swipe.types.ts +7 -0
- package/src/components/swipe/swipe.variants.test.ts +337 -0
- package/src/components/swipe/swipe.variants.ts +290 -0
- package/src/components/switch/switch.variants.ts +2 -2
- package/src/components/tabs/AGENTS.md +4 -3
- package/src/components/tabs/tabs.variants.test.ts +1 -1
- package/src/components/tabs/tabs.variants.ts +4 -6
- package/src/components/textarea/AGENTS.md +86 -0
- package/src/components/textarea/index.ts +16 -0
- package/src/components/textarea/textarea.tsx +157 -0
- package/src/components/textarea/textarea.variants.test.ts +219 -0
- package/src/components/textarea/textarea.variants.ts +171 -0
- package/src/components/toast/AGENTS.md +140 -0
- package/src/components/toast/index.ts +74 -0
- package/src/components/toast/toast-action.tsx +59 -0
- package/src/components/toast/toast-api.ts +32 -0
- package/src/components/toast/toast-close.tsx +44 -0
- package/src/components/toast/toast-content.tsx +11 -0
- package/src/components/toast/toast-description.tsx +11 -0
- package/src/components/toast/toast-indicator.tsx +34 -0
- package/src/components/toast/toast-item.tsx +308 -0
- package/src/components/toast/toast-title.tsx +13 -0
- package/src/components/toast/toast-viewport.tsx +135 -0
- package/src/components/toast/toast.context.tsx +69 -0
- package/src/components/toast/toast.store.test.ts +259 -0
- package/src/components/toast/toast.store.ts +338 -0
- package/src/components/toast/toast.timer.test.ts +64 -0
- package/src/components/toast/toast.timer.ts +51 -0
- package/src/components/toast/toast.tsx +132 -0
- package/src/components/toast/toast.variants.test.ts +393 -0
- package/src/components/toast/toast.variants.ts +304 -0
- package/src/components/toggle-button/AGENTS.md +106 -0
- package/src/components/toggle-button/index.ts +30 -0
- package/src/components/toggle-button/toggle-button-group.tsx +156 -0
- package/src/components/toggle-button/toggle-button-label.tsx +19 -0
- package/src/components/toggle-button/toggle-button.context.tsx +130 -0
- package/src/components/toggle-button/toggle-button.tsx +172 -0
- package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
- package/src/components/toggle-button/toggle-button.variants.ts +160 -0
- package/src/components/tooltip/AGENTS.md +133 -0
- package/src/components/tooltip/index.ts +28 -0
- package/src/components/tooltip/tooltip-arrow.tsx +40 -0
- package/src/components/tooltip/tooltip-content.tsx +179 -0
- package/src/components/tooltip/tooltip-description.tsx +19 -0
- package/src/components/tooltip/tooltip-text.tsx +21 -0
- package/src/components/tooltip/tooltip-title.tsx +20 -0
- package/src/components/tooltip/tooltip-trigger.tsx +103 -0
- package/src/components/tooltip/tooltip.context.tsx +68 -0
- package/src/components/tooltip/tooltip.tsx +213 -0
- package/src/components/tooltip/tooltip.variants.test.ts +231 -0
- package/src/components/tooltip/tooltip.variants.ts +177 -0
- package/src/hooks/use-calm-motion.tsx +31 -0
- package/src/lib/calm-motion.test.ts +17 -0
- package/src/lib/calm-motion.ts +23 -0
- package/src/lib/cn.test.ts +14 -0
- package/src/lib/cn.ts +2 -2
- package/src/no-gorhom.test.ts +54 -0
- package/src/styles/geometry.test.ts +1 -0
- package/src/styles/theme.css +4 -0
- package/src/styles/tokens.css +11 -0
- package/src/styles/tokens.test.ts +22 -0
- package/src/styles/tokens.ts +20 -0
- package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
- package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode, useMemo, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Image,
|
|
4
|
+
type ImageErrorEventData,
|
|
5
|
+
type ImageProps,
|
|
6
|
+
type ImageSourcePropType,
|
|
7
|
+
type NativeSyntheticEvent,
|
|
8
|
+
View,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import { IconPeople } from "../../icons/central";
|
|
11
|
+
import { cn } from "../../lib/cn";
|
|
12
|
+
import { Icon } from "../icon";
|
|
13
|
+
import { Pressable, type PressableProps } from "../pressable";
|
|
14
|
+
import { Text } from "../text";
|
|
15
|
+
import { type AvatarContextValue, AvatarProvider, useAvatarGroupContext } from "./avatar.context";
|
|
16
|
+
import {
|
|
17
|
+
resolveAvatarAccessibilityLabel,
|
|
18
|
+
resolveAvatarInitials,
|
|
19
|
+
resolveAvatarShowsImage,
|
|
20
|
+
resolveAvatarSourceKey,
|
|
21
|
+
} from "./avatar.utils";
|
|
22
|
+
import {
|
|
23
|
+
AVATAR_FOREGROUND_TOKEN,
|
|
24
|
+
type AvatarColor,
|
|
25
|
+
type AvatarSize,
|
|
26
|
+
type AvatarVariant,
|
|
27
|
+
avatarVariants,
|
|
28
|
+
resolveAvatarInteractive,
|
|
29
|
+
resolveAvatarSize,
|
|
30
|
+
} from "./avatar.variants";
|
|
31
|
+
import { AvatarBadge } from "./avatar-badge";
|
|
32
|
+
import { AvatarGroup } from "./avatar-group";
|
|
33
|
+
|
|
34
|
+
export type AvatarProps = Omit<PressableProps, "asChild" | "busy" | "children" | "disabled"> & {
|
|
35
|
+
/** The picture. The fallback shows while it loads, and in its place if it fails. */
|
|
36
|
+
source?: ImageSourcePropType;
|
|
37
|
+
/** The person's name. Read by screen readers, and its initials are the default fallback. */
|
|
38
|
+
name?: string;
|
|
39
|
+
/** Fallback text, when the initials of `name` are not what should show. */
|
|
40
|
+
fallback?: string;
|
|
41
|
+
/** How the fallback surface is painted. */
|
|
42
|
+
variant?: AvatarVariant;
|
|
43
|
+
/** What the fallback surface means. */
|
|
44
|
+
color?: AvatarColor;
|
|
45
|
+
/** Default `md`, or the enclosing group's size. */
|
|
46
|
+
size?: AvatarSize;
|
|
47
|
+
isDisabled?: boolean;
|
|
48
|
+
/** Passed to the `Image`. `onError` still fires, after the fallback has taken over. */
|
|
49
|
+
imageProps?: Omit<ImageProps, "source">;
|
|
50
|
+
/** Overlays — an `Avatar.Badge`. */
|
|
51
|
+
children?: ReactNode;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function AvatarRoot({
|
|
55
|
+
source,
|
|
56
|
+
name,
|
|
57
|
+
fallback,
|
|
58
|
+
variant = "soft",
|
|
59
|
+
color = "default",
|
|
60
|
+
size: sizeProp,
|
|
61
|
+
isDisabled = false,
|
|
62
|
+
imageProps,
|
|
63
|
+
accessibilityLabel,
|
|
64
|
+
onPress,
|
|
65
|
+
onLongPress,
|
|
66
|
+
haptic,
|
|
67
|
+
feedback = "scale",
|
|
68
|
+
pressedScale,
|
|
69
|
+
pressedOpacity,
|
|
70
|
+
className,
|
|
71
|
+
children,
|
|
72
|
+
...props
|
|
73
|
+
}: AvatarProps): ReactElement {
|
|
74
|
+
const group = useAvatarGroupContext();
|
|
75
|
+
const size = resolveAvatarSize({ size: sizeProp, groupSize: group?.size });
|
|
76
|
+
const context = useMemo<AvatarContextValue>(
|
|
77
|
+
() => ({ size, variant, color, isDisabled }),
|
|
78
|
+
[size, variant, color, isDisabled]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Remembered by key, not by object: an inline `source={{ uri }}` is a new
|
|
82
|
+
// object every render, and a new URI has to retry on its own.
|
|
83
|
+
const sourceKey = resolveAvatarSourceKey(source);
|
|
84
|
+
const [failedKey, setFailedKey] = useState<string | null>(null);
|
|
85
|
+
const showsImage = resolveAvatarShowsImage({ sourceKey, failedKey });
|
|
86
|
+
const slots = avatarVariants({ size, variant, color, isDisabled, hasImage: showsImage });
|
|
87
|
+
|
|
88
|
+
const initials = fallback?.trim() || resolveAvatarInitials(name);
|
|
89
|
+
const label = resolveAvatarAccessibilityLabel({ accessibilityLabel, name, fallback });
|
|
90
|
+
|
|
91
|
+
const onImageError = (event: NativeSyntheticEvent<ImageErrorEventData>) => {
|
|
92
|
+
setFailedKey(sourceKey);
|
|
93
|
+
imageProps?.onError?.(event);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// The fallback is always painted and the image goes over it, so the circle is
|
|
97
|
+
// never empty while a picture loads or after it fails.
|
|
98
|
+
const face = (
|
|
99
|
+
<View className={slots.face()}>
|
|
100
|
+
{initials ? (
|
|
101
|
+
<Text allowFontScaling={false} className={slots.fallbackLabel()} numberOfLines={1}>
|
|
102
|
+
{initials}
|
|
103
|
+
</Text>
|
|
104
|
+
) : (
|
|
105
|
+
<Icon className={slots.icon()} color={AVATAR_FOREGROUND_TOKEN[variant][color]} icon={IconPeople} />
|
|
106
|
+
)}
|
|
107
|
+
{showsImage && source !== undefined ? (
|
|
108
|
+
<Image
|
|
109
|
+
accessibilityIgnoresInvertColors
|
|
110
|
+
{...imageProps}
|
|
111
|
+
className={cn(slots.image(), imageProps?.className)}
|
|
112
|
+
onError={onImageError}
|
|
113
|
+
source={source}
|
|
114
|
+
/>
|
|
115
|
+
) : null}
|
|
116
|
+
</View>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (!resolveAvatarInteractive({ onPress, onLongPress })) {
|
|
120
|
+
return (
|
|
121
|
+
<AvatarProvider value={context}>
|
|
122
|
+
<View
|
|
123
|
+
accessibilityLabel={label}
|
|
124
|
+
accessibilityRole={label ? "image" : undefined}
|
|
125
|
+
accessible={label !== undefined}
|
|
126
|
+
className={slots.root({ className })}
|
|
127
|
+
{...props}
|
|
128
|
+
>
|
|
129
|
+
{face}
|
|
130
|
+
{children}
|
|
131
|
+
</View>
|
|
132
|
+
</AvatarProvider>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<AvatarProvider value={context}>
|
|
138
|
+
<Pressable
|
|
139
|
+
accessibilityLabel={label}
|
|
140
|
+
accessibilityRole="button"
|
|
141
|
+
className={slots.root({ className })}
|
|
142
|
+
disabled={isDisabled}
|
|
143
|
+
feedback={feedback}
|
|
144
|
+
haptic={haptic}
|
|
145
|
+
onLongPress={onLongPress}
|
|
146
|
+
onPress={onPress}
|
|
147
|
+
pressedOpacity={pressedOpacity}
|
|
148
|
+
pressedScale={pressedScale}
|
|
149
|
+
{...props}
|
|
150
|
+
>
|
|
151
|
+
{face}
|
|
152
|
+
{children}
|
|
153
|
+
</Pressable>
|
|
154
|
+
</AvatarProvider>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* A person, as a picture — with their initials underneath for when there is no
|
|
160
|
+
* picture, it is still loading, or it failed.
|
|
161
|
+
*
|
|
162
|
+
* **The fallback is always there.** It is painted first and the image goes on
|
|
163
|
+
* top, so a slow or dead URL never leaves an empty circle. A failure is
|
|
164
|
+
* remembered for that source only: change the URI or its headers and the image
|
|
165
|
+
* is tried again. With no `fallback` and no `name` the fallback is a person glyph.
|
|
166
|
+
*
|
|
167
|
+
* `name` is what a screen reader reads, and its initials — first and last word,
|
|
168
|
+
* `Anahera Rose Tui` → `AT` — are the default fallback. `fallback` overrides the
|
|
169
|
+
* drawn text only.
|
|
170
|
+
*
|
|
171
|
+
* `variant` and `color` paint the fallback surface, on the same six colours a
|
|
172
|
+
* `Badge` takes. A size is a fixed edge: faces line up against each other, and a
|
|
173
|
+
* circle cannot grow with OS font scaling the way a label can.
|
|
174
|
+
*
|
|
175
|
+
* **An avatar is content until it is given something to do.** With no `onPress`
|
|
176
|
+
* or `onLongPress` it is a plain `View` announced as an image; supply either and
|
|
177
|
+
* it becomes a `Pressable` announced as a button.
|
|
178
|
+
*
|
|
179
|
+
* Children are overlays: `Avatar.Badge` pins a count or a presence dot to a
|
|
180
|
+
* corner, outside the clipped circle so it is never cut in half.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* <Avatar name="Aria Whitlock" source={{ uri: user.avatarUrl }} />
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* <Avatar accessibilityLabel="Aria Whitlock, online" name="Aria Whitlock">
|
|
187
|
+
* <Avatar.Badge placement="bottom-right" />
|
|
188
|
+
* </Avatar>
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* <Avatar.Group max={3} total={team.length}>
|
|
192
|
+
* {team.map((person) => <Avatar key={person.id} name={person.name} source={person.photo} />)}
|
|
193
|
+
* </Avatar.Group>
|
|
194
|
+
*/
|
|
195
|
+
export const Avatar = Object.assign(AvatarRoot, {
|
|
196
|
+
/** An overlay pinned to a corner — a count, or a presence dot when empty. */
|
|
197
|
+
Badge: AvatarBadge,
|
|
198
|
+
/** A row of overlapping avatars, with the people who did not fit counted at the end. */
|
|
199
|
+
Group: AvatarGroup,
|
|
200
|
+
displayName: "DelacourUI.Avatar",
|
|
201
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
resolveAvatarAccessibilityLabel,
|
|
4
|
+
resolveAvatarGroup,
|
|
5
|
+
resolveAvatarInitials,
|
|
6
|
+
resolveAvatarOverflowLabel,
|
|
7
|
+
resolveAvatarShowsImage,
|
|
8
|
+
resolveAvatarSourceKey,
|
|
9
|
+
} from "./avatar.utils";
|
|
10
|
+
|
|
11
|
+
describe("resolveAvatarInitials", () => {
|
|
12
|
+
test("takes the first letter of the first and last words", () => {
|
|
13
|
+
expect(resolveAvatarInitials("Aria Whitlock")).toBe("AW");
|
|
14
|
+
expect(resolveAvatarInitials("Anahera Rose Tui")).toBe("AT");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("a single word gives a single letter", () => {
|
|
18
|
+
expect(resolveAvatarInitials("Mihi")).toBe("M");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("uppercases, and ignores surrounding and repeated whitespace", () => {
|
|
22
|
+
expect(resolveAvatarInitials(" rawiri kemp ")).toBe("RK");
|
|
23
|
+
expect(resolveAvatarInitials("lena\tvarga")).toBe("LV");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("is empty for an empty, blank or missing name", () => {
|
|
27
|
+
expect(resolveAvatarInitials("")).toBe("");
|
|
28
|
+
expect(resolveAvatarInitials(" ")).toBe("");
|
|
29
|
+
expect(resolveAvatarInitials(undefined)).toBe("");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("never splits a letter outside the basic plane", () => {
|
|
33
|
+
// A naive `word[0]` would take half of a surrogate pair and draw a box.
|
|
34
|
+
expect(resolveAvatarInitials("𝒜ria 𝒲hitlock")).toBe("𝒜𝒲");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("keeps letters in scripts with no case", () => {
|
|
38
|
+
expect(resolveAvatarInitials("森山 健二")).toBe("森健");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("skips a leading punctuation mark in a word", () => {
|
|
42
|
+
expect(resolveAvatarInitials("(Aria) 'Whitlock'")).toBe("AW");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("resolveAvatarSourceKey", () => {
|
|
47
|
+
test("is null for no source", () => {
|
|
48
|
+
expect(resolveAvatarSourceKey(undefined)).toBeNull();
|
|
49
|
+
expect(resolveAvatarSourceKey(null)).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("is null for a uri that is empty", () => {
|
|
53
|
+
expect(resolveAvatarSourceKey({ uri: "" })).toBeNull();
|
|
54
|
+
expect(resolveAvatarSourceKey({})).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("names a bundled asset by its module id", () => {
|
|
58
|
+
expect(resolveAvatarSourceKey(12)).toBe("asset:12");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("changes when the uri changes", () => {
|
|
62
|
+
expect(resolveAvatarSourceKey({ uri: "https://a.test/1.png" })).not.toBe(
|
|
63
|
+
resolveAvatarSourceKey({ uri: "https://a.test/2.png" })
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("changes when only the request headers change", () => {
|
|
68
|
+
// A token refresh is a retry, so it has to clear an earlier failure.
|
|
69
|
+
expect(resolveAvatarSourceKey({ uri: "https://a.test/1.png", headers: { Authorization: "a" } })).not.toBe(
|
|
70
|
+
resolveAvatarSourceKey({ uri: "https://a.test/1.png", headers: { Authorization: "b" } })
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("is stable across renders for an equal source", () => {
|
|
75
|
+
expect(resolveAvatarSourceKey({ uri: "https://a.test/1.png", headers: { a: "1", b: "2" } })).toBe(
|
|
76
|
+
resolveAvatarSourceKey({ uri: "https://a.test/1.png", headers: { b: "2", a: "1" } })
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("reads every entry of a multi-resolution list", () => {
|
|
81
|
+
expect(resolveAvatarSourceKey([{ uri: "https://a.test/1x.png" }, { uri: "https://a.test/2x.png" }])).toBe(
|
|
82
|
+
"uri:https://a.test/1x.png|uri:https://a.test/2x.png"
|
|
83
|
+
);
|
|
84
|
+
expect(resolveAvatarSourceKey([])).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("resolveAvatarShowsImage", () => {
|
|
89
|
+
test("shows an image that has not failed", () => {
|
|
90
|
+
expect(resolveAvatarShowsImage({ sourceKey: "uri:a", failedKey: null })).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("hides one that failed", () => {
|
|
94
|
+
expect(resolveAvatarShowsImage({ sourceKey: "uri:a", failedKey: "uri:a" })).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("retries a new source after an earlier one failed", () => {
|
|
98
|
+
expect(resolveAvatarShowsImage({ sourceKey: "uri:b", failedKey: "uri:a" })).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("never shows an image with no source", () => {
|
|
102
|
+
expect(resolveAvatarShowsImage({ sourceKey: null, failedKey: null })).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("resolveAvatarGroup", () => {
|
|
107
|
+
test("shows every child when nothing caps it", () => {
|
|
108
|
+
expect(resolveAvatarGroup({ count: 4 })).toEqual({ visible: 4, overflow: 0 });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("max caps the faces, not the row", () => {
|
|
112
|
+
expect(resolveAvatarGroup({ count: 5, max: 3 })).toEqual({ visible: 3, overflow: 2 });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("a max at or above the count adds no overflow", () => {
|
|
116
|
+
expect(resolveAvatarGroup({ count: 3, max: 3 })).toEqual({ visible: 3, overflow: 0 });
|
|
117
|
+
expect(resolveAvatarGroup({ count: 3, max: 10 })).toEqual({ visible: 3, overflow: 0 });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("total counts the people who were never passed as children", () => {
|
|
121
|
+
expect(resolveAvatarGroup({ count: 3, total: 40 })).toEqual({ visible: 3, overflow: 37 });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("max and total compose", () => {
|
|
125
|
+
expect(resolveAvatarGroup({ count: 5, max: 3, total: 40 })).toEqual({ visible: 3, overflow: 37 });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("a total below the count is ignored rather than going negative", () => {
|
|
129
|
+
expect(resolveAvatarGroup({ count: 5, total: 2 })).toEqual({ visible: 5, overflow: 0 });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("a negative, fractional or non-finite max is clamped", () => {
|
|
133
|
+
expect(resolveAvatarGroup({ count: 5, max: -1 })).toEqual({ visible: 0, overflow: 5 });
|
|
134
|
+
expect(resolveAvatarGroup({ count: 5, max: 2.7 })).toEqual({ visible: 2, overflow: 3 });
|
|
135
|
+
expect(resolveAvatarGroup({ count: 5, max: Number.NaN })).toEqual({ visible: 5, overflow: 0 });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("a non-finite total is ignored", () => {
|
|
139
|
+
expect(resolveAvatarGroup({ count: 2, total: Number.POSITIVE_INFINITY })).toEqual({ visible: 2, overflow: 0 });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("an empty group is empty", () => {
|
|
143
|
+
expect(resolveAvatarGroup({ count: 0 })).toEqual({ visible: 0, overflow: 0 });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("resolveAvatarOverflowLabel", () => {
|
|
148
|
+
test("draws a plus and the count", () => {
|
|
149
|
+
expect(resolveAvatarOverflowLabel(2)).toEqual({ text: "+2", accessibilityLabel: "2 more" });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("caps what is drawn at 99, never what is read", () => {
|
|
153
|
+
expect(resolveAvatarOverflowLabel(1234)).toEqual({ text: "99+", accessibilityLabel: "1234 more" });
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("resolveAvatarAccessibilityLabel", () => {
|
|
158
|
+
test("prefers the name", () => {
|
|
159
|
+
expect(resolveAvatarAccessibilityLabel({ name: "Aria Whitlock", fallback: "AW" })).toBe("Aria Whitlock");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("falls back to the fallback text", () => {
|
|
163
|
+
expect(resolveAvatarAccessibilityLabel({ fallback: "AW" })).toBe("AW");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("is undefined when there is nothing to read", () => {
|
|
167
|
+
expect(resolveAvatarAccessibilityLabel({ name: " " })).toBeUndefined();
|
|
168
|
+
expect(resolveAvatarAccessibilityLabel({})).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("an explicit label wins over both", () => {
|
|
172
|
+
expect(resolveAvatarAccessibilityLabel({ accessibilityLabel: "You", name: "Aria Whitlock" })).toBe("You");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The avatar's pure decisions — initials, image retry, group overflow and the
|
|
3
|
+
* name a screen reader reads.
|
|
4
|
+
*
|
|
5
|
+
* Free of React Native imports so `bun test` reaches every one of them. The
|
|
6
|
+
* source type is restated structurally rather than imported as
|
|
7
|
+
* `ImageSourcePropType` for the same reason. See AGENTS.md.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** One entry of an image source, as `Image` accepts it — a URI request or a bundled asset's module id. */
|
|
11
|
+
export type AvatarSourceEntry = number | { uri?: string; headers?: Record<string, string> };
|
|
12
|
+
|
|
13
|
+
/** Anything `Image`'s `source` accepts: one entry or a list of resolutions. */
|
|
14
|
+
export type AvatarSource = AvatarSourceEntry | readonly AvatarSourceEntry[];
|
|
15
|
+
|
|
16
|
+
/** A letter or number in any script, which is what an initial has to start with. */
|
|
17
|
+
const INITIAL = /[\p{L}\p{N}]/u;
|
|
18
|
+
|
|
19
|
+
/** The first letter or number of a word, whole — never half a surrogate pair. */
|
|
20
|
+
function initialOf(word: string): string {
|
|
21
|
+
return Array.from(word).find((char) => INITIAL.test(char)) ?? "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Up to two initials from a name: the first letter of the first word and of the
|
|
26
|
+
* last. `Anahera Rose Tui` reads `AT`, which is how a person signs rather than
|
|
27
|
+
* how a form abbreviates them. A single word gives a single letter.
|
|
28
|
+
*
|
|
29
|
+
* Walks code points rather than UTF-16 units, so a letter outside the basic
|
|
30
|
+
* plane is kept whole instead of drawn as a replacement box, and skips leading
|
|
31
|
+
* punctuation so a quoted nickname still gives a letter.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveAvatarInitials(name: string | undefined): string {
|
|
34
|
+
const words = (name ?? "").trim().split(/\s+/).filter(Boolean);
|
|
35
|
+
if (words.length === 0) return "";
|
|
36
|
+
const first = initialOf(words[0] ?? "");
|
|
37
|
+
const last = words.length > 1 ? initialOf(words[words.length - 1] ?? "") : "";
|
|
38
|
+
return `${first}${last}`.toUpperCase();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function entryKey(entry: AvatarSourceEntry): string | null {
|
|
42
|
+
if (typeof entry === "number") return `asset:${entry}`;
|
|
43
|
+
if (!entry.uri) return null;
|
|
44
|
+
if (!entry.headers) return `uri:${entry.uri}`;
|
|
45
|
+
const headers = Object.keys(entry.headers)
|
|
46
|
+
.sort()
|
|
47
|
+
.map((name) => `${name}=${entry.headers?.[name]}`)
|
|
48
|
+
.join("&");
|
|
49
|
+
return `uri:${entry.uri}#${headers}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A string that changes exactly when the image request changes.
|
|
54
|
+
*
|
|
55
|
+
* The avatar remembers which source failed by this key rather than by object
|
|
56
|
+
* identity, because an inline `source={{ uri }}` is a new object every render —
|
|
57
|
+
* keyed by identity, a failed image would retry forever. Keyed by URI alone, a
|
|
58
|
+
* token refresh in the headers would never retry at all. Header order is
|
|
59
|
+
* normalised so an equal request is an equal key.
|
|
60
|
+
*
|
|
61
|
+
* `null` means there is nothing to load.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveAvatarSourceKey(source: AvatarSource | null | undefined): string | null {
|
|
64
|
+
if (source === null || source === undefined) return null;
|
|
65
|
+
const entries: readonly AvatarSourceEntry[] = Array.isArray(source) ? source : [source as AvatarSourceEntry];
|
|
66
|
+
const keys = entries.map(entryKey).filter((key): key is string => key !== null);
|
|
67
|
+
return keys.length === 0 ? null : keys.join("|");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether the image layer is mounted.
|
|
72
|
+
*
|
|
73
|
+
* The fallback is always underneath, so this is only ever a question of whether
|
|
74
|
+
* the image goes on top. A failure is remembered for the key that failed, which
|
|
75
|
+
* is what makes a new URI retry on its own.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveAvatarShowsImage({
|
|
78
|
+
sourceKey,
|
|
79
|
+
failedKey,
|
|
80
|
+
}: {
|
|
81
|
+
sourceKey: string | null;
|
|
82
|
+
failedKey: string | null;
|
|
83
|
+
}): boolean {
|
|
84
|
+
return sourceKey !== null && sourceKey !== failedKey;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* How many faces a group draws, and how many people the trailing tile counts.
|
|
89
|
+
*
|
|
90
|
+
* `max` caps the faces, not the row: five people at `max={3}` is three faces and
|
|
91
|
+
* `+2`. `total` is the number of people when the children are only the first
|
|
92
|
+
* few of them, so three children out of forty reads `+37`. A `total` below the
|
|
93
|
+
* number of children is ignored rather than allowed to go negative, and a
|
|
94
|
+
* nonsensical `max` is clamped rather than trusted.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveAvatarGroup({ count, max, total }: { count: number; max?: number; total?: number }): {
|
|
97
|
+
visible: number;
|
|
98
|
+
overflow: number;
|
|
99
|
+
} {
|
|
100
|
+
const cap = max === undefined || Number.isNaN(max) ? count : Math.max(0, Math.floor(max));
|
|
101
|
+
const visible = Math.min(count, cap);
|
|
102
|
+
const people = total !== undefined && Number.isFinite(total) ? Math.max(count, Math.floor(total)) : count;
|
|
103
|
+
return { visible, overflow: people - visible };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The most the overflow tile draws before it reads `99+`. Three digits do not fit a small circle. */
|
|
107
|
+
export const AVATAR_OVERFLOW_DISPLAY_MAX = 99;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* What the overflow tile draws and what it reads.
|
|
111
|
+
*
|
|
112
|
+
* The drawn count stops at {@link AVATAR_OVERFLOW_DISPLAY_MAX}; the spoken one
|
|
113
|
+
* never does, because a screen reader has room for the real number.
|
|
114
|
+
*/
|
|
115
|
+
export function resolveAvatarOverflowLabel(overflow: number): { text: string; accessibilityLabel: string } {
|
|
116
|
+
return {
|
|
117
|
+
text: overflow > AVATAR_OVERFLOW_DISPLAY_MAX ? `${AVATAR_OVERFLOW_DISPLAY_MAX}+` : `+${overflow}`,
|
|
118
|
+
accessibilityLabel: `${overflow} more`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The name a screen reader gives the avatar: an explicit label, else the
|
|
124
|
+
* person's name, else the fallback text. Blank strings count as absent, so an
|
|
125
|
+
* avatar with nothing to say is not announced as an empty image.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveAvatarAccessibilityLabel({
|
|
128
|
+
accessibilityLabel,
|
|
129
|
+
name,
|
|
130
|
+
fallback,
|
|
131
|
+
}: {
|
|
132
|
+
accessibilityLabel?: string;
|
|
133
|
+
name?: string;
|
|
134
|
+
fallback?: string;
|
|
135
|
+
}): string | undefined {
|
|
136
|
+
for (const candidate of [accessibilityLabel, name, fallback]) {
|
|
137
|
+
const trimmed = candidate?.trim();
|
|
138
|
+
if (trimmed) return trimmed;
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|