@delacour/react-native-ui 0.1.0-alpha.4 → 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 +10 -6
- package/package.json +48 -4
- 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 -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 +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 +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/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 +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,185 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { declaredTokens } from "../../styles/theme-tokens.test";
|
|
3
|
+
import {
|
|
4
|
+
canSubmitFeedback,
|
|
5
|
+
FEEDBACK_DEFAULT_MAX_ROWS,
|
|
6
|
+
FEEDBACK_DEFAULT_MIN_ROWS,
|
|
7
|
+
FEEDBACK_FIELD_LINE_HEIGHT,
|
|
8
|
+
FEEDBACK_PANEL_RESIZE_MS,
|
|
9
|
+
feedbackVariants,
|
|
10
|
+
resolveFeedbackFieldHeightStyle,
|
|
11
|
+
resolveFeedbackFieldMinHeight,
|
|
12
|
+
} from "./feedback.variants";
|
|
13
|
+
|
|
14
|
+
const LIGHT = declaredTokens("light");
|
|
15
|
+
const DARK = declaredTokens("dark");
|
|
16
|
+
|
|
17
|
+
/** `border-t` and friends set a width, not a colour, and name no token. */
|
|
18
|
+
const STRUCTURAL_BORDER_SUFFIXES = new Set(["t", "b", "l", "r", "x", "y", "s", "e"]);
|
|
19
|
+
|
|
20
|
+
/** Every theme token a class string paints with, with any `/alpha` suffix dropped. */
|
|
21
|
+
function colorTokens(cls: string): string[] {
|
|
22
|
+
const tokens: string[] = [];
|
|
23
|
+
|
|
24
|
+
for (const [, utility, token] of cls.matchAll(/\b(bg|border|text)-([a-z][\w-]*)(?:\/\d+)?\b/g)) {
|
|
25
|
+
if (utility === "border" && STRUCTURAL_BORDER_SUFFIXES.has(token)) continue;
|
|
26
|
+
// `text-input-md` is a size token, not a colour.
|
|
27
|
+
if (utility === "text" && token.startsWith("input-")) continue;
|
|
28
|
+
tokens.push(token);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return tokens;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Pinned rather than derived — `tv` adds a `base` slot of its own. */
|
|
35
|
+
const SLOT_NAMES = ["content", "clip", "panel", "title", "close", "field", "footer"] as const;
|
|
36
|
+
|
|
37
|
+
describe("feedbackVariants", () => {
|
|
38
|
+
test("the shell paints the muted band and is tighter than a dialog's p-5", () => {
|
|
39
|
+
const content = feedbackVariants().content();
|
|
40
|
+
expect(content).toContain("bg-muted");
|
|
41
|
+
expect(content).toMatch(/\bp-[1-3]\b/);
|
|
42
|
+
expect(content).not.toContain("p-5");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("the panel is a recessed well on the page colour, inset with the md corner and a hairline", () => {
|
|
46
|
+
const panel = feedbackVariants().panel();
|
|
47
|
+
expect(panel).toContain("bg-background");
|
|
48
|
+
expect(panel).toContain("rounded-md");
|
|
49
|
+
expect(panel).toContain("border");
|
|
50
|
+
expect(panel).toContain("border-border");
|
|
51
|
+
expect(panel).toMatch(/\bp-\d/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("the clip hides the well's overflow mid-resize and shares its corner", () => {
|
|
55
|
+
const clip = feedbackVariants().clip();
|
|
56
|
+
expect(clip).toContain("overflow-hidden");
|
|
57
|
+
expect(clip).toContain("rounded-md");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("the title reserves clearance for the close glyph and carries no type of its own", () => {
|
|
61
|
+
const title = feedbackVariants().title();
|
|
62
|
+
expect(title).toBe("pr-8");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("the close glyph sits in the panel's top-right corner", () => {
|
|
66
|
+
const close = feedbackVariants().close();
|
|
67
|
+
expect(close).toContain("absolute");
|
|
68
|
+
expect(close).toMatch(/\btop-\d/);
|
|
69
|
+
expect(close).toMatch(/\bright-\d/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("the field has no box of its own and sets the input type on the house face", () => {
|
|
73
|
+
const field = feedbackVariants().field();
|
|
74
|
+
expect(field).toContain("font-sans");
|
|
75
|
+
expect(field).toContain("text-input-md");
|
|
76
|
+
expect(field).toContain("text-foreground");
|
|
77
|
+
expect(field).toContain("leading-6");
|
|
78
|
+
expect(field).not.toMatch(/\bborder\b/);
|
|
79
|
+
expect(field).not.toMatch(/\bbg-/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("the footer is an end-aligned row with a narrower inset than the well's", () => {
|
|
83
|
+
const footer = feedbackVariants().footer();
|
|
84
|
+
expect(footer).toContain("flex-row");
|
|
85
|
+
expect(footer).toContain("items-center");
|
|
86
|
+
expect(footer).toContain("justify-end");
|
|
87
|
+
expect(footer).toContain("gap-2");
|
|
88
|
+
expect(footer).not.toMatch(/\bp-[4-9]\b/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("a caller's className reaches each slot and wins a conflict", () => {
|
|
92
|
+
const panel = feedbackVariants().panel({ className: "rounded-xl" });
|
|
93
|
+
expect(panel).toContain("rounded-xl");
|
|
94
|
+
expect(panel).not.toContain("rounded-md");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("every token the slots name", () => {
|
|
99
|
+
test("is declared in both variants of theme.css", () => {
|
|
100
|
+
const slots = feedbackVariants();
|
|
101
|
+
for (const name of SLOT_NAMES) {
|
|
102
|
+
for (const token of colorTokens(slots[name]())) {
|
|
103
|
+
expect({ inLight: LIGHT.has(token), slot: name, token }).toEqual({ inLight: true, slot: name, token });
|
|
104
|
+
expect({ inDark: DARK.has(token), slot: name, token }).toEqual({ inDark: true, slot: name, token });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("the reader found tokens at all", () => {
|
|
110
|
+
expect(colorTokens(feedbackVariants().content())).toContain("muted");
|
|
111
|
+
expect(colorTokens(feedbackVariants().panel())).toEqual(expect.arrayContaining(["background", "border"]));
|
|
112
|
+
expect(colorTokens(feedbackVariants().field())).toEqual(["foreground"]);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("canSubmitFeedback", () => {
|
|
117
|
+
test("an empty message cannot be sent", () => {
|
|
118
|
+
expect(canSubmitFeedback({ value: "" })).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("whitespace is empty", () => {
|
|
122
|
+
expect(canSubmitFeedback({ value: " \n\t " })).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("any text can be sent", () => {
|
|
126
|
+
expect(canSubmitFeedback({ value: "The export button is hidden" })).toBe(true);
|
|
127
|
+
expect(canSubmitFeedback({ value: " x " })).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("canSubmitEmpty lets an empty message through", () => {
|
|
131
|
+
expect(canSubmitFeedback({ canSubmitEmpty: true, value: "" })).toBe(true);
|
|
132
|
+
expect(canSubmitFeedback({ canSubmitEmpty: true, value: " " })).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("disabled outranks everything", () => {
|
|
136
|
+
expect(canSubmitFeedback({ isDisabled: true, value: "text" })).toBe(false);
|
|
137
|
+
expect(canSubmitFeedback({ canSubmitEmpty: true, isDisabled: true, value: "" })).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("resolveFeedbackFieldMinHeight", () => {
|
|
142
|
+
test("the line height is the field's leading-6", () => {
|
|
143
|
+
expect(FEEDBACK_FIELD_LINE_HEIGHT).toBe(24);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("defaults to six rows", () => {
|
|
147
|
+
expect(FEEDBACK_DEFAULT_MIN_ROWS).toBe(6);
|
|
148
|
+
expect(resolveFeedbackFieldMinHeight(undefined, FEEDBACK_FIELD_LINE_HEIGHT)).toBe(144);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("is rows times the line height", () => {
|
|
152
|
+
expect(resolveFeedbackFieldMinHeight(3, 24)).toBe(72);
|
|
153
|
+
expect(resolveFeedbackFieldMinHeight(1, 20)).toBe(20);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("floors to whole rows and clamps to at least one", () => {
|
|
157
|
+
expect(resolveFeedbackFieldMinHeight(2.7, 24)).toBe(48);
|
|
158
|
+
expect(resolveFeedbackFieldMinHeight(0, 24)).toBe(24);
|
|
159
|
+
expect(resolveFeedbackFieldMinHeight(-3, 24)).toBe(24);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("falls back to the default on a number that is not one", () => {
|
|
163
|
+
expect(resolveFeedbackFieldMinHeight(Number.NaN, 24)).toBe(144);
|
|
164
|
+
expect(resolveFeedbackFieldMinHeight(Number.POSITIVE_INFINITY, 24)).toBe(144);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("resolveFeedbackFieldHeightStyle", () => {
|
|
169
|
+
test("floors at minRows and caps at maxRows", () => {
|
|
170
|
+
expect(FEEDBACK_DEFAULT_MAX_ROWS).toBe(12);
|
|
171
|
+
expect(resolveFeedbackFieldHeightStyle({})).toEqual({ maxHeight: 288, minHeight: 144 });
|
|
172
|
+
expect(resolveFeedbackFieldHeightStyle({ maxRows: 4, minRows: 2 })).toEqual({ maxHeight: 96, minHeight: 48 });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("lifts a ceiling below the floor up to it, so the field never shrinks as it grows", () => {
|
|
176
|
+
expect(resolveFeedbackFieldHeightStyle({ maxRows: 2, minRows: 6 })).toEqual({ maxHeight: 144, minHeight: 144 });
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("FEEDBACK_PANEL_RESIZE_MS", () => {
|
|
181
|
+
test("is short and finite, so a step change settles before the next tap", () => {
|
|
182
|
+
expect(FEEDBACK_PANEL_RESIZE_MS).toBeGreaterThan(0);
|
|
183
|
+
expect(FEEDBACK_PANEL_RESIZE_MS).toBeLessThanOrEqual(250);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { VariantProps } from "tailwind-variants";
|
|
2
|
+
import { tv } from "../../lib/tv";
|
|
3
|
+
|
|
4
|
+
/** How many lines tall the field is before it grows, when `minRows` is not named. */
|
|
5
|
+
export const FEEDBACK_DEFAULT_MIN_ROWS = 6;
|
|
6
|
+
|
|
7
|
+
/** The tallest the field grows before it scrolls, when `maxRows` is not named. */
|
|
8
|
+
export const FEEDBACK_DEFAULT_MAX_ROWS = 12;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* How long the well takes to follow a change in its content's height — a
|
|
12
|
+
* multi-step swap, or a line added while typing. Short, so a new line is never
|
|
13
|
+
* clipped for long, and finite, so an E2E runner's settle wait ends.
|
|
14
|
+
*/
|
|
15
|
+
export const FEEDBACK_PANEL_RESIZE_MS = 200;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Points per line of the field — the `leading-6` its slot sets. The test reads
|
|
19
|
+
* the class back, so the two cannot drift apart.
|
|
20
|
+
*/
|
|
21
|
+
export const FEEDBACK_FIELD_LINE_HEIGHT = 24;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Styling for every part of a feedback dialog.
|
|
25
|
+
*
|
|
26
|
+
* The shell is `Dialog.Content` with these classes merged over the dialog's
|
|
27
|
+
* own, so `content` only names what differs from a confirm dialog: the muted
|
|
28
|
+
* band and a tighter padding, which is what makes the well read as inset.
|
|
29
|
+
* Everything else — the corner, the hairline, the width cap — stays Dialog's.
|
|
30
|
+
*
|
|
31
|
+
* Free of React Native imports so it stays unit-testable. See AGENTS.md.
|
|
32
|
+
*/
|
|
33
|
+
export const feedbackVariants = tv({
|
|
34
|
+
slots: {
|
|
35
|
+
/** The shell: the dialog's card, on the band colour, with a tight inset around the well. */
|
|
36
|
+
content: "gap-2 bg-muted p-2",
|
|
37
|
+
/**
|
|
38
|
+
* The animated clip around the well. Its height follows the well's on a
|
|
39
|
+
* timing; its corner matches the well's so nothing pokes out mid-resize.
|
|
40
|
+
*/
|
|
41
|
+
clip: "overflow-hidden rounded-md",
|
|
42
|
+
/**
|
|
43
|
+
* The recessed well: the page colour, one step down the corner ramp from
|
|
44
|
+
* the card it sits in so the two read as nested, and a hairline. Relative
|
|
45
|
+
* so the corner ✕ positions against it rather than the shell.
|
|
46
|
+
*/
|
|
47
|
+
panel: "relative gap-2 overflow-hidden rounded-md border border-border bg-background p-4",
|
|
48
|
+
/** Clearance for `Feedback.Close`, and nothing else — the type is `Text.Header`'s. */
|
|
49
|
+
title: "pr-8",
|
|
50
|
+
close: "absolute top-3 right-3 z-10",
|
|
51
|
+
/**
|
|
52
|
+
* A raw `TextInput` with no box: the well is the box. It restates
|
|
53
|
+
* `font-sans` for the reason `Input`'s field does — a `TextInput`
|
|
54
|
+
* inherits nothing — and sets paragraph leading, which its min height
|
|
55
|
+
* is counted in.
|
|
56
|
+
*/
|
|
57
|
+
field: "p-0 font-sans text-foreground text-input-md leading-6",
|
|
58
|
+
/** Narrower than the well's inset, so the actions sit on the band rather than in it. */
|
|
59
|
+
footer: "flex-row items-center justify-end gap-2 px-1 pb-1",
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export type FeedbackVariantProps = VariantProps<typeof feedbackVariants>;
|
|
64
|
+
|
|
65
|
+
export type CanSubmitFeedbackInput = {
|
|
66
|
+
/** The draft as typed. */
|
|
67
|
+
value: string;
|
|
68
|
+
/** Allow an empty message — when chips or a rating carry the answer. Default false. */
|
|
69
|
+
canSubmitEmpty?: boolean;
|
|
70
|
+
/** The submit is off — disabled by the caller, or a send already in flight. Default false. */
|
|
71
|
+
isDisabled?: boolean;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Whether `Feedback.Submit` can be pressed.
|
|
76
|
+
*
|
|
77
|
+
* Whitespace is empty: a message of three spaces sends nothing, and `onSubmit`
|
|
78
|
+
* receives the trimmed text anyway. Disabled outranks `canSubmitEmpty`.
|
|
79
|
+
*
|
|
80
|
+
* Pure, so it is reachable from `bun test`. See AGENTS.md.
|
|
81
|
+
*/
|
|
82
|
+
export function canSubmitFeedback({
|
|
83
|
+
value,
|
|
84
|
+
canSubmitEmpty = false,
|
|
85
|
+
isDisabled = false,
|
|
86
|
+
}: CanSubmitFeedbackInput): boolean {
|
|
87
|
+
if (isDisabled) return false;
|
|
88
|
+
if (canSubmitEmpty) return true;
|
|
89
|
+
return value.trim().length > 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The field's minimum height, in points — `minRows` lines of `lineHeight`.
|
|
94
|
+
*
|
|
95
|
+
* A style, never a class: a runtime `` `min-h-[${n}px]` `` is never compiled by
|
|
96
|
+
* Tailwind's static scanner. A bad row count degrades the way `Textarea`'s
|
|
97
|
+
* does — floored to whole lines, clamped to one, the default on `NaN`.
|
|
98
|
+
*
|
|
99
|
+
* Pure, so it is reachable from `bun test`. See AGENTS.md.
|
|
100
|
+
*/
|
|
101
|
+
export function resolveFeedbackFieldMinHeight(minRows: number | undefined, lineHeight: number): number {
|
|
102
|
+
const rows =
|
|
103
|
+
minRows === undefined || !Number.isFinite(minRows) ? FEEDBACK_DEFAULT_MIN_ROWS : Math.max(1, Math.floor(minRows));
|
|
104
|
+
return rows * lineHeight;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The field's height bounds, as a style: floored at `minRows`, capped at
|
|
109
|
+
* `maxRows`, and between the two React Native's multiline `TextInput` sizes
|
|
110
|
+
* itself to its text. Past the cap it scrolls — a dialog that grew without
|
|
111
|
+
* one would push its footer under the keyboard.
|
|
112
|
+
*
|
|
113
|
+
* A ceiling below the floor is lifted up to it, so the field never shrinks as
|
|
114
|
+
* it grows. Pure, so it is reachable from `bun test`. See AGENTS.md.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveFeedbackFieldHeightStyle({
|
|
117
|
+
minRows,
|
|
118
|
+
maxRows,
|
|
119
|
+
lineHeight = FEEDBACK_FIELD_LINE_HEIGHT,
|
|
120
|
+
}: {
|
|
121
|
+
minRows?: number;
|
|
122
|
+
maxRows?: number;
|
|
123
|
+
lineHeight?: number;
|
|
124
|
+
}): { minHeight: number; maxHeight: number } {
|
|
125
|
+
const minHeight = resolveFeedbackFieldMinHeight(minRows, lineHeight);
|
|
126
|
+
const ceiling = resolveFeedbackFieldMinHeight(maxRows ?? FEEDBACK_DEFAULT_MAX_ROWS, lineHeight);
|
|
127
|
+
return { maxHeight: Math.max(minHeight, ceiling), minHeight };
|
|
128
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { Feedback, type FeedbackProps } from "./feedback";
|
|
2
|
+
export { type FeedbackContextValue, useFeedback, useFeedbackContext } from "./feedback.context";
|
|
3
|
+
export {
|
|
4
|
+
type CanSubmitFeedbackInput,
|
|
5
|
+
canSubmitFeedback,
|
|
6
|
+
FEEDBACK_DEFAULT_MAX_ROWS,
|
|
7
|
+
FEEDBACK_DEFAULT_MIN_ROWS,
|
|
8
|
+
FEEDBACK_FIELD_LINE_HEIGHT,
|
|
9
|
+
FEEDBACK_PANEL_RESIZE_MS,
|
|
10
|
+
type FeedbackVariantProps,
|
|
11
|
+
feedbackVariants,
|
|
12
|
+
resolveFeedbackFieldHeightStyle,
|
|
13
|
+
resolveFeedbackFieldMinHeight,
|
|
14
|
+
} from "./feedback.variants";
|
|
15
|
+
export type { FeedbackActionProps } from "./feedback-action";
|
|
16
|
+
export type { FeedbackCancelProps } from "./feedback-cancel";
|
|
17
|
+
export type { FeedbackCloseProps } from "./feedback-close";
|
|
18
|
+
export type { FeedbackContentProps } from "./feedback-content";
|
|
19
|
+
export type { FeedbackFieldProps } from "./feedback-field";
|
|
20
|
+
export type { FeedbackFooterProps } from "./feedback-footer";
|
|
21
|
+
export type { FeedbackPanelProps } from "./feedback-panel";
|
|
22
|
+
export type { FeedbackSubmitProps } from "./feedback-submit";
|
|
23
|
+
export type { FeedbackTitleProps } from "./feedback-title";
|
|
24
|
+
export type { FeedbackTriggerProps } from "./feedback-trigger";
|
|
@@ -52,6 +52,18 @@ A form field's layout, and the one place its state is written down. Root plus
|
|
|
52
52
|
`accessible={false}`, so the control stays the element a screen reader sees,
|
|
53
53
|
and the inner detector claims a tap on the box itself rather than firing both.
|
|
54
54
|
A field holds one control, so a second registration replaces the first.
|
|
55
|
+
- **The label names the control, through the same context.** `Field.Label`
|
|
56
|
+
registers its text with `registerLabel`, the mirror of `registerPress`: a
|
|
57
|
+
control hands its press up, a label hands its name down. A control with no
|
|
58
|
+
text of its own — a [Slider](../slider/AGENTS.md) thumb is a capsule — reads
|
|
59
|
+
`field.label` as its `accessibilityLabel`, so "Volume" is announced rather than
|
|
60
|
+
a bare "40". React Native has no `<label for>`; this is that association, done
|
|
61
|
+
by hand. `resolveFieldLabelText` decides what qualifies and it is strict on
|
|
62
|
+
purpose: a plain string or number, trimmed, and nothing else. A label built
|
|
63
|
+
from elements has no text this side of a render, and guessing at one — an
|
|
64
|
+
icon's key, say — would name the control after the wrong thing. State rather
|
|
65
|
+
than a ref for the same reason as the press: the control renders differently
|
|
66
|
+
for it, and the label registers in an effect, so it costs one render on mount.
|
|
55
67
|
- **A data-attribute class would also leave `bun test`.** Even for a part styling
|
|
56
68
|
itself, `data-invalid:text-destructive` moves the decision from `field.variants.ts`
|
|
57
69
|
into uniwind's runtime matcher, where no unit test can see it. The parts style
|
|
@@ -91,6 +103,13 @@ A form field's layout, and the one place its state is written down. Root plus
|
|
|
91
103
|
- **`Field.Group` inserts no dividers**, unlike [`ListGroup`](../list-group/AGENTS.md). A list of rows
|
|
92
104
|
without lines is a wall of text; fields are already held apart by whitespace,
|
|
93
105
|
and a rule between every one is noise.
|
|
106
|
+
- **`Field.Label` is not built on [`Label`](../label/AGENTS.md)**, the standalone
|
|
107
|
+
primitive. `Label` narrows `children` from `Text`'s and reports
|
|
108
|
+
`accessibilityState.disabled`, so wrapping it would change this part's public
|
|
109
|
+
surface. The two colour and fade identically instead, and
|
|
110
|
+
`label.variants.test.ts` holds them to it against `fieldVariants`' label slot
|
|
111
|
+
and `resolveFieldTextColor`. A required field uses `Label` directly, since
|
|
112
|
+
`Field.Label` has no `isRequired`.
|
|
94
113
|
- **There is no `Field.Title`.** On the web it exists because a `<div>` is not a
|
|
95
114
|
`<label>` — label-styled text with nothing to point `htmlFor` at. React Native
|
|
96
115
|
has neither element nor association, so it and `Field.Label` would render the
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactElement, useEffect } from "react";
|
|
2
2
|
import { Text } from "../text";
|
|
3
3
|
import { useFieldContext } from "./field.context";
|
|
4
4
|
import type { FieldTextProps } from "./field.types";
|
|
5
|
-
import { fieldVariants, resolveFieldTextColor } from "./field.variants";
|
|
5
|
+
import { fieldVariants, resolveFieldLabelText, resolveFieldTextColor } from "./field.variants";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* The name of the control this field holds.
|
|
@@ -15,19 +15,33 @@ import { fieldVariants, resolveFieldTextColor } from "./field.variants";
|
|
|
15
15
|
* There is no `htmlFor`. React Native has no `<label>` element and no
|
|
16
16
|
* label-for-control association, which is also why there is no `Field.Title`:
|
|
17
17
|
* on the web that part exists only because a `<div>` is not a `<label>`, and
|
|
18
|
-
* here both would be the same `Text`.
|
|
18
|
+
* here both would be the same `Text`. **The association is made by hand
|
|
19
|
+
* instead**: a string label hands its text to the field's context, and a control
|
|
20
|
+
* inside with no text of its own — a `Slider.Thumb` — reads it as its accessible
|
|
21
|
+
* name. `resolveFieldLabelText` decides what qualifies.
|
|
19
22
|
*/
|
|
20
|
-
export function FieldLabel({ className, color, ...props }: FieldTextProps): ReactElement {
|
|
23
|
+
export function FieldLabel({ className, color, children, ...props }: FieldTextProps): ReactElement {
|
|
21
24
|
const field = useFieldContext();
|
|
22
25
|
const isInvalid = field?.isInvalid ?? false;
|
|
23
26
|
const isDisabled = field?.isDisabled ?? false;
|
|
24
27
|
|
|
28
|
+
const registerLabel = field?.registerLabel;
|
|
29
|
+
const labelText = resolveFieldLabelText(children);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!registerLabel) return;
|
|
33
|
+
registerLabel(labelText);
|
|
34
|
+
return () => registerLabel(null);
|
|
35
|
+
}, [labelText, registerLabel]);
|
|
36
|
+
|
|
25
37
|
return (
|
|
26
38
|
<Text.Label
|
|
27
39
|
className={fieldVariants({ isDisabled, isInvalid }).label({ className })}
|
|
28
40
|
color={color ?? resolveFieldTextColor("label", isInvalid)}
|
|
29
41
|
{...props}
|
|
30
|
-
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
</Text.Label>
|
|
31
45
|
);
|
|
32
46
|
}
|
|
33
47
|
FieldLabel.displayName = "DelacourUI.Field.Label";
|
|
@@ -17,6 +17,23 @@ export type FieldContextValue = {
|
|
|
17
17
|
* second registration replaces the first.
|
|
18
18
|
*/
|
|
19
19
|
registerPress: (press: (() => void) | null) => void;
|
|
20
|
+
/**
|
|
21
|
+
* The text of the `Field.Label` inside, or `null` while there is none.
|
|
22
|
+
*
|
|
23
|
+
* What a control reads for its accessible name when it holds no text of its
|
|
24
|
+
* own — a `Slider.Thumb` is a capsule, and a `Switch` is a pill. React Native
|
|
25
|
+
* has no `<label for>`; this is that association, carried by the same context
|
|
26
|
+
* the state cascades down.
|
|
27
|
+
*/
|
|
28
|
+
label: string | null;
|
|
29
|
+
/**
|
|
30
|
+
* A `Field.Label` offers its text here. Pass `null` on unmount.
|
|
31
|
+
*
|
|
32
|
+
* The counterpart of {@link FieldContextValue.registerPress} going the same
|
|
33
|
+
* way: a control hands its press up, a label hands its name down. A field
|
|
34
|
+
* holds one label, so a second registration replaces the first.
|
|
35
|
+
*/
|
|
36
|
+
registerLabel: (label: string | null) => void;
|
|
20
37
|
};
|
|
21
38
|
|
|
22
39
|
const FieldContext = createContext<FieldContextValue | null>(null);
|
|
@@ -37,9 +37,15 @@ function FieldRoot({
|
|
|
37
37
|
// The updater form would call `press` instead of storing it.
|
|
38
38
|
const registerPress = useCallback((press: (() => void) | null) => setControlPress(() => press), []);
|
|
39
39
|
|
|
40
|
+
// The label's text, for a control inside to read as its accessible name.
|
|
41
|
+
// State for the same reason as the press: a control renders differently for
|
|
42
|
+
// it, and the label registers in an effect, so it costs one render on mount.
|
|
43
|
+
const [label, setLabel] = useState<string | null>(null);
|
|
44
|
+
const registerLabel = useCallback((next: string | null) => setLabel(next), []);
|
|
45
|
+
|
|
40
46
|
const context = useMemo<FieldContextValue>(
|
|
41
|
-
() => ({ isDisabled, isInvalid, orientation, registerPress }),
|
|
42
|
-
[isDisabled, isInvalid, orientation, registerPress]
|
|
47
|
+
() => ({ isDisabled, isInvalid, label, orientation, registerLabel, registerPress }),
|
|
48
|
+
[isDisabled, isInvalid, label, orientation, registerLabel, registerPress]
|
|
43
49
|
);
|
|
44
50
|
|
|
45
51
|
const rootClassName = fieldVariants({ isDisabled, isInvalid, orientation }).root({ className });
|
|
@@ -86,6 +92,13 @@ function FieldRoot({
|
|
|
86
92
|
* A control's own prop still wins, so one field inside an invalid group can opt
|
|
87
93
|
* out with `<Input isInvalid={false} />`.
|
|
88
94
|
*
|
|
95
|
+
* **The label names the control, too.** `Field.Label` hands its text down the
|
|
96
|
+
* same context, so a control with no text of its own — a `Slider.Thumb`, a
|
|
97
|
+
* `Switch` — reads "Volume" to a screen reader rather than a bare number. React
|
|
98
|
+
* Native has no `<label for>`; this is that association. Only a plain string or
|
|
99
|
+
* number label is lent, since a label built from elements has no text to read
|
|
100
|
+
* before it renders.
|
|
101
|
+
*
|
|
89
102
|
* **The whole row drives the control**, once one offers a press through the same
|
|
90
103
|
* context — so tapping "Accept the terms", or the description under it, ticks
|
|
91
104
|
* the `Checkbox` beside it. A checkbox in a form is a small square next to a
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type FieldTextPart,
|
|
8
8
|
fieldVariants,
|
|
9
9
|
resolveFieldInteractive,
|
|
10
|
+
resolveFieldLabelText,
|
|
10
11
|
resolveFieldTextColor,
|
|
11
12
|
} from "./field.variants";
|
|
12
13
|
|
|
@@ -214,3 +215,26 @@ describe("resolveFieldInteractive", () => {
|
|
|
214
215
|
expect(resolveFieldInteractive(() => undefined)).toBe(true);
|
|
215
216
|
});
|
|
216
217
|
});
|
|
218
|
+
|
|
219
|
+
describe("resolveFieldLabelText", () => {
|
|
220
|
+
test("hands a string label through, trimmed", () => {
|
|
221
|
+
expect(resolveFieldLabelText("Volume")).toBe("Volume");
|
|
222
|
+
expect(resolveFieldLabelText(" Volume\n")).toBe("Volume");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("reads a number the way a screen reader would", () => {
|
|
226
|
+
expect(resolveFieldLabelText(42)).toBe("42");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// A label made of elements has no text this side of a render, and an empty
|
|
230
|
+
// string is not a name — both leave the control to name itself.
|
|
231
|
+
test("offers nothing for an element, an empty string, or no children at all", () => {
|
|
232
|
+
expect(resolveFieldLabelText("")).toBeNull();
|
|
233
|
+
expect(resolveFieldLabelText(" ")).toBeNull();
|
|
234
|
+
expect(resolveFieldLabelText(undefined)).toBeNull();
|
|
235
|
+
expect(resolveFieldLabelText(null)).toBeNull();
|
|
236
|
+
expect(resolveFieldLabelText(false)).toBeNull();
|
|
237
|
+
expect(resolveFieldLabelText({ type: "Text", props: {} })).toBeNull();
|
|
238
|
+
expect(resolveFieldLabelText(["a", "b"])).toBeNull();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -133,4 +133,24 @@ export function resolveFieldInteractive(press: (() => void) | null): press is ()
|
|
|
133
133
|
return press !== null;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* The text a `Field.Label` can lend to the control beside it as its accessible
|
|
138
|
+
* name, or `null` when it has none to lend.
|
|
139
|
+
*
|
|
140
|
+
* Only a bare string or number qualifies. A label built from elements has no
|
|
141
|
+
* text this side of a render, and guessing at one would name a control after
|
|
142
|
+
* the wrong thing — an icon's key, say. Whitespace is trimmed and an empty
|
|
143
|
+
* string is an absence, because a screen reader handed `""` reads nothing at
|
|
144
|
+
* all, which is worse than reading the value alone.
|
|
145
|
+
*
|
|
146
|
+
* React Native has no `<label for>`; this is the association, done by hand
|
|
147
|
+
* through the field's context. Pure, so `bun test` reaches it. See AGENTS.md.
|
|
148
|
+
*/
|
|
149
|
+
export function resolveFieldLabelText(children: unknown): string | null {
|
|
150
|
+
if (typeof children === "number") return String(children);
|
|
151
|
+
if (typeof children !== "string") return null;
|
|
152
|
+
const text = children.trim();
|
|
153
|
+
return text === "" ? null : text;
|
|
154
|
+
}
|
|
155
|
+
|
|
136
156
|
export type FieldVariantProps = VariantProps<typeof fieldVariants>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Item
|
|
2
|
+
|
|
3
|
+
A row of media, text and actions, for lists and settings. Compound root plus
|
|
4
|
+
`Item.Media`, `Item.Content`, `Item.Title`, `Item.Description`, `Item.Actions`,
|
|
5
|
+
`Item.Header` and `Item.Footer`, and two layout parts, `Item.Group` and
|
|
6
|
+
`Item.Separator`.
|
|
7
|
+
|
|
8
|
+
`import { Item } from "@delacour/react-native-ui/item";`
|
|
9
|
+
|
|
10
|
+
## Files
|
|
11
|
+
|
|
12
|
+
| File | What it holds |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `index.ts` | → `@delacour/react-native-ui/item` |
|
|
15
|
+
| `item.tsx` | Root + the `Object.assign` compound surface, and the bare-text wrap it owns |
|
|
16
|
+
| `item-media.tsx` | `Item.Media` |
|
|
17
|
+
| `item-content.tsx` | `Item.Content` |
|
|
18
|
+
| `item-title.tsx` | `Item.Title` |
|
|
19
|
+
| `item-description.tsx` | `Item.Description` |
|
|
20
|
+
| `item-actions.tsx` | `Item.Actions` |
|
|
21
|
+
| `item-header.tsx` | `Item.Header` |
|
|
22
|
+
| `item-footer.tsx` | `Item.Footer` |
|
|
23
|
+
| `item-group.tsx` | `Item.Group` |
|
|
24
|
+
| `item-separator.tsx` | `Item.Separator` |
|
|
25
|
+
| `item.context.tsx` | `ItemProvider`, `useItem()`, `useItemContext()`, `useItemPart()` |
|
|
26
|
+
| `item.types.ts` | Prop types shared by two or more parts |
|
|
27
|
+
| `item.variants.ts` | Pure `tv()` slots and the four resolvers, no RN imports |
|
|
28
|
+
| `item.variants.test.ts` | |
|
|
29
|
+
|
|
30
|
+
## Design
|
|
31
|
+
|
|
32
|
+
- **Axes**: `variant` — `default`, `outline`, `muted`; `size` — `sm`, `md`,
|
|
33
|
+
`lg`; `orientation` — `horizontal`, `vertical`. States: `isDisabled`,
|
|
34
|
+
`isSelected`, and `busy` inherited from `Pressable`. `Item.Media` has its own
|
|
35
|
+
`variant` — `default`, `icon`, `image`.
|
|
36
|
+
- **It drops into a `ListGroup` as a row, and that is the point of it.** An item
|
|
37
|
+
reads the group's context through the `list-group.context` leaf (rule 3 —
|
|
38
|
+
never `../list-group`) and changes three things when it finds one:
|
|
39
|
+
- **The surface becomes `grouped`.** `resolveItemSurface` ignores the variant:
|
|
40
|
+
the group draws the border, fill and corner, and a row repeating any of them
|
|
41
|
+
would draw a card inside a card. `surface` rather than `variant` is the
|
|
42
|
+
`tv()` axis for that reason.
|
|
43
|
+
- **Size defaults to the group's.** `resolveItemSize` — own, else group, else
|
|
44
|
+
`md`. The two components share the `sm`/`md`/`lg` scale on purpose.
|
|
45
|
+
- **Press feedback defaults to `fade`**, not `scale` — a full-bleed row that
|
|
46
|
+
scales reads as the whole card flexing. `resolveItemFeedback`.
|
|
47
|
+
- **Row metrics are `ListGroup`'s, number for number.** The group insets each
|
|
48
|
+
divider by its own row padding, so an item padded differently would sit with
|
|
49
|
+
its text and its divider out of line. The tests assert the item's `min-h`,
|
|
50
|
+
`gap`, `px` and `py` against `listGroupVariants().item()` at every size, and
|
|
51
|
+
the bare media icon against the group's `prefixIcon` — change one without the
|
|
52
|
+
other and `bun test` fails by name.
|
|
53
|
+
- **Pressable only with a handler.** `resolveItemRender` returns one of two:
|
|
54
|
+
`pressable` (a handler — a `Pressable`, role `button`, children merged into
|
|
55
|
+
one accessible element; disabled, it keeps the role, reports `disabled` and
|
|
56
|
+
dims through `opacity-50`, which `Pressable` composes with its press
|
|
57
|
+
animation) or `static` (no handler — a plain `Animated.View` with no role, so
|
|
58
|
+
a static row never announces itself as a button). The plain branch is
|
|
59
|
+
`Animated.View` rather than `View` so the `ref` type is one type.
|
|
60
|
+
- **A pressable item must not hold another control.** iOS merges an accessible
|
|
61
|
+
element's children into it, so a `Button` in the actions of a pressable item
|
|
62
|
+
is unreachable by VoiceOver. Either leave the item static and let the actions
|
|
63
|
+
be the controls, or make the row the control: a switch row takes
|
|
64
|
+
`accessibilityRole="switch"` and `accessibilityState={{ checked }}` on the
|
|
65
|
+
item, toggles from its `onPress`, and renders the `Switch` inside for show.
|
|
66
|
+
- **`isSelected` lays `bg-accent` over any surface**, the muted fill included —
|
|
67
|
+
declared after `surface` in the `tv()` so tailwind-merge drops the losing
|
|
68
|
+
`bg-*`. It also sets `accessibilityState.selected`. What marks the choice (a
|
|
69
|
+
check, a radio) is composed into `Item.Actions` by the caller.
|
|
70
|
+
- **Icons are composed, never passed as props.** `Item.Media` and
|
|
71
|
+
`Item.Actions` each wrap their subtree in an `IconDefaultsProvider`. Media
|
|
72
|
+
gets the foreground token at the size its variant calls for — a bare icon
|
|
73
|
+
matches a `ListGroup` prefix, an `icon` tile's glyph sits two steps smaller
|
|
74
|
+
inside it. Actions gets `muted-foreground` a step lower still, so a chevron
|
|
75
|
+
reads as a hint. A `Button` in the actions publishes its own defaults and is
|
|
76
|
+
unaffected.
|
|
77
|
+
- **`Header` and `Footer` work on both orientations.** The horizontal root is
|
|
78
|
+
`flex-wrap` and the strips are `w-full`, so a strip takes a line of its own
|
|
79
|
+
above or below media, text and actions without the item having to become a
|
|
80
|
+
column.
|
|
81
|
+
- **`Content` only flexes along a row.** Vertical, it is `self-stretch`: a
|
|
82
|
+
`flex-1` column in an auto-height parent collapses to zero in Yoga.
|
|
83
|
+
- **`Item.Group` is spaced, not divided.** It is for standalone items — outlined
|
|
84
|
+
cards, a carousel — announced with `accessibilityRole="list"`. Rows sharing
|
|
85
|
+
one card with dividers belong in a `ListGroup`, which already does both.
|
|
86
|
+
`Item.Separator` is a `Separator` under the item's name, for the occasional
|
|
87
|
+
hand-placed rule in a group.
|
|
88
|
+
- **String children** are wrapped in a `Content` around a `Title`, consecutive
|
|
89
|
+
strings collapsing into one — the same rule, and the same reason, as
|
|
90
|
+
[`ListGroup`](../list-group/AGENTS.md).
|
|
91
|
+
- **Text colour goes on the text.** No slot but `title` and `description` holds
|
|
92
|
+
a `text-*` colour; the tests assert the root never does.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export { Item, type ItemProps } from "./item";
|
|
2
|
+
export { type ItemContextValue, ItemProvider, useItem, useItemContext } from "./item.context";
|
|
3
|
+
export type { ItemSlotProps, ItemTextProps } from "./item.types";
|
|
4
|
+
export {
|
|
5
|
+
ITEM_ACTIONS_ICON_TOKEN,
|
|
6
|
+
ITEM_MEDIA_ICON_TOKEN,
|
|
7
|
+
ITEM_MEDIA_VARIANTS,
|
|
8
|
+
ITEM_ORIENTATIONS,
|
|
9
|
+
ITEM_SIZES,
|
|
10
|
+
ITEM_VARIANTS,
|
|
11
|
+
type ItemMediaVariant,
|
|
12
|
+
type ItemOrientation,
|
|
13
|
+
type ItemRender,
|
|
14
|
+
type ItemSize,
|
|
15
|
+
type ItemSurface,
|
|
16
|
+
type ItemVariant,
|
|
17
|
+
type ItemVariantProps,
|
|
18
|
+
itemVariants,
|
|
19
|
+
resolveItemFeedback,
|
|
20
|
+
resolveItemRender,
|
|
21
|
+
resolveItemSize,
|
|
22
|
+
resolveItemSurface,
|
|
23
|
+
} from "./item.variants";
|
|
24
|
+
export type { ItemGroupProps } from "./item-group";
|
|
25
|
+
export type { ItemMediaProps } from "./item-media";
|