@expokit/cli 0.4.3 → 0.5.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 (242) hide show
  1. package/README.md +30 -3
  2. package/bin/expo-kit.js +749 -288
  3. package/bin/lib.mjs +313 -4
  4. package/package.json +1 -1
  5. package/registry/_docs/action-sheet.json +1 -1
  6. package/registry/_docs/alert.json +6 -0
  7. package/registry/_docs/bottom-sheet.json +6 -0
  8. package/registry/_docs/bullet.json +6 -0
  9. package/registry/_docs/dialog.json +1 -1
  10. package/registry/_docs/drawer.json +3 -3
  11. package/registry/_docs/flip-calendar.json +106 -0
  12. package/registry/_docs/glass-search-bar.json +1 -1
  13. package/registry/_docs/grain.json +6 -1
  14. package/registry/_docs/isometric-bar.json +6 -0
  15. package/registry/_docs/liquidglass-tab.json +1 -1
  16. package/registry/_docs/password-input.json +1 -1
  17. package/registry/_docs/paywall.json +144 -0
  18. package/registry/_docs/pixel-bar.json +6 -0
  19. package/registry/_docs/polar-area.json +6 -0
  20. package/registry/_docs/qr-code-button.json +9 -3
  21. package/registry/_docs/ruler-time-picker.json +147 -0
  22. package/registry/_docs/scratch-card.json +7 -1
  23. package/registry/_docs/search-bar.json +1 -1
  24. package/registry/_docs/select.json +2 -2
  25. package/registry/_docs/slide-to-confirm.json +2 -2
  26. package/registry/_docs/spinner.json +1 -1
  27. package/registry/_docs/stateful-button.json +2 -2
  28. package/registry/_docs/swipe-deck.json +1 -1
  29. package/registry/_docs/time-picker.json +2 -2
  30. package/registry/_docs/timeline-chart.json +6 -0
  31. package/registry/_docs/toast.json +15 -3
  32. package/registry/_docs/treemap.json +6 -0
  33. package/registry/_docs/waterfall.json +6 -0
  34. package/registry/accordion/files/src/components/ui/accordion.tsx +6 -4
  35. package/registry/accordion/files/src/lib/kit.tsx +462 -12
  36. package/registry/accordion.json +1 -0
  37. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +50 -58
  38. package/registry/action-sheet/files/src/lib/kit.tsx +462 -12
  39. package/registry/action-sheet.json +1 -0
  40. package/registry/alert/files/src/components/ui/alert.tsx +20 -13
  41. package/registry/alert/files/src/lib/kit.tsx +462 -12
  42. package/registry/alert.json +3 -4
  43. package/registry/assistant-app.json +1 -0
  44. package/registry/avatar/files/src/components/ui/avatar.tsx +89 -74
  45. package/registry/avatar/files/src/lib/kit.tsx +462 -12
  46. package/registry/avatar.json +1 -0
  47. package/registry/badge/files/src/components/ui/badge.tsx +36 -36
  48. package/registry/badge/files/src/lib/kit.tsx +462 -12
  49. package/registry/badge.json +1 -0
  50. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +39 -44
  51. package/registry/bottom-nav/files/src/lib/kit.tsx +462 -12
  52. package/registry/bottom-nav.json +5 -0
  53. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +357 -300
  54. package/registry/bottom-sheet/files/src/lib/kit.tsx +462 -12
  55. package/registry/bottom-sheet.json +5 -1
  56. package/registry/bullet.json +1 -0
  57. package/registry/button/files/src/components/ui/button.tsx +23 -34
  58. package/registry/button/files/src/lib/kit.tsx +462 -12
  59. package/registry/button-3d/files/src/components/ui/button-3d.tsx +6 -9
  60. package/registry/button-3d/files/src/lib/kit.tsx +462 -12
  61. package/registry/button-3d.json +1 -0
  62. package/registry/button.json +1 -0
  63. package/registry/calendar/files/src/components/ui/calendar.tsx +77 -79
  64. package/registry/calendar/files/src/lib/kit.tsx +462 -12
  65. package/registry/calendar.json +1 -0
  66. package/registry/card/files/src/components/ui/card.tsx +7 -6
  67. package/registry/card/files/src/lib/kit.tsx +462 -12
  68. package/registry/card-stack.json +1 -0
  69. package/registry/card.json +1 -0
  70. package/registry/carousel.json +1 -0
  71. package/registry/chat.json +7 -0
  72. package/registry/checkbox/files/src/components/ui/checkbox.tsx +4 -4
  73. package/registry/checkbox/files/src/lib/kit.tsx +462 -12
  74. package/registry/checkbox.json +1 -0
  75. package/registry/checkout.json +1 -0
  76. package/registry/chip-group/files/src/components/ui/chip-group.tsx +24 -24
  77. package/registry/chip-group/files/src/lib/kit.tsx +462 -12
  78. package/registry/chip-group.json +1 -0
  79. package/registry/confetti/files/src/components/ui/confetti.tsx +15 -14
  80. package/registry/confetti.json +3 -6
  81. package/registry/coupon/files/src/components/ui/coupon.tsx +47 -52
  82. package/registry/coupon/files/src/components/ui/flip-card.tsx +9 -14
  83. package/registry/coupon/files/src/components/ui/tilt-card.tsx +80 -101
  84. package/registry/coupon/files/src/lib/kit.tsx +462 -12
  85. package/registry/coupon/files/src/lib/ticket-shape.tsx +54 -44
  86. package/registry/coupon.json +1 -0
  87. package/registry/cube-pager.json +1 -0
  88. package/registry/dashboard.json +1 -0
  89. package/registry/date-picker/files/src/components/ui/calendar.tsx +77 -79
  90. package/registry/date-picker/files/src/components/ui/date-picker.tsx +64 -71
  91. package/registry/date-picker/files/src/lib/kit.tsx +462 -12
  92. package/registry/date-picker.json +1 -0
  93. package/registry/dialog/files/src/components/ui/dialog.tsx +66 -68
  94. package/registry/dialog/files/src/lib/kit.tsx +462 -12
  95. package/registry/dialog.json +1 -0
  96. package/registry/donut-shop.json +1 -0
  97. package/registry/drawer/files/src/components/ui/drawer.tsx +76 -88
  98. package/registry/drawer/files/src/lib/kit.tsx +462 -12
  99. package/registry/drawer.json +1 -0
  100. package/registry/fab.json +1 -0
  101. package/registry/fintech-app.json +1 -0
  102. package/registry/flip-calendar/files/src/components/ui/flip-calendar.tsx +632 -0
  103. package/registry/flip-calendar/files/src/lib/kit.tsx +672 -0
  104. package/registry/flip-calendar.json +24 -0
  105. package/registry/flip-card/files/src/components/ui/flip-card.tsx +9 -14
  106. package/registry/flip-card/files/src/lib/kit.tsx +462 -12
  107. package/registry/flip-card.json +1 -0
  108. package/registry/floating-input/files/src/components/ui/floating-input.tsx +33 -33
  109. package/registry/floating-input/files/src/lib/kit.tsx +462 -12
  110. package/registry/floating-input.json +1 -0
  111. package/registry/food-app.json +1 -0
  112. package/registry/gift-card.json +1 -0
  113. package/registry/glass-button.json +5 -0
  114. package/registry/glass-nav-bar.json +1 -0
  115. package/registry/glass-search-bar.json +1 -0
  116. package/registry/glass-segmented.json +1 -0
  117. package/registry/glass-sheet.json +1 -0
  118. package/registry/glass-tab-bar.json +5 -0
  119. package/registry/glass.json +1 -0
  120. package/registry/grain/files/src/components/ui/grain.tsx +16 -14
  121. package/registry/grain.json +8 -6
  122. package/registry/inbox.json +1 -0
  123. package/registry/isometric-bar.json +1 -0
  124. package/registry/kit/files/src/lib/kit.tsx +462 -12
  125. package/registry/kit.json +1 -0
  126. package/registry/liquidglass-tab/files/src/app/_layout.tsx +0 -1
  127. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +29 -16
  128. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +33 -19
  129. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +1 -1
  130. package/registry/liquidglass-tab.json +1 -0
  131. package/registry/list-item/files/src/components/ui/list-item.tsx +43 -43
  132. package/registry/list-item/files/src/lib/kit.tsx +462 -12
  133. package/registry/list-item.json +1 -0
  134. package/registry/loyalty-card.json +1 -0
  135. package/registry/morphing-button-group.json +1 -0
  136. package/registry/number-ticker.json +1 -0
  137. package/registry/offers.json +1 -0
  138. package/registry/onboarding.json +1 -0
  139. package/registry/orb-reveal.json +1 -0
  140. package/registry/otp-input/files/src/components/ui/otp-input.tsx +49 -30
  141. package/registry/otp-input/files/src/lib/kit.tsx +462 -12
  142. package/registry/otp-input.json +1 -0
  143. package/registry/parallax-layers.json +1 -0
  144. package/registry/parallax-scroll.json +1 -0
  145. package/registry/password-input/files/src/components/ui/floating-input.tsx +33 -33
  146. package/registry/password-input/files/src/components/ui/password-input.tsx +29 -25
  147. package/registry/password-input/files/src/lib/kit.tsx +462 -12
  148. package/registry/password-input.json +1 -0
  149. package/registry/payment-card/files/src/components/ui/flip-card.tsx +9 -14
  150. package/registry/payment-card/files/src/components/ui/payment-card.tsx +26 -20
  151. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +80 -101
  152. package/registry/payment-card/files/src/lib/kit.tsx +462 -12
  153. package/registry/payment-card.json +1 -0
  154. package/registry/paywall.json +23 -0
  155. package/registry/pixel-bar.json +1 -0
  156. package/registry/polar-area.json +1 -0
  157. package/registry/profile.json +1 -0
  158. package/registry/progress/files/src/components/ui/progress.tsx +31 -29
  159. package/registry/progress/files/src/lib/kit.tsx +462 -12
  160. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +21 -21
  161. package/registry/progress-steps/files/src/lib/kit.tsx +462 -12
  162. package/registry/progress-steps.json +1 -0
  163. package/registry/progress.json +1 -0
  164. package/registry/qr-code-button.json +1 -0
  165. package/registry/radio-group/files/src/components/ui/radio-group.tsx +19 -19
  166. package/registry/radio-group/files/src/lib/kit.tsx +462 -12
  167. package/registry/radio-group.json +1 -0
  168. package/registry/rating.json +1 -0
  169. package/registry/ring-carousel.json +1 -0
  170. package/registry/ruler-time-picker/files/src/components/ui/ruler-time-picker.tsx +832 -0
  171. package/registry/ruler-time-picker/files/src/lib/kit.tsx +672 -0
  172. package/registry/ruler-time-picker.json +26 -0
  173. package/registry/scratch-card.json +1 -0
  174. package/registry/search-bar/files/src/components/ui/search-bar.tsx +28 -28
  175. package/registry/search-bar/files/src/lib/kit.tsx +462 -12
  176. package/registry/search-bar.json +1 -0
  177. package/registry/search-results.json +4 -0
  178. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +11 -17
  179. package/registry/segmented-control/files/src/lib/kit.tsx +462 -12
  180. package/registry/segmented-control.json +1 -0
  181. package/registry/select/files/src/components/ui/select.tsx +107 -104
  182. package/registry/select/files/src/lib/kit.tsx +462 -12
  183. package/registry/select.json +6 -0
  184. package/registry/settings.json +1 -0
  185. package/registry/sign-in.json +1 -0
  186. package/registry/skeleton/files/src/components/ui/skeleton.tsx +149 -80
  187. package/registry/skeleton/files/src/lib/kit.tsx +462 -12
  188. package/registry/skeleton.json +1 -0
  189. package/registry/slide-to-confirm.json +1 -0
  190. package/registry/slider/files/src/components/ui/slider.tsx +106 -108
  191. package/registry/slider/files/src/lib/kit.tsx +462 -12
  192. package/registry/slider.json +5 -0
  193. package/registry/spin-wheel.json +1 -0
  194. package/registry/spinner/files/src/components/ui/spinner.tsx +39 -29
  195. package/registry/spinner/files/src/lib/kit.tsx +462 -12
  196. package/registry/spinner.json +6 -0
  197. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +39 -32
  198. package/registry/stateful-button/files/src/lib/kit.tsx +462 -12
  199. package/registry/stateful-button.json +5 -0
  200. package/registry/stepper/files/src/components/ui/stepper.tsx +40 -38
  201. package/registry/stepper/files/src/lib/kit.tsx +462 -12
  202. package/registry/stepper.json +1 -0
  203. package/registry/swipe-deck.json +1 -0
  204. package/registry/swipeable-row.json +4 -0
  205. package/registry/switch/files/src/components/ui/switch.tsx +41 -47
  206. package/registry/switch/files/src/lib/kit.tsx +462 -12
  207. package/registry/switch.json +1 -0
  208. package/registry/tabs/files/src/components/ui/tabs.tsx +61 -64
  209. package/registry/tabs/files/src/lib/kit.tsx +462 -12
  210. package/registry/tabs.json +1 -0
  211. package/registry/text-animation/files/src/components/ui/text-animation.tsx +55 -42
  212. package/registry/text-animation/files/src/lib/kit.tsx +462 -12
  213. package/registry/text-animation.json +7 -0
  214. package/registry/textarea/files/src/components/ui/textarea.tsx +10 -10
  215. package/registry/textarea/files/src/lib/kit.tsx +462 -12
  216. package/registry/textarea.json +1 -0
  217. package/registry/ticket-card.json +1 -0
  218. package/registry/ticket-shape/files/src/lib/kit.tsx +462 -12
  219. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +54 -44
  220. package/registry/ticket-shape.json +1 -0
  221. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +80 -101
  222. package/registry/tilt-card/files/src/lib/kit.tsx +462 -12
  223. package/registry/tilt-card.json +1 -0
  224. package/registry/time-picker/files/src/components/ui/time-picker.tsx +136 -131
  225. package/registry/time-picker/files/src/lib/kit.tsx +462 -12
  226. package/registry/time-picker.json +1 -0
  227. package/registry/timeline/files/src/components/ui/avatar.tsx +89 -74
  228. package/registry/timeline/files/src/components/ui/timeline.tsx +49 -57
  229. package/registry/timeline/files/src/lib/kit.tsx +462 -12
  230. package/registry/timeline-chart.json +1 -0
  231. package/registry/timeline.json +1 -0
  232. package/registry/toast/files/src/components/ui/toast.tsx +543 -465
  233. package/registry/toast/files/src/lib/kit.tsx +462 -12
  234. package/registry/toast.json +7 -5
  235. package/registry/treemap.json +1 -0
  236. package/registry/waterfall.json +1 -0
  237. package/registry/wheel-list.json +1 -0
  238. package/schema/components.schema.json +1 -0
  239. package/registry/alert/files/src/components/ui/button.tsx +0 -303
  240. package/registry/confetti/files/src/lib/kit.tsx +0 -222
  241. package/registry/grain/files/src/lib/kit.tsx +0 -222
  242. package/registry/toast/files/src/components/ui/button.tsx +0 -303
@@ -1,5 +1,5 @@
1
1
  import * as Haptics from 'expo-haptics';
2
- import React, { useEffect, useId, useRef, useState } from 'react';
2
+ import React, { useEffect, useId, useState } from 'react';
3
3
  import {
4
4
  AccessibilityInfo,
5
5
  Animated,
@@ -25,41 +25,41 @@ export type AvatarFallback = 'soft' | 'solid' | 'gradient';
25
25
 
26
26
  export interface AvatarProps {
27
27
  /** Image URL. Initials show while it loads, and stay if it fails. */
28
- src?: string;
28
+ src?: string | undefined;
29
29
  /** Used for the initials, the fallback colour (same name → same colour) and the accessibility label. */
30
- name?: string;
30
+ name?: string | undefined;
31
31
  /** 'xs' 24 · 'sm' 32 · 'md' 40 · 'lg' 56 · 'xl' 72, or any number of points. */
32
- size?: AvatarSize | number;
33
- shape?: AvatarShape;
32
+ size?: AvatarSize | number | undefined;
33
+ shape?: AvatarShape | undefined;
34
34
  /** Presence dot cut into the bottom-right edge. */
35
- status?: AvatarStatus;
35
+ status?: AvatarStatus | undefined;
36
36
  /** Soft pulse behind the status dot (default: on for 'online'). */
37
- pulse?: boolean;
37
+ pulse?: boolean | undefined;
38
38
  /** Ring around the face, separated by a clear gap. */
39
- ring?: AvatarRing;
39
+ ring?: AvatarRing | undefined;
40
40
  /** The story has been watched: the ring cross-fades to a quiet hairline. */
41
- seen?: boolean;
41
+ seen?: boolean | undefined;
42
42
  /** Ring colour override — the first entry is used as a solid colour (default: `accent`). */
43
- ringColors?: string[];
43
+ ringColors?: string[] | undefined;
44
44
  /** Spin a broken story ring — a "story is loading" state. */
45
- ringSpin?: boolean;
45
+ ringSpin?: boolean | undefined;
46
46
  /** Initials look when there is no photo. Default 'soft'. */
47
- fallback?: AvatarFallback;
47
+ fallback?: AvatarFallback | undefined;
48
48
  /** Soft layered shadow under the face (default true for 72pt and up). */
49
- elevated?: boolean;
49
+ elevated?: boolean | undefined;
50
50
  /** Notification count (`true` = dot), top-right. */
51
- badge?: number | boolean;
51
+ badge?: number | boolean | undefined;
52
52
  /** Verified seal with a check, bottom-right (bottom-left when a status dot is shown). */
53
- verified?: boolean;
53
+ verified?: boolean | undefined;
54
54
  /** Colour behind the avatar — the dot, badge and seal cut-outs are punched in it. Default: theme background. */
55
- surface?: string;
55
+ surface?: string | undefined;
56
56
  /** Ring and verified seal colour (default: theme accent). */
57
- accent?: string;
57
+ accent?: string | undefined;
58
58
  /** Makes it a button with a spring press and a light haptic tick. */
59
- onPress?: () => void;
59
+ onPress?: (() => void) | undefined;
60
60
  /** Haptic tick on press (default true). */
61
- haptics?: boolean;
62
- style?: StyleProp<ViewStyle>;
61
+ haptics?: boolean | undefined;
62
+ style?: StyleProp<ViewStyle> | undefined;
63
63
  }
64
64
 
65
65
  const SIZES: Record<AvatarSize, number> = { xs: 24, sm: 32, md: 40, lg: 56, xl: 72 };
@@ -99,7 +99,9 @@ export function colorFromName(name = '') {
99
99
  export function initialsFrom(name = '', max = 2) {
100
100
  const parts = name.trim().split(/\s+/).filter(Boolean);
101
101
  if (!parts.length) return '';
102
- const letters = parts.length === 1 ? [parts[0][0]] : [parts[0][0], parts[parts.length - 1][0]];
102
+ const first = parts[0] ?? '';
103
+ const last = parts[parts.length - 1] ?? '';
104
+ const letters = parts.length === 1 ? [first[0]] : [first[0], last[0]];
103
105
  return letters.slice(0, max).join('').toUpperCase();
104
106
  }
105
107
 
@@ -144,13 +146,13 @@ export function Avatar({
144
146
  // Remember which URL failed so a new `src` gets a fresh try without resetting state in an effect.
145
147
  const [failedSrc, setFailedSrc] = useState<string>();
146
148
  const failed = !!src && failedSrc === src;
147
- const loaded = useRef(new Animated.Value(0)).current;
148
- const press = useRef(new Animated.Value(1)).current;
149
- const spin = useRef(new Animated.Value(0)).current;
150
- const pulseV = useRef(new Animated.Value(0)).current;
149
+ const [loaded] = useState(() => new Animated.Value(0));
150
+ const [press] = useState(() => new Animated.Value(1));
151
+ const [spin] = useState(() => new Animated.Value(0));
152
+ const [pulseV] = useState(() => new Animated.Value(0));
151
153
 
152
154
  const isSeen = seen ?? ring === 'seen';
153
- const seenV = useRef(new Animated.Value(isSeen ? 1 : 0)).current;
155
+ const [seenV] = useState(() => new Animated.Value(isSeen ? 1 : 0));
154
156
 
155
157
  useEffect(() => {
156
158
  loaded.setValue(0);
@@ -248,11 +250,8 @@ export function Avatar({
248
250
  { width: px, height: px, borderRadius: faceRadius, backgroundColor: faceBg },
249
251
  showElevated
250
252
  ? {
251
- shadowColor: '#000',
252
- shadowOpacity: dark ? 0.5 : 0.14,
253
- shadowRadius: px * 0.16,
254
- shadowOffset: { width: 0, height: Math.max(2, px * 0.07) },
255
- elevation: Math.round(px * 0.1),
253
+ // boxShadow scales with the avatar and looks the same on iOS, Android and web.
254
+ boxShadow: `0px ${Math.round(Math.max(2, px * 0.07))}px ${Math.round(px * 0.32)}px rgba(0,0,0,${dark ? 0.5 : 0.14})`,
256
255
  }
257
256
  : null,
258
257
  ]}
@@ -338,7 +337,7 @@ export function Avatar({
338
337
  strokeWidth={width}
339
338
  fill="none"
340
339
  strokeLinecap="round"
341
- strokeDasharray={dashed ? [circ * 0.7, circ * 0.3] : undefined}
340
+ {...(dashed ? { strokeDasharray: [circ * 0.7, circ * 0.3] } : null)}
342
341
  />
343
342
  ) : (
344
343
  <Rect
@@ -571,12 +570,15 @@ const SEAL = (() => {
571
570
  const rr = k % 2 === 0 ? 12 : 10.1;
572
571
  return [12 + rr * Math.cos(a), 12 + rr * Math.sin(a)] as const;
573
572
  });
574
- const mid = (a: readonly number[], b: readonly number[]) => [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
573
+ type Pt = readonly [number, number];
574
+ const mid = (a: Pt, b: Pt): Pt => [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
575
575
  const f = (v: number) => v.toFixed(2);
576
- const start = mid(pts[pts.length - 1], pts[0]);
576
+ // `pts` has 2n points and every index below is taken modulo its length.
577
+ const at = (k: number) => pts[k % pts.length]!;
578
+ const start = mid(at(pts.length - 1), at(0));
577
579
  let d = `M${f(start[0])} ${f(start[1])}`;
578
580
  pts.forEach((pt, i) => {
579
- const m = mid(pt, pts[(i + 1) % pts.length]);
581
+ const m = mid(pt, at(i + 1));
580
582
  d += `Q${f(pt[0])} ${f(pt[1])} ${f(m[0])} ${f(m[1])}`;
581
583
  });
582
584
  return d + 'Z';
@@ -596,20 +598,20 @@ export interface AvatarGroupProps {
596
598
  /** `<Avatar>` elements. The group sets their size, shape and surface. */
597
599
  children: React.ReactNode;
598
600
  /** Show at most this many, then a "+N" chip. */
599
- max?: number;
600
- size?: AvatarSize | number;
601
- shape?: AvatarShape;
601
+ max?: number | undefined;
602
+ size?: AvatarSize | number | undefined;
603
+ shape?: AvatarShape | undefined;
602
604
  /** How far each avatar tucks under the previous one, as a fraction of its size (default 0.28). */
603
- overlap?: number;
605
+ overlap?: number | undefined;
604
606
  /** Real total when it is larger than the children you pass (e.g. 1,284 members). */
605
- total?: number;
607
+ total?: number | undefined;
606
608
  /** Staggered spring-in on mount (skipped with reduce motion). */
607
- animated?: boolean;
609
+ animated?: boolean | undefined;
608
610
  /** Colour of the separating borders — match what the group sits on (default: theme background). */
609
- surface?: string;
611
+ surface?: string | undefined;
610
612
  /** Makes the "+N" chip a button. */
611
- onPressMore?: () => void;
612
- style?: StyleProp<ViewStyle>;
613
+ onPressMore?: (() => void) | undefined;
614
+ style?: StyleProp<ViewStyle> | undefined;
613
615
  }
614
616
 
615
617
  /** Overlapping avatars separated by cut-out borders, with a "+N" chip and a staggered spring-in. */
@@ -625,7 +627,7 @@ export function AvatarGroup({
625
627
  onPressMore,
626
628
  style,
627
629
  }: AvatarGroupProps) {
628
- const { colors, radius, dark, font } = useKit();
630
+ const { colors, radius, dark, font, labels } = useKit();
629
631
  const ff = font.family ? { fontFamily: font.family } : null;
630
632
  const reduceMotion = useReduceMotion();
631
633
  const px = resolveSize(size);
@@ -638,8 +640,13 @@ export function AvatarGroup({
638
640
  const inner = shape === 'circle' ? radius.full : Math.min(px / 2, radius.scale(Math.round(px * 0.3)));
639
641
  const wrapCorner = shape === 'circle' ? radius.full : inner + border;
640
642
 
641
- const anims = useRef<Animated.Value[]>([]).current;
642
- while (anims.length < count) anims.push(new Animated.Value(animated ? 0 : 1));
643
+ // One value per avatar, grown (never shrunk) as children are added; this render already uses the grown list.
644
+ const [pool, setPool] = useState<Animated.Value[]>([]);
645
+ let anims = pool;
646
+ if (pool.length < count) {
647
+ anims = [...pool, ...Array.from({ length: count - pool.length }, () => new Animated.Value(animated ? 0 : 1))];
648
+ setPool(anims);
649
+ }
643
650
 
644
651
  useEffect(() => {
645
652
  if (!animated) return;
@@ -657,35 +664,41 @@ export function AvatarGroup({
657
664
  const names = shown.map((c) => c.props.name).filter(Boolean) as string[];
658
665
  const label =
659
666
  names.length === 0
660
- ? `${total ?? items.length} people`
661
- : `${names.slice(0, 3).join(', ')}${(total ?? items.length) - Math.min(3, names.length) > 0 ? ` and ${(total ?? items.length) - Math.min(3, names.length)} more` : ''}`;
662
-
663
- const wrap = (child: React.ReactNode, i: number, key: React.Key) => (
664
- <Animated.View
665
- key={key}
666
- style={{
667
- marginLeft: i === 0 ? 0 : -px * overlap,
668
- borderRadius: wrapCorner,
669
- padding: border,
670
- backgroundColor: bg,
671
- zIndex: count - i,
672
- opacity: anims[i],
673
- transform: [
674
- { translateX: anims[i].interpolate({ inputRange: [0, 1], outputRange: [-8, 0] }) },
675
- { scale: anims[i].interpolate({ inputRange: [0, 1], outputRange: [0.6, 1] }) },
676
- ],
677
- }}
678
- >
679
- {child}
680
- </Animated.View>
681
- );
667
+ ? labels.peopleCount(total ?? items.length)
668
+ : (total ?? items.length) - Math.min(3, names.length) > 0
669
+ ? labels.namesAndMore(names.slice(0, 3).join(', '), (total ?? items.length) - Math.min(3, names.length))
670
+ : names.slice(0, 3).join(', ');
671
+
672
+ // `anims` was grown to `count` above, and i < count.
673
+ const wrap = (child: React.ReactNode, i: number, key: React.Key) => {
674
+ const v = anims[i]!;
675
+ return (
676
+ <Animated.View
677
+ key={key}
678
+ style={{
679
+ marginLeft: i === 0 ? 0 : -px * overlap,
680
+ borderRadius: wrapCorner,
681
+ padding: border,
682
+ backgroundColor: bg,
683
+ zIndex: count - i,
684
+ opacity: v,
685
+ transform: [
686
+ { translateX: v.interpolate({ inputRange: [0, 1], outputRange: [-8, 0] }) },
687
+ { scale: v.interpolate({ inputRange: [0, 1], outputRange: [0.6, 1] }) },
688
+ ],
689
+ }}
690
+ >
691
+ {child}
692
+ </Animated.View>
693
+ );
694
+ };
682
695
 
683
696
  const extraText = extra > 999 ? `${(extra / 1000).toFixed(extra >= 10000 ? 0 : 1).replace(/\.0$/, '')}k` : String(extra);
684
697
  const more =
685
698
  extra > 0 ? (
686
699
  <Pressable
687
700
  accessibilityRole={onPressMore ? 'button' : 'text'}
688
- accessibilityLabel={`${extra} more`}
701
+ accessibilityLabel={labels.moreCount(extra)}
689
702
  disabled={!onPressMore}
690
703
  onPress={onPressMore}
691
704
  hitSlop={px < 44 ? Math.ceil((44 - px) / 2) : undefined}
@@ -774,20 +787,22 @@ function luminance(hex: string) {
774
787
  }
775
788
 
776
789
  function contrast(a: string, b: string) {
777
- const [x, y] = [luminance(a), luminance(b)].sort((p, q) => q - p);
790
+ const la = luminance(a);
791
+ const lb = luminance(b);
792
+ const [x, y] = la >= lb ? [la, lb] : [lb, la];
778
793
  return (x + 0.05) / (y + 0.05);
779
794
  }
780
795
 
781
796
  const styles = StyleSheet.create({
782
797
  face: { overflow: 'visible' },
783
798
  clip: { flex: 1, overflow: 'hidden', alignItems: 'center', justifyContent: 'center' },
784
- initials: { fontWeight: '400', includeFontPadding: false, textAlign: 'center' },
799
+ initials: { fontWeight: '400', includeFontPadding: false, textAlign: 'center', textAlignVertical: 'center' },
785
800
  inkShadow: { textShadowColor: 'rgba(0,0,0,0.18)', textShadowRadius: 2, textShadowOffset: { width: 0, height: 0.5 } },
786
801
  pulse: { position: 'absolute' },
787
802
  dot: { alignItems: 'center', justifyContent: 'center', overflow: 'hidden' },
788
803
  badge: { position: 'absolute', alignItems: 'center', justifyContent: 'center' },
789
- badgeText: { fontWeight: '400', letterSpacing: -0.2, includeFontPadding: false },
804
+ badgeText: { fontWeight: '400', letterSpacing: -0.2, includeFontPadding: false, textAlignVertical: 'center' },
790
805
  group: { flexDirection: 'row', alignItems: 'center' },
791
806
  more: { alignItems: 'center', justifyContent: 'center' },
792
- moreText: { fontWeight: '400', letterSpacing: -0.2 },
807
+ moreText: { fontWeight: '400', letterSpacing: -0.2, includeFontPadding: false, textAlignVertical: 'center' },
793
808
  });
@@ -3,7 +3,6 @@ import {
3
3
  AccessibilityInfo,
4
4
  Animated,
5
5
  Easing,
6
- Platform,
7
6
  Pressable,
8
7
  StyleSheet,
9
8
  Text,
@@ -45,27 +44,27 @@ export interface TimelineLink {
45
44
  }
46
45
 
47
46
  export interface TimelineItem {
48
- key?: string;
47
+ key?: string | undefined;
49
48
  title: string;
50
- description?: string;
49
+ description?: string | undefined;
51
50
  /** Timestamp or meta, e.g. "Oct 12 · 2:14 PM" or "2h". */
52
- time?: string;
51
+ time?: string | undefined;
53
52
  /** Default 'done'. */
54
- status?: TimelineStatus;
53
+ status?: TimelineStatus | undefined;
55
54
  /** Icon inside a larger dot. */
56
- icon?: TimelineIconName | ((props: TimelineIconProps) => React.ReactNode);
55
+ icon?: (TimelineIconName | ((props: TimelineIconProps) => React.ReactNode)) | undefined;
57
56
  /** Photo (initials fallback) instead of a dot. */
58
- avatar?: { src?: string; name: string };
57
+ avatar?: { src?: string; name: string } | undefined;
59
58
  /** A photo under the text — a URL, or `{ uri, aspectRatio }` (default 16:9). */
60
- image?: TimelineImageSource;
59
+ image?: TimelineImageSource | undefined;
61
60
  /** Several photos in a 1-, 2- or 3-up grid (more than three show a "+N" tile). Combined with `image`. */
62
- images?: TimelineImageSource[];
61
+ images?: TimelineImageSource[] | undefined;
63
62
  /** Small accent chips under the text. */
64
- tags?: string[];
63
+ tags?: string[] | undefined;
65
64
  /** Tappable accent links under the text. */
66
- links?: TimelineLink[];
65
+ links?: TimelineLink[] | undefined;
67
66
  /** Anything under the description: a card, a quote, tags, buttons. */
68
- content?: React.ReactNode;
67
+ content?: React.ReactNode | undefined;
69
68
  }
70
69
 
71
70
  export interface TimelineProps {
@@ -74,23 +73,23 @@ export interface TimelineProps {
74
73
  * 'default' roomy rows · 'compact' small dots, one-line rows · 'card' each event on a card ·
75
74
  * 'scroll' renders its own scroll view and fills the connector as you scroll (give it a height).
76
75
  */
77
- variant?: TimelineVariant;
76
+ variant?: TimelineVariant | undefined;
78
77
  /** Colour of done / current dots and the filled line (default: theme accent). */
79
- accent?: string;
78
+ accent?: string | undefined;
80
79
  /** Draw a check in done dots (default true). */
81
- checks?: boolean;
80
+ checks?: boolean | undefined;
82
81
  /** Entrance + connector fill, or the scroll-driven fill for 'scroll' (default true; off with reduce motion). */
83
- animated?: boolean;
82
+ animated?: boolean | undefined;
84
83
  /** Colour behind the rail — fills hollow dots and the mini status badges (default: theme background). */
85
- surface?: string;
84
+ surface?: string | undefined;
86
85
  /** 'scroll' only: rendered above the timeline inside the scroll view. */
87
- header?: React.ReactNode;
86
+ header?: React.ReactNode | undefined;
88
87
  /** 'scroll' only: rendered below the timeline inside the scroll view. */
89
- footer?: React.ReactNode;
88
+ footer?: React.ReactNode | undefined;
90
89
  /** 'scroll' only: style for the scroll view's content container (default `{ paddingVertical: 12 }`). */
91
- contentContainerStyle?: StyleProp<ViewStyle>;
90
+ contentContainerStyle?: StyleProp<ViewStyle> | undefined;
92
91
  /** Root style. For 'scroll' this is the scroll view, so set a `height` (or `flex: 1` on a screen). */
93
- style?: StyleProp<ViewStyle>;
92
+ style?: StyleProp<ViewStyle> | undefined;
94
93
  }
95
94
 
96
95
  // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
@@ -186,7 +185,7 @@ function ScrollTimeline({
186
185
  const n = items.length;
187
186
  const rail = 34;
188
187
 
189
- const scrollY = useRef(new Animated.Value(0)).current;
188
+ const [scrollY] = useState(() => new Animated.Value(0));
190
189
  const lastY = useRef(0);
191
190
  const [viewH, setViewH] = useState(0);
192
191
  const [contentH, setContentH] = useState(0);
@@ -216,7 +215,7 @@ function ScrollTimeline({
216
215
  if (!scrolls) return [] as number[];
217
216
  const anchor = viewH * SCROLL_ANCHOR;
218
217
  const k = 1 + (viewH - anchor) / maxScroll;
219
- return items.map((_, i) => (listY + nodes[i] - anchor) / k);
218
+ return items.map((_, i) => (listY + (nodes[i] ?? 0) - anchor) / k);
220
219
  }, [scrolls, viewH, maxScroll, listY, nodes, items]);
221
220
 
222
221
  // progress = lit * 2 + (near ? 1 : 0), one number so React can skip identical updates.
@@ -226,8 +225,8 @@ function ScrollTimeline({
226
225
  const update = (y: number) => {
227
226
  lastY.current = y;
228
227
  let lit = 0;
229
- while (lit <= reach && triggers[lit] <= y) lit++;
230
- const near = lit <= reach && y >= triggers[lit] - viewH * NEAR;
228
+ while (lit <= reach && (triggers[lit] ?? Infinity) <= y) lit++;
229
+ const near = lit <= reach && y >= (triggers[lit] ?? Infinity) - viewH * NEAR;
231
230
  setProgress(lit * 2 + (near ? 1 : 0));
232
231
  };
233
232
  update(lastY.current);
@@ -244,17 +243,17 @@ function ScrollTimeline({
244
243
  );
245
244
 
246
245
  const top = nodes[0] ?? 0;
247
- const fillH = reach > 0 && measured ? nodes[reach] - top : 0;
246
+ const fillH = reach > 0 && measured ? (nodes[reach] ?? top) - top : 0;
248
247
  const dottedFrom = Math.max(0, reach);
249
- const dottedH = measured && reach < n - 1 ? nodes[n - 1] - nodes[dottedFrom] : 0;
248
+ const dottedH = measured && reach < n - 1 ? (nodes[n - 1] ?? 0) - (nodes[dottedFrom] ?? 0) : 0;
250
249
 
251
250
  let fillStyle: Animated.WithAnimatedValue<ViewStyle> | null = null;
252
251
  if (fillH > 0) {
253
252
  if (frozen) {
254
253
  fillStyle = {};
255
254
  } else if (triggers.length) {
256
- const t0 = triggers[0];
257
- const t1 = Math.max(triggers[reach], t0 + 1);
255
+ const t0 = triggers[0] ?? 0;
256
+ const t1 = Math.max(triggers[reach] ?? t0, t0 + 1);
258
257
  const p = scrollY.interpolate({ inputRange: [t0, t1], outputRange: [0.0001, 1], extrapolate: 'clamp' });
259
258
  fillStyle = {
260
259
  transform: [
@@ -359,11 +358,11 @@ interface RowProps {
359
358
  arriveDelay: number;
360
359
  fillMs: number;
361
360
  /** 'scroll': what the dot shows right now (it lights up when the fill reaches it). */
362
- dotStatus?: TimelineStatus;
361
+ dotStatus?: TimelineStatus | undefined;
363
362
  /** 'scroll': the fill has reached this dot — pops it. */
364
- lit?: boolean;
363
+ lit?: boolean | undefined;
365
364
  /** 'scroll': reports the dot centre's y inside the list. */
366
- onNode?: (y: number) => void;
365
+ onNode?: ((y: number) => void) | undefined;
367
366
  }
368
367
 
369
368
  function Row({
@@ -383,7 +382,7 @@ function Row({
383
382
  lit,
384
383
  onNode,
385
384
  }: RowProps) {
386
- const { colors, radius, dark, font } = useKit();
385
+ const { colors, radius, dark, font, shadow } = useKit();
387
386
  const ff = font.family ? { fontFamily: font.family } : null;
388
387
  const compact = variant === 'compact';
389
388
  const card = variant === 'card';
@@ -399,9 +398,9 @@ function Row({
399
398
  const gapBelow = compact ? 14 : card ? 12 : scroll ? 32 : 26;
400
399
  const entrance = !still && !scroll;
401
400
 
402
- const enter = useRef(new Animated.Value(entrance ? 0 : 1)).current;
403
- const fill = useRef(new Animated.Value(still ? 1 : 0)).current;
404
- const pop = useRef(new Animated.Value(1)).current;
401
+ const [enter] = useState(() => new Animated.Value(entrance ? 0 : 1));
402
+ const [fill] = useState(() => new Animated.Value(still ? 1 : 0));
403
+ const [pop] = useState(() => new Animated.Value(1));
405
404
  const wasLit = useRef(!!lit);
406
405
  const [lineH, setLineH] = useState(0);
407
406
 
@@ -508,15 +507,8 @@ function Row({
508
507
  </>
509
508
  );
510
509
 
511
- const cardShadow: ViewStyle =
512
- Platform.OS === 'android'
513
- ? { elevation: current ? 3 : 1 }
514
- : {
515
- shadowColor: '#000',
516
- shadowOffset: { width: 0, height: current ? 6 : 3 },
517
- shadowOpacity: dark ? 0.35 : current ? 0.08 : 0.04,
518
- shadowRadius: current ? 16 : 10,
519
- };
510
+ // boxShadow via the kit: the same soft card shadow on iOS, Android and web.
511
+ const cardShadow: ViewStyle = shadow(current ? 'md' : 'sm');
520
512
 
521
513
  const dotWrap = scroll
522
514
  ? { transform: [{ scale: pop }] }
@@ -693,8 +685,8 @@ function Dot({
693
685
  }) {
694
686
  const { colors, radius, dark } = useKit();
695
687
  const uid = useId().replace(/[^a-zA-Z0-9]/g, '');
696
- const draw = useRef(new Animated.Value(still ? 1 : 0)).current; // JS: dash offsets
697
- const pulse = useRef(new Animated.Value(0)).current; // native: halo
688
+ const [draw] = useState(() => new Animated.Value(still ? 1 : 0)); // JS: dash offsets
689
+ const [pulse] = useState(() => new Animated.Value(0)); // native: halo
698
690
  const current = status === 'current';
699
691
  const marked = status === 'error' || (status === 'done' && checks);
700
692
 
@@ -884,12 +876,9 @@ function Sheen({ id, size }: { id: string; size: number }) {
884
876
  );
885
877
  }
886
878
 
879
+ /** A soft tinted glow as boxShadow, identical on iOS, Android and web. */
887
880
  function glow(color: string): ViewStyle {
888
- return Platform.OS === 'ios'
889
- ? { shadowColor: color, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.3, shadowRadius: 5 }
890
- : Platform.OS === 'android'
891
- ? { elevation: 2 }
892
- : {};
881
+ return { boxShadow: `0px 2px 10px ${alpha(color, 0.3)}` };
893
882
  }
894
883
 
895
884
  function srcOf(image: TimelineImageSource) {
@@ -905,12 +894,15 @@ function TimelineGallery({ images, dim }: { images: TimelineImageSource[]; dim:
905
894
  styles.gallery,
906
895
  { borderRadius: radius.md, borderColor: alpha(colors.foreground, dark ? 0.1 : 0.06), opacity: dim ? 0.6 : 1 },
907
896
  ];
908
- const tile = (i: number, flex: ViewStyle) => (
909
- <Photo key={i} uri={shown[i].uri} delay={i * 90} style={[flex, { borderRadius: radius.scale(4) }]} more={i === 2 && extra > 0 ? extra : 0} />
910
- );
897
+ const tile = (i: number, flex: ViewStyle) => {
898
+ const image = shown[i];
899
+ return image ? (
900
+ <Photo key={i} uri={image.uri} delay={i * 90} style={[flex, { borderRadius: radius.scale(4) }]} more={i === 2 && extra > 0 ? extra : 0} />
901
+ ) : null;
902
+ };
911
903
 
912
904
  if (shown.length === 1) {
913
- return <View style={[frame, { aspectRatio: shown[0].ratio }]}>{tile(0, styles.fill)}</View>;
905
+ return <View style={[frame, { aspectRatio: shown[0]?.ratio }]}>{tile(0, styles.fill)}</View>;
914
906
  }
915
907
  if (shown.length === 2) {
916
908
  return (
@@ -934,7 +926,7 @@ function TimelineGallery({ images, dim }: { images: TimelineImageSource[]; dim:
934
926
  function Photo({ uri, delay, style, more }: { uri: string; delay: number; style: StyleProp<ViewStyle>; more: number }) {
935
927
  const { colors, font } = useKit();
936
928
  const ff = font.family ? { fontFamily: font.family } : null;
937
- const shown = useRef(new Animated.Value(0)).current;
929
+ const [shown] = useState(() => new Animated.Value(0));
938
930
  return (
939
931
  <View style={[styles.photo, { backgroundColor: colors.muted }, style]}>
940
932
  <Animated.Image