@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.
- package/dist/index.cjs +29640 -471
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -4
- package/dist/index.d.ts +101 -4
- package/dist/index.js +29568 -407
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/AccordionItem/AccordionItem.stories.tsx +54 -0
- package/src/components/AccordionItem/AccordionItem.tsx +220 -0
- package/src/components/AccordionItem/index.ts +2 -0
- package/src/components/AccountHeader/AccountHeader.tsx +34 -29
- package/src/components/Button/Button.stories.tsx +10 -1
- package/src/components/Button/Button.tsx +32 -3
- package/src/components/ContactInfoCard/ContactInfoCard.stories.tsx +30 -0
- package/src/components/ContactInfoCard/ContactInfoCard.tsx +141 -0
- package/src/components/ContactInfoCard/index.ts +2 -0
- package/src/components/DatePicker/DatePicker.tsx +4 -1
- package/src/components/Modal/Modal.stories.tsx +100 -0
- package/src/components/Modal/Modal.tsx +165 -0
- package/src/components/Modal/index.ts +2 -0
- package/src/components/Sidebar/Sidebar.tsx +19 -3
- package/src/components/ToggleRow/ToggleRow.stories.tsx +46 -0
- package/src/components/ToggleRow/ToggleRow.tsx +121 -0
- package/src/components/ToggleRow/index.ts +2 -0
- package/src/components/VerificationCodeInput/VerificationCodeInput.tsx +80 -60
- package/src/components/index.ts +12 -0
- package/src/icons/CameraFilled/CameraFilledIcon.tsx +15 -0
- package/src/icons/CameraFilled/CameraFilledIcon.web.tsx +21 -0
- package/src/icons/CameraFilled/cameraFilledPath.ts +2 -0
- package/src/icons/CameraFilled/index.ts +1 -0
- package/src/icons/Headset/HeadsetIcon.tsx +26 -0
- package/src/icons/Headset/HeadsetIcon.web.tsx +25 -0
- package/src/icons/Headset/headsetPath.ts +6 -0
- package/src/icons/Headset/index.ts +1 -0
- package/src/icons/Hide/HideIcon.tsx +21 -0
- package/src/icons/Hide/HideIcon.web.tsx +27 -0
- package/src/icons/Hide/hidePath.ts +3 -0
- package/src/icons/Hide/index.ts +1 -0
- package/src/icons/MenuLines/MenuLinesIcon.tsx +20 -0
- package/src/icons/MenuLines/MenuLinesIcon.web.tsx +26 -0
- package/src/icons/MenuLines/index.ts +1 -0
- package/src/icons/MenuLines/menuLinesPath.ts +3 -0
- package/src/icons/index.ts +4 -0
- package/src/icons/registry.ts +11 -0
- 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
|
+
});
|
|
@@ -119,7 +119,8 @@ export type DatePickerRangeProps = DatePickerBaseProps & {
|
|
|
119
119
|
|
|
120
120
|
export type DatePickerProps = DatePickerSingleProps | DatePickerRangeProps;
|
|
121
121
|
|
|
122
|
-
const DEFAULT_WIDTH =
|
|
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
|
+
});
|
|
@@ -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
|
-
{
|
|
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
|
+
});
|