@scripso-homepad/ui 0.4.33 → 0.4.35

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.33",
3
+ "version": "0.4.35",
4
4
  "type": "module",
5
5
  "description": "Cross-platform UI components for Homepad (React Web + React Native)",
6
6
  "license": "MIT",
@@ -61,6 +61,10 @@ type DatePickerFieldProps = {
61
61
  hintClassName?: string;
62
62
  /** Opens the calendar popup on mount (useful for Storybook previews). */
63
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;
64
68
  /** Closes the popup after a complete range is selected. Defaults to true in range mode. */
65
69
  closeOnRangeComplete?: boolean;
66
70
  };
@@ -108,6 +112,8 @@ export function DatePicker(props: DatePickerProps) {
108
112
  errorClassName,
109
113
  hintClassName,
110
114
  defaultOpen = false,
115
+ open: controlledOpen,
116
+ onOpenChange,
111
117
  } = props;
112
118
  const mode = props.mode ?? "single";
113
119
  const closeOnRangeComplete =
@@ -119,7 +125,9 @@ export function DatePicker(props: DatePickerProps) {
119
125
  const wrapperRef = useRef<ComponentRef<typeof View>>(null);
120
126
  const triggerRef = useRef<ComponentRef<typeof Pressable>>(null);
121
127
  const helperRef = useRef<ComponentRef<typeof Text>>(null);
122
- const [open, setOpen] = useState(defaultOpen);
128
+ const isControlled = controlledOpen !== undefined;
129
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
130
+ const open = isControlled ? controlledOpen : uncontrolledOpen;
123
131
  const [anchor, setAnchor] = useState<{
124
132
  x: number;
125
133
  y: number;
@@ -131,6 +139,12 @@ export function DatePicker(props: DatePickerProps) {
131
139
  useApplyWebClassName(triggerRef, fieldClassName);
132
140
  useApplyWebClassName(helperRef, error ? errorClassName : hintClassName);
133
141
 
142
+ useEffect(() => {
143
+ if (!open) {
144
+ setAnchor(null);
145
+ }
146
+ }, [open]);
147
+
134
148
  useEffect(() => {
135
149
  if (!defaultOpen || disabled) {
136
150
  return;
@@ -139,7 +153,7 @@ export function DatePicker(props: DatePickerProps) {
139
153
  const frame = requestAnimationFrame(() => {
140
154
  triggerRef.current?.measureInWindow((x, y, width, height) => {
141
155
  setAnchor({ x, y, width, height });
142
- setOpen(true);
156
+ setOpenState(true);
143
157
  });
144
158
  });
145
159
 
@@ -195,8 +209,16 @@ export function DatePicker(props: DatePickerProps) {
195
209
  : 0;
196
210
  const dropdownTop = anchor ? resolveCalendarDropdownTop(anchor) : 0;
197
211
 
212
+ function setOpenState(next: boolean) {
213
+ if (!isControlled) {
214
+ setUncontrolledOpen(next);
215
+ }
216
+
217
+ onOpenChange?.(next);
218
+ }
219
+
198
220
  function closePicker() {
199
- setOpen(false);
221
+ setOpenState(false);
200
222
  setAnchor(null);
201
223
  }
202
224
 
@@ -207,7 +229,7 @@ export function DatePicker(props: DatePickerProps) {
207
229
 
208
230
  triggerRef.current?.measureInWindow((x, y, width, height) => {
209
231
  setAnchor({ x, y, width, height });
210
- setOpen(true);
232
+ setOpenState(true);
211
233
  });
212
234
  }
213
235
 
@@ -292,7 +314,14 @@ export function DatePicker(props: DatePickerProps) {
292
314
  </Text>
293
315
  ) : null}
294
316
 
295
- <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
+ >
296
325
  <View style={styles.modalRoot}>
297
326
  <Pressable
298
327
  style={styles.backdrop}
@@ -302,6 +331,7 @@ export function DatePicker(props: DatePickerProps) {
302
331
  />
303
332
  {anchor ? (
304
333
  <View
334
+ pointerEvents="box-none"
305
335
  style={[
306
336
  styles.panelPosition,
307
337
  {
@@ -312,6 +342,7 @@ export function DatePicker(props: DatePickerProps) {
312
342
  },
313
343
  ]}
314
344
  >
345
+ <View pointerEvents="auto">
315
346
  {props.mode === "range" ? (
316
347
  <Calendar
317
348
  mode="range"
@@ -343,6 +374,7 @@ export function DatePicker(props: DatePickerProps) {
343
374
  className={panelClassName}
344
375
  />
345
376
  )}
377
+ </View>
346
378
  </View>
347
379
  ) : null}
348
380
  </View>
@@ -387,10 +419,13 @@ const styles = StyleSheet.create({
387
419
  },
388
420
  modalRoot: {
389
421
  flex: 1,
422
+ width: "100%",
423
+ height: "100%",
390
424
  },
391
425
  backdrop: {
392
426
  ...StyleSheet.absoluteFillObject,
393
- backgroundColor: colors.transparent,
427
+ backgroundColor: "rgba(0, 0, 0, 0.35)",
428
+ zIndex: 0,
394
429
  },
395
430
  panelPosition: {
396
431
  position: "absolute",
@@ -31,6 +31,8 @@ export type TableActionsMenuProps = {
31
31
  triggerAriaLabel: string;
32
32
  className?: string;
33
33
  menuClassName?: string;
34
+ /** Optional custom trigger icon. Defaults to TableMoreIcon. */
35
+ triggerIcon?: ReactNode;
34
36
  /** Dropdown menu width in pixels. Defaults to 231. Ignored when menuFitContent is true. */
35
37
  menuWidth?: number;
36
38
  /** Size the menu to its content width instead of a fixed pixel width. Defaults to true so labels stay on one line. */
@@ -73,6 +75,7 @@ export function TableActionsMenu({
73
75
  triggerAriaLabel,
74
76
  className,
75
77
  menuClassName,
78
+ triggerIcon,
76
79
  menuWidth = MENU_WIDTH_PX,
77
80
  menuFitContent = true,
78
81
  }: TableActionsMenuProps) {
@@ -193,13 +196,27 @@ export function TableActionsMenu({
193
196
  item.onSelect();
194
197
  }}
195
198
  className={cn(
196
- 'flex h-[51px] w-full cursor-pointer items-center gap-2.5 px-3 text-left typography-14 font-medium text-storm-gray-900 transition-colors hover:bg-storm-gray-0',
199
+ 'flex h-[51px] w-full cursor-pointer items-center gap-2.5 px-3 text-left typography-14 font-medium transition-colors hover:bg-storm-gray-0',
197
200
  index > 0 && 'border-t border-storm-gray-50',
198
201
  item.disabled && 'cursor-not-allowed opacity-50',
202
+ item.tone === 'danger'
203
+ ? 'text-[#AE0011]'
204
+ : item.tone === 'success'
205
+ ? 'text-green-600'
206
+ : 'text-storm-gray-900',
199
207
  )}
200
208
  >
201
209
  {icon ? (
202
- <span className="inline-flex size-5 shrink-0 items-center justify-center text-storm-gray-200">
210
+ <span
211
+ className={cn(
212
+ 'inline-flex size-5 shrink-0 items-center justify-center',
213
+ item.tone === 'danger'
214
+ ? 'text-[#AE0011]'
215
+ : item.tone === 'success'
216
+ ? 'text-green-600'
217
+ : 'text-storm-gray-200',
218
+ )}
219
+ >
203
220
  {icon}
204
221
  </span>
205
222
  ) : null}
@@ -215,7 +232,7 @@ export function TableActionsMenu({
215
232
  <TableActionButton
216
233
  ref={triggerRef}
217
234
  variant="menu"
218
- icon={<TableMoreIcon className="size-5" />}
235
+ icon={triggerIcon ?? <TableMoreIcon className="size-5" />}
219
236
  ariaLabel={triggerAriaLabel}
220
237
  onClick={() => setOpen((current) => !current)}
221
238
  aria-expanded={open}