@expokit/cli 0.4.0 → 0.4.2
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 +17 -0
- package/bin/lib.mjs +1 -1
- package/bin/liquidglass.mjs +281 -0
- package/package.json +17 -3
- 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 +2 -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/app/(tabs)/_layout.tsx +3 -94
- package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +2 -1
- package/registry/liquidglass-tab/files/src/app/_layout.tsx +2 -2
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +2 -1
- package/registry/liquidglass-tab/files/src/components/LiquidGlassTabs.tsx +110 -0
- package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +31 -6
- package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +12 -4
- package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +6 -3
- package/registry/liquidglass-tab/files/src/lib/tabs.ts +4 -2
- 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, { useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
AccessibilityInfo,
|
|
3
4
|
Animated,
|
|
4
5
|
Easing,
|
|
5
6
|
Modal,
|
|
@@ -132,6 +133,9 @@ type FlatProps = DatePickerBaseProps & {
|
|
|
132
133
|
presets?: boolean | DatePickerPreset<Picked>[];
|
|
133
134
|
};
|
|
134
135
|
|
|
136
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
137
|
+
// content text scales all the way.
|
|
138
|
+
const FONT_CAP = 1.35;
|
|
135
139
|
const SIZES: Record<DatePickerSize, { h: number; font: number; pad: number; icon: number }> = {
|
|
136
140
|
sm: { h: 46, font: 15, pad: 12, icon: 17 },
|
|
137
141
|
md: { h: 56, font: 16, pad: 16, icon: 19 },
|
|
@@ -241,6 +245,10 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
241
245
|
const restIcon = iconColor ?? colors.mutedForeground;
|
|
242
246
|
const { width: winW, height: winH } = useWindowDimensions();
|
|
243
247
|
const ranged = mode === 'range';
|
|
248
|
+
const reduce = useReduceMotion();
|
|
249
|
+
// The pan responder is built once; it reads Reduce Motion through this ref.
|
|
250
|
+
const live = useRef({ reduce });
|
|
251
|
+
live.current = { reduce };
|
|
244
252
|
|
|
245
253
|
const [inner, setInner] = useState<Picked>(defaultValue);
|
|
246
254
|
const current = value !== undefined ? value : inner;
|
|
@@ -269,26 +277,36 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
269
277
|
const wide = presentation === 'dialog' || (presentation === 'auto' && winW >= 640);
|
|
270
278
|
|
|
271
279
|
useEffect(() => {
|
|
280
|
+
if (reduce) {
|
|
281
|
+
float.setValue(floating ? 1 : 0);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
272
284
|
Animated.spring(float, { toValue: floating ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
273
|
-
}, [floating, float]);
|
|
285
|
+
}, [floating, float, reduce]);
|
|
274
286
|
useEffect(() => {
|
|
275
287
|
Animated.timing(focus, { toValue: open ? 1 : 0, duration: 180, easing: Easing.out(Easing.quad), useNativeDriver: false }).start();
|
|
276
|
-
|
|
277
|
-
|
|
288
|
+
// Reduce Motion: the halo fades in place (its scale interpolation is flat).
|
|
289
|
+
if (reduce) Animated.timing(halo, { toValue: open ? 1 : 0, duration: 150, useNativeDriver: true }).start();
|
|
290
|
+
else Animated.spring(halo, { toValue: open ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
291
|
+
}, [open, focus, halo, reduce]);
|
|
278
292
|
useEffect(() => {
|
|
279
|
-
Animated.
|
|
280
|
-
|
|
293
|
+
if (reduce) Animated.timing(check, { toValue: ok ? 1 : 0, duration: 150, useNativeDriver: true }).start();
|
|
294
|
+
else Animated.spring(check, { toValue: ok ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
|
|
295
|
+
}, [ok, check, reduce]);
|
|
281
296
|
useEffect(() => {
|
|
282
|
-
|
|
297
|
+
// Reduce Motion: no shake — the red border and message carry the error.
|
|
298
|
+
if (!error || live.current.reduce) return;
|
|
283
299
|
shake.setValue(0);
|
|
284
300
|
Animated.sequence([10, -8, 6, -4, 0].map((x) => Animated.timing(shake, { toValue: x, duration: 55, useNativeDriver: true }))).start();
|
|
285
301
|
}, [error, shake]);
|
|
286
302
|
useEffect(() => {
|
|
287
303
|
if (open) {
|
|
288
304
|
drag.setValue(0);
|
|
289
|
-
|
|
305
|
+
// Reduce Motion: the panel fades in place (its translate / scale interpolations are flat).
|
|
306
|
+
if (reduce) Animated.timing(show, { toValue: 1, duration: 150, useNativeDriver: true }).start();
|
|
307
|
+
else Animated.spring(show, { toValue: 1, useNativeDriver: true, ...(asSheet ? motion.gentle : motion.snappy) }).start();
|
|
290
308
|
} else if (mounted) {
|
|
291
|
-
Animated.timing(show, { toValue: 0, duration: motion.fast + 40, easing: Easing.in(Easing.quad), useNativeDriver: true }).start(
|
|
309
|
+
Animated.timing(show, { toValue: 0, duration: reduce ? 150 : motion.fast + 40, easing: Easing.in(Easing.quad), useNativeDriver: true }).start(
|
|
292
310
|
({ finished }) => finished && setMounted(false)
|
|
293
311
|
);
|
|
294
312
|
}
|
|
@@ -395,21 +413,23 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
395
413
|
const same = (a: Picked, b: Picked) =>
|
|
396
414
|
isRange(a) && isRange(b) ? isSameDay(a.start, b.start) && isSameDay(a.end, b.end) : !isRange(a) && !isRange(b) && isSameDay(a, b);
|
|
397
415
|
|
|
398
|
-
const pan = useMemo(
|
|
399
|
-
() =>
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
416
|
+
const pan = useMemo(() => {
|
|
417
|
+
const settle = () =>
|
|
418
|
+
live.current.reduce
|
|
419
|
+
? Animated.timing(drag, { toValue: 0, duration: 150, easing: Easing.bezier(0.32, 0.72, 0, 1), useNativeDriver: true }).start()
|
|
420
|
+
: Animated.spring(drag, { toValue: 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
421
|
+
return PanResponder.create({
|
|
422
|
+
onMoveShouldSetPanResponder: (_, g) => g.dy > 4 && Math.abs(g.dy) > Math.abs(g.dx),
|
|
423
|
+
onPanResponderMove: (_, g) => drag.setValue(Math.max(0, g.dy)),
|
|
424
|
+
onPanResponderRelease: (_, g) => {
|
|
425
|
+
if (g.dy > 110 || g.vy > 1.1) {
|
|
426
|
+
setOpen(false);
|
|
427
|
+
onOpenChange?.(false);
|
|
428
|
+
} else settle();
|
|
429
|
+
},
|
|
430
|
+
onPanResponderTerminate: settle,
|
|
431
|
+
});
|
|
432
|
+
}, [drag, onOpenChange]);
|
|
413
433
|
|
|
414
434
|
const active = error ? colors.danger : tint;
|
|
415
435
|
const rest = error ? colors.danger : ok ? colors.success : ((userBorder as string | undefined) ?? colors.input);
|
|
@@ -469,7 +489,7 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
469
489
|
},
|
|
470
490
|
]}
|
|
471
491
|
>
|
|
472
|
-
<Text style={[styles.presetText, ff, { color: on ? tint : colors.foreground }]}>{p.label}</Text>
|
|
492
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.presetText, ff, { color: on ? tint : colors.foreground }]}>{p.label}</Text>
|
|
473
493
|
</Pressable>
|
|
474
494
|
);
|
|
475
495
|
})}
|
|
@@ -498,14 +518,16 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
498
518
|
accessibilityRole="button"
|
|
499
519
|
style={({ pressed }) => [styles.footBtn, { borderRadius: radius.md, opacity: draft ? (pressed ? 0.6 : 1) : 0.4 }]}
|
|
500
520
|
>
|
|
501
|
-
<Text style={[styles.footText, ff, { color: colors.foreground }]}>
|
|
521
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: colors.foreground }]}>
|
|
522
|
+
Clear
|
|
523
|
+
</Text>
|
|
502
524
|
</Pressable>
|
|
503
525
|
<Pressable
|
|
504
526
|
onPress={done}
|
|
505
527
|
accessibilityRole="button"
|
|
506
528
|
style={({ pressed }) => [styles.footBtn, styles.footMain, { backgroundColor: tint, borderRadius: radius.md, opacity: pressed ? 0.85 : 1 }]}
|
|
507
529
|
>
|
|
508
|
-
<Text style={[styles.footText, ff, { color: readableOn(tint) }]}>
|
|
530
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: readableOn(tint) }]}>
|
|
509
531
|
{draftDays > 1 ? `Done · ${draftDays} days` : 'Done'}
|
|
510
532
|
</Text>
|
|
511
533
|
</Pressable>
|
|
@@ -558,13 +580,14 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
558
580
|
borderRadius: fieldRadius + 4,
|
|
559
581
|
borderColor: alpha(active, 0.2),
|
|
560
582
|
opacity: halo,
|
|
561
|
-
transform: [{ scale: halo.interpolate({ inputRange: [0, 1], outputRange: [0.97, 1] }) }],
|
|
583
|
+
transform: [{ scale: halo.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.97, 1] }) }],
|
|
562
584
|
},
|
|
563
585
|
]}
|
|
564
586
|
/>
|
|
565
|
-
<Animated.View style={[styles.field, {
|
|
587
|
+
<Animated.View style={[styles.field, { minHeight: sz.h, borderRadius: fieldRadius, backgroundColor: bg }, userField, { borderColor }]}>
|
|
566
588
|
<Text
|
|
567
589
|
numberOfLines={1}
|
|
590
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
568
591
|
style={[
|
|
569
592
|
styles.value,
|
|
570
593
|
ff,
|
|
@@ -592,7 +615,7 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
592
615
|
]}
|
|
593
616
|
>
|
|
594
617
|
{/* The notch: the label paints the surface over the border, so it matches the rendered width. */}
|
|
595
|
-
<Animated.Text numberOfLines={1} style={[styles.label, ff, { fontSize: sz.font, backgroundColor: bg }, labelStyle, { color: labelColor }]}>
|
|
618
|
+
<Animated.Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.label, ff, { fontSize: sz.font, backgroundColor: bg }, labelStyle, { color: labelColor }]}>
|
|
596
619
|
{label}
|
|
597
620
|
</Animated.Text>
|
|
598
621
|
</Animated.View>
|
|
@@ -603,7 +626,7 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
603
626
|
style={{
|
|
604
627
|
display: ok ? 'flex' : 'none',
|
|
605
628
|
opacity: check,
|
|
606
|
-
transform: [{ scale: check.interpolate({ inputRange: [0, 1], outputRange: [0.3, 1] }) }],
|
|
629
|
+
transform: [{ scale: check.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.3, 1] }) }],
|
|
607
630
|
}}
|
|
608
631
|
>
|
|
609
632
|
<Icon name="check" size={sz.icon} color={colors.success} />
|
|
@@ -623,7 +646,7 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
623
646
|
</View>
|
|
624
647
|
</Animated.View>
|
|
625
648
|
</Pressable>
|
|
626
|
-
{message ? <FieldMessage key={tone + message} text={message} tone={tone} style={messageStyle} /> : null}
|
|
649
|
+
{message ? <FieldMessage key={tone + message} text={message} tone={tone} reduce={reduce} style={messageStyle} /> : null}
|
|
627
650
|
|
|
628
651
|
<Modal visible={mounted} transparent animationType="none" statusBarTranslucent onRequestClose={close}>
|
|
629
652
|
<Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: asSheet ? colors.overlay : 'transparent', opacity: show }]}>
|
|
@@ -644,11 +667,15 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
644
667
|
? {
|
|
645
668
|
opacity: show,
|
|
646
669
|
transform: [
|
|
647
|
-
{ scale: show.interpolate({ inputRange: [0, 1], outputRange: [0.94, 1] }) },
|
|
648
|
-
{ translateY: Animated.add(show.interpolate({ inputRange: [0, 1], outputRange: [16, 0] }), drag) },
|
|
670
|
+
{ scale: show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.94, 1] }) },
|
|
671
|
+
{ translateY: Animated.add(show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 16, 0] }), drag) },
|
|
649
672
|
],
|
|
650
673
|
}
|
|
651
|
-
: {
|
|
674
|
+
: {
|
|
675
|
+
// Reduce Motion: the sheet fades in place instead of sliding up.
|
|
676
|
+
opacity: reduce ? show : 1,
|
|
677
|
+
transform: [{ translateY: Animated.add(show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : winH * 0.85, 0] }), drag) }],
|
|
678
|
+
},
|
|
652
679
|
]}
|
|
653
680
|
>
|
|
654
681
|
<View {...pan.panHandlers} style={styles.sheetHeader}>
|
|
@@ -681,8 +708,8 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
681
708
|
{
|
|
682
709
|
opacity: show,
|
|
683
710
|
transform: [
|
|
684
|
-
{ translateY: show.interpolate({ inputRange: [0, 1], outputRange: [below ? -8 : 8, 0] }) },
|
|
685
|
-
{ scale: show.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) },
|
|
711
|
+
{ translateY: show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : below ? -8 : 8, 0] }) },
|
|
712
|
+
{ scale: show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.96, 1] }) },
|
|
686
713
|
],
|
|
687
714
|
},
|
|
688
715
|
below ? { top: anchor.y + anchor.h + 6 } : { bottom: Math.max(12, winH - anchor.y + 6) },
|
|
@@ -697,17 +724,28 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
697
724
|
}
|
|
698
725
|
|
|
699
726
|
/** Helper / error / success line that slides in; error and success carry an icon. */
|
|
700
|
-
function FieldMessage({
|
|
727
|
+
function FieldMessage({
|
|
728
|
+
text,
|
|
729
|
+
tone,
|
|
730
|
+
reduce,
|
|
731
|
+
style,
|
|
732
|
+
}: {
|
|
733
|
+
text: string;
|
|
734
|
+
tone: 'error' | 'success' | 'helper';
|
|
735
|
+
reduce: boolean;
|
|
736
|
+
style?: StyleProp<TextStyle>;
|
|
737
|
+
}) {
|
|
701
738
|
const { colors, font } = useKit();
|
|
702
739
|
const enter = useRef(new Animated.Value(tone === 'helper' ? 1 : 0)).current; // native
|
|
703
740
|
useEffect(() => {
|
|
704
|
-
Animated.
|
|
705
|
-
|
|
741
|
+
if (reduce) Animated.timing(enter, { toValue: 1, duration: 150, useNativeDriver: true }).start();
|
|
742
|
+
else Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
743
|
+
}, [enter, reduce]);
|
|
706
744
|
const color = tone === 'error' ? colors.danger : tone === 'success' ? colors.success : colors.mutedForeground;
|
|
707
745
|
return (
|
|
708
746
|
<Animated.View
|
|
709
747
|
accessibilityLiveRegion="polite"
|
|
710
|
-
style={[styles.message, { opacity: enter, transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [4, 0] }) }] }]}
|
|
748
|
+
style={[styles.message, { opacity: enter, transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 4, 0] }) }] }]}
|
|
711
749
|
>
|
|
712
750
|
{tone !== 'helper' ? <Icon name={tone === 'error' ? 'alert' : 'checkCircle'} size={14} color={color} /> : null}
|
|
713
751
|
<Text style={[styles.helper, font.family ? { fontFamily: font.family } : null, { color }, style, tone !== 'helper' && { color }]}>{text}</Text>
|
|
@@ -747,6 +785,22 @@ function Icon({ name, size, color, strokeWidth = 1.8 }: { name: IconName; size:
|
|
|
747
785
|
);
|
|
748
786
|
}
|
|
749
787
|
|
|
788
|
+
function useReduceMotion() {
|
|
789
|
+
const [reduce, setReduce] = useState(false);
|
|
790
|
+
useEffect(() => {
|
|
791
|
+
let alive = true;
|
|
792
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
793
|
+
.then((r) => alive && setReduce(r))
|
|
794
|
+
.catch(() => {});
|
|
795
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
796
|
+
return () => {
|
|
797
|
+
alive = false;
|
|
798
|
+
sub.remove();
|
|
799
|
+
};
|
|
800
|
+
}, []);
|
|
801
|
+
return reduce;
|
|
802
|
+
}
|
|
803
|
+
|
|
750
804
|
const styles = StyleSheet.create({
|
|
751
805
|
field: { borderWidth: 1.5, justifyContent: 'center' },
|
|
752
806
|
halo: { position: 'absolute', top: -4, left: -4, right: -4, bottom: -4, borderWidth: 4 },
|
|
@@ -775,11 +829,11 @@ const styles = StyleSheet.create({
|
|
|
775
829
|
presetScroll: { flexGrow: 0, flexShrink: 0 },
|
|
776
830
|
calendarScroll: { flexGrow: 0, flexShrink: 1 },
|
|
777
831
|
presets: { gap: 8, paddingHorizontal: 16, paddingVertical: 8 },
|
|
778
|
-
preset: {
|
|
832
|
+
preset: { minHeight: 34, paddingHorizontal: 14, borderWidth: 1, alignItems: 'center', justifyContent: 'center' },
|
|
779
833
|
presetText: { fontSize: 13.5, fontWeight: '400' },
|
|
780
834
|
calendar: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 6 },
|
|
781
835
|
footer: { flexDirection: 'row', flexShrink: 0, gap: 8, paddingTop: 12, paddingHorizontal: 16, borderTopWidth: StyleSheet.hairlineWidth },
|
|
782
|
-
footBtn: {
|
|
836
|
+
footBtn: { minHeight: 46, paddingHorizontal: 20, alignItems: 'center', justifyContent: 'center' },
|
|
783
837
|
footMain: { flex: 1 },
|
|
784
838
|
footText: { fontSize: 15, fontWeight: '400' },
|
|
785
839
|
popover: {
|
|
@@ -104,6 +104,9 @@ export interface DialogProps {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
const SHAKE = [10, -9, 7, -5, 3, 0];
|
|
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
|
|
|
108
111
|
/**
|
|
109
112
|
* A centred alert dialog. The scrim fades in while the card springs from 0.92 to full size; an
|
|
@@ -325,6 +328,7 @@ export function Dialog({
|
|
|
325
328
|
maxLength={prompt.maxLength}
|
|
326
329
|
selectTextOnFocus={prompt.selectTextOnFocus ?? true}
|
|
327
330
|
autoFocus
|
|
331
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
328
332
|
returnKeyType="done"
|
|
329
333
|
onSubmitEditing={() => run(list[confirmIndex], confirmIndex)}
|
|
330
334
|
onFocus={() => setFocused(true)}
|
|
@@ -542,7 +546,7 @@ function PillButton({
|
|
|
542
546
|
},
|
|
543
547
|
]}
|
|
544
548
|
>
|
|
545
|
-
<Text numberOfLines={1} style={[styles.pillText, ff, { color: fg, opacity: loading ? 0 : 1 }]}>
|
|
549
|
+
<Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.pillText, ff, { color: fg, opacity: loading ? 0 : 1 }]}>
|
|
546
550
|
{label}
|
|
547
551
|
</Text>
|
|
548
552
|
{loading ? <ActivityIndicator style={StyleSheet.absoluteFill} size="small" color={fg} /> : null}
|
|
@@ -587,7 +591,7 @@ function IosButton({
|
|
|
587
591
|
style={[styles.iosButton, style]}
|
|
588
592
|
>
|
|
589
593
|
<Animated.View pointerEvents="none" style={[StyleSheet.absoluteFill, { backgroundColor: highlight, opacity: hl }]} />
|
|
590
|
-
<Text numberOfLines={1} style={[styles.iosText, ff, { color, opacity: loading ? 0 : disabled ? 0.4 : 1 }]}>
|
|
594
|
+
<Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.iosText, ff, { color, opacity: loading ? 0 : disabled ? 0.4 : 1 }]}>
|
|
591
595
|
{label}
|
|
592
596
|
</Text>
|
|
593
597
|
{loading ? <ActivityIndicator style={StyleSheet.absoluteFill} size="small" color={color} /> : null}
|
|
@@ -750,14 +754,14 @@ const styles = StyleSheet.create({
|
|
|
750
754
|
textCenter: { textAlign: 'center' },
|
|
751
755
|
field: { marginTop: 16 },
|
|
752
756
|
fieldIos: { marginTop: 14 },
|
|
753
|
-
input: {
|
|
754
|
-
inputIos: {
|
|
757
|
+
input: { minHeight: 48, paddingHorizontal: 14, fontSize: 16, borderWidth: 1 },
|
|
758
|
+
inputIos: { minHeight: 36, paddingHorizontal: 10, fontSize: 14, borderWidth: StyleSheet.hairlineWidth },
|
|
755
759
|
error: { fontSize: 13, lineHeight: 18, fontWeight: '400', marginTop: 8 },
|
|
756
760
|
children: { marginTop: 14 },
|
|
757
761
|
row: { flexDirection: 'row' },
|
|
758
762
|
flex: { flex: 1 },
|
|
759
763
|
actions: { gap: 10, padding: 22, paddingTop: 22 },
|
|
760
|
-
pill: {
|
|
764
|
+
pill: { minHeight: 48, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 18 },
|
|
761
765
|
pillText: { fontSize: 16, lineHeight: 21, fontWeight: '400', letterSpacing: -0.1 },
|
|
762
766
|
iosActions: { borderTopWidth: StyleSheet.hairlineWidth },
|
|
763
767
|
iosButton: { minHeight: 46, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 12 },
|
|
@@ -38,6 +38,10 @@ const INLINE_BOTTOM = Platform.select({ ios: 18, android: 12, default: 10 }) ??
|
|
|
38
38
|
const MODAL_TOP = Platform.select({ ios: 52, android: (StatusBar.currentHeight ?? 24) + 8, default: 16 }) ?? 16;
|
|
39
39
|
const MODAL_BOTTOM = Platform.select({ ios: 28, android: 16, default: 12 }) ?? 12;
|
|
40
40
|
|
|
41
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
42
|
+
// content text scales all the way.
|
|
43
|
+
const FONT_CAP = 1.35;
|
|
44
|
+
|
|
41
45
|
/** Panel surface hidden past the screen edge, so overshoot and rubber-banding never show a gap. */
|
|
42
46
|
const EXTRA = 48;
|
|
43
47
|
/** Extra travel when closed so the panel's shadow is off-screen too. */
|
|
@@ -673,7 +677,7 @@ function ProfileAvatar({ name, src, size, status }: { name: string; src?: string
|
|
|
673
677
|
},
|
|
674
678
|
]}
|
|
675
679
|
>
|
|
676
|
-
<Text style={[{ color: ink, fontSize: size * 0.36, fontWeight: '400', letterSpacing: -0.2 }, ff]}>{initials}</Text>
|
|
680
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: ink, fontSize: size * 0.36, fontWeight: '400', letterSpacing: -0.2 }, ff]}>{initials}</Text>
|
|
677
681
|
{src ? <Image source={{ uri: src }} alt="" style={StyleSheet.absoluteFill} /> : null}
|
|
678
682
|
</View>
|
|
679
683
|
{status ? (
|
|
@@ -925,7 +929,7 @@ export function DrawerItem({
|
|
|
925
929
|
{ borderRadius: radius.capsule(22), backgroundColor: badgeColors.bg, transform: [{ scale: badgePop }] },
|
|
926
930
|
]}
|
|
927
931
|
>
|
|
928
|
-
<Text style={[styles.badgeText, ff, { color: badgeColors.fg }]}>
|
|
932
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.badgeText, ff, { color: badgeColors.fg }]}>
|
|
929
933
|
{typeof badge === 'number' && badge > 99 ? '99+' : badge}
|
|
930
934
|
</Text>
|
|
931
935
|
</Animated.View>
|
|
@@ -1117,7 +1121,7 @@ const styles = StyleSheet.create({
|
|
|
1117
1121
|
item: { minHeight: ITEM_H, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, gap: 14 },
|
|
1118
1122
|
itemIcon: { width: 24, alignItems: 'center', justifyContent: 'center' },
|
|
1119
1123
|
itemLabel: { flex: 1, fontSize: 15.5, fontWeight: '400', letterSpacing: -0.15 },
|
|
1120
|
-
badge: { minWidth: 22,
|
|
1124
|
+
badge: { minWidth: 22, minHeight: 22, paddingHorizontal: 7, alignItems: 'center', justifyContent: 'center' },
|
|
1121
1125
|
badgePlain: { paddingHorizontal: 2, minWidth: 0 },
|
|
1122
1126
|
badgeText: { fontSize: 13, fontWeight: '400', fontVariant: ['tabular-nums'] },
|
|
1123
1127
|
section: {
|
|
@@ -1131,7 +1135,7 @@ const styles = StyleSheet.create({
|
|
|
1131
1135
|
paddingBottom: 2,
|
|
1132
1136
|
},
|
|
1133
1137
|
sectionFlush: { paddingTop: 10 },
|
|
1134
|
-
sectionText: { fontSize: 13, lineHeight: 18, fontWeight: '400', letterSpacing: 0.1 },
|
|
1138
|
+
sectionText: { flexShrink: 1, fontSize: 13, lineHeight: 18, fontWeight: '400', letterSpacing: 0.1 },
|
|
1135
1139
|
divider: { height: StyleSheet.hairlineWidth, marginHorizontal: 14, marginTop: 10, marginBottom: 4 },
|
|
1136
1140
|
footerRow: { minHeight: 48, flexDirection: 'row', alignItems: 'center', gap: 14, paddingHorizontal: 12, paddingVertical: 8 },
|
|
1137
1141
|
footerText: { flex: 1, minWidth: 0, gap: 5 },
|
|
@@ -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 },
|