@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,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,
|
|
@@ -59,6 +60,10 @@ export interface SearchBarProps extends Omit<TextInputProps, 'style'> {
|
|
|
59
60
|
style?: StyleProp<ViewStyle>;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
64
|
+
// content text scales all the way.
|
|
65
|
+
const FONT_CAP = 1.35;
|
|
66
|
+
|
|
62
67
|
/** Every dimension comes from one table so the sizes read clearly different. */
|
|
63
68
|
const SIZES: Record<SearchBarSize, { h: number; font: number; icon: number; slot: number; r: number; clear: number; end: number }> = {
|
|
64
69
|
sm: { h: 34, font: 14, icon: 14, slot: 30, r: 9, clear: 16, end: 6 },
|
|
@@ -75,7 +80,7 @@ const webNoOutline = Platform.OS === 'web' ? ({ outlineStyle: 'none', outlineWid
|
|
|
75
80
|
* panel drops down while focused. The 'minimal' variant rests as a round icon and expands
|
|
76
81
|
* into a full field when tapped.
|
|
77
82
|
*/
|
|
78
|
-
export const SearchBar = forwardRef<TextInput
|
|
83
|
+
export const SearchBar = forwardRef<ComponentRef<typeof TextInput>, SearchBarProps>(function SearchBar(
|
|
79
84
|
{
|
|
80
85
|
variant = 'default',
|
|
81
86
|
size = 'md',
|
|
@@ -104,14 +109,16 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
|
|
|
104
109
|
},
|
|
105
110
|
ref
|
|
106
111
|
) {
|
|
107
|
-
const { colors, radius } = useKit();
|
|
112
|
+
const { colors, radius, font } = useKit();
|
|
113
|
+
const reduce = useReduceMotion();
|
|
114
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
108
115
|
const s = SIZES[size];
|
|
109
116
|
const tint = accent ?? colors.accent;
|
|
110
117
|
const minimal = variant === 'minimal';
|
|
111
118
|
const cancelEnabled = !minimal && (showCancel ?? true);
|
|
112
119
|
|
|
113
|
-
const inputRef = useRef<TextInput
|
|
114
|
-
useImperativeHandle(ref, () => inputRef.current as TextInput);
|
|
120
|
+
const inputRef = useRef<ComponentRef<typeof TextInput>>(null);
|
|
121
|
+
useImperativeHandle(ref, () => inputRef.current as ComponentRef<typeof TextInput>);
|
|
115
122
|
|
|
116
123
|
const [focused, setFocused] = useState(false);
|
|
117
124
|
const [inner, setInner] = useState(defaultValue ?? '');
|
|
@@ -139,28 +146,47 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
|
|
|
139
146
|
useEffect(() => {
|
|
140
147
|
const to = focused ? 1 : 0;
|
|
141
148
|
Animated.timing(focusJS, { toValue: to, duration: motion.fast, easing: Easing.out(Easing.quad), useNativeDriver: false }).start();
|
|
149
|
+
// Reduce Motion: the halo only fades (its scale is pinned below) and the magnifier doesn't pop.
|
|
150
|
+
if (reduce) {
|
|
151
|
+
Animated.timing(halo, { toValue: to, duration: 150, useNativeDriver: true }).start();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
142
154
|
Animated.spring(halo, { toValue: to, useNativeDriver: true, ...motion.snappy }).start();
|
|
143
155
|
if (focused) {
|
|
144
156
|
icon.setValue(0.6);
|
|
145
157
|
Animated.spring(icon, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
|
|
146
158
|
}
|
|
147
|
-
}, [focused, focusJS, halo, icon]);
|
|
159
|
+
}, [focused, focusJS, halo, icon, reduce]);
|
|
148
160
|
|
|
149
161
|
useEffect(() => {
|
|
150
162
|
const to = cancelShown ? 1 : 0;
|
|
163
|
+
// Reduce Motion: the field narrows at once and Cancel fades in place.
|
|
164
|
+
if (reduce) {
|
|
165
|
+
cancelJS.setValue(to);
|
|
166
|
+
Animated.timing(cancelN, { toValue: to, duration: 150, useNativeDriver: true }).start();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
151
169
|
Animated.spring(cancelJS, { toValue: to, useNativeDriver: false, ...motion.snappy }).start();
|
|
152
170
|
Animated.spring(cancelN, { toValue: to, useNativeDriver: true, ...motion.snappy }).start();
|
|
153
|
-
}, [cancelShown, cancelJS, cancelN]);
|
|
171
|
+
}, [cancelShown, cancelJS, cancelN, reduce]);
|
|
154
172
|
|
|
155
173
|
useEffect(() => {
|
|
156
174
|
const to = open ? 1 : 0;
|
|
175
|
+
// Reduce Motion: 'minimal' snaps to its new width; the contents still fade.
|
|
176
|
+
if (reduce) {
|
|
177
|
+
openJS.setValue(to);
|
|
178
|
+
Animated.timing(openN, { toValue: to, duration: 150, useNativeDriver: true }).start();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
157
181
|
Animated.spring(openJS, { toValue: to, useNativeDriver: false, ...motion.gentle }).start();
|
|
158
182
|
Animated.timing(openN, { toValue: to, duration: open ? motion.normal : motion.fast, delay: open ? 80 : 0, useNativeDriver: true }).start();
|
|
159
|
-
}, [open, openJS, openN]);
|
|
183
|
+
}, [open, openJS, openN, reduce]);
|
|
160
184
|
|
|
161
185
|
useEffect(() => {
|
|
162
|
-
|
|
163
|
-
|
|
186
|
+
const to = text && editable && open ? 1 : 0;
|
|
187
|
+
if (reduce) Animated.timing(clear, { toValue: to, duration: 150, useNativeDriver: true }).start();
|
|
188
|
+
else Animated.spring(clear, { toValue: to, useNativeDriver: true, ...motion.bouncy }).start();
|
|
189
|
+
}, [text, editable, open, clear, reduce]);
|
|
164
190
|
|
|
165
191
|
useEffect(() => {
|
|
166
192
|
Animated.timing(busy, { toValue: loading ? 1 : 0, duration: motion.fast, useNativeDriver: true }).start();
|
|
@@ -229,7 +255,7 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
|
|
|
229
255
|
onLayout={(e) => setRowW(e.nativeEvent.layout.width)}
|
|
230
256
|
style={[styles.row, minimal && { justifyContent: align === 'right' ? 'flex-end' : 'flex-start' }]}
|
|
231
257
|
>
|
|
232
|
-
<Animated.View style={minimal ? { width: fieldW,
|
|
258
|
+
<Animated.View style={minimal ? { width: fieldW, minHeight: s.h } : [styles.flex, { minHeight: s.h }]}>
|
|
233
259
|
<Animated.View
|
|
234
260
|
pointerEvents="none"
|
|
235
261
|
style={[
|
|
@@ -238,7 +264,7 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
|
|
|
238
264
|
borderRadius: r + 4,
|
|
239
265
|
backgroundColor: alpha(tint, 0.18),
|
|
240
266
|
opacity: halo,
|
|
241
|
-
transform: [{ scale: halo.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) }],
|
|
267
|
+
transform: [{ scale: halo.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.96, 1] }) }],
|
|
242
268
|
},
|
|
243
269
|
]}
|
|
244
270
|
/>
|
|
@@ -275,6 +301,7 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
|
|
|
275
301
|
autoCorrect={false}
|
|
276
302
|
accessibilityRole="search"
|
|
277
303
|
accessibilityLabel={props.accessibilityLabel ?? placeholder}
|
|
304
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
278
305
|
{...props}
|
|
279
306
|
autoFocus={autoFocus}
|
|
280
307
|
editable={editable}
|
|
@@ -293,12 +320,12 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
|
|
|
293
320
|
if (minimal && !text) setExpanded(false);
|
|
294
321
|
onBlur?.(e);
|
|
295
322
|
}}
|
|
296
|
-
style={[styles.input, { color: colors.foreground, fontSize: s.font,
|
|
323
|
+
style={[styles.input, ff, { color: colors.foreground, fontSize: s.font, minHeight: s.h - 3 }, webNoOutline]}
|
|
297
324
|
/>
|
|
298
325
|
</Animated.View>
|
|
299
326
|
<Animated.View
|
|
300
327
|
pointerEvents={text && open ? 'auto' : 'none'}
|
|
301
|
-
style={[styles.clear, { width: s.clear + 8, opacity: clear, transform: [{ scale: clear }] }]}
|
|
328
|
+
style={[styles.clear, { width: s.clear + 8, opacity: clear, transform: [{ scale: reduce ? 1 : clear }] }]}
|
|
302
329
|
>
|
|
303
330
|
<Pressable
|
|
304
331
|
hitSlop={12}
|
|
@@ -341,9 +368,10 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
|
|
|
341
368
|
{cancelEnabled ? (
|
|
342
369
|
<>
|
|
343
370
|
<Text
|
|
371
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
344
372
|
pointerEvents="none"
|
|
345
373
|
onLayout={(e) => setCancelW(Math.ceil(e.nativeEvent.layout.width) + 14)}
|
|
346
|
-
style={[styles.cancelText, styles.measure, { fontSize: s.font }]}
|
|
374
|
+
style={[styles.cancelText, styles.measure, ff, { fontSize: s.font }]}
|
|
347
375
|
>
|
|
348
376
|
{cancelLabel}
|
|
349
377
|
</Text>
|
|
@@ -356,7 +384,7 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
|
|
|
356
384
|
{
|
|
357
385
|
width: cancelW,
|
|
358
386
|
opacity: cancelN,
|
|
359
|
-
transform: [{ translateX: cancelN.interpolate({ inputRange: [0, 1], outputRange: [24, 0] }) }],
|
|
387
|
+
transform: [{ translateX: cancelN.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 24, 0] }) }],
|
|
360
388
|
},
|
|
361
389
|
]}
|
|
362
390
|
>
|
|
@@ -366,7 +394,7 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
|
|
|
366
394
|
accessibilityLabel={cancelLabel}
|
|
367
395
|
onPress={cancel}
|
|
368
396
|
>
|
|
369
|
-
<Text numberOfLines={1} style={[styles.cancelText, { color: tint, fontSize: s.font }]}>
|
|
397
|
+
<Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={[styles.cancelText, ff, { color: tint, fontSize: s.font }]}>
|
|
370
398
|
{cancelLabel}
|
|
371
399
|
</Text>
|
|
372
400
|
</Pressable>
|
|
@@ -377,27 +405,37 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
|
|
|
377
405
|
</View>
|
|
378
406
|
|
|
379
407
|
{suggestions ? (
|
|
380
|
-
<SuggestionPanel
|
|
408
|
+
<SuggestionPanel
|
|
409
|
+
open={panelOpen && open && visible.length > 0}
|
|
410
|
+
items={visible}
|
|
411
|
+
query={query}
|
|
412
|
+
tint={tint}
|
|
413
|
+
reduce={reduce}
|
|
414
|
+
onSelect={select}
|
|
415
|
+
/>
|
|
381
416
|
) : null}
|
|
382
417
|
</View>
|
|
383
418
|
);
|
|
384
419
|
});
|
|
385
420
|
|
|
386
|
-
/** Dropdown list under the bar: drops in, rows show an icon, bolded match and a hint. */
|
|
421
|
+
/** Dropdown list under the bar: drops in (fades with Reduce Motion), rows show an icon, bolded match and a hint. */
|
|
387
422
|
function SuggestionPanel({
|
|
388
423
|
open,
|
|
389
424
|
items,
|
|
390
425
|
query,
|
|
391
426
|
tint,
|
|
427
|
+
reduce,
|
|
392
428
|
onSelect,
|
|
393
429
|
}: {
|
|
394
430
|
open: boolean;
|
|
395
431
|
items: SearchSuggestion[];
|
|
396
432
|
query: string;
|
|
397
433
|
tint: string;
|
|
434
|
+
reduce: boolean;
|
|
398
435
|
onSelect: (item: SearchSuggestion) => void;
|
|
399
436
|
}) {
|
|
400
|
-
const { colors, radius } = useKit();
|
|
437
|
+
const { colors, radius, font } = useKit();
|
|
438
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
401
439
|
const [mounted, setMounted] = useState(open);
|
|
402
440
|
const enter = useRef(new Animated.Value(0)).current;
|
|
403
441
|
const last = useRef(items);
|
|
@@ -406,13 +444,14 @@ function SuggestionPanel({
|
|
|
406
444
|
useEffect(() => {
|
|
407
445
|
if (open) {
|
|
408
446
|
setMounted(true);
|
|
409
|
-
Animated.
|
|
447
|
+
if (reduce) Animated.timing(enter, { toValue: 1, duration: 150, useNativeDriver: true }).start();
|
|
448
|
+
else Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
410
449
|
} else {
|
|
411
450
|
Animated.timing(enter, { toValue: 0, duration: motion.fast, useNativeDriver: true }).start(({ finished }) => {
|
|
412
451
|
if (finished) setMounted(false);
|
|
413
452
|
});
|
|
414
453
|
}
|
|
415
|
-
}, [open, enter]);
|
|
454
|
+
}, [open, enter, reduce]);
|
|
416
455
|
|
|
417
456
|
if (!mounted) return null;
|
|
418
457
|
return (
|
|
@@ -426,8 +465,8 @@ function SuggestionPanel({
|
|
|
426
465
|
borderRadius: radius.lg,
|
|
427
466
|
opacity: enter,
|
|
428
467
|
transform: [
|
|
429
|
-
{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [-6, 0] }) },
|
|
430
|
-
{ scale: enter.interpolate({ inputRange: [0, 1], outputRange: [0.98, 1] }) },
|
|
468
|
+
{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : -6, 0] }) },
|
|
469
|
+
{ scale: enter.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.98, 1] }) },
|
|
431
470
|
],
|
|
432
471
|
},
|
|
433
472
|
]}
|
|
@@ -443,11 +482,11 @@ function SuggestionPanel({
|
|
|
443
482
|
<View style={[styles.optionIcon, { backgroundColor: colors.muted }]}>
|
|
444
483
|
<Icon name={item.icon ?? 'search'} size={15} color={item.icon === 'trending' ? tint : colors.mutedForeground} />
|
|
445
484
|
</View>
|
|
446
|
-
<Text numberOfLines={1} style={[styles.optionText, { color: colors.mutedForeground }]}>
|
|
485
|
+
<Text numberOfLines={1} style={[styles.optionText, ff, { color: colors.mutedForeground }]}>
|
|
447
486
|
<Highlight label={item.label} query={query} color={colors.foreground} />
|
|
448
487
|
</Text>
|
|
449
488
|
{item.hint ? (
|
|
450
|
-
<Text numberOfLines={1} style={[styles.hint, { color: colors.mutedForeground }]}>
|
|
489
|
+
<Text numberOfLines={1} style={[styles.hint, ff, { color: colors.mutedForeground }]}>
|
|
451
490
|
{item.hint}
|
|
452
491
|
</Text>
|
|
453
492
|
) : null}
|
|
@@ -496,6 +535,22 @@ function Icon({ name, size, color }: { name: 'search' | 'recent' | 'trending'; s
|
|
|
496
535
|
);
|
|
497
536
|
}
|
|
498
537
|
|
|
538
|
+
function useReduceMotion() {
|
|
539
|
+
const [reduce, setReduce] = useState(false);
|
|
540
|
+
useEffect(() => {
|
|
541
|
+
let alive = true;
|
|
542
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
543
|
+
.then((r) => alive && setReduce(r))
|
|
544
|
+
.catch(() => {});
|
|
545
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
546
|
+
return () => {
|
|
547
|
+
alive = false;
|
|
548
|
+
sub.remove();
|
|
549
|
+
};
|
|
550
|
+
}, []);
|
|
551
|
+
return reduce;
|
|
552
|
+
}
|
|
553
|
+
|
|
499
554
|
const styles = StyleSheet.create({
|
|
500
555
|
root: { alignSelf: 'stretch' },
|
|
501
556
|
disabled: { opacity: 0.5 },
|
|
@@ -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,7 @@
|
|
|
1
1
|
import * as Haptics from 'expo-haptics';
|
|
2
2
|
import React, { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import {
|
|
4
|
+
AccessibilityInfo,
|
|
4
5
|
Animated,
|
|
5
6
|
PanResponder,
|
|
6
7
|
Pressable,
|
|
@@ -48,6 +49,10 @@ export interface SegmentedControlProps {
|
|
|
48
49
|
style?: StyleProp<ViewStyle>;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
53
|
+
// content text scales all the way.
|
|
54
|
+
const FONT_CAP = 1.35;
|
|
55
|
+
|
|
51
56
|
const SIZES: Record<SegmentedControlSize, { h: number; font: number; px: number; icon: number }> = {
|
|
52
57
|
sm: { h: 32, font: 13, px: 12, icon: 14 },
|
|
53
58
|
md: { h: 40, font: 14.5, px: 16, icon: 16 },
|
|
@@ -57,7 +62,7 @@ const SIZES: Record<SegmentedControlSize, { h: number; font: number; px: number;
|
|
|
57
62
|
const INSET = 3;
|
|
58
63
|
// Liquid move: the edge in the direction of travel leads, the other one trails behind.
|
|
59
64
|
const LEAD = motion.snappy;
|
|
60
|
-
const TRAIL = { damping:
|
|
65
|
+
const TRAIL = { damping: 24, stiffness: 150, mass: 0.9 };
|
|
61
66
|
|
|
62
67
|
/**
|
|
63
68
|
* Segmented control with a liquid indicator: its leading edge springs ahead and the trailing
|
|
@@ -81,6 +86,7 @@ export function SegmentedControl({
|
|
|
81
86
|
}: SegmentedControlProps) {
|
|
82
87
|
const { colors, radius, dark } = useKit();
|
|
83
88
|
const gid = 'sc' + useId().replace(/[^a-zA-Z0-9]/g, '');
|
|
89
|
+
const reduce = useReduceMotion();
|
|
84
90
|
const items: SegmentOption[] = options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o));
|
|
85
91
|
const [inner, setInner] = useState(defaultValue ?? items[0]?.value);
|
|
86
92
|
const selected = value ?? inner;
|
|
@@ -98,7 +104,8 @@ export function SegmentedControl({
|
|
|
98
104
|
useEffect(() => {
|
|
99
105
|
const l = layouts.current[index];
|
|
100
106
|
if (!l) return;
|
|
101
|
-
|
|
107
|
+
// Reduce Motion: the indicator jumps to the new segment instead of sliding.
|
|
108
|
+
if (!placed.current || reduce) {
|
|
102
109
|
lx.setValue(l.x);
|
|
103
110
|
rx.setValue(l.x + l.width);
|
|
104
111
|
placed.current = true;
|
|
@@ -111,7 +118,7 @@ export function SegmentedControl({
|
|
|
111
118
|
Animated.spring(lx, { toValue: l.x, useNativeDriver: false, ...(right ? TRAIL : LEAD) }),
|
|
112
119
|
Animated.spring(rx, { toValue: l.x + l.width, useNativeDriver: false, ...(right ? LEAD : TRAIL) }),
|
|
113
120
|
]).start();
|
|
114
|
-
}, [index, measured, lx, rx]);
|
|
121
|
+
}, [index, measured, reduce, lx, rx]);
|
|
115
122
|
|
|
116
123
|
const onSegmentLayout = (i: number) => (e: LayoutChangeEvent) => {
|
|
117
124
|
const { x: lx, width } = e.nativeEvent.layout;
|
|
@@ -253,6 +260,7 @@ export function SegmentedControl({
|
|
|
253
260
|
activeFg={activeFg}
|
|
254
261
|
idleFg={idleFg}
|
|
255
262
|
disabled={disabled || o.disabled}
|
|
263
|
+
reduce={reduce}
|
|
256
264
|
/>
|
|
257
265
|
))}
|
|
258
266
|
</View>
|
|
@@ -271,9 +279,12 @@ interface SegmentProps {
|
|
|
271
279
|
activeFg: string;
|
|
272
280
|
idleFg: string;
|
|
273
281
|
disabled?: boolean;
|
|
282
|
+
reduce: boolean;
|
|
274
283
|
}
|
|
275
284
|
|
|
276
|
-
function Segment({ option, active, onPress, onPressIn, onLayout, fullWidth, size, height, activeFg, idleFg, disabled }: SegmentProps) {
|
|
285
|
+
function Segment({ option, active, onPress, onPressIn, onLayout, fullWidth, size, height, activeFg, idleFg, disabled, reduce }: SegmentProps) {
|
|
286
|
+
const { font } = useKit();
|
|
287
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
277
288
|
// a: text colour (JS). press: content squish, pop: little bounce on selection (native, same view).
|
|
278
289
|
const a = useRef(new Animated.Value(active ? 1 : 0)).current;
|
|
279
290
|
const press = useRef(new Animated.Value(1)).current;
|
|
@@ -281,16 +292,19 @@ function Segment({ option, active, onPress, onPressIn, onLayout, fullWidth, size
|
|
|
281
292
|
const mounted = useRef(false);
|
|
282
293
|
|
|
283
294
|
useEffect(() => {
|
|
284
|
-
Animated.timing(a, { toValue: active ? 1 : 0, duration: 180, useNativeDriver: false }).start();
|
|
285
|
-
|
|
295
|
+
Animated.timing(a, { toValue: active ? 1 : 0, duration: reduce ? 150 : 180, useNativeDriver: false }).start();
|
|
296
|
+
// Reduce Motion: no selection pop and no press squish; the label colour still fades.
|
|
297
|
+
if (active && mounted.current && !reduce) {
|
|
286
298
|
pop.setValue(0.9);
|
|
287
299
|
Animated.spring(pop, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
|
|
288
300
|
}
|
|
289
301
|
mounted.current = true;
|
|
290
|
-
}, [active, a, pop]);
|
|
302
|
+
}, [active, reduce, a, pop]);
|
|
291
303
|
|
|
292
|
-
const pressTo = (to: number) =>
|
|
304
|
+
const pressTo = (to: number) => {
|
|
305
|
+
if (reduce) return press.setValue(1);
|
|
293
306
|
Animated.spring(press, { toValue: to, useNativeDriver: true, ...(to < 1 ? motion.snappy : motion.bouncy) }).start();
|
|
307
|
+
};
|
|
294
308
|
|
|
295
309
|
const color = a.interpolate({ inputRange: [0, 1], outputRange: [idleFg, activeFg] });
|
|
296
310
|
const icon =
|
|
@@ -310,12 +324,12 @@ function Segment({ option, active, onPress, onPressIn, onLayout, fullWidth, size
|
|
|
310
324
|
onPressOut={() => pressTo(1)}
|
|
311
325
|
onLayout={onLayout}
|
|
312
326
|
hitSlop={height < 44 ? { top: (44 - height) / 2, bottom: (44 - height) / 2 } : undefined}
|
|
313
|
-
style={[styles.segment, fullWidth && styles.flex, { height, paddingHorizontal: size.px }]}
|
|
327
|
+
style={[styles.segment, fullWidth && styles.flex, { minHeight: height, paddingHorizontal: size.px }]}
|
|
314
328
|
>
|
|
315
329
|
<Animated.View style={[styles.content, { transform: [{ scale: Animated.multiply(press, pop) }] }]}>
|
|
316
330
|
{icon ? <View style={{ minWidth: size.icon, alignItems: 'center' }}>{icon}</View> : null}
|
|
317
331
|
{option.label ? (
|
|
318
|
-
<Animated.Text numberOfLines={1} style={[styles.label, { fontSize: size.font, color }]}>
|
|
332
|
+
<Animated.Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.label, ff, { fontSize: size.font, color }]}>
|
|
319
333
|
{option.label}
|
|
320
334
|
</Animated.Text>
|
|
321
335
|
) : null}
|
|
@@ -324,6 +338,22 @@ function Segment({ option, active, onPress, onPressIn, onLayout, fullWidth, size
|
|
|
324
338
|
);
|
|
325
339
|
}
|
|
326
340
|
|
|
341
|
+
function useReduceMotion() {
|
|
342
|
+
const [reduce, setReduce] = useState(false);
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
let alive = true;
|
|
345
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
346
|
+
.then((r) => alive && setReduce(r))
|
|
347
|
+
.catch(() => {});
|
|
348
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
349
|
+
return () => {
|
|
350
|
+
alive = false;
|
|
351
|
+
sub.remove();
|
|
352
|
+
};
|
|
353
|
+
}, []);
|
|
354
|
+
return reduce;
|
|
355
|
+
}
|
|
356
|
+
|
|
327
357
|
const styles = StyleSheet.create({
|
|
328
358
|
track: { flexDirection: 'row', alignItems: 'center' },
|
|
329
359
|
flex: { flex: 1 },
|
|
@@ -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
|
};
|