@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,274 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { ICON_SIZES } from "../icon/icon.variants";
|
|
3
|
+
import {
|
|
4
|
+
FAB_DIAL_STAGGER,
|
|
5
|
+
FAB_FOREGROUND_TOKEN,
|
|
6
|
+
FAB_ICON_SIZE,
|
|
7
|
+
FAB_PLACEMENTS,
|
|
8
|
+
FAB_SIZES,
|
|
9
|
+
FAB_VARIANTS,
|
|
10
|
+
fabVariants,
|
|
11
|
+
resolveDialProgress,
|
|
12
|
+
resolveFabIconSize,
|
|
13
|
+
resolveFabPlacementStyle,
|
|
14
|
+
resolveLabelSide,
|
|
15
|
+
} from "./fab.variants";
|
|
16
|
+
|
|
17
|
+
describe("resolveDialProgress", () => {
|
|
18
|
+
const count = 4;
|
|
19
|
+
const stagger = FAB_DIAL_STAGGER;
|
|
20
|
+
|
|
21
|
+
test("every action is fully closed at 0 and fully open at 1", () => {
|
|
22
|
+
for (let index = 0; index < count; index++) {
|
|
23
|
+
expect(resolveDialProgress({ open: 0, index, count, stagger })).toBe(0);
|
|
24
|
+
expect(resolveDialProgress({ open: 1, index, count, stagger })).toBe(1);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("rises monotonically with the spring, for every action", () => {
|
|
29
|
+
for (let index = 0; index < count; index++) {
|
|
30
|
+
let previous = -1;
|
|
31
|
+
for (let step = 0; step <= 100; step++) {
|
|
32
|
+
const value = resolveDialProgress({ open: step / 100, index, count, stagger });
|
|
33
|
+
expect(value).toBeGreaterThanOrEqual(previous);
|
|
34
|
+
previous = value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("stays within 0..1 when the spring overshoots either end", () => {
|
|
40
|
+
for (let index = 0; index < count; index++) {
|
|
41
|
+
expect(resolveDialProgress({ open: 1.15, index, count, stagger })).toBe(1);
|
|
42
|
+
expect(resolveDialProgress({ open: -0.1, index, count, stagger })).toBe(0);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("staggers: the action nearest the trigger leads the one beyond it", () => {
|
|
47
|
+
const nearest = resolveDialProgress({ open: 0.3, index: 0, count, stagger });
|
|
48
|
+
const farthest = resolveDialProgress({ open: 0.3, index: count - 1, count, stagger });
|
|
49
|
+
expect(nearest).toBeGreaterThan(farthest);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("the last action has not started before its window opens", () => {
|
|
53
|
+
const start = (count - 1) * stagger;
|
|
54
|
+
expect(resolveDialProgress({ open: start, index: count - 1, count, stagger })).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("a lone action follows the spring exactly", () => {
|
|
58
|
+
for (const open of [0, 0.25, 0.5, 0.75, 1]) {
|
|
59
|
+
expect(resolveDialProgress({ open, index: 0, count: 1, stagger })).toBeCloseTo(open);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("a stagger of zero puts every action on the spring", () => {
|
|
64
|
+
for (let index = 0; index < count; index++) {
|
|
65
|
+
expect(resolveDialProgress({ open: 0.4, index, count, stagger: 0 })).toBeCloseTo(0.4);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// A stagger wide enough to leave the last action no window would divide by
|
|
70
|
+
// zero or less, and a NaN written into a style freezes the action in place.
|
|
71
|
+
test("never yields NaN, even when the stagger eats the whole window", () => {
|
|
72
|
+
for (let index = 0; index < 20; index++) {
|
|
73
|
+
for (const open of [0, 0.5, 1]) {
|
|
74
|
+
const value = resolveDialProgress({ open, index, count: 20, stagger });
|
|
75
|
+
expect(Number.isNaN(value)).toBe(false);
|
|
76
|
+
expect(value).toBeGreaterThanOrEqual(0);
|
|
77
|
+
expect(value).toBeLessThanOrEqual(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("resolveFabPlacementStyle", () => {
|
|
84
|
+
test("pins bottom-end to the trailing edge, lifted by the offset and the inset", () => {
|
|
85
|
+
expect(resolveFabPlacementStyle({ placement: "bottom-end", offset: 16, insetBottom: 34 })).toEqual({
|
|
86
|
+
position: "absolute",
|
|
87
|
+
bottom: 50,
|
|
88
|
+
end: 16,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("pins bottom-start to the leading edge", () => {
|
|
93
|
+
expect(resolveFabPlacementStyle({ placement: "bottom-start", offset: 16, insetBottom: 0 })).toEqual({
|
|
94
|
+
position: "absolute",
|
|
95
|
+
bottom: 16,
|
|
96
|
+
start: 16,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Centred by spanning the row and centring inside it — `alignSelf` on an
|
|
101
|
+
// absolute child is read against the parent's cross axis, which is
|
|
102
|
+
// horizontal only in a column.
|
|
103
|
+
test("centres bottom-center by spanning the row", () => {
|
|
104
|
+
expect(resolveFabPlacementStyle({ placement: "bottom-center", offset: 24, insetBottom: 10 })).toEqual({
|
|
105
|
+
position: "absolute",
|
|
106
|
+
bottom: 34,
|
|
107
|
+
start: 0,
|
|
108
|
+
end: 0,
|
|
109
|
+
alignItems: "center",
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("uses logical edges only, so a right-to-left layout flips with no code", () => {
|
|
114
|
+
for (const placement of FAB_PLACEMENTS) {
|
|
115
|
+
const style = resolveFabPlacementStyle({ placement, offset: 16, insetBottom: 0 });
|
|
116
|
+
expect(style).not.toHaveProperty("left");
|
|
117
|
+
expect(style).not.toHaveProperty("right");
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("resolveFabIconSize", () => {
|
|
123
|
+
test("maps sm to icon-md and both larger sizes to icon-lg", () => {
|
|
124
|
+
expect(resolveFabIconSize("sm")).toBe("md");
|
|
125
|
+
expect(resolveFabIconSize("md")).toBe("lg");
|
|
126
|
+
expect(resolveFabIconSize("lg")).toBe("lg");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("the icon slot spells the same step as a class", () => {
|
|
130
|
+
for (const size of FAB_SIZES) {
|
|
131
|
+
expect(fabVariants({ size }).icon()).toBe(`size-icon-${resolveFabIconSize(size)}`);
|
|
132
|
+
}
|
|
133
|
+
expect(fabVariants({}).actionIcon()).toBe(`size-icon-${resolveFabIconSize("sm")}`);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("names a step on the shared icon scale for every size", () => {
|
|
137
|
+
for (const size of FAB_SIZES) {
|
|
138
|
+
expect(ICON_SIZES).toContain(resolveFabIconSize(size));
|
|
139
|
+
expect(FAB_ICON_SIZE[size]).toBe(resolveFabIconSize(size));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("resolveLabelSide", () => {
|
|
145
|
+
test("puts the label chip on the side facing the screen", () => {
|
|
146
|
+
expect(resolveLabelSide("bottom-end")).toBe("start");
|
|
147
|
+
expect(resolveLabelSide("bottom-start")).toBe("end");
|
|
148
|
+
expect(resolveLabelSide("bottom-center")).toBe("top");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("FAB_FOREGROUND_TOKEN", () => {
|
|
153
|
+
test("pairs every variant with the foreground of its own surface", () => {
|
|
154
|
+
expect(FAB_FOREGROUND_TOKEN).toEqual({
|
|
155
|
+
primary: "primary-foreground",
|
|
156
|
+
secondary: "secondary-foreground",
|
|
157
|
+
surface: "elevated-foreground",
|
|
158
|
+
destructive: "destructive-foreground",
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("fabVariants", () => {
|
|
164
|
+
const classes = (value: string) => value.split(/\s+/).filter(Boolean);
|
|
165
|
+
|
|
166
|
+
test("a round fab is a square footprint off the fab token, fully rounded", () => {
|
|
167
|
+
const sizes = { sm: "size-fab-sm", md: "size-fab-md", lg: "size-fab-lg" } as const;
|
|
168
|
+
for (const size of FAB_SIZES) {
|
|
169
|
+
const root = classes(fabVariants({ size }).root());
|
|
170
|
+
expect(root).toContain(sizes[size]);
|
|
171
|
+
expect(root).toContain("rounded-full");
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("an extended fab takes the height and pads its sides instead of fixing a width", () => {
|
|
176
|
+
const heights = { sm: "h-fab-sm", md: "h-fab-md", lg: "h-fab-lg" } as const;
|
|
177
|
+
for (const size of FAB_SIZES) {
|
|
178
|
+
const root = classes(fabVariants({ size, isExtended: true }).root());
|
|
179
|
+
expect(root).toContain(heights[size]);
|
|
180
|
+
expect(root).toContain("px-5");
|
|
181
|
+
expect(root).toContain("gap-2");
|
|
182
|
+
expect(root.some((name) => name.startsWith("size-"))).toBe(false);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("fills each variant from its own semantic surface", () => {
|
|
187
|
+
const fills = {
|
|
188
|
+
primary: "bg-primary",
|
|
189
|
+
secondary: "bg-secondary",
|
|
190
|
+
surface: "bg-elevated",
|
|
191
|
+
destructive: "bg-destructive",
|
|
192
|
+
} as const;
|
|
193
|
+
for (const variant of FAB_VARIANTS) {
|
|
194
|
+
expect(classes(fabVariants({ variant }).root())).toContain(fills[variant]);
|
|
195
|
+
}
|
|
196
|
+
expect(classes(fabVariants({ variant: "surface" }).root())).toContain("border-border");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("colours the label to match the variant's foreground token", () => {
|
|
200
|
+
for (const variant of FAB_VARIANTS) {
|
|
201
|
+
expect(classes(fabVariants({ variant }).label())).toContain(`text-${FAB_FOREGROUND_TOKEN[variant]}`);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Rule 1: a View does not cascade colour to its Text.
|
|
206
|
+
test("puts no text colour on the root", () => {
|
|
207
|
+
for (const variant of FAB_VARIANTS) {
|
|
208
|
+
const root = classes(fabVariants({ variant }).root());
|
|
209
|
+
expect(root.some((name) => /^text-(primary|secondary|elevated|destructive)/.test(name))).toBe(false);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("the label reads on the button's type scale", () => {
|
|
214
|
+
const label = classes(fabVariants({}).label());
|
|
215
|
+
expect(label).toContain("text-button-md");
|
|
216
|
+
expect(label).toContain("font-medium");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("casts a shadow — the one floating thing in the kit", () => {
|
|
220
|
+
expect(classes(fabVariants({}).root())).toContain("shadow-lg");
|
|
221
|
+
expect(classes(fabVariants({}).actionButton())).toContain("shadow-lg");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("fades when disabled", () => {
|
|
225
|
+
expect(classes(fabVariants({ isDisabled: true }).root())).toContain("opacity-50");
|
|
226
|
+
expect(classes(fabVariants({ isDisabled: false }).root())).not.toContain("opacity-50");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("the scrim covers its container in the overlay token", () => {
|
|
230
|
+
const scrim = classes(fabVariants({}).scrim());
|
|
231
|
+
expect(scrim).toContain("absolute");
|
|
232
|
+
expect(scrim).toContain("inset-0");
|
|
233
|
+
expect(scrim).toContain("bg-overlay");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("an action button is a small round surface", () => {
|
|
237
|
+
const button = classes(fabVariants({}).actionButton());
|
|
238
|
+
expect(button).toContain("size-fab-sm");
|
|
239
|
+
expect(button).toContain("rounded-full");
|
|
240
|
+
expect(button).toContain("bg-elevated");
|
|
241
|
+
expect(button).toContain("border-border");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("an action's label chip sits on the popover surface", () => {
|
|
245
|
+
const chip = classes(fabVariants({}).actionLabel());
|
|
246
|
+
for (const name of ["bg-popover", "rounded-md", "px-2", "py-1"]) expect(chip).toContain(name);
|
|
247
|
+
expect(classes(fabVariants({}).actionLabelText())).toContain("text-popover-foreground");
|
|
248
|
+
expect(classes(fabVariants({}).actionLabelText())).toContain("text-sm");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("the dial and each action are spaced by 12pt", () => {
|
|
252
|
+
expect(classes(fabVariants({}).dial())).toContain("gap-3");
|
|
253
|
+
expect(classes(fabVariants({}).action())).toContain("gap-3");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("an action's anchor matches the trigger's width, so the two share a centre line", () => {
|
|
257
|
+
const anchors = { sm: "w-fab-sm", md: "w-fab-md", lg: "w-fab-lg" } as const;
|
|
258
|
+
for (const size of FAB_SIZES) {
|
|
259
|
+
expect(classes(fabVariants({ size }).actionAnchor())).toContain(anchors[size]);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("lays an action out by the side its label sits on", () => {
|
|
264
|
+
expect(classes(fabVariants({ labelSide: "start" }).action())).toContain("flex-row");
|
|
265
|
+
expect(classes(fabVariants({ labelSide: "end" }).action())).toContain("flex-row-reverse");
|
|
266
|
+
expect(classes(fabVariants({ labelSide: "top" }).action())).toContain("flex-col");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("aligns the dial with the trigger's edge", () => {
|
|
270
|
+
expect(classes(fabVariants({ labelSide: "start" }).dial())).toContain("items-end");
|
|
271
|
+
expect(classes(fabVariants({ labelSide: "end" }).dial())).toContain("items-start");
|
|
272
|
+
expect(classes(fabVariants({ labelSide: "top" }).dial())).toContain("items-center");
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
import type { IconSize } from "../icon/icon.variants";
|
|
4
|
+
|
|
5
|
+
/** 44, 56 and 64pt — `--spacing-fab-*` in `tokens.css`. */
|
|
6
|
+
export const FAB_SIZES = ["sm", "md", "lg"] as const;
|
|
7
|
+
|
|
8
|
+
export const FAB_VARIANTS = ["primary", "secondary", "surface", "destructive"] as const;
|
|
9
|
+
|
|
10
|
+
/** Where a pinned fab sits along the bottom edge. Logical, so it flips under RTL. */
|
|
11
|
+
export const FAB_PLACEMENTS = ["bottom-start", "bottom-center", "bottom-end"] as const;
|
|
12
|
+
|
|
13
|
+
/** Which side of an action's button its label chip sits on. */
|
|
14
|
+
export const FAB_LABEL_SIDES = ["start", "end", "top"] as const;
|
|
15
|
+
|
|
16
|
+
export type FabSize = (typeof FAB_SIZES)[number];
|
|
17
|
+
export type FabVariant = (typeof FAB_VARIANTS)[number];
|
|
18
|
+
export type FabPlacement = (typeof FAB_PLACEMENTS)[number];
|
|
19
|
+
export type FabLabelSide = (typeof FAB_LABEL_SIDES)[number];
|
|
20
|
+
|
|
21
|
+
/** Distance from the screen's edges, before the safe-area inset. */
|
|
22
|
+
export const FAB_DEFAULT_OFFSET = 16;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* How far behind the one before it each action starts, as a fraction of the
|
|
26
|
+
* dial's spring. 0.08 keeps a four-action dial's last window at three quarters
|
|
27
|
+
* of the spring, long enough to read as a cascade rather than a pop.
|
|
28
|
+
*/
|
|
29
|
+
export const FAB_DIAL_STAGGER = 0.08;
|
|
30
|
+
|
|
31
|
+
/** The reduced-motion fade, and the glyph's turn under it, in ms. */
|
|
32
|
+
export const FAB_REDUCED_MOTION_MS = 150;
|
|
33
|
+
|
|
34
|
+
/** The glyph's turn at a fully open dial: a plus becomes a cross. */
|
|
35
|
+
export const FAB_OPEN_ROTATION_DEG = 45;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Theme token whose value colours a composed icon on each surface.
|
|
39
|
+
*
|
|
40
|
+
* Read through `useThemeColor` for an `Icon`'s `color`; the label reads the same
|
|
41
|
+
* token as a `text-*` class in {@link fabVariants}, and a test holds the two in
|
|
42
|
+
* step.
|
|
43
|
+
*/
|
|
44
|
+
export const FAB_FOREGROUND_TOKEN: Record<FabVariant, string> = {
|
|
45
|
+
primary: "primary-foreground",
|
|
46
|
+
secondary: "secondary-foreground",
|
|
47
|
+
surface: "elevated-foreground",
|
|
48
|
+
destructive: "destructive-foreground",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The step on the shared icon scale a fab's glyph is drawn at.
|
|
53
|
+
*
|
|
54
|
+
* Indexed rather than restated, the way a button's icon is: `sm` is a step
|
|
55
|
+
* down because a 44pt circle with a 20pt glyph reads as crowded, while 56 and 64
|
|
56
|
+
* both carry the larger glyph with room to spare.
|
|
57
|
+
*/
|
|
58
|
+
export const FAB_ICON_SIZE: Record<FabSize, IconSize> = {
|
|
59
|
+
sm: "md",
|
|
60
|
+
md: "lg",
|
|
61
|
+
lg: "lg",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function resolveFabIconSize(size: FabSize): IconSize {
|
|
65
|
+
return FAB_ICON_SIZE[size];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The side of an action's button its label chip sits on: the side facing into
|
|
70
|
+
* the screen, so the chip never runs off the edge the dial is pinned to.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveLabelSide(placement: FabPlacement): FabLabelSide {
|
|
73
|
+
switch (placement) {
|
|
74
|
+
case "bottom-start":
|
|
75
|
+
return "end";
|
|
76
|
+
case "bottom-center":
|
|
77
|
+
return "top";
|
|
78
|
+
default:
|
|
79
|
+
return "start";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type FabPlacementStyle =
|
|
84
|
+
| { position: "absolute"; bottom: number; start: number }
|
|
85
|
+
| { position: "absolute"; bottom: number; end: number }
|
|
86
|
+
| { position: "absolute"; bottom: number; start: 0; end: 0; alignItems: "center" };
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The style of the box a pinned fab sits in.
|
|
90
|
+
*
|
|
91
|
+
* Bottom is the offset plus the safe-area inset, so a fab on a phone with a home
|
|
92
|
+
* indicator clears it by the same distance it clears the side.
|
|
93
|
+
*
|
|
94
|
+
* Centre spans the row and centres inside it rather than reaching for
|
|
95
|
+
* `alignSelf`: an absolute child's `alignSelf` is read on the parent's cross
|
|
96
|
+
* axis, which is horizontal only when the parent is a column — a fab written
|
|
97
|
+
* into a `flex-row` would centre vertically instead. Spanning costs touches
|
|
98
|
+
* nothing because the box is `pointerEvents="box-none"`.
|
|
99
|
+
*
|
|
100
|
+
* Logical edges only (`start` / `end`), so a right-to-left layout flips with no
|
|
101
|
+
* code here.
|
|
102
|
+
*/
|
|
103
|
+
export function resolveFabPlacementStyle({
|
|
104
|
+
placement,
|
|
105
|
+
offset,
|
|
106
|
+
insetBottom,
|
|
107
|
+
}: {
|
|
108
|
+
placement: FabPlacement;
|
|
109
|
+
offset: number;
|
|
110
|
+
insetBottom: number;
|
|
111
|
+
}): FabPlacementStyle {
|
|
112
|
+
const bottom = offset + insetBottom;
|
|
113
|
+
switch (placement) {
|
|
114
|
+
case "bottom-start":
|
|
115
|
+
return { position: "absolute", bottom, start: offset };
|
|
116
|
+
case "bottom-center":
|
|
117
|
+
return { position: "absolute", bottom, start: 0, end: 0, alignItems: "center" };
|
|
118
|
+
default:
|
|
119
|
+
return { position: "absolute", bottom, end: offset };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* One action's share of the dial's spring, 0–1.
|
|
125
|
+
*
|
|
126
|
+
* One spring drives the whole dial; each action reads its own window of it,
|
|
127
|
+
* starting `index × stagger` in and running to the end. Index 0 is the action
|
|
128
|
+
* nearest the trigger, so the dial unfolds outward. Because every window is a
|
|
129
|
+
* pure function of the one value, reversing halfway through opening simply runs
|
|
130
|
+
* the same windows backwards — there are no per-action timers to fall out of
|
|
131
|
+
* step.
|
|
132
|
+
*
|
|
133
|
+
* Clamped at both ends because the spring overshoots, and floored on the window
|
|
134
|
+
* so a stagger too wide for the count degrades to a step rather than a NaN —
|
|
135
|
+
* a NaN written into a style freezes the action where it stands.
|
|
136
|
+
*
|
|
137
|
+
* A worklet, so the dial's animated styles call it on the UI thread.
|
|
138
|
+
*/
|
|
139
|
+
export function resolveDialProgress({
|
|
140
|
+
open,
|
|
141
|
+
index,
|
|
142
|
+
count,
|
|
143
|
+
stagger,
|
|
144
|
+
}: {
|
|
145
|
+
open: number;
|
|
146
|
+
index: number;
|
|
147
|
+
count: number;
|
|
148
|
+
stagger: number;
|
|
149
|
+
}): number {
|
|
150
|
+
"worklet";
|
|
151
|
+
const span = 1 - Math.max(count - 1, 0) * stagger;
|
|
152
|
+
const window = span > 0.0001 ? span : 0.0001;
|
|
153
|
+
const local = (open - index * stagger) / window;
|
|
154
|
+
return local < 0 ? 0 : local > 1 ? 1 : local;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Styling for every part of a fab and its dial.
|
|
159
|
+
*
|
|
160
|
+
* One slotted `tv()` so `size` and `variant` are declared once. A size names
|
|
161
|
+
* tokens: `size-fab-md` for a round fab, `h-fab-md` for an extended one, and the
|
|
162
|
+
* action anchor's `w-fab-md`, which centres a small action button on the
|
|
163
|
+
* trigger's own centre line. The values live in `tokens.css`.
|
|
164
|
+
*
|
|
165
|
+
* The root holds no `text-*` (rule 1); the label slot carries the colour, and a
|
|
166
|
+
* composed icon reads {@link FAB_FOREGROUND_TOKEN}.
|
|
167
|
+
*
|
|
168
|
+
* `shadow-lg` is deliberate and unique. Nothing else in the package casts a
|
|
169
|
+
* shadow; a fab is the one thing floating over content, and without one it
|
|
170
|
+
* reads as a sticker on the list rather than a control above it. No
|
|
171
|
+
* `overflow-hidden` on the root for the same reason — a clip would cut the
|
|
172
|
+
* shadow off on Android.
|
|
173
|
+
*
|
|
174
|
+
* Free of React Native imports so it stays unit-testable.
|
|
175
|
+
*/
|
|
176
|
+
export const fabVariants = tv({
|
|
177
|
+
slots: {
|
|
178
|
+
root: "flex-row items-center justify-center rounded-full shadow-lg",
|
|
179
|
+
label: "text-button-md font-medium",
|
|
180
|
+
/** Edge length an `Icon` composed into the fab inherits. */
|
|
181
|
+
icon: "",
|
|
182
|
+
scrim: "absolute inset-0 bg-overlay",
|
|
183
|
+
dial: "gap-3",
|
|
184
|
+
action: "items-center gap-3",
|
|
185
|
+
actionAnchor: "items-center",
|
|
186
|
+
actionButton: "size-fab-sm items-center justify-center rounded-full border border-border bg-elevated shadow-lg",
|
|
187
|
+
actionIcon: "size-icon-md",
|
|
188
|
+
actionLabel: "rounded-md bg-popover px-2 py-1",
|
|
189
|
+
actionLabelText: "text-popover-foreground text-sm",
|
|
190
|
+
},
|
|
191
|
+
variants: {
|
|
192
|
+
size: {
|
|
193
|
+
sm: { icon: "size-icon-md", actionAnchor: "w-fab-sm" },
|
|
194
|
+
md: { icon: "size-icon-lg", actionAnchor: "w-fab-md" },
|
|
195
|
+
lg: { icon: "size-icon-lg", actionAnchor: "w-fab-lg" },
|
|
196
|
+
},
|
|
197
|
+
variant: {
|
|
198
|
+
primary: { root: "bg-primary", label: "text-primary-foreground" },
|
|
199
|
+
secondary: { root: "bg-secondary", label: "text-secondary-foreground" },
|
|
200
|
+
surface: { root: "border border-border bg-elevated", label: "text-elevated-foreground" },
|
|
201
|
+
destructive: { root: "bg-destructive", label: "text-destructive-foreground" },
|
|
202
|
+
},
|
|
203
|
+
isExtended: {
|
|
204
|
+
true: { root: "gap-2 px-5" },
|
|
205
|
+
false: {},
|
|
206
|
+
},
|
|
207
|
+
isDisabled: {
|
|
208
|
+
true: { root: "opacity-50", actionButton: "opacity-50", actionLabel: "opacity-50" },
|
|
209
|
+
false: {},
|
|
210
|
+
},
|
|
211
|
+
labelSide: {
|
|
212
|
+
start: { dial: "items-end", action: "flex-row" },
|
|
213
|
+
end: { dial: "items-start", action: "flex-row-reverse" },
|
|
214
|
+
top: { dial: "items-center", action: "flex-col" },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
compoundVariants: [
|
|
218
|
+
{ size: "sm", isExtended: false, class: { root: "size-fab-sm" } },
|
|
219
|
+
{ size: "md", isExtended: false, class: { root: "size-fab-md" } },
|
|
220
|
+
{ size: "lg", isExtended: false, class: { root: "size-fab-lg" } },
|
|
221
|
+
{ size: "sm", isExtended: true, class: { root: "h-fab-sm" } },
|
|
222
|
+
{ size: "md", isExtended: true, class: { root: "h-fab-md" } },
|
|
223
|
+
{ size: "lg", isExtended: true, class: { root: "h-fab-lg" } },
|
|
224
|
+
],
|
|
225
|
+
defaultVariants: {
|
|
226
|
+
size: "md",
|
|
227
|
+
variant: "primary",
|
|
228
|
+
isExtended: false,
|
|
229
|
+
isDisabled: false,
|
|
230
|
+
labelSide: "start",
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
export type FabVariantProps = VariantProps<typeof fabVariants>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export { Fab, type FabProps } from "./fab";
|
|
2
|
+
export {
|
|
3
|
+
type FabContextValue,
|
|
4
|
+
type FabGroupContextValue,
|
|
5
|
+
FabProvider,
|
|
6
|
+
useFab,
|
|
7
|
+
useFabContext,
|
|
8
|
+
useFabGroup,
|
|
9
|
+
useFabGroupContext,
|
|
10
|
+
} from "./fab.context";
|
|
11
|
+
export type { FabSharedProps } from "./fab.types";
|
|
12
|
+
export {
|
|
13
|
+
FAB_DEFAULT_OFFSET,
|
|
14
|
+
FAB_DIAL_STAGGER,
|
|
15
|
+
FAB_FOREGROUND_TOKEN,
|
|
16
|
+
FAB_ICON_SIZE,
|
|
17
|
+
FAB_LABEL_SIDES,
|
|
18
|
+
FAB_OPEN_ROTATION_DEG,
|
|
19
|
+
FAB_PLACEMENTS,
|
|
20
|
+
FAB_REDUCED_MOTION_MS,
|
|
21
|
+
FAB_SIZES,
|
|
22
|
+
FAB_VARIANTS,
|
|
23
|
+
type FabLabelSide,
|
|
24
|
+
type FabPlacement,
|
|
25
|
+
type FabPlacementStyle,
|
|
26
|
+
type FabSize,
|
|
27
|
+
type FabVariant,
|
|
28
|
+
type FabVariantProps,
|
|
29
|
+
fabVariants,
|
|
30
|
+
resolveDialProgress,
|
|
31
|
+
resolveFabIconSize,
|
|
32
|
+
resolveFabPlacementStyle,
|
|
33
|
+
resolveLabelSide,
|
|
34
|
+
} from "./fab.variants";
|
|
35
|
+
export type { FabActionProps } from "./fab-action";
|
|
36
|
+
export type { FabGroupProps } from "./fab-group";
|
|
37
|
+
export type { FabLabelProps } from "./fab-label";
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Feedback
|
|
2
|
+
|
|
3
|
+
A dialog for writing: "What should we fix first?", a bug report, a one-question survey. It is a
|
|
4
|
+
[`Dialog`](../dialog/AGENTS.md) whose text field sits in a **recessed well** that says "type here",
|
|
5
|
+
with the actions on the band around it. Compound root plus `Feedback.Trigger`, `Feedback.Content`,
|
|
6
|
+
`Feedback.Panel`, `Feedback.Title`, `Feedback.Close`, `Feedback.Field`, `Feedback.Footer`,
|
|
7
|
+
`Feedback.Action`, `Feedback.Cancel` and `Feedback.Submit`, and `useFeedback()`.
|
|
8
|
+
|
|
9
|
+
`import { Feedback } from "@delacour/react-native-ui/feedback";`
|
|
10
|
+
|
|
11
|
+
Not for confirmations or warnings — those are a `Dialog`. Like every overlay it draws through
|
|
12
|
+
`Overlay.Portal`, so the app mounts `OverlayProvider` once at its root.
|
|
13
|
+
|
|
14
|
+
## Files
|
|
15
|
+
|
|
16
|
+
| File | What it holds |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `index.ts` | → `@delacour/react-native-ui/feedback` |
|
|
19
|
+
| `feedback.tsx` | Root — the open state, the draft, the send state — wrapping a `Dialog`, and the `Object.assign` surface |
|
|
20
|
+
| `feedback.context.tsx` | **Leaf.** `FeedbackProvider`, `useFeedback()`, `useFeedbackContext()`, `useFeedbackPart()` |
|
|
21
|
+
| `feedback-trigger.tsx` | `Feedback.Trigger` — `Dialog.Trigger` |
|
|
22
|
+
| `feedback-content.tsx` | `Feedback.Content` — `Dialog.Content` on the muted band, tighter padding |
|
|
23
|
+
| `feedback-panel.tsx` | `Feedback.Panel` — the well, in a clip that eases to its measured height |
|
|
24
|
+
| `feedback-title.tsx` | `Feedback.Title` — `Dialog.Title`; publishes a string title as the field's label |
|
|
25
|
+
| `feedback-close.tsx` | `Feedback.Close` — `Dialog.Close`, in the well's corner |
|
|
26
|
+
| `feedback-field.tsx` | `Feedback.Field` — a bare multiline `TextInput` bound to the draft |
|
|
27
|
+
| `feedback-footer.tsx` | `Feedback.Footer` |
|
|
28
|
+
| `feedback-action.tsx` | `Feedback.Action` — a `secondary` `Button` for custom actions |
|
|
29
|
+
| `feedback-cancel.tsx` | `Feedback.Cancel` — closes, keeps the draft |
|
|
30
|
+
| `feedback-submit.tsx` | `Feedback.Submit` — the gate, the trimmed value, the wait on a promise |
|
|
31
|
+
| `feedback.variants.ts` | The slotted `tv()`, the row and motion constants, `canSubmitFeedback` and the field-height resolvers — no RN imports |
|
|
32
|
+
| `feedback.variants.test.ts` | Slots, tokens in both themes, the submit gate, the field heights |
|
|
33
|
+
|
|
34
|
+
## Design
|
|
35
|
+
|
|
36
|
+
- **Everything a dialog does is Dialog's.** `Feedback` renders a `Dialog` and its parts *are*
|
|
37
|
+
Dialog's parts with classes merged over them — open state, portal, scrim, motion, the `modal`
|
|
38
|
+
band, the back button, the escape gesture, focus on open and return, and the keyboard lift that
|
|
39
|
+
keeps the footer above the keyboard. Nothing is restated, so a fix to Dialog is a fix here. The
|
|
40
|
+
import is `../dialog`: Dialog never imports Feedback, so there is no cycle to route around.
|
|
41
|
+
- **The shell is the band; the panel is the well.** `Feedback.Content` merges `bg-muted` and `p-2`
|
|
42
|
+
over Dialog's `bg-popover` and `p-5` through the same `tv` merge, so the corner, hairline and
|
|
43
|
+
width cap stay Dialog's. `Feedback.Panel` is `bg-background` with `rounded-md` — one step down
|
|
44
|
+
the corner ramp from the card it sits in, so the two read as nested — and a hairline. The well's
|
|
45
|
+
page colour against the band's muted one is what says "type here" without a field border.
|
|
46
|
+
- **The field has no box.** `Feedback.Field` is a raw `TextInput`, not an `Input`: the well is
|
|
47
|
+
its box, and an `Input` would draw a second border inside the first. It restates `font-sans` for
|
|
48
|
+
`Input`'s reason (a `TextInput` inherits nothing), sets `text-input-md` with paragraph leading
|
|
49
|
+
(`leading-6`, `FEEDBACK_FIELD_LINE_HEIGHT`, pinned by a test), and takes `Input`'s `accent-*`
|
|
50
|
+
placeholder, caret and selection classes from `input.variants` — a leaf, so no cycle.
|
|
51
|
+
- **The field is floored at `minRows` and capped at `maxRows`.** Six rows and twelve by default,
|
|
52
|
+
as a style (`resolveFeedbackFieldHeightStyle`) because a runtime number cannot be a class.
|
|
53
|
+
Between the two React Native's multiline `TextInput` sizes itself. `maxRows` is not in the
|
|
54
|
+
original spec: without a cap a long message grows the card until Dialog's lift, which keeps the
|
|
55
|
+
title on screen first, has to give up the footer to the keyboard.
|
|
56
|
+
- **The draft lives on the root, above `Dialog.Content`.** Content unmounts when the exit animation
|
|
57
|
+
ends; the root does not, so an uncontrolled draft survives a close and a reopen. Cancel, Close,
|
|
58
|
+
the scrim and back all keep it — a long message put down for a moment must not be thrown away.
|
|
59
|
+
`useFeedback().clear()` is the one way to empty it.
|
|
60
|
+
- **Submit never closes.** `canSubmitFeedback` gates it (empty and whitespace are empty;
|
|
61
|
+
`canSubmitEmpty` lets an empty one through for chips or a rating; disabled outranks both), and
|
|
62
|
+
`onSubmit` gets the trimmed text. A returned promise turns on the button's loading state and
|
|
63
|
+
makes the field read-only until it settles — and the dialog is still open, so a failure can be
|
|
64
|
+
shown in the well with the draft intact. The caller closes and clears on success. A rejection is
|
|
65
|
+
not swallowed: handle failures inside `onSubmit`, or the app's unhandled-rejection reporting sees
|
|
66
|
+
it, which is better than a send that failed silently.
|
|
67
|
+
- **The well eases between heights; the shell never jumps.** A multi-step flow (rating → text →
|
|
68
|
+
thanks) is the caller swapping the panel's and footer's children. `Feedback.Panel` measures the
|
|
69
|
+
well with `onLayout` and animates a clip around it to that height over `FEEDBACK_PANEL_RESIZE_MS`
|
|
70
|
+
(200ms). The spec named a Reanimated `LinearTransition` on the panel; that animates the panel's
|
|
71
|
+
frame inside a shell that has already snapped to its final size and re-centred, which is exactly
|
|
72
|
+
the jump to avoid. A real height changing on every frame moves the shell through ordinary layout
|
|
73
|
+
and keeps it centred — Accordion's reasoning for its own panel. The first layout lands without
|
|
74
|
+
animating, and under reduce motion every change does.
|
|
75
|
+
- **The field's label is the title.** `Feedback.Title` publishes its children to the root when
|
|
76
|
+
they are a plain string, and `Feedback.Field` uses that as its default `accessibilityLabel`, so
|
|
77
|
+
a screen reader announces the question rather than only the placeholder. A non-string title
|
|
78
|
+
publishes nothing; pass `accessibilityLabel` then.
|
|
79
|
+
- **Close sits in the well, not the shell.** `Feedback.Close` is `Dialog.Close` with its slot
|
|
80
|
+
re-pinned to the well's corner (`top-3 right-3`, against the panel's `p-4`); the title keeps
|
|
81
|
+
Dialog's `pr-8` clearance.
|
|
82
|
+
- **No blur backdrop.** As for every overlay: frosted scrims need `expo-blur`, not a peer.
|
|
83
|
+
|
|
84
|
+
## Testing
|
|
85
|
+
|
|
86
|
+
`bun test` reaches `feedbackVariants`, the tokens in both themes, `canSubmitFeedback` and the
|
|
87
|
+
field-height resolvers. Opening, the submit gate, the async send, the draft across close and
|
|
88
|
+
reopen, the keyboard, the multi-step resize and every dismissal path are verified on a simulator
|
|
89
|
+
through `/feedback` in the playground.
|
|
90
|
+
|
|
91
|
+
Preview media is held back with the rest of the overlays (see `docs/plans/overlays/README.md`).
|
|
92
|
+
When a capture tool is back, re-add `capture` with these settings and run
|
|
93
|
+
`bun run previews -- -- --only feedback`: `basic` `{ flow: "feedback/basic", frame: "device", hero: true }`,
|
|
94
|
+
and `sending`, `multi-step` `{ flow: "feedback/<id>", frame: "device" }`. The flows are in
|
|
95
|
+
`.argent/flows/previews/feedback/`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { Button, type ButtonProps } from "../button";
|
|
3
|
+
|
|
4
|
+
export type FeedbackActionProps = ButtonProps;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A custom footer action — "Skip", "Back", "Attach screenshot" — as a
|
|
8
|
+
* `secondary` `Button`. Pass `variant` to change it.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* <Feedback.Action onPress={back}>Back</Feedback.Action>
|
|
12
|
+
*/
|
|
13
|
+
export function FeedbackAction({ variant = "secondary", ...props }: FeedbackActionProps): ReactElement {
|
|
14
|
+
return <Button variant={variant} {...props} />;
|
|
15
|
+
}
|
|
16
|
+
FeedbackAction.displayName = "DelacourUI.Feedback.Action";
|