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,78 @@
1
+ import type {ButtonProps, ButtonVariant} from './types';
2
+ import type {ViewModifier} from '@expo/ui/swift-ui/modifiers';
3
+ import {Button as SwiftUIButton, HStack, Image, Text} from '@expo/ui/swift-ui';
4
+ import {buttonStyle, buttonBorderShape, controlSize, labelStyle, tint, disabled as disabledMod} from '@expo/ui/swift-ui/modifiers';
5
+ import {ICON_GAP, SIZE_ICON, iosSymbol, swiftBorderShape, swiftControlSize} from './shared';
6
+ import {onAccent as contrastOf} from '../accent';
7
+ import {useColor} from '../theme';
8
+
9
+ const VARIANT_STYLE: Record<ButtonVariant, 'borderedProminent' | 'bordered' | 'plain'> = {
10
+ filled: 'borderedProminent',
11
+ outlined: 'bordered',
12
+ text: 'plain',
13
+ };
14
+
15
+ /**
16
+ * iOS renders SwiftUI's native `Button`. The variant maps to a `buttonStyle`
17
+ * modifier and the accent (theme tint by default, red for destructive) is
18
+ * applied with `tint` so the button is branded consistently across platforms.
19
+ */
20
+ export function Button({
21
+ label,
22
+ onPress,
23
+ variant = 'filled',
24
+ role = 'default',
25
+ color,
26
+ size = 'medium',
27
+ shape,
28
+ prefixIcon,
29
+ suffixIcon,
30
+ hideLabel = false,
31
+ disabled,
32
+ testID,
33
+ }: ButtonProps) {
34
+ const themeTint = useColor('tint');
35
+ const destructive = useColor('destructive');
36
+ const themeOnAccent = useColor(role === 'destructive' ? 'onDestructive' : 'onTint');
37
+ const hasSuffix = !!suffixIcon && !hideLabel;
38
+ const iconOnly = hideLabel && !!prefixIcon;
39
+ const accent = color ?? (role === 'destructive' ? destructive : themeTint);
40
+ // A custom accent brings its own contrast color for filled content.
41
+ const onAccent = color ? contrastOf(color) : themeOnAccent;
42
+ const iconColor = variant === 'filled' ? onAccent : accent;
43
+ const modifiers: ViewModifier[] = [
44
+ buttonStyle(VARIANT_STYLE[variant]),
45
+ controlSize(swiftControlSize(size)),
46
+ tint(accent),
47
+ ];
48
+
49
+ if (shape) modifiers.push(buttonBorderShape(swiftBorderShape(shape)));
50
+ if (disabled) modifiers.push(disabledMod(true));
51
+ if (iconOnly) modifiers.push(labelStyle('iconOnly'));
52
+ if (hasSuffix) {
53
+ return (
54
+ <SwiftUIButton
55
+ role={role === 'destructive' ? 'destructive' : 'default'}
56
+ onPress={onPress}
57
+ modifiers={modifiers}
58
+ testID={testID}>
59
+ <HStack spacing={ICON_GAP}>
60
+ {prefixIcon ? <Image systemName={iosSymbol(prefixIcon)} color={iconColor} size={SIZE_ICON[size]}/> : null}
61
+ <Text>{label}</Text>
62
+ <Image systemName={iosSymbol(suffixIcon!)} color={iconColor} size={SIZE_ICON[size]}/>
63
+ </HStack>
64
+ </SwiftUIButton>
65
+ );
66
+ }
67
+
68
+ return (
69
+ <SwiftUIButton
70
+ label={label}
71
+ role={role === 'destructive' ? 'destructive' : 'default'}
72
+ systemImage={prefixIcon ? iosSymbol(prefixIcon) : undefined}
73
+ modifiers={modifiers}
74
+ onPress={onPress}
75
+ testID={testID}
76
+ />
77
+ );
78
+ }
@@ -0,0 +1,75 @@
1
+ import './button.css';
2
+ import type {CSSProperties} from 'react';
3
+ import type {ButtonProps} from './types';
4
+ import {SymbolView} from 'expo-symbols';
5
+ import {onAccent as contrastOf} from '../accent';
6
+ import {useColor} from '../theme';
7
+ import {SIZE_ICON} from './shared';
8
+
9
+ /**
10
+ * On web the button is a real `<button>` element styled via `button.css`, so it
11
+ * reads as a native web control rather than a ported Android/Material button.
12
+ * The accent color is passed through the `--ui-button-accent` custom property.
13
+ */
14
+ export function Button({
15
+ label,
16
+ onPress,
17
+ variant = 'filled',
18
+ role = 'default',
19
+ size = 'medium',
20
+ color,
21
+ shape,
22
+ prefixIcon,
23
+ suffixIcon,
24
+ hideLabel = false,
25
+ disabled = false,
26
+ testID,
27
+ }: ButtonProps) {
28
+ const themeTint = useColor('tint');
29
+ const destructive = useColor('destructive');
30
+ const themeOnAccent = useColor(role === 'destructive' ? 'onDestructive' : 'onTint');
31
+ const accent = color ?? (role === 'destructive' ? destructive : themeTint);
32
+ // A custom accent brings its own contrast color for filled content.
33
+ const onAccent = color ? contrastOf(color) : themeOnAccent;
34
+ const iconOnly = hideLabel && !!prefixIcon;
35
+ const iconSize = SIZE_ICON[size];
36
+ const style = color
37
+ ? ({'--ui-button-accent': color, '--ui-button-on-accent': onAccent} as CSSProperties)
38
+ : undefined;
39
+ const className = [
40
+ 'ui-button',
41
+ `ui-button--${variant}`,
42
+ `ui-button--${size}`,
43
+ shape && `ui-button--${shape}`,
44
+ !shape && 'ui-button--pill',
45
+ iconOnly && 'ui-button--icon-only',
46
+ role === 'destructive' && 'ui-button--destructive',
47
+ ].filter(Boolean).join(' ');
48
+
49
+ return (
50
+ <button
51
+ type="button"
52
+ style={style}
53
+ className={className}
54
+ disabled={disabled}
55
+ onClick={onPress}
56
+ data-testid={testID}
57
+ aria-label={iconOnly ? label : undefined}>
58
+ {prefixIcon ? (
59
+ <SymbolView
60
+ name={prefixIcon.symbol}
61
+ size={iconSize}
62
+ tintColor={variant === 'filled' ? onAccent : accent}
63
+ />
64
+ ) : null}
65
+ {!iconOnly ? <span className="ui-button__label">{label}</span> : null}
66
+ {suffixIcon && !iconOnly ? (
67
+ <SymbolView
68
+ name={suffixIcon.symbol}
69
+ size={iconSize}
70
+ tintColor={variant === 'filled' ? onAccent : accent}
71
+ />
72
+ ) : null}
73
+ </button>
74
+ );
75
+ }
@@ -0,0 +1,42 @@
1
+ import type {IconToken} from '../icons';
2
+ import type {SFSymbol} from 'expo-symbols';
3
+ import type {ButtonShape, ButtonSize} from './types';
4
+
5
+ export const ICON_GAP = 8;
6
+
7
+ export const SIZE_ICON: Record<ButtonSize, number> = {
8
+ small: 16,
9
+ medium: 18,
10
+ large: 20,
11
+ };
12
+
13
+ export const SIZE_TEXT: Record<ButtonSize, number> = {
14
+ small: 13,
15
+ medium: 15,
16
+ large: 17,
17
+ };
18
+
19
+ export function iosSymbol(token: IconToken): SFSymbol {
20
+ const {symbol} = token;
21
+ if (typeof symbol === 'string') return symbol;
22
+ return symbol.ios ?? 'questionmark';
23
+ }
24
+
25
+ export function swiftControlSize(size: ButtonSize) {
26
+ return ({small: 'small', medium: 'regular', large: 'large'} as const)[size];
27
+ }
28
+
29
+ export function swiftBorderShape(shape: ButtonShape) {
30
+ return ({rounded: 'roundedRectangle', pill: 'capsule', circle: 'circle'} as const)[shape];
31
+ }
32
+
33
+ export function androidContentPadding(size: ButtonSize, hasIcon = false) {
34
+ switch (size) {
35
+ case 'small':
36
+ return {start: hasIcon ? 12 : 16, top: 6, end: 16, bottom: 6};
37
+ case 'medium':
38
+ return {start: hasIcon ? 16 : 24, top: 10, end: 24, bottom: 10};
39
+ case 'large':
40
+ return {start: hasIcon ? 20 : 28, top: 14, end: 28, bottom: 14};
41
+ }
42
+ }
@@ -0,0 +1,72 @@
1
+ import type {IconToken} from '../icons';
2
+
3
+ /**
4
+ * Cross-platform button.
5
+ *
6
+ * Bridges the SwiftUI `Button` on iOS, the Jetpack Compose Material 3 buttons on
7
+ * Android, and the HTML `<button>` element on web. The prop surface is the
8
+ * intersection of what every platform supports, so the same component renders a
9
+ * native-feeling button everywhere.
10
+ *
11
+ * Variants map to each platform's closest native equivalent:
12
+ *
13
+ * | variant | iOS `buttonStyle` | Android component | web |
14
+ * | ---------- | ------------------- | --------------------- | ---------------- |
15
+ * | `filled` | `borderedProminent` | `Button` | solid fill |
16
+ * | `outlined` | `bordered` | `OutlinedButton` | border, no fill |
17
+ * | `text` | `plain` | `TextButton` | text only |
18
+ */
19
+ export type ButtonVariant = 'filled' | 'outlined' | 'text';
20
+
21
+ /**
22
+ * Semantic role of the button. `destructive` renders the button in a danger
23
+ * color (SwiftUI button role, Material error color, web danger styling).
24
+ */
25
+ export type ButtonRole = 'default' | 'destructive';
26
+
27
+ /** Control size of the button. */
28
+ export type ButtonSize = 'small' | 'medium' | 'large';
29
+
30
+ /** Border shape of the button. */
31
+ export type ButtonShape = 'rounded' | 'pill' | 'circle';
32
+
33
+ export interface ButtonProps {
34
+ /**
35
+ * Text shown inside the button. Required for accessibility even when
36
+ * `hideLabel` is true (icon-only mode).
37
+ */
38
+ label: string;
39
+ /** Called when the button is pressed. */
40
+ onPress?: () => void;
41
+ /**
42
+ * Visual emphasis of the button.
43
+ * @default 'filled'
44
+ */
45
+ variant?: ButtonVariant;
46
+ /**
47
+ * Semantic role of the button.
48
+ * @default 'default'
49
+ */
50
+ role?: ButtonRole;
51
+ /** Accent color (tint) for the button. Defaults to the platform/theme tint. */
52
+ color?: string;
53
+ /**
54
+ * Control size of the button.
55
+ * @default 'medium'
56
+ */
57
+ size?: ButtonSize;
58
+ /**
59
+ * Border shape of the button. When omitted, each platform uses its default.
60
+ */
61
+ shape?: ButtonShape;
62
+ /** Leading icon. Also used as the sole icon when `hideLabel` is true. */
63
+ prefixIcon?: IconToken;
64
+ /** Trailing icon. Ignored when `hideLabel` is true. */
65
+ suffixIcon?: IconToken;
66
+ /** Show only the prefix icon; `label` is kept for accessibility. */
67
+ hideLabel?: boolean;
68
+ /** Disables interaction and dims the button. */
69
+ disabled?: boolean;
70
+ /** Identifier used to locate the component in end-to-end tests. */
71
+ testID?: string;
72
+ }
package/src/css.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Side-effect CSS imports (resolved by Expo's Metro on web, no-ops on native).
2
+ declare module '*.css';
@@ -0,0 +1,108 @@
1
+ import type {DateTimePickerProps} from './types';
2
+
3
+ import {useState} from 'react';
4
+ import {useMaterialColors, Row, Text, Column, DatePickerDialog, TimePickerDialog} from '@expo/ui/jetpack-compose';
5
+ import {clip, Shapes, padding, clickable, background, fillMaxWidth} from '@expo/ui/jetpack-compose/modifiers';
6
+ import {useColor} from '../theme';
7
+ import {formatValue, useDateValue, withDatePart, withTimePart} from './shared';
8
+
9
+ /**
10
+ * Android has no inline date+time control, so the row shows the same iOS-style
11
+ * pill as web — built with Jetpack Compose primitives so it lives natively
12
+ * inside the surrounding `Host`/`FieldGroup`. Pressing the pill opens a
13
+ * Material date dialog and then, for `datetime` mode, a time dialog. Each
14
+ * dialog mounts on demand and unmounts once it reports a value or is dismissed.
15
+ */
16
+ export function DateTimePicker({
17
+ label,
18
+ value,
19
+ onChange,
20
+ mode = 'datetime',
21
+ minimumDate,
22
+ maximumDate,
23
+ disabled,
24
+ accentColor,
25
+ }: DateTimePickerProps) {
26
+ const [current, setValue] = useDateValue(value, onChange);
27
+ const [stage, setStage] = useState<'idle' | 'date' | 'time'>('idle');
28
+ const [draft, setDraft] = useState<Date | null>(null);
29
+ const colors = useMaterialColors();
30
+ const tint = useColor('tint');
31
+ // The dialogs are tinted with the live accent seed by default so they
32
+ // follow a user-supplied accent even when the native host is not seeded.
33
+ const dialogColor = accentColor ?? tint;
34
+ const labelColor = disabled ? colors.onSurfaceVariant : colors.onSurface;
35
+ const valueColor = disabled ? colors.onSurfaceVariant : (accentColor ?? colors.onSurface);
36
+ const selectableDates = minimumDate || maximumDate ? {start: minimumDate, end: maximumDate} : undefined;
37
+
38
+ const open = () => {
39
+ setStage(mode === 'time' ? 'time' : 'date');
40
+ };
41
+
42
+ const handleDate = (picked: Date) => {
43
+ const next = withDatePart(current, picked);
44
+ if (mode === 'datetime') {
45
+ setDraft(next);
46
+ setStage('time');
47
+ } else {
48
+ setValue(next);
49
+ setStage('idle');
50
+ }
51
+ };
52
+
53
+ const handleTime = (picked: Date) => {
54
+ setValue(withTimePart(draft ?? current, picked));
55
+ setDraft(null);
56
+ setStage('idle');
57
+ };
58
+
59
+ const dismissDate = () => {
60
+ setStage('idle');
61
+ };
62
+
63
+ const dismissTime = () => {
64
+ if (draft) {
65
+ setValue(draft);
66
+ setDraft(null);
67
+ }
68
+ setStage('idle');
69
+ };
70
+
71
+ return (
72
+ <Column modifiers={[fillMaxWidth()]}>
73
+ <Row
74
+ verticalAlignment="center"
75
+ horizontalArrangement="spaceBetween"
76
+ modifiers={[fillMaxWidth()]}>
77
+ {label != null ? <Text color={labelColor}>{label}</Text> : null}
78
+ <Text
79
+ color={valueColor}
80
+ modifiers={[
81
+ clip(Shapes.RoundedCorner(8)),
82
+ background(colors.surfaceContainerHighest),
83
+ ...(disabled ? [] : [clickable(open)]),
84
+ padding(12, 6, 12, 6),
85
+ ]}>
86
+ {formatValue(current, mode)}
87
+ </Text>
88
+ </Row>
89
+ {stage === 'date' && (
90
+ <DatePickerDialog
91
+ initialDate={(draft ?? current).toISOString()}
92
+ color={dialogColor}
93
+ selectableDates={selectableDates}
94
+ onDateSelected={handleDate}
95
+ onDismissRequest={dismissDate}
96
+ />
97
+ )}
98
+ {stage === 'time' && (
99
+ <TimePickerDialog
100
+ initialDate={(draft ?? current).toISOString()}
101
+ color={dialogColor}
102
+ onDateSelected={handleTime}
103
+ onDismissRequest={dismissTime}
104
+ />
105
+ )}
106
+ </Column>
107
+ );
108
+ }
@@ -0,0 +1,54 @@
1
+ import type {DateTimeMode, DateTimePickerProps} from './types';
2
+ import type {DatePickerComponent} from '@expo/ui/swift-ui';
3
+ import type {ViewModifier} from '@expo/ui/swift-ui/modifiers';
4
+
5
+ import {DatePicker} from '@expo/ui/swift-ui';
6
+ import {datePickerStyle, tint, disabled as disabledMod} from '@expo/ui/swift-ui/modifiers';
7
+ import {useDateValue} from './shared';
8
+
9
+ /**
10
+ * iOS renders the picker inline using SwiftUI's compact `DatePicker`, which is
11
+ * exactly the rounded-pill row look the other platforms emulate. Drop it
12
+ * straight into a `FieldGroup.Section` alongside other rows.
13
+ */
14
+ export function DateTimePicker({
15
+ label,
16
+ value,
17
+ onChange,
18
+ mode = 'datetime',
19
+ minimumDate,
20
+ maximumDate,
21
+ disabled,
22
+ accentColor,
23
+ testID,
24
+ }: DateTimePickerProps) {
25
+ const [current, setValue] = useDateValue(value, onChange);
26
+ const modifiers: ViewModifier[] = [datePickerStyle('compact')];
27
+
28
+ if (accentColor) modifiers.push(tint(accentColor));
29
+ if (disabled != null) modifiers.push(disabledMod(disabled));
30
+
31
+ return (
32
+ <DatePicker
33
+ title={label}
34
+ selection={current}
35
+ onDateChange={setValue}
36
+ displayedComponents={modeToComponents(mode)}
37
+ range={minimumDate || maximumDate ? {start: minimumDate, end: maximumDate} : undefined}
38
+ modifiers={modifiers}
39
+ testID={testID}
40
+ />
41
+ );
42
+ }
43
+
44
+ function modeToComponents(mode: DateTimeMode): DatePickerComponent[] {
45
+ switch (mode) {
46
+ case 'time':
47
+ return ['hourAndMinute'];
48
+ case 'datetime':
49
+ return ['date', 'hourAndMinute'];
50
+ case 'date':
51
+ default:
52
+ return ['date'];
53
+ }
54
+ }
@@ -0,0 +1,119 @@
1
+ import type {ChangeEvent, CSSProperties, MouseEvent} from 'react';
2
+ import type {DateTimePickerProps} from './types';
3
+
4
+ import {Pressable, StyleSheet, View} from 'react-native';
5
+ import {Label} from '../typography';
6
+ import {theme} from '../theme';
7
+ import {formatValue, inputType, parseInputValue, toInputValue, useDateValue} from './shared';
8
+
9
+ /**
10
+ * On web the row mirrors the native iOS/Android pill: a label on the leading
11
+ * edge and a rounded pill showing the value, with a transparent native
12
+ * `<input>` layered on top. Clicking the pill opens the browser's built-in
13
+ * date/time picker.
14
+ */
15
+ export function DateTimePicker({
16
+ label,
17
+ value,
18
+ onChange,
19
+ mode = 'datetime',
20
+ minimumDate,
21
+ maximumDate,
22
+ disabled,
23
+ accentColor,
24
+ testID,
25
+ style,
26
+ }: DateTimePickerProps) {
27
+ const [current, setValue] = useDateValue(value, onChange);
28
+
29
+ const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
30
+ const next = parseInputValue(event.target.value, mode, current);
31
+ if (next) setValue(next);
32
+ };
33
+
34
+ const openPicker = (event: MouseEvent<HTMLInputElement>) => {
35
+ event.currentTarget.showPicker();
36
+ };
37
+
38
+ return (
39
+ <View style={[styles.row, style]} testID={testID}>
40
+ {label != null ? (
41
+ <Label
42
+ color="label"
43
+ style={[styles.label, disabled && styles.disabled]}>
44
+ {label}
45
+ </Label>
46
+ ) : null}
47
+ <Pressable
48
+ disabled={disabled}
49
+ style={[styles.pill, disabled && styles.disabled]}>
50
+ {/* Label-colored value — iOS's compact DatePicker pill uses the
51
+ primary label color (unlike the menu Picker's gray value). */}
52
+ <Label
53
+ color="label"
54
+ style={[styles.value, accentColor != null && {color: accentColor}]}>
55
+ {formatValue(current, mode)}
56
+ </Label>
57
+ <input
58
+ type={inputType(mode)}
59
+ value={toInputValue(current, mode)}
60
+ min={minimumDate ? toInputValue(minimumDate, mode) : undefined}
61
+ max={maximumDate ? toInputValue(maximumDate, mode) : undefined}
62
+ disabled={disabled}
63
+ aria-label={label ?? 'Select date'}
64
+ onChange={handleChange}
65
+ onClick={openPicker}
66
+ style={{...overlayStyle, cursor: disabled ? 'default' : 'pointer'}}
67
+ />
68
+ </Pressable>
69
+ </View>
70
+ );
71
+ }
72
+
73
+ const styles = StyleSheet.create({
74
+ row: {
75
+ flexDirection: 'row',
76
+ alignItems: 'center',
77
+ justifyContent: 'space-between',
78
+ gap: 8,
79
+ width: '100%',
80
+ },
81
+ label: {
82
+ flexShrink: 1,
83
+ },
84
+ pill: {
85
+ position: 'relative',
86
+ flexShrink: 0,
87
+ borderRadius: 8,
88
+ paddingHorizontal: 11,
89
+ paddingVertical: 4,
90
+ backgroundColor: theme.pillBackground,
91
+ },
92
+ value: {
93
+ flexShrink: 0,
94
+ },
95
+ disabled: {
96
+ opacity: 0.4,
97
+ },
98
+ });
99
+
100
+ const overlayStyle: CSSProperties = {
101
+ position: 'absolute',
102
+ top: 0,
103
+ left: 0,
104
+ right: 0,
105
+ bottom: 0,
106
+ width: '100%',
107
+ height: '100%',
108
+ margin: 0,
109
+ padding: 0,
110
+ border: 'none',
111
+ opacity: 0,
112
+ appearance: 'none',
113
+ // The native picker popup derives its colors from the input's own
114
+ // background/color (not `:root`), so theme them via CSS vars. `opacity: 0`
115
+ // keeps the overlay box invisible; the popup still uses these colors.
116
+ colorScheme: 'light dark',
117
+ color: 'var(--color-label)',
118
+ background: 'var(--color-background)',
119
+ };