@bacon-pos/ui 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/lib/commonjs/components/CodeField/CodeField.js +95 -65
  2. package/lib/commonjs/components/CodeField/CodeField.js.map +1 -1
  3. package/lib/commonjs/components/CodeField/CodeField.stories.js +24 -1
  4. package/lib/commonjs/components/CodeField/CodeField.stories.js.map +1 -1
  5. package/lib/commonjs/components/CodeField/CodeField.test.js +28 -0
  6. package/lib/commonjs/components/CodeField/CodeField.test.js.map +1 -1
  7. package/lib/commonjs/components/Keypad/Keypad.js +162 -0
  8. package/lib/commonjs/components/Keypad/Keypad.js.map +1 -0
  9. package/lib/commonjs/components/Keypad/Keypad.stories.js +210 -0
  10. package/lib/commonjs/components/Keypad/Keypad.stories.js.map +1 -0
  11. package/lib/commonjs/components/Keypad/Keypad.test.js +119 -0
  12. package/lib/commonjs/components/Keypad/Keypad.test.js.map +1 -0
  13. package/lib/commonjs/components/Keypad/index.js +13 -0
  14. package/lib/commonjs/components/Keypad/index.js.map +1 -0
  15. package/lib/commonjs/components/index.js +20 -9
  16. package/lib/commonjs/components/index.js.map +1 -1
  17. package/lib/module/components/CodeField/CodeField.js +95 -65
  18. package/lib/module/components/CodeField/CodeField.js.map +1 -1
  19. package/lib/module/components/CodeField/CodeField.stories.js +23 -0
  20. package/lib/module/components/CodeField/CodeField.stories.js.map +1 -1
  21. package/lib/module/components/CodeField/CodeField.test.js +28 -0
  22. package/lib/module/components/CodeField/CodeField.test.js.map +1 -1
  23. package/lib/module/components/Keypad/Keypad.js +158 -0
  24. package/lib/module/components/Keypad/Keypad.js.map +1 -0
  25. package/lib/module/components/Keypad/Keypad.stories.js +206 -0
  26. package/lib/module/components/Keypad/Keypad.stories.js.map +1 -0
  27. package/lib/module/components/Keypad/Keypad.test.js +119 -0
  28. package/lib/module/components/Keypad/Keypad.test.js.map +1 -0
  29. package/lib/module/components/Keypad/index.js +4 -0
  30. package/lib/module/components/Keypad/index.js.map +1 -0
  31. package/lib/module/components/index.js +1 -0
  32. package/lib/module/components/index.js.map +1 -1
  33. package/lib/typescript/commonjs/components/CodeField/CodeField.d.ts +5 -1
  34. package/lib/typescript/commonjs/components/CodeField/CodeField.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/components/Keypad/Keypad.d.ts +54 -0
  36. package/lib/typescript/commonjs/components/Keypad/Keypad.d.ts.map +1 -0
  37. package/lib/typescript/commonjs/components/Keypad/index.d.ts +3 -0
  38. package/lib/typescript/commonjs/components/Keypad/index.d.ts.map +1 -0
  39. package/lib/typescript/commonjs/components/index.d.ts +1 -0
  40. package/lib/typescript/commonjs/components/index.d.ts.map +1 -1
  41. package/lib/typescript/module/components/CodeField/CodeField.d.ts +5 -1
  42. package/lib/typescript/module/components/CodeField/CodeField.d.ts.map +1 -1
  43. package/lib/typescript/module/components/Keypad/Keypad.d.ts +54 -0
  44. package/lib/typescript/module/components/Keypad/Keypad.d.ts.map +1 -0
  45. package/lib/typescript/module/components/Keypad/index.d.ts +3 -0
  46. package/lib/typescript/module/components/Keypad/index.d.ts.map +1 -0
  47. package/lib/typescript/module/components/index.d.ts +1 -0
  48. package/lib/typescript/module/components/index.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/src/components/CodeField/CodeField.tsx +104 -64
  51. package/src/components/Keypad/Keypad.tsx +191 -0
  52. package/src/components/Keypad/index.ts +2 -0
  53. package/src/components/index.ts +1 -0
@@ -13,6 +13,16 @@ import { Text } from '../Text/Text';
13
13
  /** Removes the browser's default focus outline on web (no-op on native). */
14
14
  const WEB_NO_OUTLINE = { outlineStyle: 'none', outlineWidth: 0 } as unknown as TextStyle;
15
15
 
16
+ /** Convert a `#RRGGBB` (or `#RGB`) token to an `rgba()` string at the given alpha. */
17
+ function hexToRgba(hex: string, alpha: number): string {
18
+ const h = hex.replace('#', '');
19
+ const full = h.length === 3 ? h.replace(/./g, (c) => c + c) : h;
20
+ const r = parseInt(full.slice(0, 2), 16);
21
+ const g = parseInt(full.slice(2, 4), 16);
22
+ const b = parseInt(full.slice(4, 6), 16);
23
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
24
+ }
25
+
16
26
  export type CodeFieldMode = 'numeric' | 'alpha' | 'alphanumeric';
17
27
 
18
28
  /** Chars to strip per mode (applied after upper-casing). */
@@ -33,6 +43,10 @@ export interface CodeFieldProps {
33
43
  mode?: CodeFieldMode;
34
44
  /** Called once every cell is filled. */
35
45
  onComplete?: (value: string) => void;
46
+ /** Helper text below the cells. Replaced by the error message when `error` is a string. */
47
+ helperText?: string;
48
+ /** Mark the field invalid. Pass a string to show it as the error message. */
49
+ error?: boolean | string;
36
50
  /** Disable input and dim the cells. */
37
51
  disabled?: boolean;
38
52
  /** Focus on mount. */
@@ -65,6 +79,8 @@ export function CodeField({
65
79
  length = 6,
66
80
  mode = 'numeric',
67
81
  onComplete,
82
+ helperText,
83
+ error,
68
84
  disabled = false,
69
85
  autoFocus = false,
70
86
  cellWidth = 126,
@@ -86,75 +102,99 @@ export function CodeField({
86
102
  };
87
103
 
88
104
  const activeIndex = Math.min(value.length, length - 1);
105
+ const hasError = Boolean(error);
106
+ const message = typeof error === 'string' ? error : helperText;
89
107
 
90
108
  return (
91
- <Pressable
92
- onPress={() => inputRef.current?.focus()}
93
- disabled={disabled}
94
- style={[{ position: 'relative', alignSelf: 'flex-start' }, style]}
95
- >
96
- <View style={{ flexDirection: 'row', gap }}>
97
- {Array.from({ length }).map((_, i) => {
98
- const isActive = focused && i === activeIndex && !disabled;
99
- return (
100
- <View
101
- key={i}
102
- testID="code-field-cell"
103
- style={{
104
- width: cellWidth,
105
- height: cellHeight,
106
- borderRadius: theme.radii.pill,
107
- borderWidth: 1,
108
- borderColor: isActive ? theme.colors.border.focus : theme.colors.border.default,
109
- backgroundColor: disabled
110
- ? theme.colors.disabled.background
111
- : theme.colors.background.default,
112
- alignItems: 'center',
113
- justifyContent: 'center',
114
- opacity: disabled ? 0.6 : 1,
115
- ...(isActive
116
- ? {
117
- boxShadow: [
118
- { offsetX: 0, offsetY: 0, blurRadius: 0, spreadDistance: 6, color: 'rgba(201,255,0,0.15)' },
119
- ] as ViewStyle['boxShadow'],
120
- }
121
- : null),
122
- }}
123
- >
124
- <Text
125
- color={disabled ? 'tertiary' : 'primary'}
109
+ <View style={[{ alignSelf: 'flex-start' }, style]}>
110
+ <Pressable
111
+ onPress={() => inputRef.current?.focus()}
112
+ disabled={disabled}
113
+ style={{ position: 'relative' }}
114
+ >
115
+ <View style={{ flexDirection: 'row', gap }}>
116
+ {Array.from({ length }).map((_, i) => {
117
+ const isActive = focused && i === activeIndex && !disabled;
118
+ // Error overrides the default/focus border on every cell; the active
119
+ // cell still gets the ring (red on error, lime otherwise).
120
+ const borderColor = hasError
121
+ ? theme.colors.feedback.error
122
+ : isActive
123
+ ? theme.colors.border.focus
124
+ : theme.colors.border.default;
125
+ const ringColor = hasError
126
+ ? hexToRgba(theme.colors.feedback.error, 0.15)
127
+ : 'rgba(201,255,0,0.15)';
128
+ return (
129
+ <View
130
+ key={i}
131
+ testID="code-field-cell"
126
132
  style={{
127
- fontFamily: fontFamily ?? theme.fontFamily.regular,
128
- fontSize,
129
- lineHeight: Math.round(fontSize * 1.45),
133
+ width: cellWidth,
134
+ height: cellHeight,
135
+ borderRadius: theme.radii.pill,
136
+ borderWidth: 1,
137
+ borderColor,
138
+ backgroundColor: disabled
139
+ ? theme.colors.disabled.background
140
+ : theme.colors.background.default,
141
+ alignItems: 'center',
142
+ justifyContent: 'center',
143
+ opacity: disabled ? 0.6 : 1,
144
+ ...(isActive
145
+ ? {
146
+ boxShadow: [
147
+ { offsetX: 0, offsetY: 0, blurRadius: 0, spreadDistance: 6, color: ringColor },
148
+ ] as ViewStyle['boxShadow'],
149
+ }
150
+ : null),
130
151
  }}
131
152
  >
132
- {value[i] ?? ''}
133
- </Text>
134
- </View>
135
- );
136
- })}
137
- </View>
153
+ <Text
154
+ color={disabled ? 'tertiary' : 'primary'}
155
+ style={{
156
+ fontFamily: fontFamily ?? theme.fontFamily.regular,
157
+ fontSize,
158
+ lineHeight: Math.round(fontSize * 1.45),
159
+ }}
160
+ >
161
+ {value[i] ?? ''}
162
+ </Text>
163
+ </View>
164
+ );
165
+ })}
166
+ </View>
167
+
168
+ <TextInput
169
+ ref={inputRef}
170
+ value={value}
171
+ onChangeText={handleChange}
172
+ onFocus={() => setFocused(true)}
173
+ onBlur={() => setFocused(false)}
174
+ keyboardType={mode === 'numeric' ? 'number-pad' : 'default'}
175
+ autoCapitalize="characters"
176
+ autoCorrect={false}
177
+ maxLength={length}
178
+ editable={!disabled}
179
+ caretHidden
180
+ autoFocus={autoFocus}
181
+ accessibilityLabel={accessibilityLabel}
182
+ style={[
183
+ { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, opacity: 0 },
184
+ WEB_NO_OUTLINE,
185
+ ]}
186
+ />
187
+ </Pressable>
138
188
 
139
- <TextInput
140
- ref={inputRef}
141
- value={value}
142
- onChangeText={handleChange}
143
- onFocus={() => setFocused(true)}
144
- onBlur={() => setFocused(false)}
145
- keyboardType={mode === 'numeric' ? 'number-pad' : 'default'}
146
- autoCapitalize="characters"
147
- autoCorrect={false}
148
- maxLength={length}
149
- editable={!disabled}
150
- caretHidden
151
- autoFocus={autoFocus}
152
- accessibilityLabel={accessibilityLabel}
153
- style={[
154
- { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, opacity: 0 },
155
- WEB_NO_OUTLINE,
156
- ]}
157
- />
158
- </Pressable>
189
+ {message ? (
190
+ <Text
191
+ variant="bodySmall"
192
+ color={hasError ? theme.colors.feedback.error : 'tertiary'}
193
+ style={{ marginTop: 8, marginLeft: 8 }}
194
+ >
195
+ {message}
196
+ </Text>
197
+ ) : null}
198
+ </View>
159
199
  );
160
200
  }
@@ -0,0 +1,191 @@
1
+ import { type ReactNode } from 'react';
2
+ import { Pressable, View, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { useTheme } from '../../theme';
4
+ import { Icon } from '../Icon/Icon';
5
+ import type { IconName } from '../Icon/icons.generated';
6
+ import { Text } from '../Text/Text';
7
+
8
+ /** The optional key to the left of 0. Hidden unless provided. */
9
+ export interface KeypadExtraKey {
10
+ /** Short label (e.g. "."). */
11
+ label?: string;
12
+ /** Or an icon. */
13
+ icon?: IconName;
14
+ /** If set, pressing appends this to the value (e.g. "." for decimals). */
15
+ value?: string;
16
+ /** Custom action (open a modal, navigate, …). Overrides `value`. */
17
+ onPress?: () => void;
18
+ /** Content color. */
19
+ color?: string;
20
+ /** Key background color. */
21
+ backgroundColor?: string;
22
+ }
23
+
24
+ export interface KeypadProps {
25
+ /** Controlled value being entered. Defaults to ''. */
26
+ value?: string;
27
+ /** Called with the next value on digit / backspace / extra-value press. */
28
+ onChangeText?: (value: string) => void;
29
+ /** Fired on every press with the raw key: '0'–'9', 'backspace', or 'extra'. */
30
+ onKeyPress?: (key: string) => void;
31
+ /** Max length of `value`. */
32
+ maxLength?: number;
33
+ /** Content shown above the keys (dots, amount, anything). Swappable per use case. */
34
+ header?: ReactNode;
35
+ /** The key left of 0. Hidden by default; provide to show and customize it. */
36
+ extraKey?: KeypadExtraKey;
37
+ /** Key height. Defaults to 83. */
38
+ keyHeight?: number;
39
+ /** Gap between keys. Defaults to 8. */
40
+ gap?: number;
41
+ /** Total width. Defaults to full width; the 3 columns split evenly. */
42
+ width?: number;
43
+ /** Digit font size. Defaults to 24. */
44
+ digitSize?: number;
45
+ /** Space between header and keys. Defaults to 24. */
46
+ headerGap?: number;
47
+ /** Disable all keys. */
48
+ disabled?: boolean;
49
+ style?: StyleProp<ViewStyle>;
50
+ }
51
+
52
+ const ROWS = [
53
+ ['1', '2', '3'],
54
+ ['4', '5', '6'],
55
+ ['7', '8', '9'],
56
+ ];
57
+
58
+ /**
59
+ * Numeric keypad — a 3-column pad decoupled from what it drives (bind it to a
60
+ * PIN display, an amount, etc. via `value`/`onChangeText`, and put that display
61
+ * in `header`). Backspace is fixed to the right of 0; the key left of 0 is hidden
62
+ * unless you pass `extraKey` (custom symbol/icon, action and colors). Width,
63
+ * key height, gap and digit size are all configurable.
64
+ */
65
+ export function Keypad({
66
+ value = '',
67
+ onChangeText,
68
+ onKeyPress,
69
+ maxLength,
70
+ header,
71
+ extraKey,
72
+ keyHeight = 83,
73
+ gap = 8,
74
+ width,
75
+ digitSize = 24,
76
+ headerGap = 24,
77
+ disabled = false,
78
+ style,
79
+ }: KeypadProps) {
80
+ const theme = useTheme();
81
+
82
+ const keyStyle = (bg?: string): StyleProp<ViewStyle> => ({
83
+ flex: 1,
84
+ height: keyHeight,
85
+ borderRadius: theme.radii.xl,
86
+ borderWidth: 1,
87
+ borderColor: theme.colors.border.strong,
88
+ backgroundColor: bg ?? theme.colors.action.secondary,
89
+ alignItems: 'center',
90
+ justifyContent: 'center',
91
+ });
92
+
93
+ const pressDigit = (digit: string) => {
94
+ onKeyPress?.(digit);
95
+ if (maxLength != null && value.length >= maxLength) return;
96
+ onChangeText?.(value + digit);
97
+ };
98
+
99
+ const pressBackspace = () => {
100
+ onKeyPress?.('backspace');
101
+ onChangeText?.(value.slice(0, -1));
102
+ };
103
+
104
+ const pressExtra = () => {
105
+ onKeyPress?.('extra');
106
+ if (extraKey?.onPress) {
107
+ extraKey.onPress();
108
+ return;
109
+ }
110
+ if (extraKey?.value != null) {
111
+ if (maxLength != null && value.length >= maxLength) return;
112
+ onChangeText?.(value + extraKey.value);
113
+ }
114
+ };
115
+
116
+ const DigitKey = ({ digit }: { digit: string }) => (
117
+ <Pressable
118
+ onPress={() => pressDigit(digit)}
119
+ disabled={disabled}
120
+ accessibilityRole="button"
121
+ accessibilityLabel={digit}
122
+ style={({ pressed }) => [
123
+ keyStyle(),
124
+ { opacity: disabled ? 0.5 : pressed ? 0.85 : 1 },
125
+ ]}
126
+ >
127
+ <Text
128
+ variant="h2"
129
+ color={theme.colors.action.secondaryForeground}
130
+ style={{ fontSize: digitSize, lineHeight: Math.round(digitSize * 1.35) }}
131
+ >
132
+ {digit}
133
+ </Text>
134
+ </Pressable>
135
+ );
136
+
137
+ return (
138
+ <View style={[{ width: width ?? '100%', gap }, style]}>
139
+ {header != null ? <View style={{ marginBottom: headerGap - gap }}>{header}</View> : null}
140
+
141
+ {ROWS.map((row) => (
142
+ <View key={row[0]} style={{ flexDirection: 'row', gap }}>
143
+ {row.map((digit) => (
144
+ <DigitKey key={digit} digit={digit} />
145
+ ))}
146
+ </View>
147
+ ))}
148
+
149
+ <View style={{ flexDirection: 'row', gap }}>
150
+ {extraKey ? (
151
+ <Pressable
152
+ onPress={pressExtra}
153
+ disabled={disabled}
154
+ accessibilityRole="button"
155
+ accessibilityLabel={extraKey.label ?? 'action'}
156
+ style={({ pressed }) => [
157
+ keyStyle(extraKey.backgroundColor),
158
+ { opacity: disabled ? 0.5 : pressed ? 0.85 : 1 },
159
+ ]}
160
+ >
161
+ {extraKey.icon ? (
162
+ <Icon name={extraKey.icon} size={24} color={extraKey.color ?? theme.colors.action.secondaryForeground} />
163
+ ) : (
164
+ <Text
165
+ variant="h2"
166
+ color={extraKey.color ?? theme.colors.action.secondaryForeground}
167
+ style={{ fontSize: digitSize, lineHeight: Math.round(digitSize * 1.35) }}
168
+ >
169
+ {extraKey.label ?? ''}
170
+ </Text>
171
+ )}
172
+ </Pressable>
173
+ ) : (
174
+ <View style={{ flex: 1 }} />
175
+ )}
176
+
177
+ <DigitKey digit="0" />
178
+
179
+ <Pressable
180
+ onPress={pressBackspace}
181
+ disabled={disabled}
182
+ accessibilityRole="button"
183
+ accessibilityLabel="Backspace"
184
+ style={({ pressed }) => [keyStyle(), { opacity: disabled ? 0.5 : pressed ? 0.85 : 1 }]}
185
+ >
186
+ <Icon name="arrow-left" size={24} color={theme.colors.action.secondaryForeground} />
187
+ </Pressable>
188
+ </View>
189
+ </View>
190
+ );
191
+ }
@@ -0,0 +1,2 @@
1
+ export { Keypad } from './Keypad';
2
+ export type { KeypadProps, KeypadExtraKey } from './Keypad';
@@ -10,6 +10,7 @@ export * from './Dialog';
10
10
  export * from './Icon';
11
11
  export * from './IconButton';
12
12
  export * from './Input';
13
+ export * from './Keypad';
13
14
  export * from './LinkButton';
14
15
  export * from './ProductCard';
15
16
  export * from './QuantityStepper';