@ecohouse/ui 0.1.39 → 0.1.40

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecohouse/ui",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Cross-platform presentation-only UI components for EcoHouse (React Native + React Native Web)",
@@ -716,9 +716,10 @@ const styles = StyleSheet.create({
716
716
  },
717
717
  rangeValueRow: {
718
718
  minWidth: 0,
719
+ width: "100%",
719
720
  flexDirection: "row",
720
721
  alignItems: "center",
721
- justifyContent: "flex-start",
722
+ justifyContent: "space-between",
722
723
  flexWrap: "nowrap",
723
724
  gap: 10,
724
725
  },
@@ -1,6 +1,18 @@
1
- import { forwardRef, useEffect, useMemo, useRef, type ComponentRef, type ReactNode } from "react";
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ type ComponentRef,
9
+ type ReactNode,
10
+ type TransitionEvent,
11
+ } from "react";
2
12
  import { createPortal } from "react-dom";
3
13
  import {
14
+ Animated,
15
+ Easing,
4
16
  Modal as RNModal,
5
17
  Platform,
6
18
  Pressable,
@@ -25,6 +37,8 @@ export interface DrawerProps {
25
37
  title?: string;
26
38
  /** Panel width in px. Defaults to `652`. */
27
39
  width?: number;
40
+ /** Open/close width transition duration in ms. Defaults to `320`. */
41
+ animationDuration?: number;
28
42
  /** Close when the dimmed backdrop is pressed. Defaults to `true`. */
29
43
  closeOnBackdropPress?: boolean;
30
44
  /** Accessible name for the backdrop control. Defaults to `"Close"`. */
@@ -39,36 +53,10 @@ export interface DrawerProps {
39
53
 
40
54
  const BACKDROP_COLOR = "#09090933";
41
55
  const DEFAULT_WIDTH = 652;
56
+ const DEFAULT_DURATION = 320;
42
57
  const WEB_Z_INDEX = 500;
43
58
  const CLOSE_ICON_SIZE = 24;
44
59
 
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
60
  export const Drawer = forwardRef<ComponentRef<typeof View>, DrawerProps>(function Drawer(
73
61
  {
74
62
  open,
@@ -76,6 +64,7 @@ export const Drawer = forwardRef<ComponentRef<typeof View>, DrawerProps>(functio
76
64
  children,
77
65
  title,
78
66
  width = DEFAULT_WIDTH,
67
+ animationDuration = DEFAULT_DURATION,
79
68
  closeOnBackdropPress = true,
80
69
  backdropAccessibilityLabel = "Close",
81
70
  closeAccessibilityLabel = "Close",
@@ -87,11 +76,17 @@ export const Drawer = forwardRef<ComponentRef<typeof View>, DrawerProps>(functio
87
76
  const panelRef = useRef<ComponentRef<typeof View>>(null);
88
77
  const setPanelRef = useMemo(() => mergeRefs(panelRef, forwardedRef), [forwardedRef]);
89
78
  const resolvedClassName = className?.trim() || undefined;
79
+ const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
90
80
 
91
- useApplyWebClassName(panelRef, resolvedClassName, open && Platform.OS === "web");
81
+ const [mounted, setMounted] = useState(open);
82
+ const [entered, setEntered] = useState(false);
83
+ const animatedWidth = useRef(new Animated.Value(open ? width : 0)).current;
84
+ const animatedBackdrop = useRef(new Animated.Value(open ? 1 : 0)).current;
85
+
86
+ useApplyWebClassName(panelRef, resolvedClassName, mounted && Platform.OS === "web");
92
87
 
93
88
  useEffect(() => {
94
- if (!open || Platform.OS !== "web") return undefined;
89
+ if (!mounted || Platform.OS !== "web") return undefined;
95
90
 
96
91
  const previousOverflow = document.body.style.overflow;
97
92
  document.body.style.overflow = "hidden";
@@ -105,7 +100,96 @@ export const Drawer = forwardRef<ComponentRef<typeof View>, DrawerProps>(functio
105
100
  document.body.style.overflow = previousOverflow;
106
101
  document.removeEventListener("keydown", onKeyDown);
107
102
  };
108
- }, [open, onClose]);
103
+ }, [mounted, onClose]);
104
+
105
+ // Web: mount at width 0, then expand; on close collapse then unmount.
106
+ useEffect(() => {
107
+ if (Platform.OS !== "web") return undefined;
108
+
109
+ if (closeTimeoutRef.current) {
110
+ clearTimeout(closeTimeoutRef.current);
111
+ closeTimeoutRef.current = null;
112
+ }
113
+
114
+ if (open) {
115
+ setMounted(true);
116
+ const frame = requestAnimationFrame(() => {
117
+ requestAnimationFrame(() => setEntered(true));
118
+ });
119
+ return () => cancelAnimationFrame(frame);
120
+ }
121
+
122
+ setEntered(false);
123
+ // Fallback if transitionend is skipped (e.g. reduced motion / tab background).
124
+ closeTimeoutRef.current = setTimeout(() => {
125
+ setMounted(false);
126
+ closeTimeoutRef.current = null;
127
+ }, animationDuration + 50);
128
+
129
+ return () => {
130
+ if (closeTimeoutRef.current) {
131
+ clearTimeout(closeTimeoutRef.current);
132
+ closeTimeoutRef.current = null;
133
+ }
134
+ };
135
+ }, [open, animationDuration]);
136
+
137
+ // Native: animate width + backdrop opacity.
138
+ useEffect(() => {
139
+ if (Platform.OS === "web") return undefined;
140
+
141
+ if (open) {
142
+ setMounted(true);
143
+ Animated.parallel([
144
+ Animated.timing(animatedWidth, {
145
+ toValue: width,
146
+ duration: animationDuration,
147
+ easing: Easing.out(Easing.cubic),
148
+ useNativeDriver: false,
149
+ }),
150
+ Animated.timing(animatedBackdrop, {
151
+ toValue: 1,
152
+ duration: animationDuration,
153
+ easing: Easing.out(Easing.cubic),
154
+ useNativeDriver: true,
155
+ }),
156
+ ]).start();
157
+ return undefined;
158
+ }
159
+
160
+ Animated.parallel([
161
+ Animated.timing(animatedWidth, {
162
+ toValue: 0,
163
+ duration: animationDuration,
164
+ easing: Easing.in(Easing.cubic),
165
+ useNativeDriver: false,
166
+ }),
167
+ Animated.timing(animatedBackdrop, {
168
+ toValue: 0,
169
+ duration: animationDuration,
170
+ easing: Easing.in(Easing.cubic),
171
+ useNativeDriver: true,
172
+ }),
173
+ ]).start(({ finished }) => {
174
+ if (finished) setMounted(false);
175
+ });
176
+ return undefined;
177
+ }, [open, width, animationDuration, animatedWidth, animatedBackdrop]);
178
+
179
+ const handleWebTransitionEnd = useCallback(
180
+ (event: TransitionEvent<HTMLDivElement>) => {
181
+ if (event.target !== event.currentTarget) return;
182
+ if (event.propertyName !== "width") return;
183
+ if (!open) {
184
+ if (closeTimeoutRef.current) {
185
+ clearTimeout(closeTimeoutRef.current);
186
+ closeTimeoutRef.current = null;
187
+ }
188
+ setMounted(false);
189
+ }
190
+ },
191
+ [open],
192
+ );
109
193
 
110
194
  const header =
111
195
  title != null && title !== "" ? (
@@ -125,11 +209,11 @@ export const Drawer = forwardRef<ComponentRef<typeof View>, DrawerProps>(functio
125
209
  </View>
126
210
  ) : null;
127
211
 
128
- const panel = (
212
+ const panelContent = (
129
213
  <View
130
214
  ref={setPanelRef}
131
215
  accessibilityViewIsModal
132
- style={[styles.panel, { width }, style]}
216
+ style={[styles.panelInner, { width }, style]}
133
217
  onStartShouldSetResponder={() => true}
134
218
  >
135
219
  {header}
@@ -138,50 +222,110 @@ export const Drawer = forwardRef<ComponentRef<typeof View>, DrawerProps>(functio
138
222
  );
139
223
 
140
224
  if (Platform.OS === "web") {
141
- if (!open || typeof document === "undefined") return null;
225
+ if (!mounted || typeof document === "undefined") return null;
142
226
 
143
227
  return createPortal(
144
- <div style={webRootDomStyle} role="presentation">
228
+ <div
229
+ style={{
230
+ position: "fixed",
231
+ top: 0,
232
+ right: 0,
233
+ bottom: 0,
234
+ left: 0,
235
+ zIndex: WEB_Z_INDEX,
236
+ display: "flex",
237
+ justifyContent: "flex-end",
238
+ pointerEvents: "auto",
239
+ }}
240
+ role="presentation"
241
+ >
145
242
  <button
146
243
  type="button"
147
244
  aria-label={backdropAccessibilityLabel}
148
245
  disabled={!closeOnBackdropPress}
149
246
  onClick={closeOnBackdropPress ? onClose : undefined}
150
- style={webBackdropDomStyle}
247
+ style={{
248
+ position: "absolute",
249
+ top: 0,
250
+ right: 0,
251
+ bottom: 0,
252
+ left: 0,
253
+ backgroundColor: BACKDROP_COLOR,
254
+ backdropFilter: "blur(18px)",
255
+ WebkitBackdropFilter: "blur(18px)",
256
+ border: "none",
257
+ padding: 0,
258
+ margin: 0,
259
+ cursor: closeOnBackdropPress ? "pointer" : "default",
260
+ opacity: entered ? 1 : 0,
261
+ transition: `opacity ${animationDuration}ms ease`,
262
+ }}
151
263
  />
264
+ {/*
265
+ Shell width animates 0 ↔ panel width. Inner stays fixed-width and
266
+ right-aligned so content isn't crushed and appears to slide in.
267
+ minWidth:0 is required — flex defaults would otherwise lock width to content.
268
+ */}
152
269
  <div
270
+ onTransitionEnd={handleWebTransitionEnd}
153
271
  style={{
154
272
  position: "relative",
155
273
  zIndex: 1,
156
274
  height: "100%",
275
+ width: entered
276
+ ? Math.min(width, typeof window !== "undefined" ? window.innerWidth : width)
277
+ : 0,
278
+ minWidth: 0,
157
279
  maxWidth: "100%",
158
- display: "flex",
280
+ overflow: "hidden",
281
+ transition: `width ${animationDuration}ms cubic-bezier(0.32, 0.72, 0, 1)`,
282
+ willChange: "width",
283
+ boxSizing: "border-box",
159
284
  }}
160
285
  >
161
- {panel}
286
+ <div
287
+ style={{
288
+ width,
289
+ maxWidth: "100%",
290
+ height: "100%",
291
+ marginLeft: "auto",
292
+ display: "flex",
293
+ flexDirection: "column",
294
+ }}
295
+ >
296
+ {panelContent}
297
+ </div>
162
298
  </div>
163
299
  </div>,
164
300
  document.body,
165
301
  );
166
302
  }
167
303
 
304
+ if (!mounted) return null;
305
+
168
306
  return (
169
307
  <RNModal
170
- visible={open}
308
+ visible={mounted}
171
309
  transparent
172
- animationType="slide"
310
+ animationType="none"
173
311
  onRequestClose={onClose}
174
312
  statusBarTranslucent
175
313
  >
176
314
  <View style={styles.root} accessibilityViewIsModal>
315
+ <Animated.View
316
+ pointerEvents="none"
317
+ style={[styles.backdrop, { opacity: animatedBackdrop }]}
318
+ />
177
319
  <Pressable
178
320
  accessibilityRole="button"
179
321
  accessibilityLabel={backdropAccessibilityLabel}
180
322
  disabled={!closeOnBackdropPress}
181
323
  onPress={closeOnBackdropPress ? onClose : undefined}
182
- style={styles.backdrop}
324
+ style={StyleSheet.absoluteFill}
183
325
  />
184
- {panel}
326
+ <Animated.View style={[styles.panelShell, { width: animatedWidth }]}>
327
+ {panelContent}
328
+ </Animated.View>
185
329
  </View>
186
330
  </RNModal>
187
331
  );
@@ -197,11 +341,19 @@ const styles = StyleSheet.create({
197
341
  ...StyleSheet.absoluteFillObject,
198
342
  backgroundColor: BACKDROP_COLOR,
199
343
  },
200
- panel: {
344
+ panelShell: {
201
345
  zIndex: 1,
346
+ height: "100%",
347
+ maxWidth: "100%",
348
+ overflow: "hidden",
349
+ alignItems: "flex-end",
350
+ minWidth: 0,
351
+ },
352
+ panelInner: {
202
353
  height: "100%",
203
354
  maxWidth: "100%",
204
355
  backgroundColor: colors.grey800,
356
+ flexShrink: 0,
205
357
  },
206
358
  header: {
207
359
  height: 65,
@@ -220,10 +372,11 @@ const styles = StyleSheet.create({
220
372
  fontFamily: fonts.sans,
221
373
  fontSize: 16,
222
374
  fontWeight: "600",
223
- lineHeight: 16,
375
+ lineHeight: 22,
224
376
  letterSpacing: 0,
225
377
  color: colors.white,
226
378
  marginRight: 12,
379
+ overflow: "visible",
227
380
  },
228
381
  closeButton: {
229
382
  width: CLOSE_ICON_SIZE,
@@ -9,7 +9,6 @@ export { AmenityIcon } from "./Amenity";
9
9
  export type { AmenityIconProps, AmenityName } from "./Amenity";
10
10
  export { AlertTriangleIcon } from "./AlertTriangle";
11
11
  export { AppleIcon } from "./Store";
12
- export { ArrowLeftIcon } from "./ArrowLeft";
13
12
  export { ArrowRightIcon } from "./ArrowRight";
14
13
  export { BagIcon } from "./Bag";
15
14
  export { BanIcon } from "./Ban";
@@ -26,7 +25,6 @@ export { ChevronDownIcon } from "./ChevronDown";
26
25
  export { ChevronLeftIcon } from "./ChevronLeft";
27
26
  export { ClockIcon } from "./Clock";
28
27
  export { CloseIcon } from "./Close";
29
- export { CurrentLocationIcon } from "./CurrentLocation";
30
28
  export { EditIcon } from "./Edit";
31
29
  export {
32
30
  BathroomsIcon,
package/src/index.ts CHANGED
@@ -9,10 +9,8 @@ export {
9
9
  InfoTile,
10
10
  ContactInfoCard,
11
11
  AccordionItem,
12
- Accordion,
13
12
  StepList,
14
13
  Counter,
15
- QuantityInput,
16
14
  SegmentedToggle,
17
15
  Sidebar,
18
16
  LanguageSwitcher,
@@ -23,9 +21,7 @@ export {
23
21
  DatePicker,
24
22
  FloatingActionButton,
25
23
  Modal,
26
- Drawer,
27
24
  Input,
28
- AutocompleteInput,
29
25
  VerificationCodeInput,
30
26
  Select,
31
27
  Textarea,
@@ -49,10 +45,8 @@ export type {
49
45
  InfoTileProps,
50
46
  ContactInfoCardProps,
51
47
  AccordionItemProps,
52
- AccordionProps,
53
48
  StepListProps,
54
49
  CounterProps,
55
- QuantityInputProps,
56
50
  SegmentedToggleOption,
57
51
  SegmentedToggleProps,
58
52
  SidebarProps,
@@ -74,12 +68,9 @@ export type {
74
68
  DateRange,
75
69
  FloatingActionButtonProps,
76
70
  ModalProps,
77
- DrawerProps,
78
71
  InputIcon,
79
72
  InputProps,
80
73
  InputSize,
81
- AutocompleteInputProps,
82
- AutocompleteOption,
83
74
  VerificationCodeInputHandle,
84
75
  VerificationCodeInputProps,
85
76
  SelectIcon,
@@ -128,7 +119,6 @@ export {
128
119
  iconGroups,
129
120
  iconGroupNames,
130
121
  getIconsByGroup,
131
- ArrowLeftIcon,
132
122
  ArrowRightIcon,
133
123
  BagIcon,
134
124
  BanIcon,
@@ -145,7 +135,6 @@ export {
145
135
  ChevronLeftIcon,
146
136
  ClockIcon,
147
137
  CloseIcon,
148
- CurrentLocationIcon,
149
138
  EditIcon,
150
139
  BathroomsIcon,
151
140
  BedroomsIcon,