@ecohouse/ui 0.1.33 → 0.1.35
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 +30078 -656
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -7
- package/dist/index.d.ts +114 -7
- package/dist/index.js +30007 -593
- 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 +223 -0
- package/src/components/AccordionItem/index.ts +2 -0
- package/src/components/AccountHeader/AccountHeader.tsx +50 -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 +51 -15
- package/src/components/Input/Input.tsx +54 -21
- package/src/components/LanguageSwitcher/LanguageSwitcher.stories.tsx +11 -0
- package/src/components/LanguageSwitcher/LanguageSwitcher.tsx +103 -19
- package/src/components/LanguageSwitcher/index.ts +5 -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/Select/Select.tsx +90 -5
- package/src/components/Sidebar/Sidebar.tsx +19 -3
- package/src/components/Textarea/Textarea.tsx +29 -4
- 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 +17 -1
- 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/Globe/GlobeIcon.tsx +2 -2
- package/src/icons/Globe/GlobeIcon.web.tsx +2 -2
- package/src/icons/Globe/globePath.ts +2 -2
- 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/Key/keyPath.ts +1 -1
- 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 +13 -0
- package/src/theme/typography.ts +3 -3
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { View } from "react-native";
|
|
4
|
+
import { AccordionItem } from "./AccordionItem";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/AccordionItem",
|
|
8
|
+
component: AccordionItem,
|
|
9
|
+
parameters: {
|
|
10
|
+
backgrounds: { default: "black" },
|
|
11
|
+
},
|
|
12
|
+
} satisfies Meta<typeof AccordionItem>;
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
type Story = StoryObj<typeof meta>;
|
|
16
|
+
|
|
17
|
+
function FaqListDemo() {
|
|
18
|
+
const [openId, setOpenId] = useState<string | null>("digitalKey");
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<View style={{ width: 560, gap: 20 }}>
|
|
22
|
+
<AccordionItem
|
|
23
|
+
title="Ինչպե՞ս է աշխատում թվային բանալին (QR կոդը)"
|
|
24
|
+
open={openId === "digitalKey"}
|
|
25
|
+
onOpenChange={(next) => setOpenId(next ? "digitalKey" : null)}
|
|
26
|
+
>
|
|
27
|
+
Ամրագրումը հաստատվելուց հետո Ձեր հավելվածում կձևավորվի եզակի QR կոդ։
|
|
28
|
+
</AccordionItem>
|
|
29
|
+
<AccordionItem
|
|
30
|
+
title="Ինչպե՞ս է իրականացվում մուտքը քոթեջ"
|
|
31
|
+
open={openId === "entry"}
|
|
32
|
+
onOpenChange={(next) => setOpenId(next ? "entry" : null)}
|
|
33
|
+
>
|
|
34
|
+
Մոտեցեք դռան սկաներին և ցույց տվեք QR կոդը։
|
|
35
|
+
</AccordionItem>
|
|
36
|
+
</View>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Default: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
title: "Ինչպե՞ս է աշխատում թվային բանալին (QR կոդը)",
|
|
43
|
+
children: "Ամրագրումը հաստատվելուց հետո Ձեր հավելվածում կձևավորվի եզակի QR կոդ։",
|
|
44
|
+
defaultOpen: true,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const ExclusiveList: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
title: "Ինչպե՞ս է աշխատում թվային բանալին (QR կոդը)",
|
|
51
|
+
children: "Ամրագրումը հաստատվելուց հետո Ձեր հավելվածում կձևավորվի եզակի QR կոդ։",
|
|
52
|
+
},
|
|
53
|
+
render: () => <FaqListDemo />,
|
|
54
|
+
};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type ComponentRef,
|
|
9
|
+
} from "react";
|
|
10
|
+
import {
|
|
11
|
+
Animated,
|
|
12
|
+
Easing,
|
|
13
|
+
Platform,
|
|
14
|
+
Pressable,
|
|
15
|
+
StyleSheet,
|
|
16
|
+
Text,
|
|
17
|
+
useWindowDimensions,
|
|
18
|
+
View,
|
|
19
|
+
type LayoutChangeEvent,
|
|
20
|
+
type StyleProp,
|
|
21
|
+
type ViewProps,
|
|
22
|
+
type ViewStyle,
|
|
23
|
+
} from "react-native";
|
|
24
|
+
import { ChevronDownIcon } from "../../icons";
|
|
25
|
+
import { colors, fonts } from "../../theme";
|
|
26
|
+
import { mergeRefs } from "../../utils/mergeRefs";
|
|
27
|
+
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
28
|
+
|
|
29
|
+
export interface AccordionItemProps extends Omit<ViewProps, "style" | "children"> {
|
|
30
|
+
title: string;
|
|
31
|
+
/** Expanded body text. */
|
|
32
|
+
children: string;
|
|
33
|
+
open?: boolean;
|
|
34
|
+
defaultOpen?: boolean;
|
|
35
|
+
onOpenChange?: (open: boolean) => void;
|
|
36
|
+
style?: StyleProp<ViewStyle>;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DESKTOP_MIN_WIDTH = 1024;
|
|
41
|
+
const CHEVRON_SIZE = 24;
|
|
42
|
+
const ANIMATION_DURATION = 280;
|
|
43
|
+
const BODY_GAP = 16;
|
|
44
|
+
const PADDING_X_MOBILE = 12;
|
|
45
|
+
const PADDING_X_DESKTOP = 24;
|
|
46
|
+
const RADIUS_MOBILE = 16;
|
|
47
|
+
const RADIUS_DESKTOP = 24;
|
|
48
|
+
const BODY_SIZE_MOBILE = 12;
|
|
49
|
+
const BODY_SIZE_DESKTOP = 14;
|
|
50
|
+
const BODY_LINE_MOBILE = 16;
|
|
51
|
+
const BODY_LINE_DESKTOP = 18;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* FAQ / accordion row — grey700 card.
|
|
55
|
+
* Mobile: radius 16, padding 16/12, body 12px.
|
|
56
|
+
* Desktop: radius 24, padding 16/24, body 14px.
|
|
57
|
+
*/
|
|
58
|
+
export const AccordionItem = forwardRef<ComponentRef<typeof View>, AccordionItemProps>(
|
|
59
|
+
function AccordionItem(
|
|
60
|
+
{
|
|
61
|
+
title,
|
|
62
|
+
children,
|
|
63
|
+
open: openProp,
|
|
64
|
+
defaultOpen = false,
|
|
65
|
+
onOpenChange,
|
|
66
|
+
style,
|
|
67
|
+
className,
|
|
68
|
+
accessibilityLabel,
|
|
69
|
+
...rest
|
|
70
|
+
},
|
|
71
|
+
forwardedRef,
|
|
72
|
+
) {
|
|
73
|
+
const containerRef = useRef<ComponentRef<typeof View>>(null);
|
|
74
|
+
const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
|
|
75
|
+
const isControlled = openProp !== undefined;
|
|
76
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
77
|
+
const open = isControlled ? Boolean(openProp) : uncontrolledOpen;
|
|
78
|
+
const [contentHeight, setContentHeight] = useState(0);
|
|
79
|
+
const progress = useRef(new Animated.Value(defaultOpen || openProp ? 1 : 0)).current;
|
|
80
|
+
const { width: viewportWidth } = useWindowDimensions();
|
|
81
|
+
const isDesktop = viewportWidth >= DESKTOP_MIN_WIDTH;
|
|
82
|
+
const paddingX = isDesktop ? PADDING_X_DESKTOP : PADDING_X_MOBILE;
|
|
83
|
+
const bodyFontSize = isDesktop ? BODY_SIZE_DESKTOP : BODY_SIZE_MOBILE;
|
|
84
|
+
const bodyLineHeight = isDesktop ? BODY_LINE_DESKTOP : BODY_LINE_MOBILE;
|
|
85
|
+
|
|
86
|
+
useApplyWebClassName(containerRef, className);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const animation = Animated.timing(progress, {
|
|
90
|
+
toValue: open ? 1 : 0,
|
|
91
|
+
duration: ANIMATION_DURATION,
|
|
92
|
+
easing: Easing.out(Easing.cubic),
|
|
93
|
+
useNativeDriver: false,
|
|
94
|
+
});
|
|
95
|
+
animation.start();
|
|
96
|
+
return () => animation.stop();
|
|
97
|
+
}, [open, progress]);
|
|
98
|
+
|
|
99
|
+
const toggle = useCallback(() => {
|
|
100
|
+
const next = !open;
|
|
101
|
+
if (!isControlled) setUncontrolledOpen(next);
|
|
102
|
+
onOpenChange?.(next);
|
|
103
|
+
}, [isControlled, onOpenChange, open]);
|
|
104
|
+
|
|
105
|
+
const onBodyLayout = useCallback((event: LayoutChangeEvent) => {
|
|
106
|
+
const nextHeight = event.nativeEvent.layout.height;
|
|
107
|
+
setContentHeight((current) => (Math.abs(current - nextHeight) < 0.5 ? current : nextHeight));
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
const bodyHeight = progress.interpolate({
|
|
111
|
+
inputRange: [0, 1],
|
|
112
|
+
outputRange: [0, Math.max(contentHeight, 0)],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const chevronRotate = progress.interpolate({
|
|
116
|
+
inputRange: [0, 1],
|
|
117
|
+
outputRange: ["0deg", "180deg"],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const bodyTextStyle = [styles.body, { fontSize: bodyFontSize, lineHeight: bodyLineHeight }];
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Pressable
|
|
124
|
+
{...rest}
|
|
125
|
+
ref={setContainerRef}
|
|
126
|
+
accessibilityRole="button"
|
|
127
|
+
accessibilityState={{ expanded: open }}
|
|
128
|
+
accessibilityLabel={accessibilityLabel ?? title}
|
|
129
|
+
onPress={toggle}
|
|
130
|
+
style={[
|
|
131
|
+
styles.root,
|
|
132
|
+
{
|
|
133
|
+
borderRadius: isDesktop ? RADIUS_DESKTOP : RADIUS_MOBILE,
|
|
134
|
+
paddingRight: paddingX,
|
|
135
|
+
paddingLeft: paddingX,
|
|
136
|
+
},
|
|
137
|
+
Platform.OS === "web" ? styles.rootWeb : null,
|
|
138
|
+
style,
|
|
139
|
+
]}
|
|
140
|
+
>
|
|
141
|
+
<View style={styles.header}>
|
|
142
|
+
<Text style={styles.title}>{title}</Text>
|
|
143
|
+
<Animated.View style={[styles.chevron, { transform: [{ rotate: chevronRotate }] }]}>
|
|
144
|
+
<ChevronDownIcon
|
|
145
|
+
size={CHEVRON_SIZE}
|
|
146
|
+
color={open ? colors.primary : colors.grey400}
|
|
147
|
+
strokeWidth={2}
|
|
148
|
+
/>
|
|
149
|
+
</Animated.View>
|
|
150
|
+
</View>
|
|
151
|
+
|
|
152
|
+
<View
|
|
153
|
+
style={[styles.measureHidden, { left: paddingX, right: paddingX }]}
|
|
154
|
+
pointerEvents="none"
|
|
155
|
+
onLayout={onBodyLayout}
|
|
156
|
+
>
|
|
157
|
+
<Text style={bodyTextStyle}>{children}</Text>
|
|
158
|
+
</View>
|
|
159
|
+
|
|
160
|
+
<Animated.View style={[styles.bodyClip, { height: bodyHeight }]} pointerEvents="none">
|
|
161
|
+
<View style={styles.bodyInner}>
|
|
162
|
+
<Text style={bodyTextStyle}>{children}</Text>
|
|
163
|
+
</View>
|
|
164
|
+
</Animated.View>
|
|
165
|
+
</Pressable>
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const styles = StyleSheet.create({
|
|
171
|
+
root: {
|
|
172
|
+
width: "100%",
|
|
173
|
+
paddingTop: 16,
|
|
174
|
+
paddingBottom: 16,
|
|
175
|
+
backgroundColor: colors.grey700,
|
|
176
|
+
opacity: 1,
|
|
177
|
+
overflow: "hidden",
|
|
178
|
+
},
|
|
179
|
+
rootWeb: {
|
|
180
|
+
cursor: "pointer" as unknown as ViewStyle["cursor"],
|
|
181
|
+
},
|
|
182
|
+
header: {
|
|
183
|
+
flexDirection: "row",
|
|
184
|
+
alignItems: "center",
|
|
185
|
+
gap: 16,
|
|
186
|
+
},
|
|
187
|
+
title: {
|
|
188
|
+
flex: 1,
|
|
189
|
+
minWidth: 0,
|
|
190
|
+
fontFamily: fonts.sans,
|
|
191
|
+
fontSize: 14,
|
|
192
|
+
fontWeight: "500",
|
|
193
|
+
lineHeight: 14,
|
|
194
|
+
letterSpacing: 0.42,
|
|
195
|
+
color: colors.white,
|
|
196
|
+
},
|
|
197
|
+
chevron: {
|
|
198
|
+
width: CHEVRON_SIZE,
|
|
199
|
+
height: CHEVRON_SIZE,
|
|
200
|
+
flexShrink: 0,
|
|
201
|
+
alignItems: "center",
|
|
202
|
+
justifyContent: "center",
|
|
203
|
+
},
|
|
204
|
+
measureHidden: {
|
|
205
|
+
position: "absolute",
|
|
206
|
+
top: 0,
|
|
207
|
+
opacity: 0,
|
|
208
|
+
paddingTop: BODY_GAP,
|
|
209
|
+
zIndex: -1,
|
|
210
|
+
},
|
|
211
|
+
bodyClip: {
|
|
212
|
+
overflow: "hidden",
|
|
213
|
+
},
|
|
214
|
+
bodyInner: {
|
|
215
|
+
paddingTop: BODY_GAP,
|
|
216
|
+
},
|
|
217
|
+
body: {
|
|
218
|
+
fontFamily: fonts.sans,
|
|
219
|
+
fontWeight: "400",
|
|
220
|
+
letterSpacing: 0.42,
|
|
221
|
+
color: colors.lightGrey,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { forwardRef, useMemo, useRef, type ComponentRef, type ReactNode } from "react";
|
|
2
2
|
import {
|
|
3
|
+
Platform,
|
|
3
4
|
Pressable,
|
|
4
5
|
StyleSheet,
|
|
5
6
|
Text,
|
|
6
7
|
View,
|
|
7
8
|
type StyleProp,
|
|
9
|
+
type TextStyle,
|
|
8
10
|
type ViewProps,
|
|
9
11
|
type ViewStyle,
|
|
10
12
|
} from "react-native";
|
|
11
|
-
import { BellIcon, ChevronLeftIcon,
|
|
13
|
+
import { BellIcon, ChevronLeftIcon, MenuLinesIcon } from "../../icons";
|
|
12
14
|
import { colors, fonts } from "../../theme";
|
|
13
15
|
import { mergeRefs } from "../../utils/mergeRefs";
|
|
14
|
-
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
15
16
|
|
|
16
17
|
export interface AccountHeaderProps extends Omit<ViewProps, "style" | "children"> {
|
|
17
18
|
title: string;
|
|
@@ -23,6 +24,8 @@ export interface AccountHeaderProps extends Omit<ViewProps, "style" | "children"
|
|
|
23
24
|
/** When set, shows the 28×28 menu control (mobile account shell). Ignored if `onBackPress` is set. */
|
|
24
25
|
onMenuPress?: () => void;
|
|
25
26
|
onNotificationsPress?: () => void;
|
|
27
|
+
/** Optional control rendered to the left of notifications (e.g. language switcher). */
|
|
28
|
+
languageSlot?: ReactNode;
|
|
26
29
|
/** Optional status chip (or other accessory) rendered after the title with a 12px gap. */
|
|
27
30
|
badge?: ReactNode;
|
|
28
31
|
/** Accessible name for the back control. Defaults to `"Back"`. */
|
|
@@ -31,6 +34,13 @@ export interface AccountHeaderProps extends Omit<ViewProps, "style" | "children"
|
|
|
31
34
|
menuAccessibilityLabel?: string;
|
|
32
35
|
/** Accessible name for notifications. Defaults to `"Notifications"`. */
|
|
33
36
|
notificationsAccessibilityLabel?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Web CSS class for the menu control (hide on desktop via `#account-header-menu` /
|
|
39
|
+
* `.account-header-menu` while keeping it mounted to avoid a mobile refresh flash).
|
|
40
|
+
*/
|
|
41
|
+
menuClassName?: string;
|
|
42
|
+
/** Optional title style override (e.g. mobile 14px from the web app). */
|
|
43
|
+
titleStyle?: StyleProp<TextStyle>;
|
|
34
44
|
style?: StyleProp<ViewStyle>;
|
|
35
45
|
className?: string;
|
|
36
46
|
}
|
|
@@ -44,6 +54,12 @@ const TITLE_BADGE_GAP = 12;
|
|
|
44
54
|
const MENU_TITLE_GAP = 16;
|
|
45
55
|
const RIGHT_GAP = 16;
|
|
46
56
|
|
|
57
|
+
/** RN-web applies `className` on the first paint — avoid post-hydrate class injection. */
|
|
58
|
+
function webClassName(className?: string): { className?: string } {
|
|
59
|
+
if (Platform.OS !== "web" || !className?.trim()) return {};
|
|
60
|
+
return { className: className.trim() };
|
|
61
|
+
}
|
|
62
|
+
|
|
47
63
|
export const AccountHeader = forwardRef<ComponentRef<typeof View>, AccountHeaderProps>(
|
|
48
64
|
function AccountHeader(
|
|
49
65
|
{
|
|
@@ -51,10 +67,13 @@ export const AccountHeader = forwardRef<ComponentRef<typeof View>, AccountHeader
|
|
|
51
67
|
onBackPress,
|
|
52
68
|
onMenuPress,
|
|
53
69
|
onNotificationsPress,
|
|
70
|
+
languageSlot,
|
|
54
71
|
badge,
|
|
55
72
|
backAccessibilityLabel = "Back",
|
|
56
73
|
menuAccessibilityLabel = "Open menu",
|
|
57
74
|
notificationsAccessibilityLabel = "Notifications",
|
|
75
|
+
menuClassName,
|
|
76
|
+
titleStyle,
|
|
58
77
|
style,
|
|
59
78
|
className,
|
|
60
79
|
...rest
|
|
@@ -63,19 +82,16 @@ export const AccountHeader = forwardRef<ComponentRef<typeof View>, AccountHeader
|
|
|
63
82
|
) {
|
|
64
83
|
const containerRef = useRef<ComponentRef<typeof View>>(null);
|
|
65
84
|
const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
|
|
66
|
-
const resolvedClassName = className?.trim() || undefined;
|
|
67
85
|
const showBack = onBackPress != null;
|
|
68
86
|
const showMenu = !showBack && onMenuPress != null;
|
|
69
|
-
const isCompact = showMenu;
|
|
70
|
-
|
|
71
|
-
useApplyWebClassName(containerRef, resolvedClassName);
|
|
72
87
|
|
|
73
88
|
return (
|
|
74
89
|
<View
|
|
75
90
|
{...rest}
|
|
91
|
+
{...webClassName(className)}
|
|
76
92
|
ref={setContainerRef}
|
|
77
93
|
accessibilityRole="header"
|
|
78
|
-
style={[styles.base,
|
|
94
|
+
style={[styles.base, style]}
|
|
79
95
|
>
|
|
80
96
|
<View
|
|
81
97
|
style={[
|
|
@@ -102,23 +118,24 @@ export const AccountHeader = forwardRef<ComponentRef<typeof View>, AccountHeader
|
|
|
102
118
|
|
|
103
119
|
{showMenu ? (
|
|
104
120
|
<Pressable
|
|
121
|
+
{...webClassName(menuClassName)}
|
|
122
|
+
id="account-header-menu"
|
|
123
|
+
nativeID="account-header-menu"
|
|
105
124
|
accessibilityRole="button"
|
|
106
125
|
accessibilityLabel={menuAccessibilityLabel}
|
|
107
126
|
hitSlop={8}
|
|
108
127
|
onPress={onMenuPress}
|
|
109
128
|
style={styles.menuButton}
|
|
110
129
|
>
|
|
111
|
-
<
|
|
130
|
+
<MenuLinesIcon color={colors.white} size={MENU_ICON_SIZE} strokeWidth={2} />
|
|
112
131
|
</Pressable>
|
|
113
132
|
) : null}
|
|
114
133
|
|
|
115
134
|
<View style={styles.titleRow}>
|
|
116
135
|
<Text
|
|
136
|
+
{...webClassName("account-header-title")}
|
|
117
137
|
numberOfLines={1}
|
|
118
|
-
style={[
|
|
119
|
-
styles.title,
|
|
120
|
-
showBack || !isCompact ? styles.titleDesktop : styles.titleCompact,
|
|
121
|
-
]}
|
|
138
|
+
style={[styles.title, titleStyle]}
|
|
122
139
|
>
|
|
123
140
|
{title}
|
|
124
141
|
</Text>
|
|
@@ -127,6 +144,7 @@ export const AccountHeader = forwardRef<ComponentRef<typeof View>, AccountHeader
|
|
|
127
144
|
</View>
|
|
128
145
|
|
|
129
146
|
<View style={styles.end}>
|
|
147
|
+
{languageSlot != null ? <View style={styles.languageSlot}>{languageSlot}</View> : null}
|
|
130
148
|
<Pressable
|
|
131
149
|
accessibilityRole="button"
|
|
132
150
|
accessibilityLabel={notificationsAccessibilityLabel}
|
|
@@ -141,6 +159,7 @@ export const AccountHeader = forwardRef<ComponentRef<typeof View>, AccountHeader
|
|
|
141
159
|
},
|
|
142
160
|
);
|
|
143
161
|
|
|
162
|
+
/** Desktop defaults — mobile padding/title size are overridden from the web app. */
|
|
144
163
|
const styles = StyleSheet.create({
|
|
145
164
|
base: {
|
|
146
165
|
height: HEADER_HEIGHT,
|
|
@@ -148,18 +167,16 @@ const styles = StyleSheet.create({
|
|
|
148
167
|
flexDirection: "row",
|
|
149
168
|
alignItems: "center",
|
|
150
169
|
justifyContent: "space-between",
|
|
151
|
-
|
|
152
|
-
borderBottomWidth: 1,
|
|
153
|
-
borderBottomColor: colors.grey700,
|
|
154
|
-
},
|
|
155
|
-
desktop: {
|
|
170
|
+
opacity: 1,
|
|
156
171
|
paddingTop: 16,
|
|
172
|
+
paddingRight: 24,
|
|
157
173
|
paddingBottom: 16,
|
|
158
174
|
paddingLeft: 24,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
175
|
+
backgroundColor: colors.dark,
|
|
176
|
+
borderBottomWidth: 1,
|
|
177
|
+
borderBottomColor: colors.grey700,
|
|
178
|
+
zIndex: 40,
|
|
179
|
+
position: "relative",
|
|
163
180
|
},
|
|
164
181
|
start: {
|
|
165
182
|
flex: 1,
|
|
@@ -183,6 +200,16 @@ const styles = StyleSheet.create({
|
|
|
183
200
|
end: {
|
|
184
201
|
flexShrink: 0,
|
|
185
202
|
marginLeft: RIGHT_GAP,
|
|
203
|
+
flexDirection: "row",
|
|
204
|
+
alignItems: "center",
|
|
205
|
+
gap: 8,
|
|
206
|
+
zIndex: 50,
|
|
207
|
+
position: "relative",
|
|
208
|
+
},
|
|
209
|
+
languageSlot: {
|
|
210
|
+
flexShrink: 0,
|
|
211
|
+
zIndex: 50,
|
|
212
|
+
position: "relative",
|
|
186
213
|
},
|
|
187
214
|
iconButton: {
|
|
188
215
|
width: ICON_BUTTON_SIZE,
|
|
@@ -207,16 +234,10 @@ const styles = StyleSheet.create({
|
|
|
207
234
|
flexShrink: 1,
|
|
208
235
|
fontFamily: fonts.sans,
|
|
209
236
|
fontWeight: "600",
|
|
210
|
-
letterSpacing: 0,
|
|
211
|
-
color: colors.white,
|
|
212
|
-
},
|
|
213
|
-
titleDesktop: {
|
|
214
237
|
fontSize: 16,
|
|
215
238
|
lineHeight: 22,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
fontSize: 14,
|
|
219
|
-
lineHeight: 20,
|
|
239
|
+
letterSpacing: 0,
|
|
240
|
+
color: colors.white,
|
|
220
241
|
},
|
|
221
242
|
badgeSlot: {
|
|
222
243
|
flexShrink: 0,
|
|
@@ -18,7 +18,7 @@ const meta = {
|
|
|
18
18
|
onPress: { action: "pressed" },
|
|
19
19
|
variant: {
|
|
20
20
|
control: "select",
|
|
21
|
-
options: ["primary", "secondary"],
|
|
21
|
+
options: ["primary", "secondary", "danger"],
|
|
22
22
|
},
|
|
23
23
|
size: {
|
|
24
24
|
control: "select",
|
|
@@ -63,16 +63,25 @@ export const WithCustomWidth: Story = {
|
|
|
63
63
|
},
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
export const Danger: Story = {
|
|
67
|
+
args: { variant: "danger", showIcon: false, title: "Delete account" },
|
|
68
|
+
parameters: {
|
|
69
|
+
backgrounds: { default: "black" },
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
66
73
|
export const AllVariants: Story = {
|
|
67
74
|
render: () => (
|
|
68
75
|
<View style={storyStyles.grid}>
|
|
69
76
|
<View style={storyStyles.column}>
|
|
70
77
|
<Button title="Button" variant="primary" showIcon onPress={fn()} />
|
|
71
78
|
<Button title="Button" variant="secondary" showIcon onPress={fn()} />
|
|
79
|
+
<Button title="Delete account" variant="danger" onPress={fn()} />
|
|
72
80
|
</View>
|
|
73
81
|
<View style={storyStyles.column}>
|
|
74
82
|
<Button title="Button" variant="primary" showIcon disabled onPress={fn()} />
|
|
75
83
|
<Button title="Button" variant="secondary" showIcon disabled onPress={fn()} />
|
|
84
|
+
<Button title="Delete account" variant="danger" disabled onPress={fn()} />
|
|
76
85
|
</View>
|
|
77
86
|
</View>
|
|
78
87
|
),
|
|
@@ -12,11 +12,11 @@ import {
|
|
|
12
12
|
type ViewStyle,
|
|
13
13
|
} from "react-native";
|
|
14
14
|
import { ArrowRightIcon } from "../../icons";
|
|
15
|
-
import { buttonTypography, colors } from "../../theme";
|
|
15
|
+
import { buttonTypography, colors, fonts } from "../../theme";
|
|
16
16
|
import { mergeRefs } from "../../utils/mergeRefs";
|
|
17
17
|
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
18
18
|
|
|
19
|
-
export type ButtonVariant = "primary" | "secondary";
|
|
19
|
+
export type ButtonVariant = "primary" | "secondary" | "danger";
|
|
20
20
|
export type ButtonSize = "lg" | "sm";
|
|
21
21
|
|
|
22
22
|
export interface ButtonProps extends Omit<
|
|
@@ -46,6 +46,8 @@ type VariantStyleSet = {
|
|
|
46
46
|
iconColor: string;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
const DANGER_BG = "#FFFFFF03";
|
|
50
|
+
|
|
49
51
|
const variantConfig: Record<ButtonVariant, VariantStyleSet> = {
|
|
50
52
|
primary: {
|
|
51
53
|
backgroundColor: colors.primary,
|
|
@@ -59,6 +61,12 @@ const variantConfig: Record<ButtonVariant, VariantStyleSet> = {
|
|
|
59
61
|
iconBadgeBackgroundColor: colors.grey600,
|
|
60
62
|
iconColor: colors.white,
|
|
61
63
|
},
|
|
64
|
+
danger: {
|
|
65
|
+
backgroundColor: DANGER_BG,
|
|
66
|
+
textColor: colors.red,
|
|
67
|
+
iconBadgeBackgroundColor: DANGER_BG,
|
|
68
|
+
iconColor: colors.red,
|
|
69
|
+
},
|
|
62
70
|
};
|
|
63
71
|
|
|
64
72
|
const sizeConfig = {
|
|
@@ -88,6 +96,26 @@ const sizeConfig = {
|
|
|
88
96
|
},
|
|
89
97
|
} as const satisfies Record<ButtonSize, Record<string, unknown>>;
|
|
90
98
|
|
|
99
|
+
/** Delete / destructive action — fixed 33px height, 8px radius, red label. */
|
|
100
|
+
const dangerMetrics = {
|
|
101
|
+
minHeight: 33,
|
|
102
|
+
borderRadius: 8,
|
|
103
|
+
paddingVertical: 0,
|
|
104
|
+
paddingLeft: 12,
|
|
105
|
+
paddingRightWithIcon: 4,
|
|
106
|
+
paddingRight: 12,
|
|
107
|
+
gap: 10,
|
|
108
|
+
text: {
|
|
109
|
+
fontFamily: fonts.sans,
|
|
110
|
+
fontSize: 12,
|
|
111
|
+
fontWeight: "500" as const,
|
|
112
|
+
lineHeight: 12,
|
|
113
|
+
letterSpacing: 0.36,
|
|
114
|
+
},
|
|
115
|
+
iconContainerSize: 20,
|
|
116
|
+
iconSize: 12,
|
|
117
|
+
} as const;
|
|
118
|
+
|
|
91
119
|
export const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>(
|
|
92
120
|
function Button(
|
|
93
121
|
{
|
|
@@ -112,7 +140,7 @@ export const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonPr
|
|
|
112
140
|
const containerRef = useRef<ComponentRef<typeof TouchableOpacity>>(null);
|
|
113
141
|
const textRef = useRef<ComponentRef<typeof Text>>(null);
|
|
114
142
|
const preset = variantConfig[variant];
|
|
115
|
-
const metrics = sizeConfig[size];
|
|
143
|
+
const metrics = variant === "danger" ? dangerMetrics : sizeConfig[size];
|
|
116
144
|
|
|
117
145
|
const label = typeof children !== "undefined" ? children : title;
|
|
118
146
|
const hasCustomChildren = typeof children !== "undefined";
|
|
@@ -135,6 +163,7 @@ export const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonPr
|
|
|
135
163
|
styles.base,
|
|
136
164
|
{
|
|
137
165
|
minHeight: metrics.minHeight,
|
|
166
|
+
height: variant === "danger" ? 33 : undefined,
|
|
138
167
|
borderRadius: metrics.borderRadius,
|
|
139
168
|
backgroundColor: preset.backgroundColor,
|
|
140
169
|
paddingTop: metrics.paddingVertical,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { HeadsetIcon, MailIcon, PhoneIcon } from "../../icons";
|
|
4
|
+
import { ContactInfoCard } from "./ContactInfoCard";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/ContactInfoCard",
|
|
8
|
+
component: ContactInfoCard,
|
|
9
|
+
parameters: {
|
|
10
|
+
backgrounds: { default: "black" },
|
|
11
|
+
},
|
|
12
|
+
} satisfies Meta<typeof ContactInfoCard>;
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
type Story = StoryObj<typeof meta>;
|
|
16
|
+
|
|
17
|
+
export const Default: Story = {
|
|
18
|
+
args: {
|
|
19
|
+
title: "Էլ․ հասցե",
|
|
20
|
+
value: "info@cortel.am",
|
|
21
|
+
icon: MailIcon,
|
|
22
|
+
},
|
|
23
|
+
render: (args) => (
|
|
24
|
+
<View style={{ width: 411, gap: 24 }}>
|
|
25
|
+
<ContactInfoCard {...args} />
|
|
26
|
+
<ContactInfoCard title="Հեռախոսահամար" value="+374 99 00-00-00" icon={PhoneIcon} />
|
|
27
|
+
<ContactInfoCard title="Աշխատանքային ժամեր" value="24/7 Աջակցություն" icon={HeadsetIcon} />
|
|
28
|
+
</View>
|
|
29
|
+
),
|
|
30
|
+
};
|