@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.
Files changed (72) hide show
  1. package/README.md +5 -0
  2. package/dist/index.cjs +1131 -339
  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 +1058 -286
  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/Sidebar/Sidebar.tsx +9 -1
  25. package/src/components/StatusBadge/StatusBadge.stories.tsx +54 -0
  26. package/src/components/StatusBadge/StatusBadge.tsx +108 -0
  27. package/src/components/StatusBadge/index.ts +2 -0
  28. package/src/components/StepList/StepList.stories.tsx +37 -0
  29. package/src/components/StepList/StepList.tsx +152 -0
  30. package/src/components/StepList/index.ts +2 -0
  31. package/src/components/index.ts +18 -0
  32. package/src/icons/AlertTriangle/AlertTriangleIcon.tsx +19 -0
  33. package/src/icons/AlertTriangle/AlertTriangleIcon.web.tsx +18 -0
  34. package/src/icons/AlertTriangle/alertTrianglePath.ts +3 -0
  35. package/src/icons/AlertTriangle/index.ts +1 -0
  36. package/src/icons/Bell/BellIcon.tsx +2 -2
  37. package/src/icons/Bell/BellIcon.web.tsx +2 -2
  38. package/src/icons/Bell/bellPath.ts +1 -1
  39. package/src/icons/CottageDetail/CottageDetailIcons.tsx +15 -0
  40. package/src/icons/CottageDetail/CottageDetailIcons.web.tsx +15 -0
  41. package/src/icons/CottageDetail/cottageDetailPaths.ts +3 -0
  42. package/src/icons/CottageDetail/index.ts +1 -0
  43. package/src/icons/Edit/EditIcon.tsx +20 -0
  44. package/src/icons/Edit/EditIcon.web.tsx +19 -0
  45. package/src/icons/Edit/editPath.ts +3 -0
  46. package/src/icons/Edit/index.ts +1 -0
  47. package/src/icons/HouseRules/HouseRulesIcons.tsx +62 -0
  48. package/src/icons/HouseRules/HouseRulesIcons.web.tsx +61 -0
  49. package/src/icons/HouseRules/houseRulesPaths.ts +24 -0
  50. package/src/icons/HouseRules/index.ts +2 -0
  51. package/src/icons/LocationUser/LocationUserIcon.tsx +15 -0
  52. package/src/icons/LocationUser/LocationUserIcon.web.tsx +14 -0
  53. package/src/icons/LocationUser/index.ts +1 -0
  54. package/src/icons/LocationUser/locationUserPath.ts +2 -0
  55. package/src/icons/Lock/LockIcon.tsx +21 -0
  56. package/src/icons/Lock/LockIcon.web.tsx +26 -0
  57. package/src/icons/Lock/index.ts +1 -0
  58. package/src/icons/Lock/lockPath.ts +3 -0
  59. package/src/icons/Refresh/RefreshIcon.tsx +20 -0
  60. package/src/icons/Refresh/RefreshIcon.web.tsx +19 -0
  61. package/src/icons/Refresh/index.ts +1 -0
  62. package/src/icons/Refresh/refreshPath.ts +3 -0
  63. package/src/icons/RefundStatus/RefundStatusIcon.tsx +24 -0
  64. package/src/icons/RefundStatus/RefundStatusIcon.web.tsx +23 -0
  65. package/src/icons/RefundStatus/index.ts +1 -0
  66. package/src/icons/RefundStatus/refundStatusPath.ts +3 -0
  67. package/src/icons/index.ts +8 -0
  68. package/src/icons/registry.ts +30 -0
  69. package/src/index.ts +26 -0
  70. package/src/theme/colors.ts +4 -0
  71. package/src/theme/typography.ts +1 -1
  72. 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,2 @@
1
+ export { InfoTile } from "./InfoTile";
2
+ export type { InfoTileProps } from "./InfoTile";
@@ -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
+ });
@@ -0,0 +1,2 @@
1
+ export { PaginationDots } from "./PaginationDots";
2
+ export type { PaginationDotsProps } from "./PaginationDots";
@@ -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
- flex: 1,
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,2 @@
1
+ export { StatusBadge } from "./StatusBadge";
2
+ export type { StatusBadgeProps, StatusBadgeTone } from "./StatusBadge";
@@ -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
+ });
@@ -0,0 +1,2 @@
1
+ export { StepList } from "./StepList";
2
+ export type { StepListProps } from "./StepList";