@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.
Files changed (68) hide show
  1. package/dist/index.cjs +30063 -607
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +173 -10
  4. package/dist/index.d.ts +173 -10
  5. package/dist/index.js +29989 -547
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/components/AccordionItem/AccordionItem.stories.tsx +54 -0
  9. package/src/components/AccordionItem/AccordionItem.tsx +220 -0
  10. package/src/components/AccordionItem/index.ts +2 -0
  11. package/src/components/AccountHeader/AccountHeader.tsx +34 -29
  12. package/src/components/Button/Button.stories.tsx +10 -1
  13. package/src/components/Button/Button.tsx +32 -3
  14. package/src/components/ContactInfoCard/ContactInfoCard.stories.tsx +30 -0
  15. package/src/components/ContactInfoCard/ContactInfoCard.tsx +141 -0
  16. package/src/components/ContactInfoCard/index.ts +2 -0
  17. package/src/components/DatePicker/DatePicker.tsx +119 -46
  18. package/src/components/Modal/Modal.stories.tsx +100 -0
  19. package/src/components/Modal/Modal.tsx +165 -0
  20. package/src/components/Modal/index.ts +2 -0
  21. package/src/components/Select/Select.tsx +5 -2
  22. package/src/components/Sidebar/Sidebar.tsx +42 -12
  23. package/src/components/StepPager/StepPager.tsx +150 -0
  24. package/src/components/StepPager/index.ts +2 -0
  25. package/src/components/ToggleRow/ToggleRow.stories.tsx +46 -0
  26. package/src/components/ToggleRow/ToggleRow.tsx +121 -0
  27. package/src/components/ToggleRow/index.ts +2 -0
  28. package/src/components/VerificationCodeInput/VerificationCodeInput.tsx +80 -60
  29. package/src/components/index.ts +15 -0
  30. package/src/icons/Ban/BanIcon.tsx +15 -0
  31. package/src/icons/Ban/banPath.ts +2 -0
  32. package/src/icons/Ban/index.ts +1 -0
  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/Fullscreen/FullscreenIcon.tsx +21 -0
  38. package/src/icons/Fullscreen/fullscreenPath.ts +2 -0
  39. package/src/icons/Fullscreen/index.ts +1 -0
  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/Home/homePath.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/Sort/SortIcon.tsx +3 -3
  54. package/src/icons/Sort/SortIcon.web.tsx +2 -2
  55. package/src/icons/Sort/sortPath.ts +3 -2
  56. package/src/icons/VideoOff/VideoOffIcon.tsx +21 -0
  57. package/src/icons/VideoOff/VideoOffIcon.web.tsx +27 -0
  58. package/src/icons/VideoOff/index.ts +1 -0
  59. package/src/icons/VideoOff/videoOffPath.ts +2 -0
  60. package/src/icons/ZoomOut/ZoomOutIcon.tsx +21 -0
  61. package/src/icons/ZoomOut/index.ts +1 -0
  62. package/src/icons/ZoomOut/zoomOutPath.ts +2 -0
  63. package/src/icons/index.ts +8 -0
  64. package/src/icons/registry.ts +23 -0
  65. package/src/index.ts +20 -0
  66. package/src/theme/colors.test.ts +1 -1
  67. package/src/theme/typography.ts +4 -4
  68. package/src/utils/popoverAlign.ts +85 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecohouse/ui",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
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,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
+ });
@@ -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;
@@ -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, isCompact ? styles.compact : styles.desktop, style]}
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
- <MenuIcon color={colors.white} size={MENU_ICON_SIZE} strokeWidth={1.5} />
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
- backgroundColor: colors.dark,
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
- paddingRight: 24,
160
- },
161
- compact: {
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
- titleCompact: {
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
+ };