@expokit/cli 0.2.0 → 0.4.0
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 +20 -2
- package/bin/expo-kit.js +219 -44
- package/bin/lib.mjs +110 -1
- package/bin/mcp.mjs +48 -19
- package/package.json +9 -4
- package/registry/_docs/accordion.json +16 -0
- package/registry/_docs/action-sheet.json +28 -0
- package/registry/_docs/alert.json +28 -0
- package/registry/_docs/assistant-app.json +47 -0
- package/registry/_docs/avatar.json +25 -1
- package/registry/_docs/badge.json +36 -5
- package/registry/_docs/bottom-nav.json +30 -6
- package/registry/_docs/bottom-sheet.json +28 -0
- package/registry/_docs/bullet.json +3 -0
- package/registry/_docs/button-3d.json +18 -2
- package/registry/_docs/button.json +16 -0
- package/registry/_docs/calendar.json +21 -5
- package/registry/_docs/card-stack.json +16 -0
- package/registry/_docs/card.json +20 -0
- package/registry/_docs/carousel.json +20 -0
- package/registry/_docs/chat.json +184 -0
- package/registry/_docs/checkbox.json +16 -0
- package/registry/_docs/checkout.json +10 -1
- package/registry/_docs/chip-group.json +16 -0
- package/registry/_docs/confetti.json +23 -3
- package/registry/_docs/coupon.json +16 -0
- package/registry/_docs/cube-pager.json +16 -0
- package/registry/_docs/dashboard.json +3 -0
- package/registry/_docs/date-picker.json +24 -4
- package/registry/_docs/dialog.json +24 -0
- package/registry/_docs/donut-shop.json +88 -0
- package/registry/_docs/drawer.json +32 -4
- package/registry/_docs/fab.json +24 -0
- package/registry/_docs/fintech-app.json +49 -0
- package/registry/_docs/flip-card.json +16 -0
- package/registry/_docs/floating-input.json +25 -5
- package/registry/_docs/food-app.json +47 -0
- package/registry/_docs/gift-card.json +16 -0
- package/registry/_docs/glass-button.json +24 -0
- package/registry/_docs/glass-nav-bar.json +24 -0
- package/registry/_docs/glass-search-bar.json +24 -0
- package/registry/_docs/glass-segmented.json +28 -0
- package/registry/_docs/glass-sheet.json +28 -0
- package/registry/_docs/glass-tab-bar.json +24 -0
- package/registry/_docs/glass.json +17 -1
- package/registry/_docs/grain.json +80 -0
- package/registry/_docs/inbox.json +3 -0
- package/registry/_docs/isometric-bar.json +3 -0
- package/registry/_docs/kit.json +3 -0
- package/registry/_docs/liquidglass-tab.json +3 -0
- package/registry/_docs/list-item.json +16 -0
- package/registry/_docs/loyalty-card.json +16 -0
- package/registry/_docs/morphing-button-group.json +96 -0
- package/registry/_docs/number-ticker.json +16 -0
- package/registry/_docs/offers.json +3 -0
- package/registry/_docs/onboarding.json +3 -0
- package/registry/_docs/orb-reveal.json +132 -0
- package/registry/_docs/otp-input.json +28 -4
- package/registry/_docs/parallax-layers.json +20 -0
- package/registry/_docs/parallax-scroll.json +24 -0
- package/registry/_docs/password-input.json +23 -3
- package/registry/_docs/payment-card.json +16 -0
- package/registry/_docs/pixel-bar.json +3 -0
- package/registry/_docs/polar-area.json +3 -0
- package/registry/_docs/profile.json +3 -0
- package/registry/_docs/progress-steps.json +24 -0
- package/registry/_docs/progress.json +20 -0
- package/registry/_docs/qr-code-button.json +122 -0
- package/registry/_docs/radio-group.json +20 -0
- package/registry/_docs/rating.json +24 -0
- package/registry/_docs/ring-carousel.json +20 -0
- package/registry/_docs/scratch-card.json +20 -0
- package/registry/_docs/search-bar.json +23 -3
- package/registry/_docs/search-results.json +3 -0
- package/registry/_docs/segmented-control.json +20 -0
- package/registry/_docs/select.json +33 -8
- package/registry/_docs/settings.json +9 -0
- package/registry/_docs/sign-in.json +3 -0
- package/registry/_docs/skeleton.json +22 -2
- package/registry/_docs/slide-to-confirm.json +24 -0
- package/registry/_docs/slider.json +27 -7
- package/registry/_docs/spin-wheel.json +24 -0
- package/registry/_docs/spinner.json +41 -11
- package/registry/_docs/stateful-button.json +184 -0
- package/registry/_docs/stepper.json +23 -7
- package/registry/_docs/swipe-deck.json +16 -0
- package/registry/_docs/swipeable-row.json +24 -1
- package/registry/_docs/switch.json +20 -0
- package/registry/_docs/tabs.json +24 -0
- package/registry/_docs/text-animation.json +141 -0
- package/registry/_docs/textarea.json +18 -2
- package/registry/_docs/ticket-card.json +20 -0
- package/registry/_docs/ticket-shape.json +4 -1
- package/registry/_docs/tilt-card.json +20 -4
- package/registry/_docs/time-picker.json +22 -6
- package/registry/_docs/timeline-chart.json +3 -0
- package/registry/_docs/timeline.json +18 -2
- package/registry/_docs/toast.json +32 -8
- package/registry/_docs/treemap.json +3 -0
- package/registry/_docs/waterfall.json +3 -0
- package/registry/_docs/wheel-list.json +20 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +5 -4
- package/registry/accordion/files/src/lib/kit.tsx +8 -5
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +9 -8
- package/registry/action-sheet/files/src/lib/kit.tsx +8 -5
- package/registry/alert/files/src/components/ui/alert.tsx +7 -4
- package/registry/alert/files/src/components/ui/button.tsx +4 -3
- 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 +345 -21
- package/registry/badge/files/src/lib/kit.tsx +8 -5
- package/registry/badge.json +1 -1
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +14 -7
- 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 +4 -3
- package/registry/button/files/src/lib/kit.tsx +8 -5
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +3 -2
- package/registry/button-3d/files/src/lib/kit.tsx +8 -5
- package/registry/calendar/files/src/components/ui/calendar.tsx +9 -5
- package/registry/calendar/files/src/lib/kit.tsx +8 -5
- package/registry/card/files/src/components/ui/card.tsx +8 -6
- package/registry/card/files/src/lib/kit.tsx +8 -5
- package/registry/chat.json +18 -0
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +6 -5
- package/registry/checkbox/files/src/lib/kit.tsx +8 -5
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +4 -2
- 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 +23 -20
- package/registry/coupon/files/src/components/ui/flip-card.tsx +1 -1
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +2 -2
- package/registry/coupon/files/src/lib/kit.tsx +8 -5
- package/registry/date-picker/files/src/components/ui/calendar.tsx +9 -5
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +2 -2
- package/registry/date-picker/files/src/lib/kit.tsx +8 -5
- package/registry/dialog/files/src/components/ui/dialog.tsx +12 -6
- 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 +25 -19
- 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 +1 -1
- package/registry/flip-card/files/src/lib/kit.tsx +8 -5
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +16 -6
- 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 +1 -1
- package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +1 -1
- package/registry/list-item/files/src/components/ui/list-item.tsx +20 -16
- 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 +8 -6
- package/registry/otp-input/files/src/lib/kit.tsx +8 -5
- package/registry/password-input/files/src/components/ui/floating-input.tsx +16 -6
- package/registry/password-input/files/src/components/ui/password-input.tsx +2 -2
- package/registry/password-input/files/src/lib/kit.tsx +8 -5
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +1 -1
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +27 -14
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +2 -2
- package/registry/payment-card/files/src/lib/kit.tsx +8 -5
- package/registry/progress/files/src/components/ui/progress.tsx +8 -5
- package/registry/progress/files/src/lib/kit.tsx +8 -5
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +15 -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 +10 -9
- package/registry/radio-group/files/src/lib/kit.tsx +8 -5
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +13 -11
- package/registry/search-bar/files/src/lib/kit.tsx +8 -5
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +4 -2
- package/registry/segmented-control/files/src/lib/kit.tsx +8 -5
- package/registry/select/files/src/components/ui/select.tsx +3 -3
- 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 +5 -4
- package/registry/slider/files/src/lib/kit.tsx +8 -5
- package/registry/spinner/files/src/components/ui/spinner.tsx +452 -34
- package/registry/spinner/files/src/lib/kit.tsx +8 -5
- package/registry/spinner.json +1 -1
- package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +532 -0
- package/registry/{pagination-dots → stateful-button}/files/src/lib/kit.tsx +8 -5
- package/registry/stateful-button.json +18 -0
- package/registry/stepper/files/src/components/ui/stepper.tsx +8 -4
- package/registry/stepper/files/src/lib/kit.tsx +8 -5
- package/registry/switch/files/src/components/ui/switch.tsx +4 -3
- package/registry/switch/files/src/lib/kit.tsx +8 -5
- package/registry/tabs/files/src/components/ui/tabs.tsx +10 -8
- package/registry/tabs/files/src/lib/kit.tsx +8 -5
- package/registry/text-animation/files/src/components/ui/text-animation.tsx +650 -0
- package/registry/text-animation/files/src/lib/kit.tsx +222 -0
- package/registry/text-animation.json +15 -0
- package/registry/textarea/files/src/components/ui/textarea.tsx +11 -9
- 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 +2 -2
- package/registry/tilt-card/files/src/lib/kit.tsx +8 -5
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +2 -2
- 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 +12 -10
- package/registry/timeline/files/src/lib/kit.tsx +8 -5
- package/registry/toast/files/src/components/ui/button.tsx +4 -3
- package/registry/toast/files/src/components/ui/toast.tsx +13 -11
- package/registry/toast/files/src/lib/kit.tsx +8 -5
- package/schema/components.schema.json +1 -1
- package/registry/_docs/color-swatch.json +0 -66
- package/registry/_docs/kpi-card.json +0 -11
- package/registry/_docs/like-button.json +0 -72
- package/registry/_docs/morph-button.json +0 -108
- package/registry/_docs/pagination-dots.json +0 -88
- package/registry/_docs/shimmer-button.json +0 -78
- package/registry/_docs/tag-input.json +0 -128
- 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.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,20 @@
|
|
|
1
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
AccessibilityInfo,
|
|
4
|
+
Animated,
|
|
5
|
+
Easing,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
Text,
|
|
8
|
+
View,
|
|
9
|
+
type LayoutChangeEvent,
|
|
10
|
+
type StyleProp,
|
|
11
|
+
type ViewStyle,
|
|
12
|
+
} from 'react-native';
|
|
3
13
|
import Svg, { Circle, Path } from 'react-native-svg';
|
|
4
14
|
|
|
5
15
|
import { alpha, isTextNode, motion, readableOn, type Colors, useKit } from '@/lib/kit';
|
|
6
16
|
|
|
7
|
-
export type BadgeIconName = 'bell' | 'message' | 'package' | 'cart' | 'heart';
|
|
17
|
+
export type BadgeIconName = 'bell' | 'message' | 'package' | 'cart' | 'heart' | 'clock' | 'refresh' | 'check' | 'alert';
|
|
8
18
|
|
|
9
19
|
const ICON_STROKE = { strokeWidth: 1.85, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const, fill: 'none' as const };
|
|
10
20
|
|
|
@@ -37,6 +47,20 @@ export function BadgeIcon({ name, color, size = 22 }: { name: BadgeIconName; col
|
|
|
37
47
|
<Path d="M6 6L5 3H2" {...p} />
|
|
38
48
|
</>
|
|
39
49
|
),
|
|
50
|
+
clock: (
|
|
51
|
+
<>
|
|
52
|
+
<Circle cx={12} cy={12} r={8.5} {...p} />
|
|
53
|
+
<Path d="M12 7.5V12l3 2" {...p} />
|
|
54
|
+
</>
|
|
55
|
+
),
|
|
56
|
+
refresh: <Path d="M19 12a7 7 0 1 1-2.05-4.95M19 4.5V8h-3.5" {...p} />,
|
|
57
|
+
check: <Path d="M5 12.5l4.5 4.5L19 7.5" {...p} />,
|
|
58
|
+
alert: (
|
|
59
|
+
<>
|
|
60
|
+
<Circle cx={12} cy={12} r={8.5} {...p} />
|
|
61
|
+
<Path d="M12 7.5v5M12 16.2v.3" {...p} />
|
|
62
|
+
</>
|
|
63
|
+
),
|
|
40
64
|
heart: <Path d="M12 20.5l-1.1-1C6.1 15.1 3 12.2 3 8.6A4.6 4.6 0 0 1 7.6 4c2 0 3.1 1.2 4.4 2.5C13.3 5.2 14.4 4 16.4 4A4.6 4.6 0 0 1 21 8.6c0 3.6-3.1 6.5-7.9 10.9L12 20.5z" {...p} />,
|
|
41
65
|
};
|
|
42
66
|
return (
|
|
@@ -101,6 +125,11 @@ export interface BadgeProps {
|
|
|
101
125
|
shape?: 'pill' | 'rounded';
|
|
102
126
|
/** Pulse the leading dot (dot variant) — "live" states. */
|
|
103
127
|
pulse?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* When the label, icon or colour changes: roll the old label up and the new one in, pop the icon,
|
|
130
|
+
* cross-fade the colours and spring the width (default true). Never animates on mount.
|
|
131
|
+
*/
|
|
132
|
+
animateChanges?: boolean;
|
|
104
133
|
style?: StyleProp<ViewStyle>;
|
|
105
134
|
}
|
|
106
135
|
|
|
@@ -114,7 +143,141 @@ function toneColor(colors: Colors, tone: BadgeTone) {
|
|
|
114
143
|
return { neutral: colors.mutedForeground, accent: colors.accent, success: colors.success, warning: colors.warning, danger: colors.danger }[tone];
|
|
115
144
|
}
|
|
116
145
|
|
|
117
|
-
|
|
146
|
+
function useReduceMotion() {
|
|
147
|
+
const [reduce, setReduce] = useState(false);
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
let alive = true;
|
|
150
|
+
AccessibilityInfo.isReduceMotionEnabled()
|
|
151
|
+
.then((r) => alive && setReduce(r))
|
|
152
|
+
.catch(() => {});
|
|
153
|
+
const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
|
|
154
|
+
return () => {
|
|
155
|
+
alive = false;
|
|
156
|
+
sub.remove();
|
|
157
|
+
};
|
|
158
|
+
}, []);
|
|
159
|
+
return reduce;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
interface Look {
|
|
163
|
+
bg: string;
|
|
164
|
+
fg: string;
|
|
165
|
+
border: string;
|
|
166
|
+
dot: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface Change {
|
|
170
|
+
/** Outgoing label rolling up and out; the current label rolls in under it. */
|
|
171
|
+
text?: { from: string; fg: string; perLetter: boolean; duration: number };
|
|
172
|
+
/** Outgoing icon (null when an icon was just added). */
|
|
173
|
+
icon?: { from: React.ReactNode };
|
|
174
|
+
/** Colours being faded from. */
|
|
175
|
+
look?: Look;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Short Latin labels roll letter by letter; anything else (CJK, emoji, long text) rolls as one piece. */
|
|
179
|
+
const LATIN = /^[A-Za-z0-9À-ɏ .,:;'’&+\-!?#%()/]{1,18}$/;
|
|
180
|
+
/** Roll timing in ms: delay between letters, one letter's travel, and how far the new label trails the old. */
|
|
181
|
+
const STAGGER = 24;
|
|
182
|
+
const ROLL = 320;
|
|
183
|
+
const LEAD = 70;
|
|
184
|
+
|
|
185
|
+
function labelText(node: React.ReactNode): string | null {
|
|
186
|
+
if (!isTextNode(node)) return null;
|
|
187
|
+
return Array.isArray(node) ? node.join('') : String(node);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function sameValue(a: unknown, b: unknown) {
|
|
191
|
+
if (Object.is(a, b)) return true;
|
|
192
|
+
if (!a || !b || typeof a !== 'object' || typeof b !== 'object' || React.isValidElement(a) || React.isValidElement(b)) return false;
|
|
193
|
+
try {
|
|
194
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
195
|
+
} catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Icons compare by type, key and props (ignoring colour), so an inline `<BadgeIcon />` only "changes" with its glyph. */
|
|
201
|
+
function sameIcon(a: React.ReactNode, b: React.ReactNode) {
|
|
202
|
+
if (Object.is(a, b)) return true;
|
|
203
|
+
if (!React.isValidElement(a) || !React.isValidElement(b)) return false;
|
|
204
|
+
if (a.type !== b.type || a.key !== b.key) return false;
|
|
205
|
+
const pa = a.props as Record<string, unknown>;
|
|
206
|
+
const pb = b.props as Record<string, unknown>;
|
|
207
|
+
for (const k of new Set([...Object.keys(pa), ...Object.keys(pb)])) {
|
|
208
|
+
if (k !== 'color' && !sameValue(pa[k], pb[k])) return false;
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Eased move from `from` to `to` while `v` runs start → end (held outside), sampled for the native driver. */
|
|
214
|
+
function span(v: Animated.Value, start: number, end: number, from: number, to: number, ease: (t: number) => number) {
|
|
215
|
+
const inputRange: number[] = [];
|
|
216
|
+
const outputRange: number[] = [];
|
|
217
|
+
for (let k = 0; k <= 10; k++) {
|
|
218
|
+
inputRange.push(start + ((end - start) * k) / 10);
|
|
219
|
+
outputRange.push(from + (to - from) * ease(k / 10));
|
|
220
|
+
}
|
|
221
|
+
return v.interpolate({ inputRange, outputRange, extrapolate: 'clamp' });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const easeIn = Easing.in(Easing.cubic);
|
|
225
|
+
const easeBack = Easing.out(Easing.back(1.7));
|
|
226
|
+
const easeLinear = Easing.linear;
|
|
227
|
+
|
|
228
|
+
/** One side of a label roll: letters (or the whole label) slide up out of / up into the clipped slot. */
|
|
229
|
+
function RollingLabel({
|
|
230
|
+
text,
|
|
231
|
+
color,
|
|
232
|
+
fontSize,
|
|
233
|
+
progress,
|
|
234
|
+
out,
|
|
235
|
+
perLetter,
|
|
236
|
+
duration,
|
|
237
|
+
}: {
|
|
238
|
+
text: string;
|
|
239
|
+
color: string;
|
|
240
|
+
fontSize: number;
|
|
241
|
+
progress: Animated.Value;
|
|
242
|
+
out: boolean;
|
|
243
|
+
perLetter: boolean;
|
|
244
|
+
duration: number;
|
|
245
|
+
}) {
|
|
246
|
+
const { font } = useKit();
|
|
247
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
248
|
+
const pieces = perLetter ? [...text].map((ch) => (ch === ' ' ? ' ' : ch)) : [text];
|
|
249
|
+
const distance = Math.ceil(fontSize * 1.35);
|
|
250
|
+
return (
|
|
251
|
+
<View pointerEvents="none" style={styles.roll}>
|
|
252
|
+
{pieces.map((piece, i) => {
|
|
253
|
+
const begin = ((out ? 0 : LEAD) + i * STAGGER) / duration;
|
|
254
|
+
const travel = (out ? ROLL * 0.7 : ROLL) / duration;
|
|
255
|
+
const end = Math.min(1, begin + travel);
|
|
256
|
+
const fade = Math.min(1, begin + travel * 0.5);
|
|
257
|
+
return (
|
|
258
|
+
<Animated.View
|
|
259
|
+
key={i}
|
|
260
|
+
style={{
|
|
261
|
+
opacity: out ? span(progress, begin, fade, 1, 0, easeLinear) : span(progress, begin, fade, 0, 1, easeLinear),
|
|
262
|
+
transform: [
|
|
263
|
+
{ translateY: out ? span(progress, begin, end, 0, -distance, easeIn) : span(progress, begin, end, distance, 0, easeBack) },
|
|
264
|
+
],
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
<Text numberOfLines={1} allowFontScaling={false} style={[styles.label, ff, { color, fontSize }]}>
|
|
268
|
+
{piece}
|
|
269
|
+
</Text>
|
|
270
|
+
</Animated.View>
|
|
271
|
+
);
|
|
272
|
+
})}
|
|
273
|
+
</View>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Small status label: solid, soft, outline or dot — five tones, three sizes. When its text, icon or
|
|
279
|
+
* tone changes, the label rolls over, the icon pops, the colour cross-fades and the pill springs to size.
|
|
280
|
+
*/
|
|
118
281
|
export function Badge({
|
|
119
282
|
children,
|
|
120
283
|
variant = 'soft',
|
|
@@ -124,9 +287,12 @@ export function Badge({
|
|
|
124
287
|
icon,
|
|
125
288
|
shape = 'pill',
|
|
126
289
|
pulse,
|
|
290
|
+
animateChanges = true,
|
|
127
291
|
style,
|
|
128
292
|
}: BadgeProps) {
|
|
129
|
-
const { colors, radius, dark } = useKit();
|
|
293
|
+
const { colors, radius, dark, font } = useKit();
|
|
294
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
295
|
+
const reduce = useReduceMotion();
|
|
130
296
|
const s = SIZES[size];
|
|
131
297
|
const c = color ?? toneColor(colors, tone);
|
|
132
298
|
const neutral = !color && tone === 'neutral';
|
|
@@ -155,29 +321,167 @@ export function Badge({
|
|
|
155
321
|
};
|
|
156
322
|
const { bg, fg, border } = look[variant];
|
|
157
323
|
const dotColor = neutral ? colors.mutedForeground : c;
|
|
324
|
+
const text = labelText(children);
|
|
325
|
+
|
|
326
|
+
// Change tracking: diff against the last render and start a transition — never on mount.
|
|
327
|
+
const [last, setLast] = useState({ seq: 0, text, icon, look: { bg, fg, border, dot: dotColor } as Look });
|
|
328
|
+
const [change, setChange] = useState<Change | null>(null);
|
|
329
|
+
// While set, the pill keeps this width (number), then springs (Animated.Value) to the new content.
|
|
330
|
+
const [pin, setPin] = useState<number | Animated.Value | null>(null);
|
|
331
|
+
const natural = useRef<number | null>(null);
|
|
332
|
+
const rowWidth = useRef<number | null>(null);
|
|
333
|
+
const widthV = useRef(new Animated.Value(0)).current;
|
|
334
|
+
const flat = flattenView(style);
|
|
335
|
+
const canSize = flat.width == null && flat.minWidth == null && flat.flex == null && flat.flexGrow == null && flat.alignSelf !== 'stretch';
|
|
336
|
+
|
|
337
|
+
const textChanged = text !== last.text;
|
|
338
|
+
const iconChanged = !sameIcon(icon, last.icon);
|
|
339
|
+
const lookChanged = bg !== last.look.bg || fg !== last.look.fg || border !== last.look.border || dotColor !== last.look.dot;
|
|
340
|
+
if (textChanged || iconChanged || lookChanged) {
|
|
341
|
+
setLast({ seq: last.seq + 1, text, icon, look: { bg, fg, border, dot: dotColor } });
|
|
342
|
+
const next: Change = {};
|
|
343
|
+
if (animateChanges && !reduce) {
|
|
344
|
+
if (textChanged && last.text != null && text != null) {
|
|
345
|
+
const perLetter = LATIN.test(last.text) && LATIN.test(text);
|
|
346
|
+
const n = perLetter ? Math.max(last.text.length, text.length) : 1;
|
|
347
|
+
next.text = { from: last.text, fg: last.look.fg, perLetter, duration: LEAD + (n - 1) * STAGGER + ROLL };
|
|
348
|
+
}
|
|
349
|
+
if (iconChanged && icon != null) next.icon = { from: last.icon ?? null };
|
|
350
|
+
if (lookChanged) next.look = last.look;
|
|
351
|
+
}
|
|
352
|
+
const animated = !!(next.text || next.icon || next.look);
|
|
353
|
+
setChange(animated ? next : null);
|
|
354
|
+
setPin(animated && (textChanged || iconChanged) && canSize && natural.current != null ? natural.current : null);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Fresh values per change, so the first frame of a transition already sits at its start.
|
|
358
|
+
const anim = useMemo(
|
|
359
|
+
() => ({ seq: last.seq, roll: new Animated.Value(0), pop: new Animated.Value(0), mix: new Animated.Value(0) }),
|
|
360
|
+
[last.seq]
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
if (!change) return;
|
|
365
|
+
const runs: Animated.CompositeAnimation[] = [];
|
|
366
|
+
if (change.text) {
|
|
367
|
+
runs.push(Animated.timing(anim.roll, { toValue: 1, duration: change.text.duration, easing: easeLinear, useNativeDriver: true }));
|
|
368
|
+
}
|
|
369
|
+
if (change.icon) runs.push(Animated.spring(anim.pop, { toValue: 1, useNativeDriver: true, ...motion.bouncy }));
|
|
370
|
+
if (change.look) {
|
|
371
|
+
runs.push(Animated.timing(anim.mix, { toValue: 1, duration: motion.normal, easing: Easing.out(Easing.quad), useNativeDriver: false }));
|
|
372
|
+
}
|
|
373
|
+
let left = runs.length;
|
|
374
|
+
runs.forEach((r) =>
|
|
375
|
+
r.start(({ finished }) => {
|
|
376
|
+
left -= 1;
|
|
377
|
+
if (finished && left === 0) setChange((cur) => (cur === change ? null : cur));
|
|
378
|
+
})
|
|
379
|
+
);
|
|
380
|
+
return () => runs.forEach((r) => r.stop());
|
|
381
|
+
}, [change, anim]);
|
|
382
|
+
|
|
383
|
+
const onRoot = (e: LayoutChangeEvent) => {
|
|
384
|
+
if (pin == null) natural.current = e.nativeEvent.layout.width;
|
|
385
|
+
};
|
|
386
|
+
// The icon + label row remounts on every change, so this always reports the new content's width.
|
|
387
|
+
const onRow = (e: LayoutChangeEvent) => {
|
|
388
|
+
const w = e.nativeEvent.layout.width;
|
|
389
|
+
const prev = rowWidth.current;
|
|
390
|
+
rowWidth.current = w;
|
|
391
|
+
if (typeof pin !== 'number') return;
|
|
392
|
+
if (prev == null || Math.abs(w - prev) < 0.5) {
|
|
393
|
+
setPin(null);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const to = Math.max(0, pin + (w - prev));
|
|
397
|
+
widthV.setValue(pin);
|
|
398
|
+
natural.current = to;
|
|
399
|
+
setPin(widthV);
|
|
400
|
+
Animated.spring(widthV, { toValue: to, useNativeDriver: false, ...motion.snappy }).start(({ finished }) => {
|
|
401
|
+
if (finished) setPin((p) => (p === widthV ? null : p));
|
|
402
|
+
});
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const from = change?.look;
|
|
406
|
+
const mix = (a: string, b: string) => (from ? anim.mix.interpolate({ inputRange: [0, 1], outputRange: [a, b] }) : b);
|
|
407
|
+
const roll = change?.text;
|
|
408
|
+
|
|
409
|
+
let label: React.ReactNode = children;
|
|
410
|
+
if (text != null) {
|
|
411
|
+
label = (
|
|
412
|
+
<View style={roll ? styles.clip : undefined}>
|
|
413
|
+
<Animated.Text
|
|
414
|
+
numberOfLines={1}
|
|
415
|
+
allowFontScaling={false}
|
|
416
|
+
style={[styles.label, ff, { color: from && !roll ? mix(from.fg, fg) : fg, fontSize: s.font }, roll ? styles.hidden : null]}
|
|
417
|
+
>
|
|
418
|
+
{text}
|
|
419
|
+
</Animated.Text>
|
|
420
|
+
{roll ? (
|
|
421
|
+
<>
|
|
422
|
+
<RollingLabel text={roll.from} color={roll.fg} fontSize={s.font} progress={anim.roll} out perLetter={roll.perLetter} duration={roll.duration} />
|
|
423
|
+
<RollingLabel text={text} color={fg} fontSize={s.font} progress={anim.roll} out={false} perLetter={roll.perLetter} duration={roll.duration} />
|
|
424
|
+
</>
|
|
425
|
+
) : null}
|
|
426
|
+
</View>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
158
429
|
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
430
|
+
const pop = anim.pop;
|
|
431
|
+
const iconNode =
|
|
432
|
+
icon != null && change?.icon ? (
|
|
433
|
+
<View>
|
|
434
|
+
{change.icon.from != null ? (
|
|
435
|
+
<Animated.View
|
|
436
|
+
pointerEvents="none"
|
|
437
|
+
style={[
|
|
438
|
+
styles.center,
|
|
439
|
+
{
|
|
440
|
+
opacity: pop.interpolate({ inputRange: [0, 0.4], outputRange: [1, 0], extrapolate: 'clamp' }),
|
|
441
|
+
transform: [
|
|
442
|
+
{ scale: pop.interpolate({ inputRange: [0, 0.5], outputRange: [1, 0.3], extrapolate: 'clamp' }) },
|
|
443
|
+
{ rotate: pop.interpolate({ inputRange: [0, 0.5], outputRange: ['0deg', '100deg'], extrapolate: 'clamp' }) },
|
|
444
|
+
],
|
|
445
|
+
},
|
|
446
|
+
]}
|
|
447
|
+
>
|
|
448
|
+
{change.icon.from}
|
|
449
|
+
</Animated.View>
|
|
450
|
+
) : null}
|
|
451
|
+
<Animated.View
|
|
452
|
+
style={{
|
|
453
|
+
opacity: pop.interpolate({ inputRange: [0, 0.35], outputRange: [0, 1], extrapolate: 'clamp' }),
|
|
454
|
+
transform: [
|
|
455
|
+
{ scale: pop.interpolate({ inputRange: [0, 1], outputRange: [0.3, 1] }) },
|
|
456
|
+
{ rotate: pop.interpolate({ inputRange: [0, 1], outputRange: ['-100deg', '0deg'] }) },
|
|
457
|
+
],
|
|
458
|
+
}}
|
|
459
|
+
>
|
|
460
|
+
{icon}
|
|
461
|
+
</Animated.View>
|
|
462
|
+
</View>
|
|
164
463
|
) : (
|
|
165
|
-
|
|
464
|
+
icon
|
|
166
465
|
);
|
|
167
466
|
|
|
168
467
|
return (
|
|
169
|
-
<View
|
|
468
|
+
<Animated.View
|
|
469
|
+
accessible={text != null ? true : undefined}
|
|
170
470
|
accessibilityRole="text"
|
|
471
|
+
accessibilityLabel={text ?? undefined}
|
|
472
|
+
accessibilityLiveRegion={animateChanges && text != null ? 'polite' : undefined}
|
|
473
|
+
onLayout={animateChanges ? onRoot : undefined}
|
|
171
474
|
style={[
|
|
172
475
|
styles.base,
|
|
173
476
|
{
|
|
174
477
|
height: s.h,
|
|
175
478
|
paddingHorizontal: s.px,
|
|
176
479
|
gap: s.gap,
|
|
177
|
-
backgroundColor: bg,
|
|
178
|
-
borderColor: border,
|
|
480
|
+
backgroundColor: from ? mix(from.bg, bg) : bg,
|
|
481
|
+
borderColor: from ? mix(from.border, border) : border,
|
|
179
482
|
borderRadius: shape === 'pill' ? radius.capsule(s.h) : radius.sm,
|
|
180
483
|
},
|
|
484
|
+
pin != null ? { width: pin, overflow: 'hidden' as const } : null,
|
|
181
485
|
style,
|
|
182
486
|
]}
|
|
183
487
|
>
|
|
@@ -196,12 +500,15 @@ export function Badge({
|
|
|
196
500
|
]}
|
|
197
501
|
/>
|
|
198
502
|
) : null}
|
|
199
|
-
|
|
503
|
+
{/* Colour fades on the JS driver, so it lives on this view — the pulse above owns the native transforms. */}
|
|
504
|
+
<Animated.View style={[styles.fill, { borderRadius: s.dot, backgroundColor: from ? mix(from.dot, dotColor) : dotColor }]} />
|
|
200
505
|
</View>
|
|
201
506
|
) : null}
|
|
202
|
-
{
|
|
203
|
-
|
|
204
|
-
|
|
507
|
+
<View key={last.seq} onLayout={animateChanges ? onRow : undefined} style={[styles.row, { gap: s.gap }]}>
|
|
508
|
+
{iconNode}
|
|
509
|
+
{label}
|
|
510
|
+
</View>
|
|
511
|
+
</Animated.View>
|
|
205
512
|
);
|
|
206
513
|
}
|
|
207
514
|
|
|
@@ -236,7 +543,8 @@ export function NotificationBadge({
|
|
|
236
543
|
offset = [0, 0],
|
|
237
544
|
style,
|
|
238
545
|
}: NotificationBadgeProps) {
|
|
239
|
-
const { colors } = useKit();
|
|
546
|
+
const { colors, font } = useKit();
|
|
547
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
240
548
|
const c = color ?? toneColor(colors, tone);
|
|
241
549
|
const fg = !color && tone === 'accent' ? colors.accentForeground : readableOn(c);
|
|
242
550
|
const visible = count > 0 || !!showZero;
|
|
@@ -252,7 +560,7 @@ export function NotificationBadge({
|
|
|
252
560
|
Animated.spring(show, { toValue: visible ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
|
|
253
561
|
if (visible && prev.current !== count && prev.current > 0) {
|
|
254
562
|
pop.setValue(1.35);
|
|
255
|
-
Animated.spring(pop, { toValue: 1, useNativeDriver: true, damping:
|
|
563
|
+
Animated.spring(pop, { toValue: 1, useNativeDriver: true, damping: 29, stiffness: 320, mass: 0.6 }).start();
|
|
256
564
|
}
|
|
257
565
|
prev.current = count;
|
|
258
566
|
}, [count, visible, show, pop]);
|
|
@@ -283,7 +591,7 @@ export function NotificationBadge({
|
|
|
283
591
|
]}
|
|
284
592
|
>
|
|
285
593
|
{dot ? null : (
|
|
286
|
-
<Text allowFontScaling={false} numberOfLines={1} style={[styles.count, { color: fg, fontSize: size === 'sm' ? 10 : 11.5 }]}>
|
|
594
|
+
<Text allowFontScaling={false} numberOfLines={1} style={[styles.count, ff, { color: fg, fontSize: size === 'sm' ? 10 : 11.5 }]}>
|
|
287
595
|
{text}
|
|
288
596
|
</Text>
|
|
289
597
|
)}
|
|
@@ -306,9 +614,25 @@ export function NotificationBadge({
|
|
|
306
614
|
);
|
|
307
615
|
}
|
|
308
616
|
|
|
617
|
+
/**
|
|
618
|
+
* The user's root style as one object. Not StyleSheet.flatten: with TypeScript 6 its RN 0.88 signature
|
|
619
|
+
* rejects a plain StyleProp<ViewStyle> once expo/types adds web-only keys to ViewStyle.
|
|
620
|
+
*/
|
|
621
|
+
function flattenView(style: StyleProp<ViewStyle>): ViewStyle {
|
|
622
|
+
if (!style || typeof style !== 'object') return {};
|
|
623
|
+
if (Array.isArray(style)) return Object.assign({}, ...style.map((s) => flattenView(s as StyleProp<ViewStyle>)));
|
|
624
|
+
return style as ViewStyle;
|
|
625
|
+
}
|
|
626
|
+
|
|
309
627
|
const styles = StyleSheet.create({
|
|
310
628
|
base: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', borderWidth: 1 },
|
|
311
629
|
label: { fontWeight: '400', letterSpacing: 0.1 },
|
|
630
|
+
row: { flexDirection: 'row', alignItems: 'center' },
|
|
631
|
+
// Clips the roll; the 2pt slack (cancelled by negative margins) keeps overshoot and unkerned letters whole.
|
|
632
|
+
clip: { overflow: 'hidden', padding: 2, margin: -2 },
|
|
633
|
+
hidden: { opacity: 0 },
|
|
634
|
+
roll: { position: 'absolute', top: 2, left: 2, flexDirection: 'row' },
|
|
635
|
+
center: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, alignItems: 'center', justifyContent: 'center' },
|
|
312
636
|
fill: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
|
|
313
637
|
anchor: { alignSelf: 'flex-start' },
|
|
314
638
|
bubble: { alignItems: 'center', justifyContent: 'center', borderWidth: 2 },
|
|
@@ -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
|
};
|
package/registry/badge.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "badge",
|
|
3
3
|
"type": "component",
|
|
4
4
|
"title": "Badge",
|
|
5
|
-
"description": "Status labels in solid, soft, outline and dot styles across five tones —
|
|
5
|
+
"description": "Status labels in solid, soft, outline and dot styles across five tones — when the label changes it rolls over letter by letter, the icon pops, the colour cross-fades and the pill springs to its new width. Plus NotificationBadge with BadgeIconTile/BadgeIcon anchors (or your own children), spring-in counts, and pop on change.",
|
|
6
6
|
"tier": "free",
|
|
7
7
|
"dependencies": [
|
|
8
8
|
"react-native-svg"
|
|
@@ -109,8 +109,8 @@ const PILL_H = 42;
|
|
|
109
109
|
/** Flex weight of the active item in the `pill` look (the others are 1). */
|
|
110
110
|
const PILL_GROW = 2.3;
|
|
111
111
|
// Liquid move: the edge in the direction of travel leads, the other one trails behind.
|
|
112
|
-
const LEAD = { damping:
|
|
113
|
-
const TRAIL = { damping:
|
|
112
|
+
const LEAD = { damping: 31, stiffness: 320, mass: 0.7 };
|
|
113
|
+
const TRAIL = { damping: 24, stiffness: 150, mass: 0.9 };
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
116
|
* Bottom navigation in five looks — minimal dot, floating capsule, expanding pill, notched centre
|
|
@@ -444,6 +444,8 @@ interface NavButtonProps {
|
|
|
444
444
|
}
|
|
445
445
|
|
|
446
446
|
function NavButton({ item, active, look, tint, idle, surface, fab, showLabels, reduce, onPress }: NavButtonProps) {
|
|
447
|
+
const { font } = useKit();
|
|
448
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
447
449
|
const press = useRef(new Animated.Value(1)).current; // press squish (native)
|
|
448
450
|
const pop = useRef(new Animated.Value(active ? 1 : 0)).current; // active state (native)
|
|
449
451
|
const grow = useRef(new Animated.Value(active ? 1 : 0)).current; // pill width (JS — layout)
|
|
@@ -512,6 +514,7 @@ function NavButton({ item, active, look, tint, idle, surface, fab, showLabels, r
|
|
|
512
514
|
numberOfLines={1}
|
|
513
515
|
style={[
|
|
514
516
|
styles.pillLabel,
|
|
517
|
+
ff,
|
|
515
518
|
{
|
|
516
519
|
width: labelW + 1,
|
|
517
520
|
color: tint,
|
|
@@ -528,7 +531,7 @@ function NavButton({ item, active, look, tint, idle, surface, fab, showLabels, r
|
|
|
528
531
|
{/* Invisible measurer: the label's natural width, so it never ellipsizes while growing. */}
|
|
529
532
|
{showLabels ? (
|
|
530
533
|
<View pointerEvents="none" style={styles.measure} accessibilityElementsHidden importantForAccessibility="no-hide-descendants">
|
|
531
|
-
<Text style={[styles.pillLabel, styles.selfStart]} onLayout={(e) => setLabelW(Math.ceil(e.nativeEvent.layout.width))}>
|
|
534
|
+
<Text style={[styles.pillLabel, styles.selfStart, ff]} onLayout={(e) => setLabelW(Math.ceil(e.nativeEvent.layout.width))}>
|
|
532
535
|
{item.label}
|
|
533
536
|
</Text>
|
|
534
537
|
</View>
|
|
@@ -561,6 +564,7 @@ function NavButton({ item, active, look, tint, idle, surface, fab, showLabels, r
|
|
|
561
564
|
style={[
|
|
562
565
|
styles.label,
|
|
563
566
|
styles.floatLabel,
|
|
567
|
+
ff,
|
|
564
568
|
{ color: tint, opacity: fadeIn, transform: [{ translateY: pop.interpolate({ inputRange: [0, 1], outputRange: [6, 0] }) }] },
|
|
565
569
|
]}
|
|
566
570
|
>
|
|
@@ -587,7 +591,7 @@ function NavButton({ item, active, look, tint, idle, surface, fab, showLabels, r
|
|
|
587
591
|
{badge}
|
|
588
592
|
</Animated.View>
|
|
589
593
|
{showLabels ? (
|
|
590
|
-
<Text numberOfLines={1} style={[styles.label, { color: active ? tint : idle }]}>
|
|
594
|
+
<Text numberOfLines={1} style={[styles.label, ff, { color: active ? tint : idle }]}>
|
|
591
595
|
{item.label}
|
|
592
596
|
</Text>
|
|
593
597
|
) : null}
|
|
@@ -653,6 +657,8 @@ function CentreAction({
|
|
|
653
657
|
showLabel: boolean;
|
|
654
658
|
onPress: () => void;
|
|
655
659
|
}) {
|
|
660
|
+
const { font } = useKit();
|
|
661
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
656
662
|
const press = useRef(new Animated.Value(1)).current;
|
|
657
663
|
const pressTo = (v: number) =>
|
|
658
664
|
Animated.spring(press, { toValue: v, useNativeDriver: true, ...(v < 1 ? motion.snappy : motion.bouncy) }).start();
|
|
@@ -671,7 +677,7 @@ function CentreAction({
|
|
|
671
677
|
</FabFace>
|
|
672
678
|
</Animated.View>
|
|
673
679
|
{showLabel ? (
|
|
674
|
-
<Text numberOfLines={1} style={[styles.label, styles.abs, { top: labelTop, color: labelColor }]}>
|
|
680
|
+
<Text numberOfLines={1} style={[styles.label, styles.abs, ff, { top: labelTop, color: labelColor }]}>
|
|
675
681
|
{item.label}
|
|
676
682
|
</Text>
|
|
677
683
|
) : null}
|
|
@@ -758,7 +764,8 @@ function FabFace({
|
|
|
758
764
|
|
|
759
765
|
/** Dot or count. Pops in with a spring when it appears or its value changes. */
|
|
760
766
|
function Badge({ value, ring, reduce }: { value: true | number; ring: string; reduce: boolean }) {
|
|
761
|
-
const { colors, radius } = useKit();
|
|
767
|
+
const { colors, radius, font } = useKit();
|
|
768
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
762
769
|
const s = useRef(new Animated.Value(reduce ? 1 : 0.3)).current;
|
|
763
770
|
useEffect(() => {
|
|
764
771
|
if (reduce) {
|
|
@@ -784,7 +791,7 @@ function Badge({ value, ring, reduce }: { value: true | number; ring: string; re
|
|
|
784
791
|
},
|
|
785
792
|
]}
|
|
786
793
|
>
|
|
787
|
-
{!dot && <Text style={[styles.badgeText, { color: colors.dangerForeground }]}>{text}</Text>}
|
|
794
|
+
{!dot && <Text style={[styles.badgeText, ff, { color: colors.dangerForeground }]}>{text}</Text>}
|
|
788
795
|
</Animated.View>
|
|
789
796
|
);
|
|
790
797
|
}
|
|
@@ -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,
|
|
@@ -75,7 +75,7 @@ export interface BottomSheetProps {
|
|
|
75
75
|
|
|
76
76
|
const DEFAULT_BOTTOM_INSET = Platform.select({ ios: 20, android: 12, default: 16 }) ?? 16;
|
|
77
77
|
/** Kit gentle, firmed up — glides between detents without bouncing past them. */
|
|
78
|
-
const SNAP_SPRING = { ...motion.gentle, damping:
|
|
78
|
+
const SNAP_SPRING = { ...motion.gentle, damping: 29, stiffness: 220, mass: 0.9 };
|
|
79
79
|
/** Extra sheet below the screen edge, so pulling up past the top snap never shows a gap. */
|
|
80
80
|
const OVERDRAG = 120;
|
|
81
81
|
/** Never let the sheet's top go closer than this to the top of the screen. */
|
|
@@ -122,7 +122,8 @@ export function BottomSheet({
|
|
|
122
122
|
style,
|
|
123
123
|
contentStyle,
|
|
124
124
|
}: BottomSheetProps) {
|
|
125
|
-
const { colors, radius, dark } = useKit();
|
|
125
|
+
const { colors, radius, dark, font } = useKit();
|
|
126
|
+
const ff = font.family ? { fontFamily: font.family } : null;
|
|
126
127
|
const reduceMotion = useReduceMotion();
|
|
127
128
|
const { height: screen } = useWindowDimensions();
|
|
128
129
|
|
|
@@ -164,7 +165,7 @@ export function BottomSheet({
|
|
|
164
165
|
const shown = useRef(false); // auto: slid in once measured
|
|
165
166
|
const index = useRef(initialSnap);
|
|
166
167
|
const scrollY = useRef(0);
|
|
167
|
-
const rootRef = useRef<View
|
|
168
|
+
const rootRef = useRef<ComponentRef<typeof View>>(null);
|
|
168
169
|
|
|
169
170
|
const bodyDrags = dragFromContent ?? scrollable;
|
|
170
171
|
const live = useRef({ stops, closed, max, dismissible, onClose, onSnapChange, haptics, reduceMotion, scrollable, bodyDrags, expanded });
|
|
@@ -473,12 +474,12 @@ export function BottomSheet({
|
|
|
473
474
|
<View style={[styles.header, !handle && { paddingTop: 20 }]}>
|
|
474
475
|
<View style={styles.headerText}>
|
|
475
476
|
{title ? (
|
|
476
|
-
<Text accessibilityRole="header" style={[styles.title, { color: colors.foreground }]} numberOfLines={1}>
|
|
477
|
+
<Text accessibilityRole="header" style={[styles.title, ff, { color: colors.foreground }]} numberOfLines={1}>
|
|
477
478
|
{title}
|
|
478
479
|
</Text>
|
|
479
480
|
) : null}
|
|
480
481
|
{subtitle ? (
|
|
481
|
-
<Text style={[styles.subtitle, { color: colors.mutedForeground }]} numberOfLines={1}>
|
|
482
|
+
<Text style={[styles.subtitle, ff, { color: colors.mutedForeground }]} numberOfLines={1}>
|
|
482
483
|
{subtitle}
|
|
483
484
|
</Text>
|
|
484
485
|
) : null}
|