@delacour/react-native-ui 0.1.0-alpha.4 → 0.2.0-alpha.20261010061731
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 +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 +10 -0
- package/src/components/badge/badge-close-button.tsx +2 -2
- package/src/components/badge/badge.context.tsx +2 -1
- package/src/components/badge/badge.tsx +13 -4
- package/src/components/badge/badge.variants.test.ts +26 -0
- package/src/components/badge/badge.variants.ts +21 -0
- 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 +51 -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 +7 -1
- package/src/components/button/button.variants.test.ts +190 -0
- package/src/components/button/button.variants.ts +87 -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 +92 -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 +100 -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/input/AGENTS.md +5 -0
- package/src/components/input/input.variants.test.ts +32 -0
- package/src/components/input/input.variants.ts +13 -1
- 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 +82 -0
- package/src/components/surface/index.ts +15 -0
- package/src/components/surface/surface.context.tsx +56 -0
- package/src/components/surface/surface.tsx +87 -0
- package/src/components/surface/surface.variants.test.ts +227 -0
- package/src/components/surface/surface.variants.ts +165 -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/text/AGENTS.md +8 -2
- package/src/components/text/text.tsx +7 -0
- package/src/components/text/text.variants.test.ts +24 -1
- package/src/components/text/text.variants.ts +5 -0
- package/src/components/textarea/AGENTS.md +89 -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.parity.test.ts +62 -0
- package/src/lib/cn.test.ts +14 -0
- package/src/lib/cn.ts +11 -6
- 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 +44 -0
- package/src/styles/tokens.test.ts +56 -0
- package/src/styles/tokens.ts +29 -1
- 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,304 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
import { ALERT_FOREGROUND_TOKEN, ALERT_STATUSES, type AlertStatus } from "../alert/alert.variants";
|
|
4
|
+
import type { HapticFeedback } from "../pressable/pressable";
|
|
5
|
+
|
|
6
|
+
/** Alert's statuses, the same tuple — a toast and an alert saying the same thing look like one family. */
|
|
7
|
+
export const TOAST_STATUSES = ALERT_STATUSES;
|
|
8
|
+
|
|
9
|
+
/** Alert's glyph and title colour per status — the same map, so the two cannot drift. */
|
|
10
|
+
export const TOAST_FOREGROUND_TOKEN = ALERT_FOREGROUND_TOKEN;
|
|
11
|
+
|
|
12
|
+
export const TOAST_PLACEMENTS = ["top", "bottom"] as const;
|
|
13
|
+
|
|
14
|
+
type Status = AlertStatus;
|
|
15
|
+
type Placement = (typeof TOAST_PLACEMENTS)[number];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* How long a toast stays, in milliseconds.
|
|
19
|
+
*
|
|
20
|
+
* An action buys two more seconds, because reading and then reaching for a
|
|
21
|
+
* button takes longer than reading. Under a screen reader nothing timed leaves
|
|
22
|
+
* sooner than ten seconds: a message has to be read out in full before it goes
|
|
23
|
+
* (WCAG 2.2.1, timing adjustable).
|
|
24
|
+
*/
|
|
25
|
+
export const TOAST_DURATION = {
|
|
26
|
+
default: 4000,
|
|
27
|
+
withAction: 6000,
|
|
28
|
+
screenReaderMinimum: 10_000,
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
/** How the stack recedes: three drawn, each older one 8pt toward the edge, 5% smaller and 25% fainter. */
|
|
32
|
+
export const TOAST_STACK = {
|
|
33
|
+
maxVisible: 3,
|
|
34
|
+
depthOffset: 8,
|
|
35
|
+
depthScale: 0.05,
|
|
36
|
+
depthOpacity: 0.25,
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
/** How far a toast travels in from its edge, in points, while it fades in. */
|
|
40
|
+
export const TOAST_ENTER_DISTANCE = 16;
|
|
41
|
+
|
|
42
|
+
/** The gap between entrances when several toasts are shown in one tick. */
|
|
43
|
+
export const TOAST_STAGGER_MS = 220;
|
|
44
|
+
|
|
45
|
+
/** What turns a drag into a dismissal: 40% of the toast's own extent, or a fling over 800pt/s. */
|
|
46
|
+
export const TOAST_SWIPE = {
|
|
47
|
+
distanceRatio: 0.4,
|
|
48
|
+
velocity: 800,
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
/** The furthest a toast follows a drag toward the centre, in points. */
|
|
52
|
+
export const TOAST_RUBBER_BAND = 24;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Styling for every part of a toast, and for the viewport that stacks them.
|
|
56
|
+
*
|
|
57
|
+
* The card is a `popover` surface with the card corner and a hairline — no
|
|
58
|
+
* shadow, like every surface here; the hairline is its edge over the app.
|
|
59
|
+
* `max-w-[560px]` keeps it a toast on a tablet, centred by the `item` slot
|
|
60
|
+
* that positions it.
|
|
61
|
+
*
|
|
62
|
+
* `items-start`, with the indicator as tall as the title's line, so the glyph
|
|
63
|
+
* stays level with the first line however far the description wraps; the
|
|
64
|
+
* action and the close control centre themselves on the whole card.
|
|
65
|
+
*
|
|
66
|
+
* The title takes Alert's foreground token for its status — written out, not
|
|
67
|
+
* built from the map, because Tailwind's scanner is static. A test pins each
|
|
68
|
+
* class to `ALERT_FOREGROUND_TOKEN`. The root holds no `text-*` (rule 1).
|
|
69
|
+
*
|
|
70
|
+
* Free of React Native imports so it stays unit-testable.
|
|
71
|
+
*/
|
|
72
|
+
export const toastVariants = tv({
|
|
73
|
+
slots: {
|
|
74
|
+
viewport: "absolute inset-0",
|
|
75
|
+
stack: "absolute inset-0",
|
|
76
|
+
item: "absolute inset-x-0 items-center px-3",
|
|
77
|
+
root: "w-full max-w-[560px] flex-row items-start gap-3 rounded-lg border border-border bg-popover px-4 py-3",
|
|
78
|
+
indicator: "h-5 items-center justify-center",
|
|
79
|
+
content: "min-w-0 flex-1 gap-0.5",
|
|
80
|
+
title: "font-semibold text-sm",
|
|
81
|
+
description: "text-muted-foreground text-sm",
|
|
82
|
+
action: "-my-1 h-8 items-center justify-center self-center rounded-md px-2",
|
|
83
|
+
actionLabel: "font-semibold text-primary text-sm",
|
|
84
|
+
close: "size-5 items-center justify-center self-center rounded-full",
|
|
85
|
+
/** Edge length an `Icon` composed into the toast inherits. */
|
|
86
|
+
icon: "size-icon-md",
|
|
87
|
+
},
|
|
88
|
+
variants: {
|
|
89
|
+
status: {
|
|
90
|
+
default: { title: "text-foreground" },
|
|
91
|
+
info: { title: "text-info-soft-foreground" },
|
|
92
|
+
success: { title: "text-success-soft-foreground" },
|
|
93
|
+
warning: { title: "text-warning-soft-foreground" },
|
|
94
|
+
destructive: { title: "text-destructive-soft-foreground" },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
defaultVariants: {
|
|
98
|
+
status: "default",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export type ToastVariantProps = VariantProps<typeof toastVariants>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* How long a toast stays, in ms; `0` stays until hidden.
|
|
106
|
+
*
|
|
107
|
+
* An explicit, sane `duration` wins. Without one a loading toast stays until
|
|
108
|
+
* it is updated, one with an action gets 6000 and anything else 4000. Under a
|
|
109
|
+
* screen reader a timed toast is stretched to at least 10000.
|
|
110
|
+
*/
|
|
111
|
+
export function resolveToastDuration(
|
|
112
|
+
options: { duration?: number; hasAction?: boolean; isLoading?: boolean },
|
|
113
|
+
{ isScreenReaderEnabled }: { isScreenReaderEnabled: boolean }
|
|
114
|
+
): number {
|
|
115
|
+
const { duration } = options;
|
|
116
|
+
let resolved: number;
|
|
117
|
+
if (duration !== undefined && Number.isFinite(duration) && duration >= 0) resolved = duration;
|
|
118
|
+
else if (duration === undefined && options.isLoading) resolved = 0;
|
|
119
|
+
else resolved = options.hasAction ? TOAST_DURATION.withAction : TOAST_DURATION.default;
|
|
120
|
+
|
|
121
|
+
if (resolved === 0) return 0;
|
|
122
|
+
return isScreenReaderEnabled ? Math.max(resolved, TOAST_DURATION.screenReaderMinimum) : resolved;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** What `resolveToastStack` reads from each toast. */
|
|
126
|
+
export type ToastStackInput = { id: string; placement: Placement; isExiting: boolean };
|
|
127
|
+
|
|
128
|
+
/** One toast's place in its stack. */
|
|
129
|
+
export type ToastStackEntry = {
|
|
130
|
+
id: string;
|
|
131
|
+
/** 0 is the front. An exiting toast keeps the depth it is leaving from. */
|
|
132
|
+
depth: number;
|
|
133
|
+
/** Drawn. A toast past the third is queued until one in front leaves. */
|
|
134
|
+
isVisible: boolean;
|
|
135
|
+
isExiting: boolean;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* One placement's stack, newest first.
|
|
140
|
+
*
|
|
141
|
+
* Depth counts only the toasts that are staying, so the moment one is hidden
|
|
142
|
+
* the toasts behind it move up and the next queued toast is drawn — while the
|
|
143
|
+
* hidden one is still animating out, at the depth it left. A toast that was
|
|
144
|
+
* queued when it was hidden was never drawn and is not drawn leaving.
|
|
145
|
+
*/
|
|
146
|
+
export function resolveToastStack(items: readonly ToastStackInput[], placement: Placement): ToastStackEntry[] {
|
|
147
|
+
const entries: ToastStackEntry[] = [];
|
|
148
|
+
let staying = 0;
|
|
149
|
+
for (let index = items.length - 1; index >= 0; index--) {
|
|
150
|
+
const item = items[index] as ToastStackInput;
|
|
151
|
+
if (item.placement !== placement) continue;
|
|
152
|
+
const isVisible = staying < TOAST_STACK.maxVisible;
|
|
153
|
+
entries.push({ id: item.id, depth: staying, isVisible, isExiting: item.isExiting });
|
|
154
|
+
if (!item.isExiting) staying += 1;
|
|
155
|
+
}
|
|
156
|
+
return entries;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The transform a toast at `depth` takes: further toward its entry edge,
|
|
161
|
+
* smaller and fainter. Linear in `depth`, so an animated depth moves smoothly
|
|
162
|
+
* between steps.
|
|
163
|
+
*/
|
|
164
|
+
export function resolveToastDepthStyle(
|
|
165
|
+
depth: number,
|
|
166
|
+
placement: Placement
|
|
167
|
+
): { translateY: number; scale: number; opacity: number } {
|
|
168
|
+
"worklet";
|
|
169
|
+
const towardEdge = placement === "bottom" ? 1 : -1;
|
|
170
|
+
return {
|
|
171
|
+
translateY: towardEdge * TOAST_STACK.depthOffset * depth,
|
|
172
|
+
scale: 1 - TOAST_STACK.depthScale * depth,
|
|
173
|
+
opacity: 1 - TOAST_STACK.depthOpacity * depth,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* How tall a toast is drawn at `depth`, or `null` to leave it at its own height.
|
|
179
|
+
*
|
|
180
|
+
* A toast behind takes the front toast's height. Scaled about the entry edge,
|
|
181
|
+
* a taller one would otherwise rise past the front card and show its title
|
|
182
|
+
* over it. Between depth 0 and 1 the height eases from the front's to its own,
|
|
183
|
+
* so a toast moving up grows into place rather than jumping.
|
|
184
|
+
*/
|
|
185
|
+
export function resolveToastStackedHeight({
|
|
186
|
+
depth,
|
|
187
|
+
natural,
|
|
188
|
+
front,
|
|
189
|
+
}: {
|
|
190
|
+
depth: number;
|
|
191
|
+
natural: number;
|
|
192
|
+
front: number;
|
|
193
|
+
}): number | null {
|
|
194
|
+
"worklet";
|
|
195
|
+
if (natural <= 0 || front <= 0) return null;
|
|
196
|
+
const t = Math.min(1, Math.max(0, depth));
|
|
197
|
+
return natural + (front - natural) * t;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* How long a toast waits before entering, so several shown in one tick
|
|
202
|
+
* arrive one after another. Counted from when it was shown, so a toast that
|
|
203
|
+
* sat in the queue enters as soon as it is drawn.
|
|
204
|
+
*/
|
|
205
|
+
export function resolveToastEnterDelay({
|
|
206
|
+
createdAt,
|
|
207
|
+
batchIndex,
|
|
208
|
+
now,
|
|
209
|
+
}: {
|
|
210
|
+
createdAt: number;
|
|
211
|
+
batchIndex: number;
|
|
212
|
+
now: number;
|
|
213
|
+
}): number {
|
|
214
|
+
return Math.max(0, createdAt + batchIndex * TOAST_STAGGER_MS - now);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function rubberBand(distance: number): number {
|
|
218
|
+
"worklet";
|
|
219
|
+
const magnitude = Math.abs(distance);
|
|
220
|
+
return Math.sign(distance) * ((magnitude * TOAST_RUBBER_BAND) / (magnitude + TOAST_RUBBER_BAND));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
type ToastDragInput = { placement: Placement; translationX: number; translationY: number };
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Where a dragged toast is drawn. Sideways and toward its entry edge it
|
|
227
|
+
* follows the finger; toward the centre it resists and never goes further than
|
|
228
|
+
* `TOAST_RUBBER_BAND`, which says "not this way" without a word.
|
|
229
|
+
*/
|
|
230
|
+
export function resolveToastDrag({ placement, translationX, translationY }: ToastDragInput): {
|
|
231
|
+
x: number;
|
|
232
|
+
y: number;
|
|
233
|
+
} {
|
|
234
|
+
"worklet";
|
|
235
|
+
const towardEdge = placement === "bottom" ? 1 : -1;
|
|
236
|
+
const y = translationY * towardEdge >= 0 ? translationY : rubberBand(translationY);
|
|
237
|
+
return { x: translationX, y };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export type ToastRelease = { kind: "settle" } | { kind: "dismiss"; axis: "x" | "y"; direction: 1 | -1 };
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* What a released drag does.
|
|
244
|
+
*
|
|
245
|
+
* Sideways past 40% of the toast's width, or toward its entry edge past 40%
|
|
246
|
+
* of its height, dismisses — and so does a fling over 800pt/s either way.
|
|
247
|
+
* Toward the centre never dismisses: that is not where a toast goes. When both
|
|
248
|
+
* axes qualify, the one the finger travelled further along wins.
|
|
249
|
+
*/
|
|
250
|
+
export function resolveToastRelease({
|
|
251
|
+
placement,
|
|
252
|
+
translationX,
|
|
253
|
+
translationY,
|
|
254
|
+
velocityX,
|
|
255
|
+
velocityY,
|
|
256
|
+
width,
|
|
257
|
+
height,
|
|
258
|
+
}: ToastDragInput & { velocityX: number; velocityY: number; width: number; height: number }): ToastRelease {
|
|
259
|
+
"worklet";
|
|
260
|
+
const towardEdge = placement === "bottom" ? 1 : -1;
|
|
261
|
+
|
|
262
|
+
let sideways: ToastRelease = { kind: "settle" };
|
|
263
|
+
if (Math.abs(velocityX) > TOAST_SWIPE.velocity) {
|
|
264
|
+
sideways = { kind: "dismiss", axis: "x", direction: velocityX > 0 ? 1 : -1 };
|
|
265
|
+
} else if (Math.abs(translationX) > width * TOAST_SWIPE.distanceRatio) {
|
|
266
|
+
sideways = { kind: "dismiss", axis: "x", direction: translationX > 0 ? 1 : -1 };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const travel = translationY * towardEdge;
|
|
270
|
+
const fling = velocityY * towardEdge;
|
|
271
|
+
const isEdgeward = travel >= 0 && (fling > TOAST_SWIPE.velocity || travel > height * TOAST_SWIPE.distanceRatio);
|
|
272
|
+
const edgeward: ToastRelease = isEdgeward
|
|
273
|
+
? { kind: "dismiss", axis: "y", direction: towardEdge }
|
|
274
|
+
: { kind: "settle" };
|
|
275
|
+
|
|
276
|
+
if (sideways.kind === "dismiss" && edgeward.kind === "dismiss") {
|
|
277
|
+
return Math.abs(translationX) >= Math.abs(translationY) ? sideways : edgeward;
|
|
278
|
+
}
|
|
279
|
+
return sideways.kind === "dismiss" ? sideways : edgeward;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The haptic a toast plays as it appears: the caller's, else the status's notification, else none. */
|
|
283
|
+
export function resolveToastHaptic(status: Status, haptic: HapticFeedback | false | undefined): HapticFeedback | false {
|
|
284
|
+
if (haptic !== undefined) return haptic;
|
|
285
|
+
if (status === "success") return "success";
|
|
286
|
+
if (status === "warning") return "warning";
|
|
287
|
+
if (status === "destructive") return "error";
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** What a screen reader says as a toast appears: the title, then the description. */
|
|
292
|
+
export function resolveToastAnnouncement({ title, description }: { title: string; description?: string }): string {
|
|
293
|
+
return description ? `${title}. ${description}` : title;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Whether the announcement cuts in over whatever is being read — a warning or a failure does. */
|
|
297
|
+
export function resolveToastInterrupts(status: Status): boolean {
|
|
298
|
+
return status === "destructive" || status === "warning";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** A failure is an `alert`; anything else is a plain group. */
|
|
302
|
+
export function resolveToastRole(status: Status): "alert" | "none" {
|
|
303
|
+
return status === "destructive" ? "alert" : "none";
|
|
304
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# ToggleButton
|
|
2
|
+
|
|
3
|
+
A button that stays pressed — on its own, or as one of a group that owns the
|
|
4
|
+
selection. Compound root plus `ToggleButton.Label` and `ToggleButton.Group`.
|
|
5
|
+
|
|
6
|
+
`import { ToggleButton } from "@delacour/react-native-ui/toggle-button";`
|
|
7
|
+
|
|
8
|
+
## Files
|
|
9
|
+
|
|
10
|
+
| File | What it holds |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `index.ts` | → `@delacour/react-native-ui/toggle-button` |
|
|
13
|
+
| `toggle-button.tsx` | Root + the `Object.assign` compound surface |
|
|
14
|
+
| `toggle-button-label.tsx` | `ToggleButton.Label` |
|
|
15
|
+
| `toggle-button-group.tsx` | `ToggleButton.Group`, which owns the selection |
|
|
16
|
+
| `toggle-button.context.tsx` | `ToggleButtonContext` and `ToggleButtonGroupContext`, with their hooks |
|
|
17
|
+
| `toggle-button.variants.ts` | The appearance table, the group's `tv()` and three pure resolvers |
|
|
18
|
+
| `toggle-button.variants.test.ts` | |
|
|
19
|
+
|
|
20
|
+
There is no `toggle-button.types.ts`: every prop type here has exactly one
|
|
21
|
+
consumer, so each lives beside the component it types. See
|
|
22
|
+
[Compound component layout](../../../AGENTS.md#compound-component-layout).
|
|
23
|
+
|
|
24
|
+
## Design
|
|
25
|
+
|
|
26
|
+
- **Variants**: `default`, `outline`, `ghost`. **Sizes**: the button's own —
|
|
27
|
+
`sm`, `md`, `lg` and the square `icon-sm`, `icon-md`, `icon-lg`. **Selection
|
|
28
|
+
modes**, on the group: `multiple` (default) and `single`. **Layouts**, on the
|
|
29
|
+
group: `attached` (default) and `detached`. **Orientations**, on the group:
|
|
30
|
+
`horizontal` and `vertical`.
|
|
31
|
+
- **A toggle is a `Button`, and owns no paint.** Each state names a button
|
|
32
|
+
variant — `TOGGLE_BUTTON_APPEARANCE` — and the toggle renders a `Button` with
|
|
33
|
+
whichever one applies. The fill, the border, the label colour, the colour a
|
|
34
|
+
composed `Icon` inherits, the corner, the loading spinner and the press
|
|
35
|
+
feedback all come from the button, so there is no second table of colours
|
|
36
|
+
here to drift from it. Every button variant draws the same `border` box, so
|
|
37
|
+
switching state never resizes the toggle. A test asserts every entry names a
|
|
38
|
+
real button variant and that `on` never equals `off`.
|
|
39
|
+
- **No state is destructive.** A pressed toggle is a choice, not a warning; the
|
|
40
|
+
appearance table never names `destructive` or `destructive-soft`, and a test
|
|
41
|
+
holds it there.
|
|
42
|
+
- **A square is a size, not a flag.** `size="icon-md"` is the button's own
|
|
43
|
+
square step — there is no `isIconOnly`. Always pair one with an
|
|
44
|
+
`accessibilityLabel`.
|
|
45
|
+
- **The state is announced, not only drawn.** `resolveToggleButtonAccessibility`
|
|
46
|
+
makes a standalone toggle, or a member of a `multiple` group, a
|
|
47
|
+
`togglebutton` with `accessibilityState.checked` — read out as on or off. A
|
|
48
|
+
member of a `single` group is a `radio` with `accessibilityState.selected`,
|
|
49
|
+
and that group is a `radiogroup`: announcing an either-or option as a toggle
|
|
50
|
+
promises that pressing it again turns it off, which a group with
|
|
51
|
+
`isSelectionRequired` refuses to do. A `multiple` group has no role, the same
|
|
52
|
+
call `Button.Group` makes.
|
|
53
|
+
- **The toggle keeps the role, the state and the press handler.**
|
|
54
|
+
`accessibilityRole`, `accessibilityState` and `variant` are `Omit`ted from its
|
|
55
|
+
props, and the three it computes are set *after* the caller's spread, so
|
|
56
|
+
nothing passed in can make the announced state and the drawn state disagree.
|
|
57
|
+
A caller's `onPress` still runs — first, before the state moves, so a
|
|
58
|
+
handler reading the state it closed over sees what the toggle was when
|
|
59
|
+
pressed.
|
|
60
|
+
- **A toggle works with or without a group.** On its own it is `isSelected` +
|
|
61
|
+
`onSelected`, or uncontrolled from `defaultSelected`, through
|
|
62
|
+
`useControllableState`. Inside a `ToggleButton.Group` its `value` identifies
|
|
63
|
+
it, the group owns the state, and its own `isSelected` is ignored. The hook is
|
|
64
|
+
still called in a group — a hook may not sit behind a branch — and its state
|
|
65
|
+
is never read. A grouped toggle with no `value` can never be selected, and
|
|
66
|
+
warns in development.
|
|
67
|
+
- **Children may be a function of the state.** `{({ isSelected }) => …}` is
|
|
68
|
+
for content that changes with the toggle — a label that reads *Saved* once
|
|
69
|
+
pressed, a glyph that swaps. `useToggleButton()` answers the same question
|
|
70
|
+
for a custom child that would rather read context.
|
|
71
|
+
- **`ToggleButton.Label` is a `Button.Label` with a part check.** The colour is
|
|
72
|
+
already right — the toggle hands its button the variant for the state it is
|
|
73
|
+
in — so the part adds nothing but an error that names the toggle when it is
|
|
74
|
+
rendered outside one.
|
|
75
|
+
|
|
76
|
+
## ToggleButton.Group
|
|
77
|
+
|
|
78
|
+
- **State is one array of the members' `value`s**: `selected` + `onSelected`,
|
|
79
|
+
or `defaultSelected` alone. `resolveToggleSelection` is the whole decision
|
|
80
|
+
and is pure: `multiple` adds or removes the one pressed, keeping press order;
|
|
81
|
+
`single` replaces the selection with it; a re-press in `single` clears it.
|
|
82
|
+
- **`isSelectionRequired` keeps an answer on the board.** The last selection
|
|
83
|
+
cannot be cleared, in either mode. A segmented choice like text alignment
|
|
84
|
+
always has one.
|
|
85
|
+
- **A press that changes nothing notifies nobody.** The resolver returns
|
|
86
|
+
`null` rather than the same list back, so `onSelected` is never called with a
|
|
87
|
+
value the caller already holds — the rule `Radio.Group` keeps for a re-press
|
|
88
|
+
of the current option.
|
|
89
|
+
- **A controlled `single` group handed several values collapses** to the one
|
|
90
|
+
pressed rather than carrying the others along.
|
|
91
|
+
- **`attached` is a `Button.Group`, not a copy of one.** The toggles join into
|
|
92
|
+
one run with the button's own corner squaring, hairline seam and fade-not-scale
|
|
93
|
+
feedback, and `Button.Group.Separator` works between two of them. Restating
|
|
94
|
+
any of that here is how the two runs would stop matching. An attached group
|
|
95
|
+
therefore owns the size *step* outright — controls of different heights do not
|
|
96
|
+
join — while a square member stays square (`resolveGroupedButtonSize`).
|
|
97
|
+
- **`detached` is a wrapping row, or a column, spaced by a gap** — the one thing
|
|
98
|
+
an attached run must never have. It is the shape for a row of filters. The
|
|
99
|
+
classes are compound cells on `layout` × `orientation`, and an attached group
|
|
100
|
+
emits none of them.
|
|
101
|
+
- **The axes are defaults, not overrides.** `variant`, `size`, `isDisabled` and
|
|
102
|
+
`haptic` are published raw, so a member's own value wins and one option can
|
|
103
|
+
disable itself inside a group that did not — `own ?? group ?? default`, the
|
|
104
|
+
`Checkbox.Group` ladder.
|
|
105
|
+
- **The group paints nothing.** A disabled group publishes `isDisabled` and each
|
|
106
|
+
member fades itself; a group fading too would compound the two.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export { ToggleButton, type ToggleButtonProps, type ToggleButtonRenderState } from "./toggle-button";
|
|
2
|
+
export {
|
|
3
|
+
type ToggleButtonContextValue,
|
|
4
|
+
type ToggleButtonGroupContextValue,
|
|
5
|
+
ToggleButtonGroupProvider,
|
|
6
|
+
ToggleButtonProvider,
|
|
7
|
+
useToggleButton,
|
|
8
|
+
useToggleButtonContext,
|
|
9
|
+
useToggleButtonGroup,
|
|
10
|
+
useToggleButtonGroupContext,
|
|
11
|
+
} from "./toggle-button.context";
|
|
12
|
+
export {
|
|
13
|
+
resolveToggleButtonAccessibility,
|
|
14
|
+
resolveToggleButtonVariant,
|
|
15
|
+
resolveToggleSelection,
|
|
16
|
+
TOGGLE_BUTTON_APPEARANCE,
|
|
17
|
+
TOGGLE_BUTTON_GROUP_LAYOUTS,
|
|
18
|
+
TOGGLE_BUTTON_GROUP_ORIENTATIONS,
|
|
19
|
+
TOGGLE_BUTTON_SELECTION_MODES,
|
|
20
|
+
TOGGLE_BUTTON_VARIANTS,
|
|
21
|
+
type ToggleButtonAccessibility,
|
|
22
|
+
type ToggleButtonGroupLayout,
|
|
23
|
+
type ToggleButtonGroupOrientation,
|
|
24
|
+
type ToggleButtonSelectionMode,
|
|
25
|
+
type ToggleButtonVariant,
|
|
26
|
+
type ToggleButtonVariantProps,
|
|
27
|
+
toggleButtonVariants,
|
|
28
|
+
} from "./toggle-button.variants";
|
|
29
|
+
export type { ToggleButtonGroupProps } from "./toggle-button-group";
|
|
30
|
+
export type { ToggleButtonLabelProps } from "./toggle-button-label";
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode, useCallback, useMemo } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import { useControllableState } from "../../hooks/use-controllable-state";
|
|
4
|
+
import { Button } from "../button";
|
|
5
|
+
import type { ButtonSize } from "../button/button.variants";
|
|
6
|
+
import type { HapticFeedback, PressableFeedback } from "../pressable";
|
|
7
|
+
import { type ToggleButtonGroupContextValue, ToggleButtonGroupProvider } from "./toggle-button.context";
|
|
8
|
+
import {
|
|
9
|
+
resolveToggleSelection,
|
|
10
|
+
type ToggleButtonGroupLayout,
|
|
11
|
+
type ToggleButtonGroupOrientation,
|
|
12
|
+
type ToggleButtonSelectionMode,
|
|
13
|
+
type ToggleButtonVariant,
|
|
14
|
+
toggleButtonVariants,
|
|
15
|
+
} from "./toggle-button.variants";
|
|
16
|
+
|
|
17
|
+
export type ToggleButtonGroupProps = Omit<ViewProps, "children"> & {
|
|
18
|
+
/** `multiple` marks independently; `single` is an either-or choice. Default `multiple`. */
|
|
19
|
+
selectionMode?: ToggleButtonSelectionMode;
|
|
20
|
+
/** The `value` of every selected toggle. Controlled. */
|
|
21
|
+
selected?: string[];
|
|
22
|
+
/** The values selected to begin with, while uncontrolled. */
|
|
23
|
+
defaultSelected?: string[];
|
|
24
|
+
/** Called with the whole new list. Never called for a press that changes nothing. */
|
|
25
|
+
onSelected?: (selected: string[]) => void;
|
|
26
|
+
/** Refuse to clear the last selection — a re-press of it does nothing. */
|
|
27
|
+
isSelectionRequired?: boolean;
|
|
28
|
+
/** `attached` joins the toggles into one run; `detached` spaces them apart. Default `attached`. */
|
|
29
|
+
layout?: ToggleButtonGroupLayout;
|
|
30
|
+
/** Which way the toggles run. Default `horizontal`. */
|
|
31
|
+
orientation?: ToggleButtonGroupOrientation;
|
|
32
|
+
/** A default every toggle takes unless it names its own. */
|
|
33
|
+
variant?: ToggleButtonVariant;
|
|
34
|
+
/** Shared by every toggle. An attached group owns the step outright. */
|
|
35
|
+
size?: ButtonSize;
|
|
36
|
+
/** Disables every toggle that does not say otherwise. */
|
|
37
|
+
isDisabled?: boolean;
|
|
38
|
+
/** Haptic for every toggle that does not name its own. */
|
|
39
|
+
haptic?: false | HapticFeedback;
|
|
40
|
+
/** Press treatment for every toggle. Unset, an attached toggle fades rather than scaling. */
|
|
41
|
+
feedback?: PressableFeedback;
|
|
42
|
+
className?: string;
|
|
43
|
+
children?: ReactNode;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function ToggleButtonGroupRoot({
|
|
47
|
+
selectionMode = "multiple",
|
|
48
|
+
selected,
|
|
49
|
+
defaultSelected = EMPTY,
|
|
50
|
+
onSelected,
|
|
51
|
+
isSelectionRequired = false,
|
|
52
|
+
layout = "attached",
|
|
53
|
+
orientation = "horizontal",
|
|
54
|
+
variant,
|
|
55
|
+
size,
|
|
56
|
+
isDisabled,
|
|
57
|
+
haptic,
|
|
58
|
+
feedback,
|
|
59
|
+
className,
|
|
60
|
+
children,
|
|
61
|
+
...props
|
|
62
|
+
}: ToggleButtonGroupProps): ReactElement {
|
|
63
|
+
const [values, setValues] = useControllableState<string[]>({
|
|
64
|
+
defaultValue: defaultSelected,
|
|
65
|
+
onChange: onSelected,
|
|
66
|
+
value: selected,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const toggle = useCallback(
|
|
70
|
+
(value: string) => {
|
|
71
|
+
const next = resolveToggleSelection({ current: values, isSelectionRequired, selectionMode, value });
|
|
72
|
+
if (next !== null) setValues(next);
|
|
73
|
+
},
|
|
74
|
+
[isSelectionRequired, selectionMode, setValues, values]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const context = useMemo<ToggleButtonGroupContextValue>(
|
|
78
|
+
() => ({ haptic, isDisabled, layout, orientation, selected: values, selectionMode, size, toggle, variant }),
|
|
79
|
+
[haptic, isDisabled, layout, orientation, selectionMode, size, toggle, values, variant]
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// A single-choice group is a radio group, and says so. A set of independent
|
|
83
|
+
// marks is a container with no action of its own, so it announces nothing —
|
|
84
|
+
// the same call `Button.Group` makes.
|
|
85
|
+
const accessibilityRole = selectionMode === "single" ? "radiogroup" : undefined;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<ToggleButtonGroupProvider value={context}>
|
|
89
|
+
{layout === "attached" ? (
|
|
90
|
+
<Button.Group
|
|
91
|
+
accessibilityRole={accessibilityRole}
|
|
92
|
+
className={className}
|
|
93
|
+
feedback={feedback}
|
|
94
|
+
orientation={orientation}
|
|
95
|
+
size={size}
|
|
96
|
+
{...props}
|
|
97
|
+
>
|
|
98
|
+
{children}
|
|
99
|
+
</Button.Group>
|
|
100
|
+
) : (
|
|
101
|
+
<View
|
|
102
|
+
accessibilityRole={accessibilityRole}
|
|
103
|
+
className={toggleButtonVariants({ layout, orientation }).group({ className })}
|
|
104
|
+
{...props}
|
|
105
|
+
>
|
|
106
|
+
{children}
|
|
107
|
+
</View>
|
|
108
|
+
)}
|
|
109
|
+
</ToggleButtonGroupProvider>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A stable empty list, so an uncontrolled group does not seed its state from a
|
|
115
|
+
* fresh array on every render.
|
|
116
|
+
*/
|
|
117
|
+
const EMPTY: string[] = [];
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Toggles sharing one selection.
|
|
121
|
+
*
|
|
122
|
+
* State is one array of the members' `value`s, controlled with `selected` and
|
|
123
|
+
* `onSelected` or held by the group from `defaultSelected`. `selectionMode`
|
|
124
|
+
* decides what a press does: `multiple` adds or removes the one pressed, while
|
|
125
|
+
* `single` replaces the selection with it. `isSelectionRequired` keeps the last
|
|
126
|
+
* one from being cleared — a segmented choice always has an answer.
|
|
127
|
+
*
|
|
128
|
+
* `attached` — the default — renders a `Button.Group`, so the toggles join into
|
|
129
|
+
* one run with shared corners and a hairline seam, and `Button.Group.Separator`
|
|
130
|
+
* works between them. `detached` spaces them apart and wraps, for a row of
|
|
131
|
+
* filters.
|
|
132
|
+
*
|
|
133
|
+
* `variant`, `size`, `isDisabled` and `haptic` are defaults a member may
|
|
134
|
+
* override, so one option can disable itself. An attached group owns the size
|
|
135
|
+
* step outright, because controls of different heights do not join; a square
|
|
136
|
+
* member stays square.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* <ToggleButton.Group defaultSelected={["bold"]} variant="ghost">
|
|
140
|
+
* <ToggleButton accessibilityLabel="Bold" size="icon-md" value="bold">
|
|
141
|
+
* <Icon icon={IconBold} />
|
|
142
|
+
* </ToggleButton>
|
|
143
|
+
* <ToggleButton accessibilityLabel="Italic" size="icon-md" value="italic">
|
|
144
|
+
* <Icon icon={IconItalic} />
|
|
145
|
+
* </ToggleButton>
|
|
146
|
+
* </ToggleButton.Group>
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* <ToggleButton.Group layout="detached" onSelected={setFilters} selected={filters} size="sm" variant="outline">
|
|
150
|
+
* <ToggleButton value="open">Open</ToggleButton>
|
|
151
|
+
* <ToggleButton value="closed">Closed</ToggleButton>
|
|
152
|
+
* </ToggleButton.Group>
|
|
153
|
+
*/
|
|
154
|
+
export const ToggleButtonGroup = Object.assign(ToggleButtonGroupRoot, {
|
|
155
|
+
displayName: "DelacourUI.ToggleButton.Group",
|
|
156
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Button, type ButtonLabelProps } from "../button";
|
|
3
|
+
import { useToggleButtonPart } from "./toggle-button.context";
|
|
4
|
+
|
|
5
|
+
export type ToggleButtonLabelProps = ButtonLabelProps;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The toggle's text.
|
|
9
|
+
*
|
|
10
|
+
* A `Button.Label`, and nothing more: the toggle hands its button the variant
|
|
11
|
+
* for the state it is in, so the label already reads the right colour off the
|
|
12
|
+
* button's context. A part of its own anyway, so a label placed outside a toggle
|
|
13
|
+
* fails with a message that names the toggle rather than the button.
|
|
14
|
+
*/
|
|
15
|
+
export function ToggleButtonLabel(props: ToggleButtonLabelProps): ReactElement {
|
|
16
|
+
useToggleButtonPart("ToggleButton.Label");
|
|
17
|
+
return <Button.Label {...props} />;
|
|
18
|
+
}
|
|
19
|
+
ToggleButtonLabel.displayName = "DelacourUI.ToggleButton.Label";
|