@expokit/cli 0.3.0 → 0.4.1
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/README.md +8 -4
- package/bin/expo-kit.js +137 -24
- package/bin/lib.mjs +110 -1
- package/bin/mcp.mjs +40 -18
- package/package.json +2 -2
- package/registry/_docs/accordion.json +1 -1
- package/registry/_docs/assistant-app.json +47 -0
- package/registry/_docs/avatar.json +6 -6
- package/registry/_docs/badge.json +4 -4
- package/registry/_docs/bottom-nav.json +6 -6
- package/registry/_docs/bottom-sheet.json +3 -3
- package/registry/_docs/bullet.json +6 -0
- package/registry/_docs/button-3d.json +3 -3
- package/registry/_docs/button.json +2 -2
- package/registry/_docs/calendar.json +5 -5
- package/registry/_docs/card.json +1 -1
- package/registry/_docs/checkout.json +7 -1
- package/registry/_docs/chip-group.json +1 -1
- package/registry/_docs/confetti.json +3 -3
- package/registry/_docs/date-picker.json +4 -4
- package/registry/_docs/donut-shop.json +88 -0
- package/registry/_docs/drawer.json +4 -4
- package/registry/_docs/fintech-app.json +49 -0
- package/registry/_docs/flip-card.json +4 -4
- package/registry/_docs/floating-input.json +5 -5
- package/registry/_docs/food-app.json +47 -0
- package/registry/_docs/glass.json +1 -1
- package/registry/_docs/grain.json +80 -0
- package/registry/_docs/isometric-bar.json +6 -0
- package/registry/_docs/liquidglass-tab.json +1 -1
- package/registry/_docs/morphing-button-group.json +96 -0
- package/registry/_docs/orb-reveal.json +132 -0
- package/registry/_docs/otp-input.json +4 -4
- package/registry/_docs/password-input.json +3 -3
- package/registry/_docs/payment-card.json +3 -3
- package/registry/_docs/pixel-bar.json +6 -0
- package/registry/_docs/polar-area.json +6 -0
- package/registry/_docs/progress-steps.json +4 -4
- package/registry/_docs/progress.json +2 -2
- package/registry/_docs/qr-code-button.json +122 -0
- package/registry/_docs/radio-group.json +1 -1
- package/registry/_docs/search-bar.json +4 -4
- package/registry/_docs/segmented-control.json +1 -1
- package/registry/_docs/select.json +7 -7
- package/registry/_docs/settings.json +6 -0
- package/registry/_docs/skeleton.json +3 -3
- package/registry/_docs/slider.json +6 -6
- package/registry/_docs/spinner.json +3 -3
- package/registry/_docs/stateful-button.json +1 -1
- package/registry/_docs/stepper.json +7 -7
- package/registry/_docs/tabs.json +2 -2
- package/registry/_docs/text-animation.json +4 -4
- package/registry/_docs/textarea.json +3 -3
- package/registry/_docs/ticket-shape.json +1 -1
- package/registry/_docs/tilt-card.json +4 -4
- package/registry/_docs/time-picker.json +6 -6
- package/registry/_docs/timeline-chart.json +6 -0
- package/registry/_docs/timeline.json +6 -6
- package/registry/_docs/toast.json +8 -8
- package/registry/_docs/treemap.json +6 -0
- package/registry/_docs/waterfall.json +6 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +36 -7
- package/registry/accordion/files/src/lib/kit.tsx +8 -5
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +12 -8
- package/registry/action-sheet/files/src/lib/kit.tsx +8 -5
- package/registry/alert/files/src/components/ui/alert.tsx +11 -5
- package/registry/alert/files/src/components/ui/button.tsx +35 -6
- package/registry/alert/files/src/lib/kit.tsx +8 -5
- package/registry/assistant-app.json +49 -0
- package/registry/avatar/files/src/components/ui/avatar.tsx +7 -4
- package/registry/avatar/files/src/lib/kit.tsx +8 -5
- package/registry/badge/files/src/components/ui/badge.tsx +23 -9
- package/registry/badge/files/src/lib/kit.tsx +8 -5
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +21 -8
- package/registry/bottom-nav/files/src/lib/kit.tsx +8 -5
- package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +7 -6
- package/registry/bottom-sheet/files/src/lib/kit.tsx +8 -5
- package/registry/button/files/src/components/ui/button.tsx +35 -6
- package/registry/button/files/src/lib/kit.tsx +8 -5
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +35 -7
- package/registry/button-3d/files/src/lib/kit.tsx +8 -5
- package/registry/calendar/files/src/components/ui/calendar.tsx +68 -19
- package/registry/calendar/files/src/lib/kit.tsx +8 -5
- package/registry/card/files/src/components/ui/card.tsx +32 -10
- package/registry/card/files/src/lib/kit.tsx +8 -5
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +31 -8
- package/registry/checkbox/files/src/lib/kit.tsx +8 -5
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +39 -7
- package/registry/chip-group/files/src/lib/kit.tsx +8 -5
- package/registry/confetti/files/src/lib/kit.tsx +8 -5
- package/registry/coupon/files/src/components/ui/coupon.tsx +55 -28
- package/registry/coupon/files/src/components/ui/flip-card.tsx +23 -4
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +59 -18
- package/registry/coupon/files/src/lib/kit.tsx +8 -5
- package/registry/date-picker/files/src/components/ui/calendar.tsx +68 -19
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +98 -44
- package/registry/date-picker/files/src/lib/kit.tsx +8 -5
- package/registry/dialog/files/src/components/ui/dialog.tsx +19 -9
- package/registry/dialog/files/src/lib/kit.tsx +8 -5
- package/registry/donut-shop.json +18 -0
- package/registry/drawer/files/src/components/ui/drawer.tsx +31 -21
- package/registry/drawer/files/src/lib/kit.tsx +8 -5
- package/registry/fintech-app.json +57 -0
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +23 -4
- package/registry/flip-card/files/src/lib/kit.tsx +8 -5
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +91 -32
- package/registry/floating-input/files/src/lib/kit.tsx +8 -5
- package/registry/food-app.json +55 -0
- package/registry/grain/files/src/components/ui/grain.tsx +113 -0
- package/registry/{tag-input → grain}/files/src/lib/kit.tsx +8 -5
- package/registry/grain.json +17 -0
- package/registry/kit/files/src/lib/kit.tsx +8 -5
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +2 -2
- package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +1 -1
- package/registry/list-item/files/src/components/ui/list-item.tsx +28 -17
- package/registry/list-item/files/src/lib/kit.tsx +8 -5
- package/registry/morphing-button-group.json +18 -0
- package/registry/orb-reveal.json +20 -0
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +57 -24
- package/registry/otp-input/files/src/lib/kit.tsx +8 -5
- package/registry/password-input/files/src/components/ui/floating-input.tsx +91 -32
- package/registry/password-input/files/src/components/ui/password-input.tsx +55 -21
- package/registry/password-input/files/src/lib/kit.tsx +8 -5
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +23 -4
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +35 -18
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +59 -18
- package/registry/payment-card/files/src/lib/kit.tsx +8 -5
- package/registry/progress/files/src/components/ui/progress.tsx +16 -6
- package/registry/progress/files/src/lib/kit.tsx +8 -5
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +22 -11
- package/registry/progress-steps/files/src/lib/kit.tsx +8 -5
- package/registry/qr-code-button.json +18 -0
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +48 -17
- package/registry/radio-group/files/src/lib/kit.tsx +8 -5
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +81 -26
- package/registry/search-bar/files/src/lib/kit.tsx +8 -5
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +40 -10
- package/registry/segmented-control/files/src/lib/kit.tsx +8 -5
- package/registry/select/files/src/components/ui/select.tsx +161 -86
- package/registry/select/files/src/lib/kit.tsx +8 -5
- package/registry/skeleton/files/src/components/ui/skeleton.tsx +4 -4
- package/registry/skeleton/files/src/lib/kit.tsx +8 -5
- package/registry/slider/files/src/components/ui/slider.tsx +56 -27
- package/registry/slider/files/src/lib/kit.tsx +8 -5
- package/registry/spinner/files/src/components/ui/spinner.tsx +17 -12
- package/registry/spinner/files/src/lib/kit.tsx +8 -5
- package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +12 -4
- package/registry/stateful-button/files/src/lib/kit.tsx +8 -5
- package/registry/stepper/files/src/components/ui/stepper.tsx +83 -29
- package/registry/stepper/files/src/lib/kit.tsx +8 -5
- package/registry/switch/files/src/components/ui/switch.tsx +60 -22
- package/registry/switch/files/src/lib/kit.tsx +8 -5
- package/registry/tabs/files/src/components/ui/tabs.tsx +15 -10
- package/registry/tabs/files/src/lib/kit.tsx +8 -5
- package/registry/text-animation/files/src/components/ui/text-animation.tsx +11 -6
- package/registry/text-animation/files/src/lib/kit.tsx +8 -5
- package/registry/textarea/files/src/components/ui/textarea.tsx +115 -45
- package/registry/textarea/files/src/lib/kit.tsx +8 -5
- package/registry/ticket-shape/files/src/lib/kit.tsx +8 -5
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +59 -18
- package/registry/tilt-card/files/src/lib/kit.tsx +8 -5
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +156 -75
- package/registry/time-picker/files/src/lib/kit.tsx +8 -5
- package/registry/timeline/files/src/components/ui/avatar.tsx +7 -4
- package/registry/timeline/files/src/components/ui/timeline.tsx +19 -12
- package/registry/timeline/files/src/lib/kit.tsx +8 -5
- package/registry/toast/files/src/components/ui/button.tsx +35 -6
- package/registry/toast/files/src/components/ui/toast.tsx +25 -14
- package/registry/toast/files/src/lib/kit.tsx +8 -5
- package/schema/components.schema.json +1 -1
- package/registry/_docs/color-swatch.json +0 -82
- package/registry/_docs/kpi-card.json +0 -14
- package/registry/_docs/like-button.json +0 -92
- package/registry/_docs/morph-button.json +0 -128
- package/registry/_docs/pagination-dots.json +0 -108
- package/registry/_docs/shimmer-button.json +0 -98
- package/registry/_docs/tag-input.json +0 -148
- package/registry/color-swatch.json +0 -17
- package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +0 -45
- package/registry/kpi-card.json +0 -14
- package/registry/like-button.json +0 -18
- package/registry/morph-button.json +0 -18
- package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +0 -262
- package/registry/pagination-dots/files/src/lib/kit.tsx +0 -219
- package/registry/pagination-dots.json +0 -15
- package/registry/shimmer-button.json +0 -17
- package/registry/tag-input/files/src/components/ui/tag-input.tsx +0 -720
- package/registry/tag-input.json +0 -17
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
1
|
+
import React, { useEffect, useRef, useState, type ComponentRef } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
AccessibilityInfo,
|
|
3
4
|
Animated,
|
|
4
5
|
Easing,
|
|
5
6
|
Platform,
|
|
@@ -55,6 +56,9 @@ export interface OtpInputProps {
|
|
|
55
56
|
type CellState = 'idle' | 'error' | 'success';
|
|
56
57
|
|
|
57
58
|
const GAP = 8;
|
|
59
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
60
|
+
// content text scales all the way. A capped digit (1.35 × 0.46w, line 1.2×) still fits the shortest cell (pill, 1w).
|
|
61
|
+
const FONT_CAP = 1.35;
|
|
58
62
|
/** react-native-web draws the browser focus outline; our border + halo is the focus indicator. */
|
|
59
63
|
const webNoOutline = Platform.OS === 'web' ? ({ outlineStyle: 'none', outlineWidth: 0 } as object) : null;
|
|
60
64
|
const SEP = 22;
|
|
@@ -84,7 +88,8 @@ export function OtpInput({
|
|
|
84
88
|
style,
|
|
85
89
|
}: OtpInputProps) {
|
|
86
90
|
const { colors } = useKit();
|
|
87
|
-
const
|
|
91
|
+
const reduce = useReduceMotion();
|
|
92
|
+
const input = useRef<ComponentRef<typeof TextInput>>(null);
|
|
88
93
|
const [inner, setInner] = useState('');
|
|
89
94
|
const [focused, setFocused] = useState(false);
|
|
90
95
|
const [width, setWidth] = useState(0);
|
|
@@ -92,12 +97,13 @@ export function OtpInput({
|
|
|
92
97
|
const shake = useRef(new Animated.Value(0)).current;
|
|
93
98
|
|
|
94
99
|
useEffect(() => {
|
|
95
|
-
|
|
100
|
+
// Reduce Motion: no shake; the cells still turn the danger colour.
|
|
101
|
+
if (!error || reduce) return;
|
|
96
102
|
shake.setValue(0);
|
|
97
103
|
Animated.sequence(
|
|
98
104
|
[10, -9, 7, -5, 3, 0].map((x) => Animated.timing(shake, { toValue: x, duration: 45, useNativeDriver: true }))
|
|
99
105
|
).start();
|
|
100
|
-
}, [error, shake]);
|
|
106
|
+
}, [error, shake, reduce]);
|
|
101
107
|
|
|
102
108
|
const change = (raw: string) => {
|
|
103
109
|
// No maxLength on the input, so pasting "123-456" or a longer SMS snippet still fills every cell.
|
|
@@ -152,6 +158,7 @@ export function OtpInput({
|
|
|
152
158
|
variant={variant}
|
|
153
159
|
w={cell}
|
|
154
160
|
h={cellH}
|
|
161
|
+
reduce={reduce}
|
|
155
162
|
/>
|
|
156
163
|
</React.Fragment>
|
|
157
164
|
))}
|
|
@@ -176,7 +183,7 @@ export function OtpInput({
|
|
|
176
183
|
importantForAccessibility="yes"
|
|
177
184
|
style={[styles.hidden, webNoOutline]}
|
|
178
185
|
/>
|
|
179
|
-
{message ? <Message text={message} kind={state} /> : null}
|
|
186
|
+
{message ? <Message text={message} kind={state} reduce={reduce} /> : null}
|
|
180
187
|
</View>
|
|
181
188
|
);
|
|
182
189
|
}
|
|
@@ -191,6 +198,7 @@ function Cell({
|
|
|
191
198
|
variant,
|
|
192
199
|
w,
|
|
193
200
|
h,
|
|
201
|
+
reduce,
|
|
194
202
|
}: {
|
|
195
203
|
index: number;
|
|
196
204
|
char?: string;
|
|
@@ -201,8 +209,10 @@ function Cell({
|
|
|
201
209
|
variant: OtpInputVariant;
|
|
202
210
|
w: number;
|
|
203
211
|
h: number;
|
|
212
|
+
reduce: boolean;
|
|
204
213
|
}) {
|
|
205
|
-
const { colors, radius } = useKit();
|
|
214
|
+
const { colors, radius, font: kitFont } = useKit();
|
|
215
|
+
const ff = kitFont.family ? { fontFamily: kitFont.family } : null;
|
|
206
216
|
const filled = !!char;
|
|
207
217
|
// Native driver: digit pop, caret blink, halo, success pulse.
|
|
208
218
|
const pop = useRef(new Animated.Value(filled ? 1 : 0)).current;
|
|
@@ -213,17 +223,22 @@ function Cell({
|
|
|
213
223
|
const ring = useRef(new Animated.Value(0)).current;
|
|
214
224
|
|
|
215
225
|
useEffect(() => {
|
|
216
|
-
if (char) {
|
|
226
|
+
if (char && reduce) {
|
|
227
|
+
// Reduce Motion: the digit appears without the pop.
|
|
228
|
+
pop.setValue(1);
|
|
229
|
+
} else if (char) {
|
|
217
230
|
pop.setValue(0.3);
|
|
218
231
|
Animated.spring(pop, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
|
|
219
232
|
} else {
|
|
220
233
|
Animated.timing(pop, { toValue: 0, duration: 110, useNativeDriver: true }).start();
|
|
221
234
|
}
|
|
222
|
-
}, [char, pop]);
|
|
235
|
+
}, [char, pop, reduce]);
|
|
223
236
|
|
|
224
237
|
useEffect(() => {
|
|
225
238
|
const to = active ? 1 : 0;
|
|
226
|
-
|
|
239
|
+
// Reduce Motion: the halo only fades (its scale is pinned below). The caret keeps blinking.
|
|
240
|
+
if (reduce) Animated.timing(halo, { toValue: to, duration: 150, useNativeDriver: true }).start();
|
|
241
|
+
else Animated.spring(halo, { toValue: to, useNativeDriver: true, ...motion.snappy }).start();
|
|
227
242
|
Animated.timing(ring, { toValue: to, duration: motion.fast, useNativeDriver: false }).start();
|
|
228
243
|
if (!active) return;
|
|
229
244
|
caret.setValue(1);
|
|
@@ -237,17 +252,17 @@ function Cell({
|
|
|
237
252
|
);
|
|
238
253
|
blink.start();
|
|
239
254
|
return () => blink.stop();
|
|
240
|
-
}, [active, halo, ring, caret]);
|
|
255
|
+
}, [active, halo, ring, caret, reduce]);
|
|
241
256
|
|
|
242
257
|
useEffect(() => {
|
|
243
|
-
if (state !== 'success') return;
|
|
258
|
+
if (state !== 'success' || reduce) return;
|
|
244
259
|
pulse.setValue(0);
|
|
245
260
|
Animated.sequence([
|
|
246
261
|
Animated.delay(index * 55),
|
|
247
262
|
Animated.timing(pulse, { toValue: 1, duration: 140, easing: Easing.out(Easing.quad), useNativeDriver: true }),
|
|
248
263
|
Animated.spring(pulse, { toValue: 0, useNativeDriver: true, ...motion.bouncy }),
|
|
249
264
|
]).start();
|
|
250
|
-
}, [state, index, pulse]);
|
|
265
|
+
}, [state, index, pulse, reduce]);
|
|
251
266
|
|
|
252
267
|
const underline = variant === 'underline';
|
|
253
268
|
const pill = variant === 'pill';
|
|
@@ -288,7 +303,7 @@ function Cell({
|
|
|
288
303
|
borderRadius: underline ? 10 : r + 4,
|
|
289
304
|
backgroundColor: alpha(tone, 0.18),
|
|
290
305
|
opacity: halo,
|
|
291
|
-
transform: [{ scale: halo.interpolate({ inputRange: [0, 1], outputRange: [0.86, 1] }) }],
|
|
306
|
+
transform: [{ scale: halo.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.86, 1] }) }],
|
|
292
307
|
},
|
|
293
308
|
]}
|
|
294
309
|
/>
|
|
@@ -305,7 +320,7 @@ function Cell({
|
|
|
305
320
|
{secure ? (
|
|
306
321
|
<View style={{ width: dot, height: dot, borderRadius: dot / 2, backgroundColor: fg }} />
|
|
307
322
|
) : (
|
|
308
|
-
<Text style={[styles.char, { color: fg, fontSize: font, lineHeight: Math.round(font * 1.2) }]}>{char}</Text>
|
|
323
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.char, ff, { color: fg, fontSize: font, lineHeight: Math.round(font * 1.2) }]}>{char}</Text>
|
|
309
324
|
)}
|
|
310
325
|
</Animated.View>
|
|
311
326
|
) : active ? (
|
|
@@ -316,20 +331,22 @@ function Cell({
|
|
|
316
331
|
);
|
|
317
332
|
}
|
|
318
333
|
|
|
319
|
-
/** Helper / error / success line that fades and slides in whenever its text changes. */
|
|
320
|
-
function Message({ text, kind }: { text: string; kind: CellState }) {
|
|
321
|
-
const { colors } = useKit();
|
|
334
|
+
/** Helper / error / success line that fades and slides in whenever its text changes (only fades with Reduce Motion). */
|
|
335
|
+
function Message({ text, kind, reduce }: { text: string; kind: CellState; reduce: boolean }) {
|
|
336
|
+
const { colors, font } = useKit();
|
|
337
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
322
338
|
const enter = useRef(new Animated.Value(0)).current;
|
|
323
339
|
const draw = useRef(new Animated.Value(0)).current;
|
|
324
340
|
|
|
325
341
|
useEffect(() => {
|
|
326
342
|
enter.setValue(0);
|
|
327
|
-
Animated.timing(enter, { toValue: 1, duration: motion.normal, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();
|
|
328
|
-
|
|
329
|
-
|
|
343
|
+
Animated.timing(enter, { toValue: 1, duration: reduce ? 150 : motion.normal, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();
|
|
344
|
+
// Reduce Motion: the tick is drawn at once.
|
|
345
|
+
draw.setValue(reduce ? 1 : 0);
|
|
346
|
+
if (kind === 'success' && !reduce) {
|
|
330
347
|
Animated.timing(draw, { toValue: 1, duration: 360, delay: 120, easing: Easing.out(Easing.cubic), useNativeDriver: false }).start();
|
|
331
348
|
}
|
|
332
|
-
}, [text, kind, enter, draw]);
|
|
349
|
+
}, [text, kind, enter, draw, reduce]);
|
|
333
350
|
|
|
334
351
|
const color = kind === 'error' ? colors.danger : kind === 'success' ? colors.success : colors.mutedForeground;
|
|
335
352
|
return (
|
|
@@ -337,7 +354,7 @@ function Message({ text, kind }: { text: string; kind: CellState }) {
|
|
|
337
354
|
accessibilityLiveRegion="polite"
|
|
338
355
|
style={[
|
|
339
356
|
styles.message,
|
|
340
|
-
{ opacity: enter, transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [4, 0] }) }] },
|
|
357
|
+
{ opacity: enter, transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 4, 0] }) }] },
|
|
341
358
|
]}
|
|
342
359
|
>
|
|
343
360
|
{kind === 'error' ? (
|
|
@@ -359,11 +376,27 @@ function Message({ text, kind }: { text: string; kind: CellState }) {
|
|
|
359
376
|
/>
|
|
360
377
|
</Svg>
|
|
361
378
|
) : null}
|
|
362
|
-
<Text style={[styles.messageText, { color }]}>{text}</Text>
|
|
379
|
+
<Text style={[styles.messageText, ff, { color }]}>{text}</Text>
|
|
363
380
|
</Animated.View>
|
|
364
381
|
);
|
|
365
382
|
}
|
|
366
383
|
|
|
384
|
+
function useReduceMotion() {
|
|
385
|
+
const [reduce, setReduce] = useState(false);
|
|
386
|
+
useEffect(() => {
|
|
387
|
+
let alive = true;
|
|
388
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
389
|
+
.then((r) => alive && setReduce(r))
|
|
390
|
+
.catch(() => {});
|
|
391
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
392
|
+
return () => {
|
|
393
|
+
alive = false;
|
|
394
|
+
sub.remove();
|
|
395
|
+
};
|
|
396
|
+
}, []);
|
|
397
|
+
return reduce;
|
|
398
|
+
}
|
|
399
|
+
|
|
367
400
|
const styles = StyleSheet.create({
|
|
368
401
|
root: { alignSelf: 'stretch' },
|
|
369
402
|
disabled: { opacity: 0.5 },
|
|
@@ -375,6 +408,6 @@ const styles = StyleSheet.create({
|
|
|
375
408
|
sep: { alignItems: 'center', justifyContent: 'center' },
|
|
376
409
|
dash: { width: 10, height: 2, borderRadius: 1, opacity: 0.6 },
|
|
377
410
|
message: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, marginTop: 6 },
|
|
378
|
-
messageText: { fontSize: 12.5, lineHeight: 17, fontWeight: '400' },
|
|
411
|
+
messageText: { flexShrink: 1, fontSize: 12.5, lineHeight: 17, fontWeight: '400' },
|
|
379
412
|
hidden: { position: 'absolute', width: 1, height: 1, opacity: 0 },
|
|
380
413
|
});
|
|
@@ -181,14 +181,17 @@ export function useKit(): Theme {
|
|
|
181
181
|
return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Shared motion presets so the whole kit moves the same way. Every spring is critically damped
|
|
186
|
+
* (damping ≥ 2·√(stiffness·mass)): it settles smoothly with no overshoot or wobble.
|
|
187
|
+
*/
|
|
185
188
|
export const motion = {
|
|
186
189
|
/** Toggles, selection indicators. */
|
|
187
|
-
snappy: { damping:
|
|
188
|
-
/** Press feedback, pops. */
|
|
189
|
-
bouncy: { damping:
|
|
190
|
+
snappy: { damping: 30, stiffness: 260, mass: 0.8 },
|
|
191
|
+
/** Press feedback, pops. (Kept under its old name; it no longer bounces.) */
|
|
192
|
+
bouncy: { damping: 26, stiffness: 220, mass: 0.7 },
|
|
190
193
|
/** Sheets, layout changes. */
|
|
191
|
-
gentle: { damping:
|
|
194
|
+
gentle: { damping: 27, stiffness: 160, mass: 1 },
|
|
192
195
|
fast: 160,
|
|
193
196
|
normal: 240,
|
|
194
197
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
1
|
+
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState, type ComponentRef } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
AccessibilityInfo,
|
|
3
4
|
Animated,
|
|
4
5
|
Easing,
|
|
5
6
|
Platform,
|
|
@@ -85,6 +86,9 @@ export const FLOATING_INPUT_SIZES = {
|
|
|
85
86
|
md: { height: 54, font: 16, floated: 12.5, padX: 16, icon: 18, radius: 0 },
|
|
86
87
|
lg: { height: 64, font: 18, floated: 13.5, padX: 18, icon: 20, radius: 4 },
|
|
87
88
|
};
|
|
89
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
90
|
+
// content text scales all the way.
|
|
91
|
+
const FONT_CAP = 1.35;
|
|
88
92
|
const HALO = 4;
|
|
89
93
|
/** Font size of the static label used when `floatingLabel` is false. */
|
|
90
94
|
const LABEL_ABOVE_FONT = { sm: 13, md: 14, lg: 14.5 };
|
|
@@ -99,7 +103,7 @@ const NO_WEB_OUTLINE = Platform.OS === 'web' ? ({ outlineStyle: 'none', outlineW
|
|
|
99
103
|
const AUTOFILL_CSS = `@keyframes ekAutofillOn{from{}to{}}@keyframes ekAutofillOff{from{}to{}}
|
|
100
104
|
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
105
|
input:not(:-webkit-autofill){animation:ekAutofillOff 1ms}`;
|
|
102
|
-
function useWebAutofill(inputRef: React.RefObject<TextInput | null>, onChange: (filled: boolean) => void) {
|
|
106
|
+
function useWebAutofill(inputRef: React.RefObject<ComponentRef<typeof TextInput> | null>, onChange: (filled: boolean) => void) {
|
|
103
107
|
useEffect(() => {
|
|
104
108
|
if (Platform.OS !== 'web' || typeof document === 'undefined') return;
|
|
105
109
|
if (!document.getElementById('expokit-autofill')) {
|
|
@@ -126,7 +130,7 @@ function useWebAutofill(inputRef: React.RefObject<TextInput | null>, onChange: (
|
|
|
126
130
|
* cutting a notch in the outline (Material-style). Focus draws a soft halo, errors shake,
|
|
127
131
|
* success pops a check — all spring-driven.
|
|
128
132
|
*/
|
|
129
|
-
export const FloatingInput = forwardRef<TextInput
|
|
133
|
+
export const FloatingInput = forwardRef<ComponentRef<typeof TextInput>, FloatingInputProps>(function FloatingInput(
|
|
130
134
|
{
|
|
131
135
|
label,
|
|
132
136
|
floatingLabel = true,
|
|
@@ -166,10 +170,11 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
166
170
|
ref
|
|
167
171
|
) {
|
|
168
172
|
const { colors, radius, font } = useKit();
|
|
173
|
+
const reduce = useReduceMotion();
|
|
169
174
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
170
175
|
const S = FLOATING_INPUT_SIZES[size];
|
|
171
176
|
// The user's border colour only replaces the resting one; numeric height / radius drive the layout maths.
|
|
172
|
-
const { borderColor: userBorder, ...userField } =
|
|
177
|
+
const { borderColor: userBorder, ...userField } = flattenView(fieldStyle);
|
|
173
178
|
const userLabelColor = StyleSheet.flatten(labelStyleProp)?.color;
|
|
174
179
|
const HEIGHT = typeof userField.height === 'number' ? userField.height : S.height;
|
|
175
180
|
// Floated size stays relative to the resting size, so a bigger label floats bigger at the same ratio.
|
|
@@ -179,8 +184,8 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
179
184
|
const slot = S.icon + 10; // room for the clear button / check
|
|
180
185
|
const tint = accent ?? colors.accent;
|
|
181
186
|
const bg = surface ?? colors.background;
|
|
182
|
-
const inputRef = useRef<TextInput
|
|
183
|
-
useImperativeHandle(ref, () => inputRef.current as TextInput);
|
|
187
|
+
const inputRef = useRef<ComponentRef<typeof TextInput>>(null);
|
|
188
|
+
useImperativeHandle(ref, () => inputRef.current as ComponentRef<typeof TextInput>);
|
|
184
189
|
|
|
185
190
|
const [focused, setFocused] = useState(false);
|
|
186
191
|
const [inner, setInner] = useState(defaultValue ?? '');
|
|
@@ -200,29 +205,36 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
200
205
|
const check = useRef(new Animated.Value(ok ? 1 : 0)).current; // native
|
|
201
206
|
|
|
202
207
|
useEffect(() => {
|
|
203
|
-
|
|
204
|
-
|
|
208
|
+
if (reduce) float.setValue(floating ? 1 : 0);
|
|
209
|
+
else Animated.spring(float, { toValue: floating ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
210
|
+
}, [floating, float, reduce]);
|
|
205
211
|
|
|
206
212
|
useEffect(() => {
|
|
207
213
|
Animated.timing(focus, { toValue: isFocused ? 1 : 0, duration: 180, easing: Easing.out(Easing.quad), useNativeDriver: false }).start();
|
|
208
|
-
|
|
209
|
-
|
|
214
|
+
// Reduce Motion: halo and indicator bar only fade (their scales are pinned below).
|
|
215
|
+
if (reduce) Animated.timing(glow, { toValue: isFocused ? 1 : 0, duration: 150, useNativeDriver: true }).start();
|
|
216
|
+
else Animated.spring(glow, { toValue: isFocused ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
217
|
+
}, [isFocused, focus, glow, reduce]);
|
|
210
218
|
|
|
211
219
|
useEffect(() => {
|
|
212
|
-
|
|
213
|
-
|
|
220
|
+
const to = clearable && text && editable ? 1 : 0;
|
|
221
|
+
if (reduce) Animated.timing(clear, { toValue: to, duration: 150, useNativeDriver: true }).start();
|
|
222
|
+
else Animated.spring(clear, { toValue: to, useNativeDriver: true, ...motion.bouncy }).start();
|
|
223
|
+
}, [clearable, text, editable, clear, reduce]);
|
|
214
224
|
|
|
215
225
|
useEffect(() => {
|
|
216
|
-
Animated.
|
|
217
|
-
|
|
226
|
+
if (reduce) Animated.timing(check, { toValue: ok ? 1 : 0, duration: 150, useNativeDriver: true }).start();
|
|
227
|
+
else Animated.spring(check, { toValue: ok ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
|
|
228
|
+
}, [ok, check, reduce]);
|
|
218
229
|
|
|
219
230
|
useEffect(() => {
|
|
220
|
-
|
|
231
|
+
// Reduce Motion: no shake; the red border and message still flag the error.
|
|
232
|
+
if (!error || reduce) return;
|
|
221
233
|
shake.setValue(0);
|
|
222
234
|
Animated.sequence(
|
|
223
235
|
[10, -8, 6, -4, 0].map((x) => Animated.timing(shake, { toValue: x, duration: 55, useNativeDriver: true }))
|
|
224
236
|
).start();
|
|
225
|
-
}, [error, shake]);
|
|
237
|
+
}, [error, shake, reduce]);
|
|
226
238
|
|
|
227
239
|
const setText = (next: string) => {
|
|
228
240
|
if (value === undefined) setInner(next);
|
|
@@ -310,23 +322,24 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
310
322
|
borderRadius: corner + HALO,
|
|
311
323
|
backgroundColor: active,
|
|
312
324
|
opacity: glow.interpolate({ inputRange: [0, 1], outputRange: [0, 0.18], extrapolate: 'clamp' }),
|
|
313
|
-
transform: [{ scale: glow.interpolate({ inputRange: [0, 1], outputRange: [0.97, 1] }) }],
|
|
325
|
+
transform: [{ scale: glow.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.97, 1] }) }],
|
|
314
326
|
},
|
|
315
327
|
]}
|
|
316
328
|
/>
|
|
317
329
|
)}
|
|
318
330
|
<Pressable onPress={() => inputRef.current?.focus()} disabled={!editable} accessible={false}>
|
|
319
|
-
<Animated.View style={[styles.field, boxStyle, {
|
|
331
|
+
<Animated.View style={[styles.field, boxStyle, { minHeight: HEIGHT }, userField, { borderColor }]}>
|
|
320
332
|
{left ? <View style={[styles.left, { left: underline ? 2 : S.padX - 2 }, underline && { top: contentTop }]}>{left}</View> : null}
|
|
321
333
|
<View style={[styles.row, { paddingLeft: leftPad, paddingRight: rightPad, paddingTop: contentTop }]}>
|
|
322
334
|
{prefix ? (
|
|
323
|
-
<Animated.Text style={[styles.affix, styles.prefix, ff, { fontSize: S.font }, { color: colors.mutedForeground }, prefixStyle, { opacity: affixOpacity }]}>
|
|
335
|
+
<Animated.Text maxFontSizeMultiplier={FONT_CAP} style={[styles.affix, styles.prefix, ff, { fontSize: S.font }, { color: colors.mutedForeground }, prefixStyle, { opacity: affixOpacity }]}>
|
|
324
336
|
{prefix}
|
|
325
337
|
</Animated.Text>
|
|
326
338
|
) : null}
|
|
327
339
|
<TextInput
|
|
328
340
|
ref={inputRef}
|
|
329
341
|
accessibilityLabel={label}
|
|
342
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
330
343
|
{...props}
|
|
331
344
|
maxLength={maxLength}
|
|
332
345
|
editable={editable}
|
|
@@ -347,7 +360,7 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
347
360
|
style={[styles.input, ff, { color: colors.foreground, fontSize: S.font }, inputStyle, NO_WEB_OUTLINE]}
|
|
348
361
|
/>
|
|
349
362
|
{suffix ? (
|
|
350
|
-
<Animated.Text style={[styles.affix, styles.suffix, ff, { fontSize: S.font }, { color: colors.mutedForeground }, suffixStyle, { opacity: affixOpacity }]}>
|
|
363
|
+
<Animated.Text maxFontSizeMultiplier={FONT_CAP} style={[styles.affix, styles.suffix, ff, { fontSize: S.font }, { color: colors.mutedForeground }, suffixStyle, { opacity: affixOpacity }]}>
|
|
351
364
|
{suffix}
|
|
352
365
|
</Animated.Text>
|
|
353
366
|
) : null}
|
|
@@ -358,6 +371,7 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
358
371
|
style={[styles.labelWrap, { left: leftPad, right: right ? adorn : S.padX - 4, top: contentTop, height: HEIGHT - contentTop }, labelStyle]}
|
|
359
372
|
>
|
|
360
373
|
<Animated.Text
|
|
374
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
361
375
|
numberOfLines={1}
|
|
362
376
|
style={[
|
|
363
377
|
styles.label,
|
|
@@ -380,7 +394,7 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
380
394
|
) : null}
|
|
381
395
|
{clearable ? (
|
|
382
396
|
<Animated.View
|
|
383
|
-
style={[styles.slot, { right: edge, top: contentTop, opacity: clear, transform: [{ scale: clear }] }]}
|
|
397
|
+
style={[styles.slot, { right: edge, top: contentTop, opacity: clear, transform: [{ scale: reduce ? 1 : clear }] }]}
|
|
384
398
|
pointerEvents={text && editable ? 'box-none' : 'none'}
|
|
385
399
|
>
|
|
386
400
|
<Pressable
|
|
@@ -408,8 +422,8 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
408
422
|
top: contentTop,
|
|
409
423
|
opacity: check.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
410
424
|
transform: [
|
|
411
|
-
{ scale: check.interpolate({ inputRange: [0, 1], outputRange: [0.3, 1] }) },
|
|
412
|
-
{ rotate: check.interpolate({ inputRange: [0, 1], outputRange: ['-30deg', '0deg'] }) },
|
|
425
|
+
{ scale: check.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.3, 1] }) },
|
|
426
|
+
{ rotate: check.interpolate({ inputRange: [0, 1], outputRange: [reduce ? '0deg' : '-30deg', '0deg'] }) },
|
|
413
427
|
],
|
|
414
428
|
},
|
|
415
429
|
]}
|
|
@@ -423,10 +437,14 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
423
437
|
{outlined ? null : (
|
|
424
438
|
<>
|
|
425
439
|
<View pointerEvents="none" style={[styles.baseLine, { height: underline ? 1.5 : 1, backgroundColor: rest }]} />
|
|
426
|
-
{/* Grows out from the centre on focus. */}
|
|
440
|
+
{/* Grows out from the centre on focus (fades in with Reduce Motion). */}
|
|
427
441
|
<Animated.View
|
|
428
442
|
pointerEvents="none"
|
|
429
|
-
style={[
|
|
443
|
+
style={[
|
|
444
|
+
styles.bar,
|
|
445
|
+
{ backgroundColor: active },
|
|
446
|
+
reduce ? { opacity: glowClamped } : { transform: [{ scaleX: glowClamped }] },
|
|
447
|
+
]}
|
|
430
448
|
/>
|
|
431
449
|
</>
|
|
432
450
|
)}
|
|
@@ -436,10 +454,11 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
436
454
|
{message || showCounter ? (
|
|
437
455
|
<View style={[styles.footer, underline && styles.footerFlush]}>
|
|
438
456
|
<View style={styles.messageSlot}>
|
|
439
|
-
{message ? <FieldMessage key={tone + message} text={message} tone={tone} color={toneColor} style={messageStyle} /> : null}
|
|
457
|
+
{message ? <FieldMessage key={tone + message} text={message} tone={tone} color={toneColor} style={messageStyle} reduce={reduce} /> : null}
|
|
440
458
|
</View>
|
|
441
459
|
{showCounter ? (
|
|
442
460
|
<Text
|
|
461
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
443
462
|
style={[
|
|
444
463
|
styles.count,
|
|
445
464
|
ff,
|
|
@@ -459,13 +478,27 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
459
478
|
|
|
460
479
|
type MessageTone = 'error' | 'success' | 'helper';
|
|
461
480
|
|
|
462
|
-
/** Helper / error / success line. Error and success slide up and fade in. */
|
|
463
|
-
function FieldMessage({
|
|
481
|
+
/** Helper / error / success line. Error and success slide up and fade in (only fade with Reduce Motion). */
|
|
482
|
+
function FieldMessage({
|
|
483
|
+
text,
|
|
484
|
+
tone,
|
|
485
|
+
color,
|
|
486
|
+
style,
|
|
487
|
+
reduce,
|
|
488
|
+
}: {
|
|
489
|
+
text: string;
|
|
490
|
+
tone: MessageTone;
|
|
491
|
+
color: string;
|
|
492
|
+
style?: StyleProp<TextStyle>;
|
|
493
|
+
reduce: boolean;
|
|
494
|
+
}) {
|
|
464
495
|
const { font } = useKit();
|
|
465
496
|
const enter = useRef(new Animated.Value(tone === 'helper' ? 1 : 0)).current; // native
|
|
466
497
|
useEffect(() => {
|
|
467
|
-
if (tone
|
|
468
|
-
|
|
498
|
+
if (tone === 'helper') return;
|
|
499
|
+
if (reduce) Animated.timing(enter, { toValue: 1, duration: 150, useNativeDriver: true }).start();
|
|
500
|
+
else Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
501
|
+
}, [tone, enter, reduce]);
|
|
469
502
|
|
|
470
503
|
return (
|
|
471
504
|
<Animated.View
|
|
@@ -473,7 +506,7 @@ function FieldMessage({ text, tone, color, style }: { text: string; tone: Messag
|
|
|
473
506
|
styles.message,
|
|
474
507
|
{
|
|
475
508
|
opacity: enter.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
476
|
-
transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [4, 0] }) }],
|
|
509
|
+
transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 4, 0] }) }],
|
|
477
510
|
},
|
|
478
511
|
]}
|
|
479
512
|
>
|
|
@@ -500,11 +533,37 @@ function FieldMessage({ text, tone, color, style }: { text: string; tone: Messag
|
|
|
500
533
|
);
|
|
501
534
|
}
|
|
502
535
|
|
|
536
|
+
/**
|
|
537
|
+
* The user's field style as one object. Not StyleSheet.flatten: with TypeScript 6 its RN 0.88 signature
|
|
538
|
+
* rejects a plain StyleProp<ViewStyle> once expo/types adds web-only keys to ViewStyle.
|
|
539
|
+
*/
|
|
540
|
+
function flattenView(style: StyleProp<ViewStyle>): ViewStyle {
|
|
541
|
+
if (!style || typeof style !== 'object') return {};
|
|
542
|
+
if (Array.isArray(style)) return Object.assign({}, ...style.map((s) => flattenView(s as StyleProp<ViewStyle>)));
|
|
543
|
+
return style as ViewStyle;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function useReduceMotion() {
|
|
547
|
+
const [reduce, setReduce] = useState(false);
|
|
548
|
+
useEffect(() => {
|
|
549
|
+
let alive = true;
|
|
550
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
551
|
+
.then((r) => alive && setReduce(r))
|
|
552
|
+
.catch(() => {});
|
|
553
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
554
|
+
return () => {
|
|
555
|
+
alive = false;
|
|
556
|
+
sub.remove();
|
|
557
|
+
};
|
|
558
|
+
}, []);
|
|
559
|
+
return reduce;
|
|
560
|
+
}
|
|
561
|
+
|
|
503
562
|
const styles = StyleSheet.create({
|
|
504
563
|
disabled: { opacity: 0.5 },
|
|
505
564
|
halo: { position: 'absolute', top: -HALO, left: -HALO, right: -HALO, bottom: -HALO },
|
|
506
565
|
field: { justifyContent: 'center' },
|
|
507
|
-
row: { flexDirection: 'row', alignItems: 'center',
|
|
566
|
+
row: { flexDirection: 'row', alignItems: 'center', flexGrow: 1 },
|
|
508
567
|
input: { flex: 1, alignSelf: 'stretch', paddingVertical: 0, paddingHorizontal: 0 },
|
|
509
568
|
affix: { fontWeight: '400' },
|
|
510
569
|
prefix: { marginRight: 4 },
|