aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
297 lines (294 loc) • 10.8 kB
JavaScript
'use client';
import { jsxs, jsx } from 'react/jsx-runtime';
import { useReducedMotion } from '../../hooks/useReducedMotion.js';
import React, { useRef, useState, useCallback, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { cn } from '../../lib/utilsComprehensive.js';
const createDefaultSegment = () => {
if (typeof window === "undefined") {
return {
left: 0,
top: 0,
width: 1024,
height: 768,
devicePixelRatio: 1
};
}
return {
left: 0,
top: 0,
width: window.innerWidth,
height: window.innerHeight,
devicePixelRatio: window.devicePixelRatio || 1
};
};
function GlassFoldableSupport({
children,
className,
adaptiveLayout = true,
bridgeHinge = true,
independentSegments = false,
continuousGlass = true,
foldAnimation = true,
onFoldStateChange
}) {
const isBrowser = typeof window !== "undefined";
const prefersReducedMotion = useReducedMotion();
const containerRef = useRef(null);
const [foldableInfo, setFoldableInfo] = useState({
isFoldable: false,
foldState: "unknown",
segments: [createDefaultSegment()]
});
const [layoutMode, setLayoutMode] = useState("single");
// Detect foldable device capabilities
const detectFoldableCapabilities = useCallback(async () => {
if (!isBrowser) return;
// Check for Visual Viewport API and Screen Segments
if ("getScreenDetails" in window) {
try {
const screenDetails = await window.getScreenDetails();
const segments = screenDetails.screens.map(screen => ({
left: screen.left,
top: screen.top,
width: screen.width,
height: screen.height,
devicePixelRatio: screen.devicePixelRatio
}));
const isFoldable = segments.length > 1;
let hinge = undefined;
if (isFoldable && segments.length === 2) {
// Calculate hinge position
const [segment1, segment2] = segments;
if (segment1.top === segment2.top && segment1.height === segment2.height) {
// Vertical hinge (side by side)
hinge = {
position: "vertical",
offset: Math.max(segment1.left + segment1.width, segment2.left + segment2.width) - Math.min(segment1.left, segment2.left),
width: Math.abs(segment2.left - (segment1.left + segment1.width))
};
} else if (segment1.left === segment2.left && segment1.width === segment2.width) {
// Horizontal hinge (top and bottom)
hinge = {
position: "horizontal",
offset: Math.max(segment1.top + segment1.height, segment2.top + segment2.height) - Math.min(segment1.top, segment2.top),
width: Math.abs(segment2.top - (segment1.top + segment1.height))
};
}
}
setFoldableInfo({
isFoldable,
foldState: isFoldable ? "unfolded" : "unknown",
segments,
hinge
});
setLayoutMode(isFoldable ? segments.length > 2 ? "extended" : "dual" : "single");
} catch (error) {
console.warn("Screen Details API not supported:", error);
}
}
// Fallback: Check for CSS environment variables
const supportsSpanning = typeof CSS !== "undefined" && typeof CSS.supports === "function" && (CSS.supports("(spanning: single-fold-vertical)") || CSS.supports("(spanning: single-fold-horizontal)"));
if (supportsSpanning) {
const spanning = getComputedStyle(document.documentElement).getPropertyValue("env(fold-left)") || getComputedStyle(document.documentElement).getPropertyValue("env(fold-top)");
if (spanning) {
setFoldableInfo(prev => ({
...prev,
isFoldable: true,
foldState: "unfolded"
}));
setLayoutMode("dual");
}
}
// Check for dual-screen using media queries
const matchesMedia = query => {
if (!isBrowser || typeof window.matchMedia !== "function") {
return false;
}
return window.matchMedia(query).matches;
};
const isDualScreen = matchesMedia("(spanning: single-fold-vertical)") || matchesMedia("(spanning: single-fold-horizontal)");
if (isDualScreen) {
setFoldableInfo(prev => ({
...prev,
isFoldable: true,
foldState: "unfolded"
}));
setLayoutMode("dual");
}
}, []);
// Monitor fold state changes
const monitorFoldState = useCallback(() => {
if (!isBrowser) {
return () => undefined;
}
// Listen for orientation changes that might indicate folding
const handleOrientationChange = () => {
setTimeout(detectFoldableCapabilities, 100);
};
// Listen for resize events that might indicate folding
const handleResize = () => {
detectFoldableCapabilities();
};
// Listen for visibility changes (device folding might hide content)
const handleVisibilityChange = () => {
if (document.hidden) {
setFoldableInfo(prev => ({
...prev,
foldState: "folded"
}));
} else {
setFoldableInfo(prev => ({
...prev,
foldState: prev.isFoldable ? "unfolded" : "unknown"
}));
}
};
window.addEventListener("orientationchange", handleOrientationChange);
window.addEventListener("resize", handleResize);
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
window.removeEventListener("orientationchange", handleOrientationChange);
window.removeEventListener("resize", handleResize);
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, [detectFoldableCapabilities]);
// Initialize foldable detection
useEffect(() => {
if (!isBrowser) return;
detectFoldableCapabilities();
const cleanup = monitorFoldState();
return cleanup;
}, [detectFoldableCapabilities, monitorFoldState, isBrowser]);
// Notify parent of fold state changes
useEffect(() => {
onFoldStateChange?.(foldableInfo);
}, [foldableInfo, onFoldStateChange]);
// Generate layout based on foldable info
const generateLayout = () => {
if (!adaptiveLayout || !foldableInfo.isFoldable) {
return jsx("div", {
className: 'relative glass-w-full glass-h-full',
children: children
});
}
const {
segments,
hinge
} = foldableInfo;
if (independentSegments && segments.length > 1) {
// Render independent content for each segment
const totalWidth = segments.reduce((acc, segment) => acc + segment.width, 0) || 1;
const totalHeight = segments.reduce((acc, segment) => acc + segment.height, 0) || 1;
return jsx("div", {
className: 'relative glass-w-full glass-h-full glass-flex',
children: segments.map((segment, index) => jsx(motion.div, {
className: 'relative',
style: {
width: `${segment.width / totalWidth * 100}%`,
height: `${segment.height / totalHeight * 100}%`
},
initial: {
opacity: 0,
scale: 0.95
},
animate: prefersReducedMotion ? {} : {
opacity: 1,
scale: 1
},
transition: prefersReducedMotion ? {
duration: 0
} : {
duration: 0.3
},
children: jsx("div", {
className: 'OptimizedGlass intensity={0.2} glassBlur={6} glass-w-full glass-h-full',
children: React.Children.toArray(children)[index] || children
})
}, `segment-${index}`))
});
}
// Adaptive single layout with hinge awareness
return jsxs("div", {
className: 'relative glass-w-full glass-h-full',
children: [bridgeHinge && hinge && jsx(HingeBridge, {
hinge: hinge,
continuousGlass: continuousGlass
}), jsx("div", {
className: cn("relative", hinge?.position === "vertical" && "flex", hinge?.position === "horizontal" && "flex flex-col"),
style: {
gap: hinge ? `${hinge.width}px` : undefined
},
children: children
})]
});
};
return jsxs(motion.div, {
ref: containerRef,
className: cn("relative OptimizedGlass intensity={0.2} glassBlur={6}", "transform-gpu will-change-transform", foldableInfo.isFoldable && "glass-foldable-supported", className),
"data-fold-state": foldableInfo.foldState,
"data-layout-mode": layoutMode,
animate: foldAnimation ? {
scale: foldableInfo.foldState === "folded" ? 0.95 : 1,
opacity: foldableInfo.foldState === "folded" ? 0.8 : 1
} : undefined,
transition: prefersReducedMotion ? {
duration: 0
} : {
duration: 0.3,
ease: "easeInOut"
},
children: [jsx(AnimatePresence, {
mode: "wait",
children: jsx(motion.div, {
initial: {
opacity: 0
},
animate: prefersReducedMotion ? {} : {
opacity: 1
},
exit: {
opacity: 0
},
transition: prefersReducedMotion ? {
duration: 0
} : {
duration: 0.2
},
children: generateLayout()
}, `${layoutMode}-${foldableInfo.foldState}`)
}), foldableInfo.isFoldable && jsx("div", {
className: 'absolute glass-top-2 right-2 glass-surface-primary glass-p-1 glass-radius-sm glass-text-xs opacity-50',
children: jsxs("div", {
className: "glass-flex glass-items-center glass-gap-1",
children: [jsx("div", {
className: cn("w-2 h-2 glass-radius-full", foldableInfo.foldState === "folded" && "bg-red-400", foldableInfo.foldState === "unfolded" && "bg-green-400", foldableInfo.foldState === "partial" && "bg-yellow-400", foldableInfo.foldState === "unknown" && "bg-gray-400")
}), jsx("span", {
children: layoutMode
})]
})
})]
});
}
// Hinge bridge component for seamless glass across fold
function HingeBridge({
hinge,
continuousGlass
}) {
if (!continuousGlass) return null;
return jsx("div", {
className: cn("absolute OptimizedGlass intensity={0.2} glassBlur={6} opacity-50", "pointer-events-none", hinge.position === "vertical" && "top-0 bottom-0", hinge.position === "horizontal" && "left-0 right-0"),
style: {
...(hinge.position === "vertical" ? {
left: `${hinge.offset}px`,
width: `${hinge.width}px`
} : {
top: `${hinge.offset}px`,
height: `${hinge.width}px`
}),
background: "linear-gradient(90deg, transparent, rgba(var(--glass-color-primary) / 0.1), transparent)"
}
});
}
export { GlassFoldableSupport };
//# sourceMappingURL=GlassFoldableSupport.js.map