expo-interface 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/package.json +78 -0
  4. package/src/accent.tsx +62 -0
  5. package/src/button/button.css +118 -0
  6. package/src/button/index.android.tsx +114 -0
  7. package/src/button/index.ios.tsx +78 -0
  8. package/src/button/index.tsx +75 -0
  9. package/src/button/shared.ts +42 -0
  10. package/src/button/types.ts +72 -0
  11. package/src/css.d.ts +2 -0
  12. package/src/date-time/index.android.tsx +108 -0
  13. package/src/date-time/index.ios.tsx +54 -0
  14. package/src/date-time/index.tsx +119 -0
  15. package/src/date-time/shared.ts +160 -0
  16. package/src/date-time/types.ts +41 -0
  17. package/src/field-group/field-group.css +18 -0
  18. package/src/field-group/index.android.tsx +193 -0
  19. package/src/field-group/index.tsx +8 -0
  20. package/src/field-group/index.web.tsx +24 -0
  21. package/src/fill/index.android.ts +4 -0
  22. package/src/fill/index.ios.ts +9 -0
  23. package/src/fill/index.ts +10 -0
  24. package/src/global.css +17 -0
  25. package/src/icons.ts +42 -0
  26. package/src/index.ts +59 -0
  27. package/src/link.ts +36 -0
  28. package/src/list-item/index.android.tsx +52 -0
  29. package/src/list-item/index.tsx +21 -0
  30. package/src/list-item/types.ts +21 -0
  31. package/src/picker/index.android.tsx +80 -0
  32. package/src/picker/index.ios.tsx +48 -0
  33. package/src/picker/index.tsx +145 -0
  34. package/src/picker/shared.ts +70 -0
  35. package/src/picker/types.ts +48 -0
  36. package/src/progress/index.android.tsx +24 -0
  37. package/src/progress/index.ios.tsx +20 -0
  38. package/src/progress/index.tsx +30 -0
  39. package/src/progress/progress.css +27 -0
  40. package/src/progress/types.ts +19 -0
  41. package/src/qr/index.tsx +26 -0
  42. package/src/router/external-link.tsx +26 -0
  43. package/src/screen/header.tsx +65 -0
  44. package/src/screen/host-accent.android.ts +8 -0
  45. package/src/screen/host-accent.ios.ts +9 -0
  46. package/src/screen/host-accent.ts +15 -0
  47. package/src/screen/index.tsx +79 -0
  48. package/src/sheet/index.android.tsx +25 -0
  49. package/src/sheet/index.ios.tsx +20 -0
  50. package/src/sheet/index.tsx +18 -0
  51. package/src/stack-header/index.tsx +3 -0
  52. package/src/stack-header/index.web.tsx +33 -0
  53. package/src/switch/index.android.tsx +60 -0
  54. package/src/switch/index.ios.tsx +33 -0
  55. package/src/switch/index.tsx +62 -0
  56. package/src/switch/types.ts +23 -0
  57. package/src/tab-stack/index.tsx +21 -0
  58. package/src/tabs/index.tsx +34 -0
  59. package/src/tabs/index.web.tsx +114 -0
  60. package/src/tabs/types.ts +64 -0
  61. package/src/text-field/index.android.tsx +123 -0
  62. package/src/text-field/index.ios.tsx +73 -0
  63. package/src/text-field/index.tsx +78 -0
  64. package/src/text-field/shared.ts +83 -0
  65. package/src/text-field/types.ts +64 -0
  66. package/src/theme.ts +528 -0
  67. package/src/typography/index.tsx +83 -0
  68. package/src/typography/index.web.tsx +80 -0
  69. package/src/typography/types.ts +45 -0
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Android: seed the Compose `Host` so all Material 3 children are themed by
3
+ * the accent-derived palette (`SchemeTonalSpot`), and no-arg
4
+ * `useMaterialColors()` calls inside the subtree read the same palette.
5
+ */
6
+ export function hostAccentProps(seed: string) {
7
+ return {seedColor: seed};
8
+ }
@@ -0,0 +1,9 @@
1
+ import {tint} from '@expo/ui/swift-ui/modifiers';
2
+
3
+ /**
4
+ * iOS: apply the accent seed as a Host-level SwiftUI `tint`, which cascades
5
+ * to every SwiftUI child (buttons, pickers, switches, text fields).
6
+ */
7
+ export function hostAccentProps(seed: string) {
8
+ return {modifiers: [tint(seed)]};
9
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Extra `Host` props that apply the accent seed to the native UI toolkit.
3
+ * Resolved per platform by Metro:
4
+ * - `.android.ts`: `{seedColor}` — themes all Compose children with the
5
+ * seeded Material 3 palette and publishes it via `HostPaletteContext`.
6
+ * - `.ios.ts`: `{modifiers: [tint(seed)]}` — cascades the accent to all
7
+ * SwiftUI children (standard `.tint` inheritance).
8
+ * - Web (this file): nothing; the accent flows through CSS custom properties.
9
+ *
10
+ * Typed loosely because `seedColor`/`modifiers` are platform-entry props that
11
+ * the universal `Host` type does not declare.
12
+ */
13
+ export function hostAccentProps(_seed: string): Record<string, never> {
14
+ return {};
15
+ }
@@ -0,0 +1,79 @@
1
+ import type {ColorSchemeName, ColorValue} from 'react-native';
2
+ import type {Edge} from 'react-native-safe-area-context';
3
+
4
+ import {Host} from '@expo/ui';
5
+ import {useEffect} from 'react';
6
+ import {StatusBar} from 'expo-status-bar';
7
+ import {SafeAreaView} from 'react-native-safe-area-context';
8
+ import {useColorScheme, Appearance, StyleSheet, View} from 'react-native';
9
+ import {setBackgroundColorAsync} from 'expo-system-ui';
10
+ import {useAccentSeed} from '../accent';
11
+ import * as theme from '../theme';
12
+
13
+ import {hostAccentProps} from './host-accent';
14
+
15
+ const CONTENT_EDGES: Edge[] = ['left', 'right', 'bottom'];
16
+ const BG_COLOR: Record<ColorSchemeName, ColorValue> = {
17
+ unspecified: theme.colors.light.background,
18
+ light: theme.colors.light.background,
19
+ dark: theme.colors.dark.background,
20
+ };
21
+
22
+ setBackgroundColorAsync(BG_COLOR[Appearance.getColorScheme() ?? 'unspecified']);
23
+
24
+ interface ScreenProps extends React.PropsWithChildren {
25
+ /** Whether to expect an @expo/ui or normal RN component children. */
26
+ native?: boolean;
27
+ /** Screen sits below a stack header — skip redundant top inset/padding. */
28
+ header?: boolean;
29
+ /** Whether to apply a horizontal padding to the screen. */
30
+ gutter?: boolean;
31
+ }
32
+
33
+ export function Screen({
34
+ children,
35
+ native = false,
36
+ header = false,
37
+ gutter = false,
38
+ }: ScreenProps) {
39
+ const seed = useAccentSeed();
40
+ const scheme = useColorScheme();
41
+ const backgroundColor = BG_COLOR[scheme ?? 'unspecified'];
42
+
43
+ useEffect(() => {
44
+ setBackgroundColorAsync(backgroundColor);
45
+ }, [backgroundColor]);
46
+
47
+ return (
48
+ <SafeAreaView
49
+ style={{flex: 1, backgroundColor}}
50
+ edges={header ? CONTENT_EDGES : undefined}>
51
+ <StatusBar style={scheme === 'dark' ? 'light' : 'dark'}/>
52
+ <View style={[styles.root, {paddingTop: header ? 0 : theme.inset.topBar}]}>
53
+ <View style={[styles.content, gutter ? styles.gutter : undefined]}>
54
+ {!native ? children : (
55
+ <Host style={{flex: 1}} {...hostAccentProps(seed)}>
56
+ {children}
57
+ </Host>
58
+ )}
59
+ </View>
60
+ </View>
61
+ </SafeAreaView>
62
+ );
63
+ }
64
+
65
+ const styles = StyleSheet.create({
66
+ root: {
67
+ flex: 1,
68
+ alignItems: 'center',
69
+ gap: theme.spacing.three,
70
+ },
71
+ content: {
72
+ flex: 1,
73
+ width: '100%',
74
+ maxWidth: theme.bound.contentMaxWidth,
75
+ },
76
+ gutter: {
77
+ paddingHorizontal: theme.spacing.three,
78
+ },
79
+ });
@@ -0,0 +1,25 @@
1
+ import type {BottomSheetProps} from '@expo/ui';
2
+
3
+ import {HostPaletteContext, useMaterialColors} from '@expo/ui/jetpack-compose';
4
+ import {BottomSheet} from '@expo/ui';
5
+ import {useAccentSeed} from '../accent';
6
+
7
+ /**
8
+ * Android: the sheet's internal `Host` is not seeded (no `seedColor` prop is
9
+ * forwarded by `BottomSheet`), so overlay `HostPaletteContext` with the
10
+ * accent-seeded Material 3 palette. Every `useMaterialColors()` consumer in
11
+ * the sheet (text fields, pickers, switches, list rows) then resolves the
12
+ * same palette as the seeded `Screen` Host. Controls that also take explicit
13
+ * accent colors (switch track, dialog tint, cursor) read `useColor('tint')`.
14
+ */
15
+ export function Sheet({children, ...props}: BottomSheetProps) {
16
+ const seed = useAccentSeed();
17
+ const palette = useMaterialColors({seedColor: seed});
18
+ return (
19
+ <BottomSheet {...props}>
20
+ <HostPaletteContext.Provider value={palette}>
21
+ {children}
22
+ </HostPaletteContext.Provider>
23
+ </BottomSheet>
24
+ );
25
+ }
@@ -0,0 +1,20 @@
1
+ import type {BottomSheetProps} from '@expo/ui';
2
+
3
+ import {tint} from '@expo/ui/swift-ui/modifiers';
4
+ import {BottomSheet} from '@expo/ui';
5
+ import {useAccentSeed} from '../accent';
6
+
7
+ /**
8
+ * iOS: apply the accent seed as a `tint` presentation modifier on the sheet's
9
+ * content, mirroring the Host-level cascade in `Screen` (`hostAccentProps`).
10
+ * Without it, SwiftUI children in the sheet (toggles, pickers, text fields)
11
+ * would render the default systemBlue instead of the user-supplied accent.
12
+ */
13
+ export function Sheet({children, modifiers, ...props}: BottomSheetProps) {
14
+ const seed = useAccentSeed();
15
+ return (
16
+ <BottomSheet {...props} modifiers={[tint(seed), ...(modifiers ?? [])]}>
17
+ {children}
18
+ </BottomSheet>
19
+ );
20
+ }
@@ -0,0 +1,18 @@
1
+ import type {BottomSheetProps} from '@expo/ui';
2
+ import {BottomSheet} from '@expo/ui';
3
+
4
+ /**
5
+ * Accent-aware bottom sheet. `@expo/ui`'s `BottomSheet` mounts its own `Host`
6
+ * without the accent seed (unlike `Screen`, which applies `hostAccentProps`),
7
+ * so sheet content would fall back to the platform default tint. Resolved per
8
+ * platform by Metro:
9
+ * - `.ios.tsx`: prepends a `tint(seed)` presentation modifier so the accent
10
+ * cascades to all SwiftUI children in the sheet.
11
+ * - `.android.tsx`: overlays `HostPaletteContext` with the seeded Material 3
12
+ * palette so `useMaterialColors()` consumers match the seeded `Screen` Host.
13
+ * - Web (this file): accent flows through CSS custom properties; vaul sheet
14
+ * width is constrained via `global.css`.
15
+ */
16
+ export function Sheet(props: BottomSheetProps) {
17
+ return <BottomSheet {...props}/>;
18
+ }
@@ -0,0 +1,3 @@
1
+ export function ConstrainedStackHeader() {
2
+ return null;
3
+ }
@@ -0,0 +1,33 @@
1
+ import {ScreenHeader} from '../screen/header';
2
+
3
+ interface StackHeaderProps {
4
+ navigation: {goBack: () => void};
5
+ route: {name: string};
6
+ back?: unknown;
7
+ options: {
8
+ title?: string;
9
+ headerTitle?: string | (() => React.ReactNode);
10
+ headerRight?: (props: {tintColor?: string}) => React.ReactNode;
11
+ };
12
+ }
13
+
14
+ export function ConstrainedStackHeader({
15
+ navigation,
16
+ route,
17
+ back,
18
+ options,
19
+ }: StackHeaderProps) {
20
+ const title = typeof options.headerTitle === 'string'
21
+ ? options.headerTitle
22
+ : options.title ?? route.name;
23
+
24
+ const trailing = options.headerRight?.({});
25
+
26
+ return (
27
+ <ScreenHeader
28
+ title={title}
29
+ onBack={back ? () => navigation.goBack() : undefined}
30
+ trailing={trailing}
31
+ />
32
+ );
33
+ }
@@ -0,0 +1,60 @@
1
+ import type {SwitchProps} from './types';
2
+
3
+ import {Row, Switch as ComposeSwitch, Text, useMaterialColors} from '@expo/ui/jetpack-compose';
4
+ import {fillMaxWidth, graphicsLayer, testID as testIDModifier} from '@expo/ui/jetpack-compose/modifiers';
5
+ import {useColor} from '../theme';
6
+
7
+ // Material 3's Switch has a fixed size; scale it down visually while keeping it
8
+ // pinned to the trailing edge (transformOrigin at the right).
9
+ const SCALE = 0.80;
10
+
11
+ // iOS switches use a white thumb when on; M3 defaults to `onPrimary`.
12
+ const THUMB_ON = '#FFFFFF';
13
+
14
+ /**
15
+ * The stock universal switch packs the label and toggle tightly together. Here
16
+ * the row fills the available width and pushes the Material switch to the
17
+ * trailing edge, mirroring the iOS Form row. The "on" state is colored with
18
+ * the live accent seed (matching the iOS Host `tint` cascade and the web
19
+ * `--color-tint` default) instead of the host's tonal M3 primary, so the
20
+ * switch reads identically across platforms — including inside sheets whose
21
+ * native host is not seeded.
22
+ */
23
+ export function Switch({
24
+ label,
25
+ value,
26
+ onValueChange,
27
+ disabled,
28
+ accentColor,
29
+ testID,
30
+ }: SwitchProps) {
31
+ const colors = useMaterialColors();
32
+ const tint = useColor('tint');
33
+ const toggle = (
34
+ <ComposeSwitch
35
+ value={value}
36
+ onCheckedChange={disabled ? undefined : onValueChange}
37
+ enabled={!disabled}
38
+ colors={{
39
+ checkedTrackColor: accentColor ?? tint,
40
+ checkedThumbColor: THUMB_ON,
41
+ }}
42
+ modifiers={[
43
+ graphicsLayer({scaleX: SCALE, scaleY: SCALE, transformOriginX: 1}),
44
+ ...(testID ? [testIDModifier(testID)] : []),
45
+ ]}
46
+ />
47
+ );
48
+
49
+ if (label == null) return toggle;
50
+
51
+ return (
52
+ <Row
53
+ verticalAlignment="center"
54
+ horizontalArrangement="spaceBetween"
55
+ modifiers={[fillMaxWidth()]}>
56
+ <Text color={disabled ? colors.onSurfaceVariant : colors.onSurface}>{label}</Text>
57
+ {toggle}
58
+ </Row>
59
+ );
60
+ }
@@ -0,0 +1,33 @@
1
+ import type {SwitchProps} from './types';
2
+ import type {ViewModifier} from '@expo/ui/swift-ui/modifiers';
3
+
4
+ import {Toggle} from '@expo/ui/swift-ui';
5
+ import {disabled as disabledMod, tint} from '@expo/ui/swift-ui/modifiers';
6
+
7
+ /**
8
+ * iOS renders the toggle inline using SwiftUI's `Toggle`, which is exactly the
9
+ * Form row look the other platforms emulate: a leading label with the switch
10
+ * pinned to the trailing edge. Drop it straight into a `FieldGroup.Section`.
11
+ */
12
+ export function Switch({
13
+ label,
14
+ value,
15
+ onValueChange,
16
+ disabled,
17
+ accentColor,
18
+ testID,
19
+ }: SwitchProps) {
20
+ const modifiers: ViewModifier[] = [];
21
+ if (accentColor) modifiers.push(tint(accentColor));
22
+ if (disabled) modifiers.push(disabledMod(true));
23
+
24
+ return (
25
+ <Toggle
26
+ isOn={value}
27
+ onIsOnChange={onValueChange}
28
+ label={label}
29
+ modifiers={modifiers}
30
+ testID={testID}
31
+ />
32
+ );
33
+ }
@@ -0,0 +1,62 @@
1
+ import type {SwitchProps} from './types';
2
+ import {Switch as RNSwitch, StyleSheet, View} from 'react-native';
3
+ import {Label} from '../typography';
4
+ import {theme} from '../theme';
5
+
6
+ /** iOS switches use a white thumb in both states; web defaults to a green thumb. */
7
+ const THUMB = '#ffffff';
8
+
9
+ /**
10
+ * On web the row mirrors the native iOS/Android layout: a label on the leading
11
+ * edge and the native React Native Web switch pinned to the trailing edge.
12
+ * The "on" track defaults to the accent tint for iOS parity — on iOS the
13
+ * Host-level `tint` cascade colors the SwiftUI `Toggle` with the seed.
14
+ */
15
+ export function Switch({
16
+ label,
17
+ value,
18
+ onValueChange,
19
+ disabled,
20
+ accentColor,
21
+ testID,
22
+ style,
23
+ }: SwitchProps) {
24
+ const onColor = accentColor ?? (theme.tint as string);
25
+ const offColor = theme.switchTrack;
26
+ const toggle = (
27
+ <RNSwitch
28
+ value={value}
29
+ onValueChange={onValueChange}
30
+ disabled={disabled}
31
+ thumbColor={THUMB}
32
+ trackColor={{true: onColor, false: offColor}}
33
+ testID={label == null ? testID : undefined}
34
+ {...({activeThumbColor: THUMB} as object)}
35
+ />
36
+ );
37
+
38
+ return label != null ? (
39
+ <View style={[styles.row, style]} testID={testID}>
40
+ <Label color="label" style={[styles.label, disabled && styles.disabled]}>
41
+ {label}
42
+ </Label>
43
+ {toggle}
44
+ </View>
45
+ ) : toggle;
46
+ }
47
+
48
+ const styles = StyleSheet.create({
49
+ row: {
50
+ flexDirection: 'row',
51
+ alignItems: 'center',
52
+ justifyContent: 'space-between',
53
+ gap: 8,
54
+ width: '100%',
55
+ },
56
+ label: {
57
+ flexShrink: 1,
58
+ },
59
+ disabled: {
60
+ opacity: 0.4,
61
+ },
62
+ });
@@ -0,0 +1,23 @@
1
+ import type {StyleProp, ViewStyle} from 'react-native';
2
+
3
+ /**
4
+ * Cross-platform toggle with a conformed iOS-style appearance.
5
+ *
6
+ * A controlled control: pair `value` with `onValueChange` to manage state.
7
+ */
8
+ export interface SwitchProps {
9
+ /** Label rendered at the leading edge of the row, mirroring an iOS Form row. */
10
+ label?: string;
11
+ /** Whether the switch is on. */
12
+ value: boolean;
13
+ /** Called when the user toggles the switch. */
14
+ onValueChange: (value: boolean) => void;
15
+ /** Disables interaction. */
16
+ disabled?: boolean;
17
+ /** Tint applied to the "on" track (overrides the theme accent tint). */
18
+ accentColor?: string;
19
+ /** Identifier used to locate the component in end-to-end tests. */
20
+ testID?: string;
21
+ /** Style applied to the row container (web/android only). */
22
+ style?: StyleProp<ViewStyle>;
23
+ }
@@ -0,0 +1,21 @@
1
+ import {Stack} from 'expo-router';
2
+ import {Platform} from 'react-native';
3
+ import {useNavTheme} from '../theme';
4
+
5
+ export function TabStack({title}: {title: string}) {
6
+ const {colors} = useNavTheme();
7
+
8
+ return (
9
+ <Stack
10
+ screenOptions={{
11
+ headerShown: Platform.OS !== 'web',
12
+ headerShadowVisible: false,
13
+ headerBackButtonDisplayMode: 'minimal',
14
+ headerTintColor: colors.text,
15
+ headerTitleStyle: {color: colors.text},
16
+ headerStyle: {backgroundColor: colors.background},
17
+ }}>
18
+ <Stack.Screen name="index" options={{title}}/>
19
+ </Stack>
20
+ );
21
+ }
@@ -0,0 +1,34 @@
1
+ import type {TabBarProps} from './types';
2
+ import {NativeTabs} from 'expo-router/unstable-native-tabs';
3
+ import {useColor} from '../theme';
4
+
5
+ export function Tabs({routes}: TabBarProps) {
6
+ const rippleColor = useColor('pillBackground');
7
+ const indicatorColor = useColor('backgroundElement');
8
+ const labelColor = useColor('label');
9
+
10
+ return (
11
+ <NativeTabs
12
+ backgroundColor="transparent"
13
+ indicatorColor={indicatorColor}
14
+ rippleColor={rippleColor}
15
+ labelStyle={{selected: {color: labelColor}}}
16
+ // Monochrome selected icon to match the label (and the web tab bar);
17
+ // without it iOS falls back to the default system tint.
18
+ iconColor={{selected: labelColor}}>
19
+ {routes.map(route => (
20
+ <NativeTabs.Trigger
21
+ key={route.name}
22
+ name={route.name}>
23
+ <NativeTabs.Trigger.Label>
24
+ {route.label}
25
+ </NativeTabs.Trigger.Label>
26
+ <NativeTabs.Trigger.Icon
27
+ sf={route.icon.ios}
28
+ md={route.icon.android}
29
+ />
30
+ </NativeTabs.Trigger>
31
+ ))}
32
+ </NativeTabs>
33
+ );
34
+ }
@@ -0,0 +1,114 @@
1
+ import type {TabTriggerSlotProps, TabListProps} from 'expo-router/ui';
2
+ import type {TabBarProps, TabRoute, WebLogo} from './types';
3
+
4
+ import {Tabs as WebTabs, TabSlot, TabList, TabTrigger} from 'expo-router/ui';
5
+ import {View, Pressable, StyleSheet} from 'react-native';
6
+ import {SymbolView} from 'expo-symbols';
7
+ import {Image} from 'expo-image';
8
+ import app from 'expo-constants';
9
+
10
+ import {theme, spacing, bound} from '../theme';
11
+ import {Headline, Label} from '../typography';
12
+
13
+ export function Tabs({routes, webLogo = 'icon-and-text', webIcon}: TabBarProps) {
14
+ return (
15
+ <WebTabs>
16
+ <TabSlot style={styles.slot}/>
17
+ <TabList asChild>
18
+ <WebTabList logo={webLogo} icon={webIcon}>
19
+ {routes.map(route => (
20
+ <TabTrigger key={route.name} name={route.name} href={route.href} asChild>
21
+ <TabLink icon={route.icon}>{route.label}</TabLink>
22
+ </TabTrigger>
23
+ ))}
24
+ </WebTabList>
25
+ </TabList>
26
+ </WebTabs>
27
+ );
28
+ }
29
+
30
+ export function WebTabList({logo, icon, ...props}: TabListProps & {logo: WebLogo, icon?: TabBarProps['webIcon']}) {
31
+ const isPreset = typeof logo === 'string';
32
+ const isTextOnly = logo === 'text-only';
33
+ const isIconOnly = logo === 'icon-only';
34
+ return (
35
+ <View {...props} style={styles.list}>
36
+ <View style={styles.inner}>
37
+ <View style={styles.logo}>
38
+ {!isPreset ? logo : (
39
+ <>
40
+ {!isTextOnly && icon != null && (
41
+ <Image
42
+ style={styles.icon}
43
+ source={icon}
44
+ contentFit="contain"
45
+ />
46
+ )}
47
+ {!isIconOnly && (
48
+ <Headline color="label">
49
+ {app.expoConfig?.name}
50
+ </Headline>
51
+ )}
52
+ </>
53
+ )}
54
+ </View>
55
+ {props.children}
56
+ </View>
57
+ </View>
58
+ );
59
+ }
60
+
61
+ export function TabLink({children, isFocused, icon, ...props}: TabTriggerSlotProps & {icon: TabRoute['icon']}) {
62
+ return (
63
+ <Pressable {...props} style={({pressed}) => pressed && styles.pressed}>
64
+ <View style={styles.link}>
65
+ <SymbolView name={icon} size={18} tintColor={isFocused ? theme.label : theme.secondaryLabel}/>
66
+ <Label color={isFocused ? 'label' : 'secondaryLabel'}>{children}</Label>
67
+ </View>
68
+ </Pressable>
69
+ );
70
+ }
71
+
72
+ const styles = StyleSheet.create({
73
+ slot: {
74
+ height: '100%',
75
+ },
76
+ list: {
77
+ position: 'absolute',
78
+ flexDirection: 'row',
79
+ justifyContent: 'center',
80
+ alignItems: 'center',
81
+ width: '100%',
82
+ padding: spacing.three,
83
+ },
84
+ inner: {
85
+ flexGrow: 1,
86
+ flexDirection: 'row',
87
+ alignItems: 'center',
88
+ maxWidth: bound.contentMaxWidth,
89
+ paddingHorizontal: spacing.five,
90
+ paddingVertical: spacing.three,
91
+ gap: spacing.three,
92
+ borderRadius: spacing.five,
93
+ backgroundColor: theme.backgroundElement,
94
+ },
95
+ logo: {
96
+ flexDirection: 'row',
97
+ alignItems: 'center',
98
+ marginRight: 'auto',
99
+ gap: spacing.two,
100
+ },
101
+ icon: {
102
+ width: 24,
103
+ height: 24,
104
+ borderRadius: spacing.two,
105
+ },
106
+ link: {
107
+ flexDirection: 'row',
108
+ alignItems: 'center',
109
+ gap: spacing.two,
110
+ },
111
+ pressed: {
112
+ opacity: 0.7,
113
+ },
114
+ });
@@ -0,0 +1,64 @@
1
+ import type {Href} from 'expo-router';
2
+ import type {SFSymbol, AndroidSymbol} from 'expo-symbols';
3
+ import type {ReactNode} from 'react';
4
+ import type {ImageSource} from 'expo-image';
5
+
6
+ /**
7
+ * Props for the tab bar component.
8
+ * @see Tabs
9
+ */
10
+ export interface TabBarProps {
11
+ /**
12
+ * Configures the tab bar items for native & web.
13
+ * @example
14
+ * ```ts
15
+ * const routes: readonly TabRoute[] = [
16
+ * {
17
+ * href: '/',
18
+ * name: 'index',
19
+ * label: 'Home',
20
+ * icon: {ios: 'house', android: 'home', web: 'home'},
21
+ * },
22
+ * {
23
+ * href: '/settings',
24
+ * name: 'settings',
25
+ * label: 'Settings',
26
+ * icon: {ios: 'gearshape', android: 'settings', web: 'settings'},
27
+ * },
28
+ * ];
29
+ * ```
30
+ */
31
+ routes: readonly TabRoute[];
32
+ /**
33
+ * Controls the web tab bar logo. Use a preset mode to show the app icon
34
+ * and/or name, or pass a custom node to replace them entirely.
35
+ * @default 'icon-and-text'
36
+ */
37
+ webLogo?: WebLogo;
38
+ /**
39
+ * App icon rendered by the `icon-only` and `icon-and-text` web logo presets,
40
+ * e.g. `require('./assets/icon.png')`. When omitted only the name is shown.
41
+ */
42
+ webIcon?: ImageSource | number;
43
+ }
44
+
45
+ export type WebLogo =
46
+ | 'icon-only'
47
+ | 'text-only'
48
+ | 'icon-and-text'
49
+ | ReactNode;
50
+
51
+ export interface TabRoute {
52
+ /** Route segment, matching the file name in `src/app`. */
53
+ name: string;
54
+ /** Navigation target used by the web tab bar. */
55
+ href: Href;
56
+ /** Visible tab label. */
57
+ label: string;
58
+ /** Symbol name per platform. */
59
+ icon: {
60
+ ios: SFSymbol;
61
+ android: AndroidSymbol;
62
+ web: AndroidSymbol;
63
+ };
64
+ }