@boostdev/design-system-react-native 0.2.3 → 0.2.4

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/CHANGELOG.md CHANGED
@@ -1,6 +1,22 @@
1
1
  # Changelog
2
2
 
3
3
  All notable changes to `@boostdev/design-system-react-native` are documented here.
4
+ ## [0.2.4] - 2026-06-11
5
+ ### Fixed
6
+ - Fix(form): optical vertical centering for single-line input text
7
+
8
+ Arial's asymmetric metrics (ascent 0.905em, descent 0.212em) place ink
9
+ ~0.08em below the layout-box centre on iOS. Add OPTICAL_TEXT_SHIFT = -1.5
10
+ constant to form/atoms/inputHelpers.ts (single source of truth) and apply
11
+ transform: [{translateY: OPTICAL_TEXT_SHIFT}] to the single-line TextInput
12
+ in FormInput, NumberInput, and Combobox. Verified on-simulator: shifts ink
13
+ from +1.33pt to -0.33pt at fontSize 16.
14
+
15
+ Textarea (multiline) is unchanged.
16
+
17
+ Spec invariants extended: pin translateY shift alongside existing
18
+ no-lineHeight, paddingVertical-0, and wrapper-height-≥44 assertions. ([`a3e08e5`](https://gitlab.com/b00stdev/boostdev-design-system-react-native/-/commit/a3e08e5a6f4c53ef12ba810ef96d395e4ee116e7))
19
+
4
20
  ## [0.2.3] - 2026-06-11
5
21
  ### Fixed
6
22
  - Fix(segmented-control): proper track + thumb rendering
package/dist/index.cjs CHANGED
@@ -2681,6 +2681,11 @@ function FileInput({
2681
2681
  // src/components/interaction/form/Combobox/Combobox.tsx
2682
2682
  var import_react9 = require("react");
2683
2683
  var import_react_native37 = require("react-native");
2684
+
2685
+ // src/components/interaction/form/atoms/inputHelpers.ts
2686
+ var OPTICAL_TEXT_SHIFT = -1.5;
2687
+
2688
+ // src/components/interaction/form/Combobox/Combobox.tsx
2684
2689
  var import_jsx_runtime37 = require("react/jsx-runtime");
2685
2690
  var INPUT_HEIGHT = 44;
2686
2691
  var MAX_DROPDOWN_HEIGHT = 200;
@@ -2704,6 +2709,7 @@ var styles33 = import_react_native37.StyleSheet.create({
2704
2709
  * No vertical padding: iOS centers text automatically inside the wrapper height.
2705
2710
  * textAlignVertical + includeFontPadding provide Android parity.
2706
2711
  * No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
2712
+ * transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
2707
2713
  */
2708
2714
  input: {
2709
2715
  fontFamily: import_native.font.family.body,
@@ -2711,7 +2717,8 @@ var styles33 = import_react_native37.StyleSheet.create({
2711
2717
  paddingHorizontal: import_native.spacing.s,
2712
2718
  paddingVertical: 0,
2713
2719
  textAlignVertical: "center",
2714
- includeFontPadding: false
2720
+ includeFontPadding: false,
2721
+ transform: [{ translateY: OPTICAL_TEXT_SHIFT }]
2715
2722
  },
2716
2723
  dropdown: {
2717
2724
  position: "absolute",
@@ -2990,6 +2997,7 @@ var styles35 = import_react_native39.StyleSheet.create({
2990
2997
  * No vertical padding: iOS centers text automatically inside the wrapper height.
2991
2998
  * textAlignVertical + includeFontPadding are Android-specific but harmless on iOS.
2992
2999
  * No lineHeight: explicit lineHeight on a single-line TextInput clips ascenders/descenders on iOS.
3000
+ * transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
2993
3001
  */
2994
3002
  input: {
2995
3003
  fontFamily: import_native.font.family.body,
@@ -2997,7 +3005,8 @@ var styles35 = import_react_native39.StyleSheet.create({
2997
3005
  paddingHorizontal: import_native.spacing.s,
2998
3006
  paddingVertical: 0,
2999
3007
  textAlignVertical: "center",
3000
- includeFontPadding: false
3008
+ includeFontPadding: false,
3009
+ transform: [{ translateY: OPTICAL_TEXT_SHIFT }]
3001
3010
  }
3002
3011
  });
3003
3012
  function FormInput({
@@ -3571,6 +3580,7 @@ var styles39 = import_react_native43.StyleSheet.create({
3571
3580
  * No vertical padding: iOS auto-centers within the wrapper height.
3572
3581
  * textAlignVertical + includeFontPadding provide Android parity.
3573
3582
  * No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
3583
+ * transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
3574
3584
  */
3575
3585
  input: {
3576
3586
  fontFamily: import_native.font.family.body,
@@ -3579,7 +3589,8 @@ var styles39 = import_react_native43.StyleSheet.create({
3579
3589
  paddingHorizontal: import_native.spacing.xs,
3580
3590
  paddingVertical: 0,
3581
3591
  textAlignVertical: "center",
3582
- includeFontPadding: false
3592
+ includeFontPadding: false,
3593
+ transform: [{ translateY: OPTICAL_TEXT_SHIFT }]
3583
3594
  }
3584
3595
  });
3585
3596
  function NumberInput({
package/dist/index.js CHANGED
@@ -2634,6 +2634,11 @@ import {
2634
2634
  Text as Text27,
2635
2635
  ScrollView as ScrollView4
2636
2636
  } from "react-native";
2637
+
2638
+ // src/components/interaction/form/atoms/inputHelpers.ts
2639
+ var OPTICAL_TEXT_SHIFT = -1.5;
2640
+
2641
+ // src/components/interaction/form/Combobox/Combobox.tsx
2637
2642
  import { jsx as jsx37, jsxs as jsxs23 } from "react/jsx-runtime";
2638
2643
  var INPUT_HEIGHT = 44;
2639
2644
  var MAX_DROPDOWN_HEIGHT = 200;
@@ -2657,6 +2662,7 @@ var styles33 = StyleSheet33.create({
2657
2662
  * No vertical padding: iOS centers text automatically inside the wrapper height.
2658
2663
  * textAlignVertical + includeFontPadding provide Android parity.
2659
2664
  * No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
2665
+ * transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
2660
2666
  */
2661
2667
  input: {
2662
2668
  fontFamily: font.family.body,
@@ -2664,7 +2670,8 @@ var styles33 = StyleSheet33.create({
2664
2670
  paddingHorizontal: spacing.s,
2665
2671
  paddingVertical: 0,
2666
2672
  textAlignVertical: "center",
2667
- includeFontPadding: false
2673
+ includeFontPadding: false,
2674
+ transform: [{ translateY: OPTICAL_TEXT_SHIFT }]
2668
2675
  },
2669
2676
  dropdown: {
2670
2677
  position: "absolute",
@@ -2947,6 +2954,7 @@ var styles35 = StyleSheet35.create({
2947
2954
  * No vertical padding: iOS centers text automatically inside the wrapper height.
2948
2955
  * textAlignVertical + includeFontPadding are Android-specific but harmless on iOS.
2949
2956
  * No lineHeight: explicit lineHeight on a single-line TextInput clips ascenders/descenders on iOS.
2957
+ * transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
2950
2958
  */
2951
2959
  input: {
2952
2960
  fontFamily: font.family.body,
@@ -2954,7 +2962,8 @@ var styles35 = StyleSheet35.create({
2954
2962
  paddingHorizontal: spacing.s,
2955
2963
  paddingVertical: 0,
2956
2964
  textAlignVertical: "center",
2957
- includeFontPadding: false
2965
+ includeFontPadding: false,
2966
+ transform: [{ translateY: OPTICAL_TEXT_SHIFT }]
2958
2967
  }
2959
2968
  });
2960
2969
  function FormInput({
@@ -3550,6 +3559,7 @@ var styles39 = StyleSheet39.create({
3550
3559
  * No vertical padding: iOS auto-centers within the wrapper height.
3551
3560
  * textAlignVertical + includeFontPadding provide Android parity.
3552
3561
  * No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
3562
+ * transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
3553
3563
  */
3554
3564
  input: {
3555
3565
  fontFamily: font.family.body,
@@ -3558,7 +3568,8 @@ var styles39 = StyleSheet39.create({
3558
3568
  paddingHorizontal: spacing.xs,
3559
3569
  paddingVertical: 0,
3560
3570
  textAlignVertical: "center",
3561
- includeFontPadding: false
3571
+ includeFontPadding: false,
3572
+ transform: [{ translateY: OPTICAL_TEXT_SHIFT }]
3562
3573
  }
3563
3574
  });
3564
3575
  function NumberInput({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boostdev/design-system-react-native",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
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",
@@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react';
2
2
  import { ThemeProvider } from '../../../../native/ThemeContext';
3
3
  import { Combobox } from './Combobox';
4
4
  import type { ComboboxOption } from './Combobox';
5
+ import { OPTICAL_TEXT_SHIFT } from '../atoms/inputHelpers';
5
6
 
6
7
  function renderWithTheme(ui: React.ReactElement) {
7
8
  return render(<ThemeProvider>{ui}</ThemeProvider>);
@@ -85,4 +86,18 @@ describe('Combobox (native)', () => {
85
86
  const heightPx = parseFloat(getComputedStyle(wrapper).height);
86
87
  expect(heightPx).toBeGreaterThanOrEqual(44);
87
88
  });
89
+
90
+ /**
91
+ * Optical-centering invariant: the TextInput must have a translateY transform equal
92
+ * to OPTICAL_TEXT_SHIFT to compensate for Arial's asymmetric vertical metrics.
93
+ */
94
+ it('has optical translateY shift on the TextInput (Arial centering correction)', () => {
95
+ renderWithTheme(
96
+ <Combobox label="Fruit" name="fruit" options={fruits} />,
97
+ );
98
+ const input = screen.getByRole('combobox');
99
+ const transform = getComputedStyle(input).transform;
100
+ expect(OPTICAL_TEXT_SHIFT).toBe(-1.5);
101
+ expect(transform).toMatch(/(-1\.5|-1\.50)/);
102
+ });
88
103
  });
@@ -10,6 +10,7 @@ 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
+ import { OPTICAL_TEXT_SHIFT } from '../atoms/inputHelpers';
13
14
 
14
15
  /** Fixed height for single-line control fields (satisfies 44pt touch target). */
15
16
  const INPUT_HEIGHT = 44;
@@ -58,6 +59,7 @@ const styles = StyleSheet.create({
58
59
  * No vertical padding: iOS centers text automatically inside the wrapper height.
59
60
  * textAlignVertical + includeFontPadding provide Android parity.
60
61
  * No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
62
+ * transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
61
63
  */
62
64
  input: {
63
65
  fontFamily: font.family.body,
@@ -66,6 +68,7 @@ const styles = StyleSheet.create({
66
68
  paddingVertical: 0,
67
69
  textAlignVertical: 'center',
68
70
  includeFontPadding: false,
71
+ transform: [{ translateY: OPTICAL_TEXT_SHIFT }],
69
72
  },
70
73
  dropdown: {
71
74
  position: 'absolute',
@@ -1,6 +1,7 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
  import { ThemeProvider } from '../../../../native/ThemeContext';
3
3
  import { FormInput } from './FormInput';
4
+ import { OPTICAL_TEXT_SHIFT } from '../atoms/inputHelpers';
4
5
 
5
6
  function renderWithTheme(ui: React.ReactElement) {
6
7
  return render(<ThemeProvider>{ui}</ThemeProvider>);
@@ -95,4 +96,27 @@ describe('FormInput (native)', () => {
95
96
  // Suppress unused-variable warning from destructuring
96
97
  void container;
97
98
  });
99
+
100
+ /**
101
+ * Optical-centering invariant: the TextInput must have a translateY transform equal
102
+ * to OPTICAL_TEXT_SHIFT to compensate for Arial's asymmetric vertical metrics.
103
+ *
104
+ * Arial's ascent (0.905em) vs descent (0.212em) means iOS places ink ~1.3pt below
105
+ * the geometric centre of the wrapper. The translateY shift was measured on-simulator
106
+ * to bring ink from +1.33pt to -0.33pt, which is visually centred.
107
+ *
108
+ * react-native-web serialises transform: [{translateY: n}] as a CSS transform string,
109
+ * so we check that the computed style contains the expected value.
110
+ */
111
+ it('has optical translateY shift on the TextInput (Arial centering correction)', () => {
112
+ renderWithTheme(<FormInput label="Email" name="email" value="bjorn@boonen.eu" />);
113
+ const input = screen.getByDisplayValue('bjorn@boonen.eu');
114
+ const transform = getComputedStyle(input).transform;
115
+ // react-native-web renders translateY as a matrix or translate() — check the value
116
+ // is present and matches the constant.
117
+ expect(OPTICAL_TEXT_SHIFT).toBe(-1.5);
118
+ // The transform string must encode a non-zero Y translation.
119
+ // matrix(1, 0, 0, 1, 0, -1.5) or translateY(-1.5px) — both contain the value.
120
+ expect(transform).toMatch(/(-1\.5|-1\.50)/);
121
+ });
98
122
  });
@@ -11,6 +11,7 @@ import { spacing, font, border } from '../../../../native/tokens';
11
11
  import { useTheme } from '../../../../native/ThemeContext';
12
12
  import { Label } from '../atoms/Label';
13
13
  import { Message } from '../atoms/Message';
14
+ import { OPTICAL_TEXT_SHIFT } from '../atoms/inputHelpers';
14
15
 
15
16
  /** Fixed height for single-line control fields (satisfies 44pt touch target). */
16
17
  const INPUT_HEIGHT = 44;
@@ -61,6 +62,7 @@ const styles = StyleSheet.create({
61
62
  * No vertical padding: iOS centers text automatically inside the wrapper height.
62
63
  * textAlignVertical + includeFontPadding are Android-specific but harmless on iOS.
63
64
  * No lineHeight: explicit lineHeight on a single-line TextInput clips ascenders/descenders on iOS.
65
+ * transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
64
66
  */
65
67
  input: {
66
68
  fontFamily: font.family.body,
@@ -69,6 +71,7 @@ const styles = StyleSheet.create({
69
71
  paddingVertical: 0,
70
72
  textAlignVertical: 'center',
71
73
  includeFontPadding: false,
74
+ transform: [{ translateY: OPTICAL_TEXT_SHIFT }],
72
75
  },
73
76
  });
74
77
 
@@ -1,6 +1,7 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
  import { ThemeProvider } from '../../../../native/ThemeContext';
3
3
  import { NumberInput } from './NumberInput';
4
+ import { OPTICAL_TEXT_SHIFT } from '../atoms/inputHelpers';
4
5
 
5
6
  function renderWithTheme(ui: React.ReactElement) {
6
7
  return render(<ThemeProvider>{ui}</ThemeProvider>);
@@ -84,4 +85,16 @@ describe('NumberInput (native)', () => {
84
85
  const heightPx = parseFloat(getComputedStyle(wrapper).height);
85
86
  expect(heightPx).toBeGreaterThanOrEqual(44);
86
87
  });
88
+
89
+ /**
90
+ * Optical-centering invariant: the TextInput must have a translateY transform equal
91
+ * to OPTICAL_TEXT_SHIFT to compensate for Arial's asymmetric vertical metrics.
92
+ */
93
+ it('has optical translateY shift on the TextInput (Arial centering correction)', () => {
94
+ renderWithTheme(<NumberInput label="Quantity" name="qty" value={3} />);
95
+ const input = screen.getByDisplayValue('3');
96
+ const transform = getComputedStyle(input).transform;
97
+ expect(OPTICAL_TEXT_SHIFT).toBe(-1.5);
98
+ expect(transform).toMatch(/(-1\.5|-1\.50)/);
99
+ });
87
100
  });
@@ -12,6 +12,7 @@ import { spacing, font, border } from '../../../../native/tokens';
12
12
  import { useTheme } from '../../../../native/ThemeContext';
13
13
  import { Label } from '../atoms/Label';
14
14
  import { Message } from '../atoms/Message';
15
+ import { OPTICAL_TEXT_SHIFT } from '../atoms/inputHelpers';
15
16
 
16
17
  interface NumberInputProps {
17
18
  label: string;
@@ -75,6 +76,7 @@ const styles = StyleSheet.create({
75
76
  * No vertical padding: iOS auto-centers within the wrapper height.
76
77
  * textAlignVertical + includeFontPadding provide Android parity.
77
78
  * No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
79
+ * transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
78
80
  */
79
81
  input: {
80
82
  fontFamily: font.family.body,
@@ -84,6 +86,7 @@ const styles = StyleSheet.create({
84
86
  paddingVertical: 0,
85
87
  textAlignVertical: 'center',
86
88
  includeFontPadding: false,
89
+ transform: [{ translateY: OPTICAL_TEXT_SHIFT }],
87
90
  },
88
91
  });
89
92
 
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Optical vertical correction for single-line TextInput on iOS.
3
+ *
4
+ * Arial (the DS body font) has asymmetric vertical metrics:
5
+ * ascent 0.905em vs descent 0.212em
6
+ * When iOS centers the TextInput's layout box inside the 44pt wrapper,
7
+ * the INK centre sits ~0.08em (~1.3pt at fontSize 16) below the geometric
8
+ * centre. Applying translateY here shifts the ink from +1.33pt to -0.33pt,
9
+ * which is visually perfect (verified on-simulator).
10
+ *
11
+ * Do NOT apply this to multiline TextInput (Textarea) — multiline inputs
12
+ * scroll their content and translateY would misalign the first line.
13
+ */
14
+ export const OPTICAL_TEXT_SHIFT = -1.5;