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:
2026-07-02 18:05:17 +08:00
parent 3592c6bb56
commit 34c8e2db4e
23 changed files with 497 additions and 141 deletions
@@ -0,0 +1,19 @@
.area {
position: sticky;
right: 0;
bottom: 0;
left: 0;
z-index: var(--fixed-bottom-z-index, 5);
flex: 0 0 auto;
padding:
var(--fixed-bottom-padding-top, 12px)
calc(var(--page-padding-x, 20px) + var(--app-safe-right, 0px))
calc(var(--fixed-bottom-padding-bottom, 12px) + var(--app-safe-bottom, 0px))
calc(var(--page-padding-x, 20px) + var(--app-safe-left, 0px));
background: var(--fixed-bottom-background, transparent);
}
.area[data-elevated="true"] {
box-shadow: 0 -14px 32px rgba(31, 21, 25, 0.08);
backdrop-filter: blur(18px);
}
@@ -0,0 +1,33 @@
import type { CSSProperties, ReactNode } from "react";
import styles from "./fixed-bottom-area.module.css";
export interface FixedBottomAreaProps {
children: ReactNode;
className?: string;
background?: string;
elevated?: boolean;
}
export function FixedBottomArea({
children,
className,
background,
elevated = false,
}: FixedBottomAreaProps) {
const style = background
? ({
"--fixed-bottom-background": background,
} as CSSProperties)
: undefined;
return (
<div
className={[styles.area, className].filter(Boolean).join(" ")}
data-elevated={elevated}
style={style}
>
{children}
</div>
);
}
+4
View File
@@ -5,6 +5,10 @@
export * from "./bottom-sheet";
export * from "./checkbox";
export * from "./dialog";
export * from "./fixed-bottom-area";
export * from "./loading-indicator";
export * from "./mobile-shell";
export * from "./page-scaffold";
export * from "./responsive-mobile-shell";
export * from "./scrollable-page";
export * from "./settings-section";
+4 -19
View File
@@ -1,16 +1,6 @@
/**
* 500px 移动端 ShellServer Component
*
*
* `Breakpoints.mobileMaxWidth = 500.0`)。
*
* 集中封装,调用方仅需 `<MobileShell>{...}</MobileShell>` 即可获得统一的移动端宽度约束。
*
* 本组件无 hooks,可作为 Server Component 直接 SSR。
*/
import type { ReactNode } from "react";
import styles from "./mobile-shell.module.css";
import { ResponsiveMobileShell } from "./responsive-mobile-shell";
export interface MobileShellProps {
children: ReactNode;
@@ -26,13 +16,8 @@ export function MobileShell({
className,
}: MobileShellProps) {
return (
<div className={styles.shell}>
<div
className={[styles.content, className].filter(Boolean).join(" ")}
style={background ? { background } : undefined}
>
{children}
</div>
</div>
<ResponsiveMobileShell background={background} className={className}>
{children}
</ResponsiveMobileShell>
);
}
@@ -0,0 +1,50 @@
.root {
position: relative;
display: flex;
flex: 1 1 auto;
min-height: var(--app-viewport-height, 100dvh);
min-width: 0;
flex-direction: column;
box-sizing: border-box;
color: var(--color-text-foreground, #171717);
overflow: hidden;
}
.root[data-scrollable="true"] {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.padded {
padding:
calc(var(--page-padding-y, 18px) + var(--app-safe-top, 0px))
calc(var(--page-padding-x, 20px) + var(--app-safe-right, 0px))
calc(var(--page-padding-y, 18px) + var(--app-safe-bottom, 0px))
calc(var(--page-padding-x, 20px) + var(--app-safe-left, 0px));
}
.header,
.footer {
position: relative;
z-index: 1;
flex: 0 0 auto;
}
.body {
position: relative;
z-index: 1;
display: flex;
flex: 1 1 auto;
min-height: 0;
min-width: 0;
flex-direction: column;
}
.body[data-scrollable="true"] {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.footer {
padding-bottom: var(--app-safe-bottom, 0px);
}
@@ -0,0 +1,49 @@
import type { ReactNode } from "react";
import styles from "./page-scaffold.module.css";
export interface PageScaffoldProps {
children: ReactNode;
header?: ReactNode;
footer?: ReactNode;
className?: string;
bodyClassName?: string;
padded?: boolean;
scrollable?: boolean;
bodyScrollable?: boolean;
}
/**
* Shared page layout primitive for mobile-first screens.
*
* Use it when migrating feature pages away from hand-written shell/padding
* styles. It keeps safe-area padding and scroll ownership in one place.
*/
export function PageScaffold({
children,
header,
footer,
className,
bodyClassName,
padded = true,
scrollable = false,
bodyScrollable = false,
}: PageScaffoldProps) {
return (
<main
className={[styles.root, padded ? styles.padded : "", className]
.filter(Boolean)
.join(" ")}
data-scrollable={scrollable}
>
{header ? <div className={styles.header}>{header}</div> : null}
<div
className={[styles.body, bodyClassName].filter(Boolean).join(" ")}
data-scrollable={bodyScrollable}
>
{children}
</div>
{footer ? <div className={styles.footer}>{footer}</div> : null}
</main>
);
}
@@ -1,19 +1,29 @@
.shell {
display: flex;
flex: 1 1 auto;
align-items: stretch;
justify-content: center;
width: 100%;
min-width: 0;
min-height: var(--app-viewport-height, 100dvh);
background: #000000; /* 显式黑,桌面端移动壳外圈为黑色 */
justify-content: center;
background: var(--mobile-shell-outer-background, #000000);
overflow-x: hidden;
}
.content {
position: relative;
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-width: 0;
width: 100%;
max-width: var(--app-max-width, 540px);
min-height: var(--app-viewport-height, 100dvh);
flex-direction: column;
background: var(--mobile-shell-background, transparent);
overflow-x: clip;
}
@supports not (overflow: clip) {
.content {
overflow-x: hidden;
}
}
@@ -0,0 +1,33 @@
import type { CSSProperties, ReactNode } from "react";
import styles from "./responsive-mobile-shell.module.css";
export interface ResponsiveMobileShellProps {
children: ReactNode;
/** Background used inside the mobile canvas. */
background?: string;
/** Background used outside the mobile canvas on wider screens. */
outerBackground?: string;
/** Class applied to the mobile canvas. */
className?: string;
}
export function ResponsiveMobileShell({
children,
background,
outerBackground,
className,
}: ResponsiveMobileShellProps) {
const style = {
"--mobile-shell-background": background,
"--mobile-shell-outer-background": outerBackground,
} as CSSProperties;
return (
<div className={styles.shell} style={style}>
<div className={[styles.content, className].filter(Boolean).join(" ")}>
{children}
</div>
</div>
);
}
@@ -0,0 +1,8 @@
.scroll {
flex: 1 1 auto;
min-height: 0;
min-width: 0;
overflow-y: auto;
overscroll-behavior-y: contain;
-webkit-overflow-scrolling: touch;
}
@@ -0,0 +1,16 @@
import type { ReactNode } from "react";
import styles from "./scrollable-page.module.css";
export interface ScrollablePageProps {
children: ReactNode;
className?: string;
}
export function ScrollablePage({ children, className }: ScrollablePageProps) {
return (
<div className={[styles.scroll, className].filter(Boolean).join(" ")}>
{children}
</div>
);
}