@boostdev/design-system-react-native 0.2.1 → 0.2.2

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/dist/index.cjs CHANGED
@@ -2682,24 +2682,36 @@ function FileInput({
2682
2682
  var import_react9 = require("react");
2683
2683
  var import_react_native37 = require("react-native");
2684
2684
  var import_jsx_runtime37 = require("react/jsx-runtime");
2685
+ var INPUT_HEIGHT = 44;
2685
2686
  var MAX_DROPDOWN_HEIGHT = 200;
2686
2687
  var styles33 = import_react_native37.StyleSheet.create({
2687
2688
  container: {
2688
2689
  gap: import_native.spacing.xxs
2689
2690
  },
2691
+ /**
2692
+ * Draws the pill border and provides the fixed 44pt touch-target height.
2693
+ * justifyContent: 'center' vertically centers the inner TextInput.
2694
+ * position: 'relative' is required for the dropdown's absolute positioning.
2695
+ */
2690
2696
  inputWrapper: {
2691
- position: "relative"
2697
+ position: "relative",
2698
+ height: INPUT_HEIGHT,
2699
+ justifyContent: "center",
2700
+ borderWidth: 1,
2701
+ borderRadius: import_native.border.radius.full
2692
2702
  },
2703
+ /**
2704
+ * No vertical padding: iOS centers text automatically inside the wrapper height.
2705
+ * textAlignVertical + includeFontPadding provide Android parity.
2706
+ * No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
2707
+ */
2693
2708
  input: {
2694
2709
  fontFamily: import_native.font.family.body,
2695
2710
  fontSize: import_native.font.size.body,
2696
- // No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
2697
- // lineHeight is set explicitly. minHeight: 44 satisfies the touch-target requirement.
2698
- paddingVertical: import_native.spacing.xs,
2699
2711
  paddingHorizontal: import_native.spacing.s,
2700
- borderWidth: 1,
2701
- borderRadius: import_native.border.radius.full,
2702
- minHeight: 44
2712
+ paddingVertical: 0,
2713
+ textAlignVertical: "center",
2714
+ includeFontPadding: false
2703
2715
  },
2704
2716
  dropdown: {
2705
2717
  position: "absolute",
@@ -2768,80 +2780,88 @@ function Combobox({
2768
2780
  const borderColor = error ? colors2.colorError : colors2.colorOnBg;
2769
2781
  return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_react_native37.View, { style: [styles33.container, style], children: [
2770
2782
  /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(Label, { label }),
2771
- /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_react_native37.View, { style: styles33.inputWrapper, children: [
2772
- /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2773
- import_react_native37.TextInput,
2774
- {
2775
- value: inputValue,
2776
- onChangeText: handleChangeText,
2777
- onFocus: handleFocus,
2778
- onBlur: handleBlur,
2779
- placeholder,
2780
- placeholderTextColor: colors2.colorOnBgSubtle,
2781
- editable: !disabled,
2782
- accessibilityRole: "combobox",
2783
- accessibilityLabel: typeof label === "string" ? label : name,
2784
- accessibilityState: { disabled, expanded: isOpen },
2785
- style: [
2786
- styles33.input,
2787
- {
2788
- borderColor,
2789
- color: colors2.colorOnBg,
2790
- backgroundColor: colors2.colorBg
2791
- },
2792
- disabled && { opacity: 0.4 }
2793
- ]
2794
- }
2795
- ),
2796
- isOpen && filtered.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2797
- import_react_native37.ScrollView,
2798
- {
2799
- style: [
2800
- styles33.dropdown,
2783
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
2784
+ import_react_native37.View,
2785
+ {
2786
+ style: [
2787
+ styles33.inputWrapper,
2788
+ {
2789
+ borderColor,
2790
+ backgroundColor: colors2.colorBg
2791
+ },
2792
+ disabled && { opacity: 0.4 }
2793
+ ],
2794
+ children: [
2795
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2796
+ import_react_native37.TextInput,
2801
2797
  {
2802
- borderColor: colors2.colorBgSubtle,
2803
- backgroundColor: colors2.colorBg
2798
+ value: inputValue,
2799
+ onChangeText: handleChangeText,
2800
+ onFocus: handleFocus,
2801
+ onBlur: handleBlur,
2802
+ placeholder,
2803
+ placeholderTextColor: colors2.colorOnBgSubtle,
2804
+ editable: !disabled,
2805
+ accessibilityRole: "combobox",
2806
+ accessibilityLabel: typeof label === "string" ? label : name,
2807
+ accessibilityState: { disabled, expanded: isOpen },
2808
+ style: [
2809
+ styles33.input,
2810
+ { color: colors2.colorOnBg }
2811
+ ]
2804
2812
  }
2805
- ],
2806
- keyboardShouldPersistTaps: "handled",
2807
- nestedScrollEnabled: true,
2808
- children: filtered.map((option) => {
2809
- const isSelected = option.value === value;
2810
- const isDisabled = option.disabled;
2811
- return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2812
- import_react_native37.Pressable,
2813
- {
2814
- onPress: () => !isDisabled && selectOption(option),
2815
- disabled: isDisabled,
2816
- accessibilityRole: "menuitem",
2817
- accessibilityLabel: option.label,
2818
- accessibilityState: { selected: isSelected, disabled: isDisabled },
2819
- style: ({ pressed }) => [
2820
- styles33.option,
2821
- isSelected && { backgroundColor: colors2.colorBlueSubtle },
2822
- pressed && !isDisabled && { backgroundColor: colors2.colorBgSubtle },
2823
- isDisabled && { opacity: 0.4 }
2824
- ],
2825
- children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2826
- import_react_native37.Text,
2813
+ ),
2814
+ isOpen && filtered.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2815
+ import_react_native37.ScrollView,
2816
+ {
2817
+ style: [
2818
+ styles33.dropdown,
2819
+ {
2820
+ borderColor: colors2.colorBgSubtle,
2821
+ backgroundColor: colors2.colorBg
2822
+ }
2823
+ ],
2824
+ keyboardShouldPersistTaps: "handled",
2825
+ nestedScrollEnabled: true,
2826
+ children: filtered.map((option) => {
2827
+ const isSelected = option.value === value;
2828
+ const isDisabled = option.disabled;
2829
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2830
+ import_react_native37.Pressable,
2827
2831
  {
2828
- style: [
2829
- styles33.optionText,
2830
- {
2831
- color: isSelected ? colors2.colorInteractive : colors2.colorOnBg
2832
- },
2833
- isSelected && { fontWeight: import_native.font.weight.semibold }
2832
+ onPress: () => !isDisabled && selectOption(option),
2833
+ disabled: isDisabled,
2834
+ accessibilityRole: "menuitem",
2835
+ accessibilityLabel: option.label,
2836
+ accessibilityState: { selected: isSelected, disabled: isDisabled },
2837
+ style: ({ pressed }) => [
2838
+ styles33.option,
2839
+ isSelected && { backgroundColor: colors2.colorBlueSubtle },
2840
+ pressed && !isDisabled && { backgroundColor: colors2.colorBgSubtle },
2841
+ isDisabled && { opacity: 0.4 }
2834
2842
  ],
2835
- children: option.label
2836
- }
2837
- )
2838
- },
2839
- option.value
2840
- );
2841
- })
2842
- }
2843
- )
2844
- ] }),
2843
+ children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
2844
+ import_react_native37.Text,
2845
+ {
2846
+ style: [
2847
+ styles33.optionText,
2848
+ {
2849
+ color: isSelected ? colors2.colorInteractive : colors2.colorOnBg
2850
+ },
2851
+ isSelected && { fontWeight: import_native.font.weight.semibold }
2852
+ ],
2853
+ children: option.label
2854
+ }
2855
+ )
2856
+ },
2857
+ option.value
2858
+ );
2859
+ })
2860
+ }
2861
+ )
2862
+ ]
2863
+ }
2864
+ ),
2845
2865
  /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(Message, { type: "error", message: error }),
2846
2866
  /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(Message, { type: "hint", message: hint })
2847
2867
  ] });
@@ -2929,6 +2949,7 @@ function SegmentedControl({
2929
2949
  var import_react11 = require("react");
2930
2950
  var import_react_native39 = require("react-native");
2931
2951
  var import_jsx_runtime39 = require("react/jsx-runtime");
2952
+ var INPUT_HEIGHT2 = 44;
2932
2953
  var keyboardTypeMap = {
2933
2954
  text: "default",
2934
2955
  email: "email-address",
@@ -2942,16 +2963,28 @@ var styles35 = import_react_native39.StyleSheet.create({
2942
2963
  container: {
2943
2964
  gap: import_native.spacing.xxs
2944
2965
  },
2966
+ /**
2967
+ * The wrapper draws the pill border and provides the fixed 44pt touch-target height.
2968
+ * justifyContent: 'center' vertically centers the inner TextInput on both iOS and Android.
2969
+ */
2970
+ inputWrapper: {
2971
+ height: INPUT_HEIGHT2,
2972
+ justifyContent: "center",
2973
+ borderWidth: 1,
2974
+ borderRadius: import_native.border.radius.full
2975
+ },
2976
+ /**
2977
+ * No vertical padding: iOS centers text automatically inside the wrapper height.
2978
+ * textAlignVertical + includeFontPadding are Android-specific but harmless on iOS.
2979
+ * No lineHeight: explicit lineHeight on a single-line TextInput clips ascenders/descenders on iOS.
2980
+ */
2945
2981
  input: {
2946
2982
  fontFamily: import_native.font.family.body,
2947
2983
  fontSize: import_native.font.size.body,
2948
- // No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
2949
- // lineHeight is set explicitly. minHeight: 44 satisfies the touch-target requirement.
2950
2984
  paddingHorizontal: import_native.spacing.s,
2951
- paddingVertical: import_native.spacing.xs,
2952
- borderWidth: 1,
2953
- borderRadius: import_native.border.radius.full,
2954
- minHeight: 44
2985
+ paddingVertical: 0,
2986
+ textAlignVertical: "center",
2987
+ includeFontPadding: false
2955
2988
  }
2956
2989
  });
2957
2990
  function FormInput({
@@ -2975,38 +3008,46 @@ function FormInput({
2975
3008
  return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_react_native39.View, { style: [styles35.container, style], children: [
2976
3009
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Label, { label }),
2977
3010
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2978
- import_react_native39.TextInput,
3011
+ import_react_native39.View,
2979
3012
  {
2980
- accessibilityLabel: typeof label === "string" ? label : name,
2981
- accessibilityState: { disabled },
2982
- accessibilityHint: hint,
2983
- value,
2984
- defaultValue,
2985
- placeholder,
2986
- placeholderTextColor: colors2.colorOnBgSubtle,
2987
- keyboardType: keyboardTypeMap[type],
2988
- secureTextEntry: type === "password",
2989
- autoCapitalize: type === "email" || type === "url" ? "none" : "sentences",
2990
- autoComplete: type === "email" ? "email" : type === "tel" ? "tel" : void 0,
2991
- editable: !disabled,
2992
- onChangeText: onChange,
2993
- onFocus: () => {
2994
- setFocused(true);
2995
- onFocus?.();
2996
- },
2997
- onBlur: () => {
2998
- setFocused(false);
2999
- onBlur?.();
3000
- },
3001
3013
  style: [
3002
- styles35.input,
3014
+ styles35.inputWrapper,
3003
3015
  {
3004
3016
  borderColor,
3005
- color: colors2.colorOnBg,
3006
3017
  backgroundColor: colors2.colorBg
3007
3018
  },
3008
3019
  disabled && { opacity: 0.4 }
3009
- ]
3020
+ ],
3021
+ children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
3022
+ import_react_native39.TextInput,
3023
+ {
3024
+ accessibilityLabel: typeof label === "string" ? label : name,
3025
+ accessibilityState: { disabled },
3026
+ accessibilityHint: hint,
3027
+ value,
3028
+ defaultValue,
3029
+ placeholder,
3030
+ placeholderTextColor: colors2.colorOnBgSubtle,
3031
+ keyboardType: keyboardTypeMap[type],
3032
+ secureTextEntry: type === "password",
3033
+ autoCapitalize: type === "email" || type === "url" ? "none" : "sentences",
3034
+ autoComplete: type === "email" ? "email" : type === "tel" ? "tel" : void 0,
3035
+ editable: !disabled,
3036
+ onChangeText: onChange,
3037
+ onFocus: () => {
3038
+ setFocused(true);
3039
+ onFocus?.();
3040
+ },
3041
+ onBlur: () => {
3042
+ setFocused(false);
3043
+ onBlur?.();
3044
+ },
3045
+ style: [
3046
+ styles35.input,
3047
+ { color: colors2.colorOnBg }
3048
+ ]
3049
+ }
3050
+ )
3010
3051
  }
3011
3052
  ),
3012
3053
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Message, { type: "error", message: error }),
@@ -3502,18 +3543,30 @@ var styles39 = import_react_native43.StyleSheet.create({
3502
3543
  fontWeight: import_native.font.weight.semibold,
3503
3544
  lineHeight: 24
3504
3545
  },
3505
- input: {
3546
+ /**
3547
+ * The inputWrapper holds the border and fixed height so justifyContent: 'center'
3548
+ * vertically centers the TextInput. flex: 1 lets it fill the space between steppers.
3549
+ */
3550
+ inputWrapper: {
3506
3551
  flex: 1,
3552
+ height: STEPPER_SIZE,
3553
+ justifyContent: "center",
3554
+ borderTopWidth: 1,
3555
+ borderBottomWidth: 1
3556
+ },
3557
+ /**
3558
+ * No vertical padding: iOS auto-centers within the wrapper height.
3559
+ * textAlignVertical + includeFontPadding provide Android parity.
3560
+ * No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
3561
+ */
3562
+ input: {
3507
3563
  fontFamily: import_native.font.family.body,
3508
3564
  fontSize: import_native.font.size.body,
3509
- // No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
3510
- // lineHeight is set explicitly. minHeight: STEPPER_SIZE (44) covers the touch target.
3511
3565
  textAlign: "center",
3512
3566
  paddingHorizontal: import_native.spacing.xs,
3513
- paddingVertical: import_native.spacing.xs,
3514
- borderTopWidth: 1,
3515
- borderBottomWidth: 1,
3516
- minHeight: STEPPER_SIZE
3567
+ paddingVertical: 0,
3568
+ textAlignVertical: "center",
3569
+ includeFontPadding: false
3517
3570
  }
3518
3571
  });
3519
3572
  function NumberInput({
@@ -3579,32 +3632,40 @@ function NumberInput({
3579
3632
  }
3580
3633
  ),
3581
3634
  /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3582
- import_react_native43.TextInput,
3635
+ import_react_native43.View,
3583
3636
  {
3584
- accessibilityLabel: label,
3585
- accessibilityState: { disabled },
3586
- accessibilityValue: {
3587
- min,
3588
- max,
3589
- now: currentValue,
3590
- text: String(currentValue)
3591
- },
3592
- keyboardType: "numeric",
3593
- value: String(currentValue),
3594
- editable: !disabled,
3595
- onChangeText: (text) => {
3596
- const parsed = parseFloat(text);
3597
- const safe = isNaN(parsed) ? 0 : parsed;
3598
- update(safe);
3599
- },
3600
3637
  style: [
3601
- styles39.input,
3638
+ styles39.inputWrapper,
3602
3639
  {
3603
3640
  borderColor,
3604
- color: colors2.colorOnBg,
3605
3641
  backgroundColor: colors2.colorBg
3606
3642
  }
3607
- ]
3643
+ ],
3644
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3645
+ import_react_native43.TextInput,
3646
+ {
3647
+ accessibilityLabel: label,
3648
+ accessibilityState: { disabled },
3649
+ accessibilityValue: {
3650
+ min,
3651
+ max,
3652
+ now: currentValue,
3653
+ text: String(currentValue)
3654
+ },
3655
+ keyboardType: "numeric",
3656
+ value: String(currentValue),
3657
+ editable: !disabled,
3658
+ onChangeText: (text) => {
3659
+ const parsed = parseFloat(text);
3660
+ const safe = isNaN(parsed) ? 0 : parsed;
3661
+ update(safe);
3662
+ },
3663
+ style: [
3664
+ styles39.input,
3665
+ { color: colors2.colorOnBg }
3666
+ ]
3667
+ }
3668
+ )
3608
3669
  }
3609
3670
  ),
3610
3671
  /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
package/dist/index.js CHANGED
@@ -2635,24 +2635,36 @@ import {
2635
2635
  ScrollView as ScrollView4
2636
2636
  } from "react-native";
2637
2637
  import { jsx as jsx37, jsxs as jsxs23 } from "react/jsx-runtime";
2638
+ var INPUT_HEIGHT = 44;
2638
2639
  var MAX_DROPDOWN_HEIGHT = 200;
2639
2640
  var styles33 = StyleSheet33.create({
2640
2641
  container: {
2641
2642
  gap: spacing.xxs
2642
2643
  },
2644
+ /**
2645
+ * Draws the pill border and provides the fixed 44pt touch-target height.
2646
+ * justifyContent: 'center' vertically centers the inner TextInput.
2647
+ * position: 'relative' is required for the dropdown's absolute positioning.
2648
+ */
2643
2649
  inputWrapper: {
2644
- position: "relative"
2650
+ position: "relative",
2651
+ height: INPUT_HEIGHT,
2652
+ justifyContent: "center",
2653
+ borderWidth: 1,
2654
+ borderRadius: border.radius.full
2645
2655
  },
2656
+ /**
2657
+ * No vertical padding: iOS centers text automatically inside the wrapper height.
2658
+ * textAlignVertical + includeFontPadding provide Android parity.
2659
+ * No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
2660
+ */
2646
2661
  input: {
2647
2662
  fontFamily: font.family.body,
2648
2663
  fontSize: font.size.body,
2649
- // No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
2650
- // lineHeight is set explicitly. minHeight: 44 satisfies the touch-target requirement.
2651
- paddingVertical: spacing.xs,
2652
2664
  paddingHorizontal: spacing.s,
2653
- borderWidth: 1,
2654
- borderRadius: border.radius.full,
2655
- minHeight: 44
2665
+ paddingVertical: 0,
2666
+ textAlignVertical: "center",
2667
+ includeFontPadding: false
2656
2668
  },
2657
2669
  dropdown: {
2658
2670
  position: "absolute",
@@ -2721,80 +2733,88 @@ function Combobox({
2721
2733
  const borderColor = error ? colors2.colorError : colors2.colorOnBg;
2722
2734
  return /* @__PURE__ */ jsxs23(View30, { style: [styles33.container, style], children: [
2723
2735
  /* @__PURE__ */ jsx37(Label, { label }),
2724
- /* @__PURE__ */ jsxs23(View30, { style: styles33.inputWrapper, children: [
2725
- /* @__PURE__ */ jsx37(
2726
- TextInput,
2727
- {
2728
- value: inputValue,
2729
- onChangeText: handleChangeText,
2730
- onFocus: handleFocus,
2731
- onBlur: handleBlur,
2732
- placeholder,
2733
- placeholderTextColor: colors2.colorOnBgSubtle,
2734
- editable: !disabled,
2735
- accessibilityRole: "combobox",
2736
- accessibilityLabel: typeof label === "string" ? label : name,
2737
- accessibilityState: { disabled, expanded: isOpen },
2738
- style: [
2739
- styles33.input,
2740
- {
2741
- borderColor,
2742
- color: colors2.colorOnBg,
2743
- backgroundColor: colors2.colorBg
2744
- },
2745
- disabled && { opacity: 0.4 }
2746
- ]
2747
- }
2748
- ),
2749
- isOpen && filtered.length > 0 && /* @__PURE__ */ jsx37(
2750
- ScrollView4,
2751
- {
2752
- style: [
2753
- styles33.dropdown,
2736
+ /* @__PURE__ */ jsxs23(
2737
+ View30,
2738
+ {
2739
+ style: [
2740
+ styles33.inputWrapper,
2741
+ {
2742
+ borderColor,
2743
+ backgroundColor: colors2.colorBg
2744
+ },
2745
+ disabled && { opacity: 0.4 }
2746
+ ],
2747
+ children: [
2748
+ /* @__PURE__ */ jsx37(
2749
+ TextInput,
2754
2750
  {
2755
- borderColor: colors2.colorBgSubtle,
2756
- backgroundColor: colors2.colorBg
2751
+ value: inputValue,
2752
+ onChangeText: handleChangeText,
2753
+ onFocus: handleFocus,
2754
+ onBlur: handleBlur,
2755
+ placeholder,
2756
+ placeholderTextColor: colors2.colorOnBgSubtle,
2757
+ editable: !disabled,
2758
+ accessibilityRole: "combobox",
2759
+ accessibilityLabel: typeof label === "string" ? label : name,
2760
+ accessibilityState: { disabled, expanded: isOpen },
2761
+ style: [
2762
+ styles33.input,
2763
+ { color: colors2.colorOnBg }
2764
+ ]
2757
2765
  }
2758
- ],
2759
- keyboardShouldPersistTaps: "handled",
2760
- nestedScrollEnabled: true,
2761
- children: filtered.map((option) => {
2762
- const isSelected = option.value === value;
2763
- const isDisabled = option.disabled;
2764
- return /* @__PURE__ */ jsx37(
2765
- Pressable18,
2766
- {
2767
- onPress: () => !isDisabled && selectOption(option),
2768
- disabled: isDisabled,
2769
- accessibilityRole: "menuitem",
2770
- accessibilityLabel: option.label,
2771
- accessibilityState: { selected: isSelected, disabled: isDisabled },
2772
- style: ({ pressed }) => [
2773
- styles33.option,
2774
- isSelected && { backgroundColor: colors2.colorBlueSubtle },
2775
- pressed && !isDisabled && { backgroundColor: colors2.colorBgSubtle },
2776
- isDisabled && { opacity: 0.4 }
2777
- ],
2778
- children: /* @__PURE__ */ jsx37(
2779
- Text27,
2766
+ ),
2767
+ isOpen && filtered.length > 0 && /* @__PURE__ */ jsx37(
2768
+ ScrollView4,
2769
+ {
2770
+ style: [
2771
+ styles33.dropdown,
2772
+ {
2773
+ borderColor: colors2.colorBgSubtle,
2774
+ backgroundColor: colors2.colorBg
2775
+ }
2776
+ ],
2777
+ keyboardShouldPersistTaps: "handled",
2778
+ nestedScrollEnabled: true,
2779
+ children: filtered.map((option) => {
2780
+ const isSelected = option.value === value;
2781
+ const isDisabled = option.disabled;
2782
+ return /* @__PURE__ */ jsx37(
2783
+ Pressable18,
2780
2784
  {
2781
- style: [
2782
- styles33.optionText,
2783
- {
2784
- color: isSelected ? colors2.colorInteractive : colors2.colorOnBg
2785
- },
2786
- isSelected && { fontWeight: font.weight.semibold }
2785
+ onPress: () => !isDisabled && selectOption(option),
2786
+ disabled: isDisabled,
2787
+ accessibilityRole: "menuitem",
2788
+ accessibilityLabel: option.label,
2789
+ accessibilityState: { selected: isSelected, disabled: isDisabled },
2790
+ style: ({ pressed }) => [
2791
+ styles33.option,
2792
+ isSelected && { backgroundColor: colors2.colorBlueSubtle },
2793
+ pressed && !isDisabled && { backgroundColor: colors2.colorBgSubtle },
2794
+ isDisabled && { opacity: 0.4 }
2787
2795
  ],
2788
- children: option.label
2789
- }
2790
- )
2791
- },
2792
- option.value
2793
- );
2794
- })
2795
- }
2796
- )
2797
- ] }),
2796
+ children: /* @__PURE__ */ jsx37(
2797
+ Text27,
2798
+ {
2799
+ style: [
2800
+ styles33.optionText,
2801
+ {
2802
+ color: isSelected ? colors2.colorInteractive : colors2.colorOnBg
2803
+ },
2804
+ isSelected && { fontWeight: font.weight.semibold }
2805
+ ],
2806
+ children: option.label
2807
+ }
2808
+ )
2809
+ },
2810
+ option.value
2811
+ );
2812
+ })
2813
+ }
2814
+ )
2815
+ ]
2816
+ }
2817
+ ),
2798
2818
  /* @__PURE__ */ jsx37(Message, { type: "error", message: error }),
2799
2819
  /* @__PURE__ */ jsx37(Message, { type: "hint", message: hint })
2800
2820
  ] });
@@ -2886,6 +2906,7 @@ import {
2886
2906
  TextInput as TextInput2
2887
2907
  } from "react-native";
2888
2908
  import { jsx as jsx39, jsxs as jsxs24 } from "react/jsx-runtime";
2909
+ var INPUT_HEIGHT2 = 44;
2889
2910
  var keyboardTypeMap = {
2890
2911
  text: "default",
2891
2912
  email: "email-address",
@@ -2899,16 +2920,28 @@ var styles35 = StyleSheet35.create({
2899
2920
  container: {
2900
2921
  gap: spacing.xxs
2901
2922
  },
2923
+ /**
2924
+ * The wrapper draws the pill border and provides the fixed 44pt touch-target height.
2925
+ * justifyContent: 'center' vertically centers the inner TextInput on both iOS and Android.
2926
+ */
2927
+ inputWrapper: {
2928
+ height: INPUT_HEIGHT2,
2929
+ justifyContent: "center",
2930
+ borderWidth: 1,
2931
+ borderRadius: border.radius.full
2932
+ },
2933
+ /**
2934
+ * No vertical padding: iOS centers text automatically inside the wrapper height.
2935
+ * textAlignVertical + includeFontPadding are Android-specific but harmless on iOS.
2936
+ * No lineHeight: explicit lineHeight on a single-line TextInput clips ascenders/descenders on iOS.
2937
+ */
2902
2938
  input: {
2903
2939
  fontFamily: font.family.body,
2904
2940
  fontSize: font.size.body,
2905
- // No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
2906
- // lineHeight is set explicitly. minHeight: 44 satisfies the touch-target requirement.
2907
2941
  paddingHorizontal: spacing.s,
2908
- paddingVertical: spacing.xs,
2909
- borderWidth: 1,
2910
- borderRadius: border.radius.full,
2911
- minHeight: 44
2942
+ paddingVertical: 0,
2943
+ textAlignVertical: "center",
2944
+ includeFontPadding: false
2912
2945
  }
2913
2946
  });
2914
2947
  function FormInput({
@@ -2932,38 +2965,46 @@ function FormInput({
2932
2965
  return /* @__PURE__ */ jsxs24(View32, { style: [styles35.container, style], children: [
2933
2966
  /* @__PURE__ */ jsx39(Label, { label }),
2934
2967
  /* @__PURE__ */ jsx39(
2935
- TextInput2,
2968
+ View32,
2936
2969
  {
2937
- accessibilityLabel: typeof label === "string" ? label : name,
2938
- accessibilityState: { disabled },
2939
- accessibilityHint: hint,
2940
- value,
2941
- defaultValue,
2942
- placeholder,
2943
- placeholderTextColor: colors2.colorOnBgSubtle,
2944
- keyboardType: keyboardTypeMap[type],
2945
- secureTextEntry: type === "password",
2946
- autoCapitalize: type === "email" || type === "url" ? "none" : "sentences",
2947
- autoComplete: type === "email" ? "email" : type === "tel" ? "tel" : void 0,
2948
- editable: !disabled,
2949
- onChangeText: onChange,
2950
- onFocus: () => {
2951
- setFocused(true);
2952
- onFocus?.();
2953
- },
2954
- onBlur: () => {
2955
- setFocused(false);
2956
- onBlur?.();
2957
- },
2958
2970
  style: [
2959
- styles35.input,
2971
+ styles35.inputWrapper,
2960
2972
  {
2961
2973
  borderColor,
2962
- color: colors2.colorOnBg,
2963
2974
  backgroundColor: colors2.colorBg
2964
2975
  },
2965
2976
  disabled && { opacity: 0.4 }
2966
- ]
2977
+ ],
2978
+ children: /* @__PURE__ */ jsx39(
2979
+ TextInput2,
2980
+ {
2981
+ accessibilityLabel: typeof label === "string" ? label : name,
2982
+ accessibilityState: { disabled },
2983
+ accessibilityHint: hint,
2984
+ value,
2985
+ defaultValue,
2986
+ placeholder,
2987
+ placeholderTextColor: colors2.colorOnBgSubtle,
2988
+ keyboardType: keyboardTypeMap[type],
2989
+ secureTextEntry: type === "password",
2990
+ autoCapitalize: type === "email" || type === "url" ? "none" : "sentences",
2991
+ autoComplete: type === "email" ? "email" : type === "tel" ? "tel" : void 0,
2992
+ editable: !disabled,
2993
+ onChangeText: onChange,
2994
+ onFocus: () => {
2995
+ setFocused(true);
2996
+ onFocus?.();
2997
+ },
2998
+ onBlur: () => {
2999
+ setFocused(false);
3000
+ onBlur?.();
3001
+ },
3002
+ style: [
3003
+ styles35.input,
3004
+ { color: colors2.colorOnBg }
3005
+ ]
3006
+ }
3007
+ )
2967
3008
  }
2968
3009
  ),
2969
3010
  /* @__PURE__ */ jsx39(Message, { type: "error", message: error }),
@@ -3481,18 +3522,30 @@ var styles39 = StyleSheet39.create({
3481
3522
  fontWeight: font.weight.semibold,
3482
3523
  lineHeight: 24
3483
3524
  },
3484
- input: {
3525
+ /**
3526
+ * The inputWrapper holds the border and fixed height so justifyContent: 'center'
3527
+ * vertically centers the TextInput. flex: 1 lets it fill the space between steppers.
3528
+ */
3529
+ inputWrapper: {
3485
3530
  flex: 1,
3531
+ height: STEPPER_SIZE,
3532
+ justifyContent: "center",
3533
+ borderTopWidth: 1,
3534
+ borderBottomWidth: 1
3535
+ },
3536
+ /**
3537
+ * No vertical padding: iOS auto-centers within the wrapper height.
3538
+ * textAlignVertical + includeFontPadding provide Android parity.
3539
+ * No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
3540
+ */
3541
+ input: {
3486
3542
  fontFamily: font.family.body,
3487
3543
  fontSize: font.size.body,
3488
- // No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
3489
- // lineHeight is set explicitly. minHeight: STEPPER_SIZE (44) covers the touch target.
3490
3544
  textAlign: "center",
3491
3545
  paddingHorizontal: spacing.xs,
3492
- paddingVertical: spacing.xs,
3493
- borderTopWidth: 1,
3494
- borderBottomWidth: 1,
3495
- minHeight: STEPPER_SIZE
3546
+ paddingVertical: 0,
3547
+ textAlignVertical: "center",
3548
+ includeFontPadding: false
3496
3549
  }
3497
3550
  });
3498
3551
  function NumberInput({
@@ -3558,32 +3611,40 @@ function NumberInput({
3558
3611
  }
3559
3612
  ),
3560
3613
  /* @__PURE__ */ jsx43(
3561
- TextInput4,
3614
+ View36,
3562
3615
  {
3563
- accessibilityLabel: label,
3564
- accessibilityState: { disabled },
3565
- accessibilityValue: {
3566
- min,
3567
- max,
3568
- now: currentValue,
3569
- text: String(currentValue)
3570
- },
3571
- keyboardType: "numeric",
3572
- value: String(currentValue),
3573
- editable: !disabled,
3574
- onChangeText: (text) => {
3575
- const parsed = parseFloat(text);
3576
- const safe = isNaN(parsed) ? 0 : parsed;
3577
- update(safe);
3578
- },
3579
3616
  style: [
3580
- styles39.input,
3617
+ styles39.inputWrapper,
3581
3618
  {
3582
3619
  borderColor,
3583
- color: colors2.colorOnBg,
3584
3620
  backgroundColor: colors2.colorBg
3585
3621
  }
3586
- ]
3622
+ ],
3623
+ children: /* @__PURE__ */ jsx43(
3624
+ TextInput4,
3625
+ {
3626
+ accessibilityLabel: label,
3627
+ accessibilityState: { disabled },
3628
+ accessibilityValue: {
3629
+ min,
3630
+ max,
3631
+ now: currentValue,
3632
+ text: String(currentValue)
3633
+ },
3634
+ keyboardType: "numeric",
3635
+ value: String(currentValue),
3636
+ editable: !disabled,
3637
+ onChangeText: (text) => {
3638
+ const parsed = parseFloat(text);
3639
+ const safe = isNaN(parsed) ? 0 : parsed;
3640
+ update(safe);
3641
+ },
3642
+ style: [
3643
+ styles39.input,
3644
+ { color: colors2.colorOnBg }
3645
+ ]
3646
+ }
3647
+ )
3587
3648
  }
3588
3649
  ),
3589
3650
  /* @__PURE__ */ jsx43(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boostdev/design-system-react-native",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "BoostDev React Native component library: accessible, token-driven components built on @boostdev/design-system-foundation",
5
5
  "keywords": [
6
6
  "react-native",
@@ -57,4 +57,32 @@ describe('Combobox (native)', () => {
57
57
  );
58
58
  expect(screen.getByText('Type to filter.')).toBeInTheDocument();
59
59
  });
60
+
61
+ /**
62
+ * Vertical-centering invariant: the TextInput must have no vertical padding so that
63
+ * iOS centers text automatically within the wrapper height.
64
+ */
65
+ it('has paddingVertical: 0 on the TextInput (vertical centering)', () => {
66
+ renderWithTheme(
67
+ <Combobox label="Fruit" name="fruit" options={fruits} />,
68
+ );
69
+ const input = screen.getByRole('combobox');
70
+ const style = getComputedStyle(input);
71
+ expect(parseFloat(style.paddingTop)).toBe(0);
72
+ expect(parseFloat(style.paddingBottom)).toBe(0);
73
+ });
74
+
75
+ /**
76
+ * Touch-target invariant: the inputWrapper View that draws the pill border must
77
+ * provide the 44pt touch-target height.
78
+ */
79
+ it('has height of 44pt on the pill wrapper (touch target)', () => {
80
+ renderWithTheme(
81
+ <Combobox label="Fruit" name="fruit" options={fruits} />,
82
+ );
83
+ const input = screen.getByRole('combobox');
84
+ const wrapper = input.parentElement as HTMLElement;
85
+ const heightPx = parseFloat(getComputedStyle(wrapper).height);
86
+ expect(heightPx).toBeGreaterThanOrEqual(44);
87
+ });
60
88
  });
@@ -10,6 +10,9 @@ import {
10
10
  ViewStyle,
11
11
  } from 'react-native';
12
12
  import { spacing, font, border, lineHeight } from '../../../../native/tokens'; // lineHeight used for optionText (Text, not TextInput)
13
+
14
+ /** Fixed height for single-line control fields (satisfies 44pt touch target). */
15
+ const INPUT_HEIGHT = 44;
13
16
  import { useTheme } from '../../../../native/ThemeContext';
14
17
  import { Label } from '../atoms/Label';
15
18
  import { Message } from '../atoms/Message';
@@ -39,19 +42,30 @@ const styles = StyleSheet.create({
39
42
  container: {
40
43
  gap: spacing.xxs,
41
44
  },
45
+ /**
46
+ * Draws the pill border and provides the fixed 44pt touch-target height.
47
+ * justifyContent: 'center' vertically centers the inner TextInput.
48
+ * position: 'relative' is required for the dropdown's absolute positioning.
49
+ */
42
50
  inputWrapper: {
43
51
  position: 'relative',
52
+ height: INPUT_HEIGHT,
53
+ justifyContent: 'center',
54
+ borderWidth: 1,
55
+ borderRadius: border.radius.full,
44
56
  },
57
+ /**
58
+ * No vertical padding: iOS centers text automatically inside the wrapper height.
59
+ * textAlignVertical + includeFontPadding provide Android parity.
60
+ * No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
61
+ */
45
62
  input: {
46
63
  fontFamily: font.family.body,
47
64
  fontSize: font.size.body,
48
- // No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
49
- // lineHeight is set explicitly. minHeight: 44 satisfies the touch-target requirement.
50
- paddingVertical: spacing.xs,
51
65
  paddingHorizontal: spacing.s,
52
- borderWidth: 1,
53
- borderRadius: border.radius.full,
54
- minHeight: 44,
66
+ paddingVertical: 0,
67
+ textAlignVertical: 'center',
68
+ includeFontPadding: false,
55
69
  },
56
70
  dropdown: {
57
71
  position: 'absolute',
@@ -133,7 +147,16 @@ export function Combobox({
133
147
  return (
134
148
  <View style={[styles.container, style]}>
135
149
  <Label label={label} />
136
- <View style={styles.inputWrapper}>
150
+ <View
151
+ style={[
152
+ styles.inputWrapper,
153
+ {
154
+ borderColor,
155
+ backgroundColor: colors.colorBg,
156
+ },
157
+ disabled && { opacity: 0.4 },
158
+ ]}
159
+ >
137
160
  <TextInput
138
161
  value={inputValue}
139
162
  onChangeText={handleChangeText}
@@ -147,12 +170,7 @@ export function Combobox({
147
170
  accessibilityState={{ disabled, expanded: isOpen }}
148
171
  style={[
149
172
  styles.input,
150
- {
151
- borderColor,
152
- color: colors.colorOnBg,
153
- backgroundColor: colors.colorBg,
154
- },
155
- disabled && { opacity: 0.4 },
173
+ { color: colors.colorOnBg },
156
174
  ]}
157
175
  />
158
176
  {isOpen && filtered.length > 0 && (
@@ -54,8 +54,8 @@ describe('FormInput (native)', () => {
54
54
  *
55
55
  * On iOS, an explicit lineHeight on a single-line TextInput causes the text to
56
56
  * render vertically clipped — descenders and ascenders are cut off at the line box
57
- * boundary. The minHeight: 44 token satisfies the touch-target requirement without
58
- * needing lineHeight. This test pins that invariant so it can't silently regress.
57
+ * boundary. The wrapper height + justifyContent: 'center' satisfies vertical centering
58
+ * without needing lineHeight. This test pins that invariant so it can't silently regress.
59
59
  *
60
60
  * react-native-web applies token-based styles as atomic CSS classes. getComputedStyle
61
61
  * resolves those classes in jsdom and returns "normal" when lineHeight is absent.
@@ -68,14 +68,31 @@ describe('FormInput (native)', () => {
68
68
  });
69
69
 
70
70
  /**
71
- * Touch-target invariant: single-line input must meet the 44pt minimum touch target.
72
- * This is the replacement constraint that makes lineHeight safe to omit.
71
+ * Vertical-centering invariant: the TextInput must have no vertical padding so that
72
+ * iOS centers text automatically within the wrapper height. A non-zero paddingVertical
73
+ * shifts text off-center because iOS adds its own default internal top padding on top.
73
74
  */
74
- it('has minHeight of at least 44pt on the TextInput', () => {
75
+ it('has paddingVertical: 0 on the TextInput (vertical centering)', () => {
75
76
  renderWithTheme(<FormInput label="Email" name="email" />);
76
77
  const input = screen.getByRole('textbox');
77
- // getComputedStyle resolves the atomic CSS class to a pixel value.
78
- const minHeightPx = parseFloat(getComputedStyle(input).minHeight);
79
- expect(minHeightPx).toBeGreaterThanOrEqual(44);
78
+ const style = getComputedStyle(input);
79
+ expect(parseFloat(style.paddingTop)).toBe(0);
80
+ expect(parseFloat(style.paddingBottom)).toBe(0);
81
+ });
82
+
83
+ /**
84
+ * Touch-target invariant: the wrapper View that draws the pill border must provide
85
+ * the 44pt touch-target height (replacing the minHeight that was previously on the
86
+ * TextInput itself).
87
+ */
88
+ it('has height of 44pt on the pill wrapper (touch target)', () => {
89
+ const { container } = renderWithTheme(<FormInput label="Email" name="email" />);
90
+ // The wrapper is the first div that contains the input in react-native-web's output.
91
+ const input = screen.getByRole('textbox');
92
+ const wrapper = input.parentElement as HTMLElement;
93
+ const heightPx = parseFloat(getComputedStyle(wrapper).height);
94
+ expect(heightPx).toBeGreaterThanOrEqual(44);
95
+ // Suppress unused-variable warning from destructuring
96
+ void container;
80
97
  });
81
98
  });
@@ -12,6 +12,9 @@ import { useTheme } from '../../../../native/ThemeContext';
12
12
  import { Label } from '../atoms/Label';
13
13
  import { Message } from '../atoms/Message';
14
14
 
15
+ /** Fixed height for single-line control fields (satisfies 44pt touch target). */
16
+ const INPUT_HEIGHT = 44;
17
+
15
18
  type InputType = 'text' | 'email' | 'password' | 'tel' | 'url' | 'number' | 'search';
16
19
 
17
20
  interface FormInputProps {
@@ -44,16 +47,28 @@ const styles = StyleSheet.create({
44
47
  container: {
45
48
  gap: spacing.xxs,
46
49
  },
50
+ /**
51
+ * The wrapper draws the pill border and provides the fixed 44pt touch-target height.
52
+ * justifyContent: 'center' vertically centers the inner TextInput on both iOS and Android.
53
+ */
54
+ inputWrapper: {
55
+ height: INPUT_HEIGHT,
56
+ justifyContent: 'center',
57
+ borderWidth: 1,
58
+ borderRadius: border.radius.full,
59
+ },
60
+ /**
61
+ * No vertical padding: iOS centers text automatically inside the wrapper height.
62
+ * textAlignVertical + includeFontPadding are Android-specific but harmless on iOS.
63
+ * No lineHeight: explicit lineHeight on a single-line TextInput clips ascenders/descenders on iOS.
64
+ */
47
65
  input: {
48
66
  fontFamily: font.family.body,
49
67
  fontSize: font.size.body,
50
- // No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
51
- // lineHeight is set explicitly. minHeight: 44 satisfies the touch-target requirement.
52
68
  paddingHorizontal: spacing.s,
53
- paddingVertical: spacing.xs,
54
- borderWidth: 1,
55
- borderRadius: border.radius.full,
56
- minHeight: 44,
69
+ paddingVertical: 0,
70
+ textAlignVertical: 'center',
71
+ includeFontPadding: false,
57
72
  },
58
73
  });
59
74
 
@@ -84,38 +99,44 @@ export function FormInput({
84
99
  return (
85
100
  <View style={[styles.container, style]}>
86
101
  <Label label={label} />
87
- <TextInput
88
- accessibilityLabel={typeof label === 'string' ? label : name}
89
- accessibilityState={{ disabled }}
90
- accessibilityHint={hint}
91
- value={value}
92
- defaultValue={defaultValue}
93
- placeholder={placeholder}
94
- placeholderTextColor={colors.colorOnBgSubtle}
95
- keyboardType={keyboardTypeMap[type]}
96
- secureTextEntry={type === 'password'}
97
- autoCapitalize={type === 'email' || type === 'url' ? 'none' : 'sentences'}
98
- autoComplete={type === 'email' ? 'email' : type === 'tel' ? 'tel' : undefined}
99
- editable={!disabled}
100
- onChangeText={onChange}
101
- onFocus={() => {
102
- setFocused(true);
103
- onFocus?.();
104
- }}
105
- onBlur={() => {
106
- setFocused(false);
107
- onBlur?.();
108
- }}
102
+ <View
109
103
  style={[
110
- styles.input,
104
+ styles.inputWrapper,
111
105
  {
112
106
  borderColor,
113
- color: colors.colorOnBg,
114
107
  backgroundColor: colors.colorBg,
115
108
  },
116
109
  disabled && { opacity: 0.4 },
117
110
  ]}
118
- />
111
+ >
112
+ <TextInput
113
+ accessibilityLabel={typeof label === 'string' ? label : name}
114
+ accessibilityState={{ disabled }}
115
+ accessibilityHint={hint}
116
+ value={value}
117
+ defaultValue={defaultValue}
118
+ placeholder={placeholder}
119
+ placeholderTextColor={colors.colorOnBgSubtle}
120
+ keyboardType={keyboardTypeMap[type]}
121
+ secureTextEntry={type === 'password'}
122
+ autoCapitalize={type === 'email' || type === 'url' ? 'none' : 'sentences'}
123
+ autoComplete={type === 'email' ? 'email' : type === 'tel' ? 'tel' : undefined}
124
+ editable={!disabled}
125
+ onChangeText={onChange}
126
+ onFocus={() => {
127
+ setFocused(true);
128
+ onFocus?.();
129
+ }}
130
+ onBlur={() => {
131
+ setFocused(false);
132
+ onBlur?.();
133
+ }}
134
+ style={[
135
+ styles.input,
136
+ { color: colors.colorOnBg },
137
+ ]}
138
+ />
139
+ </View>
119
140
  <Message type="error" message={error} />
120
141
  <Message type="hint" message={hint} />
121
142
  </View>
@@ -60,4 +60,28 @@ describe('NumberInput (native)', () => {
60
60
  expect(screen.getByRole('button', { name: 'Decrease' })).toBeDisabled();
61
61
  expect(screen.getByRole('button', { name: 'Increase' })).toBeDisabled();
62
62
  });
63
+
64
+ /**
65
+ * Vertical-centering invariant: the TextInput must have no vertical padding so that
66
+ * iOS centers text automatically within the wrapper height.
67
+ */
68
+ it('has paddingVertical: 0 on the TextInput (vertical centering)', () => {
69
+ renderWithTheme(<NumberInput label="Quantity" name="qty" value={3} />);
70
+ const input = screen.getByDisplayValue('3');
71
+ const style = getComputedStyle(input);
72
+ expect(parseFloat(style.paddingTop)).toBe(0);
73
+ expect(parseFloat(style.paddingBottom)).toBe(0);
74
+ });
75
+
76
+ /**
77
+ * Touch-target invariant: the wrapper View around the TextInput must provide
78
+ * the 44pt height (stepper height), replacing minHeight on the TextInput itself.
79
+ */
80
+ it('has height of 44pt on the TextInput wrapper (touch target)', () => {
81
+ renderWithTheme(<NumberInput label="Quantity" name="qty" value={3} />);
82
+ const input = screen.getByDisplayValue('3');
83
+ const wrapper = input.parentElement as HTMLElement;
84
+ const heightPx = parseFloat(getComputedStyle(wrapper).height);
85
+ expect(heightPx).toBeGreaterThanOrEqual(44);
86
+ });
63
87
  });
@@ -60,18 +60,30 @@ const styles = StyleSheet.create({
60
60
  fontWeight: font.weight.semibold,
61
61
  lineHeight: 24,
62
62
  },
63
- input: {
63
+ /**
64
+ * The inputWrapper holds the border and fixed height so justifyContent: 'center'
65
+ * vertically centers the TextInput. flex: 1 lets it fill the space between steppers.
66
+ */
67
+ inputWrapper: {
64
68
  flex: 1,
69
+ height: STEPPER_SIZE,
70
+ justifyContent: 'center',
71
+ borderTopWidth: 1,
72
+ borderBottomWidth: 1,
73
+ },
74
+ /**
75
+ * No vertical padding: iOS auto-centers within the wrapper height.
76
+ * textAlignVertical + includeFontPadding provide Android parity.
77
+ * No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
78
+ */
79
+ input: {
65
80
  fontFamily: font.family.body,
66
81
  fontSize: font.size.body,
67
- // No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
68
- // lineHeight is set explicitly. minHeight: STEPPER_SIZE (44) covers the touch target.
69
82
  textAlign: 'center',
70
83
  paddingHorizontal: spacing.xs,
71
- paddingVertical: spacing.xs,
72
- borderTopWidth: 1,
73
- borderBottomWidth: 1,
74
- minHeight: STEPPER_SIZE,
84
+ paddingVertical: 0,
85
+ textAlignVertical: 'center',
86
+ includeFontPadding: false,
75
87
  },
76
88
  });
77
89
 
@@ -140,32 +152,38 @@ export function NumberInput({
140
152
  </Text>
141
153
  </Pressable>
142
154
 
143
- <TextInput
144
- accessibilityLabel={label}
145
- accessibilityState={{ disabled }}
146
- accessibilityValue={{
147
- min,
148
- max,
149
- now: currentValue,
150
- text: String(currentValue),
151
- }}
152
- keyboardType="numeric"
153
- value={String(currentValue)}
154
- editable={!disabled}
155
- onChangeText={(text) => {
156
- const parsed = parseFloat(text);
157
- const safe = isNaN(parsed) ? 0 : parsed;
158
- update(safe);
159
- }}
155
+ <View
160
156
  style={[
161
- styles.input,
157
+ styles.inputWrapper,
162
158
  {
163
159
  borderColor,
164
- color: colors.colorOnBg,
165
160
  backgroundColor: colors.colorBg,
166
161
  },
167
162
  ]}
168
- />
163
+ >
164
+ <TextInput
165
+ accessibilityLabel={label}
166
+ accessibilityState={{ disabled }}
167
+ accessibilityValue={{
168
+ min,
169
+ max,
170
+ now: currentValue,
171
+ text: String(currentValue),
172
+ }}
173
+ keyboardType="numeric"
174
+ value={String(currentValue)}
175
+ editable={!disabled}
176
+ onChangeText={(text) => {
177
+ const parsed = parseFloat(text);
178
+ const safe = isNaN(parsed) ? 0 : parsed;
179
+ update(safe);
180
+ }}
181
+ style={[
182
+ styles.input,
183
+ { color: colors.colorOnBg },
184
+ ]}
185
+ />
186
+ </View>
169
187
 
170
188
  <Pressable
171
189
  accessibilityRole="button"