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 +5 -0
- package/README.md +63 -24
- package/examples/navigation-stack.tsx +132 -0
- package/package.json +8 -2
- package/src/components/fab.tsx +9 -10
- package/src/components/text-field.tsx +16 -5
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|
package/src/components/fab.tsx
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 -
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
395
|
-
paddingBottom
|
|
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:
|
|
551
|
+
top: labelTop,
|
|
541
552
|
paddingHorizontal: outlined ? 4 : 0,
|
|
542
553
|
maxWidth: trailingContent ? '70%' : '85%',
|
|
543
554
|
},
|