@expokit/cli 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +32 -2
- package/bin/expo-kit.js +1204 -0
- package/bin/lib.mjs +343 -0
- package/bin/mcp.mjs +338 -0
- package/package.json +44 -4
- package/registry/_docs/accordion.json +99 -0
- package/registry/_docs/action-sheet.json +110 -0
- package/registry/_docs/alert.json +127 -0
- package/registry/_docs/avatar.json +203 -0
- package/registry/_docs/badge.json +117 -0
- package/registry/_docs/bottom-nav.json +119 -0
- package/registry/_docs/bottom-sheet.json +158 -0
- package/registry/_docs/bullet.json +24 -0
- package/registry/_docs/button-3d.json +122 -0
- package/registry/_docs/button.json +115 -0
- package/registry/_docs/calendar.json +182 -0
- package/registry/_docs/card-stack.json +108 -0
- package/registry/_docs/card.json +111 -0
- package/registry/_docs/carousel.json +102 -0
- package/registry/_docs/checkbox.json +93 -0
- package/registry/_docs/checkout.json +102 -0
- package/registry/_docs/chip-group.json +116 -0
- package/registry/_docs/color-swatch.json +66 -0
- package/registry/_docs/confetti.json +82 -0
- package/registry/_docs/coupon.json +176 -0
- package/registry/_docs/cube-pager.json +84 -0
- package/registry/_docs/dashboard.json +120 -0
- package/registry/_docs/date-picker.json +249 -0
- package/registry/_docs/dialog.json +162 -0
- package/registry/_docs/drawer.json +171 -0
- package/registry/_docs/fab.json +120 -0
- package/registry/_docs/flip-card.json +111 -0
- package/registry/_docs/floating-input.json +191 -0
- package/registry/_docs/gift-card.json +108 -0
- package/registry/_docs/glass-button.json +118 -0
- package/registry/_docs/glass-nav-bar.json +78 -0
- package/registry/_docs/glass-search-bar.json +90 -0
- package/registry/_docs/glass-segmented.json +66 -0
- package/registry/_docs/glass-sheet.json +108 -0
- package/registry/_docs/glass-tab-bar.json +76 -0
- package/registry/_docs/glass.json +78 -0
- package/registry/_docs/inbox.json +72 -0
- package/registry/_docs/isometric-bar.json +72 -0
- package/registry/_docs/kit.json +11 -0
- package/registry/_docs/kpi-card.json +11 -0
- package/registry/_docs/like-button.json +72 -0
- package/registry/_docs/liquidglass-tab.json +27 -0
- package/registry/_docs/list-item.json +200 -0
- package/registry/_docs/loyalty-card.json +108 -0
- package/registry/_docs/morph-button.json +108 -0
- package/registry/_docs/number-ticker.json +96 -0
- package/registry/_docs/offers.json +78 -0
- package/registry/_docs/onboarding.json +66 -0
- package/registry/_docs/otp-input.json +134 -0
- package/registry/_docs/pagination-dots.json +88 -0
- package/registry/_docs/parallax-layers.json +84 -0
- package/registry/_docs/parallax-scroll.json +186 -0
- package/registry/_docs/password-input.json +92 -0
- package/registry/_docs/payment-card.json +153 -0
- package/registry/_docs/pixel-bar.json +66 -0
- package/registry/_docs/polar-area.json +78 -0
- package/registry/_docs/profile.json +84 -0
- package/registry/_docs/progress-steps.json +92 -0
- package/registry/_docs/progress.json +132 -0
- package/registry/_docs/radio-group.json +92 -0
- package/registry/_docs/rating.json +90 -0
- package/registry/_docs/ring-carousel.json +102 -0
- package/registry/_docs/scratch-card.json +90 -0
- package/registry/_docs/search-bar.json +140 -0
- package/registry/_docs/search-results.json +120 -0
- package/registry/_docs/segmented-control.json +103 -0
- package/registry/_docs/select.json +252 -0
- package/registry/_docs/settings.json +72 -0
- package/registry/_docs/shimmer-button.json +78 -0
- package/registry/_docs/sign-in.json +198 -0
- package/registry/_docs/skeleton.json +142 -0
- package/registry/_docs/slide-to-confirm.json +84 -0
- package/registry/_docs/slider.json +157 -0
- package/registry/_docs/spin-wheel.json +114 -0
- package/registry/_docs/spinner.json +91 -0
- package/registry/_docs/stepper.json +168 -0
- package/registry/_docs/swipe-deck.json +102 -0
- package/registry/_docs/swipeable-row.json +72 -0
- package/registry/_docs/switch.json +104 -0
- package/registry/_docs/tabs.json +105 -0
- package/registry/_docs/tag-input.json +128 -0
- package/registry/_docs/textarea.json +175 -0
- package/registry/_docs/ticket-card.json +174 -0
- package/registry/_docs/ticket-shape.json +11 -0
- package/registry/_docs/tilt-card.json +141 -0
- package/registry/_docs/time-picker.json +253 -0
- package/registry/_docs/timeline-chart.json +30 -0
- package/registry/_docs/timeline.json +156 -0
- package/registry/_docs/toast.json +161 -0
- package/registry/_docs/treemap.json +30 -0
- package/registry/_docs/waterfall.json +36 -0
- package/registry/_docs/wheel-list.json +102 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
- package/registry/accordion/files/src/lib/kit.tsx +219 -0
- package/registry/accordion.json +17 -0
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
- package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
- package/registry/action-sheet.json +18 -0
- package/registry/alert/files/src/components/ui/alert.tsx +654 -0
- package/registry/alert/files/src/components/ui/button.tsx +274 -0
- package/registry/alert/files/src/lib/kit.tsx +219 -0
- package/registry/alert.json +20 -0
- package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
- package/registry/avatar/files/src/lib/kit.tsx +219 -0
- package/registry/avatar.json +18 -0
- package/registry/badge/files/src/components/ui/badge.tsx +316 -0
- package/registry/badge/files/src/lib/kit.tsx +219 -0
- package/registry/badge.json +17 -0
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
- package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
- package/registry/bottom-nav.json +18 -0
- package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
- package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
- package/registry/bottom-sheet.json +17 -0
- package/registry/bullet.json +14 -0
- package/registry/button/files/src/components/ui/button.tsx +274 -0
- package/registry/button/files/src/lib/kit.tsx +219 -0
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
- package/registry/button-3d/files/src/lib/kit.tsx +219 -0
- package/registry/button-3d.json +18 -0
- package/registry/button.json +18 -0
- package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
- package/registry/calendar/files/src/lib/kit.tsx +219 -0
- package/registry/calendar.json +17 -0
- package/registry/card/files/src/components/ui/card.tsx +300 -0
- package/registry/card/files/src/lib/kit.tsx +219 -0
- package/registry/card-stack.json +15 -0
- package/registry/card.json +17 -0
- package/registry/carousel.json +15 -0
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
- package/registry/checkbox/files/src/lib/kit.tsx +219 -0
- package/registry/checkbox.json +17 -0
- package/registry/checkout.json +26 -0
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
- package/registry/chip-group/files/src/lib/kit.tsx +219 -0
- package/registry/chip-group.json +17 -0
- package/registry/color-swatch.json +17 -0
- package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
- package/registry/confetti/files/src/lib/kit.tsx +219 -0
- package/registry/confetti.json +15 -0
- package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
- package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
- package/registry/coupon/files/src/lib/kit.tsx +219 -0
- package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
- package/registry/coupon.json +23 -0
- package/registry/cube-pager.json +15 -0
- package/registry/dashboard.json +32 -0
- package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
- package/registry/date-picker/files/src/lib/kit.tsx +219 -0
- package/registry/date-picker.json +19 -0
- package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
- package/registry/dialog/files/src/lib/kit.tsx +219 -0
- package/registry/dialog.json +18 -0
- package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
- package/registry/drawer/files/src/lib/kit.tsx +219 -0
- package/registry/drawer.json +18 -0
- package/registry/fab.json +18 -0
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
- package/registry/flip-card/files/src/lib/kit.tsx +219 -0
- package/registry/flip-card.json +15 -0
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
- package/registry/floating-input/files/src/lib/kit.tsx +219 -0
- package/registry/floating-input.json +17 -0
- package/registry/gift-card.json +21 -0
- package/registry/glass-button.json +22 -0
- package/registry/glass-nav-bar.json +21 -0
- package/registry/glass-search-bar.json +21 -0
- package/registry/glass-segmented.json +22 -0
- package/registry/glass-sheet.json +22 -0
- package/registry/glass-tab-bar.json +22 -0
- package/registry/glass.json +19 -0
- package/registry/inbox.json +30 -0
- package/registry/isometric-bar.json +14 -0
- package/registry/kit/files/src/lib/kit.tsx +219 -0
- package/registry/kit.json +11 -0
- package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
- package/registry/kpi-card.json +14 -0
- package/registry/like-button.json +18 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
- package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
- package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
- package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
- package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
- package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
- package/registry/liquidglass-tab.json +23 -0
- package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
- package/registry/list-item/files/src/lib/kit.tsx +219 -0
- package/registry/list-item.json +17 -0
- package/registry/loyalty-card.json +23 -0
- package/registry/morph-button.json +18 -0
- package/registry/number-ticker.json +15 -0
- package/registry/offers.json +24 -0
- package/registry/onboarding.json +17 -0
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
- package/registry/otp-input/files/src/lib/kit.tsx +219 -0
- package/registry/otp-input.json +17 -0
- package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
- package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
- package/registry/pagination-dots.json +15 -0
- package/registry/parallax-layers.json +15 -0
- package/registry/parallax-scroll.json +17 -0
- package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
- package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
- package/registry/password-input/files/src/lib/kit.tsx +219 -0
- package/registry/password-input.json +19 -0
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
- package/registry/payment-card/files/src/lib/kit.tsx +219 -0
- package/registry/payment-card.json +21 -0
- package/registry/pixel-bar.json +14 -0
- package/registry/polar-area.json +14 -0
- package/registry/profile.json +17 -0
- package/registry/progress/files/src/components/ui/progress.tsx +883 -0
- package/registry/progress/files/src/lib/kit.tsx +219 -0
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
- package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
- package/registry/progress-steps.json +18 -0
- package/registry/progress.json +17 -0
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
- package/registry/radio-group/files/src/lib/kit.tsx +219 -0
- package/registry/radio-group.json +18 -0
- package/registry/rating.json +18 -0
- package/registry/ring-carousel.json +15 -0
- package/registry/scratch-card.json +18 -0
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
- package/registry/search-bar/files/src/lib/kit.tsx +219 -0
- package/registry/search-bar.json +17 -0
- package/registry/search-results.json +30 -0
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
- package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
- package/registry/segmented-control.json +18 -0
- package/registry/select/files/src/components/ui/select.tsx +1398 -0
- package/registry/select/files/src/lib/kit.tsx +219 -0
- package/registry/select.json +17 -0
- package/registry/settings.json +26 -0
- package/registry/shimmer-button.json +17 -0
- package/registry/sign-in.json +26 -0
- package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
- package/registry/skeleton/files/src/lib/kit.tsx +219 -0
- package/registry/skeleton.json +17 -0
- package/registry/slide-to-confirm.json +18 -0
- package/registry/slider/files/src/components/ui/slider.tsx +716 -0
- package/registry/slider/files/src/lib/kit.tsx +219 -0
- package/registry/slider.json +17 -0
- package/registry/spin-wheel.json +18 -0
- package/registry/spinner/files/src/components/ui/spinner.tsx +335 -0
- package/registry/spinner/files/src/lib/kit.tsx +219 -0
- package/registry/spinner.json +17 -0
- package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
- package/registry/stepper/files/src/lib/kit.tsx +219 -0
- package/registry/stepper.json +17 -0
- package/registry/swipe-deck.json +15 -0
- package/registry/swipeable-row.json +15 -0
- package/registry/switch/files/src/components/ui/switch.tsx +344 -0
- package/registry/switch/files/src/lib/kit.tsx +219 -0
- package/registry/switch.json +18 -0
- package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
- package/registry/tabs/files/src/lib/kit.tsx +219 -0
- package/registry/tabs.json +17 -0
- package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
- package/registry/tag-input/files/src/lib/kit.tsx +219 -0
- package/registry/tag-input.json +17 -0
- package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
- package/registry/textarea/files/src/lib/kit.tsx +219 -0
- package/registry/textarea.json +17 -0
- package/registry/ticket-card.json +24 -0
- package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
- package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
- package/registry/ticket-shape.json +17 -0
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
- package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
- package/registry/tilt-card.json +17 -0
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
- package/registry/time-picker/files/src/lib/kit.tsx +219 -0
- package/registry/time-picker.json +17 -0
- package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
- package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
- package/registry/timeline/files/src/lib/kit.tsx +219 -0
- package/registry/timeline-chart.json +14 -0
- package/registry/timeline.json +20 -0
- package/registry/toast/files/src/components/ui/button.tsx +274 -0
- package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
- package/registry/toast/files/src/lib/kit.tsx +219 -0
- package/registry/toast.json +20 -0
- package/registry/treemap.json +14 -0
- package/registry/waterfall.json +14 -0
- package/registry/wheel-list.json +17 -0
- package/schema/components.schema.json +13 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Easing,
|
|
5
|
+
Platform,
|
|
6
|
+
Pressable,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Text,
|
|
9
|
+
TextInput,
|
|
10
|
+
View,
|
|
11
|
+
type StyleProp,
|
|
12
|
+
type TextInputProps,
|
|
13
|
+
type TextStyle,
|
|
14
|
+
type ViewStyle,
|
|
15
|
+
} from 'react-native';
|
|
16
|
+
import Svg, { Circle, Path } from 'react-native-svg';
|
|
17
|
+
|
|
18
|
+
import { alpha, motion, useKit } from '@/lib/kit';
|
|
19
|
+
|
|
20
|
+
export type FloatingInputVariant = 'outlined' | 'filled' | 'underline';
|
|
21
|
+
|
|
22
|
+
export interface FloatingInputProps extends Omit<TextInputProps, 'style' | 'placeholder'> {
|
|
23
|
+
/** Sits inside the field like a placeholder, then floats onto the border when focused or filled. */
|
|
24
|
+
label: string;
|
|
25
|
+
/** `false` puts the label above the field as static text (no notch) and shows `placeholder` inside. */
|
|
26
|
+
floatingLabel?: boolean;
|
|
27
|
+
/** Hint inside the empty field — only used when `floatingLabel` is false. */
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
variant?: FloatingInputVariant;
|
|
30
|
+
/** Error message. Turns the field red, shows it with an icon and shakes the field once when it appears. */
|
|
31
|
+
error?: string;
|
|
32
|
+
/** Valid state: success border and an animated check. A string is shown as the message under the field. */
|
|
33
|
+
success?: boolean | string;
|
|
34
|
+
/** Hint under the field (replaced by the error / success message while one is shown). */
|
|
35
|
+
helper?: string;
|
|
36
|
+
/** Colour of the focused border, halo and floating label (default: theme accent). */
|
|
37
|
+
accent?: string;
|
|
38
|
+
/** Background behind the field — the floating label cuts its notch with this colour. */
|
|
39
|
+
surface?: string;
|
|
40
|
+
left?: React.ReactNode;
|
|
41
|
+
right?: React.ReactNode;
|
|
42
|
+
/** Text before the value, shown once the label floats — '₹', '+91', 'https://'. */
|
|
43
|
+
prefix?: string;
|
|
44
|
+
/** Text after the value, shown once the label floats — 'INR', 'kg', '.com'. */
|
|
45
|
+
suffix?: string;
|
|
46
|
+
/** Show an animated clear button while there is text. */
|
|
47
|
+
clearable?: boolean;
|
|
48
|
+
/** Show `length/maxLength` under the field (needs `maxLength`). */
|
|
49
|
+
counter?: boolean;
|
|
50
|
+
/** 'sm' 44pt / 14px · 'md' 54pt / 16px · 'lg' 64pt / 18px — type, padding and icons scale too. */
|
|
51
|
+
size?: 'sm' | 'md' | 'lg';
|
|
52
|
+
/** Root wrapper (the one that shakes) — margins, width, flex. */
|
|
53
|
+
style?: StyleProp<ViewStyle>;
|
|
54
|
+
/**
|
|
55
|
+
* The bordered box: background, border width / colour, radius, height, padding. Applied after
|
|
56
|
+
* the computed style, so it wins. A `borderColor` replaces the resting border only — focus
|
|
57
|
+
* (accent), error and success colours still take over in those states. A numeric `height` /
|
|
58
|
+
* `borderRadius` also re-centres the label and resizes the focus halo.
|
|
59
|
+
*/
|
|
60
|
+
fieldStyle?: StyleProp<ViewStyle>;
|
|
61
|
+
/** The typed text — fontSize, fontWeight, color, letterSpacing, fontFamily… */
|
|
62
|
+
inputStyle?: StyleProp<TextStyle>;
|
|
63
|
+
/**
|
|
64
|
+
* The label, floating or above the field. Its `color` is the resting colour; focus (accent),
|
|
65
|
+
* error (danger) and success colours still take over in those states. With `fontSize` the
|
|
66
|
+
* floating label rests at that size and floats at the same ratio as the default, and the notch
|
|
67
|
+
* follows the rendered label width.
|
|
68
|
+
*/
|
|
69
|
+
labelStyle?: StyleProp<TextStyle>;
|
|
70
|
+
/** Helper / error / success text under the field. `color` applies to the helper; error and success keep their tone colour. */
|
|
71
|
+
messageStyle?: StyleProp<TextStyle>;
|
|
72
|
+
/** `prefix` text. */
|
|
73
|
+
prefixStyle?: StyleProp<TextStyle>;
|
|
74
|
+
/** `suffix` text. */
|
|
75
|
+
suffixStyle?: StyleProp<TextStyle>;
|
|
76
|
+
/** The `length/maxLength` counter. Turns danger at the limit regardless. */
|
|
77
|
+
counterStyle?: StyleProp<TextStyle>;
|
|
78
|
+
/** Built-in icons at rest — the clear button (default: theme mutedForeground). */
|
|
79
|
+
iconColor?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Everything that scales with `size`: height, type, padding, icons and the floated label. */
|
|
83
|
+
export const FLOATING_INPUT_SIZES = {
|
|
84
|
+
sm: { height: 44, font: 14, floated: 11, padX: 12, icon: 16, radius: -2 },
|
|
85
|
+
md: { height: 54, font: 16, floated: 12.5, padX: 16, icon: 18, radius: 0 },
|
|
86
|
+
lg: { height: 64, font: 18, floated: 13.5, padX: 18, icon: 20, radius: 4 },
|
|
87
|
+
};
|
|
88
|
+
const HALO = 4;
|
|
89
|
+
/** Font size of the static label used when `floatingLabel` is false. */
|
|
90
|
+
const LABEL_ABOVE_FONT = { sm: 13, md: 14, lg: 14.5 };
|
|
91
|
+
/** react-native-web draws its own focus rectangle; our border + halo is the focus indicator. */
|
|
92
|
+
const NO_WEB_OUTLINE = Platform.OS === 'web' ? ({ outlineStyle: 'none', outlineWidth: 0 } as object) : null;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Web only: browser autofill paints its own (blue) background and fills the input without an
|
|
96
|
+
* onChangeText, so the label would sit on top of the text. This stylesheet keeps the field's own
|
|
97
|
+
* background and fires a 1ms animation we listen for to float the label.
|
|
98
|
+
*/
|
|
99
|
+
const AUTOFILL_CSS = `@keyframes ekAutofillOn{from{}to{}}@keyframes ekAutofillOff{from{}to{}}
|
|
100
|
+
input:-webkit-autofill,input:-webkit-autofill:hover,input:-webkit-autofill:focus{animation:ekAutofillOn 1ms;-webkit-text-fill-color:currentColor!important;caret-color:currentColor;transition:background-color 99999s 0s,color 99999s 0s!important}
|
|
101
|
+
input:not(:-webkit-autofill){animation:ekAutofillOff 1ms}`;
|
|
102
|
+
function useWebAutofill(inputRef: React.RefObject<TextInput | null>, onChange: (filled: boolean) => void) {
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (Platform.OS !== 'web' || typeof document === 'undefined') return;
|
|
105
|
+
if (!document.getElementById('expokit-autofill')) {
|
|
106
|
+
const tag = document.createElement('style');
|
|
107
|
+
tag.id = 'expokit-autofill';
|
|
108
|
+
tag.textContent = AUTOFILL_CSS;
|
|
109
|
+
document.head.appendChild(tag);
|
|
110
|
+
}
|
|
111
|
+
const node = inputRef.current as unknown as HTMLElement | null;
|
|
112
|
+
if (!node?.addEventListener) return;
|
|
113
|
+
const onAnim = (e: Event) => {
|
|
114
|
+
const name = (e as AnimationEvent).animationName;
|
|
115
|
+
if (name === 'ekAutofillOn') onChange(true);
|
|
116
|
+
else if (name === 'ekAutofillOff') onChange(false);
|
|
117
|
+
};
|
|
118
|
+
node.addEventListener('animationstart', onAnim);
|
|
119
|
+
return () => node.removeEventListener('animationstart', onAnim);
|
|
120
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
121
|
+
}, []);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Text field whose label starts as the placeholder and glides up onto the border on focus,
|
|
126
|
+
* cutting a notch in the outline (Material-style). Focus draws a soft halo, errors shake,
|
|
127
|
+
* success pops a check — all spring-driven.
|
|
128
|
+
*/
|
|
129
|
+
export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function FloatingInput(
|
|
130
|
+
{
|
|
131
|
+
label,
|
|
132
|
+
floatingLabel = true,
|
|
133
|
+
placeholder,
|
|
134
|
+
variant = 'outlined',
|
|
135
|
+
error,
|
|
136
|
+
success,
|
|
137
|
+
helper,
|
|
138
|
+
accent,
|
|
139
|
+
surface,
|
|
140
|
+
left,
|
|
141
|
+
right,
|
|
142
|
+
prefix,
|
|
143
|
+
suffix,
|
|
144
|
+
clearable,
|
|
145
|
+
counter,
|
|
146
|
+
size = 'md',
|
|
147
|
+
style,
|
|
148
|
+
fieldStyle,
|
|
149
|
+
inputStyle,
|
|
150
|
+
labelStyle: labelStyleProp,
|
|
151
|
+
messageStyle,
|
|
152
|
+
prefixStyle,
|
|
153
|
+
suffixStyle,
|
|
154
|
+
counterStyle,
|
|
155
|
+
iconColor,
|
|
156
|
+
placeholderTextColor,
|
|
157
|
+
value,
|
|
158
|
+
defaultValue,
|
|
159
|
+
onChangeText,
|
|
160
|
+
onFocus,
|
|
161
|
+
onBlur,
|
|
162
|
+
maxLength,
|
|
163
|
+
editable = true,
|
|
164
|
+
...props
|
|
165
|
+
},
|
|
166
|
+
ref
|
|
167
|
+
) {
|
|
168
|
+
const { colors, radius, font } = useKit();
|
|
169
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
170
|
+
const S = FLOATING_INPUT_SIZES[size];
|
|
171
|
+
// The user's border colour only replaces the resting one; numeric height / radius drive the layout maths.
|
|
172
|
+
const { borderColor: userBorder, ...userField } = StyleSheet.flatten(fieldStyle) ?? {};
|
|
173
|
+
const userLabelColor = StyleSheet.flatten(labelStyleProp)?.color;
|
|
174
|
+
const HEIGHT = typeof userField.height === 'number' ? userField.height : S.height;
|
|
175
|
+
// Floated size stays relative to the resting size, so a bigger label floats bigger at the same ratio.
|
|
176
|
+
const FLOAT_SCALE = S.floated / S.font;
|
|
177
|
+
const corner = typeof userField.borderRadius === 'number' ? userField.borderRadius : radius.scale(12 + S.radius);
|
|
178
|
+
const adorn = S.padX + S.icon + 10; // room for a left / right adornment
|
|
179
|
+
const slot = S.icon + 10; // room for the clear button / check
|
|
180
|
+
const tint = accent ?? colors.accent;
|
|
181
|
+
const bg = surface ?? colors.background;
|
|
182
|
+
const inputRef = useRef<TextInput>(null);
|
|
183
|
+
useImperativeHandle(ref, () => inputRef.current as TextInput);
|
|
184
|
+
|
|
185
|
+
const [focused, setFocused] = useState(false);
|
|
186
|
+
const [inner, setInner] = useState(defaultValue ?? '');
|
|
187
|
+
const [autofilled, setAutofilled] = useState(false);
|
|
188
|
+
useWebAutofill(inputRef, setAutofilled);
|
|
189
|
+
const text = value ?? inner;
|
|
190
|
+
const floating = focused || text.length > 0 || autofilled;
|
|
191
|
+
const isFocused = focused && editable;
|
|
192
|
+
const ok = !error && !!success;
|
|
193
|
+
const successText = typeof success === 'string' && ok ? success : undefined;
|
|
194
|
+
|
|
195
|
+
const float = useRef(new Animated.Value(floating ? 1 : 0)).current; // native: label + affixes
|
|
196
|
+
const glow = useRef(new Animated.Value(0)).current; // native: halo + indicator bar
|
|
197
|
+
const focus = useRef(new Animated.Value(0)).current; // JS: border / label colour
|
|
198
|
+
const shake = useRef(new Animated.Value(0)).current; // native
|
|
199
|
+
const clear = useRef(new Animated.Value(clearable && text ? 1 : 0)).current; // native
|
|
200
|
+
const check = useRef(new Animated.Value(ok ? 1 : 0)).current; // native
|
|
201
|
+
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
Animated.spring(float, { toValue: floating ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
204
|
+
}, [floating, float]);
|
|
205
|
+
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
Animated.timing(focus, { toValue: isFocused ? 1 : 0, duration: 180, easing: Easing.out(Easing.quad), useNativeDriver: false }).start();
|
|
208
|
+
Animated.spring(glow, { toValue: isFocused ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
209
|
+
}, [isFocused, focus, glow]);
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
Animated.spring(clear, { toValue: clearable && text && editable ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
|
|
213
|
+
}, [clearable, text, editable, clear]);
|
|
214
|
+
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
Animated.spring(check, { toValue: ok ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
|
|
217
|
+
}, [ok, check]);
|
|
218
|
+
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (!error) return;
|
|
221
|
+
shake.setValue(0);
|
|
222
|
+
Animated.sequence(
|
|
223
|
+
[10, -8, 6, -4, 0].map((x) => Animated.timing(shake, { toValue: x, duration: 55, useNativeDriver: true }))
|
|
224
|
+
).start();
|
|
225
|
+
}, [error, shake]);
|
|
226
|
+
|
|
227
|
+
const setText = (next: string) => {
|
|
228
|
+
if (value === undefined) setInner(next);
|
|
229
|
+
onChangeText?.(next);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const active = error ? colors.danger : tint;
|
|
233
|
+
const rest = error ? colors.danger : ok ? colors.success : ((userBorder as string | undefined) ?? colors.input);
|
|
234
|
+
const borderColor = focus.interpolate({ inputRange: [0, 1], outputRange: [rest, active] });
|
|
235
|
+
const labelColor = focus.interpolate({
|
|
236
|
+
inputRange: [0, 1],
|
|
237
|
+
outputRange: [error ? colors.danger : ok ? colors.success : ((userLabelColor as string | undefined) ?? colors.mutedForeground), active],
|
|
238
|
+
});
|
|
239
|
+
const glowClamped = glow.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' });
|
|
240
|
+
|
|
241
|
+
const outlined = variant === 'outlined';
|
|
242
|
+
const filled = variant === 'filled';
|
|
243
|
+
const underline = variant === 'underline';
|
|
244
|
+
const leftPad = underline ? (left ? S.icon + 14 : 2) : left ? adorn : S.padX;
|
|
245
|
+
const edge = right ? adorn : underline ? 0 : S.padX - 4;
|
|
246
|
+
const rightPad = (right ? adorn : underline ? 2 : S.padX) + (clearable ? slot : 0) + (ok ? slot : 0);
|
|
247
|
+
// Filled / underline push the text down so the floated label has room above it.
|
|
248
|
+
const contentTop = !floatingLabel ? 0 : filled ? HEIGHT / 3.2 : underline ? HEIGHT / 4.5 : 0;
|
|
249
|
+
const restCenter = (HEIGHT + contentTop) / 2;
|
|
250
|
+
const floatCenter = outlined ? 0 : filled ? HEIGHT * 0.26 : HEIGHT * 0.2;
|
|
251
|
+
const floatY = floatCenter - restCenter;
|
|
252
|
+
const floatX = outlined ? S.padX - 4 - leftPad : 0;
|
|
253
|
+
|
|
254
|
+
const labelStyle = {
|
|
255
|
+
transform: [
|
|
256
|
+
{ translateY: float.interpolate({ inputRange: [0, 1], outputRange: [0, floatY] }) },
|
|
257
|
+
{ translateX: float.interpolate({ inputRange: [0, 1], outputRange: [0, floatX] }) },
|
|
258
|
+
{ scale: float.interpolate({ inputRange: [0, 1], outputRange: [1, FLOAT_SCALE] }) },
|
|
259
|
+
],
|
|
260
|
+
};
|
|
261
|
+
// With the label above the field there is nothing to make room for, so affixes always show.
|
|
262
|
+
const affixOpacity = floatingLabel ? float.interpolate({ inputRange: [0.6, 1], outputRange: [0, 1], extrapolate: 'clamp' }) : 1;
|
|
263
|
+
const topLabelColor = focus.interpolate({
|
|
264
|
+
inputRange: [0, 1],
|
|
265
|
+
outputRange: [error ? colors.danger : ((userLabelColor as string | undefined) ?? colors.foreground), active],
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const boxStyle: ViewStyle = outlined
|
|
269
|
+
? { borderWidth: 1.5, borderRadius: corner, backgroundColor: bg }
|
|
270
|
+
: filled
|
|
271
|
+
? { borderRadius: corner, backgroundColor: colors.muted, overflow: 'hidden' }
|
|
272
|
+
: { backgroundColor: 'transparent' };
|
|
273
|
+
|
|
274
|
+
const message = error ?? successText ?? helper;
|
|
275
|
+
const tone: MessageTone = error ? 'error' : successText ? 'success' : 'helper';
|
|
276
|
+
const toneColor = tone === 'error' ? colors.danger : tone === 'success' ? colors.success : colors.mutedForeground;
|
|
277
|
+
const restIcon = iconColor ?? colors.mutedForeground;
|
|
278
|
+
const showCounter = counter && maxLength !== undefined;
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<Animated.View style={[{ transform: [{ translateX: shake }] }, style]}>
|
|
282
|
+
{floatingLabel ? null : (
|
|
283
|
+
<Animated.Text
|
|
284
|
+
numberOfLines={1}
|
|
285
|
+
onPress={editable ? () => inputRef.current?.focus() : undefined}
|
|
286
|
+
// The input already carries the label for screen readers.
|
|
287
|
+
accessible={false}
|
|
288
|
+
importantForAccessibility="no"
|
|
289
|
+
accessibilityElementsHidden
|
|
290
|
+
style={[
|
|
291
|
+
styles.topLabel,
|
|
292
|
+
ff,
|
|
293
|
+
{ fontSize: LABEL_ABOVE_FONT[size] },
|
|
294
|
+
underline && styles.topLabelFlush,
|
|
295
|
+
labelStyleProp,
|
|
296
|
+
{ color: topLabelColor },
|
|
297
|
+
!editable && styles.disabled,
|
|
298
|
+
]}
|
|
299
|
+
>
|
|
300
|
+
{label}
|
|
301
|
+
</Animated.Text>
|
|
302
|
+
)}
|
|
303
|
+
<View style={!editable && styles.disabled}>
|
|
304
|
+
{underline ? null : (
|
|
305
|
+
<Animated.View
|
|
306
|
+
pointerEvents="none"
|
|
307
|
+
style={[
|
|
308
|
+
styles.halo,
|
|
309
|
+
{
|
|
310
|
+
borderRadius: corner + HALO,
|
|
311
|
+
backgroundColor: active,
|
|
312
|
+
opacity: glow.interpolate({ inputRange: [0, 1], outputRange: [0, 0.18], extrapolate: 'clamp' }),
|
|
313
|
+
transform: [{ scale: glow.interpolate({ inputRange: [0, 1], outputRange: [0.97, 1] }) }],
|
|
314
|
+
},
|
|
315
|
+
]}
|
|
316
|
+
/>
|
|
317
|
+
)}
|
|
318
|
+
<Pressable onPress={() => inputRef.current?.focus()} disabled={!editable} accessible={false}>
|
|
319
|
+
<Animated.View style={[styles.field, boxStyle, { height: HEIGHT }, userField, { borderColor }]}>
|
|
320
|
+
{left ? <View style={[styles.left, { left: underline ? 2 : S.padX - 2 }, underline && { top: contentTop }]}>{left}</View> : null}
|
|
321
|
+
<View style={[styles.row, { paddingLeft: leftPad, paddingRight: rightPad, paddingTop: contentTop }]}>
|
|
322
|
+
{prefix ? (
|
|
323
|
+
<Animated.Text style={[styles.affix, styles.prefix, ff, { fontSize: S.font }, { color: colors.mutedForeground }, prefixStyle, { opacity: affixOpacity }]}>
|
|
324
|
+
{prefix}
|
|
325
|
+
</Animated.Text>
|
|
326
|
+
) : null}
|
|
327
|
+
<TextInput
|
|
328
|
+
ref={inputRef}
|
|
329
|
+
accessibilityLabel={label}
|
|
330
|
+
{...props}
|
|
331
|
+
maxLength={maxLength}
|
|
332
|
+
editable={editable}
|
|
333
|
+
value={text}
|
|
334
|
+
onChangeText={setText}
|
|
335
|
+
onFocus={(e) => {
|
|
336
|
+
setFocused(true);
|
|
337
|
+
onFocus?.(e);
|
|
338
|
+
}}
|
|
339
|
+
onBlur={(e) => {
|
|
340
|
+
setFocused(false);
|
|
341
|
+
onBlur?.(e);
|
|
342
|
+
}}
|
|
343
|
+
placeholder={floatingLabel ? undefined : placeholder}
|
|
344
|
+
placeholderTextColor={placeholderTextColor ?? colors.mutedForeground}
|
|
345
|
+
selectionColor={active}
|
|
346
|
+
cursorColor={active}
|
|
347
|
+
style={[styles.input, ff, { color: colors.foreground, fontSize: S.font }, inputStyle, NO_WEB_OUTLINE]}
|
|
348
|
+
/>
|
|
349
|
+
{suffix ? (
|
|
350
|
+
<Animated.Text style={[styles.affix, styles.suffix, ff, { fontSize: S.font }, { color: colors.mutedForeground }, suffixStyle, { opacity: affixOpacity }]}>
|
|
351
|
+
{suffix}
|
|
352
|
+
</Animated.Text>
|
|
353
|
+
) : null}
|
|
354
|
+
</View>
|
|
355
|
+
{floatingLabel ? (
|
|
356
|
+
<Animated.View
|
|
357
|
+
pointerEvents="none"
|
|
358
|
+
style={[styles.labelWrap, { left: leftPad, right: right ? adorn : S.padX - 4, top: contentTop, height: HEIGHT - contentTop }, labelStyle]}
|
|
359
|
+
>
|
|
360
|
+
<Animated.Text
|
|
361
|
+
numberOfLines={1}
|
|
362
|
+
style={[
|
|
363
|
+
styles.label,
|
|
364
|
+
ff,
|
|
365
|
+
{
|
|
366
|
+
fontSize: S.font,
|
|
367
|
+
// The notch: label paints the surface colour over the border line, so it is
|
|
368
|
+
// always exactly as wide as the rendered label.
|
|
369
|
+
backgroundColor: outlined ? bg : 'transparent',
|
|
370
|
+
paddingHorizontal: outlined ? 5 : 0,
|
|
371
|
+
marginLeft: outlined ? -5 : 0,
|
|
372
|
+
},
|
|
373
|
+
labelStyleProp,
|
|
374
|
+
{ color: labelColor },
|
|
375
|
+
]}
|
|
376
|
+
>
|
|
377
|
+
{label}
|
|
378
|
+
</Animated.Text>
|
|
379
|
+
</Animated.View>
|
|
380
|
+
) : null}
|
|
381
|
+
{clearable ? (
|
|
382
|
+
<Animated.View
|
|
383
|
+
style={[styles.slot, { right: edge, top: contentTop, opacity: clear, transform: [{ scale: clear }] }]}
|
|
384
|
+
pointerEvents={text && editable ? 'box-none' : 'none'}
|
|
385
|
+
>
|
|
386
|
+
<Pressable
|
|
387
|
+
hitSlop={(44 - S.icon) / 2}
|
|
388
|
+
accessibilityRole="button"
|
|
389
|
+
accessibilityLabel={`Clear ${label}`}
|
|
390
|
+
onPress={() => {
|
|
391
|
+
setText('');
|
|
392
|
+
inputRef.current?.focus();
|
|
393
|
+
}}
|
|
394
|
+
>
|
|
395
|
+
<Svg width={S.icon} height={S.icon} viewBox="0 0 24 24">
|
|
396
|
+
<Circle cx={12} cy={12} r={11} fill={alpha(restIcon, 0.7)} />
|
|
397
|
+
<Path d="M8.5 8.5l7 7M15.5 8.5l-7 7" stroke={bg} strokeWidth={2.4} strokeLinecap="round" />
|
|
398
|
+
</Svg>
|
|
399
|
+
</Pressable>
|
|
400
|
+
</Animated.View>
|
|
401
|
+
) : null}
|
|
402
|
+
<Animated.View
|
|
403
|
+
pointerEvents="none"
|
|
404
|
+
style={[
|
|
405
|
+
styles.slot,
|
|
406
|
+
{
|
|
407
|
+
right: edge + (clearable ? slot : 0),
|
|
408
|
+
top: contentTop,
|
|
409
|
+
opacity: check.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
410
|
+
transform: [
|
|
411
|
+
{ scale: check.interpolate({ inputRange: [0, 1], outputRange: [0.3, 1] }) },
|
|
412
|
+
{ rotate: check.interpolate({ inputRange: [0, 1], outputRange: ['-30deg', '0deg'] }) },
|
|
413
|
+
],
|
|
414
|
+
},
|
|
415
|
+
]}
|
|
416
|
+
>
|
|
417
|
+
<Svg width={S.icon + 2} height={S.icon + 2} viewBox="0 0 24 24" fill="none">
|
|
418
|
+
<Circle cx={12} cy={12} r={11} fill={alpha(colors.success, 0.16)} />
|
|
419
|
+
<Path d="M7.5 12.5l3 3 6-6.5" stroke={colors.success} strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round" />
|
|
420
|
+
</Svg>
|
|
421
|
+
</Animated.View>
|
|
422
|
+
{right ? <View style={[styles.right, { right: underline ? 0 : S.padX - 4, top: contentTop }]}>{right}</View> : null}
|
|
423
|
+
{outlined ? null : (
|
|
424
|
+
<>
|
|
425
|
+
<View pointerEvents="none" style={[styles.baseLine, { height: underline ? 1.5 : 1, backgroundColor: rest }]} />
|
|
426
|
+
{/* Grows out from the centre on focus. */}
|
|
427
|
+
<Animated.View
|
|
428
|
+
pointerEvents="none"
|
|
429
|
+
style={[styles.bar, { backgroundColor: active, transform: [{ scaleX: glowClamped }] }]}
|
|
430
|
+
/>
|
|
431
|
+
</>
|
|
432
|
+
)}
|
|
433
|
+
</Animated.View>
|
|
434
|
+
</Pressable>
|
|
435
|
+
</View>
|
|
436
|
+
{message || showCounter ? (
|
|
437
|
+
<View style={[styles.footer, underline && styles.footerFlush]}>
|
|
438
|
+
<View style={styles.messageSlot}>
|
|
439
|
+
{message ? <FieldMessage key={tone + message} text={message} tone={tone} color={toneColor} style={messageStyle} /> : null}
|
|
440
|
+
</View>
|
|
441
|
+
{showCounter ? (
|
|
442
|
+
<Text
|
|
443
|
+
style={[
|
|
444
|
+
styles.count,
|
|
445
|
+
ff,
|
|
446
|
+
{ color: colors.mutedForeground },
|
|
447
|
+
counterStyle,
|
|
448
|
+
text.length >= maxLength! && { color: colors.danger },
|
|
449
|
+
]}
|
|
450
|
+
>
|
|
451
|
+
{text.length}/{maxLength}
|
|
452
|
+
</Text>
|
|
453
|
+
) : null}
|
|
454
|
+
</View>
|
|
455
|
+
) : null}
|
|
456
|
+
</Animated.View>
|
|
457
|
+
);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
type MessageTone = 'error' | 'success' | 'helper';
|
|
461
|
+
|
|
462
|
+
/** Helper / error / success line. Error and success slide up and fade in. */
|
|
463
|
+
function FieldMessage({ text, tone, color, style }: { text: string; tone: MessageTone; color: string; style?: StyleProp<TextStyle> }) {
|
|
464
|
+
const { font } = useKit();
|
|
465
|
+
const enter = useRef(new Animated.Value(tone === 'helper' ? 1 : 0)).current; // native
|
|
466
|
+
useEffect(() => {
|
|
467
|
+
if (tone !== 'helper') Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
468
|
+
}, [tone, enter]);
|
|
469
|
+
|
|
470
|
+
return (
|
|
471
|
+
<Animated.View
|
|
472
|
+
style={[
|
|
473
|
+
styles.message,
|
|
474
|
+
{
|
|
475
|
+
opacity: enter.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
476
|
+
transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [4, 0] }) }],
|
|
477
|
+
},
|
|
478
|
+
]}
|
|
479
|
+
>
|
|
480
|
+
{tone === 'helper' ? null : (
|
|
481
|
+
<Svg width={14} height={14} viewBox="0 0 24 24" fill="none" style={styles.messageIcon}>
|
|
482
|
+
<Circle cx={12} cy={12} r={9.5} stroke={color} strokeWidth={1.8} />
|
|
483
|
+
{tone === 'error' ? (
|
|
484
|
+
<>
|
|
485
|
+
<Path d="M12 7.5v5.25" stroke={color} strokeWidth={1.8} strokeLinecap="round" />
|
|
486
|
+
<Circle cx={12} cy={16.3} r={1.1} fill={color} />
|
|
487
|
+
</>
|
|
488
|
+
) : (
|
|
489
|
+
<Path d="M8 12.3l2.7 2.7 5.3-5.6" stroke={color} strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" />
|
|
490
|
+
)}
|
|
491
|
+
</Svg>
|
|
492
|
+
)}
|
|
493
|
+
<Text
|
|
494
|
+
accessibilityLiveRegion="polite"
|
|
495
|
+
style={[styles.helper, font.family ? { fontFamily: font.family } : null, { color }, style, tone !== 'helper' && { color }]}
|
|
496
|
+
>
|
|
497
|
+
{text}
|
|
498
|
+
</Text>
|
|
499
|
+
</Animated.View>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const styles = StyleSheet.create({
|
|
504
|
+
disabled: { opacity: 0.5 },
|
|
505
|
+
halo: { position: 'absolute', top: -HALO, left: -HALO, right: -HALO, bottom: -HALO },
|
|
506
|
+
field: { justifyContent: 'center' },
|
|
507
|
+
row: { flexDirection: 'row', alignItems: 'center', height: '100%' },
|
|
508
|
+
input: { flex: 1, alignSelf: 'stretch', paddingVertical: 0, paddingHorizontal: 0 },
|
|
509
|
+
affix: { fontWeight: '400' },
|
|
510
|
+
prefix: { marginRight: 4 },
|
|
511
|
+
suffix: { marginLeft: 6 },
|
|
512
|
+
labelWrap: { position: 'absolute', justifyContent: 'center', alignItems: 'flex-start', transformOrigin: 'left center' },
|
|
513
|
+
label: { fontWeight: '400' },
|
|
514
|
+
topLabel: { fontWeight: '400', marginBottom: 6, marginHorizontal: 2 },
|
|
515
|
+
topLabelFlush: { marginHorizontal: 0 },
|
|
516
|
+
left: { position: 'absolute', left: 14, top: 0, bottom: 0, justifyContent: 'center' },
|
|
517
|
+
right: { position: 'absolute', right: 12, top: 0, bottom: 0, justifyContent: 'center' },
|
|
518
|
+
slot: { position: 'absolute', top: 0, bottom: 0, justifyContent: 'center' },
|
|
519
|
+
baseLine: { position: 'absolute', left: 0, right: 0, bottom: 0 },
|
|
520
|
+
bar: { position: 'absolute', left: 0, right: 0, bottom: 0, height: 2 },
|
|
521
|
+
footer: { flexDirection: 'row', alignItems: 'flex-start', gap: 12, marginTop: 6, marginHorizontal: 4 },
|
|
522
|
+
footerFlush: { marginHorizontal: 0 },
|
|
523
|
+
messageSlot: { flex: 1 },
|
|
524
|
+
message: { flexDirection: 'row', alignItems: 'flex-start', gap: 5 },
|
|
525
|
+
messageIcon: { marginTop: 1.5 },
|
|
526
|
+
helper: { flexShrink: 1, fontSize: 12.5, lineHeight: 17 },
|
|
527
|
+
count: { fontSize: 12.5, lineHeight: 17, fontWeight: '400', fontVariant: ['tabular-nums'] },
|
|
528
|
+
});
|