@isoline-ui/react-native 0.0.0-stage → 1.0.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 (167) hide show
  1. package/LICENSE +22 -0
  2. package/NOTICE.md +12 -0
  3. package/README.md +104 -3
  4. package/dist/amount.d.ts +3 -0
  5. package/dist/calendar.d.ts +7 -0
  6. package/dist/components/action-chip.d.ts +9 -0
  7. package/dist/components/amount-field.d.ts +12 -0
  8. package/dist/components/avatar.d.ts +17 -0
  9. package/dist/components/back-button.d.ts +12 -0
  10. package/dist/components/back-icon.d.ts +3 -0
  11. package/dist/components/badge.d.ts +13 -0
  12. package/dist/components/banner.d.ts +14 -0
  13. package/dist/components/bottom-sheet-native.d.ts +11 -0
  14. package/dist/components/bottom-sheet-web.d.ts +18 -0
  15. package/dist/components/bottom-sheet.d.ts +3 -0
  16. package/dist/components/button-group.d.ts +26 -0
  17. package/dist/components/button.d.ts +48 -0
  18. package/dist/components/checkbox.d.ts +12 -0
  19. package/dist/components/color-picker.d.ts +21 -0
  20. package/dist/components/context-menu.d.ts +26 -0
  21. package/dist/components/date-picker.d.ts +64 -0
  22. package/dist/components/dialog-context.d.ts +2 -0
  23. package/dist/components/dialog.d.ts +20 -0
  24. package/dist/components/empty-state.d.ts +15 -0
  25. package/dist/components/expand-indicator.d.ts +27 -0
  26. package/dist/components/expressive-shape.d.ts +18 -0
  27. package/dist/components/fab-menu-legacy.d.ts +31 -0
  28. package/dist/components/fab-menu.d.ts +21 -0
  29. package/dist/components/filter-chip.d.ts +69 -0
  30. package/dist/components/floating-action-button.d.ts +15 -0
  31. package/dist/components/icon-button.d.ts +40 -0
  32. package/dist/components/icons.d.ts +18 -0
  33. package/dist/components/input.d.ts +9 -0
  34. package/dist/components/keyboard-inset-view.d.ts +15 -0
  35. package/dist/components/list-controls.d.ts +38 -0
  36. package/dist/components/loading-indicator.d.ts +9 -0
  37. package/dist/components/menu-sheet.d.ts +36 -0
  38. package/dist/components/panel-sheet.d.ts +8 -0
  39. package/dist/components/password-visibility.d.ts +5 -0
  40. package/dist/components/popover-layout.d.ts +25 -0
  41. package/dist/components/popover.d.ts +20 -0
  42. package/dist/components/pressable.d.ts +12 -0
  43. package/dist/components/radio-navigation.d.ts +11 -0
  44. package/dist/components/refresh-control.d.ts +16 -0
  45. package/dist/components/row-gap.d.ts +9 -0
  46. package/dist/components/search-field.d.ts +22 -0
  47. package/dist/components/select-field.d.ts +16 -0
  48. package/dist/components/settings-list.d.ts +98 -0
  49. package/dist/components/settings-row.d.ts +13 -0
  50. package/dist/components/sheet-frame.d.ts +27 -0
  51. package/dist/components/sheet-lifecycle.d.ts +36 -0
  52. package/dist/components/sheet-transition.d.ts +36 -0
  53. package/dist/components/slider.d.ts +21 -0
  54. package/dist/components/snackbar.d.ts +10 -0
  55. package/dist/components/split-button.d.ts +38 -0
  56. package/dist/components/squish.d.ts +20 -0
  57. package/dist/components/swipeable-row.d.ts +20 -0
  58. package/dist/components/switch.d.ts +8 -0
  59. package/dist/components/tab-bar.d.ts +20 -0
  60. package/dist/components/text-field.d.ts +31 -0
  61. package/dist/components/text.d.ts +9 -0
  62. package/dist/components/time-picker.d.ts +40 -0
  63. package/dist/components/toolbar.d.ts +77 -0
  64. package/dist/components/top-bar.d.ts +27 -0
  65. package/dist/components/undo-bar.d.ts +23 -0
  66. package/dist/components/wavy-progress.d.ts +21 -0
  67. package/dist/components/zoomable-image.d.ts +18 -0
  68. package/dist/expressive-shapes.d.ts +8 -0
  69. package/dist/index.d.ts +62 -0
  70. package/dist/keyboard.d.ts +2 -0
  71. package/dist/material.d.ts +11 -0
  72. package/dist/media.d.ts +11 -0
  73. package/dist/motion.d.ts +96 -0
  74. package/dist/platform.d.ts +3 -0
  75. package/dist/preset-tokens.d.ts +264 -0
  76. package/dist/provider.d.ts +59 -0
  77. package/dist/reduced-motion.d.ts +45 -0
  78. package/dist/selection.d.ts +2 -0
  79. package/dist/shape.d.ts +99 -0
  80. package/dist/sheet-physics.d.ts +37 -0
  81. package/dist/theme.d.ts +103 -0
  82. package/dist/tokens.d.ts +2 -0
  83. package/dist/use-keyboard-height.d.ts +10 -0
  84. package/dist/utils.d.ts +3 -0
  85. package/nativewind.cjs +68 -0
  86. package/package.json +63 -6
  87. package/src/amount.ts +53 -0
  88. package/src/calendar.ts +42 -0
  89. package/src/components/action-chip.tsx +64 -0
  90. package/src/components/amount-field.tsx +49 -0
  91. package/src/components/avatar.tsx +91 -0
  92. package/src/components/back-button.tsx +39 -0
  93. package/src/components/back-icon.tsx +25 -0
  94. package/src/components/badge.tsx +41 -0
  95. package/src/components/banner.tsx +63 -0
  96. package/src/components/bottom-sheet-native.tsx +52 -0
  97. package/src/components/bottom-sheet-web.tsx +193 -0
  98. package/src/components/bottom-sheet.tsx +5 -0
  99. package/src/components/button-group.tsx +326 -0
  100. package/src/components/button.tsx +337 -0
  101. package/src/components/checkbox.tsx +71 -0
  102. package/src/components/color-picker.tsx +115 -0
  103. package/src/components/context-menu.tsx +149 -0
  104. package/src/components/date-picker.tsx +466 -0
  105. package/src/components/dialog-context.ts +3 -0
  106. package/src/components/dialog.tsx +210 -0
  107. package/src/components/empty-state.tsx +49 -0
  108. package/src/components/expand-indicator.tsx +60 -0
  109. package/src/components/expressive-shape.tsx +37 -0
  110. package/src/components/fab-menu-legacy.tsx +181 -0
  111. package/src/components/fab-menu.tsx +90 -0
  112. package/src/components/filter-chip.tsx +136 -0
  113. package/src/components/floating-action-button.tsx +79 -0
  114. package/src/components/icon-button.tsx +127 -0
  115. package/src/components/icons.tsx +43 -0
  116. package/src/components/input.tsx +80 -0
  117. package/src/components/keyboard-inset-view.tsx +28 -0
  118. package/src/components/list-controls.tsx +134 -0
  119. package/src/components/loading-indicator.tsx +183 -0
  120. package/src/components/menu-sheet.tsx +133 -0
  121. package/src/components/panel-sheet.tsx +90 -0
  122. package/src/components/password-visibility.tsx +39 -0
  123. package/src/components/popover-layout.ts +47 -0
  124. package/src/components/popover.tsx +139 -0
  125. package/src/components/pressable.tsx +108 -0
  126. package/src/components/radio-navigation.ts +52 -0
  127. package/src/components/refresh-control.tsx +39 -0
  128. package/src/components/row-gap.tsx +13 -0
  129. package/src/components/search-field.tsx +92 -0
  130. package/src/components/select-field.tsx +105 -0
  131. package/src/components/settings-list.tsx +302 -0
  132. package/src/components/settings-row.tsx +61 -0
  133. package/src/components/sheet-frame.tsx +250 -0
  134. package/src/components/sheet-lifecycle.ts +233 -0
  135. package/src/components/sheet-transition.ts +105 -0
  136. package/src/components/slider.tsx +185 -0
  137. package/src/components/snackbar.tsx +97 -0
  138. package/src/components/split-button.tsx +212 -0
  139. package/src/components/squish.ts +41 -0
  140. package/src/components/swipeable-row.tsx +109 -0
  141. package/src/components/switch.tsx +77 -0
  142. package/src/components/tab-bar.tsx +195 -0
  143. package/src/components/text-field.tsx +264 -0
  144. package/src/components/text.tsx +58 -0
  145. package/src/components/time-picker.tsx +277 -0
  146. package/src/components/toolbar.tsx +406 -0
  147. package/src/components/top-bar.tsx +91 -0
  148. package/src/components/undo-bar.tsx +137 -0
  149. package/src/components/wavy-progress.tsx +150 -0
  150. package/src/components/zoomable-image.tsx +240 -0
  151. package/src/expressive-shapes.ts +25 -0
  152. package/src/index.ts +62 -0
  153. package/src/keyboard.ts +5 -0
  154. package/src/material.ts +16 -0
  155. package/src/media.ts +51 -0
  156. package/src/motion.ts +96 -0
  157. package/src/platform.ts +13 -0
  158. package/src/preset-tokens.ts +265 -0
  159. package/src/provider.tsx +201 -0
  160. package/src/reduced-motion.ts +50 -0
  161. package/src/selection.ts +11 -0
  162. package/src/shape.ts +217 -0
  163. package/src/sheet-physics.ts +73 -0
  164. package/src/theme.ts +180 -0
  165. package/src/tokens.ts +2 -0
  166. package/src/use-keyboard-height.ts +64 -0
  167. package/src/utils.ts +7 -0
@@ -0,0 +1,77 @@
1
+ import { Platform, View, Switch as RNSwitch } from 'react-native';
2
+ import { Pressable } from './pressable.tsx';
3
+
4
+ import { useIsolinePlatform, useThemeColors } from '../provider.tsx';
5
+
6
+ export interface SwitchProps {
7
+ value: boolean;
8
+ onValueChange: (value: boolean) => void;
9
+ disabled?: boolean;
10
+ accessibilityLabel?: string;
11
+ }
12
+
13
+ /** Native switch geometry on each OS, with the app's shared colour roles. */
14
+ export function Switch({ value, onValueChange, disabled, accessibilityLabel }: SwitchProps) {
15
+ const colors = useThemeColors();
16
+ const ios = useIsolinePlatform() === 'ios';
17
+
18
+ if (Platform.OS === 'web')
19
+ return (
20
+ <Pressable
21
+ accessibilityRole="switch"
22
+ accessibilityLabel={accessibilityLabel}
23
+ accessibilityState={{ checked: value, disabled }}
24
+ disabled={disabled}
25
+ onPress={() => onValueChange(!value)}
26
+ style={{
27
+ minHeight: 44,
28
+ minWidth: ios ? 51 : 52,
29
+ justifyContent: 'center',
30
+ opacity: disabled ? 0.4 : 1,
31
+ }}
32
+ >
33
+ <View
34
+ pointerEvents="none"
35
+ style={{
36
+ width: ios ? 51 : 52,
37
+ height: ios ? 31 : 32,
38
+ padding: ios ? 3 : 2,
39
+ borderRadius: 16,
40
+ borderWidth: ios ? 0 : 2,
41
+ borderColor: value ? colors.primary : colors.outline,
42
+ backgroundColor: value ? colors.primary : colors.outlineVariant,
43
+ justifyContent: 'center',
44
+ }}
45
+ >
46
+ <View
47
+ style={{
48
+ width: ios ? 25 : 24,
49
+ height: ios ? 25 : 24,
50
+ borderRadius: 14,
51
+ backgroundColor: ios
52
+ ? '#fff'
53
+ : value
54
+ ? colors.onPrimary
55
+ : colors.onSurfaceVariant,
56
+ alignSelf: value ? 'flex-end' : 'flex-start',
57
+ }}
58
+ />
59
+ </View>
60
+ </Pressable>
61
+ );
62
+
63
+ return (
64
+ <RNSwitch
65
+ value={value}
66
+ onValueChange={onValueChange}
67
+ disabled={disabled}
68
+ trackColor={{
69
+ false: colors.outlineVariant,
70
+ true: ios ? colors.primary : colors.primaryContainer,
71
+ }}
72
+ thumbColor={ios ? undefined : value ? colors.primary : colors.onSurfaceVariant}
73
+ ios_backgroundColor={colors.outlineVariant}
74
+ accessibilityLabel={accessibilityLabel}
75
+ />
76
+ );
77
+ }
@@ -0,0 +1,195 @@
1
+ import * as React from 'react';
2
+ import { I18nManager, Platform, View, type StyleProp, type ViewStyle } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withSpring,
7
+ } from 'react-native-reanimated';
8
+ import {
9
+ Surface,
10
+ useIsolinePlatform,
11
+ useThemeColors,
12
+ useHaptics,
13
+ type MaterialOptions,
14
+ } from '../provider.tsx';
15
+ import { spatial } from '../motion.ts';
16
+ import { Pressable } from './pressable.tsx';
17
+ import { Text } from './text.tsx';
18
+ import type { IconProps } from './icons.tsx';
19
+ export interface TabBarItem<T extends string> {
20
+ id: T;
21
+ label: string;
22
+ icon?: React.ComponentType<IconProps>;
23
+ disabled?: boolean;
24
+ badge?: string | number;
25
+ }
26
+ export interface TabBarProps<T extends string> extends MaterialOptions {
27
+ items: TabBarItem<T>[];
28
+ value: T;
29
+ onChange: (value: T) => void;
30
+ accessibilityLabel?: string;
31
+ style?: StyleProp<ViewStyle>;
32
+ }
33
+ /** Controlled navigation UI: route handling belongs to the consuming app. */
34
+ export function TabBar<T extends string>({
35
+ items,
36
+ value,
37
+ onChange,
38
+ accessibilityLabel,
39
+ style,
40
+ material = 'auto',
41
+ glassStyle,
42
+ tintColor,
43
+ fallbackColor,
44
+ interactive = false,
45
+ }: TabBarProps<T>) {
46
+ const colors = useThemeColors();
47
+ const ios = useIsolinePlatform() === 'ios';
48
+ const { selectionHaptic } = useHaptics();
49
+ const refs = React.useRef<Record<string, React.ComponentRef<typeof Pressable> | null>>({});
50
+ const [width, setWidth] = React.useState(0);
51
+ const index = items.findIndex((item) => item.id === value);
52
+ const focusId =
53
+ items.find((item) => item.id === value && !item.disabled)?.id ??
54
+ items.find((item) => !item.disabled)?.id;
55
+ const visualIndex = I18nManager.isRTL
56
+ ? items.length - 1 - Math.max(0, index)
57
+ : Math.max(0, index);
58
+ const position = useSharedValue(visualIndex);
59
+ React.useEffect(() => {
60
+ position.value = withSpring(visualIndex, spatial.default);
61
+ }, [visualIndex, position]);
62
+ const segment = items.length ? Math.max(0, width - 8) / items.length : 0;
63
+ const motion = useAnimatedStyle(() => ({
64
+ transform: [{ translateX: position.value * segment }],
65
+ }));
66
+ const Marker = (Platform.OS === 'web' ? View : Animated.View) as typeof Animated.View;
67
+ return (
68
+ <Surface
69
+ material={ios ? material : 'solid'}
70
+ glassStyle={glassStyle}
71
+ tintColor={tintColor}
72
+ fallbackColor={fallbackColor}
73
+ interactive={interactive}
74
+ accessibilityRole="tablist"
75
+ accessibilityLabel={accessibilityLabel}
76
+ onLayout={(event) => setWidth(event.nativeEvent.layout.width)}
77
+ style={[
78
+ { padding: 4, borderRadius: ios ? 32 : 24, flexDirection: 'row', minHeight: 64 },
79
+ style,
80
+ ]}
81
+ >
82
+ {index >= 0 && width > 0 ? (
83
+ <Marker
84
+ pointerEvents="none"
85
+ style={[
86
+ {
87
+ position: 'absolute',
88
+ left: 4,
89
+ top: 4,
90
+ bottom: 4,
91
+ width: segment,
92
+ borderRadius: ios ? 28 : 20,
93
+ backgroundColor: colors.secondaryContainer,
94
+ },
95
+ Platform.OS === 'web'
96
+ ? { transform: [{ translateX: visualIndex * segment }] }
97
+ : motion,
98
+ ]}
99
+ />
100
+ ) : null}
101
+ {items.map((item) => {
102
+ const selected = item.id === value;
103
+ const Icon = item.icon;
104
+ return (
105
+ <Pressable
106
+ key={item.id}
107
+ ref={(node) => {
108
+ refs.current[item.id] = node;
109
+ }}
110
+ tabIndex={item.id === focusId ? 0 : -1}
111
+ {...(Platform.OS === 'web'
112
+ ? {
113
+ onKeyDown: (
114
+ event: Parameters<
115
+ NonNullable<React.ComponentProps<typeof Pressable>['onKeyDown']>
116
+ >[0],
117
+ ) => {
118
+ const key = event.nativeEvent.key;
119
+ if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(key)) return;
120
+ event.preventDefault();
121
+ const enabled = items.filter((tab) => !tab.disabled);
122
+ if (!enabled.length) return;
123
+ const current = enabled.findIndex((tab) => tab.id === item.id);
124
+ const delta =
125
+ (key === 'ArrowRight' ? 1 : -1) * (I18nManager.isRTL ? -1 : 1);
126
+ const next =
127
+ key === 'Home'
128
+ ? enabled[0]
129
+ : key === 'End'
130
+ ? enabled[enabled.length - 1]
131
+ : enabled[(current + delta + enabled.length) % enabled.length];
132
+ onChange(next.id);
133
+ refs.current[next.id]?.focus();
134
+ },
135
+ }
136
+ : {})}
137
+ disabled={item.disabled}
138
+ accessibilityRole="tab"
139
+ accessibilityLabel={item.label}
140
+ accessibilityState={{ selected, disabled: item.disabled }}
141
+ onPress={() => {
142
+ if (!selected) {
143
+ selectionHaptic();
144
+ onChange(item.id);
145
+ }
146
+ }}
147
+ style={{
148
+ flex: 1,
149
+ minWidth: 0,
150
+ minHeight: 56,
151
+ padding: 6,
152
+ alignItems: 'center',
153
+ justifyContent: 'center',
154
+ gap: 2,
155
+ borderRadius: 28,
156
+ opacity: item.disabled ? 0.4 : 1,
157
+ }}
158
+ >
159
+ {Icon ? (
160
+ <Icon size={22} color={selected ? colors.primary : colors.onSurfaceVariant} />
161
+ ) : null}
162
+ <Text
163
+ numberOfLines={1}
164
+ style={{
165
+ fontSize: 12,
166
+ lineHeight: 16,
167
+ fontWeight: selected ? '700' : '500',
168
+ color: selected ? colors.primary : colors.onSurfaceVariant,
169
+ }}
170
+ >
171
+ {item.label}
172
+ </Text>
173
+ {item.badge !== undefined ? (
174
+ <View
175
+ style={{
176
+ position: 'absolute',
177
+ right: 6,
178
+ top: 2,
179
+ paddingHorizontal: 4,
180
+ minWidth: 16,
181
+ borderRadius: 8,
182
+ backgroundColor: colors.error,
183
+ }}
184
+ >
185
+ <Text style={{ color: colors.onError, fontSize: 10, lineHeight: 14 }}>
186
+ {item.badge}
187
+ </Text>
188
+ </View>
189
+ ) : null}
190
+ </Pressable>
191
+ );
192
+ })}
193
+ </Surface>
194
+ );
195
+ }
@@ -0,0 +1,264 @@
1
+ import * as React from 'react';
2
+ import { Platform, Text as RNText, TextInput, View } from 'react-native';
3
+ import Animated, {
4
+ interpolate,
5
+ interpolateColor,
6
+ useAnimatedStyle,
7
+ useSharedValue,
8
+ withSpring,
9
+ } from 'react-native-reanimated';
10
+
11
+ import { Text } from './text.tsx';
12
+ import { PasswordVisibility } from './password-visibility.tsx';
13
+ import { effects, spatial } from '../motion.ts';
14
+ import { corner } from '../shape.ts';
15
+ import { useIsolinePlatform, useThemeColors } from '../provider.tsx';
16
+ import { cn } from '../utils.ts';
17
+
18
+ type TextInputInstance = React.ElementRef<typeof TextInput>;
19
+
20
+ /** The single-line field's height, `h-14`. */
21
+ const FIELD_HEIGHT = 56;
22
+
23
+ export type TextFieldProps = React.ComponentProps<typeof TextInput> & {
24
+ label: string;
25
+ /** Shown below the field in the error colour, which the border and label pick up too. */
26
+ error?: string | null;
27
+ /**
28
+ * The `bg-*` class of whatever surface the field sits on. The floated
29
+ * label needs an opaque patch behind it to mask the border line it
30
+ * overlaps, and that patch has to match the surface underneath — it
31
+ * defaults to the app's popover surface because every field so far lives
32
+ * in a dialog or sheet.
33
+ */
34
+ notchClassName?: string;
35
+ };
36
+
37
+ /**
38
+ * Material 3 Expressive outlined text field.
39
+ *
40
+ * The label rests inside the field, where a placeholder would sit, until
41
+ * there is a reason to move it — focus, or a value — at which point it
42
+ * floats up to overlap the border. Painting an opaque patch behind it there
43
+ * is the cheap way to get the M3 "notch": the alternative is clipping the
44
+ * border into two arcs, which this does not need.
45
+ *
46
+ * A placeholder is a hint for the empty field the reader is typing into, so
47
+ * it only shows once the label has floated out of its way; while the label
48
+ * rests inside the field the two would sit on top of each other.
49
+ */
50
+ export const TextField: React.ForwardRefExoticComponent<
51
+ TextFieldProps & React.RefAttributes<TextInputInstance>
52
+ > = React.forwardRef<TextInputInstance, TextFieldProps>(function TextField(
53
+ {
54
+ label,
55
+ error,
56
+ value,
57
+ placeholder,
58
+ style,
59
+ className,
60
+ notchClassName,
61
+ onFocus,
62
+ onBlur,
63
+ accessibilityLabel,
64
+ secureTextEntry,
65
+ ...props
66
+ },
67
+ ref,
68
+ ) {
69
+ const ios = useIsolinePlatform() === 'ios';
70
+ const web = Platform.OS === 'web';
71
+ const Box = (web ? View : Animated.View) as typeof Animated.View;
72
+ const Label = (web ? RNText : Animated.Text) as typeof Animated.Text;
73
+ const colors = useThemeColors();
74
+ const [revealed, setRevealed] = React.useState(false);
75
+ const [focused, setFocused] = React.useState(false);
76
+ const floated = focused || Boolean(value);
77
+
78
+ const float = useSharedValue(floated ? 1 : 0);
79
+ const focus = useSharedValue(focused ? 1 : 0);
80
+
81
+ React.useEffect(() => {
82
+ float.value = withSpring(floated ? 1 : 0, spatial.default);
83
+ }, [floated, float]);
84
+
85
+ React.useEffect(() => {
86
+ focus.value = withSpring(focused ? 1 : 0, effects.default);
87
+ }, [focused, focus]);
88
+
89
+ const handleFocus = React.useCallback<NonNullable<TextFieldProps['onFocus']>>(
90
+ (event) => {
91
+ setFocused(true);
92
+ onFocus?.(event);
93
+ },
94
+ [onFocus],
95
+ );
96
+
97
+ const handleBlur = React.useCallback<NonNullable<TextFieldProps['onBlur']>>(
98
+ (event) => {
99
+ setFocused(false);
100
+ onBlur?.(event);
101
+ },
102
+ [onBlur],
103
+ );
104
+
105
+ const borderStyle = useAnimatedStyle(() => ({
106
+ borderColor: error
107
+ ? colors.error
108
+ : interpolateColor(focus.value, [0, 1], [colors.outline, colors.primary]),
109
+ borderWidth: interpolate(focus.value, [0, 1], [1, 2]),
110
+ }));
111
+
112
+ const labelWrapStyle = useAnimatedStyle(() => ({
113
+ transform: [
114
+ { translateY: interpolate(float.value, [0, 1], [0, -FIELD_HEIGHT / 2]) },
115
+ { scale: interpolate(float.value, [0, 1], [1, 0.75]) },
116
+ ],
117
+ }));
118
+
119
+ const labelTextStyle = useAnimatedStyle(() => ({
120
+ color: error
121
+ ? colors.error
122
+ : interpolateColor(focus.value, [0, 1], [colors.onSurfaceVariant, colors.primary]),
123
+ }));
124
+
125
+ if (ios)
126
+ return (
127
+ <View className="gap-1.5">
128
+ <Text variant="label" style={{ color: error ? colors.error : colors.onSurfaceVariant }}>
129
+ {label}
130
+ </Text>
131
+ <View style={{ position: 'relative' }}>
132
+ <TextInput
133
+ ref={ref}
134
+ value={value}
135
+ placeholder={placeholder}
136
+ placeholderTextColor={colors.onSurfaceVariant}
137
+ onFocus={handleFocus}
138
+ onBlur={handleBlur}
139
+ accessibilityLabel={accessibilityLabel ?? label}
140
+ className={cn('text-foreground', className)}
141
+ style={[
142
+ {
143
+ minHeight: 44,
144
+ paddingHorizontal: 12,
145
+ paddingRight: secureTextEntry ? 56 : 12,
146
+ paddingVertical: 10,
147
+ borderRadius: 10,
148
+ backgroundColor: colors.surfaceContainerHigh,
149
+ borderWidth: 1,
150
+ borderColor: error ? colors.error : focused ? colors.primary : 'transparent',
151
+ fontSize: 17,
152
+ },
153
+ style,
154
+ ]}
155
+ {...props}
156
+ autoCapitalize={props.autoCapitalize ?? (secureTextEntry ? 'none' : undefined)}
157
+ autoCorrect={props.autoCorrect ?? (secureTextEntry ? false : undefined)}
158
+ secureTextEntry={secureTextEntry && !revealed}
159
+ />
160
+ {secureTextEntry ? (
161
+ <PasswordVisibility
162
+ revealed={revealed}
163
+ onPress={() => setRevealed((value) => !value)}
164
+ />
165
+ ) : null}
166
+ </View>
167
+ {error ? (
168
+ <Text variant="caption" accessibilityRole="alert" style={{ color: colors.error }}>
169
+ {error}
170
+ </Text>
171
+ ) : null}
172
+ </View>
173
+ );
174
+ return (
175
+ <View className="gap-1.5">
176
+ <View className="relative">
177
+ <TextInput
178
+ ref={ref}
179
+ value={value}
180
+ onFocus={handleFocus}
181
+ onBlur={handleBlur}
182
+ className={cn(
183
+ 'h-14 rounded px-4 pt-2 text-base text-foreground',
184
+ secureTextEntry && 'pr-14',
185
+ className,
186
+ )}
187
+ style={style}
188
+ placeholder={floated ? placeholder : undefined}
189
+ accessibilityLabel={accessibilityLabel ?? label}
190
+ {...props}
191
+ autoCapitalize={props.autoCapitalize ?? (secureTextEntry ? 'none' : undefined)}
192
+ autoCorrect={props.autoCorrect ?? (secureTextEntry ? false : undefined)}
193
+ secureTextEntry={secureTextEntry && !revealed}
194
+ />
195
+
196
+ <Box
197
+ pointerEvents="none"
198
+ style={[
199
+ web
200
+ ? {
201
+ borderWidth: focused ? 2 : 1,
202
+ borderColor: error ? colors.error : focused ? colors.primary : colors.outline,
203
+ }
204
+ : borderStyle,
205
+ { borderRadius: corner.extraSmall },
206
+ ]}
207
+ className="absolute inset-0"
208
+ />
209
+
210
+ {/* The label rests centred on the first line, where the text goes,
211
+ and floats by half the field's height onto the border. */}
212
+ <View
213
+ pointerEvents="none"
214
+ style={{ height: FIELD_HEIGHT }}
215
+ className="absolute left-3 top-0 justify-center"
216
+ >
217
+ <Box
218
+ style={[
219
+ web
220
+ ? {
221
+ transform: [
222
+ { translateY: floated ? -FIELD_HEIGHT / 2 : 0 },
223
+ { scale: floated ? 0.75 : 1 },
224
+ ],
225
+ }
226
+ : labelWrapStyle,
227
+ { transformOrigin: 'left center' },
228
+ ]}
229
+ className={cn('rounded px-1', notchClassName ?? 'bg-popover')}
230
+ >
231
+ <Label
232
+ style={
233
+ web
234
+ ? {
235
+ color: error
236
+ ? colors.error
237
+ : focused
238
+ ? colors.primary
239
+ : colors.onSurfaceVariant,
240
+ }
241
+ : labelTextStyle
242
+ }
243
+ className="text-base"
244
+ >
245
+ {label}
246
+ </Label>
247
+ </Box>
248
+ </View>
249
+ {secureTextEntry ? (
250
+ <PasswordVisibility
251
+ revealed={revealed}
252
+ onPress={() => setRevealed((value) => !value)}
253
+ />
254
+ ) : null}
255
+ </View>
256
+
257
+ {error ? (
258
+ <Text variant="caption" className="text-destructive">
259
+ {error}
260
+ </Text>
261
+ ) : null}
262
+ </View>
263
+ );
264
+ });
@@ -0,0 +1,58 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+ import * as React from 'react';
3
+ import { Text as RNText } from 'react-native';
4
+
5
+ import { useIsolinePlatform, useIsolineTheme } from '../provider.tsx';
6
+ import { cn } from '../utils.ts';
7
+ import type { TextVariant } from '../theme.ts';
8
+
9
+ const textVariants = cva('text-foreground', {
10
+ variants: {
11
+ variant: {
12
+ default: 'text-base leading-6',
13
+ display: 'text-[40px] leading-[46px] font-extrabold tracking-[-0.02em]',
14
+ title: 'text-[32px] leading-9 font-extrabold tracking-[-0.015em]',
15
+ heading: 'text-[22px] leading-7 font-bold tracking-tight',
16
+ subject: 'text-base font-semibold tracking-tight',
17
+ body: 'text-base leading-6',
18
+ reader: 'text-[18px] leading-[30px]',
19
+ muted: 'text-sm text-muted-foreground',
20
+ caption: 'text-sm text-muted-foreground',
21
+ labelLarge: 'text-[15px] font-semibold tracking-[0.01em]',
22
+ label: 'text-sm font-semibold tracking-[0.01em] text-muted-foreground',
23
+ mono: 'font-mono text-sm text-muted-foreground',
24
+ value: 'font-mono text-[15px] leading-[22px]',
25
+ valueLarge: 'font-mono text-[22px] leading-7 font-bold',
26
+ meta: 'text-[13px] leading-[18px] text-muted-foreground',
27
+ },
28
+ },
29
+ defaultVariants: { variant: 'default' },
30
+ });
31
+
32
+ export type TextProps = React.ComponentProps<typeof RNText> & VariantProps<typeof textVariants>;
33
+ export function Text({ className, variant, style, ...props }: TextProps) {
34
+ const theme = useIsolineTheme();
35
+ const ios = useIsolinePlatform() === 'ios';
36
+ const role = variant ?? 'default';
37
+ const iosType = {
38
+ default: { fontSize: 17, lineHeight: 22 },
39
+ body: { fontSize: 17, lineHeight: 22 },
40
+ display: { fontSize: 34, lineHeight: 41, fontWeight: '700' as const },
41
+ title: { fontSize: 28, lineHeight: 34, fontWeight: '700' as const },
42
+ heading: { fontSize: 22, lineHeight: 28, fontWeight: '600' as const },
43
+ caption: { fontSize: 13, lineHeight: 18 },
44
+ label: { fontSize: 13, lineHeight: 18 },
45
+ };
46
+ return (
47
+ <RNText
48
+ className={cn(textVariants({ variant }), className)}
49
+ style={[
50
+ ios && iosType[role as keyof typeof iosType],
51
+ theme.typography?.[(variant ?? 'default') as TextVariant],
52
+ style,
53
+ ]}
54
+ {...props}
55
+ />
56
+ );
57
+ }
58
+ export { textVariants };