@delacour/react-native-ui 0.1.0-alpha.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/package.json +49 -5
- package/src/components/accordion/AGENTS.md +21 -9
- package/src/components/accordion/accordion-content.tsx +7 -4
- package/src/components/accordion/accordion-item.tsx +15 -13
- package/src/components/accordion/accordion.context.tsx +11 -5
- package/src/components/accordion/accordion.variants.test.ts +64 -3
- package/src/components/accordion/accordion.variants.ts +33 -9
- package/src/components/alert/AGENTS.md +102 -0
- package/src/components/alert/alert-action.tsx +17 -0
- package/src/components/alert/alert-close-button.tsx +50 -0
- package/src/components/alert/alert-content.tsx +15 -0
- package/src/components/alert/alert-description.tsx +16 -0
- package/src/components/alert/alert-glyphs.ts +25 -0
- package/src/components/alert/alert-indicator.tsx +40 -0
- package/src/components/alert/alert-title.tsx +18 -0
- package/src/components/alert/alert.context.tsx +69 -0
- package/src/components/alert/alert.tsx +160 -0
- package/src/components/alert/alert.types.ts +10 -0
- package/src/components/alert/alert.variants.test.ts +218 -0
- package/src/components/alert/alert.variants.ts +180 -0
- package/src/components/alert/index.ts +23 -0
- package/src/components/avatar/AGENTS.md +106 -0
- package/src/components/avatar/avatar-badge.tsx +51 -0
- package/src/components/avatar/avatar-group.tsx +139 -0
- package/src/components/avatar/avatar.context.tsx +93 -0
- package/src/components/avatar/avatar.tsx +201 -0
- package/src/components/avatar/avatar.utils.test.ts +174 -0
- package/src/components/avatar/avatar.utils.ts +141 -0
- package/src/components/avatar/avatar.variants.test.ts +251 -0
- package/src/components/avatar/avatar.variants.ts +270 -0
- package/src/components/avatar/index.ts +39 -0
- package/src/components/badge/AGENTS.md +5 -0
- package/src/components/badge/badge.tsx +6 -1
- package/src/components/bottom-sheet/AGENTS.md +186 -221
- package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
- package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
- package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -207
- package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
- package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
- package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
- package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -120
- package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
- package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
- package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
- package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
- package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -52
- package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
- package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
- package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
- package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
- package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
- package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
- package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -70
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -105
- package/src/components/bottom-sheet/index.ts +39 -17
- package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
- package/src/components/button/AGENTS.md +45 -11
- package/src/components/button/button-group.tsx +44 -8
- package/src/components/button/button.context.tsx +10 -0
- package/src/components/button/button.tsx +2 -1
- package/src/components/button/button.variants.test.ts +154 -0
- package/src/components/button/button.variants.ts +71 -6
- package/src/components/button/index.ts +2 -0
- package/src/components/calendar/AGENTS.md +142 -0
- package/src/components/calendar/calendar-caption.tsx +116 -0
- package/src/components/calendar/calendar-day.tsx +103 -0
- package/src/components/calendar/calendar-grid.tsx +183 -0
- package/src/components/calendar/calendar-header.tsx +34 -0
- package/src/components/calendar/calendar-nav.tsx +51 -0
- package/src/components/calendar/calendar-picker.tsx +128 -0
- package/src/components/calendar/calendar-weekdays.tsx +48 -0
- package/src/components/calendar/calendar.context.tsx +108 -0
- package/src/components/calendar/calendar.date.test.ts +456 -0
- package/src/components/calendar/calendar.date.ts +584 -0
- package/src/components/calendar/calendar.tsx +389 -0
- package/src/components/calendar/calendar.types.ts +87 -0
- package/src/components/calendar/calendar.variants.test.ts +363 -0
- package/src/components/calendar/calendar.variants.ts +426 -0
- package/src/components/calendar/index.ts +96 -0
- package/src/components/card/AGENTS.md +88 -0
- package/src/components/card/card-action.tsx +16 -0
- package/src/components/card/card-content.tsx +12 -0
- package/src/components/card/card-description.tsx +12 -0
- package/src/components/card/card-footer.tsx +30 -0
- package/src/components/card/card-header.tsx +45 -0
- package/src/components/card/card-title.tsx +25 -0
- package/src/components/card/card.context.tsx +64 -0
- package/src/components/card/card.tsx +94 -0
- package/src/components/card/card.types.ts +13 -0
- package/src/components/card/card.variants.test.ts +240 -0
- package/src/components/card/card.variants.ts +177 -0
- package/src/components/card/index.ts +15 -0
- package/src/components/chart/AGENTS.md +9 -1
- package/src/components/chart/chart.tsx +8 -1
- package/src/components/chip/AGENTS.md +113 -0
- package/src/components/chip/chip-close-button.tsx +48 -0
- package/src/components/chip/chip-end-content.tsx +15 -0
- package/src/components/chip/chip-label.tsx +21 -0
- package/src/components/chip/chip-start-content.tsx +16 -0
- package/src/components/chip/chip.context.tsx +66 -0
- package/src/components/chip/chip.tsx +268 -0
- package/src/components/chip/chip.types.ts +10 -0
- package/src/components/chip/chip.variants.test.ts +335 -0
- package/src/components/chip/chip.variants.ts +321 -0
- package/src/components/chip/index.ts +24 -0
- package/src/components/collapsible/AGENTS.md +97 -0
- package/src/components/collapsible/collapsible-content.tsx +97 -0
- package/src/components/collapsible/collapsible-description.tsx +17 -0
- package/src/components/collapsible/collapsible-indicator.tsx +92 -0
- package/src/components/collapsible/collapsible-title.tsx +17 -0
- package/src/components/collapsible/collapsible-trigger.tsx +133 -0
- package/src/components/collapsible/collapsible.context.tsx +91 -0
- package/src/components/collapsible/collapsible.tsx +167 -0
- package/src/components/collapsible/collapsible.types.ts +9 -0
- package/src/components/collapsible/collapsible.variants.test.ts +338 -0
- package/src/components/collapsible/collapsible.variants.ts +228 -0
- package/src/components/collapsible/index.ts +32 -0
- package/src/components/context-menu/AGENTS.md +103 -0
- package/src/components/context-menu/context-menu-content.tsx +64 -0
- package/src/components/context-menu/context-menu-item.tsx +22 -0
- package/src/components/context-menu/context-menu-preview.tsx +60 -0
- package/src/components/context-menu/context-menu-trigger.tsx +160 -0
- package/src/components/context-menu/context-menu.context.tsx +91 -0
- package/src/components/context-menu/context-menu.tsx +90 -0
- package/src/components/context-menu/context-menu.variants.test.ts +266 -0
- package/src/components/context-menu/context-menu.variants.ts +147 -0
- package/src/components/context-menu/index.ts +18 -0
- package/src/components/dialog/AGENTS.md +97 -0
- package/src/components/dialog/dialog-body.tsx +21 -0
- package/src/components/dialog/dialog-close.tsx +82 -0
- package/src/components/dialog/dialog-content.tsx +153 -0
- package/src/components/dialog/dialog-description.tsx +23 -0
- package/src/components/dialog/dialog-footer.tsx +32 -0
- package/src/components/dialog/dialog-header.tsx +20 -0
- package/src/components/dialog/dialog-title.tsx +42 -0
- package/src/components/dialog/dialog-trigger.tsx +54 -0
- package/src/components/dialog/dialog.context.tsx +90 -0
- package/src/components/dialog/dialog.tsx +119 -0
- package/src/components/dialog/dialog.variants.test.ts +224 -0
- package/src/components/dialog/dialog.variants.ts +173 -0
- package/src/components/dialog/index.ts +30 -0
- package/src/components/drawer/AGENTS.md +123 -0
- package/src/components/drawer/drawer-body.tsx +46 -0
- package/src/components/drawer/drawer-close.tsx +76 -0
- package/src/components/drawer/drawer-content.tsx +186 -0
- package/src/components/drawer/drawer-description.tsx +23 -0
- package/src/components/drawer/drawer-footer.tsx +21 -0
- package/src/components/drawer/drawer-header.tsx +42 -0
- package/src/components/drawer/drawer-title.tsx +35 -0
- package/src/components/drawer/drawer-trigger.tsx +54 -0
- package/src/components/drawer/drawer.context.tsx +114 -0
- package/src/components/drawer/drawer.tsx +119 -0
- package/src/components/drawer/drawer.variants.test.ts +392 -0
- package/src/components/drawer/drawer.variants.ts +244 -0
- package/src/components/drawer/index.ts +41 -0
- package/src/components/drawer/use-drawer-pan.ts +108 -0
- package/src/components/empty-state/AGENTS.md +90 -0
- package/src/components/empty-state/empty-state-content.tsx +17 -0
- package/src/components/empty-state/empty-state-description.tsx +12 -0
- package/src/components/empty-state/empty-state-header.tsx +17 -0
- package/src/components/empty-state/empty-state-media.tsx +56 -0
- package/src/components/empty-state/empty-state-title.tsx +18 -0
- package/src/components/empty-state/empty-state.context.tsx +65 -0
- package/src/components/empty-state/empty-state.tsx +84 -0
- package/src/components/empty-state/empty-state.types.ts +14 -0
- package/src/components/empty-state/empty-state.variants.test.ts +245 -0
- package/src/components/empty-state/empty-state.variants.ts +132 -0
- package/src/components/empty-state/index.ts +20 -0
- package/src/components/fab/AGENTS.md +119 -0
- package/src/components/fab/fab-action.tsx +118 -0
- package/src/components/fab/fab-group.tsx +214 -0
- package/src/components/fab/fab-label.tsx +18 -0
- package/src/components/fab/fab.context.tsx +125 -0
- package/src/components/fab/fab.tsx +156 -0
- package/src/components/fab/fab.types.ts +17 -0
- package/src/components/fab/fab.variants.test.ts +274 -0
- package/src/components/fab/fab.variants.ts +234 -0
- package/src/components/fab/index.ts +37 -0
- package/src/components/feedback/AGENTS.md +95 -0
- package/src/components/feedback/feedback-action.tsx +16 -0
- package/src/components/feedback/feedback-cancel.tsx +34 -0
- package/src/components/feedback/feedback-close.tsx +21 -0
- package/src/components/feedback/feedback-content.tsx +24 -0
- package/src/components/feedback/feedback-field.tsx +70 -0
- package/src/components/feedback/feedback-footer.tsx +21 -0
- package/src/components/feedback/feedback-panel.tsx +54 -0
- package/src/components/feedback/feedback-submit.tsx +74 -0
- package/src/components/feedback/feedback-title.tsx +34 -0
- package/src/components/feedback/feedback-trigger.tsx +20 -0
- package/src/components/feedback/feedback.context.tsx +73 -0
- package/src/components/feedback/feedback.tsx +135 -0
- package/src/components/feedback/feedback.variants.test.ts +185 -0
- package/src/components/feedback/feedback.variants.ts +128 -0
- package/src/components/feedback/index.ts +24 -0
- package/src/components/field/AGENTS.md +19 -0
- package/src/components/field/field-label.tsx +19 -5
- package/src/components/field/field.context.tsx +17 -0
- package/src/components/field/field.tsx +15 -2
- package/src/components/field/field.variants.test.ts +24 -0
- package/src/components/field/field.variants.ts +20 -0
- package/src/components/field/index.ts +1 -0
- package/src/components/item/AGENTS.md +92 -0
- package/src/components/item/index.ts +25 -0
- package/src/components/item/item-actions.tsx +28 -0
- package/src/components/item/item-content.tsx +15 -0
- package/src/components/item/item-description.tsx +12 -0
- package/src/components/item/item-footer.tsx +13 -0
- package/src/components/item/item-group.tsx +32 -0
- package/src/components/item/item-header.tsx +13 -0
- package/src/components/item/item-media.tsx +37 -0
- package/src/components/item/item-separator.tsx +13 -0
- package/src/components/item/item-title.tsx +17 -0
- package/src/components/item/item.context.tsx +66 -0
- package/src/components/item/item.tsx +218 -0
- package/src/components/item/item.types.ts +12 -0
- package/src/components/item/item.variants.test.ts +263 -0
- package/src/components/item/item.variants.ts +200 -0
- package/src/components/kpi/AGENTS.md +125 -0
- package/src/components/kpi/index.ts +45 -0
- package/src/components/kpi/kpi-action.tsx +12 -0
- package/src/components/kpi/kpi-content.tsx +35 -0
- package/src/components/kpi/kpi-footer.tsx +52 -0
- package/src/components/kpi/kpi-group.tsx +70 -0
- package/src/components/kpi/kpi-header.tsx +25 -0
- package/src/components/kpi/kpi-icon.tsx +28 -0
- package/src/components/kpi/kpi-sparkline.tsx +145 -0
- package/src/components/kpi/kpi-stat.tsx +20 -0
- package/src/components/kpi/kpi-title.tsx +19 -0
- package/src/components/kpi/kpi-trend.tsx +105 -0
- package/src/components/kpi/kpi-value.tsx +32 -0
- package/src/components/kpi/kpi.context.tsx +111 -0
- package/src/components/kpi/kpi.tsx +154 -0
- package/src/components/kpi/kpi.types.ts +9 -0
- package/src/components/kpi/kpi.variants.test.ts +428 -0
- package/src/components/kpi/kpi.variants.ts +347 -0
- package/src/components/label/AGENTS.md +89 -0
- package/src/components/label/index.ts +10 -0
- package/src/components/label/label.tsx +97 -0
- package/src/components/label/label.variants.test.ts +176 -0
- package/src/components/label/label.variants.ts +123 -0
- package/src/components/menu/AGENTS.md +176 -0
- package/src/components/menu/index.ts +44 -0
- package/src/components/menu/menu-background.tsx +17 -0
- package/src/components/menu/menu-checkbox-item.tsx +57 -0
- package/src/components/menu/menu-content.tsx +303 -0
- package/src/components/menu/menu-item.tsx +162 -0
- package/src/components/menu/menu-label.tsx +14 -0
- package/src/components/menu/menu-radio-group.tsx +50 -0
- package/src/components/menu/menu-radio-item.tsx +57 -0
- package/src/components/menu/menu-separator.tsx +11 -0
- package/src/components/menu/menu-sub-content.tsx +72 -0
- package/src/components/menu/menu-sub-trigger.tsx +50 -0
- package/src/components/menu/menu-sub.tsx +75 -0
- package/src/components/menu/menu-trigger.tsx +57 -0
- package/src/components/menu/menu.context.tsx +214 -0
- package/src/components/menu/menu.tsx +88 -0
- package/src/components/menu/menu.types.ts +33 -0
- package/src/components/menu/menu.variants.test.ts +400 -0
- package/src/components/menu/menu.variants.ts +353 -0
- package/src/components/meter/AGENTS.md +135 -0
- package/src/components/meter/index.ts +43 -0
- package/src/components/meter/meter-fill.tsx +19 -0
- package/src/components/meter/meter-header.tsx +17 -0
- package/src/components/meter/meter-label.tsx +18 -0
- package/src/components/meter/meter-output.tsx +42 -0
- package/src/components/meter/meter-segments.tsx +83 -0
- package/src/components/meter/meter-track.tsx +16 -0
- package/src/components/meter/meter.context.tsx +62 -0
- package/src/components/meter/meter.tsx +286 -0
- package/src/components/meter/meter.types.ts +30 -0
- package/src/components/meter/meter.variants.test.ts +430 -0
- package/src/components/meter/meter.variants.ts +347 -0
- package/src/components/overlay/AGENTS.md +89 -0
- package/src/components/overlay/index.ts +27 -0
- package/src/components/overlay/overlay-portal.tsx +84 -0
- package/src/components/overlay/overlay-presence.test.ts +46 -0
- package/src/components/overlay/overlay-presence.ts +49 -0
- package/src/components/overlay/overlay-registry.test.ts +96 -0
- package/src/components/overlay/overlay-registry.ts +99 -0
- package/src/components/overlay/overlay-scrim.tsx +49 -0
- package/src/components/overlay/overlay.context.tsx +48 -0
- package/src/components/overlay/overlay.tsx +118 -0
- package/src/components/overlay/overlay.variants.test.ts +37 -0
- package/src/components/overlay/overlay.variants.ts +33 -0
- package/src/components/overlay/use-overlay-back-handler.ts +34 -0
- package/src/components/overlay/use-overlay-presence.ts +87 -0
- package/src/components/popover/AGENTS.md +138 -0
- package/src/components/popover/index.ts +48 -0
- package/src/components/popover/popover-anchor.tsx +49 -0
- package/src/components/popover/popover-arrow.tsx +89 -0
- package/src/components/popover/popover-close.tsx +73 -0
- package/src/components/popover/popover-content.tsx +235 -0
- package/src/components/popover/popover-description.tsx +18 -0
- package/src/components/popover/popover-title.tsx +33 -0
- package/src/components/popover/popover-trigger.tsx +56 -0
- package/src/components/popover/popover.context.tsx +71 -0
- package/src/components/popover/popover.position.test.ts +354 -0
- package/src/components/popover/popover.position.ts +286 -0
- package/src/components/popover/popover.tsx +123 -0
- package/src/components/popover/popover.variants.test.ts +135 -0
- package/src/components/popover/popover.variants.ts +85 -0
- package/src/components/popover/use-anchor-measure.ts +76 -0
- package/src/components/popover/use-anchored-content.ts +219 -0
- package/src/components/pressable/AGENTS.md +11 -1
- package/src/components/pressable/pressable.tsx +7 -2
- package/src/components/pressable/pressable.variants.test.ts +29 -0
- package/src/components/pressable/pressable.variants.ts +17 -0
- package/src/components/progress/AGENTS.md +120 -0
- package/src/components/progress/index.ts +40 -0
- package/src/components/progress/progress-fill.tsx +152 -0
- package/src/components/progress/progress-header.tsx +19 -0
- package/src/components/progress/progress-label.tsx +27 -0
- package/src/components/progress/progress-output.tsx +47 -0
- package/src/components/progress/progress-track.tsx +38 -0
- package/src/components/progress/progress.context.tsx +79 -0
- package/src/components/progress/progress.tsx +171 -0
- package/src/components/progress/progress.types.ts +23 -0
- package/src/components/progress/progress.variants.test.ts +335 -0
- package/src/components/progress/progress.variants.ts +284 -0
- package/src/components/progress-button/AGENTS.md +116 -0
- package/src/components/progress-button/index.ts +36 -0
- package/src/components/progress-button/progress-button-done.tsx +33 -0
- package/src/components/progress-button/progress-button-label.tsx +27 -0
- package/src/components/progress-button/progress-button.context.tsx +93 -0
- package/src/components/progress-button/progress-button.tsx +414 -0
- package/src/components/progress-button/progress-button.variants.test.ts +314 -0
- package/src/components/progress-button/progress-button.variants.ts +236 -0
- package/src/components/provider/AGENTS.md +38 -25
- package/src/components/provider/provider.tsx +38 -11
- package/src/components/radio/AGENTS.md +8 -10
- package/src/components/radio/radio.variants.test.ts +5 -6
- package/src/components/radio/radio.variants.ts +6 -7
- package/src/components/rating/AGENTS.md +143 -0
- package/src/components/rating/index.ts +43 -0
- package/src/components/rating/rating-output.tsx +39 -0
- package/src/components/rating/rating-star.tsx +70 -0
- package/src/components/rating/rating-stars.tsx +219 -0
- package/src/components/rating/rating.context.tsx +79 -0
- package/src/components/rating/rating.tsx +163 -0
- package/src/components/rating/rating.variants.test.ts +347 -0
- package/src/components/rating/rating.variants.ts +354 -0
- package/src/components/screen/screen-navbar-back-button.tsx +1 -1
- package/src/components/selection-mode/AGENTS.md +155 -0
- package/src/components/selection-mode/index.ts +43 -0
- package/src/components/selection-mode/selection-mode-action.tsx +65 -0
- package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
- package/src/components/selection-mode/selection-mode-group.tsx +123 -0
- package/src/components/selection-mode/selection-mode-header.tsx +85 -0
- package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
- package/src/components/selection-mode/selection-mode-item.tsx +182 -0
- package/src/components/selection-mode/selection-mode.context.tsx +89 -0
- package/src/components/selection-mode/selection-mode.tsx +207 -0
- package/src/components/selection-mode/selection-mode.types.ts +36 -0
- package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
- package/src/components/selection-mode/selection-mode.variants.ts +329 -0
- package/src/components/skeleton/AGENTS.md +120 -0
- package/src/components/skeleton/index.ts +15 -0
- package/src/components/skeleton/skeleton-clock.ts +48 -0
- package/src/components/skeleton/skeleton-group.tsx +64 -0
- package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
- package/src/components/skeleton/skeleton.context.tsx +45 -0
- package/src/components/skeleton/skeleton.tsx +227 -0
- package/src/components/skeleton/skeleton.variants.test.ts +245 -0
- package/src/components/skeleton/skeleton.variants.ts +222 -0
- package/src/components/slide-button/AGENTS.md +122 -0
- package/src/components/slide-button/index.ts +43 -0
- package/src/components/slide-button/slide-button-label.tsx +70 -0
- package/src/components/slide-button/slide-button-thumb.tsx +93 -0
- package/src/components/slide-button/slide-button.context.tsx +76 -0
- package/src/components/slide-button/slide-button.tsx +457 -0
- package/src/components/slide-button/slide-button.variants.test.ts +288 -0
- package/src/components/slide-button/slide-button.variants.ts +324 -0
- package/src/components/slider/index.ts +2 -0
- package/src/components/slider/slider-thumb.tsx +33 -1
- package/src/components/slider/slider.variants.test.ts +52 -0
- package/src/components/slider/slider.variants.ts +56 -2
- package/src/components/stack-card/AGENTS.md +169 -0
- package/src/components/stack-card/index.ts +37 -0
- package/src/components/stack-card/stack-card-action.tsx +66 -0
- package/src/components/stack-card/stack-card-actions.tsx +18 -0
- package/src/components/stack-card/stack-card-card.tsx +71 -0
- package/src/components/stack-card/stack-card-empty.tsx +30 -0
- package/src/components/stack-card/stack-card-slot.tsx +90 -0
- package/src/components/stack-card/stack-card-stamp.tsx +76 -0
- package/src/components/stack-card/stack-card.context.tsx +117 -0
- package/src/components/stack-card/stack-card.tsx +557 -0
- package/src/components/stack-card/stack-card.types.ts +26 -0
- package/src/components/stack-card/stack-card.variants.test.ts +355 -0
- package/src/components/stack-card/stack-card.variants.ts +360 -0
- package/src/components/steps/AGENTS.md +153 -0
- package/src/components/steps/index.ts +45 -0
- package/src/components/steps/steps-connector.tsx +31 -0
- package/src/components/steps/steps-description.tsx +26 -0
- package/src/components/steps/steps-indicator.tsx +106 -0
- package/src/components/steps/steps-item.tsx +225 -0
- package/src/components/steps/steps-panel.tsx +41 -0
- package/src/components/steps/steps-title.tsx +32 -0
- package/src/components/steps/steps-track.tsx +59 -0
- package/src/components/steps/steps.context.tsx +123 -0
- package/src/components/steps/steps.tsx +166 -0
- package/src/components/steps/steps.variants.test.ts +388 -0
- package/src/components/steps/steps.variants.ts +468 -0
- package/src/components/surface/AGENTS.md +72 -0
- package/src/components/surface/index.ts +15 -0
- package/src/components/surface/surface.context.tsx +54 -0
- package/src/components/surface/surface.tsx +70 -0
- package/src/components/surface/surface.variants.test.ts +180 -0
- package/src/components/surface/surface.variants.ts +133 -0
- package/src/components/swipe/AGENTS.md +140 -0
- package/src/components/swipe/index.ts +31 -0
- package/src/components/swipe/swipe-action.tsx +99 -0
- package/src/components/swipe/swipe-end.tsx +14 -0
- package/src/components/swipe/swipe-group.tsx +55 -0
- package/src/components/swipe/swipe-panel.tsx +55 -0
- package/src/components/swipe/swipe-start.tsx +15 -0
- package/src/components/swipe/swipe.context.tsx +135 -0
- package/src/components/swipe/swipe.tsx +393 -0
- package/src/components/swipe/swipe.types.ts +7 -0
- package/src/components/swipe/swipe.variants.test.ts +337 -0
- package/src/components/swipe/swipe.variants.ts +290 -0
- package/src/components/switch/switch.variants.ts +2 -2
- package/src/components/tabs/AGENTS.md +4 -3
- package/src/components/tabs/tabs.variants.test.ts +1 -1
- package/src/components/tabs/tabs.variants.ts +4 -6
- package/src/components/textarea/AGENTS.md +86 -0
- package/src/components/textarea/index.ts +16 -0
- package/src/components/textarea/textarea.tsx +157 -0
- package/src/components/textarea/textarea.variants.test.ts +219 -0
- package/src/components/textarea/textarea.variants.ts +171 -0
- package/src/components/toast/AGENTS.md +140 -0
- package/src/components/toast/index.ts +74 -0
- package/src/components/toast/toast-action.tsx +59 -0
- package/src/components/toast/toast-api.ts +32 -0
- package/src/components/toast/toast-close.tsx +44 -0
- package/src/components/toast/toast-content.tsx +11 -0
- package/src/components/toast/toast-description.tsx +11 -0
- package/src/components/toast/toast-indicator.tsx +34 -0
- package/src/components/toast/toast-item.tsx +308 -0
- package/src/components/toast/toast-title.tsx +13 -0
- package/src/components/toast/toast-viewport.tsx +135 -0
- package/src/components/toast/toast.context.tsx +69 -0
- package/src/components/toast/toast.store.test.ts +259 -0
- package/src/components/toast/toast.store.ts +338 -0
- package/src/components/toast/toast.timer.test.ts +64 -0
- package/src/components/toast/toast.timer.ts +51 -0
- package/src/components/toast/toast.tsx +132 -0
- package/src/components/toast/toast.variants.test.ts +393 -0
- package/src/components/toast/toast.variants.ts +304 -0
- package/src/components/toggle-button/AGENTS.md +106 -0
- package/src/components/toggle-button/index.ts +30 -0
- package/src/components/toggle-button/toggle-button-group.tsx +156 -0
- package/src/components/toggle-button/toggle-button-label.tsx +19 -0
- package/src/components/toggle-button/toggle-button.context.tsx +130 -0
- package/src/components/toggle-button/toggle-button.tsx +172 -0
- package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
- package/src/components/toggle-button/toggle-button.variants.ts +160 -0
- package/src/components/tooltip/AGENTS.md +133 -0
- package/src/components/tooltip/index.ts +28 -0
- package/src/components/tooltip/tooltip-arrow.tsx +40 -0
- package/src/components/tooltip/tooltip-content.tsx +179 -0
- package/src/components/tooltip/tooltip-description.tsx +19 -0
- package/src/components/tooltip/tooltip-text.tsx +21 -0
- package/src/components/tooltip/tooltip-title.tsx +20 -0
- package/src/components/tooltip/tooltip-trigger.tsx +103 -0
- package/src/components/tooltip/tooltip.context.tsx +68 -0
- package/src/components/tooltip/tooltip.tsx +213 -0
- package/src/components/tooltip/tooltip.variants.test.ts +231 -0
- package/src/components/tooltip/tooltip.variants.ts +177 -0
- package/src/hooks/use-calm-motion.tsx +31 -0
- package/src/lib/calm-motion.test.ts +17 -0
- package/src/lib/calm-motion.ts +23 -0
- package/src/lib/cn.test.ts +14 -0
- package/src/lib/cn.ts +2 -2
- package/src/no-gorhom.test.ts +54 -0
- package/src/styles/geometry.test.ts +1 -0
- package/src/styles/theme.css +4 -0
- package/src/styles/tokens.css +11 -0
- package/src/styles/tokens.test.ts +22 -0
- package/src/styles/tokens.ts +20 -0
- package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
- package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { declarationCount } from "../../styles/theme-tokens.test";
|
|
3
|
+
import { SLIDER_COLORS, SLIDER_OUTPUT_TEXT_SIZE, SLIDER_SIZES } from "../slider/slider.variants";
|
|
4
|
+
import { TEXT_SIZES } from "../text/text.variants";
|
|
5
|
+
import {
|
|
6
|
+
fillTranslate,
|
|
7
|
+
formatProgressValue,
|
|
8
|
+
indeterminateSegment,
|
|
9
|
+
PROGRESS_COLORS,
|
|
10
|
+
PROGRESS_DEFAULT_COLOR,
|
|
11
|
+
PROGRESS_DEFAULT_SIZE,
|
|
12
|
+
PROGRESS_FILL_DURATION_MS,
|
|
13
|
+
PROGRESS_INDETERMINATE_DURATION_MS,
|
|
14
|
+
PROGRESS_INDETERMINATE_SEGMENT,
|
|
15
|
+
PROGRESS_MAX_VALUE,
|
|
16
|
+
PROGRESS_MIN_VALUE,
|
|
17
|
+
PROGRESS_OUTPUT_TEXT_SIZE,
|
|
18
|
+
PROGRESS_PULSE,
|
|
19
|
+
PROGRESS_SIZES,
|
|
20
|
+
progressRatio,
|
|
21
|
+
progressVariants,
|
|
22
|
+
resolveProgressAccessibility,
|
|
23
|
+
resolveProgressAxes,
|
|
24
|
+
} from "./progress.variants";
|
|
25
|
+
|
|
26
|
+
/** The bare colour a `bg-*` class names — `bg-success` yields `success`. */
|
|
27
|
+
function backgroundToken(value: string): string | undefined {
|
|
28
|
+
return value.match(/\bbg-([\w-]+)\b/)?.[1];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A slot's class string, with `tv`'s empty-slot `undefined` flattened. */
|
|
32
|
+
function cls(value: string | undefined): string {
|
|
33
|
+
return value ?? "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The `h-*` step a class string sets — `h-1.5` yields 1.5. */
|
|
37
|
+
function heightStep(value: string): number {
|
|
38
|
+
return Number(value.match(/\bh-(\d+(?:\.\d+)?)\b/)?.[1]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("the theme reader", () => {
|
|
42
|
+
// Guard: every token assertion below is worthless if the parse found nothing.
|
|
43
|
+
test("finds both variants of the theme", () => {
|
|
44
|
+
expect(declarationCount("primary")).toBe(2);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("the axes", () => {
|
|
49
|
+
// A meter stacked on this bar, and a slider beside it, have to be able to
|
|
50
|
+
// name the same colour and the same size with the same word.
|
|
51
|
+
test("share the slider's six colours and three sizes", () => {
|
|
52
|
+
expect([...PROGRESS_COLORS]).toEqual([...SLIDER_COLORS]);
|
|
53
|
+
expect([...PROGRESS_SIZES]).toEqual([...SLIDER_SIZES]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("default to values the axes actually hold", () => {
|
|
57
|
+
expect(PROGRESS_COLORS).toContain(PROGRESS_DEFAULT_COLOR);
|
|
58
|
+
expect(PROGRESS_SIZES).toContain(PROGRESS_DEFAULT_SIZE);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("default the range to 0–100", () => {
|
|
62
|
+
expect(PROGRESS_MIN_VALUE).toBe(0);
|
|
63
|
+
expect(PROGRESS_MAX_VALUE).toBe(100);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("resolveProgressAxes", () => {
|
|
68
|
+
test("falls back to the defaults", () => {
|
|
69
|
+
expect(resolveProgressAxes({})).toEqual({
|
|
70
|
+
color: PROGRESS_DEFAULT_COLOR,
|
|
71
|
+
isIndeterminate: false,
|
|
72
|
+
size: PROGRESS_DEFAULT_SIZE,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("keeps what the call site named", () => {
|
|
77
|
+
expect(resolveProgressAxes({ color: "success", isIndeterminate: true, size: "lg" })).toEqual({
|
|
78
|
+
color: "success",
|
|
79
|
+
isIndeterminate: true,
|
|
80
|
+
size: "lg",
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// The progress bar is not a form control, so an enclosing Field has nothing
|
|
85
|
+
// to hand it. The resolver takes no second argument, and this pins that.
|
|
86
|
+
test("takes the call site alone", () => {
|
|
87
|
+
expect(resolveProgressAxes.length).toBe(1);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("progressVariants", () => {
|
|
92
|
+
test("paints the fill differently for every colour", () => {
|
|
93
|
+
const painted = new Set(PROGRESS_COLORS.map((color) => cls(progressVariants({ color }).fill())));
|
|
94
|
+
expect(painted.size).toBe(PROGRESS_COLORS.length);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("never paints the track in a colour", () => {
|
|
98
|
+
const tracks = new Set(PROGRESS_COLORS.map((color) => cls(progressVariants({ color }).track())));
|
|
99
|
+
expect(tracks.size).toBe(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("names only tokens both variants of the theme declare", () => {
|
|
103
|
+
const tokens = [
|
|
104
|
+
backgroundToken(cls(progressVariants({}).track())),
|
|
105
|
+
...PROGRESS_COLORS.map((color) => backgroundToken(cls(progressVariants({ color }).fill()))),
|
|
106
|
+
];
|
|
107
|
+
for (const token of tokens) {
|
|
108
|
+
expect(token).toBeDefined();
|
|
109
|
+
expect(declarationCount(token as string)).toBe(2);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("never paints the fill in the track's own colour", () => {
|
|
114
|
+
const track = backgroundToken(cls(progressVariants({}).track()));
|
|
115
|
+
for (const color of PROGRESS_COLORS) {
|
|
116
|
+
expect(backgroundToken(cls(progressVariants({ color }).fill()))).not.toBe(track);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("thickens the track as the size steps up", () => {
|
|
121
|
+
const heights = PROGRESS_SIZES.map((size) => heightStep(cls(progressVariants({ size }).track())));
|
|
122
|
+
for (const height of heights) expect(Number.isFinite(height)).toBe(true);
|
|
123
|
+
expect([...heights].sort((a, b) => a - b)).toEqual(heights);
|
|
124
|
+
expect(new Set(heights).size).toBe(heights.length);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// The fill slides under the track's rounded clip; a track that stops clipping
|
|
128
|
+
// shows the fill parked to its left at every value below the maximum.
|
|
129
|
+
test("clips the fill to the track", () => {
|
|
130
|
+
expect(cls(progressVariants({}).track())).toMatch(/\boverflow-hidden\b/);
|
|
131
|
+
expect(cls(progressVariants({}).fill())).toMatch(/\babsolute\b/);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// No slot worn by a View carries type — the readout names a Text step instead.
|
|
135
|
+
test("puts no text styling on a View slot", () => {
|
|
136
|
+
for (const size of PROGRESS_SIZES) {
|
|
137
|
+
const slots = progressVariants({ size });
|
|
138
|
+
for (const slot of [slots.root(), slots.track(), slots.fill()]) {
|
|
139
|
+
expect(cls(slot)).not.toMatch(/\b(text|font)-/);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("PROGRESS_OUTPUT_TEXT_SIZE", () => {
|
|
146
|
+
test("names a size Text actually has, for every progress size", () => {
|
|
147
|
+
for (const size of PROGRESS_SIZES) {
|
|
148
|
+
expect(TEXT_SIZES).toContain(PROGRESS_OUTPUT_TEXT_SIZE[size]);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// A progress bar under a slider in one form reads at the same type step.
|
|
153
|
+
test("matches the slider's readout", () => {
|
|
154
|
+
expect(PROGRESS_OUTPUT_TEXT_SIZE).toEqual(SLIDER_OUTPUT_TEXT_SIZE);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("the motion constants", () => {
|
|
159
|
+
test("are positive and ordered the way the eye reads them", () => {
|
|
160
|
+
expect(PROGRESS_FILL_DURATION_MS).toBeGreaterThan(0);
|
|
161
|
+
expect(PROGRESS_INDETERMINATE_DURATION_MS).toBeGreaterThan(PROGRESS_FILL_DURATION_MS);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("keep the indeterminate segment a strict fraction of the track", () => {
|
|
165
|
+
expect(PROGRESS_INDETERMINATE_SEGMENT).toBeGreaterThan(0);
|
|
166
|
+
expect(PROGRESS_INDETERMINATE_SEGMENT).toBeLessThan(1);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// A pulse that dims to nothing reads as the bar disappearing, not breathing.
|
|
170
|
+
test("never pulses to fully transparent", () => {
|
|
171
|
+
expect(PROGRESS_PULSE.from).toBeGreaterThan(0);
|
|
172
|
+
expect(PROGRESS_PULSE.to).toBe(1);
|
|
173
|
+
expect(PROGRESS_PULSE.from).toBeLessThan(PROGRESS_PULSE.to);
|
|
174
|
+
expect(PROGRESS_PULSE.durationMs).toBeGreaterThan(0);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("progressRatio", () => {
|
|
179
|
+
test("maps the range onto 0–1", () => {
|
|
180
|
+
expect(progressRatio(0, 0, 100)).toBe(0);
|
|
181
|
+
expect(progressRatio(25, 0, 100)).toBe(0.25);
|
|
182
|
+
expect(progressRatio(100, 0, 100)).toBe(1);
|
|
183
|
+
expect(progressRatio(18, 0, 24)).toBe(0.75);
|
|
184
|
+
expect(progressRatio(15, 10, 20)).toBe(0.5);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("clamps rather than extrapolating", () => {
|
|
188
|
+
expect(progressRatio(-5, 0, 100)).toBe(0);
|
|
189
|
+
expect(progressRatio(150, 0, 100)).toBe(1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// NaN in a shared value freezes the fill for good — no later frame recovers.
|
|
193
|
+
test("returns 0 for a degenerate range or a value that is not a number", () => {
|
|
194
|
+
expect(progressRatio(5, 10, 10)).toBe(0);
|
|
195
|
+
expect(progressRatio(5, 20, 10)).toBe(0);
|
|
196
|
+
expect(progressRatio(Number.NaN, 0, 100)).toBe(0);
|
|
197
|
+
expect(progressRatio(Number.POSITIVE_INFINITY, 0, 100)).toBe(0);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("fillTranslate", () => {
|
|
202
|
+
test("parks the full-length fill left of the track by what is still to go", () => {
|
|
203
|
+
expect(fillTranslate({ ratio: 0, trackSize: 200 })).toBe(-200);
|
|
204
|
+
expect(fillTranslate({ ratio: 0.25, trackSize: 200 })).toBe(-150);
|
|
205
|
+
expect(fillTranslate({ ratio: 1, trackSize: 200 })).toBe(0);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("clamps a ratio outside 0–1", () => {
|
|
209
|
+
expect(fillTranslate({ ratio: -1, trackSize: 200 })).toBe(-200);
|
|
210
|
+
expect(fillTranslate({ ratio: 2, trackSize: 200 })).toBe(0);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// An unmeasured track is 0 wide; the bar is hidden until it is measured.
|
|
214
|
+
test("is 0 for an unmeasured track", () => {
|
|
215
|
+
expect(fillTranslate({ ratio: 0.5, trackSize: 0 })).toBe(0);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("indeterminateSegment", () => {
|
|
220
|
+
test("sizes the segment as a fraction of the track", () => {
|
|
221
|
+
expect(indeterminateSegment({ phase: 0, segment: 0.4, trackSize: 200 }).width).toBe(80);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Both ends of the loop are off-track, so the seam where it repeats is never
|
|
225
|
+
// on screen.
|
|
226
|
+
test("starts wholly left of the track and ends wholly right of it", () => {
|
|
227
|
+
const start = indeterminateSegment({ phase: 0, segment: 0.4, trackSize: 200 });
|
|
228
|
+
const end = indeterminateSegment({ phase: 1, segment: 0.4, trackSize: 200 });
|
|
229
|
+
expect(start.translate + start.width).toBe(0);
|
|
230
|
+
expect(end.translate).toBe(200);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("moves monotonically", () => {
|
|
234
|
+
let last = Number.NEGATIVE_INFINITY;
|
|
235
|
+
for (let phase = 0; phase <= 1; phase += 0.1) {
|
|
236
|
+
const { translate } = indeterminateSegment({ phase, segment: 0.4, trackSize: 200 });
|
|
237
|
+
expect(translate).toBeGreaterThan(last);
|
|
238
|
+
last = translate;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("draws nothing on an unmeasured track", () => {
|
|
243
|
+
expect(indeterminateSegment({ phase: 0.5, segment: 0.4, trackSize: 0 })).toEqual({ translate: 0, width: 0 });
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("formatProgressValue", () => {
|
|
248
|
+
test("reads as a whole percentage of the range by default", () => {
|
|
249
|
+
expect(formatProgressValue({ maxValue: 100, minValue: 0, value: 72 })).toBe("72%");
|
|
250
|
+
expect(formatProgressValue({ maxValue: 24, minValue: 0, value: 18 })).toBe("75%");
|
|
251
|
+
expect(formatProgressValue({ maxValue: 100, minValue: 0, value: 33.4 })).toBe("33%");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("clamps the percentage the way the fill clamps", () => {
|
|
255
|
+
expect(formatProgressValue({ maxValue: 100, minValue: 0, value: 140 })).toBe("100%");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("formats the value itself when given a non-percent style", () => {
|
|
259
|
+
const formatted = formatProgressValue({
|
|
260
|
+
formatOptions: { currency: "NZD", maximumFractionDigits: 0, style: "currency" },
|
|
261
|
+
maxValue: 2000,
|
|
262
|
+
minValue: 0,
|
|
263
|
+
value: 1250,
|
|
264
|
+
});
|
|
265
|
+
expect(formatted).toMatch(/1,250/);
|
|
266
|
+
expect(formatted).not.toMatch(/%/);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// `style: "percent"` would print 7200% if it formatted the value, so a percent
|
|
270
|
+
// style still formats the ratio — with the caller's other options applied.
|
|
271
|
+
test("keeps formatting the ratio under a percent style", () => {
|
|
272
|
+
expect(
|
|
273
|
+
formatProgressValue({
|
|
274
|
+
formatOptions: { maximumFractionDigits: 1, style: "percent" },
|
|
275
|
+
maxValue: 1000,
|
|
276
|
+
minValue: 0,
|
|
277
|
+
value: 125,
|
|
278
|
+
})
|
|
279
|
+
).toBe("12.5%");
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("resolveProgressAccessibility", () => {
|
|
284
|
+
test("reports the range and the value for a determinate bar", () => {
|
|
285
|
+
expect(resolveProgressAccessibility({ isIndeterminate: false, maxValue: 100, minValue: 0, value: 72 })).toEqual({
|
|
286
|
+
busy: false,
|
|
287
|
+
value: { max: 100, min: 0, now: 72, text: "72%" },
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// "18 of 24" says more than "75%": the count is what the screen is about.
|
|
292
|
+
test("reads a count as value of maximum when the range is not 0–100", () => {
|
|
293
|
+
expect(
|
|
294
|
+
resolveProgressAccessibility({ isIndeterminate: false, maxValue: 24, minValue: 0, value: 18 }).value
|
|
295
|
+
).toEqual({ max: 24, min: 0, now: 18, text: "18 of 24" });
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("speaks a formatted value when the readout formats one", () => {
|
|
299
|
+
const { value } = resolveProgressAccessibility({
|
|
300
|
+
formatOptions: { currency: "NZD", maximumFractionDigits: 0, style: "currency" },
|
|
301
|
+
isIndeterminate: false,
|
|
302
|
+
maxValue: 2000,
|
|
303
|
+
minValue: 0,
|
|
304
|
+
value: 1250,
|
|
305
|
+
});
|
|
306
|
+
expect(value?.text).toMatch(/1,250 of .*2,000/);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("prefers the caller's own words", () => {
|
|
310
|
+
expect(
|
|
311
|
+
resolveProgressAccessibility({
|
|
312
|
+
isIndeterminate: false,
|
|
313
|
+
maxValue: 10,
|
|
314
|
+
minValue: 0,
|
|
315
|
+
value: 3,
|
|
316
|
+
valueLabel: "Step 3 of 10",
|
|
317
|
+
}).value?.text
|
|
318
|
+
).toBe("Step 3 of 10");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("clamps the reported value into the range", () => {
|
|
322
|
+
expect(
|
|
323
|
+
resolveProgressAccessibility({ isIndeterminate: false, maxValue: 100, minValue: 0, value: 130 }).value?.now
|
|
324
|
+
).toBe(100);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// An indeterminate bar has no value to report, and a `now` of 0 would be read
|
|
328
|
+
// out as "zero percent" — which is a lie about work that is under way.
|
|
329
|
+
test("reports busy and no value while indeterminate", () => {
|
|
330
|
+
expect(resolveProgressAccessibility({ isIndeterminate: true, maxValue: 100, minValue: 0, value: 40 })).toEqual({
|
|
331
|
+
busy: true,
|
|
332
|
+
value: undefined,
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
});
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
import type { TextSize } from "../text/text.variants";
|
|
4
|
+
|
|
5
|
+
/** What the filled part of the bar means. The slider's set, so the two name a colour alike. */
|
|
6
|
+
export const PROGRESS_COLORS = ["default", "primary", "success", "warning", "destructive", "info"] as const;
|
|
7
|
+
|
|
8
|
+
export const PROGRESS_SIZES = ["sm", "md", "lg"] as const;
|
|
9
|
+
|
|
10
|
+
export type ProgressColor = (typeof PROGRESS_COLORS)[number];
|
|
11
|
+
export type ProgressSize = (typeof PROGRESS_SIZES)[number];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The axes a progress bar falls back to when nothing names one.
|
|
15
|
+
*
|
|
16
|
+
* Named once because two places read them — `defaultVariants` below and
|
|
17
|
+
* {@link resolveProgressAxes}, which runs before `tv` is ever called.
|
|
18
|
+
*/
|
|
19
|
+
export const PROGRESS_DEFAULT_COLOR: ProgressColor = "default";
|
|
20
|
+
export const PROGRESS_DEFAULT_SIZE: ProgressSize = "md";
|
|
21
|
+
|
|
22
|
+
/** The range a progress bar covers when the caller names none. */
|
|
23
|
+
export const PROGRESS_MIN_VALUE = 0;
|
|
24
|
+
export const PROGRESS_MAX_VALUE = 100;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How long the fill takes to reach a new value.
|
|
28
|
+
*
|
|
29
|
+
* Short enough that a bar fed every hundred milliseconds by an upload never
|
|
30
|
+
* falls visibly behind, long enough that a jump from 20 to 60 reads as travel
|
|
31
|
+
* rather than as a cut.
|
|
32
|
+
*/
|
|
33
|
+
export const PROGRESS_FILL_DURATION_MS = 320;
|
|
34
|
+
|
|
35
|
+
/** One pass of the indeterminate segment, from off the left edge to off the right. */
|
|
36
|
+
export const PROGRESS_INDETERMINATE_DURATION_MS = 1400;
|
|
37
|
+
|
|
38
|
+
/** The indeterminate segment's length, as a fraction of the track. */
|
|
39
|
+
export const PROGRESS_INDETERMINATE_SEGMENT = 0.4;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The reduce-motion stand-in for the indeterminate sweep: the whole track fills
|
|
43
|
+
* and breathes between these two opacities.
|
|
44
|
+
*
|
|
45
|
+
* Never down to zero — a bar that fades out entirely reads as the work having
|
|
46
|
+
* stopped, which is the one thing an indeterminate bar must not say.
|
|
47
|
+
*/
|
|
48
|
+
export const PROGRESS_PULSE = { from: 0.35, to: 1, durationMs: 900 } as const;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The `Text` size step each progress size hands its label and its readout.
|
|
52
|
+
*
|
|
53
|
+
* The slider's own map, restated rather than imported so this leaf imports no
|
|
54
|
+
* other component's variants; a test pins the two together.
|
|
55
|
+
*/
|
|
56
|
+
export const PROGRESS_OUTPUT_TEXT_SIZE: Record<ProgressSize, TextSize> = { sm: "sm", md: "md", lg: "lg" };
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Styling for every part of a progress bar.
|
|
60
|
+
*
|
|
61
|
+
* One slotted `tv()` because `progress-label.tsx`, `progress-output.tsx`,
|
|
62
|
+
* `progress-track.tsx` and `progress-fill.tsx` cannot import the root without
|
|
63
|
+
* closing a cycle (AGENTS.md rule 3), yet all four read the same `color` and
|
|
64
|
+
* `size`.
|
|
65
|
+
*
|
|
66
|
+
* **The colour paints the fill and never the track.** An empty bar is the same
|
|
67
|
+
* chrome at every colour — the slider's groove, `bg-secondary` — and a test
|
|
68
|
+
* pins that.
|
|
69
|
+
*
|
|
70
|
+
* **The fill is a full-length bar slid under a clipping track**, not a bar whose
|
|
71
|
+
* width changes. Its position is a `translateX`, which is a transform — composited
|
|
72
|
+
* on the UI thread with no layout pass per frame, where an animated `width`
|
|
73
|
+
* re-lays the bar out every frame of every update. The track's `overflow-hidden`
|
|
74
|
+
* and `rounded-full` are what make that read as a bar growing from the left: the
|
|
75
|
+
* part parked beyond the start is clipped, and the leading end keeps its round
|
|
76
|
+
* cap because it is the fill's own end rather than a cut.
|
|
77
|
+
*
|
|
78
|
+
* **The size is the track's thickness and nothing else.** There is no thumb to
|
|
79
|
+
* keep flush and no touch target to pad, so these are plain spacing steps —
|
|
80
|
+
* thinner than the slider's groove, because a bar you read is lighter than a
|
|
81
|
+
* groove you grab.
|
|
82
|
+
*
|
|
83
|
+
* Free of React Native imports so it stays unit-testable — `bun test` cannot parse
|
|
84
|
+
* React Native's Flow-typed source. See AGENTS.md.
|
|
85
|
+
*/
|
|
86
|
+
export const progressVariants = tv({
|
|
87
|
+
slots: {
|
|
88
|
+
/** The column holding the header and the track. */
|
|
89
|
+
root: "w-full gap-2",
|
|
90
|
+
/** The row a label and a readout share, when both are given. */
|
|
91
|
+
header: "flex-row items-center justify-between gap-3",
|
|
92
|
+
/** Handed to a `Text.Label`. Layout only. */
|
|
93
|
+
label: "shrink",
|
|
94
|
+
/** Handed to a `Text.Label`. Layout only. */
|
|
95
|
+
output: "",
|
|
96
|
+
/** The clipped, rounded groove the fill slides along. */
|
|
97
|
+
track: "relative w-full overflow-hidden rounded-full bg-secondary",
|
|
98
|
+
/** The painted bar. Its position is an animated transform, never a class. */
|
|
99
|
+
fill: "absolute top-0 bottom-0 left-0 rounded-full",
|
|
100
|
+
},
|
|
101
|
+
variants: {
|
|
102
|
+
size: {
|
|
103
|
+
sm: { track: "h-1" },
|
|
104
|
+
md: { track: "h-2" },
|
|
105
|
+
lg: { track: "h-3" },
|
|
106
|
+
},
|
|
107
|
+
color: {
|
|
108
|
+
default: { fill: "bg-foreground" },
|
|
109
|
+
primary: { fill: "bg-primary" },
|
|
110
|
+
success: { fill: "bg-success" },
|
|
111
|
+
warning: { fill: "bg-warning" },
|
|
112
|
+
destructive: { fill: "bg-destructive" },
|
|
113
|
+
info: { fill: "bg-info" },
|
|
114
|
+
},
|
|
115
|
+
// The empty branches are load-bearing typing: `tv` types a map holding only
|
|
116
|
+
// `true` as `true` rather than `boolean`.
|
|
117
|
+
isIndeterminate: { true: {}, false: {} },
|
|
118
|
+
},
|
|
119
|
+
defaultVariants: {
|
|
120
|
+
color: PROGRESS_DEFAULT_COLOR,
|
|
121
|
+
size: PROGRESS_DEFAULT_SIZE,
|
|
122
|
+
isIndeterminate: false,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Where a value sits in the range, as 0–1.
|
|
128
|
+
*
|
|
129
|
+
* Clamped rather than extrapolated, and `0` for a degenerate range or a value
|
|
130
|
+
* that is not a finite number: a `NaN` written into a shared value freezes the
|
|
131
|
+
* fill for good, and an upload that divides by a total it has not learned yet
|
|
132
|
+
* produces exactly that.
|
|
133
|
+
*/
|
|
134
|
+
export function progressRatio(value: number, minValue: number, maxValue: number): number {
|
|
135
|
+
"worklet";
|
|
136
|
+
const span = maxValue - minValue;
|
|
137
|
+
if (!(span > 0) || !Number.isFinite(value)) return 0;
|
|
138
|
+
const ratio = (value - minValue) / span;
|
|
139
|
+
return ratio < 0 ? 0 : ratio > 1 ? 1 : ratio;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The fill's `translateX`: how far left of the track's start the full-length bar
|
|
144
|
+
* is parked.
|
|
145
|
+
*
|
|
146
|
+
* A bar at `ratio` shows `ratio × trackSize` of itself, so it sits back by the
|
|
147
|
+
* rest. `0` on an unmeasured track — the fill is hidden until the first layout,
|
|
148
|
+
* so there is nothing to place.
|
|
149
|
+
*/
|
|
150
|
+
export function fillTranslate({ ratio, trackSize }: { ratio: number; trackSize: number }): number {
|
|
151
|
+
"worklet";
|
|
152
|
+
if (trackSize <= 0) return 0;
|
|
153
|
+
const clamped = ratio < 0 ? 0 : ratio > 1 ? 1 : ratio;
|
|
154
|
+
return (clamped - 1) * trackSize;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The indeterminate segment's box at one point in its loop.
|
|
159
|
+
*
|
|
160
|
+
* `phase` runs 0–1. At 0 the segment sits wholly off the track's left edge and at
|
|
161
|
+
* 1 wholly off its right, so the frame where the loop restarts is never on screen
|
|
162
|
+
* — a sweep that began at the track's start would pop into view every pass.
|
|
163
|
+
*/
|
|
164
|
+
export function indeterminateSegment({
|
|
165
|
+
phase,
|
|
166
|
+
segment,
|
|
167
|
+
trackSize,
|
|
168
|
+
}: {
|
|
169
|
+
phase: number;
|
|
170
|
+
segment: number;
|
|
171
|
+
trackSize: number;
|
|
172
|
+
}): { translate: number; width: number } {
|
|
173
|
+
"worklet";
|
|
174
|
+
if (trackSize <= 0) return { translate: 0, width: 0 };
|
|
175
|
+
const width = segment * trackSize;
|
|
176
|
+
return { translate: -width + phase * (trackSize + width), width };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** What {@link formatProgressValue} and {@link resolveProgressAccessibility} read. */
|
|
180
|
+
export type ProgressFormatInput = {
|
|
181
|
+
value: number;
|
|
182
|
+
minValue: number;
|
|
183
|
+
maxValue: number;
|
|
184
|
+
formatOptions?: Intl.NumberFormatOptions;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** Whether the readout speaks a percentage of the range, rather than the value itself. */
|
|
188
|
+
function isPercentFormat(formatOptions: Intl.NumberFormatOptions | undefined): boolean {
|
|
189
|
+
return formatOptions?.style === undefined || formatOptions.style === "percent";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The readout's default text.
|
|
194
|
+
*
|
|
195
|
+
* With no `formatOptions`, or a `percent` style, it is the share of the range —
|
|
196
|
+
* `72%`, or `75%` for 18 of 24. A percent style formats the *ratio* rather than
|
|
197
|
+
* the value, because `Intl` multiplies by a hundred and a value of 72 would read
|
|
198
|
+
* `7,200%`. Any other style formats the value itself, so a budget reads `$1,250`.
|
|
199
|
+
*
|
|
200
|
+
* JS-thread only — `Intl` is not available to a worklet.
|
|
201
|
+
*/
|
|
202
|
+
export function formatProgressValue({ value, minValue, maxValue, formatOptions }: ProgressFormatInput): string {
|
|
203
|
+
if (isPercentFormat(formatOptions)) {
|
|
204
|
+
const format = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0, ...formatOptions, style: "percent" });
|
|
205
|
+
return format.format(progressRatio(value, minValue, maxValue));
|
|
206
|
+
}
|
|
207
|
+
return new Intl.NumberFormat(undefined, formatOptions).format(value);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The accessibility surface a progress bar publishes on its root. */
|
|
211
|
+
export type ProgressAccessibility = {
|
|
212
|
+
busy: boolean;
|
|
213
|
+
value: { min: number; max: number; now: number; text: string } | undefined;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* What assistive technology is told.
|
|
218
|
+
*
|
|
219
|
+
* **The spoken text names the range, not only the share of it.** A bar over
|
|
220
|
+
* 0–100 reads its percentage; any other range reads `18 of 24`, formatted the way
|
|
221
|
+
* the readout formats the value — because the count is what the screen is about,
|
|
222
|
+
* and "seventy-five percent" makes the listener do the division back.
|
|
223
|
+
* `valueLabel` replaces both.
|
|
224
|
+
*
|
|
225
|
+
* **An indeterminate bar reports `busy` and no value at all.** A `now` of zero
|
|
226
|
+
* would be read out as "zero percent", which is a lie about work that is under
|
|
227
|
+
* way.
|
|
228
|
+
*/
|
|
229
|
+
export function resolveProgressAccessibility({
|
|
230
|
+
value,
|
|
231
|
+
minValue,
|
|
232
|
+
maxValue,
|
|
233
|
+
formatOptions,
|
|
234
|
+
isIndeterminate,
|
|
235
|
+
valueLabel,
|
|
236
|
+
}: ProgressFormatInput & { isIndeterminate: boolean; valueLabel?: string }): ProgressAccessibility {
|
|
237
|
+
if (isIndeterminate) return { busy: true, value: undefined };
|
|
238
|
+
|
|
239
|
+
const ratio = progressRatio(value, minValue, maxValue);
|
|
240
|
+
const now = minValue + ratio * (maxValue - minValue);
|
|
241
|
+
const isHundredScale = minValue === 0 && maxValue === 100;
|
|
242
|
+
|
|
243
|
+
let text = valueLabel;
|
|
244
|
+
if (text === undefined) {
|
|
245
|
+
if (isHundredScale && isPercentFormat(formatOptions)) {
|
|
246
|
+
text = formatProgressValue({ formatOptions, maxValue, minValue, value });
|
|
247
|
+
} else {
|
|
248
|
+
const format = new Intl.NumberFormat(
|
|
249
|
+
undefined,
|
|
250
|
+
isPercentFormat(formatOptions) ? { maximumFractionDigits: 2 } : formatOptions
|
|
251
|
+
);
|
|
252
|
+
text = `${format.format(now)} of ${format.format(maxValue)}`;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { busy: false, value: { max: maxValue, min: minValue, now, text } };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** What a progress bar was given at its own call site. */
|
|
260
|
+
export type ProgressOwnAxes = {
|
|
261
|
+
color?: ProgressColor;
|
|
262
|
+
size?: ProgressSize;
|
|
263
|
+
isIndeterminate?: boolean;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/** Every axis settled, ready to hand to {@link progressVariants} and to context. */
|
|
267
|
+
export type ProgressAxes = Required<ProgressOwnAxes>;
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Settles a progress bar's axes.
|
|
271
|
+
*
|
|
272
|
+
* One rung, deliberately: a progress bar is an output, not a form control, so an
|
|
273
|
+
* enclosing `Field` has no disabled or invalid state to hand it. `??` rather than
|
|
274
|
+
* `||`, so an explicit `false` is a value rather than an absence.
|
|
275
|
+
*/
|
|
276
|
+
export function resolveProgressAxes(own: ProgressOwnAxes): ProgressAxes {
|
|
277
|
+
return {
|
|
278
|
+
color: own.color ?? PROGRESS_DEFAULT_COLOR,
|
|
279
|
+
isIndeterminate: own.isIndeterminate ?? false,
|
|
280
|
+
size: own.size ?? PROGRESS_DEFAULT_SIZE,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export type ProgressVariantProps = VariantProps<typeof progressVariants>;
|