@expokit/cli 0.0.0-stage → 0.3.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 +46 -2
- package/bin/expo-kit.js +1266 -0
- package/bin/lib.mjs +343 -0
- package/bin/mcp.mjs +345 -0
- package/package.json +49 -4
- package/registry/_docs/accordion.json +115 -0
- package/registry/_docs/action-sheet.json +138 -0
- package/registry/_docs/alert.json +155 -0
- package/registry/_docs/avatar.json +227 -0
- package/registry/_docs/badge.json +148 -0
- package/registry/_docs/bottom-nav.json +143 -0
- package/registry/_docs/bottom-sheet.json +186 -0
- package/registry/_docs/bullet.json +27 -0
- package/registry/_docs/button-3d.json +138 -0
- package/registry/_docs/button.json +131 -0
- package/registry/_docs/calendar.json +198 -0
- package/registry/_docs/card-stack.json +124 -0
- package/registry/_docs/card.json +131 -0
- package/registry/_docs/carousel.json +122 -0
- package/registry/_docs/chat.json +184 -0
- package/registry/_docs/checkbox.json +109 -0
- package/registry/_docs/checkout.json +105 -0
- package/registry/_docs/chip-group.json +132 -0
- package/registry/_docs/color-swatch.json +82 -0
- package/registry/_docs/confetti.json +102 -0
- package/registry/_docs/coupon.json +192 -0
- package/registry/_docs/cube-pager.json +100 -0
- package/registry/_docs/dashboard.json +123 -0
- package/registry/_docs/date-picker.json +269 -0
- package/registry/_docs/dialog.json +186 -0
- package/registry/_docs/drawer.json +199 -0
- package/registry/_docs/fab.json +144 -0
- package/registry/_docs/flip-card.json +127 -0
- package/registry/_docs/floating-input.json +211 -0
- package/registry/_docs/gift-card.json +124 -0
- package/registry/_docs/glass-button.json +142 -0
- package/registry/_docs/glass-nav-bar.json +102 -0
- package/registry/_docs/glass-search-bar.json +114 -0
- package/registry/_docs/glass-segmented.json +94 -0
- package/registry/_docs/glass-sheet.json +136 -0
- package/registry/_docs/glass-tab-bar.json +100 -0
- package/registry/_docs/glass.json +94 -0
- package/registry/_docs/inbox.json +75 -0
- package/registry/_docs/isometric-bar.json +75 -0
- package/registry/_docs/kit.json +14 -0
- package/registry/_docs/kpi-card.json +14 -0
- package/registry/_docs/like-button.json +92 -0
- package/registry/_docs/liquidglass-tab.json +30 -0
- package/registry/_docs/list-item.json +216 -0
- package/registry/_docs/loyalty-card.json +124 -0
- package/registry/_docs/morph-button.json +128 -0
- package/registry/_docs/number-ticker.json +112 -0
- package/registry/_docs/offers.json +81 -0
- package/registry/_docs/onboarding.json +69 -0
- package/registry/_docs/otp-input.json +158 -0
- package/registry/_docs/pagination-dots.json +108 -0
- package/registry/_docs/parallax-layers.json +104 -0
- package/registry/_docs/parallax-scroll.json +210 -0
- package/registry/_docs/password-input.json +112 -0
- package/registry/_docs/payment-card.json +169 -0
- package/registry/_docs/pixel-bar.json +69 -0
- package/registry/_docs/polar-area.json +81 -0
- package/registry/_docs/profile.json +87 -0
- package/registry/_docs/progress-steps.json +116 -0
- package/registry/_docs/progress.json +152 -0
- package/registry/_docs/radio-group.json +112 -0
- package/registry/_docs/rating.json +114 -0
- package/registry/_docs/ring-carousel.json +122 -0
- package/registry/_docs/scratch-card.json +110 -0
- package/registry/_docs/search-bar.json +160 -0
- package/registry/_docs/search-results.json +123 -0
- package/registry/_docs/segmented-control.json +123 -0
- package/registry/_docs/select.json +277 -0
- package/registry/_docs/settings.json +75 -0
- package/registry/_docs/shimmer-button.json +98 -0
- package/registry/_docs/sign-in.json +201 -0
- package/registry/_docs/skeleton.json +162 -0
- package/registry/_docs/slide-to-confirm.json +108 -0
- package/registry/_docs/slider.json +177 -0
- package/registry/_docs/spin-wheel.json +138 -0
- package/registry/_docs/spinner.json +121 -0
- package/registry/_docs/stateful-button.json +184 -0
- package/registry/_docs/stepper.json +184 -0
- package/registry/_docs/swipe-deck.json +118 -0
- package/registry/_docs/swipeable-row.json +95 -0
- package/registry/_docs/switch.json +124 -0
- package/registry/_docs/tabs.json +129 -0
- package/registry/_docs/tag-input.json +148 -0
- package/registry/_docs/text-animation.json +141 -0
- package/registry/_docs/textarea.json +191 -0
- package/registry/_docs/ticket-card.json +194 -0
- package/registry/_docs/ticket-shape.json +14 -0
- package/registry/_docs/tilt-card.json +157 -0
- package/registry/_docs/time-picker.json +269 -0
- package/registry/_docs/timeline-chart.json +33 -0
- package/registry/_docs/timeline.json +172 -0
- package/registry/_docs/toast.json +185 -0
- package/registry/_docs/treemap.json +33 -0
- package/registry/_docs/waterfall.json +39 -0
- package/registry/_docs/wheel-list.json +122 -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 +626 -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/chat.json +18 -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 +748 -0
- package/registry/spinner/files/src/lib/kit.tsx +219 -0
- package/registry/spinner.json +17 -0
- package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +532 -0
- package/registry/stateful-button/files/src/lib/kit.tsx +219 -0
- package/registry/stateful-button.json +18 -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/text-animation/files/src/components/ui/text-animation.tsx +650 -0
- package/registry/text-animation/files/src/lib/kit.tsx +219 -0
- package/registry/text-animation.json +15 -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,553 @@
|
|
|
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 TextareaVariant = 'outlined' | 'filled';
|
|
21
|
+
|
|
22
|
+
export interface TextareaProps extends Omit<TextInputProps, 'style' | 'multiline'> {
|
|
23
|
+
/** Optional label that rests on the first line and floats up on focus (FloatingInput feel). */
|
|
24
|
+
label?: string;
|
|
25
|
+
/** `false` puts the label above the field as static text (no notch); `placeholder` shows inside. */
|
|
26
|
+
floatingLabel?: boolean;
|
|
27
|
+
variant?: TextareaVariant;
|
|
28
|
+
/** 'sm' 14px · 'md' 16px · 'lg' 18px — line height, padding and the floated label scale too. */
|
|
29
|
+
size?: 'sm' | 'md' | 'lg';
|
|
30
|
+
/** Rows shown when empty (default 3). */
|
|
31
|
+
minRows?: number;
|
|
32
|
+
/** The field grows line by line up to this many rows, then scrolls (default 8). */
|
|
33
|
+
maxRows?: number;
|
|
34
|
+
/** Character counter under the field. Defaults to on when `maxLength` is set. */
|
|
35
|
+
showCount?: boolean;
|
|
36
|
+
/** When false, `maxLength` is a soft limit: typing past it is allowed and the counter goes negative in red. */
|
|
37
|
+
enforceMaxLength?: boolean;
|
|
38
|
+
/** Error message. Turns the field red, shows it with an icon and shakes the field once when it appears. */
|
|
39
|
+
error?: string;
|
|
40
|
+
/** Valid state: success border and an animated check. A string is shown as the message under the field. */
|
|
41
|
+
success?: boolean | string;
|
|
42
|
+
/** Hint under the field (replaced by the error / success message while one is shown). */
|
|
43
|
+
helper?: string;
|
|
44
|
+
/** Focus colour (default: theme accent). */
|
|
45
|
+
accent?: string;
|
|
46
|
+
/** Background behind the field — the floating label cuts its notch with this colour. */
|
|
47
|
+
surface?: string;
|
|
48
|
+
/** Rendered inside the bottom edge of the field — attach / emoji buttons, a send button. */
|
|
49
|
+
toolbar?: React.ReactNode;
|
|
50
|
+
/** Root wrapper (the one that shakes) — margins, width, flex. */
|
|
51
|
+
style?: StyleProp<ViewStyle>;
|
|
52
|
+
/**
|
|
53
|
+
* The bordered box: background, border width / colour, radius. Applied after the computed style,
|
|
54
|
+
* so it wins. A `borderColor` replaces the resting border only — focus (accent), error and
|
|
55
|
+
* success colours still take over. `height` is ignored: the box grows with `minRows` / `maxRows`.
|
|
56
|
+
*/
|
|
57
|
+
fieldStyle?: StyleProp<ViewStyle>;
|
|
58
|
+
/**
|
|
59
|
+
* The typed text — fontSize, lineHeight, fontWeight, color, letterSpacing, fontFamily… Row height
|
|
60
|
+
* follows `lineHeight` (or scales with `fontSize` when only that is given).
|
|
61
|
+
*/
|
|
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. A `fontSize` sets the resting size and the
|
|
66
|
+
* label floats at the same ratio as the default; the notch follows the rendered label width.
|
|
67
|
+
*/
|
|
68
|
+
labelStyle?: StyleProp<TextStyle>;
|
|
69
|
+
/** Helper / error / success text under the field. `color` applies to the helper; error and success keep their tone colour. */
|
|
70
|
+
messageStyle?: StyleProp<TextStyle>;
|
|
71
|
+
/** The character counter (`42/280` and the characters-left number). Warning / danger colours still win near the limit. */
|
|
72
|
+
counterStyle?: StyleProp<TextStyle>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Everything that scales with `size`. */
|
|
76
|
+
export const TEXTAREA_SIZES = {
|
|
77
|
+
sm: { font: 14, line: 20, floated: 11, padX: 12, padY: 10 },
|
|
78
|
+
md: { font: 16, line: 22, floated: 12.5, padX: 16, padY: 14 },
|
|
79
|
+
lg: { font: 18, line: 26, floated: 13.5, padX: 18, padY: 16 },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const BORDER = 1.5;
|
|
83
|
+
const HALO = 4;
|
|
84
|
+
/** Font size of the static label used when `floatingLabel` is false. */
|
|
85
|
+
const LABEL_ABOVE_FONT = { sm: 13, md: 14, lg: 14.5 };
|
|
86
|
+
const RING = 20;
|
|
87
|
+
const RING_STROKE = 2.5;
|
|
88
|
+
const RING_R = (RING - RING_STROKE) / 2;
|
|
89
|
+
const RING_C = 2 * Math.PI * RING_R;
|
|
90
|
+
/** react-native-web draws its own focus rectangle; our border + halo is the focus indicator. */
|
|
91
|
+
const NO_WEB_OUTLINE = Platform.OS === 'web' ? ({ outlineStyle: 'none', outlineWidth: 0 } as object) : null;
|
|
92
|
+
|
|
93
|
+
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Multiline field that glides taller line by line between `minRows` and `maxRows`, with an
|
|
97
|
+
* optional floating label, a focus halo, a counter that morphs into a progress ring near
|
|
98
|
+
* `maxLength`, and a `toolbar` slot for chat composers.
|
|
99
|
+
*/
|
|
100
|
+
export const Textarea = forwardRef<TextInput, TextareaProps>(function Textarea(
|
|
101
|
+
{
|
|
102
|
+
label,
|
|
103
|
+
floatingLabel = true,
|
|
104
|
+
variant = 'outlined',
|
|
105
|
+
size = 'md',
|
|
106
|
+
minRows = 3,
|
|
107
|
+
maxRows = 8,
|
|
108
|
+
maxLength,
|
|
109
|
+
showCount,
|
|
110
|
+
enforceMaxLength = true,
|
|
111
|
+
error,
|
|
112
|
+
success,
|
|
113
|
+
helper,
|
|
114
|
+
accent,
|
|
115
|
+
surface,
|
|
116
|
+
toolbar,
|
|
117
|
+
style,
|
|
118
|
+
fieldStyle,
|
|
119
|
+
inputStyle,
|
|
120
|
+
labelStyle,
|
|
121
|
+
messageStyle,
|
|
122
|
+
counterStyle,
|
|
123
|
+
placeholderTextColor,
|
|
124
|
+
value,
|
|
125
|
+
defaultValue,
|
|
126
|
+
onChangeText,
|
|
127
|
+
onFocus,
|
|
128
|
+
onBlur,
|
|
129
|
+
placeholder,
|
|
130
|
+
editable = true,
|
|
131
|
+
...props
|
|
132
|
+
},
|
|
133
|
+
ref
|
|
134
|
+
) {
|
|
135
|
+
const { colors, radius, font } = useKit();
|
|
136
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
137
|
+
const S = TEXTAREA_SIZES[size];
|
|
138
|
+
// Height is driven by rows, so the user's `height` is dropped; their border colour replaces the resting one.
|
|
139
|
+
const { borderColor: userBorder, height: _h, ...userField } = StyleSheet.flatten(fieldStyle) ?? {};
|
|
140
|
+
const userInput = StyleSheet.flatten(inputStyle) ?? {};
|
|
141
|
+
const userLabelColor = StyleSheet.flatten(labelStyle)?.color as string | undefined;
|
|
142
|
+
const FONT = typeof userInput.fontSize === 'number' ? userInput.fontSize : S.font;
|
|
143
|
+
const LINE = typeof userInput.lineHeight === 'number' ? userInput.lineHeight : Math.round((FONT * S.line) / S.font);
|
|
144
|
+
const corner = typeof userField.borderRadius === 'number' ? userField.borderRadius : radius.md;
|
|
145
|
+
// The hidden mirror must measure text exactly like the input does.
|
|
146
|
+
const metrics: TextStyle = { ...ff };
|
|
147
|
+
if (userInput.fontFamily !== undefined) metrics.fontFamily = userInput.fontFamily;
|
|
148
|
+
if (userInput.fontWeight !== undefined) metrics.fontWeight = userInput.fontWeight;
|
|
149
|
+
if (userInput.fontStyle !== undefined) metrics.fontStyle = userInput.fontStyle;
|
|
150
|
+
if (userInput.letterSpacing !== undefined) metrics.letterSpacing = userInput.letterSpacing;
|
|
151
|
+
const tint = accent ?? colors.accent;
|
|
152
|
+
const bg = surface ?? colors.background;
|
|
153
|
+
const filled = variant === 'filled';
|
|
154
|
+
const inputRef = useRef<TextInput>(null);
|
|
155
|
+
useImperativeHandle(ref, () => inputRef.current as TextInput);
|
|
156
|
+
|
|
157
|
+
const [focused, setFocused] = useState(false);
|
|
158
|
+
const [inner, setInner] = useState(defaultValue ?? '');
|
|
159
|
+
const text = value ?? inner;
|
|
160
|
+
const floating = focused || text.length > 0;
|
|
161
|
+
/** Label rendered inside the field (and floated); otherwise it sits above as static text. */
|
|
162
|
+
const inLabel = label && floatingLabel ? label : undefined;
|
|
163
|
+
const topLabel = label && !floatingLabel ? label : undefined;
|
|
164
|
+
const isFocused = focused && editable;
|
|
165
|
+
const over = maxLength !== undefined && text.length > maxLength;
|
|
166
|
+
const ok = !error && !over && !!success;
|
|
167
|
+
const successText = typeof success === 'string' && ok ? success : undefined;
|
|
168
|
+
|
|
169
|
+
const [contentH, setContentH] = useState<number | null>(null);
|
|
170
|
+
const [toolbarH, setToolbarH] = useState<number | null>(toolbar ? null : 0);
|
|
171
|
+
|
|
172
|
+
const lo = Math.max(1, minRows);
|
|
173
|
+
const hi = Math.max(lo, maxRows);
|
|
174
|
+
const lines = Math.round((contentH ?? LINE) / LINE);
|
|
175
|
+
const rows = Math.min(hi, Math.max(lo, lines));
|
|
176
|
+
const padTop = filled && inLabel ? S.padY + 14 : S.padY;
|
|
177
|
+
const padBottom = toolbar ? 4 : filled ? S.padY - 2 : S.padY;
|
|
178
|
+
const height = padTop + rows * LINE + padBottom + (toolbar ? toolbarH ?? 0 : 0) + BORDER * 2;
|
|
179
|
+
|
|
180
|
+
const grow = useRef(new Animated.Value(height)).current; // JS: height
|
|
181
|
+
const focus = useRef(new Animated.Value(0)).current; // JS: colours
|
|
182
|
+
const glow = useRef(new Animated.Value(0)).current; // native: halo
|
|
183
|
+
const float = useRef(new Animated.Value(floating ? 1 : 0)).current; // native: label
|
|
184
|
+
const shake = useRef(new Animated.Value(0)).current; // native
|
|
185
|
+
const check = useRef(new Animated.Value(ok ? 1 : 0)).current; // native
|
|
186
|
+
const settled = useRef(false);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
// Snap to the first measured size; glide on every change after that.
|
|
190
|
+
if (!settled.current) {
|
|
191
|
+
grow.setValue(height);
|
|
192
|
+
if (contentH !== null && toolbarH !== null) settled.current = true;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
Animated.timing(grow, { toValue: height, duration: 200, easing: Easing.out(Easing.cubic), useNativeDriver: false }).start();
|
|
196
|
+
}, [height, grow, contentH, toolbarH]);
|
|
197
|
+
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
Animated.spring(float, { toValue: floating ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
200
|
+
}, [floating, float]);
|
|
201
|
+
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
Animated.timing(focus, { toValue: isFocused ? 1 : 0, duration: 180, easing: Easing.out(Easing.quad), useNativeDriver: false }).start();
|
|
204
|
+
Animated.spring(glow, { toValue: isFocused ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
205
|
+
}, [isFocused, focus, glow]);
|
|
206
|
+
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
Animated.spring(check, { toValue: ok ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
|
|
209
|
+
}, [ok, check]);
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (!error) return;
|
|
213
|
+
shake.setValue(0);
|
|
214
|
+
Animated.sequence(
|
|
215
|
+
[10, -8, 6, -4, 0].map((x) => Animated.timing(shake, { toValue: x, duration: 55, useNativeDriver: true }))
|
|
216
|
+
).start();
|
|
217
|
+
}, [error, shake]);
|
|
218
|
+
|
|
219
|
+
const setText = (next: string) => {
|
|
220
|
+
if (value === undefined) setInner(next);
|
|
221
|
+
onChangeText?.(next);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const bad = !!error || over;
|
|
225
|
+
const active = bad ? colors.danger : tint;
|
|
226
|
+
const rest = bad ? colors.danger : ok ? colors.success : ((userBorder as string | undefined) ?? (filled ? alpha(colors.input, 0) : colors.input));
|
|
227
|
+
const borderColor = focus.interpolate({ inputRange: [0, 1], outputRange: [rest, active] });
|
|
228
|
+
const labelColor = focus.interpolate({
|
|
229
|
+
inputRange: [0, 1],
|
|
230
|
+
outputRange: [bad ? colors.danger : ok ? colors.success : (userLabelColor ?? colors.mutedForeground), active],
|
|
231
|
+
});
|
|
232
|
+
const topLabelColor = focus.interpolate({
|
|
233
|
+
inputRange: [0, 1],
|
|
234
|
+
outputRange: [bad ? colors.danger : (userLabelColor ?? colors.foreground), active],
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Label rests on the first line; floats onto the border (outlined) or into the top padding (filled).
|
|
238
|
+
const restCenter = padTop + LINE / 2;
|
|
239
|
+
const floatY = filled ? S.padY + 1 - restCenter : -restCenter - BORDER / 2;
|
|
240
|
+
const floatX = filled ? 0 : 12 - S.padX;
|
|
241
|
+
const floatScale = S.floated / S.font;
|
|
242
|
+
const checkSize = 20;
|
|
243
|
+
|
|
244
|
+
const counting = (showCount ?? maxLength !== undefined) && maxLength !== undefined;
|
|
245
|
+
const message = error ?? successText ?? helper;
|
|
246
|
+
const tone: MessageTone = error ? 'error' : successText ? 'success' : 'helper';
|
|
247
|
+
const toneColor = tone === 'error' ? colors.danger : tone === 'success' ? colors.success : colors.mutedForeground;
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<Animated.View style={[{ transform: [{ translateX: shake }] }, style]}>
|
|
251
|
+
{topLabel ? (
|
|
252
|
+
<Animated.Text
|
|
253
|
+
numberOfLines={1}
|
|
254
|
+
onPress={editable ? () => inputRef.current?.focus() : undefined}
|
|
255
|
+
// The input already carries the label for screen readers.
|
|
256
|
+
accessible={false}
|
|
257
|
+
importantForAccessibility="no"
|
|
258
|
+
accessibilityElementsHidden
|
|
259
|
+
style={[styles.topLabel, ff, { fontSize: LABEL_ABOVE_FONT[size] }, labelStyle, { color: topLabelColor }, !editable && styles.disabled]}
|
|
260
|
+
>
|
|
261
|
+
{topLabel}
|
|
262
|
+
</Animated.Text>
|
|
263
|
+
) : null}
|
|
264
|
+
<View style={!editable && styles.disabled}>
|
|
265
|
+
<Animated.View
|
|
266
|
+
pointerEvents="none"
|
|
267
|
+
style={[
|
|
268
|
+
styles.halo,
|
|
269
|
+
{
|
|
270
|
+
borderRadius: corner + HALO,
|
|
271
|
+
backgroundColor: active,
|
|
272
|
+
opacity: glow.interpolate({ inputRange: [0, 1], outputRange: [0, 0.18], extrapolate: 'clamp' }),
|
|
273
|
+
transform: [{ scale: glow.interpolate({ inputRange: [0, 1], outputRange: [0.98, 1] }) }],
|
|
274
|
+
},
|
|
275
|
+
]}
|
|
276
|
+
/>
|
|
277
|
+
<Pressable onPress={() => inputRef.current?.focus()} disabled={!editable} accessible={false}>
|
|
278
|
+
<Animated.View
|
|
279
|
+
style={[
|
|
280
|
+
styles.field,
|
|
281
|
+
{
|
|
282
|
+
height: grow,
|
|
283
|
+
borderRadius: radius.md,
|
|
284
|
+
backgroundColor: filled ? colors.muted : bg,
|
|
285
|
+
},
|
|
286
|
+
userField,
|
|
287
|
+
{ borderColor },
|
|
288
|
+
]}
|
|
289
|
+
>
|
|
290
|
+
{/* Invisible mirror of the text: its height tells us how many lines to show, on every platform. */}
|
|
291
|
+
<View pointerEvents="none" style={[styles.mirrorBox, { left: S.padX - BORDER, right: S.padX - BORDER + (ok ? 28 : 0) }]}>
|
|
292
|
+
<Text
|
|
293
|
+
onLayout={(e) => setContentH(e.nativeEvent.layout.height)}
|
|
294
|
+
style={[styles.mirror, metrics, { fontSize: FONT, lineHeight: LINE }]}
|
|
295
|
+
>
|
|
296
|
+
{text.endsWith('\n') || !text ? text + ' ' : text}
|
|
297
|
+
</Text>
|
|
298
|
+
</View>
|
|
299
|
+
<TextInput
|
|
300
|
+
ref={inputRef}
|
|
301
|
+
accessibilityLabel={label ?? placeholder}
|
|
302
|
+
{...props}
|
|
303
|
+
multiline
|
|
304
|
+
maxLength={enforceMaxLength ? maxLength : undefined}
|
|
305
|
+
editable={editable}
|
|
306
|
+
value={text}
|
|
307
|
+
onChangeText={setText}
|
|
308
|
+
onFocus={(e) => {
|
|
309
|
+
setFocused(true);
|
|
310
|
+
onFocus?.(e);
|
|
311
|
+
}}
|
|
312
|
+
onBlur={(e) => {
|
|
313
|
+
setFocused(false);
|
|
314
|
+
onBlur?.(e);
|
|
315
|
+
}}
|
|
316
|
+
placeholder={!inLabel || floating ? placeholder : undefined}
|
|
317
|
+
placeholderTextColor={placeholderTextColor ?? colors.mutedForeground}
|
|
318
|
+
selectionColor={active}
|
|
319
|
+
cursorColor={active}
|
|
320
|
+
scrollEnabled={lines > hi}
|
|
321
|
+
textAlignVertical="top"
|
|
322
|
+
style={[
|
|
323
|
+
styles.input,
|
|
324
|
+
ff,
|
|
325
|
+
{ color: colors.foreground, fontSize: S.font },
|
|
326
|
+
inputStyle,
|
|
327
|
+
{
|
|
328
|
+
lineHeight: LINE,
|
|
329
|
+
paddingTop: padTop,
|
|
330
|
+
paddingBottom: padBottom,
|
|
331
|
+
paddingLeft: S.padX - BORDER,
|
|
332
|
+
paddingRight: S.padX - BORDER + (ok ? 28 : 0),
|
|
333
|
+
},
|
|
334
|
+
NO_WEB_OUTLINE,
|
|
335
|
+
]}
|
|
336
|
+
/>
|
|
337
|
+
{toolbar ? (
|
|
338
|
+
<View onLayout={(e) => setToolbarH(e.nativeEvent.layout.height)} style={styles.toolbar}>
|
|
339
|
+
{toolbar}
|
|
340
|
+
</View>
|
|
341
|
+
) : null}
|
|
342
|
+
{inLabel ? (
|
|
343
|
+
<Animated.View
|
|
344
|
+
pointerEvents="none"
|
|
345
|
+
style={[
|
|
346
|
+
styles.labelWrap,
|
|
347
|
+
{
|
|
348
|
+
top: padTop,
|
|
349
|
+
left: S.padX - BORDER,
|
|
350
|
+
right: S.padX,
|
|
351
|
+
height: LINE,
|
|
352
|
+
transform: [
|
|
353
|
+
{ translateY: float.interpolate({ inputRange: [0, 1], outputRange: [0, floatY] }) },
|
|
354
|
+
{ translateX: float.interpolate({ inputRange: [0, 1], outputRange: [0, floatX] }) },
|
|
355
|
+
{ scale: float.interpolate({ inputRange: [0, 1], outputRange: [1, floatScale] }) },
|
|
356
|
+
],
|
|
357
|
+
},
|
|
358
|
+
]}
|
|
359
|
+
>
|
|
360
|
+
<Animated.Text
|
|
361
|
+
numberOfLines={1}
|
|
362
|
+
style={[
|
|
363
|
+
styles.label,
|
|
364
|
+
ff,
|
|
365
|
+
{
|
|
366
|
+
fontSize: S.font,
|
|
367
|
+
// The notch: the label paints the surface over the border, so it matches the rendered width.
|
|
368
|
+
backgroundColor: filled ? 'transparent' : bg,
|
|
369
|
+
paddingHorizontal: filled ? 0 : 5,
|
|
370
|
+
marginLeft: filled ? 0 : -5,
|
|
371
|
+
},
|
|
372
|
+
labelStyle,
|
|
373
|
+
{ color: labelColor },
|
|
374
|
+
]}
|
|
375
|
+
>
|
|
376
|
+
{inLabel}
|
|
377
|
+
</Animated.Text>
|
|
378
|
+
</Animated.View>
|
|
379
|
+
) : null}
|
|
380
|
+
<Animated.View
|
|
381
|
+
pointerEvents="none"
|
|
382
|
+
style={[
|
|
383
|
+
styles.check,
|
|
384
|
+
{
|
|
385
|
+
top: padTop - BORDER + (LINE - checkSize) / 2,
|
|
386
|
+
right: S.padX - BORDER - 4,
|
|
387
|
+
opacity: check.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
388
|
+
transform: [
|
|
389
|
+
{ scale: check.interpolate({ inputRange: [0, 1], outputRange: [0.3, 1] }) },
|
|
390
|
+
{ rotate: check.interpolate({ inputRange: [0, 1], outputRange: ['-30deg', '0deg'] }) },
|
|
391
|
+
],
|
|
392
|
+
},
|
|
393
|
+
]}
|
|
394
|
+
>
|
|
395
|
+
<Svg width={checkSize} height={checkSize} viewBox="0 0 24 24" fill="none">
|
|
396
|
+
<Circle cx={12} cy={12} r={11} fill={alpha(colors.success, 0.16)} />
|
|
397
|
+
<Path d="M7.5 12.5l3 3 6-6.5" stroke={colors.success} strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round" />
|
|
398
|
+
</Svg>
|
|
399
|
+
</Animated.View>
|
|
400
|
+
</Animated.View>
|
|
401
|
+
</Pressable>
|
|
402
|
+
</View>
|
|
403
|
+
{message || counting ? (
|
|
404
|
+
<View style={styles.footer}>
|
|
405
|
+
<View style={styles.messageSlot}>
|
|
406
|
+
{message ? <FieldMessage key={tone + message} text={message} tone={tone} color={toneColor} style={messageStyle} /> : null}
|
|
407
|
+
</View>
|
|
408
|
+
{counting ? <Counter length={text.length} max={maxLength} tint={tint} style={counterStyle} /> : null}
|
|
409
|
+
</View>
|
|
410
|
+
) : null}
|
|
411
|
+
</Animated.View>
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
type MessageTone = 'error' | 'success' | 'helper';
|
|
416
|
+
|
|
417
|
+
/** Helper / error / success line. Error and success slide up and fade in. */
|
|
418
|
+
function FieldMessage({ text, tone, color, style }: { text: string; tone: MessageTone; color: string; style?: StyleProp<TextStyle> }) {
|
|
419
|
+
const { font } = useKit();
|
|
420
|
+
const enter = useRef(new Animated.Value(tone === 'helper' ? 1 : 0)).current; // native
|
|
421
|
+
useEffect(() => {
|
|
422
|
+
if (tone !== 'helper') Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
423
|
+
}, [tone, enter]);
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
<Animated.View
|
|
427
|
+
style={[
|
|
428
|
+
styles.message,
|
|
429
|
+
{
|
|
430
|
+
opacity: enter.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
431
|
+
transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [4, 0] }) }],
|
|
432
|
+
},
|
|
433
|
+
]}
|
|
434
|
+
>
|
|
435
|
+
{tone === 'helper' ? null : (
|
|
436
|
+
<Svg width={14} height={14} viewBox="0 0 24 24" fill="none" style={styles.messageIcon}>
|
|
437
|
+
<Circle cx={12} cy={12} r={9.5} stroke={color} strokeWidth={1.8} />
|
|
438
|
+
{tone === 'error' ? (
|
|
439
|
+
<>
|
|
440
|
+
<Path d="M12 7.5v5.25" stroke={color} strokeWidth={1.8} strokeLinecap="round" />
|
|
441
|
+
<Circle cx={12} cy={16.3} r={1.1} fill={color} />
|
|
442
|
+
</>
|
|
443
|
+
) : (
|
|
444
|
+
<Path d="M8 12.3l2.7 2.7 5.3-5.6" stroke={color} strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" />
|
|
445
|
+
)}
|
|
446
|
+
</Svg>
|
|
447
|
+
)}
|
|
448
|
+
<Text
|
|
449
|
+
accessibilityLiveRegion="polite"
|
|
450
|
+
style={[styles.helper, font.family ? { fontFamily: font.family } : null, { color }, style, tone !== 'helper' && { color }]}
|
|
451
|
+
>
|
|
452
|
+
{text}
|
|
453
|
+
</Text>
|
|
454
|
+
</Animated.View>
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** "42/280", turning into a filling ring with the characters left once you get close. */
|
|
459
|
+
function Counter({ length, max, tint, style }: { length: number; max: number; tint: string; style?: StyleProp<TextStyle> }) {
|
|
460
|
+
const { colors, font } = useKit();
|
|
461
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
462
|
+
const remaining = max - length;
|
|
463
|
+
const ratio = length / max;
|
|
464
|
+
const near = remaining <= Math.max(10, Math.round(max * 0.2));
|
|
465
|
+
const color = ratio >= 1 ? colors.danger : ratio >= 0.9 ? colors.warning : tint;
|
|
466
|
+
|
|
467
|
+
const mode = useRef(new Animated.Value(near ? 1 : 0)).current; // native
|
|
468
|
+
const pulse = useRef(new Animated.Value(1)).current; // native
|
|
469
|
+
const progress = useRef(new Animated.Value(Math.min(1, ratio))).current; // JS (SVG prop)
|
|
470
|
+
|
|
471
|
+
useEffect(() => {
|
|
472
|
+
Animated.spring(mode, { toValue: near ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
473
|
+
}, [near, mode]);
|
|
474
|
+
|
|
475
|
+
useEffect(() => {
|
|
476
|
+
Animated.spring(progress, { toValue: Math.min(1, ratio), useNativeDriver: false, ...motion.snappy }).start();
|
|
477
|
+
}, [ratio, progress]);
|
|
478
|
+
|
|
479
|
+
const limitHit = remaining <= 0;
|
|
480
|
+
useEffect(() => {
|
|
481
|
+
if (!limitHit) return;
|
|
482
|
+
pulse.setValue(1.35);
|
|
483
|
+
Animated.spring(pulse, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
|
|
484
|
+
}, [limitHit, pulse]);
|
|
485
|
+
|
|
486
|
+
return (
|
|
487
|
+
<View
|
|
488
|
+
style={styles.counter}
|
|
489
|
+
accessible
|
|
490
|
+
accessibilityRole="text"
|
|
491
|
+
accessibilityLabel={remaining >= 0 ? `${remaining} of ${max} characters left` : `${-remaining} characters over the ${max} limit`}
|
|
492
|
+
>
|
|
493
|
+
<Animated.Text
|
|
494
|
+
style={[
|
|
495
|
+
styles.count,
|
|
496
|
+
ff,
|
|
497
|
+
{ color: colors.mutedForeground },
|
|
498
|
+
style,
|
|
499
|
+
{ opacity: mode.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }) },
|
|
500
|
+
]}
|
|
501
|
+
>
|
|
502
|
+
{length}/{max}
|
|
503
|
+
</Animated.Text>
|
|
504
|
+
<Animated.View
|
|
505
|
+
style={[
|
|
506
|
+
styles.ringRow,
|
|
507
|
+
{ opacity: mode, transform: [{ scale: mode.interpolate({ inputRange: [0, 1], outputRange: [0.6, 1] }) }] },
|
|
508
|
+
]}
|
|
509
|
+
>
|
|
510
|
+
<Text style={[styles.count, ff, { color: colors.mutedForeground }, style, ratio >= 0.9 && { color }]}>{remaining}</Text>
|
|
511
|
+
<Animated.View style={{ transform: [{ scale: pulse }] }}>
|
|
512
|
+
<Svg width={RING} height={RING}>
|
|
513
|
+
<Circle cx={RING / 2} cy={RING / 2} r={RING_R} stroke={colors.border} strokeWidth={RING_STROKE} fill="none" />
|
|
514
|
+
<AnimatedCircle
|
|
515
|
+
cx={RING / 2}
|
|
516
|
+
cy={RING / 2}
|
|
517
|
+
r={RING_R}
|
|
518
|
+
stroke={color}
|
|
519
|
+
strokeWidth={RING_STROKE}
|
|
520
|
+
fill="none"
|
|
521
|
+
strokeLinecap="round"
|
|
522
|
+
strokeDasharray={`${RING_C} ${RING_C}`}
|
|
523
|
+
strokeDashoffset={progress.interpolate({ inputRange: [0, 1], outputRange: [RING_C, 0] })}
|
|
524
|
+
transform={`rotate(-90 ${RING / 2} ${RING / 2})`}
|
|
525
|
+
/>
|
|
526
|
+
</Svg>
|
|
527
|
+
</Animated.View>
|
|
528
|
+
</Animated.View>
|
|
529
|
+
</View>
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const styles = StyleSheet.create({
|
|
534
|
+
disabled: { opacity: 0.5 },
|
|
535
|
+
halo: { position: 'absolute', top: -HALO, left: -HALO, right: -HALO, bottom: -HALO },
|
|
536
|
+
field: { borderWidth: BORDER },
|
|
537
|
+
input: { flex: 1 },
|
|
538
|
+
mirrorBox: { position: 'absolute', top: 0, height: 0, overflow: 'hidden' },
|
|
539
|
+
mirror: { position: 'absolute', top: 0, left: 0, right: 0, opacity: 0 },
|
|
540
|
+
toolbar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, paddingBottom: 8, gap: 4 },
|
|
541
|
+
labelWrap: { position: 'absolute', justifyContent: 'center', alignItems: 'flex-start', transformOrigin: 'left center' },
|
|
542
|
+
label: { fontWeight: '400' },
|
|
543
|
+
topLabel: { fontWeight: '400', marginBottom: 6, marginHorizontal: 2 },
|
|
544
|
+
check: { position: 'absolute' },
|
|
545
|
+
footer: { flexDirection: 'row', alignItems: 'center', marginTop: 6, gap: 12, minHeight: RING, marginHorizontal: 4 },
|
|
546
|
+
messageSlot: { flex: 1 },
|
|
547
|
+
message: { flexDirection: 'row', alignItems: 'flex-start', gap: 5 },
|
|
548
|
+
messageIcon: { marginTop: 1.5 },
|
|
549
|
+
helper: { flexShrink: 1, fontSize: 12.5, lineHeight: 17 },
|
|
550
|
+
counter: { minWidth: 56, height: RING, alignItems: 'flex-end', justifyContent: 'center' },
|
|
551
|
+
count: { fontSize: 12.5, fontVariant: ['tabular-nums'], fontWeight: '400' },
|
|
552
|
+
ringRow: { position: 'absolute', right: 0, flexDirection: 'row', alignItems: 'center', gap: 6 },
|
|
553
|
+
});
|