@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.
Files changed (232) hide show
  1. package/README.md +20 -2
  2. package/bin/expo-kit.js +219 -44
  3. package/bin/lib.mjs +110 -1
  4. package/bin/mcp.mjs +48 -19
  5. package/package.json +9 -4
  6. package/registry/_docs/accordion.json +16 -0
  7. package/registry/_docs/action-sheet.json +28 -0
  8. package/registry/_docs/alert.json +28 -0
  9. package/registry/_docs/assistant-app.json +47 -0
  10. package/registry/_docs/avatar.json +25 -1
  11. package/registry/_docs/badge.json +36 -5
  12. package/registry/_docs/bottom-nav.json +30 -6
  13. package/registry/_docs/bottom-sheet.json +28 -0
  14. package/registry/_docs/bullet.json +3 -0
  15. package/registry/_docs/button-3d.json +18 -2
  16. package/registry/_docs/button.json +16 -0
  17. package/registry/_docs/calendar.json +21 -5
  18. package/registry/_docs/card-stack.json +16 -0
  19. package/registry/_docs/card.json +20 -0
  20. package/registry/_docs/carousel.json +20 -0
  21. package/registry/_docs/chat.json +184 -0
  22. package/registry/_docs/checkbox.json +16 -0
  23. package/registry/_docs/checkout.json +10 -1
  24. package/registry/_docs/chip-group.json +16 -0
  25. package/registry/_docs/confetti.json +23 -3
  26. package/registry/_docs/coupon.json +16 -0
  27. package/registry/_docs/cube-pager.json +16 -0
  28. package/registry/_docs/dashboard.json +3 -0
  29. package/registry/_docs/date-picker.json +24 -4
  30. package/registry/_docs/dialog.json +24 -0
  31. package/registry/_docs/donut-shop.json +88 -0
  32. package/registry/_docs/drawer.json +32 -4
  33. package/registry/_docs/fab.json +24 -0
  34. package/registry/_docs/fintech-app.json +49 -0
  35. package/registry/_docs/flip-card.json +16 -0
  36. package/registry/_docs/floating-input.json +25 -5
  37. package/registry/_docs/food-app.json +47 -0
  38. package/registry/_docs/gift-card.json +16 -0
  39. package/registry/_docs/glass-button.json +24 -0
  40. package/registry/_docs/glass-nav-bar.json +24 -0
  41. package/registry/_docs/glass-search-bar.json +24 -0
  42. package/registry/_docs/glass-segmented.json +28 -0
  43. package/registry/_docs/glass-sheet.json +28 -0
  44. package/registry/_docs/glass-tab-bar.json +24 -0
  45. package/registry/_docs/glass.json +17 -1
  46. package/registry/_docs/grain.json +80 -0
  47. package/registry/_docs/inbox.json +3 -0
  48. package/registry/_docs/isometric-bar.json +3 -0
  49. package/registry/_docs/kit.json +3 -0
  50. package/registry/_docs/liquidglass-tab.json +3 -0
  51. package/registry/_docs/list-item.json +16 -0
  52. package/registry/_docs/loyalty-card.json +16 -0
  53. package/registry/_docs/morphing-button-group.json +96 -0
  54. package/registry/_docs/number-ticker.json +16 -0
  55. package/registry/_docs/offers.json +3 -0
  56. package/registry/_docs/onboarding.json +3 -0
  57. package/registry/_docs/orb-reveal.json +132 -0
  58. package/registry/_docs/otp-input.json +28 -4
  59. package/registry/_docs/parallax-layers.json +20 -0
  60. package/registry/_docs/parallax-scroll.json +24 -0
  61. package/registry/_docs/password-input.json +23 -3
  62. package/registry/_docs/payment-card.json +16 -0
  63. package/registry/_docs/pixel-bar.json +3 -0
  64. package/registry/_docs/polar-area.json +3 -0
  65. package/registry/_docs/profile.json +3 -0
  66. package/registry/_docs/progress-steps.json +24 -0
  67. package/registry/_docs/progress.json +20 -0
  68. package/registry/_docs/qr-code-button.json +122 -0
  69. package/registry/_docs/radio-group.json +20 -0
  70. package/registry/_docs/rating.json +24 -0
  71. package/registry/_docs/ring-carousel.json +20 -0
  72. package/registry/_docs/scratch-card.json +20 -0
  73. package/registry/_docs/search-bar.json +23 -3
  74. package/registry/_docs/search-results.json +3 -0
  75. package/registry/_docs/segmented-control.json +20 -0
  76. package/registry/_docs/select.json +33 -8
  77. package/registry/_docs/settings.json +9 -0
  78. package/registry/_docs/sign-in.json +3 -0
  79. package/registry/_docs/skeleton.json +22 -2
  80. package/registry/_docs/slide-to-confirm.json +24 -0
  81. package/registry/_docs/slider.json +27 -7
  82. package/registry/_docs/spin-wheel.json +24 -0
  83. package/registry/_docs/spinner.json +41 -11
  84. package/registry/_docs/stateful-button.json +184 -0
  85. package/registry/_docs/stepper.json +23 -7
  86. package/registry/_docs/swipe-deck.json +16 -0
  87. package/registry/_docs/swipeable-row.json +24 -1
  88. package/registry/_docs/switch.json +20 -0
  89. package/registry/_docs/tabs.json +24 -0
  90. package/registry/_docs/text-animation.json +141 -0
  91. package/registry/_docs/textarea.json +18 -2
  92. package/registry/_docs/ticket-card.json +20 -0
  93. package/registry/_docs/ticket-shape.json +4 -1
  94. package/registry/_docs/tilt-card.json +20 -4
  95. package/registry/_docs/time-picker.json +22 -6
  96. package/registry/_docs/timeline-chart.json +3 -0
  97. package/registry/_docs/timeline.json +18 -2
  98. package/registry/_docs/toast.json +32 -8
  99. package/registry/_docs/treemap.json +3 -0
  100. package/registry/_docs/waterfall.json +3 -0
  101. package/registry/_docs/wheel-list.json +20 -0
  102. package/registry/accordion/files/src/components/ui/accordion.tsx +5 -4
  103. package/registry/accordion/files/src/lib/kit.tsx +8 -5
  104. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +9 -8
  105. package/registry/action-sheet/files/src/lib/kit.tsx +8 -5
  106. package/registry/alert/files/src/components/ui/alert.tsx +7 -4
  107. package/registry/alert/files/src/components/ui/button.tsx +4 -3
  108. package/registry/alert/files/src/lib/kit.tsx +8 -5
  109. package/registry/assistant-app.json +49 -0
  110. package/registry/avatar/files/src/components/ui/avatar.tsx +7 -4
  111. package/registry/avatar/files/src/lib/kit.tsx +8 -5
  112. package/registry/badge/files/src/components/ui/badge.tsx +345 -21
  113. package/registry/badge/files/src/lib/kit.tsx +8 -5
  114. package/registry/badge.json +1 -1
  115. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +14 -7
  116. package/registry/bottom-nav/files/src/lib/kit.tsx +8 -5
  117. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +7 -6
  118. package/registry/bottom-sheet/files/src/lib/kit.tsx +8 -5
  119. package/registry/button/files/src/components/ui/button.tsx +4 -3
  120. package/registry/button/files/src/lib/kit.tsx +8 -5
  121. package/registry/button-3d/files/src/components/ui/button-3d.tsx +3 -2
  122. package/registry/button-3d/files/src/lib/kit.tsx +8 -5
  123. package/registry/calendar/files/src/components/ui/calendar.tsx +9 -5
  124. package/registry/calendar/files/src/lib/kit.tsx +8 -5
  125. package/registry/card/files/src/components/ui/card.tsx +8 -6
  126. package/registry/card/files/src/lib/kit.tsx +8 -5
  127. package/registry/chat.json +18 -0
  128. package/registry/checkbox/files/src/components/ui/checkbox.tsx +6 -5
  129. package/registry/checkbox/files/src/lib/kit.tsx +8 -5
  130. package/registry/chip-group/files/src/components/ui/chip-group.tsx +4 -2
  131. package/registry/chip-group/files/src/lib/kit.tsx +8 -5
  132. package/registry/confetti/files/src/lib/kit.tsx +8 -5
  133. package/registry/coupon/files/src/components/ui/coupon.tsx +23 -20
  134. package/registry/coupon/files/src/components/ui/flip-card.tsx +1 -1
  135. package/registry/coupon/files/src/components/ui/tilt-card.tsx +2 -2
  136. package/registry/coupon/files/src/lib/kit.tsx +8 -5
  137. package/registry/date-picker/files/src/components/ui/calendar.tsx +9 -5
  138. package/registry/date-picker/files/src/components/ui/date-picker.tsx +2 -2
  139. package/registry/date-picker/files/src/lib/kit.tsx +8 -5
  140. package/registry/dialog/files/src/components/ui/dialog.tsx +12 -6
  141. package/registry/dialog/files/src/lib/kit.tsx +8 -5
  142. package/registry/donut-shop.json +18 -0
  143. package/registry/drawer/files/src/components/ui/drawer.tsx +25 -19
  144. package/registry/drawer/files/src/lib/kit.tsx +8 -5
  145. package/registry/fintech-app.json +57 -0
  146. package/registry/flip-card/files/src/components/ui/flip-card.tsx +1 -1
  147. package/registry/flip-card/files/src/lib/kit.tsx +8 -5
  148. package/registry/floating-input/files/src/components/ui/floating-input.tsx +16 -6
  149. package/registry/floating-input/files/src/lib/kit.tsx +8 -5
  150. package/registry/food-app.json +55 -0
  151. package/registry/grain/files/src/components/ui/grain.tsx +113 -0
  152. package/registry/{tag-input → grain}/files/src/lib/kit.tsx +8 -5
  153. package/registry/grain.json +17 -0
  154. package/registry/kit/files/src/lib/kit.tsx +8 -5
  155. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +1 -1
  156. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +1 -1
  157. package/registry/list-item/files/src/components/ui/list-item.tsx +20 -16
  158. package/registry/list-item/files/src/lib/kit.tsx +8 -5
  159. package/registry/morphing-button-group.json +18 -0
  160. package/registry/orb-reveal.json +20 -0
  161. package/registry/otp-input/files/src/components/ui/otp-input.tsx +8 -6
  162. package/registry/otp-input/files/src/lib/kit.tsx +8 -5
  163. package/registry/password-input/files/src/components/ui/floating-input.tsx +16 -6
  164. package/registry/password-input/files/src/components/ui/password-input.tsx +2 -2
  165. package/registry/password-input/files/src/lib/kit.tsx +8 -5
  166. package/registry/payment-card/files/src/components/ui/flip-card.tsx +1 -1
  167. package/registry/payment-card/files/src/components/ui/payment-card.tsx +27 -14
  168. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +2 -2
  169. package/registry/payment-card/files/src/lib/kit.tsx +8 -5
  170. package/registry/progress/files/src/components/ui/progress.tsx +8 -5
  171. package/registry/progress/files/src/lib/kit.tsx +8 -5
  172. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +15 -11
  173. package/registry/progress-steps/files/src/lib/kit.tsx +8 -5
  174. package/registry/qr-code-button.json +18 -0
  175. package/registry/radio-group/files/src/components/ui/radio-group.tsx +10 -9
  176. package/registry/radio-group/files/src/lib/kit.tsx +8 -5
  177. package/registry/search-bar/files/src/components/ui/search-bar.tsx +13 -11
  178. package/registry/search-bar/files/src/lib/kit.tsx +8 -5
  179. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +4 -2
  180. package/registry/segmented-control/files/src/lib/kit.tsx +8 -5
  181. package/registry/select/files/src/components/ui/select.tsx +3 -3
  182. package/registry/select/files/src/lib/kit.tsx +8 -5
  183. package/registry/skeleton/files/src/components/ui/skeleton.tsx +4 -4
  184. package/registry/skeleton/files/src/lib/kit.tsx +8 -5
  185. package/registry/slider/files/src/components/ui/slider.tsx +5 -4
  186. package/registry/slider/files/src/lib/kit.tsx +8 -5
  187. package/registry/spinner/files/src/components/ui/spinner.tsx +452 -34
  188. package/registry/spinner/files/src/lib/kit.tsx +8 -5
  189. package/registry/spinner.json +1 -1
  190. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +532 -0
  191. package/registry/{pagination-dots → stateful-button}/files/src/lib/kit.tsx +8 -5
  192. package/registry/stateful-button.json +18 -0
  193. package/registry/stepper/files/src/components/ui/stepper.tsx +8 -4
  194. package/registry/stepper/files/src/lib/kit.tsx +8 -5
  195. package/registry/switch/files/src/components/ui/switch.tsx +4 -3
  196. package/registry/switch/files/src/lib/kit.tsx +8 -5
  197. package/registry/tabs/files/src/components/ui/tabs.tsx +10 -8
  198. package/registry/tabs/files/src/lib/kit.tsx +8 -5
  199. package/registry/text-animation/files/src/components/ui/text-animation.tsx +650 -0
  200. package/registry/text-animation/files/src/lib/kit.tsx +222 -0
  201. package/registry/text-animation.json +15 -0
  202. package/registry/textarea/files/src/components/ui/textarea.tsx +11 -9
  203. package/registry/textarea/files/src/lib/kit.tsx +8 -5
  204. package/registry/ticket-shape/files/src/lib/kit.tsx +8 -5
  205. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +2 -2
  206. package/registry/tilt-card/files/src/lib/kit.tsx +8 -5
  207. package/registry/time-picker/files/src/components/ui/time-picker.tsx +2 -2
  208. package/registry/time-picker/files/src/lib/kit.tsx +8 -5
  209. package/registry/timeline/files/src/components/ui/avatar.tsx +7 -4
  210. package/registry/timeline/files/src/components/ui/timeline.tsx +12 -10
  211. package/registry/timeline/files/src/lib/kit.tsx +8 -5
  212. package/registry/toast/files/src/components/ui/button.tsx +4 -3
  213. package/registry/toast/files/src/components/ui/toast.tsx +13 -11
  214. package/registry/toast/files/src/lib/kit.tsx +8 -5
  215. package/schema/components.schema.json +1 -1
  216. package/registry/_docs/color-swatch.json +0 -66
  217. package/registry/_docs/kpi-card.json +0 -11
  218. package/registry/_docs/like-button.json +0 -72
  219. package/registry/_docs/morph-button.json +0 -108
  220. package/registry/_docs/pagination-dots.json +0 -88
  221. package/registry/_docs/shimmer-button.json +0 -78
  222. package/registry/_docs/tag-input.json +0 -128
  223. package/registry/color-swatch.json +0 -17
  224. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +0 -45
  225. package/registry/kpi-card.json +0 -14
  226. package/registry/like-button.json +0 -18
  227. package/registry/morph-button.json +0 -18
  228. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +0 -262
  229. package/registry/pagination-dots.json +0 -15
  230. package/registry/shimmer-button.json +0 -17
  231. package/registry/tag-input/files/src/components/ui/tag-input.tsx +0 -720
  232. package/registry/tag-input.json +0 -17
@@ -1,10 +1,20 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { Animated, Easing, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
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
- /** Small status label: solid, soft, outline or dot — five tones, three sizes. */
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 label =
160
- isTextNode(children) ? (
161
- <Text numberOfLines={1} allowFontScaling={false} style={[styles.label, { color: fg, fontSize: s.font }]}>
162
- {children}
163
- </Text>
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
- children
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
- <View style={[styles.fill, { borderRadius: s.dot, backgroundColor: dotColor }]} />
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
- {icon}
203
- {label}
204
- </View>
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: 9, stiffness: 320, mass: 0.6 }).start();
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
- /** 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
  };
@@ -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 — plus NotificationBadge with BadgeIconTile/BadgeIcon anchors (or your own children), spring-in counts, and pop on change.",
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: 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}