@ecohouse/ui 0.1.36 → 0.1.39
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 +2 -0
- package/dist/index.cjs +1169 -428
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -2
- package/dist/index.d.ts +101 -2
- package/dist/index.js +1092 -358
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/components/Accordion/Accordion.stories.tsx +47 -0
- package/src/components/Accordion/Accordion.tsx +165 -0
- package/src/components/Accordion/index.ts +2 -0
- package/src/components/AutocompleteInput/AutocompleteInput.stories.tsx +80 -0
- package/src/components/AutocompleteInput/AutocompleteInput.tsx +313 -0
- package/src/components/AutocompleteInput/index.ts +2 -0
- package/src/components/Counter/Counter.tsx +61 -26
- package/src/components/Drawer/Drawer.stories.tsx +79 -0
- package/src/components/Drawer/Drawer.tsx +239 -0
- package/src/components/Drawer/index.ts +2 -0
- package/src/components/QuantityInput/QuantityInput.stories.tsx +45 -0
- package/src/components/QuantityInput/QuantityInput.tsx +153 -0
- package/src/components/QuantityInput/index.ts +2 -0
- package/src/components/StatusBadge/StatusBadge.tsx +1 -1
- package/src/components/index.ts +12 -0
- package/src/icons/ArrowLeft/ArrowLeftIcon.test.ts +10 -0
- package/src/icons/ArrowLeft/ArrowLeftIcon.tsx +21 -0
- package/src/icons/ArrowLeft/ArrowLeftIcon.web.tsx +26 -0
- package/src/icons/ArrowLeft/arrowLeftPath.ts +3 -0
- package/src/icons/ArrowLeft/index.ts +1 -0
- package/src/icons/CalendarMinus/CalendarMinusIcon.tsx +25 -0
- package/src/icons/CalendarMinus/CalendarMinusIcon.web.tsx +31 -0
- package/src/icons/CalendarMinus/calendarMinusPath.ts +3 -0
- package/src/icons/CalendarMinus/index.ts +1 -0
- package/src/icons/CurrentLocation/CurrentLocationIcon.test.ts +10 -0
- package/src/icons/CurrentLocation/CurrentLocationIcon.tsx +25 -0
- package/src/icons/CurrentLocation/CurrentLocationIcon.web.tsx +30 -0
- package/src/icons/CurrentLocation/currentLocationPath.ts +3 -0
- package/src/icons/CurrentLocation/index.ts +1 -0
- package/src/icons/index.ts +3 -0
- package/src/icons/registry.ts +3 -0
- package/src/index.ts +12 -0
- package/src/theme/colors.test.ts +1 -0
- package/src/theme/colors.ts +2 -0
- package/src/web/styles/account-header.css +22 -0
|
@@ -23,6 +23,8 @@ export interface CounterProps extends Omit<ViewProps, "style" | "children"> {
|
|
|
23
23
|
max?: number;
|
|
24
24
|
step?: number;
|
|
25
25
|
disabled?: boolean;
|
|
26
|
+
/** Places the value before both controls for use inside input-like fields. */
|
|
27
|
+
layout?: "controls-around" | "value-first";
|
|
26
28
|
style?: StyleProp<ViewStyle>;
|
|
27
29
|
className?: string;
|
|
28
30
|
hitSlop?: PressableProps["hitSlop"];
|
|
@@ -51,6 +53,7 @@ export const Counter = forwardRef<ComponentRef<typeof View>, CounterProps>(funct
|
|
|
51
53
|
max,
|
|
52
54
|
step = 1,
|
|
53
55
|
disabled = false,
|
|
56
|
+
layout = "controls-around",
|
|
54
57
|
style,
|
|
55
58
|
className,
|
|
56
59
|
hitSlop,
|
|
@@ -78,33 +81,57 @@ export const Counter = forwardRef<ComponentRef<typeof View>, CounterProps>(funct
|
|
|
78
81
|
onValueChange?.(next);
|
|
79
82
|
};
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
84
|
+
const decrement = (
|
|
85
|
+
<Pressable
|
|
86
|
+
disabled={decrementDisabled}
|
|
87
|
+
hitSlop={hitSlop}
|
|
88
|
+
onPress={() => updateValue(resolvedValue - step)}
|
|
89
|
+
accessibilityRole="button"
|
|
90
|
+
accessibilityLabel="Decrease value"
|
|
91
|
+
accessibilityState={{ disabled: decrementDisabled }}
|
|
92
|
+
style={[styles.button, webBlurStyle, decrementDisabled && styles.buttonDisabled]}
|
|
93
|
+
>
|
|
94
|
+
<MinusIcon />
|
|
95
|
+
</Pressable>
|
|
96
|
+
);
|
|
97
|
+
const increment = (
|
|
98
|
+
<Pressable
|
|
99
|
+
disabled={incrementDisabled}
|
|
100
|
+
hitSlop={hitSlop}
|
|
101
|
+
onPress={() => updateValue(resolvedValue + step)}
|
|
102
|
+
accessibilityRole="button"
|
|
103
|
+
accessibilityLabel="Increase value"
|
|
104
|
+
accessibilityState={{ disabled: incrementDisabled }}
|
|
105
|
+
style={[styles.button, webBlurStyle, incrementDisabled && styles.buttonDisabled]}
|
|
106
|
+
>
|
|
107
|
+
<PlusIcon />
|
|
108
|
+
</Pressable>
|
|
109
|
+
);
|
|
110
|
+
const valueNode = (
|
|
111
|
+
<Text style={[styles.value, layout === "value-first" && styles.leadingValue]}>
|
|
112
|
+
{resolvedValue}
|
|
113
|
+
</Text>
|
|
114
|
+
);
|
|
96
115
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
116
|
+
return (
|
|
117
|
+
<View
|
|
118
|
+
{...rest}
|
|
119
|
+
ref={setContainerRef}
|
|
120
|
+
style={[styles.base, layout === "value-first" && styles.valueFirst, style]}
|
|
121
|
+
>
|
|
122
|
+
{layout === "value-first" ? (
|
|
123
|
+
<>
|
|
124
|
+
{valueNode}
|
|
125
|
+
{decrement}
|
|
126
|
+
{increment}
|
|
127
|
+
</>
|
|
128
|
+
) : (
|
|
129
|
+
<>
|
|
130
|
+
{decrement}
|
|
131
|
+
{valueNode}
|
|
132
|
+
{increment}
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
108
135
|
</View>
|
|
109
136
|
);
|
|
110
137
|
});
|
|
@@ -116,6 +143,11 @@ const styles = StyleSheet.create({
|
|
|
116
143
|
alignSelf: "flex-start",
|
|
117
144
|
gap: 17,
|
|
118
145
|
},
|
|
146
|
+
valueFirst: {
|
|
147
|
+
alignSelf: "stretch",
|
|
148
|
+
flex: 1,
|
|
149
|
+
gap: 6,
|
|
150
|
+
},
|
|
119
151
|
button: {
|
|
120
152
|
width: BUTTON_SIZE,
|
|
121
153
|
height: BUTTON_SIZE,
|
|
@@ -132,4 +164,7 @@ const styles = StyleSheet.create({
|
|
|
132
164
|
...counterTypography,
|
|
133
165
|
color: colors.white,
|
|
134
166
|
},
|
|
167
|
+
leadingValue: {
|
|
168
|
+
flex: 1,
|
|
169
|
+
},
|
|
135
170
|
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
4
|
+
import { colors, fonts } from "../../theme";
|
|
5
|
+
import { Drawer } from "./Drawer";
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "Components/Drawer",
|
|
9
|
+
component: Drawer,
|
|
10
|
+
parameters: {
|
|
11
|
+
backgrounds: { default: "black" },
|
|
12
|
+
},
|
|
13
|
+
} satisfies Meta<typeof Drawer>;
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof meta>;
|
|
17
|
+
|
|
18
|
+
function DrawerDemo() {
|
|
19
|
+
const [open, setOpen] = useState(true);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<View style={styles.page}>
|
|
23
|
+
<Pressable onPress={() => setOpen(true)} style={styles.openButton}>
|
|
24
|
+
<Text style={styles.openLabel}>Open drawer</Text>
|
|
25
|
+
</Pressable>
|
|
26
|
+
|
|
27
|
+
<Drawer
|
|
28
|
+
open={open}
|
|
29
|
+
onClose={() => setOpen(false)}
|
|
30
|
+
title="Չեղարկված ամրագրումներ"
|
|
31
|
+
closeAccessibilityLabel="Close drawer"
|
|
32
|
+
>
|
|
33
|
+
<View style={styles.body}>
|
|
34
|
+
<Text style={styles.bodyText}>Drawer body content</Text>
|
|
35
|
+
</View>
|
|
36
|
+
</Drawer>
|
|
37
|
+
</View>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const Default: Story = {
|
|
42
|
+
args: {
|
|
43
|
+
open: true,
|
|
44
|
+
onClose: () => undefined,
|
|
45
|
+
children: null,
|
|
46
|
+
},
|
|
47
|
+
render: () => <DrawerDemo />,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const styles = StyleSheet.create({
|
|
51
|
+
page: {
|
|
52
|
+
minHeight: 360,
|
|
53
|
+
alignItems: "center",
|
|
54
|
+
justifyContent: "center",
|
|
55
|
+
padding: 24,
|
|
56
|
+
},
|
|
57
|
+
openButton: {
|
|
58
|
+
paddingHorizontal: 16,
|
|
59
|
+
paddingVertical: 10,
|
|
60
|
+
borderRadius: 40,
|
|
61
|
+
backgroundColor: colors.primary,
|
|
62
|
+
},
|
|
63
|
+
openLabel: {
|
|
64
|
+
fontFamily: fonts.sans,
|
|
65
|
+
fontSize: 14,
|
|
66
|
+
fontWeight: "600",
|
|
67
|
+
color: colors.white,
|
|
68
|
+
},
|
|
69
|
+
body: {
|
|
70
|
+
flex: 1,
|
|
71
|
+
padding: 24,
|
|
72
|
+
gap: 24,
|
|
73
|
+
},
|
|
74
|
+
bodyText: {
|
|
75
|
+
fontFamily: fonts.sans,
|
|
76
|
+
fontSize: 14,
|
|
77
|
+
color: colors.lightGrey,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { forwardRef, useEffect, useMemo, useRef, type ComponentRef, type ReactNode } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import {
|
|
4
|
+
Modal as RNModal,
|
|
5
|
+
Platform,
|
|
6
|
+
Pressable,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Text,
|
|
9
|
+
View,
|
|
10
|
+
type StyleProp,
|
|
11
|
+
type ViewStyle,
|
|
12
|
+
} from "react-native";
|
|
13
|
+
import { CloseIcon } from "../../icons";
|
|
14
|
+
import { colors, fonts } from "../../theme";
|
|
15
|
+
import { mergeRefs } from "../../utils/mergeRefs";
|
|
16
|
+
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
17
|
+
|
|
18
|
+
export interface DrawerProps {
|
|
19
|
+
/** Controls visibility. */
|
|
20
|
+
open: boolean;
|
|
21
|
+
/** Called on backdrop press, Escape / Android back, or header close. */
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
/** Header title. When set, the standard drawer header is rendered. */
|
|
25
|
+
title?: string;
|
|
26
|
+
/** Panel width in px. Defaults to `652`. */
|
|
27
|
+
width?: number;
|
|
28
|
+
/** Close when the dimmed backdrop is pressed. Defaults to `true`. */
|
|
29
|
+
closeOnBackdropPress?: boolean;
|
|
30
|
+
/** Accessible name for the backdrop control. Defaults to `"Close"`. */
|
|
31
|
+
backdropAccessibilityLabel?: string;
|
|
32
|
+
/** Accessible name for the header close control. Defaults to `"Close"`. */
|
|
33
|
+
closeAccessibilityLabel?: string;
|
|
34
|
+
/** Style for the drawer panel. */
|
|
35
|
+
style?: StyleProp<ViewStyle>;
|
|
36
|
+
/** Web CSS class applied to the drawer panel. */
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const BACKDROP_COLOR = "#09090933";
|
|
41
|
+
const DEFAULT_WIDTH = 652;
|
|
42
|
+
const WEB_Z_INDEX = 500;
|
|
43
|
+
const CLOSE_ICON_SIZE = 24;
|
|
44
|
+
|
|
45
|
+
/** Plain DOM backdrop — RN StyleSheet often drops `backdrop-filter`. */
|
|
46
|
+
const webBackdropDomStyle = {
|
|
47
|
+
position: "absolute",
|
|
48
|
+
top: 0,
|
|
49
|
+
right: 0,
|
|
50
|
+
bottom: 0,
|
|
51
|
+
left: 0,
|
|
52
|
+
backgroundColor: BACKDROP_COLOR,
|
|
53
|
+
backdropFilter: "blur(18px)",
|
|
54
|
+
WebkitBackdropFilter: "blur(18px)",
|
|
55
|
+
border: "none",
|
|
56
|
+
padding: 0,
|
|
57
|
+
margin: 0,
|
|
58
|
+
cursor: "pointer",
|
|
59
|
+
} as const;
|
|
60
|
+
|
|
61
|
+
const webRootDomStyle = {
|
|
62
|
+
position: "fixed",
|
|
63
|
+
top: 0,
|
|
64
|
+
right: 0,
|
|
65
|
+
bottom: 0,
|
|
66
|
+
left: 0,
|
|
67
|
+
zIndex: WEB_Z_INDEX,
|
|
68
|
+
display: "flex",
|
|
69
|
+
justifyContent: "flex-end",
|
|
70
|
+
} as const;
|
|
71
|
+
|
|
72
|
+
export const Drawer = forwardRef<ComponentRef<typeof View>, DrawerProps>(function Drawer(
|
|
73
|
+
{
|
|
74
|
+
open,
|
|
75
|
+
onClose,
|
|
76
|
+
children,
|
|
77
|
+
title,
|
|
78
|
+
width = DEFAULT_WIDTH,
|
|
79
|
+
closeOnBackdropPress = true,
|
|
80
|
+
backdropAccessibilityLabel = "Close",
|
|
81
|
+
closeAccessibilityLabel = "Close",
|
|
82
|
+
style,
|
|
83
|
+
className,
|
|
84
|
+
},
|
|
85
|
+
forwardedRef,
|
|
86
|
+
) {
|
|
87
|
+
const panelRef = useRef<ComponentRef<typeof View>>(null);
|
|
88
|
+
const setPanelRef = useMemo(() => mergeRefs(panelRef, forwardedRef), [forwardedRef]);
|
|
89
|
+
const resolvedClassName = className?.trim() || undefined;
|
|
90
|
+
|
|
91
|
+
useApplyWebClassName(panelRef, resolvedClassName, open && Platform.OS === "web");
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!open || Platform.OS !== "web") return undefined;
|
|
95
|
+
|
|
96
|
+
const previousOverflow = document.body.style.overflow;
|
|
97
|
+
document.body.style.overflow = "hidden";
|
|
98
|
+
|
|
99
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
100
|
+
if (event.key === "Escape") onClose();
|
|
101
|
+
};
|
|
102
|
+
document.addEventListener("keydown", onKeyDown);
|
|
103
|
+
|
|
104
|
+
return () => {
|
|
105
|
+
document.body.style.overflow = previousOverflow;
|
|
106
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
107
|
+
};
|
|
108
|
+
}, [open, onClose]);
|
|
109
|
+
|
|
110
|
+
const header =
|
|
111
|
+
title != null && title !== "" ? (
|
|
112
|
+
<View style={styles.header}>
|
|
113
|
+
<Text style={styles.title} numberOfLines={1}>
|
|
114
|
+
{title}
|
|
115
|
+
</Text>
|
|
116
|
+
<Pressable
|
|
117
|
+
accessibilityRole="button"
|
|
118
|
+
accessibilityLabel={closeAccessibilityLabel}
|
|
119
|
+
hitSlop={8}
|
|
120
|
+
onPress={onClose}
|
|
121
|
+
style={styles.closeButton}
|
|
122
|
+
>
|
|
123
|
+
<CloseIcon color={colors.white} size={CLOSE_ICON_SIZE} />
|
|
124
|
+
</Pressable>
|
|
125
|
+
</View>
|
|
126
|
+
) : null;
|
|
127
|
+
|
|
128
|
+
const panel = (
|
|
129
|
+
<View
|
|
130
|
+
ref={setPanelRef}
|
|
131
|
+
accessibilityViewIsModal
|
|
132
|
+
style={[styles.panel, { width }, style]}
|
|
133
|
+
onStartShouldSetResponder={() => true}
|
|
134
|
+
>
|
|
135
|
+
{header}
|
|
136
|
+
<View style={styles.body}>{children}</View>
|
|
137
|
+
</View>
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (Platform.OS === "web") {
|
|
141
|
+
if (!open || typeof document === "undefined") return null;
|
|
142
|
+
|
|
143
|
+
return createPortal(
|
|
144
|
+
<div style={webRootDomStyle} role="presentation">
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
aria-label={backdropAccessibilityLabel}
|
|
148
|
+
disabled={!closeOnBackdropPress}
|
|
149
|
+
onClick={closeOnBackdropPress ? onClose : undefined}
|
|
150
|
+
style={webBackdropDomStyle}
|
|
151
|
+
/>
|
|
152
|
+
<div
|
|
153
|
+
style={{
|
|
154
|
+
position: "relative",
|
|
155
|
+
zIndex: 1,
|
|
156
|
+
height: "100%",
|
|
157
|
+
maxWidth: "100%",
|
|
158
|
+
display: "flex",
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
{panel}
|
|
162
|
+
</div>
|
|
163
|
+
</div>,
|
|
164
|
+
document.body,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<RNModal
|
|
170
|
+
visible={open}
|
|
171
|
+
transparent
|
|
172
|
+
animationType="slide"
|
|
173
|
+
onRequestClose={onClose}
|
|
174
|
+
statusBarTranslucent
|
|
175
|
+
>
|
|
176
|
+
<View style={styles.root} accessibilityViewIsModal>
|
|
177
|
+
<Pressable
|
|
178
|
+
accessibilityRole="button"
|
|
179
|
+
accessibilityLabel={backdropAccessibilityLabel}
|
|
180
|
+
disabled={!closeOnBackdropPress}
|
|
181
|
+
onPress={closeOnBackdropPress ? onClose : undefined}
|
|
182
|
+
style={styles.backdrop}
|
|
183
|
+
/>
|
|
184
|
+
{panel}
|
|
185
|
+
</View>
|
|
186
|
+
</RNModal>
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const styles = StyleSheet.create({
|
|
191
|
+
root: {
|
|
192
|
+
flex: 1,
|
|
193
|
+
flexDirection: "row",
|
|
194
|
+
justifyContent: "flex-end",
|
|
195
|
+
},
|
|
196
|
+
backdrop: {
|
|
197
|
+
...StyleSheet.absoluteFillObject,
|
|
198
|
+
backgroundColor: BACKDROP_COLOR,
|
|
199
|
+
},
|
|
200
|
+
panel: {
|
|
201
|
+
zIndex: 1,
|
|
202
|
+
height: "100%",
|
|
203
|
+
maxWidth: "100%",
|
|
204
|
+
backgroundColor: colors.grey800,
|
|
205
|
+
},
|
|
206
|
+
header: {
|
|
207
|
+
height: 65,
|
|
208
|
+
flexDirection: "row",
|
|
209
|
+
alignItems: "center",
|
|
210
|
+
justifyContent: "space-between",
|
|
211
|
+
paddingTop: 20,
|
|
212
|
+
paddingRight: 24,
|
|
213
|
+
paddingBottom: 20,
|
|
214
|
+
paddingLeft: 24,
|
|
215
|
+
borderBottomWidth: 1,
|
|
216
|
+
borderBottomColor: colors.grey650,
|
|
217
|
+
},
|
|
218
|
+
title: {
|
|
219
|
+
flex: 1,
|
|
220
|
+
fontFamily: fonts.sans,
|
|
221
|
+
fontSize: 16,
|
|
222
|
+
fontWeight: "600",
|
|
223
|
+
lineHeight: 16,
|
|
224
|
+
letterSpacing: 0,
|
|
225
|
+
color: colors.white,
|
|
226
|
+
marginRight: 12,
|
|
227
|
+
},
|
|
228
|
+
closeButton: {
|
|
229
|
+
width: CLOSE_ICON_SIZE,
|
|
230
|
+
height: CLOSE_ICON_SIZE,
|
|
231
|
+
alignItems: "center",
|
|
232
|
+
justifyContent: "center",
|
|
233
|
+
flexShrink: 0,
|
|
234
|
+
},
|
|
235
|
+
body: {
|
|
236
|
+
flex: 1,
|
|
237
|
+
minHeight: 0,
|
|
238
|
+
},
|
|
239
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { GuestsIcon } from "../../icons";
|
|
4
|
+
import { QuantityInput } from "./QuantityInput";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/QuantityInput",
|
|
8
|
+
component: QuantityInput,
|
|
9
|
+
args: {
|
|
10
|
+
label: "Maximum guests",
|
|
11
|
+
icon: GuestsIcon,
|
|
12
|
+
defaultValue: 1,
|
|
13
|
+
min: 1,
|
|
14
|
+
},
|
|
15
|
+
parameters: {
|
|
16
|
+
backgrounds: { default: "black" },
|
|
17
|
+
},
|
|
18
|
+
} satisfies Meta<typeof QuantityInput>;
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof meta>;
|
|
22
|
+
|
|
23
|
+
export const Default: Story = {};
|
|
24
|
+
|
|
25
|
+
export const WithError: Story = {
|
|
26
|
+
args: { error: true, hint: "Choose at least one guest" },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function ControlledQuantityInput() {
|
|
30
|
+
const [value, setValue] = useState(1);
|
|
31
|
+
return (
|
|
32
|
+
<QuantityInput
|
|
33
|
+
icon={GuestsIcon}
|
|
34
|
+
label="Maximum guests"
|
|
35
|
+
max={20}
|
|
36
|
+
min={1}
|
|
37
|
+
onValueChange={setValue}
|
|
38
|
+
value={value}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const Controlled: Story = {
|
|
44
|
+
render: () => <ControlledQuantityInput />,
|
|
45
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
import { Counter } from "../Counter";
|
|
15
|
+
|
|
16
|
+
export interface QuantityInputProps extends Omit<ViewProps, "style" | "children"> {
|
|
17
|
+
label: string;
|
|
18
|
+
icon: IconComponent;
|
|
19
|
+
iconProps?: Omit<IconProps, "size">;
|
|
20
|
+
iconSize?: number;
|
|
21
|
+
value?: number;
|
|
22
|
+
defaultValue?: number;
|
|
23
|
+
onValueChange?: (value: number) => void;
|
|
24
|
+
min?: number;
|
|
25
|
+
max?: number;
|
|
26
|
+
step?: number;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
error?: boolean;
|
|
29
|
+
hint?: string;
|
|
30
|
+
showHint?: boolean;
|
|
31
|
+
style?: StyleProp<ViewStyle>;
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Input-like labeled quantity control with an icon and increment/decrement actions. */
|
|
36
|
+
export const QuantityInput = forwardRef<ComponentRef<typeof View>, QuantityInputProps>(
|
|
37
|
+
function QuantityInput(
|
|
38
|
+
{
|
|
39
|
+
label,
|
|
40
|
+
icon: Icon,
|
|
41
|
+
iconProps,
|
|
42
|
+
iconSize = 20,
|
|
43
|
+
value,
|
|
44
|
+
defaultValue,
|
|
45
|
+
onValueChange,
|
|
46
|
+
min = 1,
|
|
47
|
+
max,
|
|
48
|
+
step = 1,
|
|
49
|
+
disabled = false,
|
|
50
|
+
error = false,
|
|
51
|
+
hint,
|
|
52
|
+
showHint = true,
|
|
53
|
+
style,
|
|
54
|
+
className,
|
|
55
|
+
accessibilityLabel,
|
|
56
|
+
...rest
|
|
57
|
+
},
|
|
58
|
+
forwardedRef,
|
|
59
|
+
) {
|
|
60
|
+
const rootRef = useRef<ComponentRef<typeof View>>(null);
|
|
61
|
+
const setRootRef = useMemo(() => mergeRefs(rootRef, forwardedRef), [forwardedRef]);
|
|
62
|
+
|
|
63
|
+
useApplyWebClassName(rootRef, className?.trim() || undefined);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<View
|
|
67
|
+
{...rest}
|
|
68
|
+
ref={setRootRef}
|
|
69
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
70
|
+
accessibilityState={{ disabled }}
|
|
71
|
+
style={[styles.root, disabled && styles.disabled, style]}
|
|
72
|
+
>
|
|
73
|
+
<Text style={[styles.label, error && styles.errorText]}>{label}</Text>
|
|
74
|
+
<View style={[styles.field, error && styles.errorField]}>
|
|
75
|
+
<View style={styles.iconSlot}>
|
|
76
|
+
<Icon
|
|
77
|
+
{...iconProps}
|
|
78
|
+
color={iconProps?.color ?? (error ? colors.red : colors.grey150)}
|
|
79
|
+
size={iconSize}
|
|
80
|
+
/>
|
|
81
|
+
</View>
|
|
82
|
+
<Counter
|
|
83
|
+
defaultValue={defaultValue}
|
|
84
|
+
disabled={disabled}
|
|
85
|
+
layout="value-first"
|
|
86
|
+
max={max}
|
|
87
|
+
min={min}
|
|
88
|
+
onValueChange={onValueChange}
|
|
89
|
+
step={step}
|
|
90
|
+
value={value}
|
|
91
|
+
/>
|
|
92
|
+
</View>
|
|
93
|
+
{showHint && hint ? (
|
|
94
|
+
<Text style={[styles.hint, error && styles.errorText]}>{hint}</Text>
|
|
95
|
+
) : null}
|
|
96
|
+
</View>
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const styles = StyleSheet.create({
|
|
102
|
+
root: {
|
|
103
|
+
alignSelf: "stretch",
|
|
104
|
+
flexDirection: "column",
|
|
105
|
+
gap: 8,
|
|
106
|
+
minWidth: 0,
|
|
107
|
+
},
|
|
108
|
+
label: {
|
|
109
|
+
color: colors.white,
|
|
110
|
+
fontFamily: fonts.sans,
|
|
111
|
+
fontSize: 12,
|
|
112
|
+
fontWeight: "500",
|
|
113
|
+
letterSpacing: -0.24,
|
|
114
|
+
lineHeight: 12,
|
|
115
|
+
},
|
|
116
|
+
field: {
|
|
117
|
+
alignItems: "center",
|
|
118
|
+
alignSelf: "stretch",
|
|
119
|
+
backgroundColor: colors.grey700,
|
|
120
|
+
borderColor: colors.grey700,
|
|
121
|
+
borderRadius: 60,
|
|
122
|
+
borderWidth: 1,
|
|
123
|
+
flexDirection: "row",
|
|
124
|
+
gap: 10,
|
|
125
|
+
height: 44,
|
|
126
|
+
paddingLeft: 16,
|
|
127
|
+
paddingRight: 6,
|
|
128
|
+
paddingVertical: 5,
|
|
129
|
+
},
|
|
130
|
+
iconSlot: {
|
|
131
|
+
alignItems: "center",
|
|
132
|
+
flexShrink: 0,
|
|
133
|
+
height: 20,
|
|
134
|
+
justifyContent: "center",
|
|
135
|
+
width: 20,
|
|
136
|
+
},
|
|
137
|
+
hint: {
|
|
138
|
+
color: colors.grey100,
|
|
139
|
+
fontFamily: fonts.sans,
|
|
140
|
+
fontSize: 12,
|
|
141
|
+
fontWeight: "400",
|
|
142
|
+
lineHeight: 12,
|
|
143
|
+
},
|
|
144
|
+
errorField: {
|
|
145
|
+
borderColor: colors.redMuted,
|
|
146
|
+
},
|
|
147
|
+
errorText: {
|
|
148
|
+
color: colors.red,
|
|
149
|
+
},
|
|
150
|
+
disabled: {
|
|
151
|
+
opacity: 0.6,
|
|
152
|
+
},
|
|
153
|
+
});
|
package/src/components/index.ts
CHANGED
|
@@ -28,12 +28,18 @@ export type { ContactInfoCardProps } from "./ContactInfoCard";
|
|
|
28
28
|
export { AccordionItem } from "./AccordionItem";
|
|
29
29
|
export type { AccordionItemProps } from "./AccordionItem";
|
|
30
30
|
|
|
31
|
+
export { Accordion } from "./Accordion";
|
|
32
|
+
export type { AccordionProps } from "./Accordion";
|
|
33
|
+
|
|
31
34
|
export { StepList } from "./StepList";
|
|
32
35
|
export type { StepListProps } from "./StepList";
|
|
33
36
|
|
|
34
37
|
export { Counter } from "./Counter";
|
|
35
38
|
export type { CounterProps } from "./Counter";
|
|
36
39
|
|
|
40
|
+
export { QuantityInput } from "./QuantityInput";
|
|
41
|
+
export type { QuantityInputProps } from "./QuantityInput";
|
|
42
|
+
|
|
37
43
|
export { SegmentedToggle } from "./SegmentedToggle";
|
|
38
44
|
export type { SegmentedToggleOption, SegmentedToggleProps } from "./SegmentedToggle";
|
|
39
45
|
|
|
@@ -71,9 +77,15 @@ export type { FloatingActionButtonProps } from "./FloatingActionButton";
|
|
|
71
77
|
export { Modal } from "./Modal";
|
|
72
78
|
export type { ModalProps } from "./Modal";
|
|
73
79
|
|
|
80
|
+
export { Drawer } from "./Drawer";
|
|
81
|
+
export type { DrawerProps } from "./Drawer";
|
|
82
|
+
|
|
74
83
|
export { Input } from "./Input";
|
|
75
84
|
export type { InputIcon, InputProps, InputSize } from "./Input";
|
|
76
85
|
|
|
86
|
+
export { AutocompleteInput } from "./AutocompleteInput";
|
|
87
|
+
export type { AutocompleteInputProps, AutocompleteOption } from "./AutocompleteInput";
|
|
88
|
+
|
|
77
89
|
export { VerificationCodeInput } from "./VerificationCodeInput";
|
|
78
90
|
export type {
|
|
79
91
|
VerificationCodeInputHandle,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ARROW_LEFT_PATH } from "./arrowLeftPath";
|
|
3
|
+
|
|
4
|
+
describe("ArrowLeftIcon", () => {
|
|
5
|
+
it("uses the approved 24px path", () => {
|
|
6
|
+
expect(ARROW_LEFT_PATH).toBe(
|
|
7
|
+
"M10.1972 6.59961L4.80078 11.9994L10.1972 17.3996M4.80078 11.9965H19.202",
|
|
8
|
+
);
|
|
9
|
+
});
|
|
10
|
+
});
|