@ecohouse/ui 0.1.32 → 0.1.34

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 (68) hide show
  1. package/dist/index.cjs +30063 -607
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +173 -10
  4. package/dist/index.d.ts +173 -10
  5. package/dist/index.js +29989 -547
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/components/AccordionItem/AccordionItem.stories.tsx +54 -0
  9. package/src/components/AccordionItem/AccordionItem.tsx +220 -0
  10. package/src/components/AccordionItem/index.ts +2 -0
  11. package/src/components/AccountHeader/AccountHeader.tsx +34 -29
  12. package/src/components/Button/Button.stories.tsx +10 -1
  13. package/src/components/Button/Button.tsx +32 -3
  14. package/src/components/ContactInfoCard/ContactInfoCard.stories.tsx +30 -0
  15. package/src/components/ContactInfoCard/ContactInfoCard.tsx +141 -0
  16. package/src/components/ContactInfoCard/index.ts +2 -0
  17. package/src/components/DatePicker/DatePicker.tsx +119 -46
  18. package/src/components/Modal/Modal.stories.tsx +100 -0
  19. package/src/components/Modal/Modal.tsx +165 -0
  20. package/src/components/Modal/index.ts +2 -0
  21. package/src/components/Select/Select.tsx +5 -2
  22. package/src/components/Sidebar/Sidebar.tsx +42 -12
  23. package/src/components/StepPager/StepPager.tsx +150 -0
  24. package/src/components/StepPager/index.ts +2 -0
  25. package/src/components/ToggleRow/ToggleRow.stories.tsx +46 -0
  26. package/src/components/ToggleRow/ToggleRow.tsx +121 -0
  27. package/src/components/ToggleRow/index.ts +2 -0
  28. package/src/components/VerificationCodeInput/VerificationCodeInput.tsx +80 -60
  29. package/src/components/index.ts +15 -0
  30. package/src/icons/Ban/BanIcon.tsx +15 -0
  31. package/src/icons/Ban/banPath.ts +2 -0
  32. package/src/icons/Ban/index.ts +1 -0
  33. package/src/icons/CameraFilled/CameraFilledIcon.tsx +15 -0
  34. package/src/icons/CameraFilled/CameraFilledIcon.web.tsx +21 -0
  35. package/src/icons/CameraFilled/cameraFilledPath.ts +2 -0
  36. package/src/icons/CameraFilled/index.ts +1 -0
  37. package/src/icons/Fullscreen/FullscreenIcon.tsx +21 -0
  38. package/src/icons/Fullscreen/fullscreenPath.ts +2 -0
  39. package/src/icons/Fullscreen/index.ts +1 -0
  40. package/src/icons/Headset/HeadsetIcon.tsx +26 -0
  41. package/src/icons/Headset/HeadsetIcon.web.tsx +25 -0
  42. package/src/icons/Headset/headsetPath.ts +6 -0
  43. package/src/icons/Headset/index.ts +1 -0
  44. package/src/icons/Hide/HideIcon.tsx +21 -0
  45. package/src/icons/Hide/HideIcon.web.tsx +27 -0
  46. package/src/icons/Hide/hidePath.ts +3 -0
  47. package/src/icons/Hide/index.ts +1 -0
  48. package/src/icons/Home/homePath.ts +1 -1
  49. package/src/icons/MenuLines/MenuLinesIcon.tsx +20 -0
  50. package/src/icons/MenuLines/MenuLinesIcon.web.tsx +26 -0
  51. package/src/icons/MenuLines/index.ts +1 -0
  52. package/src/icons/MenuLines/menuLinesPath.ts +3 -0
  53. package/src/icons/Sort/SortIcon.tsx +3 -3
  54. package/src/icons/Sort/SortIcon.web.tsx +2 -2
  55. package/src/icons/Sort/sortPath.ts +3 -2
  56. package/src/icons/VideoOff/VideoOffIcon.tsx +21 -0
  57. package/src/icons/VideoOff/VideoOffIcon.web.tsx +27 -0
  58. package/src/icons/VideoOff/index.ts +1 -0
  59. package/src/icons/VideoOff/videoOffPath.ts +2 -0
  60. package/src/icons/ZoomOut/ZoomOutIcon.tsx +21 -0
  61. package/src/icons/ZoomOut/index.ts +1 -0
  62. package/src/icons/ZoomOut/zoomOutPath.ts +2 -0
  63. package/src/icons/index.ts +8 -0
  64. package/src/icons/registry.ts +23 -0
  65. package/src/index.ts +20 -0
  66. package/src/theme/colors.test.ts +1 -1
  67. package/src/theme/typography.ts +4 -4
  68. package/src/utils/popoverAlign.ts +85 -0
@@ -61,6 +61,8 @@ type SelectBaseProps = Omit<ViewProps, "style" | "children"> & {
61
61
  loading?: boolean;
62
62
  /** Text shown when no options match. */
63
63
  emptyText?: string;
64
+ /** Override color for the selected value text in the trigger. */
65
+ valueColor?: string;
64
66
  style?: StyleProp<ViewStyle>;
65
67
  className?: string;
66
68
  };
@@ -264,6 +266,7 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
264
266
  onSearchChange,
265
267
  loading = false,
266
268
  emptyText = "No results",
269
+ valueColor,
267
270
  style,
268
271
  className,
269
272
  accessibilityLabel,
@@ -508,7 +511,7 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
508
511
 
509
512
  const leftIconNode = renderStatefulIcon(leftIcon, UserIcon, {
510
513
  size: ICON_SIZE,
511
- color: error ? colors.red : isOpen ? colors.primary : colors.grey100,
514
+ color: error ? colors.red : colors.primary,
512
515
  });
513
516
  const hasLeftIcon = showLeftIcon || leftIcon != null;
514
517
 
@@ -520,7 +523,7 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
520
523
  const triggerTextColor = hasValue
521
524
  ? error
522
525
  ? colors.red
523
- : colors.white
526
+ : (valueColor ?? colors.white)
524
527
  : error
525
528
  ? colors.red
526
529
  : colors.grey100;
@@ -18,7 +18,7 @@ import {
18
18
  type ViewStyle,
19
19
  } from "react-native";
20
20
  import type { IconComponent } from "../../icons";
21
- import { SidebarIcon } from "../../icons";
21
+ import { CloseIcon, SidebarIcon } from "../../icons";
22
22
  import { BrandLogo, MenuItem } from "../../primitives";
23
23
  import { colors, fonts } from "../../theme";
24
24
  import { mergeRefs } from "../../utils/mergeRefs";
@@ -59,6 +59,12 @@ export interface SidebarProps extends Omit<ViewProps, "style" | "children"> {
59
59
  collapsed?: boolean;
60
60
  defaultCollapsed?: boolean;
61
61
  onCollapsedChange?: (collapsed: boolean) => void;
62
+ /** When false, hides the expand/collapse control. Defaults to `true`. */
63
+ showCollapseToggle?: boolean;
64
+ /** When set, shows a close control in the header (e.g. mobile drawer). */
65
+ onClose?: () => void;
66
+ /** Accessible label for the close control. Defaults to `"Close"`. */
67
+ closeAccessibilityLabel?: string;
62
68
  style?: StyleProp<ViewStyle>;
63
69
  className?: string;
64
70
  }
@@ -86,6 +92,9 @@ export const Sidebar = forwardRef<ComponentRef<typeof View>, SidebarProps>(funct
86
92
  collapsed: collapsedProp,
87
93
  defaultCollapsed = false,
88
94
  onCollapsedChange,
95
+ showCollapseToggle = true,
96
+ onClose,
97
+ closeAccessibilityLabel = "Close",
89
98
  style,
90
99
  className,
91
100
  ...rest
@@ -136,7 +145,13 @@ export const Sidebar = forwardRef<ComponentRef<typeof View>, SidebarProps>(funct
136
145
  style={[styles.root, { width: widthAnim }, style]}
137
146
  accessibilityRole="menu"
138
147
  >
139
- <View style={[styles.header, collapsed ? styles.headerCollapsed : styles.headerExpanded]}>
148
+ <View
149
+ style={[
150
+ styles.header,
151
+ collapsed ? styles.headerCollapsed : styles.headerExpanded,
152
+ !showCollapseToggle && !onClose ? styles.headerNoToggle : null,
153
+ ]}
154
+ >
140
155
  <Animated.View
141
156
  pointerEvents={collapsed ? "none" : "auto"}
142
157
  style={[
@@ -157,16 +172,28 @@ export const Sidebar = forwardRef<ComponentRef<typeof View>, SidebarProps>(funct
157
172
  />
158
173
  </Animated.View>
159
174
 
160
- <Pressable
161
- accessibilityRole="button"
162
- accessibilityLabel={collapsed ? expandAccessibilityLabel : collapseAccessibilityLabel}
163
- accessibilityState={{ expanded: !collapsed }}
164
- hitSlop={8}
165
- onPress={() => setCollapsed(!collapsed)}
166
- style={styles.toggleButton}
167
- >
168
- <SidebarIcon color={colors.grey100} size={TOGGLE_SIZE} strokeWidth={2} />
169
- </Pressable>
175
+ {onClose ? (
176
+ <Pressable
177
+ accessibilityRole="button"
178
+ accessibilityLabel={closeAccessibilityLabel}
179
+ hitSlop={8}
180
+ onPress={onClose}
181
+ style={styles.toggleButton}
182
+ >
183
+ <CloseIcon color={colors.grey100} size={TOGGLE_SIZE} strokeWidth={2} />
184
+ </Pressable>
185
+ ) : showCollapseToggle ? (
186
+ <Pressable
187
+ accessibilityRole="button"
188
+ accessibilityLabel={collapsed ? expandAccessibilityLabel : collapseAccessibilityLabel}
189
+ accessibilityState={{ expanded: !collapsed }}
190
+ hitSlop={8}
191
+ onPress={() => setCollapsed(!collapsed)}
192
+ style={styles.toggleButton}
193
+ >
194
+ <SidebarIcon color={colors.grey100} size={TOGGLE_SIZE} strokeWidth={2} />
195
+ </Pressable>
196
+ ) : null}
170
197
  </View>
171
198
 
172
199
  <View
@@ -242,6 +269,9 @@ const styles = StyleSheet.create({
242
269
  justifyContent: "space-between",
243
270
  paddingHorizontal: 24,
244
271
  },
272
+ headerNoToggle: {
273
+ justifyContent: "flex-start",
274
+ },
245
275
  headerCollapsed: {
246
276
  justifyContent: "center",
247
277
  paddingHorizontal: 0,
@@ -0,0 +1,150 @@
1
+ import { forwardRef, useMemo, useRef, type ComponentRef } from "react";
2
+ import {
3
+ Platform,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ type StyleProp,
9
+ type ViewProps,
10
+ type ViewStyle,
11
+ } from "react-native";
12
+ import { ChevronLeftIcon } from "../../icons";
13
+ import { colors, fonts } from "../../theme";
14
+ import { mergeRefs } from "../../utils/mergeRefs";
15
+ import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
16
+
17
+ export interface StepPagerProps extends Omit<ViewProps, "style" | "children"> {
18
+ /** Zero-based active step index. */
19
+ index: number;
20
+ /** Total number of steps. */
21
+ count: number;
22
+ onPrevious?: () => void;
23
+ onNext?: () => void;
24
+ previousAccessibilityLabel?: string;
25
+ nextAccessibilityLabel?: string;
26
+ style?: StyleProp<ViewStyle>;
27
+ className?: string;
28
+ }
29
+
30
+ const WIDTH = 121;
31
+ const HEIGHT = 48;
32
+ const ICON_SIZE = 24;
33
+
34
+ /**
35
+ * Compact prev / current-of-total / next control (e.g. camera fullscreen pager).
36
+ * Fixed 121×48 pill, 12 gap/padding, 77 radius.
37
+ */
38
+ export const StepPager = forwardRef<ComponentRef<typeof View>, StepPagerProps>(function StepPager(
39
+ {
40
+ index,
41
+ count,
42
+ onPrevious,
43
+ onNext,
44
+ previousAccessibilityLabel = "Previous",
45
+ nextAccessibilityLabel = "Next",
46
+ style,
47
+ className,
48
+ accessibilityLabel,
49
+ ...rest
50
+ },
51
+ forwardedRef,
52
+ ) {
53
+ const containerRef = useRef<ComponentRef<typeof View>>(null);
54
+ const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
55
+ const safeCount = Math.max(0, count);
56
+ const safeIndex = safeCount === 0 ? 0 : Math.min(Math.max(index, 0), safeCount - 1);
57
+ const label = safeCount === 0 ? "0/0" : `${safeIndex + 1}/${safeCount}`;
58
+ const atStart = safeIndex <= 0;
59
+ const atEnd = safeIndex >= safeCount - 1;
60
+
61
+ useApplyWebClassName(containerRef, className);
62
+
63
+ return (
64
+ <View
65
+ {...rest}
66
+ ref={setContainerRef}
67
+ accessibilityRole="adjustable"
68
+ accessibilityLabel={accessibilityLabel ?? label}
69
+ accessibilityValue={{
70
+ min: safeCount === 0 ? 0 : 1,
71
+ max: safeCount,
72
+ now: safeCount === 0 ? 0 : safeIndex + 1,
73
+ text: label,
74
+ }}
75
+ style={[styles.root, webBlurStyle, style]}
76
+ >
77
+ <Pressable
78
+ onPress={onPrevious}
79
+ disabled={atStart || !onPrevious}
80
+ hitSlop={4}
81
+ accessibilityRole="button"
82
+ accessibilityLabel={previousAccessibilityLabel}
83
+ accessibilityState={{ disabled: atStart || !onPrevious }}
84
+ style={[styles.navButton, (atStart || !onPrevious) && styles.navDisabled]}
85
+ >
86
+ <ChevronLeftIcon size={ICON_SIZE} color={colors.white} strokeWidth={2} />
87
+ </Pressable>
88
+
89
+ <Text style={styles.label}>{label}</Text>
90
+
91
+ <Pressable
92
+ onPress={onNext}
93
+ disabled={atEnd || !onNext}
94
+ hitSlop={4}
95
+ accessibilityRole="button"
96
+ accessibilityLabel={nextAccessibilityLabel}
97
+ accessibilityState={{ disabled: atEnd || !onNext }}
98
+ style={[styles.navButton, (atEnd || !onNext) && styles.navDisabled]}
99
+ >
100
+ <View style={styles.chevronRight}>
101
+ <ChevronLeftIcon size={ICON_SIZE} color={colors.white} strokeWidth={2} />
102
+ </View>
103
+ </Pressable>
104
+ </View>
105
+ );
106
+ });
107
+
108
+ const webBlurStyle: ViewStyle =
109
+ Platform.OS === "web"
110
+ ? ({
111
+ backdropFilter: "blur(50px)",
112
+ WebkitBackdropFilter: "blur(50px)",
113
+ } as ViewStyle)
114
+ : {};
115
+
116
+ const styles = StyleSheet.create({
117
+ root: {
118
+ width: WIDTH,
119
+ height: HEIGHT,
120
+ flexDirection: "row",
121
+ alignItems: "center",
122
+ justifyContent: "center",
123
+ gap: 12,
124
+ padding: 12,
125
+ borderRadius: 77,
126
+ backgroundColor: colors.whiteAlpha6,
127
+ overflow: "hidden",
128
+ },
129
+ navButton: {
130
+ width: ICON_SIZE,
131
+ height: ICON_SIZE,
132
+ alignItems: "center",
133
+ justifyContent: "center",
134
+ },
135
+ navDisabled: {
136
+ opacity: 0.35,
137
+ },
138
+ chevronRight: {
139
+ transform: [{ scaleX: -1 }],
140
+ },
141
+ label: {
142
+ fontFamily: fonts.sans,
143
+ fontSize: 16,
144
+ fontWeight: "700",
145
+ lineHeight: 16,
146
+ letterSpacing: 0,
147
+ color: colors.white,
148
+ textAlign: "center",
149
+ },
150
+ });
@@ -0,0 +1,2 @@
1
+ export { StepPager } from "./StepPager";
2
+ export type { StepPagerProps } from "./StepPager";
@@ -0,0 +1,46 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { fn } from "storybook/test";
3
+ import { View, StyleSheet } from "react-native";
4
+ import { ToggleRow } from "./ToggleRow";
5
+
6
+ const meta = {
7
+ title: "Components/ToggleRow",
8
+ component: ToggleRow,
9
+ args: {
10
+ label: "Bookings and digital key",
11
+ defaultValue: false,
12
+ onValueChange: fn(),
13
+ },
14
+ parameters: {
15
+ backgrounds: { default: "black" },
16
+ },
17
+ } satisfies Meta<typeof ToggleRow>;
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof meta>;
21
+
22
+ export const Default: Story = {};
23
+
24
+ export const Active: Story = {
25
+ args: { defaultValue: true },
26
+ };
27
+
28
+ export const Stack: Story = {
29
+ render: () => (
30
+ <View style={styles.stack}>
31
+ <ToggleRow label="Bookings and digital key" defaultValue={false} onValueChange={fn()} />
32
+ <ToggleRow
33
+ label="Email notifications"
34
+ defaultValue={false}
35
+ onValueChange={fn()}
36
+ showBorder={false}
37
+ />
38
+ </View>
39
+ ),
40
+ };
41
+
42
+ const styles = StyleSheet.create({
43
+ stack: {
44
+ width: 462,
45
+ },
46
+ });
@@ -0,0 +1,121 @@
1
+ import { forwardRef, useMemo, useRef, type ComponentRef } from "react";
2
+ import {
3
+ StyleSheet,
4
+ Text,
5
+ useWindowDimensions,
6
+ View,
7
+ type StyleProp,
8
+ type ViewProps,
9
+ type ViewStyle,
10
+ } from "react-native";
11
+ import { colors, fonts } from "../../theme";
12
+ import { mergeRefs } from "../../utils/mergeRefs";
13
+ import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
14
+ import { Toggle } from "../Toggle";
15
+
16
+ export interface ToggleRowProps extends Omit<ViewProps, "style" | "children"> {
17
+ label: string;
18
+ value?: boolean;
19
+ defaultValue?: boolean;
20
+ onValueChange?: (value: boolean) => void;
21
+ disabled?: boolean;
22
+ /** When false, hides the bottom border. Defaults to true. */
23
+ showBorder?: boolean;
24
+ style?: StyleProp<ViewStyle>;
25
+ className?: string;
26
+ }
27
+
28
+ const DESKTOP_MIN_WIDTH = 1024;
29
+
30
+ /**
31
+ * Settings toggle line — label left, switch right.
32
+ * Mobile: height 57, padding 16/12, label 12px.
33
+ * Desktop: height 65, padding 20/12, label 14px.
34
+ */
35
+ export const ToggleRow = forwardRef<ComponentRef<typeof View>, ToggleRowProps>(function ToggleRow(
36
+ {
37
+ label,
38
+ value,
39
+ defaultValue = false,
40
+ onValueChange,
41
+ disabled = false,
42
+ showBorder = true,
43
+ style,
44
+ className,
45
+ accessibilityLabel,
46
+ ...rest
47
+ },
48
+ forwardedRef,
49
+ ) {
50
+ const containerRef = useRef<ComponentRef<typeof View>>(null);
51
+ const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
52
+ const { width: viewportWidth } = useWindowDimensions();
53
+ const isDesktop = viewportWidth >= DESKTOP_MIN_WIDTH;
54
+ const labelSize = isDesktop ? 14 : 12;
55
+
56
+ useApplyWebClassName(containerRef, className);
57
+
58
+ return (
59
+ <View
60
+ {...rest}
61
+ ref={setContainerRef}
62
+ style={[
63
+ styles.root,
64
+ {
65
+ minHeight: isDesktop ? 65 : 57,
66
+ paddingTop: isDesktop ? 20 : 16,
67
+ paddingBottom: isDesktop ? 20 : 16,
68
+ },
69
+ !showBorder && styles.noBorder,
70
+ style,
71
+ ]}
72
+ >
73
+ <Text
74
+ style={[
75
+ styles.label,
76
+ {
77
+ fontSize: labelSize,
78
+ lineHeight: isDesktop ? 20 : 18,
79
+ letterSpacing: labelSize * 0.03,
80
+ },
81
+ ]}
82
+ numberOfLines={2}
83
+ >
84
+ {label}
85
+ </Text>
86
+ <Toggle
87
+ value={value}
88
+ defaultValue={defaultValue}
89
+ onValueChange={onValueChange}
90
+ disabled={disabled}
91
+ accessibilityLabel={accessibilityLabel ?? label}
92
+ />
93
+ </View>
94
+ );
95
+ });
96
+
97
+ const styles = StyleSheet.create({
98
+ root: {
99
+ width: "100%",
100
+ flexDirection: "row",
101
+ alignItems: "center",
102
+ gap: 40,
103
+ paddingRight: 12,
104
+ paddingLeft: 12,
105
+ borderBottomWidth: 1,
106
+ borderBottomColor: colors.grey650,
107
+ opacity: 1,
108
+ },
109
+ noBorder: {
110
+ borderBottomWidth: 0,
111
+ },
112
+ label: {
113
+ flex: 1,
114
+ minWidth: 0,
115
+ fontFamily: fonts.sans,
116
+ fontWeight: "700",
117
+ color: colors.white,
118
+ // RN-web clips Armenian descenders when line boxes are tight.
119
+ overflow: "visible",
120
+ },
121
+ });
@@ -0,0 +1,2 @@
1
+ export { ToggleRow } from "./ToggleRow";
2
+ export type { ToggleRowProps } from "./ToggleRow";
@@ -9,6 +9,7 @@ import {
9
9
  import {
10
10
  Platform,
11
11
  StyleSheet,
12
+ Text,
12
13
  TextInput,
13
14
  useWindowDimensions,
14
15
  View,
@@ -24,12 +25,13 @@ import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
24
25
  import { normalizeVerificationCode, updateVerificationCode } from "./VerificationCodeInput.utils";
25
26
 
26
27
  const DEFAULT_LENGTH = 6;
27
- const DESKTOP_CELL_WIDTH = 54;
28
- const MOBILE_CELL_WIDTH = 50;
29
- const CELL_HEIGHT = 68;
28
+ const DESKTOP_CELL_WIDTH = 54.333335876464844;
29
+ const MOBILE_CELL_WIDTH = 45.16666793823242;
30
+ const DESKTOP_CELL_HEIGHT = 68;
31
+ const MOBILE_CELL_HEIGHT = 58;
30
32
  const DESKTOP_GAP = 12;
31
- const MOBILE_GAP = 4;
32
- const MOBILE_BREAKPOINT = 640;
33
+ const MOBILE_GAP = 12;
34
+ const MOBILE_BREAKPOINT = 1024;
33
35
 
34
36
  export interface VerificationCodeInputHandle {
35
37
  clear: () => void;
@@ -47,6 +49,8 @@ export interface VerificationCodeInputProps extends Omit<ViewProps, "children" |
47
49
  /** Called when Enter/submit is pressed from a cell. */
48
50
  onSubmit?: (value: string) => void;
49
51
  error?: boolean;
52
+ /** Shown under the cells when `error` is true. */
53
+ errorMessage?: string;
50
54
  disabled?: boolean;
51
55
  autoFocus?: boolean;
52
56
  /** Localized accessible name for each zero-based cell index. */
@@ -68,6 +72,7 @@ export const VerificationCodeInput = forwardRef<
68
72
  onComplete,
69
73
  onSubmit,
70
74
  error = false,
75
+ errorMessage,
71
76
  disabled = false,
72
77
  autoFocus = false,
73
78
  getDigitAccessibilityLabel,
@@ -161,71 +166,77 @@ export const VerificationCodeInput = forwardRef<
161
166
 
162
167
  const mobile = viewportWidth < MOBILE_BREAKPOINT;
163
168
  const cellWidth = mobile ? MOBILE_CELL_WIDTH : DESKTOP_CELL_WIDTH;
169
+ const cellHeight = mobile ? MOBILE_CELL_HEIGHT : DESKTOP_CELL_HEIGHT;
164
170
  const cellGap = mobile ? MOBILE_GAP : DESKTOP_GAP;
165
- const rootWidth = cellWidth * safeLength + cellGap * Math.max(0, safeLength - 1);
171
+ const rowWidth = cellWidth * safeLength + cellGap * Math.max(0, safeLength - 1);
172
+ const showErrorMessage = Boolean(error && errorMessage);
166
173
 
167
174
  return (
168
- <View
169
- {...viewProps}
170
- ref={rootRef}
171
- style={[
172
- styles.root,
173
- { width: rootWidth, maxWidth: "100%", gap: cellGap },
174
- disabled && styles.disabled,
175
- style,
176
- ]}
177
- >
178
- {Array.from({ length: safeLength }, (_, index) => {
179
- const active = focusedIndex === index;
180
- const borderColor = error ? colors.redHover : active ? colors.primary : colors.grey600;
181
-
182
- return (
183
- <TextInput
184
- ref={(element) => {
185
- inputRefs.current[index] = element;
186
- }}
187
- accessibilityLabel={
188
- getDigitAccessibilityLabel?.(index) ?? `Verification code digit ${index + 1}`
189
- }
190
- accessibilityState={{ disabled }}
191
- autoComplete={index === 0 ? "one-time-code" : "off"}
192
- autoFocus={autoFocus && index === 0}
193
- caretHidden={Platform.OS !== "web"}
194
- editable={!disabled}
195
- inputMode="numeric"
196
- key={index}
197
- keyboardType="number-pad"
198
- maxLength={safeLength}
199
- onBlur={() => setFocusedIndex((current) => (current === index ? null : current))}
200
- onChangeText={(text) => handleChange(index, text)}
201
- onFocus={() => setFocusedIndex(index)}
202
- onKeyPress={(event) => handleKeyPress(index, event)}
203
- onSubmitEditing={() => onSubmit?.(currentCode)}
204
- selectTextOnFocus
205
- style={[
206
- styles.cell,
207
- {
208
- width: cellWidth,
209
- height: CELL_HEIGHT,
210
- borderColor,
211
- color: error ? colors.red : colors.white,
212
- },
213
- Platform.OS === "web" ? styles.cellWeb : null,
214
- Platform.OS === "android" ? styles.cellAndroid : null,
215
- inputStyle,
216
- ]}
217
- value={currentCode[index] ?? ""}
218
- />
219
- );
220
- })}
175
+ <View {...viewProps} ref={rootRef} style={[styles.wrapper, disabled && styles.disabled, style]}>
176
+ <View style={[styles.row, { width: rowWidth, maxWidth: "100%", gap: cellGap }]}>
177
+ {Array.from({ length: safeLength }, (_, index) => {
178
+ const active = focusedIndex === index;
179
+ const borderColor = error ? colors.red : active ? colors.primary : colors.grey600;
180
+
181
+ return (
182
+ <TextInput
183
+ ref={(element) => {
184
+ inputRefs.current[index] = element;
185
+ }}
186
+ accessibilityLabel={
187
+ getDigitAccessibilityLabel?.(index) ?? `Verification code digit ${index + 1}`
188
+ }
189
+ accessibilityState={{ disabled }}
190
+ autoComplete={index === 0 ? "one-time-code" : "off"}
191
+ autoFocus={autoFocus && index === 0}
192
+ caretHidden={Platform.OS !== "web"}
193
+ editable={!disabled}
194
+ inputMode="numeric"
195
+ key={index}
196
+ keyboardType="number-pad"
197
+ maxLength={safeLength}
198
+ onBlur={() => setFocusedIndex((current) => (current === index ? null : current))}
199
+ onChangeText={(text) => handleChange(index, text)}
200
+ onFocus={() => setFocusedIndex(index)}
201
+ onKeyPress={(event) => handleKeyPress(index, event)}
202
+ onSubmitEditing={() => onSubmit?.(currentCode)}
203
+ selectTextOnFocus
204
+ style={[
205
+ styles.cell,
206
+ {
207
+ width: cellWidth,
208
+ height: cellHeight,
209
+ borderColor,
210
+ color: error ? colors.red : colors.white,
211
+ },
212
+ Platform.OS === "web" ? styles.cellWeb : null,
213
+ Platform.OS === "android" ? styles.cellAndroid : null,
214
+ inputStyle,
215
+ ]}
216
+ value={currentCode[index] ?? ""}
217
+ />
218
+ );
219
+ })}
220
+ </View>
221
+
222
+ {showErrorMessage ? (
223
+ <Text accessibilityLiveRegion="assertive" style={styles.errorMessage}>
224
+ {errorMessage}
225
+ </Text>
226
+ ) : null}
221
227
  </View>
222
228
  );
223
229
  });
224
230
 
225
231
  const styles = StyleSheet.create({
226
- root: {
232
+ wrapper: {
227
233
  minWidth: 0,
228
234
  alignSelf: "center",
235
+ alignItems: "center",
236
+ gap: 12,
237
+ },
238
+ row: {
239
+ minWidth: 0,
229
240
  flexDirection: "row",
230
241
  justifyContent: "center",
231
242
  },
@@ -252,4 +263,13 @@ const styles = StyleSheet.create({
252
263
  cellAndroid: {
253
264
  includeFontPadding: false,
254
265
  },
266
+ errorMessage: {
267
+ fontFamily: fonts.sans,
268
+ fontSize: 12,
269
+ fontWeight: "400",
270
+ lineHeight: 12,
271
+ letterSpacing: 0,
272
+ textAlign: "center",
273
+ color: colors.red,
274
+ },
255
275
  });
@@ -16,9 +16,18 @@ export type { StatusBadgeProps, StatusBadgeTone } from "./StatusBadge";
16
16
  export { PaginationDots } from "./PaginationDots";
17
17
  export type { PaginationDotsProps } from "./PaginationDots";
18
18
 
19
+ export { StepPager } from "./StepPager";
20
+ export type { StepPagerProps } from "./StepPager";
21
+
19
22
  export { InfoTile } from "./InfoTile";
20
23
  export type { InfoTileProps } from "./InfoTile";
21
24
 
25
+ export { ContactInfoCard } from "./ContactInfoCard";
26
+ export type { ContactInfoCardProps } from "./ContactInfoCard";
27
+
28
+ export { AccordionItem } from "./AccordionItem";
29
+ export type { AccordionItemProps } from "./AccordionItem";
30
+
22
31
  export { StepList } from "./StepList";
23
32
  export type { StepListProps } from "./StepList";
24
33
 
@@ -55,6 +64,9 @@ export type {
55
64
  export { FloatingActionButton } from "./FloatingActionButton";
56
65
  export type { FloatingActionButtonProps } from "./FloatingActionButton";
57
66
 
67
+ export { Modal } from "./Modal";
68
+ export type { ModalProps } from "./Modal";
69
+
58
70
  export { Input } from "./Input";
59
71
  export type { InputIcon, InputProps, InputSize } from "./Input";
60
72
 
@@ -79,6 +91,9 @@ export type { TextareaProps } from "./Textarea";
79
91
  export { Toggle } from "./Toggle";
80
92
  export type { ToggleProps } from "./Toggle";
81
93
 
94
+ export { ToggleRow } from "./ToggleRow";
95
+ export type { ToggleRowProps } from "./ToggleRow";
96
+
82
97
  export { RatingInput } from "./RatingInput";
83
98
  export type { RatingInputProps } from "./RatingInput";
84
99