mobile-design 0.1.2 → 0.1.3

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/COMPONENTS.md CHANGED
@@ -686,6 +686,8 @@ Recoge información mediante campos Filled u Outlined, con etiquetas, ayuda y er
686
686
 
687
687
  El contador muestra la longitud. `maxLength` establece el límite. El ref `TextFieldHandle` ofrece `focus`, `blur`, `clear` e `isFocused`. Para un área de texto de altura fija combina `multiline`, `autoGrow={false}` e `inputStyle` con una altura.
688
688
 
689
+ El tamaño se puede usar con Filled y Outlined. Las alturas base corresponden a la superficie del campo con una línea a escala de texto normal. La ayuda se muestra debajo. En un campo multilínea, `autoGrow` conserva su comportamiento.
690
+
689
691
  **Propiedades de `TextField`**
690
692
 
691
693
  | Propiedad | Tipo | Inicial | Uso | Ejemplo |
@@ -730,6 +732,7 @@ El contador muestra la longitud. `maxLength` establece el límite. El ref `TextF
730
732
  | `trailing` | `React.ReactNode` | — | Slot al final lógico del contenido. | `trailing={<Text variant="labelSmall">Opcional</Text>}` |
731
733
  | `trailingIcon` | `any` | — | Icono al final del control. | `trailingIcon="close"` para borrar el contenido del campo |
732
734
  | `trailingIconAccessibilityLabel` | `string` | — | Nombre accesible para la acción del icono final. | `trailingIconAccessibilityLabel="Borrar nombre"` |
735
+ | `size` | `"standard" \| "compact"` | `'standard'` | Altura base de una línea de 56 dp con `standard` o 48 dp con `compact`, en Filled y Outlined. Respeta el escalado del texto. | `size="compact"` para un campo de menor altura. |
733
736
  | `variant` | `"filled" \| "outlined"` | — | Tratamiento visual o modalidad del componente. | `variant="outlined"` para usar el tratamiento con contorno |
734
737
 
735
738
  <details>
@@ -761,6 +764,7 @@ export default function Example() {
761
764
 
762
765
  return (
763
766
  <TextField
767
+ size="compact"
764
768
  variant="outlined"
765
769
  label="Nombre de la colección"
766
770
  value={name}
@@ -4147,6 +4151,7 @@ Recibe las props del área desplazable de Gorhom. `BottomSheetFlatList` utiliza
4147
4151
  | `trailingIcon` | `IconSource` | — | Icono al final del control. | `trailingIcon="close"` |
4148
4152
  | `trailingIconAccessibilityLabel` | `string` | — | Nombre accesible para la acción del icono final. | `trailingIconAccessibilityLabel="Borrar texto"` |
4149
4153
  | `testID` | `string` | — | Identificador del campo para automatización. | `testID="new-collection-name"` |
4154
+ | `size` | `"standard" \| "compact"` | `'standard'` | Altura base de una línea de 56 dp con `standard` o 48 dp con `compact`, en Filled y Outlined. Respeta el escalado del texto. | `size="compact"` |
4150
4155
  | `variant` | `"outlined" \| "filled"` | — | Tratamiento visual o modalidad del componente. | `variant="outlined"` |
4151
4156
  | `accessibilityHint` | `string` | — | Contexto adicional sobre la acción. | `accessibilityHint="Escribe un nombre de hasta sesenta caracteres"` |
4152
4157
  | `accessibilityLabel` | `string` | — | Nombre que anuncia la tecnología de asistencia. | `accessibilityLabel="Nombre editable de la colección"` |
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  Componentes Material Design 3 para React Native y React Native Web. El paquete integra el tema, las fuentes y los componentes sobre React Native Paper.
6
6
 
7
- [Instalación](#instalación-desde-npm) · [Dependencias](#para-qué-sirve-cada-dependencia) · [Primer componente](#primer-componente) · [Proveedor](#configurar-el-proveedor) · [Temas y colores](#temas-y-colores) · [Tipografía e iconos](#tipografía-e-iconos) · [Índice de componentes](#índice-de-componentes)
7
+ [Instalación](#instalación-desde-npm) · [Dependencias](#para-qué-sirve-cada-dependencia) · [Primer componente](#primer-componente) · [Proveedor](#configurar-el-proveedor) · [Temas y colores](#temas-y-colores) · [Stack](#tema-del-stack) · [Tipografía e iconos](#tipografía-e-iconos) · [Índice de componentes](#índice-de-componentes)
8
8
 
9
9
  La referencia detallada de propiedades y ejemplos viaja con el paquete. Puedes abrir `node_modules/mobile-design/COMPONENTS.md` para consultar la versión instalada. [Referencia en este código fuente](COMPONENTS.md) · [Referencia de la versión publicada](https://cdn.jsdelivr.net/npm/mobile-design@latest/COMPONENTS.md)
10
10
 
@@ -251,39 +251,76 @@ export function createBrandTheme(mode: 'light' | 'dark') {
251
251
 
252
252
  Pasa `theme={createBrandTheme(mode)}` al proveedor, donde `mode` viene del sistema o de la preferencia guardada por tu app. Para ofrecer varios temas de marca, conserva un mapa de roles por marca y modo y usa la marca activa al construir el tema. `createMobileDesignColors` adapta roles existentes y espera un registro completo de tokens. En particular, `surfaceDisabled`, `onSurfaceDisabled` y `backdrop` son roles planos de compatibilidad que el adaptador de Paper no incorpora.
253
253
 
254
- En el workspace de desarrollo existe el comando `pnpm theme:generate '#246B57'`. Genera las dos paletas mediante Material Color Utilities y escribe el módulo de colores, sus tipos y el bloque CSS generado del workspace. `pnpm theme:generate md3-baseline` restaura la paleta MD3 publicada y `pnpm theme:generate neutral` genera el preset neutro. Estos comandos corresponden al código fuente del workspace. En una app que instala el paquete usa los roles de tokens, el exportador externo y las APIs de tema anteriores.
254
+ En el workspace de desarrollo existe el comando `pnpm theme:generate '#246B57'`. Genera las dos paletas mediante Material Color Utilities y escribe el módulo de colores y sus tipos dentro de @mobile-design/tokens. `pnpm theme:generate md3-baseline` restaura la paleta MD3 publicada y `pnpm theme:generate neutral` genera el preset neutro. Estos comandos corresponden al código fuente del workspace. En una app que instala el paquete usa los roles de tokens, el exportador externo y las APIs de tema anteriores.
255
+
256
+ ### Tema del stack
257
+
258
+ ```ts
259
+ import { DarkTheme, DefaultTheme } from '@react-navigation/native';
260
+ import { createMobileDesignTheme } from 'mobile-design/react-native';
261
+ import { adaptNavigationTheme } from 'react-native-paper';
262
+
263
+ export const { LightTheme: navLight, DarkTheme: navDark } = adaptNavigationTheme({
264
+ reactNavigationLight: DefaultTheme,
265
+ reactNavigationDark: DarkTheme,
266
+ materialLight: createMobileDesignTheme({ colorScheme: 'light' }),
267
+ materialDark: createMobileDesignTheme({ colorScheme: 'dark' })
268
+ });
269
+ ```
270
+
271
+ Dentro de `MobileDesignProvider`, usa `useMobileTheme().dark` para elegir el tema de `NavigationContainer`. Con `headerShown: false`, tu `TopAppBar` usa los colores y Roboto del proveedor. [Stack completo](examples/navigation-stack.tsx) con tres pantallas y un `ScreenLayout` reutilizable. Usa `<ScreenLayout title="Perfil">…</ScreenLayout>` para componer otra pantalla.
255
272
 
256
273
  ## Tipografía e iconos
257
274
 
258
- Roboto es la tipografía predeterminada. El proveedor usa un subconjunto de Material Symbols Rounded que cubre los controles y ejemplos de integración. El proveedor carga las fuentes predeterminadas que utiliza el tema o el renderizador activo. Para elegir otra tipografía, registra su familia y los pesos que utilices en tu app y pasa, por ejemplo, `fontFamily="Inter"`.
275
+ Para **Roboto y los símbolos incluidos**, basta `MobileDesignProvider`. El proveedor carga esas fuentes cuando se usan.
276
+
277
+ ### Incrustar fuentes con Expo
278
+
279
+ Usa el plugin si necesitas una fuente incrustada en Android o iOS, disponible desde el arranque. Añade la entrada a `expo.plugins` en `app.json` y recompila. Las rutas parten de la raíz de la app.
280
+
281
+ ```json
282
+ {
283
+ "expo": {
284
+ "plugins": [["expo-font", {
285
+ "android": { "fonts": [{
286
+ "fontFamily": "Inter",
287
+ "fontDefinitions": [
288
+ { "path": "./assets/fonts/Inter-Regular.ttf", "weight": 400 },
289
+ { "path": "./assets/fonts/Inter-Bold.ttf", "weight": 700 }
290
+ ]
291
+ }] },
292
+ "ios": { "fonts": ["./assets/fonts/Inter-Regular.ttf", "./assets/fonts/Inter-Bold.ttf"] }
293
+ }]]
294
+ }
295
+ }
296
+ ```
297
+
298
+ Selecciona `fontFamily="Inter"` en el proveedor. En iOS usa el nombre interno de la familia. Incluye los pesos y estilos que necesites.
299
+
300
+ ### Cargar una fuente propia al iniciar
259
301
 
260
- Agrupa las caras de una misma familia para que Expo pueda elegir el peso correcto. Coloca los archivos de tu fuente en la app y adapta las rutas del ejemplo. Monta `CustomTypeface` en el lugar que ocupa el proveedor en el primer ejemplo.
302
+ Usa `useFonts` para cargar la familia en ejecución, también en web.
261
303
 
262
304
  ```tsx
263
305
  import { useFonts } from 'expo-font';
264
306
  import { MobileDesignProvider, Text } from 'mobile-design/react-native';
265
307
 
266
- const fonts = [
267
- {
268
- fontFamily: 'Inter',
269
- fontDefinitions: [
270
- { path: require('./assets/fonts/Inter-Regular.ttf'), weight: 400 },
271
- { path: require('./assets/fonts/Inter-Bold.ttf'), weight: 700 },
272
- ],
273
- },
274
- ] satisfies Parameters<typeof useFonts>[0];
275
-
276
308
  export function CustomTypeface() {
277
- const [ready, error] = useFonts(fonts);
278
- if (error) throw error;
279
- if (!ready) return null;
280
-
281
- return (
282
- <MobileDesignProvider fontFamily="Inter">
283
- <Text variant="bodyLarge">Contenido</Text>
284
- <Text variant="titleLarge" style={{ fontWeight: '700' }}>Encabezado</Text>
285
- </MobileDesignProvider>
286
- );
309
+ const [ready, error] = useFonts([{
310
+ fontFamily: 'Inter',
311
+ fontDefinitions: [
312
+ { path: require('./assets/fonts/Inter-Regular.ttf'), weight: 400 },
313
+ { path: require('./assets/fonts/Inter-Bold.ttf'), weight: 700 },
314
+ ],
315
+ }]);
316
+ if (error) throw error;
317
+ if (!ready) return null;
318
+ return (
319
+ <MobileDesignProvider fontFamily="Inter">
320
+ <Text variant="bodyLarge">Contenido</Text>
321
+ <Text variant="titleLarge" style={{ fontWeight: '700' }}>Encabezado</Text>
322
+ </MobileDesignProvider>
323
+ );
287
324
  }
288
325
  ```
289
326
 
@@ -505,6 +542,7 @@ El contador muestra la longitud. `maxLength` establece el límite. El ref `TextF
505
542
  | --- | --- | --- | --- |
506
543
  | `value` · requerida | `string` | — | Valor actual controlado por la pantalla. |
507
544
  | `onChangeText` · requerida | `(value: string) => void` | — | Comunica el nuevo texto para actualizar el valor controlado. |
545
+ | `size` | `"standard" \| "compact"` | `'standard'` | Altura base de una línea de 56 dp con `standard` o 48 dp con `compact`, en Filled y Outlined. Respeta el escalado del texto. |
508
546
  | `variant` | `"filled" \| "outlined"` | — | Tratamiento visual o modalidad del componente. |
509
547
  | `error` | `boolean` | — | Activa el tratamiento visual y accesible de error. |
510
548
 
@@ -519,6 +557,7 @@ export default function Example() {
519
557
 
520
558
  return (
521
559
  <TextField
560
+ size="compact"
522
561
  variant="outlined"
523
562
  label="Nombre de la colección"
524
563
  value={name}
@@ -0,0 +1,132 @@
1
+ import { mobileDesignTokens } from '@mobile-design/tokens';
2
+ import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native';
3
+ import { createNativeStackNavigator, type NativeStackScreenProps } from '@react-navigation/native-stack';
4
+ import type { PropsWithChildren } from 'react';
5
+ import { ScrollView, StatusBar, StyleSheet, View } from 'react-native';
6
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
7
+ import { adaptNavigationTheme } from 'react-native-paper';
8
+ import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
9
+ import { Button, MobileDesignProvider, Text, TopAppBar, createMobileDesignTheme, useMobileTheme } from 'mobile-design/react-native';
10
+
11
+ type RootStackParamList = {
12
+ SignIn: undefined;
13
+ SignUp: undefined;
14
+ ForgotPassword: undefined;
15
+ };
16
+
17
+ type ScreenProps<Route extends keyof RootStackParamList> = NativeStackScreenProps<RootStackParamList, Route>;
18
+
19
+ const Stack = createNativeStackNavigator<RootStackParamList>();
20
+
21
+ const { LightTheme: navigationLightTheme, DarkTheme: navigationDarkTheme } = adaptNavigationTheme({
22
+ reactNavigationLight: DefaultTheme,
23
+ reactNavigationDark: DarkTheme,
24
+ materialLight: createMobileDesignTheme({ colorScheme: 'light', fontFamily: 'Roboto' }),
25
+ materialDark: createMobileDesignTheme({ colorScheme: 'dark', fontFamily: 'Roboto' }),
26
+ });
27
+
28
+ type ScreenLayoutProps = PropsWithChildren<{
29
+ title: string;
30
+ heading?: string;
31
+ description?: string;
32
+ onBack?: () => void;
33
+ scroll?: boolean;
34
+ }>;
35
+
36
+ export function ScreenLayout({ title, heading, description, onBack, scroll = true, children }: ScreenLayoutProps) {
37
+ const { colors } = useMobileTheme();
38
+ const content = (
39
+ <>
40
+ {heading ? <Text variant="headlineSmall" accessibilityRole="header">{heading}</Text> : null}
41
+ {description ? <Text variant="bodyMedium" style={{ color: colors.onSurfaceVariant }}>{description}</Text> : null}
42
+ {children}
43
+ </>
44
+ );
45
+
46
+ return (
47
+ <View style={[styles.fill, { backgroundColor: colors.surface }]}>
48
+ <TopAppBar
49
+ title={title}
50
+ navigationIcon={onBack ? 'arrow_back' : undefined}
51
+ navigationLabel="Volver"
52
+ onNavigationPress={onBack}
53
+ />
54
+ <SafeAreaView style={styles.fill} edges={['left', 'right', 'bottom']}>
55
+ {scroll ? <ScrollView contentContainerStyle={styles.content}>{content}</ScrollView> : content}
56
+ </SafeAreaView>
57
+ </View>
58
+ );
59
+ }
60
+
61
+ function SignInScreen({ navigation }: ScreenProps<'SignIn'>) {
62
+ return (
63
+ <ScreenLayout title="Iniciar sesión" heading="Bienvenido" description="Elige cómo continuar con tu cuenta.">
64
+ <Button variant="filled" onPress={() => navigation.navigate('SignUp')}>Crear cuenta</Button>
65
+ <Button variant="text" onPress={() => navigation.navigate('ForgotPassword')}>Olvidé mi contraseña</Button>
66
+ </ScreenLayout>
67
+ );
68
+ }
69
+
70
+ function SignUpScreen({ navigation }: ScreenProps<'SignUp'>) {
71
+ return (
72
+ <ScreenLayout
73
+ title="Crear cuenta"
74
+ description="Aquí puedes integrar los pasos de registro de tu app."
75
+ onBack={() => navigation.goBack()}
76
+ />
77
+ );
78
+ }
79
+
80
+ function ForgotPasswordScreen({ navigation }: ScreenProps<'ForgotPassword'>) {
81
+ return (
82
+ <ScreenLayout
83
+ title="Recuperar contraseña"
84
+ description="Aquí puedes integrar la solicitud del código y el cambio de contraseña."
85
+ onBack={() => navigation.goBack()}
86
+ />
87
+ );
88
+ }
89
+
90
+ function RootNavigator() {
91
+ const { animation, colors, dark } = useMobileTheme();
92
+ const navigationTheme = dark ? navigationDarkTheme : navigationLightTheme;
93
+
94
+ return (
95
+ <NavigationContainer theme={navigationTheme}>
96
+ <StatusBar barStyle={dark ? 'light-content' : 'dark-content'} />
97
+ <Stack.Navigator
98
+ initialRouteName="SignIn"
99
+ screenOptions={{
100
+ headerShown: false,
101
+ animation: animation.scale === 0 ? 'none' : 'default',
102
+ contentStyle: { backgroundColor: colors.surface },
103
+ }}
104
+ >
105
+ <Stack.Screen name="SignIn" component={SignInScreen} />
106
+ <Stack.Screen name="SignUp" component={SignUpScreen} />
107
+ <Stack.Screen name="ForgotPassword" component={ForgotPasswordScreen} />
108
+ </Stack.Navigator>
109
+ </NavigationContainer>
110
+ );
111
+ }
112
+
113
+ export default function App() {
114
+ return (
115
+ <GestureHandlerRootView style={styles.fill}>
116
+ <SafeAreaProvider>
117
+ <MobileDesignProvider fontFamily="Roboto">
118
+ <RootNavigator />
119
+ </MobileDesignProvider>
120
+ </SafeAreaProvider>
121
+ </GestureHandlerRootView>
122
+ );
123
+ }
124
+
125
+ const styles = StyleSheet.create({
126
+ fill: { flex: 1 },
127
+ content: {
128
+ gap: mobileDesignTokens.spacing.small,
129
+ paddingHorizontal: mobileDesignTokens.spacing.medium,
130
+ paddingVertical: mobileDesignTokens.spacing.large,
131
+ },
132
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-design",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Material Design 3 components for React Native and React Native Web, built on React Native Paper",
5
5
  "keywords": [
6
6
  "material-design",
@@ -32,13 +32,14 @@
32
32
  "assets/logo.svg",
33
33
  "README.md",
34
34
  "COMPONENTS.md",
35
+ "examples/navigation-stack.tsx",
35
36
  "LICENSE",
36
37
  "NOTICE",
37
38
  "LICENSE-Apache-2.0.txt"
38
39
  ],
39
40
  "sideEffects": false,
40
41
  "dependencies": {
41
- "@mobile-design/tokens": "0.1.2"
42
+ "@mobile-design/tokens": "0.1.3"
42
43
  },
43
44
  "peerDependencies": {
44
45
  "react": ">=19.2.0",
@@ -62,5 +63,10 @@
62
63
  "react-native-web": {
63
64
  "optional": true
64
65
  }
66
+ },
67
+ "repository": {
68
+ "type": "git",
69
+ "url": "https://github.com/nicolas-rm/mobile-design.git",
70
+ "directory": "library/packages/mobile-design"
65
71
  }
66
72
  }
@@ -539,7 +539,8 @@ function MenuItem({
539
539
  const { fontScale } = useWindowDimensions();
540
540
  const colors = roleColors(theme, variant);
541
541
  const disabled = Boolean(item.disabled || (item.items && !item.items.length));
542
- const [width, setWidth] = useState(56);
542
+ const iconSpace = item.icon ? 32 : 0;
543
+ const [labelWidth, setLabelWidth] = useState<number | null>(null);
543
544
  const [height, setHeight] = useState(56);
544
545
  const [focused, setFocused] = useState(false);
545
546
  const [hovered, setHovered] = useState(false);
@@ -568,7 +569,9 @@ function MenuItem({
568
569
  },
569
570
  [alpha, widthFraction]
570
571
  );
571
- const naturalWidth = Math.max(56, Math.min(maxWidth, width));
572
+ // Keep the icon space live even before native text layout reports a new measurement.
573
+ const measuredWidth = labelWidth === null ? 56 : labelWidth + 49 + iconSpace;
574
+ const naturalWidth = Math.max(56, Math.min(maxWidth, measuredWidth));
572
575
  const geometry = useAnimatedStyle(() => ({ width: naturalWidth * Math.max(0, widthFraction.get()) }));
573
576
  const fade = useAnimatedStyle(() => ({ opacity: Math.max(0, Math.min(1, alpha.get())) }));
574
577
  const contentOffset = useAnimatedStyle(() => {
@@ -661,19 +664,19 @@ function MenuItem({
661
664
  </Animated.View>
662
665
  </AnimatedSurface>
663
666
  <View
664
- key={JSON.stringify([item.label, fontScale, maxWidth, theme.fonts.titleMedium])}
667
+ key={JSON.stringify([item.label, fontScale, maxWidth, iconSpace, theme.fonts.titleMedium])}
665
668
  pointerEvents="none"
666
669
  accessibilityElementsHidden
667
670
  importantForAccessibility="no-hide-descendants"
668
671
  {...(Platform.OS === 'web' ? { 'aria-hidden': true } : {})}
669
- style={[styles.measure, { maxWidth: Math.max(1, maxWidth - 48 - (item.icon ? 32 : 0)) }]}
672
+ style={[styles.measure, { maxWidth: Math.max(1, maxWidth - 48 - iconSpace) }]}
670
673
  >
671
674
  <Text
672
675
  variant="titleMedium"
673
676
  onLayout={
674
677
  Platform.OS === 'web'
675
678
  ? (event) => {
676
- setWidth(Math.ceil(event.nativeEvent.layout.width) + 49 + (item.icon ? 32 : 0));
679
+ setLabelWidth(Math.ceil(event.nativeEvent.layout.width));
677
680
  setHeight(Math.max(56, Math.ceil(event.nativeEvent.layout.height) + 16));
678
681
  }
679
682
  : undefined
@@ -681,11 +684,7 @@ function MenuItem({
681
684
  onTextLayout={
682
685
  Platform.OS !== 'web'
683
686
  ? (event) => {
684
- setWidth(
685
- Math.ceil(Math.max(0, ...event.nativeEvent.lines.map((line) => line.width))) +
686
- 49 +
687
- (item.icon ? 32 : 0)
688
- );
687
+ setLabelWidth(Math.ceil(Math.max(0, ...event.nativeEvent.lines.map((line) => line.width))));
689
688
  setHeight(
690
689
  Math.max(56, Math.ceil(event.nativeEvent.lines.reduce((sum, line) => sum + line.height, 0)) + 16)
691
690
  );
@@ -41,6 +41,8 @@ export interface TextFieldProps extends Omit<TextInputProps, 'style' | 'value' |
41
41
  value: string;
42
42
  onChangeText: (value: string) => void;
43
43
  variant?: 'filled' | 'outlined';
44
+ /** Compact starts at 48 dp. Both sizes grow with text scaling and multiline content. */
45
+ size?: 'standard' | 'compact';
44
46
  label?: string;
45
47
  disabled?: boolean;
46
48
  readOnly?: boolean;
@@ -83,6 +85,7 @@ export const TextField = forwardRef<TextFieldHandle, TextFieldProps>(function Te
83
85
  value,
84
86
  onChangeText,
85
87
  variant = 'filled',
88
+ size = 'standard',
86
89
  label,
87
90
  disabled = false,
88
91
  readOnly = false,
@@ -209,7 +212,15 @@ export const TextField = forwardRef<TextFieldHandle, TextFieldProps>(function Te
209
212
  ) : null);
210
213
  const labelStart = leadingContent ? 52 : 16;
211
214
  const lineHeight = 24 * fontScale;
212
- const minimumHeight = Math.max(56, label && !outlined ? 16 + 40 * fontScale : 32 + lineHeight);
215
+ const compact = size === 'compact';
216
+ const verticalPadding = compact ? 12 : 16;
217
+ const filledLabelPadding = verticalPadding - 8;
218
+ const hasFilledLabel = Boolean(label) && !outlined;
219
+ const paddingTop = hasFilledLabel ? filledLabelPadding + 16 * fontScale : verticalPadding;
220
+ const paddingBottom = hasFilledLabel ? filledLabelPadding : verticalPadding;
221
+ const minimumHeight = Math.max(compact ? 48 : 56, paddingTop + lineHeight + paddingBottom);
222
+ const labelTop = outlined ? verticalPadding : filledLabelPadding + 8 * fontScale;
223
+ const labelTravel = outlined ? labelTop + 8 * fontScale : 8 * fontScale;
213
224
 
214
225
  // RNW reports scrollHeight including the previous minimum height. Measure the
215
226
  // natural textarea synchronously, restoring its style before the next paint,
@@ -315,7 +326,7 @@ export const TextField = forwardRef<TextFieldHandle, TextFieldProps>(function Te
315
326
  left: direction === 'rtl' ? undefined : labelStart + (outlined ? (16 - labelStart) * progress.get() - 4 : 0),
316
327
  right: direction === 'rtl' ? labelStart + (outlined ? (16 - labelStart) * progress.get() - 4 : 0) : undefined,
317
328
  backgroundColor: outlined && progress.get() > 0.001 ? fieldColor : 'transparent',
318
- transform: [{ translateY: outlined ? -(16 + 8 * fontScale) * progress.get() : -8 * fontScale * progress.get() }],
329
+ transform: [{ translateY: -labelTravel * progress.get() }],
319
330
  }));
320
331
  const focusInput = () => {
321
332
  // Read-only fields remain focusable for selecting and copying text.
@@ -391,8 +402,8 @@ export const TextField = forwardRef<TextFieldHandle, TextFieldProps>(function Te
391
402
  flexDirection: direction === 'rtl' ? 'row-reverse' : 'row',
392
403
  paddingLeft: (direction === 'rtl' ? trailingContent : leadingContent) ? 4 : 16,
393
404
  paddingRight: (direction === 'rtl' ? leadingContent : trailingContent) ? 4 : 16,
394
- paddingTop: label && !outlined ? 8 + 16 * fontScale : 16,
395
- paddingBottom: label && !outlined ? 8 : 16,
405
+ paddingTop,
406
+ paddingBottom,
396
407
  },
397
408
  ]}
398
409
  >
@@ -537,7 +548,7 @@ export const TextField = forwardRef<TextFieldHandle, TextFieldProps>(function Te
537
548
  style={[
538
549
  styles.label,
539
550
  {
540
- top: outlined ? 16 : 8 + 8 * fontScale,
551
+ top: labelTop,
541
552
  paddingHorizontal: outlined ? 4 : 0,
542
553
  maxWidth: trailingContent ? '70%' : '85%',
543
554
  },