@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.
Files changed (94) hide show
  1. package/README.md +5 -0
  2. package/dist/index.cjs +1594 -523
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +218 -10
  5. package/dist/index.d.ts +218 -10
  6. package/dist/index.js +1516 -471
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/AccountHeader/AccountHeader.stories.tsx +70 -0
  10. package/src/components/AccountHeader/AccountHeader.tsx +224 -0
  11. package/src/components/AccountHeader/index.ts +2 -0
  12. package/src/components/AvatarSimple/AvatarSimple.tsx +2 -2
  13. package/src/components/Button/Button.tsx +4 -1
  14. package/src/components/DatePicker/DatePicker.tsx +129 -54
  15. package/src/components/FloatingActionButton/FloatingActionButton.stories.tsx +42 -0
  16. package/src/components/FloatingActionButton/FloatingActionButton.tsx +77 -0
  17. package/src/components/FloatingActionButton/index.ts +2 -0
  18. package/src/components/InfoTile/InfoTile.stories.tsx +80 -0
  19. package/src/components/InfoTile/InfoTile.tsx +118 -0
  20. package/src/components/InfoTile/index.ts +2 -0
  21. package/src/components/PaginationDots/PaginationDots.stories.tsx +48 -0
  22. package/src/components/PaginationDots/PaginationDots.tsx +111 -0
  23. package/src/components/PaginationDots/index.ts +2 -0
  24. package/src/components/Select/Select.tsx +5 -2
  25. package/src/components/Sidebar/Sidebar.tsx +25 -11
  26. package/src/components/StatusBadge/StatusBadge.stories.tsx +54 -0
  27. package/src/components/StatusBadge/StatusBadge.tsx +108 -0
  28. package/src/components/StatusBadge/index.ts +2 -0
  29. package/src/components/StepList/StepList.stories.tsx +37 -0
  30. package/src/components/StepList/StepList.tsx +152 -0
  31. package/src/components/StepList/index.ts +2 -0
  32. package/src/components/StepPager/StepPager.tsx +150 -0
  33. package/src/components/StepPager/index.ts +2 -0
  34. package/src/components/index.ts +21 -0
  35. package/src/icons/AlertTriangle/AlertTriangleIcon.tsx +19 -0
  36. package/src/icons/AlertTriangle/AlertTriangleIcon.web.tsx +18 -0
  37. package/src/icons/AlertTriangle/alertTrianglePath.ts +3 -0
  38. package/src/icons/AlertTriangle/index.ts +1 -0
  39. package/src/icons/Ban/BanIcon.tsx +15 -0
  40. package/src/icons/Ban/banPath.ts +2 -0
  41. package/src/icons/Ban/index.ts +1 -0
  42. package/src/icons/Bell/BellIcon.tsx +2 -2
  43. package/src/icons/Bell/BellIcon.web.tsx +2 -2
  44. package/src/icons/Bell/bellPath.ts +1 -1
  45. package/src/icons/CottageDetail/CottageDetailIcons.tsx +15 -0
  46. package/src/icons/CottageDetail/CottageDetailIcons.web.tsx +15 -0
  47. package/src/icons/CottageDetail/cottageDetailPaths.ts +3 -0
  48. package/src/icons/CottageDetail/index.ts +1 -0
  49. package/src/icons/Edit/EditIcon.tsx +20 -0
  50. package/src/icons/Edit/EditIcon.web.tsx +19 -0
  51. package/src/icons/Edit/editPath.ts +3 -0
  52. package/src/icons/Edit/index.ts +1 -0
  53. package/src/icons/Fullscreen/FullscreenIcon.tsx +21 -0
  54. package/src/icons/Fullscreen/fullscreenPath.ts +2 -0
  55. package/src/icons/Fullscreen/index.ts +1 -0
  56. package/src/icons/Home/homePath.ts +1 -1
  57. package/src/icons/HouseRules/HouseRulesIcons.tsx +62 -0
  58. package/src/icons/HouseRules/HouseRulesIcons.web.tsx +61 -0
  59. package/src/icons/HouseRules/houseRulesPaths.ts +24 -0
  60. package/src/icons/HouseRules/index.ts +2 -0
  61. package/src/icons/LocationUser/LocationUserIcon.tsx +15 -0
  62. package/src/icons/LocationUser/LocationUserIcon.web.tsx +14 -0
  63. package/src/icons/LocationUser/index.ts +1 -0
  64. package/src/icons/LocationUser/locationUserPath.ts +2 -0
  65. package/src/icons/Lock/LockIcon.tsx +21 -0
  66. package/src/icons/Lock/LockIcon.web.tsx +26 -0
  67. package/src/icons/Lock/index.ts +1 -0
  68. package/src/icons/Lock/lockPath.ts +3 -0
  69. package/src/icons/Refresh/RefreshIcon.tsx +20 -0
  70. package/src/icons/Refresh/RefreshIcon.web.tsx +19 -0
  71. package/src/icons/Refresh/index.ts +1 -0
  72. package/src/icons/Refresh/refreshPath.ts +3 -0
  73. package/src/icons/RefundStatus/RefundStatusIcon.tsx +24 -0
  74. package/src/icons/RefundStatus/RefundStatusIcon.web.tsx +23 -0
  75. package/src/icons/RefundStatus/index.ts +1 -0
  76. package/src/icons/RefundStatus/refundStatusPath.ts +3 -0
  77. package/src/icons/Sort/SortIcon.tsx +3 -3
  78. package/src/icons/Sort/SortIcon.web.tsx +2 -2
  79. package/src/icons/Sort/sortPath.ts +3 -2
  80. package/src/icons/VideoOff/VideoOffIcon.tsx +21 -0
  81. package/src/icons/VideoOff/VideoOffIcon.web.tsx +27 -0
  82. package/src/icons/VideoOff/index.ts +1 -0
  83. package/src/icons/VideoOff/videoOffPath.ts +2 -0
  84. package/src/icons/ZoomOut/ZoomOutIcon.tsx +21 -0
  85. package/src/icons/ZoomOut/index.ts +1 -0
  86. package/src/icons/ZoomOut/zoomOutPath.ts +2 -0
  87. package/src/icons/index.ts +12 -0
  88. package/src/icons/registry.ts +42 -0
  89. package/src/index.ts +34 -0
  90. package/src/theme/colors.test.ts +1 -1
  91. package/src/theme/colors.ts +4 -0
  92. package/src/theme/typography.ts +5 -5
  93. package/src/utils/dateUtils.ts +25 -0
  94. package/src/utils/popoverAlign.ts +85 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecohouse/ui",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Cross-platform presentation-only UI components for EcoHouse (React Native + React Native Web)",
@@ -0,0 +1,70 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { fn } from "storybook/test";
3
+ import { StyleSheet, View } from "react-native";
4
+ import { StatusBadge } from "../StatusBadge";
5
+ import { AccountHeader } from "./AccountHeader";
6
+
7
+ const meta = {
8
+ title: "Components/AccountHeader",
9
+ component: AccountHeader,
10
+ args: {
11
+ title: "Իմ ամրագրումները",
12
+ onNotificationsPress: fn(),
13
+ notificationsAccessibilityLabel: "Ծանուցումներ",
14
+ menuAccessibilityLabel: "Բացել ընտրացանկը",
15
+ },
16
+ argTypes: {
17
+ onMenuPress: { action: "menu press" },
18
+ onNotificationsPress: { action: "notifications press" },
19
+ },
20
+ parameters: {
21
+ backgrounds: { default: "black" },
22
+ layout: "fullscreen",
23
+ },
24
+ } satisfies Meta<typeof AccountHeader>;
25
+
26
+ export default meta;
27
+ type Story = StoryObj<typeof meta>;
28
+
29
+ export const Desktop: Story = {
30
+ render: (args) => (
31
+ <View style={styles.desktopFrame}>
32
+ <AccountHeader {...args} />
33
+ </View>
34
+ ),
35
+ };
36
+
37
+ export const Mobile: Story = {
38
+ args: {
39
+ onMenuPress: fn(),
40
+ },
41
+ render: (args) => (
42
+ <View style={styles.mobileFrame}>
43
+ <AccountHeader {...args} />
44
+ </View>
45
+ ),
46
+ };
47
+
48
+ export const WithBackAndStatus: Story = {
49
+ args: {
50
+ title: "Ամրագրում #BK-9087",
51
+ onBackPress: fn(),
52
+ },
53
+ render: (args) => (
54
+ <View style={styles.desktopFrame}>
55
+ <AccountHeader {...args} badge={<StatusBadge label="Ակտիվ" tone="active" />} />
56
+ </View>
57
+ ),
58
+ };
59
+
60
+ const styles = StyleSheet.create({
61
+ desktopFrame: {
62
+ width: "100%",
63
+ maxWidth: 960,
64
+ backgroundColor: "#090909",
65
+ },
66
+ mobileFrame: {
67
+ width: 390,
68
+ backgroundColor: "#090909",
69
+ },
70
+ });
@@ -0,0 +1,224 @@
1
+ import { forwardRef, useMemo, useRef, type ComponentRef, type ReactNode } from "react";
2
+ import {
3
+ Pressable,
4
+ StyleSheet,
5
+ Text,
6
+ View,
7
+ type StyleProp,
8
+ type ViewProps,
9
+ type ViewStyle,
10
+ } from "react-native";
11
+ import { BellIcon, ChevronLeftIcon, MenuIcon } from "../../icons";
12
+ import { colors, fonts } from "../../theme";
13
+ import { mergeRefs } from "../../utils/mergeRefs";
14
+ import { useApplyWebClassName } from "../../utils/useApplyWebClassName";
15
+
16
+ export interface AccountHeaderProps extends Omit<ViewProps, "style" | "children"> {
17
+ title: string;
18
+ /**
19
+ * When set, shows a 32×32 back control (takes priority over `onMenuPress`).
20
+ * Used on nested account pages such as booking details.
21
+ */
22
+ onBackPress?: () => void;
23
+ /** When set, shows the 28×28 menu control (mobile account shell). Ignored if `onBackPress` is set. */
24
+ onMenuPress?: () => void;
25
+ onNotificationsPress?: () => void;
26
+ /** Optional status chip (or other accessory) rendered after the title with a 12px gap. */
27
+ badge?: ReactNode;
28
+ /** Accessible name for the back control. Defaults to `"Back"`. */
29
+ backAccessibilityLabel?: string;
30
+ /** Accessible name for the menu control. Defaults to `"Open menu"`. */
31
+ menuAccessibilityLabel?: string;
32
+ /** Accessible name for notifications. Defaults to `"Notifications"`. */
33
+ notificationsAccessibilityLabel?: string;
34
+ style?: StyleProp<ViewStyle>;
35
+ className?: string;
36
+ }
37
+
38
+ const HEADER_HEIGHT = 64;
39
+ const MENU_ICON_SIZE = 28;
40
+ const ICON_BUTTON_SIZE = 32;
41
+ const ICON_BUTTON_ICON_SIZE = 16;
42
+ const BACK_TITLE_GAP = 12;
43
+ const TITLE_BADGE_GAP = 12;
44
+ const MENU_TITLE_GAP = 16;
45
+ const RIGHT_GAP = 16;
46
+
47
+ export const AccountHeader = forwardRef<ComponentRef<typeof View>, AccountHeaderProps>(
48
+ function AccountHeader(
49
+ {
50
+ title,
51
+ onBackPress,
52
+ onMenuPress,
53
+ onNotificationsPress,
54
+ badge,
55
+ backAccessibilityLabel = "Back",
56
+ menuAccessibilityLabel = "Open menu",
57
+ notificationsAccessibilityLabel = "Notifications",
58
+ style,
59
+ className,
60
+ ...rest
61
+ },
62
+ forwardedRef,
63
+ ) {
64
+ const containerRef = useRef<ComponentRef<typeof View>>(null);
65
+ const setContainerRef = useMemo(() => mergeRefs(containerRef, forwardedRef), [forwardedRef]);
66
+ const resolvedClassName = className?.trim() || undefined;
67
+ const showBack = onBackPress != null;
68
+ const showMenu = !showBack && onMenuPress != null;
69
+ const isCompact = showMenu;
70
+
71
+ useApplyWebClassName(containerRef, resolvedClassName);
72
+
73
+ return (
74
+ <View
75
+ {...rest}
76
+ ref={setContainerRef}
77
+ accessibilityRole="header"
78
+ style={[styles.base, isCompact ? styles.compact : styles.desktop, style]}
79
+ >
80
+ <View
81
+ style={[
82
+ styles.start,
83
+ showBack ? styles.startWithBack : null,
84
+ showMenu ? styles.startWithMenu : null,
85
+ ]}
86
+ >
87
+ {showBack ? (
88
+ <Pressable
89
+ accessibilityRole="button"
90
+ accessibilityLabel={backAccessibilityLabel}
91
+ hitSlop={8}
92
+ onPress={onBackPress}
93
+ style={styles.iconButton}
94
+ >
95
+ <ChevronLeftIcon
96
+ color={colors.white}
97
+ size={ICON_BUTTON_ICON_SIZE}
98
+ strokeWidth={1.5}
99
+ />
100
+ </Pressable>
101
+ ) : null}
102
+
103
+ {showMenu ? (
104
+ <Pressable
105
+ accessibilityRole="button"
106
+ accessibilityLabel={menuAccessibilityLabel}
107
+ hitSlop={8}
108
+ onPress={onMenuPress}
109
+ style={styles.menuButton}
110
+ >
111
+ <MenuIcon color={colors.white} size={MENU_ICON_SIZE} strokeWidth={1.5} />
112
+ </Pressable>
113
+ ) : null}
114
+
115
+ <View style={styles.titleRow}>
116
+ <Text
117
+ numberOfLines={1}
118
+ style={[
119
+ styles.title,
120
+ showBack || !isCompact ? styles.titleDesktop : styles.titleCompact,
121
+ ]}
122
+ >
123
+ {title}
124
+ </Text>
125
+ {badge != null ? <View style={styles.badgeSlot}>{badge}</View> : null}
126
+ </View>
127
+ </View>
128
+
129
+ <View style={styles.end}>
130
+ <Pressable
131
+ accessibilityRole="button"
132
+ accessibilityLabel={notificationsAccessibilityLabel}
133
+ onPress={onNotificationsPress}
134
+ style={styles.iconButton}
135
+ >
136
+ <BellIcon color={colors.white} size={ICON_BUTTON_ICON_SIZE} strokeWidth={1.5} />
137
+ </Pressable>
138
+ </View>
139
+ </View>
140
+ );
141
+ },
142
+ );
143
+
144
+ const styles = StyleSheet.create({
145
+ base: {
146
+ height: HEADER_HEIGHT,
147
+ width: "100%",
148
+ flexDirection: "row",
149
+ alignItems: "center",
150
+ justifyContent: "space-between",
151
+ backgroundColor: colors.dark,
152
+ borderBottomWidth: 1,
153
+ borderBottomColor: colors.grey700,
154
+ },
155
+ desktop: {
156
+ paddingTop: 16,
157
+ paddingBottom: 16,
158
+ paddingLeft: 24,
159
+ paddingRight: 24,
160
+ },
161
+ compact: {
162
+ padding: 16,
163
+ },
164
+ start: {
165
+ flex: 1,
166
+ minWidth: 0,
167
+ flexDirection: "row",
168
+ alignItems: "center",
169
+ },
170
+ startWithBack: {
171
+ gap: BACK_TITLE_GAP,
172
+ },
173
+ startWithMenu: {
174
+ gap: MENU_TITLE_GAP,
175
+ },
176
+ titleRow: {
177
+ flex: 1,
178
+ minWidth: 0,
179
+ flexDirection: "row",
180
+ alignItems: "center",
181
+ gap: TITLE_BADGE_GAP,
182
+ },
183
+ end: {
184
+ flexShrink: 0,
185
+ marginLeft: RIGHT_GAP,
186
+ },
187
+ iconButton: {
188
+ width: ICON_BUTTON_SIZE,
189
+ height: ICON_BUTTON_SIZE,
190
+ padding: 8,
191
+ alignItems: "center",
192
+ justifyContent: "center",
193
+ borderRadius: 5,
194
+ backgroundColor: colors.grey800,
195
+ borderWidth: 1,
196
+ borderColor: colors.grey700,
197
+ flexShrink: 0,
198
+ },
199
+ menuButton: {
200
+ width: MENU_ICON_SIZE,
201
+ height: MENU_ICON_SIZE,
202
+ alignItems: "center",
203
+ justifyContent: "center",
204
+ flexShrink: 0,
205
+ },
206
+ title: {
207
+ flexShrink: 1,
208
+ fontFamily: fonts.sans,
209
+ fontWeight: "600",
210
+ letterSpacing: 0,
211
+ color: colors.white,
212
+ },
213
+ titleDesktop: {
214
+ fontSize: 16,
215
+ lineHeight: 22,
216
+ },
217
+ titleCompact: {
218
+ fontSize: 14,
219
+ lineHeight: 20,
220
+ },
221
+ badgeSlot: {
222
+ flexShrink: 0,
223
+ },
224
+ });
@@ -0,0 +1,2 @@
1
+ export { AccountHeader } from "./AccountHeader";
2
+ export type { AccountHeaderProps } from "./AccountHeader";
@@ -83,10 +83,10 @@ export const AvatarSimple = forwardRef<ComponentRef<typeof View>, AvatarSimplePr
83
83
  {!compact ? (
84
84
  <View style={styles.details}>
85
85
  <View style={styles.textBlock}>
86
- <Text style={styles.name} numberOfLines={1}>
86
+ <Text style={styles.name} numberOfLines={1} ellipsizeMode="tail">
87
87
  {name}
88
88
  </Text>
89
- <Text style={styles.email} numberOfLines={1}>
89
+ <Text style={styles.email} numberOfLines={1} ellipsizeMode="tail">
90
90
  {email}
91
91
  </Text>
92
92
  </View>
@@ -31,6 +31,8 @@ export interface ButtonProps extends Omit<
31
31
  size?: ButtonSize;
32
32
  showIcon?: boolean;
33
33
  icon?: ReactNode;
34
+ /** Overrides the trailing icon badge fill (defaults to the variant preset). */
35
+ iconBadgeBackgroundColor?: string;
34
36
  style?: StyleProp<ViewStyle>;
35
37
  textStyle?: StyleProp<TextStyle>;
36
38
  className?: string;
@@ -97,6 +99,7 @@ export const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonPr
97
99
  size = "lg",
98
100
  showIcon = false,
99
101
  icon,
102
+ iconBadgeBackgroundColor,
100
103
  style,
101
104
  textStyle,
102
105
  className,
@@ -182,7 +185,7 @@ export const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonPr
182
185
  width: metrics.iconContainerSize,
183
186
  height: metrics.iconContainerSize,
184
187
  borderRadius: metrics.iconContainerSize / 2,
185
- backgroundColor: preset.iconBadgeBackgroundColor,
188
+ backgroundColor: iconBadgeBackgroundColor ?? preset.iconBadgeBackgroundColor,
186
189
  },
187
190
  ]}
188
191
  >
@@ -2,6 +2,7 @@ import {
2
2
  forwardRef,
3
3
  useCallback,
4
4
  useEffect,
5
+ useLayoutEffect,
5
6
  useMemo,
6
7
  useRef,
7
8
  useState,
@@ -22,6 +23,7 @@ import { colors, datePickerTypography } from "../../theme";
22
23
  import {
23
24
  addMonths,
24
25
  formatDate,
26
+ formatDateDayShortMonth,
25
27
  getMonthMatrix,
26
28
  isBefore,
27
29
  isDateDisabled,
@@ -92,6 +94,11 @@ type DatePickerBaseProps = Omit<ViewProps, "style" | "children"> & {
92
94
  weekStartsOn?: 0 | 1;
93
95
  /** Styles the calendar popup independently from the trigger width. */
94
96
  calendarStyle?: StyleProp<ViewStyle>;
97
+ /**
98
+ * When true, only the calendar panel is rendered (no field/trigger).
99
+ * Use with controlled `open` for icon-button / popover hosts.
100
+ */
101
+ hideTrigger?: boolean;
95
102
  style?: StyleProp<ViewStyle>;
96
103
  className?: string;
97
104
  };
@@ -137,9 +144,7 @@ function RangeTriggerField({
137
144
  {label}
138
145
  </Text>
139
146
  <View style={styles.rangeValueRow}>
140
- <Text numberOfLines={1} style={styles.rangeDate}>
141
- {value}
142
- </Text>
147
+ <Text style={styles.rangeDate}>{value}</Text>
143
148
  {time ? (
144
149
  <View style={styles.rangeTimeWrap}>
145
150
  <ClockFilledIcon color={colors.lightGrey} size={16} />
@@ -172,9 +177,10 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
172
177
  defaultVisibleMonth,
173
178
  monthNames = DEFAULT_MONTH_NAMES,
174
179
  weekdayLabels = DEFAULT_WEEKDAY_LABELS,
175
- formatValue = formatDate,
180
+ formatValue: formatValueProp,
176
181
  weekStartsOn = 1,
177
182
  calendarStyle,
183
+ hideTrigger = false,
178
184
  style,
179
185
  className,
180
186
  accessibilityLabel,
@@ -188,6 +194,10 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
188
194
  } = props;
189
195
 
190
196
  const isRange = props.range === true;
197
+ const usesRangeTrigger = isRange && variant !== "default";
198
+ const formatValue =
199
+ formatValueProp ??
200
+ (usesRangeTrigger ? (date: Date) => formatDateDayShortMonth(date, monthNames) : formatDate);
191
201
  const valueProp = props.value;
192
202
  const rootRef = useRef<ComponentRef<typeof View>>(null);
193
203
  const triggerRef = useRef<ComponentRef<typeof Pressable>>(null);
@@ -204,6 +214,8 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
204
214
  );
205
215
  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
206
216
  const [hoveredDayKey, setHoveredDayKey] = useState<number | null>(null);
217
+ /** Web: flip calendar to the trigger's right edge when it would overflow the viewport. */
218
+ const [menuAlignEnd, setMenuAlignEnd] = useState(false);
207
219
 
208
220
  const rangeValue: DateRange = isRange
209
221
  ? isValueControlled
@@ -218,6 +230,13 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
218
230
 
219
231
  const isOpen = isOpenControlled ? openProp : uncontrolledOpen;
220
232
 
233
+ const defaultMenuWidth = usesRangeTrigger ? RANGE_TRIGGER_WIDTH : DEFAULT_WIDTH;
234
+ const resolvedMenuWidth = useMemo(() => {
235
+ const flat = StyleSheet.flatten(calendarStyle);
236
+ const width = flat?.width;
237
+ return typeof width === "number" ? width : defaultMenuWidth;
238
+ }, [calendarStyle, defaultMenuWidth]);
239
+
221
240
  const [visibleMonth, setVisibleMonth] = useState(() => {
222
241
  const anchor =
223
242
  defaultVisibleMonth ?? (isRange ? rangeValue.start : singleValue) ?? new Date();
@@ -235,11 +254,33 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
235
254
  setVisibleMonth(new Date(anchor.getFullYear(), anchor.getMonth(), 1));
236
255
  } else {
237
256
  setHoveredDayKey(null);
257
+ setMenuAlignEnd(false);
238
258
  }
239
259
  },
240
260
  [isOpenControlled, isRange, onOpenChange, rangeValue.start, singleValue],
241
261
  );
242
262
 
263
+ // Prefer left alignment; flip to right when the menu would overflow the viewport (web).
264
+ useLayoutEffect(() => {
265
+ if (Platform.OS !== "web" || !isOpen || hideTrigger) return;
266
+
267
+ const updateMenuAlign = () => {
268
+ const trigger = triggerRef.current as unknown as {
269
+ getBoundingClientRect?: () => DOMRect;
270
+ } | null;
271
+ const rect = trigger?.getBoundingClientRect?.();
272
+ if (!rect) return;
273
+
274
+ const viewportPadding = 8;
275
+ const overflowsRight = rect.left + resolvedMenuWidth > window.innerWidth - viewportPadding;
276
+ setMenuAlignEnd(overflowsRight);
277
+ };
278
+
279
+ updateMenuAlign();
280
+ window.addEventListener("resize", updateMenuAlign);
281
+ return () => window.removeEventListener("resize", updateMenuAlign);
282
+ }, [hideTrigger, isOpen, resolvedMenuWidth]);
283
+
243
284
  // Close on outside click (web only; native has no document-level events).
244
285
  useEffect(() => {
245
286
  if (Platform.OS !== "web" || !isOpen) return;
@@ -347,7 +388,6 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
347
388
  ? formatValue(singleValue)
348
389
  : placeholder;
349
390
 
350
- const usesRangeTrigger = isRange && variant !== "default";
351
391
  const usesVerticalRangeTrigger = usesRangeTrigger && variant === "range-vertical";
352
392
  const triggerBorderColor = usesRangeTrigger
353
393
  ? isOpen
@@ -369,6 +409,7 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
369
409
  style={[
370
410
  styles.root,
371
411
  usesRangeTrigger && styles.rangeRoot,
412
+ hideTrigger && styles.rootHideTrigger,
372
413
  isOpen && styles.rootOpen,
373
414
  disabled && styles.disabled,
374
415
  style,
@@ -376,52 +417,65 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
376
417
  accessibilityState={{ disabled, expanded: isOpen }}
377
418
  >
378
419
  <View style={[styles.triggerWrap, isOpen && styles.triggerWrapOpen]}>
379
- <Pressable
380
- ref={triggerRef}
381
- onPress={handleTriggerPress}
382
- disabled={disabled}
383
- accessibilityRole="button"
384
- accessibilityLabel={resolvedAccessibilityLabel}
385
- accessibilityState={{ disabled, expanded: isOpen }}
386
- style={[
387
- styles.trigger,
388
- usesRangeTrigger && styles.rangeTrigger,
389
- usesRangeTrigger &&
390
- (usesVerticalRangeTrigger
391
- ? styles.rangeTriggerVertical
392
- : styles.rangeTriggerHorizontal),
393
- {
394
- borderColor: triggerBorderColor,
395
- backgroundColor: usesRangeTrigger ? "transparent" : colors.grey700,
396
- },
397
- ]}
398
- >
399
- {usesRangeTrigger ? (
400
- <>
401
- <RangeTriggerField label={startLabel} time={startTime} value={startValue} />
402
- <View
403
- style={
404
- usesVerticalRangeTrigger
405
- ? styles.rangeDividerVertical
406
- : styles.rangeDividerHorizontal
407
- }
408
- />
409
- <RangeTriggerField label={endLabel} time={endTime} value={endValue} />
410
- </>
411
- ) : (
412
- <>
413
- <Text style={[styles.triggerText, { color: triggerTextColor }]} numberOfLines={1}>
414
- {triggerLabel}
415
- </Text>
416
- <View style={styles.iconSlot}>
417
- <CalendarOutlineIcon size={ICON_SIZE} color={colors.grey100} />
418
- </View>
419
- </>
420
- )}
421
- </Pressable>
420
+ {hideTrigger ? null : (
421
+ <Pressable
422
+ ref={triggerRef}
423
+ onPress={handleTriggerPress}
424
+ disabled={disabled}
425
+ accessibilityRole="button"
426
+ accessibilityLabel={resolvedAccessibilityLabel}
427
+ accessibilityState={{ disabled, expanded: isOpen }}
428
+ style={[
429
+ styles.trigger,
430
+ usesRangeTrigger && styles.rangeTrigger,
431
+ usesRangeTrigger &&
432
+ (usesVerticalRangeTrigger
433
+ ? styles.rangeTriggerVertical
434
+ : styles.rangeTriggerHorizontal),
435
+ {
436
+ borderColor: triggerBorderColor,
437
+ backgroundColor: usesRangeTrigger ? "transparent" : colors.grey700,
438
+ },
439
+ ]}
440
+ >
441
+ {usesRangeTrigger ? (
442
+ <>
443
+ <RangeTriggerField label={startLabel} time={startTime} value={startValue} />
444
+ <View
445
+ style={
446
+ usesVerticalRangeTrigger
447
+ ? styles.rangeDividerVertical
448
+ : styles.rangeDividerHorizontal
449
+ }
450
+ />
451
+ <RangeTriggerField label={endLabel} time={endTime} value={endValue} />
452
+ </>
453
+ ) : (
454
+ <>
455
+ <Text style={[styles.triggerText, { color: triggerTextColor }]} numberOfLines={1}>
456
+ {triggerLabel}
457
+ </Text>
458
+ <View style={styles.iconSlot}>
459
+ <CalendarOutlineIcon size={ICON_SIZE} color={colors.grey100} />
460
+ </View>
461
+ </>
462
+ )}
463
+ </Pressable>
464
+ )}
422
465
 
423
466
  {isOpen ? (
424
- <View style={[styles.menu, usesRangeTrigger && styles.rangeMenu, calendarStyle]}>
467
+ <View
468
+ style={[
469
+ styles.menu,
470
+ usesRangeTrigger && styles.rangeMenu,
471
+ hideTrigger
472
+ ? styles.menuInline
473
+ : menuAlignEnd
474
+ ? styles.menuAlignEnd
475
+ : styles.menuAlignStart,
476
+ calendarStyle,
477
+ ]}
478
+ >
425
479
  <View style={styles.calendarHeader}>
426
480
  <Pressable
427
481
  onPress={handlePrevMonth}
@@ -588,6 +642,10 @@ const styles = StyleSheet.create({
588
642
  gap: 8,
589
643
  alignSelf: "flex-start",
590
644
  },
645
+ rootHideTrigger: {
646
+ width: "auto",
647
+ gap: 0,
648
+ },
591
649
  rootOpen: {
592
650
  zIndex: 30,
593
651
  },
@@ -641,7 +699,7 @@ const styles = StyleSheet.create({
641
699
  justifyContent: "center",
642
700
  gap: 8,
643
701
  paddingVertical: 12,
644
- paddingHorizontal: 16,
702
+ paddingHorizontal: 12,
645
703
  },
646
704
  rangeLabel: {
647
705
  ...datePickerTypography.rangeLabel,
@@ -651,13 +709,16 @@ const styles = StyleSheet.create({
651
709
  minWidth: 0,
652
710
  flexDirection: "row",
653
711
  alignItems: "center",
654
- justifyContent: "space-between",
655
- gap: 12,
712
+ justifyContent: "flex-start",
713
+ flexWrap: "nowrap",
714
+ gap: 10,
656
715
  },
657
716
  rangeDate: {
658
717
  ...datePickerTypography.rangeDate,
659
- flexShrink: 1,
718
+ flexShrink: 0,
660
719
  color: colors.white,
720
+ // @ts-expect-error RN Web — keep the full date on one line
721
+ whiteSpace: "nowrap",
661
722
  },
662
723
  rangeTimeWrap: {
663
724
  flexShrink: 0,
@@ -689,7 +750,6 @@ const styles = StyleSheet.create({
689
750
  menu: {
690
751
  position: "absolute",
691
752
  top: "100%",
692
- left: 0,
693
753
  width: DEFAULT_WIDTH,
694
754
  marginTop: 8,
695
755
  zIndex: 10,
@@ -706,6 +766,21 @@ const styles = StyleSheet.create({
706
766
  },
707
767
  }),
708
768
  },
769
+ menuAlignStart: {
770
+ left: 0,
771
+ right: "auto",
772
+ },
773
+ menuAlignEnd: {
774
+ left: "auto",
775
+ right: 0,
776
+ },
777
+ menuInline: {
778
+ position: "relative",
779
+ top: "auto",
780
+ left: "auto",
781
+ right: "auto",
782
+ marginTop: 0,
783
+ },
709
784
  rangeMenu: {
710
785
  width: RANGE_TRIGGER_WIDTH,
711
786
  },