@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optilogic/core",
3
- "version": "1.4.0",
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>
@@ -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="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
+ )}
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()}