@scripso-homepad/ui 0.4.37 → 0.4.39

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.37",
3
+ "version": "0.4.39",
4
4
  "type": "module",
5
5
  "description": "Cross-platform UI components for Homepad (React Web + React Native)",
6
6
  "license": "MIT",
@@ -598,6 +598,7 @@ const styles = StyleSheet.create({
598
598
  overflow: "hidden",
599
599
  textOverflow: "ellipsis",
600
600
  whiteSpace: "nowrap",
601
+ cursor: "pointer",
601
602
  } as const)
602
603
  : null),
603
604
  },
@@ -98,7 +98,10 @@ export function getSelectFieldStyles(state: InputVisualState): SelectFieldStyleS
98
98
  borderColor: colors.stormGray50,
99
99
  backgroundColor: colors.stormGray50,
100
100
  },
101
- value: { ...valueBase, color: colors.stormGray300 },
101
+ value: {
102
+ ...valueBase,
103
+ color: colors.stormGray300,
104
+ },
102
105
  placeholder: colors.stormGray300,
103
106
  icon: colors.stormGray150,
104
107
  };
@@ -155,6 +158,7 @@ export const selectFieldMetrics = StyleSheet.create({
155
158
  ...(Platform.OS === "web"
156
159
  ? ({
157
160
  boxSizing: "border-box",
161
+ cursor: "pointer",
158
162
  } as ViewStyle)
159
163
  : null),
160
164
  },
@@ -169,6 +173,7 @@ export const selectFieldMetrics = StyleSheet.create({
169
173
  ...(Platform.OS === "web"
170
174
  ? ({
171
175
  outlineStyle: "none",
176
+ cursor: "pointer",
172
177
  } as TextStyle)
173
178
  : null),
174
179
  },
@@ -182,6 +187,7 @@ export const selectDropdownMetrics = StyleSheet.create({
182
187
  backgroundColor: colors.white,
183
188
  padding: SELECT_DROPDOWN_PADDING,
184
189
  overflow: "hidden",
190
+ ...(Platform.OS === "web" ? ({ cursor: "pointer" } as ViewStyle) : null),
185
191
  },
186
192
  option: {
187
193
  width: "100%",
@@ -194,6 +200,7 @@ export const selectDropdownMetrics = StyleSheet.create({
194
200
  paddingLeft: spacing.sm,
195
201
  justifyContent: "center",
196
202
  alignItems: "flex-start",
203
+ ...(Platform.OS === "web" ? ({ cursor: "pointer" } as ViewStyle) : null),
197
204
  },
198
205
  optionLabel: {
199
206
  fontFamily: fonts.sans,
@@ -204,6 +211,7 @@ export const selectDropdownMetrics = StyleSheet.create({
204
211
  textAlign: "left",
205
212
  color: colors.slateBlue,
206
213
  width: "100%",
214
+ ...(Platform.OS === "web" ? ({ cursor: "pointer" } as TextStyle) : null),
207
215
  },
208
216
  optionLabelSelected: {
209
217
  fontWeight: fontWeight.medium,
@@ -1,6 +1,7 @@
1
1
  import { useEffect, type ReactNode } from 'react';
2
2
 
3
3
  import { Button } from '../../components/Button';
4
+ import { useBackdropDismiss } from '../hooks/useBackdropDismiss';
4
5
  import { cn } from '../utils/cn';
5
6
 
6
7
  export type ConfirmModalProps = {
@@ -35,6 +36,8 @@ export function ConfirmModal({
35
36
  containerClassName,
36
37
  buttonClassName,
37
38
  }: ConfirmModalProps) {
39
+ const backdropDismiss = useBackdropDismiss(onCancel, closeOnBackdrop);
40
+
38
41
  useEffect(() => {
39
42
  if (!open) {
40
43
  return;
@@ -65,7 +68,8 @@ export function ConfirmModal({
65
68
  return (
66
69
  <div
67
70
  className="fixed inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-md bg-storm-gray-850/40"
68
- onClick={closeOnBackdrop ? onCancel : undefined}
71
+ onPointerDown={backdropDismiss.onPointerDown}
72
+ onClick={backdropDismiss.onClick}
69
73
  role="presentation"
70
74
  >
71
75
  <div
@@ -77,9 +81,6 @@ export function ConfirmModal({
77
81
  'flex w-full max-w-md flex-col gap-8 rounded-3xl bg-white p-6 opacity-100 md:p-8',
78
82
  containerClassName,
79
83
  )}
80
- onClick={(event) => {
81
- event.stopPropagation();
82
- }}
83
84
  >
84
85
  <header className="flex flex-col items-center gap-3 text-center">
85
86
  {icon ? (
@@ -1,6 +1,7 @@
1
1
  import { X } from 'lucide-react';
2
2
  import { useEffect, useState, type ReactNode } from 'react';
3
3
 
4
+ import { useBackdropDismiss } from '../hooks/useBackdropDismiss';
4
5
  import { cn } from '../utils/cn';
5
6
 
6
7
  import './drawer-scroll.css';
@@ -72,6 +73,7 @@ export function Drawer({
72
73
  const [isPresent, setIsPresent] = useState(open);
73
74
  const [isAnimatedIn, setIsAnimatedIn] = useState(false);
74
75
  const [isTransformSettled, setIsTransformSettled] = useState(open);
76
+ const backdropDismiss = useBackdropDismiss(onClose);
75
77
 
76
78
  useEffect(() => {
77
79
  if (open) {
@@ -151,7 +153,8 @@ export function Drawer({
151
153
  'absolute inset-0 z-0 cursor-pointer bg-storm-gray-850/40 backdrop-blur-[12px] transition-opacity duration-300 ease-in-out',
152
154
  isAnimatedIn ? 'opacity-100' : 'opacity-0',
153
155
  )}
154
- onClick={onClose}
156
+ onPointerDown={backdropDismiss.onPointerDown}
157
+ onClick={backdropDismiss.onClick}
155
158
  />
156
159
 
157
160
  <aside
@@ -2,6 +2,7 @@ import { useEffect, type ReactNode } from 'react';
2
2
  import { createPortal } from 'react-dom';
3
3
 
4
4
  import { Button } from '../../components/Button';
5
+ import { useBackdropDismiss } from '../hooks/useBackdropDismiss';
5
6
  import { cn } from '../utils/cn';
6
7
  import './drawer-scroll.css';
7
8
 
@@ -61,6 +62,8 @@ export function Modal({
61
62
  contentClassName,
62
63
  buttonClassName,
63
64
  }: ModalProps) {
65
+ const backdropDismiss = useBackdropDismiss(onCancel, closeOnBackdrop);
66
+
64
67
  useEffect(() => {
65
68
  if (!open) {
66
69
  return;
@@ -95,15 +98,8 @@ export function Modal({
95
98
  const modal = (
96
99
  <div
97
100
  className="fixed inset-0 z-[100] flex items-center justify-center p-4 backdrop-blur-[12px] bg-storm-gray-850/40"
98
- onClick={
99
- closeOnBackdrop
100
- ? (event) => {
101
- if (event.target === event.currentTarget) {
102
- onCancel();
103
- }
104
- }
105
- : undefined
106
- }
101
+ onPointerDown={backdropDismiss.onPointerDown}
102
+ onClick={backdropDismiss.onClick}
107
103
  role="presentation"
108
104
  >
109
105
  <div
@@ -115,9 +111,6 @@ export function Modal({
115
111
  'flex max-h-[min(90dvh,calc(100dvh-2rem))] w-full max-w-2xl flex-col gap-6 rounded-2xl bg-white p-4 opacity-100 md:p-6',
116
112
  containerClassName,
117
113
  )}
118
- onClick={(event) => {
119
- event.stopPropagation();
120
- }}
121
114
  >
122
115
  <header className={cn('flex shrink-0', icon ? 'items-center gap-2.5' : undefined)}>
123
116
  {icon ? (
@@ -0,0 +1,44 @@
1
+ import { useCallback, useRef, type MouseEvent, type PointerEvent } from 'react';
2
+
3
+ type BackdropDismissHandlers = {
4
+ onPointerDown: (event: PointerEvent<HTMLElement>) => void;
5
+ onClick: (event: MouseEvent<HTMLElement>) => void;
6
+ };
7
+
8
+ /**
9
+ * Dismiss only on a true outside click: pointer must both press and release
10
+ * on the backdrop. Dragging from inside the dialog and releasing outside
11
+ * must not close.
12
+ */
13
+ export function useBackdropDismiss(
14
+ onDismiss: () => void,
15
+ enabled = true,
16
+ ): BackdropDismissHandlers {
17
+ const pointerDownOnBackdropRef = useRef(false);
18
+
19
+ const onPointerDown = useCallback(
20
+ (event: PointerEvent<HTMLElement>) => {
21
+ pointerDownOnBackdropRef.current =
22
+ enabled && event.target === event.currentTarget;
23
+ },
24
+ [enabled],
25
+ );
26
+
27
+ const onClick = useCallback(
28
+ (event: MouseEvent<HTMLElement>) => {
29
+ const shouldDismiss =
30
+ enabled &&
31
+ pointerDownOnBackdropRef.current &&
32
+ event.target === event.currentTarget;
33
+
34
+ pointerDownOnBackdropRef.current = false;
35
+
36
+ if (shouldDismiss) {
37
+ onDismiss();
38
+ }
39
+ },
40
+ [enabled, onDismiss],
41
+ );
42
+
43
+ return { onPointerDown, onClick };
44
+ }
package/src/web/index.ts CHANGED
@@ -128,6 +128,7 @@ export type { ConfirmModalProps } from './components/ConfirmModal';
128
128
 
129
129
  export { useMediaQuery } from './hooks/useMediaQuery';
130
130
  export { useOnClickOutside } from './hooks/useOnClickOutside';
131
+ export { useBackdropDismiss } from './hooks/useBackdropDismiss';
131
132
  export { cn } from './utils/cn';
132
133
 
133
134
  export type {