@expokit/cli 0.4.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/bin/lib.mjs +1 -1
- package/package.json +1 -1
- package/registry/_docs/accordion.json +1 -1
- package/registry/_docs/avatar.json +6 -6
- package/registry/_docs/badge.json +3 -3
- 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 +2 -2
- package/registry/_docs/button.json +2 -2
- package/registry/_docs/calendar.json +5 -5
- package/registry/_docs/card.json +1 -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/drawer.json +4 -4
- package/registry/_docs/flip-card.json +4 -4
- package/registry/_docs/floating-input.json +1 -1
- package/registry/_docs/grain.json +2 -2
- package/registry/_docs/isometric-bar.json +6 -0
- package/registry/_docs/liquidglass-tab.json +1 -1
- package/registry/_docs/otp-input.json +1 -1
- 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/radio-group.json +1 -1
- package/registry/_docs/search-bar.json +3 -3
- package/registry/_docs/segmented-control.json +1 -1
- package/registry/_docs/select.json +6 -6
- package/registry/_docs/skeleton.json +1 -1
- package/registry/_docs/slider.json +6 -6
- package/registry/_docs/spinner.json +2 -2
- package/registry/_docs/stateful-button.json +1 -1
- package/registry/_docs/stepper.json +3 -3
- package/registry/_docs/tabs.json +2 -2
- package/registry/_docs/text-animation.json +4 -4
- package/registry/_docs/textarea.json +1 -1
- package/registry/_docs/tilt-card.json +3 -3
- package/registry/_docs/time-picker.json +6 -6
- package/registry/_docs/timeline-chart.json +6 -0
- package/registry/_docs/timeline.json +4 -4
- package/registry/_docs/treemap.json +6 -0
- package/registry/_docs/waterfall.json +6 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +31 -3
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +4 -1
- package/registry/alert/files/src/components/ui/alert.tsx +5 -2
- package/registry/alert/files/src/components/ui/button.tsx +32 -4
- package/registry/badge/files/src/components/ui/badge.tsx +2 -2
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +11 -5
- package/registry/button/files/src/components/ui/button.tsx +32 -4
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +33 -6
- package/registry/calendar/files/src/components/ui/calendar.tsx +62 -17
- package/registry/card/files/src/components/ui/card.tsx +25 -5
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +26 -4
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +35 -5
- package/registry/coupon/files/src/components/ui/coupon.tsx +45 -21
- package/registry/coupon/files/src/components/ui/flip-card.tsx +22 -3
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +57 -16
- package/registry/date-picker/files/src/components/ui/calendar.tsx +62 -17
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +96 -42
- package/registry/dialog/files/src/components/ui/dialog.tsx +9 -5
- package/registry/drawer/files/src/components/ui/drawer.tsx +8 -4
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +22 -3
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +75 -26
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +1 -1
- package/registry/list-item/files/src/components/ui/list-item.tsx +10 -3
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +50 -19
- package/registry/password-input/files/src/components/ui/floating-input.tsx +75 -26
- package/registry/password-input/files/src/components/ui/password-input.tsx +53 -19
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +22 -3
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +19 -15
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +57 -16
- package/registry/progress/files/src/components/ui/progress.tsx +9 -2
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +11 -4
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +39 -9
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +70 -17
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +37 -9
- package/registry/select/files/src/components/ui/select.tsx +158 -83
- package/registry/slider/files/src/components/ui/slider.tsx +53 -25
- package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +12 -4
- package/registry/stepper/files/src/components/ui/stepper.tsx +76 -26
- package/registry/switch/files/src/components/ui/switch.tsx +56 -19
- package/registry/tabs/files/src/components/ui/tabs.tsx +7 -4
- package/registry/text-animation/files/src/components/ui/text-animation.tsx +7 -2
- package/registry/textarea/files/src/components/ui/textarea.tsx +104 -36
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +57 -16
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +154 -73
- package/registry/timeline/files/src/components/ui/timeline.tsx +9 -4
- package/registry/toast/files/src/components/ui/button.tsx +32 -4
- package/registry/toast/files/src/components/ui/toast.tsx +15 -6
|
@@ -1,5 +1,6 @@
|
|
|
1
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 };
|
|
@@ -166,6 +170,7 @@ export const FloatingInput = forwardRef<ComponentRef<typeof TextInput>, Floating
|
|
|
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.
|
|
@@ -200,29 +205,36 @@ export const FloatingInput = forwardRef<ComponentRef<typeof TextInput>, Floating
|
|
|
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<ComponentRef<typeof TextInput>, Floating
|
|
|
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<ComponentRef<typeof TextInput>, Floating
|
|
|
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<ComponentRef<typeof TextInput>, Floating
|
|
|
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<ComponentRef<typeof TextInput>, Floating
|
|
|
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<ComponentRef<typeof TextInput>, Floating
|
|
|
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<ComponentRef<typeof TextInput>, Floating
|
|
|
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<ComponentRef<typeof TextInput>, Floating
|
|
|
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<ComponentRef<typeof TextInput>, Floating
|
|
|
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
|
>
|
|
@@ -510,11 +543,27 @@ function flattenView(style: StyleProp<ViewStyle>): ViewStyle {
|
|
|
510
543
|
return style as ViewStyle;
|
|
511
544
|
}
|
|
512
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
|
+
|
|
513
562
|
const styles = StyleSheet.create({
|
|
514
563
|
disabled: { opacity: 0.5 },
|
|
515
564
|
halo: { position: 'absolute', top: -HALO, left: -HALO, right: -HALO, bottom: -HALO },
|
|
516
565
|
field: { justifyContent: 'center' },
|
|
517
|
-
row: { flexDirection: 'row', alignItems: 'center',
|
|
566
|
+
row: { flexDirection: 'row', alignItems: 'center', flexGrow: 1 },
|
|
518
567
|
input: { flex: 1, alignSelf: 'stretch', paddingVertical: 0, paddingHorizontal: 0 },
|
|
519
568
|
affix: { fontWeight: '400' },
|
|
520
569
|
prefix: { marginRight: 4 },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { forwardRef, useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
|
|
2
|
-
import { Animated, Pressable, StyleSheet, Text, View, type TextInput } from 'react-native';
|
|
2
|
+
import { AccessibilityInfo, Animated, Pressable, StyleSheet, Text, View, type TextInput } from 'react-native';
|
|
3
3
|
import Svg, { Circle, Path } from 'react-native-svg';
|
|
4
4
|
|
|
5
5
|
import { FloatingInput, type FloatingInputProps } from '@/components/ui/floating-input';
|
|
@@ -34,6 +34,10 @@ export interface PasswordInputProps extends Omit<FloatingInputProps, 'secureText
|
|
|
34
34
|
mismatchMessage?: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
38
|
+
// content text scales all the way.
|
|
39
|
+
const FONT_CAP = 1.35;
|
|
40
|
+
|
|
37
41
|
const LEVEL_LABELS = ['Too short', 'Weak', 'Okay', 'Strong', 'Very strong'];
|
|
38
42
|
const ICON = { sm: 18, md: 22, lg: 24 };
|
|
39
43
|
|
|
@@ -70,6 +74,7 @@ export const PasswordInput = forwardRef<ComponentRef<typeof TextInput>, Password
|
|
|
70
74
|
ref
|
|
71
75
|
) {
|
|
72
76
|
const { colors } = useKit();
|
|
77
|
+
const reduce = useReduceMotion();
|
|
73
78
|
const [hidden, setHidden] = useState(true);
|
|
74
79
|
const [inner, setInner] = useState(defaultValue ?? '');
|
|
75
80
|
const text = value ?? inner;
|
|
@@ -84,17 +89,21 @@ export const PasswordInput = forwardRef<ComponentRef<typeof TextInput>, Password
|
|
|
84
89
|
|
|
85
90
|
const eye = useRef(new Animated.Value(hidden ? 0 : 1)).current; // native: 1 = visible
|
|
86
91
|
useEffect(() => {
|
|
87
|
-
Animated.
|
|
88
|
-
|
|
92
|
+
if (reduce) Animated.timing(eye, { toValue: hidden ? 0 : 1, duration: 150, useNativeDriver: true }).start();
|
|
93
|
+
else Animated.spring(eye, { toValue: hidden ? 0 : 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
94
|
+
}, [hidden, eye, reduce]);
|
|
89
95
|
|
|
90
96
|
const iconColor = iconColorProp ?? colors.mutedForeground;
|
|
91
97
|
const iconSize = ICON[size];
|
|
92
98
|
const fade = (on: 0 | 1) => ({
|
|
93
99
|
opacity: eye.interpolate({ inputRange: [0, 1], outputRange: on ? [0, 1] : [1, 0], extrapolate: 'clamp' }),
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
// Reduce Motion: a plain cross-fade, no spin or scale.
|
|
101
|
+
transform: reduce
|
|
102
|
+
? []
|
|
103
|
+
: [
|
|
104
|
+
{ scale: eye.interpolate({ inputRange: [0, 1], outputRange: on ? [0.6, 1] : [1, 0.6] }) },
|
|
105
|
+
{ rotate: eye.interpolate({ inputRange: [0, 1], outputRange: on ? ['-45deg', '0deg'] : ['0deg', '45deg'] }) },
|
|
106
|
+
],
|
|
98
107
|
});
|
|
99
108
|
|
|
100
109
|
return (
|
|
@@ -137,8 +146,8 @@ export const PasswordInput = forwardRef<ComponentRef<typeof TextInput>, Password
|
|
|
137
146
|
</Pressable>
|
|
138
147
|
}
|
|
139
148
|
/>
|
|
140
|
-
{strength && !confirming && text.length > 0 ? <Meter level={level} /> : null}
|
|
141
|
-
{rules && !confirming ? <Checklist rules={rules} value={text} /> : null}
|
|
149
|
+
{strength && !confirming && text.length > 0 ? <Meter level={level} reduce={reduce} /> : null}
|
|
150
|
+
{rules && !confirming ? <Checklist rules={rules} value={text} reduce={reduce} /> : null}
|
|
142
151
|
</View>
|
|
143
152
|
);
|
|
144
153
|
});
|
|
@@ -151,7 +160,7 @@ const EYE_OFF =
|
|
|
151
160
|
'M2 2l20 20';
|
|
152
161
|
|
|
153
162
|
/** Four segments that spring-fill left to right; the colour slides with the score. */
|
|
154
|
-
function Meter({ level }: { level: number }) {
|
|
163
|
+
function Meter({ level, reduce }: { level: number; reduce: boolean }) {
|
|
155
164
|
const { colors, dark, font } = useKit();
|
|
156
165
|
const palette = [colors.mutedForeground, colors.danger, colors.warning, colors.success, colors.success];
|
|
157
166
|
const fills = useRef([0, 1, 2, 3].map(() => new Animated.Value(0))).current; // native: scaleX
|
|
@@ -159,16 +168,24 @@ function Meter({ level }: { level: number }) {
|
|
|
159
168
|
const enter = useRef(new Animated.Value(0)).current; // native
|
|
160
169
|
|
|
161
170
|
useEffect(() => {
|
|
162
|
-
|
|
171
|
+
if (reduce) enter.setValue(1);
|
|
172
|
+
else Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
173
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
163
174
|
}, [enter]);
|
|
164
175
|
|
|
165
176
|
useEffect(() => {
|
|
177
|
+
if (reduce) {
|
|
178
|
+
// Segments appear filled at once; the colour still fades.
|
|
179
|
+
fills.forEach((a, i) => a.setValue(i < level ? 1 : 0));
|
|
180
|
+
Animated.timing(tone, { toValue: level, duration: 150, useNativeDriver: false }).start();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
166
183
|
Animated.stagger(
|
|
167
184
|
50,
|
|
168
185
|
fills.map((a, i) => Animated.spring(a, { toValue: i < level ? 1 : 0, useNativeDriver: true, ...motion.snappy }))
|
|
169
186
|
).start();
|
|
170
187
|
Animated.timing(tone, { toValue: level, duration: 220, useNativeDriver: false }).start();
|
|
171
|
-
}, [level, fills, tone]);
|
|
188
|
+
}, [level, fills, tone, reduce]);
|
|
172
189
|
|
|
173
190
|
const color = tone.interpolate({ inputRange: [0, 1, 2, 3, 4], outputRange: palette });
|
|
174
191
|
const track = dark ? colors.muted : colors.border;
|
|
@@ -199,27 +216,28 @@ function Meter({ level }: { level: number }) {
|
|
|
199
216
|
</View>
|
|
200
217
|
))}
|
|
201
218
|
</View>
|
|
202
|
-
<Text style={[styles.level, font.family ? { fontFamily: font.family } : null, { color: palette[level] }]}>{LEVEL_LABELS[level]}</Text>
|
|
219
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.level, font.family ? { fontFamily: font.family } : null, { color: palette[level] }]}>{LEVEL_LABELS[level]}</Text>
|
|
203
220
|
</Animated.View>
|
|
204
221
|
);
|
|
205
222
|
}
|
|
206
223
|
|
|
207
|
-
function Checklist({ rules, value }: { rules: PasswordRequirement[]; value: string }) {
|
|
224
|
+
function Checklist({ rules, value, reduce }: { rules: PasswordRequirement[]; value: string; reduce: boolean }) {
|
|
208
225
|
return (
|
|
209
226
|
<View style={styles.checklist} accessibilityRole="list">
|
|
210
227
|
{rules.map((r) => (
|
|
211
|
-
<Requirement key={r.label} label={r.label} met={r.test(value)} />
|
|
228
|
+
<Requirement key={r.label} label={r.label} met={r.test(value)} reduce={reduce} />
|
|
212
229
|
))}
|
|
213
230
|
</View>
|
|
214
231
|
);
|
|
215
232
|
}
|
|
216
233
|
|
|
217
|
-
function Requirement({ label, met }: { label: string; met: boolean }) {
|
|
234
|
+
function Requirement({ label, met, reduce }: { label: string; met: boolean; reduce: boolean }) {
|
|
218
235
|
const { colors, dark, font } = useKit();
|
|
219
236
|
const tick = useRef(new Animated.Value(met ? 1 : 0)).current; // native
|
|
220
237
|
useEffect(() => {
|
|
221
|
-
Animated.
|
|
222
|
-
|
|
238
|
+
if (reduce) Animated.timing(tick, { toValue: met ? 1 : 0, duration: 150, useNativeDriver: true }).start();
|
|
239
|
+
else Animated.spring(tick, { toValue: met ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
|
|
240
|
+
}, [met, tick, reduce]);
|
|
223
241
|
|
|
224
242
|
return (
|
|
225
243
|
<View
|
|
@@ -237,7 +255,7 @@ function Requirement({ label, met }: { label: string; met: boolean }) {
|
|
|
237
255
|
StyleSheet.absoluteFill,
|
|
238
256
|
{
|
|
239
257
|
opacity: tick.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
240
|
-
transform: [{ scale: tick.interpolate({ inputRange: [0, 1], outputRange: [0.2, 1] }) }],
|
|
258
|
+
transform: reduce ? [] : [{ scale: tick.interpolate({ inputRange: [0, 1], outputRange: [0.2, 1] }) }],
|
|
241
259
|
},
|
|
242
260
|
]}
|
|
243
261
|
>
|
|
@@ -258,6 +276,22 @@ function Requirement({ label, met }: { label: string; met: boolean }) {
|
|
|
258
276
|
);
|
|
259
277
|
}
|
|
260
278
|
|
|
279
|
+
function useReduceMotion() {
|
|
280
|
+
const [reduce, setReduce] = useState(false);
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
let alive = true;
|
|
283
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
284
|
+
.then((r) => alive && setReduce(r))
|
|
285
|
+
.catch(() => {});
|
|
286
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
287
|
+
return () => {
|
|
288
|
+
alive = false;
|
|
289
|
+
sub.remove();
|
|
290
|
+
};
|
|
291
|
+
}, []);
|
|
292
|
+
return reduce;
|
|
293
|
+
}
|
|
294
|
+
|
|
261
295
|
const styles = StyleSheet.create({
|
|
262
296
|
meter: { flexDirection: 'row', alignItems: 'center', gap: 10, marginTop: 10, marginHorizontal: 4 },
|
|
263
297
|
bars: { flex: 1, flexDirection: 'row', gap: 6 },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import { Animated, Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
import { AccessibilityInfo, Animated, Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { motion } from '@/lib/kit';
|
|
5
5
|
|
|
@@ -48,12 +48,15 @@ const SQUEEZE = {
|
|
|
48
48
|
* The flip as an animated 0 → 1 value, springing like FlipCard's own, plus `shadowScaleX` — pass
|
|
49
49
|
* `progress` to the FlipCard and `shadowScaleX` to a TiltCard wrapped round it, so the tilt
|
|
50
50
|
* shadow narrows as the card turns edge-on instead of showing as a dark box behind it.
|
|
51
|
+
* With Reduce Motion on, the card turns over at once (no 3D turn).
|
|
51
52
|
*/
|
|
52
53
|
export function useFlipProgress(flipped: boolean) {
|
|
54
|
+
const reduce = useReduceMotion();
|
|
53
55
|
const progress = useRef(new Animated.Value(flipped ? 1 : 0)).current;
|
|
54
56
|
useEffect(() => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
if (reduce) progress.setValue(flipped ? 1 : 0);
|
|
58
|
+
else Animated.spring(progress, { toValue: flipped ? 1 : 0, ...motion.gentle, useNativeDriver: true }).start();
|
|
59
|
+
}, [flipped, progress, reduce]);
|
|
57
60
|
const shadowScaleX = useMemo(() => progress.interpolate(SQUEEZE), [progress]);
|
|
58
61
|
return { progress, shadowScaleX };
|
|
59
62
|
}
|
|
@@ -185,6 +188,22 @@ export function FlipCard({
|
|
|
185
188
|
);
|
|
186
189
|
}
|
|
187
190
|
|
|
191
|
+
function useReduceMotion() {
|
|
192
|
+
const [reduce, setReduce] = useState(false);
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
let alive = true;
|
|
195
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
196
|
+
.then((r) => alive && setReduce(r))
|
|
197
|
+
.catch(() => {});
|
|
198
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
199
|
+
return () => {
|
|
200
|
+
alive = false;
|
|
201
|
+
sub.remove();
|
|
202
|
+
};
|
|
203
|
+
}, []);
|
|
204
|
+
return reduce;
|
|
205
|
+
}
|
|
206
|
+
|
|
188
207
|
const styles = StyleSheet.create({
|
|
189
208
|
face: { overflow: 'hidden', backfaceVisibility: 'hidden' },
|
|
190
209
|
back: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
|
|
@@ -71,6 +71,10 @@ export interface PaymentCardLabels {
|
|
|
71
71
|
accessibility?: string;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// Everything is printed on a fixed-shape card, so all its text is control text: it stops growing at iOS's largest
|
|
75
|
+
// standard text size (xxxL ≈ 1.35×) so the card keeps its shape.
|
|
76
|
+
const FONT_CAP = 1.35;
|
|
77
|
+
|
|
74
78
|
/**
|
|
75
79
|
* "4821 7590 1234 0317" → groups of 4 (Amex 4-6-5, 14-digit Diners 4-6-4, 19-digit cards get a
|
|
76
80
|
* fifth group). Masking hides every digit but the last four, whatever the length. Keeps • placeholders.
|
|
@@ -111,19 +115,19 @@ function BrandMark({ brand, ink }: { brand: PaymentCardBrand; ink: string }) {
|
|
|
111
115
|
);
|
|
112
116
|
}
|
|
113
117
|
if (brand === 'visa') {
|
|
114
|
-
return <Text style={[styles.visa, { color: ink }]}>VISA</Text>;
|
|
118
|
+
return <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.visa, { color: ink }]}>VISA</Text>;
|
|
115
119
|
}
|
|
116
120
|
if (brand === 'amex') {
|
|
117
121
|
return (
|
|
118
122
|
<View style={[styles.amex, { borderColor: ink }]}>
|
|
119
|
-
<Text style={{ color: ink, fontSize: 9, fontWeight: '800', letterSpacing: 0.6, lineHeight: 10 }}>AMERICAN</Text>
|
|
120
|
-
<Text style={{ color: ink, fontSize: 9, fontWeight: '800', letterSpacing: 0.6, lineHeight: 10 }}>EXPRESS</Text>
|
|
123
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={{ color: ink, fontSize: 9, fontWeight: '800', letterSpacing: 0.6, lineHeight: 10 }}>AMERICAN</Text>
|
|
124
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={{ color: ink, fontSize: 9, fontWeight: '800', letterSpacing: 0.6, lineHeight: 10 }}>EXPRESS</Text>
|
|
121
125
|
</View>
|
|
122
126
|
);
|
|
123
127
|
}
|
|
124
128
|
return (
|
|
125
129
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 3 }}>
|
|
126
|
-
<Text style={{ color: ink, fontSize: 18, fontWeight: '800', fontStyle: 'italic', letterSpacing: -0.3 }}>RuPay</Text>
|
|
130
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={{ color: ink, fontSize: 18, fontWeight: '800', fontStyle: 'italic', letterSpacing: -0.3 }}>RuPay</Text>
|
|
127
131
|
<Svg width={14} height={16} viewBox="0 0 14 16">
|
|
128
132
|
<Path d="M2 1 L10 8 L2 15 Z" fill="#F47920" />
|
|
129
133
|
<Path d="M6 1 L14 8 L6 15 Z" fill="#0B8E36" />
|
|
@@ -278,28 +282,28 @@ export function PaymentCard({
|
|
|
278
282
|
{tone === 'light' ? <Shade strength={image ? 0.38 : 0.2} /> : null}
|
|
279
283
|
<View style={styles.inner}>
|
|
280
284
|
<View style={styles.row}>
|
|
281
|
-
<Text style={[{ color: ink, fontSize: 16, fontWeight: '700', letterSpacing: -0.2 }, ff, emboss]}>{bank ?? ''}</Text>
|
|
282
|
-
{label ? <Text style={[{ color: soft, fontSize: 11, fontWeight: '600', letterSpacing: 1 }, ff, emboss]}>{label.toUpperCase()}</Text> : null}
|
|
285
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ flexShrink: 1, color: ink, fontSize: 16, fontWeight: '700', letterSpacing: -0.2 }, ff, emboss]}>{bank ?? ''}</Text>
|
|
286
|
+
{label ? <Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: soft, fontSize: 11, fontWeight: '600', letterSpacing: 1 }, ff, emboss]}>{label.toUpperCase()}</Text> : null}
|
|
283
287
|
</View>
|
|
284
288
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
|
|
285
289
|
<Chip finish={chip} />
|
|
286
290
|
<Contactless ink={ink} />
|
|
287
291
|
</View>
|
|
288
292
|
<View style={{ gap: 10 }}>
|
|
289
|
-
<Text style={[{ color: ink, fontSize: 19, fontWeight: '500', letterSpacing: 2, fontVariant: ['tabular-nums'] }, ff, emboss]}>
|
|
293
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: ink, fontSize: 19, fontWeight: '500', letterSpacing: 2, fontVariant: ['tabular-nums'] }, ff, emboss]}>
|
|
290
294
|
{formatNumber(number, brand, masked)}
|
|
291
295
|
</Text>
|
|
292
296
|
<View style={[styles.row, { alignItems: 'flex-end' }]}>
|
|
293
297
|
<View style={{ flexDirection: 'row', gap: 20, flexShrink: 1 }}>
|
|
294
298
|
<View style={{ flexShrink: 1 }}>
|
|
295
|
-
<Text style={[styles.tiny, ff, { color: soft }]}>{text.cardholder}</Text>
|
|
296
|
-
<Text style={[{ color: ink, fontSize: 13, fontWeight: '600', letterSpacing: 0.8 }, ff, emboss]} numberOfLines={1}>
|
|
299
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.tiny, ff, { color: soft }]}>{text.cardholder}</Text>
|
|
300
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: ink, fontSize: 13, fontWeight: '600', letterSpacing: 0.8 }, ff, emboss]} numberOfLines={1}>
|
|
297
301
|
{name.toUpperCase()}
|
|
298
302
|
</Text>
|
|
299
303
|
</View>
|
|
300
304
|
<View>
|
|
301
|
-
<Text style={[styles.tiny, ff, { color: soft }]}>{text.validThru}</Text>
|
|
302
|
-
<Text style={[{ color: ink, fontSize: 13, fontWeight: '600', fontVariant: ['tabular-nums'] }, ff, emboss]}>{expiry}</Text>
|
|
305
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.tiny, ff, { color: soft }]}>{text.validThru}</Text>
|
|
306
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: ink, fontSize: 13, fontWeight: '600', fontVariant: ['tabular-nums'] }, ff, emboss]}>{expiry}</Text>
|
|
303
307
|
</View>
|
|
304
308
|
</View>
|
|
305
309
|
<BrandMark brand={brand} ink={ink} />
|
|
@@ -309,7 +313,7 @@ export function PaymentCard({
|
|
|
309
313
|
{frozen ? (
|
|
310
314
|
<View style={[StyleSheet.absoluteFill, styles.center, { backgroundColor: 'rgba(214,232,255,0.28)' }]}>
|
|
311
315
|
<View style={styles.frozenPill}>
|
|
312
|
-
<Text style={[{ color: '#ffffff', fontSize: 13, fontWeight: '700', letterSpacing: 0.4 }, ff]}>❄︎ {text.frozen}</Text>
|
|
316
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: '#ffffff', fontSize: 13, fontWeight: '700', letterSpacing: 0.4 }, ff]}>❄︎ {text.frozen}</Text>
|
|
313
317
|
</View>
|
|
314
318
|
</View>
|
|
315
319
|
) : null}
|
|
@@ -330,15 +334,15 @@ export function PaymentCard({
|
|
|
330
334
|
</Svg>
|
|
331
335
|
</View>
|
|
332
336
|
<View style={styles.cvv}>
|
|
333
|
-
<Text style={[{ color: '#111', fontSize: 15, fontWeight: '700', fontStyle: 'italic', letterSpacing: 1.5, fontVariant: ['tabular-nums'] }, ff]}>{cvv ?? '•••'}</Text>
|
|
337
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: '#111', fontSize: 15, fontWeight: '700', fontStyle: 'italic', letterSpacing: 1.5, fontVariant: ['tabular-nums'] }, ff]}>{cvv ?? '•••'}</Text>
|
|
334
338
|
</View>
|
|
335
339
|
</View>
|
|
336
340
|
<View style={styles.backFoot}>
|
|
337
341
|
<View style={{ flex: 1, gap: 3 }}>
|
|
338
|
-
<Text style={[{ color: 'rgba(255,255,255,0.78)', fontSize: 9, lineHeight: 12 }, ff]}>
|
|
342
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: 'rgba(255,255,255,0.78)', fontSize: 9, lineHeight: 12 }, ff]}>
|
|
339
343
|
{fill(text.legal)}
|
|
340
344
|
</Text>
|
|
341
|
-
<Text style={[{ color: 'rgba(255,255,255,0.9)', fontSize: 10, fontWeight: '600' }, ff]}>{fill(text.back)}</Text>
|
|
345
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: 'rgba(255,255,255,0.9)', fontSize: 10, fontWeight: '600' }, ff]}>{fill(text.back)}</Text>
|
|
342
346
|
</View>
|
|
343
347
|
{/* Security hologram. */}
|
|
344
348
|
<Svg width={40} height={30} viewBox="0 0 40 30">
|