@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,347 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { declarationCount } from "../../styles/theme-tokens.test";
|
|
3
|
+
import { TEXT_SIZES } from "../text/text.variants";
|
|
4
|
+
import {
|
|
5
|
+
clampRating,
|
|
6
|
+
describeRating,
|
|
7
|
+
formatRatingValue,
|
|
8
|
+
normalizeRatingCount,
|
|
9
|
+
normalizeRatingStep,
|
|
10
|
+
RATING_COLORS,
|
|
11
|
+
RATING_COUNT,
|
|
12
|
+
RATING_DEFAULT_COLOR,
|
|
13
|
+
RATING_DEFAULT_SIZE,
|
|
14
|
+
RATING_EMPTY_OPACITY,
|
|
15
|
+
RATING_FILL_TOKEN,
|
|
16
|
+
RATING_OUTPUT_TEXT_SIZE,
|
|
17
|
+
RATING_SIZES,
|
|
18
|
+
RATING_STAR_PATH,
|
|
19
|
+
RATING_STEP,
|
|
20
|
+
RATING_TAP_SLOP,
|
|
21
|
+
ratingFromOffset,
|
|
22
|
+
ratingVariants,
|
|
23
|
+
resolveRatingAxes,
|
|
24
|
+
resolveRatingPaint,
|
|
25
|
+
shouldClearRating,
|
|
26
|
+
starFillOf,
|
|
27
|
+
stepRating,
|
|
28
|
+
} from "./rating.variants";
|
|
29
|
+
|
|
30
|
+
/** Tailwind's spacing scale is quarter-rem steps, so a step is four points. */
|
|
31
|
+
const POINTS_PER_STEP = 4;
|
|
32
|
+
|
|
33
|
+
/** The tap target every control in this library has to clear. */
|
|
34
|
+
const MINIMUM_TARGET_PT = 44;
|
|
35
|
+
|
|
36
|
+
/** The `--spacing-icon-*` scale in points, as `tokens.css` declares it. */
|
|
37
|
+
const ICON_POINTS: Record<string, number> = { xs: 14, sm: 16, md: 18, lg: 20, xl: 24, "2xl": 32 };
|
|
38
|
+
|
|
39
|
+
/** The `py-*` step a class string sets — `py-2.5` yields 2.5. */
|
|
40
|
+
function paddingYStep(value: string): number {
|
|
41
|
+
return Number(value.match(/\bpy-(\d+(?:\.\d+)?)\b/)?.[1]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The icon step a `size-icon-*` class names — `size-icon-xl` yields `xl`. */
|
|
45
|
+
function iconStep(value: string): string | undefined {
|
|
46
|
+
return value.match(/\bsize-icon-([\w]+)\b/)?.[1];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("normalizeRatingCount", () => {
|
|
50
|
+
test("a positive whole number passes through", () => {
|
|
51
|
+
expect(normalizeRatingCount(5)).toBe(5);
|
|
52
|
+
expect(normalizeRatingCount(10)).toBe(10);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("a fraction rounds down, never up", () => {
|
|
56
|
+
expect(normalizeRatingCount(4.9)).toBe(4);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("anything that cannot be a star count falls back to the default", () => {
|
|
60
|
+
for (const bad of [0, -3, 0.5, Number.NaN, Number.POSITIVE_INFINITY]) {
|
|
61
|
+
expect(normalizeRatingCount(bad)).toBe(RATING_COUNT);
|
|
62
|
+
}
|
|
63
|
+
expect(normalizeRatingCount(undefined)).toBe(RATING_COUNT);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("normalizeRatingStep", () => {
|
|
68
|
+
test("whole and half stars pass through", () => {
|
|
69
|
+
expect(normalizeRatingStep(1)).toBe(1);
|
|
70
|
+
expect(normalizeRatingStep(0.5)).toBe(0.5);
|
|
71
|
+
expect(normalizeRatingStep(0.25)).toBe(0.25);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("a step that does not divide one star falls back to whole stars", () => {
|
|
75
|
+
expect(normalizeRatingStep(0.3)).toBe(RATING_STEP);
|
|
76
|
+
expect(normalizeRatingStep(0.4)).toBe(RATING_STEP);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("a step past one star, zero, negative or non-finite falls back to whole stars", () => {
|
|
80
|
+
for (const bad of [0, -0.5, 2, Number.NaN, Number.POSITIVE_INFINITY]) {
|
|
81
|
+
expect(normalizeRatingStep(bad)).toBe(RATING_STEP);
|
|
82
|
+
}
|
|
83
|
+
expect(normalizeRatingStep(undefined)).toBe(RATING_STEP);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("clampRating", () => {
|
|
88
|
+
test("holds a value inside zero and the count", () => {
|
|
89
|
+
expect(clampRating(3.5, 5)).toBe(3.5);
|
|
90
|
+
expect(clampRating(-1, 5)).toBe(0);
|
|
91
|
+
expect(clampRating(9, 5)).toBe(5);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("a non-finite rating renders as zero rather than as a NaN width", () => {
|
|
95
|
+
expect(clampRating(Number.NaN, 5)).toBe(0);
|
|
96
|
+
expect(clampRating(Number.POSITIVE_INFINITY, 5)).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("does not snap — a read-only display may show any value", () => {
|
|
100
|
+
expect(clampRating(3.7, 5)).toBe(3.7);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("starFillOf", () => {
|
|
105
|
+
test("every star before the value is full, every star after it is empty", () => {
|
|
106
|
+
expect([0, 1, 2, 3, 4].map((index) => starFillOf(index, 3))).toEqual([1, 1, 1, 0, 0]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("the star the value lands inside is partly filled", () => {
|
|
110
|
+
expect(starFillOf(3, 3.5)).toBe(0.5);
|
|
111
|
+
expect(starFillOf(2, 2.25)).toBeCloseTo(0.25);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("zero fills nothing", () => {
|
|
115
|
+
expect([0, 1, 2, 3, 4].map((index) => starFillOf(index, 0))).toEqual([0, 0, 0, 0, 0]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("the fills sum back to the value", () => {
|
|
119
|
+
for (const value of [0, 0.5, 1, 2.5, 3.7, 5]) {
|
|
120
|
+
const sum = [0, 1, 2, 3, 4].reduce((total, index) => total + starFillOf(index, value), 0);
|
|
121
|
+
expect(sum).toBeCloseTo(value);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("ratingFromOffset", () => {
|
|
127
|
+
const width = 200;
|
|
128
|
+
const count = 5;
|
|
129
|
+
|
|
130
|
+
test("a touch inside a star's cell takes that star, whole stars", () => {
|
|
131
|
+
expect(ratingFromOffset({ count, position: 10, step: 1, width })).toBe(1);
|
|
132
|
+
expect(ratingFromOffset({ count, position: 39, step: 1, width })).toBe(1);
|
|
133
|
+
expect(ratingFromOffset({ count, position: 41, step: 1, width })).toBe(2);
|
|
134
|
+
expect(ratingFromOffset({ count, position: 199, step: 1, width })).toBe(5);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("half stars split each cell at its centre", () => {
|
|
138
|
+
expect(ratingFromOffset({ count, position: 10, step: 0.5, width })).toBe(0.5);
|
|
139
|
+
expect(ratingFromOffset({ count, position: 30, step: 0.5, width })).toBe(1);
|
|
140
|
+
expect(ratingFromOffset({ count, position: 130, step: 0.5, width })).toBe(3.5);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("a cell edge belongs to the star before it, not a sliver of the next", () => {
|
|
144
|
+
expect(ratingFromOffset({ count, position: 80, step: 1, width })).toBe(2);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("a drag past the start holds the lowest stop rather than clearing", () => {
|
|
148
|
+
expect(ratingFromOffset({ count, position: -50, step: 1, width })).toBe(1);
|
|
149
|
+
expect(ratingFromOffset({ count, position: 0, step: 0.5, width })).toBe(0.5);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("a drag past the end holds the count", () => {
|
|
153
|
+
expect(ratingFromOffset({ count, position: 900, step: 0.5, width })).toBe(5);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("an unmeasured row reports zero rather than dividing by it", () => {
|
|
157
|
+
expect(ratingFromOffset({ count, position: 40, step: 1, width: 0 })).toBe(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("quarter steps do not leak floating-point noise", () => {
|
|
161
|
+
const value = ratingFromOffset({ count, position: 43, step: 0.25, width });
|
|
162
|
+
expect(value).toBe(1.25);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("stepRating", () => {
|
|
167
|
+
test("an increment moves by one step and stops at the count", () => {
|
|
168
|
+
expect(stepRating(3, 1, 1, 5)).toBe(4);
|
|
169
|
+
expect(stepRating(4.5, 1, 1, 5)).toBe(5);
|
|
170
|
+
expect(stepRating(5, 1, 0.5, 5)).toBe(5);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("a decrement moves by one step and stops at zero", () => {
|
|
174
|
+
expect(stepRating(3, -1, 0.5, 5)).toBe(2.5);
|
|
175
|
+
expect(stepRating(0, -1, 1, 5)).toBe(0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("a value between stops lands on the next stop rather than a step past it", () => {
|
|
179
|
+
expect(stepRating(3.7, 1, 1, 5)).toBe(4);
|
|
180
|
+
expect(stepRating(3.7, -1, 1, 5)).toBe(3);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("shouldClearRating", () => {
|
|
185
|
+
test("a tap on the current value clears it when clearing is allowed", () => {
|
|
186
|
+
expect(shouldClearRating({ allowClear: true, startValue: 3, touchedValue: 3, travel: 0 })).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("never clears when clearing is off", () => {
|
|
190
|
+
expect(shouldClearRating({ allowClear: false, startValue: 3, touchedValue: 3, travel: 0 })).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("a tap on another star moves the value instead", () => {
|
|
194
|
+
expect(shouldClearRating({ allowClear: true, startValue: 3, touchedValue: 4, travel: 0 })).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("a drag that comes back to where it started is a drag, not a tap", () => {
|
|
198
|
+
expect(shouldClearRating({ allowClear: true, startValue: 3, touchedValue: 3, travel: RATING_TAP_SLOP + 1 })).toBe(
|
|
199
|
+
false
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("an empty rating has nothing to clear", () => {
|
|
204
|
+
expect(shouldClearRating({ allowClear: true, startValue: 0, touchedValue: 0, travel: 0 })).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("formatRatingValue and describeRating", () => {
|
|
209
|
+
test("whole values read without a decimal", () => {
|
|
210
|
+
expect(formatRatingValue(4)).toBe("4");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("fractions keep at most two places", () => {
|
|
214
|
+
expect(formatRatingValue(3.5)).toBe("3.5");
|
|
215
|
+
expect(formatRatingValue(3.666)).toBe("3.67");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("the spoken value names the count", () => {
|
|
219
|
+
expect(describeRating(3.5, 5)).toBe("3.5 out of 5");
|
|
220
|
+
expect(describeRating(0, 10)).toBe("0 out of 10");
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("resolveRatingPaint", () => {
|
|
225
|
+
test("every colour names a fill token", () => {
|
|
226
|
+
for (const color of RATING_COLORS) {
|
|
227
|
+
expect(resolveRatingPaint({ color, isInvalid: false }).fill).toBe(RATING_FILL_TOKEN[color]);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("invalid outranks the colour, on the filled and the empty stars both", () => {
|
|
232
|
+
for (const color of RATING_COLORS) {
|
|
233
|
+
expect(resolveRatingPaint({ color, isInvalid: true })).toEqual({
|
|
234
|
+
empty: "destructive",
|
|
235
|
+
fill: "destructive",
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("an empty star is the same chrome at every colour", () => {
|
|
241
|
+
const empties = new Set(RATING_COLORS.map((color) => resolveRatingPaint({ color, isInvalid: false }).empty));
|
|
242
|
+
expect(empties.size).toBe(1);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("the empty star is faint but not invisible", () => {
|
|
246
|
+
expect(RATING_EMPTY_OPACITY).toBeGreaterThan(0);
|
|
247
|
+
expect(RATING_EMPTY_OPACITY).toBeLessThan(1);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("every token named is declared in both variants of theme.css", () => {
|
|
251
|
+
const tokens = new Set<string>();
|
|
252
|
+
for (const color of RATING_COLORS) {
|
|
253
|
+
for (const isInvalid of [true, false]) {
|
|
254
|
+
const paint = resolveRatingPaint({ color, isInvalid });
|
|
255
|
+
tokens.add(paint.fill);
|
|
256
|
+
tokens.add(paint.empty);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const token of tokens) {
|
|
260
|
+
expect(declarationCount(token)).toBe(2);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("ratingVariants", () => {
|
|
266
|
+
test("the touch area clears 44pt at every size", () => {
|
|
267
|
+
for (const size of RATING_SIZES) {
|
|
268
|
+
const slots = ratingVariants({ size });
|
|
269
|
+
const glyph = ICON_POINTS[iconStep(slots.glyph()) ?? ""];
|
|
270
|
+
const padding = paddingYStep(slots.stars()) * POINTS_PER_STEP;
|
|
271
|
+
expect(glyph).toBeDefined();
|
|
272
|
+
expect((glyph ?? 0) + padding * 2).toBe(MINIMUM_TARGET_PT);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("the glyph grows with the size", () => {
|
|
277
|
+
const points = RATING_SIZES.map((size) => ICON_POINTS[iconStep(ratingVariants({ size }).glyph()) ?? ""] ?? 0);
|
|
278
|
+
expect(points).toEqual([...points].sort((a, b) => a - b));
|
|
279
|
+
expect(new Set(points).size).toBe(RATING_SIZES.length);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("disabled fades the root, read-only does not", () => {
|
|
283
|
+
expect(ratingVariants({ isDisabled: true }).root()).toContain("opacity-50");
|
|
284
|
+
expect(ratingVariants({ isDisabled: false }).root() ?? "").not.toContain("opacity");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("the fill clip hides what overhangs its width", () => {
|
|
288
|
+
expect(ratingVariants({}).clip()).toContain("overflow-hidden");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("a caller's class merges onto the slot", () => {
|
|
292
|
+
expect(ratingVariants({}).root({ className: "mt-4" })).toContain("mt-4");
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe("defaults", () => {
|
|
297
|
+
test("tv's defaults and the resolver's are the same pair", () => {
|
|
298
|
+
const axes = resolveRatingAxes({});
|
|
299
|
+
expect(axes.color).toBe(RATING_DEFAULT_COLOR);
|
|
300
|
+
expect(axes.size).toBe(RATING_DEFAULT_SIZE);
|
|
301
|
+
expect(ratingVariants({}).glyph()).toBe(ratingVariants({ size: RATING_DEFAULT_SIZE }).glyph());
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("the output names a size Text actually has", () => {
|
|
305
|
+
for (const size of RATING_SIZES) {
|
|
306
|
+
expect(TEXT_SIZES).toContain(RATING_OUTPUT_TEXT_SIZE[size]);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("the star path is a closed ten-point outline inside the 24pt box", () => {
|
|
311
|
+
expect(RATING_STAR_PATH.startsWith("M")).toBe(true);
|
|
312
|
+
expect(RATING_STAR_PATH.endsWith("Z")).toBe(true);
|
|
313
|
+
const numbers = RATING_STAR_PATH.match(/-?\d+(?:\.\d+)?/g)?.map(Number) ?? [];
|
|
314
|
+
expect(numbers.length).toBe(20);
|
|
315
|
+
for (const coordinate of numbers) {
|
|
316
|
+
expect(coordinate).toBeGreaterThanOrEqual(0);
|
|
317
|
+
expect(coordinate).toBeLessThanOrEqual(24);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("resolveRatingAxes", () => {
|
|
323
|
+
test("a Field's state reaches the rating", () => {
|
|
324
|
+
expect(resolveRatingAxes({ field: { isDisabled: true, isInvalid: true } })).toMatchObject({
|
|
325
|
+
isDisabled: true,
|
|
326
|
+
isInvalid: true,
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("an explicit false beats the Field", () => {
|
|
331
|
+
expect(
|
|
332
|
+
resolveRatingAxes({ field: { isDisabled: true, isInvalid: true }, own: { isDisabled: false, isInvalid: false } })
|
|
333
|
+
).toMatchObject({ isDisabled: false, isInvalid: false });
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("read-only is the rating's own, never the Field's", () => {
|
|
337
|
+
expect(resolveRatingAxes({ field: { isDisabled: false, isInvalid: false } }).isReadOnly).toBe(false);
|
|
338
|
+
expect(resolveRatingAxes({ own: { isReadOnly: true } }).isReadOnly).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("a Field carries no paint axis", () => {
|
|
342
|
+
const field = { color: "info", isDisabled: false, isInvalid: false, size: "lg" } as const;
|
|
343
|
+
const axes = resolveRatingAxes({ field });
|
|
344
|
+
expect(axes.color).toBe(RATING_DEFAULT_COLOR);
|
|
345
|
+
expect(axes.size).toBe(RATING_DEFAULT_SIZE);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
import type { TextSize } from "../text/text.variants";
|
|
4
|
+
|
|
5
|
+
/** What a filled star means. Slider's set, reusing tokens the theme already has. */
|
|
6
|
+
export const RATING_COLORS = ["default", "primary", "success", "warning", "destructive", "info"] as const;
|
|
7
|
+
|
|
8
|
+
export const RATING_SIZES = ["sm", "md", "lg"] as const;
|
|
9
|
+
|
|
10
|
+
export type RatingColor = (typeof RATING_COLORS)[number];
|
|
11
|
+
export type RatingSize = (typeof RATING_SIZES)[number];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The axes a rating falls back to when nothing names one.
|
|
15
|
+
*
|
|
16
|
+
* Named once because two places read them — `defaultVariants` below and
|
|
17
|
+
* {@link resolveRatingAxes}, which runs before `tv` is ever called. A test pins
|
|
18
|
+
* the pair.
|
|
19
|
+
*
|
|
20
|
+
* `warning` rather than `default` because a star is read as amber before it is
|
|
21
|
+
* read as a star: a row of ink-coloured stars looks like a set of icons, not a
|
|
22
|
+
* score.
|
|
23
|
+
*/
|
|
24
|
+
export const RATING_DEFAULT_COLOR: RatingColor = "warning";
|
|
25
|
+
export const RATING_DEFAULT_SIZE: RatingSize = "md";
|
|
26
|
+
|
|
27
|
+
/** How many stars a rating draws when the caller names no count. */
|
|
28
|
+
export const RATING_COUNT = 5;
|
|
29
|
+
|
|
30
|
+
/** The increment a rating snaps to when the caller names none. `0.5` is half stars. */
|
|
31
|
+
export const RATING_STEP = 1;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* How far a touch may travel and still count as a tap, in points.
|
|
35
|
+
*
|
|
36
|
+
* Only `allowClear` reads it: a tap on the current value clears the rating, and a
|
|
37
|
+
* drag that wanders off and comes back to the same star must not.
|
|
38
|
+
*/
|
|
39
|
+
export const RATING_TAP_SLOP = 6;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* How strongly an empty star is painted, as a fill opacity.
|
|
43
|
+
*
|
|
44
|
+
* An empty star is `muted-foreground` at this opacity rather than a token of its
|
|
45
|
+
* own. No token in the set sits where it needs to: `border` and `input` vanish
|
|
46
|
+
* against a light card, and `muted-foreground` at full strength reads as a
|
|
47
|
+
* disabled *filled* star. A fraction of it is legible on both themes, and the
|
|
48
|
+
* package has no token to add for one component's empty state (rule 11).
|
|
49
|
+
*/
|
|
50
|
+
export const RATING_EMPTY_OPACITY = 0.4;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The token each colour fills a star with.
|
|
54
|
+
*
|
|
55
|
+
* A record rather than a class because the star is an SVG path, and a path's
|
|
56
|
+
* `fill` is a paint prop that no `className` reaches without wrapping `Svg` in
|
|
57
|
+
* `withUniwind` — which rule 7 forbids. `useThemeColor` reads the raw name.
|
|
58
|
+
*/
|
|
59
|
+
export const RATING_FILL_TOKEN: Record<RatingColor, string> = {
|
|
60
|
+
default: "foreground",
|
|
61
|
+
primary: "primary",
|
|
62
|
+
success: "success",
|
|
63
|
+
warning: "warning",
|
|
64
|
+
destructive: "destructive",
|
|
65
|
+
info: "info",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** The token an empty star is painted with, at {@link RATING_EMPTY_OPACITY}. */
|
|
69
|
+
const RATING_EMPTY_TOKEN = "muted-foreground";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The `Text` size step each rating size hands its output.
|
|
73
|
+
*
|
|
74
|
+
* `Rating.Output` renders `Text.Label` and names this step rather than restating
|
|
75
|
+
* a type scale — the weight and colour stay in the preset. A test asserts every
|
|
76
|
+
* value here is a size `Text` actually has.
|
|
77
|
+
*/
|
|
78
|
+
export const RATING_OUTPUT_TEXT_SIZE: Record<RatingSize, TextSize> = { sm: "sm", md: "md", lg: "lg" };
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* One star, as a path in a 24-point box.
|
|
82
|
+
*
|
|
83
|
+
* Ten vertices at a 0.42 inner-to-outer ratio, centred a little low so the two
|
|
84
|
+
* lower points and the upper one sit at the same distance from the box's edge.
|
|
85
|
+
* The glyph is drawn with a round-joined stroke in its own fill colour, which is
|
|
86
|
+
* what softens the points to match Central Icons' rounded set without a curve in
|
|
87
|
+
* the path.
|
|
88
|
+
*
|
|
89
|
+
* Drawn here rather than taken from the icon set because the set is outlined:
|
|
90
|
+
* a filled star, and the clipped half of one, has no glyph there to draw.
|
|
91
|
+
*/
|
|
92
|
+
export const RATING_STAR_PATH =
|
|
93
|
+
"M12 2.3L14.65 9.26L22.08 9.62L16.28 14.29L18.23 21.48L12 17.4L5.77 21.48L7.72 14.29L1.92 9.62L9.35 9.26Z";
|
|
94
|
+
|
|
95
|
+
/** The stroke that rounds the star's points, in the path's own 24-point units. */
|
|
96
|
+
export const RATING_STAR_STROKE = 1.5;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Styling for every part of a rating.
|
|
100
|
+
*
|
|
101
|
+
* One slotted `tv()` rather than a call per part, because `rating-stars.tsx`,
|
|
102
|
+
* `rating-star.tsx` and `rating-output.tsx` cannot import the root without
|
|
103
|
+
* closing a cycle (AGENTS.md rule 3) yet all three read the same `size`.
|
|
104
|
+
*
|
|
105
|
+
* **The glyph is a step on the icon scale**, not a number of its own: `sm`, `md`
|
|
106
|
+
* and `lg` are `icon-lg`, `icon-xl` and `icon-2xl`. A star is a mark, the way a
|
|
107
|
+
* checkbox's tick is, and sits on the scale every other mark in the package does.
|
|
108
|
+
*
|
|
109
|
+
* **The row is the touch target, padded on the cross axis only.** The glyph and
|
|
110
|
+
* that padding sum to 44 at every size, and a test asserts the sum. The main
|
|
111
|
+
* axis is left unpadded because the value is read straight off the touch's
|
|
112
|
+
* offset along the row — a gutter there would put every value off by it.
|
|
113
|
+
*
|
|
114
|
+
* **Each star sits in a cell that carries the gap**, as padding on both sides,
|
|
115
|
+
* so every cell is the same width and a star's half is the half of its cell. A
|
|
116
|
+
* `gap-*` on the row would leave dead space between the cells that belongs to no
|
|
117
|
+
* star, and a touch there would have to be assigned somewhere by a rule nobody
|
|
118
|
+
* could see.
|
|
119
|
+
*
|
|
120
|
+
* **The colour is not here.** A star is an SVG path, and its fill is a paint prop
|
|
121
|
+
* — see {@link RATING_FILL_TOKEN} and {@link resolveRatingPaint}.
|
|
122
|
+
*
|
|
123
|
+
* Free of React Native imports so it stays unit-testable. See AGENTS.md.
|
|
124
|
+
*/
|
|
125
|
+
export const ratingVariants = tv({
|
|
126
|
+
slots: {
|
|
127
|
+
/** The column holding the stars and the readout. */
|
|
128
|
+
root: "items-start gap-1",
|
|
129
|
+
/** The row of stars, and the surface the drag is claimed on. */
|
|
130
|
+
stars: "flex-row items-center self-start",
|
|
131
|
+
/** One star's share of the row: its glyph plus half the gap either side. */
|
|
132
|
+
cell: "items-center justify-center",
|
|
133
|
+
/** The glyph's box. The empty star fills it; the clip overlays it. */
|
|
134
|
+
glyph: "relative",
|
|
135
|
+
/** The filled star's window. Its width is the star's fill, as a percentage. */
|
|
136
|
+
clip: "absolute bottom-0 left-0 top-0 overflow-hidden",
|
|
137
|
+
/** Handed to a `Text.Label`. Layout only. */
|
|
138
|
+
output: "",
|
|
139
|
+
},
|
|
140
|
+
variants: {
|
|
141
|
+
size: {
|
|
142
|
+
sm: { stars: "py-3", cell: "px-0.5", glyph: "size-icon-lg" },
|
|
143
|
+
md: { stars: "py-2.5", cell: "px-1", glyph: "size-icon-xl" },
|
|
144
|
+
lg: { stars: "py-1.5", cell: "px-1", glyph: "size-icon-2xl" },
|
|
145
|
+
},
|
|
146
|
+
isDisabled: { true: { root: "opacity-50" }, false: {} },
|
|
147
|
+
},
|
|
148
|
+
defaultVariants: {
|
|
149
|
+
size: RATING_DEFAULT_SIZE,
|
|
150
|
+
isDisabled: false,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A star count the row can draw.
|
|
156
|
+
*
|
|
157
|
+
* A positive whole number passes through and a fraction rounds **down** — a
|
|
158
|
+
* count of 4.9 is four stars, because a fifth star that can only be part-filled
|
|
159
|
+
* is not a star anyone asked for. Anything that is not a count at all — zero,
|
|
160
|
+
* negative, non-finite, below one after rounding — falls back to
|
|
161
|
+
* {@link RATING_COUNT} rather than drawing nothing, since a rating with no stars
|
|
162
|
+
* has no way to say so.
|
|
163
|
+
*/
|
|
164
|
+
export function normalizeRatingCount(count: number | undefined): number {
|
|
165
|
+
if (count === undefined || !Number.isFinite(count)) return RATING_COUNT;
|
|
166
|
+
const whole = Math.floor(count);
|
|
167
|
+
return whole >= 1 ? whole : RATING_COUNT;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A step the stars can land on.
|
|
172
|
+
*
|
|
173
|
+
* It has to divide one star: 0.5 and 0.25 do, 0.3 and 0.4 do not, and a step
|
|
174
|
+
* that does not would leave a star whose last stop is somewhere short of full.
|
|
175
|
+
* Anything else — past one star, zero, negative, non-finite — falls back to
|
|
176
|
+
* whole stars rather than throwing, because a rating is usually fed from a
|
|
177
|
+
* server that got the number from somewhere.
|
|
178
|
+
*/
|
|
179
|
+
export function normalizeRatingStep(step: number | undefined): number {
|
|
180
|
+
if (step === undefined || !Number.isFinite(step) || step <= 0 || step > 1) return RATING_STEP;
|
|
181
|
+
const divisions = 1 / step;
|
|
182
|
+
return Math.abs(divisions - Math.round(divisions)) < 1e-9 ? step : RATING_STEP;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The value held inside zero and the count.
|
|
187
|
+
*
|
|
188
|
+
* It does **not** snap. `step` constrains what a finger can set; a read-only
|
|
189
|
+
* average of 3.7 is drawn at 3.7. A non-finite value is zero, because a star
|
|
190
|
+
* whose width came out of `NaN` is a star that is not drawn and a row that lies.
|
|
191
|
+
*/
|
|
192
|
+
export function clampRating(value: number, count: number): number {
|
|
193
|
+
if (!Number.isFinite(value)) return 0;
|
|
194
|
+
return value < 0 ? 0 : value > count ? count : value;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* How much of the star at `index` is filled, as 0–1.
|
|
199
|
+
*
|
|
200
|
+
* Every star before the value is full, every star after it empty, and the one
|
|
201
|
+
* the value lands inside takes the remainder — so the fills always sum back to
|
|
202
|
+
* the value, which is what a test asserts.
|
|
203
|
+
*/
|
|
204
|
+
export function starFillOf(index: number, value: number): number {
|
|
205
|
+
const fill = value - index;
|
|
206
|
+
return fill < 0 ? 0 : fill > 1 ? 1 : fill;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The value a touch at `position` points at.
|
|
211
|
+
*
|
|
212
|
+
* `position` is the touch's offset along the row, `width` the row's own measured
|
|
213
|
+
* width, and every star owns an equal cell of it. The value rounds **up** to the
|
|
214
|
+
* next stop, so any touch inside a star's cell takes that star — with half stars,
|
|
215
|
+
* its left half is the half and its right half the whole. Ending exactly on a
|
|
216
|
+
* cell edge belongs to the star before it, not a sliver of the next.
|
|
217
|
+
*
|
|
218
|
+
* A drag past the start holds the lowest stop rather than clearing. Clearing is
|
|
219
|
+
* a separate, deliberate gesture — see {@link shouldClearRating} — because a drag
|
|
220
|
+
* that overshoots the first star is someone rating one, not someone withdrawing.
|
|
221
|
+
*
|
|
222
|
+
* `width <= 0` is **not measured yet**, and reports zero rather than dividing.
|
|
223
|
+
*/
|
|
224
|
+
export function ratingFromOffset({
|
|
225
|
+
position,
|
|
226
|
+
width,
|
|
227
|
+
count,
|
|
228
|
+
step,
|
|
229
|
+
}: {
|
|
230
|
+
position: number;
|
|
231
|
+
width: number;
|
|
232
|
+
count: number;
|
|
233
|
+
step: number;
|
|
234
|
+
}): number {
|
|
235
|
+
"worklet";
|
|
236
|
+
if (width <= 0 || count <= 0 || step <= 0) return 0;
|
|
237
|
+
|
|
238
|
+
const raw = (position / width) * count;
|
|
239
|
+
const clamped = raw < 0 ? 0 : raw > count ? count : raw;
|
|
240
|
+
// The epsilon keeps a touch exactly on a stop from rounding up a whole step
|
|
241
|
+
// past it, which is where `Math.ceil` and binary floating point disagree.
|
|
242
|
+
const snapped = Math.ceil(clamped / step - 1e-9) * step;
|
|
243
|
+
const bounded = snapped < step ? step : snapped > count ? count : snapped;
|
|
244
|
+
|
|
245
|
+
return Math.round(bounded * 1e10) / 1e10;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* One assistive increment or decrement from `value`.
|
|
250
|
+
*
|
|
251
|
+
* A value between stops — a read-only 3.7 made interactive — lands on the
|
|
252
|
+
* **next** stop in the direction asked rather than a whole step past it, so an
|
|
253
|
+
* increment from 3.7 in whole stars is 4, not 4.7 clamped to 5.
|
|
254
|
+
*/
|
|
255
|
+
export function stepRating(value: number, direction: 1 | -1, step: number, count: number): number {
|
|
256
|
+
const stops = value / step;
|
|
257
|
+
const onStop = Math.abs(stops - Math.round(stops)) < 1e-9;
|
|
258
|
+
const base = onStop ? Math.round(stops) : direction > 0 ? Math.floor(stops) : Math.ceil(stops);
|
|
259
|
+
const next = (base + direction) * step;
|
|
260
|
+
const bounded = next < 0 ? 0 : next > count ? count : next;
|
|
261
|
+
|
|
262
|
+
return Math.round(bounded * 1e10) / 1e10;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Whether a gesture that has just ended should clear the rating.
|
|
267
|
+
*
|
|
268
|
+
* Only a **tap**, only on the **value already held**, and only when the caller
|
|
269
|
+
* asked for it. A drag that wanders off and comes back to where it started is a
|
|
270
|
+
* drag — reading `travel` is what tells them apart. A rating already at zero has
|
|
271
|
+
* nothing to clear.
|
|
272
|
+
*/
|
|
273
|
+
export function shouldClearRating({
|
|
274
|
+
allowClear,
|
|
275
|
+
startValue,
|
|
276
|
+
touchedValue,
|
|
277
|
+
travel,
|
|
278
|
+
}: {
|
|
279
|
+
allowClear: boolean;
|
|
280
|
+
startValue: number;
|
|
281
|
+
touchedValue: number;
|
|
282
|
+
travel: number;
|
|
283
|
+
}): boolean {
|
|
284
|
+
"worklet";
|
|
285
|
+
if (!allowClear || startValue <= 0) return false;
|
|
286
|
+
if (travel > RATING_TAP_SLOP) return false;
|
|
287
|
+
return touchedValue === startValue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The value as a readout: no decimals for a whole value, at most two otherwise.
|
|
292
|
+
*
|
|
293
|
+
* JS-thread only — `Intl` is not available to a worklet.
|
|
294
|
+
*/
|
|
295
|
+
export function formatRatingValue(value: number): string {
|
|
296
|
+
return new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(value);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** What a screen reader says for the value: "3.5 out of 5". */
|
|
300
|
+
export function describeRating(value: number, count: number): string {
|
|
301
|
+
return `${formatRatingValue(value)} out of ${count}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The two tokens a rating's stars are painted with. */
|
|
305
|
+
export type RatingPaint = { fill: string; empty: string };
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Which tokens paint the filled and the empty stars.
|
|
309
|
+
*
|
|
310
|
+
* **An empty star is the same chrome at every colour**, the way an empty slider
|
|
311
|
+
* groove is — a test asserts that. **Invalid outranks the colour, on both**: a
|
|
312
|
+
* required rating left at zero has no filled star to turn red, so the empty
|
|
313
|
+
* outlines have to carry the signal or there is none.
|
|
314
|
+
*/
|
|
315
|
+
export function resolveRatingPaint({ color, isInvalid }: { color: RatingColor; isInvalid: boolean }): RatingPaint {
|
|
316
|
+
if (isInvalid) return { fill: "destructive", empty: "destructive" };
|
|
317
|
+
return { fill: RATING_FILL_TOKEN[color], empty: RATING_EMPTY_TOKEN };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** What a rating was given at its own call site. */
|
|
321
|
+
export type RatingOwnAxes = {
|
|
322
|
+
color?: RatingColor;
|
|
323
|
+
size?: RatingSize;
|
|
324
|
+
isDisabled?: boolean;
|
|
325
|
+
isInvalid?: boolean;
|
|
326
|
+
isReadOnly?: boolean;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/** What an enclosing `Field` publishes, or null outside one. */
|
|
330
|
+
export type RatingFieldAxes = { isDisabled?: boolean; isInvalid?: boolean };
|
|
331
|
+
|
|
332
|
+
/** Every axis settled, ready to hand to {@link ratingVariants} and to context. */
|
|
333
|
+
export type RatingAxes = Required<RatingOwnAxes>;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Settles a rating's axes from the two places they can come from.
|
|
337
|
+
*
|
|
338
|
+
* The same two-rung ladder `Slider` runs: the rating's own props, then an
|
|
339
|
+
* enclosing `Field`, with `??` throughout so an explicit `false` is a value.
|
|
340
|
+
* A `Field` reaches the two *state* axes only. `isReadOnly` is the rating's own:
|
|
341
|
+
* a field has no notion of it, and a form that shows a score it will not let
|
|
342
|
+
* you change says so at the control.
|
|
343
|
+
*/
|
|
344
|
+
export function resolveRatingAxes({ own, field }: { own?: RatingOwnAxes; field?: RatingFieldAxes | null }): RatingAxes {
|
|
345
|
+
return {
|
|
346
|
+
color: own?.color ?? RATING_DEFAULT_COLOR,
|
|
347
|
+
size: own?.size ?? RATING_DEFAULT_SIZE,
|
|
348
|
+
isDisabled: own?.isDisabled ?? field?.isDisabled ?? false,
|
|
349
|
+
isInvalid: own?.isInvalid ?? field?.isInvalid ?? false,
|
|
350
|
+
isReadOnly: own?.isReadOnly ?? false,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export type RatingVariantProps = VariantProps<typeof ratingVariants>;
|