@entropy-softworks/ui 2026.9.1 → 2026.9.3
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.
|
@@ -7,18 +7,29 @@ import React from "react";
|
|
|
7
7
|
*
|
|
8
8
|
* # Sizing
|
|
9
9
|
*
|
|
10
|
-
* The pad
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* The pad fills the width it is given instead of sitting at a fixed 288px. On a
|
|
11
|
+
* modern phone a fixed pad reads as a small control marooned in the middle of a
|
|
12
|
+
* large screen — and this is the primary way into the app for a device-only
|
|
13
|
+
* vault, so it should feel like the screen's purpose rather than a widget on it.
|
|
14
|
+
* Keys grow with the available width and stop at `MAX_KEY` so a tablet does not
|
|
15
|
+
* get comedy-sized buttons.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* The width comes from the pad's own `onLayout`, not from the viewport. It is
|
|
18
|
+
* usually inside a card inside a padded screen, and every one of those layers
|
|
19
|
+
* takes width the window knows nothing about — sizing off the window overflowed
|
|
20
|
+
* the card it sat in.
|
|
18
21
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
+
* # How the keys read
|
|
23
|
+
*
|
|
24
|
+
* A hairline ring at rest and nothing else: no fill, mono digits, the same
|
|
25
|
+
* quiet treatment the rest of the product uses. The ring is what makes a target
|
|
26
|
+
* a target — an earlier pass filled the keys as well, and that was too heavy
|
|
27
|
+
* beside everything around it.
|
|
28
|
+
*
|
|
29
|
+
* The press is where the weight goes instead: the ring brightens, a fill fades
|
|
30
|
+
* in under it, and the key dips in scale. Three layered responses because the
|
|
31
|
+
* resting state is deliberately understated, and "did that register?" is the
|
|
32
|
+
* entire question somebody entering a PIN is asking.
|
|
22
33
|
*/
|
|
23
34
|
export interface PinPadProps {
|
|
24
35
|
length?: number;
|
|
@@ -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;AAOxE
|
|
1
|
+
{"version":3,"file":"pin-pad.d.ts","sourceRoot":"","sources":["../../src/components/pin-pad.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAOxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;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,qBAwJb;AA4DD,eAAe,MAAM,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
import { Platform, Pressable,
|
|
2
|
+
import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
|
|
3
3
|
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
|
|
4
4
|
import * as Haptics from "expo-haptics";
|
|
5
5
|
import { useColorScheme } from "nativewind";
|
|
@@ -9,18 +9,25 @@ const KEYS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
|
|
9
9
|
const MIN_KEY = 76;
|
|
10
10
|
const MAX_KEY = 104;
|
|
11
11
|
const GAP = 16;
|
|
12
|
-
/**
|
|
13
|
-
const
|
|
12
|
+
/** Assumed width for the single frame before the first measurement lands. */
|
|
13
|
+
const ASSUMED_WIDTH = 320;
|
|
14
14
|
export function PinPad({ length = 6, onComplete, onChange, onBiometric, resetKey, error, haptics = true, }) {
|
|
15
15
|
const { colorScheme } = useColorScheme();
|
|
16
16
|
const isDark = colorScheme === "dark";
|
|
17
17
|
const iconColor = isDark ? "#FFFFFF" : "#000000";
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
const [available, setAvailable] = useState(0);
|
|
19
|
+
const onMeasure = (event) => {
|
|
20
|
+
const measured = event.nativeEvent.layout.width;
|
|
21
|
+
if (measured > 0 && Math.abs(measured - available) > 1) {
|
|
22
|
+
setAvailable(measured);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
// Three keys and two gaps have to fit in the width this pad was actually
|
|
26
|
+
// given. Clamped at both ends: MIN_KEY keeps the pad usable in a narrow
|
|
27
|
+
// column even if that means it overflows a little, MAX_KEY stops a tablet
|
|
28
|
+
// from rendering three dinner plates.
|
|
29
|
+
const width = available || ASSUMED_WIDTH;
|
|
30
|
+
const keySize = Math.max(MIN_KEY, Math.min(MAX_KEY, Math.floor((width - 2 * GAP) / 3)));
|
|
24
31
|
const padWidth = keySize * 3 + GAP * 2;
|
|
25
32
|
const dotSize = Math.round(keySize * 0.17);
|
|
26
33
|
const [pin, setPin] = useState("");
|
|
@@ -103,7 +110,7 @@ export function PinPad({ length = 6, onComplete, onChange, onBiometric, resetKey
|
|
|
103
110
|
window.addEventListener("keydown", onKey);
|
|
104
111
|
return () => window.removeEventListener("keydown", onKey);
|
|
105
112
|
}, [press, back, onComplete, length]);
|
|
106
|
-
return (<View className="items-center gap-8">
|
|
113
|
+
return (<View className="w-full items-center gap-8" onLayout={onMeasure}>
|
|
107
114
|
{/* dots */}
|
|
108
115
|
<View className="flex-row" style={{ gap: Math.round(dotSize * 0.9) }}>
|
|
109
116
|
{Array.from({ length }).map((_, i) => (<View key={i} style={{ width: dotSize, height: dotSize, borderRadius: dotSize / 2 }} className={i < pin.length
|
|
@@ -123,20 +130,25 @@ export function PinPad({ length = 6, onComplete, onChange, onBiometric, resetKey
|
|
|
123
130
|
</View>);
|
|
124
131
|
}
|
|
125
132
|
function Key({ label, icon, size, onPress, accessibilityLabel, }) {
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
133
|
+
// The resting key is a ring and a digit, so the press has to carry all the
|
|
134
|
+
// feedback: scale dips, and the fill fades up from nothing underneath. Driven
|
|
135
|
+
// from one shared value so the two cannot fall out of step.
|
|
129
136
|
const pressed = useSharedValue(0);
|
|
130
137
|
const pressStyle = useAnimatedStyle(() => ({
|
|
131
138
|
transform: [{ scale: 1 - pressed.value * 0.08 }],
|
|
132
139
|
}));
|
|
140
|
+
const fillStyle = useAnimatedStyle(() => ({ opacity: pressed.value }));
|
|
133
141
|
return (<Animated.View style={pressStyle}>
|
|
134
142
|
<Pressable onPress={onPress} onPressIn={() => {
|
|
135
143
|
pressed.value = withTiming(1, { duration: 60 });
|
|
136
144
|
}} onPressOut={() => {
|
|
137
145
|
pressed.value = withTiming(0, { duration: 160 });
|
|
138
|
-
}} style={{ width: size, height: size, borderRadius: size / 2 }} className="items-center justify-center border border-border
|
|
139
|
-
{
|
|
146
|
+
}} style={{ width: size, height: size, borderRadius: size / 2 }} className="items-center justify-center overflow-hidden border border-border dark:border-neutral-700" accessibilityRole="button" accessibilityLabel={accessibilityLabel ?? label ?? "key"}>
|
|
147
|
+
{/* The pressed fill, behind the digit. A sibling with animated opacity
|
|
148
|
+
rather than an animated backgroundColor: colour has to come from the
|
|
149
|
+
theme's classes, and those cannot be interpolated. */}
|
|
150
|
+
<Animated.View pointerEvents="none" style={[fillStyle, StyleSheet.absoluteFillObject]} className="bg-secondary dark:bg-neutral-800"/>
|
|
151
|
+
{icon ?? (<Text className="font-mono text-foreground dark:text-white" style={{ fontSize: Math.round(size * 0.36) }}>
|
|
140
152
|
{label}
|
|
141
153
|
</Text>)}
|
|
142
154
|
</Pressable>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pin-pad.jsx","sourceRoot":"","sources":["../../src/components/pin-pad.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACxE,OAAO,
|
|
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,5 +1,5 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
import { Platform, Pressable,
|
|
2
|
+
import { type LayoutChangeEvent, Platform, Pressable, StyleSheet, Text, View } from "react-native";
|
|
3
3
|
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
|
|
4
4
|
import * as Haptics from "expo-haptics";
|
|
5
5
|
import { useColorScheme } from "nativewind";
|
|
@@ -13,18 +13,29 @@ import { Delete, Fingerprint } from "lucide-react-native";
|
|
|
13
13
|
*
|
|
14
14
|
* # Sizing
|
|
15
15
|
*
|
|
16
|
-
* The pad
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* The pad fills the width it is given instead of sitting at a fixed 288px. On a
|
|
17
|
+
* modern phone a fixed pad reads as a small control marooned in the middle of a
|
|
18
|
+
* large screen — and this is the primary way into the app for a device-only
|
|
19
|
+
* vault, so it should feel like the screen's purpose rather than a widget on it.
|
|
20
|
+
* Keys grow with the available width and stop at `MAX_KEY` so a tablet does not
|
|
21
|
+
* get comedy-sized buttons.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
23
|
+
* The width comes from the pad's own `onLayout`, not from the viewport. It is
|
|
24
|
+
* usually inside a card inside a padded screen, and every one of those layers
|
|
25
|
+
* takes width the window knows nothing about — sizing off the window overflowed
|
|
26
|
+
* the card it sat in.
|
|
24
27
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
+
* # How the keys read
|
|
29
|
+
*
|
|
30
|
+
* A hairline ring at rest and nothing else: no fill, mono digits, the same
|
|
31
|
+
* quiet treatment the rest of the product uses. The ring is what makes a target
|
|
32
|
+
* a target — an earlier pass filled the keys as well, and that was too heavy
|
|
33
|
+
* beside everything around it.
|
|
34
|
+
*
|
|
35
|
+
* The press is where the weight goes instead: the ring brightens, a fill fades
|
|
36
|
+
* in under it, and the key dips in scale. Three layered responses because the
|
|
37
|
+
* resting state is deliberately understated, and "did that register?" is the
|
|
38
|
+
* entire question somebody entering a PIN is asking.
|
|
28
39
|
*/
|
|
29
40
|
export interface PinPadProps {
|
|
30
41
|
length?: number;
|
|
@@ -49,8 +60,8 @@ const KEYS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] as const;
|
|
|
49
60
|
const MIN_KEY = 76;
|
|
50
61
|
const MAX_KEY = 104;
|
|
51
62
|
const GAP = 16;
|
|
52
|
-
/**
|
|
53
|
-
const
|
|
63
|
+
/** Assumed width for the single frame before the first measurement lands. */
|
|
64
|
+
const ASSUMED_WIDTH = 320;
|
|
54
65
|
|
|
55
66
|
export function PinPad({
|
|
56
67
|
length = 6,
|
|
@@ -64,16 +75,20 @@ export function PinPad({
|
|
|
64
75
|
const { colorScheme } = useColorScheme();
|
|
65
76
|
const isDark = colorScheme === "dark";
|
|
66
77
|
const iconColor = isDark ? "#FFFFFF" : "#000000";
|
|
67
|
-
const
|
|
78
|
+
const [available, setAvailable] = useState(0);
|
|
79
|
+
const onMeasure = (event: LayoutChangeEvent) => {
|
|
80
|
+
const measured = event.nativeEvent.layout.width;
|
|
81
|
+
if (measured > 0 && Math.abs(measured - available) > 1) {
|
|
82
|
+
setAvailable(measured);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
68
85
|
|
|
69
|
-
// Three keys and two gaps have to fit
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
Math.min(MAX_KEY, Math.floor((width - SIDE_PADDING - 2 * GAP) / 3))
|
|
76
|
-
);
|
|
86
|
+
// Three keys and two gaps have to fit in the width this pad was actually
|
|
87
|
+
// given. Clamped at both ends: MIN_KEY keeps the pad usable in a narrow
|
|
88
|
+
// column even if that means it overflows a little, MAX_KEY stops a tablet
|
|
89
|
+
// from rendering three dinner plates.
|
|
90
|
+
const width = available || ASSUMED_WIDTH;
|
|
91
|
+
const keySize = Math.max(MIN_KEY, Math.min(MAX_KEY, Math.floor((width - 2 * GAP) / 3)));
|
|
77
92
|
const padWidth = keySize * 3 + GAP * 2;
|
|
78
93
|
const dotSize = Math.round(keySize * 0.17);
|
|
79
94
|
|
|
@@ -165,7 +180,7 @@ export function PinPad({
|
|
|
165
180
|
}, [press, back, onComplete, length]);
|
|
166
181
|
|
|
167
182
|
return (
|
|
168
|
-
<View className="items-center gap-8">
|
|
183
|
+
<View className="w-full items-center gap-8" onLayout={onMeasure}>
|
|
169
184
|
{/* dots */}
|
|
170
185
|
<View className="flex-row" style={{ gap: Math.round(dotSize * 0.9) }}>
|
|
171
186
|
{Array.from({ length }).map((_, i) => (
|
|
@@ -223,13 +238,14 @@ function Key({
|
|
|
223
238
|
onPress: () => void;
|
|
224
239
|
accessibilityLabel?: string;
|
|
225
240
|
}) {
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
241
|
+
// The resting key is a ring and a digit, so the press has to carry all the
|
|
242
|
+
// feedback: scale dips, and the fill fades up from nothing underneath. Driven
|
|
243
|
+
// from one shared value so the two cannot fall out of step.
|
|
229
244
|
const pressed = useSharedValue(0);
|
|
230
245
|
const pressStyle = useAnimatedStyle(() => ({
|
|
231
246
|
transform: [{ scale: 1 - pressed.value * 0.08 }],
|
|
232
247
|
}));
|
|
248
|
+
const fillStyle = useAnimatedStyle(() => ({ opacity: pressed.value }));
|
|
233
249
|
|
|
234
250
|
return (
|
|
235
251
|
<Animated.View style={pressStyle}>
|
|
@@ -242,14 +258,22 @@ function Key({
|
|
|
242
258
|
pressed.value = withTiming(0, { duration: 160 });
|
|
243
259
|
}}
|
|
244
260
|
style={{ width: size, height: size, borderRadius: size / 2 }}
|
|
245
|
-
className="items-center justify-center border border-border
|
|
261
|
+
className="items-center justify-center overflow-hidden border border-border dark:border-neutral-700"
|
|
246
262
|
accessibilityRole="button"
|
|
247
263
|
accessibilityLabel={accessibilityLabel ?? label ?? "key"}
|
|
248
264
|
>
|
|
265
|
+
{/* The pressed fill, behind the digit. A sibling with animated opacity
|
|
266
|
+
rather than an animated backgroundColor: colour has to come from the
|
|
267
|
+
theme's classes, and those cannot be interpolated. */}
|
|
268
|
+
<Animated.View
|
|
269
|
+
pointerEvents="none"
|
|
270
|
+
style={[fillStyle, StyleSheet.absoluteFillObject]}
|
|
271
|
+
className="bg-secondary dark:bg-neutral-800"
|
|
272
|
+
/>
|
|
249
273
|
{icon ?? (
|
|
250
274
|
<Text
|
|
251
|
-
className="font-
|
|
252
|
-
style={{ fontSize: Math.round(size * 0.
|
|
275
|
+
className="font-mono text-foreground dark:text-white"
|
|
276
|
+
style={{ fontSize: Math.round(size * 0.36) }}
|
|
253
277
|
>
|
|
254
278
|
{label}
|
|
255
279
|
</Text>
|