@ecohouse/ui 0.1.29 → 0.1.30
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 +3 -0
- package/dist/index.cjs +586 -197
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -2
- package/dist/index.d.ts +64 -2
- package/dist/index.js +585 -198
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/AvatarSimple/AvatarSimple.stories.tsx +62 -0
- package/src/components/AvatarSimple/AvatarSimple.tsx +178 -0
- package/src/components/AvatarSimple/index.ts +2 -0
- package/src/components/SegmentedToggle/SegmentedToggle.stories.tsx +22 -0
- package/src/components/SegmentedToggle/SegmentedToggle.tsx +51 -18
- package/src/components/Sidebar/Sidebar.stories.tsx +119 -0
- package/src/components/Sidebar/Sidebar.tsx +305 -0
- package/src/components/Sidebar/index.ts +2 -0
- package/src/components/index.ts +6 -0
- package/src/icons/LogOut/logOutPath.ts +1 -1
- package/src/index.ts +6 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type ComponentRef,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import {
|
|
11
|
+
Animated,
|
|
12
|
+
Easing,
|
|
13
|
+
Pressable,
|
|
14
|
+
StyleSheet,
|
|
15
|
+
View,
|
|
16
|
+
type StyleProp,
|
|
17
|
+
type ViewProps,
|
|
18
|
+
type ViewStyle,
|
|
19
|
+
} from "react-native";
|
|
20
|
+
import type { IconComponent } from "../../icons";
|
|
21
|
+
import { SidebarIcon } from "../../icons";
|
|
22
|
+
import { BrandLogo, MenuItem } from "../../primitives";
|
|
23
|
+
import { colors, fonts } from "../../theme";
|
|
24
|
+
import { mergeRefs } from "../../utils/mergeRefs";
|
|
25
|
+
import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
|
|
26
|
+
import { AvatarSimple } from "../AvatarSimple";
|
|
27
|
+
|
|
28
|
+
export interface SidebarItem {
|
|
29
|
+
/** Stable identity for React keys. */
|
|
30
|
+
key: string;
|
|
31
|
+
label: string;
|
|
32
|
+
/** Icon component — colored by the sidebar based on active state. */
|
|
33
|
+
icon: IconComponent;
|
|
34
|
+
/** Marks the current route / selection. */
|
|
35
|
+
active?: boolean;
|
|
36
|
+
onPress?: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SidebarUser {
|
|
40
|
+
name: string;
|
|
41
|
+
email: string;
|
|
42
|
+
imageUrl?: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SidebarProps extends Omit<ViewProps, "style" | "children"> {
|
|
46
|
+
items: readonly SidebarItem[];
|
|
47
|
+
user: SidebarUser;
|
|
48
|
+
onLogOut?: () => void;
|
|
49
|
+
/** Accessible name for the brand mark. Defaults to `"Cortel Booking"`. */
|
|
50
|
+
logoAccessibilityLabel?: string;
|
|
51
|
+
/** Accessible name for the nav landmark. Defaults to `"Account navigation"`. */
|
|
52
|
+
navigationAccessibilityLabel?: string;
|
|
53
|
+
/** Accessible label when the sidebar is expanded. Defaults to `"Collapse sidebar"`. */
|
|
54
|
+
collapseAccessibilityLabel?: string;
|
|
55
|
+
/** Accessible label when the sidebar is collapsed. Defaults to `"Expand sidebar"`. */
|
|
56
|
+
expandAccessibilityLabel?: string;
|
|
57
|
+
/** Accessible label for the logout control. Defaults to `"Log out"`. */
|
|
58
|
+
logOutAccessibilityLabel?: string;
|
|
59
|
+
collapsed?: boolean;
|
|
60
|
+
defaultCollapsed?: boolean;
|
|
61
|
+
onCollapsedChange?: (collapsed: boolean) => void;
|
|
62
|
+
style?: StyleProp<ViewStyle>;
|
|
63
|
+
className?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const SIDEBAR_WIDTH = 300;
|
|
67
|
+
const SIDEBAR_COLLAPSED_WIDTH = 72;
|
|
68
|
+
const HEADER_HEIGHT = 64;
|
|
69
|
+
const LOGO_WIDTH = 31;
|
|
70
|
+
const LOGO_HEIGHT = 44;
|
|
71
|
+
const TOGGLE_SIZE = 20;
|
|
72
|
+
const MENU_ICON_SIZE = 16;
|
|
73
|
+
const PROFILE_SECTION_HEIGHT = 80;
|
|
74
|
+
const SIDEBAR_TRANSITION_MS = 300;
|
|
75
|
+
|
|
76
|
+
export const Sidebar = forwardRef<ComponentRef<typeof View>, SidebarProps>(function Sidebar(
|
|
77
|
+
{
|
|
78
|
+
items,
|
|
79
|
+
user,
|
|
80
|
+
onLogOut,
|
|
81
|
+
logoAccessibilityLabel = "Cortel Booking",
|
|
82
|
+
navigationAccessibilityLabel = "Account navigation",
|
|
83
|
+
collapseAccessibilityLabel = "Collapse sidebar",
|
|
84
|
+
expandAccessibilityLabel = "Expand sidebar",
|
|
85
|
+
logOutAccessibilityLabel = "Log out",
|
|
86
|
+
collapsed: collapsedProp,
|
|
87
|
+
defaultCollapsed = false,
|
|
88
|
+
onCollapsedChange,
|
|
89
|
+
style,
|
|
90
|
+
className,
|
|
91
|
+
...rest
|
|
92
|
+
},
|
|
93
|
+
forwardedRef,
|
|
94
|
+
) {
|
|
95
|
+
const containerRef = useRef<ComponentRef<typeof View>>(null);
|
|
96
|
+
const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
|
|
97
|
+
const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState(defaultCollapsed);
|
|
98
|
+
const collapsed = collapsedProp ?? uncontrolledCollapsed;
|
|
99
|
+
const widthAnim = useRef(
|
|
100
|
+
new Animated.Value(collapsed ? SIDEBAR_COLLAPSED_WIDTH : SIDEBAR_WIDTH),
|
|
101
|
+
).current;
|
|
102
|
+
const labelOpacity = useRef(new Animated.Value(collapsed ? 0 : 1)).current;
|
|
103
|
+
|
|
104
|
+
useApplyWebClassName(containerRef, className);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const nextWidth = collapsed ? SIDEBAR_COLLAPSED_WIDTH : SIDEBAR_WIDTH;
|
|
108
|
+
const nextOpacity = collapsed ? 0 : 1;
|
|
109
|
+
const timing = {
|
|
110
|
+
duration: SIDEBAR_TRANSITION_MS,
|
|
111
|
+
easing: Easing.inOut(Easing.ease),
|
|
112
|
+
useNativeDriver: false,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const widthAnimation = Animated.timing(widthAnim, { ...timing, toValue: nextWidth });
|
|
116
|
+
const opacityAnimation = Animated.timing(labelOpacity, { ...timing, toValue: nextOpacity });
|
|
117
|
+
|
|
118
|
+
Animated.parallel([widthAnimation, opacityAnimation]).start();
|
|
119
|
+
return () => {
|
|
120
|
+
widthAnimation.stop();
|
|
121
|
+
opacityAnimation.stop();
|
|
122
|
+
};
|
|
123
|
+
}, [collapsed, labelOpacity, widthAnim]);
|
|
124
|
+
|
|
125
|
+
const setCollapsed = (next: boolean) => {
|
|
126
|
+
onCollapsedChange?.(next);
|
|
127
|
+
if (collapsedProp === undefined) {
|
|
128
|
+
setUncontrolledCollapsed(next);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<Animated.View
|
|
134
|
+
{...rest}
|
|
135
|
+
ref={setContainerRef}
|
|
136
|
+
style={[styles.root, { width: widthAnim }, style]}
|
|
137
|
+
accessibilityRole="menu"
|
|
138
|
+
>
|
|
139
|
+
<View style={[styles.header, collapsed ? styles.headerCollapsed : styles.headerExpanded]}>
|
|
140
|
+
<Animated.View
|
|
141
|
+
pointerEvents={collapsed ? "none" : "auto"}
|
|
142
|
+
style={[
|
|
143
|
+
styles.logoSlot,
|
|
144
|
+
{
|
|
145
|
+
opacity: labelOpacity,
|
|
146
|
+
maxWidth: collapsed ? 0 : LOGO_WIDTH,
|
|
147
|
+
},
|
|
148
|
+
]}
|
|
149
|
+
accessibilityElementsHidden={collapsed}
|
|
150
|
+
importantForAccessibility={collapsed ? "no-hide-descendants" : "auto"}
|
|
151
|
+
>
|
|
152
|
+
<BrandLogo
|
|
153
|
+
accessibilityLabel={logoAccessibilityLabel}
|
|
154
|
+
height={LOGO_HEIGHT}
|
|
155
|
+
variant="brand"
|
|
156
|
+
width={LOGO_WIDTH}
|
|
157
|
+
/>
|
|
158
|
+
</Animated.View>
|
|
159
|
+
|
|
160
|
+
<Pressable
|
|
161
|
+
accessibilityRole="button"
|
|
162
|
+
accessibilityLabel={collapsed ? expandAccessibilityLabel : collapseAccessibilityLabel}
|
|
163
|
+
accessibilityState={{ expanded: !collapsed }}
|
|
164
|
+
hitSlop={8}
|
|
165
|
+
onPress={() => setCollapsed(!collapsed)}
|
|
166
|
+
style={styles.toggleButton}
|
|
167
|
+
>
|
|
168
|
+
<SidebarIcon color={colors.grey100} size={TOGGLE_SIZE} strokeWidth={2} />
|
|
169
|
+
</Pressable>
|
|
170
|
+
</View>
|
|
171
|
+
|
|
172
|
+
<View
|
|
173
|
+
accessibilityLabel={navigationAccessibilityLabel}
|
|
174
|
+
accessibilityRole="menu"
|
|
175
|
+
style={styles.nav}
|
|
176
|
+
>
|
|
177
|
+
{items.map((item) => {
|
|
178
|
+
const isActive = Boolean(item.active);
|
|
179
|
+
const iconColor = isActive ? colors.primary : colors.grey300;
|
|
180
|
+
const Icon = item.icon;
|
|
181
|
+
const iconNode: ReactNode = (
|
|
182
|
+
<Icon color={iconColor} size={MENU_ICON_SIZE} strokeWidth={1.5} />
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<MenuItem
|
|
187
|
+
key={item.key}
|
|
188
|
+
icon={iconNode}
|
|
189
|
+
label={item.label}
|
|
190
|
+
accessibilityLabel={item.label}
|
|
191
|
+
backgroundColor={isActive ? colors.grey700 : "transparent"}
|
|
192
|
+
hoverColor={colors.grey700}
|
|
193
|
+
onPress={item.onPress}
|
|
194
|
+
labelStyle={[
|
|
195
|
+
styles.menuLabel,
|
|
196
|
+
{ color: isActive ? colors.white : colors.grey200 },
|
|
197
|
+
collapsed ? styles.menuLabelCollapsed : styles.menuLabelExpanded,
|
|
198
|
+
]}
|
|
199
|
+
style={[styles.menuItem, collapsed ? styles.menuItemCollapsed : null]}
|
|
200
|
+
/>
|
|
201
|
+
);
|
|
202
|
+
})}
|
|
203
|
+
</View>
|
|
204
|
+
|
|
205
|
+
<View style={[styles.footer, collapsed ? styles.footerCollapsed : styles.footerExpanded]}>
|
|
206
|
+
<AvatarSimple
|
|
207
|
+
name={user.name}
|
|
208
|
+
email={user.email}
|
|
209
|
+
imageUrl={user.imageUrl}
|
|
210
|
+
onLogOut={onLogOut}
|
|
211
|
+
logOutAccessibilityLabel={logOutAccessibilityLabel}
|
|
212
|
+
compact={collapsed}
|
|
213
|
+
/>
|
|
214
|
+
</View>
|
|
215
|
+
</Animated.View>
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const styles = StyleSheet.create({
|
|
220
|
+
root: {
|
|
221
|
+
height: "100%",
|
|
222
|
+
flexShrink: 0,
|
|
223
|
+
backgroundColor: colors.dark,
|
|
224
|
+
borderRightWidth: 1,
|
|
225
|
+
borderRightColor: colors.grey700,
|
|
226
|
+
overflow: "hidden",
|
|
227
|
+
},
|
|
228
|
+
header: {
|
|
229
|
+
zIndex: 1,
|
|
230
|
+
height: HEADER_HEIGHT,
|
|
231
|
+
width: "100%",
|
|
232
|
+
flexDirection: "row",
|
|
233
|
+
alignItems: "center",
|
|
234
|
+
borderBottomWidth: 1,
|
|
235
|
+
borderBottomColor: colors.grey700,
|
|
236
|
+
backgroundColor: colors.dark,
|
|
237
|
+
},
|
|
238
|
+
headerExpanded: {
|
|
239
|
+
justifyContent: "space-between",
|
|
240
|
+
paddingHorizontal: 24,
|
|
241
|
+
},
|
|
242
|
+
headerCollapsed: {
|
|
243
|
+
justifyContent: "center",
|
|
244
|
+
paddingHorizontal: 0,
|
|
245
|
+
},
|
|
246
|
+
logoSlot: {
|
|
247
|
+
minWidth: 0,
|
|
248
|
+
height: LOGO_HEIGHT,
|
|
249
|
+
flexShrink: 0,
|
|
250
|
+
overflow: "hidden",
|
|
251
|
+
},
|
|
252
|
+
toggleButton: {
|
|
253
|
+
width: TOGGLE_SIZE,
|
|
254
|
+
height: TOGGLE_SIZE,
|
|
255
|
+
alignItems: "center",
|
|
256
|
+
justifyContent: "center",
|
|
257
|
+
flexShrink: 0,
|
|
258
|
+
},
|
|
259
|
+
nav: {
|
|
260
|
+
flex: 1,
|
|
261
|
+
width: "100%",
|
|
262
|
+
padding: 16,
|
|
263
|
+
gap: 10,
|
|
264
|
+
},
|
|
265
|
+
menuItem: {
|
|
266
|
+
gap: 8,
|
|
267
|
+
},
|
|
268
|
+
menuItemCollapsed: {
|
|
269
|
+
gap: 0,
|
|
270
|
+
},
|
|
271
|
+
menuLabel: {
|
|
272
|
+
fontFamily: fonts.sans,
|
|
273
|
+
fontSize: 12,
|
|
274
|
+
fontWeight: "600",
|
|
275
|
+
lineHeight: 16,
|
|
276
|
+
letterSpacing: 0,
|
|
277
|
+
},
|
|
278
|
+
menuLabelExpanded: {
|
|
279
|
+
flexGrow: 1,
|
|
280
|
+
flexShrink: 1,
|
|
281
|
+
opacity: 1,
|
|
282
|
+
},
|
|
283
|
+
menuLabelCollapsed: {
|
|
284
|
+
flexGrow: 0,
|
|
285
|
+
flexShrink: 0,
|
|
286
|
+
width: 0,
|
|
287
|
+
maxWidth: 0,
|
|
288
|
+
opacity: 0,
|
|
289
|
+
overflow: "hidden",
|
|
290
|
+
},
|
|
291
|
+
footer: {
|
|
292
|
+
height: PROFILE_SECTION_HEIGHT,
|
|
293
|
+
width: "100%",
|
|
294
|
+
flexShrink: 0,
|
|
295
|
+
alignItems: "center",
|
|
296
|
+
justifyContent: "center",
|
|
297
|
+
paddingVertical: 12,
|
|
298
|
+
},
|
|
299
|
+
footerExpanded: {
|
|
300
|
+
paddingHorizontal: 16,
|
|
301
|
+
},
|
|
302
|
+
footerCollapsed: {
|
|
303
|
+
paddingHorizontal: 0,
|
|
304
|
+
},
|
|
305
|
+
});
|
package/src/components/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { Avatar } from "./Avatar";
|
|
2
2
|
export type { AvatarProps, AvatarMenuItem } from "./Avatar";
|
|
3
3
|
|
|
4
|
+
export { AvatarSimple } from "./AvatarSimple";
|
|
5
|
+
export type { AvatarSimpleProps } from "./AvatarSimple";
|
|
6
|
+
|
|
4
7
|
export { Badge } from "./Badge";
|
|
5
8
|
export type { BadgeProps, BadgeVariant } from "./Badge";
|
|
6
9
|
|
|
@@ -10,6 +13,9 @@ export type { CounterProps } from "./Counter";
|
|
|
10
13
|
export { SegmentedToggle } from "./SegmentedToggle";
|
|
11
14
|
export type { SegmentedToggleOption, SegmentedToggleProps } from "./SegmentedToggle";
|
|
12
15
|
|
|
16
|
+
export { Sidebar } from "./Sidebar";
|
|
17
|
+
export type { SidebarProps, SidebarItem, SidebarUser } from "./Sidebar";
|
|
18
|
+
|
|
13
19
|
export { LanguageSwitcher, defaultLanguageOptions } from "./LanguageSwitcher";
|
|
14
20
|
export type { LanguageOption, LanguageSwitcherProps } from "./LanguageSwitcher";
|
|
15
21
|
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** LogOut icon path — 16×16 viewBox. */
|
|
2
2
|
export const LOG_OUT_PATH =
|
|
3
|
-
"M10.6667 4.66667L14 8L10.6667 11.3333M14
|
|
3
|
+
"M10.6667 4.66667L14 8L10.6667 11.3333M14 8H6M6 2H5.2C4.0799 2 3.51984 2 3.09202 2.21799C2.7157 2.40973 2.40973 2.71569 2.21799 3.09202C2 3.51984 2 4.07989 2 5.2V10.8C2 11.9201 2 12.4802 2.21799 12.908C2.40973 13.2843 2.71569 13.5903 3.09202 13.782C3.51984 14 4.0799 14 5.2 14H6";
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export {
|
|
2
2
|
Avatar,
|
|
3
|
+
AvatarSimple,
|
|
3
4
|
Badge,
|
|
4
5
|
Counter,
|
|
5
6
|
SegmentedToggle,
|
|
7
|
+
Sidebar,
|
|
6
8
|
LanguageSwitcher,
|
|
7
9
|
defaultLanguageOptions,
|
|
8
10
|
StatCard,
|
|
@@ -21,11 +23,15 @@ export {
|
|
|
21
23
|
export type {
|
|
22
24
|
AvatarProps,
|
|
23
25
|
AvatarMenuItem,
|
|
26
|
+
AvatarSimpleProps,
|
|
24
27
|
BadgeProps,
|
|
25
28
|
BadgeVariant,
|
|
26
29
|
CounterProps,
|
|
27
30
|
SegmentedToggleOption,
|
|
28
31
|
SegmentedToggleProps,
|
|
32
|
+
SidebarProps,
|
|
33
|
+
SidebarItem,
|
|
34
|
+
SidebarUser,
|
|
29
35
|
LanguageOption,
|
|
30
36
|
LanguageSwitcherProps,
|
|
31
37
|
StatCardProps,
|