@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.
Files changed (56) hide show
  1. package/dist/index.cjs +30078 -656
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +114 -7
  4. package/dist/index.d.ts +114 -7
  5. package/dist/index.js +30007 -593
  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 +223 -0
  10. package/src/components/AccordionItem/index.ts +2 -0
  11. package/src/components/AccountHeader/AccountHeader.tsx +50 -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 +51 -15
  18. package/src/components/Input/Input.tsx +54 -21
  19. package/src/components/LanguageSwitcher/LanguageSwitcher.stories.tsx +11 -0
  20. package/src/components/LanguageSwitcher/LanguageSwitcher.tsx +103 -19
  21. package/src/components/LanguageSwitcher/index.ts +5 -1
  22. package/src/components/Modal/Modal.stories.tsx +100 -0
  23. package/src/components/Modal/Modal.tsx +165 -0
  24. package/src/components/Modal/index.ts +2 -0
  25. package/src/components/Select/Select.tsx +90 -5
  26. package/src/components/Sidebar/Sidebar.tsx +19 -3
  27. package/src/components/Textarea/Textarea.tsx +29 -4
  28. package/src/components/ToggleRow/ToggleRow.stories.tsx +46 -0
  29. package/src/components/ToggleRow/ToggleRow.tsx +121 -0
  30. package/src/components/ToggleRow/index.ts +2 -0
  31. package/src/components/VerificationCodeInput/VerificationCodeInput.tsx +80 -60
  32. package/src/components/index.ts +17 -1
  33. package/src/icons/CameraFilled/CameraFilledIcon.tsx +15 -0
  34. package/src/icons/CameraFilled/CameraFilledIcon.web.tsx +21 -0
  35. package/src/icons/CameraFilled/cameraFilledPath.ts +2 -0
  36. package/src/icons/CameraFilled/index.ts +1 -0
  37. package/src/icons/Globe/GlobeIcon.tsx +2 -2
  38. package/src/icons/Globe/GlobeIcon.web.tsx +2 -2
  39. package/src/icons/Globe/globePath.ts +2 -2
  40. package/src/icons/Headset/HeadsetIcon.tsx +26 -0
  41. package/src/icons/Headset/HeadsetIcon.web.tsx +25 -0
  42. package/src/icons/Headset/headsetPath.ts +6 -0
  43. package/src/icons/Headset/index.ts +1 -0
  44. package/src/icons/Hide/HideIcon.tsx +21 -0
  45. package/src/icons/Hide/HideIcon.web.tsx +27 -0
  46. package/src/icons/Hide/hidePath.ts +3 -0
  47. package/src/icons/Hide/index.ts +1 -0
  48. package/src/icons/Key/keyPath.ts +1 -1
  49. package/src/icons/MenuLines/MenuLinesIcon.tsx +20 -0
  50. package/src/icons/MenuLines/MenuLinesIcon.web.tsx +26 -0
  51. package/src/icons/MenuLines/index.ts +1 -0
  52. package/src/icons/MenuLines/menuLinesPath.ts +3 -0
  53. package/src/icons/index.ts +4 -0
  54. package/src/icons/registry.ts +11 -0
  55. package/src/index.ts +13 -0
  56. package/src/theme/typography.ts +3 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecohouse/ui",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Cross-platform presentation-only UI components for EcoHouse (React Native + React Native Web)",
@@ -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
+ });
@@ -0,0 +1,2 @@
1
+ export { AccordionItem } from "./AccordionItem";
2
+ export type { AccordionItemProps } from "./AccordionItem";
@@ -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, MenuIcon } from "../../icons";
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, isCompact ? styles.compact : styles.desktop, style]}
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
- <MenuIcon color={colors.white} size={MENU_ICON_SIZE} strokeWidth={1.5} />
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
- backgroundColor: colors.dark,
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
- paddingRight: 24,
160
- },
161
- compact: {
162
- padding: 16,
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
- titleCompact: {
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
+ };