@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.
Files changed (188) hide show
  1. package/README.md +8 -4
  2. package/bin/expo-kit.js +137 -24
  3. package/bin/lib.mjs +110 -1
  4. package/bin/mcp.mjs +40 -18
  5. package/package.json +2 -2
  6. package/registry/_docs/accordion.json +1 -1
  7. package/registry/_docs/assistant-app.json +47 -0
  8. package/registry/_docs/avatar.json +6 -6
  9. package/registry/_docs/badge.json +4 -4
  10. package/registry/_docs/bottom-nav.json +6 -6
  11. package/registry/_docs/bottom-sheet.json +3 -3
  12. package/registry/_docs/bullet.json +6 -0
  13. package/registry/_docs/button-3d.json +3 -3
  14. package/registry/_docs/button.json +2 -2
  15. package/registry/_docs/calendar.json +5 -5
  16. package/registry/_docs/card.json +1 -1
  17. package/registry/_docs/checkout.json +7 -1
  18. package/registry/_docs/chip-group.json +1 -1
  19. package/registry/_docs/confetti.json +3 -3
  20. package/registry/_docs/date-picker.json +4 -4
  21. package/registry/_docs/donut-shop.json +88 -0
  22. package/registry/_docs/drawer.json +4 -4
  23. package/registry/_docs/fintech-app.json +49 -0
  24. package/registry/_docs/flip-card.json +4 -4
  25. package/registry/_docs/floating-input.json +5 -5
  26. package/registry/_docs/food-app.json +47 -0
  27. package/registry/_docs/glass.json +1 -1
  28. package/registry/_docs/grain.json +80 -0
  29. package/registry/_docs/isometric-bar.json +6 -0
  30. package/registry/_docs/liquidglass-tab.json +1 -1
  31. package/registry/_docs/morphing-button-group.json +96 -0
  32. package/registry/_docs/orb-reveal.json +132 -0
  33. package/registry/_docs/otp-input.json +4 -4
  34. package/registry/_docs/password-input.json +3 -3
  35. package/registry/_docs/payment-card.json +3 -3
  36. package/registry/_docs/pixel-bar.json +6 -0
  37. package/registry/_docs/polar-area.json +6 -0
  38. package/registry/_docs/progress-steps.json +4 -4
  39. package/registry/_docs/progress.json +2 -2
  40. package/registry/_docs/qr-code-button.json +122 -0
  41. package/registry/_docs/radio-group.json +1 -1
  42. package/registry/_docs/search-bar.json +4 -4
  43. package/registry/_docs/segmented-control.json +1 -1
  44. package/registry/_docs/select.json +7 -7
  45. package/registry/_docs/settings.json +6 -0
  46. package/registry/_docs/skeleton.json +3 -3
  47. package/registry/_docs/slider.json +6 -6
  48. package/registry/_docs/spinner.json +3 -3
  49. package/registry/_docs/stateful-button.json +1 -1
  50. package/registry/_docs/stepper.json +7 -7
  51. package/registry/_docs/tabs.json +2 -2
  52. package/registry/_docs/text-animation.json +4 -4
  53. package/registry/_docs/textarea.json +3 -3
  54. package/registry/_docs/ticket-shape.json +1 -1
  55. package/registry/_docs/tilt-card.json +4 -4
  56. package/registry/_docs/time-picker.json +6 -6
  57. package/registry/_docs/timeline-chart.json +6 -0
  58. package/registry/_docs/timeline.json +6 -6
  59. package/registry/_docs/toast.json +8 -8
  60. package/registry/_docs/treemap.json +6 -0
  61. package/registry/_docs/waterfall.json +6 -0
  62. package/registry/accordion/files/src/components/ui/accordion.tsx +36 -7
  63. package/registry/accordion/files/src/lib/kit.tsx +8 -5
  64. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +12 -8
  65. package/registry/action-sheet/files/src/lib/kit.tsx +8 -5
  66. package/registry/alert/files/src/components/ui/alert.tsx +11 -5
  67. package/registry/alert/files/src/components/ui/button.tsx +35 -6
  68. package/registry/alert/files/src/lib/kit.tsx +8 -5
  69. package/registry/assistant-app.json +49 -0
  70. package/registry/avatar/files/src/components/ui/avatar.tsx +7 -4
  71. package/registry/avatar/files/src/lib/kit.tsx +8 -5
  72. package/registry/badge/files/src/components/ui/badge.tsx +23 -9
  73. package/registry/badge/files/src/lib/kit.tsx +8 -5
  74. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +21 -8
  75. package/registry/bottom-nav/files/src/lib/kit.tsx +8 -5
  76. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +7 -6
  77. package/registry/bottom-sheet/files/src/lib/kit.tsx +8 -5
  78. package/registry/button/files/src/components/ui/button.tsx +35 -6
  79. package/registry/button/files/src/lib/kit.tsx +8 -5
  80. package/registry/button-3d/files/src/components/ui/button-3d.tsx +35 -7
  81. package/registry/button-3d/files/src/lib/kit.tsx +8 -5
  82. package/registry/calendar/files/src/components/ui/calendar.tsx +68 -19
  83. package/registry/calendar/files/src/lib/kit.tsx +8 -5
  84. package/registry/card/files/src/components/ui/card.tsx +32 -10
  85. package/registry/card/files/src/lib/kit.tsx +8 -5
  86. package/registry/checkbox/files/src/components/ui/checkbox.tsx +31 -8
  87. package/registry/checkbox/files/src/lib/kit.tsx +8 -5
  88. package/registry/chip-group/files/src/components/ui/chip-group.tsx +39 -7
  89. package/registry/chip-group/files/src/lib/kit.tsx +8 -5
  90. package/registry/confetti/files/src/lib/kit.tsx +8 -5
  91. package/registry/coupon/files/src/components/ui/coupon.tsx +55 -28
  92. package/registry/coupon/files/src/components/ui/flip-card.tsx +23 -4
  93. package/registry/coupon/files/src/components/ui/tilt-card.tsx +59 -18
  94. package/registry/coupon/files/src/lib/kit.tsx +8 -5
  95. package/registry/date-picker/files/src/components/ui/calendar.tsx +68 -19
  96. package/registry/date-picker/files/src/components/ui/date-picker.tsx +98 -44
  97. package/registry/date-picker/files/src/lib/kit.tsx +8 -5
  98. package/registry/dialog/files/src/components/ui/dialog.tsx +19 -9
  99. package/registry/dialog/files/src/lib/kit.tsx +8 -5
  100. package/registry/donut-shop.json +18 -0
  101. package/registry/drawer/files/src/components/ui/drawer.tsx +31 -21
  102. package/registry/drawer/files/src/lib/kit.tsx +8 -5
  103. package/registry/fintech-app.json +57 -0
  104. package/registry/flip-card/files/src/components/ui/flip-card.tsx +23 -4
  105. package/registry/flip-card/files/src/lib/kit.tsx +8 -5
  106. package/registry/floating-input/files/src/components/ui/floating-input.tsx +91 -32
  107. package/registry/floating-input/files/src/lib/kit.tsx +8 -5
  108. package/registry/food-app.json +55 -0
  109. package/registry/grain/files/src/components/ui/grain.tsx +113 -0
  110. package/registry/{tag-input → grain}/files/src/lib/kit.tsx +8 -5
  111. package/registry/grain.json +17 -0
  112. package/registry/kit/files/src/lib/kit.tsx +8 -5
  113. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +2 -2
  114. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +1 -1
  115. package/registry/list-item/files/src/components/ui/list-item.tsx +28 -17
  116. package/registry/list-item/files/src/lib/kit.tsx +8 -5
  117. package/registry/morphing-button-group.json +18 -0
  118. package/registry/orb-reveal.json +20 -0
  119. package/registry/otp-input/files/src/components/ui/otp-input.tsx +57 -24
  120. package/registry/otp-input/files/src/lib/kit.tsx +8 -5
  121. package/registry/password-input/files/src/components/ui/floating-input.tsx +91 -32
  122. package/registry/password-input/files/src/components/ui/password-input.tsx +55 -21
  123. package/registry/password-input/files/src/lib/kit.tsx +8 -5
  124. package/registry/payment-card/files/src/components/ui/flip-card.tsx +23 -4
  125. package/registry/payment-card/files/src/components/ui/payment-card.tsx +35 -18
  126. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +59 -18
  127. package/registry/payment-card/files/src/lib/kit.tsx +8 -5
  128. package/registry/progress/files/src/components/ui/progress.tsx +16 -6
  129. package/registry/progress/files/src/lib/kit.tsx +8 -5
  130. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +22 -11
  131. package/registry/progress-steps/files/src/lib/kit.tsx +8 -5
  132. package/registry/qr-code-button.json +18 -0
  133. package/registry/radio-group/files/src/components/ui/radio-group.tsx +48 -17
  134. package/registry/radio-group/files/src/lib/kit.tsx +8 -5
  135. package/registry/search-bar/files/src/components/ui/search-bar.tsx +81 -26
  136. package/registry/search-bar/files/src/lib/kit.tsx +8 -5
  137. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +40 -10
  138. package/registry/segmented-control/files/src/lib/kit.tsx +8 -5
  139. package/registry/select/files/src/components/ui/select.tsx +161 -86
  140. package/registry/select/files/src/lib/kit.tsx +8 -5
  141. package/registry/skeleton/files/src/components/ui/skeleton.tsx +4 -4
  142. package/registry/skeleton/files/src/lib/kit.tsx +8 -5
  143. package/registry/slider/files/src/components/ui/slider.tsx +56 -27
  144. package/registry/slider/files/src/lib/kit.tsx +8 -5
  145. package/registry/spinner/files/src/components/ui/spinner.tsx +17 -12
  146. package/registry/spinner/files/src/lib/kit.tsx +8 -5
  147. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +12 -4
  148. package/registry/stateful-button/files/src/lib/kit.tsx +8 -5
  149. package/registry/stepper/files/src/components/ui/stepper.tsx +83 -29
  150. package/registry/stepper/files/src/lib/kit.tsx +8 -5
  151. package/registry/switch/files/src/components/ui/switch.tsx +60 -22
  152. package/registry/switch/files/src/lib/kit.tsx +8 -5
  153. package/registry/tabs/files/src/components/ui/tabs.tsx +15 -10
  154. package/registry/tabs/files/src/lib/kit.tsx +8 -5
  155. package/registry/text-animation/files/src/components/ui/text-animation.tsx +11 -6
  156. package/registry/text-animation/files/src/lib/kit.tsx +8 -5
  157. package/registry/textarea/files/src/components/ui/textarea.tsx +115 -45
  158. package/registry/textarea/files/src/lib/kit.tsx +8 -5
  159. package/registry/ticket-shape/files/src/lib/kit.tsx +8 -5
  160. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +59 -18
  161. package/registry/tilt-card/files/src/lib/kit.tsx +8 -5
  162. package/registry/time-picker/files/src/components/ui/time-picker.tsx +156 -75
  163. package/registry/time-picker/files/src/lib/kit.tsx +8 -5
  164. package/registry/timeline/files/src/components/ui/avatar.tsx +7 -4
  165. package/registry/timeline/files/src/components/ui/timeline.tsx +19 -12
  166. package/registry/timeline/files/src/lib/kit.tsx +8 -5
  167. package/registry/toast/files/src/components/ui/button.tsx +35 -6
  168. package/registry/toast/files/src/components/ui/toast.tsx +25 -14
  169. package/registry/toast/files/src/lib/kit.tsx +8 -5
  170. package/schema/components.schema.json +1 -1
  171. package/registry/_docs/color-swatch.json +0 -82
  172. package/registry/_docs/kpi-card.json +0 -14
  173. package/registry/_docs/like-button.json +0 -92
  174. package/registry/_docs/morph-button.json +0 -128
  175. package/registry/_docs/pagination-dots.json +0 -108
  176. package/registry/_docs/shimmer-button.json +0 -98
  177. package/registry/_docs/tag-input.json +0 -148
  178. package/registry/color-swatch.json +0 -17
  179. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +0 -45
  180. package/registry/kpi-card.json +0 -14
  181. package/registry/like-button.json +0 -18
  182. package/registry/morph-button.json +0 -18
  183. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +0 -262
  184. package/registry/pagination-dots/files/src/lib/kit.tsx +0 -219
  185. package/registry/pagination-dots.json +0 -15
  186. package/registry/shimmer-button.json +0 -17
  187. package/registry/tag-input/files/src/components/ui/tag-input.tsx +0 -720
  188. package/registry/tag-input.json +0 -17
@@ -181,14 +181,17 @@ export function useKit(): Theme {
181
181
  return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
182
182
  }
183
183
 
184
- /** Shared motion presets so the whole kit moves the same way. */
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: 18, stiffness: 260, mass: 0.8 },
188
- /** Press feedback, pops. */
189
- bouncy: { damping: 11, stiffness: 220, mass: 0.7 },
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: 22, stiffness: 160, mass: 1 },
194
+ gentle: { damping: 27, stiffness: 160, mass: 1 },
192
195
  fast: 160,
193
196
  normal: 240,
194
197
  };
@@ -64,6 +64,9 @@ export interface AlertProps {
64
64
  style?: StyleProp<ViewStyle>;
65
65
  }
66
66
 
67
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
68
+ // content text scales all the way.
69
+ const FONT_CAP = 1.35;
67
70
  const HALO = 34;
68
71
  const HALO_COMPACT = 28;
69
72
 
@@ -89,7 +92,8 @@ export function Alert({
89
92
  color,
90
93
  style,
91
94
  }: AlertProps) {
92
- const { colors, radius, dark } = useKit();
95
+ const { colors, radius, dark, font } = useKit();
96
+ const ff = font.family ? { fontFamily: font.family } : null;
93
97
  const uid = useId().replace(/[^a-zA-Z0-9]/g, '');
94
98
  const reduceMotion = useReduceMotion();
95
99
  const tones: Record<AlertVariant, string> = {
@@ -322,9 +326,9 @@ export function Alert({
322
326
  <>
323
327
  {iconNode}
324
328
  <View style={[styles.body, { paddingTop: textTop }]}>
325
- {title ? <Text style={[styles.title, { color: fg }]}>{title}</Text> : null}
329
+ {title ? <Text style={[styles.title, ff, { color: fg }]}>{title}</Text> : null}
326
330
  {typeof description === 'string' ? (
327
- <Text style={[styles.desc, { color: title ? sub : fg }]}>{description}</Text>
331
+ <Text style={[styles.desc, ff, { color: title ? sub : fg }]}>{description}</Text>
328
332
  ) : (
329
333
  description
330
334
  )}
@@ -443,6 +447,8 @@ function Pill({
443
447
  bg: string;
444
448
  fg: string;
445
449
  }) {
450
+ const { font } = useKit();
451
+ const ff = font.family ? { fontFamily: font.family } : null;
446
452
  return (
447
453
  <Pressable
448
454
  accessibilityRole="button"
@@ -451,11 +457,11 @@ function Pill({
451
457
  hitSlop={(44 - height) / 2}
452
458
  style={({ pressed }) => [
453
459
  styles.pill,
454
- { height, borderRadius: radius, backgroundColor: bg, paddingHorizontal: height >= 32 ? 14 : 12 },
460
+ { minHeight: height, borderRadius: radius, backgroundColor: bg, paddingHorizontal: height >= 32 ? 14 : 12 },
455
461
  pressed ? { opacity: 0.75, transform: [{ scale: 0.97 }] } : null,
456
462
  ]}
457
463
  >
458
- <Text style={[styles.pillText, { color: fg }]} numberOfLines={1}>
464
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.pillText, ff, { color: fg }]} numberOfLines={1}>
459
465
  {action.label}
460
466
  </Text>
461
467
  </Pressable>
@@ -1,6 +1,7 @@
1
1
  import * as Haptics from 'expo-haptics';
2
- import React, { useId, useRef } from 'react';
2
+ import React, { useEffect, useId, useRef, useState } from 'react';
3
3
  import {
4
+ AccessibilityInfo,
4
5
  ActivityIndicator,
5
6
  Animated,
6
7
  Pressable,
@@ -55,6 +56,10 @@ export interface ButtonProps extends Omit<PressableProps, 'style' | 'children'>
55
56
  textStyle?: StyleProp<TextStyle>;
56
57
  }
57
58
 
59
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
60
+ // content text scales all the way.
61
+ const FONT_CAP = 1.35;
62
+
58
63
  const SIZES: Record<ButtonSize, { h: number; px: number; font: number; gap: number }> = {
59
64
  sm: { h: 36, px: 14, font: 14, gap: 6 },
60
65
  md: { h: 46, px: 18, font: 15.5, gap: 8 },
@@ -116,8 +121,10 @@ export function Button({
116
121
  onPressOut,
117
122
  ...props
118
123
  }: ButtonProps) {
119
- const { colors, radius, dark } = useKit();
124
+ const { colors, radius, dark, font } = useKit();
125
+ const ff = font.family ? { fontFamily: font.family } : null;
120
126
  const gid = 'btn' + useId().replace(/[^a-zA-Z0-9]/g, '');
127
+ const reduce = useReduceMotion();
121
128
  const scale = useRef(new Animated.Value(1)).current;
122
129
  const isDisabled = disabled || loading;
123
130
  const tint = color ?? colors.accent;
@@ -144,12 +151,17 @@ export function Button({
144
151
  const solid = bg !== 'transparent';
145
152
  const quiet = variant === 'ghost' || variant === 'outline' || variant === 'link';
146
153
 
147
- const spring = (to: number) => Animated.spring(scale, { toValue: to, useNativeDriver: true, ...motion.bouncy }).start();
154
+ // Reduce Motion: no squish; the pressed wash / opacity still marks the press.
155
+ const spring = (to: number) => {
156
+ if (reduce) return scale.setValue(1);
157
+ Animated.spring(scale, { toValue: to, useNativeDriver: true, ...motion.bouncy }).start();
158
+ };
148
159
 
149
160
  const label = isTextNode(children) ? (
150
161
  <Text
151
162
  numberOfLines={1}
152
- style={[styles.label, { color: fg, fontSize: s.font }, variant === 'link' && styles.link, textStyle]}
163
+ maxFontSizeMultiplier={FONT_CAP}
164
+ style={[styles.label, ff, { color: fg, fontSize: s.font }, variant === 'link' && styles.link, textStyle]}
153
165
  >
154
166
  {children}
155
167
  </Text>
@@ -184,7 +196,8 @@ export function Button({
184
196
  style={({ pressed }) => [
185
197
  styles.base,
186
198
  {
187
- height: s.h,
199
+ minHeight: s.h,
200
+ height: size === 'icon' ? s.h : undefined,
188
201
  width: size === 'icon' ? s.h : undefined,
189
202
  paddingHorizontal: variant === 'link' ? 0 : s.px,
190
203
  gap: s.gap,
@@ -235,7 +248,7 @@ export function Button({
235
248
  <View style={styles.row}>
236
249
  <ActivityIndicator size="small" color={fg} />
237
250
  {size !== 'icon' && isTextNode(children) ? (
238
- <Text style={[styles.label, { color: fg, fontSize: s.font, opacity: 0.85 }]}>{children}</Text>
251
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.label, ff, { color: fg, fontSize: s.font, opacity: 0.85 }]}>{children}</Text>
239
252
  ) : null}
240
253
  </View>
241
254
  ) : (
@@ -252,6 +265,22 @@ export function Button({
252
265
  );
253
266
  }
254
267
 
268
+ function useReduceMotion() {
269
+ const [reduce, setReduce] = useState(false);
270
+ useEffect(() => {
271
+ let alive = true;
272
+ AccessibilityInfo.isReduceMotionEnabled()
273
+ .then((r) => alive && setReduce(r))
274
+ .catch(() => {});
275
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
276
+ return () => {
277
+ alive = false;
278
+ sub.remove();
279
+ };
280
+ }, []);
281
+ return reduce;
282
+ }
283
+
255
284
  const styles = StyleSheet.create({
256
285
  base: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
257
286
  row: { flexDirection: 'row', alignItems: 'center', gap: 8 },
@@ -181,14 +181,17 @@ export function useKit(): Theme {
181
181
  return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
182
182
  }
183
183
 
184
- /** Shared motion presets so the whole kit moves the same way. */
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: 18, stiffness: 260, mass: 0.8 },
188
- /** Press feedback, pops. */
189
- bouncy: { damping: 11, stiffness: 220, mass: 0.7 },
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: 22, stiffness: 160, mass: 1 },
194
+ gentle: { damping: 27, stiffness: 160, mass: 1 },
192
195
  fast: 160,
193
196
  normal: 240,
194
197
  };
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "assistant-app",
3
+ "type": "template",
4
+ "title": "AI assistant app",
5
+ "description": "A complete Expo Router app for an AI chat assistant: onboarding once, Apple / Google / email-code sign in, a streaming chat with suggestions and stop, conversation history, profile and settings.",
6
+ "tier": "pro",
7
+ "dependencies": [
8
+ "react-native-svg",
9
+ "expo-haptics",
10
+ "expo-router",
11
+ "expo-splash-screen",
12
+ "expo-status-bar",
13
+ "react-native-safe-area-context",
14
+ "react-native-screens",
15
+ "@react-native-async-storage/async-storage"
16
+ ],
17
+ "registryDependencies": [
18
+ "kit",
19
+ "bottom-nav",
20
+ "onboarding",
21
+ "sign-in",
22
+ "chat",
23
+ "inbox",
24
+ "profile",
25
+ "settings"
26
+ ],
27
+ "sharedFiles": [
28
+ "lib/kit.tsx",
29
+ "components/ui/bottom-nav.tsx",
30
+ "components/blocks/onboarding.tsx",
31
+ "components/ui/button.tsx",
32
+ "components/ui/floating-input.tsx",
33
+ "components/ui/password-input.tsx",
34
+ "components/ui/otp-input.tsx",
35
+ "components/blocks/sign-in.tsx",
36
+ "components/blocks/chat.tsx",
37
+ "components/ui/avatar.tsx",
38
+ "components/ui/badge.tsx",
39
+ "components/ui/chip-group.tsx",
40
+ "components/ui/search-bar.tsx",
41
+ "components/ui/swipeable-row.tsx",
42
+ "components/blocks/inbox.tsx",
43
+ "components/blocks/profile.tsx",
44
+ "components/ui/segmented-control.tsx",
45
+ "components/ui/switch.tsx",
46
+ "components/blocks/settings.tsx"
47
+ ],
48
+ "filesDir": null
49
+ }
@@ -131,7 +131,8 @@ export function Avatar({
131
131
  haptics = true,
132
132
  style,
133
133
  }: AvatarProps) {
134
- const { colors, radius, dark } = useKit();
134
+ const { colors, radius, dark, font } = useKit();
135
+ const ff = font.family ? { fontFamily: font.family } : null;
135
136
  const reduceMotion = useReduceMotion();
136
137
  const uid = useId().replace(/[^a-zA-Z0-9]/g, '');
137
138
  const px = resolveSize(size);
@@ -280,6 +281,7 @@ export function Avatar({
280
281
  numberOfLines={1}
281
282
  style={[
282
283
  styles.initials,
284
+ ff,
283
285
  {
284
286
  color: ink,
285
287
  fontSize: Math.max(9, px * (initials.length > 1 ? 0.37 : 0.42)),
@@ -448,7 +450,7 @@ export function Avatar({
448
450
  {badgeText ? (
449
451
  <Text
450
452
  allowFontScaling={false}
451
- style={[styles.badgeText, { color: colors.dangerForeground, fontSize: Math.max(9, badgeH * 0.6) }]}
453
+ style={[styles.badgeText, ff, { color: colors.dangerForeground, fontSize: Math.max(9, badgeH * 0.6) }]}
452
454
  >
453
455
  {badgeText}
454
456
  </Text>
@@ -623,7 +625,8 @@ export function AvatarGroup({
623
625
  onPressMore,
624
626
  style,
625
627
  }: AvatarGroupProps) {
626
- const { colors, radius, dark } = useKit();
628
+ const { colors, radius, dark, font } = useKit();
629
+ const ff = font.family ? { fontFamily: font.family } : null;
627
630
  const reduceMotion = useReduceMotion();
628
631
  const px = resolveSize(size);
629
632
  const bg = surface ?? colors.background;
@@ -699,7 +702,7 @@ export function AvatarGroup({
699
702
  <Text
700
703
  allowFontScaling={false}
701
704
  numberOfLines={1}
702
- style={[styles.moreText, { color: colors.foreground, fontSize: Math.max(8, px * (extraText.length <= 2 ? 0.34 : extraText.length === 3 ? 0.29 : 0.24)) }]}
705
+ style={[styles.moreText, ff, { color: colors.foreground, fontSize: Math.max(8, px * (extraText.length <= 2 ? 0.34 : extraText.length === 3 ? 0.29 : 0.24)) }]}
703
706
  >
704
707
  +{extraText}
705
708
  </Text>
@@ -181,14 +181,17 @@ export function useKit(): Theme {
181
181
  return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
182
182
  }
183
183
 
184
- /** Shared motion presets so the whole kit moves the same way. */
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: 18, stiffness: 260, mass: 0.8 },
188
- /** Press feedback, pops. */
189
- bouncy: { damping: 11, stiffness: 220, mass: 0.7 },
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: 22, stiffness: 160, mass: 1 },
194
+ gentle: { damping: 27, stiffness: 160, mass: 1 },
192
195
  fast: 160,
193
196
  normal: 240,
194
197
  };
@@ -222,7 +222,7 @@ function span(v: Animated.Value, start: number, end: number, from: number, to: n
222
222
  }
223
223
 
224
224
  const easeIn = Easing.in(Easing.cubic);
225
- const easeBack = Easing.out(Easing.back(1.7));
225
+ const easeSettle = Easing.bezier(0.32, 0.72, 0, 1);
226
226
  const easeLinear = Easing.linear;
227
227
 
228
228
  /** One side of a label roll: letters (or the whole label) slide up out of / up into the clipped slot. */
@@ -243,6 +243,8 @@ function RollingLabel({
243
243
  perLetter: boolean;
244
244
  duration: number;
245
245
  }) {
246
+ const { font } = useKit();
247
+ const ff = font.family ? { fontFamily: font.family } : null;
246
248
  const pieces = perLetter ? [...text].map((ch) => (ch === ' ' ? ' ' : ch)) : [text];
247
249
  const distance = Math.ceil(fontSize * 1.35);
248
250
  return (
@@ -258,11 +260,11 @@ function RollingLabel({
258
260
  style={{
259
261
  opacity: out ? span(progress, begin, fade, 1, 0, easeLinear) : span(progress, begin, fade, 0, 1, easeLinear),
260
262
  transform: [
261
- { translateY: out ? span(progress, begin, end, 0, -distance, easeIn) : span(progress, begin, end, distance, 0, easeBack) },
263
+ { translateY: out ? span(progress, begin, end, 0, -distance, easeIn) : span(progress, begin, end, distance, 0, easeSettle) },
262
264
  ],
263
265
  }}
264
266
  >
265
- <Text numberOfLines={1} allowFontScaling={false} style={[styles.label, { color, fontSize }]}>
267
+ <Text numberOfLines={1} allowFontScaling={false} style={[styles.label, ff, { color, fontSize }]}>
266
268
  {piece}
267
269
  </Text>
268
270
  </Animated.View>
@@ -288,7 +290,8 @@ export function Badge({
288
290
  animateChanges = true,
289
291
  style,
290
292
  }: BadgeProps) {
291
- const { colors, radius, dark } = useKit();
293
+ const { colors, radius, dark, font } = useKit();
294
+ const ff = font.family ? { fontFamily: font.family } : null;
292
295
  const reduce = useReduceMotion();
293
296
  const s = SIZES[size];
294
297
  const c = color ?? toneColor(colors, tone);
@@ -328,7 +331,7 @@ export function Badge({
328
331
  const natural = useRef<number | null>(null);
329
332
  const rowWidth = useRef<number | null>(null);
330
333
  const widthV = useRef(new Animated.Value(0)).current;
331
- const flat = StyleSheet.flatten(style) ?? {};
334
+ const flat = flattenView(style);
332
335
  const canSize = flat.width == null && flat.minWidth == null && flat.flex == null && flat.flexGrow == null && flat.alignSelf !== 'stretch';
333
336
 
334
337
  const textChanged = text !== last.text;
@@ -410,7 +413,7 @@ export function Badge({
410
413
  <Animated.Text
411
414
  numberOfLines={1}
412
415
  allowFontScaling={false}
413
- style={[styles.label, { color: from && !roll ? mix(from.fg, fg) : fg, fontSize: s.font }, roll ? styles.hidden : null]}
416
+ style={[styles.label, ff, { color: from && !roll ? mix(from.fg, fg) : fg, fontSize: s.font }, roll ? styles.hidden : null]}
414
417
  >
415
418
  {text}
416
419
  </Animated.Text>
@@ -540,7 +543,8 @@ export function NotificationBadge({
540
543
  offset = [0, 0],
541
544
  style,
542
545
  }: NotificationBadgeProps) {
543
- const { colors } = useKit();
546
+ const { colors, font } = useKit();
547
+ const ff = font.family ? { fontFamily: font.family } : null;
544
548
  const c = color ?? toneColor(colors, tone);
545
549
  const fg = !color && tone === 'accent' ? colors.accentForeground : readableOn(c);
546
550
  const visible = count > 0 || !!showZero;
@@ -556,7 +560,7 @@ export function NotificationBadge({
556
560
  Animated.spring(show, { toValue: visible ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
557
561
  if (visible && prev.current !== count && prev.current > 0) {
558
562
  pop.setValue(1.35);
559
- Animated.spring(pop, { toValue: 1, useNativeDriver: true, damping: 9, stiffness: 320, mass: 0.6 }).start();
563
+ Animated.spring(pop, { toValue: 1, useNativeDriver: true, damping: 29, stiffness: 320, mass: 0.6 }).start();
560
564
  }
561
565
  prev.current = count;
562
566
  }, [count, visible, show, pop]);
@@ -587,7 +591,7 @@ export function NotificationBadge({
587
591
  ]}
588
592
  >
589
593
  {dot ? null : (
590
- <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 }]}>
591
595
  {text}
592
596
  </Text>
593
597
  )}
@@ -610,6 +614,16 @@ export function NotificationBadge({
610
614
  );
611
615
  }
612
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
+
613
627
  const styles = StyleSheet.create({
614
628
  base: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', borderWidth: 1 },
615
629
  label: { fontWeight: '400', letterSpacing: 0.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
- /** Shared motion presets so the whole kit moves the same way. */
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: 18, stiffness: 260, mass: 0.8 },
188
- /** Press feedback, pops. */
189
- bouncy: { damping: 11, stiffness: 220, mass: 0.7 },
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: 22, stiffness: 160, mass: 1 },
194
+ gentle: { damping: 27, stiffness: 160, mass: 1 },
192
195
  fast: 160,
193
196
  normal: 240,
194
197
  };
@@ -99,6 +99,10 @@ export interface BottomNavProps {
99
99
  style?: StyleProp<ViewStyle>;
100
100
  }
101
101
 
102
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
103
+ // content text scales all the way.
104
+ const FONT_CAP = 1.35;
105
+
102
106
  const ROW_H = 60;
103
107
  const PAD_X = 6;
104
108
  const PAD_TOP = 6;
@@ -109,8 +113,8 @@ const PILL_H = 42;
109
113
  /** Flex weight of the active item in the `pill` look (the others are 1). */
110
114
  const PILL_GROW = 2.3;
111
115
  // Liquid move: the edge in the direction of travel leads, the other one trails behind.
112
- const LEAD = { damping: 22, stiffness: 320, mass: 0.7 };
113
- const TRAIL = { damping: 20, stiffness: 150, mass: 0.9 };
116
+ const LEAD = { damping: 31, stiffness: 320, mass: 0.7 };
117
+ const TRAIL = { damping: 24, stiffness: 150, mass: 0.9 };
114
118
 
115
119
  /**
116
120
  * Bottom navigation in five looks — minimal dot, floating capsule, expanding pill, notched centre
@@ -444,6 +448,8 @@ interface NavButtonProps {
444
448
  }
445
449
 
446
450
  function NavButton({ item, active, look, tint, idle, surface, fab, showLabels, reduce, onPress }: NavButtonProps) {
451
+ const { font } = useKit();
452
+ const ff = font.family ? { fontFamily: font.family } : null;
447
453
  const press = useRef(new Animated.Value(1)).current; // press squish (native)
448
454
  const pop = useRef(new Animated.Value(active ? 1 : 0)).current; // active state (native)
449
455
  const grow = useRef(new Animated.Value(active ? 1 : 0)).current; // pill width (JS — layout)
@@ -510,8 +516,10 @@ function NavButton({ item, active, look, tint, idle, surface, fab, showLabels, r
510
516
  >
511
517
  <Animated.Text
512
518
  numberOfLines={1}
519
+ maxFontSizeMultiplier={FONT_CAP}
513
520
  style={[
514
521
  styles.pillLabel,
522
+ ff,
515
523
  {
516
524
  width: labelW + 1,
517
525
  color: tint,
@@ -528,7 +536,7 @@ function NavButton({ item, active, look, tint, idle, surface, fab, showLabels, r
528
536
  {/* Invisible measurer: the label's natural width, so it never ellipsizes while growing. */}
529
537
  {showLabels ? (
530
538
  <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))}>
539
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.pillLabel, styles.selfStart, ff]} onLayout={(e) => setLabelW(Math.ceil(e.nativeEvent.layout.width))}>
532
540
  {item.label}
533
541
  </Text>
534
542
  </View>
@@ -558,9 +566,11 @@ function NavButton({ item, active, look, tint, idle, surface, fab, showLabels, r
558
566
  {showLabels ? (
559
567
  <Animated.Text
560
568
  numberOfLines={1}
569
+ maxFontSizeMultiplier={FONT_CAP}
561
570
  style={[
562
571
  styles.label,
563
572
  styles.floatLabel,
573
+ ff,
564
574
  { color: tint, opacity: fadeIn, transform: [{ translateY: pop.interpolate({ inputRange: [0, 1], outputRange: [6, 0] }) }] },
565
575
  ]}
566
576
  >
@@ -587,7 +597,7 @@ function NavButton({ item, active, look, tint, idle, surface, fab, showLabels, r
587
597
  {badge}
588
598
  </Animated.View>
589
599
  {showLabels ? (
590
- <Text numberOfLines={1} style={[styles.label, { color: active ? tint : idle }]}>
600
+ <Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.label, ff, { color: active ? tint : idle }]}>
591
601
  {item.label}
592
602
  </Text>
593
603
  ) : null}
@@ -653,6 +663,8 @@ function CentreAction({
653
663
  showLabel: boolean;
654
664
  onPress: () => void;
655
665
  }) {
666
+ const { font } = useKit();
667
+ const ff = font.family ? { fontFamily: font.family } : null;
656
668
  const press = useRef(new Animated.Value(1)).current;
657
669
  const pressTo = (v: number) =>
658
670
  Animated.spring(press, { toValue: v, useNativeDriver: true, ...(v < 1 ? motion.snappy : motion.bouncy) }).start();
@@ -671,7 +683,7 @@ function CentreAction({
671
683
  </FabFace>
672
684
  </Animated.View>
673
685
  {showLabel ? (
674
- <Text numberOfLines={1} style={[styles.label, styles.abs, { top: labelTop, color: labelColor }]}>
686
+ <Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.label, styles.abs, ff, { top: labelTop, color: labelColor }]}>
675
687
  {item.label}
676
688
  </Text>
677
689
  ) : null}
@@ -758,7 +770,8 @@ function FabFace({
758
770
 
759
771
  /** Dot or count. Pops in with a spring when it appears or its value changes. */
760
772
  function Badge({ value, ring, reduce }: { value: true | number; ring: string; reduce: boolean }) {
761
- const { colors, radius } = useKit();
773
+ const { colors, radius, font } = useKit();
774
+ const ff = font.family ? { fontFamily: font.family } : null;
762
775
  const s = useRef(new Animated.Value(reduce ? 1 : 0.3)).current;
763
776
  useEffect(() => {
764
777
  if (reduce) {
@@ -784,7 +797,7 @@ function Badge({ value, ring, reduce }: { value: true | number; ring: string; re
784
797
  },
785
798
  ]}
786
799
  >
787
- {!dot && <Text style={[styles.badgeText, { color: colors.dangerForeground }]}>{text}</Text>}
800
+ {!dot && <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.badgeText, ff, { color: colors.dangerForeground }]}>{text}</Text>}
788
801
  </Animated.View>
789
802
  );
790
803
  }
@@ -1036,7 +1049,7 @@ const styles = StyleSheet.create({
1036
1049
  top: -6,
1037
1050
  right: -11,
1038
1051
  minWidth: 19,
1039
- height: 19,
1052
+ minHeight: 19,
1040
1053
  paddingHorizontal: 4,
1041
1054
  alignItems: 'center',
1042
1055
  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
- /** Shared motion presets so the whole kit moves the same way. */
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: 18, stiffness: 260, mass: 0.8 },
188
- /** Press feedback, pops. */
189
- bouncy: { damping: 11, stiffness: 220, mass: 0.7 },
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: 22, stiffness: 160, mass: 1 },
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: 26, stiffness: 220, mass: 0.9 };
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>(null);
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}
@@ -181,14 +181,17 @@ export function useKit(): Theme {
181
181
  return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
182
182
  }
183
183
 
184
- /** Shared motion presets so the whole kit moves the same way. */
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: 18, stiffness: 260, mass: 0.8 },
188
- /** Press feedback, pops. */
189
- bouncy: { damping: 11, stiffness: 220, mass: 0.7 },
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: 22, stiffness: 160, mass: 1 },
194
+ gentle: { damping: 27, stiffness: 160, mass: 1 },
192
195
  fast: 160,
193
196
  normal: 240,
194
197
  };