@scripso-homepad/ui 0.4.31 → 0.4.34

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": "@scripso-homepad/ui",
3
- "version": "0.4.31",
3
+ "version": "0.4.34",
4
4
  "type": "module",
5
5
  "description": "Cross-platform UI components for Homepad (React Web + React Native)",
6
6
  "license": "MIT",
@@ -81,12 +81,9 @@ function resolveDayCellStyle(day: CalendarDayCell): StyleProp<ViewStyle> {
81
81
  return [calendarDropdownMetrics.dayCell, calendarDropdownMetrics.dayCellSelected];
82
82
  }
83
83
 
84
- const useMutedBackground =
85
- day.inCurrentMonth && !day.isToday && (day.isPast || day.isDisabled);
86
-
87
84
  return [
88
85
  calendarDropdownMetrics.dayCell,
89
- useMutedBackground ? calendarDropdownMetrics.dayCellPast : null,
86
+ day.isDisabled ? calendarDropdownMetrics.dayCellDisabled : null,
90
87
  ];
91
88
  }
92
89
 
@@ -103,7 +100,7 @@ function resolveDayLabelStyle(day: CalendarDayCell): StyleProp<TextStyle> {
103
100
  return [calendarDropdownMetrics.dayLabel, calendarDropdownMetrics.dayLabelToday];
104
101
  }
105
102
 
106
- if (day.isPast || day.isDisabled) {
103
+ if (day.isDisabled) {
107
104
  return [calendarDropdownMetrics.dayLabel, calendarDropdownMetrics.dayLabelMuted];
108
105
  }
109
106
 
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useRef, useState, type ComponentRef } from "react";
2
2
  import {
3
3
  Modal,
4
+ Platform,
4
5
  Pressable,
5
6
  StyleSheet,
6
7
  Text,
@@ -19,6 +20,7 @@ import {
19
20
  calendarFieldTypography,
20
21
  calendarLabelTypography,
21
22
  resolveCalendarDropdownLeft,
23
+ resolveCalendarDropdownLeftCentered,
22
24
  resolveCalendarDropdownTop,
23
25
  } from "../theme/calendar";
24
26
  import { getSelectFieldStyles, resolveInputVisualState, SELECT_LABEL_GAP } from "../theme/select";
@@ -59,6 +61,10 @@ type DatePickerFieldProps = {
59
61
  hintClassName?: string;
60
62
  /** Opens the calendar popup on mount (useful for Storybook previews). */
61
63
  defaultOpen?: boolean;
64
+ /** Controlled open state for the calendar popup. */
65
+ open?: boolean;
66
+ /** Called when the calendar popup opens or closes. */
67
+ onOpenChange?: (open: boolean) => void;
62
68
  /** Closes the popup after a complete range is selected. Defaults to true in range mode. */
63
69
  closeOnRangeComplete?: boolean;
64
70
  };
@@ -78,6 +84,8 @@ export type DatePickerRangeProps = Omit<CalendarRangeProps, "style" | "className
78
84
 
79
85
  export type DatePickerProps = DatePickerSingleProps | DatePickerRangeProps;
80
86
 
87
+ const isDesktopCalendar = Platform.OS === "web";
88
+
81
89
  export function DatePicker(props: DatePickerProps) {
82
90
  const {
83
91
  label,
@@ -104,6 +112,8 @@ export function DatePicker(props: DatePickerProps) {
104
112
  errorClassName,
105
113
  hintClassName,
106
114
  defaultOpen = false,
115
+ open: controlledOpen,
116
+ onOpenChange,
107
117
  } = props;
108
118
  const mode = props.mode ?? "single";
109
119
  const closeOnRangeComplete =
@@ -115,7 +125,9 @@ export function DatePicker(props: DatePickerProps) {
115
125
  const wrapperRef = useRef<ComponentRef<typeof View>>(null);
116
126
  const triggerRef = useRef<ComponentRef<typeof Pressable>>(null);
117
127
  const helperRef = useRef<ComponentRef<typeof Text>>(null);
118
- const [open, setOpen] = useState(defaultOpen);
128
+ const isControlled = controlledOpen !== undefined;
129
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
130
+ const open = isControlled ? controlledOpen : uncontrolledOpen;
119
131
  const [anchor, setAnchor] = useState<{
120
132
  x: number;
121
133
  y: number;
@@ -127,6 +139,12 @@ export function DatePicker(props: DatePickerProps) {
127
139
  useApplyWebClassName(triggerRef, fieldClassName);
128
140
  useApplyWebClassName(helperRef, error ? errorClassName : hintClassName);
129
141
 
142
+ useEffect(() => {
143
+ if (!open) {
144
+ setAnchor(null);
145
+ }
146
+ }, [open]);
147
+
130
148
  useEffect(() => {
131
149
  if (!defaultOpen || disabled) {
132
150
  return;
@@ -135,7 +153,7 @@ export function DatePicker(props: DatePickerProps) {
135
153
  const frame = requestAnimationFrame(() => {
136
154
  triggerRef.current?.measureInWindow((x, y, width, height) => {
137
155
  setAnchor({ x, y, width, height });
138
- setOpen(true);
156
+ setOpenState(true);
139
157
  });
140
158
  });
141
159
 
@@ -184,15 +202,23 @@ export function DatePicker(props: DatePickerProps) {
184
202
  };
185
203
  const helperMessage = error ?? hint;
186
204
  const panelWidth = CALENDAR_PANEL_WIDTH;
187
- const panelLeft = anchor ? resolveCalendarDropdownLeft(anchor, panelWidth) : 0;
205
+ const panelLeft = anchor
206
+ ? isDesktopCalendar
207
+ ? resolveCalendarDropdownLeft(anchor, panelWidth)
208
+ : resolveCalendarDropdownLeftCentered(panelWidth)
209
+ : 0;
188
210
  const dropdownTop = anchor ? resolveCalendarDropdownTop(anchor) : 0;
189
- const resolvedDefaultViewDate =
190
- defaultViewDate ??
191
- (props.mode === "range" ? props.value?.start : props.value) ??
192
- today;
211
+
212
+ function setOpenState(next: boolean) {
213
+ if (!isControlled) {
214
+ setUncontrolledOpen(next);
215
+ }
216
+
217
+ onOpenChange?.(next);
218
+ }
193
219
 
194
220
  function closePicker() {
195
- setOpen(false);
221
+ setOpenState(false);
196
222
  setAnchor(null);
197
223
  }
198
224
 
@@ -203,10 +229,15 @@ export function DatePicker(props: DatePickerProps) {
203
229
 
204
230
  triggerRef.current?.measureInWindow((x, y, width, height) => {
205
231
  setAnchor({ x, y, width, height });
206
- setOpen(true);
232
+ setOpenState(true);
207
233
  });
208
234
  }
209
235
 
236
+ const resolvedDefaultViewDate =
237
+ defaultViewDate ??
238
+ (props.mode === "range" ? props.value?.start : props.value) ??
239
+ today;
240
+
210
241
  function handleSingleSelect(date: Date | undefined) {
211
242
  if (props.mode === "range") {
212
243
  return;
@@ -283,7 +314,14 @@ export function DatePicker(props: DatePickerProps) {
283
314
  </Text>
284
315
  ) : null}
285
316
 
286
- <Modal visible={open} transparent animationType="fade" onRequestClose={closePicker}>
317
+ <Modal
318
+ visible={open}
319
+ transparent
320
+ animationType="fade"
321
+ onRequestClose={closePicker}
322
+ statusBarTranslucent
323
+ {...(Platform.OS === "ios" ? { presentationStyle: "overFullScreen" as const } : null)}
324
+ >
287
325
  <View style={styles.modalRoot}>
288
326
  <Pressable
289
327
  style={styles.backdrop}
@@ -293,6 +331,7 @@ export function DatePicker(props: DatePickerProps) {
293
331
  />
294
332
  {anchor ? (
295
333
  <View
334
+ pointerEvents="box-none"
296
335
  style={[
297
336
  styles.panelPosition,
298
337
  {
@@ -303,6 +342,7 @@ export function DatePicker(props: DatePickerProps) {
303
342
  },
304
343
  ]}
305
344
  >
345
+ <View pointerEvents="auto">
306
346
  {props.mode === "range" ? (
307
347
  <Calendar
308
348
  mode="range"
@@ -334,6 +374,7 @@ export function DatePicker(props: DatePickerProps) {
334
374
  className={panelClassName}
335
375
  />
336
376
  )}
377
+ </View>
337
378
  </View>
338
379
  ) : null}
339
380
  </View>
@@ -378,10 +419,13 @@ const styles = StyleSheet.create({
378
419
  },
379
420
  modalRoot: {
380
421
  flex: 1,
422
+ width: "100%",
423
+ height: "100%",
381
424
  },
382
425
  backdrop: {
383
426
  ...StyleSheet.absoluteFillObject,
384
- backgroundColor: colors.transparent,
427
+ backgroundColor: "rgba(0, 0, 0, 0.35)",
428
+ zIndex: 0,
385
429
  },
386
430
  panelPosition: {
387
431
  position: "absolute",
@@ -182,7 +182,7 @@ export const calendarDropdownMetrics = StyleSheet.create({
182
182
  } as ViewStyle)
183
183
  : null),
184
184
  },
185
- dayCellPast: {
185
+ dayCellDisabled: {
186
186
  backgroundColor: colors.stormGray50,
187
187
  },
188
188
  dayCellSelected: {
@@ -321,6 +321,14 @@ export function resolveCalendarDropdownLeft(
321
321
  return Math.max(0, Math.min(anchor.x, maxLeft));
322
322
  }
323
323
 
324
+ export function resolveCalendarDropdownLeftCentered(
325
+ panelWidth: number = CALENDAR_PANEL_WIDTH,
326
+ ): number {
327
+ const windowWidth = Dimensions.get("window").width;
328
+
329
+ return Math.max(0, (windowWidth - panelWidth) / 2);
330
+ }
331
+
324
332
  export const calendarFieldMetrics = StyleSheet.create({
325
333
  container: {
326
334
  flexDirection: "row",