@entropy-softworks/ui 2026.8.42 → 2026.9.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.
@@ -4,6 +4,21 @@ import React from "react";
4
4
  * the active palette via NativeWind classes). Self-manages the entered
5
5
  * digits and fires `onComplete` when `length` is reached; pass a changing
6
6
  * `resetKey` to clear it (e.g. after a wrong PIN).
7
+ *
8
+ * # Sizing
9
+ *
10
+ * The pad measures itself against the viewport instead of sitting at a fixed
11
+ * 288px. On a modern phone a fixed pad reads as a small control marooned in the
12
+ * middle of a large screen — and this is the primary way into the app for a
13
+ * device-only vault, so it should feel like the screen's purpose rather than a
14
+ * widget on it. Keys grow with the available width and stop at `MAX_KEY` so a
15
+ * tablet does not get comedy-sized buttons.
16
+ *
17
+ * # Why the keys have a fill
18
+ *
19
+ * They previously had no background at all until the moment of the press, which
20
+ * left three columns of bare digits that did not read as pressable. A resting
21
+ * fill plus a border is what makes a target look like a target.
7
22
  */
8
23
  export interface PinPadProps {
9
24
  length?: number;
@@ -14,7 +29,13 @@ export interface PinPadProps {
14
29
  /** Bump to clear the entered digits. */
15
30
  resetKey?: string | number;
16
31
  error?: boolean;
32
+ /**
33
+ * Haptic feedback on each key. On by default, and a no-op on web and on any
34
+ * device without a haptic engine. Pass `false` where a press already triggers
35
+ * feedback of its own.
36
+ */
37
+ haptics?: boolean;
17
38
  }
18
- export declare function PinPad({ length, onComplete, onChange, onBiometric, resetKey, error, }: PinPadProps): React.JSX.Element;
39
+ export declare function PinPad({ length, onComplete, onChange, onBiometric, resetKey, error, haptics, }: PinPadProps): React.JSX.Element;
19
40
  export default PinPad;
20
41
  //# sourceMappingURL=pin-pad.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pin-pad.d.ts","sourceRoot":"","sources":["../../src/components/pin-pad.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAKxE;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAID,wBAAgB,MAAM,CAAC,EACrB,MAAU,EACV,UAAU,EACV,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,KAAK,GACN,EAAE,WAAW,qBAqGb;AAyBD,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"pin-pad.d.ts","sourceRoot":"","sources":["../../src/components/pin-pad.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAOxE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAWD,wBAAgB,MAAM,CAAC,EACrB,MAAU,EACV,UAAU,EACV,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,KAAK,EACL,OAAc,GACf,EAAE,WAAW,qBAoJb;AAmDD,eAAe,MAAM,CAAC"}
@@ -1,11 +1,28 @@
1
1
  import React, { useCallback, useEffect, useRef, useState } from "react";
2
- import { Platform, Pressable, Text, View } from "react-native";
2
+ import { Platform, Pressable, Text, useWindowDimensions, View } from "react-native";
3
+ import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
4
+ import * as Haptics from "expo-haptics";
3
5
  import { useColorScheme } from "nativewind";
4
6
  import { Delete, Fingerprint } from "lucide-react-native";
5
7
  const KEYS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
6
- export function PinPad({ length = 6, onComplete, onChange, onBiometric, resetKey, error, }) {
8
+ /** Key diameter bounds, and the gap between them. */
9
+ const MIN_KEY = 76;
10
+ const MAX_KEY = 104;
11
+ const GAP = 16;
12
+ /** Room left for the screen's own horizontal padding. */
13
+ const SIDE_PADDING = 48;
14
+ export function PinPad({ length = 6, onComplete, onChange, onBiometric, resetKey, error, haptics = true, }) {
7
15
  const { colorScheme } = useColorScheme();
8
- const iconColor = colorScheme === "dark" ? "#FFFFFF" : "#000000";
16
+ const isDark = colorScheme === "dark";
17
+ const iconColor = isDark ? "#FFFFFF" : "#000000";
18
+ const { width } = useWindowDimensions();
19
+ // Three keys and two gaps have to fit inside the viewport less the screen's
20
+ // padding. Clamped at both ends: MIN_KEY keeps the pad usable on a small
21
+ // phone even if that means it is the widest thing on screen, MAX_KEY stops a
22
+ // tablet from rendering three dinner plates.
23
+ const keySize = Math.max(MIN_KEY, Math.min(MAX_KEY, Math.floor((width - SIDE_PADDING - 2 * GAP) / 3)));
24
+ const padWidth = keySize * 3 + GAP * 2;
25
+ const dotSize = Math.round(keySize * 0.17);
9
26
  const [pin, setPin] = useState("");
10
27
  // Source of truth for the latest digits — lets callbacks fire OUTSIDE the
11
28
  // setPin updater (calling a parent setState inside an updater triggers
@@ -21,11 +38,28 @@ export function PinPad({ length = 6, onComplete, onChange, onBiometric, resetKey
21
38
  setPin("");
22
39
  }
23
40
  }
41
+ /**
42
+ * A tap you can feel.
43
+ *
44
+ * Deliberately fire-and-forget: the feedback is worth nothing if the digit
45
+ * waits for it, and a device with no haptic engine rejects the promise rather
46
+ * than throwing, which must not surface as an unhandled rejection over a
47
+ * keypress.
48
+ */
49
+ const tap = useCallback(() => {
50
+ if (!haptics || Platform.OS === "web") {
51
+ return;
52
+ }
53
+ void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {
54
+ // No haptic engine, or the OS declined. Nothing to say about it.
55
+ });
56
+ }, [haptics]);
24
57
  const press = useCallback((d) => {
25
58
  const cur = pinRef.current;
26
59
  if (cur.length >= length) {
27
60
  return;
28
61
  }
62
+ tap();
29
63
  const next = cur + d;
30
64
  pinRef.current = next;
31
65
  setPin(next);
@@ -33,13 +67,17 @@ export function PinPad({ length = 6, onComplete, onChange, onBiometric, resetKey
33
67
  if (next.length === length) {
34
68
  onComplete(next);
35
69
  }
36
- }, [length, onChange, onComplete]);
70
+ }, [length, onChange, onComplete, tap]);
37
71
  const back = useCallback(() => {
72
+ if (pinRef.current.length === 0) {
73
+ return;
74
+ }
75
+ tap();
38
76
  const next = pinRef.current.slice(0, -1);
39
77
  pinRef.current = next;
40
78
  setPin(next);
41
79
  onChange?.(next);
42
- }, [onChange]);
80
+ }, [onChange, tap]);
43
81
  // Physical keyboard / numpad support (web): digits type, Backspace/Delete
44
82
  // removes, Enter submits a full PIN.
45
83
  useEffect(() => {
@@ -65,29 +103,44 @@ export function PinPad({ length = 6, onComplete, onChange, onBiometric, resetKey
65
103
  window.addEventListener("keydown", onKey);
66
104
  return () => window.removeEventListener("keydown", onKey);
67
105
  }, [press, back, onComplete, length]);
68
- return (<View className="items-center gap-7">
106
+ return (<View className="items-center gap-8">
69
107
  {/* dots */}
70
- <View className="flex-row gap-3">
71
- {Array.from({ length }).map((_, i) => (<View key={i} className={i < pin.length
108
+ <View className="flex-row" style={{ gap: Math.round(dotSize * 0.9) }}>
109
+ {Array.from({ length }).map((_, i) => (<View key={i} style={{ width: dotSize, height: dotSize, borderRadius: dotSize / 2 }} className={i < pin.length
72
110
  ? error
73
- ? "h-3.5 w-3.5 rounded-full bg-destructive dark:bg-red-900"
74
- : "h-3.5 w-3.5 rounded-full bg-primary dark:bg-white"
75
- : "h-3.5 w-3.5 rounded-full border-2 border-border dark:border-neutral-800"}/>))}
111
+ ? "bg-destructive dark:bg-red-900"
112
+ : "bg-primary dark:bg-white"
113
+ : "border-2 border-border dark:border-neutral-700"}/>))}
76
114
  </View>
77
115
 
78
116
  {/* keypad */}
79
- <View className="w-72 flex-row flex-wrap justify-center gap-3">
80
- {KEYS.map((k) => (<Key key={k} label={k} onPress={() => press(k)}/>))}
81
- {onBiometric ? (<Key onPress={onBiometric} icon={<Fingerprint size={26} color={iconColor}/>}/>) : (<View className="h-20 w-20"/>)}
82
- <Key label="0" onPress={() => press("0")}/>
83
- <Key onPress={back} icon={<Delete size={24} color={iconColor}/>}/>
117
+ <View className="flex-row flex-wrap justify-center" style={{ width: padWidth, gap: GAP }}>
118
+ {KEYS.map((k) => (<Key key={k} label={k} size={keySize} onPress={() => press(k)}/>))}
119
+ {onBiometric ? (<Key size={keySize} onPress={onBiometric} icon={<Fingerprint size={Math.round(keySize * 0.34)} color={iconColor}/>} accessibilityLabel="Unlock with biometrics"/>) : (<View style={{ width: keySize, height: keySize }}/>)}
120
+ <Key label="0" size={keySize} onPress={() => press("0")}/>
121
+ <Key size={keySize} onPress={back} icon={<Delete size={Math.round(keySize * 0.3)} color={iconColor}/>} accessibilityLabel="Delete"/>
84
122
  </View>
85
123
  </View>);
86
124
  }
87
- function Key({ label, icon, onPress, }) {
88
- return (<Pressable onPress={onPress} className="h-20 w-20 items-center justify-center rounded-full active:bg-secondary dark:active:bg-neutral-800" accessibilityRole="button" accessibilityLabel={label ?? "key"}>
89
- {icon ?? (<Text className="font-sans-medium text-2xl text-foreground dark:text-white">{label}</Text>)}
90
- </Pressable>);
125
+ function Key({ label, icon, size, onPress, accessibilityLabel, }) {
126
+ // Same two-part response as Button: a dip in scale and a lift in fill. The
127
+ // fill change alone was too easy to miss on a dark screen, and on a keypad
128
+ // "did that register?" is the entire question the user is asking.
129
+ const pressed = useSharedValue(0);
130
+ const pressStyle = useAnimatedStyle(() => ({
131
+ transform: [{ scale: 1 - pressed.value * 0.08 }],
132
+ }));
133
+ return (<Animated.View style={pressStyle}>
134
+ <Pressable onPress={onPress} onPressIn={() => {
135
+ pressed.value = withTiming(1, { duration: 60 });
136
+ }} onPressOut={() => {
137
+ pressed.value = withTiming(0, { duration: 160 });
138
+ }} style={{ width: size, height: size, borderRadius: size / 2 }} className="items-center justify-center border border-border bg-secondary active:bg-border dark:border-neutral-700 dark:bg-neutral-900 dark:active:bg-neutral-700" accessibilityRole="button" accessibilityLabel={accessibilityLabel ?? label ?? "key"}>
139
+ {icon ?? (<Text className="font-sans-medium text-foreground dark:text-white" style={{ fontSize: Math.round(size * 0.38) }}>
140
+ {label}
141
+ </Text>)}
142
+ </Pressable>
143
+ </Animated.View>);
91
144
  }
92
145
  export default PinPad;
93
146
  //# sourceMappingURL=pin-pad.jsx.map
@@ -1 +1 @@
1
- {"version":3,"file":"pin-pad.jsx","sourceRoot":"","sources":["../../src/components/pin-pad.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pin-pad.jsx","sourceRoot":"","sources":["../../src/components/pin-pad.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entropy-softworks/ui",
3
- "version": "2026.8.42",
3
+ "version": "2026.9.1",
4
4
  "private": false,
5
5
  "description": "Shared React Native UI primitives, auth screens, hooks, and services for Entropy Softworks Expo apps",
6
6
  "license": "MIT",
@@ -1,5 +1,7 @@
1
1
  import React, { useCallback, useEffect, useRef, useState } from "react";
2
- import { Platform, Pressable, Text, View } from "react-native";
2
+ import { Platform, Pressable, Text, useWindowDimensions, View } from "react-native";
3
+ import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
4
+ import * as Haptics from "expo-haptics";
3
5
  import { useColorScheme } from "nativewind";
4
6
  import { Delete, Fingerprint } from "lucide-react-native";
5
7
 
@@ -8,6 +10,21 @@ import { Delete, Fingerprint } from "lucide-react-native";
8
10
  * the active palette via NativeWind classes). Self-manages the entered
9
11
  * digits and fires `onComplete` when `length` is reached; pass a changing
10
12
  * `resetKey` to clear it (e.g. after a wrong PIN).
13
+ *
14
+ * # Sizing
15
+ *
16
+ * The pad measures itself against the viewport instead of sitting at a fixed
17
+ * 288px. On a modern phone a fixed pad reads as a small control marooned in the
18
+ * middle of a large screen — and this is the primary way into the app for a
19
+ * device-only vault, so it should feel like the screen's purpose rather than a
20
+ * widget on it. Keys grow with the available width and stop at `MAX_KEY` so a
21
+ * tablet does not get comedy-sized buttons.
22
+ *
23
+ * # Why the keys have a fill
24
+ *
25
+ * They previously had no background at all until the moment of the press, which
26
+ * left three columns of bare digits that did not read as pressable. A resting
27
+ * fill plus a border is what makes a target look like a target.
11
28
  */
12
29
  export interface PinPadProps {
13
30
  length?: number;
@@ -18,10 +35,23 @@ export interface PinPadProps {
18
35
  /** Bump to clear the entered digits. */
19
36
  resetKey?: string | number;
20
37
  error?: boolean;
38
+ /**
39
+ * Haptic feedback on each key. On by default, and a no-op on web and on any
40
+ * device without a haptic engine. Pass `false` where a press already triggers
41
+ * feedback of its own.
42
+ */
43
+ haptics?: boolean;
21
44
  }
22
45
 
23
46
  const KEYS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] as const;
24
47
 
48
+ /** Key diameter bounds, and the gap between them. */
49
+ const MIN_KEY = 76;
50
+ const MAX_KEY = 104;
51
+ const GAP = 16;
52
+ /** Room left for the screen's own horizontal padding. */
53
+ const SIDE_PADDING = 48;
54
+
25
55
  export function PinPad({
26
56
  length = 6,
27
57
  onComplete,
@@ -29,9 +59,24 @@ export function PinPad({
29
59
  onBiometric,
30
60
  resetKey,
31
61
  error,
62
+ haptics = true,
32
63
  }: PinPadProps) {
33
64
  const { colorScheme } = useColorScheme();
34
- const iconColor = colorScheme === "dark" ? "#FFFFFF" : "#000000";
65
+ const isDark = colorScheme === "dark";
66
+ const iconColor = isDark ? "#FFFFFF" : "#000000";
67
+ const { width } = useWindowDimensions();
68
+
69
+ // Three keys and two gaps have to fit inside the viewport less the screen's
70
+ // padding. Clamped at both ends: MIN_KEY keeps the pad usable on a small
71
+ // phone even if that means it is the widest thing on screen, MAX_KEY stops a
72
+ // tablet from rendering three dinner plates.
73
+ const keySize = Math.max(
74
+ MIN_KEY,
75
+ Math.min(MAX_KEY, Math.floor((width - SIDE_PADDING - 2 * GAP) / 3))
76
+ );
77
+ const padWidth = keySize * 3 + GAP * 2;
78
+ const dotSize = Math.round(keySize * 0.17);
79
+
35
80
  const [pin, setPin] = useState("");
36
81
  // Source of truth for the latest digits — lets callbacks fire OUTSIDE the
37
82
  // setPin updater (calling a parent setState inside an updater triggers
@@ -49,12 +94,30 @@ export function PinPad({
49
94
  }
50
95
  }
51
96
 
97
+ /**
98
+ * A tap you can feel.
99
+ *
100
+ * Deliberately fire-and-forget: the feedback is worth nothing if the digit
101
+ * waits for it, and a device with no haptic engine rejects the promise rather
102
+ * than throwing, which must not surface as an unhandled rejection over a
103
+ * keypress.
104
+ */
105
+ const tap = useCallback(() => {
106
+ if (!haptics || Platform.OS === "web") {
107
+ return;
108
+ }
109
+ void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {
110
+ // No haptic engine, or the OS declined. Nothing to say about it.
111
+ });
112
+ }, [haptics]);
113
+
52
114
  const press = useCallback(
53
115
  (d: string) => {
54
116
  const cur = pinRef.current;
55
117
  if (cur.length >= length) {
56
118
  return;
57
119
  }
120
+ tap();
58
121
  const next = cur + d;
59
122
  pinRef.current = next;
60
123
  setPin(next);
@@ -63,15 +126,19 @@ export function PinPad({
63
126
  onComplete(next);
64
127
  }
65
128
  },
66
- [length, onChange, onComplete]
129
+ [length, onChange, onComplete, tap]
67
130
  );
68
131
 
69
132
  const back = useCallback(() => {
133
+ if (pinRef.current.length === 0) {
134
+ return;
135
+ }
136
+ tap();
70
137
  const next = pinRef.current.slice(0, -1);
71
138
  pinRef.current = next;
72
139
  setPin(next);
73
140
  onChange?.(next);
74
- }, [onChange]);
141
+ }, [onChange, tap]);
75
142
 
76
143
  // Physical keyboard / numpad support (web): digits type, Backspace/Delete
77
144
  // removes, Enter submits a full PIN.
@@ -98,35 +165,46 @@ export function PinPad({
98
165
  }, [press, back, onComplete, length]);
99
166
 
100
167
  return (
101
- <View className="items-center gap-7">
168
+ <View className="items-center gap-8">
102
169
  {/* dots */}
103
- <View className="flex-row gap-3">
170
+ <View className="flex-row" style={{ gap: Math.round(dotSize * 0.9) }}>
104
171
  {Array.from({ length }).map((_, i) => (
105
172
  <View
106
173
  key={i}
174
+ style={{ width: dotSize, height: dotSize, borderRadius: dotSize / 2 }}
107
175
  className={
108
176
  i < pin.length
109
177
  ? error
110
- ? "h-3.5 w-3.5 rounded-full bg-destructive dark:bg-red-900"
111
- : "h-3.5 w-3.5 rounded-full bg-primary dark:bg-white"
112
- : "h-3.5 w-3.5 rounded-full border-2 border-border dark:border-neutral-800"
178
+ ? "bg-destructive dark:bg-red-900"
179
+ : "bg-primary dark:bg-white"
180
+ : "border-2 border-border dark:border-neutral-700"
113
181
  }
114
182
  />
115
183
  ))}
116
184
  </View>
117
185
 
118
186
  {/* keypad */}
119
- <View className="w-72 flex-row flex-wrap justify-center gap-3">
187
+ <View className="flex-row flex-wrap justify-center" style={{ width: padWidth, gap: GAP }}>
120
188
  {KEYS.map((k) => (
121
- <Key key={k} label={k} onPress={() => press(k)} />
189
+ <Key key={k} label={k} size={keySize} onPress={() => press(k)} />
122
190
  ))}
123
191
  {onBiometric ? (
124
- <Key onPress={onBiometric} icon={<Fingerprint size={26} color={iconColor} />} />
192
+ <Key
193
+ size={keySize}
194
+ onPress={onBiometric}
195
+ icon={<Fingerprint size={Math.round(keySize * 0.34)} color={iconColor} />}
196
+ accessibilityLabel="Unlock with biometrics"
197
+ />
125
198
  ) : (
126
- <View className="h-20 w-20" />
199
+ <View style={{ width: keySize, height: keySize }} />
127
200
  )}
128
- <Key label="0" onPress={() => press("0")} />
129
- <Key onPress={back} icon={<Delete size={24} color={iconColor} />} />
201
+ <Key label="0" size={keySize} onPress={() => press("0")} />
202
+ <Key
203
+ size={keySize}
204
+ onPress={back}
205
+ icon={<Delete size={Math.round(keySize * 0.3)} color={iconColor} />}
206
+ accessibilityLabel="Delete"
207
+ />
130
208
  </View>
131
209
  </View>
132
210
  );
@@ -135,23 +213,49 @@ export function PinPad({
135
213
  function Key({
136
214
  label,
137
215
  icon,
216
+ size,
138
217
  onPress,
218
+ accessibilityLabel,
139
219
  }: {
140
220
  label?: string;
141
221
  icon?: React.ReactNode;
222
+ size: number;
142
223
  onPress: () => void;
224
+ accessibilityLabel?: string;
143
225
  }) {
226
+ // Same two-part response as Button: a dip in scale and a lift in fill. The
227
+ // fill change alone was too easy to miss on a dark screen, and on a keypad
228
+ // "did that register?" is the entire question the user is asking.
229
+ const pressed = useSharedValue(0);
230
+ const pressStyle = useAnimatedStyle(() => ({
231
+ transform: [{ scale: 1 - pressed.value * 0.08 }],
232
+ }));
233
+
144
234
  return (
145
- <Pressable
146
- onPress={onPress}
147
- className="h-20 w-20 items-center justify-center rounded-full active:bg-secondary dark:active:bg-neutral-800"
148
- accessibilityRole="button"
149
- accessibilityLabel={label ?? "key"}
150
- >
151
- {icon ?? (
152
- <Text className="font-sans-medium text-2xl text-foreground dark:text-white">{label}</Text>
153
- )}
154
- </Pressable>
235
+ <Animated.View style={pressStyle}>
236
+ <Pressable
237
+ onPress={onPress}
238
+ onPressIn={() => {
239
+ pressed.value = withTiming(1, { duration: 60 });
240
+ }}
241
+ onPressOut={() => {
242
+ pressed.value = withTiming(0, { duration: 160 });
243
+ }}
244
+ style={{ width: size, height: size, borderRadius: size / 2 }}
245
+ className="items-center justify-center border border-border bg-secondary active:bg-border dark:border-neutral-700 dark:bg-neutral-900 dark:active:bg-neutral-700"
246
+ accessibilityRole="button"
247
+ accessibilityLabel={accessibilityLabel ?? label ?? "key"}
248
+ >
249
+ {icon ?? (
250
+ <Text
251
+ className="font-sans-medium text-foreground dark:text-white"
252
+ style={{ fontSize: Math.round(size * 0.38) }}
253
+ >
254
+ {label}
255
+ </Text>
256
+ )}
257
+ </Pressable>
258
+ </Animated.View>
155
259
  );
156
260
  }
157
261