@ecohouse/ui 0.1.39 → 0.1.41

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.41",
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,109 @@ 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
+ style={[styles.backdrop, { opacity: animatedBackdrop, pointerEvents: "none" }]}
317
+ />
177
318
  <Pressable
178
319
  accessibilityRole="button"
179
320
  accessibilityLabel={backdropAccessibilityLabel}
180
321
  disabled={!closeOnBackdropPress}
181
322
  onPress={closeOnBackdropPress ? onClose : undefined}
182
- style={styles.backdrop}
323
+ style={StyleSheet.absoluteFill}
183
324
  />
184
- {panel}
325
+ <Animated.View style={[styles.panelShell, { width: animatedWidth }]}>
326
+ {panelContent}
327
+ </Animated.View>
185
328
  </View>
186
329
  </RNModal>
187
330
  );
@@ -197,11 +340,19 @@ const styles = StyleSheet.create({
197
340
  ...StyleSheet.absoluteFillObject,
198
341
  backgroundColor: BACKDROP_COLOR,
199
342
  },
200
- panel: {
343
+ panelShell: {
201
344
  zIndex: 1,
345
+ height: "100%",
346
+ maxWidth: "100%",
347
+ overflow: "hidden",
348
+ alignItems: "flex-end",
349
+ minWidth: 0,
350
+ },
351
+ panelInner: {
202
352
  height: "100%",
203
353
  maxWidth: "100%",
204
354
  backgroundColor: colors.grey800,
355
+ flexShrink: 0,
205
356
  },
206
357
  header: {
207
358
  height: 65,
@@ -220,10 +371,11 @@ const styles = StyleSheet.create({
220
371
  fontFamily: fonts.sans,
221
372
  fontSize: 16,
222
373
  fontWeight: "600",
223
- lineHeight: 16,
374
+ lineHeight: 22,
224
375
  letterSpacing: 0,
225
376
  color: colors.white,
226
377
  marginRight: 12,
378
+ overflow: "visible",
227
379
  },
228
380
  closeButton: {
229
381
  width: CLOSE_ICON_SIZE,
@@ -198,7 +198,7 @@ function MultiValueChips({ options, disabled, onRemove }: MultiValueChipsProps)
198
198
  return (
199
199
  <View style={styles.multiValueRoot}>
200
200
  {/* Hidden copy of every chip; RN has no synchronous text measurement. */}
201
- <View pointerEvents="none" style={styles.measureRow}>
201
+ <View style={[styles.measureRow, { pointerEvents: "none" }]}>
202
202
  {options.map((option) => (
203
203
  <View
204
204
  key={`measure-${option.value}`}
@@ -723,7 +723,7 @@ export const Select = forwardRef<ComponentRef<typeof View>, SelectProps>(
723
723
  ]}
724
724
  >
725
725
  {multiple ? (
726
- <View pointerEvents="none" style={styles.itemCheckbox}>
726
+ <View style={[styles.itemCheckbox, { pointerEvents: "none" }]}>
727
727
  <Checkbox value={isSelected} variant="light" />
728
728
  </View>
729
729
  ) : null}
@@ -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,
@@ -25,7 +23,6 @@ export {
25
23
  Modal,
26
24
  Drawer,
27
25
  Input,
28
- AutocompleteInput,
29
26
  VerificationCodeInput,
30
27
  Select,
31
28
  Textarea,
@@ -49,10 +46,8 @@ export type {
49
46
  InfoTileProps,
50
47
  ContactInfoCardProps,
51
48
  AccordionItemProps,
52
- AccordionProps,
53
49
  StepListProps,
54
50
  CounterProps,
55
- QuantityInputProps,
56
51
  SegmentedToggleOption,
57
52
  SegmentedToggleProps,
58
53
  SidebarProps,
@@ -78,8 +73,6 @@ export type {
78
73
  InputIcon,
79
74
  InputProps,
80
75
  InputSize,
81
- AutocompleteInputProps,
82
- AutocompleteOption,
83
76
  VerificationCodeInputHandle,
84
77
  VerificationCodeInputProps,
85
78
  SelectIcon,
@@ -128,7 +121,6 @@ export {
128
121
  iconGroups,
129
122
  iconGroupNames,
130
123
  getIconsByGroup,
131
- ArrowLeftIcon,
132
124
  ArrowRightIcon,
133
125
  BagIcon,
134
126
  BanIcon,
@@ -145,7 +137,6 @@ export {
145
137
  ChevronLeftIcon,
146
138
  ClockIcon,
147
139
  CloseIcon,
148
- CurrentLocationIcon,
149
140
  EditIcon,
150
141
  BathroomsIcon,
151
142
  BedroomsIcon,