@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optilogic/core",
3
- "version": "1.3.7",
3
+ "version": "1.5.0",
4
4
  "description": "Core UI components for Optilogic - A professional React component library",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -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 onClick={handleCancel}>
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
- : undefined
89
- }
120
+ : "",
121
+ mobileSheetActionClasses,
122
+ ]
123
+ .filter(Boolean)
124
+ .join(" ")}
90
125
  >
91
126
  {confirmLabel}
92
127
  </AlertDialogAction>
@@ -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="fixed inset-0 flex items-center justify-center p-4"
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
  >