@expokit/cli 0.4.0 → 0.4.2

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 (102) hide show
  1. package/README.md +17 -0
  2. package/bin/lib.mjs +1 -1
  3. package/bin/liquidglass.mjs +281 -0
  4. package/package.json +17 -3
  5. package/registry/_docs/accordion.json +1 -1
  6. package/registry/_docs/avatar.json +6 -6
  7. package/registry/_docs/badge.json +3 -3
  8. package/registry/_docs/bottom-nav.json +6 -6
  9. package/registry/_docs/bottom-sheet.json +3 -3
  10. package/registry/_docs/bullet.json +6 -0
  11. package/registry/_docs/button-3d.json +2 -2
  12. package/registry/_docs/button.json +2 -2
  13. package/registry/_docs/calendar.json +5 -5
  14. package/registry/_docs/card.json +1 -1
  15. package/registry/_docs/chip-group.json +1 -1
  16. package/registry/_docs/confetti.json +3 -3
  17. package/registry/_docs/date-picker.json +4 -4
  18. package/registry/_docs/drawer.json +4 -4
  19. package/registry/_docs/flip-card.json +4 -4
  20. package/registry/_docs/floating-input.json +1 -1
  21. package/registry/_docs/grain.json +2 -2
  22. package/registry/_docs/isometric-bar.json +6 -0
  23. package/registry/_docs/liquidglass-tab.json +2 -1
  24. package/registry/_docs/otp-input.json +1 -1
  25. package/registry/_docs/payment-card.json +3 -3
  26. package/registry/_docs/pixel-bar.json +6 -0
  27. package/registry/_docs/polar-area.json +6 -0
  28. package/registry/_docs/progress-steps.json +4 -4
  29. package/registry/_docs/progress.json +2 -2
  30. package/registry/_docs/radio-group.json +1 -1
  31. package/registry/_docs/search-bar.json +3 -3
  32. package/registry/_docs/segmented-control.json +1 -1
  33. package/registry/_docs/select.json +6 -6
  34. package/registry/_docs/skeleton.json +1 -1
  35. package/registry/_docs/slider.json +6 -6
  36. package/registry/_docs/spinner.json +2 -2
  37. package/registry/_docs/stateful-button.json +1 -1
  38. package/registry/_docs/stepper.json +3 -3
  39. package/registry/_docs/tabs.json +2 -2
  40. package/registry/_docs/text-animation.json +4 -4
  41. package/registry/_docs/textarea.json +1 -1
  42. package/registry/_docs/tilt-card.json +3 -3
  43. package/registry/_docs/time-picker.json +6 -6
  44. package/registry/_docs/timeline-chart.json +6 -0
  45. package/registry/_docs/timeline.json +4 -4
  46. package/registry/_docs/treemap.json +6 -0
  47. package/registry/_docs/waterfall.json +6 -0
  48. package/registry/accordion/files/src/components/ui/accordion.tsx +31 -3
  49. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +4 -1
  50. package/registry/alert/files/src/components/ui/alert.tsx +5 -2
  51. package/registry/alert/files/src/components/ui/button.tsx +32 -4
  52. package/registry/badge/files/src/components/ui/badge.tsx +2 -2
  53. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +11 -5
  54. package/registry/button/files/src/components/ui/button.tsx +32 -4
  55. package/registry/button-3d/files/src/components/ui/button-3d.tsx +33 -6
  56. package/registry/calendar/files/src/components/ui/calendar.tsx +62 -17
  57. package/registry/card/files/src/components/ui/card.tsx +25 -5
  58. package/registry/checkbox/files/src/components/ui/checkbox.tsx +26 -4
  59. package/registry/chip-group/files/src/components/ui/chip-group.tsx +35 -5
  60. package/registry/coupon/files/src/components/ui/coupon.tsx +45 -21
  61. package/registry/coupon/files/src/components/ui/flip-card.tsx +22 -3
  62. package/registry/coupon/files/src/components/ui/tilt-card.tsx +57 -16
  63. package/registry/date-picker/files/src/components/ui/calendar.tsx +62 -17
  64. package/registry/date-picker/files/src/components/ui/date-picker.tsx +96 -42
  65. package/registry/dialog/files/src/components/ui/dialog.tsx +9 -5
  66. package/registry/drawer/files/src/components/ui/drawer.tsx +8 -4
  67. package/registry/flip-card/files/src/components/ui/flip-card.tsx +22 -3
  68. package/registry/floating-input/files/src/components/ui/floating-input.tsx +75 -26
  69. package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +3 -94
  70. package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +2 -1
  71. package/registry/liquidglass-tab/files/src/app/_layout.tsx +2 -2
  72. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +2 -1
  73. package/registry/liquidglass-tab/files/src/components/LiquidGlassTabs.tsx +110 -0
  74. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +31 -6
  75. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +12 -4
  76. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +6 -3
  77. package/registry/liquidglass-tab/files/src/lib/tabs.ts +4 -2
  78. package/registry/list-item/files/src/components/ui/list-item.tsx +10 -3
  79. package/registry/otp-input/files/src/components/ui/otp-input.tsx +50 -19
  80. package/registry/password-input/files/src/components/ui/floating-input.tsx +75 -26
  81. package/registry/password-input/files/src/components/ui/password-input.tsx +53 -19
  82. package/registry/payment-card/files/src/components/ui/flip-card.tsx +22 -3
  83. package/registry/payment-card/files/src/components/ui/payment-card.tsx +19 -15
  84. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +57 -16
  85. package/registry/progress/files/src/components/ui/progress.tsx +9 -2
  86. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +11 -4
  87. package/registry/radio-group/files/src/components/ui/radio-group.tsx +39 -9
  88. package/registry/search-bar/files/src/components/ui/search-bar.tsx +70 -17
  89. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +37 -9
  90. package/registry/select/files/src/components/ui/select.tsx +158 -83
  91. package/registry/slider/files/src/components/ui/slider.tsx +53 -25
  92. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +12 -4
  93. package/registry/stepper/files/src/components/ui/stepper.tsx +76 -26
  94. package/registry/switch/files/src/components/ui/switch.tsx +56 -19
  95. package/registry/tabs/files/src/components/ui/tabs.tsx +7 -4
  96. package/registry/text-animation/files/src/components/ui/text-animation.tsx +7 -2
  97. package/registry/textarea/files/src/components/ui/textarea.tsx +104 -36
  98. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +57 -16
  99. package/registry/time-picker/files/src/components/ui/time-picker.tsx +154 -73
  100. package/registry/timeline/files/src/components/ui/timeline.tsx +9 -4
  101. package/registry/toast/files/src/components/ui/button.tsx +32 -4
  102. package/registry/toast/files/src/components/ui/toast.tsx +15 -6
@@ -25,6 +25,12 @@
25
25
  "(v) => string",
26
26
  "compact",
27
27
  "Axis labels."
28
+ ],
29
+ [
30
+ "accessibilityLabel",
31
+ "string",
32
+ "auto summary",
33
+ "What screen readers announce; defaults to a summary of the data."
28
34
  ]
29
35
  ],
30
36
  "behavior": [],
@@ -136,17 +136,17 @@
136
136
  {
137
137
  "name": "OrderTracking",
138
138
  "title": "Order Tracking",
139
- "code": "import { Text, View } from 'react-native';\nimport { Timeline } from '@/components/ui/timeline';\nimport { alpha, useKit } from '@/lib/kit';\n\nexport default function OrderTracking() {\n const { colors, radius, dark } = useKit();\n return (\n <View style={{ gap: 18 }}>\n <View style={{ flexDirection: 'row', alignItems: 'flex-end', justifyContent: 'space-between' }}>\n <View style={{ gap: 2 }}>\n <Text style={{ fontSize: 12.5, letterSpacing: 0.4, color: colors.mutedForeground }}>ORDER #4821</Text>\n <Text style={{ fontSize: 20, letterSpacing: -0.4, color: colors.foreground }}>Arriving today</Text>\n </View>\n <View\n style={{\n paddingHorizontal: 10,\n paddingVertical: 4,\n borderRadius: radius.capsule(24),\n backgroundColor: alpha(colors.accent, dark ? 0.2 : 0.1),\n }}\n >\n <Text style={{ fontSize: 12.5, color: colors.accent }}>by 6 PM</Text>\n </View>\n </View>\n <Timeline\n items={[\n { title: 'Ordered', time: 'Mon 9:12', description: 'Paid with Apple Pay · 2 items', icon: 'cart' },\n { title: 'Packed', time: 'Tue 16:40', description: 'Left the Rotterdam warehouse', icon: 'package' },\n {\n title: 'Out for delivery',\n time: '11:05',\n description: 'Jonas is 6 stops away in a white van',\n icon: 'truck',\n status: 'current',\n },\n { title: 'Delivered', time: 'Est. 17:30', description: 'Leave at the front door if no one answers', icon: 'home', status: 'upcoming' },\n ]}\n />\n </View>\n );\n}\n"
139
+ "code": "import { Text, View } from 'react-native';\nimport { Timeline } from '@/components/ui/timeline';\nimport { alpha, useKit } from '@/lib/kit';\n\n/** Control text stops at iOS's largest standard size. */\nconst FONT_CAP = 1.35;\n\nexport default function OrderTracking() {\n const { colors, radius, dark } = useKit();\n return (\n <View style={{ gap: 18 }}>\n <View style={{ flexDirection: 'row', alignItems: 'flex-end', justifyContent: 'space-between' }}>\n <View style={{ flexShrink: 1, gap: 2 }}>\n <Text style={{ fontSize: 12.5, letterSpacing: 0.4, color: colors.mutedForeground }}>ORDER #4821</Text>\n <Text style={{ fontSize: 20, letterSpacing: -0.4, color: colors.foreground }}>Arriving today</Text>\n </View>\n <View\n style={{\n paddingHorizontal: 10,\n paddingVertical: 4,\n borderRadius: radius.capsule(24),\n backgroundColor: alpha(colors.accent, dark ? 0.2 : 0.1),\n }}\n >\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 12.5, color: colors.accent }}>by 6 PM</Text>\n </View>\n </View>\n <Timeline\n items={[\n { title: 'Ordered', time: 'Mon 9:12', description: 'Paid with Apple Pay · 2 items', icon: 'cart' },\n { title: 'Packed', time: 'Tue 16:40', description: 'Left the Rotterdam warehouse', icon: 'package' },\n {\n title: 'Out for delivery',\n time: '11:05',\n description: 'Jonas is 6 stops away in a white van',\n icon: 'truck',\n status: 'current',\n },\n { title: 'Delivered', time: 'Est. 17:30', description: 'Leave at the front door if no one answers', icon: 'home', status: 'upcoming' },\n ]}\n />\n </View>\n );\n}\n"
140
140
  },
141
141
  {
142
142
  "name": "Activity",
143
143
  "title": "Activity",
144
- "code": "import { Text, View } from 'react-native';\nimport { Timeline } from '@/components/ui/timeline';\nimport { alpha, useKit } from '@/lib/kit';\n\nexport default function Activity() {\n const { colors, radius, dark } = useKit();\n return (\n <Timeline\n items={[\n {\n title: 'Priya Shah commented',\n time: '2m',\n avatar: { name: 'Priya Shah', src: 'https://i.pravatar.cc/120?img=32' },\n status: 'current',\n content: (\n <View\n style={{\n padding: 12,\n borderRadius: radius.md,\n backgroundColor: alpha(colors.foreground, dark ? 0.06 : 0.035),\n }}\n >\n <Text style={{ fontSize: 14, lineHeight: 20, color: colors.foreground }}>\n Love the new onboarding — can we try the shorter headline on step two?\n </Text>\n </View>\n ),\n },\n {\n title: 'Marco Bianchi uploaded 3 photos',\n time: '1h',\n description: 'Moodboard · Autumn campaign',\n avatar: { name: 'Marco Bianchi', src: 'https://i.pravatar.cc/120?img=15' },\n image: { uri: 'https://picsum.photos/id/1043/800/450', aspectRatio: 16 / 9 },\n },\n {\n title: 'Lena Fischer shared a file',\n time: '3h',\n avatar: { name: 'Lena Fischer' },\n content: (\n <View\n style={{\n flexDirection: 'row',\n alignItems: 'center',\n gap: 12,\n padding: 12,\n borderRadius: radius.md,\n borderWidth: 1,\n borderColor: alpha(colors.foreground, dark ? 0.1 : 0.06),\n backgroundColor: colors.card,\n }}\n >\n <View\n style={{\n width: 36,\n height: 36,\n borderRadius: radius.sm,\n backgroundColor: alpha(colors.danger, 0.12),\n alignItems: 'center',\n justifyContent: 'center',\n }}\n >\n <Text style={{ fontSize: 11, color: colors.danger }}>PDF</Text>\n </View>\n <View style={{ flex: 1 }}>\n <Text numberOfLines={1} style={{ fontSize: 14, color: colors.foreground }}>Q4 brand guidelines.pdf</Text>\n <Text style={{ fontSize: 12.5, color: colors.mutedForeground }}>4.2 MB · 18 pages</Text>\n </View>\n </View>\n ),\n },\n { title: 'Nightly build failed', time: 'Yesterday', description: 'iOS · Signing certificate expired', status: 'error', icon: 'bug' },\n ]}\n />\n );\n}\n"
144
+ "code": "import { Text, View } from 'react-native';\nimport { Timeline } from '@/components/ui/timeline';\nimport { alpha, useKit } from '@/lib/kit';\n\n/** Control text stops at iOS's largest standard size. */\nconst FONT_CAP = 1.35;\n\nexport default function Activity() {\n const { colors, radius, dark } = useKit();\n return (\n <Timeline\n items={[\n {\n title: 'Priya Shah commented',\n time: '2m',\n avatar: { name: 'Priya Shah', src: 'https://i.pravatar.cc/120?img=32' },\n status: 'current',\n content: (\n <View\n style={{\n padding: 12,\n borderRadius: radius.md,\n backgroundColor: alpha(colors.foreground, dark ? 0.06 : 0.035),\n }}\n >\n <Text style={{ fontSize: 14, lineHeight: 20, color: colors.foreground }}>\n Love the new onboarding — can we try the shorter headline on step two?\n </Text>\n </View>\n ),\n },\n {\n title: 'Marco Bianchi uploaded 3 photos',\n time: '1h',\n description: 'Moodboard · Autumn campaign',\n avatar: { name: 'Marco Bianchi', src: 'https://i.pravatar.cc/120?img=15' },\n image: { uri: 'https://picsum.photos/id/1043/800/450', aspectRatio: 16 / 9 },\n },\n {\n title: 'Lena Fischer shared a file',\n time: '3h',\n avatar: { name: 'Lena Fischer' },\n content: (\n <View\n style={{\n flexDirection: 'row',\n alignItems: 'center',\n gap: 12,\n padding: 12,\n borderRadius: radius.md,\n borderWidth: 1,\n borderColor: alpha(colors.foreground, dark ? 0.1 : 0.06),\n backgroundColor: colors.card,\n }}\n >\n <View\n style={{\n width: 36,\n height: 36,\n borderRadius: radius.sm,\n backgroundColor: alpha(colors.danger, 0.12),\n alignItems: 'center',\n justifyContent: 'center',\n }}\n >\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 11, color: colors.danger }}>PDF</Text>\n </View>\n <View style={{ flex: 1 }}>\n <Text numberOfLines={1} style={{ fontSize: 14, color: colors.foreground }}>Q4 brand guidelines.pdf</Text>\n <Text style={{ fontSize: 12.5, color: colors.mutedForeground }}>4.2 MB · 18 pages</Text>\n </View>\n </View>\n ),\n },\n { title: 'Nightly build failed', time: 'Yesterday', description: 'iOS · Signing certificate expired', status: 'error', icon: 'bug' },\n ]}\n />\n );\n}\n"
145
145
  },
146
146
  {
147
147
  "name": "Release",
148
148
  "title": "Release",
149
- "code": "import { Text, View } from 'react-native';\nimport { Timeline } from '@/components/ui/timeline';\nimport { alpha, useKit } from '@/lib/kit';\n\nexport default function Release() {\n const { colors, radius, dark } = useKit();\n return (\n <Timeline\n variant=\"card\"\n items={[\n { title: 'v3.0 — Widgets', time: 'Next week', description: 'Home-screen widgets for iOS and Android.', icon: 'rocket', status: 'upcoming' },\n {\n title: 'v2.4 — Offline mode',\n time: 'Oct 8',\n description: 'Edit notes without a connection; changes sync when you are back online.',\n icon: 'star',\n status: 'current',\n content: (\n <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6 }}>\n {['New', 'Sync', 'iOS', 'Android'].map((t) => (\n <View\n key={t}\n style={{\n paddingHorizontal: 9,\n paddingVertical: 3,\n borderRadius: radius.capsule(22),\n backgroundColor: alpha(colors.accent, dark ? 0.2 : 0.1),\n }}\n >\n <Text style={{ fontSize: 12, color: colors.accent }}>{t}</Text>\n </View>\n ))}\n </View>\n ),\n },\n { title: 'v2.3.2 — Fixes', time: 'Sep 29', description: 'Fixed a crash when sharing large PDFs.', icon: 'bug' },\n { title: 'v2.3 — Shared folders', time: 'Sep 12', description: 'Invite people to a folder and edit together.', icon: 'user' },\n ]}\n />\n );\n}\n"
149
+ "code": "import { Text, View } from 'react-native';\nimport { Timeline } from '@/components/ui/timeline';\nimport { alpha, useKit } from '@/lib/kit';\n\n/** Control text stops at iOS's largest standard size. */\nconst FONT_CAP = 1.35;\n\nexport default function Release() {\n const { colors, radius, dark } = useKit();\n return (\n <Timeline\n variant=\"card\"\n items={[\n { title: 'v3.0 — Widgets', time: 'Next week', description: 'Home-screen widgets for iOS and Android.', icon: 'rocket', status: 'upcoming' },\n {\n title: 'v2.4 — Offline mode',\n time: 'Oct 8',\n description: 'Edit notes without a connection; changes sync when you are back online.',\n icon: 'star',\n status: 'current',\n content: (\n <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6 }}>\n {['New', 'Sync', 'iOS', 'Android'].map((t) => (\n <View\n key={t}\n style={{\n paddingHorizontal: 9,\n paddingVertical: 3,\n borderRadius: radius.capsule(22),\n backgroundColor: alpha(colors.accent, dark ? 0.2 : 0.1),\n }}\n >\n <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: 12, color: colors.accent }}>{t}</Text>\n </View>\n ))}\n </View>\n ),\n },\n { title: 'v2.3.2 — Fixes', time: 'Sep 29', description: 'Fixed a crash when sharing large PDFs.', icon: 'bug' },\n { title: 'v2.3 — Shared folders', time: 'Sep 12', description: 'Invite people to a folder and edit together.', icon: 'user' },\n ]}\n />\n );\n}\n"
150
150
  },
151
151
  {
152
152
  "name": "Compact",
@@ -156,7 +156,7 @@
156
156
  {
157
157
  "name": "Support",
158
158
  "title": "Support",
159
- "code": "import { Text, View } from 'react-native';\nimport { Avatar } from '@/components/ui/avatar';\nimport { Timeline } from '@/components/ui/timeline';\nimport { alpha, useKit } from '@/lib/kit';\n\nexport default function Support() {\n const { colors } = useKit();\n return (\n <View style={{ gap: 14 }}>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>\n <Avatar size=\"sm\" name=\"Support\" status=\"online\" />\n <Text style={{ fontSize: 15, color: colors.foreground }}>Refund request #RF-2207</Text>\n </View>\n <Timeline\n variant=\"compact\"\n accent={colors.success}\n items={[\n { title: 'Request received', time: 'Oct 9' },\n { title: 'Return label sent', time: 'Oct 9' },\n { title: 'Card refund declined', time: 'Oct 12', status: 'error' },\n { title: 'Refund to store credit', time: 'Now', status: 'current' },\n ]}\n />\n </View>\n );\n}\n"
159
+ "code": "import { Text, View } from 'react-native';\nimport { Avatar } from '@/components/ui/avatar';\nimport { Timeline } from '@/components/ui/timeline';\nimport { alpha, useKit } from '@/lib/kit';\n\nexport default function Support() {\n const { colors } = useKit();\n return (\n <View style={{ gap: 14 }}>\n <View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>\n <Avatar size=\"sm\" name=\"Support\" status=\"online\" />\n <Text style={{ flexShrink: 1, fontSize: 15, color: colors.foreground }}>Refund request #RF-2207</Text>\n </View>\n <Timeline\n variant=\"compact\"\n accent={colors.success}\n items={[\n { title: 'Request received', time: 'Oct 9' },\n { title: 'Return label sent', time: 'Oct 9' },\n { title: 'Card refund declined', time: 'Oct 12', status: 'error' },\n { title: 'Refund to store credit', time: 'Now', status: 'current' },\n ]}\n />\n </View>\n );\n}\n"
160
160
  },
161
161
  {
162
162
  "name": "Journey",
@@ -25,6 +25,12 @@
25
25
  "string[]",
26
26
  "theme palette",
27
27
  "Tile colours."
28
+ ],
29
+ [
30
+ "accessibilityLabel",
31
+ "string",
32
+ "auto summary",
33
+ "What screen readers announce; defaults to a summary of the data."
28
34
  ]
29
35
  ],
30
36
  "behavior": [],
@@ -31,6 +31,12 @@
31
31
  "boolean",
32
32
  "true",
33
33
  "Change labels."
34
+ ],
35
+ [
36
+ "accessibilityLabel",
37
+ "string",
38
+ "auto summary",
39
+ "What screen readers announce; defaults to a summary of the data."
34
40
  ]
35
41
  ],
36
42
  "behavior": [],
@@ -1,5 +1,6 @@
1
1
  import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import {
3
+ AccessibilityInfo,
3
4
  Animated,
4
5
  Pressable,
5
6
  StyleSheet,
@@ -36,6 +37,7 @@ interface Ctx {
36
37
  toggle: (v: string) => void;
37
38
  variant: AccordionVariant;
38
39
  tint: string;
40
+ reduce: boolean;
39
41
  }
40
42
  const AccordionContext = createContext<Ctx | null>(null);
41
43
  const PositionContext = createContext({ first: true, last: true });
@@ -53,6 +55,7 @@ export function Accordion({
53
55
  style,
54
56
  }: AccordionProps) {
55
57
  const { colors, radius } = useKit();
58
+ const reduce = useReduceMotion();
56
59
  const [inner, setInner] = useState(defaultValue);
57
60
  const open = value ?? inner;
58
61
  const openRef = useRef(open);
@@ -72,7 +75,10 @@ export function Accordion({
72
75
  [type, collapsible, value, onValueChange]
73
76
  );
74
77
 
75
- const ctx = useMemo(() => ({ open, toggle, variant, tint: accent ?? colors.accent }), [open, toggle, variant, accent, colors.accent]);
78
+ const ctx = useMemo(
79
+ () => ({ open, toggle, variant, tint: accent ?? colors.accent, reduce }),
80
+ [open, toggle, variant, accent, colors.accent, reduce]
81
+ );
76
82
  const items = React.Children.toArray(children);
77
83
 
78
84
  return (
@@ -117,7 +123,7 @@ export interface AccordionItemProps {
117
123
  export function AccordionItem({ value, title, subtitle, icon, children, disabled, style }: AccordionItemProps) {
118
124
  const ctx = useContext(AccordionContext);
119
125
  if (!ctx) throw new Error('AccordionItem must be used inside <Accordion>');
120
- const { open: openValues, toggle, variant, tint } = ctx;
126
+ const { open: openValues, toggle, variant, tint, reduce } = ctx;
121
127
  const { last } = useContext(PositionContext);
122
128
  const { colors, radius, dark, font } = useKit();
123
129
  const ff = font.family ? { fontFamily: font.family } : null;
@@ -129,9 +135,15 @@ export function AccordionItem({ value, title, subtitle, icon, children, disabled
129
135
  const turn = useRef(new Animated.Value(open ? 1 : 0)).current;
130
136
 
131
137
  useEffect(() => {
138
+ // Reduce Motion: open and close at once, chevron flips without turning.
139
+ if (reduce) {
140
+ progress.setValue(open ? 1 : 0);
141
+ turn.setValue(open ? 1 : 0);
142
+ return;
143
+ }
132
144
  Animated.spring(progress, { toValue: open ? 1 : 0, useNativeDriver: false, ...motion.gentle, overshootClamping: true }).start();
133
145
  Animated.spring(turn, { toValue: open ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
134
- }, [open, progress, turn]);
146
+ }, [open, progress, turn, reduce]);
135
147
 
136
148
  const onLayout = (e: LayoutChangeEvent) => {
137
149
  const h = Math.ceil(e.nativeEvent.layout.height);
@@ -222,6 +234,22 @@ export function AccordionItem({ value, title, subtitle, icon, children, disabled
222
234
 
223
235
  Accordion.Item = AccordionItem;
224
236
 
237
+ function useReduceMotion() {
238
+ const [reduce, setReduce] = useState(false);
239
+ useEffect(() => {
240
+ let alive = true;
241
+ AccessibilityInfo.isReduceMotionEnabled()
242
+ .then((r) => alive && setReduce(r))
243
+ .catch(() => {});
244
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
245
+ return () => {
246
+ alive = false;
247
+ sub.remove();
248
+ };
249
+ }, []);
250
+ return reduce;
251
+ }
252
+
225
253
  const styles = StyleSheet.create({
226
254
  separated: { gap: 10 },
227
255
  header: { flexDirection: 'row', alignItems: 'center', minHeight: 56, paddingVertical: 14, gap: 12 },
@@ -104,6 +104,9 @@ export interface ActionSheetProps {
104
104
  style?: StyleProp<ViewStyle>;
105
105
  }
106
106
 
107
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
108
+ // content text scales all the way.
109
+ const FONT_CAP = 1.35;
107
110
  const DEFAULT_BOTTOM_INSET = Platform.select({ ios: 20, android: 12, default: 16 }) ?? 16;
108
111
 
109
112
  /** iOS-style resistance: moves 1:1 at first, then ever more slowly towards `limit`. */
@@ -296,7 +299,7 @@ export function ActionSheet({
296
299
  <View style={[styles.tileIcon, { backgroundColor: fill, borderRadius: radius.scale(16) }]}>
297
300
  {icon(a, fg, 24)}
298
301
  </View>
299
- <Text numberOfLines={2} style={[styles.tileLabel, ff, { color: a.destructive ? colors.danger : colors.foreground }]}>
302
+ <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={2} style={[styles.tileLabel, ff, { color: a.destructive ? colors.danger : colors.foreground }]}>
300
303
  {a.label}
301
304
  </Text>
302
305
  </Row>
@@ -64,6 +64,9 @@ export interface AlertProps {
64
64
  style?: StyleProp<ViewStyle>;
65
65
  }
66
66
 
67
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
68
+ // content text scales all the way.
69
+ const FONT_CAP = 1.35;
67
70
  const HALO = 34;
68
71
  const HALO_COMPACT = 28;
69
72
 
@@ -454,11 +457,11 @@ function Pill({
454
457
  hitSlop={(44 - height) / 2}
455
458
  style={({ pressed }) => [
456
459
  styles.pill,
457
- { height, borderRadius: radius, backgroundColor: bg, paddingHorizontal: height >= 32 ? 14 : 12 },
460
+ { minHeight: height, borderRadius: radius, backgroundColor: bg, paddingHorizontal: height >= 32 ? 14 : 12 },
458
461
  pressed ? { opacity: 0.75, transform: [{ scale: 0.97 }] } : null,
459
462
  ]}
460
463
  >
461
- <Text style={[styles.pillText, ff, { color: fg }]} numberOfLines={1}>
464
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.pillText, ff, { color: fg }]} numberOfLines={1}>
462
465
  {action.label}
463
466
  </Text>
464
467
  </Pressable>
@@ -1,6 +1,7 @@
1
1
  import * as Haptics from 'expo-haptics';
2
- import React, { useId, useRef } from 'react';
2
+ import React, { useEffect, useId, useRef, useState } from 'react';
3
3
  import {
4
+ AccessibilityInfo,
4
5
  ActivityIndicator,
5
6
  Animated,
6
7
  Pressable,
@@ -55,6 +56,10 @@ export interface ButtonProps extends Omit<PressableProps, 'style' | 'children'>
55
56
  textStyle?: StyleProp<TextStyle>;
56
57
  }
57
58
 
59
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
60
+ // content text scales all the way.
61
+ const FONT_CAP = 1.35;
62
+
58
63
  const SIZES: Record<ButtonSize, { h: number; px: number; font: number; gap: number }> = {
59
64
  sm: { h: 36, px: 14, font: 14, gap: 6 },
60
65
  md: { h: 46, px: 18, font: 15.5, gap: 8 },
@@ -119,6 +124,7 @@ export function Button({
119
124
  const { colors, radius, dark, font } = useKit();
120
125
  const ff = font.family ? { fontFamily: font.family } : null;
121
126
  const gid = 'btn' + useId().replace(/[^a-zA-Z0-9]/g, '');
127
+ const reduce = useReduceMotion();
122
128
  const scale = useRef(new Animated.Value(1)).current;
123
129
  const isDisabled = disabled || loading;
124
130
  const tint = color ?? colors.accent;
@@ -145,11 +151,16 @@ export function Button({
145
151
  const solid = bg !== 'transparent';
146
152
  const quiet = variant === 'ghost' || variant === 'outline' || variant === 'link';
147
153
 
148
- 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
+ };
149
159
 
150
160
  const label = isTextNode(children) ? (
151
161
  <Text
152
162
  numberOfLines={1}
163
+ maxFontSizeMultiplier={FONT_CAP}
153
164
  style={[styles.label, ff, { color: fg, fontSize: s.font }, variant === 'link' && styles.link, textStyle]}
154
165
  >
155
166
  {children}
@@ -185,7 +196,8 @@ export function Button({
185
196
  style={({ pressed }) => [
186
197
  styles.base,
187
198
  {
188
- height: s.h,
199
+ minHeight: s.h,
200
+ height: size === 'icon' ? s.h : undefined,
189
201
  width: size === 'icon' ? s.h : undefined,
190
202
  paddingHorizontal: variant === 'link' ? 0 : s.px,
191
203
  gap: s.gap,
@@ -236,7 +248,7 @@ export function Button({
236
248
  <View style={styles.row}>
237
249
  <ActivityIndicator size="small" color={fg} />
238
250
  {size !== 'icon' && isTextNode(children) ? (
239
- <Text style={[styles.label, ff, { 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>
240
252
  ) : null}
241
253
  </View>
242
254
  ) : (
@@ -253,6 +265,22 @@ export function Button({
253
265
  );
254
266
  }
255
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
+
256
284
  const styles = StyleSheet.create({
257
285
  base: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
258
286
  row: { flexDirection: 'row', alignItems: 'center', gap: 8 },
@@ -222,7 +222,7 @@ function span(v: Animated.Value, start: number, end: number, from: number, to: n
222
222
  }
223
223
 
224
224
  const easeIn = Easing.in(Easing.cubic);
225
- const easeBack = Easing.out(Easing.back(1.7));
225
+ const easeSettle = Easing.bezier(0.32, 0.72, 0, 1);
226
226
  const easeLinear = Easing.linear;
227
227
 
228
228
  /** One side of a label roll: letters (or the whole label) slide up out of / up into the clipped slot. */
@@ -260,7 +260,7 @@ function RollingLabel({
260
260
  style={{
261
261
  opacity: out ? span(progress, begin, fade, 1, 0, easeLinear) : span(progress, begin, fade, 0, 1, easeLinear),
262
262
  transform: [
263
- { translateY: out ? span(progress, begin, end, 0, -distance, easeIn) : span(progress, begin, end, distance, 0, easeBack) },
263
+ { translateY: out ? span(progress, begin, end, 0, -distance, easeIn) : span(progress, begin, end, distance, 0, easeSettle) },
264
264
  ],
265
265
  }}
266
266
  >
@@ -99,6 +99,10 @@ export interface BottomNavProps {
99
99
  style?: StyleProp<ViewStyle>;
100
100
  }
101
101
 
102
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
103
+ // content text scales all the way.
104
+ const FONT_CAP = 1.35;
105
+
102
106
  const ROW_H = 60;
103
107
  const PAD_X = 6;
104
108
  const PAD_TOP = 6;
@@ -512,6 +516,7 @@ function NavButton({ item, active, look, tint, idle, surface, fab, showLabels, r
512
516
  >
513
517
  <Animated.Text
514
518
  numberOfLines={1}
519
+ maxFontSizeMultiplier={FONT_CAP}
515
520
  style={[
516
521
  styles.pillLabel,
517
522
  ff,
@@ -531,7 +536,7 @@ function NavButton({ item, active, look, tint, idle, surface, fab, showLabels, r
531
536
  {/* Invisible measurer: the label's natural width, so it never ellipsizes while growing. */}
532
537
  {showLabels ? (
533
538
  <View pointerEvents="none" style={styles.measure} accessibilityElementsHidden importantForAccessibility="no-hide-descendants">
534
- <Text style={[styles.pillLabel, styles.selfStart, ff]} onLayout={(e) => setLabelW(Math.ceil(e.nativeEvent.layout.width))}>
539
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.pillLabel, styles.selfStart, ff]} onLayout={(e) => setLabelW(Math.ceil(e.nativeEvent.layout.width))}>
535
540
  {item.label}
536
541
  </Text>
537
542
  </View>
@@ -561,6 +566,7 @@ function NavButton({ item, active, look, tint, idle, surface, fab, showLabels, r
561
566
  {showLabels ? (
562
567
  <Animated.Text
563
568
  numberOfLines={1}
569
+ maxFontSizeMultiplier={FONT_CAP}
564
570
  style={[
565
571
  styles.label,
566
572
  styles.floatLabel,
@@ -591,7 +597,7 @@ function NavButton({ item, active, look, tint, idle, surface, fab, showLabels, r
591
597
  {badge}
592
598
  </Animated.View>
593
599
  {showLabels ? (
594
- <Text numberOfLines={1} style={[styles.label, ff, { color: active ? tint : idle }]}>
600
+ <Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.label, ff, { color: active ? tint : idle }]}>
595
601
  {item.label}
596
602
  </Text>
597
603
  ) : null}
@@ -677,7 +683,7 @@ function CentreAction({
677
683
  </FabFace>
678
684
  </Animated.View>
679
685
  {showLabel ? (
680
- <Text numberOfLines={1} style={[styles.label, styles.abs, ff, { top: labelTop, color: labelColor }]}>
686
+ <Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.label, styles.abs, ff, { top: labelTop, color: labelColor }]}>
681
687
  {item.label}
682
688
  </Text>
683
689
  ) : null}
@@ -791,7 +797,7 @@ function Badge({ value, ring, reduce }: { value: true | number; ring: string; re
791
797
  },
792
798
  ]}
793
799
  >
794
- {!dot && <Text style={[styles.badgeText, ff, { color: colors.dangerForeground }]}>{text}</Text>}
800
+ {!dot && <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.badgeText, ff, { color: colors.dangerForeground }]}>{text}</Text>}
795
801
  </Animated.View>
796
802
  );
797
803
  }
@@ -1043,7 +1049,7 @@ const styles = StyleSheet.create({
1043
1049
  top: -6,
1044
1050
  right: -11,
1045
1051
  minWidth: 19,
1046
- height: 19,
1052
+ minHeight: 19,
1047
1053
  paddingHorizontal: 4,
1048
1054
  alignItems: 'center',
1049
1055
  justifyContent: 'center',
@@ -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 },
@@ -119,6 +124,7 @@ export function Button({
119
124
  const { colors, radius, dark, font } = useKit();
120
125
  const ff = font.family ? { fontFamily: font.family } : null;
121
126
  const gid = 'btn' + useId().replace(/[^a-zA-Z0-9]/g, '');
127
+ const reduce = useReduceMotion();
122
128
  const scale = useRef(new Animated.Value(1)).current;
123
129
  const isDisabled = disabled || loading;
124
130
  const tint = color ?? colors.accent;
@@ -145,11 +151,16 @@ export function Button({
145
151
  const solid = bg !== 'transparent';
146
152
  const quiet = variant === 'ghost' || variant === 'outline' || variant === 'link';
147
153
 
148
- 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
+ };
149
159
 
150
160
  const label = isTextNode(children) ? (
151
161
  <Text
152
162
  numberOfLines={1}
163
+ maxFontSizeMultiplier={FONT_CAP}
153
164
  style={[styles.label, ff, { color: fg, fontSize: s.font }, variant === 'link' && styles.link, textStyle]}
154
165
  >
155
166
  {children}
@@ -185,7 +196,8 @@ export function Button({
185
196
  style={({ pressed }) => [
186
197
  styles.base,
187
198
  {
188
- height: s.h,
199
+ minHeight: s.h,
200
+ height: size === 'icon' ? s.h : undefined,
189
201
  width: size === 'icon' ? s.h : undefined,
190
202
  paddingHorizontal: variant === 'link' ? 0 : s.px,
191
203
  gap: s.gap,
@@ -236,7 +248,7 @@ export function Button({
236
248
  <View style={styles.row}>
237
249
  <ActivityIndicator size="small" color={fg} />
238
250
  {size !== 'icon' && isTextNode(children) ? (
239
- <Text style={[styles.label, ff, { 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>
240
252
  ) : null}
241
253
  </View>
242
254
  ) : (
@@ -253,6 +265,22 @@ export function Button({
253
265
  );
254
266
  }
255
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
+
256
284
  const styles = StyleSheet.create({
257
285
  base: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
258
286
  row: { flexDirection: 'row', alignItems: 'center', gap: 8 },
@@ -1,5 +1,5 @@
1
- import React, { useRef } from 'react';
2
- import { ActivityIndicator, Animated, Platform, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { AccessibilityInfo, ActivityIndicator, Animated, Platform, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
3
3
  import * as Haptics from 'expo-haptics';
4
4
  import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';
5
5
 
@@ -38,6 +38,10 @@ export interface Button3DProps {
38
38
  style?: StyleProp<ViewStyle>;
39
39
  }
40
40
 
41
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
42
+ // content text scales all the way.
43
+ const FONT_CAP = 1.35;
44
+
41
45
  const SIZES = {
42
46
  sm: { h: 36, px: 14, font: 13, depth: 4, gap: 6 },
43
47
  md: { h: 46, px: 18, font: 15, depth: 5, gap: 8 },
@@ -106,6 +110,7 @@ export function Button3D({
106
110
  style,
107
111
  }: Button3DProps) {
108
112
  const { dark, font } = useKit();
113
+ const reduce = useReduceMotion();
109
114
  const ff = font.family ? { fontFamily: font.family } : null;
110
115
  const s = SIZES[size];
111
116
  const depth = depthProp ?? s.depth;
@@ -116,14 +121,17 @@ export function Button3D({
116
121
  const gid = `b3d-${color ? color.replace('#', '') : kind}`;
117
122
 
118
123
  const press = useRef(new Animated.Value(0)).current;
119
- const to = (v: number) => Animated.spring(press, { toValue: v, ...(v ? motion.snappy : motion.bouncy), useNativeDriver: true }).start();
124
+ const to = (v: number) =>
125
+ reduce
126
+ ? Animated.timing(press, { toValue: v, duration: 120, useNativeDriver: true }).start()
127
+ : Animated.spring(press, { toValue: v, ...(v ? motion.snappy : motion.bouncy), useNativeDriver: true }).start();
120
128
 
121
129
  const label = loading ? (
122
130
  <ActivityIndicator color={m.ink} size="small" />
123
131
  ) : isTextNode(children) ? (
124
132
  // No numberOfLines: on a narrow fixed-width key ("esc", "Q") the label may use the padding
125
133
  // instead of being cut to an ellipsis.
126
- <Text style={[{ color: m.ink, fontSize: round ? s.font + 4 : s.font, fontWeight: '600', letterSpacing: 0.1, flexShrink: 0, textAlign: 'center' }, ff]}>
134
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: m.ink, fontSize: round ? s.font + 4 : s.font, fontWeight: '600', letterSpacing: 0.1, flexShrink: 0, textAlign: 'center' }, ff]}>
127
135
  {children}
128
136
  </Text>
129
137
  ) : (
@@ -185,12 +193,15 @@ export function Button3D({
185
193
  style={[
186
194
  styles.face,
187
195
  {
188
- height: s.h,
196
+ // A circle keeps its fixed size; other keys may grow taller for a large text size.
197
+ minHeight: s.h,
198
+ height: round ? s.h : undefined,
189
199
  paddingHorizontal: round ? 0 : s.px,
190
200
  gap: s.gap,
191
201
  borderRadius: corner,
192
202
  elevation: 4,
193
- transform: [{ translateY: press.interpolate({ inputRange: [0, 1], outputRange: [0, depth] }) }],
203
+ // Reduce Motion: the key doesn't sink; only the contact shadow dims.
204
+ transform: reduce ? [] : [{ translateY: press.interpolate({ inputRange: [0, 1], outputRange: [0, depth] }) }],
194
205
  },
195
206
  ]}
196
207
  >
@@ -206,6 +217,22 @@ export function Button3D({
206
217
  );
207
218
  }
208
219
 
220
+ function useReduceMotion() {
221
+ const [reduce, setReduce] = useState(false);
222
+ useEffect(() => {
223
+ let alive = true;
224
+ AccessibilityInfo.isReduceMotionEnabled()
225
+ .then((r) => alive && setReduce(r))
226
+ .catch(() => {});
227
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
228
+ return () => {
229
+ alive = false;
230
+ sub.remove();
231
+ };
232
+ }, []);
233
+ return reduce;
234
+ }
235
+
209
236
  const styles = StyleSheet.create({
210
237
  fill: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
211
238
  shadow: { shadowColor: '#000', shadowRadius: 8, shadowOffset: { width: 0, height: 5 }, elevation: 3 },