@optilogic/core 1.4.0 → 1.5.0
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/dist/index.cjs +49 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.js +49 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/confirmation-modal.tsx +41 -6
- package/src/components/modal.tsx +55 -1
package/package.json
CHANGED
|
@@ -11,6 +11,35 @@ import {
|
|
|
11
11
|
AlertDialogTitle,
|
|
12
12
|
} from "./alert-dialog";
|
|
13
13
|
|
|
14
|
+
// On touch devices (no hover), AlertDialog's centered card competes with
|
|
15
|
+
// thumb reach and the action buttons are too small. Under
|
|
16
|
+
// `@media (pointer: coarse) and (hover: none)` we re-anchor the dialog to
|
|
17
|
+
// the bottom edge of the screen, full-width, with safe-area-aware padding
|
|
18
|
+
// and a stacked, taller button row.
|
|
19
|
+
const mobileSheetContentClasses = [
|
|
20
|
+
"[@media(pointer:coarse)and(hover:none)]:!top-auto",
|
|
21
|
+
"[@media(pointer:coarse)and(hover:none)]:!bottom-0",
|
|
22
|
+
"[@media(pointer:coarse)and(hover:none)]:!left-0",
|
|
23
|
+
"[@media(pointer:coarse)and(hover:none)]:!translate-x-0",
|
|
24
|
+
"[@media(pointer:coarse)and(hover:none)]:!translate-y-0",
|
|
25
|
+
"[@media(pointer:coarse)and(hover:none)]:!w-screen",
|
|
26
|
+
"[@media(pointer:coarse)and(hover:none)]:!max-w-none",
|
|
27
|
+
"[@media(pointer:coarse)and(hover:none)]:!rounded-t-xl",
|
|
28
|
+
"[@media(pointer:coarse)and(hover:none)]:!rounded-b-none",
|
|
29
|
+
"[@media(pointer:coarse)and(hover:none)]:!pb-[calc(1.5rem+env(safe-area-inset-bottom))]",
|
|
30
|
+
].join(" ");
|
|
31
|
+
|
|
32
|
+
const mobileSheetFooterClasses = [
|
|
33
|
+
// Stack buttons full-width with comfortable tap targets on touch devices.
|
|
34
|
+
"[@media(pointer:coarse)and(hover:none)]:!flex-col",
|
|
35
|
+
"[@media(pointer:coarse)and(hover:none)]:!gap-2",
|
|
36
|
+
].join(" ");
|
|
37
|
+
|
|
38
|
+
const mobileSheetActionClasses = [
|
|
39
|
+
"[@media(pointer:coarse)and(hover:none)]:!w-full",
|
|
40
|
+
"[@media(pointer:coarse)and(hover:none)]:!min-h-11",
|
|
41
|
+
].join(" ");
|
|
42
|
+
|
|
14
43
|
export interface ConfirmationModalProps {
|
|
15
44
|
/** Whether the modal is open */
|
|
16
45
|
open: boolean;
|
|
@@ -71,22 +100,28 @@ export function ConfirmationModal({
|
|
|
71
100
|
|
|
72
101
|
return (
|
|
73
102
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
74
|
-
<AlertDialogContent>
|
|
103
|
+
<AlertDialogContent className={mobileSheetContentClasses}>
|
|
75
104
|
<AlertDialogHeader>
|
|
76
105
|
<AlertDialogTitle>{title}</AlertDialogTitle>
|
|
77
106
|
<AlertDialogDescription>{description}</AlertDialogDescription>
|
|
78
107
|
</AlertDialogHeader>
|
|
79
|
-
<AlertDialogFooter>
|
|
80
|
-
<AlertDialogCancel
|
|
108
|
+
<AlertDialogFooter className={mobileSheetFooterClasses}>
|
|
109
|
+
<AlertDialogCancel
|
|
110
|
+
onClick={handleCancel}
|
|
111
|
+
className={mobileSheetActionClasses}
|
|
112
|
+
>
|
|
81
113
|
{cancelLabel}
|
|
82
114
|
</AlertDialogCancel>
|
|
83
115
|
<AlertDialogAction
|
|
84
116
|
onClick={handleConfirm}
|
|
85
|
-
className={
|
|
117
|
+
className={[
|
|
86
118
|
destructive
|
|
87
119
|
? "bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
88
|
-
:
|
|
89
|
-
|
|
120
|
+
: "",
|
|
121
|
+
mobileSheetActionClasses,
|
|
122
|
+
]
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.join(" ")}
|
|
90
125
|
>
|
|
91
126
|
{confirmLabel}
|
|
92
127
|
</AlertDialogAction>
|
package/src/components/modal.tsx
CHANGED
|
@@ -53,6 +53,22 @@ export interface ModalProps {
|
|
|
53
53
|
* `"w-[80vw] max-w-none"`.
|
|
54
54
|
*/
|
|
55
55
|
contentClassName?: string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Opt into mobile-friendly rendering. When `true`, the modal renders full-
|
|
59
|
+
* screen (no border, no rounded corners, fills the viewport) under
|
|
60
|
+
* `@media (pointer: coarse) and (hover: none)` — i.e. on touch devices
|
|
61
|
+
* without hover. On desktop / hybrid input devices the named `size` is
|
|
62
|
+
* preserved. Default: `false`.
|
|
63
|
+
*/
|
|
64
|
+
responsive?: boolean;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Force mobile (full-screen) rendering regardless of device. Useful for
|
|
68
|
+
* Storybook, manual QA, and the rare case where sheet-style rendering is
|
|
69
|
+
* desired on desktop. Overrides `responsive`. Default: `false`.
|
|
70
|
+
*/
|
|
71
|
+
forceMobile?: boolean;
|
|
56
72
|
}
|
|
57
73
|
|
|
58
74
|
/**
|
|
@@ -85,6 +101,18 @@ export interface ModalProps {
|
|
|
85
101
|
* >
|
|
86
102
|
* ...
|
|
87
103
|
* </Modal>
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* // Full-screen on mobile (touch devices), `lg` width on desktop
|
|
107
|
+
* <Modal
|
|
108
|
+
* isOpen={open}
|
|
109
|
+
* onClose={() => setOpen(false)}
|
|
110
|
+
* title="New Database"
|
|
111
|
+
* size="lg"
|
|
112
|
+
* responsive
|
|
113
|
+
* >
|
|
114
|
+
* ...
|
|
115
|
+
* </Modal>
|
|
88
116
|
*/
|
|
89
117
|
export function Modal({
|
|
90
118
|
isOpen,
|
|
@@ -96,6 +124,8 @@ export function Modal({
|
|
|
96
124
|
zIndex = 50,
|
|
97
125
|
className,
|
|
98
126
|
contentClassName,
|
|
127
|
+
responsive = false,
|
|
128
|
+
forceMobile = false,
|
|
99
129
|
}: ModalProps) {
|
|
100
130
|
React.useEffect(() => {
|
|
101
131
|
if (!isOpen) return;
|
|
@@ -133,9 +163,31 @@ export function Modal({
|
|
|
133
163
|
full: "max-w-[95vw]",
|
|
134
164
|
};
|
|
135
165
|
|
|
166
|
+
// Mobile rendering: full-screen, no chrome. Applied via media query when
|
|
167
|
+
// `responsive` is on, or unconditionally when `forceMobile` is on. The `!`
|
|
168
|
+
// important modifier ensures the mobile values win over the base utilities
|
|
169
|
+
// (`p-4`, `rounded-lg`, `border`, `max-w-*`, `max-h-[90vh]`) regardless of
|
|
170
|
+
// class ordering in the generated stylesheet.
|
|
171
|
+
const responsiveOuter =
|
|
172
|
+
"[@media(pointer:coarse)and(hover:none)]:!p-0";
|
|
173
|
+
const responsiveFrame =
|
|
174
|
+
"[@media(pointer:coarse)and(hover:none)]:!w-screen " +
|
|
175
|
+
"[@media(pointer:coarse)and(hover:none)]:!h-[100dvh] " +
|
|
176
|
+
"[@media(pointer:coarse)and(hover:none)]:!max-h-[100dvh] " +
|
|
177
|
+
"[@media(pointer:coarse)and(hover:none)]:!max-w-none " +
|
|
178
|
+
"[@media(pointer:coarse)and(hover:none)]:!rounded-none " +
|
|
179
|
+
"[@media(pointer:coarse)and(hover:none)]:!border-0";
|
|
180
|
+
const forcedOuter = "!p-0";
|
|
181
|
+
const forcedFrame =
|
|
182
|
+
"!w-screen !h-[100dvh] !max-h-[100dvh] !max-w-none !rounded-none !border-0";
|
|
183
|
+
|
|
136
184
|
return (
|
|
137
185
|
<div
|
|
138
|
-
className=
|
|
186
|
+
className={cn(
|
|
187
|
+
"fixed inset-0 flex items-center justify-center p-4",
|
|
188
|
+
responsive && responsiveOuter,
|
|
189
|
+
forceMobile && forcedOuter
|
|
190
|
+
)}
|
|
139
191
|
style={{ zIndex }}
|
|
140
192
|
onClick={onClose}
|
|
141
193
|
>
|
|
@@ -148,6 +200,8 @@ export function Modal({
|
|
|
148
200
|
"flex flex-col",
|
|
149
201
|
"max-h-[90vh]",
|
|
150
202
|
sizeClasses[size],
|
|
203
|
+
responsive && responsiveFrame,
|
|
204
|
+
forceMobile && forcedFrame,
|
|
151
205
|
contentClassName
|
|
152
206
|
)}
|
|
153
207
|
onClick={(e) => e.stopPropagation()}
|