@optilogic/core 1.3.7 → 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 +56 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -4
- package/dist/index.d.ts +49 -4
- package/dist/index.js +56 -8
- 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 +84 -5
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
|
@@ -31,9 +31,10 @@ export interface ModalProps {
|
|
|
31
31
|
footer?: React.ReactNode;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Size variant
|
|
34
|
+
* Size variant. Maps to a `max-width` on the modal frame. For arbitrary
|
|
35
|
+
* widths, use `contentClassName` to override.
|
|
35
36
|
*/
|
|
36
|
-
size?: "sm" | "md" | "lg";
|
|
37
|
+
size?: "sm" | "md" | "lg" | "xl" | "2xl" | "full";
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* Z-index for stacking modals (default: 50)
|
|
@@ -41,9 +42,33 @@ export interface ModalProps {
|
|
|
41
42
|
zIndex?: number;
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
|
-
* Additional class names for modal content
|
|
45
|
+
* Additional class names for the modal body (the scrollable content area).
|
|
45
46
|
*/
|
|
46
47
|
className?: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Additional class names for the modal frame (the outer card containing
|
|
51
|
+
* header, body, and footer). Merges with the `size` class — pass width
|
|
52
|
+
* utilities here to override the named size, e.g. `"max-w-[1200px]"` or
|
|
53
|
+
* `"w-[80vw] max-w-none"`.
|
|
54
|
+
*/
|
|
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;
|
|
47
72
|
}
|
|
48
73
|
|
|
49
74
|
/**
|
|
@@ -65,6 +90,29 @@ export interface ModalProps {
|
|
|
65
90
|
* <Button variant="primary" onClick={handleConfirm}>Confirm</Button>
|
|
66
91
|
* </footer>
|
|
67
92
|
* </Modal>
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* // Custom width override
|
|
96
|
+
* <Modal
|
|
97
|
+
* isOpen={open}
|
|
98
|
+
* onClose={() => setOpen(false)}
|
|
99
|
+
* title="Report"
|
|
100
|
+
* contentClassName="max-w-[1200px]"
|
|
101
|
+
* >
|
|
102
|
+
* ...
|
|
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>
|
|
68
116
|
*/
|
|
69
117
|
export function Modal({
|
|
70
118
|
isOpen,
|
|
@@ -75,6 +123,9 @@ export function Modal({
|
|
|
75
123
|
size = "md",
|
|
76
124
|
zIndex = 50,
|
|
77
125
|
className,
|
|
126
|
+
contentClassName,
|
|
127
|
+
responsive = false,
|
|
128
|
+
forceMobile = false,
|
|
78
129
|
}: ModalProps) {
|
|
79
130
|
React.useEffect(() => {
|
|
80
131
|
if (!isOpen) return;
|
|
@@ -107,11 +158,36 @@ export function Modal({
|
|
|
107
158
|
sm: "max-w-md",
|
|
108
159
|
md: "max-w-lg",
|
|
109
160
|
lg: "max-w-2xl",
|
|
161
|
+
xl: "max-w-4xl",
|
|
162
|
+
"2xl": "max-w-6xl",
|
|
163
|
+
full: "max-w-[95vw]",
|
|
110
164
|
};
|
|
111
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
|
+
|
|
112
184
|
return (
|
|
113
185
|
<div
|
|
114
|
-
className=
|
|
186
|
+
className={cn(
|
|
187
|
+
"fixed inset-0 flex items-center justify-center p-4",
|
|
188
|
+
responsive && responsiveOuter,
|
|
189
|
+
forceMobile && forcedOuter
|
|
190
|
+
)}
|
|
115
191
|
style={{ zIndex }}
|
|
116
192
|
onClick={onClose}
|
|
117
193
|
>
|
|
@@ -123,7 +199,10 @@ export function Modal({
|
|
|
123
199
|
"bg-card border border-border rounded-lg shadow-lg",
|
|
124
200
|
"flex flex-col",
|
|
125
201
|
"max-h-[90vh]",
|
|
126
|
-
sizeClasses[size]
|
|
202
|
+
sizeClasses[size],
|
|
203
|
+
responsive && responsiveFrame,
|
|
204
|
+
forceMobile && forcedFrame,
|
|
205
|
+
contentClassName
|
|
127
206
|
)}
|
|
128
207
|
onClick={(e) => e.stopPropagation()}
|
|
129
208
|
>
|