@ecohouse/ui 0.1.30 → 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.
- package/README.md +5 -0
- package/dist/index.cjs +1131 -339
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +147 -5
- package/dist/index.d.ts +147 -5
- package/dist/index.js +1058 -286
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/AccountHeader/AccountHeader.stories.tsx +70 -0
- package/src/components/AccountHeader/AccountHeader.tsx +224 -0
- package/src/components/AccountHeader/index.ts +2 -0
- package/src/components/AvatarSimple/AvatarSimple.tsx +2 -2
- package/src/components/Button/Button.tsx +4 -1
- package/src/components/DatePicker/DatePicker.tsx +14 -9
- package/src/components/FloatingActionButton/FloatingActionButton.stories.tsx +42 -0
- package/src/components/FloatingActionButton/FloatingActionButton.tsx +77 -0
- package/src/components/FloatingActionButton/index.ts +2 -0
- package/src/components/InfoTile/InfoTile.stories.tsx +80 -0
- package/src/components/InfoTile/InfoTile.tsx +118 -0
- package/src/components/InfoTile/index.ts +2 -0
- package/src/components/PaginationDots/PaginationDots.stories.tsx +48 -0
- package/src/components/PaginationDots/PaginationDots.tsx +111 -0
- package/src/components/PaginationDots/index.ts +2 -0
- package/src/components/Sidebar/Sidebar.tsx +9 -1
- package/src/components/StatusBadge/StatusBadge.stories.tsx +54 -0
- package/src/components/StatusBadge/StatusBadge.tsx +108 -0
- package/src/components/StatusBadge/index.ts +2 -0
- package/src/components/StepList/StepList.stories.tsx +37 -0
- package/src/components/StepList/StepList.tsx +152 -0
- package/src/components/StepList/index.ts +2 -0
- package/src/components/index.ts +18 -0
- package/src/icons/AlertTriangle/AlertTriangleIcon.tsx +19 -0
- package/src/icons/AlertTriangle/AlertTriangleIcon.web.tsx +18 -0
- package/src/icons/AlertTriangle/alertTrianglePath.ts +3 -0
- package/src/icons/AlertTriangle/index.ts +1 -0
- package/src/icons/Bell/BellIcon.tsx +2 -2
- package/src/icons/Bell/BellIcon.web.tsx +2 -2
- package/src/icons/Bell/bellPath.ts +1 -1
- package/src/icons/CottageDetail/CottageDetailIcons.tsx +15 -0
- package/src/icons/CottageDetail/CottageDetailIcons.web.tsx +15 -0
- package/src/icons/CottageDetail/cottageDetailPaths.ts +3 -0
- package/src/icons/CottageDetail/index.ts +1 -0
- package/src/icons/Edit/EditIcon.tsx +20 -0
- package/src/icons/Edit/EditIcon.web.tsx +19 -0
- package/src/icons/Edit/editPath.ts +3 -0
- package/src/icons/Edit/index.ts +1 -0
- package/src/icons/HouseRules/HouseRulesIcons.tsx +62 -0
- package/src/icons/HouseRules/HouseRulesIcons.web.tsx +61 -0
- package/src/icons/HouseRules/houseRulesPaths.ts +24 -0
- package/src/icons/HouseRules/index.ts +2 -0
- package/src/icons/LocationUser/LocationUserIcon.tsx +15 -0
- package/src/icons/LocationUser/LocationUserIcon.web.tsx +14 -0
- package/src/icons/LocationUser/index.ts +1 -0
- package/src/icons/LocationUser/locationUserPath.ts +2 -0
- package/src/icons/Lock/LockIcon.tsx +21 -0
- package/src/icons/Lock/LockIcon.web.tsx +26 -0
- package/src/icons/Lock/index.ts +1 -0
- package/src/icons/Lock/lockPath.ts +3 -0
- package/src/icons/Refresh/RefreshIcon.tsx +20 -0
- package/src/icons/Refresh/RefreshIcon.web.tsx +19 -0
- package/src/icons/Refresh/index.ts +1 -0
- package/src/icons/Refresh/refreshPath.ts +3 -0
- package/src/icons/RefundStatus/RefundStatusIcon.tsx +24 -0
- package/src/icons/RefundStatus/RefundStatusIcon.web.tsx +23 -0
- package/src/icons/RefundStatus/index.ts +1 -0
- package/src/icons/RefundStatus/refundStatusPath.ts +3 -0
- package/src/icons/index.ts +8 -0
- package/src/icons/registry.ts +30 -0
- package/src/index.ts +26 -0
- package/src/theme/colors.ts +4 -0
- package/src/theme/typography.ts +1 -1
- package/src/utils/dateUtils.ts +25 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { forwardRef, useMemo, useRef, type ComponentRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
StyleSheet,
|
|
4
|
+
Text,
|
|
5
|
+
View,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type ViewProps,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import type { IconComponent, IconProps } from "../../icons";
|
|
11
|
+
import { colors, fonts } from "../../theme";
|
|
12
|
+
import { mergeRefs } from "../../utils/mergeRefs";
|
|
13
|
+
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
14
|
+
|
|
15
|
+
export interface InfoTileProps extends Omit<ViewProps, "style" | "children"> {
|
|
16
|
+
title: string;
|
|
17
|
+
/** Optional supporting line under the title. */
|
|
18
|
+
description?: string;
|
|
19
|
+
icon: IconComponent;
|
|
20
|
+
iconProps?: Omit<IconProps, "size">;
|
|
21
|
+
iconSize?: number;
|
|
22
|
+
/** Icon slot fill. Defaults to `grey600`. */
|
|
23
|
+
iconSlotBackgroundColor?: string;
|
|
24
|
+
style?: StyleProp<ViewStyle>;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ICON_SLOT = 36;
|
|
29
|
+
const ICON_SIZE = 20;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Compact info tile — icon slot + title (+ optional description).
|
|
33
|
+
* Padding 12, radius 16, icon container 36×36.
|
|
34
|
+
*/
|
|
35
|
+
export const InfoTile = forwardRef<ComponentRef<typeof View>, InfoTileProps>(function InfoTile(
|
|
36
|
+
{
|
|
37
|
+
title,
|
|
38
|
+
description,
|
|
39
|
+
icon: Icon,
|
|
40
|
+
iconProps,
|
|
41
|
+
iconSize = ICON_SIZE,
|
|
42
|
+
iconSlotBackgroundColor = colors.grey600,
|
|
43
|
+
style,
|
|
44
|
+
className,
|
|
45
|
+
accessibilityLabel,
|
|
46
|
+
...rest
|
|
47
|
+
},
|
|
48
|
+
forwardedRef,
|
|
49
|
+
) {
|
|
50
|
+
const containerRef = useRef<ComponentRef<typeof View>>(null);
|
|
51
|
+
const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
|
|
52
|
+
const hasDescription = Boolean(description?.trim());
|
|
53
|
+
|
|
54
|
+
useApplyWebClassName(containerRef, className);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<View
|
|
58
|
+
{...rest}
|
|
59
|
+
ref={setContainerRef}
|
|
60
|
+
accessibilityRole="text"
|
|
61
|
+
accessibilityLabel={
|
|
62
|
+
accessibilityLabel ?? (hasDescription ? `${title}. ${description}` : title)
|
|
63
|
+
}
|
|
64
|
+
style={[styles.root, style]}
|
|
65
|
+
>
|
|
66
|
+
<View style={[styles.iconSlot, { backgroundColor: iconSlotBackgroundColor }]}>
|
|
67
|
+
<Icon {...iconProps} size={iconSize} color={iconProps?.color ?? colors.white} />
|
|
68
|
+
</View>
|
|
69
|
+
<View style={styles.textBlock}>
|
|
70
|
+
<Text style={styles.title}>{title}</Text>
|
|
71
|
+
{hasDescription ? <Text style={styles.description}>{description}</Text> : null}
|
|
72
|
+
</View>
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const styles = StyleSheet.create({
|
|
78
|
+
root: {
|
|
79
|
+
flexDirection: "row",
|
|
80
|
+
alignItems: "center",
|
|
81
|
+
alignSelf: "flex-start",
|
|
82
|
+
gap: 16,
|
|
83
|
+
padding: 12,
|
|
84
|
+
borderRadius: 16,
|
|
85
|
+
backgroundColor: colors.grey700,
|
|
86
|
+
},
|
|
87
|
+
iconSlot: {
|
|
88
|
+
width: ICON_SLOT,
|
|
89
|
+
height: ICON_SLOT,
|
|
90
|
+
padding: 8,
|
|
91
|
+
borderRadius: 8,
|
|
92
|
+
alignItems: "center",
|
|
93
|
+
justifyContent: "center",
|
|
94
|
+
flexShrink: 0,
|
|
95
|
+
},
|
|
96
|
+
textBlock: {
|
|
97
|
+
flexShrink: 1,
|
|
98
|
+
minWidth: 0,
|
|
99
|
+
flexDirection: "column",
|
|
100
|
+
gap: 6,
|
|
101
|
+
},
|
|
102
|
+
title: {
|
|
103
|
+
fontFamily: fonts.sans,
|
|
104
|
+
fontSize: 14,
|
|
105
|
+
fontWeight: "500",
|
|
106
|
+
lineHeight: 14,
|
|
107
|
+
letterSpacing: 0,
|
|
108
|
+
color: colors.white,
|
|
109
|
+
},
|
|
110
|
+
description: {
|
|
111
|
+
fontFamily: fonts.sans,
|
|
112
|
+
fontSize: 12,
|
|
113
|
+
fontWeight: "400",
|
|
114
|
+
lineHeight: 12,
|
|
115
|
+
letterSpacing: 0,
|
|
116
|
+
color: colors.lightGrey,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { StyleSheet, View } from "react-native";
|
|
4
|
+
import { PaginationDots } from "./PaginationDots";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/PaginationDots",
|
|
8
|
+
component: PaginationDots,
|
|
9
|
+
args: {
|
|
10
|
+
count: 4,
|
|
11
|
+
activeIndex: 0,
|
|
12
|
+
},
|
|
13
|
+
parameters: {
|
|
14
|
+
backgrounds: { default: "black" },
|
|
15
|
+
},
|
|
16
|
+
} satisfies Meta<typeof PaginationDots>;
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof meta>;
|
|
20
|
+
|
|
21
|
+
export const Default: Story = {
|
|
22
|
+
render: (args) => (
|
|
23
|
+
<View style={styles.frame}>
|
|
24
|
+
<PaginationDots {...args} />
|
|
25
|
+
</View>
|
|
26
|
+
),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function InteractivePaginationDots() {
|
|
30
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
31
|
+
return (
|
|
32
|
+
<View style={styles.frame}>
|
|
33
|
+
<PaginationDots count={4} activeIndex={activeIndex} onDotPress={setActiveIndex} />
|
|
34
|
+
</View>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const Interactive: Story = {
|
|
39
|
+
render: () => <InteractivePaginationDots />,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const styles = StyleSheet.create({
|
|
43
|
+
frame: {
|
|
44
|
+
padding: 24,
|
|
45
|
+
backgroundColor: "#090909",
|
|
46
|
+
alignItems: "center",
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { forwardRef, useMemo, useRef, type ComponentRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
View,
|
|
6
|
+
type StyleProp,
|
|
7
|
+
type ViewProps,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import { colors } from "../../theme";
|
|
11
|
+
import { mergeRefs } from "../../utils/mergeRefs";
|
|
12
|
+
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
13
|
+
|
|
14
|
+
export interface PaginationDotsProps extends Omit<ViewProps, "style" | "children"> {
|
|
15
|
+
/** Total number of steps / slides. */
|
|
16
|
+
count: number;
|
|
17
|
+
/** Zero-based active index. */
|
|
18
|
+
activeIndex: number;
|
|
19
|
+
/** Called when a dot is pressed. */
|
|
20
|
+
onDotPress?: (index: number) => void;
|
|
21
|
+
/** Accessible name for the control group. */
|
|
22
|
+
accessibilityLabel?: string;
|
|
23
|
+
style?: StyleProp<ViewStyle>;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Total width of the active pill + inactive dots + gaps. */
|
|
28
|
+
const TRACK_WIDTH = 48;
|
|
29
|
+
const DOT_HEIGHT = 6;
|
|
30
|
+
const INACTIVE_SIZE = 6;
|
|
31
|
+
const GAP = 4;
|
|
32
|
+
|
|
33
|
+
function activePillWidth(count: number) {
|
|
34
|
+
if (count <= 1) return TRACK_WIDTH;
|
|
35
|
+
const inactiveCount = count - 1;
|
|
36
|
+
return TRACK_WIDTH - inactiveCount * INACTIVE_SIZE - inactiveCount * GAP;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Carousel / stepper pagination.
|
|
41
|
+
* Whole track is `48` wide; height `6`; gap `4`; inactive dots `6×6`;
|
|
42
|
+
* active pill takes the remaining width.
|
|
43
|
+
*/
|
|
44
|
+
export const PaginationDots = forwardRef<ComponentRef<typeof View>, PaginationDotsProps>(
|
|
45
|
+
function PaginationDots(
|
|
46
|
+
{ count, activeIndex, onDotPress, accessibilityLabel, style, className, ...rest },
|
|
47
|
+
forwardedRef,
|
|
48
|
+
) {
|
|
49
|
+
const containerRef = useRef<ComponentRef<typeof View>>(null);
|
|
50
|
+
const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
|
|
51
|
+
const safeCount = Math.max(0, count);
|
|
52
|
+
const safeActive = safeCount === 0 ? 0 : Math.min(Math.max(activeIndex, 0), safeCount - 1);
|
|
53
|
+
const pillWidth = Math.max(INACTIVE_SIZE, activePillWidth(safeCount));
|
|
54
|
+
|
|
55
|
+
useApplyWebClassName(containerRef, className);
|
|
56
|
+
|
|
57
|
+
if (safeCount <= 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<View
|
|
63
|
+
{...rest}
|
|
64
|
+
ref={setContainerRef}
|
|
65
|
+
accessibilityRole="adjustable"
|
|
66
|
+
accessibilityLabel={accessibilityLabel ?? `Slide ${safeActive + 1} of ${safeCount}`}
|
|
67
|
+
style={[styles.root, style]}
|
|
68
|
+
>
|
|
69
|
+
{Array.from({ length: safeCount }, (_, index) => {
|
|
70
|
+
const active = index === safeActive;
|
|
71
|
+
return (
|
|
72
|
+
<Pressable
|
|
73
|
+
key={index}
|
|
74
|
+
accessibilityRole="button"
|
|
75
|
+
accessibilityLabel={`Go to slide ${index + 1}`}
|
|
76
|
+
accessibilityState={{ selected: active }}
|
|
77
|
+
disabled={onDotPress == null}
|
|
78
|
+
hitSlop={8}
|
|
79
|
+
onPress={() => onDotPress?.(index)}
|
|
80
|
+
style={[
|
|
81
|
+
styles.dot,
|
|
82
|
+
active ? [styles.dotActive, { width: pillWidth }] : styles.dotInactive,
|
|
83
|
+
]}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
})}
|
|
87
|
+
</View>
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const styles = StyleSheet.create({
|
|
93
|
+
root: {
|
|
94
|
+
width: TRACK_WIDTH,
|
|
95
|
+
flexDirection: "row",
|
|
96
|
+
alignItems: "center",
|
|
97
|
+
justifyContent: "center",
|
|
98
|
+
gap: GAP,
|
|
99
|
+
},
|
|
100
|
+
dot: {
|
|
101
|
+
height: DOT_HEIGHT,
|
|
102
|
+
borderRadius: DOT_HEIGHT,
|
|
103
|
+
},
|
|
104
|
+
dotActive: {
|
|
105
|
+
backgroundColor: colors.white,
|
|
106
|
+
},
|
|
107
|
+
dotInactive: {
|
|
108
|
+
width: INACTIVE_SIZE,
|
|
109
|
+
backgroundColor: colors.whiteAlpha40,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
@@ -218,7 +218,10 @@ export const Sidebar = forwardRef<ComponentRef<typeof View>, SidebarProps>(funct
|
|
|
218
218
|
|
|
219
219
|
const styles = StyleSheet.create({
|
|
220
220
|
root: {
|
|
221
|
+
flexDirection: "column",
|
|
222
|
+
alignSelf: "stretch",
|
|
221
223
|
height: "100%",
|
|
224
|
+
minHeight: "100%",
|
|
222
225
|
flexShrink: 0,
|
|
223
226
|
backgroundColor: colors.dark,
|
|
224
227
|
borderRightWidth: 1,
|
|
@@ -257,8 +260,11 @@ const styles = StyleSheet.create({
|
|
|
257
260
|
flexShrink: 0,
|
|
258
261
|
},
|
|
259
262
|
nav: {
|
|
260
|
-
|
|
263
|
+
flexGrow: 1,
|
|
264
|
+
flexShrink: 1,
|
|
265
|
+
flexBasis: 0,
|
|
261
266
|
width: "100%",
|
|
267
|
+
minHeight: 0,
|
|
262
268
|
padding: 16,
|
|
263
269
|
gap: 10,
|
|
264
270
|
},
|
|
@@ -291,7 +297,9 @@ const styles = StyleSheet.create({
|
|
|
291
297
|
footer: {
|
|
292
298
|
height: PROFILE_SECTION_HEIGHT,
|
|
293
299
|
width: "100%",
|
|
300
|
+
flexGrow: 0,
|
|
294
301
|
flexShrink: 0,
|
|
302
|
+
marginTop: "auto",
|
|
295
303
|
alignItems: "center",
|
|
296
304
|
justifyContent: "center",
|
|
297
305
|
paddingVertical: 12,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
|
+
import { StatusBadge } from "./StatusBadge";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Components/StatusBadge",
|
|
7
|
+
component: StatusBadge,
|
|
8
|
+
args: {
|
|
9
|
+
label: "Ակտիվ",
|
|
10
|
+
tone: "active",
|
|
11
|
+
},
|
|
12
|
+
parameters: {
|
|
13
|
+
backgrounds: { default: "black" },
|
|
14
|
+
},
|
|
15
|
+
} satisfies Meta<typeof StatusBadge>;
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
export const Active: Story = {};
|
|
21
|
+
|
|
22
|
+
export const Completed: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
label: "Ավարտված",
|
|
25
|
+
tone: "completed",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Cancelled: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
label: "Չեղարկված",
|
|
32
|
+
tone: "cancelled",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const AllTones: Story = {
|
|
37
|
+
render: () => (
|
|
38
|
+
<View style={styles.row}>
|
|
39
|
+
<StatusBadge label="Ակտիվ" tone="active" />
|
|
40
|
+
<StatusBadge label="Ավարտված" tone="completed" />
|
|
41
|
+
<StatusBadge label="Չեղարկված" tone="cancelled" />
|
|
42
|
+
</View>
|
|
43
|
+
),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const styles = StyleSheet.create({
|
|
47
|
+
row: {
|
|
48
|
+
flexDirection: "row",
|
|
49
|
+
flexWrap: "wrap",
|
|
50
|
+
gap: 12,
|
|
51
|
+
padding: 24,
|
|
52
|
+
backgroundColor: "#090909",
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { forwardRef, useMemo, useRef, type ComponentRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
Text,
|
|
6
|
+
View,
|
|
7
|
+
type StyleProp,
|
|
8
|
+
type ViewProps,
|
|
9
|
+
type ViewStyle,
|
|
10
|
+
} from "react-native";
|
|
11
|
+
import { colors, fonts } from "../../theme";
|
|
12
|
+
import { mergeRefs } from "../../utils/mergeRefs";
|
|
13
|
+
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
14
|
+
|
|
15
|
+
export type StatusBadgeTone = "active" | "completed" | "cancelled";
|
|
16
|
+
|
|
17
|
+
export interface StatusBadgeProps extends Omit<ViewProps, "style" | "children"> {
|
|
18
|
+
label: string;
|
|
19
|
+
tone?: StatusBadgeTone;
|
|
20
|
+
style?: StyleProp<ViewStyle>;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const toneConfig = {
|
|
25
|
+
active: {
|
|
26
|
+
backgroundColor: colors.greenMuted,
|
|
27
|
+
color: colors.green,
|
|
28
|
+
},
|
|
29
|
+
completed: {
|
|
30
|
+
backgroundColor: colors.greyMuted,
|
|
31
|
+
color: colors.grey0,
|
|
32
|
+
},
|
|
33
|
+
cancelled: {
|
|
34
|
+
backgroundColor: colors.cancelledMuted,
|
|
35
|
+
color: colors.red,
|
|
36
|
+
},
|
|
37
|
+
} as const satisfies Record<StatusBadgeTone, { backgroundColor: string; color: string }>;
|
|
38
|
+
|
|
39
|
+
const webBlurStyle: ViewStyle =
|
|
40
|
+
Platform.OS === "web"
|
|
41
|
+
? ({
|
|
42
|
+
backdropFilter: "blur(39.5px)",
|
|
43
|
+
WebkitBackdropFilter: "blur(39.5px)",
|
|
44
|
+
} as ViewStyle)
|
|
45
|
+
: {};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compact status chip (e.g. booking active / completed / cancelled).
|
|
49
|
+
* Height 28, pill radius, Semibold 12 — distinct from amenity `Badge`.
|
|
50
|
+
*/
|
|
51
|
+
export const StatusBadge = forwardRef<ComponentRef<typeof View>, StatusBadgeProps>(
|
|
52
|
+
function StatusBadge(
|
|
53
|
+
{ label, tone = "active", style, className, accessibilityLabel, ...rest },
|
|
54
|
+
forwardedRef,
|
|
55
|
+
) {
|
|
56
|
+
const containerRef = useRef<ComponentRef<typeof View>>(null);
|
|
57
|
+
const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
|
|
58
|
+
const preset = toneConfig[tone];
|
|
59
|
+
|
|
60
|
+
useApplyWebClassName(containerRef, className);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<View
|
|
64
|
+
{...rest}
|
|
65
|
+
ref={setContainerRef}
|
|
66
|
+
accessibilityRole="text"
|
|
67
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
68
|
+
style={[styles.base, { backgroundColor: preset.backgroundColor }, webBlurStyle, style]}
|
|
69
|
+
>
|
|
70
|
+
<Text
|
|
71
|
+
style={[
|
|
72
|
+
styles.label,
|
|
73
|
+
{ color: preset.color },
|
|
74
|
+
Platform.OS === "android" ? styles.labelAndroid : null,
|
|
75
|
+
]}
|
|
76
|
+
>
|
|
77
|
+
{label}
|
|
78
|
+
</Text>
|
|
79
|
+
</View>
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const styles = StyleSheet.create({
|
|
85
|
+
base: {
|
|
86
|
+
height: 28,
|
|
87
|
+
minHeight: 28,
|
|
88
|
+
flexDirection: "row",
|
|
89
|
+
alignItems: "center",
|
|
90
|
+
alignSelf: "flex-start",
|
|
91
|
+
justifyContent: "center",
|
|
92
|
+
gap: 6,
|
|
93
|
+
paddingVertical: 6,
|
|
94
|
+
paddingHorizontal: 12,
|
|
95
|
+
borderRadius: 50,
|
|
96
|
+
overflow: "hidden",
|
|
97
|
+
},
|
|
98
|
+
label: {
|
|
99
|
+
fontFamily: fonts.sans,
|
|
100
|
+
fontSize: 12,
|
|
101
|
+
fontWeight: "600",
|
|
102
|
+
lineHeight: 12,
|
|
103
|
+
letterSpacing: 0,
|
|
104
|
+
},
|
|
105
|
+
labelAndroid: {
|
|
106
|
+
includeFontPadding: false,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
|
+
import { StepList } from "./StepList";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Components/StepList",
|
|
7
|
+
component: StepList,
|
|
8
|
+
args: {
|
|
9
|
+
steps: [
|
|
10
|
+
"Ստացեք ակտիվ QR կոդը մուտքի օրը՝ ժամը 10:00-ին, այս էջի «Թվային բանալի» բաժնում։",
|
|
11
|
+
"Մոտեցեք քոթեջի մուտքի դռան սարքին (սկաներին) և պահեք հեռախոսի էկրանը։",
|
|
12
|
+
"Հաջող սկանավորումից հետո դուռը ավտոմատ կապակողպվի։",
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
parameters: {
|
|
16
|
+
backgrounds: { default: "black" },
|
|
17
|
+
},
|
|
18
|
+
} satisfies Meta<typeof StepList>;
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof meta>;
|
|
22
|
+
|
|
23
|
+
export const Default: Story = {
|
|
24
|
+
render: (args) => (
|
|
25
|
+
<View style={styles.frame}>
|
|
26
|
+
<StepList {...args} />
|
|
27
|
+
</View>
|
|
28
|
+
),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const styles = StyleSheet.create({
|
|
32
|
+
frame: {
|
|
33
|
+
padding: 24,
|
|
34
|
+
maxWidth: 560,
|
|
35
|
+
backgroundColor: "#090909",
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { forwardRef, useMemo, useRef, type ComponentRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
Text,
|
|
6
|
+
View,
|
|
7
|
+
type StyleProp,
|
|
8
|
+
type ViewProps,
|
|
9
|
+
type ViewStyle,
|
|
10
|
+
} from "react-native";
|
|
11
|
+
import { colors, fonts } from "../../theme";
|
|
12
|
+
import { mergeRefs } from "../../utils/mergeRefs";
|
|
13
|
+
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
14
|
+
|
|
15
|
+
export interface StepListProps extends Omit<ViewProps, "style" | "children"> {
|
|
16
|
+
/** Step descriptions, numbered automatically from 1. */
|
|
17
|
+
steps: string[];
|
|
18
|
+
style?: StyleProp<ViewStyle>;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const BADGE_SIZE = 42;
|
|
23
|
+
const ROW_GAP = 12;
|
|
24
|
+
const CONNECTOR_WIDTH = 2;
|
|
25
|
+
const CONNECTOR_HEIGHT = 19;
|
|
26
|
+
const CONNECTOR_GAP = 8;
|
|
27
|
+
|
|
28
|
+
const webBlurStyle: ViewStyle =
|
|
29
|
+
Platform.OS === "web"
|
|
30
|
+
? ({
|
|
31
|
+
backdropFilter: "blur(20px)",
|
|
32
|
+
WebkitBackdropFilter: "blur(20px)",
|
|
33
|
+
} as ViewStyle)
|
|
34
|
+
: {};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Vertical numbered process list — 42px badges, 2×19 connectors with 8px gaps.
|
|
38
|
+
*/
|
|
39
|
+
export const StepList = forwardRef<ComponentRef<typeof View>, StepListProps>(function StepList(
|
|
40
|
+
{ steps, style, className, accessibilityLabel, ...rest },
|
|
41
|
+
forwardedRef,
|
|
42
|
+
) {
|
|
43
|
+
const containerRef = useRef<ComponentRef<typeof View>>(null);
|
|
44
|
+
const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
|
|
45
|
+
|
|
46
|
+
useApplyWebClassName(containerRef, className);
|
|
47
|
+
|
|
48
|
+
if (steps.length === 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<View
|
|
54
|
+
{...rest}
|
|
55
|
+
ref={setContainerRef}
|
|
56
|
+
accessibilityRole="summary"
|
|
57
|
+
accessibilityLabel={accessibilityLabel}
|
|
58
|
+
style={[styles.root, style]}
|
|
59
|
+
>
|
|
60
|
+
{steps.map((description, index) => {
|
|
61
|
+
const isLast = index === steps.length - 1;
|
|
62
|
+
const number = index + 1;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<View key={`${number}-${description}`} style={styles.stepBlock}>
|
|
66
|
+
<View style={styles.row}>
|
|
67
|
+
<View style={[styles.badge, webBlurStyle]}>
|
|
68
|
+
<Text
|
|
69
|
+
style={[
|
|
70
|
+
styles.badgeLabel,
|
|
71
|
+
Platform.OS === "android" ? styles.labelAndroid : null,
|
|
72
|
+
]}
|
|
73
|
+
>
|
|
74
|
+
{String(number)}
|
|
75
|
+
</Text>
|
|
76
|
+
</View>
|
|
77
|
+
<Text
|
|
78
|
+
style={[styles.description, Platform.OS === "android" ? styles.labelAndroid : null]}
|
|
79
|
+
>
|
|
80
|
+
{description}
|
|
81
|
+
</Text>
|
|
82
|
+
</View>
|
|
83
|
+
|
|
84
|
+
{!isLast ? (
|
|
85
|
+
<View style={styles.connectorSlot}>
|
|
86
|
+
<View style={styles.connector} />
|
|
87
|
+
</View>
|
|
88
|
+
) : null}
|
|
89
|
+
</View>
|
|
90
|
+
);
|
|
91
|
+
})}
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const styles = StyleSheet.create({
|
|
97
|
+
root: {
|
|
98
|
+
width: "100%",
|
|
99
|
+
},
|
|
100
|
+
stepBlock: {
|
|
101
|
+
width: "100%",
|
|
102
|
+
},
|
|
103
|
+
row: {
|
|
104
|
+
flexDirection: "row",
|
|
105
|
+
alignItems: "center",
|
|
106
|
+
gap: ROW_GAP,
|
|
107
|
+
width: "100%",
|
|
108
|
+
},
|
|
109
|
+
badge: {
|
|
110
|
+
width: BADGE_SIZE,
|
|
111
|
+
height: BADGE_SIZE,
|
|
112
|
+
borderRadius: 79,
|
|
113
|
+
backgroundColor: colors.grey600,
|
|
114
|
+
alignItems: "center",
|
|
115
|
+
justifyContent: "center",
|
|
116
|
+
flexShrink: 0,
|
|
117
|
+
overflow: "hidden",
|
|
118
|
+
},
|
|
119
|
+
badgeLabel: {
|
|
120
|
+
fontFamily: fonts.sans,
|
|
121
|
+
fontSize: 14,
|
|
122
|
+
fontWeight: "500",
|
|
123
|
+
lineHeight: 14,
|
|
124
|
+
letterSpacing: 0,
|
|
125
|
+
color: colors.white,
|
|
126
|
+
textAlign: "center",
|
|
127
|
+
},
|
|
128
|
+
description: {
|
|
129
|
+
flex: 1,
|
|
130
|
+
minWidth: 0,
|
|
131
|
+
fontFamily: fonts.sans,
|
|
132
|
+
fontSize: 14,
|
|
133
|
+
fontWeight: "400",
|
|
134
|
+
lineHeight: 14,
|
|
135
|
+
letterSpacing: 0,
|
|
136
|
+
color: colors.white,
|
|
137
|
+
},
|
|
138
|
+
connectorSlot: {
|
|
139
|
+
width: BADGE_SIZE,
|
|
140
|
+
alignItems: "center",
|
|
141
|
+
paddingVertical: CONNECTOR_GAP,
|
|
142
|
+
},
|
|
143
|
+
connector: {
|
|
144
|
+
width: CONNECTOR_WIDTH,
|
|
145
|
+
height: CONNECTOR_HEIGHT,
|
|
146
|
+
borderRadius: 98,
|
|
147
|
+
backgroundColor: colors.grey600,
|
|
148
|
+
},
|
|
149
|
+
labelAndroid: {
|
|
150
|
+
includeFontPadding: false,
|
|
151
|
+
},
|
|
152
|
+
});
|