@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 },
|
|
@@ -157,7 +157,7 @@ function TabButton({
|
|
|
157
157
|
}) {
|
|
158
158
|
const press = useRef(new Animated.Value(1)).current;
|
|
159
159
|
const to = (v: number) =>
|
|
160
|
-
Animated.spring(press, { toValue: v, useNativeDriver: true, speed: 40, bounciness:
|
|
160
|
+
Animated.spring(press, { toValue: v, useNativeDriver: true, speed: 40, bounciness: 0 }).start();
|
|
161
161
|
|
|
162
162
|
return (
|
|
163
163
|
<Pressable
|
|
@@ -104,6 +104,9 @@ const SIZES: Record<ListItemSize, { minH: number; tile: number; glyph: number; a
|
|
|
104
104
|
lg: { minH: 68, tile: 38, glyph: 21, avatar: 48, title: 17, sub: 14, padY: 14 },
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
108
|
+
// content text scales all the way.
|
|
109
|
+
const FONT_CAP = 1.35;
|
|
107
110
|
const PAD_X = 16;
|
|
108
111
|
const GAP = 12;
|
|
109
112
|
const AVATAR_COLORS = ['#f97316', '#0ea5e9', '#22c55e', '#a855f7', '#ec4899', '#14b8a6', '#6366f1', '#eab308'];
|
|
@@ -219,7 +222,9 @@ export function ListItem({
|
|
|
219
222
|
{badge === true ? <View style={[styles.dot, { backgroundColor: tint }]} /> : null}
|
|
220
223
|
{badgeText ? (
|
|
221
224
|
<View style={[styles.badge, { borderRadius: radius.capsule(22), backgroundColor: tint }]}>
|
|
222
|
-
<Text style={[styles.badgeText, ff, { color: readableOn(tint) }]}>
|
|
225
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.badgeText, ff, { color: readableOn(tint) }]}>
|
|
226
|
+
{badgeText}
|
|
227
|
+
</Text>
|
|
223
228
|
</View>
|
|
224
229
|
) : null}
|
|
225
230
|
{isTextNode(trailing) ? <Text style={[styles.value, ff, { color: colors.mutedForeground }]}>{trailing}</Text> : trailing}
|
|
@@ -360,7 +365,9 @@ function AvatarCircle({ src, name = '', color, online, size, ring }: ListItemAva
|
|
|
360
365
|
return (
|
|
361
366
|
<View style={{ width: size, height: size }}>
|
|
362
367
|
<View style={[styles.avatar, { width: size, height: size, borderRadius: size / 2, backgroundColor: bg }]}>
|
|
363
|
-
<Text style={[{ color: readableOn(bg), fontSize: size * 0.38, fontWeight: '400' }, ff]}>
|
|
368
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: readableOn(bg), fontSize: size * 0.38, fontWeight: '400' }, ff]}>
|
|
369
|
+
{initials}
|
|
370
|
+
</Text>
|
|
364
371
|
{src && !failed ? (
|
|
365
372
|
<Image source={{ uri: src }} onError={() => setFailed(true)} style={[StyleSheet.absoluteFill, { borderRadius: size / 2 }]} />
|
|
366
373
|
) : null}
|
|
@@ -499,7 +506,7 @@ const styles = StyleSheet.create({
|
|
|
499
506
|
trailing: { flexDirection: 'row', alignItems: 'center', gap: 8, flexShrink: 0, maxWidth: '50%' },
|
|
500
507
|
value: { fontSize: 15, flexShrink: 1 },
|
|
501
508
|
dot: { width: 9, height: 9, borderRadius: 5 },
|
|
502
|
-
badge: { minWidth: 22,
|
|
509
|
+
badge: { minWidth: 22, minHeight: 22, paddingHorizontal: 7, alignItems: 'center', justifyContent: 'center' },
|
|
503
510
|
badgeText: { fontSize: 12, fontWeight: '400', fontVariant: ['tabular-nums'] },
|
|
504
511
|
divider: { position: 'absolute', right: 0, bottom: 0, height: StyleSheet.hairlineWidth },
|
|
505
512
|
avatar: { alignItems: 'center', justifyContent: 'center', overflow: 'hidden' },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
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,6 +88,7 @@ export function OtpInput({
|
|
|
84
88
|
style,
|
|
85
89
|
}: OtpInputProps) {
|
|
86
90
|
const { colors } = useKit();
|
|
91
|
+
const reduce = useReduceMotion();
|
|
87
92
|
const input = useRef<ComponentRef<typeof TextInput>>(null);
|
|
88
93
|
const [inner, setInner] = useState('');
|
|
89
94
|
const [focused, setFocused] = useState(false);
|
|
@@ -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,6 +209,7 @@ function Cell({
|
|
|
201
209
|
variant: OtpInputVariant;
|
|
202
210
|
w: number;
|
|
203
211
|
h: number;
|
|
212
|
+
reduce: boolean;
|
|
204
213
|
}) {
|
|
205
214
|
const { colors, radius, font: kitFont } = useKit();
|
|
206
215
|
const ff = kitFont.family ? { fontFamily: kitFont.family } : null;
|
|
@@ -214,17 +223,22 @@ function Cell({
|
|
|
214
223
|
const ring = useRef(new Animated.Value(0)).current;
|
|
215
224
|
|
|
216
225
|
useEffect(() => {
|
|
217
|
-
if (char) {
|
|
226
|
+
if (char && reduce) {
|
|
227
|
+
// Reduce Motion: the digit appears without the pop.
|
|
228
|
+
pop.setValue(1);
|
|
229
|
+
} else if (char) {
|
|
218
230
|
pop.setValue(0.3);
|
|
219
231
|
Animated.spring(pop, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
|
|
220
232
|
} else {
|
|
221
233
|
Animated.timing(pop, { toValue: 0, duration: 110, useNativeDriver: true }).start();
|
|
222
234
|
}
|
|
223
|
-
}, [char, pop]);
|
|
235
|
+
}, [char, pop, reduce]);
|
|
224
236
|
|
|
225
237
|
useEffect(() => {
|
|
226
238
|
const to = active ? 1 : 0;
|
|
227
|
-
|
|
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();
|
|
228
242
|
Animated.timing(ring, { toValue: to, duration: motion.fast, useNativeDriver: false }).start();
|
|
229
243
|
if (!active) return;
|
|
230
244
|
caret.setValue(1);
|
|
@@ -238,17 +252,17 @@ function Cell({
|
|
|
238
252
|
);
|
|
239
253
|
blink.start();
|
|
240
254
|
return () => blink.stop();
|
|
241
|
-
}, [active, halo, ring, caret]);
|
|
255
|
+
}, [active, halo, ring, caret, reduce]);
|
|
242
256
|
|
|
243
257
|
useEffect(() => {
|
|
244
|
-
if (state !== 'success') return;
|
|
258
|
+
if (state !== 'success' || reduce) return;
|
|
245
259
|
pulse.setValue(0);
|
|
246
260
|
Animated.sequence([
|
|
247
261
|
Animated.delay(index * 55),
|
|
248
262
|
Animated.timing(pulse, { toValue: 1, duration: 140, easing: Easing.out(Easing.quad), useNativeDriver: true }),
|
|
249
263
|
Animated.spring(pulse, { toValue: 0, useNativeDriver: true, ...motion.bouncy }),
|
|
250
264
|
]).start();
|
|
251
|
-
}, [state, index, pulse]);
|
|
265
|
+
}, [state, index, pulse, reduce]);
|
|
252
266
|
|
|
253
267
|
const underline = variant === 'underline';
|
|
254
268
|
const pill = variant === 'pill';
|
|
@@ -289,7 +303,7 @@ function Cell({
|
|
|
289
303
|
borderRadius: underline ? 10 : r + 4,
|
|
290
304
|
backgroundColor: alpha(tone, 0.18),
|
|
291
305
|
opacity: halo,
|
|
292
|
-
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] }) }],
|
|
293
307
|
},
|
|
294
308
|
]}
|
|
295
309
|
/>
|
|
@@ -306,7 +320,7 @@ function Cell({
|
|
|
306
320
|
{secure ? (
|
|
307
321
|
<View style={{ width: dot, height: dot, borderRadius: dot / 2, backgroundColor: fg }} />
|
|
308
322
|
) : (
|
|
309
|
-
<Text style={[styles.char, ff, { 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>
|
|
310
324
|
)}
|
|
311
325
|
</Animated.View>
|
|
312
326
|
) : active ? (
|
|
@@ -317,8 +331,8 @@ function Cell({
|
|
|
317
331
|
);
|
|
318
332
|
}
|
|
319
333
|
|
|
320
|
-
/** Helper / error / success line that fades and slides in whenever its text changes. */
|
|
321
|
-
function Message({ text, kind }: { text: string; kind: CellState }) {
|
|
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 }) {
|
|
322
336
|
const { colors, font } = useKit();
|
|
323
337
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
324
338
|
const enter = useRef(new Animated.Value(0)).current;
|
|
@@ -326,12 +340,13 @@ function Message({ text, kind }: { text: string; kind: CellState }) {
|
|
|
326
340
|
|
|
327
341
|
useEffect(() => {
|
|
328
342
|
enter.setValue(0);
|
|
329
|
-
Animated.timing(enter, { toValue: 1, duration: motion.normal, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();
|
|
330
|
-
|
|
331
|
-
|
|
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) {
|
|
332
347
|
Animated.timing(draw, { toValue: 1, duration: 360, delay: 120, easing: Easing.out(Easing.cubic), useNativeDriver: false }).start();
|
|
333
348
|
}
|
|
334
|
-
}, [text, kind, enter, draw]);
|
|
349
|
+
}, [text, kind, enter, draw, reduce]);
|
|
335
350
|
|
|
336
351
|
const color = kind === 'error' ? colors.danger : kind === 'success' ? colors.success : colors.mutedForeground;
|
|
337
352
|
return (
|
|
@@ -339,7 +354,7 @@ function Message({ text, kind }: { text: string; kind: CellState }) {
|
|
|
339
354
|
accessibilityLiveRegion="polite"
|
|
340
355
|
style={[
|
|
341
356
|
styles.message,
|
|
342
|
-
{ 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] }) }] },
|
|
343
358
|
]}
|
|
344
359
|
>
|
|
345
360
|
{kind === 'error' ? (
|
|
@@ -366,6 +381,22 @@ function Message({ text, kind }: { text: string; kind: CellState }) {
|
|
|
366
381
|
);
|
|
367
382
|
}
|
|
368
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
|
+
|
|
369
400
|
const styles = StyleSheet.create({
|
|
370
401
|
root: { alignSelf: 'stretch' },
|
|
371
402
|
disabled: { opacity: 0.5 },
|
|
@@ -377,6 +408,6 @@ const styles = StyleSheet.create({
|
|
|
377
408
|
sep: { alignItems: 'center', justifyContent: 'center' },
|
|
378
409
|
dash: { width: 10, height: 2, borderRadius: 1, opacity: 0.6 },
|
|
379
410
|
message: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, marginTop: 6 },
|
|
380
|
-
messageText: { fontSize: 12.5, lineHeight: 17, fontWeight: '400' },
|
|
411
|
+
messageText: { flexShrink: 1, fontSize: 12.5, lineHeight: 17, fontWeight: '400' },
|
|
381
412
|
hidden: { position: 'absolute', width: 1, height: 1, opacity: 0 },
|
|
382
413
|
});
|