@expokit/cli 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/bin/expo-kit.js +137 -24
- package/bin/lib.mjs +110 -1
- package/bin/mcp.mjs +40 -18
- package/package.json +2 -2
- package/registry/_docs/accordion.json +1 -1
- package/registry/_docs/assistant-app.json +47 -0
- package/registry/_docs/avatar.json +6 -6
- package/registry/_docs/badge.json +4 -4
- package/registry/_docs/bottom-nav.json +6 -6
- package/registry/_docs/bottom-sheet.json +3 -3
- package/registry/_docs/bullet.json +6 -0
- package/registry/_docs/button-3d.json +3 -3
- package/registry/_docs/button.json +2 -2
- package/registry/_docs/calendar.json +5 -5
- package/registry/_docs/card.json +1 -1
- package/registry/_docs/checkout.json +7 -1
- package/registry/_docs/chip-group.json +1 -1
- package/registry/_docs/confetti.json +3 -3
- package/registry/_docs/date-picker.json +4 -4
- package/registry/_docs/donut-shop.json +88 -0
- package/registry/_docs/drawer.json +4 -4
- package/registry/_docs/fintech-app.json +49 -0
- package/registry/_docs/flip-card.json +4 -4
- package/registry/_docs/floating-input.json +5 -5
- package/registry/_docs/food-app.json +47 -0
- package/registry/_docs/glass.json +1 -1
- package/registry/_docs/grain.json +80 -0
- package/registry/_docs/isometric-bar.json +6 -0
- package/registry/_docs/liquidglass-tab.json +1 -1
- package/registry/_docs/morphing-button-group.json +96 -0
- package/registry/_docs/orb-reveal.json +132 -0
- package/registry/_docs/otp-input.json +4 -4
- package/registry/_docs/password-input.json +3 -3
- package/registry/_docs/payment-card.json +3 -3
- package/registry/_docs/pixel-bar.json +6 -0
- package/registry/_docs/polar-area.json +6 -0
- package/registry/_docs/progress-steps.json +4 -4
- package/registry/_docs/progress.json +2 -2
- package/registry/_docs/qr-code-button.json +122 -0
- package/registry/_docs/radio-group.json +1 -1
- package/registry/_docs/search-bar.json +4 -4
- package/registry/_docs/segmented-control.json +1 -1
- package/registry/_docs/select.json +7 -7
- package/registry/_docs/settings.json +6 -0
- package/registry/_docs/skeleton.json +3 -3
- package/registry/_docs/slider.json +6 -6
- package/registry/_docs/spinner.json +3 -3
- package/registry/_docs/stateful-button.json +1 -1
- package/registry/_docs/stepper.json +7 -7
- package/registry/_docs/tabs.json +2 -2
- package/registry/_docs/text-animation.json +4 -4
- package/registry/_docs/textarea.json +3 -3
- package/registry/_docs/ticket-shape.json +1 -1
- package/registry/_docs/tilt-card.json +4 -4
- package/registry/_docs/time-picker.json +6 -6
- package/registry/_docs/timeline-chart.json +6 -0
- package/registry/_docs/timeline.json +6 -6
- package/registry/_docs/toast.json +8 -8
- package/registry/_docs/treemap.json +6 -0
- package/registry/_docs/waterfall.json +6 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +36 -7
- package/registry/accordion/files/src/lib/kit.tsx +8 -5
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +12 -8
- package/registry/action-sheet/files/src/lib/kit.tsx +8 -5
- package/registry/alert/files/src/components/ui/alert.tsx +11 -5
- package/registry/alert/files/src/components/ui/button.tsx +35 -6
- package/registry/alert/files/src/lib/kit.tsx +8 -5
- package/registry/assistant-app.json +49 -0
- package/registry/avatar/files/src/components/ui/avatar.tsx +7 -4
- package/registry/avatar/files/src/lib/kit.tsx +8 -5
- package/registry/badge/files/src/components/ui/badge.tsx +23 -9
- package/registry/badge/files/src/lib/kit.tsx +8 -5
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +21 -8
- package/registry/bottom-nav/files/src/lib/kit.tsx +8 -5
- package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +7 -6
- package/registry/bottom-sheet/files/src/lib/kit.tsx +8 -5
- package/registry/button/files/src/components/ui/button.tsx +35 -6
- package/registry/button/files/src/lib/kit.tsx +8 -5
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +35 -7
- package/registry/button-3d/files/src/lib/kit.tsx +8 -5
- package/registry/calendar/files/src/components/ui/calendar.tsx +68 -19
- package/registry/calendar/files/src/lib/kit.tsx +8 -5
- package/registry/card/files/src/components/ui/card.tsx +32 -10
- package/registry/card/files/src/lib/kit.tsx +8 -5
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +31 -8
- package/registry/checkbox/files/src/lib/kit.tsx +8 -5
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +39 -7
- package/registry/chip-group/files/src/lib/kit.tsx +8 -5
- package/registry/confetti/files/src/lib/kit.tsx +8 -5
- package/registry/coupon/files/src/components/ui/coupon.tsx +55 -28
- package/registry/coupon/files/src/components/ui/flip-card.tsx +23 -4
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +59 -18
- package/registry/coupon/files/src/lib/kit.tsx +8 -5
- package/registry/date-picker/files/src/components/ui/calendar.tsx +68 -19
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +98 -44
- package/registry/date-picker/files/src/lib/kit.tsx +8 -5
- package/registry/dialog/files/src/components/ui/dialog.tsx +19 -9
- package/registry/dialog/files/src/lib/kit.tsx +8 -5
- package/registry/donut-shop.json +18 -0
- package/registry/drawer/files/src/components/ui/drawer.tsx +31 -21
- package/registry/drawer/files/src/lib/kit.tsx +8 -5
- package/registry/fintech-app.json +57 -0
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +23 -4
- package/registry/flip-card/files/src/lib/kit.tsx +8 -5
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +91 -32
- package/registry/floating-input/files/src/lib/kit.tsx +8 -5
- package/registry/food-app.json +55 -0
- package/registry/grain/files/src/components/ui/grain.tsx +113 -0
- package/registry/{tag-input → grain}/files/src/lib/kit.tsx +8 -5
- package/registry/grain.json +17 -0
- package/registry/kit/files/src/lib/kit.tsx +8 -5
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +2 -2
- package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +1 -1
- package/registry/list-item/files/src/components/ui/list-item.tsx +28 -17
- package/registry/list-item/files/src/lib/kit.tsx +8 -5
- package/registry/morphing-button-group.json +18 -0
- package/registry/orb-reveal.json +20 -0
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +57 -24
- package/registry/otp-input/files/src/lib/kit.tsx +8 -5
- package/registry/password-input/files/src/components/ui/floating-input.tsx +91 -32
- package/registry/password-input/files/src/components/ui/password-input.tsx +55 -21
- package/registry/password-input/files/src/lib/kit.tsx +8 -5
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +23 -4
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +35 -18
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +59 -18
- package/registry/payment-card/files/src/lib/kit.tsx +8 -5
- package/registry/progress/files/src/components/ui/progress.tsx +16 -6
- package/registry/progress/files/src/lib/kit.tsx +8 -5
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +22 -11
- package/registry/progress-steps/files/src/lib/kit.tsx +8 -5
- package/registry/qr-code-button.json +18 -0
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +48 -17
- package/registry/radio-group/files/src/lib/kit.tsx +8 -5
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +81 -26
- package/registry/search-bar/files/src/lib/kit.tsx +8 -5
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +40 -10
- package/registry/segmented-control/files/src/lib/kit.tsx +8 -5
- package/registry/select/files/src/components/ui/select.tsx +161 -86
- package/registry/select/files/src/lib/kit.tsx +8 -5
- package/registry/skeleton/files/src/components/ui/skeleton.tsx +4 -4
- package/registry/skeleton/files/src/lib/kit.tsx +8 -5
- package/registry/slider/files/src/components/ui/slider.tsx +56 -27
- package/registry/slider/files/src/lib/kit.tsx +8 -5
- package/registry/spinner/files/src/components/ui/spinner.tsx +17 -12
- package/registry/spinner/files/src/lib/kit.tsx +8 -5
- package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +12 -4
- package/registry/stateful-button/files/src/lib/kit.tsx +8 -5
- package/registry/stepper/files/src/components/ui/stepper.tsx +83 -29
- package/registry/stepper/files/src/lib/kit.tsx +8 -5
- package/registry/switch/files/src/components/ui/switch.tsx +60 -22
- package/registry/switch/files/src/lib/kit.tsx +8 -5
- package/registry/tabs/files/src/components/ui/tabs.tsx +15 -10
- package/registry/tabs/files/src/lib/kit.tsx +8 -5
- package/registry/text-animation/files/src/components/ui/text-animation.tsx +11 -6
- package/registry/text-animation/files/src/lib/kit.tsx +8 -5
- package/registry/textarea/files/src/components/ui/textarea.tsx +115 -45
- package/registry/textarea/files/src/lib/kit.tsx +8 -5
- package/registry/ticket-shape/files/src/lib/kit.tsx +8 -5
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +59 -18
- package/registry/tilt-card/files/src/lib/kit.tsx +8 -5
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +156 -75
- package/registry/time-picker/files/src/lib/kit.tsx +8 -5
- package/registry/timeline/files/src/components/ui/avatar.tsx +7 -4
- package/registry/timeline/files/src/components/ui/timeline.tsx +19 -12
- package/registry/timeline/files/src/lib/kit.tsx +8 -5
- package/registry/toast/files/src/components/ui/button.tsx +35 -6
- package/registry/toast/files/src/components/ui/toast.tsx +25 -14
- package/registry/toast/files/src/lib/kit.tsx +8 -5
- package/schema/components.schema.json +1 -1
- package/registry/_docs/color-swatch.json +0 -82
- package/registry/_docs/kpi-card.json +0 -14
- package/registry/_docs/like-button.json +0 -92
- package/registry/_docs/morph-button.json +0 -128
- package/registry/_docs/pagination-dots.json +0 -108
- package/registry/_docs/shimmer-button.json +0 -98
- package/registry/_docs/tag-input.json +0 -148
- package/registry/color-swatch.json +0 -17
- package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +0 -45
- package/registry/kpi-card.json +0 -14
- package/registry/like-button.json +0 -18
- package/registry/morph-button.json +0 -18
- package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +0 -262
- package/registry/pagination-dots/files/src/lib/kit.tsx +0 -219
- package/registry/pagination-dots.json +0 -15
- package/registry/shimmer-button.json +0 -17
- package/registry/tag-input/files/src/components/ui/tag-input.tsx +0 -720
- package/registry/tag-input.json +0 -17
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { Animated, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
import { AccessibilityInfo, Animated, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
import Svg, { Path } from 'react-native-svg';
|
|
4
4
|
|
|
5
5
|
import { alpha, useKit } from '@/lib/kit';
|
|
@@ -38,7 +38,9 @@ export function Checkbox({
|
|
|
38
38
|
shape = 'square',
|
|
39
39
|
style,
|
|
40
40
|
}: CheckboxProps) {
|
|
41
|
-
const { colors, radius } = useKit();
|
|
41
|
+
const { colors, radius, font } = useKit();
|
|
42
|
+
const reduce = useReduceMotion();
|
|
43
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
42
44
|
const tint = accent ?? colors.accent;
|
|
43
45
|
const corner = shape === 'circle' ? size / 2 : Math.min(size / 2, radius.sm + 1);
|
|
44
46
|
const [inner, setInner] = useState(defaultChecked);
|
|
@@ -49,12 +51,17 @@ export function Checkbox({
|
|
|
49
51
|
const burst = useRef(new Animated.Value(0)).current;
|
|
50
52
|
|
|
51
53
|
useEffect(() => {
|
|
52
|
-
|
|
54
|
+
// Reduce Motion: a short fill fade, no burst ring and no press squish.
|
|
55
|
+
if (reduce) {
|
|
56
|
+
Animated.timing(progress, { toValue: on ? 1 : 0, duration: 150, useNativeDriver: false }).start();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
Animated.spring(progress, { toValue: on ? 1 : 0, useNativeDriver: false, speed: 14, bounciness: 0 }).start();
|
|
53
60
|
if (on) {
|
|
54
61
|
burst.setValue(0);
|
|
55
62
|
Animated.timing(burst, { toValue: 1, duration: 450, useNativeDriver: true }).start();
|
|
56
63
|
}
|
|
57
|
-
}, [on, progress, burst]);
|
|
64
|
+
}, [on, progress, burst, reduce]);
|
|
58
65
|
|
|
59
66
|
const toggle = () => {
|
|
60
67
|
if (checked === undefined) setInner(!on);
|
|
@@ -71,8 +78,8 @@ export function Checkbox({
|
|
|
71
78
|
accessibilityState={{ checked: on, disabled }}
|
|
72
79
|
disabled={disabled}
|
|
73
80
|
onPress={toggle}
|
|
74
|
-
onPressIn={() => Animated.spring(press, { toValue: 0.82, useNativeDriver: true, speed: 50 }).start()}
|
|
75
|
-
onPressOut={() => Animated.spring(press, { toValue: 1, useNativeDriver: true, speed: 20, bounciness:
|
|
81
|
+
onPressIn={() => !reduce && Animated.spring(press, { toValue: 0.82, useNativeDriver: true, speed: 50, bounciness: 0 }).start()}
|
|
82
|
+
onPressOut={() => !reduce && Animated.spring(press, { toValue: 1, useNativeDriver: true, speed: 20, bounciness: 0 }).start()}
|
|
76
83
|
style={[styles.row, { opacity: disabled ? 0.5 : 1 }, style]}
|
|
77
84
|
>
|
|
78
85
|
<Animated.View style={{ transform: [{ scale: press }] }}>
|
|
@@ -119,7 +126,7 @@ export function Checkbox({
|
|
|
119
126
|
{label ? (
|
|
120
127
|
<View style={styles.text}>
|
|
121
128
|
<View style={styles.labelBox}>
|
|
122
|
-
<Animated.Text style={[styles.label, { color: progress.interpolate({ inputRange: [0, 1], outputRange: [fg, strike ? muted : fg] }) }]}>
|
|
129
|
+
<Animated.Text style={[styles.label, ff, { color: progress.interpolate({ inputRange: [0, 1], outputRange: [fg, strike ? muted : fg] }) }]}>
|
|
123
130
|
{label}
|
|
124
131
|
</Animated.Text>
|
|
125
132
|
{strike ? (
|
|
@@ -131,13 +138,29 @@ export function Checkbox({
|
|
|
131
138
|
/>
|
|
132
139
|
) : null}
|
|
133
140
|
</View>
|
|
134
|
-
{description ? <Text style={[styles.description, { color: muted }]}>{description}</Text> : null}
|
|
141
|
+
{description ? <Text style={[styles.description, ff, { color: muted }]}>{description}</Text> : null}
|
|
135
142
|
</View>
|
|
136
143
|
) : null}
|
|
137
144
|
</Pressable>
|
|
138
145
|
);
|
|
139
146
|
}
|
|
140
147
|
|
|
148
|
+
function useReduceMotion() {
|
|
149
|
+
const [reduce, setReduce] = useState(false);
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
let alive = true;
|
|
152
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
153
|
+
.then((r) => alive && setReduce(r))
|
|
154
|
+
.catch(() => {});
|
|
155
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
156
|
+
return () => {
|
|
157
|
+
alive = false;
|
|
158
|
+
sub.remove();
|
|
159
|
+
};
|
|
160
|
+
}, []);
|
|
161
|
+
return reduce;
|
|
162
|
+
}
|
|
163
|
+
|
|
141
164
|
const styles = StyleSheet.create({
|
|
142
165
|
row: { flexDirection: 'row', alignItems: 'flex-start', gap: 12 },
|
|
143
166
|
box: { borderWidth: 2, alignItems: 'center', justifyContent: 'center' },
|
|
@@ -181,14 +181,17 @@ export function useKit(): Theme {
|
|
|
181
181
|
return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Shared motion presets so the whole kit moves the same way. Every spring is critically damped
|
|
186
|
+
* (damping ≥ 2·√(stiffness·mass)): it settles smoothly with no overshoot or wobble.
|
|
187
|
+
*/
|
|
185
188
|
export const motion = {
|
|
186
189
|
/** Toggles, selection indicators. */
|
|
187
|
-
snappy: { damping:
|
|
188
|
-
/** Press feedback, pops. */
|
|
189
|
-
bouncy: { damping:
|
|
190
|
+
snappy: { damping: 30, stiffness: 260, mass: 0.8 },
|
|
191
|
+
/** Press feedback, pops. (Kept under its old name; it no longer bounces.) */
|
|
192
|
+
bouncy: { damping: 26, stiffness: 220, mass: 0.7 },
|
|
190
193
|
/** Sheets, layout changes. */
|
|
191
|
-
gentle: { damping:
|
|
194
|
+
gentle: { damping: 27, stiffness: 160, mass: 1 },
|
|
192
195
|
fast: 160,
|
|
193
196
|
normal: 240,
|
|
194
197
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
AccessibilityInfo,
|
|
3
4
|
Animated,
|
|
4
5
|
Pressable,
|
|
5
6
|
ScrollView,
|
|
@@ -56,6 +57,10 @@ export interface ChipGroupProps {
|
|
|
56
57
|
style?: StyleProp<ViewStyle>;
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
61
|
+
// content text scales all the way.
|
|
62
|
+
const FONT_CAP = 1.35;
|
|
63
|
+
|
|
59
64
|
const SIZES: Record<ChipGroupSize, { h: number; px: number; font: number; check: number }> = {
|
|
60
65
|
sm: { h: 30, px: 12, font: 13, check: 14 },
|
|
61
66
|
md: { h: 36, px: 14, font: 14.5, check: 16 },
|
|
@@ -84,6 +89,7 @@ export function ChipGroup({
|
|
|
84
89
|
style,
|
|
85
90
|
}: ChipGroupProps) {
|
|
86
91
|
const { colors } = useKit();
|
|
92
|
+
const reduce = useReduceMotion();
|
|
87
93
|
const tint = accent ?? colors.accent;
|
|
88
94
|
const items: ChipOption[] = options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o));
|
|
89
95
|
const [inner, setInner] = useState<string[]>(defaultValue);
|
|
@@ -114,6 +120,7 @@ export function ChipGroup({
|
|
|
114
120
|
showCheck={showCheck}
|
|
115
121
|
multiple={multiple}
|
|
116
122
|
disabled={disabled || o.disabled}
|
|
123
|
+
reduce={reduce}
|
|
117
124
|
/>
|
|
118
125
|
));
|
|
119
126
|
|
|
@@ -148,20 +155,26 @@ interface ChipProps {
|
|
|
148
155
|
showCheck: boolean;
|
|
149
156
|
multiple: boolean;
|
|
150
157
|
disabled?: boolean;
|
|
158
|
+
reduce: boolean;
|
|
151
159
|
}
|
|
152
160
|
|
|
153
|
-
function Chip({ option, active, onPress, variant, size, tint, showCheck, multiple, disabled }: ChipProps) {
|
|
154
|
-
const { colors, radius } = useKit();
|
|
161
|
+
function Chip({ option, active, onPress, variant, size, tint, showCheck, multiple, disabled, reduce }: ChipProps) {
|
|
162
|
+
const { colors, radius, font } = useKit();
|
|
163
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
155
164
|
// p: colours, check width + stroke (JS driver). press: squish (native, on the outer view).
|
|
156
165
|
const p = useRef(new Animated.Value(active ? 1 : 0)).current;
|
|
157
166
|
const press = useRef(new Animated.Value(1)).current;
|
|
158
167
|
|
|
159
168
|
useEffect(() => {
|
|
160
|
-
|
|
161
|
-
|
|
169
|
+
// Reduce Motion: the chip swaps state at once (no widening, no check draw).
|
|
170
|
+
if (reduce) p.setValue(active ? 1 : 0);
|
|
171
|
+
else Animated.spring(p, { toValue: active ? 1 : 0, useNativeDriver: false, ...motion.snappy }).start();
|
|
172
|
+
}, [active, p, reduce]);
|
|
162
173
|
|
|
163
|
-
const pressTo = (to: number) =>
|
|
174
|
+
const pressTo = (to: number) => {
|
|
175
|
+
if (reduce) return;
|
|
164
176
|
Animated.spring(press, { toValue: to, useNativeDriver: true, ...(to < 1 ? motion.snappy : motion.bouncy) }).start();
|
|
177
|
+
};
|
|
165
178
|
|
|
166
179
|
const look: Record<ChipGroupVariant, { bg: [string, string]; border: [string, string]; fg: [string, string] }> = {
|
|
167
180
|
solid: {
|
|
@@ -200,7 +213,7 @@ function Chip({ option, active, onPress, variant, size, tint, showCheck, multipl
|
|
|
200
213
|
style={[
|
|
201
214
|
styles.chip,
|
|
202
215
|
{
|
|
203
|
-
|
|
216
|
+
minHeight: size.h,
|
|
204
217
|
borderRadius: radius.capsule(size.h),
|
|
205
218
|
paddingHorizontal: size.px - 1,
|
|
206
219
|
backgroundColor: p.interpolate({ ...range, outputRange: l.bg }),
|
|
@@ -237,15 +250,18 @@ function Chip({ option, active, onPress, variant, size, tint, showCheck, multipl
|
|
|
237
250
|
{option.label ? (
|
|
238
251
|
<Animated.Text
|
|
239
252
|
numberOfLines={1}
|
|
240
|
-
|
|
253
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
254
|
+
style={[styles.label, ff, { fontSize: size.font, color: p.interpolate({ ...range, outputRange: l.fg }) }]}
|
|
241
255
|
>
|
|
242
256
|
{option.label}
|
|
243
257
|
</Animated.Text>
|
|
244
258
|
) : null}
|
|
245
259
|
{option.count !== undefined ? (
|
|
246
260
|
<Animated.Text
|
|
261
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
247
262
|
style={[
|
|
248
263
|
styles.count,
|
|
264
|
+
ff,
|
|
249
265
|
{ fontSize: size.font - 1.5, color: p.interpolate({ ...range, outputRange: [colors.mutedForeground, l.fg[1]] }) },
|
|
250
266
|
]}
|
|
251
267
|
>
|
|
@@ -258,6 +274,22 @@ function Chip({ option, active, onPress, variant, size, tint, showCheck, multipl
|
|
|
258
274
|
);
|
|
259
275
|
}
|
|
260
276
|
|
|
277
|
+
function useReduceMotion() {
|
|
278
|
+
const [reduce, setReduce] = useState(false);
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
let alive = true;
|
|
281
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
282
|
+
.then((r) => alive && setReduce(r))
|
|
283
|
+
.catch(() => {});
|
|
284
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
285
|
+
return () => {
|
|
286
|
+
alive = false;
|
|
287
|
+
sub.remove();
|
|
288
|
+
};
|
|
289
|
+
}, []);
|
|
290
|
+
return reduce;
|
|
291
|
+
}
|
|
292
|
+
|
|
261
293
|
const styles = StyleSheet.create({
|
|
262
294
|
row: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
|
263
295
|
wrap: { flexWrap: 'wrap' },
|
|
@@ -181,14 +181,17 @@ export function useKit(): Theme {
|
|
|
181
181
|
return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Shared motion presets so the whole kit moves the same way. Every spring is critically damped
|
|
186
|
+
* (damping ≥ 2·√(stiffness·mass)): it settles smoothly with no overshoot or wobble.
|
|
187
|
+
*/
|
|
185
188
|
export const motion = {
|
|
186
189
|
/** Toggles, selection indicators. */
|
|
187
|
-
snappy: { damping:
|
|
188
|
-
/** Press feedback, pops. */
|
|
189
|
-
bouncy: { damping:
|
|
190
|
+
snappy: { damping: 30, stiffness: 260, mass: 0.8 },
|
|
191
|
+
/** Press feedback, pops. (Kept under its old name; it no longer bounces.) */
|
|
192
|
+
bouncy: { damping: 26, stiffness: 220, mass: 0.7 },
|
|
190
193
|
/** Sheets, layout changes. */
|
|
191
|
-
gentle: { damping:
|
|
194
|
+
gentle: { damping: 27, stiffness: 160, mass: 1 },
|
|
192
195
|
fast: 160,
|
|
193
196
|
normal: 240,
|
|
194
197
|
};
|
|
@@ -181,14 +181,17 @@ export function useKit(): Theme {
|
|
|
181
181
|
return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Shared motion presets so the whole kit moves the same way. Every spring is critically damped
|
|
186
|
+
* (damping ≥ 2·√(stiffness·mass)): it settles smoothly with no overshoot or wobble.
|
|
187
|
+
*/
|
|
185
188
|
export const motion = {
|
|
186
189
|
/** Toggles, selection indicators. */
|
|
187
|
-
snappy: { damping:
|
|
188
|
-
/** Press feedback, pops. */
|
|
189
|
-
bouncy: { damping:
|
|
190
|
+
snappy: { damping: 30, stiffness: 260, mass: 0.8 },
|
|
191
|
+
/** Press feedback, pops. (Kept under its old name; it no longer bounces.) */
|
|
192
|
+
bouncy: { damping: 26, stiffness: 220, mass: 0.7 },
|
|
190
193
|
/** Sheets, layout changes. */
|
|
191
|
-
gentle: { damping:
|
|
194
|
+
gentle: { damping: 27, stiffness: 160, mass: 1 },
|
|
192
195
|
fast: 160,
|
|
193
196
|
normal: 240,
|
|
194
197
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { Animated, Easing, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
import { AccessibilityInfo, Animated, Easing, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
import Svg, { Defs, LinearGradient, Path, Rect, Stop } from 'react-native-svg';
|
|
4
4
|
|
|
5
5
|
import { FlipCard, useFlipProgress } from '@/components/ui/flip-card';
|
|
@@ -170,6 +170,10 @@ function contrast(a: string, b: string) {
|
|
|
170
170
|
return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
174
|
+
// content text scales all the way.
|
|
175
|
+
const FONT_CAP = 1.35;
|
|
176
|
+
|
|
173
177
|
const DAY = 86_400_000;
|
|
174
178
|
const pad2 = (n: number) => String(n).padStart(2, '0');
|
|
175
179
|
|
|
@@ -197,7 +201,9 @@ function useRemaining(at?: Date | number) {
|
|
|
197
201
|
}
|
|
198
202
|
|
|
199
203
|
/** The promo code in a dashed chip; a tap copies it and flips the icon to a check for a moment. */
|
|
200
|
-
function CodeChip({ code, color, ink, fontSize, onCopy, labels }: { code: string; color: string; ink: string; fontSize: number; onCopy?: (code: string) => void; labels: Required<CouponLabels
|
|
204
|
+
function CodeChip({ code, color, ink, fontSize, onCopy, labels, reduce }: { code: string; color: string; ink: string; fontSize: number; onCopy?: (code: string) => void; labels: Required<CouponLabels>; reduce: boolean }) {
|
|
205
|
+
const { font } = useKit();
|
|
206
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
201
207
|
const [copied, setCopied] = useState(false);
|
|
202
208
|
const [box, setBox] = useState({ w: 0, h: 0 });
|
|
203
209
|
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
@@ -206,8 +212,10 @@ function CodeChip({ code, color, ink, fontSize, onCopy, labels }: { code: string
|
|
|
206
212
|
const copy = () => {
|
|
207
213
|
onCopy?.(code);
|
|
208
214
|
setCopied(true);
|
|
209
|
-
|
|
210
|
-
|
|
215
|
+
if (!reduce) {
|
|
216
|
+
pop.setValue(0.4);
|
|
217
|
+
Animated.spring(pop, { toValue: 1, ...motion.bouncy, useNativeDriver: true }).start();
|
|
218
|
+
}
|
|
211
219
|
clearTimeout(timer.current);
|
|
212
220
|
timer.current = setTimeout(() => setCopied(false), 1600);
|
|
213
221
|
};
|
|
@@ -226,7 +234,7 @@ function CodeChip({ code, color, ink, fontSize, onCopy, labels }: { code: string
|
|
|
226
234
|
<Rect x={0.75} y={0.75} width={box.w - 1.5} height={box.h - 1.5} rx={8} fill="none" stroke={alpha(color, 0.75)} strokeWidth={1.5} strokeDasharray={[5, 4]} />
|
|
227
235
|
</Svg>
|
|
228
236
|
) : null}
|
|
229
|
-
<Text numberOfLines={1} style={{ color: ink, fontSize, fontWeight: '800', letterSpacing: 1, flexShrink: 1 }}>
|
|
237
|
+
<Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[{ color: ink, fontSize, fontWeight: '800', letterSpacing: 1, flexShrink: 1 }, ff]}>
|
|
230
238
|
{code}
|
|
231
239
|
</Text>
|
|
232
240
|
{onCopy ? (
|
|
@@ -325,7 +333,9 @@ export function Coupon({
|
|
|
325
333
|
labels: labelsProp,
|
|
326
334
|
style,
|
|
327
335
|
}: CouponProps) {
|
|
328
|
-
const { colors, dark } = useKit();
|
|
336
|
+
const { colors, dark, font } = useKit();
|
|
337
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
338
|
+
const reduce = useReduceMotion();
|
|
329
339
|
const theme = typeof themeProp === 'object' ? themeProp : couponThemes[themeProp];
|
|
330
340
|
const fills = typeof theme.fill === 'string' ? [theme.fill] : theme.fill;
|
|
331
341
|
const accent = theme.accent ?? fills[fills.length - 1];
|
|
@@ -421,7 +431,7 @@ export function Coupon({
|
|
|
421
431
|
left !== undefined && left > 0 && !spent ? (
|
|
422
432
|
<View style={[styles.timer, { backgroundColor: timerBg }]}>
|
|
423
433
|
<Icon d={ICON.clock} size={11} color={timerInk} />
|
|
424
|
-
<Text style={{ color: timerInk, fontSize: 10.5, fontWeight: '800', fontVariant: ['tabular-nums'] }}>
|
|
434
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: timerInk, fontSize: 10.5, fontWeight: '800', fontVariant: ['tabular-nums'] }, ff]}>
|
|
425
435
|
{labels.endsIn} {formatCountdown(left)}
|
|
426
436
|
</Text>
|
|
427
437
|
</View>
|
|
@@ -433,7 +443,7 @@ export function Coupon({
|
|
|
433
443
|
<View style={[styles.row, { gap: 6, flexShrink: 1 }]}>
|
|
434
444
|
{logo}
|
|
435
445
|
{brand ? (
|
|
436
|
-
<Text numberOfLines={1} style={{ color: horizontal ? bodySoft : valueSoft, fontSize: 10.5, fontWeight: '800', letterSpacing: 1.2, flexShrink: 1 }}>
|
|
446
|
+
<Text numberOfLines={1} style={[{ color: horizontal ? bodySoft : valueSoft, fontSize: 10.5, fontWeight: '800', letterSpacing: 1.2, flexShrink: 1 }, ff]}>
|
|
437
447
|
{brand.toUpperCase()}
|
|
438
448
|
</Text>
|
|
439
449
|
) : null}
|
|
@@ -445,15 +455,15 @@ export function Coupon({
|
|
|
445
455
|
// Value panel ------------------------------------------------------------------------------------
|
|
446
456
|
const valueBlock = (
|
|
447
457
|
<View style={{ alignItems: 'center' }}>
|
|
448
|
-
{prefix ? <Text style={[{ color: valueSoft, fontSize: 10.5, fontWeight: '900', letterSpacing: 1.6 }, lift]}>{prefix.toUpperCase()}</Text> : null}
|
|
458
|
+
{prefix ? <Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: valueSoft, fontSize: 10.5, fontWeight: '900', letterSpacing: 1.6 }, ff, lift]}>{prefix.toUpperCase()}</Text> : null}
|
|
449
459
|
{locked ? <Icon d={ICON.lock} size={valueSize * 0.6} color={valueInk} /> : null}
|
|
450
460
|
{/* Vertical coupons set "50% OFF" on one line when it fits, and stack a long unit ("CASHBACK"). */}
|
|
451
461
|
<View style={{ flexDirection: inline ? 'row' : 'column', alignItems: inline ? 'flex-end' : 'center', gap: inline ? 6 : 0 }}>
|
|
452
|
-
<Text numberOfLines={1} adjustsFontSizeToFit style={[{ color: valueInk, fontSize: valueSize, lineHeight: valueSize * 1.08, fontWeight: '900', letterSpacing: -1.2 }, lift]}>
|
|
462
|
+
<Text numberOfLines={1} adjustsFontSizeToFit maxFontSizeMultiplier={FONT_CAP} style={[{ color: valueInk, fontSize: valueSize, lineHeight: valueSize * 1.08, fontWeight: '900', letterSpacing: -1.2 }, ff, lift]}>
|
|
453
463
|
{value}
|
|
454
464
|
</Text>
|
|
455
465
|
{unit ? (
|
|
456
|
-
<Text style={[{ color: valueInk, fontSize: horizontal ? 12.5 : 16, fontWeight: '900', letterSpacing: 2.4, marginBottom: inline ? valueSize * 0.14 : 0 }, lift]}>
|
|
466
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: valueInk, fontSize: horizontal ? 12.5 : 16, fontWeight: '900', letterSpacing: 2.4, marginBottom: inline ? valueSize * 0.14 : 0 }, ff, lift]}>
|
|
457
467
|
{unit.toUpperCase()}
|
|
458
468
|
</Text>
|
|
459
469
|
) : null}
|
|
@@ -482,7 +492,7 @@ export function Coupon({
|
|
|
482
492
|
{brandRow}
|
|
483
493
|
{valueBlock}
|
|
484
494
|
{title ? (
|
|
485
|
-
<Text numberOfLines={2} style={[{ color: valueInk, fontSize: s.title + 1, fontWeight: '800', textAlign: 'center', letterSpacing: -0.3 }, lift]}>
|
|
495
|
+
<Text numberOfLines={2} style={[{ color: valueInk, fontSize: s.title + 1, fontWeight: '800', textAlign: 'center', letterSpacing: -0.3 }, ff, lift]}>
|
|
486
496
|
{title}
|
|
487
497
|
</Text>
|
|
488
498
|
) : null}
|
|
@@ -505,13 +515,13 @@ export function Coupon({
|
|
|
505
515
|
{
|
|
506
516
|
marginLeft: 'auto',
|
|
507
517
|
backgroundColor: applied ? '#16a34a' : locked || spent ? alpha(bodyInk, 0.12) : accent,
|
|
508
|
-
transform: [{ scale: pressed ? 0.95 : 1 }],
|
|
518
|
+
transform: [{ scale: pressed && !reduce ? 0.95 : 1 }],
|
|
509
519
|
paddingHorizontal: size === 'sm' ? 11 : size === 'md' ? 13 : 16,
|
|
510
520
|
},
|
|
511
521
|
]}
|
|
512
522
|
>
|
|
513
523
|
{applied || locked ? <Icon d={applied ? ICON.check : ICON.lock} size={13} color={applied ? '#ffffff' : bodySoft} /> : null}
|
|
514
|
-
<Text numberOfLines={1} style={{ color: applied ? '#ffffff' : locked || spent ? bodySoft : readableOn(accent), fontSize: s.chip - 0.5, fontWeight: '800' }}>
|
|
524
|
+
<Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[{ color: applied ? '#ffffff' : locked || spent ? bodySoft : readableOn(accent), fontSize: s.chip - 0.5, fontWeight: '800' }, ff]}>
|
|
515
525
|
{applied ? labels.applied : locked ? labels.locked : labels.apply}
|
|
516
526
|
</Text>
|
|
517
527
|
</Pressable>
|
|
@@ -520,11 +530,11 @@ export function Coupon({
|
|
|
520
530
|
const footer =
|
|
521
531
|
expires || canFlip ? (
|
|
522
532
|
<View style={[styles.row, { justifyContent: 'flex-start', gap: 6 }]}>
|
|
523
|
-
{expires ? <Text style={{ color: bodySoft, fontSize: 11, fontWeight: '600' }}>{expires}</Text> : null}
|
|
524
|
-
{expires && canFlip ? <Text style={{ color: bodySoft, fontSize: 11 }}>·</Text> : null}
|
|
533
|
+
{expires ? <Text style={[{ color: bodySoft, fontSize: 11, fontWeight: '600', flexShrink: 1 }, ff]}>{expires}</Text> : null}
|
|
534
|
+
{expires && canFlip ? <Text style={[{ color: bodySoft, fontSize: 11 }, ff]}>·</Text> : null}
|
|
525
535
|
{canFlip ? (
|
|
526
536
|
<Pressable onPress={toggle} hitSlop={10} accessibilityRole="button" accessibilityLabel={labels.termsTitle}>
|
|
527
|
-
<Text style={{ color: textAccent, fontSize: 11, fontWeight: '800', textDecorationLine: 'underline' }}>{labels.terms}</Text>
|
|
537
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: textAccent, fontSize: 11, fontWeight: '800', textDecorationLine: 'underline' }, ff]}>{labels.terms}</Text>
|
|
528
538
|
</Pressable>
|
|
529
539
|
) : null}
|
|
530
540
|
</View>
|
|
@@ -552,16 +562,16 @@ export function Coupon({
|
|
|
552
562
|
>
|
|
553
563
|
{horizontal ? brandRow : null}
|
|
554
564
|
{horizontal && title ? (
|
|
555
|
-
<Text numberOfLines={2} style={{ color: bodyInk, fontSize: s.title, fontWeight: '800', letterSpacing: -0.3 }}>
|
|
565
|
+
<Text numberOfLines={2} style={[{ color: bodyInk, fontSize: s.title, fontWeight: '800', letterSpacing: -0.3 }, ff]}>
|
|
556
566
|
{title}
|
|
557
567
|
</Text>
|
|
558
568
|
) : null}
|
|
559
569
|
{locked && lockedHint ? (
|
|
560
|
-
<Text numberOfLines={2} style={{ color: textAccent, fontSize: s.desc, fontWeight: '700' }}>
|
|
570
|
+
<Text numberOfLines={2} style={[{ color: textAccent, fontSize: s.desc, fontWeight: '700' }, ff]}>
|
|
561
571
|
{lockedHint}
|
|
562
572
|
</Text>
|
|
563
573
|
) : description ? (
|
|
564
|
-
<Text numberOfLines={size === 'sm' ? 1 : 2} style={{ color: bodySoft, fontSize: s.desc, lineHeight: s.desc * 1.35 }}>
|
|
574
|
+
<Text numberOfLines={size === 'sm' ? 1 : 2} style={[{ color: bodySoft, fontSize: s.desc, lineHeight: s.desc * 1.35 }, ff]}>
|
|
565
575
|
{description}
|
|
566
576
|
</Text>
|
|
567
577
|
) : null}
|
|
@@ -569,7 +579,7 @@ export function Coupon({
|
|
|
569
579
|
// The code never gets cut short: when it and the button don't fit side by side, the button
|
|
570
580
|
// wraps under it (and stays right-aligned).
|
|
571
581
|
<View style={[styles.row, { gap: 8, marginTop: 2, flexWrap: 'wrap' }]}>
|
|
572
|
-
{code ? <CodeChip code={code} color={textAccent} ink={bodyInk} fontSize={s.chip} onCopy={spent || locked ? undefined : onCopy} labels={labels} /> : <View />}
|
|
582
|
+
{code ? <CodeChip code={code} color={textAccent} ink={bodyInk} fontSize={s.chip} onCopy={spent || locked ? undefined : onCopy} labels={labels} reduce={reduce} /> : <View />}
|
|
573
583
|
{action}
|
|
574
584
|
</View>
|
|
575
585
|
) : null}
|
|
@@ -579,7 +589,8 @@ export function Coupon({
|
|
|
579
589
|
|
|
580
590
|
const { width: _w, height: _h, ...shape } = outline;
|
|
581
591
|
const glareClip = measured ? ticketSafeRects(outline) : undefined;
|
|
582
|
-
|
|
592
|
+
// The sweep is decorative, so Reduce Motion drops it.
|
|
593
|
+
const valueWindows = shine && !reduce && measured && !spent && !locked
|
|
583
594
|
? ticketSafeRects({ width: valueBox.width, height: valueBox.height, corners: valueCorners, edges: valueEdges, edgeSize: v.edgeSize })
|
|
584
595
|
: null;
|
|
585
596
|
|
|
@@ -627,23 +638,23 @@ export function Coupon({
|
|
|
627
638
|
<TicketShape {...shape} fill={body} style={StyleSheet.absoluteFill}>
|
|
628
639
|
<Pressable onPress={toggle} accessibilityRole="button" accessibilityHint={labels.back} style={[StyleSheet.absoluteFill, { padding: s.pad + edgePad, gap: 6, flexDirection: horizontal ? 'row' : 'column' }]}>
|
|
629
640
|
<View style={{ flex: 1, gap: 4 }}>
|
|
630
|
-
<Text style={{ color: bodyInk, fontSize: 12, fontWeight: '800', letterSpacing: 0.3 }}>{labels.termsTitle}</Text>
|
|
641
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: bodyInk, fontSize: 12, fontWeight: '800', letterSpacing: 0.3 }, ff]}>{labels.termsTitle}</Text>
|
|
631
642
|
{terms!.slice(0, size === 'lg' ? 5 : 4).map((t, i) => (
|
|
632
643
|
<View key={i} style={{ flexDirection: 'row', gap: 6 }}>
|
|
633
|
-
<Text style={{ color: textAccent, fontSize: 11, fontWeight: '900' }}>•</Text>
|
|
634
|
-
<Text numberOfLines={2} style={{ color: bodySoft, fontSize: 11, lineHeight: 14.5, flex: 1 }}>
|
|
644
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: textAccent, fontSize: 11, fontWeight: '900' }, ff]}>•</Text>
|
|
645
|
+
<Text numberOfLines={2} maxFontSizeMultiplier={FONT_CAP} style={[{ color: bodySoft, fontSize: 11, lineHeight: 14.5, flex: 1 }, ff]}>
|
|
635
646
|
{t}
|
|
636
647
|
</Text>
|
|
637
648
|
</View>
|
|
638
649
|
))}
|
|
639
|
-
<Text style={{ color: alpha(bodyInk, 0.4), fontSize: 10, fontWeight: '700', marginTop: 'auto' }}>{labels.back}</Text>
|
|
650
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: alpha(bodyInk, 0.4), fontSize: 10, fontWeight: '700', marginTop: 'auto' }, ff]}>{labels.back}</Text>
|
|
640
651
|
</View>
|
|
641
652
|
{code ? (
|
|
642
653
|
<View style={{ alignItems: 'center', justifyContent: 'center', gap: 4 }}>
|
|
643
654
|
<View style={styles.plate}>
|
|
644
655
|
<BarcodeLike code={code} width={horizontal ? 74 : 200} height={horizontal ? 54 : 46} />
|
|
645
656
|
</View>
|
|
646
|
-
<Text style={{ color: bodyInk, fontSize: 10, fontWeight: '800', letterSpacing: 1.2 }}>{code}</Text>
|
|
657
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: bodyInk, fontSize: 10, fontWeight: '800', letterSpacing: 1.2 }, ff]}>{code}</Text>
|
|
647
658
|
</View>
|
|
648
659
|
) : null}
|
|
649
660
|
</Pressable>
|
|
@@ -665,7 +676,7 @@ export function Coupon({
|
|
|
665
676
|
return (
|
|
666
677
|
<Pressable onPress={onPress} disabled={!onPress} accessibilityRole={onPress ? 'button' : undefined} style={style}>
|
|
667
678
|
{({ pressed }) => (
|
|
668
|
-
<View style={{ transform: [{ scale: pressed ? 0.985 : 1 }] }}>
|
|
679
|
+
<View style={{ transform: [{ scale: pressed && !reduce ? 0.985 : 1 }] }}>
|
|
669
680
|
{/* The plate waits for the painted outline (no black flash on mount), and a dimmed (used /
|
|
670
681
|
expired) coupon is see-through, so it gets none. */}
|
|
671
682
|
{spent || !measured ? null : <View pointerEvents="none" style={[styles.shadow, { top: inset, bottom: inset - 4, left: inset, right: inset, borderRadius: v.r, shadowOpacity: dark ? 0.5 : 0.2 }]} />}
|
|
@@ -677,6 +688,22 @@ export function Coupon({
|
|
|
677
688
|
);
|
|
678
689
|
}
|
|
679
690
|
|
|
691
|
+
function useReduceMotion() {
|
|
692
|
+
const [reduce, setReduce] = useState(false);
|
|
693
|
+
useEffect(() => {
|
|
694
|
+
let alive = true;
|
|
695
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
696
|
+
.then((r) => alive && setReduce(r))
|
|
697
|
+
.catch(() => {});
|
|
698
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
699
|
+
return () => {
|
|
700
|
+
alive = false;
|
|
701
|
+
sub.remove();
|
|
702
|
+
};
|
|
703
|
+
}, []);
|
|
704
|
+
return reduce;
|
|
705
|
+
}
|
|
706
|
+
|
|
680
707
|
const styles = StyleSheet.create({
|
|
681
708
|
row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
|
682
709
|
center: { alignItems: 'center', justifyContent: 'center' },
|
|
@@ -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
|
}
|
|
@@ -121,7 +124,7 @@ export function FlipCard({
|
|
|
121
124
|
borderRadius: radius,
|
|
122
125
|
opacity,
|
|
123
126
|
transform: [{ perspective }, { [axis]: turn } as never, { scale: lift }],
|
|
124
|
-
}) as Animated.
|
|
127
|
+
}) as Animated.WithAnimatedValue<ViewStyle>;
|
|
125
128
|
|
|
126
129
|
// The shadow squeezes along the turning axis as the card goes edge-on (see SQUEEZE) and fades a little.
|
|
127
130
|
const squeeze = p.interpolate(SQUEEZE);
|
|
@@ -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 },
|