indas-ui 0.1.7 → 0.1.8

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.
@@ -64,7 +64,11 @@ const modalSizes = {
64
64
  * @param breakpoint - Pixel width threshold (default 768)
65
65
  */
66
66
  function useIsMobile(breakpoint = 768) {
67
- const [isMobile, setIsMobile] = React.useState(false);
67
+ return useMobileQuery(breakpoint) ?? false;
68
+ }
69
+ /** As useIsMobile, but null until the width has been read (the first render, and on the server). */
70
+ function useMobileQuery(breakpoint) {
71
+ const [isMobile, setIsMobile] = React.useState(null);
68
72
  React.useEffect(() => {
69
73
  const mq = window.matchMedia(`(max-width: ${breakpoint}px)`);
70
74
  setIsMobile(mq.matches);
@@ -241,7 +245,22 @@ ModalOverlay.displayName = DialogPrimitive.Overlay.displayName;
241
245
  */
242
246
  const ModalContent = React.forwardRef(({ className, children, hideCloseButton, disableOutsideClick, size, style, 'aria-describedby': ariaDescribedBy, ...props }, ref) => {
243
247
  const stableRef = useStableRef(ref);
244
- const isMobile = useIsMobile();
248
+ // The page and the dialog are different trees, so switching between them remounts everything
249
+ // inside: a form half filled in was wiped when a tablet turned, or a window was dragged, across
250
+ // the breakpoint. So the layout follows the screen only while closed; open, it keeps the one it
251
+ // opened with, and catches up once closed. The content exists only while open, so its ref says.
252
+ const live = useMobileQuery(768);
253
+ const [open, setOpen] = React.useState(false);
254
+ const [held, setHeld] = React.useState(null);
255
+ // The first reading always counts: before it the width is unknown, and a modal open at page load
256
+ // on a phone must still become the page.
257
+ if (live !== null && held !== live && (held === null || !open))
258
+ setHeld(live);
259
+ const isMobile = held ?? false;
260
+ const contentRef = React.useCallback((node) => {
261
+ stableRef(node);
262
+ setOpen(node !== null);
263
+ }, [stableRef]);
245
264
  // Nested modals must paint above their parent. Radix portals every modal to
246
265
  // <body>, so equal z-index makes stacking fall back to DOM order and the
247
266
  // child renders behind / overlapping the parent.
@@ -256,12 +275,12 @@ const ModalContent = React.forwardRef(({ className, children, hideCloseButton, d
256
275
  }, [hasOpenChild]);
257
276
  // On mobile: render as a full-screen page
258
277
  if (isMobile) {
259
- return ((0, jsx_runtime_1.jsx)(DialogPrimitive.Portal, { children: (0, jsx_runtime_1.jsx)(DialogPrimitive.Content, { ref: stableRef, "aria-describedby": ariaDescribedBy, style: { zIndex, ...style }, className: (0, utils_1.cn)("fixed inset-0 flex flex-col", "bg-[rgb(var(--bg-surface))]", className),
278
+ return ((0, jsx_runtime_1.jsx)(DialogPrimitive.Portal, { children: (0, jsx_runtime_1.jsx)(DialogPrimitive.Content, { ref: contentRef, "aria-describedby": ariaDescribedBy, style: { zIndex, ...style }, className: (0, utils_1.cn)("fixed inset-0 flex flex-col", "bg-[rgb(var(--bg-surface))]", className),
260
279
  // Always block outside click on mobile — there's nothing "outside"
261
280
  onInteractOutside: (e) => e.preventDefault(), onEscapeKeyDown: handleEscapeKeyDown, ...props, children: (0, jsx_runtime_1.jsx)(ModalMobileContext.Provider, { value: { isMobilePage: true, onClose: undefined }, children: children }) }) }));
262
281
  }
263
282
  // Desktop: unchanged Radix dialog
264
- return ((0, jsx_runtime_1.jsxs)(ModalPortal, { children: [(0, jsx_runtime_1.jsx)(ModalOverlay, { style: { zIndex: zIndex - 1 } }), (0, jsx_runtime_1.jsxs)(DialogPrimitive.Content, { ref: stableRef, "aria-describedby": ariaDescribedBy, style: { zIndex, ...style }, className: (0, utils_1.cn)("fixed left-[50%] top-[50%] grid translate-x-[-50%] translate-y-[-50%]", "border border-[rgb(var(--bd-default))] bg-[rgb(var(--bg-surface))] shadow-lg", "rounded-lg sm:rounded-lg", size ? modalSizes[size] : "max-w-lg w-full mx-4 h-auto max-h-[85vh]",
283
+ return ((0, jsx_runtime_1.jsxs)(ModalPortal, { children: [(0, jsx_runtime_1.jsx)(ModalOverlay, { style: { zIndex: zIndex - 1 } }), (0, jsx_runtime_1.jsxs)(DialogPrimitive.Content, { ref: contentRef, "aria-describedby": ariaDescribedBy, style: { zIndex, ...style }, className: (0, utils_1.cn)("fixed left-[50%] top-[50%] grid translate-x-[-50%] translate-y-[-50%]", "border border-[rgb(var(--bd-default))] bg-[rgb(var(--bg-surface))] shadow-lg", "rounded-lg sm:rounded-lg", size ? modalSizes[size] : "max-w-lg w-full mx-4 h-auto max-h-[85vh]",
265
284
  // Gap + padding only for standard modals — fullscreen/master/p-0 modals
266
285
  // run their own flush header/body/footer layout, so the gap shows as bands.
267
286
  size !== 'master' && size !== 'fullscreen' && !className?.includes('p-0') && "gap-3 sm:gap-4 p-4 sm:p-6", className), onInteractOutside: (e) => {
@@ -25,7 +25,11 @@ const modalSizes = {
25
25
  * @param breakpoint - Pixel width threshold (default 768)
26
26
  */
27
27
  export function useIsMobile(breakpoint = 768) {
28
- const [isMobile, setIsMobile] = React.useState(false);
28
+ return useMobileQuery(breakpoint) ?? false;
29
+ }
30
+ /** As useIsMobile, but null until the width has been read (the first render, and on the server). */
31
+ function useMobileQuery(breakpoint) {
32
+ const [isMobile, setIsMobile] = React.useState(null);
29
33
  React.useEffect(() => {
30
34
  const mq = window.matchMedia(`(max-width: ${breakpoint}px)`);
31
35
  setIsMobile(mq.matches);
@@ -198,7 +202,22 @@ ModalOverlay.displayName = DialogPrimitive.Overlay.displayName;
198
202
  */
199
203
  const ModalContent = React.forwardRef(({ className, children, hideCloseButton, disableOutsideClick, size, style, 'aria-describedby': ariaDescribedBy, ...props }, ref) => {
200
204
  const stableRef = useStableRef(ref);
201
- const isMobile = useIsMobile();
205
+ // The page and the dialog are different trees, so switching between them remounts everything
206
+ // inside: a form half filled in was wiped when a tablet turned, or a window was dragged, across
207
+ // the breakpoint. So the layout follows the screen only while closed; open, it keeps the one it
208
+ // opened with, and catches up once closed. The content exists only while open, so its ref says.
209
+ const live = useMobileQuery(768);
210
+ const [open, setOpen] = React.useState(false);
211
+ const [held, setHeld] = React.useState(null);
212
+ // The first reading always counts: before it the width is unknown, and a modal open at page load
213
+ // on a phone must still become the page.
214
+ if (live !== null && held !== live && (held === null || !open))
215
+ setHeld(live);
216
+ const isMobile = held ?? false;
217
+ const contentRef = React.useCallback((node) => {
218
+ stableRef(node);
219
+ setOpen(node !== null);
220
+ }, [stableRef]);
202
221
  // Nested modals must paint above their parent. Radix portals every modal to
203
222
  // <body>, so equal z-index makes stacking fall back to DOM order and the
204
223
  // child renders behind / overlapping the parent.
@@ -213,12 +232,12 @@ const ModalContent = React.forwardRef(({ className, children, hideCloseButton, d
213
232
  }, [hasOpenChild]);
214
233
  // On mobile: render as a full-screen page
215
234
  if (isMobile) {
216
- return (_jsx(DialogPrimitive.Portal, { children: _jsx(DialogPrimitive.Content, { ref: stableRef, "aria-describedby": ariaDescribedBy, style: { zIndex, ...style }, className: cn("fixed inset-0 flex flex-col", "bg-[rgb(var(--bg-surface))]", className),
235
+ return (_jsx(DialogPrimitive.Portal, { children: _jsx(DialogPrimitive.Content, { ref: contentRef, "aria-describedby": ariaDescribedBy, style: { zIndex, ...style }, className: cn("fixed inset-0 flex flex-col", "bg-[rgb(var(--bg-surface))]", className),
217
236
  // Always block outside click on mobile — there's nothing "outside"
218
237
  onInteractOutside: (e) => e.preventDefault(), onEscapeKeyDown: handleEscapeKeyDown, ...props, children: _jsx(ModalMobileContext.Provider, { value: { isMobilePage: true, onClose: undefined }, children: children }) }) }));
219
238
  }
220
239
  // Desktop: unchanged Radix dialog
221
- return (_jsxs(ModalPortal, { children: [_jsx(ModalOverlay, { style: { zIndex: zIndex - 1 } }), _jsxs(DialogPrimitive.Content, { ref: stableRef, "aria-describedby": ariaDescribedBy, style: { zIndex, ...style }, className: cn("fixed left-[50%] top-[50%] grid translate-x-[-50%] translate-y-[-50%]", "border border-[rgb(var(--bd-default))] bg-[rgb(var(--bg-surface))] shadow-lg", "rounded-lg sm:rounded-lg", size ? modalSizes[size] : "max-w-lg w-full mx-4 h-auto max-h-[85vh]",
240
+ return (_jsxs(ModalPortal, { children: [_jsx(ModalOverlay, { style: { zIndex: zIndex - 1 } }), _jsxs(DialogPrimitive.Content, { ref: contentRef, "aria-describedby": ariaDescribedBy, style: { zIndex, ...style }, className: cn("fixed left-[50%] top-[50%] grid translate-x-[-50%] translate-y-[-50%]", "border border-[rgb(var(--bd-default))] bg-[rgb(var(--bg-surface))] shadow-lg", "rounded-lg sm:rounded-lg", size ? modalSizes[size] : "max-w-lg w-full mx-4 h-auto max-h-[85vh]",
222
241
  // Gap + padding only for standard modals — fullscreen/master/p-0 modals
223
242
  // run their own flush header/body/footer layout, so the gap shows as bands.
224
243
  size !== 'master' && size !== 'fullscreen' && !className?.includes('p-0') && "gap-3 sm:gap-4 p-4 sm:p-6", className), onInteractOutside: (e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indas-ui",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Indas UI — React component library for business apps: Tailwind design tokens, Radix primitives, Lucide icons, DataGrid, dashboards and charts.",
5
5
  "license": "MIT",
6
6
  "author": "Indas Analytics Pvt Ltd",