feat: Implement responsive page layout components and CSS variables
- Added PageScaffold component for shared page layout with safe-area padding and scroll management. - Introduced ResponsiveMobileShell for mobile canvas layout with customizable backgrounds. - Created ScrollablePage component for scrollable content areas. - Updated CSS styles for coins rules screen, sidebar components, and subscription sections to use responsive design tokens. - Added viewport CSS variables provider to synchronize CSS variables with the visual viewport. - Refactored breakpoints and dimensions for better responsiveness across devices.
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { ViewportCssVarsProvider } from "@/providers/viewport-css-vars-provider";
|
||||
import { AuthProvider } from "@/stores/auth/auth-context";
|
||||
import { AuthStatusChecker } from "@/stores/auth/auth-status-checker";
|
||||
import { OAuthSessionSync } from "@/stores/auth/oauth-session-sync";
|
||||
@@ -34,23 +35,25 @@ export interface RootProvidersProps {
|
||||
|
||||
export function RootProviders({ children }: RootProvidersProps) {
|
||||
return (
|
||||
<AuthProvider>
|
||||
{/* AuthStatusChecker 必须在 AuthProvider 内部(依赖 useAuthDispatch),
|
||||
* 必须在 UserProvider/ChatProvider 外部(它们可能在 init 完成前就 mount) */}
|
||||
<AuthStatusChecker />
|
||||
{/* OAuthSessionSync 同样依赖 AuthProvider 上下文 */}
|
||||
<OAuthSessionSync />
|
||||
<UserProvider>
|
||||
<UserAuthSync />
|
||||
<PaymentProvider>
|
||||
<ChatProvider>
|
||||
<ChatAuthSync />
|
||||
<PaymentSuccessSync />
|
||||
{children}
|
||||
<div id="toast-portal" />
|
||||
</ChatProvider>
|
||||
</PaymentProvider>
|
||||
</UserProvider>
|
||||
</AuthProvider>
|
||||
<ViewportCssVarsProvider>
|
||||
<AuthProvider>
|
||||
{/* AuthStatusChecker 必须在 AuthProvider 内部(依赖 useAuthDispatch),
|
||||
* 必须在 UserProvider/ChatProvider 外部(它们可能在 init 完成前就 mount) */}
|
||||
<AuthStatusChecker />
|
||||
{/* OAuthSessionSync 同样依赖 AuthProvider 上下文 */}
|
||||
<OAuthSessionSync />
|
||||
<UserProvider>
|
||||
<UserAuthSync />
|
||||
<PaymentProvider>
|
||||
<ChatProvider>
|
||||
<ChatAuthSync />
|
||||
<PaymentSuccessSync />
|
||||
{children}
|
||||
<div id="toast-portal" />
|
||||
</ChatProvider>
|
||||
</PaymentProvider>
|
||||
</UserProvider>
|
||||
</AuthProvider>
|
||||
</ViewportCssVarsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, type ReactNode } from "react";
|
||||
|
||||
const CSS_VAR_VIEWPORT_HEIGHT = "--app-viewport-height";
|
||||
const CSS_VAR_VISIBLE_HEIGHT = "--app-visible-height";
|
||||
const CSS_VAR_VISIBLE_OFFSET_TOP = "--app-visible-offset-top";
|
||||
const CSS_VAR_SAFE_TOP = "--app-safe-top";
|
||||
const CSS_VAR_SAFE_RIGHT = "--app-safe-right";
|
||||
const CSS_VAR_SAFE_BOTTOM = "--app-safe-bottom";
|
||||
const CSS_VAR_SAFE_LEFT = "--app-safe-left";
|
||||
|
||||
export interface ViewportCssVarsProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps viewport-related CSS variables in sync with the real visual viewport.
|
||||
*
|
||||
* CSS already has safe defaults (`100dvh` and `env(safe-area-inset-*)`). This
|
||||
* client provider improves Safari/PWA/WebView behavior after hydration without
|
||||
* pushing viewport measurements into React state.
|
||||
*/
|
||||
export function ViewportCssVarsProvider({
|
||||
children,
|
||||
}: ViewportCssVarsProviderProps) {
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
let frameId: number | null = null;
|
||||
|
||||
const applyVars = () => {
|
||||
frameId = null;
|
||||
const root = document.documentElement;
|
||||
const viewport = window.visualViewport;
|
||||
const layoutHeight = window.innerHeight;
|
||||
const layoutWidth = window.innerWidth;
|
||||
const visibleHeight = viewport?.height ?? layoutHeight;
|
||||
const visibleWidth = viewport?.width ?? layoutWidth;
|
||||
const visibleOffsetTop = viewport?.offsetTop ?? 0;
|
||||
|
||||
root.style.setProperty(CSS_VAR_VIEWPORT_HEIGHT, `${layoutHeight}px`);
|
||||
root.style.setProperty(CSS_VAR_VISIBLE_HEIGHT, `${visibleHeight}px`);
|
||||
root.style.setProperty(
|
||||
CSS_VAR_VISIBLE_OFFSET_TOP,
|
||||
`${visibleOffsetTop}px`,
|
||||
);
|
||||
root.style.setProperty(CSS_VAR_SAFE_TOP, "env(safe-area-inset-top, 0px)");
|
||||
root.style.setProperty(
|
||||
CSS_VAR_SAFE_RIGHT,
|
||||
"env(safe-area-inset-right, 0px)",
|
||||
);
|
||||
root.style.setProperty(
|
||||
CSS_VAR_SAFE_BOTTOM,
|
||||
"env(safe-area-inset-bottom, 0px)",
|
||||
);
|
||||
root.style.setProperty(
|
||||
CSS_VAR_SAFE_LEFT,
|
||||
"env(safe-area-inset-left, 0px)",
|
||||
);
|
||||
root.dataset.viewportWidth = String(Math.round(visibleWidth));
|
||||
root.dataset.viewportHeight = String(Math.round(visibleHeight));
|
||||
};
|
||||
|
||||
const scheduleApply = () => {
|
||||
if (frameId != null) return;
|
||||
frameId = window.requestAnimationFrame(applyVars);
|
||||
};
|
||||
|
||||
applyVars();
|
||||
|
||||
window.addEventListener("resize", scheduleApply);
|
||||
window.addEventListener("orientationchange", scheduleApply);
|
||||
window.visualViewport?.addEventListener("resize", scheduleApply);
|
||||
window.visualViewport?.addEventListener("scroll", scheduleApply);
|
||||
|
||||
return () => {
|
||||
if (frameId != null) window.cancelAnimationFrame(frameId);
|
||||
window.removeEventListener("resize", scheduleApply);
|
||||
window.removeEventListener("orientationchange", scheduleApply);
|
||||
window.visualViewport?.removeEventListener("resize", scheduleApply);
|
||||
window.visualViewport?.removeEventListener("scroll", scheduleApply);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return children;
|
||||
}
|
||||
Reference in New Issue
Block a user