@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.
- package/README.md +17 -0
- package/bin/lib.mjs +1 -1
- package/bin/liquidglass.mjs +281 -0
- package/package.json +17 -3
- package/registry/_docs/accordion.json +1 -1
- package/registry/_docs/avatar.json +6 -6
- package/registry/_docs/badge.json +3 -3
- package/registry/_docs/bottom-nav.json +6 -6
- package/registry/_docs/bottom-sheet.json +3 -3
- package/registry/_docs/bullet.json +6 -0
- package/registry/_docs/button-3d.json +2 -2
- package/registry/_docs/button.json +2 -2
- package/registry/_docs/calendar.json +5 -5
- package/registry/_docs/card.json +1 -1
- package/registry/_docs/chip-group.json +1 -1
- package/registry/_docs/confetti.json +3 -3
- package/registry/_docs/date-picker.json +4 -4
- package/registry/_docs/drawer.json +4 -4
- package/registry/_docs/flip-card.json +4 -4
- package/registry/_docs/floating-input.json +1 -1
- package/registry/_docs/grain.json +2 -2
- package/registry/_docs/isometric-bar.json +6 -0
- package/registry/_docs/liquidglass-tab.json +2 -1
- package/registry/_docs/otp-input.json +1 -1
- package/registry/_docs/payment-card.json +3 -3
- package/registry/_docs/pixel-bar.json +6 -0
- package/registry/_docs/polar-area.json +6 -0
- package/registry/_docs/progress-steps.json +4 -4
- package/registry/_docs/progress.json +2 -2
- package/registry/_docs/radio-group.json +1 -1
- package/registry/_docs/search-bar.json +3 -3
- package/registry/_docs/segmented-control.json +1 -1
- package/registry/_docs/select.json +6 -6
- package/registry/_docs/skeleton.json +1 -1
- package/registry/_docs/slider.json +6 -6
- package/registry/_docs/spinner.json +2 -2
- package/registry/_docs/stateful-button.json +1 -1
- package/registry/_docs/stepper.json +3 -3
- package/registry/_docs/tabs.json +2 -2
- package/registry/_docs/text-animation.json +4 -4
- package/registry/_docs/textarea.json +1 -1
- package/registry/_docs/tilt-card.json +3 -3
- package/registry/_docs/time-picker.json +6 -6
- package/registry/_docs/timeline-chart.json +6 -0
- package/registry/_docs/timeline.json +4 -4
- package/registry/_docs/treemap.json +6 -0
- package/registry/_docs/waterfall.json +6 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +31 -3
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +4 -1
- package/registry/alert/files/src/components/ui/alert.tsx +5 -2
- package/registry/alert/files/src/components/ui/button.tsx +32 -4
- package/registry/badge/files/src/components/ui/badge.tsx +2 -2
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +11 -5
- package/registry/button/files/src/components/ui/button.tsx +32 -4
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +33 -6
- package/registry/calendar/files/src/components/ui/calendar.tsx +62 -17
- package/registry/card/files/src/components/ui/card.tsx +25 -5
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +26 -4
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +35 -5
- package/registry/coupon/files/src/components/ui/coupon.tsx +45 -21
- package/registry/coupon/files/src/components/ui/flip-card.tsx +22 -3
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +57 -16
- package/registry/date-picker/files/src/components/ui/calendar.tsx +62 -17
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +96 -42
- package/registry/dialog/files/src/components/ui/dialog.tsx +9 -5
- package/registry/drawer/files/src/components/ui/drawer.tsx +8 -4
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +22 -3
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +75 -26
- package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +3 -94
- package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +2 -1
- package/registry/liquidglass-tab/files/src/app/_layout.tsx +2 -2
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +2 -1
- package/registry/liquidglass-tab/files/src/components/LiquidGlassTabs.tsx +110 -0
- package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +31 -6
- package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +12 -4
- package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +6 -3
- package/registry/liquidglass-tab/files/src/lib/tabs.ts +4 -2
- package/registry/list-item/files/src/components/ui/list-item.tsx +10 -3
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +50 -19
- package/registry/password-input/files/src/components/ui/floating-input.tsx +75 -26
- package/registry/password-input/files/src/components/ui/password-input.tsx +53 -19
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +22 -3
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +19 -15
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +57 -16
- package/registry/progress/files/src/components/ui/progress.tsx +9 -2
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +11 -4
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +39 -9
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +70 -17
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +37 -9
- package/registry/select/files/src/components/ui/select.tsx +158 -83
- package/registry/slider/files/src/components/ui/slider.tsx +53 -25
- package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +12 -4
- package/registry/stepper/files/src/components/ui/stepper.tsx +76 -26
- package/registry/switch/files/src/components/ui/switch.tsx +56 -19
- package/registry/tabs/files/src/components/ui/tabs.tsx +7 -4
- package/registry/text-animation/files/src/components/ui/text-animation.tsx +7 -2
- package/registry/textarea/files/src/components/ui/textarea.tsx +104 -36
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +57 -16
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +154 -73
- package/registry/timeline/files/src/components/ui/timeline.tsx +9 -4
- package/registry/toast/files/src/components/ui/button.tsx +32 -4
- package/registry/toast/files/src/components/ui/toast.tsx +15 -6
|
@@ -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",
|
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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 },
|