@ecohouse/ui 0.1.31 → 0.1.33
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 +1594 -523
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +218 -10
- package/dist/index.d.ts +218 -10
- package/dist/index.js +1516 -471
- 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 +129 -54
- 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/Select/Select.tsx +5 -2
- package/src/components/Sidebar/Sidebar.tsx +25 -11
- 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/StepPager/StepPager.tsx +150 -0
- package/src/components/StepPager/index.ts +2 -0
- package/src/components/index.ts +21 -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/Ban/BanIcon.tsx +15 -0
- package/src/icons/Ban/banPath.ts +2 -0
- package/src/icons/Ban/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/Fullscreen/FullscreenIcon.tsx +21 -0
- package/src/icons/Fullscreen/fullscreenPath.ts +2 -0
- package/src/icons/Fullscreen/index.ts +1 -0
- package/src/icons/Home/homePath.ts +1 -1
- 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/Sort/SortIcon.tsx +3 -3
- package/src/icons/Sort/SortIcon.web.tsx +2 -2
- package/src/icons/Sort/sortPath.ts +3 -2
- package/src/icons/VideoOff/VideoOffIcon.tsx +21 -0
- package/src/icons/VideoOff/VideoOffIcon.web.tsx +27 -0
- package/src/icons/VideoOff/index.ts +1 -0
- package/src/icons/VideoOff/videoOffPath.ts +2 -0
- package/src/icons/ZoomOut/ZoomOutIcon.tsx +21 -0
- package/src/icons/ZoomOut/index.ts +1 -0
- package/src/icons/ZoomOut/zoomOutPath.ts +2 -0
- package/src/icons/index.ts +12 -0
- package/src/icons/registry.ts +42 -0
- package/src/index.ts +34 -0
- package/src/theme/colors.test.ts +1 -1
- package/src/theme/colors.ts +4 -0
- package/src/theme/typography.ts +5 -5
- package/src/utils/dateUtils.ts +25 -0
- package/src/utils/popoverAlign.ts +85 -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,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
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -61,6 +61,8 @@ type SelectBaseProps = Omit<ViewProps, "style" | "children"> & {
|
|
|
61
61
|
loading?: boolean;
|
|
62
62
|
/** Text shown when no options match. */
|
|
63
63
|
emptyText?: string;
|
|
64
|
+
/** Override color for the selected value text in the trigger. */
|
|
65
|
+
valueColor?: string;
|
|
64
66
|
style?: StyleProp<ViewStyle>;
|
|
65
67
|
className?: string;
|
|
66
68
|
};
|
|
@@ -264,6 +266,7 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
|
|
|
264
266
|
onSearchChange,
|
|
265
267
|
loading = false,
|
|
266
268
|
emptyText = "No results",
|
|
269
|
+
valueColor,
|
|
267
270
|
style,
|
|
268
271
|
className,
|
|
269
272
|
accessibilityLabel,
|
|
@@ -508,7 +511,7 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
|
|
|
508
511
|
|
|
509
512
|
const leftIconNode = renderStatefulIcon(leftIcon, UserIcon, {
|
|
510
513
|
size: ICON_SIZE,
|
|
511
|
-
color: error ? colors.red :
|
|
514
|
+
color: error ? colors.red : colors.primary,
|
|
512
515
|
});
|
|
513
516
|
const hasLeftIcon = showLeftIcon || leftIcon != null;
|
|
514
517
|
|
|
@@ -520,7 +523,7 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
|
|
|
520
523
|
const triggerTextColor = hasValue
|
|
521
524
|
? error
|
|
522
525
|
? colors.red
|
|
523
|
-
: colors.white
|
|
526
|
+
: (valueColor ?? colors.white)
|
|
524
527
|
: error
|
|
525
528
|
? colors.red
|
|
526
529
|
: colors.grey100;
|
|
@@ -59,6 +59,8 @@ export interface SidebarProps extends Omit<ViewProps, "style" | "children"> {
|
|
|
59
59
|
collapsed?: boolean;
|
|
60
60
|
defaultCollapsed?: boolean;
|
|
61
61
|
onCollapsedChange?: (collapsed: boolean) => void;
|
|
62
|
+
/** When false, hides the expand/collapse control. Defaults to `true`. */
|
|
63
|
+
showCollapseToggle?: boolean;
|
|
62
64
|
style?: StyleProp<ViewStyle>;
|
|
63
65
|
className?: string;
|
|
64
66
|
}
|
|
@@ -86,6 +88,7 @@ export const Sidebar = forwardRef<ComponentRef<typeof View>, SidebarProps>(funct
|
|
|
86
88
|
collapsed: collapsedProp,
|
|
87
89
|
defaultCollapsed = false,
|
|
88
90
|
onCollapsedChange,
|
|
91
|
+
showCollapseToggle = true,
|
|
89
92
|
style,
|
|
90
93
|
className,
|
|
91
94
|
...rest
|
|
@@ -136,7 +139,13 @@ export const Sidebar = forwardRef<ComponentRef<typeof View>, SidebarProps>(funct
|
|
|
136
139
|
style={[styles.root, { width: widthAnim }, style]}
|
|
137
140
|
accessibilityRole="menu"
|
|
138
141
|
>
|
|
139
|
-
<View
|
|
142
|
+
<View
|
|
143
|
+
style={[
|
|
144
|
+
styles.header,
|
|
145
|
+
collapsed ? styles.headerCollapsed : styles.headerExpanded,
|
|
146
|
+
!showCollapseToggle ? styles.headerNoToggle : null,
|
|
147
|
+
]}
|
|
148
|
+
>
|
|
140
149
|
<Animated.View
|
|
141
150
|
pointerEvents={collapsed ? "none" : "auto"}
|
|
142
151
|
style={[
|
|
@@ -157,16 +166,18 @@ export const Sidebar = forwardRef<ComponentRef<typeof View>, SidebarProps>(funct
|
|
|
157
166
|
/>
|
|
158
167
|
</Animated.View>
|
|
159
168
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
{showCollapseToggle ? (
|
|
170
|
+
<Pressable
|
|
171
|
+
accessibilityRole="button"
|
|
172
|
+
accessibilityLabel={collapsed ? expandAccessibilityLabel : collapseAccessibilityLabel}
|
|
173
|
+
accessibilityState={{ expanded: !collapsed }}
|
|
174
|
+
hitSlop={8}
|
|
175
|
+
onPress={() => setCollapsed(!collapsed)}
|
|
176
|
+
style={styles.toggleButton}
|
|
177
|
+
>
|
|
178
|
+
<SidebarIcon color={colors.grey100} size={TOGGLE_SIZE} strokeWidth={2} />
|
|
179
|
+
</Pressable>
|
|
180
|
+
) : null}
|
|
170
181
|
</View>
|
|
171
182
|
|
|
172
183
|
<View
|
|
@@ -242,6 +253,9 @@ const styles = StyleSheet.create({
|
|
|
242
253
|
justifyContent: "space-between",
|
|
243
254
|
paddingHorizontal: 24,
|
|
244
255
|
},
|
|
256
|
+
headerNoToggle: {
|
|
257
|
+
justifyContent: "flex-start",
|
|
258
|
+
},
|
|
245
259
|
headerCollapsed: {
|
|
246
260
|
justifyContent: "center",
|
|
247
261
|
paddingHorizontal: 0,
|
|
@@ -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
|
+
});
|