@expokit/cli 0.3.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.
Files changed (165) 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/assistant-app.json +47 -0
  7. package/registry/_docs/avatar.json +1 -1
  8. package/registry/_docs/badge.json +1 -1
  9. package/registry/_docs/bottom-nav.json +6 -6
  10. package/registry/_docs/button-3d.json +2 -2
  11. package/registry/_docs/calendar.json +5 -5
  12. package/registry/_docs/checkout.json +7 -1
  13. package/registry/_docs/confetti.json +3 -3
  14. package/registry/_docs/date-picker.json +4 -4
  15. package/registry/_docs/donut-shop.json +88 -0
  16. package/registry/_docs/drawer.json +4 -4
  17. package/registry/_docs/fintech-app.json +49 -0
  18. package/registry/_docs/floating-input.json +5 -5
  19. package/registry/_docs/food-app.json +47 -0
  20. package/registry/_docs/glass.json +1 -1
  21. package/registry/_docs/grain.json +80 -0
  22. package/registry/_docs/morphing-button-group.json +96 -0
  23. package/registry/_docs/orb-reveal.json +132 -0
  24. package/registry/_docs/otp-input.json +4 -4
  25. package/registry/_docs/password-input.json +3 -3
  26. package/registry/_docs/qr-code-button.json +122 -0
  27. package/registry/_docs/search-bar.json +3 -3
  28. package/registry/_docs/select.json +7 -7
  29. package/registry/_docs/settings.json +6 -0
  30. package/registry/_docs/skeleton.json +2 -2
  31. package/registry/_docs/slider.json +6 -6
  32. package/registry/_docs/spinner.json +1 -1
  33. package/registry/_docs/stepper.json +7 -7
  34. package/registry/_docs/textarea.json +2 -2
  35. package/registry/_docs/ticket-shape.json +1 -1
  36. package/registry/_docs/tilt-card.json +4 -4
  37. package/registry/_docs/time-picker.json +6 -6
  38. package/registry/_docs/timeline.json +2 -2
  39. package/registry/_docs/toast.json +8 -8
  40. package/registry/accordion/files/src/components/ui/accordion.tsx +5 -4
  41. package/registry/accordion/files/src/lib/kit.tsx +8 -5
  42. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +9 -8
  43. package/registry/action-sheet/files/src/lib/kit.tsx +8 -5
  44. package/registry/alert/files/src/components/ui/alert.tsx +7 -4
  45. package/registry/alert/files/src/components/ui/button.tsx +4 -3
  46. package/registry/alert/files/src/lib/kit.tsx +8 -5
  47. package/registry/assistant-app.json +49 -0
  48. package/registry/avatar/files/src/components/ui/avatar.tsx +7 -4
  49. package/registry/avatar/files/src/lib/kit.tsx +8 -5
  50. package/registry/badge/files/src/components/ui/badge.tsx +21 -7
  51. package/registry/badge/files/src/lib/kit.tsx +8 -5
  52. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +14 -7
  53. package/registry/bottom-nav/files/src/lib/kit.tsx +8 -5
  54. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +7 -6
  55. package/registry/bottom-sheet/files/src/lib/kit.tsx +8 -5
  56. package/registry/button/files/src/components/ui/button.tsx +4 -3
  57. package/registry/button/files/src/lib/kit.tsx +8 -5
  58. package/registry/button-3d/files/src/components/ui/button-3d.tsx +3 -2
  59. package/registry/button-3d/files/src/lib/kit.tsx +8 -5
  60. package/registry/calendar/files/src/components/ui/calendar.tsx +9 -5
  61. package/registry/calendar/files/src/lib/kit.tsx +8 -5
  62. package/registry/card/files/src/components/ui/card.tsx +8 -6
  63. package/registry/card/files/src/lib/kit.tsx +8 -5
  64. package/registry/checkbox/files/src/components/ui/checkbox.tsx +6 -5
  65. package/registry/checkbox/files/src/lib/kit.tsx +8 -5
  66. package/registry/chip-group/files/src/components/ui/chip-group.tsx +4 -2
  67. package/registry/chip-group/files/src/lib/kit.tsx +8 -5
  68. package/registry/confetti/files/src/lib/kit.tsx +8 -5
  69. package/registry/coupon/files/src/components/ui/coupon.tsx +23 -20
  70. package/registry/coupon/files/src/components/ui/flip-card.tsx +1 -1
  71. package/registry/coupon/files/src/components/ui/tilt-card.tsx +2 -2
  72. package/registry/coupon/files/src/lib/kit.tsx +8 -5
  73. package/registry/date-picker/files/src/components/ui/calendar.tsx +9 -5
  74. package/registry/date-picker/files/src/components/ui/date-picker.tsx +2 -2
  75. package/registry/date-picker/files/src/lib/kit.tsx +8 -5
  76. package/registry/dialog/files/src/components/ui/dialog.tsx +12 -6
  77. package/registry/dialog/files/src/lib/kit.tsx +8 -5
  78. package/registry/donut-shop.json +18 -0
  79. package/registry/drawer/files/src/components/ui/drawer.tsx +25 -19
  80. package/registry/drawer/files/src/lib/kit.tsx +8 -5
  81. package/registry/fintech-app.json +57 -0
  82. package/registry/flip-card/files/src/components/ui/flip-card.tsx +1 -1
  83. package/registry/flip-card/files/src/lib/kit.tsx +8 -5
  84. package/registry/floating-input/files/src/components/ui/floating-input.tsx +16 -6
  85. package/registry/floating-input/files/src/lib/kit.tsx +8 -5
  86. package/registry/food-app.json +55 -0
  87. package/registry/grain/files/src/components/ui/grain.tsx +113 -0
  88. package/registry/{tag-input → grain}/files/src/lib/kit.tsx +8 -5
  89. package/registry/grain.json +17 -0
  90. package/registry/kit/files/src/lib/kit.tsx +8 -5
  91. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +1 -1
  92. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +1 -1
  93. package/registry/list-item/files/src/components/ui/list-item.tsx +20 -16
  94. package/registry/list-item/files/src/lib/kit.tsx +8 -5
  95. package/registry/morphing-button-group.json +18 -0
  96. package/registry/orb-reveal.json +20 -0
  97. package/registry/otp-input/files/src/components/ui/otp-input.tsx +8 -6
  98. package/registry/otp-input/files/src/lib/kit.tsx +8 -5
  99. package/registry/password-input/files/src/components/ui/floating-input.tsx +16 -6
  100. package/registry/password-input/files/src/components/ui/password-input.tsx +2 -2
  101. package/registry/password-input/files/src/lib/kit.tsx +8 -5
  102. package/registry/payment-card/files/src/components/ui/flip-card.tsx +1 -1
  103. package/registry/payment-card/files/src/components/ui/payment-card.tsx +27 -14
  104. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +2 -2
  105. package/registry/payment-card/files/src/lib/kit.tsx +8 -5
  106. package/registry/progress/files/src/components/ui/progress.tsx +8 -5
  107. package/registry/progress/files/src/lib/kit.tsx +8 -5
  108. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +15 -11
  109. package/registry/progress-steps/files/src/lib/kit.tsx +8 -5
  110. package/registry/qr-code-button.json +18 -0
  111. package/registry/radio-group/files/src/components/ui/radio-group.tsx +10 -9
  112. package/registry/radio-group/files/src/lib/kit.tsx +8 -5
  113. package/registry/search-bar/files/src/components/ui/search-bar.tsx +13 -11
  114. package/registry/search-bar/files/src/lib/kit.tsx +8 -5
  115. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +4 -2
  116. package/registry/segmented-control/files/src/lib/kit.tsx +8 -5
  117. package/registry/select/files/src/components/ui/select.tsx +3 -3
  118. package/registry/select/files/src/lib/kit.tsx +8 -5
  119. package/registry/skeleton/files/src/components/ui/skeleton.tsx +4 -4
  120. package/registry/skeleton/files/src/lib/kit.tsx +8 -5
  121. package/registry/slider/files/src/components/ui/slider.tsx +5 -4
  122. package/registry/slider/files/src/lib/kit.tsx +8 -5
  123. package/registry/spinner/files/src/components/ui/spinner.tsx +17 -12
  124. package/registry/spinner/files/src/lib/kit.tsx +8 -5
  125. package/registry/stateful-button/files/src/lib/kit.tsx +8 -5
  126. package/registry/stepper/files/src/components/ui/stepper.tsx +8 -4
  127. package/registry/stepper/files/src/lib/kit.tsx +8 -5
  128. package/registry/switch/files/src/components/ui/switch.tsx +4 -3
  129. package/registry/switch/files/src/lib/kit.tsx +8 -5
  130. package/registry/tabs/files/src/components/ui/tabs.tsx +10 -8
  131. package/registry/tabs/files/src/lib/kit.tsx +8 -5
  132. package/registry/text-animation/files/src/components/ui/text-animation.tsx +4 -4
  133. package/registry/text-animation/files/src/lib/kit.tsx +8 -5
  134. package/registry/textarea/files/src/components/ui/textarea.tsx +11 -9
  135. package/registry/textarea/files/src/lib/kit.tsx +8 -5
  136. package/registry/ticket-shape/files/src/lib/kit.tsx +8 -5
  137. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +2 -2
  138. package/registry/tilt-card/files/src/lib/kit.tsx +8 -5
  139. package/registry/time-picker/files/src/components/ui/time-picker.tsx +2 -2
  140. package/registry/time-picker/files/src/lib/kit.tsx +8 -5
  141. package/registry/timeline/files/src/components/ui/avatar.tsx +7 -4
  142. package/registry/timeline/files/src/components/ui/timeline.tsx +12 -10
  143. package/registry/timeline/files/src/lib/kit.tsx +8 -5
  144. package/registry/toast/files/src/components/ui/button.tsx +4 -3
  145. package/registry/toast/files/src/components/ui/toast.tsx +13 -11
  146. package/registry/toast/files/src/lib/kit.tsx +8 -5
  147. package/schema/components.schema.json +1 -1
  148. package/registry/_docs/color-swatch.json +0 -82
  149. package/registry/_docs/kpi-card.json +0 -14
  150. package/registry/_docs/like-button.json +0 -92
  151. package/registry/_docs/morph-button.json +0 -128
  152. package/registry/_docs/pagination-dots.json +0 -108
  153. package/registry/_docs/shimmer-button.json +0 -98
  154. package/registry/_docs/tag-input.json +0 -148
  155. package/registry/color-swatch.json +0 -17
  156. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +0 -45
  157. package/registry/kpi-card.json +0 -14
  158. package/registry/like-button.json +0 -18
  159. package/registry/morph-button.json +0 -18
  160. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +0 -262
  161. package/registry/pagination-dots/files/src/lib/kit.tsx +0 -219
  162. package/registry/pagination-dots.json +0 -15
  163. package/registry/shimmer-button.json +0 -17
  164. package/registry/tag-input/files/src/components/ui/tag-input.tsx +0 -720
  165. package/registry/tag-input.json +0 -17
@@ -89,7 +89,8 @@ export function Alert({
89
89
  color,
90
90
  style,
91
91
  }: AlertProps) {
92
- const { colors, radius, dark } = useKit();
92
+ const { colors, radius, dark, font } = useKit();
93
+ const ff = font.family ? { fontFamily: font.family } : null;
93
94
  const uid = useId().replace(/[^a-zA-Z0-9]/g, '');
94
95
  const reduceMotion = useReduceMotion();
95
96
  const tones: Record<AlertVariant, string> = {
@@ -322,9 +323,9 @@ export function Alert({
322
323
  <>
323
324
  {iconNode}
324
325
  <View style={[styles.body, { paddingTop: textTop }]}>
325
- {title ? <Text style={[styles.title, { color: fg }]}>{title}</Text> : null}
326
+ {title ? <Text style={[styles.title, ff, { color: fg }]}>{title}</Text> : null}
326
327
  {typeof description === 'string' ? (
327
- <Text style={[styles.desc, { color: title ? sub : fg }]}>{description}</Text>
328
+ <Text style={[styles.desc, ff, { color: title ? sub : fg }]}>{description}</Text>
328
329
  ) : (
329
330
  description
330
331
  )}
@@ -443,6 +444,8 @@ function Pill({
443
444
  bg: string;
444
445
  fg: string;
445
446
  }) {
447
+ const { font } = useKit();
448
+ const ff = font.family ? { fontFamily: font.family } : null;
446
449
  return (
447
450
  <Pressable
448
451
  accessibilityRole="button"
@@ -455,7 +458,7 @@ function Pill({
455
458
  pressed ? { opacity: 0.75, transform: [{ scale: 0.97 }] } : null,
456
459
  ]}
457
460
  >
458
- <Text style={[styles.pillText, { color: fg }]} numberOfLines={1}>
461
+ <Text style={[styles.pillText, ff, { color: fg }]} numberOfLines={1}>
459
462
  {action.label}
460
463
  </Text>
461
464
  </Pressable>
@@ -116,7 +116,8 @@ export function Button({
116
116
  onPressOut,
117
117
  ...props
118
118
  }: ButtonProps) {
119
- const { colors, radius, dark } = useKit();
119
+ const { colors, radius, dark, font } = useKit();
120
+ const ff = font.family ? { fontFamily: font.family } : null;
120
121
  const gid = 'btn' + useId().replace(/[^a-zA-Z0-9]/g, '');
121
122
  const scale = useRef(new Animated.Value(1)).current;
122
123
  const isDisabled = disabled || loading;
@@ -149,7 +150,7 @@ export function Button({
149
150
  const label = isTextNode(children) ? (
150
151
  <Text
151
152
  numberOfLines={1}
152
- style={[styles.label, { color: fg, fontSize: s.font }, variant === 'link' && styles.link, textStyle]}
153
+ style={[styles.label, ff, { color: fg, fontSize: s.font }, variant === 'link' && styles.link, textStyle]}
153
154
  >
154
155
  {children}
155
156
  </Text>
@@ -235,7 +236,7 @@ export function Button({
235
236
  <View style={styles.row}>
236
237
  <ActivityIndicator size="small" color={fg} />
237
238
  {size !== 'icon' && isTextNode(children) ? (
238
- <Text style={[styles.label, { color: fg, fontSize: s.font, opacity: 0.85 }]}>{children}</Text>
239
+ <Text style={[styles.label, ff, { color: fg, fontSize: s.font, opacity: 0.85 }]}>{children}</Text>
239
240
  ) : null}
240
241
  </View>
241
242
  ) : (
@@ -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
  };
@@ -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 (
@@ -262,7 +264,7 @@ function RollingLabel({
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
  };
@@ -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: 22, stiffness: 320, mass: 0.7 };
113
- const TRAIL = { damping: 20, stiffness: 150, mass: 0.9 };
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
- /** 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
  };
@@ -116,7 +116,8 @@ export function Button({
116
116
  onPressOut,
117
117
  ...props
118
118
  }: ButtonProps) {
119
- const { colors, radius, dark } = useKit();
119
+ const { colors, radius, dark, font } = useKit();
120
+ const ff = font.family ? { fontFamily: font.family } : null;
120
121
  const gid = 'btn' + useId().replace(/[^a-zA-Z0-9]/g, '');
121
122
  const scale = useRef(new Animated.Value(1)).current;
122
123
  const isDisabled = disabled || loading;
@@ -149,7 +150,7 @@ export function Button({
149
150
  const label = isTextNode(children) ? (
150
151
  <Text
151
152
  numberOfLines={1}
152
- style={[styles.label, { color: fg, fontSize: s.font }, variant === 'link' && styles.link, textStyle]}
153
+ style={[styles.label, ff, { color: fg, fontSize: s.font }, variant === 'link' && styles.link, textStyle]}
153
154
  >
154
155
  {children}
155
156
  </Text>
@@ -235,7 +236,7 @@ export function Button({
235
236
  <View style={styles.row}>
236
237
  <ActivityIndicator size="small" color={fg} />
237
238
  {size !== 'icon' && isTextNode(children) ? (
238
- <Text style={[styles.label, { color: fg, fontSize: s.font, opacity: 0.85 }]}>{children}</Text>
239
+ <Text style={[styles.label, ff, { color: fg, fontSize: s.font, opacity: 0.85 }]}>{children}</Text>
239
240
  ) : null}
240
241
  </View>
241
242
  ) : (
@@ -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
  };
@@ -105,7 +105,8 @@ export function Button3D({
105
105
  accessibilityLabel,
106
106
  style,
107
107
  }: Button3DProps) {
108
- const { dark } = useKit();
108
+ const { dark, font } = useKit();
109
+ const ff = font.family ? { fontFamily: font.family } : null;
109
110
  const s = SIZES[size];
110
111
  const depth = depthProp ?? s.depth;
111
112
  const kind = appearance === 'auto' ? (dark ? 'light' : 'dark') : appearance;
@@ -122,7 +123,7 @@ export function Button3D({
122
123
  ) : isTextNode(children) ? (
123
124
  // No numberOfLines: on a narrow fixed-width key ("esc", "Q") the label may use the padding
124
125
  // instead of being cut to an ellipsis.
125
- <Text style={{ color: m.ink, fontSize: round ? s.font + 4 : s.font, fontWeight: '600', letterSpacing: 0.1, flexShrink: 0, textAlign: 'center' }}>
126
+ <Text style={[{ color: m.ink, fontSize: round ? s.font + 4 : s.font, fontWeight: '600', letterSpacing: 0.1, flexShrink: 0, textAlign: 'center' }, ff]}>
126
127
  {children}
127
128
  </Text>
128
129
  ) : (
@@ -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
  };