@ecohouse/ui 0.1.31 → 0.1.32

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 (71) hide show
  1. package/README.md +5 -0
  2. package/dist/index.cjs +1122 -338
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +147 -5
  5. package/dist/index.d.ts +147 -5
  6. package/dist/index.js +1049 -285
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/AccountHeader/AccountHeader.stories.tsx +70 -0
  10. package/src/components/AccountHeader/AccountHeader.tsx +224 -0
  11. package/src/components/AccountHeader/index.ts +2 -0
  12. package/src/components/AvatarSimple/AvatarSimple.tsx +2 -2
  13. package/src/components/Button/Button.tsx +4 -1
  14. package/src/components/DatePicker/DatePicker.tsx +14 -9
  15. package/src/components/FloatingActionButton/FloatingActionButton.stories.tsx +42 -0
  16. package/src/components/FloatingActionButton/FloatingActionButton.tsx +77 -0
  17. package/src/components/FloatingActionButton/index.ts +2 -0
  18. package/src/components/InfoTile/InfoTile.stories.tsx +80 -0
  19. package/src/components/InfoTile/InfoTile.tsx +118 -0
  20. package/src/components/InfoTile/index.ts +2 -0
  21. package/src/components/PaginationDots/PaginationDots.stories.tsx +48 -0
  22. package/src/components/PaginationDots/PaginationDots.tsx +111 -0
  23. package/src/components/PaginationDots/index.ts +2 -0
  24. package/src/components/StatusBadge/StatusBadge.stories.tsx +54 -0
  25. package/src/components/StatusBadge/StatusBadge.tsx +108 -0
  26. package/src/components/StatusBadge/index.ts +2 -0
  27. package/src/components/StepList/StepList.stories.tsx +37 -0
  28. package/src/components/StepList/StepList.tsx +152 -0
  29. package/src/components/StepList/index.ts +2 -0
  30. package/src/components/index.ts +18 -0
  31. package/src/icons/AlertTriangle/AlertTriangleIcon.tsx +19 -0
  32. package/src/icons/AlertTriangle/AlertTriangleIcon.web.tsx +18 -0
  33. package/src/icons/AlertTriangle/alertTrianglePath.ts +3 -0
  34. package/src/icons/AlertTriangle/index.ts +1 -0
  35. package/src/icons/Bell/BellIcon.tsx +2 -2
  36. package/src/icons/Bell/BellIcon.web.tsx +2 -2
  37. package/src/icons/Bell/bellPath.ts +1 -1
  38. package/src/icons/CottageDetail/CottageDetailIcons.tsx +15 -0
  39. package/src/icons/CottageDetail/CottageDetailIcons.web.tsx +15 -0
  40. package/src/icons/CottageDetail/cottageDetailPaths.ts +3 -0
  41. package/src/icons/CottageDetail/index.ts +1 -0
  42. package/src/icons/Edit/EditIcon.tsx +20 -0
  43. package/src/icons/Edit/EditIcon.web.tsx +19 -0
  44. package/src/icons/Edit/editPath.ts +3 -0
  45. package/src/icons/Edit/index.ts +1 -0
  46. package/src/icons/HouseRules/HouseRulesIcons.tsx +62 -0
  47. package/src/icons/HouseRules/HouseRulesIcons.web.tsx +61 -0
  48. package/src/icons/HouseRules/houseRulesPaths.ts +24 -0
  49. package/src/icons/HouseRules/index.ts +2 -0
  50. package/src/icons/LocationUser/LocationUserIcon.tsx +15 -0
  51. package/src/icons/LocationUser/LocationUserIcon.web.tsx +14 -0
  52. package/src/icons/LocationUser/index.ts +1 -0
  53. package/src/icons/LocationUser/locationUserPath.ts +2 -0
  54. package/src/icons/Lock/LockIcon.tsx +21 -0
  55. package/src/icons/Lock/LockIcon.web.tsx +26 -0
  56. package/src/icons/Lock/index.ts +1 -0
  57. package/src/icons/Lock/lockPath.ts +3 -0
  58. package/src/icons/Refresh/RefreshIcon.tsx +20 -0
  59. package/src/icons/Refresh/RefreshIcon.web.tsx +19 -0
  60. package/src/icons/Refresh/index.ts +1 -0
  61. package/src/icons/Refresh/refreshPath.ts +3 -0
  62. package/src/icons/RefundStatus/RefundStatusIcon.tsx +24 -0
  63. package/src/icons/RefundStatus/RefundStatusIcon.web.tsx +23 -0
  64. package/src/icons/RefundStatus/index.ts +1 -0
  65. package/src/icons/RefundStatus/refundStatusPath.ts +3 -0
  66. package/src/icons/index.ts +8 -0
  67. package/src/icons/registry.ts +30 -0
  68. package/src/index.ts +26 -0
  69. package/src/theme/colors.ts +4 -0
  70. package/src/theme/typography.ts +1 -1
  71. package/src/utils/dateUtils.ts +25 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecohouse/ui",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
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,70 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { fn } from "storybook/test";
3
+ import { StyleSheet, View } from "react-native";
4
+ import { StatusBadge } from "../StatusBadge";
5
+ import { AccountHeader } from "./AccountHeader";
6
+
7
+ const meta = {
8
+ title: "Components/AccountHeader",
9
+ component: AccountHeader,
10
+ args: {
11
+ title: "Իմ ամրագրումները",
12
+ onNotificationsPress: fn(),
13
+ notificationsAccessibilityLabel: "Ծանուցումներ",
14
+ menuAccessibilityLabel: "Բացել ընտրացանկը",
15
+ },
16
+ argTypes: {
17
+ onMenuPress: { action: "menu press" },
18
+ onNotificationsPress: { action: "notifications press" },
19
+ },
20
+ parameters: {
21
+ backgrounds: { default: "black" },
22
+ layout: "fullscreen",
23
+ },
24
+ } satisfies Meta<typeof AccountHeader>;
25
+
26
+ export default meta;
27
+ type Story = StoryObj<typeof meta>;
28
+
29
+ export const Desktop: Story = {
30
+ render: (args) => (
31
+ <View style={styles.desktopFrame}>
32
+ <AccountHeader {...args} />
33
+ </View>
34
+ ),
35
+ };
36
+
37
+ export const Mobile: Story = {
38
+ args: {
39
+ onMenuPress: fn(),
40
+ },
41
+ render: (args) => (
42
+ <View style={styles.mobileFrame}>
43
+ <AccountHeader {...args} />
44
+ </View>
45
+ ),
46
+ };
47
+
48
+ export const WithBackAndStatus: Story = {
49
+ args: {
50
+ title: "Ամրագրում #BK-9087",
51
+ onBackPress: fn(),
52
+ },
53
+ render: (args) => (
54
+ <View style={styles.desktopFrame}>
55
+ <AccountHeader {...args} badge={<StatusBadge label="Ակտիվ" tone="active" />} />
56
+ </View>
57
+ ),
58
+ };
59
+
60
+ const styles = StyleSheet.create({
61
+ desktopFrame: {
62
+ width: "100%",
63
+ maxWidth: 960,
64
+ backgroundColor: "#090909",
65
+ },
66
+ mobileFrame: {
67
+ width: 390,
68
+ backgroundColor: "#090909",
69
+ },
70
+ });
@@ -0,0 +1,224 @@
1
+ import { forwardRef, useMemo, useRef, type ComponentRef, type ReactNode } from "react";
2
+ import {
3
+ Pressable,
4
+ StyleSheet,
5
+ Text,
6
+ View,
7
+ type StyleProp,
8
+ type ViewProps,
9
+ type ViewStyle,
10
+ } from "react-native";
11
+ import { BellIcon, ChevronLeftIcon, MenuIcon } from "../../icons";
12
+ import { colors, fonts } from "../../theme";
13
+ import { mergeRefs } from "../../utils/mergeRefs";
14
+ import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
15
+
16
+ export interface AccountHeaderProps extends Omit<ViewProps, "style" | "children"> {
17
+ title: string;
18
+ /**
19
+ * When set, shows a 32×32 back control (takes priority over `onMenuPress`).
20
+ * Used on nested account pages such as booking details.
21
+ */
22
+ onBackPress?: () => void;
23
+ /** When set, shows the 28×28 menu control (mobile account shell). Ignored if `onBackPress` is set. */
24
+ onMenuPress?: () => void;
25
+ onNotificationsPress?: () => void;
26
+ /** Optional status chip (or other accessory) rendered after the title with a 12px gap. */
27
+ badge?: ReactNode;
28
+ /** Accessible name for the back control. Defaults to `"Back"`. */
29
+ backAccessibilityLabel?: string;
30
+ /** Accessible name for the menu control. Defaults to `"Open menu"`. */
31
+ menuAccessibilityLabel?: string;
32
+ /** Accessible name for notifications. Defaults to `"Notifications"`. */
33
+ notificationsAccessibilityLabel?: string;
34
+ style?: StyleProp<ViewStyle>;
35
+ className?: string;
36
+ }
37
+
38
+ const HEADER_HEIGHT = 64;
39
+ const MENU_ICON_SIZE = 28;
40
+ const ICON_BUTTON_SIZE = 32;
41
+ const ICON_BUTTON_ICON_SIZE = 16;
42
+ const BACK_TITLE_GAP = 12;
43
+ const TITLE_BADGE_GAP = 12;
44
+ const MENU_TITLE_GAP = 16;
45
+ const RIGHT_GAP = 16;
46
+
47
+ export const AccountHeader = forwardRef<ComponentRef<typeof View>, AccountHeaderProps>(
48
+ function AccountHeader(
49
+ {
50
+ title,
51
+ onBackPress,
52
+ onMenuPress,
53
+ onNotificationsPress,
54
+ badge,
55
+ backAccessibilityLabel = "Back",
56
+ menuAccessibilityLabel = "Open menu",
57
+ notificationsAccessibilityLabel = "Notifications",
58
+ style,
59
+ className,
60
+ ...rest
61
+ },
62
+ forwardedRef,
63
+ ) {
64
+ const containerRef = useRef<ComponentRef<typeof View>>(null);
65
+ const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
66
+ const resolvedClassName = className?.trim() || undefined;
67
+ const showBack = onBackPress != null;
68
+ const showMenu = !showBack && onMenuPress != null;
69
+ const isCompact = showMenu;
70
+
71
+ useApplyWebClassName(containerRef, resolvedClassName);
72
+
73
+ return (
74
+ <View
75
+ {...rest}
76
+ ref={setContainerRef}
77
+ accessibilityRole="header"
78
+ style={[styles.base, isCompact ? styles.compact : styles.desktop, style]}
79
+ >
80
+ <View
81
+ style={[
82
+ styles.start,
83
+ showBack ? styles.startWithBack : null,
84
+ showMenu ? styles.startWithMenu : null,
85
+ ]}
86
+ >
87
+ {showBack ? (
88
+ <Pressable
89
+ accessibilityRole="button"
90
+ accessibilityLabel={backAccessibilityLabel}
91
+ hitSlop={8}
92
+ onPress={onBackPress}
93
+ style={styles.iconButton}
94
+ >
95
+ <ChevronLeftIcon
96
+ color={colors.white}
97
+ size={ICON_BUTTON_ICON_SIZE}
98
+ strokeWidth={1.5}
99
+ />
100
+ </Pressable>
101
+ ) : null}
102
+
103
+ {showMenu ? (
104
+ <Pressable
105
+ accessibilityRole="button"
106
+ accessibilityLabel={menuAccessibilityLabel}
107
+ hitSlop={8}
108
+ onPress={onMenuPress}
109
+ style={styles.menuButton}
110
+ >
111
+ <MenuIcon color={colors.white} size={MENU_ICON_SIZE} strokeWidth={1.5} />
112
+ </Pressable>
113
+ ) : null}
114
+
115
+ <View style={styles.titleRow}>
116
+ <Text
117
+ numberOfLines={1}
118
+ style={[
119
+ styles.title,
120
+ showBack || !isCompact ? styles.titleDesktop : styles.titleCompact,
121
+ ]}
122
+ >
123
+ {title}
124
+ </Text>
125
+ {badge != null ? <View style={styles.badgeSlot}>{badge}</View> : null}
126
+ </View>
127
+ </View>
128
+
129
+ <View style={styles.end}>
130
+ <Pressable
131
+ accessibilityRole="button"
132
+ accessibilityLabel={notificationsAccessibilityLabel}
133
+ onPress={onNotificationsPress}
134
+ style={styles.iconButton}
135
+ >
136
+ <BellIcon color={colors.white} size={ICON_BUTTON_ICON_SIZE} strokeWidth={1.5} />
137
+ </Pressable>
138
+ </View>
139
+ </View>
140
+ );
141
+ },
142
+ );
143
+
144
+ const styles = StyleSheet.create({
145
+ base: {
146
+ height: HEADER_HEIGHT,
147
+ width: "100%",
148
+ flexDirection: "row",
149
+ alignItems: "center",
150
+ justifyContent: "space-between",
151
+ backgroundColor: colors.dark,
152
+ borderBottomWidth: 1,
153
+ borderBottomColor: colors.grey700,
154
+ },
155
+ desktop: {
156
+ paddingTop: 16,
157
+ paddingBottom: 16,
158
+ paddingLeft: 24,
159
+ paddingRight: 24,
160
+ },
161
+ compact: {
162
+ padding: 16,
163
+ },
164
+ start: {
165
+ flex: 1,
166
+ minWidth: 0,
167
+ flexDirection: "row",
168
+ alignItems: "center",
169
+ },
170
+ startWithBack: {
171
+ gap: BACK_TITLE_GAP,
172
+ },
173
+ startWithMenu: {
174
+ gap: MENU_TITLE_GAP,
175
+ },
176
+ titleRow: {
177
+ flex: 1,
178
+ minWidth: 0,
179
+ flexDirection: "row",
180
+ alignItems: "center",
181
+ gap: TITLE_BADGE_GAP,
182
+ },
183
+ end: {
184
+ flexShrink: 0,
185
+ marginLeft: RIGHT_GAP,
186
+ },
187
+ iconButton: {
188
+ width: ICON_BUTTON_SIZE,
189
+ height: ICON_BUTTON_SIZE,
190
+ padding: 8,
191
+ alignItems: "center",
192
+ justifyContent: "center",
193
+ borderRadius: 5,
194
+ backgroundColor: colors.grey800,
195
+ borderWidth: 1,
196
+ borderColor: colors.grey700,
197
+ flexShrink: 0,
198
+ },
199
+ menuButton: {
200
+ width: MENU_ICON_SIZE,
201
+ height: MENU_ICON_SIZE,
202
+ alignItems: "center",
203
+ justifyContent: "center",
204
+ flexShrink: 0,
205
+ },
206
+ title: {
207
+ flexShrink: 1,
208
+ fontFamily: fonts.sans,
209
+ fontWeight: "600",
210
+ letterSpacing: 0,
211
+ color: colors.white,
212
+ },
213
+ titleDesktop: {
214
+ fontSize: 16,
215
+ lineHeight: 22,
216
+ },
217
+ titleCompact: {
218
+ fontSize: 14,
219
+ lineHeight: 20,
220
+ },
221
+ badgeSlot: {
222
+ flexShrink: 0,
223
+ },
224
+ });
@@ -0,0 +1,2 @@
1
+ export { AccountHeader } from "./AccountHeader";
2
+ export type { AccountHeaderProps } from "./AccountHeader";
@@ -83,10 +83,10 @@ export const AvatarSimple = forwardRef<ComponentRef<typeof View>, AvatarSimplePr
83
83
  {!compact ? (
84
84
  <View style={styles.details}>
85
85
  <View style={styles.textBlock}>
86
- <Text style={styles.name} numberOfLines={1}>
86
+ <Text style={styles.name} numberOfLines={1} ellipsizeMode="tail">
87
87
  {name}
88
88
  </Text>
89
- <Text style={styles.email} numberOfLines={1}>
89
+ <Text style={styles.email} numberOfLines={1} ellipsizeMode="tail">
90
90
  {email}
91
91
  </Text>
92
92
  </View>
@@ -31,6 +31,8 @@ export interface ButtonProps extends Omit<
31
31
  size?: ButtonSize;
32
32
  showIcon?: boolean;
33
33
  icon?: ReactNode;
34
+ /** Overrides the trailing icon badge fill (defaults to the variant preset). */
35
+ iconBadgeBackgroundColor?: string;
34
36
  style?: StyleProp<ViewStyle>;
35
37
  textStyle?: StyleProp<TextStyle>;
36
38
  className?: string;
@@ -97,6 +99,7 @@ export const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonPr
97
99
  size = "lg",
98
100
  showIcon = false,
99
101
  icon,
102
+ iconBadgeBackgroundColor,
100
103
  style,
101
104
  textStyle,
102
105
  className,
@@ -182,7 +185,7 @@ export const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonPr
182
185
  width: metrics.iconContainerSize,
183
186
  height: metrics.iconContainerSize,
184
187
  borderRadius: metrics.iconContainerSize / 2,
185
- backgroundColor: preset.iconBadgeBackgroundColor,
188
+ backgroundColor: iconBadgeBackgroundColor ?? preset.iconBadgeBackgroundColor,
186
189
  },
187
190
  ]}
188
191
  >
@@ -22,6 +22,7 @@ import { colors, datePickerTypography } from "../../theme";
22
22
  import {
23
23
  addMonths,
24
24
  formatDate,
25
+ formatDateDayShortMonth,
25
26
  getMonthMatrix,
26
27
  isBefore,
27
28
  isDateDisabled,
@@ -137,9 +138,7 @@ function RangeTriggerField({
137
138
  {label}
138
139
  </Text>
139
140
  <View style={styles.rangeValueRow}>
140
- <Text numberOfLines={1} style={styles.rangeDate}>
141
- {value}
142
- </Text>
141
+ <Text style={styles.rangeDate}>{value}</Text>
143
142
  {time ? (
144
143
  <View style={styles.rangeTimeWrap}>
145
144
  <ClockFilledIcon color={colors.lightGrey} size={16} />
@@ -172,7 +171,7 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
172
171
  defaultVisibleMonth,
173
172
  monthNames = DEFAULT_MONTH_NAMES,
174
173
  weekdayLabels = DEFAULT_WEEKDAY_LABELS,
175
- formatValue = formatDate,
174
+ formatValue: formatValueProp,
176
175
  weekStartsOn = 1,
177
176
  calendarStyle,
178
177
  style,
@@ -188,6 +187,10 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
188
187
  } = props;
189
188
 
190
189
  const isRange = props.range === true;
190
+ const usesRangeTrigger = isRange && variant !== "default";
191
+ const formatValue =
192
+ formatValueProp ??
193
+ (usesRangeTrigger ? (date: Date) => formatDateDayShortMonth(date, monthNames) : formatDate);
191
194
  const valueProp = props.value;
192
195
  const rootRef = useRef<ComponentRef<typeof View>>(null);
193
196
  const triggerRef = useRef<ComponentRef<typeof Pressable>>(null);
@@ -347,7 +350,6 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
347
350
  ? formatValue(singleValue)
348
351
  : placeholder;
349
352
 
350
- const usesRangeTrigger = isRange && variant !== "default";
351
353
  const usesVerticalRangeTrigger = usesRangeTrigger && variant === "range-vertical";
352
354
  const triggerBorderColor = usesRangeTrigger
353
355
  ? isOpen
@@ -641,7 +643,7 @@ const styles = StyleSheet.create({
641
643
  justifyContent: "center",
642
644
  gap: 8,
643
645
  paddingVertical: 12,
644
- paddingHorizontal: 16,
646
+ paddingHorizontal: 12,
645
647
  },
646
648
  rangeLabel: {
647
649
  ...datePickerTypography.rangeLabel,
@@ -651,13 +653,16 @@ const styles = StyleSheet.create({
651
653
  minWidth: 0,
652
654
  flexDirection: "row",
653
655
  alignItems: "center",
654
- justifyContent: "space-between",
655
- gap: 12,
656
+ justifyContent: "flex-start",
657
+ flexWrap: "nowrap",
658
+ gap: 10,
656
659
  },
657
660
  rangeDate: {
658
661
  ...datePickerTypography.rangeDate,
659
- flexShrink: 1,
662
+ flexShrink: 0,
660
663
  color: colors.white,
664
+ // @ts-expect-error RN Web — keep the full date on one line
665
+ whiteSpace: "nowrap",
661
666
  },
662
667
  rangeTimeWrap: {
663
668
  flexShrink: 0,
@@ -0,0 +1,42 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { fn } from "storybook/test";
3
+ import { StyleSheet, View } from "react-native";
4
+ import { FloatingActionButton } from "./FloatingActionButton";
5
+
6
+ const meta = {
7
+ title: "Components/FloatingActionButton",
8
+ component: FloatingActionButton,
9
+ args: {
10
+ onPress: fn(),
11
+ accessibilityLabel: "Նոր ամրագրում",
12
+ },
13
+ parameters: {
14
+ backgrounds: { default: "black" },
15
+ layout: "fullscreen",
16
+ },
17
+ } satisfies Meta<typeof FloatingActionButton>;
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof meta>;
21
+
22
+ export const Default: Story = {
23
+ render: (args) => (
24
+ <View style={styles.frame}>
25
+ <FloatingActionButton {...args} style={styles.fab} />
26
+ </View>
27
+ ),
28
+ };
29
+
30
+ const styles = StyleSheet.create({
31
+ frame: {
32
+ position: "relative",
33
+ width: 390,
34
+ height: 640,
35
+ backgroundColor: "#090909",
36
+ },
37
+ fab: {
38
+ position: "absolute",
39
+ right: 12,
40
+ bottom: 20,
41
+ },
42
+ });
@@ -0,0 +1,77 @@
1
+ import { forwardRef, useMemo, useRef, type ComponentRef, type ReactNode } from "react";
2
+ import {
3
+ StyleSheet,
4
+ TouchableOpacity,
5
+ type GestureResponderEvent,
6
+ type StyleProp,
7
+ type TouchableOpacityProps,
8
+ type ViewStyle,
9
+ } from "react-native";
10
+ import { PlusIcon } from "../../icons";
11
+ import { colors } from "../../theme";
12
+ import { mergeRefs } from "../../utils/mergeRefs";
13
+ import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
14
+
15
+ export interface FloatingActionButtonProps extends Omit<
16
+ TouchableOpacityProps,
17
+ "onPress" | "disabled" | "style" | "children"
18
+ > {
19
+ onPress: (event: GestureResponderEvent) => void;
20
+ disabled?: boolean;
21
+ /** Defaults to a 24×24 white plus icon. */
22
+ icon?: ReactNode;
23
+ accessibilityLabel?: string;
24
+ style?: StyleProp<ViewStyle>;
25
+ className?: string;
26
+ }
27
+
28
+ const SIZE = 48;
29
+ const PADDING = 12;
30
+ const ICON_SIZE = 24;
31
+ const RADIUS = 64;
32
+
33
+ export const FloatingActionButton = forwardRef<
34
+ ComponentRef<typeof TouchableOpacity>,
35
+ FloatingActionButtonProps
36
+ >(function FloatingActionButton(
37
+ { onPress, disabled = false, icon, accessibilityLabel = "Add", style, className, ...rest },
38
+ forwardedRef,
39
+ ) {
40
+ const containerRef = useRef<ComponentRef<typeof TouchableOpacity>>(null);
41
+ const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
42
+ const resolvedClassName = className?.trim() || undefined;
43
+
44
+ useApplyWebClassName(containerRef, resolvedClassName);
45
+
46
+ return (
47
+ <TouchableOpacity
48
+ {...rest}
49
+ ref={setContainerRef}
50
+ onPress={onPress}
51
+ disabled={disabled}
52
+ activeOpacity={0.7}
53
+ accessibilityRole="button"
54
+ accessibilityLabel={accessibilityLabel}
55
+ accessibilityState={{ disabled }}
56
+ style={[styles.base, disabled && styles.disabled, style]}
57
+ >
58
+ {icon ?? <PlusIcon color={colors.white} size={ICON_SIZE} strokeWidth={2} />}
59
+ </TouchableOpacity>
60
+ );
61
+ });
62
+
63
+ const styles = StyleSheet.create({
64
+ base: {
65
+ width: SIZE,
66
+ height: SIZE,
67
+ padding: PADDING,
68
+ borderRadius: RADIUS,
69
+ backgroundColor: colors.primary,
70
+ alignItems: "center",
71
+ justifyContent: "center",
72
+ gap: 10,
73
+ },
74
+ disabled: {
75
+ opacity: 0.6,
76
+ },
77
+ });
@@ -0,0 +1,2 @@
1
+ export { FloatingActionButton } from "./FloatingActionButton";
2
+ export type { FloatingActionButtonProps } from "./FloatingActionButton";
@@ -0,0 +1,80 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { StyleSheet, View } from "react-native";
3
+ import { NoSmokingIcon, PawIcon, QuietHoursIcon, TrashIcon } from "../../icons";
4
+ import { InfoTile } from "./InfoTile";
5
+
6
+ const meta = {
7
+ title: "Components/InfoTile",
8
+ component: InfoTile,
9
+ args: {
10
+ title: "Լռության ժամեր",
11
+ description: "23:00 - 08:00",
12
+ icon: QuietHoursIcon,
13
+ },
14
+ parameters: {
15
+ backgrounds: { default: "black" },
16
+ },
17
+ } satisfies Meta<typeof InfoTile>;
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof meta>;
21
+
22
+ export const Default: Story = {
23
+ render: (args) => (
24
+ <View style={styles.frame}>
25
+ <InfoTile {...args} />
26
+ </View>
27
+ ),
28
+ };
29
+
30
+ export const HouseRulesGrid: Story = {
31
+ render: () => (
32
+ <View style={styles.grid}>
33
+ <InfoTile
34
+ title="Լռության ժամեր"
35
+ description="23:00 - 08:00"
36
+ icon={QuietHoursIcon}
37
+ style={styles.tile}
38
+ />
39
+ <InfoTile
40
+ title="Ծխելը արգելված է"
41
+ description="Տան ներսում խստիվ արգելվում է"
42
+ icon={NoSmokingIcon}
43
+ style={styles.tile}
44
+ />
45
+ <InfoTile
46
+ title="Ընտանի կենդանիներ"
47
+ description="Թույլատրված չեն"
48
+ icon={PawIcon}
49
+ style={styles.tile}
50
+ />
51
+ <InfoTile
52
+ title="Աղբի հեռացում"
53
+ description="Խնդրում ենք աղբը թողնել դրսի աղբամանում"
54
+ icon={TrashIcon}
55
+ style={styles.tile}
56
+ />
57
+ </View>
58
+ ),
59
+ };
60
+
61
+ const styles = StyleSheet.create({
62
+ frame: {
63
+ padding: 24,
64
+ backgroundColor: "#090909",
65
+ maxWidth: 420,
66
+ },
67
+ grid: {
68
+ padding: 24,
69
+ backgroundColor: "#090909",
70
+ flexDirection: "row",
71
+ flexWrap: "wrap",
72
+ gap: 12,
73
+ maxWidth: 720,
74
+ },
75
+ tile: {
76
+ flexGrow: 1,
77
+ flexBasis: "45%",
78
+ minWidth: 280,
79
+ },
80
+ });