@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 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.
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
- * # Why the keys have a fill
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
- * 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.
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;;;;;;;;;;;;;;;;;;;;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
+ {"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, Text, useWindowDimensions, View } from "react-native";
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
- /** Room left for the screen's own horizontal padding. */
13
- const SIDE_PADDING = 48;
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 { 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)));
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
- // 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.
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 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) }}>
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":"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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.9.1",
3
+ "version": "2026.9.3",
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,5 @@
1
1
  import React, { useCallback, useEffect, useRef, useState } from "react";
2
- import { Platform, Pressable, Text, useWindowDimensions, View } from "react-native";
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 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.
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
- * # Why the keys have a fill
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
- * 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.
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
- /** Room left for the screen's own horizontal padding. */
53
- const SIDE_PADDING = 48;
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 { width } = useWindowDimensions();
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 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
- );
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
- // 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.
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 bg-secondary active:bg-border dark:border-neutral-700 dark:bg-neutral-900 dark:active:bg-neutral-700"
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-sans-medium text-foreground dark:text-white"
252
- style={{ fontSize: Math.round(size * 0.38) }}
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>