@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,245 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
resolveSkeletonAccessibility,
|
|
4
|
+
resolveSkeletonAnimation,
|
|
5
|
+
resolveSkeletonLineWidths,
|
|
6
|
+
SKELETON_ANIMATIONS,
|
|
7
|
+
SKELETON_FILL_CLASS,
|
|
8
|
+
SKELETON_LAST_LINE_WIDTH,
|
|
9
|
+
SKELETON_PULSE_MIN_OPACITY,
|
|
10
|
+
SKELETON_SHAPES,
|
|
11
|
+
SKELETON_SHIMMER_PEAK_OPACITY,
|
|
12
|
+
SKELETON_SHIMMER_STOPS,
|
|
13
|
+
skeletonPulseOpacity,
|
|
14
|
+
skeletonShimmerOffset,
|
|
15
|
+
skeletonVariants,
|
|
16
|
+
} from "./skeleton.variants";
|
|
17
|
+
|
|
18
|
+
describe("resolveSkeletonAnimation", () => {
|
|
19
|
+
test("passes every requested animation through when motion is allowed", () => {
|
|
20
|
+
for (const animation of SKELETON_ANIMATIONS) {
|
|
21
|
+
expect(resolveSkeletonAnimation(animation, false)).toBe(animation);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// The shape is the message. A placeholder that stops moving still reads as
|
|
26
|
+
// content on its way, so reduce-motion stills it rather than hiding it.
|
|
27
|
+
test("stills every animation under reduce motion", () => {
|
|
28
|
+
for (const animation of SKELETON_ANIMATIONS) {
|
|
29
|
+
expect(resolveSkeletonAnimation(animation, true)).toBe("none");
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("shimmers by default", () => {
|
|
34
|
+
expect(resolveSkeletonAnimation(undefined, false)).toBe("shimmer");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("skeletonPulseOpacity", () => {
|
|
39
|
+
test("starts and ends a cycle fully opaque, so the loop has no seam", () => {
|
|
40
|
+
expect(skeletonPulseOpacity(0)).toBeCloseTo(1, 10);
|
|
41
|
+
expect(skeletonPulseOpacity(1)).toBeCloseTo(1, 10);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("reaches its floor half way through the cycle", () => {
|
|
45
|
+
expect(skeletonPulseOpacity(0.5)).toBeCloseTo(SKELETON_PULSE_MIN_OPACITY, 10);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("never leaves the band between the floor and opaque", () => {
|
|
49
|
+
for (let step = 0; step <= 100; step++) {
|
|
50
|
+
const opacity = skeletonPulseOpacity(step / 100);
|
|
51
|
+
expect(opacity).toBeGreaterThanOrEqual(SKELETON_PULSE_MIN_OPACITY - 1e-9);
|
|
52
|
+
expect(opacity).toBeLessThanOrEqual(1 + 1e-9);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("is symmetric, so it breathes in and out at one rate", () => {
|
|
57
|
+
expect(skeletonPulseOpacity(0.2)).toBeCloseTo(skeletonPulseOpacity(0.8), 10);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// A floor at zero would make the placeholder blink out of existence, which
|
|
61
|
+
// reads as a layout that is failing rather than one that is loading.
|
|
62
|
+
test("keeps the floor visible", () => {
|
|
63
|
+
expect(SKELETON_PULSE_MIN_OPACITY).toBeGreaterThan(0.3);
|
|
64
|
+
expect(SKELETON_PULSE_MIN_OPACITY).toBeLessThan(1);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("skeletonShimmerOffset", () => {
|
|
69
|
+
// Both ends of the sweep sit fully outside the placeholder, so the band
|
|
70
|
+
// enters from nothing and leaves to nothing and the loop has no visible jump.
|
|
71
|
+
test("starts wholly off the leading edge", () => {
|
|
72
|
+
expect(skeletonShimmerOffset(0, 200, 80)).toBe(-80);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("ends wholly off the trailing edge", () => {
|
|
76
|
+
expect(skeletonShimmerOffset(1, 200, 80)).toBe(200);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("advances linearly", () => {
|
|
80
|
+
const a = skeletonShimmerOffset(0.25, 200, 80);
|
|
81
|
+
const b = skeletonShimmerOffset(0.5, 200, 80);
|
|
82
|
+
const c = skeletonShimmerOffset(0.75, 200, 80);
|
|
83
|
+
expect(b - a).toBeCloseTo(c - b, 10);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("stays off-screen before the placeholder has been measured", () => {
|
|
87
|
+
expect(skeletonShimmerOffset(0.5, 0, 80)).toBeLessThanOrEqual(0);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("SKELETON_SHIMMER_STOPS", () => {
|
|
92
|
+
test("fades in from nothing and back out to nothing", () => {
|
|
93
|
+
expect(SKELETON_SHIMMER_STOPS.at(0)).toEqual({ offset: 0, opacity: 0 });
|
|
94
|
+
expect(SKELETON_SHIMMER_STOPS.at(-1)).toEqual({ offset: 1, opacity: 0 });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("peaks in the middle of the band", () => {
|
|
98
|
+
const peak = SKELETON_SHIMMER_STOPS.reduce((best, stop) => (stop.opacity > best.opacity ? stop : best));
|
|
99
|
+
expect(peak.offset).toBe(0.5);
|
|
100
|
+
expect(peak.opacity).toBe(SKELETON_SHIMMER_PEAK_OPACITY);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("is symmetric about its centre", () => {
|
|
104
|
+
const offsets = SKELETON_SHIMMER_STOPS.map((stop) => stop.offset);
|
|
105
|
+
const mirrored = [...SKELETON_SHIMMER_STOPS].reverse().map((stop) => 1 - stop.offset);
|
|
106
|
+
expect(mirrored).toEqual(offsets);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("advances along the band", () => {
|
|
110
|
+
for (const [index, stop] of SKELETON_SHIMMER_STOPS.slice(1).entries()) {
|
|
111
|
+
expect(stop.offset).toBeGreaterThan(SKELETON_SHIMMER_STOPS[index]?.offset ?? 1);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("resolveSkeletonLineWidths", () => {
|
|
117
|
+
test("gives one width per line", () => {
|
|
118
|
+
expect(resolveSkeletonLineWidths(4)).toHaveLength(4);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("shortens only the last line, the way a paragraph ends", () => {
|
|
122
|
+
expect(resolveSkeletonLineWidths(3)).toEqual(["100%", "100%", `${SKELETON_LAST_LINE_WIDTH * 100}%`]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("takes a custom last-line fraction", () => {
|
|
126
|
+
expect(resolveSkeletonLineWidths(2, 0.4)).toEqual(["100%", "40%"]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// A single line is a title or a label, and whoever wrote it knows how long
|
|
130
|
+
// it is. Shortening it would second-guess a width they did not ask for.
|
|
131
|
+
test("leaves a single line full width", () => {
|
|
132
|
+
expect(resolveSkeletonLineWidths(1)).toEqual(["100%"]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("draws nothing for a count below one or a fraction", () => {
|
|
136
|
+
expect(resolveSkeletonLineWidths(0)).toEqual([]);
|
|
137
|
+
expect(resolveSkeletonLineWidths(-2)).toEqual([]);
|
|
138
|
+
expect(resolveSkeletonLineWidths(2.7)).toHaveLength(2);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("clamps the last-line fraction into the line", () => {
|
|
142
|
+
expect(resolveSkeletonLineWidths(2, 1.5)).toEqual(["100%", "100%"]);
|
|
143
|
+
expect(resolveSkeletonLineWidths(2, -1)).toEqual(["100%", "0%"]);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("resolveSkeletonAccessibility", () => {
|
|
148
|
+
test("hides an unlabelled placeholder from assistive technology", () => {
|
|
149
|
+
const a11y = resolveSkeletonAccessibility({ isLoading: true });
|
|
150
|
+
expect(a11y.kind).toBe("hidden");
|
|
151
|
+
expect(a11y.props.accessibilityElementsHidden).toBe(true);
|
|
152
|
+
expect(a11y.props.importantForAccessibility).toBe("no-hide-descendants");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("announces a labelled placeholder once, as a busy status", () => {
|
|
156
|
+
const a11y = resolveSkeletonAccessibility({ isLoading: true, label: "Loading messages" });
|
|
157
|
+
expect(a11y.kind).toBe("status");
|
|
158
|
+
expect(a11y.props.accessible).toBe(true);
|
|
159
|
+
expect(a11y.props.accessibilityLabel).toBe("Loading messages");
|
|
160
|
+
expect(a11y.props.accessibilityRole).toBe("progressbar");
|
|
161
|
+
expect(a11y.props.accessibilityState).toEqual({ busy: true });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("treats an empty label as no label", () => {
|
|
165
|
+
expect(resolveSkeletonAccessibility({ isLoading: true, label: "" }).kind).toBe("hidden");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Once the content has landed it speaks for itself; the skeleton must not
|
|
169
|
+
// sit between it and a screen reader.
|
|
170
|
+
test("steps aside entirely once loaded", () => {
|
|
171
|
+
for (const label of [undefined, "Loading"]) {
|
|
172
|
+
const a11y = resolveSkeletonAccessibility({ isLoading: false, label });
|
|
173
|
+
expect(a11y.kind).toBe("content");
|
|
174
|
+
expect(a11y.props).toEqual({});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("skeletonVariants root slot", () => {
|
|
180
|
+
test("paints and clips only while loading", () => {
|
|
181
|
+
const loading = skeletonVariants({ isLoading: true }).root();
|
|
182
|
+
expect(loading).toContain(SKELETON_FILL_CLASS);
|
|
183
|
+
expect(loading).toContain("overflow-hidden");
|
|
184
|
+
|
|
185
|
+
const loaded = skeletonVariants({ isLoading: false }).root();
|
|
186
|
+
expect(loaded).not.toContain(SKELETON_FILL_CLASS);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("gives every shape a corner", () => {
|
|
190
|
+
for (const shape of SKELETON_SHAPES) {
|
|
191
|
+
expect(skeletonVariants({ shape }).root()).toMatch(/\brounded-/);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("draws a circle as a circle", () => {
|
|
196
|
+
expect(skeletonVariants({ shape: "circle" }).root()).toContain("rounded-full");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// With children the content sizes the placeholder, so a default size would
|
|
200
|
+
// only fight it — and would clip the content once it lands.
|
|
201
|
+
test("sizes a childless shape, and leaves a wrapping one to its content", () => {
|
|
202
|
+
for (const shape of SKELETON_SHAPES) {
|
|
203
|
+
expect(skeletonVariants({ shape, isEmpty: true }).root()).toMatch(/\b(h|size)-/);
|
|
204
|
+
expect(skeletonVariants({ shape, isEmpty: false }).root()).not.toMatch(/\b(h|w|size)-/);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("lets a caller's size beat the shape's default", () => {
|
|
209
|
+
const cls = skeletonVariants({ shape: "circle", isEmpty: true }).root({ className: "size-16" });
|
|
210
|
+
expect(cls).toContain("size-16");
|
|
211
|
+
expect(cls).not.toContain("size-10");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("lets a caller's corner beat the shape's", () => {
|
|
215
|
+
const cls = skeletonVariants({ shape: "rect" }).root({ className: "rounded-full" });
|
|
216
|
+
expect(cls).toContain("rounded-full");
|
|
217
|
+
expect(cls).not.toContain("rounded-lg");
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("SKELETON_FILL_CLASS", () => {
|
|
222
|
+
// An opaque surface token sits too close to the light page to read. A tint
|
|
223
|
+
// of a foreground colour steps off any surface it lands on, in both themes.
|
|
224
|
+
test("is a translucent tint of a foreground colour", () => {
|
|
225
|
+
expect(SKELETON_FILL_CLASS).toMatch(/^bg-[\w-]*foreground\/\d+$/);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("skeletonVariants band slot", () => {
|
|
230
|
+
test("is pinned to the leading edge and spans the full height", () => {
|
|
231
|
+
const cls = skeletonVariants().band();
|
|
232
|
+
expect(cls).toContain("absolute");
|
|
233
|
+
expect(cls).toContain("left-0");
|
|
234
|
+
expect(cls).toContain("inset-y-0");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// A share of the placeholder, bounded both ways — and a class, so Yoga sizes
|
|
238
|
+
// it and no frame of the sweep has to write a layout prop.
|
|
239
|
+
test("takes its width from layout, as a bounded share of the placeholder", () => {
|
|
240
|
+
const cls = skeletonVariants().band();
|
|
241
|
+
expect(cls).toContain("w-3/5");
|
|
242
|
+
expect(cls).toContain("min-w-12");
|
|
243
|
+
expect(cls).toContain("max-w-60");
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { tv } from "../../lib/tv";
|
|
2
|
+
|
|
3
|
+
export const SKELETON_SHAPES = ["rect", "line", "circle"] as const;
|
|
4
|
+
export const SKELETON_ANIMATIONS = ["shimmer", "pulse", "none"] as const;
|
|
5
|
+
|
|
6
|
+
export type SkeletonShape = (typeof SKELETON_SHAPES)[number];
|
|
7
|
+
export type SkeletonAnimation = (typeof SKELETON_ANIMATIONS)[number];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The placeholder's fill: the muted foreground at a fifth of its strength.
|
|
11
|
+
*
|
|
12
|
+
* Translucent on purpose. `bg-muted` sits within 0.015 lightness of the light
|
|
13
|
+
* page and vanished on it; a tint of a foreground colour instead lands a clear
|
|
14
|
+
* step off whatever surface it is on — page or card, light or dark — and still
|
|
15
|
+
* leaves room above it for the `elevated` glint.
|
|
16
|
+
*/
|
|
17
|
+
export const SKELETON_FILL_CLASS = "bg-muted-foreground/20";
|
|
18
|
+
|
|
19
|
+
/** Animation used with no `animation` prop and nothing to inherit from. */
|
|
20
|
+
export const SKELETON_FALLBACK_ANIMATION: SkeletonAnimation = "shimmer";
|
|
21
|
+
|
|
22
|
+
/** One full cycle of the shared clock — a shimmer sweep, or one breath of a pulse. */
|
|
23
|
+
export const SKELETON_CYCLE_MS = 1500;
|
|
24
|
+
|
|
25
|
+
/** How long the content takes to fade in once it lands. */
|
|
26
|
+
export const SKELETON_REVEAL_MS = 200;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The lowest opacity a pulse dims to.
|
|
30
|
+
*
|
|
31
|
+
* Well above zero on purpose: a placeholder that blinks out of existence reads
|
|
32
|
+
* as a layout that is failing rather than one that is loading.
|
|
33
|
+
*/
|
|
34
|
+
export const SKELETON_PULSE_MIN_OPACITY = 0.5;
|
|
35
|
+
|
|
36
|
+
/** Alpha at the centre of the shimmer band, over the `elevated` token. */
|
|
37
|
+
export const SKELETON_SHIMMER_PEAK_OPACITY = 0.9;
|
|
38
|
+
|
|
39
|
+
/** How far across the last line of a `Skeleton.Lines` paragraph runs. */
|
|
40
|
+
export const SKELETON_LAST_LINE_WIDTH = 0.6;
|
|
41
|
+
|
|
42
|
+
/** One `<Stop>` of the shimmer band's gradient. */
|
|
43
|
+
export type SkeletonShimmerStop = { offset: number; opacity: number };
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The shimmer band's gradient, left to right.
|
|
47
|
+
*
|
|
48
|
+
* Transparent at both edges so the band has no visible boundary, and symmetric
|
|
49
|
+
* so it reads the same whichever way it is travelling. The quarter stops sit
|
|
50
|
+
* low rather than on a straight ramp, which keeps the highlight a soft glint
|
|
51
|
+
* instead of a triangle with a bright ridge down its middle.
|
|
52
|
+
*/
|
|
53
|
+
export const SKELETON_SHIMMER_STOPS: readonly SkeletonShimmerStop[] = [
|
|
54
|
+
{ offset: 0, opacity: 0 },
|
|
55
|
+
{ offset: 0.25, opacity: SKELETON_SHIMMER_PEAK_OPACITY * 0.35 },
|
|
56
|
+
{ offset: 0.5, opacity: SKELETON_SHIMMER_PEAK_OPACITY },
|
|
57
|
+
{ offset: 0.75, opacity: SKELETON_SHIMMER_PEAK_OPACITY * 0.35 },
|
|
58
|
+
{ offset: 1, opacity: 0 },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The animation a skeleton actually runs.
|
|
63
|
+
*
|
|
64
|
+
* Calm motion — the OS reduce-motion setting, or an app's `isMotionCalm` — stills
|
|
65
|
+
* every animation rather than hiding the placeholder: the shape is the message,
|
|
66
|
+
* and a shape that stops moving still reads as content on its way. The clock then
|
|
67
|
+
* never starts, which is also why the timing itself can set `ReduceMotion.Never` —
|
|
68
|
+
* see AGENTS.md.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveSkeletonAnimation(animation: SkeletonAnimation | undefined, isCalm: boolean): SkeletonAnimation {
|
|
71
|
+
if (isCalm) return "none";
|
|
72
|
+
return animation ?? SKELETON_FALLBACK_ANIMATION;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A pulse's opacity at a point in the cycle — opaque at both ends, at the floor
|
|
77
|
+
* half way.
|
|
78
|
+
*
|
|
79
|
+
* A cosine rather than a triangle, so it eases into each turn instead of
|
|
80
|
+
* bouncing off it, and so 0 and 1 meet with no seam as the clock wraps.
|
|
81
|
+
*
|
|
82
|
+
* Marked `"worklet"` and written flat, calling nothing but `Math`, for the
|
|
83
|
+
* reason `switch.variants.ts` gives: a module-scope worklet that calls a sibling
|
|
84
|
+
* works only while that sibling happens to be declared first.
|
|
85
|
+
*/
|
|
86
|
+
export function skeletonPulseOpacity(progress: number): number {
|
|
87
|
+
"worklet";
|
|
88
|
+
const mid = (1 + SKELETON_PULSE_MIN_OPACITY) / 2;
|
|
89
|
+
const amplitude = (1 - SKELETON_PULSE_MIN_OPACITY) / 2;
|
|
90
|
+
return mid + amplitude * Math.cos(2 * Math.PI * progress);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Where the band's leading edge sits at a point in the sweep.
|
|
95
|
+
*
|
|
96
|
+
* Runs from wholly off the start edge to wholly off the end edge, so it enters
|
|
97
|
+
* from nothing and leaves to nothing and the wrap back to 0 is invisible.
|
|
98
|
+
* Before the placeholder is measured `width` is 0 and the band stays parked
|
|
99
|
+
* off-screen.
|
|
100
|
+
*/
|
|
101
|
+
export function skeletonShimmerOffset(progress: number, width: number, bandWidth: number): number {
|
|
102
|
+
"worklet";
|
|
103
|
+
return -bandWidth + (width + bandWidth) * progress;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The width of each line in a `Skeleton.Lines` paragraph.
|
|
108
|
+
*
|
|
109
|
+
* Only the last line is shortened, the way a paragraph ends — and only when
|
|
110
|
+
* there is more than one, since a single line is a title or a label whose
|
|
111
|
+
* length the caller already knows. A fractional count rounds down.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveSkeletonLineWidths(lines: number, lastLineWidth = SKELETON_LAST_LINE_WIDTH): `${number}%`[] {
|
|
114
|
+
const count = Math.max(0, Math.floor(lines));
|
|
115
|
+
const last = Math.min(1, Math.max(0, lastLineWidth));
|
|
116
|
+
|
|
117
|
+
return Array.from({ length: count }, (_unused, index) =>
|
|
118
|
+
count > 1 && index === count - 1 ? (`${Math.round(last * 100)}%` as const) : ("100%" as const)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Accessibility props a skeleton spreads onto its root. A subset of React Native's `ViewProps`. */
|
|
123
|
+
export type SkeletonAccessibilityProps = {
|
|
124
|
+
accessible?: boolean;
|
|
125
|
+
accessibilityLabel?: string;
|
|
126
|
+
accessibilityRole?: "progressbar";
|
|
127
|
+
accessibilityState?: { busy: boolean };
|
|
128
|
+
accessibilityElementsHidden?: boolean;
|
|
129
|
+
importantForAccessibility?: "no-hide-descendants";
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/** What a skeleton is to assistive technology, and the props that make it so. */
|
|
133
|
+
export type SkeletonAccessibility =
|
|
134
|
+
| { kind: "hidden"; props: SkeletonAccessibilityProps }
|
|
135
|
+
| { kind: "status"; props: SkeletonAccessibilityProps }
|
|
136
|
+
| { kind: "content"; props: SkeletonAccessibilityProps };
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* How a skeleton presents to a screen reader.
|
|
140
|
+
*
|
|
141
|
+
* An unlabelled placeholder is a picture of content that is not there, so it is
|
|
142
|
+
* hidden outright — a screen of grey boxes would otherwise announce nothing
|
|
143
|
+
* worth hearing, many times over. A labelled one announces once, as a busy
|
|
144
|
+
* progress status. Once loaded the skeleton steps aside and the content speaks
|
|
145
|
+
* for itself.
|
|
146
|
+
*/
|
|
147
|
+
export function resolveSkeletonAccessibility({
|
|
148
|
+
label,
|
|
149
|
+
isLoading,
|
|
150
|
+
}: {
|
|
151
|
+
label?: string;
|
|
152
|
+
isLoading: boolean;
|
|
153
|
+
}): SkeletonAccessibility {
|
|
154
|
+
if (!isLoading) return { kind: "content", props: {} };
|
|
155
|
+
|
|
156
|
+
if (label) {
|
|
157
|
+
return {
|
|
158
|
+
kind: "status",
|
|
159
|
+
props: {
|
|
160
|
+
accessible: true,
|
|
161
|
+
accessibilityLabel: label,
|
|
162
|
+
accessibilityRole: "progressbar",
|
|
163
|
+
accessibilityState: { busy: true },
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
kind: "hidden",
|
|
170
|
+
props: { accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants" },
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Styling for every part of a skeleton.
|
|
176
|
+
*
|
|
177
|
+
* `isEmpty` is whether the skeleton wraps content. A wrapping skeleton is sized
|
|
178
|
+
* by that content — rendered invisibly underneath while loading — so its shape
|
|
179
|
+
* contributes only a corner; a default height would fight the content and then
|
|
180
|
+
* clip it once it lands. A childless one needs a size of its own, and the
|
|
181
|
+
* shape supplies a sensible one that a caller's className overrides.
|
|
182
|
+
*
|
|
183
|
+
* The paint and the clip belong to the loading state alone, so the content is
|
|
184
|
+
* not left on a grey card, or cropped, once it is shown.
|
|
185
|
+
*
|
|
186
|
+
* Free of React Native imports so it stays unit-testable. See AGENTS.md.
|
|
187
|
+
*/
|
|
188
|
+
export const skeletonVariants = tv({
|
|
189
|
+
slots: {
|
|
190
|
+
root: "",
|
|
191
|
+
content: "",
|
|
192
|
+
// Yoga sizes the band, never an animated style: 60% of the placeholder,
|
|
193
|
+
// held between 48 and 240pt so a chip still gets a visible glint and a
|
|
194
|
+
// full-bleed card does not get a wash. See `skeleton-shimmer.tsx`.
|
|
195
|
+
band: "absolute inset-y-0 left-0 w-3/5 min-w-12 max-w-60",
|
|
196
|
+
},
|
|
197
|
+
variants: {
|
|
198
|
+
shape: {
|
|
199
|
+
rect: { root: "rounded-lg" },
|
|
200
|
+
line: { root: "rounded-sm" },
|
|
201
|
+
circle: { root: "rounded-full" },
|
|
202
|
+
},
|
|
203
|
+
isLoading: {
|
|
204
|
+
true: { root: `overflow-hidden ${SKELETON_FILL_CLASS}` },
|
|
205
|
+
false: {},
|
|
206
|
+
},
|
|
207
|
+
isEmpty: {
|
|
208
|
+
true: {},
|
|
209
|
+
false: {},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
compoundVariants: [
|
|
213
|
+
{ shape: "rect", isEmpty: true, className: { root: "h-24 w-full" } },
|
|
214
|
+
{ shape: "line", isEmpty: true, className: { root: "h-3.5 w-full" } },
|
|
215
|
+
{ shape: "circle", isEmpty: true, className: { root: "size-10" } },
|
|
216
|
+
],
|
|
217
|
+
defaultVariants: {
|
|
218
|
+
shape: "rect",
|
|
219
|
+
isLoading: true,
|
|
220
|
+
isEmpty: true,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# SlideButton
|
|
2
|
+
|
|
3
|
+
A control confirmed by dragging a handle across a rail — "Slide to ship", "Slide
|
|
4
|
+
to transfer $240". For an action that deserves deliberation where a dialog would
|
|
5
|
+
be too much ceremony. Compound root plus `SlideButton.Label` and
|
|
6
|
+
`SlideButton.Thumb`.
|
|
7
|
+
|
|
8
|
+
`import { SlideButton } from "@delacour/react-native-ui/slide-button";`
|
|
9
|
+
|
|
10
|
+
## Files
|
|
11
|
+
|
|
12
|
+
| File | What it holds |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `index.ts` | → `@delacour/react-native-ui/slide-button` |
|
|
15
|
+
| `slide-button.tsx` | Root, the gesture, the internal trail, and the `Object.assign` compound surface |
|
|
16
|
+
| `slide-button-label.tsx` | `SlideButton.Label` |
|
|
17
|
+
| `slide-button-thumb.tsx` | `SlideButton.Thumb`, and the internal chevron-to-tick glyph |
|
|
18
|
+
| `slide-button.context.tsx` | `SlideButtonContext`, `useSlideButton` |
|
|
19
|
+
| `slide-button.variants.ts` | The slotted `tv()`, the motion constants and the pure resolvers |
|
|
20
|
+
| `slide-button.variants.test.ts` | |
|
|
21
|
+
|
|
22
|
+
There is no `slide-button.types.ts`: every prop type has exactly one consumer.
|
|
23
|
+
|
|
24
|
+
## Design
|
|
25
|
+
|
|
26
|
+
- **Variants**: `secondary` (default), `destructive`, `success`. **Sizes**:
|
|
27
|
+
`sm`, `md`, `lg`. Plus `isFullWidth` and `isDisabled`.
|
|
28
|
+
- **There is no `primary` variant.** The handle is neutral, so a primary rail
|
|
29
|
+
with a neutral handle draws the same picture as `secondary` in any theme whose
|
|
30
|
+
primary sits close to its foreground — the default palette included. Two names
|
|
31
|
+
for one look is a choice a caller cannot see the difference in.
|
|
32
|
+
- **The rail is a button's box.** `h-button-*` and `rounded-button-*` at each
|
|
33
|
+
size, so a slide button stacks level with a `Button` or a progress button in
|
|
34
|
+
the same column. A test pins the classes; it names a TODO for the category
|
|
35
|
+
lead to compare against `progressButtonVariants` directly once that lands.
|
|
36
|
+
- **The handle is neutral in every variant** — `bg-background` with a
|
|
37
|
+
`border-border` hairline, inset 4pt, about 1.6× as wide as it is tall. Only the
|
|
38
|
+
rail, the trail and the label carry the variant. A coloured handle on a
|
|
39
|
+
coloured trail would disappear into its own wake.
|
|
40
|
+
- **The handle's width is measured, not tabulated.** `resolveSlideHandleWidth`
|
|
41
|
+
reads the rail's laid-out height, so a consumer who retunes
|
|
42
|
+
`--spacing-button-*` keeps the handle in proportion.
|
|
43
|
+
- **The trail ends under the handle's middle, not at its rear edge.** A stadium's
|
|
44
|
+
rear is a curve; a trail stopping at the straight edge leaves a sliver of rail
|
|
45
|
+
above and below it. Ending under the handle reads as one shape — the handle
|
|
46
|
+
dragging its colour along — while still sharing an edge with it as seen.
|
|
47
|
+
- **The trail is invisible at rest.** It fades in over the first 8pt of travel
|
|
48
|
+
(`resolveSlideTrailOpacity`). Ending under the handle's middle, it would
|
|
49
|
+
otherwise peek out behind the resting handle's rear curve — a coloured sliver
|
|
50
|
+
that makes an untouched control look half-started.
|
|
51
|
+
- **The label changes colour where the trail covers it.** A second copy in the
|
|
52
|
+
trail's foreground (`labelOnTrail`) is clipped to the trail's width
|
|
53
|
+
(`resolveSlideTrailWidth`, shared with the trail). A state variant's trail is
|
|
54
|
+
its full colour, and the soft label colour on it is the same hue on the same
|
|
55
|
+
hue: the words vanished exactly where the trail reached them.
|
|
56
|
+
- **The label keeps a handle's width clear on both sides**
|
|
57
|
+
(`resolveSlideLabelGutter`). Symmetric, so it stays centred; a long label
|
|
58
|
+
truncates before it runs under the resting handle.
|
|
59
|
+
- **The label is centred in the whole rail and never fades or moves.** The handle
|
|
60
|
+
passes over it. A label that faded would leave the control saying nothing for
|
|
61
|
+
the second half of the gesture, the half where the hand most wants to know what
|
|
62
|
+
it is confirming.
|
|
63
|
+
- **The handle tracks the finger 1:1; only the release is sprung.** Any easing on
|
|
64
|
+
the way out would make the handle lag the thumb, which reads as resistance.
|
|
65
|
+
- **A release confirms on look-ahead *and* reach.** `resolveSlideRelease`
|
|
66
|
+
projects the release `velocity × 0.08s` ahead; it confirms only when that
|
|
67
|
+
projection reaches the threshold **and** the hand itself got within 0.25 of it.
|
|
68
|
+
The second condition is what stops a hard flick from halfway: the habit-driven
|
|
69
|
+
swipe this control exists to refuse.
|
|
70
|
+
- **`threshold={1}` has no shortcut at all.** Only a release within half a point
|
|
71
|
+
of the far end confirms. That is the setting for something irreversible.
|
|
72
|
+
- **`onComplete` fires the moment the release confirms**, alongside the handle's
|
|
73
|
+
spring to the end — not when the spring settles. A spring can be interrupted
|
|
74
|
+
(an unmount, a re-layout), and a confirmation that hung on its callback could
|
|
75
|
+
be silently lost.
|
|
76
|
+
- **A requested completion holds the handle at the end.** Controlled, the release
|
|
77
|
+
reports `onCompletedChange(true)` and the handle waits at the end until
|
|
78
|
+
`isCompleted` answers. Springing back while the caller's work is in flight
|
|
79
|
+
would read as a refusal. `isCompleted` going false springs it home; true keeps
|
|
80
|
+
it there with the tick.
|
|
81
|
+
- **A rejected promise takes the handle home.** `onComplete` may return a
|
|
82
|
+
promise. If it rejects, the button reports `onCompletedChange(false)` and the
|
|
83
|
+
handle springs back, so a failed request never looks confirmed and a
|
|
84
|
+
controlled caller has a way out of the pending state that does not need a
|
|
85
|
+
`true` it never meant.
|
|
86
|
+
- **Disabled refuses the drag outright.** The handle does not move at all. A
|
|
87
|
+
handle that followed and sprang back would read as a failed slide rather than
|
|
88
|
+
a refused one.
|
|
89
|
+
- **Disabled still claims the drag.** The pan stays enabled and every callback
|
|
90
|
+
returns early. A disabled recognizer lets the touch fall through to whatever
|
|
91
|
+
is behind it — inside a stack with a full-screen back swipe, dragging a
|
|
92
|
+
disabled slide navigated back. Found on the simulator.
|
|
93
|
+
- **One `Gesture.Pan()` on the rail, `activeOffsetX ±8`, `failOffsetY ±12`.** A
|
|
94
|
+
vertical scroll that starts on the rail fails the pan first, so the control
|
|
95
|
+
survives inside a vertical `ScrollView`. A cancelled pan returns home.
|
|
96
|
+
- **RTL flips with `I18nManager.isRTL`.** Every positioned part uses logical
|
|
97
|
+
insets (`start-*`), the translation and the drag are multiplied by `-1`, and
|
|
98
|
+
the chevron becomes `IconChevronLeft`.
|
|
99
|
+
- **Haptics are off by default.** `haptic` names the tick played the first time
|
|
100
|
+
a drag crosses the threshold (re-armed if it drops back under); a `success`
|
|
101
|
+
knock follows on commit. Both are worklet calls, in the frame that earned them.
|
|
102
|
+
- **Reduced motion keeps the travel.** Springs become a 180ms `withTiming`. The
|
|
103
|
+
handle still moves to the end: a confirmation control that showed nothing on
|
|
104
|
+
confirming would be broken.
|
|
105
|
+
- **The chevron crosses into a tick** — opacity and a quarter turn, 180ms, off
|
|
106
|
+
one shared value. Children on `SlideButton.Thumb` replace both marks.
|
|
107
|
+
- **Screen readers confirm with one action.** The rail is a `button` whose label
|
|
108
|
+
is the `SlideButton.Label` text (or an explicit `accessibilityLabel`), with an
|
|
109
|
+
`activate` action labelled by `accessibilityActionLabel` — default "Confirm";
|
|
110
|
+
pass an imperative that names the outcome, "Ship order". Activating moves the
|
|
111
|
+
handle to the end and fires `onComplete`. `accessibilityState` carries
|
|
112
|
+
`disabled` and `checked: isCompleted`.
|
|
113
|
+
- **The thumb is drawn last however it was written**, and composed in when the
|
|
114
|
+
children hold none — `Switch`'s rule. Part detection checks `displayName` as
|
|
115
|
+
well as identity, for the reason `Switch` documents.
|
|
116
|
+
|
|
117
|
+
## Out of scope
|
|
118
|
+
|
|
119
|
+
- A `primary` variant — see above.
|
|
120
|
+
- A vertical slide.
|
|
121
|
+
- A label that changes on completion. The caller swaps the label's text from
|
|
122
|
+
`isCompleted` if they want one.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export { SlideButton, type SlideButtonProps } from "./slide-button";
|
|
2
|
+
export {
|
|
3
|
+
type SlideButtonContextValue,
|
|
4
|
+
SlideButtonProvider,
|
|
5
|
+
useSlideButton,
|
|
6
|
+
useSlideButtonContext,
|
|
7
|
+
} from "./slide-button.context";
|
|
8
|
+
export {
|
|
9
|
+
isSlideArmed,
|
|
10
|
+
resolveSlideHandleWidth,
|
|
11
|
+
resolveSlideLabelGutter,
|
|
12
|
+
resolveSlideRelease,
|
|
13
|
+
resolveSlideThreshold,
|
|
14
|
+
resolveSlideTrailOpacity,
|
|
15
|
+
resolveSlideTrailWidth,
|
|
16
|
+
resolveSlideTravel,
|
|
17
|
+
SLIDE_BUTTON_ACTIVE_OFFSET_X,
|
|
18
|
+
SLIDE_BUTTON_ARM_SLACK,
|
|
19
|
+
SLIDE_BUTTON_DEFAULT_AUTO_RESET_MS,
|
|
20
|
+
SLIDE_BUTTON_DEFAULT_SIZE,
|
|
21
|
+
SLIDE_BUTTON_DEFAULT_THRESHOLD,
|
|
22
|
+
SLIDE_BUTTON_DEFAULT_VARIANT,
|
|
23
|
+
SLIDE_BUTTON_FAIL_OFFSET_Y,
|
|
24
|
+
SLIDE_BUTTON_FAR_END_SLOP,
|
|
25
|
+
SLIDE_BUTTON_GLYPH_MS,
|
|
26
|
+
SLIDE_BUTTON_GLYPH_TOKEN,
|
|
27
|
+
SLIDE_BUTTON_HANDLE_RATIO,
|
|
28
|
+
SLIDE_BUTTON_INSET,
|
|
29
|
+
SLIDE_BUTTON_LOOKAHEAD,
|
|
30
|
+
SLIDE_BUTTON_MIN_THRESHOLD,
|
|
31
|
+
SLIDE_BUTTON_REDUCED_MOTION_MS,
|
|
32
|
+
SLIDE_BUTTON_SIZES,
|
|
33
|
+
SLIDE_BUTTON_SPRING,
|
|
34
|
+
SLIDE_BUTTON_TRAIL_FADE,
|
|
35
|
+
SLIDE_BUTTON_VARIANTS,
|
|
36
|
+
type SlideButtonSize,
|
|
37
|
+
type SlideButtonVariant,
|
|
38
|
+
type SlideButtonVariantProps,
|
|
39
|
+
type SlideRelease,
|
|
40
|
+
slideButtonVariants,
|
|
41
|
+
} from "./slide-button.variants";
|
|
42
|
+
export type { SlideButtonLabelProps } from "./slide-button-label";
|
|
43
|
+
export type { SlideButtonThumbProps } from "./slide-button-thumb";
|