@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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Text
|
|
2
2
|
|
|
3
3
|
The library's type scale, and the one component that reproduces React Native's
|
|
4
|
-
own text inheritance through classNames. Compound root plus
|
|
4
|
+
own text inheritance through classNames. Compound root plus thirteen presets:
|
|
5
5
|
`Display`, `Title`, `Header`, `Subheader`, `Paragraph`, `Label`, `Caption`,
|
|
6
|
-
`Overline`, and the four inline ones — `Strong`, `Emphasis`, `Link`, `Code`.
|
|
6
|
+
`Overline`, `Kicker`, and the four inline ones — `Strong`, `Emphasis`, `Link`, `Code`.
|
|
7
7
|
|
|
8
8
|
`import { Text } from "@delacour/react-native-ui/text";`
|
|
9
9
|
|
|
@@ -61,6 +61,12 @@ own text inheritance through classNames. Compound root plus twelve presets:
|
|
|
61
61
|
so Tailwind v4's logical-property utilities resolve to a value RN rejects.
|
|
62
62
|
`transform: "none"` emits `normal-case` rather than an empty string, so it can
|
|
63
63
|
actually clear an `Overline`'s `uppercase`.
|
|
64
|
+
- **`Kicker` is the mono, 10-point, 0.2em-tracked label — an opt-in sibling of
|
|
65
|
+
`Overline`, not a replacement.** Its size and tracking are the `--text-kicker`
|
|
66
|
+
and `--tracking-kicker` tokens in `tokens.css`, registered with
|
|
67
|
+
`tailwind-merge` as a pair (`KICKER_TOKEN`) so `text-kicker` is read as a
|
|
68
|
+
size and never as a colour. `font-mono` is deliberate: it labels, it does not
|
|
69
|
+
read as a sentence. Nothing in the default look uses it.
|
|
64
70
|
- **A nested `Text.Code` cannot be padded.** A nested `<Text>` is laid out by the
|
|
65
71
|
platform's text engine — an `NSAttributedString` run on iOS, a `Span` on
|
|
66
72
|
Android — and both ignore padding, margin and border radius on an inner
|
|
@@ -142,6 +142,12 @@ function TextOverline(props: TextPresetProps): ReactElement {
|
|
|
142
142
|
}
|
|
143
143
|
TextOverline.displayName = "DelacourUI.Text.Overline";
|
|
144
144
|
|
|
145
|
+
/** The tiny tracked mono label over a group, a screen or a field. */
|
|
146
|
+
function TextKicker(props: TextPresetProps): ReactElement {
|
|
147
|
+
return <TextRoot variant="kicker" {...props} />;
|
|
148
|
+
}
|
|
149
|
+
TextKicker.displayName = "DelacourUI.Text.Kicker";
|
|
150
|
+
|
|
145
151
|
/**
|
|
146
152
|
* Inline emphasis by weight.
|
|
147
153
|
*
|
|
@@ -238,6 +244,7 @@ export const Text = Object.assign(TextRoot, {
|
|
|
238
244
|
/** Supporting copy — 14pt on the muted token. */
|
|
239
245
|
Caption: TextCaption,
|
|
240
246
|
/** An eyebrow above a section — 12pt semibold, uppercase, muted. */
|
|
247
|
+
Kicker: TextKicker,
|
|
241
248
|
Overline: TextOverline,
|
|
242
249
|
/** Inline emphasis by weight. Inherits size and colour from the text around it. */
|
|
243
250
|
Strong: TextStrong,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from "./text.variants";
|
|
18
18
|
|
|
19
19
|
/** Tailwind's type scale, smallest first — position, so a test can say "a step down". */
|
|
20
|
-
const TYPE_SCALE = ["text-xs", "text-sm", "text-base", "text-lg", "text-xl", "text-2xl", "text-3xl"];
|
|
20
|
+
const TYPE_SCALE = ["text-kicker", "text-xs", "text-sm", "text-base", "text-lg", "text-xl", "text-2xl", "text-3xl"];
|
|
21
21
|
|
|
22
22
|
/** The three alignment utilities, which share the `text-` prefix with sizes and colours. */
|
|
23
23
|
const ALIGNMENTS = ["text-left", "text-center", "text-right"];
|
|
@@ -420,3 +420,26 @@ describe("TEXT_MAX_FONT_SIZE_MULTIPLIER", () => {
|
|
|
420
420
|
expect(TEXT_MAX_FONT_SIZE_MULTIPLIER).toBeGreaterThan(1);
|
|
421
421
|
});
|
|
422
422
|
});
|
|
423
|
+
|
|
424
|
+
describe("the kicker variant", () => {
|
|
425
|
+
const cls = textVariants({ variant: "kicker" });
|
|
426
|
+
|
|
427
|
+
test("is a ten-point uppercase mono label, tracked wide", () => {
|
|
428
|
+
expect(cls).toContain("font-mono");
|
|
429
|
+
expect(cls).toContain("text-kicker");
|
|
430
|
+
expect(cls).toContain("uppercase");
|
|
431
|
+
expect(cls).toContain("tracking-kicker");
|
|
432
|
+
expect(cls).toContain("text-muted-foreground");
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("is listed, and is not an inline variant", () => {
|
|
436
|
+
expect(TEXT_VARIANTS).toContain("kicker");
|
|
437
|
+
expect(isInlineTextVariant("kicker")).toBe(false);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("survives resolveTextClass without losing its size to the base class", () => {
|
|
441
|
+
const resolved = resolveTextClass({ variant: "kicker" });
|
|
442
|
+
expect(resolved).toContain("text-kicker");
|
|
443
|
+
expect(resolved).not.toContain("text-base");
|
|
444
|
+
});
|
|
445
|
+
});
|
|
@@ -18,6 +18,7 @@ export const TEXT_VARIANTS = [
|
|
|
18
18
|
"label",
|
|
19
19
|
"caption",
|
|
20
20
|
"overline",
|
|
21
|
+
"kicker",
|
|
21
22
|
"strong",
|
|
22
23
|
"emphasis",
|
|
23
24
|
"link",
|
|
@@ -136,6 +137,10 @@ export const textVariants = tv({
|
|
|
136
137
|
label: "font-medium text-sm text-foreground",
|
|
137
138
|
caption: "text-sm text-muted-foreground",
|
|
138
139
|
overline: "font-semibold text-muted-foreground text-xs uppercase tracking-wide",
|
|
140
|
+
// The tiny tracked mono label over a group or a screen. `font-mono`
|
|
141
|
+
// rather than `font-sans`, which is the point of it: it reads as a
|
|
142
|
+
// measurement, not a sentence. Tokens, not literals — see tokens.css.
|
|
143
|
+
kicker: "font-mono text-kicker text-muted-foreground uppercase tracking-kicker",
|
|
139
144
|
// The four below emit a delta and nothing else. See TEXT_INLINE_VARIANTS.
|
|
140
145
|
strong: "font-semibold",
|
|
141
146
|
emphasis: "italic",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Textarea
|
|
2
|
+
|
|
3
|
+
A multiline text field, sized in rows. One component, no parts: it renders an
|
|
4
|
+
[`Input`](../input/AGENTS.md) and adds a height and an optional character count.
|
|
5
|
+
|
|
6
|
+
`import { Textarea } from "@delacour/react-native-ui/textarea";`
|
|
7
|
+
|
|
8
|
+
## Files
|
|
9
|
+
|
|
10
|
+
| File | What it holds |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `index.ts` | → `@delacour/react-native-ui/textarea` |
|
|
13
|
+
| `textarea.tsx` | `Textarea` |
|
|
14
|
+
| `textarea.variants.ts` | Pure `tv()` slots, the line metrics and the height and count resolvers, no RN imports |
|
|
15
|
+
| `textarea.variants.test.ts` | |
|
|
16
|
+
|
|
17
|
+
## Design
|
|
18
|
+
|
|
19
|
+
- **The box is `Input`'s, and nothing here restates it.** Variants (`primary`,
|
|
20
|
+
`secondary`), sizes (`sm`, `md`, `lg`), the focus ring, the invalid border
|
|
21
|
+
that outranks it, the disabled fade, the `accent-*` caret and placeholder, and
|
|
22
|
+
the `Field` cascade all come from rendering an `Input` with `multiline`. A
|
|
23
|
+
second `tv()` for the same border would be a second thing that can drift, and
|
|
24
|
+
a textarea under a row of single-line fields has to read as the same control
|
|
25
|
+
at a different height — not as a close match.
|
|
26
|
+
- **`rows` is a line count, and it becomes points in JavaScript.** A row count
|
|
27
|
+
only turns into a height once the height of a line is a number, so
|
|
28
|
+
`TEXTAREA_LINE_HEIGHTS` holds it per size and `resolveTextareaRowsHeight` adds
|
|
29
|
+
the padding and the border either side. The result is a **style**, never a
|
|
30
|
+
class: a runtime `` `h-[${n}px]` `` is never compiled by Tailwind's static
|
|
31
|
+
scanner and would draw nothing — the rule in
|
|
32
|
+
[Sizing](../../../AGENTS.md#sizing). `size` moves the type and the leading, so
|
|
33
|
+
a small three-row field and a large one are both three rows.
|
|
34
|
+
- **Every number the height counts is pinned to the class that draws it.** The
|
|
35
|
+
line height is set by a `leading-*` class in `textareaVariants`' `field` slot,
|
|
36
|
+
and the padding is `Input`'s multiline `py-*`. `textarea.variants.test.ts`
|
|
37
|
+
resolves both classes and fails if either disagrees with its number — a
|
|
38
|
+
retune of `Input`'s padding would otherwise mis-size every textarea by a few
|
|
39
|
+
points, silently, with the last line clipped at the bottom edge.
|
|
40
|
+
- **A paragraph gets paragraph leading.** `Input` sets `leading-tight` because
|
|
41
|
+
it centres one line in a fixed box; four lines at 1.25× read as a wall. The
|
|
42
|
+
textarea's `leading-5`/`-6`/`-7` replace it through `Input`'s own className
|
|
43
|
+
merge rather than sitting beside it, and a test holds each between 1.2× and
|
|
44
|
+
1.6× the `--text-input-*` size so the type scale can be retuned without this
|
|
45
|
+
going stale.
|
|
46
|
+
- **`autoGrow` measures nothing.** It sets `minHeight` at `rows` and
|
|
47
|
+
`maxHeight` at `maxRows`, and between the two React Native's multiline
|
|
48
|
+
`TextInput` sizes itself to its content; past the cap it scrolls. An
|
|
49
|
+
`onContentSizeChange` loop would draw one frame at the old height after every
|
|
50
|
+
new line, which is exactly the jump a growing field exists to avoid. The field
|
|
51
|
+
never shrinks below `rows`, so a message typed and cleared goes back to where
|
|
52
|
+
it started rather than collapsing to one line.
|
|
53
|
+
- **`maxRows` only type-checks with `autoGrow`, and `showCount` only with
|
|
54
|
+
`maxLength`.** Both are unions in `TextareaProps`. A ceiling on a field that
|
|
55
|
+
does not grow is a number nothing reads, and a count with no limit to count
|
|
56
|
+
towards says nothing — a runtime warning would only find either out on a
|
|
57
|
+
device.
|
|
58
|
+
- **A bad row count degrades rather than throws.** `resolveTextareaRows` floors
|
|
59
|
+
to whole lines, clamps to at least one, falls back on `NaN` or `Infinity`, and
|
|
60
|
+
lifts a `maxRows` below `rows` up to it — otherwise a growing field would
|
|
61
|
+
shrink as it grew.
|
|
62
|
+
- **The count turns destructive at the limit, not past it.** Native `maxLength`
|
|
63
|
+
stops typing at the limit, so "over" is a state someone typing never sees;
|
|
64
|
+
the count has to change at the moment the keyboard stops working. It renders
|
|
65
|
+
`Text.Caption` and passes its colour through the preset's `color` prop, so the
|
|
66
|
+
`count` slot carries no colour class to fight it. It uses `tabular-nums` so it
|
|
67
|
+
does not jitter as it counts, and announces as "12 of 280 characters" rather
|
|
68
|
+
than as a fraction.
|
|
69
|
+
- **The count works controlled and uncontrolled.** A `value` is read directly;
|
|
70
|
+
without one, the last text the field reported is, seeded from `defaultValue`
|
|
71
|
+
so the first frame is already right.
|
|
72
|
+
- **There is always a wrapping `View`.** `className` reaches the field, as it
|
|
73
|
+
does on `Input`, and `containerClassName` reaches the wrapper that holds the
|
|
74
|
+
field and its count. The wrapper is there whether or not the count is, so
|
|
75
|
+
flipping `showCount` never remounts the field and drops focus.
|
|
76
|
+
- **`multiline` and `numberOfLines` are withheld.** It is always multiline, and
|
|
77
|
+
`rows` is its height; `numberOfLines` would be a second answer to the same
|
|
78
|
+
question, and on Android the one that wins.
|
|
79
|
+
- **There is no `label`, `description` or `errorMessage` prop.** A textarea goes
|
|
80
|
+
in a [`Field`](../field/AGENTS.md) the way an `Input` does — the reason
|
|
81
|
+
`Input` ships no label part holds here too. `apps/playground`'s
|
|
82
|
+
`/textarea` gallery ends with a composed form showing the trade at a call site.
|
|
83
|
+
- **Not for `Input.Group` or `Button.Group`.** An inner `Input` would read
|
|
84
|
+
either context and take its box or its corner, but a decorated or joined
|
|
85
|
+
paragraph is not a control this library draws, and the height math assumes
|
|
86
|
+
the box is the field's own.
|
|
87
|
+
|
|
88
|
+
- **Takes `Input`'s `etched` variant** — the box is `Input`'s, so the third `INPUT_VARIANTS` value
|
|
89
|
+
reaches the textarea unchanged.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { Textarea, type TextareaProps } from "./textarea";
|
|
2
|
+
export {
|
|
3
|
+
resolveTextareaCount,
|
|
4
|
+
resolveTextareaFieldClass,
|
|
5
|
+
resolveTextareaHeightStyle,
|
|
6
|
+
resolveTextareaRows,
|
|
7
|
+
resolveTextareaRowsHeight,
|
|
8
|
+
TEXTAREA_BORDER_WIDTH,
|
|
9
|
+
TEXTAREA_DEFAULT_MAX_ROWS,
|
|
10
|
+
TEXTAREA_DEFAULT_ROWS,
|
|
11
|
+
TEXTAREA_LINE_HEIGHTS,
|
|
12
|
+
TEXTAREA_PADDING_Y,
|
|
13
|
+
type TextareaCount,
|
|
14
|
+
type TextareaVariantProps,
|
|
15
|
+
textareaVariants,
|
|
16
|
+
} from "./textarea.variants";
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { type ReactElement, useCallback, useState } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { Input, type InputProps } from "../input";
|
|
4
|
+
import { Text } from "../text";
|
|
5
|
+
import {
|
|
6
|
+
resolveTextareaCount,
|
|
7
|
+
resolveTextareaFieldClass,
|
|
8
|
+
resolveTextareaHeightStyle,
|
|
9
|
+
textareaVariants,
|
|
10
|
+
} from "./textarea.variants";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether the field grows with its text.
|
|
14
|
+
*
|
|
15
|
+
* A union so `maxRows` is only accepted where it means something: on a field
|
|
16
|
+
* that does not grow, a ceiling is a number nothing reads.
|
|
17
|
+
*/
|
|
18
|
+
type TextareaGrowProps =
|
|
19
|
+
| {
|
|
20
|
+
/** Stay `rows` tall and scroll past it. The default. */
|
|
21
|
+
autoGrow?: false;
|
|
22
|
+
maxRows?: never;
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
/** Grow with the text, from `rows` up to `maxRows`, then scroll. */
|
|
26
|
+
autoGrow: true;
|
|
27
|
+
/** The tallest the field grows, in lines. Default 10. */
|
|
28
|
+
maxRows?: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether a character count sits under the field.
|
|
33
|
+
*
|
|
34
|
+
* A union so the count cannot be asked for without a limit: a count with
|
|
35
|
+
* nothing to count towards says nothing, and a runtime check would only find
|
|
36
|
+
* that out on a device.
|
|
37
|
+
*/
|
|
38
|
+
type TextareaCountProps =
|
|
39
|
+
| {
|
|
40
|
+
showCount?: false;
|
|
41
|
+
/** The most characters the field accepts. */
|
|
42
|
+
maxLength?: number;
|
|
43
|
+
}
|
|
44
|
+
| {
|
|
45
|
+
/** Show how much of `maxLength` is used, under the field's trailing edge. */
|
|
46
|
+
showCount: true;
|
|
47
|
+
maxLength: number;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type TextareaProps = Omit<InputProps, "multiline" | "numberOfLines" | "maxLength"> &
|
|
51
|
+
TextareaGrowProps &
|
|
52
|
+
TextareaCountProps & {
|
|
53
|
+
/** How many lines tall the field is — its height when fixed, its floor when growing. Default 4. */
|
|
54
|
+
rows?: number;
|
|
55
|
+
/** Class for the view holding the field and its count. `className` styles the field itself. */
|
|
56
|
+
containerClassName?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A multiline text field, sized in rows.
|
|
61
|
+
*
|
|
62
|
+
* The box is `Input`'s — its variants, sizes, focus ring, invalid border,
|
|
63
|
+
* disabled fade and the `Field` cascade all come from there unchanged, so a
|
|
64
|
+
* textarea and the single-line fields above it in a form are the same control
|
|
65
|
+
* at a different height rather than two that happen to match.
|
|
66
|
+
*
|
|
67
|
+
* What this adds is the height. `rows` is a line count, and it becomes points
|
|
68
|
+
* through the paragraph leading and the box's own padding and border — see
|
|
69
|
+
* `resolveTextareaHeightStyle`. It is applied as a style, never a class,
|
|
70
|
+
* because a runtime number cannot be a Tailwind class. `size` changes the type
|
|
71
|
+
* and leading, so a small three-row field and a large one are both three rows.
|
|
72
|
+
*
|
|
73
|
+
* `autoGrow` floors the field at `rows` and caps it at `maxRows`; between the
|
|
74
|
+
* two, React Native's multiline `TextInput` sizes itself to its content, and
|
|
75
|
+
* past the cap it scrolls. `showCount` with `maxLength` puts a count under the
|
|
76
|
+
* trailing edge that turns destructive at the limit.
|
|
77
|
+
*
|
|
78
|
+
* `multiline` and `numberOfLines` are withheld: it is always multiline, and its
|
|
79
|
+
* height comes from `rows`.
|
|
80
|
+
*
|
|
81
|
+
* Label, description and error come from `Field`, exactly as they do for
|
|
82
|
+
* `Input` — there is no `label` prop.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* <Field>
|
|
86
|
+
* <Field.Label>Notes</Field.Label>
|
|
87
|
+
* <Textarea placeholder="Anything we should know?" />
|
|
88
|
+
* </Field>
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* <Textarea autoGrow maxRows={8} rows={2} placeholder="Message" />
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* <Textarea maxLength={280} onChangeText={setStatus} showCount value={status} />
|
|
95
|
+
*/
|
|
96
|
+
export function Textarea({
|
|
97
|
+
rows,
|
|
98
|
+
autoGrow,
|
|
99
|
+
maxRows,
|
|
100
|
+
showCount,
|
|
101
|
+
maxLength,
|
|
102
|
+
size,
|
|
103
|
+
className,
|
|
104
|
+
containerClassName,
|
|
105
|
+
style,
|
|
106
|
+
value,
|
|
107
|
+
defaultValue,
|
|
108
|
+
onChangeText,
|
|
109
|
+
...props
|
|
110
|
+
}: TextareaProps): ReactElement {
|
|
111
|
+
const resolvedSize = size ?? "md";
|
|
112
|
+
const slots = textareaVariants({ size: resolvedSize });
|
|
113
|
+
|
|
114
|
+
// The count reads the value's length whether or not the caller owns the
|
|
115
|
+
// value. Controlled, `value` is the truth; uncontrolled, the last text the
|
|
116
|
+
// field reported is — seeded from `defaultValue` so the count is right on
|
|
117
|
+
// the first frame rather than after the first keystroke.
|
|
118
|
+
const [ownLength, setOwnLength] = useState(() => defaultValue?.length ?? 0);
|
|
119
|
+
const length = value === undefined ? ownLength : value.length;
|
|
120
|
+
|
|
121
|
+
const handleChangeText = useCallback(
|
|
122
|
+
(text: string) => {
|
|
123
|
+
setOwnLength(text.length);
|
|
124
|
+
onChangeText?.(text);
|
|
125
|
+
},
|
|
126
|
+
[onChangeText]
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const count = showCount && maxLength !== undefined ? resolveTextareaCount({ length, maxLength }) : null;
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<View className={slots.root({ className: containerClassName })}>
|
|
133
|
+
<Input
|
|
134
|
+
{...props}
|
|
135
|
+
className={resolveTextareaFieldClass({ className, size: resolvedSize })}
|
|
136
|
+
defaultValue={defaultValue}
|
|
137
|
+
maxLength={maxLength}
|
|
138
|
+
multiline
|
|
139
|
+
onChangeText={handleChangeText}
|
|
140
|
+
size={resolvedSize}
|
|
141
|
+
style={[resolveTextareaHeightStyle({ autoGrow, maxRows, rows, size: resolvedSize }), style]}
|
|
142
|
+
value={value}
|
|
143
|
+
/>
|
|
144
|
+
{count ? (
|
|
145
|
+
<Text.Caption
|
|
146
|
+
accessibilityLabel={count.accessibilityLabel}
|
|
147
|
+
accessibilityLiveRegion="polite"
|
|
148
|
+
className={slots.count()}
|
|
149
|
+
color={count.color}
|
|
150
|
+
>
|
|
151
|
+
{count.label}
|
|
152
|
+
</Text.Caption>
|
|
153
|
+
) : null}
|
|
154
|
+
</View>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
Textarea.displayName = "DelacourUI.Textarea";
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { INPUT_SIZES, inputVariants, resolveInputFieldClass } from "../input/input.variants";
|
|
5
|
+
import {
|
|
6
|
+
resolveTextareaCount,
|
|
7
|
+
resolveTextareaFieldClass,
|
|
8
|
+
resolveTextareaHeightStyle,
|
|
9
|
+
resolveTextareaRows,
|
|
10
|
+
resolveTextareaRowsHeight,
|
|
11
|
+
TEXTAREA_BORDER_WIDTH,
|
|
12
|
+
TEXTAREA_DEFAULT_MAX_ROWS,
|
|
13
|
+
TEXTAREA_DEFAULT_ROWS,
|
|
14
|
+
TEXTAREA_LINE_HEIGHTS,
|
|
15
|
+
TEXTAREA_PADDING_Y,
|
|
16
|
+
textareaVariants,
|
|
17
|
+
} from "./textarea.variants";
|
|
18
|
+
|
|
19
|
+
const TOKENS_CSS = readFileSync(join(import.meta.dirname, "../../styles/tokens.css"), "utf-8");
|
|
20
|
+
|
|
21
|
+
/** Points behind a `--text-input-*` token in `tokens.css`. */
|
|
22
|
+
function inputTextPoints(size: string): number {
|
|
23
|
+
const match = TOKENS_CSS.match(new RegExp(`--text-input-${size}:\\s*([\\d.]+)px;`));
|
|
24
|
+
if (!match?.[1]) throw new Error(`tokens.css defines no --text-input-${size}`);
|
|
25
|
+
return Number(match[1]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Points behind the first `<prefix>-N` spacing utility in a class string, at Tailwind's 4pt step. */
|
|
29
|
+
function spacingPoints(cls: string, prefix: string): number | undefined {
|
|
30
|
+
const match = cls.match(new RegExp(`(?:^|\\s)${prefix}-(\\d+(?:\\.\\d+)?)(?:\\s|$)`));
|
|
31
|
+
return match?.[1] === undefined ? undefined : Number(match[1]) * 4;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("the metrics the height is derived from", () => {
|
|
35
|
+
// The height is a number, so the padding it counts has to be the padding
|
|
36
|
+
// the box actually wears. That padding is `Input`'s multiline `py-*`, and
|
|
37
|
+
// restating it here is only safe because this test reads it back.
|
|
38
|
+
test("count the vertical padding Input's multiline box wears", () => {
|
|
39
|
+
for (const size of INPUT_SIZES) {
|
|
40
|
+
const cls = resolveInputFieldClass({ isMultiline: true, size });
|
|
41
|
+
expect(spacingPoints(cls, "py")).toBe(TEXTAREA_PADDING_Y[size]);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("count the one-pixel border every variant draws", () => {
|
|
46
|
+
expect(inputVariants().root()).toMatch(/(?:^|\s)border(?:\s|$)/);
|
|
47
|
+
expect(TEXTAREA_BORDER_WIDTH).toBe(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("give each size the line height its field class sets", () => {
|
|
51
|
+
for (const size of INPUT_SIZES) {
|
|
52
|
+
const cls = resolveTextareaFieldClass({ size });
|
|
53
|
+
expect(spacingPoints(cls, "leading")).toBe(TEXTAREA_LINE_HEIGHTS[size]);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// A line shorter than its type clips descenders; one under 1.2× reads as
|
|
58
|
+
// a wall. The ratio is what is pinned, so the type scale can be retuned.
|
|
59
|
+
test("set every line between 1.2× and 1.6× the value's type size", () => {
|
|
60
|
+
for (const size of INPUT_SIZES) {
|
|
61
|
+
const ratio = TEXTAREA_LINE_HEIGHTS[size] / inputTextPoints(size);
|
|
62
|
+
expect(ratio).toBeGreaterThanOrEqual(1.2);
|
|
63
|
+
expect(ratio).toBeLessThanOrEqual(1.6);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("step the line height with the size", () => {
|
|
68
|
+
const heights = INPUT_SIZES.map((size) => TEXTAREA_LINE_HEIGHTS[size]);
|
|
69
|
+
expect([...heights].sort((a, b) => a - b)).toEqual(heights);
|
|
70
|
+
expect(new Set(heights).size).toBe(heights.length);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("resolveTextareaFieldClass", () => {
|
|
75
|
+
test("replaces Input's single-line leading rather than adding a second", () => {
|
|
76
|
+
for (const size of INPUT_SIZES) {
|
|
77
|
+
const cls = resolveInputFieldClass({
|
|
78
|
+
className: resolveTextareaFieldClass({ size }),
|
|
79
|
+
isMultiline: true,
|
|
80
|
+
size,
|
|
81
|
+
});
|
|
82
|
+
expect(cls).not.toContain("leading-tight");
|
|
83
|
+
expect(cls.match(/\bleading-/g)).toHaveLength(1);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("merges an incoming className last", () => {
|
|
88
|
+
expect(resolveTextareaFieldClass({ className: "leading-9", size: "md" })).toContain("leading-9");
|
|
89
|
+
expect(resolveTextareaFieldClass({ className: "leading-9", size: "md" })).not.toContain("leading-6");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("resolveTextareaRows", () => {
|
|
94
|
+
test("defaults to four rows and a ten-row ceiling", () => {
|
|
95
|
+
expect(TEXTAREA_DEFAULT_ROWS).toBe(4);
|
|
96
|
+
expect(TEXTAREA_DEFAULT_MAX_ROWS).toBe(10);
|
|
97
|
+
expect(resolveTextareaRows({})).toEqual({ maxRows: 10, rows: 4 });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("never goes below one row", () => {
|
|
101
|
+
expect(resolveTextareaRows({ rows: 0 }).rows).toBe(1);
|
|
102
|
+
expect(resolveTextareaRows({ rows: -3 }).rows).toBe(1);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("rounds a fractional row count down to whole lines", () => {
|
|
106
|
+
expect(resolveTextareaRows({ rows: 2.7 }).rows).toBe(2);
|
|
107
|
+
expect(resolveTextareaRows({ maxRows: 5.9, rows: 2 }).maxRows).toBe(5);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// A ceiling below the floor would make the field shrink as it grew.
|
|
111
|
+
test("lifts a ceiling below the floor up to it", () => {
|
|
112
|
+
expect(resolveTextareaRows({ maxRows: 2, rows: 6 })).toEqual({ maxRows: 6, rows: 6 });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("falls back on a non-finite count", () => {
|
|
116
|
+
expect(resolveTextareaRows({ rows: Number.NaN })).toEqual({ maxRows: 10, rows: 4 });
|
|
117
|
+
expect(resolveTextareaRows({ maxRows: Number.POSITIVE_INFINITY, rows: 3 }).maxRows).toBe(10);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("resolveTextareaRowsHeight", () => {
|
|
122
|
+
test("is the lines plus the padding and border either side", () => {
|
|
123
|
+
for (const size of INPUT_SIZES) {
|
|
124
|
+
const expected = 3 * TEXTAREA_LINE_HEIGHTS[size] + 2 * TEXTAREA_PADDING_Y[size] + 2 * TEXTAREA_BORDER_WIDTH;
|
|
125
|
+
expect(resolveTextareaRowsHeight({ rows: 3, size })).toBe(expected);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("grows by exactly one line per row", () => {
|
|
130
|
+
for (const size of INPUT_SIZES) {
|
|
131
|
+
const step = resolveTextareaRowsHeight({ rows: 5, size }) - resolveTextareaRowsHeight({ rows: 4, size });
|
|
132
|
+
expect(step).toBe(TEXTAREA_LINE_HEIGHTS[size]);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// A one-row textarea should not be shorter than the single-line field it
|
|
137
|
+
// sits beside in a form.
|
|
138
|
+
test("is never shorter than a single-line Input at one row", () => {
|
|
139
|
+
const INPUT_HEIGHTS = { lg: 52, md: 44, sm: 36 } as const;
|
|
140
|
+
for (const size of INPUT_SIZES) {
|
|
141
|
+
expect(resolveTextareaRowsHeight({ rows: 1, size })).toBeGreaterThanOrEqual(INPUT_HEIGHTS[size]);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("resolveTextareaHeightStyle", () => {
|
|
147
|
+
test("fixes the height at the row count when it does not grow", () => {
|
|
148
|
+
expect(resolveTextareaHeightStyle({ rows: 3, size: "md" })).toEqual({
|
|
149
|
+
height: resolveTextareaRowsHeight({ rows: 3, size: "md" }),
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("floors at rows and caps at maxRows when it grows", () => {
|
|
154
|
+
expect(resolveTextareaHeightStyle({ autoGrow: true, maxRows: 6, rows: 2, size: "sm" })).toEqual({
|
|
155
|
+
maxHeight: resolveTextareaRowsHeight({ rows: 6, size: "sm" }),
|
|
156
|
+
minHeight: resolveTextareaRowsHeight({ rows: 2, size: "sm" }),
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("caps a growing field at ten rows when maxRows is unset", () => {
|
|
161
|
+
expect(resolveTextareaHeightStyle({ autoGrow: true, size: "md" })).toEqual({
|
|
162
|
+
maxHeight: resolveTextareaRowsHeight({ rows: 10, size: "md" }),
|
|
163
|
+
minHeight: resolveTextareaRowsHeight({ rows: 4, size: "md" }),
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("ignores maxRows on a field that does not grow", () => {
|
|
168
|
+
expect(resolveTextareaHeightStyle({ maxRows: 8, rows: 2, size: "lg" })).toEqual({
|
|
169
|
+
height: resolveTextareaRowsHeight({ rows: 2, size: "lg" }),
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("resolveTextareaCount", () => {
|
|
175
|
+
test("reads as used over limit", () => {
|
|
176
|
+
expect(resolveTextareaCount({ length: 12, maxLength: 280 }).label).toBe("12/280");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("announces the count as a sentence", () => {
|
|
180
|
+
expect(resolveTextareaCount({ length: 1, maxLength: 280 }).accessibilityLabel).toBe("1 of 280 characters");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("reports the limit once it is reached, and not before", () => {
|
|
184
|
+
expect(resolveTextareaCount({ length: 279, maxLength: 280 }).isAtLimit).toBe(false);
|
|
185
|
+
expect(resolveTextareaCount({ length: 280, maxLength: 280 }).isAtLimit).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("is muted under the limit and destructive at it", () => {
|
|
189
|
+
expect(resolveTextareaCount({ length: 3, maxLength: 10 }).color).toBe("muted");
|
|
190
|
+
expect(resolveTextareaCount({ length: 10, maxLength: 10 }).color).toBe("destructive");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// A controlled value set past the limit in code is still over it; the
|
|
194
|
+
// native `maxLength` only stops typing.
|
|
195
|
+
test("still reports a value past the limit", () => {
|
|
196
|
+
expect(resolveTextareaCount({ length: 300, maxLength: 280 })).toEqual({
|
|
197
|
+
accessibilityLabel: "300 of 280 characters",
|
|
198
|
+
color: "destructive",
|
|
199
|
+
isAtLimit: true,
|
|
200
|
+
label: "300/280",
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("the count slot", () => {
|
|
206
|
+
test("sits at the trailing edge", () => {
|
|
207
|
+
expect(textareaVariants().count()).toContain("self-end");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("uses tabular figures, so the count does not jitter as it changes", () => {
|
|
211
|
+
expect(textareaVariants().count()).toContain("tabular-nums");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Colour belongs to the `Text` preset's `color` prop, not to a class that
|
|
215
|
+
// would fight it in the merge.
|
|
216
|
+
test("carries no text colour of its own", () => {
|
|
217
|
+
expect(textareaVariants().count()).not.toMatch(/\btext-(?:muted|destructive|foreground)/);
|
|
218
|
+
});
|
|
219
|
+
});
|