@expokit/cli 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/README.md +8 -4
  2. package/bin/expo-kit.js +137 -24
  3. package/bin/lib.mjs +110 -1
  4. package/bin/mcp.mjs +40 -18
  5. package/package.json +2 -2
  6. package/registry/_docs/accordion.json +1 -1
  7. package/registry/_docs/assistant-app.json +47 -0
  8. package/registry/_docs/avatar.json +6 -6
  9. package/registry/_docs/badge.json +4 -4
  10. package/registry/_docs/bottom-nav.json +6 -6
  11. package/registry/_docs/bottom-sheet.json +3 -3
  12. package/registry/_docs/bullet.json +6 -0
  13. package/registry/_docs/button-3d.json +3 -3
  14. package/registry/_docs/button.json +2 -2
  15. package/registry/_docs/calendar.json +5 -5
  16. package/registry/_docs/card.json +1 -1
  17. package/registry/_docs/checkout.json +7 -1
  18. package/registry/_docs/chip-group.json +1 -1
  19. package/registry/_docs/confetti.json +3 -3
  20. package/registry/_docs/date-picker.json +4 -4
  21. package/registry/_docs/donut-shop.json +88 -0
  22. package/registry/_docs/drawer.json +4 -4
  23. package/registry/_docs/fintech-app.json +49 -0
  24. package/registry/_docs/flip-card.json +4 -4
  25. package/registry/_docs/floating-input.json +5 -5
  26. package/registry/_docs/food-app.json +47 -0
  27. package/registry/_docs/glass.json +1 -1
  28. package/registry/_docs/grain.json +80 -0
  29. package/registry/_docs/isometric-bar.json +6 -0
  30. package/registry/_docs/liquidglass-tab.json +1 -1
  31. package/registry/_docs/morphing-button-group.json +96 -0
  32. package/registry/_docs/orb-reveal.json +132 -0
  33. package/registry/_docs/otp-input.json +4 -4
  34. package/registry/_docs/password-input.json +3 -3
  35. package/registry/_docs/payment-card.json +3 -3
  36. package/registry/_docs/pixel-bar.json +6 -0
  37. package/registry/_docs/polar-area.json +6 -0
  38. package/registry/_docs/progress-steps.json +4 -4
  39. package/registry/_docs/progress.json +2 -2
  40. package/registry/_docs/qr-code-button.json +122 -0
  41. package/registry/_docs/radio-group.json +1 -1
  42. package/registry/_docs/search-bar.json +4 -4
  43. package/registry/_docs/segmented-control.json +1 -1
  44. package/registry/_docs/select.json +7 -7
  45. package/registry/_docs/settings.json +6 -0
  46. package/registry/_docs/skeleton.json +3 -3
  47. package/registry/_docs/slider.json +6 -6
  48. package/registry/_docs/spinner.json +3 -3
  49. package/registry/_docs/stateful-button.json +1 -1
  50. package/registry/_docs/stepper.json +7 -7
  51. package/registry/_docs/tabs.json +2 -2
  52. package/registry/_docs/text-animation.json +4 -4
  53. package/registry/_docs/textarea.json +3 -3
  54. package/registry/_docs/ticket-shape.json +1 -1
  55. package/registry/_docs/tilt-card.json +4 -4
  56. package/registry/_docs/time-picker.json +6 -6
  57. package/registry/_docs/timeline-chart.json +6 -0
  58. package/registry/_docs/timeline.json +6 -6
  59. package/registry/_docs/toast.json +8 -8
  60. package/registry/_docs/treemap.json +6 -0
  61. package/registry/_docs/waterfall.json +6 -0
  62. package/registry/accordion/files/src/components/ui/accordion.tsx +36 -7
  63. package/registry/accordion/files/src/lib/kit.tsx +8 -5
  64. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +12 -8
  65. package/registry/action-sheet/files/src/lib/kit.tsx +8 -5
  66. package/registry/alert/files/src/components/ui/alert.tsx +11 -5
  67. package/registry/alert/files/src/components/ui/button.tsx +35 -6
  68. package/registry/alert/files/src/lib/kit.tsx +8 -5
  69. package/registry/assistant-app.json +49 -0
  70. package/registry/avatar/files/src/components/ui/avatar.tsx +7 -4
  71. package/registry/avatar/files/src/lib/kit.tsx +8 -5
  72. package/registry/badge/files/src/components/ui/badge.tsx +23 -9
  73. package/registry/badge/files/src/lib/kit.tsx +8 -5
  74. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +21 -8
  75. package/registry/bottom-nav/files/src/lib/kit.tsx +8 -5
  76. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +7 -6
  77. package/registry/bottom-sheet/files/src/lib/kit.tsx +8 -5
  78. package/registry/button/files/src/components/ui/button.tsx +35 -6
  79. package/registry/button/files/src/lib/kit.tsx +8 -5
  80. package/registry/button-3d/files/src/components/ui/button-3d.tsx +35 -7
  81. package/registry/button-3d/files/src/lib/kit.tsx +8 -5
  82. package/registry/calendar/files/src/components/ui/calendar.tsx +68 -19
  83. package/registry/calendar/files/src/lib/kit.tsx +8 -5
  84. package/registry/card/files/src/components/ui/card.tsx +32 -10
  85. package/registry/card/files/src/lib/kit.tsx +8 -5
  86. package/registry/checkbox/files/src/components/ui/checkbox.tsx +31 -8
  87. package/registry/checkbox/files/src/lib/kit.tsx +8 -5
  88. package/registry/chip-group/files/src/components/ui/chip-group.tsx +39 -7
  89. package/registry/chip-group/files/src/lib/kit.tsx +8 -5
  90. package/registry/confetti/files/src/lib/kit.tsx +8 -5
  91. package/registry/coupon/files/src/components/ui/coupon.tsx +55 -28
  92. package/registry/coupon/files/src/components/ui/flip-card.tsx +23 -4
  93. package/registry/coupon/files/src/components/ui/tilt-card.tsx +59 -18
  94. package/registry/coupon/files/src/lib/kit.tsx +8 -5
  95. package/registry/date-picker/files/src/components/ui/calendar.tsx +68 -19
  96. package/registry/date-picker/files/src/components/ui/date-picker.tsx +98 -44
  97. package/registry/date-picker/files/src/lib/kit.tsx +8 -5
  98. package/registry/dialog/files/src/components/ui/dialog.tsx +19 -9
  99. package/registry/dialog/files/src/lib/kit.tsx +8 -5
  100. package/registry/donut-shop.json +18 -0
  101. package/registry/drawer/files/src/components/ui/drawer.tsx +31 -21
  102. package/registry/drawer/files/src/lib/kit.tsx +8 -5
  103. package/registry/fintech-app.json +57 -0
  104. package/registry/flip-card/files/src/components/ui/flip-card.tsx +23 -4
  105. package/registry/flip-card/files/src/lib/kit.tsx +8 -5
  106. package/registry/floating-input/files/src/components/ui/floating-input.tsx +91 -32
  107. package/registry/floating-input/files/src/lib/kit.tsx +8 -5
  108. package/registry/food-app.json +55 -0
  109. package/registry/grain/files/src/components/ui/grain.tsx +113 -0
  110. package/registry/{tag-input → grain}/files/src/lib/kit.tsx +8 -5
  111. package/registry/grain.json +17 -0
  112. package/registry/kit/files/src/lib/kit.tsx +8 -5
  113. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +2 -2
  114. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +1 -1
  115. package/registry/list-item/files/src/components/ui/list-item.tsx +28 -17
  116. package/registry/list-item/files/src/lib/kit.tsx +8 -5
  117. package/registry/morphing-button-group.json +18 -0
  118. package/registry/orb-reveal.json +20 -0
  119. package/registry/otp-input/files/src/components/ui/otp-input.tsx +57 -24
  120. package/registry/otp-input/files/src/lib/kit.tsx +8 -5
  121. package/registry/password-input/files/src/components/ui/floating-input.tsx +91 -32
  122. package/registry/password-input/files/src/components/ui/password-input.tsx +55 -21
  123. package/registry/password-input/files/src/lib/kit.tsx +8 -5
  124. package/registry/payment-card/files/src/components/ui/flip-card.tsx +23 -4
  125. package/registry/payment-card/files/src/components/ui/payment-card.tsx +35 -18
  126. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +59 -18
  127. package/registry/payment-card/files/src/lib/kit.tsx +8 -5
  128. package/registry/progress/files/src/components/ui/progress.tsx +16 -6
  129. package/registry/progress/files/src/lib/kit.tsx +8 -5
  130. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +22 -11
  131. package/registry/progress-steps/files/src/lib/kit.tsx +8 -5
  132. package/registry/qr-code-button.json +18 -0
  133. package/registry/radio-group/files/src/components/ui/radio-group.tsx +48 -17
  134. package/registry/radio-group/files/src/lib/kit.tsx +8 -5
  135. package/registry/search-bar/files/src/components/ui/search-bar.tsx +81 -26
  136. package/registry/search-bar/files/src/lib/kit.tsx +8 -5
  137. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +40 -10
  138. package/registry/segmented-control/files/src/lib/kit.tsx +8 -5
  139. package/registry/select/files/src/components/ui/select.tsx +161 -86
  140. package/registry/select/files/src/lib/kit.tsx +8 -5
  141. package/registry/skeleton/files/src/components/ui/skeleton.tsx +4 -4
  142. package/registry/skeleton/files/src/lib/kit.tsx +8 -5
  143. package/registry/slider/files/src/components/ui/slider.tsx +56 -27
  144. package/registry/slider/files/src/lib/kit.tsx +8 -5
  145. package/registry/spinner/files/src/components/ui/spinner.tsx +17 -12
  146. package/registry/spinner/files/src/lib/kit.tsx +8 -5
  147. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +12 -4
  148. package/registry/stateful-button/files/src/lib/kit.tsx +8 -5
  149. package/registry/stepper/files/src/components/ui/stepper.tsx +83 -29
  150. package/registry/stepper/files/src/lib/kit.tsx +8 -5
  151. package/registry/switch/files/src/components/ui/switch.tsx +60 -22
  152. package/registry/switch/files/src/lib/kit.tsx +8 -5
  153. package/registry/tabs/files/src/components/ui/tabs.tsx +15 -10
  154. package/registry/tabs/files/src/lib/kit.tsx +8 -5
  155. package/registry/text-animation/files/src/components/ui/text-animation.tsx +11 -6
  156. package/registry/text-animation/files/src/lib/kit.tsx +8 -5
  157. package/registry/textarea/files/src/components/ui/textarea.tsx +115 -45
  158. package/registry/textarea/files/src/lib/kit.tsx +8 -5
  159. package/registry/ticket-shape/files/src/lib/kit.tsx +8 -5
  160. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +59 -18
  161. package/registry/tilt-card/files/src/lib/kit.tsx +8 -5
  162. package/registry/time-picker/files/src/components/ui/time-picker.tsx +156 -75
  163. package/registry/time-picker/files/src/lib/kit.tsx +8 -5
  164. package/registry/timeline/files/src/components/ui/avatar.tsx +7 -4
  165. package/registry/timeline/files/src/components/ui/timeline.tsx +19 -12
  166. package/registry/timeline/files/src/lib/kit.tsx +8 -5
  167. package/registry/toast/files/src/components/ui/button.tsx +35 -6
  168. package/registry/toast/files/src/components/ui/toast.tsx +25 -14
  169. package/registry/toast/files/src/lib/kit.tsx +8 -5
  170. package/schema/components.schema.json +1 -1
  171. package/registry/_docs/color-swatch.json +0 -82
  172. package/registry/_docs/kpi-card.json +0 -14
  173. package/registry/_docs/like-button.json +0 -92
  174. package/registry/_docs/morph-button.json +0 -128
  175. package/registry/_docs/pagination-dots.json +0 -108
  176. package/registry/_docs/shimmer-button.json +0 -98
  177. package/registry/_docs/tag-input.json +0 -148
  178. package/registry/color-swatch.json +0 -17
  179. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +0 -45
  180. package/registry/kpi-card.json +0 -14
  181. package/registry/like-button.json +0 -18
  182. package/registry/morph-button.json +0 -18
  183. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +0 -262
  184. package/registry/pagination-dots/files/src/lib/kit.tsx +0 -219
  185. package/registry/pagination-dots.json +0 -15
  186. package/registry/shimmer-button.json +0 -17
  187. package/registry/tag-input/files/src/components/ui/tag-input.tsx +0 -720
  188. package/registry/tag-input.json +0 -17
@@ -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>
@@ -93,6 +93,10 @@ export interface TimelineProps {
93
93
  style?: StyleProp<ViewStyle>;
94
94
  }
95
95
 
96
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
97
+ // content text scales all the way.
98
+ const FONT_CAP = 1.35;
99
+
96
100
  const ENTER_STAGGER = 70;
97
101
  const FILL_START = 160;
98
102
  /** Where the fill point sits in the viewport at the top of the scroll (it slides to the bottom by the end). */
@@ -244,7 +248,7 @@ function ScrollTimeline({
244
248
  const dottedFrom = Math.max(0, reach);
245
249
  const dottedH = measured && reach < n - 1 ? nodes[n - 1] - nodes[dottedFrom] : 0;
246
250
 
247
- let fillStyle: Animated.WithAnimatedObject<ViewStyle> | null = null;
251
+ let fillStyle: Animated.WithAnimatedValue<ViewStyle> | null = null;
248
252
  if (fillH > 0) {
249
253
  if (frozen) {
250
254
  fillStyle = {};
@@ -379,7 +383,8 @@ function Row({
379
383
  lit,
380
384
  onNode,
381
385
  }: RowProps) {
382
- const { colors, radius, dark } = useKit();
386
+ const { colors, radius, dark, font } = useKit();
387
+ const ff = font.family ? { fontFamily: font.family } : null;
383
388
  const compact = variant === 'compact';
384
389
  const card = variant === 'card';
385
390
  const scroll = variant === 'scroll';
@@ -454,21 +459,21 @@ function Row({
454
459
  <View style={[styles.titleRow, compact && styles.compactRow]}>
455
460
  <Text
456
461
  numberOfLines={compact ? 1 : 2}
457
- style={[styles.title, { fontSize: titleSize, lineHeight: titleLine, color: titleColor }]}
462
+ style={[styles.title, ff, { fontSize: titleSize, lineHeight: titleLine, color: titleColor }]}
458
463
  >
459
464
  {item.title}
460
465
  </Text>
461
466
  {item.time ? (
462
467
  <Text
463
468
  numberOfLines={1}
464
- style={[styles.time, { lineHeight: titleLine, color: current ? tint : colors.mutedForeground }]}
469
+ style={[styles.time, ff, { lineHeight: titleLine, color: current ? tint : colors.mutedForeground }]}
465
470
  >
466
471
  {item.time}
467
472
  </Text>
468
473
  ) : null}
469
474
  </View>
470
475
  {item.description && !compact ? (
471
- <Text style={[styles.desc, { color: colors.mutedForeground }]}>{item.description}</Text>
476
+ <Text style={[styles.desc, ff, { color: colors.mutedForeground }]}>{item.description}</Text>
472
477
  ) : null}
473
478
  {photos.length ? <TimelineGallery images={photos} dim={muted} /> : null}
474
479
  {item.tags?.length || hasLinks ? (
@@ -478,7 +483,7 @@ function Row({
478
483
  key={`t-${t}`}
479
484
  style={[styles.tag, { borderRadius: radius.capsule(22), backgroundColor: alpha(tint, dark ? 0.2 : 0.1) }]}
480
485
  >
481
- <Text style={[styles.tagText, { color: tint }]}>{t}</Text>
486
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.tagText, ff, { color: tint }]}>{t}</Text>
482
487
  </View>
483
488
  ))}
484
489
  {item.links?.map((l) => (
@@ -490,14 +495,14 @@ function Row({
490
495
  hitSlop={12}
491
496
  style={({ pressed }) => [styles.link, { opacity: pressed ? 0.55 : 1 }]}
492
497
  >
493
- <Text style={[styles.linkText, { color: tint }]}>{l.label} →</Text>
498
+ <Text style={[styles.linkText, ff, { color: tint }]}>{l.label} →</Text>
494
499
  </Pressable>
495
500
  ))}
496
501
  </View>
497
502
  ) : null}
498
503
  {item.content ? (
499
504
  <View style={styles.content}>
500
- {isTextNode(item.content) ? <Text style={{ color: colors.foreground }}>{item.content}</Text> : item.content}
505
+ {isTextNode(item.content) ? <Text style={[ff, { color: colors.foreground }]}>{item.content}</Text> : item.content}
501
506
  </View>
502
507
  ) : null}
503
508
  </>
@@ -927,7 +932,8 @@ function TimelineGallery({ images, dim }: { images: TimelineImageSource[]; dim:
927
932
  }
928
933
 
929
934
  function Photo({ uri, delay, style, more }: { uri: string; delay: number; style: StyleProp<ViewStyle>; more: number }) {
930
- const { colors } = useKit();
935
+ const { colors, font } = useKit();
936
+ const ff = font.family ? { fontFamily: font.family } : null;
931
937
  const shown = useRef(new Animated.Value(0)).current;
932
938
  return (
933
939
  <View style={[styles.photo, { backgroundColor: colors.muted }, style]}>
@@ -939,7 +945,7 @@ function Photo({ uri, delay, style, more }: { uri: string; delay: number; style:
939
945
  />
940
946
  {more > 0 ? (
941
947
  <View style={[StyleSheet.absoluteFill, styles.more]}>
942
- <Text style={styles.moreText}>+{more}</Text>
948
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.moreText, ff]}>+{more}</Text>
943
949
  </View>
944
950
  ) : null}
945
951
  </View>
@@ -1037,10 +1043,11 @@ const styles = StyleSheet.create({
1037
1043
  mini: { position: 'absolute', alignItems: 'center', justifyContent: 'center' },
1038
1044
  bodyWrap: { flex: 1, minWidth: 0 },
1039
1045
  card: { borderWidth: 1 },
1040
- titleRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 10 },
1046
+ // Wraps only when the time no longer fits beside the title (large text sizes): it drops below.
1047
+ titleRow: { flexDirection: 'row', flexWrap: 'wrap', alignItems: 'flex-start', gap: 10 },
1041
1048
  compactRow: { alignItems: 'center' },
1042
1049
  title: { flex: 1, fontWeight: '400', letterSpacing: -0.2 },
1043
- time: { fontSize: 12.5, fontWeight: '400', fontVariant: ['tabular-nums'] },
1050
+ time: { flexShrink: 1, fontSize: 12.5, fontWeight: '400', fontVariant: ['tabular-nums'] },
1044
1051
  desc: { fontSize: 14, fontWeight: '400', lineHeight: 20, marginTop: 3 },
1045
1052
  gallery: { marginTop: 12, width: '100%', overflow: 'hidden', borderWidth: 1 },
1046
1053
  grid: { flexDirection: 'row', gap: 3 },
@@ -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,6 +1,7 @@
1
1
  import * as Haptics from 'expo-haptics';
2
- import React, { useId, useRef } from 'react';
2
+ import React, { useEffect, useId, useRef, useState } from 'react';
3
3
  import {
4
+ AccessibilityInfo,
4
5
  ActivityIndicator,
5
6
  Animated,
6
7
  Pressable,
@@ -55,6 +56,10 @@ export interface ButtonProps extends Omit<PressableProps, 'style' | 'children'>
55
56
  textStyle?: StyleProp<TextStyle>;
56
57
  }
57
58
 
59
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
60
+ // content text scales all the way.
61
+ const FONT_CAP = 1.35;
62
+
58
63
  const SIZES: Record<ButtonSize, { h: number; px: number; font: number; gap: number }> = {
59
64
  sm: { h: 36, px: 14, font: 14, gap: 6 },
60
65
  md: { h: 46, px: 18, font: 15.5, gap: 8 },
@@ -116,8 +121,10 @@ export function Button({
116
121
  onPressOut,
117
122
  ...props
118
123
  }: ButtonProps) {
119
- const { colors, radius, dark } = useKit();
124
+ const { colors, radius, dark, font } = useKit();
125
+ const ff = font.family ? { fontFamily: font.family } : null;
120
126
  const gid = 'btn' + useId().replace(/[^a-zA-Z0-9]/g, '');
127
+ const reduce = useReduceMotion();
121
128
  const scale = useRef(new Animated.Value(1)).current;
122
129
  const isDisabled = disabled || loading;
123
130
  const tint = color ?? colors.accent;
@@ -144,12 +151,17 @@ export function Button({
144
151
  const solid = bg !== 'transparent';
145
152
  const quiet = variant === 'ghost' || variant === 'outline' || variant === 'link';
146
153
 
147
- const spring = (to: number) => Animated.spring(scale, { toValue: to, useNativeDriver: true, ...motion.bouncy }).start();
154
+ // Reduce Motion: no squish; the pressed wash / opacity still marks the press.
155
+ const spring = (to: number) => {
156
+ if (reduce) return scale.setValue(1);
157
+ Animated.spring(scale, { toValue: to, useNativeDriver: true, ...motion.bouncy }).start();
158
+ };
148
159
 
149
160
  const label = isTextNode(children) ? (
150
161
  <Text
151
162
  numberOfLines={1}
152
- style={[styles.label, { color: fg, fontSize: s.font }, variant === 'link' && styles.link, textStyle]}
163
+ maxFontSizeMultiplier={FONT_CAP}
164
+ style={[styles.label, ff, { color: fg, fontSize: s.font }, variant === 'link' && styles.link, textStyle]}
153
165
  >
154
166
  {children}
155
167
  </Text>
@@ -184,7 +196,8 @@ export function Button({
184
196
  style={({ pressed }) => [
185
197
  styles.base,
186
198
  {
187
- height: s.h,
199
+ minHeight: s.h,
200
+ height: size === 'icon' ? s.h : undefined,
188
201
  width: size === 'icon' ? s.h : undefined,
189
202
  paddingHorizontal: variant === 'link' ? 0 : s.px,
190
203
  gap: s.gap,
@@ -235,7 +248,7 @@ export function Button({
235
248
  <View style={styles.row}>
236
249
  <ActivityIndicator size="small" color={fg} />
237
250
  {size !== 'icon' && isTextNode(children) ? (
238
- <Text style={[styles.label, { color: fg, fontSize: s.font, opacity: 0.85 }]}>{children}</Text>
251
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.label, ff, { color: fg, fontSize: s.font, opacity: 0.85 }]}>{children}</Text>
239
252
  ) : null}
240
253
  </View>
241
254
  ) : (
@@ -252,6 +265,22 @@ export function Button({
252
265
  );
253
266
  }
254
267
 
268
+ function useReduceMotion() {
269
+ const [reduce, setReduce] = useState(false);
270
+ useEffect(() => {
271
+ let alive = true;
272
+ AccessibilityInfo.isReduceMotionEnabled()
273
+ .then((r) => alive && setReduce(r))
274
+ .catch(() => {});
275
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
276
+ return () => {
277
+ alive = false;
278
+ sub.remove();
279
+ };
280
+ }, []);
281
+ return reduce;
282
+ }
283
+
255
284
  const styles = StyleSheet.create({
256
285
  base: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
257
286
  row: { flexDirection: 'row', alignItems: 'center', gap: 8 },
@@ -271,6 +271,9 @@ export interface ToasterProps {
271
271
 
272
272
  const PEEK = 12;
273
273
  const GUESS_HEIGHT = 64;
274
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
275
+ // content text scales all the way.
276
+ const FONT_CAP = 1.35;
274
277
 
275
278
  /**
276
279
  * Hosts the toasts. It fills its parent absolutely (touches pass through), so drop it at the
@@ -417,7 +420,8 @@ function ToastItem({
417
420
  onRemoved,
418
421
  }: ToastItemProps) {
419
422
  const toast = useToast();
420
- const { colors, radius, dark } = useKit();
423
+ const { colors, radius, dark, font } = useKit();
424
+ const ff = font.family ? { fontFamily: font.family } : null;
421
425
  const toward = edge === 'bottom' ? 1 : -1; // swipe direction that dismisses toward the edge
422
426
  const last = useRef(layout ?? { y: 0, scale: 1, hidden: false, faded: false });
423
427
  if (layout) last.current = layout;
@@ -505,8 +509,8 @@ function ToastItem({
505
509
  },
506
510
  onPanResponderTerminate: () => {
507
511
  setTouching(false);
508
- Animated.spring(dragX, { toValue: 0, useNativeDriver: true }).start();
509
- Animated.spring(dragY, { toValue: 0, useNativeDriver: true }).start();
512
+ Animated.spring(dragX, { toValue: 0, ...motion.snappy, useNativeDriver: true }).start();
513
+ Animated.spring(dragY, { toValue: 0, ...motion.snappy, useNativeDriver: true }).start();
510
514
  },
511
515
  });
512
516
  })(),
@@ -572,11 +576,11 @@ function ToastItem({
572
576
  <Animated.View style={[styles.row, minimal ? styles.padMinimal : icon ? styles.padIcon : styles.pad, { opacity: content }]}>
573
577
  {icon ? <View style={!minimal && data.description ? styles.iconTop : null}>{icon}</View> : null}
574
578
  <View style={minimal ? styles.textMinimal : styles.text}>
575
- <Text numberOfLines={minimal ? 1 : 3} style={[styles.title, { color: look.title }]}>
579
+ <Text numberOfLines={minimal ? 1 : 3} style={[styles.title, ff, { color: look.title }]}>
576
580
  {data.title}
577
581
  </Text>
578
582
  {data.description ? (
579
- <Text numberOfLines={minimal ? 2 : 4} style={[styles.desc, minimal && styles.descMinimal, { color: look.desc }]}>
583
+ <Text numberOfLines={minimal ? 2 : 4} style={[styles.desc, minimal && styles.descMinimal, ff, { color: look.desc }]}>
580
584
  {data.description}
581
585
  </Text>
582
586
  ) : null}
@@ -594,7 +598,9 @@ function ToastItem({
594
598
  { borderRadius: radius.capsule(32), backgroundColor: look.actionBg, opacity: pressed ? 0.7 : 1 },
595
599
  ]}
596
600
  >
597
- <Text style={[styles.actionText, { color: look.actionFg }]}>{data.action.label}</Text>
601
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.actionText, ff, { color: look.actionFg }]}>
602
+ {data.action.label}
603
+ </Text>
598
604
  </Pressable>
599
605
  ) : null}
600
606
  {closeButton ? (
@@ -648,7 +654,8 @@ interface IslandToasterProps {
648
654
  * fade are native-driven on their own wrapper views.
649
655
  */
650
656
  function IslandToaster({ store, toasts, duration, offset, haptics, reduceMotion, style }: IslandToasterProps) {
651
- const { colors, radius, dark } = useKit();
657
+ const { colors, radius, dark, font } = useKit();
658
+ const ff = font.family ? { fontFamily: font.family } : null;
652
659
  const active = toasts.filter((t) => !t.dismissed);
653
660
  const front = active[0];
654
661
 
@@ -893,18 +900,20 @@ function IslandToaster({ store, toasts, duration, offset, haptics, reduceMotion,
893
900
  >
894
901
  {icon ? <View style={expanded && shown.description ? styles.iconTop : null}>{icon}</View> : null}
895
902
  <View style={styles.text}>
896
- <Text numberOfLines={expanded ? 3 : 1} style={[styles.title, { color: fg }]}>
903
+ <Text numberOfLines={expanded ? 3 : 1} style={[styles.title, ff, { color: fg }]}>
897
904
  {shown.title}
898
905
  </Text>
899
906
  {shown.description ? (
900
- <Text numberOfLines={expanded ? undefined : 1} style={[styles.desc, { color: alpha(fg, 0.62) }]}>
907
+ <Text numberOfLines={expanded ? undefined : 1} style={[styles.desc, ff, { color: alpha(fg, 0.62) }]}>
901
908
  {shown.description}
902
909
  </Text>
903
910
  ) : null}
904
911
  </View>
905
912
  {!expanded && shown.action ? (
906
913
  <View style={[styles.islandChip, { borderRadius: radius.capsule(24), backgroundColor: alpha(fg, 0.14) }]}>
907
- <Text style={[styles.islandChipText, { color: fg }]}>{shown.action.label}</Text>
914
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.islandChipText, ff, { color: fg }]}>
915
+ {shown.action.label}
916
+ </Text>
908
917
  </View>
909
918
  ) : null}
910
919
  </Pressable>
@@ -921,7 +930,9 @@ function IslandToaster({ store, toasts, duration, offset, haptics, reduceMotion,
921
930
  { borderRadius: radius.capsule(44), backgroundColor: alpha(fg, pressed ? 0.24 : 0.16) },
922
931
  ]}
923
932
  >
924
- <Text style={[styles.title, { color: fg }]}>{shown.action.label}</Text>
933
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.title, ff, { color: fg }]}>
934
+ {shown.action.label}
935
+ </Text>
925
936
  </Pressable>
926
937
  </View>
927
938
  ) : null}
@@ -1265,7 +1276,7 @@ const styles = StyleSheet.create({
1265
1276
  title: { fontSize: 14.5, fontWeight: '400', lineHeight: 20, letterSpacing: -0.1 },
1266
1277
  desc: { fontSize: 13.5, fontWeight: '400', lineHeight: 19 },
1267
1278
  descMinimal: { fontSize: 13, lineHeight: 17 },
1268
- action: { paddingHorizontal: 12, height: 32, alignItems: 'center', justifyContent: 'center' },
1279
+ action: { paddingHorizontal: 12, minHeight: 32, alignItems: 'center', justifyContent: 'center' },
1269
1280
  actionText: { fontSize: 13, fontWeight: '400' },
1270
1281
  close: { alignSelf: 'flex-start', padding: 2, marginRight: -4 },
1271
1282
  bar: { position: 'absolute', left: 0, right: 0, bottom: 0, height: 2, transformOrigin: 'left' },
@@ -1275,10 +1286,10 @@ const styles = StyleSheet.create({
1275
1286
  islandPlace: { position: 'absolute', top: 0 },
1276
1287
  islandPad: { paddingVertical: 14, paddingHorizontal: 20 },
1277
1288
  islandPadIcon: { paddingTop: 13, paddingBottom: 15, paddingLeft: 14, paddingRight: 18 },
1278
- islandChip: { paddingHorizontal: 10, height: 24, justifyContent: 'center' },
1289
+ islandChip: { paddingHorizontal: 10, minHeight: 24, justifyContent: 'center' },
1279
1290
  islandChipText: { fontSize: 12.5, fontWeight: '400' },
1280
1291
  islandActions: { paddingHorizontal: 14, paddingBottom: 16, marginTop: -2 },
1281
- islandAction: { height: 44, alignItems: 'center', justifyContent: 'center' },
1292
+ islandAction: { minHeight: 44, alignItems: 'center', justifyContent: 'center' },
1282
1293
  islandTrack: { position: 'absolute', left: 28, right: 28, bottom: 6, height: 2, borderRadius: 1, overflow: 'hidden' },
1283
1294
  islandFill: { flex: 1, transformOrigin: 'left' },
1284
1295
  });
@@ -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
  {
2
- "$schema": "https://expokit.karandev.in/schema.json",
2
+ "$schema": "https://expokit.in/schema.json",
3
3
  "aliases": {
4
4
  "components": "@/components",
5
5
  "lib": "@/lib",
@@ -1,82 +0,0 @@
1
- {
2
- "name": "color-swatch",
3
- "category": "Selection",
4
- "aliases": [],
5
- "exportName": "ColorSwatch",
6
- "files": [
7
- "src/components/ui/color-swatch.tsx"
8
- ],
9
- "summary": "A colour picker where the chosen swatch settles in as a ring springs out around it.",
10
- "props": [
11
- [
12
- "colors",
13
- "(string | { color, label? })[]",
14
- "10-colour palette",
15
- "Your swatches (hex)."
16
- ],
17
- [
18
- "value / onValueChange",
19
- "string",
20
- "—",
21
- "Controlled selection (or use defaultValue)."
22
- ],
23
- [
24
- "defaultValue",
25
- "string",
26
- "—",
27
- "Initial selection when uncontrolled."
28
- ],
29
- [
30
- "variant",
31
- "\"circle\" | \"square\" | \"pill\"",
32
- "\"circle\"",
33
- "Swatch shape."
34
- ],
35
- [
36
- "size",
37
- "\"sm\" | \"md\" | \"lg\"",
38
- "\"md\"",
39
- "28 / 36 / 44pt swatches."
40
- ],
41
- [
42
- "scrollable",
43
- "boolean",
44
- "false",
45
- "One horizontally scrolling row instead of a wrapping grid."
46
- ],
47
- [
48
- "showLabels",
49
- "boolean",
50
- "false",
51
- "Colour names under each swatch."
52
- ],
53
- [
54
- "disabled",
55
- "boolean",
56
- "false",
57
- "Dims and blocks presses."
58
- ],
59
- [
60
- "style",
61
- "StyleProp<ViewStyle>",
62
- "—",
63
- "Style for the wrapping grid (the ScrollView when scrollable)."
64
- ]
65
- ],
66
- "behavior": [
67
- [
68
- "Touch",
69
- "Tap a swatch to choose it. Each swatch's hit area is padded out to 44pt."
70
- ],
71
- [
72
- "Motion",
73
- "The chosen swatch settles inward while a ring and check spring out. The check uses whichever ink reads best on that colour."
74
- ],
75
- [
76
- "Screen readers",
77
- "A radiogroup labelled \"Colour\" by default; each swatch is a radio named by its label or hex value."
78
- ]
79
- ],
80
- "usage": null,
81
- "examples": []
82
- }
@@ -1,14 +0,0 @@
1
- {
2
- "name": "kpi-card",
3
- "category": null,
4
- "aliases": [],
5
- "exportName": "KpiCard",
6
- "files": [
7
- "src/components/charts/kpi-card.tsx"
8
- ],
9
- "summary": null,
10
- "props": [],
11
- "behavior": [],
12
- "usage": null,
13
- "examples": []
14
- }
@@ -1,92 +0,0 @@
1
- {
2
- "name": "like-button",
3
- "category": "Buttons",
4
- "aliases": [],
5
- "exportName": "LikeButton",
6
- "files": [
7
- "src/components/ui/like-button.tsx"
8
- ],
9
- "summary": "A heart, star or bookmark that pops and bursts into particles when liked.",
10
- "props": [
11
- [
12
- "liked / onChange",
13
- "boolean",
14
- "—",
15
- "Controlled state (or use defaultLiked)."
16
- ],
17
- [
18
- "defaultLiked",
19
- "boolean",
20
- "false",
21
- "Initial state when uncontrolled; `count` is the count for this state."
22
- ],
23
- [
24
- "variant",
25
- "\"heart\" | \"star\" | \"bookmark\"",
26
- "\"heart\"",
27
- "Heart, star or bookmark icon; also picks the default liked colour and label."
28
- ],
29
- [
30
- "count",
31
- "number",
32
- "—",
33
- "Shows a rolling counter (1.2K formatting). Uncontrolled: the count for defaultLiked; the component adds the user's like."
34
- ],
35
- [
36
- "size",
37
- "number",
38
- "26",
39
- "Icon size."
40
- ],
41
- [
42
- "color",
43
- "string",
44
- "danger / warning / accent",
45
- "Liked colour (per variant default)."
46
- ],
47
- [
48
- "particleColors",
49
- "string[]",
50
- "liked colour + theme",
51
- "Colours of the burst dots."
52
- ],
53
- [
54
- "haptics",
55
- "boolean",
56
- "true",
57
- "Tap when liking."
58
- ],
59
- [
60
- "disabled",
61
- "boolean",
62
- "false",
63
- "Blocks presses."
64
- ],
65
- [
66
- "style",
67
- "StyleProp<ViewStyle>",
68
- "—",
69
- "Style for the pressable row holding the icon and counter."
70
- ]
71
- ],
72
- "behavior": [
73
- [
74
- "Motion",
75
- "Liking pops the icon, fills it on a bouncy spring and throws a ring and a burst of coloured dots. The counter rolls up or down to the new number."
76
- ],
77
- [
78
- "Haptics",
79
- "A light impact when it is liked; un-liking is silent."
80
- ],
81
- [
82
- "Screen readers",
83
- "A button labelled \"Like\", \"Favourite\" or \"Bookmark\" (or your label), with a selected state and the count as its value."
84
- ],
85
- [
86
- "Touch",
87
- "Small icons get a hitSlop that brings the target up to 44pt."
88
- ]
89
- ],
90
- "usage": null,
91
- "examples": []
92
- }