@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,10 +1,12 @@
|
|
|
1
1
|
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
AccessibilityInfo,
|
|
3
4
|
Animated,
|
|
4
5
|
Pressable,
|
|
5
6
|
StyleSheet,
|
|
6
7
|
Text,
|
|
7
8
|
View,
|
|
9
|
+
useWindowDimensions,
|
|
8
10
|
type AccessibilityActionEvent,
|
|
9
11
|
type LayoutChangeEvent,
|
|
10
12
|
type StyleProp,
|
|
@@ -65,6 +67,10 @@ export interface StepperProps {
|
|
|
65
67
|
valueStyle?: StyleProp<TextStyle>;
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
71
|
+
// content text scales all the way.
|
|
72
|
+
const FONT_CAP = 1.35;
|
|
73
|
+
|
|
68
74
|
/** Every dimension in one table so the sizes read clearly different. */
|
|
69
75
|
const SIZES: Record<StepperSize | 'xs', { btn: number; font: number; icon: number; num: number }> = {
|
|
70
76
|
xs: { btn: 28, font: 15, icon: 12, num: 26 },
|
|
@@ -118,7 +124,9 @@ export function Stepper({
|
|
|
118
124
|
buttonStyle,
|
|
119
125
|
valueStyle,
|
|
120
126
|
}: StepperProps) {
|
|
121
|
-
const { colors, radius } = useKit();
|
|
127
|
+
const { colors, radius, font } = useKit();
|
|
128
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
129
|
+
const reduce = useReduceMotion();
|
|
122
130
|
const variant: StepperVariant = variantProp in LEGACY ? LEGACY[variantProp as StepperLegacyVariant] : (variantProp as StepperVariant);
|
|
123
131
|
const vertical = variant !== 'cart' && (orientation ? orientation === 'vertical' : variantProp === 'vertical');
|
|
124
132
|
const tint = accent ?? colors.accent;
|
|
@@ -133,9 +141,11 @@ export function Stepper({
|
|
|
133
141
|
const [track, setTrack] = useState({ v: current, dir: 1 });
|
|
134
142
|
if (track.v !== current) setTrack({ v: current, dir: current > track.v ? 1 : -1 });
|
|
135
143
|
|
|
136
|
-
// Press past a limit: the number nudges toward the wall and springs back
|
|
144
|
+
// Press past a limit: the number nudges toward the wall and springs back (not with Reduce Motion;
|
|
145
|
+
// the dimmed button already says it's the limit).
|
|
137
146
|
const bump = useRef(new Animated.Value(0)).current;
|
|
138
147
|
const nudge = (dir: 1 | -1) => {
|
|
148
|
+
if (reduce) return;
|
|
139
149
|
Animated.sequence([
|
|
140
150
|
Animated.timing(bump, { toValue: dir * 4, duration: 60, useNativeDriver: true }),
|
|
141
151
|
Animated.spring(bump, { toValue: 0, useNativeDriver: true, ...motion.bouncy }),
|
|
@@ -199,6 +209,7 @@ export function Stepper({
|
|
|
199
209
|
fontSize,
|
|
200
210
|
lineHeight: Math.round(fontSize * 1.3),
|
|
201
211
|
color: numColor,
|
|
212
|
+
...ff,
|
|
202
213
|
...flatValue,
|
|
203
214
|
};
|
|
204
215
|
|
|
@@ -236,6 +247,7 @@ export function Stepper({
|
|
|
236
247
|
limit={dir === 1 ? atMax : atMin}
|
|
237
248
|
disabled={!!disabled}
|
|
238
249
|
label={a11y ?? `${dir === 1 ? 'Increase' : 'Decrease'}${title ? ` ${title}` : ''}`}
|
|
250
|
+
reduce={reduce}
|
|
239
251
|
style={buttonStyle}
|
|
240
252
|
onPressIn={() => startRepeat(dir)}
|
|
241
253
|
onPressOut={stopRepeat}
|
|
@@ -244,9 +256,9 @@ export function Stepper({
|
|
|
244
256
|
|
|
245
257
|
const number = (
|
|
246
258
|
<View style={styles.roll}>
|
|
247
|
-
<RollingText text={text} dir={track.dir} style={numStyle} />
|
|
259
|
+
<RollingText text={text} dir={track.dir} style={numStyle} reduce={reduce} />
|
|
248
260
|
{suffixText ? (
|
|
249
|
-
<Text style={[styles.num, styles.suffix, numStyle, { fontSize: fontSize * 0.82, color: isCart ? alpha(onCart, 0.8) : colors.mutedForeground }]}>
|
|
261
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.num, styles.suffix, numStyle, { fontSize: fontSize * 0.82, color: isCart ? alpha(onCart, 0.8) : colors.mutedForeground }]}>
|
|
250
262
|
{suffixText}
|
|
251
263
|
</Text>
|
|
252
264
|
) : null}
|
|
@@ -285,6 +297,7 @@ export function Stepper({
|
|
|
285
297
|
addLabel={addLabel}
|
|
286
298
|
bump={bump}
|
|
287
299
|
disabled={!!disabled}
|
|
300
|
+
reduce={reduce}
|
|
288
301
|
dec={button(-1, removes ? ICON.trash : ICON.minus, removes ? `Remove${title ? ` ${title}` : ''}` : undefined)}
|
|
289
302
|
inc={button(1)}
|
|
290
303
|
onAdd={() => startRepeat(1)}
|
|
@@ -298,7 +311,7 @@ export function Stepper({
|
|
|
298
311
|
styles.value,
|
|
299
312
|
vertical
|
|
300
313
|
? { minHeight: s.num, minWidth: s.btn, paddingVertical: 2 }
|
|
301
|
-
: { minWidth: s.num,
|
|
314
|
+
: { minWidth: s.num, minHeight: s.btn, paddingHorizontal: variant === 'outline' ? 8 : 4 },
|
|
302
315
|
{ transform: [vertical ? { translateY: Animated.multiply(bump, -1) } : { translateX: bump }] },
|
|
303
316
|
]}
|
|
304
317
|
>
|
|
@@ -312,8 +325,8 @@ export function Stepper({
|
|
|
312
325
|
return (
|
|
313
326
|
<View style={[styles.labelled, style]}>
|
|
314
327
|
<View style={styles.texts}>
|
|
315
|
-
{title ? <Text style={[styles.label, { color: colors.foreground }]}>{title}</Text> : null}
|
|
316
|
-
{description ? <Text style={[styles.description, { color: colors.mutedForeground }]}>{description}</Text> : null}
|
|
328
|
+
{title ? <Text style={[styles.label, ff, { color: colors.foreground }]}>{title}</Text> : null}
|
|
329
|
+
{description ? <Text style={[styles.description, ff, { color: colors.mutedForeground }]}>{description}</Text> : null}
|
|
317
330
|
</View>
|
|
318
331
|
{control}
|
|
319
332
|
</View>
|
|
@@ -338,6 +351,7 @@ function CartControl({
|
|
|
338
351
|
addLabel,
|
|
339
352
|
bump,
|
|
340
353
|
disabled,
|
|
354
|
+
reduce,
|
|
341
355
|
dec,
|
|
342
356
|
inc,
|
|
343
357
|
onAdd,
|
|
@@ -356,11 +370,14 @@ function CartControl({
|
|
|
356
370
|
addLabel?: string;
|
|
357
371
|
bump: Animated.Value;
|
|
358
372
|
disabled: boolean;
|
|
373
|
+
reduce: boolean;
|
|
359
374
|
dec: React.ReactNode;
|
|
360
375
|
inc: React.ReactNode;
|
|
361
376
|
onAdd: () => void;
|
|
362
377
|
onAddEnd: () => void;
|
|
363
378
|
} & React.ComponentProps<typeof View>) {
|
|
379
|
+
const { font } = useKit();
|
|
380
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
364
381
|
// Natural widths, measured off-screen so the spring always lands on the real size.
|
|
365
382
|
const [textW, setTextW] = useState(0);
|
|
366
383
|
const [addW, setAddW] = useState(0);
|
|
@@ -376,18 +393,20 @@ function CartControl({
|
|
|
376
393
|
|
|
377
394
|
const measured = textW > 0 && (!addLabel || addW > 0);
|
|
378
395
|
useEffect(() => {
|
|
379
|
-
// Snap until the rulers have reported; after that every change springs.
|
|
380
|
-
if (!settled.current) {
|
|
396
|
+
// Snap until the rulers have reported; after that every change springs (or snaps, with Reduce Motion).
|
|
397
|
+
if (!settled.current || reduce) {
|
|
381
398
|
width.setValue(target);
|
|
382
399
|
settled.current = measured;
|
|
383
400
|
return;
|
|
384
401
|
}
|
|
385
402
|
Animated.spring(width, { toValue: target, useNativeDriver: false, ...motion.snappy }).start();
|
|
386
|
-
}, [target, measured, width]);
|
|
403
|
+
}, [target, measured, width, reduce]);
|
|
387
404
|
|
|
388
405
|
useEffect(() => {
|
|
389
|
-
|
|
390
|
-
|
|
406
|
+
// Reduce Motion: − and the number only fade in (their slide is flattened below).
|
|
407
|
+
if (reduce) Animated.timing(open, { toValue: collapsed ? 0 : 1, duration: 150, useNativeDriver: true }).start();
|
|
408
|
+
else Animated.spring(open, { toValue: collapsed ? 0 : 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
409
|
+
}, [collapsed, open, reduce]);
|
|
391
410
|
|
|
392
411
|
const measure = (set: (n: number) => void) => (e: LayoutChangeEvent) => set(Math.ceil(e.nativeEvent.layout.width));
|
|
393
412
|
|
|
@@ -403,15 +422,15 @@ function CartControl({
|
|
|
403
422
|
{/* Off-screen rulers inside a wide box, so nothing squeezes them. */}
|
|
404
423
|
<View pointerEvents="none" style={[styles.ruler, styles.hidden]}>
|
|
405
424
|
<View onLayout={measure(setTextW)} style={styles.row}>
|
|
406
|
-
<Text style={[styles.num, numStyle]}>{text}</Text>
|
|
407
|
-
{suffixText ? <Text style={[styles.num, styles.suffix, numStyle, { fontSize: (numStyle.fontSize ?? s.font) * 0.82 }]}>{suffixText}</Text> : null}
|
|
425
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.num, numStyle]}>{text}</Text>
|
|
426
|
+
{suffixText ? <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.num, styles.suffix, numStyle, { fontSize: (numStyle.fontSize ?? s.font) * 0.82 }]}>{suffixText}</Text> : null}
|
|
408
427
|
</View>
|
|
409
428
|
</View>
|
|
410
429
|
|
|
411
430
|
<View style={[styles.cartRow, { width: openW }]} pointerEvents="box-none">
|
|
412
431
|
<Animated.View
|
|
413
432
|
pointerEvents={collapsed ? 'none' : 'auto'}
|
|
414
|
-
style={[styles.row, { opacity: open, transform: [{ translateX: open.interpolate({ inputRange: [0, 1], outputRange: [s.btn * 0.5, 0] }) }] }]}
|
|
433
|
+
style={[styles.row, { opacity: open, transform: [{ translateX: open.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : s.btn * 0.5, 0] }) }] }]}
|
|
415
434
|
>
|
|
416
435
|
{dec}
|
|
417
436
|
<Animated.View style={[styles.value, { width: valueW, height: s.btn, transform: [{ translateX: bump }] }]}>{number}</Animated.View>
|
|
@@ -437,8 +456,8 @@ function CartControl({
|
|
|
437
456
|
onPressOut={onAddEnd}
|
|
438
457
|
style={({ pressed }) => [styles.add, { height: s.btn, paddingHorizontal: Math.round(s.btn * 0.36) }, pressed && { backgroundColor: alpha(fg, 0.16) }]}
|
|
439
458
|
>
|
|
440
|
-
<Glyph d={ICON.plus} size={s.icon} color={fg} />
|
|
441
|
-
<Text style={[styles.addText, { color: fg, fontSize: s.font - 2 }]}>{addLabel}</Text>
|
|
459
|
+
<Glyph d={ICON.plus} size={s.icon} color={fg} reduce={reduce} />
|
|
460
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.addText, ff, { color: fg, fontSize: s.font - 2 }]}>{addLabel}</Text>
|
|
442
461
|
</Pressable>
|
|
443
462
|
</Animated.View>
|
|
444
463
|
</View>
|
|
@@ -466,6 +485,7 @@ function StepButton({
|
|
|
466
485
|
limit,
|
|
467
486
|
disabled,
|
|
468
487
|
label,
|
|
488
|
+
reduce,
|
|
469
489
|
style,
|
|
470
490
|
onPressIn,
|
|
471
491
|
onPressOut,
|
|
@@ -481,6 +501,7 @@ function StepButton({
|
|
|
481
501
|
limit: boolean;
|
|
482
502
|
disabled: boolean;
|
|
483
503
|
label: string;
|
|
504
|
+
reduce: boolean;
|
|
484
505
|
style?: StyleProp<ViewStyle>;
|
|
485
506
|
onPressIn: () => void;
|
|
486
507
|
onPressOut: () => void;
|
|
@@ -488,7 +509,11 @@ function StepButton({
|
|
|
488
509
|
// Scale and dim share one native-driven view; nothing JS-driven touches it.
|
|
489
510
|
const scale = useRef(new Animated.Value(1)).current;
|
|
490
511
|
const dim = useRef(new Animated.Value(limit ? 0.32 : 1)).current;
|
|
491
|
-
|
|
512
|
+
// Reduce Motion: no press squeeze; the pressed background still shows the press.
|
|
513
|
+
const spring = (to: number) => {
|
|
514
|
+
if (reduce) scale.setValue(1);
|
|
515
|
+
else Animated.spring(scale, { toValue: to, useNativeDriver: true, ...motion.bouncy }).start();
|
|
516
|
+
};
|
|
492
517
|
|
|
493
518
|
useEffect(() => {
|
|
494
519
|
Animated.timing(dim, { toValue: limit ? 0.32 : 1, duration: motion.fast, useNativeDriver: true }).start();
|
|
@@ -529,22 +554,23 @@ function StepButton({
|
|
|
529
554
|
style,
|
|
530
555
|
]}
|
|
531
556
|
>
|
|
532
|
-
<Glyph d={glyph} size={icon} color={fg} />
|
|
557
|
+
<Glyph d={glyph} size={icon} color={fg} reduce={reduce} />
|
|
533
558
|
</Pressable>
|
|
534
559
|
</Animated.View>
|
|
535
560
|
);
|
|
536
561
|
}
|
|
537
562
|
|
|
538
563
|
/** Stroked icon; pops in when its shape changes (− ↔ trash). */
|
|
539
|
-
function Glyph({ d, size, color }: { d: string; size: number; color: string }) {
|
|
564
|
+
function Glyph({ d, size, color, reduce }: { d: string; size: number; color: string; reduce: boolean }) {
|
|
540
565
|
const pop = useRef(new Animated.Value(1)).current;
|
|
541
566
|
const prev = useRef(d);
|
|
542
567
|
useEffect(() => {
|
|
543
568
|
if (prev.current === d) return;
|
|
544
569
|
prev.current = d;
|
|
570
|
+
if (reduce) return;
|
|
545
571
|
pop.setValue(0.55);
|
|
546
572
|
Animated.spring(pop, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
|
|
547
|
-
}, [d, pop]);
|
|
573
|
+
}, [d, pop, reduce]);
|
|
548
574
|
const trash = d === ICON.trash;
|
|
549
575
|
return (
|
|
550
576
|
<Animated.View style={{ transform: [{ scale: pop }] }}>
|
|
@@ -555,37 +581,48 @@ function Glyph({ d, size, color }: { d: string; size: number; color: string }) {
|
|
|
555
581
|
);
|
|
556
582
|
}
|
|
557
583
|
|
|
558
|
-
/** Text where every changed character slides out and its replacement slides in. */
|
|
559
|
-
function RollingText({ text, dir, style }: { text: string; dir: number; style: TextStyle }) {
|
|
584
|
+
/** Text where every changed character slides out and its replacement slides in (swaps at once with Reduce Motion). */
|
|
585
|
+
function RollingText({ text, dir, style, reduce }: { text: string; dir: number; style: TextStyle; reduce: boolean }) {
|
|
560
586
|
const chars = [...text];
|
|
561
587
|
return (
|
|
562
588
|
<View style={styles.roll}>
|
|
563
589
|
{chars.map((c, i) => (
|
|
564
590
|
// Keyed from the right so the ones digit stays the ones digit when the number grows.
|
|
565
|
-
<RollingChar key={chars.length - i} char={c} dir={dir} style={style} />
|
|
591
|
+
<RollingChar key={chars.length - i} char={c} dir={dir} style={style} reduce={reduce} />
|
|
566
592
|
))}
|
|
567
593
|
</View>
|
|
568
594
|
);
|
|
569
595
|
}
|
|
570
596
|
|
|
571
|
-
function RollingChar({ char, dir, style }: { char: string; dir: number; style: TextStyle }) {
|
|
597
|
+
function RollingChar({ char, dir, style, reduce }: { char: string; dir: number; style: TextStyle; reduce: boolean }) {
|
|
572
598
|
const [pair, setPair] = useState<{ cur: string; prev: string | null; dir: number }>({ cur: char, prev: null, dir });
|
|
573
599
|
if (pair.cur !== char) setPair({ cur: char, prev: pair.cur, dir });
|
|
574
600
|
const t = useRef(new Animated.Value(1)).current;
|
|
575
|
-
|
|
601
|
+
// The digit window follows the text's own (capped) Dynamic Type scale, so a scaled digit isn't cropped.
|
|
602
|
+
const { fontScale } = useWindowDimensions();
|
|
603
|
+
const h = ((style.lineHeight as number) ?? 24) * Math.min(fontScale, FONT_CAP);
|
|
576
604
|
|
|
605
|
+
// Each change rolls once (a Reduce Motion toggle alone doesn't replay it).
|
|
606
|
+
const played = useRef(pair);
|
|
577
607
|
useLayoutEffect(() => {
|
|
578
|
-
if (pair.prev === null) return;
|
|
608
|
+
if (pair.prev === null || played.current === pair) return;
|
|
609
|
+
played.current = pair;
|
|
610
|
+
if (reduce) {
|
|
611
|
+
// At 1 the old character is gone and the new one is in place: the final value, no roll.
|
|
612
|
+
t.setValue(1);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
579
615
|
t.setValue(0);
|
|
580
616
|
Animated.spring(t, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
581
|
-
}, [pair, t]);
|
|
617
|
+
}, [pair, t, reduce]);
|
|
582
618
|
|
|
583
619
|
const d = pair.dir * h * 0.8;
|
|
584
620
|
return (
|
|
585
621
|
<View style={[styles.charBox, { height: h }]}>
|
|
586
|
-
<Text style={[styles.num, style, styles.hidden]}>{pair.cur}</Text>
|
|
622
|
+
<Text maxFontSizeMultiplier={FONT_CAP} style={[styles.num, style, styles.hidden]}>{pair.cur}</Text>
|
|
587
623
|
{pair.prev !== null ? (
|
|
588
624
|
<Animated.Text
|
|
625
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
589
626
|
style={[
|
|
590
627
|
styles.num,
|
|
591
628
|
styles.layer,
|
|
@@ -603,6 +640,7 @@ function RollingChar({ char, dir, style }: { char: string; dir: number; style: T
|
|
|
603
640
|
</Animated.Text>
|
|
604
641
|
) : null}
|
|
605
642
|
<Animated.Text
|
|
643
|
+
maxFontSizeMultiplier={FONT_CAP}
|
|
606
644
|
style={[
|
|
607
645
|
styles.num,
|
|
608
646
|
styles.layer,
|
|
@@ -619,6 +657,22 @@ function RollingChar({ char, dir, style }: { char: string; dir: number; style: T
|
|
|
619
657
|
);
|
|
620
658
|
}
|
|
621
659
|
|
|
660
|
+
function useReduceMotion() {
|
|
661
|
+
const [reduce, setReduce] = useState(false);
|
|
662
|
+
useEffect(() => {
|
|
663
|
+
let alive = true;
|
|
664
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
665
|
+
.then((r) => alive && setReduce(r))
|
|
666
|
+
.catch(() => {});
|
|
667
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
668
|
+
return () => {
|
|
669
|
+
alive = false;
|
|
670
|
+
sub.remove();
|
|
671
|
+
};
|
|
672
|
+
}, []);
|
|
673
|
+
return reduce;
|
|
674
|
+
}
|
|
675
|
+
|
|
622
676
|
const styles = StyleSheet.create({
|
|
623
677
|
// Hugs its content in a column without hijacking the cross-axis alignment of a row.
|
|
624
678
|
self: { alignItems: 'flex-start', justifyContent: 'center' },
|
|
@@ -181,14 +181,17 @@ export function useKit(): Theme {
|
|
|
181
181
|
return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Shared motion presets so the whole kit moves the same way. Every spring is critically damped
|
|
186
|
+
* (damping ≥ 2·√(stiffness·mass)): it settles smoothly with no overshoot or wobble.
|
|
187
|
+
*/
|
|
185
188
|
export const motion = {
|
|
186
189
|
/** Toggles, selection indicators. */
|
|
187
|
-
snappy: { damping:
|
|
188
|
-
/** Press feedback, pops. */
|
|
189
|
-
bouncy: { damping:
|
|
190
|
+
snappy: { damping: 30, stiffness: 260, mass: 0.8 },
|
|
191
|
+
/** Press feedback, pops. (Kept under its old name; it no longer bounces.) */
|
|
192
|
+
bouncy: { damping: 26, stiffness: 220, mass: 0.7 },
|
|
190
193
|
/** Sheets, layout changes. */
|
|
191
|
-
gentle: { damping:
|
|
194
|
+
gentle: { damping: 27, stiffness: 160, mass: 1 },
|
|
192
195
|
fast: 160,
|
|
193
196
|
normal: 240,
|
|
194
197
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Haptics from 'expo-haptics';
|
|
2
2
|
import React, { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { Animated, PanResponder, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { AccessibilityInfo, Animated, PanResponder, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
4
4
|
import Svg, { Circle, Defs, LinearGradient, Path, Rect, Stop } from 'react-native-svg';
|
|
5
5
|
|
|
6
6
|
import { alpha, motion, readableOn, useKit } from '@/lib/kit';
|
|
@@ -63,8 +63,10 @@ export function Switch({
|
|
|
63
63
|
accessibilityLabel,
|
|
64
64
|
style,
|
|
65
65
|
}: SwitchProps) {
|
|
66
|
-
const { colors, dark } = useKit();
|
|
66
|
+
const { colors, dark, font } = useKit();
|
|
67
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
67
68
|
const gid = 'sw' + useId().replace(/[^a-zA-Z0-9]/g, '');
|
|
69
|
+
const reduce = useReduceMotion();
|
|
68
70
|
const tint = accent ?? colors.accent;
|
|
69
71
|
const [inner, setInner] = useState(defaultValue);
|
|
70
72
|
const on = value ?? inner;
|
|
@@ -87,18 +89,21 @@ export function Switch({
|
|
|
87
89
|
const stretch = useRef(new Animated.Value(0)).current;
|
|
88
90
|
const glyph = useRef(new Animated.Value(on ? 1 : 0)).current;
|
|
89
91
|
const halo = useRef(new Animated.Value(0)).current;
|
|
92
|
+
// Reduce Motion: the thumb jumps, so the colours follow this instead and still fade (JS driver).
|
|
93
|
+
const tone = useRef(new Animated.Value(on ? 1 : 0)).current;
|
|
90
94
|
|
|
91
95
|
useEffect(() => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
const to = on ? 1 : 0;
|
|
97
|
+
if (reduce) {
|
|
98
|
+
pos.setValue(to);
|
|
99
|
+
glyph.setValue(to);
|
|
100
|
+
Animated.timing(tone, { toValue: to, duration: 150, useNativeDriver: false }).start();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
tone.setValue(to);
|
|
104
|
+
Animated.spring(pos, { toValue: to, useNativeDriver: false, ...motion.snappy }).start();
|
|
105
|
+
Animated.spring(glyph, { toValue: to, useNativeDriver: true, ...motion.bouncy }).start();
|
|
106
|
+
}, [on, reduce, pos, glyph, tone]);
|
|
102
107
|
|
|
103
108
|
const toggle = () => {
|
|
104
109
|
if (haptics) Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
|
|
@@ -113,8 +118,24 @@ export function Switch({
|
|
|
113
118
|
// Drag the thumb across: pos follows the finger, release settles on the nearer side.
|
|
114
119
|
// A touch that never moves is a tap and just toggles. On web the wrapping Pressable also
|
|
115
120
|
// gets a click after the gesture, so it ignores presses right after one (`endedAt`).
|
|
116
|
-
const live = useRef({ on, toggle, travel: c1 - c0 });
|
|
117
|
-
live.current = { on, toggle, travel: c1 - c0 };
|
|
121
|
+
const live = useRef({ on, toggle, travel: c1 - c0, reduce });
|
|
122
|
+
live.current = { on, toggle, travel: c1 - c0, reduce };
|
|
123
|
+
|
|
124
|
+
// Halo: 0 hidden · 1 held (glow around the thumb) · 2 released (expands and fades out).
|
|
125
|
+
// Reduce Motion: no squash & stretch, and the halo only fades (its scale is pinned below).
|
|
126
|
+
const press = (to: number) => {
|
|
127
|
+
const still = live.current.reduce;
|
|
128
|
+
if (!still) Animated.spring(stretch, { toValue: to, useNativeDriver: false, ...(to ? motion.snappy : motion.bouncy) }).start();
|
|
129
|
+
if (to) Animated.spring(halo, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
|
|
130
|
+
else
|
|
131
|
+
Animated.timing(halo, { toValue: 2, duration: still ? 150 : 420, useNativeDriver: true }).start(
|
|
132
|
+
({ finished }) => finished && halo.setValue(0)
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
const settle = (to: number) => {
|
|
136
|
+
if (live.current.reduce) pos.setValue(to);
|
|
137
|
+
else Animated.spring(pos, { toValue: to, useNativeDriver: false, ...motion.snappy }).start();
|
|
138
|
+
};
|
|
118
139
|
const drag = useRef({ from: 0, moved: false, endedAt: 0 });
|
|
119
140
|
const responder = useMemo(
|
|
120
141
|
() =>
|
|
@@ -137,12 +158,12 @@ export function Switch({
|
|
|
137
158
|
press(0);
|
|
138
159
|
if (!drag.current.moved) return flip();
|
|
139
160
|
const next = clamp01(drag.current.from + g.dx / live.current.travel) > 0.5;
|
|
140
|
-
|
|
161
|
+
settle(next ? 1 : 0);
|
|
141
162
|
if (next !== now) flip();
|
|
142
163
|
},
|
|
143
164
|
onPanResponderTerminate: () => {
|
|
144
165
|
press(0);
|
|
145
|
-
|
|
166
|
+
settle(live.current.on ? 1 : 0);
|
|
146
167
|
},
|
|
147
168
|
}),
|
|
148
169
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -163,12 +184,13 @@ export function Switch({
|
|
|
163
184
|
Animated.multiply(stretch, growH / 2)
|
|
164
185
|
);
|
|
165
186
|
|
|
187
|
+
const paint = reduce ? tone : pos;
|
|
166
188
|
const offTrack = material ? alpha(colors.mutedForeground, 0.12) : alpha(colors.foreground, dark ? 0.16 : 0.1);
|
|
167
|
-
const trackBg =
|
|
168
|
-
const trackBorder =
|
|
189
|
+
const trackBg = paint.interpolate({ inputRange: [0, 1], outputRange: [offTrack, tint], extrapolate: 'clamp' });
|
|
190
|
+
const trackBorder = paint.interpolate({ inputRange: [0, 1], outputRange: [colors.mutedForeground, tint], extrapolate: 'clamp' });
|
|
169
191
|
const onFg = readableOn(tint);
|
|
170
192
|
const thumbBg = material
|
|
171
|
-
?
|
|
193
|
+
? paint.interpolate({ inputRange: [0, 1], outputRange: [colors.mutedForeground, onFg], extrapolate: 'clamp' })
|
|
172
194
|
: '#ffffff';
|
|
173
195
|
|
|
174
196
|
const iconSize = dOn * 0.62;
|
|
@@ -221,7 +243,7 @@ export function Switch({
|
|
|
221
243
|
borderRadius: haloSize / 2,
|
|
222
244
|
backgroundColor: alpha(on ? tint : colors.mutedForeground, 0.2),
|
|
223
245
|
opacity: halo.interpolate({ inputRange: [0, 1, 2], outputRange: [0, 1, 0] }),
|
|
224
|
-
transform: [{ scale: halo.interpolate({ inputRange: [0, 1, 2], outputRange: [0.5, 1, 1.45] }) }],
|
|
246
|
+
transform: [{ scale: halo.interpolate({ inputRange: [0, 1, 2], outputRange: reduce ? [1, 1, 1] : [0.5, 1, 1.45] }) }],
|
|
225
247
|
},
|
|
226
248
|
]}
|
|
227
249
|
/>
|
|
@@ -315,14 +337,30 @@ export function Switch({
|
|
|
315
337
|
return (
|
|
316
338
|
<Pressable {...a11y} style={[styles.row, { opacity: disabled ? 0.45 : 1 }, style]}>
|
|
317
339
|
<View style={styles.text}>
|
|
318
|
-
<Text style={[styles.label, { color: colors.foreground }]}>{label}</Text>
|
|
319
|
-
{description ? <Text style={[styles.description, { color: colors.mutedForeground }]}>{description}</Text> : null}
|
|
340
|
+
<Text style={[styles.label, ff, { color: colors.foreground }]}>{label}</Text>
|
|
341
|
+
{description ? <Text style={[styles.description, ff, { color: colors.mutedForeground }]}>{description}</Text> : null}
|
|
320
342
|
</View>
|
|
321
343
|
{control}
|
|
322
344
|
</Pressable>
|
|
323
345
|
);
|
|
324
346
|
}
|
|
325
347
|
|
|
348
|
+
function useReduceMotion() {
|
|
349
|
+
const [reduce, setReduce] = useState(false);
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
let alive = true;
|
|
352
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
353
|
+
.then((r) => alive && setReduce(r))
|
|
354
|
+
.catch(() => {});
|
|
355
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
356
|
+
return () => {
|
|
357
|
+
alive = false;
|
|
358
|
+
sub.remove();
|
|
359
|
+
};
|
|
360
|
+
}, []);
|
|
361
|
+
return reduce;
|
|
362
|
+
}
|
|
363
|
+
|
|
326
364
|
const styles = StyleSheet.create({
|
|
327
365
|
row: { flexDirection: 'row', alignItems: 'center', gap: 16, minHeight: 44 },
|
|
328
366
|
text: { 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
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as Haptics from 'expo-haptics';
|
|
2
|
-
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import React, { useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
AccessibilityInfo,
|
|
5
5
|
Animated,
|
|
@@ -64,8 +64,11 @@ type Box = { x: number; w: number };
|
|
|
64
64
|
|
|
65
65
|
// Liquid move: the edge in the direction of travel leads, the other one trails behind.
|
|
66
66
|
const LEAD = motion.snappy;
|
|
67
|
-
const TRAIL = { damping:
|
|
67
|
+
const TRAIL = { damping: 24, stiffness: 150, mass: 0.9 };
|
|
68
68
|
const UNDERLINE = 3;
|
|
69
|
+
// Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
|
|
70
|
+
// content text scales all the way.
|
|
71
|
+
const FONT_CAP = 1.35;
|
|
69
72
|
|
|
70
73
|
/**
|
|
71
74
|
* Underline tabs with swipeable content panes. The underline is measured to each label and moves
|
|
@@ -123,7 +126,7 @@ export function Tabs({
|
|
|
123
126
|
const release = useRef({ v: 0, dragged: false });
|
|
124
127
|
const paging = useRef(false); // pager spring in flight (its JS value is stale until it ends)
|
|
125
128
|
const [settle, setSettle] = useState(0);
|
|
126
|
-
const scrollRef = useRef<ScrollView
|
|
129
|
+
const scrollRef = useRef<ComponentRef<typeof ScrollView>>(null);
|
|
127
130
|
|
|
128
131
|
const edgesOf = (i: number): [number, number] | null => {
|
|
129
132
|
const t = tabs.current[i];
|
|
@@ -173,7 +176,7 @@ export function Tabs({
|
|
|
173
176
|
Animated.timing(pan, { toValue: to, duration: 180, useNativeDriver: true }).start();
|
|
174
177
|
} else {
|
|
175
178
|
paging.current = true;
|
|
176
|
-
Animated.spring(pan, { toValue: to, velocity: v, useNativeDriver: true, ...motion.snappy, damping:
|
|
179
|
+
Animated.spring(pan, { toValue: to, velocity: v, useNativeDriver: true, ...motion.snappy, damping: 30 }).start(
|
|
177
180
|
({ finished }) => {
|
|
178
181
|
if (finished) paging.current = false;
|
|
179
182
|
}
|
|
@@ -425,7 +428,8 @@ interface TabProps {
|
|
|
425
428
|
}
|
|
426
429
|
|
|
427
430
|
function Tab({ item, active, mode, height, fontSize, tint, reduce, onPress, onLayout, onLabelLayout }: TabProps) {
|
|
428
|
-
const { colors } = useKit();
|
|
431
|
+
const { colors, font } = useKit();
|
|
432
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
429
433
|
const a = useRef(new Animated.Value(active ? 1 : 0)).current; // label colour (JS)
|
|
430
434
|
const press = useRef(new Animated.Value(1)).current; // squish (native)
|
|
431
435
|
|
|
@@ -454,7 +458,7 @@ function Tab({ item, active, mode, height, fontSize, tint, reduce, onPress, onLa
|
|
|
454
458
|
hitSlop={slop ? { top: slop, bottom: slop } : undefined}
|
|
455
459
|
style={[
|
|
456
460
|
styles.tab,
|
|
457
|
-
{ height, paddingHorizontal: mode === 'fill' ? 8 : 14, opacity: item.disabled ? 0.4 : 1 },
|
|
461
|
+
{ minHeight: height, paddingHorizontal: mode === 'fill' ? 8 : 14, opacity: item.disabled ? 0.4 : 1 },
|
|
458
462
|
mode === 'fill' && styles.fill,
|
|
459
463
|
mode === 'auto' && styles.grow,
|
|
460
464
|
]}
|
|
@@ -462,7 +466,7 @@ function Tab({ item, active, mode, height, fontSize, tint, reduce, onPress, onLa
|
|
|
462
466
|
{/* Measured: the underline spans exactly this (icon + label). */}
|
|
463
467
|
<Animated.View onLayout={onLabelLayout} style={[styles.labelRow, { transform: [{ scale: press }] }]}>
|
|
464
468
|
{icon}
|
|
465
|
-
<Animated.Text numberOfLines={1} style={[styles.label, { color, fontSize }]}>
|
|
469
|
+
<Animated.Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={[styles.label, ff, { color, fontSize }]}>
|
|
466
470
|
{item.label}
|
|
467
471
|
</Animated.Text>
|
|
468
472
|
</Animated.View>
|
|
@@ -473,7 +477,8 @@ function Tab({ item, active, mode, height, fontSize, tint, reduce, onPress, onLa
|
|
|
473
477
|
|
|
474
478
|
/** Count bubble: accent when its tab is active, muted otherwise. Pops when the value changes. */
|
|
475
479
|
function TabBadge({ value, active, tint, reduce }: { value: number | string; active: boolean; tint: string; reduce: boolean }) {
|
|
476
|
-
const { colors, radius, dark } = useKit();
|
|
480
|
+
const { colors, radius, dark, font } = useKit();
|
|
481
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
477
482
|
const s = useRef(new Animated.Value(1)).current;
|
|
478
483
|
const first = useRef(true);
|
|
479
484
|
useEffect(() => {
|
|
@@ -496,7 +501,7 @@ function TabBadge({ value, active, tint, reduce }: { value: number | string; act
|
|
|
496
501
|
},
|
|
497
502
|
]}
|
|
498
503
|
>
|
|
499
|
-
<Animated.Text style={[styles.badgeText, { color: active ? readableOn(tint) : colors.mutedForeground }]}>{text}</Animated.Text>
|
|
504
|
+
<Animated.Text maxFontSizeMultiplier={FONT_CAP} style={[styles.badgeText, ff, { color: active ? readableOn(tint) : colors.mutedForeground }]}>{text}</Animated.Text>
|
|
500
505
|
</Animated.View>
|
|
501
506
|
);
|
|
502
507
|
}
|
|
@@ -531,7 +536,7 @@ const styles = StyleSheet.create({
|
|
|
531
536
|
labelRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
|
532
537
|
label: { fontWeight: '400', letterSpacing: -0.1 },
|
|
533
538
|
underline: { position: 'absolute', bottom: 0, height: UNDERLINE },
|
|
534
|
-
badge: { minWidth: 18,
|
|
539
|
+
badge: { minWidth: 18, minHeight: 18, paddingHorizontal: 5, alignItems: 'center', justifyContent: 'center' },
|
|
535
540
|
badgeText: { fontSize: 11, fontWeight: '400', fontVariant: ['tabular-nums'] },
|
|
536
541
|
viewport: { overflow: 'hidden' },
|
|
537
542
|
pager: { flexDirection: 'row', alignItems: 'flex-start' },
|
|
@@ -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
|
};
|