@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
|
@@ -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
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fintech-app",
|
|
3
|
+
"type": "template",
|
|
4
|
+
"title": "Business banking app",
|
|
5
|
+
"description": "A complete Expo Router app for a small-business bank account: onboarding once, phone + OTP sign in, a live revenue dashboard, cards you can reveal and freeze, slide-to-pay transfers, profile and settings.",
|
|
6
|
+
"tier": "pro",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"react-native-svg",
|
|
9
|
+
"expo-haptics",
|
|
10
|
+
"expo-router",
|
|
11
|
+
"expo-splash-screen",
|
|
12
|
+
"expo-status-bar",
|
|
13
|
+
"react-native-safe-area-context",
|
|
14
|
+
"react-native-screens",
|
|
15
|
+
"@react-native-async-storage/async-storage"
|
|
16
|
+
],
|
|
17
|
+
"registryDependencies": [
|
|
18
|
+
"kit",
|
|
19
|
+
"bottom-nav",
|
|
20
|
+
"onboarding",
|
|
21
|
+
"sign-in",
|
|
22
|
+
"dashboard",
|
|
23
|
+
"payment-card",
|
|
24
|
+
"switch",
|
|
25
|
+
"avatar",
|
|
26
|
+
"chip-group",
|
|
27
|
+
"slide-to-confirm",
|
|
28
|
+
"profile",
|
|
29
|
+
"settings"
|
|
30
|
+
],
|
|
31
|
+
"sharedFiles": [
|
|
32
|
+
"lib/kit.tsx",
|
|
33
|
+
"components/ui/bottom-nav.tsx",
|
|
34
|
+
"components/blocks/onboarding.tsx",
|
|
35
|
+
"components/ui/button.tsx",
|
|
36
|
+
"components/ui/floating-input.tsx",
|
|
37
|
+
"components/ui/password-input.tsx",
|
|
38
|
+
"components/ui/otp-input.tsx",
|
|
39
|
+
"components/blocks/sign-in.tsx",
|
|
40
|
+
"components/ui/avatar.tsx",
|
|
41
|
+
"components/ui/badge.tsx",
|
|
42
|
+
"components/ui/list-item.tsx",
|
|
43
|
+
"components/ui/number-ticker.tsx",
|
|
44
|
+
"components/ui/segmented-control.tsx",
|
|
45
|
+
"components/ui/skeleton.tsx",
|
|
46
|
+
"components/blocks/dashboard.tsx",
|
|
47
|
+
"components/ui/tilt-card.tsx",
|
|
48
|
+
"components/ui/flip-card.tsx",
|
|
49
|
+
"components/ui/payment-card.tsx",
|
|
50
|
+
"components/ui/switch.tsx",
|
|
51
|
+
"components/ui/chip-group.tsx",
|
|
52
|
+
"components/ui/slide-to-confirm.tsx",
|
|
53
|
+
"components/blocks/profile.tsx",
|
|
54
|
+
"components/blocks/settings.tsx"
|
|
55
|
+
],
|
|
56
|
+
"filesDir": null
|
|
57
|
+
}
|
|
@@ -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 },
|
|
@@ -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, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
1
|
+
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState, type ComponentRef } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
AccessibilityInfo,
|
|
3
4
|
Animated,
|
|
4
5
|
Easing,
|
|
5
6
|
Platform,
|
|
@@ -85,6 +86,9 @@ export const FLOATING_INPUT_SIZES = {
|
|
|
85
86
|
md: { height: 54, font: 16, floated: 12.5, padX: 16, icon: 18, radius: 0 },
|
|
86
87
|
lg: { height: 64, font: 18, floated: 13.5, padX: 18, icon: 20, radius: 4 },
|
|
87
88
|
};
|
|
89
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
90
|
+
// content text scales all the way.
|
|
91
|
+
const FONT_CAP = 1.35;
|
|
88
92
|
const HALO = 4;
|
|
89
93
|
/** Font size of the static label used when `floatingLabel` is false. */
|
|
90
94
|
const LABEL_ABOVE_FONT = { sm: 13, md: 14, lg: 14.5 };
|
|
@@ -99,7 +103,7 @@ const NO_WEB_OUTLINE = Platform.OS === 'web' ? ({ outlineStyle: 'none', outlineW
|
|
|
99
103
|
const AUTOFILL_CSS = `@keyframes ekAutofillOn{from{}to{}}@keyframes ekAutofillOff{from{}to{}}
|
|
100
104
|
input:-webkit-autofill,input:-webkit-autofill:hover,input:-webkit-autofill:focus{animation:ekAutofillOn 1ms;-webkit-text-fill-color:currentColor!important;caret-color:currentColor;transition:background-color 99999s 0s,color 99999s 0s!important}
|
|
101
105
|
input:not(:-webkit-autofill){animation:ekAutofillOff 1ms}`;
|
|
102
|
-
function useWebAutofill(inputRef: React.RefObject<TextInput | null>, onChange: (filled: boolean) => void) {
|
|
106
|
+
function useWebAutofill(inputRef: React.RefObject<ComponentRef<typeof TextInput> | null>, onChange: (filled: boolean) => void) {
|
|
103
107
|
useEffect(() => {
|
|
104
108
|
if (Platform.OS !== 'web' || typeof document === 'undefined') return;
|
|
105
109
|
if (!document.getElementById('expokit-autofill')) {
|
|
@@ -126,7 +130,7 @@ function useWebAutofill(inputRef: React.RefObject<TextInput | null>, onChange: (
|
|
|
126
130
|
* cutting a notch in the outline (Material-style). Focus draws a soft halo, errors shake,
|
|
127
131
|
* success pops a check — all spring-driven.
|
|
128
132
|
*/
|
|
129
|
-
export const FloatingInput = forwardRef<TextInput
|
|
133
|
+
export const FloatingInput = forwardRef<ComponentRef<typeof TextInput>, FloatingInputProps>(function FloatingInput(
|
|
130
134
|
{
|
|
131
135
|
label,
|
|
132
136
|
floatingLabel = true,
|
|
@@ -166,10 +170,11 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
166
170
|
ref
|
|
167
171
|
) {
|
|
168
172
|
const { colors, radius, font } = useKit();
|
|
173
|
+
const reduce = useReduceMotion();
|
|
169
174
|
const ff = font.family ? { fontFamily: font.family } : null;
|
|
170
175
|
const S = FLOATING_INPUT_SIZES[size];
|
|
171
176
|
// The user's border colour only replaces the resting one; numeric height / radius drive the layout maths.
|
|
172
|
-
const { borderColor: userBorder, ...userField } =
|
|
177
|
+
const { borderColor: userBorder, ...userField } = flattenView(fieldStyle);
|
|
173
178
|
const userLabelColor = StyleSheet.flatten(labelStyleProp)?.color;
|
|
174
179
|
const HEIGHT = typeof userField.height === 'number' ? userField.height : S.height;
|
|
175
180
|
// Floated size stays relative to the resting size, so a bigger label floats bigger at the same ratio.
|
|
@@ -179,8 +184,8 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
179
184
|
const slot = S.icon + 10; // room for the clear button / check
|
|
180
185
|
const tint = accent ?? colors.accent;
|
|
181
186
|
const bg = surface ?? colors.background;
|
|
182
|
-
const inputRef = useRef<TextInput
|
|
183
|
-
useImperativeHandle(ref, () => inputRef.current as TextInput);
|
|
187
|
+
const inputRef = useRef<ComponentRef<typeof TextInput>>(null);
|
|
188
|
+
useImperativeHandle(ref, () => inputRef.current as ComponentRef<typeof TextInput>);
|
|
184
189
|
|
|
185
190
|
const [focused, setFocused] = useState(false);
|
|
186
191
|
const [inner, setInner] = useState(defaultValue ?? '');
|
|
@@ -200,29 +205,36 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
200
205
|
const check = useRef(new Animated.Value(ok ? 1 : 0)).current; // native
|
|
201
206
|
|
|
202
207
|
useEffect(() => {
|
|
203
|
-
|
|
204
|
-
|
|
208
|
+
if (reduce) float.setValue(floating ? 1 : 0);
|
|
209
|
+
else Animated.spring(float, { toValue: floating ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
210
|
+
}, [floating, float, reduce]);
|
|
205
211
|
|
|
206
212
|
useEffect(() => {
|
|
207
213
|
Animated.timing(focus, { toValue: isFocused ? 1 : 0, duration: 180, easing: Easing.out(Easing.quad), useNativeDriver: false }).start();
|
|
208
|
-
|
|
209
|
-
|
|
214
|
+
// Reduce Motion: halo and indicator bar only fade (their scales are pinned below).
|
|
215
|
+
if (reduce) Animated.timing(glow, { toValue: isFocused ? 1 : 0, duration: 150, useNativeDriver: true }).start();
|
|
216
|
+
else Animated.spring(glow, { toValue: isFocused ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
|
|
217
|
+
}, [isFocused, focus, glow, reduce]);
|
|
210
218
|
|
|
211
219
|
useEffect(() => {
|
|
212
|
-
|
|
213
|
-
|
|
220
|
+
const to = clearable && text && editable ? 1 : 0;
|
|
221
|
+
if (reduce) Animated.timing(clear, { toValue: to, duration: 150, useNativeDriver: true }).start();
|
|
222
|
+
else Animated.spring(clear, { toValue: to, useNativeDriver: true, ...motion.bouncy }).start();
|
|
223
|
+
}, [clearable, text, editable, clear, reduce]);
|
|
214
224
|
|
|
215
225
|
useEffect(() => {
|
|
216
|
-
Animated.
|
|
217
|
-
|
|
226
|
+
if (reduce) Animated.timing(check, { toValue: ok ? 1 : 0, duration: 150, useNativeDriver: true }).start();
|
|
227
|
+
else Animated.spring(check, { toValue: ok ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
|
|
228
|
+
}, [ok, check, reduce]);
|
|
218
229
|
|
|
219
230
|
useEffect(() => {
|
|
220
|
-
|
|
231
|
+
// Reduce Motion: no shake; the red border and message still flag the error.
|
|
232
|
+
if (!error || reduce) return;
|
|
221
233
|
shake.setValue(0);
|
|
222
234
|
Animated.sequence(
|
|
223
235
|
[10, -8, 6, -4, 0].map((x) => Animated.timing(shake, { toValue: x, duration: 55, useNativeDriver: true }))
|
|
224
236
|
).start();
|
|
225
|
-
}, [error, shake]);
|
|
237
|
+
}, [error, shake, reduce]);
|
|
226
238
|
|
|
227
239
|
const setText = (next: string) => {
|
|
228
240
|
if (value === undefined) setInner(next);
|
|
@@ -310,23 +322,24 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
310
322
|
borderRadius: corner + HALO,
|
|
311
323
|
backgroundColor: active,
|
|
312
324
|
opacity: glow.interpolate({ inputRange: [0, 1], outputRange: [0, 0.18], extrapolate: 'clamp' }),
|
|
313
|
-
transform: [{ scale: glow.interpolate({ inputRange: [0, 1], outputRange: [0.97, 1] }) }],
|
|
325
|
+
transform: [{ scale: glow.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.97, 1] }) }],
|
|
314
326
|
},
|
|
315
327
|
]}
|
|
316
328
|
/>
|
|
317
329
|
)}
|
|
318
330
|
<Pressable onPress={() => inputRef.current?.focus()} disabled={!editable} accessible={false}>
|
|
319
|
-
<Animated.View style={[styles.field, boxStyle, {
|
|
331
|
+
<Animated.View style={[styles.field, boxStyle, { minHeight: HEIGHT }, userField, { borderColor }]}>
|
|
320
332
|
{left ? <View style={[styles.left, { left: underline ? 2 : S.padX - 2 }, underline && { top: contentTop }]}>{left}</View> : null}
|
|
321
333
|
<View style={[styles.row, { paddingLeft: leftPad, paddingRight: rightPad, paddingTop: contentTop }]}>
|
|
322
334
|
{prefix ? (
|
|
323
|
-
<Animated.Text style={[styles.affix, styles.prefix, ff, { fontSize: S.font }, { color: colors.mutedForeground }, prefixStyle, { opacity: affixOpacity }]}>
|
|
335
|
+
<Animated.Text maxFontSizeMultiplier={FONT_CAP} style={[styles.affix, styles.prefix, ff, { fontSize: S.font }, { color: colors.mutedForeground }, prefixStyle, { opacity: affixOpacity }]}>
|
|
324
336
|
{prefix}
|
|
325
337
|
</Animated.Text>
|
|
326
338
|
) : null}
|
|
327
339
|
<TextInput
|
|
328
340
|
ref={inputRef}
|
|
329
341
|
accessibilityLabel={label}
|
|
342
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
330
343
|
{...props}
|
|
331
344
|
maxLength={maxLength}
|
|
332
345
|
editable={editable}
|
|
@@ -347,7 +360,7 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
347
360
|
style={[styles.input, ff, { color: colors.foreground, fontSize: S.font }, inputStyle, NO_WEB_OUTLINE]}
|
|
348
361
|
/>
|
|
349
362
|
{suffix ? (
|
|
350
|
-
<Animated.Text style={[styles.affix, styles.suffix, ff, { fontSize: S.font }, { color: colors.mutedForeground }, suffixStyle, { opacity: affixOpacity }]}>
|
|
363
|
+
<Animated.Text maxFontSizeMultiplier={FONT_CAP} style={[styles.affix, styles.suffix, ff, { fontSize: S.font }, { color: colors.mutedForeground }, suffixStyle, { opacity: affixOpacity }]}>
|
|
351
364
|
{suffix}
|
|
352
365
|
</Animated.Text>
|
|
353
366
|
) : null}
|
|
@@ -358,6 +371,7 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
358
371
|
style={[styles.labelWrap, { left: leftPad, right: right ? adorn : S.padX - 4, top: contentTop, height: HEIGHT - contentTop }, labelStyle]}
|
|
359
372
|
>
|
|
360
373
|
<Animated.Text
|
|
374
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
361
375
|
numberOfLines={1}
|
|
362
376
|
style={[
|
|
363
377
|
styles.label,
|
|
@@ -380,7 +394,7 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
380
394
|
) : null}
|
|
381
395
|
{clearable ? (
|
|
382
396
|
<Animated.View
|
|
383
|
-
style={[styles.slot, { right: edge, top: contentTop, opacity: clear, transform: [{ scale: clear }] }]}
|
|
397
|
+
style={[styles.slot, { right: edge, top: contentTop, opacity: clear, transform: [{ scale: reduce ? 1 : clear }] }]}
|
|
384
398
|
pointerEvents={text && editable ? 'box-none' : 'none'}
|
|
385
399
|
>
|
|
386
400
|
<Pressable
|
|
@@ -408,8 +422,8 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
408
422
|
top: contentTop,
|
|
409
423
|
opacity: check.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
410
424
|
transform: [
|
|
411
|
-
{ scale: check.interpolate({ inputRange: [0, 1], outputRange: [0.3, 1] }) },
|
|
412
|
-
{ rotate: check.interpolate({ inputRange: [0, 1], outputRange: ['-30deg', '0deg'] }) },
|
|
425
|
+
{ scale: check.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.3, 1] }) },
|
|
426
|
+
{ rotate: check.interpolate({ inputRange: [0, 1], outputRange: [reduce ? '0deg' : '-30deg', '0deg'] }) },
|
|
413
427
|
],
|
|
414
428
|
},
|
|
415
429
|
]}
|
|
@@ -423,10 +437,14 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
423
437
|
{outlined ? null : (
|
|
424
438
|
<>
|
|
425
439
|
<View pointerEvents="none" style={[styles.baseLine, { height: underline ? 1.5 : 1, backgroundColor: rest }]} />
|
|
426
|
-
{/* Grows out from the centre on focus. */}
|
|
440
|
+
{/* Grows out from the centre on focus (fades in with Reduce Motion). */}
|
|
427
441
|
<Animated.View
|
|
428
442
|
pointerEvents="none"
|
|
429
|
-
style={[
|
|
443
|
+
style={[
|
|
444
|
+
styles.bar,
|
|
445
|
+
{ backgroundColor: active },
|
|
446
|
+
reduce ? { opacity: glowClamped } : { transform: [{ scaleX: glowClamped }] },
|
|
447
|
+
]}
|
|
430
448
|
/>
|
|
431
449
|
</>
|
|
432
450
|
)}
|
|
@@ -436,10 +454,11 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
436
454
|
{message || showCounter ? (
|
|
437
455
|
<View style={[styles.footer, underline && styles.footerFlush]}>
|
|
438
456
|
<View style={styles.messageSlot}>
|
|
439
|
-
{message ? <FieldMessage key={tone + message} text={message} tone={tone} color={toneColor} style={messageStyle} /> : null}
|
|
457
|
+
{message ? <FieldMessage key={tone + message} text={message} tone={tone} color={toneColor} style={messageStyle} reduce={reduce} /> : null}
|
|
440
458
|
</View>
|
|
441
459
|
{showCounter ? (
|
|
442
460
|
<Text
|
|
461
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
443
462
|
style={[
|
|
444
463
|
styles.count,
|
|
445
464
|
ff,
|
|
@@ -459,13 +478,27 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
|
|
|
459
478
|
|
|
460
479
|
type MessageTone = 'error' | 'success' | 'helper';
|
|
461
480
|
|
|
462
|
-
/** Helper / error / success line. Error and success slide up and fade in. */
|
|
463
|
-
function FieldMessage({
|
|
481
|
+
/** Helper / error / success line. Error and success slide up and fade in (only fade with Reduce Motion). */
|
|
482
|
+
function FieldMessage({
|
|
483
|
+
text,
|
|
484
|
+
tone,
|
|
485
|
+
color,
|
|
486
|
+
style,
|
|
487
|
+
reduce,
|
|
488
|
+
}: {
|
|
489
|
+
text: string;
|
|
490
|
+
tone: MessageTone;
|
|
491
|
+
color: string;
|
|
492
|
+
style?: StyleProp<TextStyle>;
|
|
493
|
+
reduce: boolean;
|
|
494
|
+
}) {
|
|
464
495
|
const { font } = useKit();
|
|
465
496
|
const enter = useRef(new Animated.Value(tone === 'helper' ? 1 : 0)).current; // native
|
|
466
497
|
useEffect(() => {
|
|
467
|
-
if (tone
|
|
468
|
-
|
|
498
|
+
if (tone === 'helper') return;
|
|
499
|
+
if (reduce) Animated.timing(enter, { toValue: 1, duration: 150, useNativeDriver: true }).start();
|
|
500
|
+
else Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
501
|
+
}, [tone, enter, reduce]);
|
|
469
502
|
|
|
470
503
|
return (
|
|
471
504
|
<Animated.View
|
|
@@ -473,7 +506,7 @@ function FieldMessage({ text, tone, color, style }: { text: string; tone: Messag
|
|
|
473
506
|
styles.message,
|
|
474
507
|
{
|
|
475
508
|
opacity: enter.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
476
|
-
transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [4, 0] }) }],
|
|
509
|
+
transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 4, 0] }) }],
|
|
477
510
|
},
|
|
478
511
|
]}
|
|
479
512
|
>
|
|
@@ -500,11 +533,37 @@ function FieldMessage({ text, tone, color, style }: { text: string; tone: Messag
|
|
|
500
533
|
);
|
|
501
534
|
}
|
|
502
535
|
|
|
536
|
+
/**
|
|
537
|
+
* The user's field style as one object. Not StyleSheet.flatten: with TypeScript 6 its RN 0.88 signature
|
|
538
|
+
* rejects a plain StyleProp<ViewStyle> once expo/types adds web-only keys to ViewStyle.
|
|
539
|
+
*/
|
|
540
|
+
function flattenView(style: StyleProp<ViewStyle>): ViewStyle {
|
|
541
|
+
if (!style || typeof style !== 'object') return {};
|
|
542
|
+
if (Array.isArray(style)) return Object.assign({}, ...style.map((s) => flattenView(s as StyleProp<ViewStyle>)));
|
|
543
|
+
return style as ViewStyle;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function useReduceMotion() {
|
|
547
|
+
const [reduce, setReduce] = useState(false);
|
|
548
|
+
useEffect(() => {
|
|
549
|
+
let alive = true;
|
|
550
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
551
|
+
.then((r) => alive && setReduce(r))
|
|
552
|
+
.catch(() => {});
|
|
553
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
554
|
+
return () => {
|
|
555
|
+
alive = false;
|
|
556
|
+
sub.remove();
|
|
557
|
+
};
|
|
558
|
+
}, []);
|
|
559
|
+
return reduce;
|
|
560
|
+
}
|
|
561
|
+
|
|
503
562
|
const styles = StyleSheet.create({
|
|
504
563
|
disabled: { opacity: 0.5 },
|
|
505
564
|
halo: { position: 'absolute', top: -HALO, left: -HALO, right: -HALO, bottom: -HALO },
|
|
506
565
|
field: { justifyContent: 'center' },
|
|
507
|
-
row: { flexDirection: 'row', alignItems: 'center',
|
|
566
|
+
row: { flexDirection: 'row', alignItems: 'center', flexGrow: 1 },
|
|
508
567
|
input: { flex: 1, alignSelf: 'stretch', paddingVertical: 0, paddingHorizontal: 0 },
|
|
509
568
|
affix: { fontWeight: '400' },
|
|
510
569
|
prefix: { marginRight: 4 },
|
|
@@ -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
|
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "food-app",
|
|
3
|
+
"type": "template",
|
|
4
|
+
"title": "Food ordering app",
|
|
5
|
+
"description": "A complete Expo Router app for a donut shop: onboarding once, sign in, a rolling menu, coupons, bag with checkout, profile and settings with a working light / dark switch.",
|
|
6
|
+
"tier": "pro",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"react-native-svg",
|
|
9
|
+
"expo-haptics",
|
|
10
|
+
"expo-router",
|
|
11
|
+
"expo-splash-screen",
|
|
12
|
+
"expo-status-bar",
|
|
13
|
+
"react-native-safe-area-context",
|
|
14
|
+
"react-native-screens",
|
|
15
|
+
"@react-native-async-storage/async-storage"
|
|
16
|
+
],
|
|
17
|
+
"registryDependencies": [
|
|
18
|
+
"kit",
|
|
19
|
+
"bottom-nav",
|
|
20
|
+
"onboarding",
|
|
21
|
+
"sign-in",
|
|
22
|
+
"donut-shop",
|
|
23
|
+
"offers",
|
|
24
|
+
"checkout",
|
|
25
|
+
"profile",
|
|
26
|
+
"settings"
|
|
27
|
+
],
|
|
28
|
+
"sharedFiles": [
|
|
29
|
+
"lib/kit.tsx",
|
|
30
|
+
"components/ui/bottom-nav.tsx",
|
|
31
|
+
"components/blocks/onboarding.tsx",
|
|
32
|
+
"components/ui/button.tsx",
|
|
33
|
+
"components/ui/floating-input.tsx",
|
|
34
|
+
"components/ui/password-input.tsx",
|
|
35
|
+
"components/ui/otp-input.tsx",
|
|
36
|
+
"components/blocks/sign-in.tsx",
|
|
37
|
+
"components/blocks/donut-shop.tsx",
|
|
38
|
+
"components/ui/tilt-card.tsx",
|
|
39
|
+
"components/ui/flip-card.tsx",
|
|
40
|
+
"lib/ticket-shape.tsx",
|
|
41
|
+
"components/ui/coupon.tsx",
|
|
42
|
+
"components/ui/confetti.tsx",
|
|
43
|
+
"components/blocks/offers.tsx",
|
|
44
|
+
"components/ui/slide-to-confirm.tsx",
|
|
45
|
+
"components/ui/stepper.tsx",
|
|
46
|
+
"components/blocks/checkout.tsx",
|
|
47
|
+
"components/blocks/profile.tsx",
|
|
48
|
+
"components/ui/avatar.tsx",
|
|
49
|
+
"components/ui/badge.tsx",
|
|
50
|
+
"components/ui/segmented-control.tsx",
|
|
51
|
+
"components/ui/switch.tsx",
|
|
52
|
+
"components/blocks/settings.tsx"
|
|
53
|
+
],
|
|
54
|
+
"filesDir": null
|
|
55
|
+
}
|