@ecohouse/ui 0.1.33 → 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 (45) hide show
  1. package/dist/index.cjs +29640 -471
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +101 -4
  4. package/dist/index.d.ts +101 -4
  5. package/dist/index.js +29568 -407
  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 +4 -1
  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/Sidebar/Sidebar.tsx +19 -3
  22. package/src/components/ToggleRow/ToggleRow.stories.tsx +46 -0
  23. package/src/components/ToggleRow/ToggleRow.tsx +121 -0
  24. package/src/components/ToggleRow/index.ts +2 -0
  25. package/src/components/VerificationCodeInput/VerificationCodeInput.tsx +80 -60
  26. package/src/components/index.ts +12 -0
  27. package/src/icons/CameraFilled/CameraFilledIcon.tsx +15 -0
  28. package/src/icons/CameraFilled/CameraFilledIcon.web.tsx +21 -0
  29. package/src/icons/CameraFilled/cameraFilledPath.ts +2 -0
  30. package/src/icons/CameraFilled/index.ts +1 -0
  31. package/src/icons/Headset/HeadsetIcon.tsx +26 -0
  32. package/src/icons/Headset/HeadsetIcon.web.tsx +25 -0
  33. package/src/icons/Headset/headsetPath.ts +6 -0
  34. package/src/icons/Headset/index.ts +1 -0
  35. package/src/icons/Hide/HideIcon.tsx +21 -0
  36. package/src/icons/Hide/HideIcon.web.tsx +27 -0
  37. package/src/icons/Hide/hidePath.ts +3 -0
  38. package/src/icons/Hide/index.ts +1 -0
  39. package/src/icons/MenuLines/MenuLinesIcon.tsx +20 -0
  40. package/src/icons/MenuLines/MenuLinesIcon.web.tsx +26 -0
  41. package/src/icons/MenuLines/index.ts +1 -0
  42. package/src/icons/MenuLines/menuLinesPath.ts +3 -0
  43. package/src/icons/index.ts +4 -0
  44. package/src/icons/registry.ts +11 -0
  45. package/src/index.ts +12 -0
@@ -0,0 +1,141 @@
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 type { IconComponent, IconProps } from "../../icons";
12
+ import { colors, fonts } from "../../theme";
13
+ import { mergeRefs } from "../../utils/mergeRefs";
14
+ import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
15
+
16
+ export interface ContactInfoCardProps extends Omit<ViewProps, "style" | "children"> {
17
+ title: string;
18
+ value: string;
19
+ icon: IconComponent;
20
+ iconProps?: Omit<IconProps, "size">;
21
+ iconSize?: number;
22
+ /** Icon slot fill. Defaults to `#E38E5E0F`. */
23
+ iconSlotBackgroundColor?: string;
24
+ style?: StyleProp<ViewStyle>;
25
+ className?: string;
26
+ }
27
+
28
+ const DESKTOP_MIN_WIDTH = 1024;
29
+ const CARD_HEIGHT_MOBILE = 78;
30
+ const CARD_HEIGHT_DESKTOP = 96;
31
+ const CARD_PADDING_MOBILE = 16;
32
+ const CARD_PADDING_DESKTOP = 24;
33
+ const CARD_GAP_MOBILE = 16;
34
+ const CARD_GAP_DESKTOP = 24;
35
+ const ICON_SLOT = 48;
36
+ const ICON_SIZE = 24;
37
+ const ICON_SLOT_BG = "#E38E5E0F";
38
+ const ICON_COLOR = "#B76533";
39
+
40
+ /**
41
+ * Support contact strip — icon slot + title/value.
42
+ * Mobile: height 78, padding 16, gap 16, radius 20.
43
+ * Desktop: height 96, padding 24, gap 24, radius 20.
44
+ */
45
+ export const ContactInfoCard = forwardRef<ComponentRef<typeof View>, ContactInfoCardProps>(
46
+ function ContactInfoCard(
47
+ {
48
+ title,
49
+ value,
50
+ icon: Icon,
51
+ iconProps,
52
+ iconSize = ICON_SIZE,
53
+ iconSlotBackgroundColor = ICON_SLOT_BG,
54
+ style,
55
+ className,
56
+ accessibilityLabel,
57
+ ...rest
58
+ },
59
+ forwardedRef,
60
+ ) {
61
+ const containerRef = useRef<ComponentRef<typeof View>>(null);
62
+ const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
63
+ const { width: viewportWidth } = useWindowDimensions();
64
+ const isDesktop = viewportWidth >= DESKTOP_MIN_WIDTH;
65
+
66
+ useApplyWebClassName(containerRef, className);
67
+
68
+ return (
69
+ <View
70
+ {...rest}
71
+ ref={setContainerRef}
72
+ accessibilityRole="text"
73
+ accessibilityLabel={accessibilityLabel ?? `${title}. ${value}`}
74
+ style={[
75
+ styles.root,
76
+ {
77
+ height: isDesktop ? CARD_HEIGHT_DESKTOP : CARD_HEIGHT_MOBILE,
78
+ gap: isDesktop ? CARD_GAP_DESKTOP : CARD_GAP_MOBILE,
79
+ padding: isDesktop ? CARD_PADDING_DESKTOP : CARD_PADDING_MOBILE,
80
+ },
81
+ style,
82
+ ]}
83
+ >
84
+ <View style={[styles.iconSlot, { backgroundColor: iconSlotBackgroundColor }]}>
85
+ <Icon
86
+ {...iconProps}
87
+ size={iconSize}
88
+ color={iconProps?.color ?? ICON_COLOR}
89
+ strokeWidth={iconProps?.strokeWidth ?? 2}
90
+ />
91
+ </View>
92
+ <View style={styles.textBlock}>
93
+ <Text style={styles.title}>{title}</Text>
94
+ <Text style={styles.value}>{value}</Text>
95
+ </View>
96
+ </View>
97
+ );
98
+ },
99
+ );
100
+
101
+ const styles = StyleSheet.create({
102
+ root: {
103
+ width: "100%",
104
+ flexDirection: "row",
105
+ alignItems: "center",
106
+ borderRadius: 20,
107
+ backgroundColor: colors.grey750,
108
+ opacity: 1,
109
+ },
110
+ iconSlot: {
111
+ width: ICON_SLOT,
112
+ height: ICON_SLOT,
113
+ padding: 12,
114
+ borderRadius: 12,
115
+ alignItems: "center",
116
+ justifyContent: "center",
117
+ flexShrink: 0,
118
+ },
119
+ textBlock: {
120
+ flexShrink: 1,
121
+ minWidth: 0,
122
+ flexDirection: "column",
123
+ gap: 8,
124
+ },
125
+ title: {
126
+ fontFamily: fonts.sans,
127
+ fontSize: 14,
128
+ fontWeight: "600",
129
+ lineHeight: 14,
130
+ letterSpacing: 0.42,
131
+ color: colors.white,
132
+ },
133
+ value: {
134
+ fontFamily: fonts.sans,
135
+ fontSize: 14,
136
+ fontWeight: "400",
137
+ lineHeight: 14,
138
+ letterSpacing: 0.42,
139
+ color: colors.lightGrey,
140
+ },
141
+ });
@@ -0,0 +1,2 @@
1
+ export { ContactInfoCard } from "./ContactInfoCard";
2
+ export type { ContactInfoCardProps } from "./ContactInfoCard";
@@ -119,7 +119,8 @@ export type DatePickerRangeProps = DatePickerBaseProps & {
119
119
 
120
120
  export type DatePickerProps = DatePickerSingleProps | DatePickerRangeProps;
121
121
 
122
- const DEFAULT_WIDTH = 380;
122
+ const DEFAULT_WIDTH = 320;
123
+ const DEFAULT_MENU_HEIGHT = 388;
123
124
  const RANGE_TRIGGER_WIDTH = 382;
124
125
  const TRIGGER_HEIGHT = 44;
125
126
  const TRIGGER_RADIUS = 60;
@@ -751,11 +752,13 @@ const styles = StyleSheet.create({
751
752
  position: "absolute",
752
753
  top: "100%",
753
754
  width: DEFAULT_WIDTH,
755
+ height: DEFAULT_MENU_HEIGHT,
754
756
  marginTop: 8,
755
757
  zIndex: 10,
756
758
  borderRadius: MENU_RADIUS,
757
759
  padding: 12,
758
760
  gap: 0,
761
+ opacity: 1,
759
762
  backgroundColor: colors.grey700,
760
763
  ...Platform.select({
761
764
  web: {
@@ -0,0 +1,100 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react-vite";
3
+ import { Pressable, StyleSheet, Text, View } from "react-native";
4
+ import { colors, fonts } from "../../theme";
5
+ import { Modal } from "./Modal";
6
+
7
+ const meta = {
8
+ title: "Components/Modal",
9
+ component: Modal,
10
+ parameters: {
11
+ backgrounds: { default: "black" },
12
+ },
13
+ } satisfies Meta<typeof Modal>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ function ModalDemo() {
19
+ const [open, setOpen] = useState(true);
20
+
21
+ return (
22
+ <View style={styles.page}>
23
+ <Pressable onPress={() => setOpen(true)} style={styles.openButton}>
24
+ <Text style={styles.openLabel}>Open modal</Text>
25
+ </Pressable>
26
+
27
+ <Modal open={open} onClose={() => setOpen(false)}>
28
+ <View style={styles.panel}>
29
+ <Text style={styles.title}>Modal</Text>
30
+ <Text style={styles.body}>Backdrop uses #09090933 + blur(20px).</Text>
31
+ <Pressable onPress={() => setOpen(false)} style={styles.closeButton}>
32
+ <Text style={styles.closeLabel}>Close</Text>
33
+ </Pressable>
34
+ </View>
35
+ </Modal>
36
+ </View>
37
+ );
38
+ }
39
+
40
+ export const Default: Story = {
41
+ args: {
42
+ open: true,
43
+ onClose: () => undefined,
44
+ children: null,
45
+ },
46
+ render: () => <ModalDemo />,
47
+ };
48
+
49
+ const styles = StyleSheet.create({
50
+ page: {
51
+ minHeight: 360,
52
+ alignItems: "center",
53
+ justifyContent: "center",
54
+ padding: 24,
55
+ },
56
+ openButton: {
57
+ paddingHorizontal: 16,
58
+ paddingVertical: 10,
59
+ borderRadius: 40,
60
+ backgroundColor: colors.primary,
61
+ },
62
+ openLabel: {
63
+ fontFamily: fonts.sans,
64
+ fontSize: 14,
65
+ fontWeight: "600",
66
+ color: colors.white,
67
+ },
68
+ panel: {
69
+ width: 320,
70
+ gap: 16,
71
+ padding: 24,
72
+ borderRadius: 24,
73
+ backgroundColor: colors.dark,
74
+ },
75
+ title: {
76
+ fontFamily: fonts.sans,
77
+ fontSize: 18,
78
+ fontWeight: "700",
79
+ color: colors.white,
80
+ },
81
+ body: {
82
+ fontFamily: fonts.sans,
83
+ fontSize: 14,
84
+ lineHeight: 17.5,
85
+ color: colors.grey100,
86
+ },
87
+ closeButton: {
88
+ alignSelf: "flex-start",
89
+ paddingHorizontal: 16,
90
+ paddingVertical: 10,
91
+ borderRadius: 40,
92
+ backgroundColor: colors.grey700,
93
+ },
94
+ closeLabel: {
95
+ fontFamily: fonts.sans,
96
+ fontSize: 14,
97
+ fontWeight: "600",
98
+ color: colors.white,
99
+ },
100
+ });
@@ -0,0 +1,165 @@
1
+ import { forwardRef, useEffect, useMemo, useRef, type ComponentRef, type ReactNode } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import {
4
+ Modal as RNModal,
5
+ Platform,
6
+ Pressable,
7
+ StyleSheet,
8
+ View,
9
+ type StyleProp,
10
+ type ViewStyle,
11
+ } from "react-native";
12
+ import { mergeRefs } from "../../utils/mergeRefs";
13
+ import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
14
+
15
+ export interface ModalProps {
16
+ /** Controls visibility. */
17
+ open: boolean;
18
+ /** Called on backdrop press, Escape / Android back, or explicit close. */
19
+ onClose: () => void;
20
+ children: ReactNode;
21
+ /** Close when the dimmed backdrop is pressed. Defaults to `true`. */
22
+ closeOnBackdropPress?: boolean;
23
+ /** Accessible name for the backdrop control. Defaults to `"Close"`. */
24
+ backdropAccessibilityLabel?: string;
25
+ /** Style for the centered content wrapper around `children`. */
26
+ contentStyle?: StyleProp<ViewStyle>;
27
+ /** Web CSS class applied to the content wrapper. */
28
+ className?: string;
29
+ }
30
+
31
+ const BACKDROP_COLOR = "#09090933";
32
+ const WEB_Z_INDEX = 500;
33
+
34
+ /** Plain DOM backdrop — RN StyleSheet / Modal host often drops `backdrop-filter`. */
35
+ const webBackdropDomStyle = {
36
+ position: "absolute",
37
+ top: 0,
38
+ right: 0,
39
+ bottom: 0,
40
+ left: 0,
41
+ backgroundColor: BACKDROP_COLOR,
42
+ backdropFilter: "blur(20px)",
43
+ WebkitBackdropFilter: "blur(20px)",
44
+ border: "none",
45
+ padding: 0,
46
+ margin: 0,
47
+ cursor: "pointer",
48
+ } as const;
49
+
50
+ const webRootDomStyle = {
51
+ position: "fixed",
52
+ top: 0,
53
+ right: 0,
54
+ bottom: 0,
55
+ left: 0,
56
+ zIndex: WEB_Z_INDEX,
57
+ display: "flex",
58
+ alignItems: "center",
59
+ justifyContent: "center",
60
+ } as const;
61
+
62
+ export const Modal = forwardRef<ComponentRef<typeof View>, ModalProps>(function Modal(
63
+ {
64
+ open,
65
+ onClose,
66
+ children,
67
+ closeOnBackdropPress = true,
68
+ backdropAccessibilityLabel = "Close",
69
+ contentStyle,
70
+ className,
71
+ },
72
+ forwardedRef,
73
+ ) {
74
+ const contentRef = useRef<ComponentRef<typeof View>>(null);
75
+ const setContentRef = useMemo(() => mergeRefs(contentRef, forwardedRef), [forwardedRef]);
76
+ const resolvedClassName = className?.trim() || undefined;
77
+
78
+ useApplyWebClassName(contentRef, resolvedClassName, open && Platform.OS === "web");
79
+
80
+ useEffect(() => {
81
+ if (!open || Platform.OS !== "web") return undefined;
82
+
83
+ const previousOverflow = document.body.style.overflow;
84
+ document.body.style.overflow = "hidden";
85
+
86
+ const onKeyDown = (event: KeyboardEvent) => {
87
+ if (event.key === "Escape") onClose();
88
+ };
89
+ document.addEventListener("keydown", onKeyDown);
90
+
91
+ return () => {
92
+ document.body.style.overflow = previousOverflow;
93
+ document.removeEventListener("keydown", onKeyDown);
94
+ };
95
+ }, [open, onClose]);
96
+
97
+ const content = (
98
+ <View
99
+ ref={setContentRef}
100
+ style={[styles.content, contentStyle]}
101
+ onStartShouldSetResponder={() => true}
102
+ >
103
+ {children}
104
+ </View>
105
+ );
106
+
107
+ // Web: portal + real DOM backdrop so `backdrop-filter` blurs the page behind.
108
+ if (Platform.OS === "web") {
109
+ if (!open || typeof document === "undefined") return null;
110
+
111
+ return createPortal(
112
+ <div style={webRootDomStyle} role="presentation">
113
+ <button
114
+ type="button"
115
+ aria-label={backdropAccessibilityLabel}
116
+ disabled={!closeOnBackdropPress}
117
+ onClick={closeOnBackdropPress ? onClose : undefined}
118
+ style={webBackdropDomStyle}
119
+ />
120
+ <div style={{ position: "relative", zIndex: 1, maxWidth: "100%", maxHeight: "100%" }}>
121
+ {content}
122
+ </div>
123
+ </div>,
124
+ document.body,
125
+ );
126
+ }
127
+
128
+ return (
129
+ <RNModal
130
+ visible={open}
131
+ transparent
132
+ animationType="fade"
133
+ onRequestClose={onClose}
134
+ statusBarTranslucent
135
+ >
136
+ <View style={styles.root} accessibilityViewIsModal>
137
+ <Pressable
138
+ accessibilityRole="button"
139
+ accessibilityLabel={backdropAccessibilityLabel}
140
+ disabled={!closeOnBackdropPress}
141
+ onPress={closeOnBackdropPress ? onClose : undefined}
142
+ style={styles.backdrop}
143
+ />
144
+ {content}
145
+ </View>
146
+ </RNModal>
147
+ );
148
+ });
149
+
150
+ const styles = StyleSheet.create({
151
+ root: {
152
+ flex: 1,
153
+ alignItems: "center",
154
+ justifyContent: "center",
155
+ },
156
+ backdrop: {
157
+ ...StyleSheet.absoluteFillObject,
158
+ backgroundColor: BACKDROP_COLOR,
159
+ },
160
+ content: {
161
+ zIndex: 1,
162
+ maxWidth: "100%",
163
+ maxHeight: "100%",
164
+ },
165
+ });
@@ -0,0 +1,2 @@
1
+ export { Modal } from "./Modal";
2
+ export type { ModalProps } from "./Modal";
@@ -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";
@@ -61,6 +61,10 @@ export interface SidebarProps extends Omit<ViewProps, "style" | "children"> {
61
61
  onCollapsedChange?: (collapsed: boolean) => void;
62
62
  /** When false, hides the expand/collapse control. Defaults to `true`. */
63
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;
64
68
  style?: StyleProp<ViewStyle>;
65
69
  className?: string;
66
70
  }
@@ -89,6 +93,8 @@ export const Sidebar = forwardRef<ComponentRef<typeof View>, SidebarProps>(funct
89
93
  defaultCollapsed = false,
90
94
  onCollapsedChange,
91
95
  showCollapseToggle = true,
96
+ onClose,
97
+ closeAccessibilityLabel = "Close",
92
98
  style,
93
99
  className,
94
100
  ...rest
@@ -143,7 +149,7 @@ export const Sidebar = forwardRef<ComponentRef<typeof View>, SidebarProps>(funct
143
149
  style={[
144
150
  styles.header,
145
151
  collapsed ? styles.headerCollapsed : styles.headerExpanded,
146
- !showCollapseToggle ? styles.headerNoToggle : null,
152
+ !showCollapseToggle && !onClose ? styles.headerNoToggle : null,
147
153
  ]}
148
154
  >
149
155
  <Animated.View
@@ -166,7 +172,17 @@ export const Sidebar = forwardRef<ComponentRef<typeof View>, SidebarProps>(funct
166
172
  />
167
173
  </Animated.View>
168
174
 
169
- {showCollapseToggle ? (
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 ? (
170
186
  <Pressable
171
187
  accessibilityRole="button"
172
188
  accessibilityLabel={collapsed ? expandAccessibilityLabel : collapseAccessibilityLabel}
@@ -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";