panelui-native 0.16.0 → 0.19.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 (66) hide show
  1. package/lib/module/components/bottom-sheet/index.js +38 -10
  2. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  3. package/lib/module/components/checkbox/index.js +24 -11
  4. package/lib/module/components/checkbox/index.js.map +1 -1
  5. package/lib/module/components/field/index.js +337 -0
  6. package/lib/module/components/field/index.js.map +1 -0
  7. package/lib/module/components/flow/flow-paths.js +378 -0
  8. package/lib/module/components/flow/flow-paths.js.map +1 -0
  9. package/lib/module/components/flow/index.js +1598 -0
  10. package/lib/module/components/flow/index.js.map +1 -0
  11. package/lib/module/components/form/index.js +77 -0
  12. package/lib/module/components/form/index.js.map +1 -0
  13. package/lib/module/components/form/use-field.js +50 -0
  14. package/lib/module/components/form/use-field.js.map +1 -0
  15. package/lib/module/components/form/use-form.js +217 -0
  16. package/lib/module/components/form/use-form.js.map +1 -0
  17. package/lib/module/components/frame/index.js +124 -22
  18. package/lib/module/components/frame/index.js.map +1 -1
  19. package/lib/module/components/otp-input/index.js +303 -0
  20. package/lib/module/components/otp-input/index.js.map +1 -0
  21. package/lib/module/components/signature/index.js +500 -0
  22. package/lib/module/components/signature/index.js.map +1 -0
  23. package/lib/module/icons/index.js +218 -0
  24. package/lib/module/icons/index.js.map +1 -1
  25. package/lib/module/index.js +6 -1
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/typescript/src/components/bottom-sheet/index.d.ts +15 -1
  28. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  29. package/lib/typescript/src/components/checkbox/index.d.ts +8 -0
  30. package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
  31. package/lib/typescript/src/components/field/index.d.ts +94 -0
  32. package/lib/typescript/src/components/field/index.d.ts.map +1 -0
  33. package/lib/typescript/src/components/flow/flow-paths.d.ts +89 -0
  34. package/lib/typescript/src/components/flow/flow-paths.d.ts.map +1 -0
  35. package/lib/typescript/src/components/flow/index.d.ts +310 -0
  36. package/lib/typescript/src/components/flow/index.d.ts.map +1 -0
  37. package/lib/typescript/src/components/form/index.d.ts +64 -0
  38. package/lib/typescript/src/components/form/index.d.ts.map +1 -0
  39. package/lib/typescript/src/components/form/use-field.d.ts +16 -0
  40. package/lib/typescript/src/components/form/use-field.d.ts.map +1 -0
  41. package/lib/typescript/src/components/form/use-form.d.ts +37 -0
  42. package/lib/typescript/src/components/form/use-form.d.ts.map +1 -0
  43. package/lib/typescript/src/components/frame/index.d.ts +39 -2
  44. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  45. package/lib/typescript/src/components/otp-input/index.d.ts +141 -0
  46. package/lib/typescript/src/components/otp-input/index.d.ts.map +1 -0
  47. package/lib/typescript/src/components/signature/index.d.ts +254 -0
  48. package/lib/typescript/src/components/signature/index.d.ts.map +1 -0
  49. package/lib/typescript/src/icons/index.d.ts +17 -0
  50. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  51. package/lib/typescript/src/index.d.ts +7 -2
  52. package/lib/typescript/src/index.d.ts.map +1 -1
  53. package/package.json +9 -1
  54. package/src/components/bottom-sheet/index.tsx +50 -12
  55. package/src/components/checkbox/index.tsx +40 -12
  56. package/src/components/field/index.tsx +329 -0
  57. package/src/components/flow/flow-paths.ts +348 -0
  58. package/src/components/flow/index.tsx +1924 -0
  59. package/src/components/form/index.tsx +85 -0
  60. package/src/components/form/use-field.ts +65 -0
  61. package/src/components/form/use-form.ts +252 -0
  62. package/src/components/frame/index.tsx +162 -14
  63. package/src/components/otp-input/index.tsx +396 -0
  64. package/src/components/signature/index.tsx +681 -0
  65. package/src/icons/index.tsx +159 -0
  66. package/src/index.ts +72 -0
@@ -24,15 +24,35 @@ import Animated, {
24
24
  withTiming,
25
25
  } from 'react-native-reanimated';
26
26
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
27
+ import { tv } from 'tailwind-variants';
27
28
  import { getNativeUI } from '../../native';
28
29
  import { XIcon } from '../../icons';
29
30
  import { Portal } from '../../primitives/portal';
31
+ import { Scrim } from '../../primitives/scrim';
30
32
  import { cn } from '../../utils/cn';
31
33
 
32
34
  const SPRING = { damping: 22, stiffness: 280, mass: 0.7 } as const;
33
35
  const DISMISS_DISTANCE = 120;
34
36
  const DISMISS_VELOCITY = 800;
35
37
 
38
+ const sheetVariants = tv({
39
+ base: 'border border-border bg-popover px-5 pt-2 shadow-lg',
40
+ variants: {
41
+ detached: {
42
+ // Docked: the sheet is continuous with the bottom of the screen, so the
43
+ // bottom edge is not a real edge and drawing a line along it would be a
44
+ // rule through the middle of nothing.
45
+ false: 'rounded-t-3xl border-b-0',
46
+ // Floating: all four edges are real, so all four are drawn and all four
47
+ // corners are rounded.
48
+ true: 'mx-4 mb-6 rounded-3xl',
49
+ },
50
+ },
51
+ defaultVariants: {
52
+ detached: false,
53
+ },
54
+ });
55
+
36
56
  interface BottomSheetContextValue {
37
57
  open: boolean;
38
58
  setOpen: (open: boolean) => void;
@@ -178,6 +198,20 @@ export interface BottomSheetContentProps extends ViewProps {
178
198
  * sheet; ignored by the native sheet, which has its own dismiss affordances.
179
199
  */
180
200
  showClose?: boolean;
201
+ /**
202
+ * Float the sheet clear of the screen edges instead of docking it to the
203
+ * bottom, so it reads as a card laid over the app rather than a drawer
204
+ * pulled out of it. All four corners round and the bottom border comes back,
205
+ * since a floating sheet has four real edges where a docked one has three.
206
+ * Ignored by the native sheet, which the platform positions itself.
207
+ */
208
+ detached?: boolean;
209
+ /**
210
+ * Frost the screen behind the sheet instead of dimming it, so what is behind
211
+ * stays legible as shape and colour while losing its detail. Needs the
212
+ * optional `expo-blur`; without it this dims, rather than failing.
213
+ */
214
+ blur?: boolean;
181
215
  children?: ReactNode;
182
216
  }
183
217
 
@@ -185,6 +219,8 @@ function BottomSheetContent({
185
219
  className,
186
220
  dismissible = true,
187
221
  showClose = true,
222
+ detached = false,
223
+ blur = false,
188
224
  children,
189
225
  ...props
190
226
  }: BottomSheetContentProps) {
@@ -275,27 +311,29 @@ function BottomSheetContent({
275
311
  subtree — re-provide the context so nested consumers keep working. */}
276
312
  <BottomSheetContext.Provider value={context}>
277
313
  <View className="absolute inset-0 justify-end">
278
- <Animated.View
279
- entering={FadeIn.duration(180)}
280
- exiting={FadeOut.duration(180)}
281
- className="absolute inset-0"
282
- >
314
+ <View className="absolute inset-0">
315
+ {/* Scrim draws the backdrop and its own fade; the Pressable over it
316
+ is what closes the sheet, since the scrim takes no touches. */}
317
+ <Scrim blur={blur} />
283
318
  <Pressable
284
319
  accessibilityLabel="Close sheet"
285
- className="flex-1 bg-black/50"
320
+ className="flex-1"
286
321
  onPress={dismissible ? close : undefined}
287
322
  />
288
- </Animated.View>
323
+ </View>
289
324
  <GestureDetector gesture={pan}>
290
325
  <Animated.View
291
326
  entering={SlideInDown.springify().damping(22).stiffness(240).mass(0.8)}
292
327
  exiting={SlideOutDown.duration(200)}
293
- style={[sheetStyle, { paddingBottom: Math.max(insets.bottom, 16) }]}
328
+ style={[
329
+ sheetStyle,
330
+ // A detached sheet's own bottom margin already clears the home
331
+ // indicator, so it takes plain padding rather than stacking the
332
+ // inset on top of the gap.
333
+ { paddingBottom: detached ? 16 : Math.max(insets.bottom, 16) },
334
+ ]}
294
335
  accessibilityViewIsModal
295
- className={cn(
296
- 'rounded-t-3xl border border-b-0 border-border bg-popover px-5 pt-2 shadow-lg',
297
- className
298
- )}
336
+ className={sheetVariants({ detached, className })}
299
337
  {...props}
300
338
  >
301
339
  <View className="mb-3 self-center">
@@ -8,7 +8,7 @@ import Animated, {
8
8
  } from 'react-native-reanimated';
9
9
  import { tv, type VariantProps } from 'tailwind-variants';
10
10
  import { useCSSVariable } from 'uniwind';
11
- import { CheckIcon } from '../../icons';
11
+ import { CheckIcon, MinusIcon } from '../../icons';
12
12
  import { Text } from '../../primitives/text';
13
13
 
14
14
  const checkboxVariants = tv({
@@ -55,6 +55,14 @@ export interface CheckboxProps extends VariantProps<typeof checkboxVariants> {
55
55
  className?: string;
56
56
  checked: boolean;
57
57
  onCheckedChange?: (checked: boolean) => void;
58
+ /**
59
+ * A third, in-between state for a box that governs a group of others — some
60
+ * on, some off. It fills like a checked box but shows a dash rather than a
61
+ * tick, and announces itself as `mixed` to a screen reader. Pressing it
62
+ * resolves the ambiguity by turning the whole group on, so the press reports
63
+ * `true`. `indeterminate` overrides `checked` for what is drawn.
64
+ */
65
+ indeterminate?: boolean;
58
66
  disabled?: boolean;
59
67
  /** Optional label rendered next to the box; pressing it also toggles. */
60
68
  label?: string;
@@ -64,31 +72,45 @@ export interface CheckboxProps extends VariantProps<typeof checkboxVariants> {
64
72
 
65
73
  export const Checkbox = forwardRef<View, CheckboxProps>(
66
74
  (
67
- { className, checked, onCheckedChange, disabled, label, description, variant },
75
+ {
76
+ className,
77
+ checked,
78
+ onCheckedChange,
79
+ indeterminate,
80
+ disabled,
81
+ label,
82
+ description,
83
+ variant,
84
+ },
68
85
  ref
69
86
  ) => {
70
- const progress = useSharedValue(checked ? 1 : 0);
71
- const slots = checkboxVariants({ variant, checked, disabled: !!disabled });
87
+ // The dash and the tick share one fill, so the box is "on" for either —
88
+ // what changes is the glyph, not whether the fill is shown.
89
+ const on = checked || !!indeterminate;
90
+ const progress = useSharedValue(on ? 1 : 0);
91
+ const slots = checkboxVariants({ variant, checked: on, disabled: !!disabled });
72
92
  const checkColor = useCSSVariable('--color-primary-foreground');
73
93
 
74
94
  useEffect(() => {
75
- progress.value = checked
95
+ progress.value = on
76
96
  ? withSpring(1, { damping: 15, stiffness: 300, mass: 0.5 })
77
97
  : withTiming(0, { duration: 120 });
78
- }, [checked, progress]);
98
+ }, [on, progress]);
79
99
 
80
100
  const fillStyle = useAnimatedStyle(() => ({
81
101
  opacity: progress.value,
82
102
  transform: [{ scale: 0.6 + progress.value * 0.4 }],
83
103
  }));
84
104
 
105
+ const glyphColor = typeof checkColor === 'string' ? checkColor : '#fff';
85
106
  const box = (
86
107
  <View className={slots.box()}>
87
108
  <Animated.View style={fillStyle} className={slots.fill()}>
88
- <CheckIcon
89
- size={12}
90
- color={typeof checkColor === 'string' ? checkColor : '#fff'}
91
- />
109
+ {indeterminate ? (
110
+ <MinusIcon size={12} color={glyphColor} />
111
+ ) : (
112
+ <CheckIcon size={12} color={glyphColor} />
113
+ )}
92
114
  </Animated.View>
93
115
  </View>
94
116
  );
@@ -97,11 +119,17 @@ export const Checkbox = forwardRef<View, CheckboxProps>(
97
119
  <Pressable
98
120
  ref={ref}
99
121
  accessibilityRole="checkbox"
100
- accessibilityState={{ checked, disabled: !!disabled }}
122
+ // `mixed` is the a11y state for a box that governs a partly-on group;
123
+ // a plain boolean would tell a screen reader the group is fully one way.
124
+ accessibilityState={{
125
+ checked: indeterminate ? 'mixed' : checked,
126
+ disabled: !!disabled,
127
+ }}
101
128
  accessibilityLabel={label}
102
129
  accessibilityHint={description}
103
130
  disabled={disabled}
104
- onPress={() => onCheckedChange?.(!checked)}
131
+ // An indeterminate box resolves upward: the press turns the group on.
132
+ onPress={() => onCheckedChange?.(indeterminate ? true : !checked)}
105
133
  hitSlop={8}
106
134
  // A description makes the row two lines tall — align the box to the
107
135
  // label rather than centring it against the whole block.
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Field — the layout and validation-state kit a form control composes into.
3
+ *
4
+ * PanelUI's own controls (`Input`, `Checkbox`, `Switch`, `RadioGroup`) already
5
+ * carry their own label, description and error slot, so the common case needs
6
+ * no wrapper at all — pass those props straight through. `Field` exists for
7
+ * what doesn't fit that shape: a horizontal row pairing a control with a
8
+ * label off to the side (a `Switch` in a settings list), several controls
9
+ * grouped under one legend, or a rule breaking a long form into sections.
10
+ *
11
+ * `invalid` / `disabled` / `required` set on the root flow to `Field.Label`
12
+ * and `Field.Description` through context, so a multi-field row states its
13
+ * validity once instead of repeating the prop at every leaf.
14
+ *
15
+ * ```tsx
16
+ * <Field orientation="horizontal">
17
+ * <Field.Content>
18
+ * <Field.Title>Marketing emails</Field.Title>
19
+ * <Field.Description>Product updates, at most weekly.</Field.Description>
20
+ * </Field.Content>
21
+ * <Switch value={subscribed} onValueChange={setSubscribed} />
22
+ * </Field>
23
+ * ```
24
+ */
25
+ import {
26
+ createContext,
27
+ forwardRef,
28
+ useContext,
29
+ useMemo,
30
+ type ReactNode,
31
+ } from 'react';
32
+ import { View, type Text as RNText, type ViewProps } from 'react-native';
33
+ import { tv } from 'tailwind-variants';
34
+ import { Text, type TextProps } from '../../primitives/text';
35
+ import { Label, type LabelProps } from '../label';
36
+ import { Separator } from '../separator';
37
+
38
+ const fieldVariants = tv({
39
+ slots: {
40
+ root: 'gap-2',
41
+ content: 'flex-1 flex-col gap-0.5',
42
+ description: 'text-sm text-muted-foreground',
43
+ error: 'gap-1',
44
+ errorText: 'text-sm text-destructive',
45
+ bullet: 'flex-row gap-1.5',
46
+ bulletDot: 'text-sm text-destructive',
47
+ set: 'gap-4',
48
+ legend: 'text-foreground',
49
+ group: 'gap-6',
50
+ separator: 'flex-row items-center gap-3',
51
+ separatorLabel: 'text-xs text-muted-foreground',
52
+ title: 'text-base font-medium text-foreground',
53
+ },
54
+ variants: {
55
+ orientation: {
56
+ vertical: { root: 'flex-col' },
57
+ horizontal: { root: 'flex-row items-center justify-between gap-4' },
58
+ },
59
+ disabled: {
60
+ true: { root: 'opacity-[0.64]' },
61
+ },
62
+ legendVariant: {
63
+ legend: { legend: 'text-base font-semibold' },
64
+ label: { legend: 'text-sm font-medium' },
65
+ },
66
+ },
67
+ defaultVariants: {
68
+ orientation: 'vertical',
69
+ },
70
+ });
71
+
72
+ interface FieldState {
73
+ invalid: boolean;
74
+ disabled: boolean;
75
+ required: boolean;
76
+ }
77
+
78
+ const FieldContext = createContext<FieldState>({
79
+ invalid: false,
80
+ disabled: false,
81
+ required: false,
82
+ });
83
+
84
+ export interface FieldProps extends ViewProps {
85
+ className?: string;
86
+ /** `horizontal` puts a label beside the control instead of above it. */
87
+ orientation?: 'vertical' | 'horizontal';
88
+ /** Marks every `Field.Label`/`Field.Description` below as invalid. */
89
+ invalid?: boolean;
90
+ disabled?: boolean;
91
+ /** Marks every `Field.Label` below as required, same as `Label`'s own prop. */
92
+ required?: boolean;
93
+ children?: ReactNode;
94
+ }
95
+
96
+ const FieldRoot = forwardRef<View, FieldProps>(
97
+ (
98
+ {
99
+ className,
100
+ orientation = 'vertical',
101
+ invalid = false,
102
+ disabled = false,
103
+ required = false,
104
+ children,
105
+ ...props
106
+ },
107
+ ref
108
+ ) => {
109
+ const { root } = fieldVariants({ orientation, disabled });
110
+ const state = useMemo(
111
+ () => ({ invalid, disabled, required }),
112
+ [invalid, disabled, required]
113
+ );
114
+
115
+ return (
116
+ <FieldContext.Provider value={state}>
117
+ <View
118
+ ref={ref}
119
+ role="group"
120
+ accessibilityState={{ disabled }}
121
+ className={root({ className })}
122
+ {...props}
123
+ >
124
+ {children}
125
+ </View>
126
+ </FieldContext.Provider>
127
+ );
128
+ }
129
+ );
130
+ FieldRoot.displayName = 'Field';
131
+
132
+ export interface FieldContentProps extends ViewProps {
133
+ className?: string;
134
+ children?: ReactNode;
135
+ }
136
+
137
+ const FieldContent = forwardRef<View, FieldContentProps>(
138
+ ({ className, ...props }, ref) => {
139
+ const { content } = fieldVariants();
140
+ return <View ref={ref} className={content({ className })} {...props} />;
141
+ }
142
+ );
143
+ FieldContent.displayName = 'Field.Content';
144
+
145
+ export interface FieldLabelProps extends LabelProps {}
146
+
147
+ const FieldLabel = forwardRef<View, FieldLabelProps>(
148
+ ({ isRequired, isInvalid, isDisabled, ...props }, ref) => {
149
+ const ctx = useContext(FieldContext);
150
+ return (
151
+ <Label
152
+ ref={ref}
153
+ isRequired={isRequired ?? ctx.required}
154
+ isInvalid={isInvalid ?? ctx.invalid}
155
+ isDisabled={isDisabled ?? ctx.disabled}
156
+ {...props}
157
+ />
158
+ );
159
+ }
160
+ );
161
+ FieldLabel.displayName = 'Field.Label';
162
+
163
+ export interface FieldDescriptionProps extends TextProps {
164
+ className?: string;
165
+ }
166
+
167
+ const FieldDescription = forwardRef<RNText, FieldDescriptionProps>(
168
+ ({ className, ...props }, ref) => {
169
+ const { disabled } = useContext(FieldContext);
170
+ const { description } = fieldVariants({ disabled });
171
+ return <Text ref={ref} className={description({ className })} {...props} />;
172
+ }
173
+ );
174
+ FieldDescription.displayName = 'Field.Description';
175
+
176
+ export interface FieldErrorProps extends Omit<ViewProps, 'children'> {
177
+ className?: string;
178
+ /**
179
+ * Error messages to render, deduplicated by message. A single entry renders
180
+ * as plain text; more than one renders as a bulleted list, since RN has no
181
+ * `<ul>`. Prefer this over `children` when the messages come from a form's
182
+ * validation state, which is naturally an array.
183
+ */
184
+ errors?: Array<string | { message?: string } | null | undefined>;
185
+ children?: ReactNode;
186
+ }
187
+
188
+ const FieldError = forwardRef<View, FieldErrorProps>(
189
+ ({ className, errors, children, ...props }, ref) => {
190
+ const messages = useMemo(() => {
191
+ if (!errors) return null;
192
+ const seen = new Set<string>();
193
+ const out: string[] = [];
194
+ for (const item of errors) {
195
+ const message = typeof item === 'string' ? item : item?.message;
196
+ if (message && !seen.has(message)) {
197
+ seen.add(message);
198
+ out.push(message);
199
+ }
200
+ }
201
+ return out;
202
+ }, [errors]);
203
+
204
+ const hasContent = messages ? messages.length > 0 : !!children;
205
+ if (!hasContent) return null;
206
+
207
+ const { error, errorText, bullet, bulletDot } = fieldVariants();
208
+
209
+ return (
210
+ <View
211
+ ref={ref}
212
+ // Web-aligned `role`, same as `Separator` already uses — RN's older
213
+ // accessibilityRole list has no alert, the ARIA-aligned one does.
214
+ role="alert"
215
+ accessibilityLiveRegion="polite"
216
+ className={error({ className })}
217
+ {...props}
218
+ >
219
+ {messages ? (
220
+ messages.length === 1 ? (
221
+ <Text className={errorText()}>{messages[0]}</Text>
222
+ ) : (
223
+ messages.map((message) => (
224
+ <View key={message} className={bullet()}>
225
+ <Text className={bulletDot()}>{'•'}</Text>
226
+ <Text className={errorText()}>{message}</Text>
227
+ </View>
228
+ ))
229
+ )
230
+ ) : typeof children === 'string' ? (
231
+ <Text className={errorText()}>{children}</Text>
232
+ ) : (
233
+ children
234
+ )}
235
+ </View>
236
+ );
237
+ }
238
+ );
239
+ FieldError.displayName = 'Field.Error';
240
+
241
+ export interface FieldSetProps extends ViewProps {
242
+ className?: string;
243
+ children?: ReactNode;
244
+ }
245
+
246
+ const FieldSet = forwardRef<View, FieldSetProps>(
247
+ ({ className, ...props }, ref) => {
248
+ const { set } = fieldVariants();
249
+ return <View ref={ref} role="group" className={set({ className })} {...props} />;
250
+ }
251
+ );
252
+ FieldSet.displayName = 'Field.Set';
253
+
254
+ export interface FieldLegendProps extends TextProps {
255
+ className?: string;
256
+ /** `legend` reads as a section heading; `label` sits closer to a field label. */
257
+ variant?: 'legend' | 'label';
258
+ }
259
+
260
+ const FieldLegend = forwardRef<RNText, FieldLegendProps>(
261
+ ({ className, variant = 'legend', ...props }, ref) => {
262
+ const { legend } = fieldVariants({ legendVariant: variant });
263
+ return (
264
+ <Text ref={ref} role="heading" className={legend({ className })} {...props} />
265
+ );
266
+ }
267
+ );
268
+ FieldLegend.displayName = 'Field.Legend';
269
+
270
+ export interface FieldGroupProps extends ViewProps {
271
+ className?: string;
272
+ children?: ReactNode;
273
+ }
274
+
275
+ const FieldGroup = forwardRef<View, FieldGroupProps>(
276
+ ({ className, ...props }, ref) => {
277
+ const { group } = fieldVariants();
278
+ return <View ref={ref} className={group({ className })} {...props} />;
279
+ }
280
+ );
281
+ FieldGroup.displayName = 'Field.Group';
282
+
283
+ export interface FieldSeparatorProps extends ViewProps {
284
+ className?: string;
285
+ /** Centered text, e.g. "Or" — omit for a plain rule. */
286
+ children?: ReactNode;
287
+ }
288
+
289
+ const FieldSeparator = forwardRef<View, FieldSeparatorProps>(
290
+ ({ className, children, ...props }, ref) => {
291
+ const { separator, separatorLabel } = fieldVariants();
292
+ return (
293
+ <View ref={ref} className={separator({ className })} {...props}>
294
+ <Separator className="flex-1" />
295
+ {children ? (
296
+ <>
297
+ <Text className={separatorLabel()}>{children}</Text>
298
+ <Separator className="flex-1" />
299
+ </>
300
+ ) : null}
301
+ </View>
302
+ );
303
+ }
304
+ );
305
+ FieldSeparator.displayName = 'Field.Separator';
306
+
307
+ export interface FieldTitleProps extends TextProps {
308
+ className?: string;
309
+ }
310
+
311
+ const FieldTitle = forwardRef<RNText, FieldTitleProps>(
312
+ ({ className, ...props }, ref) => {
313
+ const { title } = fieldVariants();
314
+ return <Text ref={ref} className={title({ className })} {...props} />;
315
+ }
316
+ );
317
+ FieldTitle.displayName = 'Field.Title';
318
+
319
+ export const Field = Object.assign(FieldRoot, {
320
+ Content: FieldContent,
321
+ Label: FieldLabel,
322
+ Description: FieldDescription,
323
+ Error: FieldError,
324
+ Set: FieldSet,
325
+ Legend: FieldLegend,
326
+ Group: FieldGroup,
327
+ Separator: FieldSeparator,
328
+ Title: FieldTitle,
329
+ });