@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,6 +1,7 @@
|
|
|
1
1
|
import * as Haptics from 'expo-haptics';
|
|
2
|
-
import React, { useId, useRef } from 'react';
|
|
2
|
+
import React, { useEffect, useId, useRef, useState } from 'react';
|
|
3
3
|
import {
|
|
4
|
+
AccessibilityInfo,
|
|
4
5
|
ActivityIndicator,
|
|
5
6
|
Animated,
|
|
6
7
|
Pressable,
|
|
@@ -55,6 +56,10 @@ export interface ButtonProps extends Omit<PressableProps, 'style' | 'children'>
|
|
|
55
56
|
textStyle?: StyleProp<TextStyle>;
|
|
56
57
|
}
|
|
57
58
|
|
|
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.
|
|
61
|
+
const FONT_CAP = 1.35;
|
|
62
|
+
|
|
58
63
|
const SIZES: Record<ButtonSize, { h: number; px: number; font: number; gap: number }> = {
|
|
59
64
|
sm: { h: 36, px: 14, font: 14, gap: 6 },
|
|
60
65
|
md: { h: 46, px: 18, font: 15.5, gap: 8 },
|
|
@@ -116,8 +121,10 @@ export function Button({
|
|
|
116
121
|
onPressOut,
|
|
117
122
|
...props
|
|
118
123
|
}: ButtonProps) {
|
|
119
|
-
const { colors, radius, dark } = useKit();
|
|
124
|
+
const { colors, radius, dark, font } = useKit();
|
|
125
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
120
126
|
const gid = 'btn' + useId().replace(/[^a-zA-Z0-9]/g, '');
|
|
127
|
+
const reduce = useReduceMotion();
|
|
121
128
|
const scale = useRef(new Animated.Value(1)).current;
|
|
122
129
|
const isDisabled = disabled || loading;
|
|
123
130
|
const tint = color ?? colors.accent;
|
|
@@ -144,12 +151,17 @@ export function Button({
|
|
|
144
151
|
const solid = bg !== 'transparent';
|
|
145
152
|
const quiet = variant === 'ghost' || variant === 'outline' || variant === 'link';
|
|
146
153
|
|
|
147
|
-
|
|
154
|
+
// Reduce Motion: no squish; the pressed wash / opacity still marks the press.
|
|
155
|
+
const spring = (to: number) => {
|
|
156
|
+
if (reduce) return scale.setValue(1);
|
|
157
|
+
Animated.spring(scale, { toValue: to, useNativeDriver: true, ...motion.bouncy }).start();
|
|
158
|
+
};
|
|
148
159
|
|
|
149
160
|
const label = isTextNode(children) ? (
|
|
150
161
|
<Text
|
|
151
162
|
numberOfLines={1}
|
|
152
|
-
|
|
163
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
164
|
+
style={[styles.label, ff, { color: fg, fontSize: s.font }, variant === 'link' && styles.link, textStyle]}
|
|
153
165
|
>
|
|
154
166
|
{children}
|
|
155
167
|
</Text>
|
|
@@ -184,7 +196,8 @@ export function Button({
|
|
|
184
196
|
style={({ pressed }) => [
|
|
185
197
|
styles.base,
|
|
186
198
|
{
|
|
187
|
-
|
|
199
|
+
minHeight: s.h,
|
|
200
|
+
height: size === 'icon' ? s.h : undefined,
|
|
188
201
|
width: size === 'icon' ? s.h : undefined,
|
|
189
202
|
paddingHorizontal: variant === 'link' ? 0 : s.px,
|
|
190
203
|
gap: s.gap,
|
|
@@ -235,7 +248,7 @@ export function Button({
|
|
|
235
248
|
<View style={styles.row}>
|
|
236
249
|
<ActivityIndicator size="small" color={fg} />
|
|
237
250
|
{size !== 'icon' && isTextNode(children) ? (
|
|
238
|
-
<Text style={[styles.label, { color: fg, fontSize: s.font, opacity: 0.85 }]}>{children}</Text>
|
|
251
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.label, ff, { color: fg, fontSize: s.font, opacity: 0.85 }]}>{children}</Text>
|
|
239
252
|
) : null}
|
|
240
253
|
</View>
|
|
241
254
|
) : (
|
|
@@ -252,6 +265,22 @@ export function Button({
|
|
|
252
265
|
);
|
|
253
266
|
}
|
|
254
267
|
|
|
268
|
+
function useReduceMotion() {
|
|
269
|
+
const [reduce, setReduce] = useState(false);
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
let alive = true;
|
|
272
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
273
|
+
.then((r) => alive && setReduce(r))
|
|
274
|
+
.catch(() => {});
|
|
275
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
276
|
+
return () => {
|
|
277
|
+
alive = false;
|
|
278
|
+
sub.remove();
|
|
279
|
+
};
|
|
280
|
+
}, []);
|
|
281
|
+
return reduce;
|
|
282
|
+
}
|
|
283
|
+
|
|
255
284
|
const styles = StyleSheet.create({
|
|
256
285
|
base: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
|
|
257
286
|
row: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
|
@@ -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
|
-
import React, { useRef } from 'react';
|
|
2
|
-
import { ActivityIndicator, Animated, Platform, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { AccessibilityInfo, ActivityIndicator, Animated, Platform, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
import * as Haptics from 'expo-haptics';
|
|
4
4
|
import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';
|
|
5
5
|
|
|
@@ -38,6 +38,10 @@ export interface Button3DProps {
|
|
|
38
38
|
style?: StyleProp<ViewStyle>;
|
|
39
39
|
}
|
|
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
|
const SIZES = {
|
|
42
46
|
sm: { h: 36, px: 14, font: 13, depth: 4, gap: 6 },
|
|
43
47
|
md: { h: 46, px: 18, font: 15, depth: 5, gap: 8 },
|
|
@@ -105,7 +109,9 @@ export function Button3D({
|
|
|
105
109
|
accessibilityLabel,
|
|
106
110
|
style,
|
|
107
111
|
}: Button3DProps) {
|
|
108
|
-
const { dark } = useKit();
|
|
112
|
+
const { dark, font } = useKit();
|
|
113
|
+
const reduce = useReduceMotion();
|
|
114
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
109
115
|
const s = SIZES[size];
|
|
110
116
|
const depth = depthProp ?? s.depth;
|
|
111
117
|
const kind = appearance === 'auto' ? (dark ? 'light' : 'dark') : appearance;
|
|
@@ -115,14 +121,17 @@ export function Button3D({
|
|
|
115
121
|
const gid = `b3d-${color ? color.replace('#', '') : kind}`;
|
|
116
122
|
|
|
117
123
|
const press = useRef(new Animated.Value(0)).current;
|
|
118
|
-
const to = (v: number) =>
|
|
124
|
+
const to = (v: number) =>
|
|
125
|
+
reduce
|
|
126
|
+
? Animated.timing(press, { toValue: v, duration: 120, useNativeDriver: true }).start()
|
|
127
|
+
: Animated.spring(press, { toValue: v, ...(v ? motion.snappy : motion.bouncy), useNativeDriver: true }).start();
|
|
119
128
|
|
|
120
129
|
const label = loading ? (
|
|
121
130
|
<ActivityIndicator color={m.ink} size="small" />
|
|
122
131
|
) : isTextNode(children) ? (
|
|
123
132
|
// No numberOfLines: on a narrow fixed-width key ("esc", "Q") the label may use the padding
|
|
124
133
|
// instead of being cut to an ellipsis.
|
|
125
|
-
<Text style={{ color: m.ink, fontSize: round ? s.font + 4 : s.font, fontWeight: '600', letterSpacing: 0.1, flexShrink: 0, textAlign: 'center' }}>
|
|
134
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: m.ink, fontSize: round ? s.font + 4 : s.font, fontWeight: '600', letterSpacing: 0.1, flexShrink: 0, textAlign: 'center' }, ff]}>
|
|
126
135
|
{children}
|
|
127
136
|
</Text>
|
|
128
137
|
) : (
|
|
@@ -184,12 +193,15 @@ export function Button3D({
|
|
|
184
193
|
style={[
|
|
185
194
|
styles.face,
|
|
186
195
|
{
|
|
187
|
-
|
|
196
|
+
// A circle keeps its fixed size; other keys may grow taller for a large text size.
|
|
197
|
+
minHeight: s.h,
|
|
198
|
+
height: round ? s.h : undefined,
|
|
188
199
|
paddingHorizontal: round ? 0 : s.px,
|
|
189
200
|
gap: s.gap,
|
|
190
201
|
borderRadius: corner,
|
|
191
202
|
elevation: 4,
|
|
192
|
-
|
|
203
|
+
// Reduce Motion: the key doesn't sink; only the contact shadow dims.
|
|
204
|
+
transform: reduce ? [] : [{ translateY: press.interpolate({ inputRange: [0, 1], outputRange: [0, depth] }) }],
|
|
193
205
|
},
|
|
194
206
|
]}
|
|
195
207
|
>
|
|
@@ -205,6 +217,22 @@ export function Button3D({
|
|
|
205
217
|
);
|
|
206
218
|
}
|
|
207
219
|
|
|
220
|
+
function useReduceMotion() {
|
|
221
|
+
const [reduce, setReduce] = useState(false);
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
let alive = true;
|
|
224
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
225
|
+
.then((r) => alive && setReduce(r))
|
|
226
|
+
.catch(() => {});
|
|
227
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
228
|
+
return () => {
|
|
229
|
+
alive = false;
|
|
230
|
+
sub.remove();
|
|
231
|
+
};
|
|
232
|
+
}, []);
|
|
233
|
+
return reduce;
|
|
234
|
+
}
|
|
235
|
+
|
|
208
236
|
const styles = StyleSheet.create({
|
|
209
237
|
fill: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
|
|
210
238
|
shadow: { shadowColor: '#000', shadowRadius: 8, shadowOffset: { width: 0, height: 5 }, elevation: 3 },
|
|
@@ -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,6 +1,8 @@
|
|
|
1
1
|
import React, { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
AccessibilityInfo,
|
|
3
4
|
Animated,
|
|
5
|
+
Easing,
|
|
4
6
|
PanResponder,
|
|
5
7
|
Pressable,
|
|
6
8
|
StyleSheet,
|
|
@@ -98,6 +100,9 @@ type FlatProps = CalendarBaseProps & {
|
|
|
98
100
|
onChange?: (value: CalendarValue) => void;
|
|
99
101
|
};
|
|
100
102
|
|
|
103
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
104
|
+
// content text scales all the way.
|
|
105
|
+
const FONT_CAP = 1.35;
|
|
101
106
|
const CELL_H = 48;
|
|
102
107
|
const CIRCLE = 38;
|
|
103
108
|
const CIRCLE_TOP = 2;
|
|
@@ -201,9 +206,11 @@ export function Calendar(props: CalendarProps) {
|
|
|
201
206
|
accent,
|
|
202
207
|
style,
|
|
203
208
|
} = props as unknown as FlatProps;
|
|
204
|
-
const { colors, radius } = useKit();
|
|
209
|
+
const { colors, radius, font } = useKit();
|
|
210
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
205
211
|
const tint = accent ?? colors.accent;
|
|
206
212
|
const fmt = useFormatters(locale);
|
|
213
|
+
const reduce = useReduceMotion();
|
|
207
214
|
|
|
208
215
|
const [inner, setInner] = useState<CalendarValue>(defaultValue ?? null);
|
|
209
216
|
const current = value !== undefined ? value : inner;
|
|
@@ -251,7 +258,11 @@ export function Calendar(props: CalendarProps) {
|
|
|
251
258
|
setLeaving({ month: prev, dir });
|
|
252
259
|
slide.stopAnimation();
|
|
253
260
|
slide.setValue(0);
|
|
254
|
-
|
|
261
|
+
// Reduce Motion: a short cross-fade in place (the translate interpolations are flat).
|
|
262
|
+
const anim = reduce
|
|
263
|
+
? Animated.timing(slide, { toValue: 1, duration: 150, easing: Easing.bezier(0.32, 0.72, 0, 1), useNativeDriver: true })
|
|
264
|
+
: Animated.spring(slide, { toValue: 1, useNativeDriver: true, ...motion.gentle });
|
|
265
|
+
anim.start(({ finished }) => {
|
|
255
266
|
if (finished) setLeaving(null);
|
|
256
267
|
});
|
|
257
268
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -266,7 +277,8 @@ export function Calendar(props: CalendarProps) {
|
|
|
266
277
|
if (next !== 'days' && view === 'days') setPickYear(visible.getFullYear());
|
|
267
278
|
setView(next);
|
|
268
279
|
viewAnim.setValue(0);
|
|
269
|
-
Animated.
|
|
280
|
+
if (reduce) Animated.timing(viewAnim, { toValue: 1, duration: 150, useNativeDriver: true }).start();
|
|
281
|
+
else Animated.spring(viewAnim, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
270
282
|
};
|
|
271
283
|
|
|
272
284
|
/* selection */
|
|
@@ -331,11 +343,13 @@ export function Calendar(props: CalendarProps) {
|
|
|
331
343
|
manyKeys: many ? many.map(toDateKey).join('|') : undefined,
|
|
332
344
|
tint,
|
|
333
345
|
fullLabel: fmt.full,
|
|
346
|
+
reduce,
|
|
334
347
|
onDayPress,
|
|
335
348
|
};
|
|
336
349
|
|
|
337
350
|
const W = width || 320;
|
|
338
|
-
|
|
351
|
+
// Reduce Motion: months swap with a fade only, no sideways slide.
|
|
352
|
+
const dir = reduce ? 0 : dirRef.current;
|
|
339
353
|
const prevDisabled = view === 'days' ? monthIndex(visible) - 1 < minM : view === 'months' ? pickYear * 12 - 1 < minM : false;
|
|
340
354
|
const nextDisabled = view === 'days' ? monthIndex(visible) + 1 > maxM : view === 'months' ? (pickYear + 1) * 12 > maxM : false;
|
|
341
355
|
const decadeStart = Math.floor(pickYear / 12) * 12;
|
|
@@ -368,6 +382,7 @@ export function Calendar(props: CalendarProps) {
|
|
|
368
382
|
numberOfLines={1}
|
|
369
383
|
style={[
|
|
370
384
|
styles.title,
|
|
385
|
+
ff,
|
|
371
386
|
{ color: view === 'days' ? colors.foreground : tint },
|
|
372
387
|
view === 'days'
|
|
373
388
|
? {
|
|
@@ -379,7 +394,7 @@ export function Calendar(props: CalendarProps) {
|
|
|
379
394
|
>
|
|
380
395
|
{title}
|
|
381
396
|
</Animated.Text>
|
|
382
|
-
{monthPicker ? <Chevron turned={view !== 'days'} color={view === 'days' ? colors.mutedForeground : tint} /> : null}
|
|
397
|
+
{monthPicker ? <Chevron turned={view !== 'days'} color={view === 'days' ? colors.mutedForeground : tint} reduce={reduce} /> : null}
|
|
383
398
|
</Pressable>
|
|
384
399
|
<View style={styles.navs}>
|
|
385
400
|
{showJump ? (
|
|
@@ -390,7 +405,9 @@ export function Calendar(props: CalendarProps) {
|
|
|
390
405
|
hitSlop={4}
|
|
391
406
|
style={({ pressed }) => [styles.todayBtn, { borderRadius: radius.capsule(32), backgroundColor: pressed ? colors.secondary : colors.muted }]}
|
|
392
407
|
>
|
|
393
|
-
<Text style={[styles.todayText, { color: tint }]}>
|
|
408
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.todayText, ff, { color: tint }]}>
|
|
409
|
+
Today
|
|
410
|
+
</Text>
|
|
394
411
|
</Pressable>
|
|
395
412
|
) : null}
|
|
396
413
|
<NavButton dir={-1} disabled={prevDisabled} onPress={() => step(-1)} />
|
|
@@ -402,12 +419,12 @@ export function Calendar(props: CalendarProps) {
|
|
|
402
419
|
<Animated.View
|
|
403
420
|
style={{
|
|
404
421
|
opacity: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
405
|
-
transform: [{ scale: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [1.04, 1] }) }],
|
|
422
|
+
transform: [{ scale: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 1.04, 1] }) }],
|
|
406
423
|
}}
|
|
407
424
|
>
|
|
408
425
|
<View style={styles.weekRow} accessibilityElementsHidden importantForAccessibility="no-hide-descendants">
|
|
409
426
|
{weekdays.map((w) => (
|
|
410
|
-
<Text key={w.long} numberOfLines={1} style={[styles.weekday, { color: colors.mutedForeground }]}>
|
|
427
|
+
<Text key={w.long} numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.weekday, ff, { color: colors.mutedForeground }]}>
|
|
411
428
|
{w.short}
|
|
412
429
|
</Text>
|
|
413
430
|
))}
|
|
@@ -420,7 +437,7 @@ export function Calendar(props: CalendarProps) {
|
|
|
420
437
|
styles.leaving,
|
|
421
438
|
{
|
|
422
439
|
opacity: slide.interpolate({ inputRange: [0, 0.7], outputRange: [1, 0], extrapolate: 'clamp' }),
|
|
423
|
-
transform: [{ translateX: slide.interpolate({ inputRange: [0, 1], outputRange: [0, -leaving.dir * W * 0.6] }) }],
|
|
440
|
+
transform: [{ translateX: slide.interpolate({ inputRange: [0, 1], outputRange: [0, reduce ? 0 : -leaving.dir * W * 0.6] }) }],
|
|
424
441
|
},
|
|
425
442
|
]}
|
|
426
443
|
>
|
|
@@ -444,7 +461,7 @@ export function Calendar(props: CalendarProps) {
|
|
|
444
461
|
{ minHeight: gridH ? gridH + 28 : undefined },
|
|
445
462
|
{
|
|
446
463
|
opacity: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
447
|
-
transform: [{ scale: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [0.94, 1] }) }],
|
|
464
|
+
transform: [{ scale: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.94, 1] }) }],
|
|
448
465
|
},
|
|
449
466
|
]}
|
|
450
467
|
>
|
|
@@ -486,7 +503,9 @@ export function Calendar(props: CalendarProps) {
|
|
|
486
503
|
},
|
|
487
504
|
]}
|
|
488
505
|
>
|
|
489
|
-
<Text style={[styles.pickText, { color: selected ? readableOn(tint) : isNow ? tint : colors.foreground }]}>
|
|
506
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.pickText, ff, { color: selected ? readableOn(tint) : isNow ? tint : colors.foreground }]}>
|
|
507
|
+
{label}
|
|
508
|
+
</Text>
|
|
490
509
|
</View>
|
|
491
510
|
)}
|
|
492
511
|
</Pressable>
|
|
@@ -518,6 +537,7 @@ interface MonthGridProps {
|
|
|
518
537
|
manyKeys?: string;
|
|
519
538
|
tint: string;
|
|
520
539
|
fullLabel: (d: Date) => string;
|
|
540
|
+
reduce: boolean;
|
|
521
541
|
onDayPress: (d: Date) => void;
|
|
522
542
|
}
|
|
523
543
|
|
|
@@ -539,6 +559,7 @@ const MonthGrid = memo(function MonthGrid({
|
|
|
539
559
|
manyKeys,
|
|
540
560
|
tint,
|
|
541
561
|
fullLabel,
|
|
562
|
+
reduce,
|
|
542
563
|
onDayPress,
|
|
543
564
|
}: MonthGridProps) {
|
|
544
565
|
const weeks = useMemo(() => buildWeeks(month, weekStartsOn, fixedWeeks), [month, weekStartsOn, fixedWeeks]);
|
|
@@ -593,6 +614,7 @@ const MonthGrid = memo(function MonthGrid({
|
|
|
593
614
|
highlight={highlightedDates?.[key]}
|
|
594
615
|
tile={tiles && inMonth && !inBand}
|
|
595
616
|
tint={tint}
|
|
617
|
+
reduce={reduce}
|
|
596
618
|
onPress={onDayPress}
|
|
597
619
|
/>
|
|
598
620
|
);
|
|
@@ -618,6 +640,7 @@ interface DayCellProps {
|
|
|
618
640
|
highlight?: string;
|
|
619
641
|
tile: boolean;
|
|
620
642
|
tint: string;
|
|
643
|
+
reduce: boolean;
|
|
621
644
|
onPress: (d: Date) => void;
|
|
622
645
|
}
|
|
623
646
|
|
|
@@ -636,15 +659,19 @@ const DayCell = memo(function DayCell({
|
|
|
636
659
|
highlight,
|
|
637
660
|
tile,
|
|
638
661
|
tint,
|
|
662
|
+
reduce,
|
|
639
663
|
onPress,
|
|
640
664
|
}: DayCellProps) {
|
|
641
|
-
const { colors, radius } = useKit();
|
|
665
|
+
const { colors, radius, font } = useKit();
|
|
666
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
642
667
|
const r = tileRadius(radius.md);
|
|
643
668
|
const pop = useRef(new Animated.Value(selected ? 1 : 0)).current; // native: circle scale + opacity
|
|
644
669
|
const bandIn = useRef(new Animated.Value(band === 'none' ? 0 : 1)).current; // native: band opacity
|
|
645
670
|
useEffect(() => {
|
|
646
|
-
|
|
647
|
-
|
|
671
|
+
// Reduce Motion: the selection fills in with a quick fade, no scale pop.
|
|
672
|
+
if (reduce) Animated.timing(pop, { toValue: selected ? 1 : 0, duration: 150, useNativeDriver: true }).start();
|
|
673
|
+
else Animated.spring(pop, { toValue: selected ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
|
|
674
|
+
}, [selected, pop, reduce]);
|
|
648
675
|
useEffect(() => {
|
|
649
676
|
Animated.timing(bandIn, { toValue: band === 'none' ? 0 : 1, duration: motion.normal, useNativeDriver: true }).start();
|
|
650
677
|
}, [band, bandIn]);
|
|
@@ -711,14 +738,16 @@ const DayCell = memo(function DayCell({
|
|
|
711
738
|
borderRadius: r,
|
|
712
739
|
backgroundColor: tint,
|
|
713
740
|
opacity: pop.interpolate({ inputRange: [0, 0.4], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
714
|
-
transform: [{ scale: pop.interpolate({ inputRange: [0, 1], outputRange: [0.45, 1] }) }],
|
|
741
|
+
transform: [{ scale: pop.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.45, 1] }) }],
|
|
715
742
|
},
|
|
716
743
|
]}
|
|
717
744
|
/>
|
|
718
745
|
<View pointerEvents="none" style={styles.label}>
|
|
719
746
|
<Text
|
|
747
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
720
748
|
style={[
|
|
721
749
|
styles.day,
|
|
750
|
+
ff,
|
|
722
751
|
{ color: textColor },
|
|
723
752
|
(today || selected) && styles.dayStrong,
|
|
724
753
|
outside && !selected && styles.outside,
|
|
@@ -768,11 +797,15 @@ function NavButton({ dir, disabled, onPress }: { dir: 1 | -1; disabled: boolean;
|
|
|
768
797
|
);
|
|
769
798
|
}
|
|
770
799
|
|
|
771
|
-
function Chevron({ turned, color }: { turned: boolean; color: string }) {
|
|
800
|
+
function Chevron({ turned, color, reduce }: { turned: boolean; color: string; reduce: boolean }) {
|
|
772
801
|
const turn = useRef(new Animated.Value(turned ? 1 : 0)).current; // native
|
|
773
802
|
useEffect(() => {
|
|
803
|
+
if (reduce) {
|
|
804
|
+
turn.setValue(turned ? 1 : 0);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
774
807
|
Animated.spring(turn, { toValue: turned ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
775
|
-
}, [turned, turn]);
|
|
808
|
+
}, [turned, turn, reduce]);
|
|
776
809
|
return (
|
|
777
810
|
<Animated.View style={{ transform: [{ rotate: turn.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '180deg'] }) }] }}>
|
|
778
811
|
<Svg width={16} height={16} viewBox="0 0 24 24" fill="none">
|
|
@@ -782,6 +815,22 @@ function Chevron({ turned, color }: { turned: boolean; color: string }) {
|
|
|
782
815
|
);
|
|
783
816
|
}
|
|
784
817
|
|
|
818
|
+
function useReduceMotion() {
|
|
819
|
+
const [reduce, setReduce] = useState(false);
|
|
820
|
+
useEffect(() => {
|
|
821
|
+
let alive = true;
|
|
822
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
823
|
+
.then((r) => alive && setReduce(r))
|
|
824
|
+
.catch(() => {});
|
|
825
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
826
|
+
return () => {
|
|
827
|
+
alive = false;
|
|
828
|
+
sub.remove();
|
|
829
|
+
};
|
|
830
|
+
}, []);
|
|
831
|
+
return reduce;
|
|
832
|
+
}
|
|
833
|
+
|
|
785
834
|
const styles = StyleSheet.create({
|
|
786
835
|
root: { alignSelf: 'stretch' },
|
|
787
836
|
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10, minHeight: 40 },
|
|
@@ -789,7 +838,7 @@ const styles = StyleSheet.create({
|
|
|
789
838
|
title: { fontSize: 17, fontWeight: '400', letterSpacing: -0.3, flexShrink: 1 },
|
|
790
839
|
navs: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
|
791
840
|
nav: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center' },
|
|
792
|
-
todayBtn: {
|
|
841
|
+
todayBtn: { minHeight: 32, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center' },
|
|
793
842
|
todayText: { fontSize: 13, fontWeight: '400' },
|
|
794
843
|
weekRow: { flexDirection: 'row', marginBottom: 6 },
|
|
795
844
|
weekday: { flex: 1, textAlign: 'center', fontSize: 13, fontWeight: '400', letterSpacing: 0.2 },
|
|
@@ -817,6 +866,6 @@ const styles = StyleSheet.create({
|
|
|
817
866
|
dot: { width: 5, height: 5, borderRadius: 2.5 },
|
|
818
867
|
pickGrid: { flexDirection: 'row', flexWrap: 'wrap', alignContent: 'center', paddingTop: 4 },
|
|
819
868
|
pickCell: { width: '33.333%', height: 56, alignItems: 'center', justifyContent: 'center' },
|
|
820
|
-
pickInner: { minWidth: 76,
|
|
869
|
+
pickInner: { minWidth: 76, minHeight: 40, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center', borderWidth: 1.5 },
|
|
821
870
|
pickText: { fontSize: 15, fontWeight: '400' },
|
|
822
871
|
});
|
|
@@ -181,14 +181,17 @@ export function useKit(): Theme {
|
|
|
181
181
|
return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Shared motion presets so the whole kit moves the same way. Every spring is critically damped
|
|
186
|
+
* (damping ≥ 2·√(stiffness·mass)): it settles smoothly with no overshoot or wobble.
|
|
187
|
+
*/
|
|
185
188
|
export const motion = {
|
|
186
189
|
/** Toggles, selection indicators. */
|
|
187
|
-
snappy: { damping:
|
|
188
|
-
/** Press feedback, pops. */
|
|
189
|
-
bouncy: { damping:
|
|
190
|
+
snappy: { damping: 30, stiffness: 260, mass: 0.8 },
|
|
191
|
+
/** Press feedback, pops. (Kept under its old name; it no longer bounces.) */
|
|
192
|
+
bouncy: { damping: 26, stiffness: 220, mass: 0.7 },
|
|
190
193
|
/** Sheets, layout changes. */
|
|
191
|
-
gentle: { damping:
|
|
194
|
+
gentle: { damping: 27, stiffness: 160, mass: 1 },
|
|
192
195
|
fast: 160,
|
|
193
196
|
normal: 240,
|
|
194
197
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { createContext, useContext, useId, useRef, useState } from 'react';
|
|
1
|
+
import React, { createContext, useContext, useEffect, useId, useRef, useState, type ComponentRef } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
AccessibilityInfo,
|
|
3
4
|
Animated,
|
|
4
5
|
Image,
|
|
5
6
|
Platform,
|
|
@@ -67,6 +68,7 @@ export function Card({
|
|
|
67
68
|
const interactive = !!(onPress || onLongPress || pressable);
|
|
68
69
|
const corner = radius.scale(18);
|
|
69
70
|
const gid = 'card' + useId().replace(/[^a-zA-Z0-9]/g, '');
|
|
71
|
+
const reduce = useReduceMotion();
|
|
70
72
|
|
|
71
73
|
const scale = useRef(new Animated.Value(1)).current;
|
|
72
74
|
const glow = useRef(new Animated.Value(0)).current;
|
|
@@ -87,18 +89,20 @@ export function Card({
|
|
|
87
89
|
|
|
88
90
|
const border = (surfaces[variant].borderWidth as number | undefined) ?? 0;
|
|
89
91
|
|
|
92
|
+
// Reduce Motion: no press squish and the spotlight doesn't grow; the glow still fades in and out.
|
|
90
93
|
const pressIn = (e: GestureResponderEvent) => {
|
|
91
|
-
Animated.spring(scale, { toValue: 0.975, useNativeDriver: true, ...motion.snappy }).start();
|
|
94
|
+
if (!reduce) Animated.spring(scale, { toValue: 0.975, useNativeDriver: true, ...motion.snappy }).start();
|
|
92
95
|
if (highlight === 'none') return;
|
|
93
96
|
moveSpot(e);
|
|
94
97
|
Animated.spring(glow, { toValue: 1, useNativeDriver: true, ...motion.gentle }).start();
|
|
95
98
|
};
|
|
96
99
|
const pressOut = () => {
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
if (reduce) scale.setValue(1);
|
|
101
|
+
else Animated.spring(scale, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
|
|
102
|
+
if (highlight !== 'none') Animated.timing(glow, { toValue: 0, duration: reduce ? 150 : 420, useNativeDriver: true }).start();
|
|
99
103
|
};
|
|
100
104
|
// Touch coordinates are relative to whichever child was hit, so work from page coordinates.
|
|
101
|
-
const clipRef = useRef<View
|
|
105
|
+
const clipRef = useRef<ComponentRef<typeof View>>(null);
|
|
102
106
|
const origin = useRef({ x: 0, y: 0 });
|
|
103
107
|
const placeSpot = (pageX: number, pageY: number) =>
|
|
104
108
|
spot.setValue({ x: pageX - origin.current.x - spotD / 2, y: pageY - origin.current.y - spotD / 2 });
|
|
@@ -125,7 +129,7 @@ export function Card({
|
|
|
125
129
|
transform: [
|
|
126
130
|
{ translateX: spot.x },
|
|
127
131
|
{ translateY: spot.y },
|
|
128
|
-
{ scale: glow.interpolate({ inputRange: [0, 1], outputRange: [0.4, 1] }) },
|
|
132
|
+
{ scale: glow.interpolate({ inputRange: [0, 1], outputRange: reduce ? [1, 1] : [0.4, 1] }) },
|
|
129
133
|
],
|
|
130
134
|
},
|
|
131
135
|
]}
|
|
@@ -215,18 +219,20 @@ export interface CardTextProps {
|
|
|
215
219
|
}
|
|
216
220
|
|
|
217
221
|
export function CardTitle({ children, numberOfLines, style }: CardTextProps) {
|
|
218
|
-
const { colors } = useKit();
|
|
222
|
+
const { colors, font } = useKit();
|
|
223
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
219
224
|
return (
|
|
220
|
-
<Text accessibilityRole="header" numberOfLines={numberOfLines} style={[styles.title, { color: colors.cardForeground }, style]}>
|
|
225
|
+
<Text accessibilityRole="header" numberOfLines={numberOfLines} style={[styles.title, ff, { color: colors.cardForeground }, style]}>
|
|
221
226
|
{children}
|
|
222
227
|
</Text>
|
|
223
228
|
);
|
|
224
229
|
}
|
|
225
230
|
|
|
226
231
|
export function CardDescription({ children, numberOfLines, style }: CardTextProps) {
|
|
227
|
-
const { colors } = useKit();
|
|
232
|
+
const { colors, font } = useKit();
|
|
233
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
228
234
|
return (
|
|
229
|
-
<Text numberOfLines={numberOfLines} style={[styles.description, { color: colors.mutedForeground }, style]}>
|
|
235
|
+
<Text numberOfLines={numberOfLines} style={[styles.description, ff, { color: colors.mutedForeground }, style]}>
|
|
230
236
|
{children}
|
|
231
237
|
</Text>
|
|
232
238
|
);
|
|
@@ -286,6 +292,22 @@ function shadow(opacity: number, y: number, blur: number, elevation = 2): ViewSt
|
|
|
286
292
|
return { shadowColor: '#000', shadowOpacity: opacity, shadowRadius: blur / 2, shadowOffset: { width: 0, height: y }, elevation };
|
|
287
293
|
}
|
|
288
294
|
|
|
295
|
+
function useReduceMotion() {
|
|
296
|
+
const [reduce, setReduce] = useState(false);
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
let alive = true;
|
|
299
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
300
|
+
.then((r) => alive && setReduce(r))
|
|
301
|
+
.catch(() => {});
|
|
302
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
303
|
+
return () => {
|
|
304
|
+
alive = false;
|
|
305
|
+
sub.remove();
|
|
306
|
+
};
|
|
307
|
+
}, []);
|
|
308
|
+
return reduce;
|
|
309
|
+
}
|
|
310
|
+
|
|
289
311
|
const styles = StyleSheet.create({
|
|
290
312
|
clip: { overflow: 'hidden' },
|
|
291
313
|
spot: { position: 'absolute', top: 0, left: 0 },
|
|
@@ -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
|
};
|