@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.
- package/dist/index.cjs +30063 -607
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +173 -10
- package/dist/index.d.ts +173 -10
- package/dist/index.js +29989 -547
- 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 +119 -46
- 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 +5 -2
- package/src/components/Sidebar/Sidebar.tsx +42 -12
- package/src/components/StepPager/StepPager.tsx +150 -0
- package/src/components/StepPager/index.ts +2 -0
- 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 +15 -0
- package/src/icons/Ban/BanIcon.tsx +15 -0
- package/src/icons/Ban/banPath.ts +2 -0
- package/src/icons/Ban/index.ts +1 -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/Fullscreen/FullscreenIcon.tsx +21 -0
- package/src/icons/Fullscreen/fullscreenPath.ts +2 -0
- package/src/icons/Fullscreen/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/Home/homePath.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/Sort/SortIcon.tsx +3 -3
- package/src/icons/Sort/SortIcon.web.tsx +2 -2
- package/src/icons/Sort/sortPath.ts +3 -2
- package/src/icons/VideoOff/VideoOffIcon.tsx +21 -0
- package/src/icons/VideoOff/VideoOffIcon.web.tsx +27 -0
- package/src/icons/VideoOff/index.ts +1 -0
- package/src/icons/VideoOff/videoOffPath.ts +2 -0
- package/src/icons/ZoomOut/ZoomOutIcon.tsx +21 -0
- package/src/icons/ZoomOut/index.ts +1 -0
- package/src/icons/ZoomOut/zoomOutPath.ts +2 -0
- package/src/icons/index.ts +8 -0
- package/src/icons/registry.ts +23 -0
- package/src/index.ts +20 -0
- package/src/theme/colors.test.ts +1 -1
- package/src/theme/typography.ts +4 -4
- package/src/utils/popoverAlign.ts +85 -0
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,220 @@
|
|
|
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
|
+
Pressable,
|
|
14
|
+
StyleSheet,
|
|
15
|
+
Text,
|
|
16
|
+
useWindowDimensions,
|
|
17
|
+
View,
|
|
18
|
+
type LayoutChangeEvent,
|
|
19
|
+
type StyleProp,
|
|
20
|
+
type ViewProps,
|
|
21
|
+
type ViewStyle,
|
|
22
|
+
} from "react-native";
|
|
23
|
+
import { ChevronDownIcon } from "../../icons";
|
|
24
|
+
import { colors, fonts } from "../../theme";
|
|
25
|
+
import { mergeRefs } from "../../utils/mergeRefs";
|
|
26
|
+
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
27
|
+
|
|
28
|
+
export interface AccordionItemProps extends Omit<ViewProps, "style" | "children"> {
|
|
29
|
+
title: string;
|
|
30
|
+
/** Expanded body text. */
|
|
31
|
+
children: string;
|
|
32
|
+
open?: boolean;
|
|
33
|
+
defaultOpen?: boolean;
|
|
34
|
+
onOpenChange?: (open: boolean) => void;
|
|
35
|
+
style?: StyleProp<ViewStyle>;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DESKTOP_MIN_WIDTH = 1024;
|
|
40
|
+
const CHEVRON_SIZE = 24;
|
|
41
|
+
const ANIMATION_DURATION = 280;
|
|
42
|
+
const BODY_GAP = 16;
|
|
43
|
+
const PADDING_X_MOBILE = 12;
|
|
44
|
+
const PADDING_X_DESKTOP = 24;
|
|
45
|
+
const RADIUS_MOBILE = 16;
|
|
46
|
+
const RADIUS_DESKTOP = 24;
|
|
47
|
+
const BODY_SIZE_MOBILE = 12;
|
|
48
|
+
const BODY_SIZE_DESKTOP = 14;
|
|
49
|
+
const BODY_LINE_MOBILE = 16;
|
|
50
|
+
const BODY_LINE_DESKTOP = 18;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* FAQ / accordion row — grey700 card.
|
|
54
|
+
* Mobile: radius 16, padding 16/12, body 12px.
|
|
55
|
+
* Desktop: radius 24, padding 16/24, body 14px.
|
|
56
|
+
*/
|
|
57
|
+
export const AccordionItem = forwardRef<ComponentRef<typeof View>, AccordionItemProps>(
|
|
58
|
+
function AccordionItem(
|
|
59
|
+
{
|
|
60
|
+
title,
|
|
61
|
+
children,
|
|
62
|
+
open: openProp,
|
|
63
|
+
defaultOpen = false,
|
|
64
|
+
onOpenChange,
|
|
65
|
+
style,
|
|
66
|
+
className,
|
|
67
|
+
accessibilityLabel,
|
|
68
|
+
...rest
|
|
69
|
+
},
|
|
70
|
+
forwardedRef,
|
|
71
|
+
) {
|
|
72
|
+
const containerRef = useRef<ComponentRef<typeof View>>(null);
|
|
73
|
+
const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
|
|
74
|
+
const isControlled = openProp !== undefined;
|
|
75
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
76
|
+
const open = isControlled ? Boolean(openProp) : uncontrolledOpen;
|
|
77
|
+
const [contentHeight, setContentHeight] = useState(0);
|
|
78
|
+
const progress = useRef(new Animated.Value(defaultOpen || openProp ? 1 : 0)).current;
|
|
79
|
+
const { width: viewportWidth } = useWindowDimensions();
|
|
80
|
+
const isDesktop = viewportWidth >= DESKTOP_MIN_WIDTH;
|
|
81
|
+
const paddingX = isDesktop ? PADDING_X_DESKTOP : PADDING_X_MOBILE;
|
|
82
|
+
const bodyFontSize = isDesktop ? BODY_SIZE_DESKTOP : BODY_SIZE_MOBILE;
|
|
83
|
+
const bodyLineHeight = isDesktop ? BODY_LINE_DESKTOP : BODY_LINE_MOBILE;
|
|
84
|
+
|
|
85
|
+
useApplyWebClassName(containerRef, className);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const animation = Animated.timing(progress, {
|
|
89
|
+
toValue: open ? 1 : 0,
|
|
90
|
+
duration: ANIMATION_DURATION,
|
|
91
|
+
easing: Easing.out(Easing.cubic),
|
|
92
|
+
useNativeDriver: false,
|
|
93
|
+
});
|
|
94
|
+
animation.start();
|
|
95
|
+
return () => animation.stop();
|
|
96
|
+
}, [open, progress]);
|
|
97
|
+
|
|
98
|
+
const toggle = useCallback(() => {
|
|
99
|
+
const next = !open;
|
|
100
|
+
if (!isControlled) setUncontrolledOpen(next);
|
|
101
|
+
onOpenChange?.(next);
|
|
102
|
+
}, [isControlled, onOpenChange, open]);
|
|
103
|
+
|
|
104
|
+
const onBodyLayout = useCallback((event: LayoutChangeEvent) => {
|
|
105
|
+
const nextHeight = event.nativeEvent.layout.height;
|
|
106
|
+
setContentHeight((current) => (Math.abs(current - nextHeight) < 0.5 ? current : nextHeight));
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
const bodyHeight = progress.interpolate({
|
|
110
|
+
inputRange: [0, 1],
|
|
111
|
+
outputRange: [0, Math.max(contentHeight, 0)],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const chevronRotate = progress.interpolate({
|
|
115
|
+
inputRange: [0, 1],
|
|
116
|
+
outputRange: ["0deg", "180deg"],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const bodyTextStyle = [styles.body, { fontSize: bodyFontSize, lineHeight: bodyLineHeight }];
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<View
|
|
123
|
+
{...rest}
|
|
124
|
+
ref={setContainerRef}
|
|
125
|
+
style={[
|
|
126
|
+
styles.root,
|
|
127
|
+
{
|
|
128
|
+
borderRadius: isDesktop ? RADIUS_DESKTOP : RADIUS_MOBILE,
|
|
129
|
+
paddingRight: paddingX,
|
|
130
|
+
paddingLeft: paddingX,
|
|
131
|
+
},
|
|
132
|
+
style,
|
|
133
|
+
]}
|
|
134
|
+
>
|
|
135
|
+
<Pressable
|
|
136
|
+
accessibilityRole="button"
|
|
137
|
+
accessibilityState={{ expanded: open }}
|
|
138
|
+
accessibilityLabel={accessibilityLabel ?? title}
|
|
139
|
+
onPress={toggle}
|
|
140
|
+
style={styles.header}
|
|
141
|
+
>
|
|
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
|
+
</Pressable>
|
|
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 }]}>
|
|
161
|
+
<View style={styles.bodyInner}>
|
|
162
|
+
<Text style={bodyTextStyle}>{children}</Text>
|
|
163
|
+
</View>
|
|
164
|
+
</Animated.View>
|
|
165
|
+
</View>
|
|
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
|
+
header: {
|
|
180
|
+
flexDirection: "row",
|
|
181
|
+
alignItems: "center",
|
|
182
|
+
gap: 16,
|
|
183
|
+
},
|
|
184
|
+
title: {
|
|
185
|
+
flex: 1,
|
|
186
|
+
minWidth: 0,
|
|
187
|
+
fontFamily: fonts.sans,
|
|
188
|
+
fontSize: 14,
|
|
189
|
+
fontWeight: "500",
|
|
190
|
+
lineHeight: 14,
|
|
191
|
+
letterSpacing: 0.42,
|
|
192
|
+
color: colors.white,
|
|
193
|
+
},
|
|
194
|
+
chevron: {
|
|
195
|
+
width: CHEVRON_SIZE,
|
|
196
|
+
height: CHEVRON_SIZE,
|
|
197
|
+
flexShrink: 0,
|
|
198
|
+
alignItems: "center",
|
|
199
|
+
justifyContent: "center",
|
|
200
|
+
},
|
|
201
|
+
measureHidden: {
|
|
202
|
+
position: "absolute",
|
|
203
|
+
top: 0,
|
|
204
|
+
opacity: 0,
|
|
205
|
+
paddingTop: BODY_GAP,
|
|
206
|
+
zIndex: -1,
|
|
207
|
+
},
|
|
208
|
+
bodyClip: {
|
|
209
|
+
overflow: "hidden",
|
|
210
|
+
},
|
|
211
|
+
bodyInner: {
|
|
212
|
+
paddingTop: BODY_GAP,
|
|
213
|
+
},
|
|
214
|
+
body: {
|
|
215
|
+
fontFamily: fonts.sans,
|
|
216
|
+
fontWeight: "400",
|
|
217
|
+
letterSpacing: 0.42,
|
|
218
|
+
color: colors.lightGrey,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
@@ -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;
|
|
@@ -31,6 +32,13 @@ export interface AccountHeaderProps extends Omit<ViewProps, "style" | "children"
|
|
|
31
32
|
menuAccessibilityLabel?: string;
|
|
32
33
|
/** Accessible name for notifications. Defaults to `"Notifications"`. */
|
|
33
34
|
notificationsAccessibilityLabel?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Web CSS class for the menu control (hide on desktop via `#account-header-menu` /
|
|
37
|
+
* `.account-header-menu` while keeping it mounted to avoid a mobile refresh flash).
|
|
38
|
+
*/
|
|
39
|
+
menuClassName?: string;
|
|
40
|
+
/** Optional title style override (e.g. mobile 14px from the web app). */
|
|
41
|
+
titleStyle?: StyleProp<TextStyle>;
|
|
34
42
|
style?: StyleProp<ViewStyle>;
|
|
35
43
|
className?: string;
|
|
36
44
|
}
|
|
@@ -44,6 +52,12 @@ const TITLE_BADGE_GAP = 12;
|
|
|
44
52
|
const MENU_TITLE_GAP = 16;
|
|
45
53
|
const RIGHT_GAP = 16;
|
|
46
54
|
|
|
55
|
+
/** RN-web applies `className` on the first paint — avoid post-hydrate class injection. */
|
|
56
|
+
function webClassName(className?: string): { className?: string } {
|
|
57
|
+
if (Platform.OS !== "web" || !className?.trim()) return {};
|
|
58
|
+
return { className: className.trim() };
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
export const AccountHeader = forwardRef<ComponentRef<typeof View>, AccountHeaderProps>(
|
|
48
62
|
function AccountHeader(
|
|
49
63
|
{
|
|
@@ -55,6 +69,8 @@ export const AccountHeader = forwardRef<ComponentRef<typeof View>, AccountHeader
|
|
|
55
69
|
backAccessibilityLabel = "Back",
|
|
56
70
|
menuAccessibilityLabel = "Open menu",
|
|
57
71
|
notificationsAccessibilityLabel = "Notifications",
|
|
72
|
+
menuClassName,
|
|
73
|
+
titleStyle,
|
|
58
74
|
style,
|
|
59
75
|
className,
|
|
60
76
|
...rest
|
|
@@ -63,19 +79,16 @@ export const AccountHeader = forwardRef<ComponentRef<typeof View>, AccountHeader
|
|
|
63
79
|
) {
|
|
64
80
|
const containerRef = useRef<ComponentRef<typeof View>>(null);
|
|
65
81
|
const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
|
|
66
|
-
const resolvedClassName = className?.trim() || undefined;
|
|
67
82
|
const showBack = onBackPress != null;
|
|
68
83
|
const showMenu = !showBack && onMenuPress != null;
|
|
69
|
-
const isCompact = showMenu;
|
|
70
|
-
|
|
71
|
-
useApplyWebClassName(containerRef, resolvedClassName);
|
|
72
84
|
|
|
73
85
|
return (
|
|
74
86
|
<View
|
|
75
87
|
{...rest}
|
|
88
|
+
{...webClassName(className)}
|
|
76
89
|
ref={setContainerRef}
|
|
77
90
|
accessibilityRole="header"
|
|
78
|
-
style={[styles.base,
|
|
91
|
+
style={[styles.base, style]}
|
|
79
92
|
>
|
|
80
93
|
<View
|
|
81
94
|
style={[
|
|
@@ -102,23 +115,24 @@ export const AccountHeader = forwardRef<ComponentRef<typeof View>, AccountHeader
|
|
|
102
115
|
|
|
103
116
|
{showMenu ? (
|
|
104
117
|
<Pressable
|
|
118
|
+
{...webClassName(menuClassName)}
|
|
119
|
+
id="account-header-menu"
|
|
120
|
+
nativeID="account-header-menu"
|
|
105
121
|
accessibilityRole="button"
|
|
106
122
|
accessibilityLabel={menuAccessibilityLabel}
|
|
107
123
|
hitSlop={8}
|
|
108
124
|
onPress={onMenuPress}
|
|
109
125
|
style={styles.menuButton}
|
|
110
126
|
>
|
|
111
|
-
<
|
|
127
|
+
<MenuLinesIcon color={colors.white} size={MENU_ICON_SIZE} strokeWidth={2} />
|
|
112
128
|
</Pressable>
|
|
113
129
|
) : null}
|
|
114
130
|
|
|
115
131
|
<View style={styles.titleRow}>
|
|
116
132
|
<Text
|
|
133
|
+
{...webClassName("account-header-title")}
|
|
117
134
|
numberOfLines={1}
|
|
118
|
-
style={[
|
|
119
|
-
styles.title,
|
|
120
|
-
showBack || !isCompact ? styles.titleDesktop : styles.titleCompact,
|
|
121
|
-
]}
|
|
135
|
+
style={[styles.title, titleStyle]}
|
|
122
136
|
>
|
|
123
137
|
{title}
|
|
124
138
|
</Text>
|
|
@@ -141,6 +155,7 @@ export const AccountHeader = forwardRef<ComponentRef<typeof View>, AccountHeader
|
|
|
141
155
|
},
|
|
142
156
|
);
|
|
143
157
|
|
|
158
|
+
/** Desktop defaults — mobile padding/title size are overridden from the web app. */
|
|
144
159
|
const styles = StyleSheet.create({
|
|
145
160
|
base: {
|
|
146
161
|
height: HEADER_HEIGHT,
|
|
@@ -148,18 +163,14 @@ const styles = StyleSheet.create({
|
|
|
148
163
|
flexDirection: "row",
|
|
149
164
|
alignItems: "center",
|
|
150
165
|
justifyContent: "space-between",
|
|
151
|
-
|
|
152
|
-
borderBottomWidth: 1,
|
|
153
|
-
borderBottomColor: colors.grey700,
|
|
154
|
-
},
|
|
155
|
-
desktop: {
|
|
166
|
+
opacity: 1,
|
|
156
167
|
paddingTop: 16,
|
|
168
|
+
paddingRight: 24,
|
|
157
169
|
paddingBottom: 16,
|
|
158
170
|
paddingLeft: 24,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
padding: 16,
|
|
171
|
+
backgroundColor: colors.dark,
|
|
172
|
+
borderBottomWidth: 1,
|
|
173
|
+
borderBottomColor: colors.grey700,
|
|
163
174
|
},
|
|
164
175
|
start: {
|
|
165
176
|
flex: 1,
|
|
@@ -207,16 +218,10 @@ const styles = StyleSheet.create({
|
|
|
207
218
|
flexShrink: 1,
|
|
208
219
|
fontFamily: fonts.sans,
|
|
209
220
|
fontWeight: "600",
|
|
210
|
-
letterSpacing: 0,
|
|
211
|
-
color: colors.white,
|
|
212
|
-
},
|
|
213
|
-
titleDesktop: {
|
|
214
221
|
fontSize: 16,
|
|
215
222
|
lineHeight: 22,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
fontSize: 14,
|
|
219
|
-
lineHeight: 20,
|
|
223
|
+
letterSpacing: 0,
|
|
224
|
+
color: colors.white,
|
|
220
225
|
},
|
|
221
226
|
badgeSlot: {
|
|
222
227
|
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
|
+
};
|