orn-ui 0.2.9 → 0.3.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/AGENTS.md +67 -52
- package/README.md +5 -9
- package/lib/commonjs/atoms/Avatar.js +77 -5
- package/lib/commonjs/atoms/Avatar.js.map +1 -1
- package/lib/commonjs/atoms/Badge.js +17 -8
- package/lib/commonjs/atoms/Badge.js.map +1 -1
- package/lib/commonjs/atoms/Button.js +108 -39
- package/lib/commonjs/atoms/Button.js.map +1 -1
- package/lib/commonjs/atoms/Card.js +24 -2
- package/lib/commonjs/atoms/Card.js.map +1 -1
- package/lib/commonjs/atoms/Checkbox.js +41 -10
- package/lib/commonjs/atoms/Checkbox.js.map +1 -1
- package/lib/commonjs/atoms/Divider.js +30 -5
- package/lib/commonjs/atoms/Divider.js.map +1 -1
- package/lib/commonjs/atoms/EmptyState.js +58 -20
- package/lib/commonjs/atoms/EmptyState.js.map +1 -1
- package/lib/commonjs/atoms/Fab.js +32 -12
- package/lib/commonjs/atoms/Fab.js.map +1 -1
- package/lib/commonjs/atoms/Gradient.js +102 -36
- package/lib/commonjs/atoms/Gradient.js.map +1 -1
- package/lib/commonjs/atoms/IconButton.js +25 -4
- package/lib/commonjs/atoms/IconButton.js.map +1 -1
- package/lib/commonjs/atoms/Image.js +61 -25
- package/lib/commonjs/atoms/Image.js.map +1 -1
- package/lib/commonjs/atoms/Input.js +105 -17
- package/lib/commonjs/atoms/Input.js.map +1 -1
- package/lib/commonjs/atoms/KeyValueRow.js +37 -9
- package/lib/commonjs/atoms/KeyValueRow.js.map +1 -1
- package/lib/commonjs/atoms/PressableScale.js +25 -5
- package/lib/commonjs/atoms/PressableScale.js.map +1 -1
- package/lib/commonjs/atoms/ShinyText.js +29 -29
- package/lib/commonjs/atoms/Skeleton.js +28 -13
- package/lib/commonjs/atoms/Skeleton.js.map +1 -1
- package/lib/commonjs/atoms/Spinner.js +81 -22
- package/lib/commonjs/atoms/Spinner.js.map +1 -1
- package/lib/commonjs/atoms/Text.js +56 -5
- package/lib/commonjs/atoms/Text.js.map +1 -1
- package/lib/commonjs/atoms/Transition.js +46 -43
- package/lib/commonjs/atoms/Transition.js.map +1 -1
- package/lib/commonjs/atoms/index.js +6 -0
- package/lib/commonjs/atoms/index.js.map +1 -1
- package/lib/commonjs/atoms/inputValidation.js +56 -0
- package/lib/commonjs/atoms/inputValidation.js.map +1 -0
- package/lib/commonjs/icons/Icon.js +4 -4
- package/lib/commonjs/icons/defaultIcons.js +576 -24
- package/lib/commonjs/icons/defaultIcons.js.map +1 -1
- package/lib/commonjs/icons/index.js +6 -0
- package/lib/commonjs/icons/index.js.map +1 -1
- package/lib/commonjs/molecules/AvatarHeader.js +47 -14
- package/lib/commonjs/molecules/AvatarHeader.js.map +1 -1
- package/lib/commonjs/molecules/FormActions.js +25 -9
- package/lib/commonjs/molecules/FormActions.js.map +1 -1
- package/lib/commonjs/molecules/InfoRow.js +30 -14
- package/lib/commonjs/molecules/InfoRow.js.map +1 -1
- package/lib/commonjs/molecules/OptionCard.js +37 -22
- package/lib/commonjs/molecules/OptionCard.js.map +1 -1
- package/lib/commonjs/molecules/SegmentedControl.js +103 -14
- package/lib/commonjs/molecules/SegmentedControl.js.map +1 -1
- package/lib/commonjs/molecules/Stepper.js +61 -16
- package/lib/commonjs/molecules/Stepper.js.map +1 -1
- package/lib/commonjs/molecules/Steps.js +105 -41
- package/lib/commonjs/molecules/Steps.js.map +1 -1
- package/lib/commonjs/molecules/SymmetricGrid.js +26 -18
- package/lib/commonjs/molecules/SymmetricGrid.js.map +1 -1
- package/lib/commonjs/organisms/Alert.js +49 -35
- package/lib/commonjs/organisms/Alert.js.map +1 -1
- package/lib/commonjs/organisms/AlertProvider.js +10 -10
- package/lib/commonjs/organisms/BottomSheet.js +137 -85
- package/lib/commonjs/organisms/BottomSheet.js.map +1 -1
- package/lib/commonjs/organisms/Chart.js +197 -156
- package/lib/commonjs/organisms/Chart.js.map +1 -1
- package/lib/commonjs/organisms/DateField.js +36 -20
- package/lib/commonjs/organisms/DateField.js.map +1 -1
- package/lib/commonjs/organisms/DatePicker.js +56 -33
- package/lib/commonjs/organisms/DatePicker.js.map +1 -1
- package/lib/commonjs/organisms/List.js +62 -34
- package/lib/commonjs/organisms/List.js.map +1 -1
- package/lib/commonjs/organisms/Modal.js +120 -82
- package/lib/commonjs/organisms/Modal.js.map +1 -1
- package/lib/commonjs/organisms/NavigationBar.js +63 -24
- package/lib/commonjs/organisms/NavigationBar.js.map +1 -1
- package/lib/commonjs/organisms/OptionWheel.js +64 -64
- package/lib/commonjs/organisms/OptionWheel.js.map +1 -1
- package/lib/commonjs/organisms/ReorderableList.js +48 -27
- package/lib/commonjs/organisms/ReorderableList.js.map +1 -1
- package/lib/commonjs/organisms/Screen.js +68 -35
- package/lib/commonjs/organisms/Screen.js.map +1 -1
- package/lib/commonjs/organisms/SearchList.js +56 -26
- package/lib/commonjs/organisms/SearchList.js.map +1 -1
- package/lib/commonjs/organisms/Select.js +45 -24
- package/lib/commonjs/organisms/Select.js.map +1 -1
- package/lib/commonjs/organisms/Slides.js +36 -23
- package/lib/commonjs/organisms/Slides.js.map +1 -1
- package/lib/commonjs/organisms/ThemeToggle.js +10 -9
- package/lib/commonjs/organisms/ThemeToggle.js.map +1 -1
- package/lib/commonjs/organisms/Timeline.js +73 -56
- package/lib/commonjs/organisms/Timeline.js.map +1 -1
- package/lib/commonjs/organisms/Toast.js +46 -15
- package/lib/commonjs/organisms/Toast.js.map +1 -1
- package/lib/commonjs/organisms/ToastProvider.js +14 -10
- package/lib/commonjs/organisms/ToastProvider.js.map +1 -1
- package/lib/commonjs/organisms/Wizard.js +40 -15
- package/lib/commonjs/organisms/Wizard.js.map +1 -1
- package/lib/commonjs/organisms/chart/frame.js +25 -25
- package/lib/commonjs/organisms/chart/frame.js.map +1 -1
- package/lib/commonjs/organisms/chart/marks.js +42 -42
- package/lib/commonjs/organisms/chart/math.js +84 -85
- package/lib/commonjs/organisms/chart/math.js.map +1 -1
- package/lib/commonjs/organisms/chart/palette.js +28 -28
- package/lib/commonjs/organisms/imperativeBridge.js +5 -5
- package/lib/commonjs/organisms/imperativeBridge.js.map +1 -1
- package/lib/commonjs/organisms/useSlides.js +53 -21
- package/lib/commonjs/organisms/useSlides.js.map +1 -1
- package/lib/commonjs/safe-area/SafeAreaUIProvider.js +9 -66
- package/lib/commonjs/safe-area/SafeAreaUIProvider.js.map +1 -1
- package/lib/commonjs/safe-area/index.js.map +1 -1
- package/lib/commonjs/theme/KeyboardAvoidance.js +206 -0
- package/lib/commonjs/theme/KeyboardAvoidance.js.map +1 -0
- package/lib/commonjs/theme/NativeOverlay.js +263 -0
- package/lib/commonjs/theme/NativeOverlay.js.map +1 -0
- package/lib/commonjs/theme/UIProvider.js +86 -33
- package/lib/commonjs/theme/UIProvider.js.map +1 -1
- package/lib/commonjs/theme/colors.js +17 -17
- package/lib/commonjs/theme/colors.js.map +1 -1
- package/lib/commonjs/theme/createStyles.js +19 -16
- package/lib/commonjs/theme/createStyles.js.map +1 -1
- package/lib/commonjs/theme/createTheme.js +6 -6
- package/lib/commonjs/theme/index.js +6 -0
- package/lib/commonjs/theme/index.js.map +1 -1
- package/lib/commonjs/theme/insets.js +1 -1
- package/lib/commonjs/theme/labels.js +29 -1
- package/lib/commonjs/theme/labels.js.map +1 -1
- package/lib/commonjs/theme/palettes.js +25 -23
- package/lib/commonjs/theme/palettes.js.map +1 -1
- package/lib/commonjs/theme/tokens.js +28 -14
- package/lib/commonjs/theme/tokens.js.map +1 -1
- package/lib/commonjs/theme/useKeyboardField.js +36 -0
- package/lib/commonjs/theme/useKeyboardField.js.map +1 -0
- package/lib/commonjs/theme/useReduceMotion.js +31 -0
- package/lib/commonjs/theme/useReduceMotion.js.map +1 -0
- package/lib/module/atoms/Avatar.js +79 -8
- package/lib/module/atoms/Avatar.js.map +1 -1
- package/lib/module/atoms/Badge.js +17 -8
- package/lib/module/atoms/Badge.js.map +1 -1
- package/lib/module/atoms/Button.js +110 -41
- package/lib/module/atoms/Button.js.map +1 -1
- package/lib/module/atoms/Card.js +24 -2
- package/lib/module/atoms/Card.js.map +1 -1
- package/lib/module/atoms/Checkbox.js +42 -11
- package/lib/module/atoms/Checkbox.js.map +1 -1
- package/lib/module/atoms/Divider.js +31 -6
- package/lib/module/atoms/Divider.js.map +1 -1
- package/lib/module/atoms/EmptyState.js +58 -20
- package/lib/module/atoms/EmptyState.js.map +1 -1
- package/lib/module/atoms/Fab.js +33 -13
- package/lib/module/atoms/Fab.js.map +1 -1
- package/lib/module/atoms/Gradient.js +103 -37
- package/lib/module/atoms/Gradient.js.map +1 -1
- package/lib/module/atoms/IconButton.js +25 -4
- package/lib/module/atoms/IconButton.js.map +1 -1
- package/lib/module/atoms/Image.js +61 -25
- package/lib/module/atoms/Image.js.map +1 -1
- package/lib/module/atoms/Input.js +108 -20
- package/lib/module/atoms/Input.js.map +1 -1
- package/lib/module/atoms/KeyValueRow.js +37 -9
- package/lib/module/atoms/KeyValueRow.js.map +1 -1
- package/lib/module/atoms/PressableScale.js +25 -5
- package/lib/module/atoms/PressableScale.js.map +1 -1
- package/lib/module/atoms/ShinyText.js +29 -29
- package/lib/module/atoms/Skeleton.js +30 -13
- package/lib/module/atoms/Skeleton.js.map +1 -1
- package/lib/module/atoms/Spinner.js +82 -23
- package/lib/module/atoms/Spinner.js.map +1 -1
- package/lib/module/atoms/Text.js +56 -5
- package/lib/module/atoms/Text.js.map +1 -1
- package/lib/module/atoms/Transition.js +41 -42
- package/lib/module/atoms/Transition.js.map +1 -1
- package/lib/module/atoms/index.js +1 -1
- package/lib/module/atoms/index.js.map +1 -1
- package/lib/module/atoms/inputValidation.js +52 -0
- package/lib/module/atoms/inputValidation.js.map +1 -0
- package/lib/module/icons/Icon.js +4 -4
- package/lib/module/icons/defaultIcons.js +574 -24
- package/lib/module/icons/defaultIcons.js.map +1 -1
- package/lib/module/icons/index.js +1 -1
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/molecules/AvatarHeader.js +47 -14
- package/lib/module/molecules/AvatarHeader.js.map +1 -1
- package/lib/module/molecules/FormActions.js +25 -9
- package/lib/module/molecules/FormActions.js.map +1 -1
- package/lib/module/molecules/InfoRow.js +30 -14
- package/lib/module/molecules/InfoRow.js.map +1 -1
- package/lib/module/molecules/OptionCard.js +37 -22
- package/lib/module/molecules/OptionCard.js.map +1 -1
- package/lib/module/molecules/SegmentedControl.js +107 -18
- package/lib/module/molecules/SegmentedControl.js.map +1 -1
- package/lib/module/molecules/Stepper.js +63 -18
- package/lib/module/molecules/Stepper.js.map +1 -1
- package/lib/module/molecules/Steps.js +107 -43
- package/lib/module/molecules/Steps.js.map +1 -1
- package/lib/module/molecules/SymmetricGrid.js +26 -18
- package/lib/module/molecules/SymmetricGrid.js.map +1 -1
- package/lib/module/organisms/Alert.js +51 -37
- package/lib/module/organisms/Alert.js.map +1 -1
- package/lib/module/organisms/AlertProvider.js +10 -10
- package/lib/module/organisms/BottomSheet.js +142 -88
- package/lib/module/organisms/BottomSheet.js.map +1 -1
- package/lib/module/organisms/Chart.js +199 -158
- package/lib/module/organisms/Chart.js.map +1 -1
- package/lib/module/organisms/DateField.js +36 -20
- package/lib/module/organisms/DateField.js.map +1 -1
- package/lib/module/organisms/DatePicker.js +56 -33
- package/lib/module/organisms/DatePicker.js.map +1 -1
- package/lib/module/organisms/List.js +62 -34
- package/lib/module/organisms/List.js.map +1 -1
- package/lib/module/organisms/Modal.js +122 -84
- package/lib/module/organisms/Modal.js.map +1 -1
- package/lib/module/organisms/NavigationBar.js +64 -25
- package/lib/module/organisms/NavigationBar.js.map +1 -1
- package/lib/module/organisms/OptionWheel.js +64 -64
- package/lib/module/organisms/OptionWheel.js.map +1 -1
- package/lib/module/organisms/ReorderableList.js +48 -27
- package/lib/module/organisms/ReorderableList.js.map +1 -1
- package/lib/module/organisms/Screen.js +70 -37
- package/lib/module/organisms/Screen.js.map +1 -1
- package/lib/module/organisms/SearchList.js +56 -26
- package/lib/module/organisms/SearchList.js.map +1 -1
- package/lib/module/organisms/Select.js +46 -25
- package/lib/module/organisms/Select.js.map +1 -1
- package/lib/module/organisms/Slides.js +37 -24
- package/lib/module/organisms/Slides.js.map +1 -1
- package/lib/module/organisms/ThemeToggle.js +11 -10
- package/lib/module/organisms/ThemeToggle.js.map +1 -1
- package/lib/module/organisms/Timeline.js +73 -56
- package/lib/module/organisms/Timeline.js.map +1 -1
- package/lib/module/organisms/Toast.js +47 -16
- package/lib/module/organisms/Toast.js.map +1 -1
- package/lib/module/organisms/ToastProvider.js +15 -11
- package/lib/module/organisms/ToastProvider.js.map +1 -1
- package/lib/module/organisms/Wizard.js +41 -16
- package/lib/module/organisms/Wizard.js.map +1 -1
- package/lib/module/organisms/chart/frame.js +25 -25
- package/lib/module/organisms/chart/frame.js.map +1 -1
- package/lib/module/organisms/chart/marks.js +42 -42
- package/lib/module/organisms/chart/math.js +84 -85
- package/lib/module/organisms/chart/math.js.map +1 -1
- package/lib/module/organisms/chart/palette.js +28 -28
- package/lib/module/organisms/imperativeBridge.js +5 -5
- package/lib/module/organisms/imperativeBridge.js.map +1 -1
- package/lib/module/organisms/useSlides.js +53 -21
- package/lib/module/organisms/useSlides.js.map +1 -1
- package/lib/module/safe-area/SafeAreaUIProvider.js +10 -65
- package/lib/module/safe-area/SafeAreaUIProvider.js.map +1 -1
- package/lib/module/safe-area/index.js +1 -0
- package/lib/module/safe-area/index.js.map +1 -1
- package/lib/module/theme/KeyboardAvoidance.js +198 -0
- package/lib/module/theme/KeyboardAvoidance.js.map +1 -0
- package/lib/module/theme/NativeOverlay.js +257 -0
- package/lib/module/theme/NativeOverlay.js.map +1 -0
- package/lib/module/theme/UIProvider.js +88 -35
- package/lib/module/theme/UIProvider.js.map +1 -1
- package/lib/module/theme/colors.js +17 -17
- package/lib/module/theme/colors.js.map +1 -1
- package/lib/module/theme/createStyles.js +20 -16
- package/lib/module/theme/createStyles.js.map +1 -1
- package/lib/module/theme/createTheme.js +6 -6
- package/lib/module/theme/index.js +1 -1
- package/lib/module/theme/index.js.map +1 -1
- package/lib/module/theme/insets.js +1 -1
- package/lib/module/theme/labels.js +29 -1
- package/lib/module/theme/labels.js.map +1 -1
- package/lib/module/theme/palettes.js +25 -23
- package/lib/module/theme/palettes.js.map +1 -1
- package/lib/module/theme/tokens.js +27 -13
- package/lib/module/theme/tokens.js.map +1 -1
- package/lib/module/theme/useKeyboardField.js +33 -0
- package/lib/module/theme/useKeyboardField.js.map +1 -0
- package/lib/module/theme/useReduceMotion.js +28 -0
- package/lib/module/theme/useReduceMotion.js.map +1 -0
- package/lib/typescript/atoms/Avatar.d.ts +35 -5
- package/lib/typescript/atoms/Avatar.d.ts.map +1 -1
- package/lib/typescript/atoms/Badge.d.ts +14 -3
- package/lib/typescript/atoms/Badge.d.ts.map +1 -1
- package/lib/typescript/atoms/Button.d.ts +34 -3
- package/lib/typescript/atoms/Button.d.ts.map +1 -1
- package/lib/typescript/atoms/Card.d.ts +13 -2
- package/lib/typescript/atoms/Card.d.ts.map +1 -1
- package/lib/typescript/atoms/Checkbox.d.ts +26 -1
- package/lib/typescript/atoms/Checkbox.d.ts.map +1 -1
- package/lib/typescript/atoms/Divider.d.ts +18 -1
- package/lib/typescript/atoms/Divider.d.ts.map +1 -1
- package/lib/typescript/atoms/EmptyState.d.ts +30 -3
- package/lib/typescript/atoms/EmptyState.d.ts.map +1 -1
- package/lib/typescript/atoms/Fab.d.ts +24 -4
- package/lib/typescript/atoms/Fab.d.ts.map +1 -1
- package/lib/typescript/atoms/Gradient.d.ts +21 -15
- package/lib/typescript/atoms/Gradient.d.ts.map +1 -1
- package/lib/typescript/atoms/IconButton.d.ts +18 -4
- package/lib/typescript/atoms/IconButton.d.ts.map +1 -1
- package/lib/typescript/atoms/Image.d.ts +39 -17
- package/lib/typescript/atoms/Image.d.ts.map +1 -1
- package/lib/typescript/atoms/Input.d.ts +57 -7
- package/lib/typescript/atoms/Input.d.ts.map +1 -1
- package/lib/typescript/atoms/KeyValueRow.d.ts +16 -4
- package/lib/typescript/atoms/KeyValueRow.d.ts.map +1 -1
- package/lib/typescript/atoms/PressableScale.d.ts +12 -5
- package/lib/typescript/atoms/PressableScale.d.ts.map +1 -1
- package/lib/typescript/atoms/ShinyText.d.ts +17 -17
- package/lib/typescript/atoms/ShinyText.d.ts.map +1 -1
- package/lib/typescript/atoms/Skeleton.d.ts +12 -7
- package/lib/typescript/atoms/Skeleton.d.ts.map +1 -1
- package/lib/typescript/atoms/Spinner.d.ts +29 -1
- package/lib/typescript/atoms/Spinner.d.ts.map +1 -1
- package/lib/typescript/atoms/Text.d.ts +46 -0
- package/lib/typescript/atoms/Text.d.ts.map +1 -1
- package/lib/typescript/atoms/Transition.d.ts +19 -15
- package/lib/typescript/atoms/Transition.d.ts.map +1 -1
- package/lib/typescript/atoms/index.d.ts +3 -3
- package/lib/typescript/atoms/index.d.ts.map +1 -1
- package/lib/typescript/atoms/inputValidation.d.ts +29 -0
- package/lib/typescript/atoms/inputValidation.d.ts.map +1 -0
- package/lib/typescript/icons/Icon.d.ts +4 -4
- package/lib/typescript/icons/defaultIcons.d.ts +19 -2
- package/lib/typescript/icons/defaultIcons.d.ts.map +1 -1
- package/lib/typescript/icons/index.d.ts +2 -2
- package/lib/typescript/icons/index.d.ts.map +1 -1
- package/lib/typescript/icons/types.d.ts +13 -3
- package/lib/typescript/icons/types.d.ts.map +1 -1
- package/lib/typescript/molecules/AvatarHeader.d.ts +23 -4
- package/lib/typescript/molecules/AvatarHeader.d.ts.map +1 -1
- package/lib/typescript/molecules/FormActions.d.ts +21 -4
- package/lib/typescript/molecules/FormActions.d.ts.map +1 -1
- package/lib/typescript/molecules/InfoRow.d.ts +17 -3
- package/lib/typescript/molecules/InfoRow.d.ts.map +1 -1
- package/lib/typescript/molecules/OptionCard.d.ts +20 -5
- package/lib/typescript/molecules/OptionCard.d.ts.map +1 -1
- package/lib/typescript/molecules/SegmentedControl.d.ts +18 -2
- package/lib/typescript/molecules/SegmentedControl.d.ts.map +1 -1
- package/lib/typescript/molecules/Stepper.d.ts +23 -7
- package/lib/typescript/molecules/Stepper.d.ts.map +1 -1
- package/lib/typescript/molecules/Steps.d.ts +18 -4
- package/lib/typescript/molecules/Steps.d.ts.map +1 -1
- package/lib/typescript/molecules/SymmetricGrid.d.ts +15 -5
- package/lib/typescript/molecules/SymmetricGrid.d.ts.map +1 -1
- package/lib/typescript/organisms/Alert.d.ts +28 -2
- package/lib/typescript/organisms/Alert.d.ts.map +1 -1
- package/lib/typescript/organisms/AlertProvider.d.ts +10 -10
- package/lib/typescript/organisms/AlertProvider.d.ts.map +1 -1
- package/lib/typescript/organisms/BottomSheet.d.ts +20 -5
- package/lib/typescript/organisms/BottomSheet.d.ts.map +1 -1
- package/lib/typescript/organisms/Chart.d.ts +86 -75
- package/lib/typescript/organisms/Chart.d.ts.map +1 -1
- package/lib/typescript/organisms/DateField.d.ts +17 -3
- package/lib/typescript/organisms/DateField.d.ts.map +1 -1
- package/lib/typescript/organisms/DatePicker.d.ts +7 -11
- package/lib/typescript/organisms/DatePicker.d.ts.map +1 -1
- package/lib/typescript/organisms/List.d.ts +30 -3
- package/lib/typescript/organisms/List.d.ts.map +1 -1
- package/lib/typescript/organisms/Modal.d.ts +30 -3
- package/lib/typescript/organisms/Modal.d.ts.map +1 -1
- package/lib/typescript/organisms/NavigationBar.d.ts +26 -7
- package/lib/typescript/organisms/NavigationBar.d.ts.map +1 -1
- package/lib/typescript/organisms/OptionWheel.d.ts +20 -16
- package/lib/typescript/organisms/OptionWheel.d.ts.map +1 -1
- package/lib/typescript/organisms/ReorderableList.d.ts +20 -11
- package/lib/typescript/organisms/ReorderableList.d.ts.map +1 -1
- package/lib/typescript/organisms/Screen.d.ts +27 -8
- package/lib/typescript/organisms/Screen.d.ts.map +1 -1
- package/lib/typescript/organisms/SearchList.d.ts +46 -3
- package/lib/typescript/organisms/SearchList.d.ts.map +1 -1
- package/lib/typescript/organisms/Select.d.ts +26 -6
- package/lib/typescript/organisms/Select.d.ts.map +1 -1
- package/lib/typescript/organisms/Slides.d.ts +44 -26
- package/lib/typescript/organisms/Slides.d.ts.map +1 -1
- package/lib/typescript/organisms/ThemeToggle.d.ts +6 -2
- package/lib/typescript/organisms/ThemeToggle.d.ts.map +1 -1
- package/lib/typescript/organisms/Timeline.d.ts +26 -9
- package/lib/typescript/organisms/Timeline.d.ts.map +1 -1
- package/lib/typescript/organisms/Toast.d.ts +19 -4
- package/lib/typescript/organisms/Toast.d.ts.map +1 -1
- package/lib/typescript/organisms/ToastProvider.d.ts +8 -8
- package/lib/typescript/organisms/ToastProvider.d.ts.map +1 -1
- package/lib/typescript/organisms/Wizard.d.ts +24 -6
- package/lib/typescript/organisms/Wizard.d.ts.map +1 -1
- package/lib/typescript/organisms/chart/frame.d.ts +21 -21
- package/lib/typescript/organisms/chart/frame.d.ts.map +1 -1
- package/lib/typescript/organisms/chart/marks.d.ts +28 -28
- package/lib/typescript/organisms/chart/marks.d.ts.map +1 -1
- package/lib/typescript/organisms/chart/math.d.ts +69 -70
- package/lib/typescript/organisms/chart/math.d.ts.map +1 -1
- package/lib/typescript/organisms/chart/palette.d.ts +2 -2
- package/lib/typescript/organisms/chart/palette.d.ts.map +1 -1
- package/lib/typescript/organisms/imperativeBridge.d.ts +4 -4
- package/lib/typescript/organisms/useSlides.d.ts +8 -16
- package/lib/typescript/organisms/useSlides.d.ts.map +1 -1
- package/lib/typescript/safe-area/SafeAreaUIProvider.d.ts +9 -7
- package/lib/typescript/safe-area/SafeAreaUIProvider.d.ts.map +1 -1
- package/lib/typescript/safe-area/index.d.ts.map +1 -1
- package/lib/typescript/theme/KeyboardAvoidance.d.ts +62 -0
- package/lib/typescript/theme/KeyboardAvoidance.d.ts.map +1 -0
- package/lib/typescript/theme/NativeOverlay.d.ts +43 -0
- package/lib/typescript/theme/NativeOverlay.d.ts.map +1 -0
- package/lib/typescript/theme/UIProvider.d.ts +38 -20
- package/lib/typescript/theme/UIProvider.d.ts.map +1 -1
- package/lib/typescript/theme/colors.d.ts +13 -13
- package/lib/typescript/theme/colors.d.ts.map +1 -1
- package/lib/typescript/theme/createStyles.d.ts +20 -14
- package/lib/typescript/theme/createStyles.d.ts.map +1 -1
- package/lib/typescript/theme/createTheme.d.ts +5 -5
- package/lib/typescript/theme/index.d.ts +1 -1
- package/lib/typescript/theme/index.d.ts.map +1 -1
- package/lib/typescript/theme/insets.d.ts +1 -1
- package/lib/typescript/theme/insets.d.ts.map +1 -1
- package/lib/typescript/theme/labels.d.ts +57 -2
- package/lib/typescript/theme/labels.d.ts.map +1 -1
- package/lib/typescript/theme/palettes.d.ts +25 -19
- package/lib/typescript/theme/palettes.d.ts.map +1 -1
- package/lib/typescript/theme/tokens.d.ts +15 -8
- package/lib/typescript/theme/tokens.d.ts.map +1 -1
- package/lib/typescript/theme/types.d.ts +7 -7
- package/lib/typescript/theme/types.d.ts.map +1 -1
- package/lib/typescript/theme/useKeyboardField.d.ts +17 -0
- package/lib/typescript/theme/useKeyboardField.d.ts.map +1 -0
- package/lib/typescript/theme/useReduceMotion.d.ts +7 -0
- package/lib/typescript/theme/useReduceMotion.d.ts.map +1 -0
- package/llms.txt +4 -4
- package/package.json +15 -14
- package/registry/alert-provider.json +4 -3
- package/registry/alert.json +3 -2
- package/registry/avatar-header.json +1 -1
- package/registry/avatar.json +3 -2
- package/registry/badge.json +1 -1
- package/registry/bottom-sheet.json +1 -1
- package/registry/button.json +3 -2
- package/registry/card.json +1 -1
- package/registry/chart.json +5 -5
- package/registry/checkbox.json +3 -2
- package/registry/core.json +31 -15
- package/registry/date-field.json +1 -1
- package/registry/date-picker.json +3 -2
- package/registry/divider.json +1 -1
- package/registry/empty-state.json +4 -2
- package/registry/fab.json +3 -2
- package/registry/form-actions.json +3 -2
- package/registry/gradient.json +1 -1
- package/registry/icon-button.json +3 -2
- package/registry/image.json +1 -1
- package/registry/index.json +79 -61
- package/registry/info-row.json +1 -1
- package/registry/input.json +5 -1
- package/registry/key-value-row.json +1 -1
- package/registry/list.json +3 -1
- package/registry/manifest.json +3 -1
- package/registry/modal.json +1 -1
- package/registry/navigation-bar.json +1 -1
- package/registry/option-card.json +3 -2
- package/registry/option-wheel.json +1 -1
- package/registry/pressable-scale.json +3 -2
- package/registry/reorderable-list.json +1 -1
- package/registry/screen.json +1 -1
- package/registry/search-list.json +7 -1
- package/registry/segmented-control.json +1 -1
- package/registry/select.json +1 -1
- package/registry/shiny-text.json +1 -1
- package/registry/skeleton.json +3 -2
- package/registry/slides.json +2 -2
- package/registry/spinner.json +1 -1
- package/registry/stepper.json +1 -1
- package/registry/steps.json +1 -1
- package/registry/symmetric-grid.json +1 -1
- package/registry/theme-toggle.json +1 -1
- package/registry/timeline.json +1 -1
- package/registry/title.json +1 -1
- package/registry/toast-provider.json +2 -2
- package/registry/toast.json +1 -1
- package/registry/transition.json +1 -1
- package/registry/wizard.json +1 -1
- package/src/atoms/Avatar.tsx +117 -26
- package/src/atoms/Badge.tsx +20 -9
- package/src/atoms/Button.tsx +102 -30
- package/src/atoms/Card.tsx +23 -3
- package/src/atoms/Checkbox.tsx +49 -12
- package/src/atoms/Divider.tsx +34 -4
- package/src/atoms/EmptyState.tsx +63 -16
- package/src/atoms/Fab.tsx +56 -9
- package/src/atoms/Gradient.tsx +92 -41
- package/src/atoms/IconButton.tsx +27 -4
- package/src/atoms/Image.tsx +74 -28
- package/src/atoms/Input.tsx +129 -19
- package/src/atoms/KeyValueRow.tsx +72 -28
- package/src/atoms/PressableScale.tsx +31 -7
- package/src/atoms/ShinyText.tsx +31 -31
- package/src/atoms/Skeleton.tsx +37 -17
- package/src/atoms/Spinner.tsx +87 -24
- package/src/atoms/Text.tsx +66 -6
- package/src/atoms/Transition.tsx +40 -44
- package/src/atoms/index.ts +3 -3
- package/src/atoms/inputValidation.ts +59 -0
- package/src/icons/Icon.tsx +4 -4
- package/src/icons/defaultIcons.tsx +445 -22
- package/src/icons/index.ts +2 -2
- package/src/icons/types.ts +26 -4
- package/src/molecules/AvatarHeader.tsx +88 -34
- package/src/molecules/FormActions.tsx +36 -6
- package/src/molecules/InfoRow.tsx +39 -12
- package/src/molecules/OptionCard.tsx +55 -20
- package/src/molecules/SegmentedControl.tsx +135 -15
- package/src/molecules/Stepper.tsx +73 -19
- package/src/molecules/Steps.tsx +121 -39
- package/src/molecules/SymmetricGrid.tsx +28 -12
- package/src/organisms/Alert.tsx +97 -26
- package/src/organisms/AlertProvider.tsx +13 -13
- package/src/organisms/BottomSheet.tsx +115 -43
- package/src/organisms/Chart.tsx +223 -165
- package/src/organisms/DateField.tsx +40 -15
- package/src/organisms/DatePicker.tsx +53 -32
- package/src/organisms/List.tsx +64 -15
- package/src/organisms/Modal.tsx +122 -63
- package/src/organisms/NavigationBar.tsx +73 -23
- package/src/organisms/OptionWheel.tsx +70 -66
- package/src/organisms/ReorderableList.tsx +49 -27
- package/src/organisms/Screen.tsx +79 -42
- package/src/organisms/SearchList.tsx +84 -20
- package/src/organisms/Select.tsx +63 -29
- package/src/organisms/Slides.tsx +63 -44
- package/src/organisms/ThemeToggle.tsx +15 -6
- package/src/organisms/Timeline.tsx +79 -62
- package/src/organisms/Toast.tsx +50 -15
- package/src/organisms/ToastProvider.tsx +17 -13
- package/src/organisms/Wizard.tsx +58 -21
- package/src/organisms/chart/frame.tsx +35 -35
- package/src/organisms/chart/marks.tsx +53 -53
- package/src/organisms/chart/math.ts +93 -94
- package/src/organisms/chart/palette.ts +28 -28
- package/src/organisms/imperativeBridge.ts +5 -5
- package/src/organisms/useSlides.ts +66 -31
- package/src/safe-area/SafeAreaUIProvider.tsx +9 -70
- package/src/safe-area/index.ts +1 -0
- package/src/theme/KeyboardAvoidance.tsx +216 -0
- package/src/theme/NativeOverlay.tsx +269 -0
- package/src/theme/UIProvider.tsx +109 -46
- package/src/theme/colors.ts +21 -21
- package/src/theme/createStyles.ts +23 -19
- package/src/theme/createTheme.ts +6 -6
- package/src/theme/index.ts +1 -1
- package/src/theme/insets.ts +1 -1
- package/src/theme/labels.ts +85 -2
- package/src/theme/palettes.ts +33 -25
- package/src/theme/tokens.ts +27 -13
- package/src/theme/types.ts +7 -7
- package/src/theme/useKeyboardField.ts +33 -0
- package/src/theme/useReduceMotion.ts +27 -0
|
@@ -13,16 +13,17 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
{
|
|
15
15
|
"path": "organisms/AlertProvider.tsx",
|
|
16
|
-
"content": "import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';\nimport { Alert, type AlertButton, type AlertType } from './Alert';\nimport { createImperativeBridge } from './imperativeBridge';\n\nexport interface AlertOptions {\n title: string;\n message?: string;\n type?: AlertType;\n confirmText?: string;\n cancelText?: string;\n}\n\nexport interface ConfirmOptions extends AlertOptions {\n /**
|
|
16
|
+
"content": "import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';\nimport { Alert, type AlertButton, type AlertType } from './Alert';\nimport { createImperativeBridge } from './imperativeBridge';\n\nexport interface AlertOptions {\n title: string;\n message?: string;\n type?: AlertType;\n confirmText?: string;\n cancelText?: string;\n}\n\nexport interface ConfirmOptions extends AlertOptions {\n /** Shows the confirm button as destructive/red (for \"delete\", etc.). */\n destructive?: boolean;\n}\n\ninterface AlertContextValue {\n /** Shows a single-button alert. Resolves when it closes. */\n alert: (options: AlertOptions) => Promise<void>;\n /** Shows confirm/cancel. Resolves true if confirmed, false if cancelled. */\n confirm: (options: ConfirmOptions) => Promise<boolean>;\n hide: () => void;\n}\n\nconst AlertContext = createContext<AlertContextValue | undefined>(undefined);\nconst bridge = createImperativeBridge<AlertContextValue>('AlertProvider');\n\n/**\n * Opens an alert from outside the React tree —a service, an interceptor—\n * without going through the hook. Without a mounted <AlertProvider> it\n * resolves immediately, so an `await` never hangs.\n */\nexport function showAlert(options: AlertOptions): Promise<void> {\n return bridge.get()?.alert(options) ?? Promise.resolve();\n}\n\n/** Same as showAlert, but resolves true/false. Without a provider, false. */\nexport function showConfirm(options: ConfirmOptions): Promise<boolean> {\n return bridge.get()?.confirm(options) ?? Promise.resolve(false);\n}\n\nexport function hideAlert(): void {\n bridge.get()?.hide();\n}\n\ninterface InternalState {\n visible: boolean;\n title: string;\n message?: string;\n type: AlertType;\n buttons: AlertButton[];\n}\n\nconst HIDDEN: InternalState = { visible: false, title: '', type: 'info', buttons: [] };\n\n/**\n * AlertProvider\n * Mounts a single imperatively controlled <Alert>. Place it once near the\n * root (inside UIProvider). Async handlers can do\n * `const ok = await confirm({ title: 'Delete', destructive: true })`.\n */\nexport function AlertProvider({ children }: { children: React.ReactNode }) {\n const [state, setState] = useState<InternalState>(HIDDEN);\n const resolverRef = useRef<((value: any) => void) | null>(null);\n // If alert()/confirm() are called again while one is already open, the\n // previous request is resolved (not discarded) before showing the new one:\n // this keeps its promise from hanging forever.\n const settlePending = useCallback(() => {\n resolverRef.current?.(undefined);\n resolverRef.current = null;\n }, []);\n\n const hide = useCallback(() => {\n setState(HIDDEN);\n settlePending();\n }, [settlePending]);\n\n const alertFn = useCallback((options: AlertOptions): Promise<void> => {\n settlePending();\n return new Promise((resolve) => {\n resolverRef.current = resolve;\n setState({\n visible: true,\n title: options.title,\n message: options.message,\n type: options.type ?? 'info',\n buttons: [\n {\n text: options.confirmText ?? 'OK',\n onPress: () => {\n setState(HIDDEN);\n resolve();\n resolverRef.current = null;\n },\n },\n ],\n });\n });\n }, [settlePending]);\n\n const confirmFn = useCallback((options: ConfirmOptions): Promise<boolean> => {\n settlePending();\n return new Promise((resolve) => {\n resolverRef.current = resolve;\n setState({\n visible: true,\n title: options.title,\n message: options.message,\n type: options.type ?? 'question',\n buttons: [\n {\n text: options.cancelText ?? 'Cancel',\n style: 'cancel',\n variant: 'outline',\n onPress: () => {\n setState(HIDDEN);\n resolve(false);\n resolverRef.current = null;\n },\n },\n {\n text: options.confirmText ?? 'OK',\n variant: options.destructive ? 'destructive' : 'primary',\n style: options.destructive ? 'destructive' : 'default',\n onPress: () => {\n setState(HIDDEN);\n resolve(true);\n resolverRef.current = null;\n },\n },\n ],\n });\n });\n }, [settlePending]);\n\n const value = useMemo<AlertContextValue>(() => ({ alert: alertFn, confirm: confirmFn, hide }), [alertFn, confirmFn, hide]);\n\n useEffect(() => {\n bridge.set(value);\n return () => bridge.set(null);\n }, [value]);\n\n return (\n <AlertContext.Provider value={value}>\n {children}\n <Alert visible={state.visible} title={state.title} message={state.message} type={state.type} buttons={state.buttons} onClose={hide} />\n </AlertContext.Provider>\n );\n}\n\nexport function useAlert(): AlertContextValue {\n const ctx = useContext(AlertContext);\n if (!ctx) {\n throw new Error('orn-ui: useAlert must be used within an <AlertProvider>.');\n }\n return ctx;\n}\n"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"path": "organisms/imperativeBridge.ts",
|
|
20
|
-
"content": "/**\n *
|
|
20
|
+
"content": "/**\n * Bridge between the mounted provider and code that isn't a component\n * (services, API layer, interceptors). The provider registers itself on mount\n * and clears itself on unmount, so outside the tree there is never a live\n * reference left to a dead context.\n */\nexport interface ImperativeBridge<T> {\n set: (value: T | null) => void;\n get: () => T | null;\n}\n\nexport function createImperativeBridge<T>(providerName: string): ImperativeBridge<T> {\n let current: T | null = null;\n\n return {\n set: (value) => {\n current = value;\n },\n get: () => {\n if (!current && __DEV__) {\n console.warn(\n `orn-ui: the imperative API was called without a mounted <${providerName}>. The call was ignored.`\n );\n }\n return current;\n },\n };\n}\n"
|
|
21
21
|
}
|
|
22
22
|
],
|
|
23
23
|
"registryDependencies": [
|
|
24
24
|
"core",
|
|
25
25
|
"alert",
|
|
26
|
-
"button"
|
|
26
|
+
"button",
|
|
27
|
+
"transition"
|
|
27
28
|
]
|
|
28
29
|
}
|
package/registry/alert.json
CHANGED
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "organisms/Alert.tsx",
|
|
12
|
-
"content": "import React, { memo } from 'react';\nimport {
|
|
12
|
+
"content": "import React, { memo } from 'react';\nimport { Text, TouchableWithoutFeedback, View } from 'react-native';\nimport { NativeOverlay } from '../theme/NativeOverlay';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors, useLabels, useTheme } from '../theme/UIProvider';\nimport { Button, type ButtonVariant } from '../atoms/Button';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\n\nexport type AlertType = 'success' | 'error' | 'warning' | 'info' | 'question';\n\nexport interface AlertButton {\n /** Button label. */\n text: string;\n /** Called on press; without it the button calls `onClose`. */\n onPress?: () => void;\n /** 'cancel' renders outline, 'destructive' renders in the error color. */\n style?: 'cancel' | 'default' | 'destructive';\n /** Explicit Button variant; wins over `style`. */\n variant?: ButtonVariant;\n}\n\nexport interface AlertProps {\n /** Shows or hides the dialog. */\n visible: boolean;\n /** Short heading; screen readers announce it as a header. */\n title: string;\n /** Optional explanation under the title. */\n message?: string;\n /** Icon and color of the dialog. @default 'info' */\n type?: AlertType;\n /** Tap on the backdrop, Android back, VoiceOver escape. Without it the backdrop does nothing. */\n onClose?: () => void;\n /** Custom buttons; three or more stack vertically. */\n buttons?: AlertButton[];\n /** Shorthand: label of the confirm button. @default labels.confirm */\n confirmText?: string;\n /** Shorthand: label of the cancel button (shown with `onCancel` or `cancelText`). @default labels.cancel */\n cancelText?: string;\n /** Shorthand: confirm action. */\n onConfirm?: () => void;\n /** Shorthand: cancel action (falls back to `onClose`). */\n onCancel?: () => void;\n /** The confirm button uses the destructive color (delete, discard). @default false */\n destructive?: boolean;\n /** Draws the dialog inside its parent instead of over the screen (docs, previews). @default false */\n inline?: boolean;\n}\n\nconst ICON_BY_TYPE: Record<AlertType, IconName> = {\n success: 'check',\n error: 'alert',\n warning: 'warning',\n question: 'question',\n info: 'info',\n};\n\n/** Readable line length for a short dialog on tablets too. */\nconst DIALOG_MAX_WIDTH = 340;\nconst ICON_SIZE = 48;\n/** Above any sibling of the inline host. */\nconst INLINE_Z = 9999;\n\nconst useStyles = createStyles((theme) => ({\n overlay: {\n flex: 1,\n backgroundColor: theme.colors.overlay,\n justifyContent: 'center',\n alignItems: 'center',\n padding: theme.tokens.spacing.xl,\n },\n fill: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },\n overlayInline: { zIndex: INLINE_Z },\n container: {\n // Raised: in dark mode the plain surface sank into the dimmed backdrop.\n backgroundColor: theme.colors.surfaceRaised,\n borderRadius: theme.tokens.radius.xl,\n padding: theme.tokens.spacing.xl,\n width: '100%',\n maxWidth: DIALOG_MAX_WIDTH,\n alignItems: 'center',\n ...theme.tokens.shadow.md,\n },\n iconContainer: { marginBottom: theme.tokens.spacing.lg },\n title: {\n fontSize: theme.tokens.fontSize.xl,\n lineHeight: theme.tokens.lineHeight.lg,\n fontWeight: theme.tokens.fontWeight.bold,\n color: theme.colors.text,\n textAlign: 'center',\n marginBottom: theme.tokens.spacing.sm,\n },\n message: {\n fontSize: theme.tokens.fontSize.md,\n lineHeight: theme.tokens.lineHeight.md,\n color: theme.colors.textLight,\n textAlign: 'center',\n },\n buttonContainer: {\n flexDirection: 'row',\n justifyContent: 'center',\n gap: theme.tokens.spacing.sm,\n width: '100%',\n marginTop: theme.tokens.spacing.xl,\n },\n buttonContainerVertical: { flexDirection: 'column' },\n buttonHorizontal: { flex: 1 },\n buttonVertical: { width: '100%' },\n}));\n\n/** Derives the list of buttons from props, without mutating the `buttons` received through props. */\nfunction resolveButtons(props: AlertProps, labels: { confirm: string; cancel: string; close: string }): AlertButton[] {\n const { buttons, confirmText, cancelText, onConfirm, onCancel, onClose, destructive } = props;\n if (buttons && buttons.length > 0) return buttons;\n\n if (confirmText || onConfirm) {\n const list: AlertButton[] = [];\n if (cancelText || onCancel) {\n list.push({ text: cancelText || labels.cancel, onPress: onCancel || onClose, variant: 'outline', style: 'cancel' });\n }\n list.push({ text: confirmText || labels.confirm, onPress: onConfirm, variant: destructive ? 'destructive' : 'primary' });\n return list;\n }\n\n return [{ text: labels.close, onPress: onClose, variant: 'outline' }];\n}\n\n/**\n * Alert\n * Declarative dialog component. For imperative use (showing it from a\n * handler without lifting your own state), use AlertProvider + useAlert().\n *\n * Gestalt, figure-ground: the overlay dims everything behind, so the dialog\n * is the only figure. Proximity: icon, title and message sit together and the\n * buttons a larger step below, as the answer block. Von Restorff: only the\n * main action is filled (or red when `destructive`); cancel is outline.\n * Hick: two buttons side by side; three or more stack so each reads alone.\n *\n * Accessibility: the title is a header, the dialog is modal for VoiceOver and\n * the escape gesture (two-finger Z) calls `onClose`.\n */\nexport const Alert = memo((props: AlertProps) => {\n const { visible, title, message, type = 'info', onClose, inline = false } = props;\n const theme = useTheme();\n const colors = useColors();\n const styles = useStyles();\n const labels = useLabels();\n const allowFontScaling = useAllowFontScaling();\n\n if (!visible) return null;\n\n const iconColor: Record<AlertType, string> = {\n // Icons over the dialog's surface: `*Text` role, not the full accent.\n success: colors.successText,\n error: colors.errorText,\n warning: colors.warningText,\n question: colors.primaryText,\n info: colors.secondaryText,\n };\n\n const effectiveButtons = resolveButtons(props, { confirm: labels.confirm, cancel: labels.cancel, close: labels.close });\n const vertical = effectiveButtons.length > 2;\n\n const content = (\n <TouchableWithoutFeedback onPress={onClose} accessibilityRole=\"none\">\n <View style={[styles.overlay, inline && [styles.fill, styles.overlayInline]]}>\n <TouchableWithoutFeedback>\n <View\n style={styles.container}\n accessibilityRole=\"alert\"\n accessibilityViewIsModal\n onAccessibilityEscape={onClose}\n >\n <View style={styles.iconContainer}>\n <Icon name={ICON_BY_TYPE[type]} size={ICON_SIZE} color={iconColor[type]} />\n </View>\n\n <Text allowFontScaling={allowFontScaling} style={styles.title} accessibilityRole=\"header\">\n {title}\n </Text>\n\n {!!message && (\n <Text allowFontScaling={allowFontScaling} style={styles.message}>\n {message}\n </Text>\n )}\n\n <View style={[styles.buttonContainer, vertical && styles.buttonContainerVertical]}>\n {effectiveButtons.map((btn, index) => {\n const isCancel = btn.style === 'cancel';\n let variant: ButtonVariant = btn.variant || 'primary';\n if (!btn.variant && isCancel) variant = 'outline';\n if (!btn.variant && btn.style === 'destructive') variant = 'destructive';\n\n return (\n <Button\n key={index}\n title={btn.text}\n onPress={btn.onPress || onClose || (() => {})}\n variant={variant}\n style={vertical ? styles.buttonVertical : styles.buttonHorizontal}\n textStyle={{ fontSize: theme.tokens.fontSize.md }}\n />\n );\n })}\n </View>\n </View>\n </TouchableWithoutFeedback>\n </View>\n </TouchableWithoutFeedback>\n );\n\n if (inline) {\n return (\n <View style={styles.fill} pointerEvents=\"box-none\">\n {content}\n </View>\n );\n }\n\n return (\n <NativeOverlay\n dialog\n transparent\n animationType=\"fade\"\n statusBarTranslucent\n presentationStyle=\"overFullScreen\"\n onRequestClose={onClose || (() => {})}\n >\n {content}\n </NativeOverlay>\n );\n});\nAlert.displayName = 'Alert';\n"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"registryDependencies": [
|
|
16
16
|
"core",
|
|
17
|
-
"button"
|
|
17
|
+
"button",
|
|
18
|
+
"transition"
|
|
18
19
|
]
|
|
19
20
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "molecules/AvatarHeader.tsx",
|
|
12
|
-
"content": "import React, { memo } from 'react';\nimport { Text, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors } from '../theme/UIProvider';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\n\nexport interface AvatarHeaderProps {\n /** Icon to show inside the circle. If omitted, `initials` is used instead. */\n iconName?: IconName;\n iconColor?: string;\n /** Short text (e.g. 2 letters) shown when `iconName` isn't provided. */\n initials?: string;\n title: string;\n subtitle?: React.ReactNode;\n style?: StyleProp<ViewStyle>;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { alignItems: 'center'
|
|
12
|
+
"content": "import React, { memo } from 'react';\nimport { Text, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors } from '../theme/UIProvider';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\n\nexport interface AvatarHeaderProps {\n /** Icon to show inside the circle. If omitted, `initials` is used instead. */\n iconName?: IconName;\n /** Icon color. Keep AA contrast against `primarySoft`, the circle's fill. @default colors.primary */\n iconColor?: string;\n /** Short text (e.g. 2 letters) shown when `iconName` isn't provided. Without either, a `user` icon. */\n initials?: string;\n /** Main heading of the screen or modal; announced as a header. */\n title: string;\n /** Supporting line under the title. A string gets the muted style; a node renders as is. */\n subtitle?: React.ReactNode;\n /**\n * Third and last line, the least important detail (\"Joined March 2023\").\n * Three lines is the limit: beyond that the header stops being scannable.\n */\n caption?: string;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nconst CIRCLE = 80;\nconst ICON_SIZE = 40;\n\nconst useStyles = createStyles((theme) => ({\n // No outer margin: the space below belongs to the parent's layout.\n container: { alignItems: 'center' },\n avatar: {\n width: CIRCLE,\n height: CIRCLE,\n borderRadius: CIRCLE / 2,\n backgroundColor: theme.colors.primarySoft,\n justifyContent: 'center',\n alignItems: 'center',\n marginBottom: theme.tokens.spacing.md,\n },\n initials: {\n fontSize: theme.tokens.fontSize.xxl,\n fontWeight: theme.tokens.fontWeight.bold,\n color: theme.colors.primaryText,\n },\n title: {\n fontSize: theme.tokens.fontSize.xl,\n lineHeight: theme.tokens.lineHeight.lg,\n fontWeight: theme.tokens.fontWeight.bold,\n textAlign: 'center',\n color: theme.colors.text,\n marginBottom: theme.tokens.spacing.xs,\n },\n subtitle: {\n fontSize: theme.tokens.fontSize.md,\n lineHeight: theme.tokens.lineHeight.md,\n color: theme.colors.textLight,\n textAlign: 'center',\n },\n caption: {\n fontSize: theme.tokens.fontSize.sm,\n lineHeight: theme.tokens.lineHeight.sm,\n color: theme.colors.textLight,\n textAlign: 'center',\n marginTop: theme.tokens.spacing.xxs,\n },\n}));\n\n/**\n * AvatarHeader\n * 80x80 circle with an icon or initials + centered title/subtitle. Typical\n * header of detail modals (client, product, etc).\n *\n * Gestalt, symmetry and focal point: everything sits on one centered axis and\n * the circle is the single large shape, so the eye lands there first and then\n * drops to the title. Proximity: title and subtitle are closer to each other\n * than to the circle, reading as one name + its detail. Hierarchy by size:\n * title, subtitle, caption — at most three lines of text. Long text wraps\n * centered, so the axis holds. No outer margin: the parent sets the spacing.\n *\n * Accessibility: the title is a header; the circle is decorative (the\n * initials would only repeat the title).\n */\nexport const AvatarHeader = memo(\n ({ iconName, iconColor, initials, title, subtitle, caption, style, testID }: AvatarHeaderProps) => {\n const colors = useColors();\n const styles = useStyles();\n const allowFontScaling = useAllowFontScaling();\n const fallbackIcon: IconName | undefined = iconName ?? (initials ? undefined : 'user');\n\n return (\n <View style={[styles.container, style]} testID={testID}>\n <View style={styles.avatar} accessibilityElementsHidden importantForAccessibility=\"no-hide-descendants\">\n {fallbackIcon ? (\n <Icon name={fallbackIcon} size={ICON_SIZE} color={iconColor ?? colors.primary} />\n ) : (\n <Text allowFontScaling={allowFontScaling} style={styles.initials}>\n {initials}\n </Text>\n )}\n </View>\n <Text allowFontScaling={allowFontScaling} style={styles.title} accessibilityRole=\"header\">\n {title}\n </Text>\n {typeof subtitle === 'string' ? (\n <Text allowFontScaling={allowFontScaling} style={styles.subtitle}>\n {subtitle}\n </Text>\n ) : (\n subtitle\n )}\n {!!caption && (\n <Text allowFontScaling={allowFontScaling} style={styles.caption}>\n {caption}\n </Text>\n )}\n </View>\n );\n }\n);\nAvatarHeader.displayName = 'AvatarHeader';\n"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"registryDependencies": [
|
package/registry/avatar.json
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
"type": "component",
|
|
5
5
|
"category": "atoms",
|
|
6
6
|
"exportedNames": [
|
|
7
|
-
"Avatar"
|
|
7
|
+
"Avatar",
|
|
8
|
+
"initialsOf"
|
|
8
9
|
],
|
|
9
10
|
"files": [
|
|
10
11
|
{
|
|
11
12
|
"path": "atoms/Avatar.tsx",
|
|
12
|
-
"content": "import React, { memo } from 'react';\nimport { StyleSheet
|
|
13
|
+
"content": "import React, { memo, useState } from 'react';\nimport {\n Image as RNImage,\n StyleSheet,\n Text,\n View,\n type ImageSourcePropType,\n type StyleProp,\n type ViewStyle,\n} from 'react-native';\nimport { useAllowFontScaling, useColors, useTheme } from '../theme/UIProvider';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\n\nexport interface AvatarProps {\n /** Diameter in pt. @default 44 */\n size?: number;\n /** Photo. If it fails to load, falls back to the initials of `name` (or the icon). */\n source?: ImageSourcePropType;\n /** Person's name: produces the initials and the accessible name. */\n name?: string;\n /** Icon shown when there is no photo or name. @default 'user' */\n iconName?: IconName;\n /** Fill of the circle. @default colors.primarySoft */\n backgroundColor?: string;\n /** Color of the initials and icon. @default colors.primaryText */\n color?: string;\n /** Custom content; takes precedence over source/name/iconName. */\n children?: React.ReactNode;\n /** Accessible name. Without it (and without `name`) the avatar is decorative. @default name */\n accessibilityLabel?: string;\n style?: StyleProp<ViewStyle>;\n testID?: string;\n}\n\nconst styles = StyleSheet.create({\n circle: { justifyContent: 'center', alignItems: 'center', overflow: 'hidden' },\n});\n\n/** \"Ada Lovelace\" → \"AL\", \"ada\" → \"A\", \" \" → \"\". Handles characters outside the BMP. */\nexport function initialsOf(name: string): string {\n const words = name.trim().split(/\\s+/).filter(Boolean);\n if (words.length === 0) return '';\n const first = Array.from(words[0] as string)[0] ?? '';\n const last = words.length > 1 ? (Array.from(words[words.length - 1] as string)[0] ?? '') : '';\n return (first + last).toUpperCase();\n}\n\n/**\n * Avatar\n * Circle with a photo, initials or an icon. Used on the left of list rows\n * or as a detail header (alongside AvatarHeader).\n *\n * Priority: `children` → `source` (if it loads) → initials of `name` →\n * `iconName` → `user` icon. A broken photo doesn't leave an empty circle: it\n * falls back to the initials.\n *\n * Gestalt:\n * - Similarity: all avatars in a list share shape and size, so a column of\n * people scans as a series even when it mixes photos, initials and icons.\n * - Closure: the circle clips the photo (overflow hidden); initials and the\n * icon scale with `size` so the content always stays inside.\n *\n * Accessibility: with `name` (or `accessibilityLabel`) it announces itself as\n * an image with that name; with neither it is decorative.\n */\nexport const Avatar = memo(\n ({\n size = 44,\n source,\n name,\n iconName,\n backgroundColor,\n color,\n children,\n accessibilityLabel,\n style,\n testID,\n }: AvatarProps) => {\n const colors = useColors();\n const theme = useTheme();\n const allowFontScaling = useAllowFontScaling();\n const [failed, setFailed] = useState(false);\n const fg = color ?? colors.primaryText;\n const label = accessibilityLabel ?? name;\n const initials = name ? initialsOf(name) : '';\n\n let content: React.ReactNode = children;\n if (content == null) {\n if (source && !failed) {\n content = (\n <RNImage\n source={source}\n style={{ width: size, height: size }}\n onError={() => setFailed(true)}\n accessibilityIgnoresInvertColors\n />\n );\n } else if (initials) {\n content = (\n <Text\n allowFontScaling={allowFontScaling}\n numberOfLines={1}\n style={{ color: fg, fontSize: Math.round(size * 0.4), fontWeight: theme.tokens.fontWeight.semibold }}\n >\n {initials}\n </Text>\n );\n } else {\n content = <Icon name={iconName ?? 'user'} size={Math.round(size * 0.5)} color={fg} />;\n }\n }\n\n return (\n <View\n testID={testID}\n accessible={!!label}\n accessibilityRole={label ? 'image' : undefined}\n accessibilityLabel={label}\n style={[\n styles.circle,\n { width: size, height: size, borderRadius: size / 2, backgroundColor: backgroundColor || colors.primarySoft },\n style,\n ]}\n >\n {content}\n </View>\n );\n }\n);\nAvatar.displayName = 'Avatar';\n"
|
|
13
14
|
}
|
|
14
15
|
],
|
|
15
16
|
"registryDependencies": [
|
package/registry/badge.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "atoms/Badge.tsx",
|
|
12
|
-
"content": "import React, { memo } from 'react';\nimport { Text, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useTheme } from '../theme/UIProvider';\n\nexport type BadgeVariant = 'success' | 'error' | 'warning' | 'info' | 'neutral';\n\nexport interface BadgeProps {\n label: string;\n variant?: BadgeVariant;\n /** Manual override, takes precedence over `variant`. */\n backgroundColor?: string;\n textColor?: string;\n style?: StyleProp<ViewStyle>;\n}\n\nconst useStyles = createStyles((theme) => ({\n badge: {\n paddingHorizontal: theme.tokens.spacing.md
|
|
12
|
+
"content": "import React, { memo } from 'react';\nimport { Text, View, type StyleProp, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useTheme } from '../theme/UIProvider';\n\nexport type BadgeVariant = 'success' | 'error' | 'warning' | 'info' | 'neutral';\n\nexport interface BadgeProps {\n /** Text of the pill, on a single line (truncated with \"…\"). */\n label: string;\n /** Semantic color set: success, error, warning, info or neutral. @default 'neutral' */\n variant?: BadgeVariant;\n /** Manual override, takes precedence over `variant`. */\n backgroundColor?: string;\n /** Manual text color, takes precedence over `variant`. */\n textColor?: string;\n style?: StyleProp<ViewStyle>;\n}\n\nconst useStyles = createStyles((theme) => ({\n badge: {\n paddingHorizontal: theme.tokens.spacing.md,\n paddingVertical: theme.tokens.spacing.xs,\n borderRadius: theme.tokens.radius.full,\n alignSelf: 'flex-start',\n },\n text: { fontSize: theme.tokens.fontSize.xs, fontWeight: theme.tokens.fontWeight.bold, letterSpacing: theme.tokens.letterSpacing.wide },\n}));\n\n/**\n * Badge\n * Status pill. `variant` covers the common cases (success/error/warning/\n * info/neutral); `backgroundColor`/`textColor` are for cases that don't\n * fit the semantic set.\n *\n * Gestalt, similarity: all badges share shape, size and typography; only\n * the color changes, and it changes by meaning (green = ok, red = error…),\n * so a column of statuses scans at a glance. The meaning also goes in the\n * text: never rely on color alone (color blindness).\n * Every `*Soft`/`*Text` pair in the theme meets WCAG AA in light and dark;\n * with custom colors, contrast is up to whoever passes them.\n * A single line: a long label is truncated with \"…\" instead of deforming the pill.\n */\nexport const Badge = memo(({ label, variant = 'neutral', backgroundColor, textColor, style }: BadgeProps) => {\n const theme = useTheme();\n const styles = useStyles();\n const allowFontScaling = useAllowFontScaling();\n const { colors } = theme;\n\n const palette: Record<BadgeVariant, { bg: string; fg: string }> = {\n // fg is the `*Text` role, not the full accent: the accent over its own\n // tint gives ~2.2:1 in light — the label was unreadable.\n success: { bg: colors.successSoft, fg: colors.successText },\n error: { bg: colors.errorSoft, fg: colors.errorText },\n warning: { bg: colors.warningSoft, fg: colors.warningText },\n info: { bg: colors.secondarySoft, fg: colors.secondaryText },\n neutral: { bg: colors.inputBackground, fg: colors.textLight },\n };\n\n return (\n <View style={[styles.badge, { backgroundColor: backgroundColor ?? palette[variant].bg }, style]}>\n <Text allowFontScaling={allowFontScaling} numberOfLines={1} style={[styles.text, { color: textColor ?? palette[variant].fg }]}>\n {label}\n </Text>\n </View>\n );\n});\nBadge.displayName = 'Badge';\n"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"registryDependencies": [
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "organisms/BottomSheet.tsx",
|
|
12
|
-
"content": "import React, { memo, useEffect, useRef, useState } from 'react';\nimport {\n Animated,\n Dimensions,\n Keyboard,\n KeyboardAvoidingView,\n Modal,\n PanResponder,\n Platform,\n Pressable,\n ScrollView,\n View,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useColors, useModalSafeAreaBoundary } from '../theme/UIProvider';\nimport { Title } from '../atoms/Text';\nimport { IconButton } from '../atoms/IconButton';\nimport { Transition } from '../atoms/Transition';\n\nconst { height: SCREEN_HEIGHT } = Dimensions.get('window');\nconst DISMISS_THRESHOLD = 100;\n\nexport type FooterPlacement = 'fixed' | 'scroll' | 'hide-with-keyboard';\n\nexport interface BottomSheetProps {\n visible: boolean;\n onClose: () => void;\n children: React.ReactNode;\n /** With a title, the header appears, with its close button. */\n title?: string;\n /** Sheet actions. Where they live is decided by `footerPlacement`. */\n footer?: React.ReactNode;\n /**\n * 'fixed' anchors it to the bottom of the sheet; 'scroll' leaves it at the\n * end of the content; 'hide-with-keyboard' hides it while typing, which in a\n * long form gives that strip back to the focused field. @default 'fixed'\n */\n footerPlacement?: FooterPlacement;\n /** The content scrolls when it doesn't fit. @default true */\n scrollable?: boolean;\n /** Dragging the sheet down closes it. @default true */\n draggable?: boolean;\n /** Fraction of the screen (0-1) or pixels. @default 0.9 */\n maxHeight?: number;\n closeAccessibilityLabel?: string;\n containerStyle?: ViewStyle;\n contentStyle?: ViewStyle;\n testID?: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { flex: 1, justifyContent: 'flex-end' },\n backdrop: { backgroundColor: theme.colors.overlay },\n sheet: {\n backgroundColor: theme.colors.surface,\n borderTopLeftRadius: theme.tokens.radius.xxl,\n borderTopRightRadius: theme.tokens.radius.xxl,\n ...theme.tokens.shadow.lg,\n },\n handleArea: { paddingVertical: theme.tokens.spacing.md, alignItems: 'center' },\n handle: { width: 40, height: 5, borderRadius: 3, backgroundColor: theme.colors.border },\n header: {\n flexDirection: 'row',\n alignItems: 'center',\n gap: theme.tokens.spacing.md,\n paddingHorizontal: theme.tokens.spacing.xl,\n paddingBottom: theme.tokens.spacing.md,\n },\n headerSpaced: { paddingTop: theme.tokens.spacing.xl },\n title: { flex: 1, fontSize: theme.tokens.fontSize.xl, fontWeight: theme.tokens.fontWeight.bold },\n content: { paddingHorizontal: theme.tokens.spacing.xl },\n scroll: { flexShrink: 1 },\n footer: {\n paddingHorizontal: theme.tokens.spacing.xl,\n paddingTop: theme.tokens.spacing.md,\n borderTopWidth: 1,\n borderTopColor: theme.colors.border,\n },\n footerInline: { paddingTop: theme.tokens.spacing.xl },\n}));\n\nconst absoluteFill: ViewStyle = { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 };\n\n/**\n * BottomSheet\n * Hoja que entra desde abajo, con arrastre para cerrar (PanResponder, no un\n * gesture-handler externo: la librería es cero dependencias). Header, footer y\n * scroll son opcionales, así que cubre tanto la hoja desnuda como el formulario\n * completo.\n */\nexport const BottomSheet = memo(\n ({\n visible,\n onClose,\n children,\n title,\n footer,\n footerPlacement = 'fixed',\n scrollable = true,\n draggable = true,\n maxHeight = 0.9,\n closeAccessibilityLabel = 'Close',\n containerStyle,\n contentStyle,\n testID,\n }: BottomSheetProps) => {\n const styles = useStyles();\n const colors = useColors();\n const ModalSafeAreaBoundary = useModalSafeAreaBoundary();\n const [mounted, setMounted] = useState(visible);\n const [keyboardOpen, setKeyboardOpen] = useState(false);\n const progress = useRef(new Animated.Value(0)).current;\n // El PanResponder de más abajo se crea una sola vez; leer `onClose`\n // directo del closure daría siempre el de ese primer render. El ref\n // refleja la última prop sin importar cuándo se creó la closure.\n const onCloseRef = useRef(onClose);\n onCloseRef.current = onClose;\n\n useEffect(() => {\n const animation = visible\n ? Animated.timing(progress, { toValue: 1, duration: 250, useNativeDriver: true })\n : Animated.timing(progress, { toValue: 0, duration: 200, useNativeDriver: true });\n if (visible) setMounted(true);\n animation.start(({ finished }) => {\n if (!visible && finished) setMounted(false);\n });\n // Si el efecto vuelve a correr (o el componente se desmonta) antes de\n // que termine, se corta: si no, un `finished` tardío podría llamar\n // `setMounted` sobre un componente ya desmontado.\n return () => animation.stop();\n }, [visible, progress]);\n\n useEffect(() => {\n if (footerPlacement !== 'hide-with-keyboard') return;\n const [show, hide] =\n Platform.OS === 'ios' ? ['keyboardWillShow', 'keyboardWillHide'] : ['keyboardDidShow', 'keyboardDidHide'];\n const subs = [\n Keyboard.addListener(show as 'keyboardDidShow', () => setKeyboardOpen(true)),\n Keyboard.addListener(hide as 'keyboardDidHide', () => setKeyboardOpen(false)),\n ];\n return () => subs.forEach((sub) => sub.remove());\n }, [footerPlacement]);\n\n const panResponder = useRef(\n PanResponder.create({\n onMoveShouldSetPanResponder: (_, gesture) => gesture.dy > 5 && Math.abs(gesture.dy) > Math.abs(gesture.dx),\n onPanResponderMove: (_, gesture) => {\n if (gesture.dy > 0) progress.setValue(1 - Math.min(gesture.dy, SCREEN_HEIGHT) / SCREEN_HEIGHT);\n },\n onPanResponderRelease: (_, gesture) => {\n if (gesture.dy > DISMISS_THRESHOLD) {\n onCloseRef.current();\n } else {\n Animated.timing(progress, { toValue: 1, duration: 150, useNativeDriver: true }).start();\n }\n },\n })\n ).current;\n\n if (!mounted) return null;\n\n // Porcentaje y no píxeles: se mide contra el contenedor, que con el teclado\n // abierto es más bajo, así que la hoja entra siempre entera.\n const resolvedMaxHeight: number | `${number}%` = maxHeight <= 1 ? `${maxHeight * 100}%` : maxHeight;\n const hasHeader = !!title;\n\n const scrolledFooter = !!footer && footerPlacement === 'scroll' && (\n <View style={styles.footerInline}>{footer}</View>\n );\n const pinnedFooter =\n !!footer && footerPlacement !== 'scroll' && !(footerPlacement === 'hide-with-keyboard' && keyboardOpen) ? (\n <View style={styles.footer}>{footer}</View>\n ) : null;\n\n const body = scrollable ? (\n <ScrollView\n style={styles.scroll}\n contentContainerStyle={[styles.content, contentStyle]}\n keyboardShouldPersistTaps=\"handled\"\n showsVerticalScrollIndicator={false}\n >\n {children}\n {scrolledFooter}\n </ScrollView>\n ) : (\n <View style={[styles.content, contentStyle]}>\n {children}\n {scrolledFooter}\n </View>\n );\n\n return (\n <Modal visible transparent animationType=\"none\" statusBarTranslucent onRequestClose={onClose}>\n {/* La hoja está anclada abajo, así que el teclado la tapa entera: hay\n que empujarla, no ajustar insets adentro del scroll. Una sola\n compensación, la del contenedor. */}\n <KeyboardAvoidingView\n style={styles.container}\n behavior={Platform.OS === 'ios' ? 'padding' : 'height'}\n >\n <Transition value={progress} preset=\"fade\" style={[absoluteFill, styles.backdrop]}>\n {/* Con header, el botón de cerrar ya expone la acción: repetir el\n label acá deja dos elementos con el mismo nombre. */}\n <Pressable\n style={absoluteFill}\n onPress={onClose}\n testID=\"bottom-sheet-backdrop\"\n accessibilityRole={hasHeader ? undefined : 'button'}\n accessibilityLabel={hasHeader ? undefined : closeAccessibilityLabel}\n />\n </Transition>\n\n {/* Insets medidos dentro de esta ventana nativa (el Modal nativo que\n usa BottomSheet por debajo), no los de la raíz de la app. */}\n <ModalSafeAreaBoundary>\n {(insets) => (\n <Transition\n value={progress}\n preset=\"slide-up\"\n distance={SCREEN_HEIGHT}\n testID={testID}\n style={[\n styles.sheet,\n { maxHeight: resolvedMaxHeight, paddingBottom: Math.max(insets.bottom, 20) },\n containerStyle,\n ]}\n >\n {draggable && (\n <View style={styles.handleArea} testID=\"bottom-sheet-handle\" {...panResponder.panHandlers}>\n <View style={styles.handle} />\n </View>\n )}\n\n {hasHeader && (\n <View style={[styles.header, !draggable && styles.headerSpaced]}>\n <Title style={styles.title} numberOfLines={1}>\n {title}\n </Title>\n <IconButton\n iconName=\"close\"\n size={24}\n color={colors.text}\n onPress={onClose}\n accessibilityLabel={closeAccessibilityLabel}\n />\n </View>\n )}\n\n {body}\n\n {pinnedFooter}\n </Transition>\n )}\n </ModalSafeAreaBoundary>\n </KeyboardAvoidingView>\n </Modal>\n );\n }\n);\nBottomSheet.displayName = 'BottomSheet';\n"
|
|
12
|
+
"content": "import React, { memo, useEffect, useRef, useState } from 'react';\nimport {\n Animated,\n Keyboard,\n KeyboardAvoidingView,\n PanResponder,\n Platform,\n Pressable,\n ScrollView,\n View,\n useWindowDimensions,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useColors, useLabels, useModalSafeAreaBoundary, useTheme } from '../theme/UIProvider';\nimport { useReduceMotion } from '../theme/useReduceMotion';\nimport { KeyboardHandled } from '../theme/KeyboardAvoidance';\nimport { NativeOverlay } from '../theme/NativeOverlay';\nimport { Title } from '../atoms/Text';\nimport { IconButton } from '../atoms/IconButton';\nimport { Transition } from '../atoms/Transition';\n\n/** Drag distance (pt) that closes the sheet on release. */\nconst DISMISS_DISTANCE = 100;\n/** Downward release speed that closes it even after a short drag (a flick). */\nconst DISMISS_VELOCITY = 0.5;\nconst HANDLE_WIDTH = 40;\nconst HANDLE_HEIGHT = 5;\nconst HANDLE_OPACITY = 0.4;\nconst CLOSE_ICON_SIZE = 24;\n\nexport type FooterPlacement = 'fixed' | 'scroll' | 'hide-with-keyboard';\n\nexport interface BottomSheetProps {\n /** Shows or hides the sheet (it slides in and out). */\n visible: boolean;\n /** Called by the backdrop, the close button, the drag and Android back. */\n onClose: () => void;\n /** Sheet content. */\n children: React.ReactNode;\n /** With a title, the header appears, with its close button. */\n title?: string;\n /** Sheet actions. Where they live is decided by `footerPlacement`. */\n footer?: React.ReactNode;\n /**\n * 'fixed' anchors it to the bottom of the sheet; 'scroll' leaves it at the\n * end of the content; 'hide-with-keyboard' hides it while typing, which in a\n * long form gives that strip back to the focused field. @default 'fixed'\n */\n footerPlacement?: FooterPlacement;\n /** The content scrolls when it doesn't fit. @default true */\n scrollable?: boolean;\n /** Dragging the sheet down closes it. @default true */\n draggable?: boolean;\n /** Fraction of the screen (0-1) or pixels. @default 0.9 */\n maxHeight?: number;\n /** Accessible name of the close button and backdrop. @default labels.close */\n closeAccessibilityLabel?: string;\n /** Style of the sheet itself (background, radius). */\n containerStyle?: ViewStyle;\n /** Style of the content area (padding). */\n contentStyle?: ViewStyle;\n /** Draws the sheet inside its parent instead of over the screen (docs, previews). @default false */\n inline?: boolean;\n testID?: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n container: { flex: 1, justifyContent: 'flex-end' },\n backdrop: { backgroundColor: theme.colors.overlay },\n sheet: {\n // Raised: in dark mode the plain surface sank into the dimmed backdrop.\n backgroundColor: theme.colors.surfaceRaised,\n borderTopLeftRadius: theme.tokens.radius.xxl,\n borderTopRightRadius: theme.tokens.radius.xxl,\n ...theme.tokens.shadow.lg,\n },\n handleArea: { paddingVertical: theme.tokens.spacing.md, alignItems: 'center' },\n handle: {\n width: HANDLE_WIDTH,\n height: HANDLE_HEIGHT,\n borderRadius: HANDLE_HEIGHT / 2,\n // textLight, faded: `border` was nearly invisible on a white sheet, and\n // the handle is the only hint that the sheet can be dragged.\n backgroundColor: theme.colors.textLight,\n opacity: HANDLE_OPACITY,\n },\n header: {\n flexDirection: 'row',\n alignItems: 'center',\n gap: theme.tokens.spacing.md,\n paddingHorizontal: theme.tokens.spacing.xl,\n paddingBottom: theme.tokens.spacing.md,\n },\n headerSpaced: { paddingTop: theme.tokens.spacing.xl },\n title: { flex: 1, fontSize: theme.tokens.fontSize.xl, fontWeight: theme.tokens.fontWeight.bold },\n content: { paddingHorizontal: theme.tokens.spacing.xl },\n scroll: { flexShrink: 1 },\n footer: {\n paddingHorizontal: theme.tokens.spacing.xl,\n paddingTop: theme.tokens.spacing.md,\n borderTopWidth: 1,\n borderTopColor: theme.colors.border,\n },\n footerInline: { paddingTop: theme.tokens.spacing.xl },\n}));\n\nconst absoluteFill: ViewStyle = { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 };\n\n/**\n * BottomSheet\n * Sheet that slides in from the bottom, with drag to close (PanResponder, not\n * an external gesture-handler: the library is zero-dependency). Header, footer\n * and scroll are optional, so it covers both the bare sheet and the full\n * form.\n *\n * Gestalt, figure-ground: the backdrop dims the screen, so the sheet is the\n * only figure. Common region: header, content and footer share one panel;\n * the footer's top border separates the actions from the content. Continuity:\n * it slides from the edge it lives on and the drag follows the finger back\n * down; a short flick closes it too. Fitts: content sits in the thumb zone.\n * Reduce Motion: it appears and leaves without sliding.\n */\nexport const BottomSheet = memo(\n ({\n visible,\n onClose,\n children,\n title,\n footer,\n footerPlacement = 'fixed',\n scrollable = true,\n draggable = true,\n maxHeight = 0.9,\n closeAccessibilityLabel,\n containerStyle,\n contentStyle,\n inline = false,\n testID,\n }: BottomSheetProps) => {\n const styles = useStyles();\n const colors = useColors();\n const theme = useTheme();\n const labels = useLabels();\n const reduceMotion = useReduceMotion();\n const { height: screenHeight } = useWindowDimensions();\n const closeLabel = closeAccessibilityLabel ?? labels.close;\n const { duration } = theme.tokens;\n // The PanResponder is created once: it reads these through refs.\n const screenHeightRef = useRef(screenHeight);\n screenHeightRef.current = screenHeight;\n const snapBackMs = useRef(duration.fast);\n snapBackMs.current = reduceMotion ? 0 : duration.fast;\n const ModalSafeAreaBoundary = useModalSafeAreaBoundary();\n const [mounted, setMounted] = useState(visible);\n const [keyboardOpen, setKeyboardOpen] = useState(false);\n const progress = useRef(new Animated.Value(0)).current;\n // The PanResponder further down is created only once; reading `onClose`\n // straight from the closure would always give the one from that first\n // render. The ref reflects the latest prop no matter when the closure was created.\n const onCloseRef = useRef(onClose);\n onCloseRef.current = onClose;\n\n useEffect(() => {\n const animation = Animated.timing(progress, {\n toValue: visible ? 1 : 0,\n duration: reduceMotion ? 0 : visible ? duration.slow : duration.base,\n useNativeDriver: true,\n });\n if (visible) setMounted(true);\n animation.start(({ finished }) => {\n if (!visible && finished) setMounted(false);\n });\n // If the effect runs again (or the component unmounts) before it\n // finishes, it is cut short: otherwise, a late `finished` could call\n // `setMounted` on an already unmounted component.\n return () => animation.stop();\n }, [visible, progress, reduceMotion, duration.slow, duration.base]);\n\n useEffect(() => {\n if (footerPlacement !== 'hide-with-keyboard') return;\n const [show, hide] =\n Platform.OS === 'ios' ? ['keyboardWillShow', 'keyboardWillHide'] : ['keyboardDidShow', 'keyboardDidHide'];\n const subs = [\n Keyboard.addListener(show as 'keyboardDidShow', () => setKeyboardOpen(true)),\n Keyboard.addListener(hide as 'keyboardDidHide', () => setKeyboardOpen(false)),\n ];\n return () => subs.forEach((sub) => sub.remove());\n }, [footerPlacement]);\n\n const panResponder = useRef(\n PanResponder.create({\n onMoveShouldSetPanResponder: (_, gesture) => gesture.dy > 5 && Math.abs(gesture.dy) > Math.abs(gesture.dx),\n onPanResponderMove: (_, gesture) => {\n const h = screenHeightRef.current;\n if (gesture.dy > 0) progress.setValue(1 - Math.min(gesture.dy, h) / h);\n },\n onPanResponderRelease: (_, gesture) => {\n if (gesture.dy > DISMISS_DISTANCE || (gesture.dy > 0 && gesture.vy > DISMISS_VELOCITY)) {\n onCloseRef.current();\n } else {\n Animated.timing(progress, { toValue: 1, duration: snapBackMs.current, useNativeDriver: true }).start();\n }\n },\n })\n ).current;\n\n if (!mounted) return null;\n\n // Percentage, not pixels: it is measured against the container, which is\n // shorter with the keyboard open, so the sheet always fits entirely.\n const resolvedMaxHeight: number | `${number}%` = maxHeight <= 1 ? `${maxHeight * 100}%` : maxHeight;\n const hasHeader = !!title;\n\n const scrolledFooter = !!footer && footerPlacement === 'scroll' && (\n <View style={styles.footerInline}>{footer}</View>\n );\n const pinnedFooter =\n !!footer && footerPlacement !== 'scroll' && !(footerPlacement === 'hide-with-keyboard' && keyboardOpen) ? (\n <View style={styles.footer}>{footer}</View>\n ) : null;\n\n const body = scrollable ? (\n <ScrollView\n style={styles.scroll}\n contentContainerStyle={[styles.content, contentStyle]}\n keyboardShouldPersistTaps=\"handled\"\n showsVerticalScrollIndicator={false}\n >\n {children}\n {scrolledFooter}\n </ScrollView>\n ) : (\n <View style={[styles.content, contentStyle]}>\n {children}\n {scrolledFooter}\n </View>\n );\n\n const tree = (\n <>\n {/* The sheet is anchored at the bottom, so the keyboard covers it\n entirely: it has to be pushed up, not adjusted with insets inside\n the scroll. A single compensation, the container's. */}\n <KeyboardHandled>\n <KeyboardAvoidingView\n style={styles.container}\n behavior={Platform.OS === 'ios' ? 'padding' : 'height'}\n >\n <Transition value={progress} preset=\"fade\" style={[absoluteFill, styles.backdrop]}>\n {/* With a header, the close button already exposes the action:\n repeating the label here leaves two elements with the same name. */}\n <Pressable\n style={absoluteFill}\n onPress={onClose}\n testID=\"bottom-sheet-backdrop\"\n accessibilityRole={hasHeader ? undefined : 'button'}\n accessibilityLabel={hasHeader ? undefined : closeLabel}\n />\n </Transition>\n\n {/* Insets measured inside this native window (the native Modal\n BottomSheet uses underneath), not the app root's. */}\n <ModalSafeAreaBoundary>\n {(insets) => (\n <Transition\n value={progress}\n preset=\"slide-up\"\n distance={screenHeight}\n testID={testID}\n style={[\n styles.sheet,\n { maxHeight: resolvedMaxHeight, paddingBottom: Math.max(insets.bottom, theme.tokens.spacing.xl) },\n containerStyle,\n ]}\n >\n {draggable && (\n // Decorative for screen readers: closing is already exposed by the\n // close button (with a title) or by the backdrop (without one).\n <View\n style={styles.handleArea}\n testID=\"bottom-sheet-handle\"\n accessibilityElementsHidden\n importantForAccessibility=\"no-hide-descendants\"\n {...panResponder.panHandlers}\n >\n <View style={styles.handle} />\n </View>\n )}\n\n {hasHeader && (\n <View style={[styles.header, !draggable && styles.headerSpaced]}>\n <Title style={styles.title} numberOfLines={1}>\n {title}\n </Title>\n <IconButton\n iconName=\"close\"\n size={CLOSE_ICON_SIZE}\n color={colors.text}\n onPress={onClose}\n accessibilityLabel={closeLabel}\n />\n </View>\n )}\n\n {body}\n\n {pinnedFooter}\n </Transition>\n )}\n </ModalSafeAreaBoundary>\n </KeyboardAvoidingView>\n </KeyboardHandled>\n </>\n );\n\n if (inline) {\n return (\n <View style={absoluteFill} pointerEvents=\"box-none\">\n {tree}\n </View>\n );\n }\n\n return (\n // closing: while the exit animation plays, no other overlay layers into it.\n <NativeOverlay closing={!visible} transparent animationType=\"none\" statusBarTranslucent onRequestClose={onClose}>\n {tree}\n </NativeOverlay>\n );\n }\n);\nBottomSheet.displayName = 'BottomSheet';\n"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"registryDependencies": [
|
package/registry/button.json
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "atoms/Button.tsx",
|
|
12
|
-
"content": "import React, { memo, useRef } from 'react';\nimport {\n ActivityIndicator,\n Animated,\n Pressable,\n Text,\n type GestureResponderEvent,\n type StyleProp,\n type TextStyle,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors, useTheme } from '../theme/UIProvider';\nimport { Icon } from '../icons/Icon';\nimport type { IconName } from '../icons/types';\n\nconst AnimatedPressable = Animated.createAnimatedComponent(Pressable);\n\nexport type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'link' | 'destructive';\nexport type ButtonSize = 'sm' | 'md' | 'lg';\n\nexport interface ButtonProps {\n title: string;\n onPress: (e: GestureResponderEvent) => void;\n variant?: ButtonVariant;\n size?: ButtonSize;\n loading?: boolean;\n disabled?: boolean;\n style?: StyleProp<ViewStyle>;\n textStyle?: StyleProp<TextStyle>;\n leftIcon?: React.ReactNode;\n rightIcon?: React.ReactNode;\n leftIconName?: IconName;\n rightIconName?: IconName;\n iconColor?: string;\n testID?: string;\n accessibilityLabel?: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n base: {\n flexDirection: 'row',\n alignItems: 'center',\n justifyContent: 'center',\n borderRadius: theme.tokens.radius.lg,\n borderWidth: 0,\n },\n sm: { paddingVertical: theme.tokens.spacing.sm, paddingHorizontal: theme.tokens.spacing.md },\n md: { paddingVertical: theme.tokens.spacing.lg - 1, paddingHorizontal: theme.tokens.spacing.xl },\n lg: { paddingVertical: theme.tokens.spacing.lg + 2, paddingHorizontal: theme.tokens.spacing.xxl },\n link: { paddingVertical: theme.tokens.spacing.xs, paddingHorizontal: 0, justifyContent: 'flex-start' },\n outline: { borderWidth: 1 },\n filledShadow: { ...theme.tokens.shadow.md, shadowColor: '#000' },\n text: { fontWeight: theme.tokens.fontWeight.semibold, textAlign: 'center' },\n textSm: { fontSize: theme.tokens.fontSize.sm },\n textMd: { fontSize: theme.tokens.fontSize.lg },\n textLg: { fontSize: theme.tokens.fontSize.xl },\n underline: { textDecorationLine: 'underline' },\n iconLeft: { marginRight: theme.tokens.spacing.sm },\n iconRight: { marginLeft: theme.tokens.spacing.sm },\n}));\n\nfunction resolveColors(variant: ButtonVariant, disabled: boolean, colors: ReturnType<typeof useColors>) {\n if (disabled) {\n return { background: colors.inputBackground, text: colors.textLight, border: 'transparent' };\n }\n switch (variant) {\n // Las variantes sin relleno pintan texto sobre el fondo de la pantalla:\n // van con el rol `*Text`. Las rellenas van con `on*` sobre el acento.\n case 'outline':\n return { background: 'transparent', text: colors.primaryText, border: colors.primary };\n case 'ghost':\n return { background: 'transparent', text: colors.primaryText, border: 'transparent' };\n case 'link':\n return { background: 'transparent', text: colors.primaryText, border: 'transparent' };\n case 'secondary':\n return { background: colors.secondary, text: colors.onSecondary, border: 'transparent' };\n case 'destructive':\n return { background: colors.error, text: colors.onError, border: 'transparent' };\n case 'primary':\n default:\n return { background: colors.primary, text: colors.onPrimary, border: 'transparent' };\n }\n}\n\n/**\n * Button\n * Único componente de botón de la librería. `variant` decide el estilo\n * semántico, `size` la densidad. El feedback de \"presionado\" anima solo\n * `transform` (Animated.spring, useNativeDriver) — nunca opacity ni layout.\n */\nexport const Button = memo(\n ({\n title,\n onPress,\n variant = 'primary',\n size = 'md',\n loading = false,\n disabled = false,\n style,\n textStyle,\n leftIcon,\n rightIcon,\n leftIconName,\n rightIconName,\n iconColor,\n testID,\n accessibilityLabel,\n }: ButtonProps) => {\n const styles = useStyles();\n const colors = useColors();\n const theme = useTheme();\n const allowFontScaling = useAllowFontScaling();\n const scale = useRef(new Animated.Value(1)).current;\n const isDisabled = disabled || loading;\n const isFilled = variant === 'primary' || variant === 'secondary' || variant === 'destructive';\n\n const resolved = resolveColors(variant, disabled, colors);\n const effectiveIconColor = iconColor ?? resolved.text;\n\n const animateTo = (value: number) => {\n Animated.spring(scale, { toValue: value, useNativeDriver: true, speed: 50, bounciness: 6 }).start();\n };\n\n const containerStyle: StyleProp<ViewStyle> = [\n styles.base,\n styles[size],\n variant === 'link' && styles.link,\n variant === 'outline' && [styles.outline, { borderColor: resolved.border }],\n { backgroundColor: resolved.background },\n isFilled && !disabled && styles.filledShadow,\n { opacity: isDisabled ? 0.7 : 1 },\n style,\n ];\n\n const labelStyle: StyleProp<TextStyle> = [\n styles.text,\n size === 'sm' ? styles.textSm : size === 'lg' ? styles.textLg : styles.textMd,\n { color: resolved.text },\n variant === 'link' && styles.underline,\n textStyle,\n ];\n\n const iconSize = size === 'sm' ? 16 : size === 'lg' ? 22 : 20;\n\n return (\n // El Pressable ES el contenedor animado, no un wrapper alrededor de uno.\n // Con un Animated.View interno, un `style` de layout (flex, width,\n // margin) caía en el hijo mientras el Pressable — que es quien ocupa\n // lugar en el layout del padre — quedaba sin él: el ancho colapsaba y\n // el label desaparecía (visible en los botones en fila del Alert).\n <AnimatedPressable\n onPress={onPress}\n onPressIn={() => {\n if (!isDisabled) animateTo(0.95);\n }}\n onPressOut={() => {\n if (!isDisabled) animateTo(1);\n }}\n disabled={isDisabled}\n testID={testID}\n accessibilityRole=\"button\"\n accessibilityLabel={accessibilityLabel ?? title}\n accessibilityState={{ disabled: isDisabled, busy: loading }}\n style={[containerStyle, { transform: [{ scale }] }]}\n >\n {loading ? (\n <ActivityIndicator size=\"small\" color={resolved.text} />\n ) : (\n <>\n {leftIcon}\n {!leftIcon && leftIconName && (\n <Icon name={leftIconName} size={iconSize} color={effectiveIconColor} style={styles.iconLeft} />\n )}\n <Text allowFontScaling={allowFontScaling} style={labelStyle}>\n {title}\n </Text>\n {rightIcon}\n {!rightIcon && rightIconName && (\n <Icon name={rightIconName} size={iconSize} color={effectiveIconColor} style={styles.iconRight} />\n )}\n </>\n )}\n </AnimatedPressable>\n );\n }\n);\nButton.displayName = 'Button';\n"
|
|
12
|
+
"content": "import React, { memo, useRef } from 'react';\nimport {\n ActivityIndicator,\n Animated,\n Pressable,\n Text,\n View,\n type Insets,\n type GestureResponderEvent,\n type StyleProp,\n type TextStyle,\n type ViewStyle,\n} from 'react-native';\nimport { createStyles } from '../theme/createStyles';\nimport { useAllowFontScaling, useColors, useTheme } from '../theme/UIProvider';\nimport { Icon } from '../icons/Icon';\nimport { useReduceMotion } from './Transition';\nimport type { IconName } from '../icons/types';\n\nconst AnimatedPressable = Animated.createAnimatedComponent(Pressable);\n\nexport type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'link' | 'destructive';\nexport type ButtonSize = 'sm' | 'md' | 'lg';\n\nexport interface ButtonProps {\n /** Visible label. Also the accessible name unless `accessibilityLabel` is set. */\n title: string;\n /** Called on press. Not called while `disabled` or `loading`. */\n onPress: (e: GestureResponderEvent) => void;\n /**\n * Semantic style: `primary` is the filled focal action; the others step the visual weight down.\n * @default 'primary'\n */\n variant?: ButtonVariant;\n /** Density: padding and font size. `sm` gets extra touch area through `hitSlop`. @default 'md' */\n size?: ButtonSize;\n /** Shows a spinner over the hidden label (the width doesn't change) and blocks presses. @default false */\n loading?: boolean;\n /** Paints the disabled colors and ignores presses. @default false */\n disabled?: boolean;\n style?: StyleProp<ViewStyle>;\n /** Extra style for the label text. */\n textStyle?: StyleProp<TextStyle>;\n /** Custom node to the left of the label. Takes precedence over `leftIconName`. */\n leftIcon?: React.ReactNode;\n /** Custom node to the right of the label. Takes precedence over `rightIconName`. */\n rightIcon?: React.ReactNode;\n /** Icon from the library's set to the left of the label, sized to match `size`. */\n leftIconName?: IconName;\n /** Icon from the library's set to the right of the label, sized to match `size`. */\n rightIconName?: IconName;\n /** Color of `leftIconName`/`rightIconName`. Defaults to the label color of the variant. */\n iconColor?: string;\n testID?: string;\n /** What screen readers announce. @default title */\n accessibilityLabel?: string;\n}\n\nconst useStyles = createStyles((theme) => ({\n base: {\n flexDirection: 'row',\n alignItems: 'center',\n justifyContent: 'center',\n borderRadius: theme.tokens.radius.lg,\n borderWidth: 0,\n },\n sm: { paddingVertical: theme.tokens.spacing.sm, paddingHorizontal: theme.tokens.spacing.md },\n md: { paddingVertical: theme.tokens.spacing.lg, paddingHorizontal: theme.tokens.spacing.xl },\n lg: { paddingVertical: theme.tokens.spacing.xl, paddingHorizontal: theme.tokens.spacing.xxl },\n link: { paddingVertical: theme.tokens.spacing.xs, paddingHorizontal: 0, justifyContent: 'flex-start' },\n outline: { borderWidth: 1 },\n filledShadow: theme.tokens.shadow.md,\n text: { fontWeight: theme.tokens.fontWeight.semibold, textAlign: 'center' },\n textSm: { fontSize: theme.tokens.fontSize.sm },\n textMd: { fontSize: theme.tokens.fontSize.lg },\n textLg: { fontSize: theme.tokens.fontSize.xl },\n underline: { textDecorationLine: 'underline' },\n iconLeft: { marginRight: theme.tokens.spacing.sm },\n iconRight: { marginLeft: theme.tokens.spacing.sm },\n // The label keeps taking its space (invisible) while loading: the button\n // doesn't change width and the spinner is centered on top.\n hidden: { opacity: 0 },\n spinner: { position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, alignItems: 'center', justifyContent: 'center' },\n}));\n\n/**\n * Extra touch area for the sizes that fall below 44pt in height\n * (Fitts): `sm` measures ~30pt and `link` ~27pt. hitSlop enlarges the area\n * that receives the touch without changing how it looks.\n */\nfunction useHitSlop(size: ButtonSize, variant: ButtonVariant): Insets | undefined {\n const theme = useTheme();\n if (variant === 'link') {\n const v = theme.tokens.spacing.md;\n return { top: v, bottom: v, left: theme.tokens.spacing.sm, right: theme.tokens.spacing.sm };\n }\n if (size === 'sm') {\n const v = theme.tokens.spacing.sm;\n return { top: v, bottom: v };\n }\n return undefined;\n}\n\nfunction resolveColors(variant: ButtonVariant, disabled: boolean, colors: ReturnType<typeof useColors>) {\n if (disabled) {\n return { background: colors.inputBackground, text: colors.textLight, border: 'transparent' };\n }\n switch (variant) {\n // The unfilled variants paint text over the screen background: they\n // use the `*Text` role. The filled ones use `on*` over the accent.\n case 'outline':\n return { background: 'transparent', text: colors.primaryText, border: colors.primary };\n case 'ghost':\n return { background: 'transparent', text: colors.primaryText, border: 'transparent' };\n case 'link':\n return { background: 'transparent', text: colors.primaryText, border: 'transparent' };\n case 'secondary':\n return { background: colors.secondary, text: colors.onSecondary, border: 'transparent' };\n case 'destructive':\n return { background: colors.error, text: colors.onError, border: 'transparent' };\n case 'primary':\n default:\n return { background: colors.primary, text: colors.onPrimary, border: 'transparent' };\n }\n}\n\n/**\n * Button\n * The library's only button component. `variant` sets the semantic style,\n * `size` the density. The \"pressed\" feedback animates only `transform`\n * (Animated.spring, useNativeDriver) — never opacity or layout — and is\n * skipped when Reduce Motion is on.\n *\n * Gestalt:\n * - Focal point (Von Restorff): `primary` is the only one filled with the\n * accent and a shadow; there should be one per screen or group. The other\n * variants step the visual weight down (secondary → outline → ghost → link).\n * - Similarity: all variants share shape, radius and typography, so a row of\n * buttons reads as a single group of actions; only what conveys the\n * hierarchy changes (fill, border, color).\n * - Proximity: icon and label sit `spacing.sm` apart, much less than the\n * padding, so they read as a single label.\n *\n * Fitts: `sm` and `link` are less than 44pt tall and make up for it with `hitSlop`.\n * While `loading`, the label is invisible but keeps its space: the button\n * doesn't change width (no layout jumps).\n */\nexport const Button = memo(\n ({\n title,\n onPress,\n variant = 'primary',\n size = 'md',\n loading = false,\n disabled = false,\n style,\n textStyle,\n leftIcon,\n rightIcon,\n leftIconName,\n rightIconName,\n iconColor,\n testID,\n accessibilityLabel,\n }: ButtonProps) => {\n const styles = useStyles();\n const colors = useColors();\n const theme = useTheme();\n const allowFontScaling = useAllowFontScaling();\n const scale = useRef(new Animated.Value(1)).current;\n const reduceMotion = useReduceMotion();\n const hitSlop = useHitSlop(size, variant);\n const isDisabled = disabled || loading;\n const isFilled = variant === 'primary' || variant === 'secondary' || variant === 'destructive';\n\n const resolved = resolveColors(variant, disabled, colors);\n const effectiveIconColor = iconColor ?? resolved.text;\n\n const animateTo = (value: number) => {\n if (reduceMotion) return;\n Animated.spring(scale, { toValue: value, useNativeDriver: true, speed: 50, bounciness: 6 }).start();\n };\n\n const containerStyle: StyleProp<ViewStyle> = [\n styles.base,\n styles[size],\n variant === 'link' && styles.link,\n variant === 'outline' && [styles.outline, { borderColor: resolved.border }],\n { backgroundColor: resolved.background },\n isFilled && !disabled && styles.filledShadow,\n // Disabled is already marked by its colors (resolveColors); lowering the\n // opacity on top of that left it almost invisible (figure and ground).\n // Only `loading` dims it, to signal that the button is busy.\n { opacity: loading && !disabled ? 0.7 : 1 },\n style,\n ];\n\n const labelStyle: StyleProp<TextStyle> = [\n styles.text,\n size === 'sm' ? styles.textSm : size === 'lg' ? styles.textLg : styles.textMd,\n { color: resolved.text },\n variant === 'link' && styles.underline,\n textStyle,\n ];\n\n const iconSize = size === 'sm' ? 16 : size === 'lg' ? 22 : 20;\n\n return (\n // The Pressable IS the animated container, not a wrapper around one.\n // With an inner Animated.View, a layout `style` (flex, width, margin)\n // landed on the child while the Pressable — which is what takes up\n // space in the parent's layout — was left without it: the width\n // collapsed and the label disappeared (visible in the Alert's row of buttons).\n <AnimatedPressable\n onPress={onPress}\n onPressIn={() => {\n if (!isDisabled) animateTo(0.95);\n }}\n onPressOut={() => {\n if (!isDisabled) animateTo(1);\n }}\n disabled={isDisabled}\n hitSlop={hitSlop}\n testID={testID}\n accessibilityRole=\"button\"\n accessibilityLabel={accessibilityLabel ?? title}\n accessibilityState={{ disabled: isDisabled, busy: loading }}\n style={[containerStyle, { transform: [{ scale }] }]}\n >\n {leftIcon ? <View style={[styles.iconLeft, loading && styles.hidden]}>{leftIcon}</View> : null}\n {!leftIcon && leftIconName && (\n <Icon\n name={leftIconName}\n size={iconSize}\n color={effectiveIconColor}\n style={[styles.iconLeft, loading && styles.hidden]}\n />\n )}\n <Text allowFontScaling={allowFontScaling} style={[labelStyle, loading && styles.hidden]}>\n {title}\n </Text>\n {rightIcon ? <View style={[styles.iconRight, loading && styles.hidden]}>{rightIcon}</View> : null}\n {!rightIcon && rightIconName && (\n <Icon\n name={rightIconName}\n size={iconSize}\n color={effectiveIconColor}\n style={[styles.iconRight, loading && styles.hidden]}\n />\n )}\n {loading && (\n <View style={styles.spinner} pointerEvents=\"none\">\n <ActivityIndicator size=\"small\" color={resolved.text} />\n </View>\n )}\n </AnimatedPressable>\n );\n }\n);\nButton.displayName = 'Button';\n"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"registryDependencies": [
|
|
16
|
-
"core"
|
|
16
|
+
"core",
|
|
17
|
+
"transition"
|
|
17
18
|
]
|
|
18
19
|
}
|
package/registry/card.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "atoms/Card.tsx",
|
|
12
|
-
"content": "import React, { memo } from 'react';\nimport { View, type ViewProps, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\n\nexport interface CardProps extends ViewProps {\n children: React.ReactNode;\n style?: ViewStyle
|
|
12
|
+
"content": "import React, { memo } from 'react';\nimport { View, type StyleProp, type ViewProps, type ViewStyle } from 'react-native';\nimport { createStyles } from '../theme/createStyles';\n\nexport interface CardProps extends ViewProps {\n /** Content of the card, inside its padding, background and border. */\n children: React.ReactNode;\n style?: StyleProp<ViewStyle>;\n}\n\nconst CARD_BORDER_DARK = 1;\n\nconst useStyles = createStyles((theme) => ({\n card: {\n // surfaceRaised: lighter than surface in dark, so a card on a sheet or\n // on another card still reads as a separate tile (figure and ground).\n backgroundColor: theme.colors.surfaceRaised,\n borderRadius: theme.tokens.radius.xl,\n padding: theme.tokens.spacing.lg,\n ...theme.tokens.shadow.sm,\n // In dark the shadow isn't visible and surface barely stands out from the\n // background (or not at all, on another surface): a 1pt border restores\n // the outline (figure and ground). A hairline was invisible.\n ...(theme.scheme === 'dark' && { borderWidth: CARD_BORDER_DARK, borderColor: theme.colors.border }),\n },\n}));\n\n/**\n * Card\n * Surface container: groups related content.\n *\n * Gestalt, common region: everything inside the same background and border\n * reads as a group, even when other elements are nearby. Figure and ground:\n * in light the shadow sets it apart; in dark (where the shadow isn't visible)\n * a thin border does. The inner padding (`spacing.lg`) must be larger than the\n * spacing between the elements inside, so the content reads as one block.\n */\n\nexport const Card = memo(({ children, style, ...props }: CardProps) => {\n const styles = useStyles();\n return (\n <View style={[styles.card, style]} {...props}>\n {children}\n </View>\n );\n});\nCard.displayName = 'Card';\n"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"registryDependencies": [
|